From 71c36052e21d2eb36bbece2da71bb16606ebbbde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Mon, 5 Jan 2026 15:57:20 -0800 Subject: [PATCH] Fix onboarding crash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes a crash that occurred when clicking "follow all" during onboarding. This fix works by making `Contacts` and `PostBox` isolated into a specific Swift Actor, and updating direct and indirect usages accordingly. Changelog-Fixed: Fixed a crash that occurred when clicking "follow all" during onboarding. Closes: https://github.com/damus-io/damus/issues/3422 Co-authored-by: alltheseas <64376233+alltheseas@users.noreply.github.com> Signed-off-by: Daniel D’Aquino --- .../NotificationExtensionState.swift | 1 + .../NotificationService.swift | 39 +++++++++---------- damus/ContentView.swift | 6 ++- .../NostrNetworkManager.swift | 1 + .../UserRelayListManager.swift | 13 +++++-- damus/Core/Storage/DamusState.swift | 2 + .../DMs/Views/DirectMessagesView.swift | 1 + damus/Features/Events/EventView.swift | 2 + damus/Features/Follows/Models/Contacts.swift | 1 + .../NIP05/Models/NIP05DomainEventsModel.swift | 2 +- .../Models/NotificationsManager.swift | 2 +- .../Views/NotificationsView.swift | 2 + damus/Features/Posting/Models/PostBox.swift | 4 +- .../Profile/Models/ProfileModel.swift | 1 + .../Features/Profile/Views/ProfileName.swift | 1 + .../Profile/Views/ProfilePicView.swift | 1 + .../Search/Views/SearchResultsView.swift | 3 +- .../Timeline/Models/FriendFilter.swift | 1 + .../Features/Timeline/Models/HomeModel.swift | 7 +++- damus/Features/Wallet/Views/WalletView.swift | 1 + damus/Features/Zaps/Views/NoteZapButton.swift | 10 ++--- damus/Shared/Components/NIP05Badge.swift | 1 + damus/TestData.swift | 2 + damusTests/DamusCacheManagerTests.swift | 1 + damusTests/EventGroupViewTests.swift | 3 ++ .../Models/ContactCardManagerTests.swift | 6 +++ damusTests/MutingTests.swift | 1 + damusTests/NoteContentViewTests.swift | 12 ++++++ damusTests/RepostedTests.swift | 1 + damusTests/WalletConnectTests.swift | 5 ++- damusTests/ZapTests.swift | 2 + 31 files changed, 96 insertions(+), 39 deletions(-) diff --git a/DamusNotificationService/NotificationExtensionState.swift b/DamusNotificationService/NotificationExtensionState.swift index afe60557..fe616e3b 100644 --- a/DamusNotificationService/NotificationExtensionState.swift +++ b/DamusNotificationService/NotificationExtensionState.swift @@ -7,6 +7,7 @@ import Foundation +@MainActor struct NotificationExtensionState: HeadlessDamusState { let ndb: Ndb let settings: UserSettingsStore diff --git a/DamusNotificationService/NotificationService.swift b/DamusNotificationService/NotificationService.swift index ca286a49..1b06024b 100644 --- a/DamusNotificationService/NotificationService.swift +++ b/DamusNotificationService/NotificationService.swift @@ -44,30 +44,29 @@ class NotificationService: UNNotificationServiceExtension { // Log that we got a push notification Log.debug("Got nostr event push notification from pubkey %s", for: .push_notifications, nostr_event.pubkey.hex()) - guard let state = NotificationExtensionState() else { - Log.debug("Failed to open nostrdb", for: .push_notifications) + Task { + guard let state = await NotificationExtensionState() else { + Log.debug("Failed to open nostrdb", for: .push_notifications) - // Something failed to initialize so let's go for the next best thing - guard let improved_content = NotificationFormatter.shared.format_message(event: nostr_event) else { - // We cannot format this nostr event. Suppress notification. - contentHandler(UNNotificationContent()) + // Something failed to initialize so let's go for the next best thing + guard let improved_content = NotificationFormatter.shared.format_message(event: nostr_event) else { + // We cannot format this nostr event. Suppress notification. + contentHandler(UNNotificationContent()) + return + } + contentHandler(improved_content) return } - contentHandler(improved_content) - return - } - let sender_profile = { - let profile = try? state.profiles.lookup(id: nostr_event.pubkey) - let picture = ((profile?.picture.map { URL(string: $0) }) ?? URL(string: robohash(nostr_event.pubkey)))! - return ProfileBuf(picture: picture, - name: profile?.name, - display_name: profile?.display_name, - nip05: profile?.nip05) - }() - let sender_pubkey = nostr_event.pubkey - - Task { + let sender_profile = { + let profile = try? state.profiles.lookup(id: nostr_event.pubkey) + let picture = ((profile?.picture.map { URL(string: $0) }) ?? URL(string: robohash(nostr_event.pubkey)))! + return ProfileBuf(picture: picture, + name: profile?.name, + display_name: profile?.display_name, + nip05: profile?.nip05) + }() + let sender_pubkey = nostr_event.pubkey // Don't show notification details that match mute list. // TODO: Remove this code block once we get notification suppression entitlement from Apple. It will be covered by the `guard should_display_notification` block diff --git a/damus/ContentView.swift b/damus/ContentView.swift index e91754ab..1405593e 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -1015,6 +1015,7 @@ func timeline_name(_ timeline: Timeline?) -> String { } @discardableResult +@MainActor func handle_unfollow(state: DamusState, unfollow: FollowRef) async -> Bool { guard let keypair = state.keypair.to_full() else { return false @@ -1043,6 +1044,7 @@ func handle_unfollow(state: DamusState, unfollow: FollowRef) async -> Bool { } @discardableResult +@MainActor func handle_follow(state: DamusState, follow: FollowRef) async -> Bool { guard let keypair = state.keypair.to_full() else { return false @@ -1071,9 +1073,9 @@ func handle_follow(state: DamusState, follow: FollowRef) async -> Bool { func handle_follow_notif(state: DamusState, target: FollowTarget) async -> Bool { switch target { case .pubkey(let pk): - state.contacts.add_friend_pubkey(pk) + await state.contacts.add_friend_pubkey(pk) case .contact(let ev): - state.contacts.add_friend_contact(ev) + await state.contacts.add_friend_contact(ev) } return await handle_follow(state: state, follow: target.follow_ref) diff --git a/damus/Core/Networking/NostrNetworkManager/NostrNetworkManager.swift b/damus/Core/Networking/NostrNetworkManager/NostrNetworkManager.swift index 9b22e0d4..9e8a188e 100644 --- a/damus/Core/Networking/NostrNetworkManager/NostrNetworkManager.swift +++ b/damus/Core/Networking/NostrNetworkManager/NostrNetworkManager.swift @@ -191,6 +191,7 @@ extension NostrNetworkManager { /// The latest contact list `NostrEvent` /// /// Note: Read-only access, because `NostrNetworkManager` does not manage contact lists. + @MainActor var latestContactListEvent: NostrEvent? { get } /// Default bootstrap relays to start with when a user relay list is not present diff --git a/damus/Core/Networking/NostrNetworkManager/UserRelayListManager.swift b/damus/Core/Networking/NostrNetworkManager/UserRelayListManager.swift index 033c975e..ee7b31b5 100644 --- a/damus/Core/Networking/NostrNetworkManager/UserRelayListManager.swift +++ b/damus/Core/Networking/NostrNetworkManager/UserRelayListManager.swift @@ -30,6 +30,7 @@ extension NostrNetworkManager { // MARK: - Computing the relays to connect to + @MainActor private func relaysToConnectTo() -> [RelayPool.RelayDescriptor] { return self.computeRelaysToConnectTo(with: self.getBestEffortRelayList()) } @@ -49,6 +50,7 @@ extension NostrNetworkManager { /// It attempts to get a relay list from the user. If one is not available, it uses the default bootstrap list. /// /// This is always guaranteed to return a relay list. + @MainActor func getBestEffortRelayList() -> NIP65.RelayList { guard let userCurrentRelayList = self.getUserCurrentRelayList() else { return NIP65.RelayList(relays: delegate.bootstrapRelays) @@ -59,6 +61,7 @@ extension NostrNetworkManager { /// Gets the user's current relay list. /// /// It attempts to get a NIP-65 relay list from the local database, or falls back to a legacy list. + @MainActor func getUserCurrentRelayList() -> NIP65.RelayList? { if let latestRelayListEvent = try? self.getLatestNIP65RelayList() { return latestRelayListEvent } if let latestRelayListEvent = try? self.getLatestKind3RelayList() { return latestRelayListEvent } @@ -93,6 +96,7 @@ extension NostrNetworkManager { /// Gets the latest `kind:3` relay list from NostrDB. /// /// This is `private` because it is part of internal logic. Callers should use the higher level functions. + @MainActor private func getLatestKind3RelayList() throws(LoadingError) -> NIP65.RelayList? { guard let latestContactListEvent = delegate.latestContactListEvent else { return nil } guard let legacyContactList = try? NIP65.RelayList.fromLegacyContactList(latestContactListEvent) else { throw .relayListParseError } @@ -114,6 +118,7 @@ extension NostrNetworkManager { /// Gets the creation date of the user's current relay list, with preference to NIP-65 relay lists /// - Returns: The current relay list's creation date + @MainActor private func getUserCurrentRelayListCreationDate() -> UInt32? { if let latestNIP65RelayListEvent = self.getLatestNIP65RelayListEvent() { return latestNIP65RelayListEvent.created_at } if let latestKind3RelayListEvent = delegate.latestContactListEvent { return latestKind3RelayListEvent.created_at } @@ -134,7 +139,7 @@ extension NostrNetworkManager { func listenAndHandleRelayUpdates() async { let filter = NostrFilter(kinds: [.relay_list], authors: [delegate.keypair.pubkey]) for await noteLender in self.reader.streamIndefinitely(filters: [filter]) { - let currentRelayListCreationDate = self.getUserCurrentRelayListCreationDate() + let currentRelayListCreationDate = await self.getUserCurrentRelayListCreationDate() guard let note = noteLender.justGetACopy() else { continue } guard note.pubkey == self.delegate.keypair.pubkey else { continue } // Ensure this new list was ours guard note.created_at > (currentRelayListCreationDate ?? 0) else { continue } // Ensure this is a newer list @@ -147,7 +152,7 @@ extension NostrNetworkManager { // MARK: - Editing the user's relay list func upsert(relay: NIP65.RelayList.RelayItem, force: Bool = false, overwriteExisting: Bool = false) async throws(UpdateError) { - guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList } + guard let currentUserRelayList = await force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList } guard !currentUserRelayList.relays.keys.contains(relay.url) || overwriteExisting else { throw .relayAlreadyExists } var newList = currentUserRelayList.relays newList[relay.url] = relay @@ -155,13 +160,13 @@ extension NostrNetworkManager { } func insert(relay: NIP65.RelayList.RelayItem, force: Bool = false) async throws(UpdateError) { - guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList } + guard let currentUserRelayList = await force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList } guard currentUserRelayList.relays[relay.url] == nil else { throw .relayAlreadyExists } try await self.upsert(relay: relay, force: force) } func remove(relayURL: RelayURL, force: Bool = false) async throws(UpdateError) { - guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList } + guard let currentUserRelayList = await force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList } guard currentUserRelayList.relays.keys.contains(relayURL) || force else { throw .noSuchRelay } var newList = currentUserRelayList.relays newList[relayURL] = nil diff --git a/damus/Core/Storage/DamusState.swift b/damus/Core/Storage/DamusState.swift index 2896cbdd..e62f3bba 100644 --- a/damus/Core/Storage/DamusState.swift +++ b/damus/Core/Storage/DamusState.swift @@ -177,6 +177,7 @@ class DamusState: HeadlessDamusState, ObservableObject { } } + @MainActor static var empty: DamusState { let empty_pub: Pubkey = .empty let empty_sec: Privkey = .empty @@ -226,6 +227,7 @@ fileprivate extension DamusState { set { self.settings.latestRelayListEventIdHex = newValue } } + @MainActor var latestContactListEvent: NostrEvent? { self.contacts.event } var bootstrapRelays: [RelayURL] { get_default_bootstrap_relays() } var developerMode: Bool { self.settings.developer_mode } diff --git a/damus/Features/DMs/Views/DirectMessagesView.swift b/damus/Features/DMs/Views/DirectMessagesView.swift index ad1e8451..d431fa61 100644 --- a/damus/Features/DMs/Views/DirectMessagesView.swift +++ b/damus/Features/DMs/Views/DirectMessagesView.swift @@ -122,6 +122,7 @@ struct DirectMessagesView: View { } } +@MainActor func would_filter_non_friends_from_dms(contacts: Contacts, dms: [DirectMessageModel]) -> Bool { for dm in dms { if !FriendFilter.friends_of_friends.filter(contacts: contacts, pubkey: dm.pubkey) { diff --git a/damus/Features/Events/EventView.swift b/damus/Features/Events/EventView.swift index ff1d59e6..d4bb91e5 100644 --- a/damus/Features/Events/EventView.swift +++ b/damus/Features/Events/EventView.swift @@ -58,6 +58,7 @@ struct EventView: View { } // blame the porn bots for this code +@MainActor func should_blur_images(settings: UserSettingsStore, contacts: Contacts, ev: NostrEvent, our_pubkey: Pubkey, booster_pubkey: Pubkey? = nil) -> Bool { if settings.undistractMode { return true @@ -80,6 +81,7 @@ func should_blur_images(settings: UserSettingsStore, contacts: Contacts, ev: Nos } // blame the porn bots for this code too +@MainActor func should_blur_images(damus_state: DamusState, ev: NostrEvent) -> Bool { return should_blur_images( settings: damus_state.settings, diff --git a/damus/Features/Follows/Models/Contacts.swift b/damus/Features/Follows/Models/Contacts.swift index ba7b816a..3bc78a70 100644 --- a/damus/Features/Follows/Models/Contacts.swift +++ b/damus/Features/Follows/Models/Contacts.swift @@ -7,6 +7,7 @@ import Foundation +@MainActor class Contacts { private var friends: Set = Set() private var friend_of_friends: Set = Set() diff --git a/damus/Features/NIP05/Models/NIP05DomainEventsModel.swift b/damus/Features/NIP05/Models/NIP05DomainEventsModel.swift index 6b0bf2f3..88505fcc 100644 --- a/damus/Features/NIP05/Models/NIP05DomainEventsModel.swift +++ b/damus/Features/NIP05/Models/NIP05DomainEventsModel.swift @@ -46,7 +46,7 @@ class NIP05DomainEventsModel: ObservableObject { filter.kinds = [.text, .longform, .highlight] var authors = Set() - for pubkey in state.contacts.get_friend_of_friends_list() { + for pubkey in await state.contacts.get_friend_of_friends_list() { guard let profile = try? state.profiles.lookup(id: pubkey), let nip05_str = profile.nip05, let nip05 = NIP05.parse(nip05_str), diff --git a/damus/Features/Notifications/Models/NotificationsManager.swift b/damus/Features/Notifications/Models/NotificationsManager.swift index 72f1d369..c1b2bdee 100644 --- a/damus/Features/Notifications/Models/NotificationsManager.swift +++ b/damus/Features/Notifications/Models/NotificationsManager.swift @@ -36,7 +36,7 @@ func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent } if state.settings.notification_only_from_following, - state.contacts.follow_state(ev.pubkey) != .follows + await state.contacts.follow_state(ev.pubkey) != .follows { return false } diff --git a/damus/Features/Notifications/Views/NotificationsView.swift b/damus/Features/Notifications/Views/NotificationsView.swift index 7a78f8db..94bb8868 100644 --- a/damus/Features/Notifications/Views/NotificationsView.swift +++ b/damus/Features/Notifications/Views/NotificationsView.swift @@ -33,6 +33,7 @@ class NotificationFilter: ObservableObject, Equatable { self.hellthread_notification_max_pubkeys = hellthread_notification_max_pubkeys } + @MainActor func filter(contacts: Contacts, items: [NotificationItem]) -> [NotificationItem] { return items.reduce(into: []) { acc, item in @@ -214,6 +215,7 @@ struct NotificationsView_Previews: PreviewProvider { } } +@MainActor func would_filter_non_friends_from_notifications(contacts: Contacts, state: NotificationFilterState, items: [NotificationItem]) -> Bool { for item in items { // this is only valid depending on which tab we're looking at diff --git a/damus/Features/Posting/Models/PostBox.swift b/damus/Features/Posting/Models/PostBox.swift index b34e7b60..97edaaed 100644 --- a/damus/Features/Posting/Models/PostBox.swift +++ b/damus/Features/Posting/Models/PostBox.swift @@ -53,7 +53,7 @@ enum CancelSendErr { case too_late } -class PostBox { +actor PostBox { private let pool: RelayPool var events: [NoteId: PostedEvent] @@ -65,7 +65,7 @@ class PostBox { Task { await self.pool.register_handler(sub_id: "postbox", filters: nil, to: nil, handler: streamContinuation) } } for await (relayUrl, connectionEvent) in stream { - handle_event(relay_id: relayUrl, connectionEvent) + await handle_event(relay_id: relayUrl, connectionEvent) } } } diff --git a/damus/Features/Profile/Models/ProfileModel.swift b/damus/Features/Profile/Models/ProfileModel.swift index fa67159d..ab93f892 100644 --- a/damus/Features/Profile/Models/ProfileModel.swift +++ b/damus/Features/Profile/Models/ProfileModel.swift @@ -136,6 +136,7 @@ class ProfileModel: ObservableObject, Equatable { conversationListener = nil } + @MainActor func handle_profile_contact_event(_ ev: NostrEvent) { process_contact_event(state: damus, ev: ev) diff --git a/damus/Features/Profile/Views/ProfileName.swift b/damus/Features/Profile/Views/ProfileName.swift index a83f8003..e53e6c6c 100644 --- a/damus/Features/Profile/Views/ProfileName.swift +++ b/damus/Features/Profile/Views/ProfileName.swift @@ -20,6 +20,7 @@ enum FriendType { } } +@MainActor func get_friend_type(contacts: Contacts, pubkey: Pubkey) -> FriendType? { if contacts.is_friend_or_self(pubkey) { return .friend diff --git a/damus/Features/Profile/Views/ProfilePicView.swift b/damus/Features/Profile/Views/ProfilePicView.swift index 2543b42d..e8d2fb0a 100644 --- a/damus/Features/Profile/Views/ProfilePicView.swift +++ b/damus/Features/Profile/Views/ProfilePicView.swift @@ -141,6 +141,7 @@ func get_profile_url(picture: String?, pubkey: Pubkey, profiles: Profiles) -> UR return URL(string: robohash(pubkey))! } +@MainActor func make_preview_profiles(_ pubkey: Pubkey) -> Profiles { let profiles = Profiles(ndb: test_damus_state.ndb) //let picture = "http://cdn.jb55.com/img/red-me.jpg" diff --git a/damus/Features/Search/Views/SearchResultsView.swift b/damus/Features/Search/Views/SearchResultsView.swift index a3d60723..55a05250 100644 --- a/damus/Features/Search/Views/SearchResultsView.swift +++ b/damus/Features/Search/Views/SearchResultsView.swift @@ -206,7 +206,7 @@ struct SearchResultsView_Previews: PreviewProvider { } */ - +@MainActor func search_for_string(profiles: Profiles, contacts: Contacts, search new: String) -> Search? { guard new.count != 0 else { return nil @@ -267,6 +267,7 @@ func make_hashtagable(_ str: String) -> String { return String(new.filter{$0 != " "}) } +@MainActor func search_profiles(profiles: Profiles, contacts: Contacts, search: String) -> [Pubkey] { // Search by hex pubkey. if let pubkey = hex_decode_pubkey(search), diff --git a/damus/Features/Timeline/Models/FriendFilter.swift b/damus/Features/Timeline/Models/FriendFilter.swift index 315a1ed4..1b5b18c2 100644 --- a/damus/Features/Timeline/Models/FriendFilter.swift +++ b/damus/Features/Timeline/Models/FriendFilter.swift @@ -23,6 +23,7 @@ enum FriendFilter: String, StringCodable { self.rawValue } + @MainActor func filter(contacts: Contacts, pubkey: Pubkey) -> Bool { switch self { case .all: diff --git a/damus/Features/Timeline/Models/HomeModel.swift b/damus/Features/Timeline/Models/HomeModel.swift index 5133b8c8..b61595e2 100644 --- a/damus/Features/Timeline/Models/HomeModel.swift +++ b/damus/Features/Timeline/Models/HomeModel.swift @@ -26,6 +26,7 @@ enum HomeResubFilter { return nil } + @MainActor func filter(contacts: Contacts, ev: NostrEvent) -> Bool { switch self { case .pubkey(let pk): @@ -340,7 +341,7 @@ class HomeModel: ContactsDelegate, ObservableObject { // since command results are not returned for ephemeral events, // remove the request from the postbox which is likely failing over and over - if damus_state.nostrNetwork.postbox.remove_relayer(relay_id: nwc.relay, event_id: resp.req_id) { + if await damus_state.nostrNetwork.postbox.remove_relayer(relay_id: nwc.relay, event_id: resp.req_id) { Log.debug("HomeModel: got NWC response, removed %s from the postbox", for: .nwc, resp.req_id.hex()) } else { Log.debug("HomeModel: got NWC response, %s not found in the postbox, nothing to remove", for: .nwc, resp.req_id.hex()) @@ -925,6 +926,7 @@ func update_signal_from_pool(signal: SignalModel, pool: RelayPool) async { } } +@MainActor func add_contact_if_friend(contacts: Contacts, ev: NostrEvent) { if !contacts.is_friend(ev.pubkey) { return @@ -933,6 +935,7 @@ func add_contact_if_friend(contacts: Contacts, ev: NostrEvent) { contacts.add_friend_contact(ev) } +@MainActor func load_our_contacts(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) { let contacts = state.contacts let new_refs = Set(ev.referenced_follows) @@ -1043,6 +1046,7 @@ func robohash(_ pk: Pubkey) -> String { return "https://robohash.org/" + pk.hex() } +@MainActor func load_our_stuff(state: DamusState, ev: NostrEvent) { guard ev.pubkey == state.pubkey else { return @@ -1061,6 +1065,7 @@ func load_our_stuff(state: DamusState, ev: NostrEvent) { load_our_contacts(state: state, m_old_ev: m_old_ev, ev: ev) } +@MainActor func process_contact_event(state: DamusState, ev: NostrEvent) { load_our_stuff(state: state, ev: ev) add_contact_if_friend(contacts: state.contacts, ev: ev) diff --git a/damus/Features/Wallet/Views/WalletView.swift b/damus/Features/Wallet/Views/WalletView.swift index 6064b66e..2c967ceb 100644 --- a/damus/Features/Wallet/Views/WalletView.swift +++ b/damus/Features/Wallet/Views/WalletView.swift @@ -144,6 +144,7 @@ struct WalletView: View { } } +@MainActor let test_wallet_connect_url = WalletConnectURL(pubkey: test_pubkey, relay: .init("wss://relay.damus.io")!, keypair: test_damus_state.keypair.to_full()!, lud16: "jb55@sendsats.com") struct WalletView_Previews: PreviewProvider { diff --git a/damus/Features/Zaps/Views/NoteZapButton.swift b/damus/Features/Zaps/Views/NoteZapButton.swift index fe15f396..b76bf5aa 100644 --- a/damus/Features/Zaps/Views/NoteZapButton.swift +++ b/damus/Features/Zaps/Views/NoteZapButton.swift @@ -70,7 +70,7 @@ struct NoteZapButton: View { return Color.orange } - func tap() { + func tap() async { guard let our_zap else { Task { await send_zap(damus_state: damus_state, target: target, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: damus_state.settings.default_zap_type) } return @@ -84,7 +84,7 @@ struct NoteZapButton: View { print("cancel_zap: we already have a real zap, can't cancel") break case .pending(let pzap): - guard let res = cancel_zap(zap: pzap, box: damus_state.nostrNetwork.postbox, zapcache: damus_state.zaps, evcache: damus_state.events) else { + guard let res = await cancel_zap(zap: pzap, box: damus_state.nostrNetwork.postbox, zapcache: damus_state.zaps, evcache: damus_state.events) else { UIImpactFeedbackGenerator(style: .soft).impactOccurred() return @@ -146,7 +146,7 @@ struct NoteZapButton: View { .highPriorityGesture(TapGesture().onEnded { guard !damus_state.settings.nozaps else { return } - tap() + Task { await tap() } }) } } @@ -276,7 +276,7 @@ enum CancelZapErr { case not_nwc } -func cancel_zap(zap: PendingZap, box: PostBox, zapcache: Zaps, evcache: EventCache) -> CancelZapErr? { +func cancel_zap(zap: PendingZap, box: PostBox, zapcache: Zaps, evcache: EventCache) async -> CancelZapErr? { guard case .nwc(let nwc_state) = zap.state else { return .not_nwc } @@ -298,7 +298,7 @@ func cancel_zap(zap: PendingZap, box: PostBox, zapcache: Zaps, evcache: EventCac return .already_confirmed case .postbox_pending(let nwc_req): - if let err = box.cancel_send(evid: nwc_req.id) { + if let err = await box.cancel_send(evid: nwc_req.id) { return .send_err(err) } let reqid = ZapRequestId(from_pending: zap) diff --git a/damus/Shared/Components/NIP05Badge.swift b/damus/Shared/Components/NIP05Badge.swift index cac0aded..50190afa 100644 --- a/damus/Shared/Components/NIP05Badge.swift +++ b/damus/Shared/Components/NIP05Badge.swift @@ -109,6 +109,7 @@ extension View { } } +@MainActor func use_nip05_color(pubkey: Pubkey, contacts: Contacts) -> Bool { return contacts.is_friend_or_self(pubkey) ? true : false } diff --git a/damus/TestData.swift b/damus/TestData.swift index a312d136..f0b5b38e 100644 --- a/damus/TestData.swift +++ b/damus/TestData.swift @@ -63,9 +63,11 @@ let test_private_zap = Zap(event: test_note, invoice: test_zap_invoice, zapper: let test_pending_zap = PendingZap(amount_msat: 10000, target: .note(id: test_note.id, author: test_note.pubkey), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice))) +@MainActor let test_following_model = FollowingModel(damus_state: test_damus_state, contacts: [test_pubkey, test_pubkey_2], hashtags: [Hashtag(hashtag: "grownostr"), Hashtag(hashtag: "zapathon")]) +@MainActor var test_damus_state: DamusState = ({ // Create a unique temporary directory var tempDir: String! diff --git a/damusTests/DamusCacheManagerTests.swift b/damusTests/DamusCacheManagerTests.swift index dc90beea..674e2be8 100644 --- a/damusTests/DamusCacheManagerTests.swift +++ b/damusTests/DamusCacheManagerTests.swift @@ -23,6 +23,7 @@ final class DamusCacheManagerTests: XCTestCase { } /// Simple smoke test to check if clearing cache will crash the system + @MainActor func testCacheManagerSmoke() throws { for _ in Range(0...20) { DamusCacheManager.shared.clear_cache(damus_state: test_damus_state) diff --git a/damusTests/EventGroupViewTests.swift b/damusTests/EventGroupViewTests.swift index ff960989..b3240b11 100644 --- a/damusTests/EventGroupViewTests.swift +++ b/damusTests/EventGroupViewTests.swift @@ -18,6 +18,7 @@ final class EventGroupViewTests: XCTestCase { // Put teardown code here. This method is called after the invocation of each test method in the class. } + @MainActor func testEventAuthorName() { let damusState = test_damus_state let damus_name = "17ldvg64:nq5mhr77" @@ -26,6 +27,7 @@ final class EventGroupViewTests: XCTestCase { XCTAssertEqual(event_author_name(profiles: damusState.profiles, pubkey: ANON_PUBKEY), "Anonymous") } + @MainActor func testEventGroupUniquePubkeys() { let damusState = test_damus_state @@ -48,6 +50,7 @@ final class EventGroupViewTests: XCTestCase { XCTAssertEqual(event_group_unique_pubkeys(profiles: damusState.profiles, group: .repost(EventGroup(events: [repost1, repost2, repost3]))), [pk1, pk2, pk3]) } + @MainActor func testReactingToText() throws { let enUsLocale = Locale(identifier: "en-US") let damusState = test_damus_state diff --git a/damusTests/Models/ContactCardManagerTests.swift b/damusTests/Models/ContactCardManagerTests.swift index 84e714ac..e16424ec 100644 --- a/damusTests/Models/ContactCardManagerTests.swift +++ b/damusTests/Models/ContactCardManagerTests.swift @@ -23,6 +23,7 @@ final class ContactCardManagerTests: XCTestCase { XCTAssertFalse(result) } + @MainActor func testIsFavorite_WhenPubkeyExists_ReturnsTrue() { // Given: A pubkey added to favorites let sut = ContactCardManager() @@ -36,6 +37,7 @@ final class ContactCardManagerTests: XCTestCase { XCTAssertTrue(result) } + @MainActor func testIsFavorite_WhenPubkeyDoesNotExist_ReturnsFalse() { // Given: A different pubkey added to favorites let sut = ContactCardManager() @@ -50,6 +52,7 @@ final class ContactCardManagerTests: XCTestCase { XCTAssertFalse(result) } + @MainActor func testToggleFavorite_WhenNotFavorite_AddsToFavorites() { // Given: A pubkey not in favorites let sut = ContactCardManager() @@ -64,6 +67,7 @@ final class ContactCardManagerTests: XCTestCase { XCTAssertEqual(sut.favorites.count, 1) } + @MainActor func testToggleFavorite_WhenAlreadyFavorite_RemovesFromFavorites() { // Given: A pubkey already in favorites let sut = ContactCardManager() @@ -105,6 +109,7 @@ final class ContactCardManagerTests: XCTestCase { XCTAssertTrue(sut.isFavorite(targetPubkey)) } + @MainActor func testloadEvent_WithContactCard_RemovesFromFavorites() { // Given: A contact card event without favorite tag (unfavorite) let sut = ContactCardManager() @@ -219,6 +224,7 @@ final class ContactCardManagerTests: XCTestCase { XCTAssertTrue(sut.isFavorite(targetPubkey)) } + @MainActor func testFilter_WithFavoritePubkey_ReturnsTrue() { // Given: A pubkey in favorites let sut = ContactCardManager() diff --git a/damusTests/MutingTests.swift b/damusTests/MutingTests.swift index 26c9bb1b..b61ff068 100644 --- a/damusTests/MutingTests.swift +++ b/damusTests/MutingTests.swift @@ -10,6 +10,7 @@ import XCTest @testable import damus final class MutingTests: XCTestCase { + @MainActor func testWordMuting() async { // Setup some test data let test_note = NostrEvent( diff --git a/damusTests/NoteContentViewTests.swift b/damusTests/NoteContentViewTests.swift index 398f61a5..b46dba1b 100644 --- a/damusTests/NoteContentViewTests.swift +++ b/damusTests/NoteContentViewTests.swift @@ -344,6 +344,7 @@ class NoteContentViewTests: XCTestCase { /// Quick test that exercises the direct parsing methods (i.e. not fetching blocks from nostrdb) from `NdbBlockGroup`, and its bridging code with C. /// The parsing logic itself already has test coverage at the nostrdb level. + @MainActor func testDirectBlockParsing() { let kp = test_keypair_full let dm: NdbNote = NIP04.create_dm("Test", to_pk: kp.pubkey, tags: [], keypair: kp.to_keypair())! @@ -360,24 +361,28 @@ class NoteContentViewTests: XCTestCase { }) } + @MainActor func testMentionStr_Pubkey_ContainsAbbreviated() throws { let compatibleText = createCompatibleText(test_pubkey.npub) assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: "17ldvg64:nq5mhr77") } + @MainActor func testMentionStr_Pubkey_ContainsFullBech32() { let compatableText = createCompatibleText(test_pubkey.npub) assertCompatibleTextHasExpectedString(compatibleText: compatableText, expected: test_pubkey.npub) } + @MainActor func testMentionStr_Nprofile_ContainsAbbreviated() throws { let compatibleText = createCompatibleText("nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p") assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: "180cvv07:wsyjh6w6") } + @MainActor func testMentionStr_Nprofile_ContainsFullBech32() throws { let bech = "nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p" let compatibleText = createCompatibleText(bech) @@ -385,18 +390,21 @@ class NoteContentViewTests: XCTestCase { assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: bech) } + @MainActor func testMentionStr_Note_ContainsAbbreviated() { let compatibleText = createCompatibleText(test_note.id.bech32) assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: "note1qqq:qqn2l0z3") } + @MainActor func testMentionStr_Note_ContainsFullBech32() { let compatibleText = createCompatibleText(test_note.id.bech32) assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: test_note.id.bech32) } + @MainActor func testMentionStr_Nevent_ContainsAbbreviated() { let bech = "nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ez6un9d3shjtn4de6x2argwghx6egpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5nxnepm" let compatibleText = createCompatibleText(bech) @@ -404,6 +412,7 @@ class NoteContentViewTests: XCTestCase { assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: "nevent1q:t5nxnepm") } + @MainActor func testMentionStr_Nevent_ContainsFullBech32() throws { let bech = "nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ez6un9d3shjtn4de6x2argwghx6egpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5nxnepm" let compatibleText = createCompatibleText(bech) @@ -411,6 +420,7 @@ class NoteContentViewTests: XCTestCase { assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: bech) } + @MainActor func testMentionStr_Naddr_ContainsAbbreviated() { let bech = "naddr1qqxnzdesxqmnxvpexqunzvpcqyt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueqzypve7elhmamff3sr5mgxxms4a0rppkmhmn7504h96pfcdkpplvl2jqcyqqq823cnmhuld" let compatibleText = createCompatibleText(bech) @@ -418,6 +428,7 @@ class NoteContentViewTests: XCTestCase { assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: "naddr1qq:3cnmhuld") } + @MainActor func testMentionStr_Naddr_ContainsFullBech32() { let bech = "naddr1qqxnzdesxqmnxvpexqunzvpcqyt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueqzypve7elhmamff3sr5mgxxms4a0rppkmhmn7504h96pfcdkpplvl2jqcyqqq823cnmhuld" let compatibleText = createCompatibleText(bech) @@ -436,6 +447,7 @@ private func assertCompatibleTextHasExpectedString(compatibleText: CompatibleTex XCTAssertTrue(hasExpected) } +@MainActor private func createCompatibleText(_ bechString: String) -> CompatibleText { guard let mentionRef = Bech32Object.parse(bechString)?.toMentionRef() else { XCTFail("Failed to create MentionRef from Bech32 string") diff --git a/damusTests/RepostedTests.swift b/damusTests/RepostedTests.swift index 0fce7f72..b8cde31f 100644 --- a/damusTests/RepostedTests.swift +++ b/damusTests/RepostedTests.swift @@ -10,6 +10,7 @@ import XCTest final class RepostedTests: XCTestCase { + @MainActor func testPeopleRepostedText() throws { let enUsLocale = Locale(identifier: "en-US") let damusState = test_damus_state diff --git a/damusTests/WalletConnectTests.swift b/damusTests/WalletConnectTests.swift index 4f6f96d5..f8651484 100644 --- a/damusTests/WalletConnectTests.swift +++ b/damusTests/WalletConnectTests.swift @@ -94,8 +94,9 @@ final class WalletConnectTests: XCTestCase { XCTAssertEqual(pool.all_descriptors.count, 1) XCTAssertEqual(pool.all_descriptors[0].variant, .nwc) XCTAssertEqual(pool.all_descriptors[0].url.url.absoluteString, "ws://127.0.0.1") - XCTAssertEqual(box.events.count, 1) - let ev = box.events.first!.value + let boxEventCount = await box.events.count + XCTAssertEqual(boxEventCount, 1) + let ev = await box.events.first!.value XCTAssertEqual(ev.skip_ephemeral, false) XCTAssertEqual(ev.remaining.count, 1) XCTAssertEqual(ev.remaining[0].relay.url.absoluteString, "ws://127.0.0.1") diff --git a/damusTests/ZapTests.swift b/damusTests/ZapTests.swift index 2936f17d..f475a231 100644 --- a/damusTests/ZapTests.swift +++ b/damusTests/ZapTests.swift @@ -17,6 +17,7 @@ final class ZapTests: XCTestCase { // Put teardown code here. This method is called after the invocation of each test method in the class. } + @MainActor func test_alby_zap() throws { let zapjson = "eyJjb250ZW50Ijoi4pqhTm9uLWN1c3RvZGlhbCB6YXAgZnJvbSBteSBBbGJ5IEh1YiIsImNyZWF0ZWRfYXQiOjE3MjQ2ODUwNDcsImlkIjoiNGM3NWFiMWU3MDk4Y2NiN2FlYjhmZjdkNDIwMjM2ZDM1N2U1OGNjZmI3OWZiZTEwMTcwNGNiMzY0OTg3YjY4YSIsImtpbmQiOjk3MzUsInB1YmtleSI6Ijc5ZjAwZDNmNWExOWVjODA2MTg5ZmNhYjAzYzFiZTRmZjgxZDE4ZWU0ZjY1M2M4OGZhYzQxZmUwMzU3MGY0MzIiLCJzaWciOiI3OWM5ZDJjN2ExZWI1NmNhZjMyOTY1ZTRkMDJlYjJiYjFmYTY3NGViMDM4ZWE2MmFjZTg2YzBiMzA2OTJhMjU0YWU0M2JhNmMzNjcyMDJkZjgxNzQ5NGNhNTg4NzRkNWI1OWMxY2VhMDdjZTk5Mjk0MmIyOWYwZmVlZmJlM2FiZCIsInRhZ3MiOltbInAiLCIxNWI1Y2Y2Y2RmNGZkMWMwMmYyOGJjY2UwZjE5N2NhZmFlNGM4YzdjNjZhM2UyZTIzYWY5ZmU2MTA4NzUzMTVlIl0sWyJlIiwiYmNiMmZjZmUxYzQ2N2M1ZWM4Mjg1ZTM4NWMzNmVjMTM4Nzk3MDljZWQ5ZDg4MDBjYjM0MGViZjIxOGMzMjEwZCJdLFsiUCIsIjA1MjFkYjk1MzEwOTZkZmY3MDBkY2Y0MTBiMDFkYjQ3YWI2NTk4ZGU3ZTVlZjJjNWEyYmQ3ZTExNjAzMTViZjYiXSxbImJvbHQxMSIsImxuYmMxMHUxcG52ZXhoM2RwdXUyZDJ6bm4wZGNra3hhdG53M2hrZzZ0cGRzczg1Y3RzeXBuOHltbWR5cGtoamd6cGQzMzhqZ3pndzQzcW5wNHEyMjhhMnp0eGt3emF5cHZ6cnNoODIzcW5nbXY5N2YydjlwdXd2dHNhZGV0eXBtdXR5c2N3cHA1dmxjbGwwMHpwcGhoMzJ3OHV0NWpwcDVhMmZtcWg4c3o3bnUyaDd2MDdyMHU1bHN3ZzVsc3NwNXh2YXFlZnpsY2t6bXYwdzg5bHIwazB5dnI1eGQybmc1MmE1cmNkYXJmbTRmMGEwd2dwdXE5cXl5c2dxY3FwY3hxeXo1dnFlcHMzOXNleDUyc2ZtdHU5Z25tNWRhcGs1bGdsZDRwcDk2dXI1YTRhbTk0MHEyNXd6ZHNycmo1MjN4eWEwcnV4YTVscjk2M2cwMjk2cjZtZGZ5MjR2NjUzZXZjcHh5cjBtbWhnd21zcXh2cmhmZCJdLFsicHJlaW1hZ2UiLCJhZDA0N2MwMmZlNWYwNTljODA4NzdkNzk0YmU4OGU0N2M2NDRlYmVkZmRmZTY2M2IyODljOTMxNmRiNDk1ZjJkIl0sWyJkZXNjcmlwdGlvbiIsIntcImtpbmRcIjo5NzM0LFwiY3JlYXRlZF9hdFwiOjE3MjQ2ODUwMzgsXCJjb250ZW50XCI6XCLimqFOb24tY3VzdG9kaWFsIHphcCBmcm9tIG15IEFsYnkgSHViXCIsXCJ0YWdzXCI6W1tcInBcIixcIjE1YjVjZjZjZGY0ZmQxYzAyZjI4YmNjZTBmMTk3Y2FmYWU0YzhjN2M2NmEzZTJlMjNhZjlmZTYxMDg3NTMxNWVcIl0sW1wicmVsYXlzXCIsXCJ3c3M6Ly9wdXJwbGVwYWcuZXMvXCIsXCJ3c3M6Ly9yZWxheS5nZXRhbGJ5LmNvbS92MVwiLFwid3NzOi8vbm9zdHIubW9tL1wiLFwid3NzOi8vbm9zdHIub3h0ci5kZXYvXCIsXCJ3c3M6Ly9ub3MubG9sL1wiLFwid3NzOi8vbm9zdHIud2luZS9cIixcIndzczovL3JlbGF5LmRhbXVzLmlvL1wiLFwid3NzOi8vcmVsYXkubm90b3NoaS53aW4vXCIsXCJ3c3M6Ly9lZGVuLm5vc3RyLmxhbmQvXCJdLFtcImFtb3VudFwiLFwiMTAwMDAwMFwiXSxbXCJlXCIsXCJiY2IyZmNmZTFjNDY3YzVlYzgyODVlMzg1YzM2ZWMxMzg3OTcwOWNlZDlkODgwMGNiMzQwZWJmMjE4YzMyMTBkXCJdXSxcInB1YmtleVwiOlwiMDUyMWRiOTUzMTA5NmRmZjcwMGRjZjQxMGIwMWRiNDdhYjY1OThkZTdlNWVmMmM1YTJiZDdlMTE2MDMxNWJmNlwiLFwiaWRcIjpcIjU3ZDg2MTIwMDc1MjFjMGI1MzJiOTFhZjI0OTgwOTVhMjUxZTYzZjQyNTE4N2U2Yzk1NzAwZmQwYTZiYWI3ZDRcIixcInNpZ1wiOlwiNzk4ZDczNTExOGJjZDE0MjI4YTEyYjZkNTI0MjNmZjI1YmI0ZWQ4Y2Q1ZGFjZjJmNTk3MWVmNTczZmRjM2ZjMDVmYzc5MzE4NWU2OTY4MmNjYTI0M2Q2NGYxNDdhNDQ5ODk2OGEwYmMyODhhZTgzZTc1YzAzZTk5ZjkzNmE2MDNcIn0iXV19Cg==" @@ -75,6 +76,7 @@ final class ZapTests: XCTestCase { XCTAssertEqual(message, decrypted.content) } + @MainActor func testZap() throws { let zapjson = "eyJpZCI6IjUzNmJlZTllODNjODE4ZTNiODJjMTAxOTM1MTI4YWUyN2EwZDQyOTAwMzlhYWYyNTNlZmU1ZjA5MjMyYzE5NjIiLCJwdWJrZXkiOiI5NjMwZjQ2NGNjYTZhNTE0N2FhOGEzNWYwYmNkZDNjZTQ4NTMyNGU3MzJmZDM5ZTA5MjMzYjFkODQ4MjM4ZjMxIiwiY3JlYXRlZF9hdCI6MTY3NDIwNDUzNSwia2luZCI6OTczNSwidGFncyI6W1sicCIsIjMyZTE4Mjc2MzU0NTBlYmIzYzVhN2QxMmMxZjhlN2IyYjUxNDQzOWFjMTBhNjdlZWYzZDlmZDljNWM2OGUyNDUiXSxbImJvbHQxMSIsImxuYmMxMHUxcDN1NTR0bnNwNTcyOXF2eG5renRqamtkNTg1eW4wbDg2MzBzMm01eDZsNTZ3eXk0ZWMybnU4eHV6NjI5eHFwcDV2MnE3aHVjNGpwamgwM2Z4OHVqZXQ1Nms3OWd4cXg3bWUycGV2ejZqMms4dDhtNGxnNXZxaHA1eWc1MDU3OGNtdWoyNG1mdDNxcnNybWd3ZjMwa2U3YXY3ZDc3Z2FtZmxkazlrNHNmMzltcXhxeWp3NXFjcXBqcnpqcTJoeWVoNXEzNmx3eDZ6dHd5cmw2dm1tcnZ6NnJ1ZndqZnI4N3lremZuYXR1a200dWRzNHl6YWszc3FxOW1jcXFxcXFxcWxncXFxcTg2cXF5ZzlxeHBxeXNncWFkeWVjdmR6ZjI3MHBkMzZyc2FmbDA3azQ1ZmNqMnN5OGU1djJ0ZW5kNTB2OTU3NnV4cDNkdmp6amV1aHJlODl5cGdjbTkwZDZsbTAwNGszMHlqNGF2NW1jc3M1bnl4NHU5bmVyOWdwcHY2eXF3Il0sWyJkZXNjcmlwdGlvbiIsIntcImlkXCI6XCJiMDkyMTYzNGIxYmI4ZWUzNTg0YmJiZjJlOGQ3OTBhZDk4NTk5ZDhlMDhmODFjNzAwZGRiZTQ4MjAxNTY4Yjk3XCIsXCJwdWJrZXlcIjpcIjdmYTU2ZjVkNjk2MmFiMWUzY2Q0MjRlNzU4YzMwMDJiODY2NWY3YjBkOGRjZWU5ZmU5ZTI4OGQ3NzUxYWMxOTRcIixcImNyZWF0ZWRfYXRcIjoxNjc0MjA0NTMxLFwia2luZFwiOjk3MzQsXCJ0YWdzXCI6W1tcInBcIixcIjMyZTE4Mjc2MzU0NTBlYmIzYzVhN2QxMmMxZjhlN2IyYjUxNDQzOWFjMTBhNjdlZWYzZDlmZDljNWM2OGUyNDVcIl0sW1wicmVsYXlzXCIsXCJ3c3M6Ly9yZWxheS5zbm9ydC5zb2NpYWxcIixcIndzczovL3JlbGF5LmRhbXVzLmlvXCIsXCJ3c3M6Ly9ub3N0ci1wdWIud2VsbG9yZGVyLm5ldFwiLFwid3NzOi8vbm9zdHIudjBsLmlvXCIsXCJ3c3M6Ly9wcml2YXRlLW5vc3RyLnYwbC5pb1wiLFwid3NzOi8vbm9zdHIuemViZWRlZS5jbG91ZFwiLFwid3NzOi8vcmVsYXkubm9zdHIuaW5mby9cIl1dLFwiY29udGVudFwiOlwiXCIsXCJzaWdcIjpcImQwODQwNGU2MjVmOWM1NjMzYWZhZGQxMWMxMTBiYTg4ZmNkYjRiOWUwOTJiOTg0MGU3NDgyYThkNTM3YjFmYzExODY5MmNmZDEzMWRkODMzNTM2NDc2OWE2NzE3NTRhZDdhYTk3MzEzNjgzYTRhZDdlZmI3NjQ3NmMwNGU1ZjE3XCJ9Il0sWyJwcmVpbWFnZSIsIjNlMDJhM2FmOGM4YmNmMmEzNzUzYzg3ZjMxMTJjNjU2YTIwMTE0ZWUwZTk4ZDgyMTliYzU2ZjVlOGE3MjM1YjMiXV0sImNvbnRlbnQiOiIiLCJzaWciOiIzYWI0NGQwZTIyMjhiYmQ0ZDIzNDFjM2ZhNzQwOTZjZmY2ZjU1Y2ZkYTk5YTVkYWRjY2Y0NWM2NjQ2MzdlMjExNTFiMmY5ZGQwMDQwZjFhMjRlOWY4Njg2NzM4YjE2YmY4MTM0YmRiZTQxYTIxOGM5MTFmN2JiMzFlNTk1NzhkMSJ9Cg=="