diff --git a/bitchat/App/ConversationStore.swift b/bitchat/App/ConversationStore.swift index 4a201c90..df467075 100644 --- a/bitchat/App/ConversationStore.swift +++ b/bitchat/App/ConversationStore.swift @@ -417,6 +417,14 @@ final class ConversationStore: ObservableObject { } } + /// Erases the persisted last-active conversation. Called from the panic + /// wipe so a restored DM/channel pointer cannot survive an emergency clear. + /// The store owns its injected `storage`, so the key is removed through it + /// (never by reaching into `.standard`), and `lastActiveKey` stays private. + func clearPersistedLastActive() { + storage.removeObject(forKey: lastActiveKey) + } + /// Decides what to present at launch from the value persisted last /// session. Pure aside from reading the init snapshot: performs no /// selection mutation and never writes `activeChannel`, so it cannot race diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 49f38560..15088037 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -1177,6 +1177,9 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele // single-writer ConversationStore; the derived `messages` view and // the legacy mirror empty with it) conversations.clearAll() + // Also erase the persisted last-active pointer (#1064) so a wiped + // DM/channel cannot be restored on the next launch. + conversations.clearPersistedLastActive() pendingGeohashSystemMessages.removeAll() // Delete all keychain data (including Noise and Nostr keys) diff --git a/bitchatTests/ConversationStoreLastActiveTests.swift b/bitchatTests/ConversationStoreLastActiveTests.swift index cd57869b..faf5adc4 100644 --- a/bitchatTests/ConversationStoreLastActiveTests.swift +++ b/bitchatTests/ConversationStoreLastActiveTests.swift @@ -87,6 +87,31 @@ final class ConversationStoreLastActiveTests: XCTestCase { ) } + func test_clearPersistedLastActiveErasesRestoreRecord() { + // #1064 panic wipe: clearing the persisted last-active pointer means a + // wiped DM/channel cannot be restored on the next launch. + let storage = makeStorage() + + // A DM is persisted and would otherwise restore. + let session = ConversationStore(storage: storage) + session.setSelectedPrivatePeer(peerID) + XCTAssertEqual( + ConversationStore(storage: storage) + .restoreLastActiveConversation(isPeerResolvable: { _ in true }), + .restoredDirectChat(peerID) + ) + + // The panic path erases the pointer through the store's own storage. + session.clearPersistedLastActive() + + // The next launch has nothing to restore and falls back to the list. + XCTAssertEqual( + ConversationStore(storage: storage) + .restoreLastActiveConversation(isPeerResolvable: { _ in true }), + .conversationList + ) + } + func test_firstLaunchPresentsConversationList() { let storage = makeStorage()