diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
index 4c5fadb60..4334137a8 100644
--- a/damus.xcodeproj/project.pbxproj
+++ b/damus.xcodeproj/project.pbxproj
@@ -1448,10 +1448,19 @@
 		D74AAFD22B155E78006CF0F4 /* WalletConnect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09612A098D0E00943473 /* WalletConnect.swift */; };
 		D74AAFD42B155ECB006CF0F4 /* Zaps+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74AAFD32B155ECB006CF0F4 /* Zaps+.swift */; };
 		D74AAFD62B155F0C006CF0F4 /* WalletConnect+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74AAFD52B155F0C006CF0F4 /* WalletConnect+.swift */; };
+		D74EA08A2D2BF2A7002290DD /* URLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D767066E2C8BB3CE00F09726 /* URLHandler.swift */; };
+		D74EA08E2D2E271E002290DD /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA08D2D2E271E002290DD /* ErrorView.swift */; };
+		D74EA08F2D2E271E002290DD /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA08D2D2E271E002290DD /* ErrorView.swift */; };
+		D74EA0902D2E271E002290DD /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA08D2D2E271E002290DD /* ErrorView.swift */; };
+		D74EA0912D2E3464002290DD /* URLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D767066E2C8BB3CE00F09726 /* URLHandler.swift */; };
+		D74EA0932D2E77B9002290DD /* LoadableThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA0922D2E77B9002290DD /* LoadableThreadView.swift */; };
+		D74EA0942D2E77B9002290DD /* LoadableThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA0922D2E77B9002290DD /* LoadableThreadView.swift */; };
+		D74EA0952D2E77B9002290DD /* LoadableThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA0922D2E77B9002290DD /* LoadableThreadView.swift */; };
 		D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F43092B23F0BE00425B75 /* DamusPurple.swift */; };
 		D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F430B2B23FB9B00425B75 /* StoreObserver.swift */; };
 		D753CEAA2BE9DE04001C3A5D /* MutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D753CEA92BE9DE04001C3A5D /* MutingTests.swift */; };
 		D76556D62B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */; };
+		D767066F2C8BB3CF00F09726 /* URLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D767066E2C8BB3CE00F09726 /* URLHandler.swift */; };
 		D76874F32AE3632B00FB0F68 /* ProfileZapLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76874F22AE3632B00FB0F68 /* ProfileZapLinkView.swift */; };
 		D773BC5F2C6D538500349F0A /* CommentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D773BC5E2C6D538500349F0A /* CommentItem.swift */; };
 		D773BC602C6D538500349F0A /* CommentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D773BC5E2C6D538500349F0A /* CommentItem.swift */; };
@@ -2429,10 +2438,13 @@
 		D74AAFCE2B155D8C006CF0F4 /* ZapDataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapDataModel.swift; sourceTree = "<group>"; };
 		D74AAFD32B155ECB006CF0F4 /* Zaps+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Zaps+.swift"; sourceTree = "<group>"; };
 		D74AAFD52B155F0C006CF0F4 /* WalletConnect+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WalletConnect+.swift"; sourceTree = "<group>"; };
+		D74EA08D2D2E271E002290DD /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
+		D74EA0922D2E77B9002290DD /* LoadableThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableThreadView.swift; sourceTree = "<group>"; };
 		D74F43092B23F0BE00425B75 /* DamusPurple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurple.swift; sourceTree = "<group>"; };
 		D74F430B2B23FB9B00425B75 /* StoreObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreObserver.swift; sourceTree = "<group>"; };
 		D753CEA92BE9DE04001C3A5D /* MutingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutingTests.swift; sourceTree = "<group>"; };
 		D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleWelcomeView.swift; sourceTree = "<group>"; };
+		D767066E2C8BB3CE00F09726 /* URLHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLHandler.swift; sourceTree = "<group>"; };
 		D76874F22AE3632B00FB0F68 /* ProfileZapLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZapLinkView.swift; sourceTree = "<group>"; };
 		D773BC5E2C6D538500349F0A /* CommentItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentItem.swift; sourceTree = "<group>"; };
 		D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileActionSheetView.swift; sourceTree = "<group>"; };
@@ -2727,6 +2739,7 @@
 				D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */,
 				5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */,
 				D773BC5E2C6D538500349F0A /* CommentItem.swift */,
+				D767066E2C8BB3CE00F09726 /* URLHandler.swift */,
 			);
 			path = Models;
 			sourceTree = "<group>";
@@ -3067,6 +3080,7 @@
 		4C75EFA227FA576C0006080F /* Views */ = {
 			isa = PBXGroup;
 			children = (
+				D74EA08C2D2E26E6002290DD /* ErrorHandling */,
 				D7D68FF72C9E01A80015A515 /* Utils */,
 				D78DB85D2C20FE9E00F0AB12 /* Chat */,
 				D71AC4CA2BA8E3320076268E /* Extensions */,
@@ -3138,6 +3152,7 @@
 				D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */,
 				D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */,
 				D71AD8FC2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift */,
+				D74EA0922D2E77B9002290DD /* LoadableThreadView.swift */,
 			);
 			path = Views;
 			sourceTree = "<group>";
@@ -3858,6 +3873,14 @@
 			path = Mocking;
 			sourceTree = "<group>";
 		};
