Improve robustness of the URL handler
This commit improves reliability on the handling of external URLs. This was achieved through the following improvements: 1. The URL handler interface is now well-defined, with more clear inputs and outputs, to avoid silent failures and error paths that are hard to see within convoluted logic paths 2. Side effects during URL parsing were almost completely removed for more predictable behavior 3. Error handling logic was added to present errors to the user in a user-friendly manner, instead of silently failing 4. Event loading logic was moved into a special new thread view, which makes its own internal state evident to the user (i.e. whether the note is loading, loaded, or if the note could not be found) These changes make the URL opening logic more predictable, easy to refactor, and helps ensure the user always gets some outcome from opening a URL, even if it means showing a "not found" or "error" screen, to eliminate cases where nothing seems to happen. Closes: https://github.com/damus-io/damus/issues/2429 Changelog-Fixed: Improved robustness of the URL handler Changelog-Added: Added user-friendly error view for errors around the app that would not fit in other places Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
//
|
||||
// URLHandler.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2024-09-06.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Parses URLs into actions within the app.
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - This exists so that we can separate the logic of parsing the URL and the actual action within the app. That makes the code more readable, testable, and extensible
|
||||
struct DamusURLHandler {
|
||||
/// Parses a URL, handles any needed actions within damus state, and returns the view to be opened in the app
|
||||
///
|
||||
/// Side effects: May mutate `damus_state` in some circumstances
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - damus_state: The Damus state. May be mutated as part of this function
|
||||
/// - url: The URL to be opened
|
||||
/// - Returns: A view to be shown to the user
|
||||
static func handle_opening_url_and_compute_view_action(damus_state: DamusState, url: URL) async -> ContentView.ViewOpenAction {
|
||||
let parsed_url_info = parse_url(url: url)
|
||||
|
||||
switch parsed_url_info {
|
||||
case .profile(let pubkey):
|
||||
return .route(.ProfileByKey(pubkey: pubkey))
|
||||
case .filter(let nostrFilter):
|
||||
let search = SearchModel(state: damus_state, search: nostrFilter)
|
||||
return .route(.Search(search: search))
|
||||
case .event(let nostrEvent):
|
||||
let thread = ThreadModel(event: nostrEvent, damus_state: damus_state)
|
||||
return .route(.Thread(thread: thread))
|
||||
case .event_reference(let event_reference):
|
||||
return .route(.ThreadFromReference(note_reference: event_reference))
|
||||
case .wallet_connect(let walletConnectURL):
|
||||
damus_state.wallet.new(walletConnectURL)
|
||||
return .route(.Wallet(wallet: damus_state.wallet))
|
||||
case .script(let data):
|
||||
let model = ScriptModel(data: data, state: .not_loaded)
|
||||
return .route(.Script(script: model))
|
||||
case .purple(let purple_url):
|
||||
return await damus_state.purple.handle(purple_url: purple_url)
|
||||
case nil:
|
||||
break
|
||||
}
|
||||
return .sheet(.error(ErrorView.UserPresentableError(
|
||||
user_visible_description: NSLocalizedString("Could not parse the URL you are trying to open.", comment: "User visible error description"),
|
||||
tip: NSLocalizedString("Please try again, check the URL for typos, or contact support for further help.", comment: "User visible error tips"),
|
||||
technical_info: "Could not find a suitable open action. User tried to open this URL: \(url.absoluteString)"
|
||||
)))
|
||||
}
|
||||
|
||||
/// Parses a URL into a structured information object.
|
||||
///
|
||||
/// This function does not cause any mutations on the app, or any side-effects.
|
||||
///
|
||||
/// - Parameter url: The URL to be parsed
|
||||
/// - Returns: Structured information about the contents inside the URL. Returns `nil` if URL is not compatible, invalid, or could not be parsed for some reason.
|
||||
static func parse_url(url: URL) -> ParsedURLInfo? {
|
||||
if let purple_url = DamusPurpleURL(url: url) {
|
||||
return .purple(purple_url)
|
||||
}
|
||||
|
||||
if let nwc = WalletConnectURL(str: url.absoluteString) {
|
||||
return .wallet_connect(nwc)
|
||||
}
|
||||
|
||||
guard let link = decode_nostr_uri(url.absoluteString) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch link {
|
||||
case .ref(let ref):
|
||||
switch ref {
|
||||
case .pubkey(let pk):
|
||||
return .profile(pk)
|
||||
case .event(let noteid):
|
||||
return .event_reference(.note_id(noteid))
|
||||
case .hashtag(let ht):
|
||||
return .filter(.filter_hashtag([ht.hashtag]))
|
||||
case .param, .quote, .reference:
|
||||
// doesn't really make sense here
|
||||
break
|
||||
case .naddr(let naddr):
|
||||
return .event_reference(.naddr(naddr))
|
||||
}
|
||||
case .filter(let filt):
|
||||
return .filter(filt)
|
||||
case .script(let script):
|
||||
return .script(script)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
enum ParsedURLInfo {
|
||||
case profile(Pubkey)
|
||||
case filter(NostrFilter)
|
||||
case event(NostrEvent)
|
||||
case event_reference(LoadableThreadModel.NoteReference)
|
||||
case wallet_connect(WalletConnectURL)
|
||||
case script([UInt8])
|
||||
case purple(DamusPurpleURL)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user