mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-11-01 07:53:26 +00:00
Review fixes: geo dedup and geoDM names in recent chats
- A person still visible in the geohash roster could also get a recent chat row (the builder only subtracted mesh rows). Visible geohash pubkeys now join the identity set via their PeerID(nostr_:) form, so the absent-from-rosters contract holds on location channels too. - GeoDM rows went through resolveNickname, which only consults mesh identity sources and rendered nostr_… keys as an opaque anon fallback. They now resolve through geohashDisplayName (Nostr key mapping, bare-key fallback). Both pinned in the recent-chats architecture test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
71c2ea2a93
commit
1f348c1e8c
@ -265,7 +265,7 @@ final class PeerListModel: ObservableObject {
|
||||
|
||||
let geohashPeople = buildGeohashPeople()
|
||||
let groupRows = buildGroupRows()
|
||||
let recentChatRows = buildRecentChatRows(meshRows: meshRows)
|
||||
let recentChatRows = buildRecentChatRows(meshRows: meshRows, geohashPeople: geohashPeople)
|
||||
|
||||
self.meshRows = meshRows
|
||||
reachableMeshPeerCount = meshCounts.reachable
|
||||
@ -295,7 +295,10 @@ final class PeerListModel: ObservableObject {
|
||||
/// mirrored under both an ephemeral and a stable peer ID collapse to
|
||||
/// one row per identity (newest activity wins), matching how the
|
||||
/// private-chat coordinators consolidate on open.
|
||||
private func buildRecentChatRows(meshRows: [MeshPeerRow]) -> [RecentChatRow] {
|
||||
private func buildRecentChatRows(
|
||||
meshRows: [MeshPeerRow],
|
||||
geohashPeople: [GeohashPersonRow]
|
||||
) -> [RecentChatRow] {
|
||||
// A conversation can be keyed by the stable Noise peer ID while the
|
||||
// roster lists the ephemeral one — compare fingerprints too.
|
||||
var visibleIdentities = Set<String>()
|
||||
@ -305,6 +308,12 @@ final class PeerListModel: ObservableObject {
|
||||
visibleIdentities.insert(fingerprint)
|
||||
}
|
||||
}
|
||||
// Someone visible in the geohash section must not also get a chat
|
||||
// row: their GeoDM conversation is keyed by the nostr_ form of the
|
||||
// same pubkey the roster lists.
|
||||
for person in geohashPeople {
|
||||
visibleIdentities.insert(PeerID(nostr_: person.id).id)
|
||||
}
|
||||
|
||||
struct Candidate {
|
||||
let peerID: PeerID
|
||||
@ -337,13 +346,23 @@ final class PeerListModel: ObservableObject {
|
||||
.map { candidate in
|
||||
RecentChatRow(
|
||||
peerID: candidate.peerID,
|
||||
displayName: chatViewModel.resolveNickname(for: candidate.peerID),
|
||||
displayName: displayName(for: candidate.peerID),
|
||||
hasUnread: chatViewModel.hasUnreadMessages(for: candidate.peerID),
|
||||
lastActivity: candidate.lastActivity
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// GeoDM conversations resolve through the Nostr mapping —
|
||||
/// `resolveNickname` only consults mesh identity sources and would
|
||||
/// render a `nostr_…` key as an opaque `anonnost` fallback.
|
||||
private func displayName(for peerID: PeerID) -> String {
|
||||
if peerID.isGeoDM {
|
||||
return chatViewModel.geohashDisplayName(for: peerID)
|
||||
}
|
||||
return chatViewModel.resolveNickname(for: peerID)
|
||||
}
|
||||
|
||||
private static func changedConversationID(_ change: ConversationChange) -> ConversationID {
|
||||
switch change {
|
||||
case .appended(let id, _),
|
||||
|
||||
@ -506,6 +506,40 @@ struct AppArchitectureTests {
|
||||
#expect(peerListModel.recentChatRows.first?.lastActivity == Date(timeIntervalSince1970: 100))
|
||||
// The roster doesn't list this peer — that's exactly why the row exists.
|
||||
#expect(!peerListModel.meshRows.contains { $0.peerID == offlinePeerID })
|
||||
|
||||
// A geoDM thread resolves through the Nostr mapping (bare-key
|
||||
// fallback here), never resolveNickname's mesh-only anon fallback.
|
||||
let pubkeyHex = String(repeating: "ab", count: 32)
|
||||
let geoDMPeer = PeerID(nostr_: pubkeyHex)
|
||||
viewModel.seedPrivateChat([
|
||||
BitchatMessage(
|
||||
id: "geo-1",
|
||||
sender: "stranger",
|
||||
content: "hello from another cell",
|
||||
timestamp: Date(timeIntervalSince1970: 300),
|
||||
isRelay: false,
|
||||
isPrivate: true,
|
||||
recipientNickname: "me",
|
||||
senderPeerID: geoDMPeer
|
||||
)
|
||||
], for: geoDMPeer)
|
||||
|
||||
await waitUntil {
|
||||
peerListModel.recentChatRows.contains { $0.peerID == geoDMPeer }
|
||||
}
|
||||
let geoRow = peerListModel.recentChatRows.first { $0.peerID == geoDMPeer }
|
||||
#expect(geoRow?.displayName == geoDMPeer.bare)
|
||||
|
||||
// While that person is visible in the geohash roster, the chat row
|
||||
// collapses — same absent-from-rosters contract as mesh.
|
||||
viewModel.participantTracker.setActiveGeohash("u4pruy")
|
||||
viewModel.participantTracker.recordParticipant(pubkeyHex: pubkeyHex, geohash: "u4pruy")
|
||||
|
||||
await waitUntil {
|
||||
!peerListModel.recentChatRows.contains { $0.peerID == geoDMPeer }
|
||||
}
|
||||
#expect(!peerListModel.recentChatRows.contains { $0.peerID == geoDMPeer })
|
||||
#expect(peerListModel.recentChatRows.map(\.peerID) == [offlinePeerID])
|
||||
}
|
||||
|
||||
@Test("PrivateConversationModel resolves canonical header state for the selected DM")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user