+		D74EA08C2D2E26E6002290DD /* ErrorHandling */ = {
+			isa = PBXGroup;
+			children = (
+				D74EA08D2D2E271E002290DD /* ErrorView.swift */,
+			);
+			path = ErrorHandling;
+			sourceTree = "<group>";
+		};
 		D74F43082B23F09300425B75 /* Purple */ = {
 			isa = PBXGroup;
 			children = (
@@ -4642,6 +4665,7 @@
 				4CA927612A290E340098A105 /* EventShell.swift in Sources */,
 				4C363AA428296DEE006E126D /* SearchModel.swift in Sources */,
 				4C8D00CC29DF92DF0036AF10 /* Hashtags.swift in Sources */,
+				D74EA0942D2E77B9002290DD /* LoadableThreadView.swift in Sources */,
 				4CEE2AF3280B25C500AB5EEF /* ProfilePicView.swift in Sources */,
 				4CC7AAF6297F1A6A00430951 /* EventBody.swift in Sources */,
 				D76556D62B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift in Sources */,
@@ -4748,6 +4772,7 @@
 				D7CB5D4E2B11728000AD4105 /* NewEventsBits.swift in Sources */,
 				4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */,
 				B57B4C622B312BD700A232C0 /* ReconnectRelaysNotify.swift in Sources */,
+				D767066F2C8BB3CF00F09726 /* URLHandler.swift in Sources */,
 				D7ADD3DE2B53854300F104C4 /* DamusPurpleURL.swift in Sources */,
 				E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */,
 				4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */,
@@ -4760,6 +4785,7 @@
 				4C9AA14A2A4587A6003F49FD /* NotificationStatusModel.swift in Sources */,
 				D7100C5C2B77016700C59298 /* IAPProductStateView.swift in Sources */,
 				4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
+				D74EA0902D2E271E002290DD /* ErrorView.swift in Sources */,
 				4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */,
 				BA37598E2ABCCE500018D73B /* VideoCaptureProcessor.swift in Sources */,
 				4C9B0DF32A65C46800CBDA21 /* ProfileEditButton.swift in Sources */,
@@ -4905,6 +4931,7 @@
 				82D6FAEC2CD99F7900C925F4 /* OnlyZapsNotify.swift in Sources */,
 				82D6FAED2CD99F7900C925F4 /* PostNotify.swift in Sources */,
 				82D6FAEE2CD99F7900C925F4 /* PresentSheetNotify.swift in Sources */,
+				D74EA0932D2E77B9002290DD /* LoadableThreadView.swift in Sources */,
 				82D6FAEF2CD99F7900C925F4 /* ProfileUpdatedNotify.swift in Sources */,
 				82D6FAF02CD99F7900C925F4 /* ReportNotify.swift in Sources */,
 				82D6FAF12CD99F7900C925F4 /* ScrollToTopNotify.swift in Sources */,
@@ -5071,6 +5098,7 @@
 				82D6FB922CD99F7900C925F4 /* Wallet.swift in Sources */,
 				82D6FB932CD99F7900C925F4 /* Report.swift in Sources */,
 				82D6FB942CD99F7900C925F4 /* LibreTranslateServer.swift in Sources */,
+				D74EA08E2D2E271E002290DD /* ErrorView.swift in Sources */,
 				82D6FB952CD99F7900C925F4 /* TranslationService.swift in Sources */,
 				82D6FB962CD99F7900C925F4 /* DeepLPlan.swift in Sources */,
 				82D6FB972CD99F7900C925F4 /* ZapsModel.swift in Sources */,
@@ -5252,6 +5280,7 @@
 				82D6FC492CD99F7900C925F4 /* BigButton.swift in Sources */,
 				82D6FC4A2CD99F7900C925F4 /* AddRelayView.swift in Sources */,
 				82D6FC4B2CD99F7900C925F4 /* BlocksView.swift in Sources */,
+				D74EA0912D2E3464002290DD /* URLHandler.swift in Sources */,
 				82D6FC4C2CD99F7900C925F4 /* BookmarksView.swift in Sources */,
 				82D6FC4D2CD99F7900C925F4 /* CarouselView.swift in Sources */,
 				82D6FC4E2CD99F7900C925F4 /* ConfigView.swift in Sources */,
@@ -5342,6 +5371,7 @@
 				D73E5E3A2C6A97F4007EB227 /* SwipeToDismiss.swift in Sources */,
 				D73E5E3B2C6A97F4007EB227 /* MusicController.swift in Sources */,
 				D73E5E3C2C6A97F4007EB227 /* UserStatusView.swift in Sources */,
+				D74EA08F2D2E271E002290DD /* ErrorView.swift in Sources */,
 				D73E5E3E2C6A97F4007EB227 /* SearchHeaderView.swift in Sources */,
 				D73E5E3F2C6A97F4007EB227 /* DamusGradient.swift in Sources */,
 				D73E5E402C6A97F4007EB227 /* AlbyGradient.swift in Sources */,
@@ -5437,6 +5467,7 @@
 				D73E5E9C2C6A97F4007EB227 /* Reply.swift in Sources */,
 				D73E5E9D2C6A97F4007EB227 /* SearchModel.swift in Sources */,
 				D73E5E9E2C6A97F4007EB227 /* NostrFilter+Hashable.swift in Sources */,
+				D74EA0952D2E77B9002290DD /* LoadableThreadView.swift in Sources */,
 				D73E5F912C6AA71B007EB227 /* InputDismissKeyboard.swift in Sources */,
 				D73E5E9F2C6A97F4007EB227 /* CreateAccountModel.swift in Sources */,
 				D73E5EA12C6A97F4007EB227 /* SignalModel.swift in Sources */,
@@ -5765,6 +5796,7 @@
 				D703D7622C670ACB00A400EA /* ByteBuffer.swift in Sources */,
 				D703D79A2C670DFD00A400EA /* bech32.c in Sources */,
 				D703D7B62C67118200A400EA /* String+extension.swift in Sources */,
+				D74EA08A2D2BF2A7002290DD /* URLHandler.swift in Sources */,
 				D703D76C2C670B3900A400EA /* Post.swift in Sources */,
 				D703D77A2C670BEB00A400EA /* VeriferOptions.swift in Sources */,
 				D73E5F9E2C6AA9F7007EB227 /* nostrscript.c in Sources */,
diff --git a/damus/ContentView.swift b/damus/ContentView.swift
index 108179a4c..f86e258e8 100644
--- a/damus/ContentView.swift
+++ b/damus/ContentView.swift
@@ -31,7 +31,8 @@ enum Sheets: Identifiable {
     case onboardingSuggestions
     case purple(DamusPurpleURL)
     case purple_onboarding
-    
+    case error(ErrorView.UserPresentableError)
+
     static func zap(target: ZapTarget, lnurl: String) -> Sheets {
         return .zap(ZapSheet(target: target, lnurl: lnurl))
     }
@@ -53,6 +54,7 @@ enum Sheets: Identifiable {
         case .onboardingSuggestions: return "onboarding-suggestions"
         case .purple(let purple_url): return "purple" + purple_url.url_string()
         case .purple_onboarding: return "purple_onboarding"
+        case .error(_): return "error"
         }
     }
 }
@@ -339,36 +341,14 @@ struct ContentView: View {
                 DamusPurpleURLSheetView(damus_state: damus_state!, purple_url: purple_url)
             case .purple_onboarding:
                 DamusPurpleNewUserOnboardingView(damus_state: damus_state)
+            case .error(let error):
+                ErrorView(damus_state: damus_state!, error: error)
             }
         }
         .onOpenURL { url in
-            on_open_url(state: damus_state!, url: url) { res in
-                guard let res else {
-                    return
-                }
-                
-                switch res {
-                    case .filter(let filt): self.open_search(filt: filt)
-                    case .profile(let pk):  self.open_profile(pubkey: pk)
-                    case .event(let ev):    self.open_event(ev: ev)
-                    case .wallet_connect(let nwc): self.open_wallet(nwc: nwc)
-                    case .script(let data): self.open_script(data)
-                    case .purple(let purple_url):
-                        if case let .welcome(checkout_id) = purple_url.variant {
-                            // If this is a welcome link, do the following before showing the onboarding screen:
-                            // 1. Check if this is legitimate and good to go.
-                            // 2. Mark as complete if this is good to go.
-                            Task {
-                                let is_good_to_go = try? await damus_state.purple.check_and_mark_ln_checkout_is_good_to_go(checkout_id: checkout_id)
-                                if is_good_to_go == true {
-                                    self.active_sheet = .purple(purple_url)
-                                }
-                            }
-                        }
-                        else {
-                            self.active_sheet = .purple(purple_url)
-                        }
-                }
+            Task {
+                let open_action = await DamusURLHandler.handle_opening_url_and_compute_view_action(damus_state: self.damus_state, url: url)
+                self.execute_open_action(open_action)
             }
         }
         .onReceive(handle_notify(.compose)) { action in
@@ -783,6 +763,39 @@ struct ContentView: View {
             break
         }
     }
+    
+    /// An open action within the app
+    /// This is used to model, store, and communicate a desired view action to be taken as a result of opening an object,
+    /// for example a URL
+    ///
+    /// ## Implementation notes
+    ///
+    /// - The reason this was created was to separate URL parsing logic, the underlying actions that mutate the state of the app, and the action to be taken on the view layer as a result. This makes it easier to test, to read the URL handling code, and to add new functionality in between the two (e.g. a confirmation screen before proceeding with a given open action)
+    enum ViewOpenAction {
+        /// Open a page route
+        case route(Route)
+        /// Open a sheet
+        case sheet(Sheets)
+        /// Do nothing.
+        ///
+        /// ## Implementation notes
+        /// - This is used here instead of Optional values to make semantics explicit and force better programming intent, instead of accidentally doing nothing because of Swift's syntax sugar.
+        case no_action
+    }
+    
+    /// Executes an action to open something in the app view
+    ///
+    /// - Parameter open_action: The action to perform
+    func execute_open_action(_ open_action: ViewOpenAction) {
+        switch open_action {
+        case .route(let route):
+            navigationCoordinator.push(route: route)
+        case .sheet(let sheet):
+            self.active_sheet = sheet
+        case .no_action:
+            return
+        }
+    }
 }
 
 struct TopbarSideMenuButton: View {
@@ -934,10 +947,38 @@ enum FoundEvent {
     case event(NostrEvent)
 }
 
+/// Finds an event from NostrDB if it exists, or from the network
+///
+/// This is the callback version. There is also an asyc/await version of this function.
+///
+/// - Parameters:
+///   - state: Damus state
+///   - query_: The query, including the event being looked for, and the relays to use when looking
+///   - callback: The function to call with results
 func find_event(state: DamusState, query query_: FindEvent, callback: @escaping (FoundEvent?) -> ()) {
     return find_event_with_subid(state: state, query: query_, subid: UUID().description, callback: callback)
 }
 
+/// Finds an event from NostrDB if it exists, or from the network
+///
+/// This is a the async/await version of `find_event`. Use this when using callbacks is impossible or cumbersome.
+///
+/// - Parameters:
+///   - state: Damus state
+///   - query_: The query, including the event being looked for, and the relays to use when looking
+///   - callback: The function to call with results
+func find_event(state: DamusState, query query_: FindEvent) async -> FoundEvent? {
+    await withCheckedContinuation { continuation in
+        find_event(state: state, query: query_) { event in
+            var already_resumed = false
+            if !already_resumed {   // Ensure we do not resume twice, as it causes a crash
+                continuation.resume(returning: event)
+                already_resumed = true
+            }
+        }
+    }
+}
+
 func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: String, callback: @escaping (FoundEvent?) -> ()) {
 
     var filter: NostrFilter? = nil
@@ -1008,6 +1049,15 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
     }
 }
 
