fix: gate in-sheet actions on launch presentation; require isFavorite for DM restore

Addresses two Codex P2 review comments on #1364:

- ContentSheetViews fingerprint drill-down and image-picker gates missed
  the launch-presentation flag, so on the launch-fallback path (only
  presentsConversationListOnLaunch true) tapping a peer fingerprint was a
  silent no-op. Add || appChromeModel.presentsConversationListOnLaunch to
  both in-sheet gate conditions to match the ContentView call site.

- isDirectChatRestorable treated any non-nil favorite record as restorable,
  but removeFavorite retains a record (isFavorite: false, theyFavoritedUs:
  true) when the peer still favorites us, so a DM to an unfavorited peer
  reopened on restart. Key the resolver on isFavorite, not record existence.

Adds a regression test for the unfavorited-but-still-favorited-by-them peer.
This commit is contained in:
ecgang 2026-07-05 20:51:23 -07:00
parent 5a890559c1
commit c01be3b359
3 changed files with 24 additions and 3 deletions

View File

@ -185,7 +185,7 @@ final class AppRuntime: ObservableObject {
favorites: FavoritesPersistenceService
) -> Bool {
isDirectChatRestorable(peerID, isPeerFavorited: {
favorites.getFavoriteStatus(forPeerID: $0.toShort()) != nil
favorites.getFavoriteStatus(forPeerID: $0.toShort())?.isFavorite ?? false
})
}

View File

@ -85,7 +85,7 @@ struct ContentPeopleSheetView: View {
}
}
.navigationDestination(isPresented: Binding(
get: { appChromeModel.showingFingerprintFor != nil && (showSidebar || privateConversationModel.selectedPeerID != nil) },
get: { appChromeModel.showingFingerprintFor != nil && (showSidebar || privateConversationModel.selectedPeerID != nil || appChromeModel.presentsConversationListOnLaunch) },
set: { isPresented in
if !isPresented {
appChromeModel.clearFingerprint()
@ -105,7 +105,7 @@ struct ContentPeopleSheetView: View {
#endif
#if os(iOS)
.fullScreenCover(isPresented: Binding(
get: { showImagePicker && (showSidebar || privateConversationModel.selectedPeerID != nil) },
get: { showImagePicker && (showSidebar || privateConversationModel.selectedPeerID != nil || appChromeModel.presentsConversationListOnLaunch) },
set: { newValue in
if !newValue {
showImagePicker = false

View File

@ -183,6 +183,27 @@ final class ConversationStoreLastActiveTests: XCTestCase {
)
}
func test_production_unfavoritedPeerWhoStillFavoritesUsIsNotRestorable() {
// removeFavorite RETAINS a record (isFavorite: false, theyFavoritedUs:
// true) when the peer still favorites us. The resolver must key on
// isFavorite, not mere record existence otherwise a DM to a peer we
// deliberately unfavorited reopens on restart, contradicting the
// "is a persisted favorite" contract. Regression for Codex P2 review.
let favorites = FavoritesPersistenceService(keychain: MockKeychain())
let noiseKey = Data((0..<32).map(UInt8.init))
favorites.addFavorite(peerNoisePublicKey: noiseKey, peerNickname: "Alice")
favorites.updatePeerFavoritedUs(peerNoisePublicKey: noiseKey, favorited: true)
favorites.removeFavorite(peerNoisePublicKey: noiseKey)
let fullHexPeer = PeerID(str: noiseKey.hexEncodedString())
// The record survives (they still favorite us) but isFavorite is false.
XCTAssertNotNil(favorites.getFavoriteStatus(forPeerID: fullHexPeer.toShort()))
XCTAssertFalse(favorites.getFavoriteStatus(forPeerID: fullHexPeer.toShort())!.isFavorite)
XCTAssertFalse(
AppRuntime.isDirectChatRestorable(fullHexPeer, favorites: favorites)
)
}
// MARK: - Helpers
private func makeStorage() -> UserDefaults {