+
+/// Finds a replaceable event based on an `naddr` address.
+///
+/// This is the callback version of the function. There is another function that makes use of async/await
+///
+/// - Parameters:
+///   - damus_state: The Damus state
+///   - naddr: the `naddr` address
+///   - callback: A function to handle the found event
 func naddrLookup(damus_state: DamusState, naddr: NAddr, callback: @escaping (NostrEvent?) -> ()) {
     var nostrKinds: [NostrKind]? = NostrKind(rawValue: naddr.kind).map { [$0] }
 
@@ -1036,6 +1086,26 @@ func naddrLookup(damus_state: DamusState, naddr: NAddr, callback: @escaping (Nos
     }
 }
 
+/// Finds a replaceable event based on an `naddr` address.
+///
+/// This is the async/await version of the function. Another version of this function which makes use of callback functions also exists .
+///
+/// - Parameters:
+///   - damus_state: The Damus state
+///   - naddr: the `naddr` address
+///   - callback: A function to handle the found event
+func naddrLookup(damus_state: DamusState, naddr: NAddr) async -> NostrEvent? {
+    await withCheckedContinuation { continuation in
+        var already_resumed = false
+        naddrLookup(damus_state: damus_state, naddr: naddr) { event in
+            if !already_resumed {   // Ensure we do not resume twice, as it causes a crash
+                continuation.resume(returning: event)
+                already_resumed = true
+            }
+        }
+    }
+}
+
 func timeline_name(_ timeline: Timeline?) -> String {
     guard let timeline else {
         return ""
@@ -1147,63 +1217,6 @@ func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: Ev
 }
 
 
-enum OpenResult {
-    case profile(Pubkey)
-    case filter(NostrFilter)
-    case event(NostrEvent)
-    case wallet_connect(WalletConnectURL)
-    case script([UInt8])
-    case purple(DamusPurpleURL)
-}
-
-func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) -> Void) {
-    if let purple_url = DamusPurpleURL(url: url) {
-        result(.purple(purple_url))
-        return
-    }
-    
-    if let nwc = WalletConnectURL(str: url.absoluteString) {
-        result(.wallet_connect(nwc))
-        return
-    }
-    
-    guard let link = decode_nostr_uri(url.absoluteString) else {
-        result(nil)
-        return
-    }
-    
-    switch link {
-    case .ref(let ref):
-        switch ref {
-        case .pubkey(let pk):
-            result(.profile(pk))
-        case .event(let noteid):
-            find_event(state: state, query: .event(evid: noteid)) { res in
-                guard let res, case .event(let ev) = res else { return }
-                result(.event(ev))
-            }
-        case .hashtag(let ht):
-            result(.filter(.filter_hashtag([ht.hashtag])))
-        case .param, .quote, .reference:
-            // doesn't really make sense here
-            break
-        case .naddr(let naddr):
-            naddrLookup(damus_state: state, naddr: naddr) { res in
-                guard let res = res else { return }
-                result(.event(res))
-            }
-        }
-    case .filter(let filt):
-        result(.filter(filt))
-        break
-        // TODO: handle filter searches?
-    case .script(let script):
-        result(.script(script))
-        break
-    }
-}
-
-
 func logout(_ state: DamusState?)
 {
     state?.close()
diff --git a/damus/Models/LongformEvent.swift b/damus/Models/LongformEvent.swift
index 4a278bf8c..32c12d048 100644
--- a/damus/Models/LongformEvent.swift
+++ b/damus/Models/LongformEvent.swift
@@ -2,7 +2,7 @@
 //  LongformEvent.swift
 //  damus
 //
-//  Created by Daniel Nogueira on 2023-11-24.
+//  Created by Daniel D'Aquino on 2023-11-24.
 //
 
 import Foundation
diff --git a/damus/Models/Purple/DamusPurple.swift b/damus/Models/Purple/DamusPurple.swift
index 9574e02a0..dbfef6a67 100644
--- a/damus/Models/Purple/DamusPurple.swift
+++ b/damus/Models/Purple/DamusPurple.swift
@@ -363,6 +363,42 @@ class DamusPurple: StoreObserverDelegate {
         return freshly_completed_checkouts
     }
     
+    /// Handles a Purple URL
+    /// - Parameter purple_url: The Purple URL being opened
+    /// - Returns: A view to be shown in the UI
+    @MainActor
+    func handle(purple_url: DamusPurpleURL) async -> ContentView.ViewOpenAction {
+        if case let .welcome(checkout_id) = purple_url.variant {
+            // If this is a welcome link, do the following before showing the onboarding screen:
+            // 1. Check if this is legitimate and good to go.
+            // 2. Mark as complete if this is good to go.
+            let is_good_to_go = try? await check_and_mark_ln_checkout_is_good_to_go(checkout_id: checkout_id)
+            switch is_good_to_go {
+            case .some(let is_good_to_go):
+                if is_good_to_go {
+                    return .sheet(.purple(purple_url))  // ALL GOOD, SHOW WELCOME SHEET
+                }
+                else {
+                    return .sheet(.error(.init(
+                        user_visible_description: NSLocalizedString("You clicked on a Purple welcome link, but it does not look like the checkout is completed yet. This is likely a bug.", comment: "Error label upon continuing in the app from a Damus Purple purchase"),
+                        tip: NSLocalizedString("Please double-check the checkout web page, or go to the Side Menu → \"Purple\" to check your account status. If you have already paid, but still don't see your account active, please save the URL of the checkout page where you came from, contact our support, and give us the URL to help you with this issue.", comment: "User-facing tips on what to do if a Purple welcome link doesn't work"),
+                        technical_info: "Handling Purple URL \"\(purple_url)\" failed, the `is_good_to_go` result was `\(is_good_to_go)`"
+                    )))
+                }
+            case .none:
+                return .sheet(.error(.init(
+                    user_visible_description: NSLocalizedString("You clicked on a Purple welcome link, but we could not find your checkout. This is likely a bug.", comment: "Error label upon continuing in the app from a Damus Purple purchase"),
+                    tip: NSLocalizedString("Please double-check the checkout web page, or go to the Side Menu → \"Purple\" to check your account status. If you have already paid, but still don't see your account active, please save the URL of the checkout page where you came from, contact our support, and give us the URL to help you with this issue", comment: "User-facing tips on what to do if a Purple welcome link doesn't work"),
+                    technical_info: "Handling Purple URL \"\(purple_url)\" failed, the `is_good_to_go` result was `\(String(describing: is_good_to_go))`"
+                )))
+            }
+        }
+        else {
+            // Show the purple url contents
+            return .sheet(.purple(purple_url))
+        }
+    }
+    
     @MainActor
     /// This function checks the status of a specific checkout id with the server
     /// You should use this result immediately, since it will internally be marked as handled
diff --git a/damus/Models/Purple/DamusPurpleURL.swift b/damus/Models/Purple/DamusPurpleURL.swift
index 8fe454450..198f0ce73 100644
--- a/damus/Models/Purple/DamusPurpleURL.swift
+++ b/damus/Models/Purple/DamusPurpleURL.swift
@@ -2,7 +2,7 @@
 //  DamusPurpleURL.swift
 //  damus
 //
-//  Created by Daniel Nogueira on 2024-01-13.
+//  Created by Daniel D'Aquino on 2024-01-13.
 //
 
 import Foundation
diff --git a/damus/Models/URLHandler.swift b/damus/Models/URLHandler.swift
new file mode 100644
index 000000000..34e3f450a
--- /dev/null
+++ b/damus/Models/URLHandler.swift
@@ -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)
+    }
+}
diff --git a/damus/Util/Constants.swift b/damus/Util/Constants.swift
index 3f6d7acdc..c3d133fac 100644
--- a/damus/Util/Constants.swift
+++ b/damus/Util/Constants.swift
@@ -7,6 +7,10 @@
 
 import Foundation
 
+/// General app-wide constants
+///
+/// ## Implementation notes:
+/// - Force unwrapping in this class is generally ok, because the contents are static, and so we can easily provide guarantees that they will not crash the app.
 class Constants {
     //static let EXAMPLE_DEMOS: DamusState = .empty
     static let DAMUS_APP_GROUP_IDENTIFIER: String = "group.com.damus"
@@ -32,6 +36,9 @@ class Constants {
     static let DAMUS_WEBSITE_STAGING_URL: URL = URL(string: "https://staging.damus.io")!
     static let DAMUS_WEBSITE_PRODUCTION_URL: URL = URL(string: "https://damus.io")!
     
+    // MARK: Damus Company Info
+    static let SUPPORT_PUBKEY: Pubkey = Pubkey(hex: "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681")!
+    
     // MARK: General constants
     static let GIF_IMAGE_TYPE: String = "com.compuserve.gif"
 }
diff --git a/damus/Util/Router.swift b/damus/Util/Router.swift
index 8e45d1008..2fff0b823 100644
--- a/damus/Util/Router.swift
+++ b/damus/Util/Router.swift
@@ -32,6 +32,7 @@ enum Route: Hashable {
     case DeveloperSettings(settings: UserSettingsStore)
     case FirstAidSettings(settings: UserSettingsStore)
     case Thread(thread: ThreadModel)
+    case ThreadFromReference(note_reference: LoadableThreadModel.NoteReference)
     case Reposts(reposts: EventsModel)
     case QuoteReposts(quotes: EventsModel)
     case Reactions(reactions: EventsModel)
@@ -96,6 +97,8 @@ enum Route: Hashable {
         case .Thread(let thread):
             ChatroomThreadView(damus: damusState, thread: thread)
             //ThreadView(state: damusState, thread: thread)
+        case .ThreadFromReference(let note_reference):
+            LoadableThreadView(state: damusState, note_reference: note_reference)
         case .Reposts(let reposts):
             RepostsView(damus_state: damusState, model: reposts)
         case .QuoteReposts(let quote_reposts):
@@ -187,6 +190,9 @@ enum Route: Hashable {
         case .Thread(let threadModel):
             hasher.combine("thread")
             hasher.combine(threadModel.event.id)
+        case .ThreadFromReference(note_reference: let note_reference):
+            hasher.combine("thread_from_reference")
+            hasher.combine(note_reference)
         case .Reposts(let reposts):
             hasher.combine("reposts")
             hasher.combine(reposts.target)
diff --git a/damus/Views/ErrorHandling/ErrorView.swift b/damus/Views/ErrorHandling/ErrorView.swift
new file mode 100644
index 000000000..2b49a8ca5
--- /dev/null
+++ b/damus/Views/ErrorHandling/ErrorView.swift
@@ -0,0 +1,119 @@
+//
+//  ErrorView.swift
+//  damus
+//
+//  Created by Daniel D'Aquino on 2025-01-08.
+//
+
+import SwiftUI
+
+/// A generic user-presentable error view
+///
+/// Use this to handle and display errors to the user when it does not make sense to create a custom error view.
+/// This includes good error handling UX practices, such as:
+/// - Clear description
+/// - Actionable advice for the user on what to do next.
+/// - One-click support contact options
+struct ErrorView: View {
+    let damus_state: DamusState?
+    let error: UserPresentableError
+    
+    @Environment(\.dismiss) var dismiss
+    
+    var body: some View {
+        VStack(spacing: 6) {
+            Image(systemName: "exclamationmark.circle")
+                .resizable()
+                .frame(width: 30, height: 30)
+                .foregroundStyle(.red)
+                .accessibilityHidden(true)
+            Text("Oops!", comment: "Heading for an error screen")
+                .font(.title)
+                .bold()
+                .padding(.bottom, 10)
+                .accessibilityHeading(.h1)
+            Text(error.user_visible_description)
+                .foregroundStyle(.secondary)
+            
+            VStack(alignment: .leading, spacing: 6) {
+                HStack(spacing: 5) {
+                    Image(systemName: "sparkles")
+                        .accessibilityHidden(true)
+                    Text("Advice", comment: "Heading for some advice text to help the user with an error")
+                        .font(.headline)
+                        .accessibilityHeading(.h3)
+                }
+                Text(error.tip)
+            }
+            .padding()
+            .background(Color.secondary.opacity(0.2))
+            .cornerRadius(10)
+            .padding(.vertical, 30)
+            
+            Spacer()
+            
+            if let damus_state, damus_state.is_privkey_user {
+                Button(action: {
+                    damus_state.nav.push(route: .DMChat(dms: .init(our_pubkey: damus_state.keypair.pubkey, pubkey: Constants.SUPPORT_PUBKEY)))
+                    dismiss()
+                }, label: {
+                    Text("Contact support via DMs", comment: "Button label to contact support from an error screen")
+                })
+                .padding(.vertical, 20)
+            }
+            Text("Contact support via email at [support@damus.io](mailto:support@damus.io)", comment: "Text to contact support via email")
+                .font(.caption)
+                .foregroundStyle(.secondary)
+        }
+        .padding(20)
+        .padding(.top, 20)
+    }
+    
+    /// An error that is displayed to the user, and can be sent to the Developers as well.
+    struct UserPresentableError {
+        /// The description of the error to be shown to the user
+        ///
+        /// **Requirements:**
+        /// - This should not be technical. It should use accessible language
+        /// - Should be localized
+        /// - It should try to explain the user what happened, and — if possible — why.
+        let user_visible_description: String
+        
+        /// Helpful tip/advice to the user, to help them overcome the error
+        ///
+        /// **Requirements:**
+        /// - Should provide actionable advice to the user
+        /// - This should not be overly technical
+        /// - Should be localized
+        /// - Should NOT include support contact (The view that will display this will already include support contact options)
+        ///
+        /// **Implementation notes:**
+        /// - This is NOT an optional value, because part of good UX is making sure error messages are actionable, which is something that is often forgotten. It's not uncommon for error messages to be written in vague, technical, and/or unactionable terms, but this is when the user needs help the most. And so this field is made mandatory to force developers to write actionable content to the user
+        let tip: String
+        
+        /// Technical information about the error, which will be sendable to developers
+        ///
+        /// Note: This is still unutilized, but this will be used in the future.
+        ///
+        /// **Requirements**
+        /// - Should never include any sensitive info
+        /// - Should be in English. The developers are the main audience.
+        /// - Should include helping info, such as context in which the error happens.
+        /// - Should be technical
+        let technical_info: String?
+        
+    }
+}
+
+
+
+#Preview {
+    ErrorView(
+        damus_state: test_damus_state,
+        error: .init(
+            user_visible_description: "We are still too early",
+            tip: "Stay humble, keep building, stack sats",
+            technical_info: nil
+        )
+    )
+}
diff --git a/damus/Views/LoadableThreadView.swift b/damus/Views/LoadableThreadView.swift
new file mode 100644
index 000000000..5379ae2a8
--- /dev/null
+++ b/damus/Views/LoadableThreadView.swift
@@ -0,0 +1,216 @@
+//
+//  LoadableThreadView.swift
+//  damus
+//
+//  Created by Daniel D'Aquino on 2025-01-08.
+//
+
+import SwiftUI
+
+
+/// A view model for `LoadableThreadView`
+///
+/// This takes a note reference, automatically tries to load it, and updates itself to reflect its current state
+///
+///
+class LoadableThreadModel: ObservableObject {
+    let damus_state: DamusState
+    let note_reference: NoteReference
+    @Published var state: ThreadModelLoadingState = .loading
+    /// The time period after which it will give up loading the view.
+    /// Written in nanoseconds
+    let TIMEOUT: UInt64 = 10 * 1_000_000_000    // 10 seconds
+    
+    init(damus_state: DamusState, note_reference: NoteReference) {
+        self.damus_state = damus_state
+        self.note_reference = note_reference
+        Task { await self.load() }
+    }
+    
+    func load() async {
+        // Start the loading process in a separate task to manage the timeout independently.
+        let loadTask = Task { @MainActor in
+            self.state = await executeLoadingLogic()
+        }
+
+        // Setup a timer to cancel the load after the timeout period
+        let timeoutTask = Task { @MainActor in
+            try await Task.sleep(nanoseconds: TIMEOUT)
+            loadTask.cancel() // This sends a cancellation signal to the load task.
+            self.state = .not_found
+        }
+        
+        await loadTask.value
+        timeoutTask.cancel() // Cancel the timeout task if loading finishes earlier.
+    }
+    
+    private func executeLoadingLogic() async -> ThreadModelLoadingState {
+        switch note_reference {
+        case .note_id(let note_id):
+            let res = await find_event(state: damus_state, query: .event(evid: note_id))
+            guard let res, case .event(let ev) = res else { return .not_found }
+            return .loaded(model: ThreadModel(event: ev, damus_state: damus_state))
+        case .naddr(let naddr):
+            guard let event = await naddrLookup(damus_state: damus_state, naddr: naddr) else { return .not_found }
+            return .loaded(model: ThreadModel(event: event, damus_state: damus_state))
+        }
+    }
+    
+    enum ThreadModelLoadingState {
+        case loading
+        case loaded(model: ThreadModel)
+        case not_found
+    }
+    
+    enum NoteReference: Hashable {
+        case note_id(NoteId)
+        case naddr(NAddr)
+    }
+}
+
+struct LoadableThreadView: View {
+    let state: DamusState
+    @StateObject var loadable_thread: LoadableThreadModel
+    var loading: Bool {
+        switch loadable_thread.state {
+        case .loading:
+            return true
+        case .loaded, .not_found:
+            return false
+        }
+    }
+    
+    init(state: DamusState, note_reference: LoadableThreadModel.NoteReference) {
+        self.state = state
+        self._loadable_thread = StateObject.init(wrappedValue: LoadableThreadModel(damus_state: state, note_reference: note_reference))
+    }
+    
+    var body: some View {
+        switch self.loadable_thread.state {
+        case .loading:
+            ScrollView(.vertical) {
+                self.skeleton
+                    .redacted(reason: loading ? .placeholder : [])
+                    .shimmer(loading)
+                    .accessibilityElement(children: .ignore)
+                    .accessibilityLabel(NSLocalizedString("Loading thread", comment: "Accessibility label for the thread view when it is loading"))
+            }
+        case .loaded(model: let thread_model):
+            ChatroomThreadView(damus: state, thread: thread_model)
+        case .not_found:
+            self.not_found
+        }
+    }
+    
+    var not_found: some View {
+        VStack(spacing: 6) {
+            Image(systemName: "questionmark.app")
+                .resizable()
+                .frame(width: 30, height: 30)
+                .accessibilityHidden(true)
+            Text("Note not found", comment: "Heading for the thread view in a not found error state")
+                .font(.title)
+                .bold()
+                .padding(.bottom, 10)
+            Text("We were unable to find the note you were looking for.", comment: "Text for the thread view when it is unable to find the note the user is looking for")
+                .multilineTextAlignment(.center)
+                .foregroundStyle(.secondary)
+            
+            VStack(alignment: .leading, spacing: 6) {
+                HStack(spacing: 5) {
+                    Image(systemName: "sparkles")
+                        .accessibilityHidden(true)
+                    Text("Advice", comment: "Heading for some advice text to help the user with an error")
+                        .font(.headline)
+                }
+                Text("Try checking the link again, your internet connection, whether you need to connect to a specific relay to access this content.", comment: "Tips on what to do if a note cannot be found.")
+            }
+            .padding()
+            .background(Color.secondary.opacity(0.2))
+            .cornerRadius(10)
+            .padding(.vertical, 30)
+        }
+        .padding()
+    }
+    
+    // MARK: Skeleton views
+    // Implementation notes
+    // - No localization is needed because the text will be redacted
+    // - No accessibility label is needed because these will be summarized into a single accessibility label at the top-level view. See `body` in this struct
+    
+    var skeleton: some View {
+        VStack(alignment: .leading, spacing: 40) {
+            self.skeleton_selected_event
+            self.skeleton_chat_event(message: "Nice! Have you tried Damus?", right: false)
+            self.skeleton_chat_event(message: "Yes, it's awesome.", right: true)
+            Spacer()
+        }
+        .padding()
+    }
+    
+    func skeleton_chat_event(message: String, right: Bool) -> some View {
+        HStack(alignment: .center) {
+            if !right {
+                self.skeleton_chat_user_avatar
+            }
+            else {
+                Spacer()
+            }
+            ChatBubble(
+                direction: right ? .right : .left,
+                stroke_content: Color.accentColor.opacity(0),
+                stroke_style: .init(lineWidth: 4),
+                background_style: Color.secondary.opacity(0.5),
+                content: {
+                    Text(message)
+                        .padding()
+                }
+            )
+            if right {
+                self.skeleton_chat_user_avatar
+            }
+            else {
+                Spacer()
+            }
+        }
+    }
+    
+    var skeleton_selected_event: some View {
+        VStack(alignment: .leading, spacing: 10) {
+            HStack {
+                Circle()
+                    .frame(width: 50, height: 50)
+                    .foregroundStyle(.secondary.opacity(0.5))
+                Text("Satoshi Nakamoto")
+                    .bold()
+            }
+            Text("Nostr is the super app. Because it’s actually an ecosystem of apps, all of which make each other better. People haven’t grasped that yet. They will when it’s more accessible and onboarding is more straightforward and intuitive.")
+            HStack {
+                self.skeleton_action_item
+                Spacer()
+                self.skeleton_action_item
+                Spacer()
+                self.skeleton_action_item
+                Spacer()
+                self.skeleton_action_item
+            }
+        }
+    }
+    
+    var skeleton_chat_user_avatar: some View {
+        Circle()
+            .fill(.secondary.opacity(0.5))
+            .frame(width: 35, height: 35)
+            .padding(.bottom, -21)
+    }
+    
+    var skeleton_action_item: some View {
+        Circle()
+            .fill(Color.secondary.opacity(0.5))
+            .frame(width: 25, height: 25)
+    }
+}
+
+#Preview("Loadable") {
+    LoadableThreadView(state: test_damus_state, note_reference: .note_id(test_thread_note_1.id))
+}