mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 13:25:20 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1bf069c74 | ||
|
|
2e43770d0e | ||
|
|
4b0634d1d0 |
@@ -39,10 +39,8 @@
|
||||
0481A3592E6D929E00FC845E /* tor-nolzma.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481A3572E6D929E00FC845E /* tor-nolzma.xcframework */; };
|
||||
0481A35B2E6D9BEF00FC845E /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481A35A2E6D9BEF00FC845E /* libz.tbd */; };
|
||||
0481A35D2E6DA18600FC845E /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481A35C2E6DA18600FC845E /* libz.tbd */; };
|
||||
0C0EFA112E6EAAAA00ABCDEF /* CTorHost.c in Sources */ = {isa = PBXBuildFile; fileRef = 0C0EFA102E6EAAAA00ABCDEF /* CTorHost.c */; };
|
||||
0C0EFA122E6EAAAA00ABCDF0 /* CTorHost.c in Sources */ = {isa = PBXBuildFile; fileRef = 0C0EFA102E6EAAAA00ABCDEF /* CTorHost.c */; };
|
||||
0C0EFA162E6EAABB00ABCDF4 /* TorNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0EFA132E6EAABB00ABCDF1 /* TorNotifications.swift */; };
|
||||
0C0EFA172E6EAABB00ABCDF5 /* TorNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0EFA132E6EAABB00ABCDF1 /* TorNotifications.swift */; };
|
||||
0481A3902E734CAE00FC845E /* CommandProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A38F2E734CAE00FC845E /* CommandProcessorTests.swift */; };
|
||||
0481A3912E734CAE00FC845E /* CommandProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A38F2E734CAE00FC845E /* CommandProcessorTests.swift */; };
|
||||
048A4BE72E5CCCC300162C4A /* TransportConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 048A4BE62E5CCCC300162C4A /* TransportConfig.swift */; };
|
||||
048A4BE82E5CCCC300162C4A /* TransportConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 048A4BE62E5CCCC300162C4A /* TransportConfig.swift */; };
|
||||
048A4BE92E5CCCC300162C4B /* TransportConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 048A4BE62E5CCCC300162C4A /* TransportConfig.swift */; };
|
||||
@@ -76,6 +74,10 @@
|
||||
049BD3B52E51F319001A566B /* MessageRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD3B02E51F319001A566B /* MessageRouter.swift */; };
|
||||
0AE840940F21AFC07C226636 /* PrivateChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A262EDDC04B7D7B5E31F321 /* PrivateChatE2ETests.swift */; };
|
||||
0B6F25559A21F8C69C8357C6 /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3CC6FA298729906109F61B /* BinaryProtocolTests.swift */; };
|
||||
0C0EFA112E6EAAAA00ABCDEF /* CTorHost.c in Sources */ = {isa = PBXBuildFile; fileRef = 0C0EFA102E6EAAAA00ABCDEF /* CTorHost.c */; };
|
||||
0C0EFA122E6EAAAA00ABCDF0 /* CTorHost.c in Sources */ = {isa = PBXBuildFile; fileRef = 0C0EFA102E6EAAAA00ABCDEF /* CTorHost.c */; };
|
||||
0C0EFA162E6EAABB00ABCDF4 /* TorNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0EFA132E6EAABB00ABCDF1 /* TorNotifications.swift */; };
|
||||
0C0EFA172E6EAABB00ABCDF5 /* TorNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0EFA132E6EAABB00ABCDF1 /* TorNotifications.swift */; };
|
||||
10E68BB889356219189E38EC /* BitchatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF625BB3AD919322C01A46B2 /* BitchatApp.swift */; };
|
||||
1234567890ABCDEFFEDCBA13 /* PeerDisplayNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1234567890ABCDEFFEDCBA02 /* PeerDisplayNameResolver.swift */; };
|
||||
1234567890ABCDEFFEDCBA14 /* PeerDisplayNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1234567890ABCDEFFEDCBA02 /* PeerDisplayNameResolver.swift */; };
|
||||
@@ -162,12 +164,12 @@
|
||||
E2DCF7817344F1CCDB8B7B2F /* SecureIdentityStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC18D910D6FF2E8B1B6C885 /* SecureIdentityStateManager.swift */; };
|
||||
E65BBB6544FE0159F3C6C3A8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 95F16C3A4A5621C74461D8D3 /* LaunchScreen.storyboard */; };
|
||||
EC5241969D2550B97629EBD0 /* SecureLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE7EFB209C86BBD956B749EC /* SecureLogger.swift */; };
|
||||
F0A1B2C3D4E5F60718293A4B /* OSLog+Categories.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A1B2C3D4E5F60718293A4A /* OSLog+Categories.swift */; };
|
||||
F0A1B2C3D4E5F60718293A4C /* OSLog+Categories.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A1B2C3D4E5F60718293A4A /* OSLog+Categories.swift */; };
|
||||
ED83C7AC1E6BEF15389C0132 /* PrivateChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A262EDDC04B7D7B5E31F321 /* PrivateChatE2ETests.swift */; };
|
||||
EE8C3ECADAB3083A2687D50B /* NostrProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C272F137CE00FC5A96E0CC06 /* NostrProtocolTests.swift */; };
|
||||
EF49C600C1E464710DD6CA29 /* InputValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90CB7A5CD1D1A521CD31F380 /* InputValidator.swift */; };
|
||||
F06732B1719EE13C5D09CE77 /* NostrProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5A9FF4AEA8A923317ED26A /* NostrProtocol.swift */; };
|
||||
F0A1B2C3D4E5F60718293A4B /* OSLog+Categories.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A1B2C3D4E5F60718293A4A /* OSLog+Categories.swift */; };
|
||||
F0A1B2C3D4E5F60718293A4C /* OSLog+Categories.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A1B2C3D4E5F60718293A4A /* OSLog+Categories.swift */; };
|
||||
F455F011B3B648ADA233F998 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; };
|
||||
FB8819B4C84FAFEF5C36B216 /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136696FC4436A02D98CE6A77 /* KeychainManager.swift */; };
|
||||
FBC409E105493C491531B59A /* NostrProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5A9FF4AEA8A923317ED26A /* NostrProtocol.swift */; };
|
||||
@@ -226,12 +228,11 @@
|
||||
047502B82E560F690083520F /* RelayController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayController.swift; sourceTree = "<group>"; };
|
||||
0481A3432E6D869F00FC845E /* TorManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorManager.swift; sourceTree = "<group>"; };
|
||||
0481A3442E6D869F00FC845E /* TorURLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorURLSession.swift; sourceTree = "<group>"; };
|
||||
0C0EFA102E6EAAAA00ABCDEF /* CTorHost.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = CTorHost.c; sourceTree = "<group>"; };
|
||||
0C0EFA132E6EAABB00ABCDF1 /* TorNotifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorNotifications.swift; sourceTree = "<group>"; };
|
||||
0481A3532E6D877600FC845E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
0481A3572E6D929E00FC845E /* tor-nolzma.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = "tor-nolzma.xcframework"; sourceTree = "<group>"; };
|
||||
0481A35A2E6D9BEF00FC845E /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; };
|
||||
0481A35C2E6DA18600FC845E /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; };
|
||||
0481A38F2E734CAE00FC845E /* CommandProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandProcessorTests.swift; sourceTree = "<group>"; };
|
||||
048A4BE62E5CCCC300162C4A /* TransportConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportConfig.swift; sourceTree = "<group>"; };
|
||||
048A4C272E5FCD6600162C4A /* GeohashBookmarksStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeohashBookmarksStore.swift; sourceTree = "<group>"; };
|
||||
048A4C2A2E5FCE0300162C4A /* GeohashBookmarksStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeohashBookmarksStoreTests.swift; sourceTree = "<group>"; };
|
||||
@@ -249,6 +250,8 @@
|
||||
049BD3B12E51F319001A566B /* NostrTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrTransport.swift; sourceTree = "<group>"; };
|
||||
05BA20BC0F123F1507C5C247 /* IdentityModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityModels.swift; sourceTree = "<group>"; };
|
||||
0B3CC6FA298729906109F61B /* BinaryProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocolTests.swift; sourceTree = "<group>"; };
|
||||
0C0EFA102E6EAAAA00ABCDEF /* CTorHost.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = CTorHost.c; sourceTree = "<group>"; };
|
||||
0C0EFA132E6EAABB00ABCDF1 /* TorNotifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorNotifications.swift; sourceTree = "<group>"; };
|
||||
11186E29A064E8D210880E1B /* BitchatPeer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatPeer.swift; sourceTree = "<group>"; };
|
||||
1234567890ABCDEFFEDCBA02 /* PeerDisplayNameResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerDisplayNameResolver.swift; sourceTree = "<group>"; };
|
||||
136696FC4436A02D98CE6A77 /* KeychainManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainManager.swift; sourceTree = "<group>"; };
|
||||
@@ -299,8 +302,8 @@
|
||||
E95DBE6A48626C5AE287245E /* NoiseProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseProtocolTests.swift; sourceTree = "<group>"; };
|
||||
EA706D8E5097785414646A8E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
EE7EFB209C86BBD956B749EC /* SecureLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureLogger.swift; sourceTree = "<group>"; };
|
||||
F0A1B2C3D4E5F60718293A4A /* OSLog+Categories.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSLog+Categories.swift"; sourceTree = "<group>"; };
|
||||
EF625BB3AD919322C01A46B2 /* BitchatApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatApp.swift; sourceTree = "<group>"; };
|
||||
F0A1B2C3D4E5F60718293A4A /* OSLog+Categories.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSLog+Categories.swift"; sourceTree = "<group>"; };
|
||||
FC75901A0F0073B5BB8356E7 /* TestConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConstants.swift; sourceTree = "<group>"; };
|
||||
FDC18D910D6FF2E8B1B6C885 /* SecureIdentityStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdentityStateManager.swift; sourceTree = "<group>"; };
|
||||
FE7CCF2BD78A3F3DAE6DA145 /* MockBLEService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBLEService.swift; sourceTree = "<group>"; };
|
||||
@@ -542,8 +545,9 @@
|
||||
C3D98EB3E1B455E321F519F4 /* bitchatTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
048A4C2A2E5FCE0300162C4A /* GeohashBookmarksStoreTests.swift */,
|
||||
D69A18D27F9A565FD6041E12 /* Info.plist */,
|
||||
0481A38F2E734CAE00FC845E /* CommandProcessorTests.swift */,
|
||||
048A4C2A2E5FCE0300162C4A /* GeohashBookmarksStoreTests.swift */,
|
||||
047502912E547ACC0083520F /* LocationChannelsTests.swift */,
|
||||
C272F137CE00FC5A96E0CC06 /* NostrProtocolTests.swift */,
|
||||
980B109CBA72BC996455C62B /* BLEServiceTests.swift */,
|
||||
@@ -940,6 +944,7 @@
|
||||
047502802E53A0FC0083520F /* FragmentationTests.swift in Sources */,
|
||||
8F282E9CCA5AE1ECC001D2E4 /* IntegrationTests.swift in Sources */,
|
||||
047502B12E55E8450083520F /* InputValidatorTests.swift in Sources */,
|
||||
0481A3912E734CAE00FC845E /* CommandProcessorTests.swift in Sources */,
|
||||
D727EA273CB214FC32612469 /* MockBluetoothMeshService.swift in Sources */,
|
||||
047502932E547ACC0083520F /* LocationChannelsTests.swift in Sources */,
|
||||
048A4C2B2E5FCE0300162C4A /* GeohashBookmarksStoreTests.swift in Sources */,
|
||||
@@ -963,6 +968,7 @@
|
||||
047502812E53A0FC0083520F /* FragmentationTests.swift in Sources */,
|
||||
686441ABC2AF83EE98E6ECF2 /* IntegrationTests.swift in Sources */,
|
||||
047502B02E55E8450083520F /* InputValidatorTests.swift in Sources */,
|
||||
0481A3902E734CAE00FC845E /* CommandProcessorTests.swift in Sources */,
|
||||
8851F08D88C5B1DE7B9F55C6 /* MockBluetoothMeshService.swift in Sources */,
|
||||
047502922E547ACC0083520F /* LocationChannelsTests.swift in Sources */,
|
||||
048A4C2C2E5FCE0300162C4A /* GeohashBookmarksStoreTests.swift in Sources */,
|
||||
|
||||
@@ -107,7 +107,7 @@ struct NostrProtocol {
|
||||
teleported: Bool = false
|
||||
) throws -> NostrEvent {
|
||||
var tags = [["g", geohash]]
|
||||
if let nickname = nickname, !nickname.isEmpty {
|
||||
if let nickname = nickname?.trimmingCharacters(in: .whitespacesAndNewlines), !nickname.isEmpty {
|
||||
tags.append(["n", nickname])
|
||||
}
|
||||
if teleported {
|
||||
|
||||
@@ -50,9 +50,9 @@ final class CommandProcessor {
|
||||
case "/clear":
|
||||
return handleClear()
|
||||
case "/hug":
|
||||
return handleEmote(args, action: "hugs", emoji: "🫂")
|
||||
return handleEmote(args, command: "hug", action: "hugs", emoji: "🫂")
|
||||
case "/slap":
|
||||
return handleEmote(args, action: "slaps", emoji: "🐟", suffix: " around a bit with a large trout")
|
||||
return handleEmote(args, command: "slap", action: "slaps", emoji: "🐟", suffix: " around a bit with a large trout")
|
||||
case "/block":
|
||||
return handleBlock(args)
|
||||
case "/unblock":
|
||||
@@ -129,17 +129,17 @@ final class CommandProcessor {
|
||||
return .handled
|
||||
}
|
||||
|
||||
private func handleEmote(_ args: String, action: String, emoji: String, suffix: String = "") -> CommandResult {
|
||||
private func handleEmote(_ args: String, command: String, action: String, emoji: String, suffix: String = "") -> CommandResult {
|
||||
let targetName = args.trimmingCharacters(in: .whitespaces)
|
||||
guard !targetName.isEmpty else {
|
||||
return .error(message: "usage: /\(action) <nickname>")
|
||||
return .error(message: "usage: /\(command) <nickname>")
|
||||
}
|
||||
|
||||
let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName
|
||||
|
||||
guard let targetPeerID = chatViewModel?.getPeerIDForNickname(nickname),
|
||||
let myNickname = chatViewModel?.nickname else {
|
||||
return .error(message: "cannot \(action) \(nickname): not found")
|
||||
return .error(message: "cannot \(command) \(nickname): not found")
|
||||
}
|
||||
|
||||
let emoteContent = "* \(emoji) \(myNickname) \(action) \(nickname)\(suffix) *"
|
||||
|
||||
@@ -13,12 +13,16 @@ final class FavoritesPersistenceService: ObservableObject {
|
||||
let theyFavoritedUs: Bool
|
||||
let favoritedAt: Date
|
||||
let lastUpdated: Date
|
||||
// Track what we last sent as OUR npub to this peer, to avoid resending unless it changes
|
||||
// Note: we do not track which npub we last sent to them; sending happens only on favorite toggle
|
||||
|
||||
var isMutual: Bool {
|
||||
isFavorite && theyFavoritedUs
|
||||
}
|
||||
}
|
||||
|
||||
// We intentionally do not track when we last sent our npub; sending happens only on favorite toggle.
|
||||
|
||||
private static let storageKey = "chat.bitchat.favorites"
|
||||
private static let keychainService = "chat.bitchat.favorites"
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ final class MessageRouter {
|
||||
}
|
||||
|
||||
func sendFavoriteNotification(to peerID: String, isFavorite: Bool) {
|
||||
// Route via mesh when connected; else use Nostr
|
||||
if mesh.isPeerConnected(peerID) {
|
||||
mesh.sendFavoriteNotification(to: peerID, isFavorite: isFavorite)
|
||||
} else {
|
||||
|
||||
@@ -37,6 +37,8 @@ enum TransportConfig {
|
||||
|
||||
// UI thresholds
|
||||
static let uiLateInsertThreshold: TimeInterval = 15.0
|
||||
// Geohash public chats are more sensitive to ordering; use a tighter threshold
|
||||
static let uiLateInsertThresholdGeo: TimeInterval = 0.0
|
||||
static let uiProcessedNostrEventsCap: Int = 2000
|
||||
static let uiChannelInactivityThresholdSeconds: TimeInterval = 9 * 60
|
||||
|
||||
|
||||
@@ -195,27 +195,6 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
||||
let favoriteStatus = favorites[noiseKey] {
|
||||
peer.favoriteStatus = favoriteStatus
|
||||
peer.nostrPublicKey = favoriteStatus.peerNostrPublicKey
|
||||
} else {
|
||||
// Check by nickname for reconnected peers
|
||||
let favoriteByNickname = favorites.values.first {
|
||||
$0.peerNickname == peerInfo.nickname
|
||||
}
|
||||
|
||||
if let favorite = favoriteByNickname,
|
||||
let noiseKey = peerInfo.noisePublicKey {
|
||||
SecureLogger.debug("🔄 Found favorite for '\(peerInfo.nickname)' by nickname, updating noise key", category: .session)
|
||||
|
||||
// Update the favorite's key in persistence
|
||||
favoritesService.updateNoisePublicKey(
|
||||
from: favorite.peerNoisePublicKey,
|
||||
to: noiseKey,
|
||||
peerNickname: peerInfo.nickname
|
||||
)
|
||||
|
||||
// Get updated favorite
|
||||
peer.favoriteStatus = favoritesService.getFavoriteStatus(for: noiseKey)
|
||||
peer.nostrPublicKey = peer.favoriteStatus?.peerNostrPublicKey ?? favorite.peerNostrPublicKey
|
||||
}
|
||||
}
|
||||
|
||||
return peer
|
||||
|
||||
@@ -420,6 +420,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
private var geoSamplingSubs: [String: String] = [:] // subID -> geohash
|
||||
private var lastGeoNotificationAt: [String: Date] = [:] // geohash -> last notify time
|
||||
|
||||
|
||||
// MARK: - Message Delivery Tracking
|
||||
|
||||
// Delivery tracking
|
||||
@@ -897,7 +898,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
if Date().timeIntervalSince(eventTime) < 15 { return }
|
||||
}
|
||||
if let nickTag = event.tags.first(where: { $0.first == "n" }), nickTag.count >= 2 {
|
||||
let nick = nickTag[1]
|
||||
let nick = nickTag[1].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
self.geoNicknames[event.pubkey.lowercased()] = nick
|
||||
}
|
||||
// Store mapping for geohash sender IDs used in messages (ensures consistent colors)
|
||||
@@ -928,7 +929,9 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
}
|
||||
let senderName = self.displayNameForNostrPubkey(event.pubkey)
|
||||
let content = event.content.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let timestamp = Date(timeIntervalSince1970: TimeInterval(event.created_at))
|
||||
// Clamp future timestamps to now to avoid future-dated messages skewing order
|
||||
let rawTs = Date(timeIntervalSince1970: TimeInterval(event.created_at))
|
||||
let timestamp = min(rawTs, Date())
|
||||
let mentions = self.parseMentions(from: content)
|
||||
let msg = BitchatMessage(
|
||||
id: event.id,
|
||||
@@ -1614,7 +1617,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
}
|
||||
// Cache nickname from tag if present
|
||||
if let nickTag = event.tags.first(where: { $0.first == "n" }), nickTag.count >= 2 {
|
||||
let nick = nickTag[1]
|
||||
let nick = nickTag[1].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
self.geoNicknames[event.pubkey.lowercased()] = nick
|
||||
}
|
||||
// If this pubkey is blocked, skip mapping, participants, and timeline
|
||||
@@ -1636,7 +1639,9 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
return
|
||||
}
|
||||
let timestamp = Date(timeIntervalSince1970: TimeInterval(event.created_at))
|
||||
// Clamp future timestamps
|
||||
let rawTs = Date(timeIntervalSince1970: TimeInterval(event.created_at))
|
||||
let timestamp = min(rawTs, Date())
|
||||
let mentions = self.parseMentions(from: content)
|
||||
let msg = BitchatMessage(
|
||||
id: event.id,
|
||||
@@ -2000,7 +2005,9 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
let senderSuffix = String(event.pubkey.suffix(4))
|
||||
let nick = self.geoNicknames[event.pubkey.lowercased()]
|
||||
let senderName = (nick?.isEmpty == false ? nick! : "anon") + "#" + senderSuffix
|
||||
let ts = Date(timeIntervalSince1970: TimeInterval(event.created_at))
|
||||
// Clamp future timestamps
|
||||
let rawTs = Date(timeIntervalSince1970: TimeInterval(event.created_at))
|
||||
let ts = min(rawTs, Date())
|
||||
let mentions = self.parseMentions(from: content)
|
||||
let msg = BitchatMessage(
|
||||
id: event.id,
|
||||
@@ -2958,6 +2965,14 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
// When in a geohash channel, allow resolving by geohash participant nickname
|
||||
switch LocationChannelManager.shared.selectedChannel {
|
||||
case .location:
|
||||
// If a disambiguation suffix is present (e.g., "name#abcd"), try exact displayName match first
|
||||
if nickname.contains("#") {
|
||||
if let person = visibleGeohashPeople().first(where: { $0.displayName == nickname }) {
|
||||
let convKey = "nostr_" + String(person.id.prefix(TransportConfig.nostrConvKeyPrefixLength))
|
||||
nostrKeyMapping[convKey] = person.id
|
||||
return convKey
|
||||
}
|
||||
}
|
||||
let base: String = {
|
||||
if let hashIndex = nickname.firstIndex(of: "#") { return String(nickname[..<hashIndex]) }
|
||||
return nickname
|
||||
@@ -4585,16 +4600,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
// Register ephemeral session with identity manager
|
||||
SecureIdentityStateManager.shared.registerEphemeralSession(peerID: peerID)
|
||||
|
||||
// Check if we favorite this peer and resend notification on reconnect
|
||||
// This ensures Nostr key mapping is maintained across reconnections
|
||||
if let peer = unifiedPeerService.getPeer(by: peerID),
|
||||
let favoriteStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: peer.noisePublicKey),
|
||||
favoriteStatus.isFavorite {
|
||||
// Resend favorite notification with our Nostr key after a short delay
|
||||
try? await Task.sleep(nanoseconds: TransportConfig.uiAsyncMediumSleepNs) // 0.5 seconds
|
||||
meshService.sendFavoriteNotification(to: peerID, isFavorite: true)
|
||||
SecureLogger.debug("📤 Resent favorite notification to reconnected peer \(peerID)", category: .session)
|
||||
}
|
||||
// Intentionally do not resend favorites on reconnect.
|
||||
// We only send our npub when a favorite is toggled on, or if our npub changes.
|
||||
|
||||
// Force UI refresh
|
||||
objectWillChange.send()
|
||||
@@ -4943,6 +4950,26 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
messages.append(systemMessage)
|
||||
}
|
||||
|
||||
/// Add a system message to the mesh timeline only (never geohash).
|
||||
/// If mesh is currently active, also append to the visible `messages`.
|
||||
@MainActor
|
||||
private func addMeshOnlySystemMessage(_ content: String) {
|
||||
let systemMessage = BitchatMessage(
|
||||
sender: "system",
|
||||
content: content,
|
||||
timestamp: Date(),
|
||||
isRelay: false
|
||||
)
|
||||
// Persist to mesh timeline
|
||||
meshTimeline.append(systemMessage)
|
||||
trimMeshTimelineIfNeeded()
|
||||
// Only show inline if mesh is the active channel
|
||||
if case .mesh = activeChannel {
|
||||
messages.append(systemMessage)
|
||||
}
|
||||
objectWillChange.send()
|
||||
}
|
||||
|
||||
/// Public helper to add a system message to the public chat timeline.
|
||||
/// Also persists the message into the active channel's backing store so it survives timeline rebinds.
|
||||
@MainActor
|
||||
@@ -5370,8 +5397,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
SecureLogger.warning("⚠️ Cannot get Noise key for peer \(peerID)", category: .session)
|
||||
return
|
||||
}
|
||||
// Determine prior state to avoid duplicate system messages on repeated notifications
|
||||
let prior = FavoritesPersistenceService.shared.getFavoriteStatus(for: finalNoiseKey)?.theyFavoritedUs ?? false
|
||||
|
||||
// Update the favorite relationship
|
||||
// Update the favorite relationship (idempotent storage)
|
||||
FavoritesPersistenceService.shared.updatePeerFavoritedUs(
|
||||
peerNoisePublicKey: finalNoiseKey,
|
||||
favorited: isFavorite,
|
||||
@@ -5379,14 +5408,16 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
peerNostrPublicKey: nostrPubkey
|
||||
)
|
||||
|
||||
// If they favorited us and provided their Nostr key, ensure it's stored
|
||||
// If they favorited us and provided their Nostr key, ensure it's stored (log only)
|
||||
if isFavorite && nostrPubkey != nil {
|
||||
SecureLogger.info("💾 Storing Nostr key association for \(senderNickname): \(nostrPubkey!.prefix(16))...", category: .session)
|
||||
}
|
||||
|
||||
// Show system message
|
||||
let action = isFavorite ? "favorited" : "unfavorited"
|
||||
addSystemMessage("\(senderNickname) \(action) you")
|
||||
// Only show a system message when the state changes, and only in mesh
|
||||
if prior != isFavorite {
|
||||
let action = isFavorite ? "favorited" : "unfavorited"
|
||||
addMeshOnlySystemMessage("\(senderNickname) \(action) you")
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@@ -5970,10 +6001,20 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
isBatchingPublic = true
|
||||
// Rough chronological order: sort the batch by timestamp before inserting
|
||||
added.sort { $0.timestamp < $1.timestamp }
|
||||
// Insert late arrivals into approximate position; append recent ones
|
||||
// Channel-aware insertion policy: geohash uses strict ordering; mesh allows small out-of-order appends
|
||||
let threshold: TimeInterval = {
|
||||
switch activeChannel {
|
||||
case .location: return TransportConfig.uiLateInsertThresholdGeo
|
||||
case .mesh: return TransportConfig.uiLateInsertThreshold
|
||||
}
|
||||
}()
|
||||
let lastTs = messages.last?.timestamp ?? .distantPast
|
||||
for m in added {
|
||||
if m.timestamp < lastTs.addingTimeInterval(-lateInsertThreshold) {
|
||||
if m.timestamp < lastTs.addingTimeInterval(-threshold) {
|
||||
let idx = insertionIndexByTimestamp(m.timestamp)
|
||||
if idx >= messages.count { messages.append(m) } else { messages.insert(m, at: idx) }
|
||||
} else if threshold == 0 {
|
||||
// Strict ordering for geohash: always insert by timestamp
|
||||
let idx = insertionIndexByTimestamp(m.timestamp)
|
||||
if idx >= messages.count { messages.append(m) } else { messages.insert(m, at: idx) }
|
||||
} else {
|
||||
|
||||
@@ -444,7 +444,19 @@ struct ContentView: View {
|
||||
let id = url.path.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||||
let peerID = id.removingPercentEncoding ?? id
|
||||
selectedMessageSenderID = peerID
|
||||
selectedMessageSender = viewModel.messages.last(where: { $0.senderPeerID == peerID })?.sender
|
||||
// Derive a stable display name from the peerID instead of peeking at the last message,
|
||||
// which may be a transformed system action (sender == "system").
|
||||
if peerID.hasPrefix("nostr") {
|
||||
// For geohash senders, resolve display name via mapping (works for "nostr:" and "nostr_" keys)
|
||||
selectedMessageSender = viewModel.geohashDisplayName(for: peerID)
|
||||
} else {
|
||||
// Mesh sender: use current mesh nickname if available; otherwise fall back to last non-system message
|
||||
if let name = viewModel.meshService.peerNickname(peerID: peerID) {
|
||||
selectedMessageSender = name
|
||||
} else {
|
||||
selectedMessageSender = viewModel.messages.last(where: { $0.senderPeerID == peerID && $0.sender != "system" })?.sender
|
||||
}
|
||||
}
|
||||
showMessageActions = true
|
||||
}
|
||||
.onOpenURL { url in
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import XCTest
|
||||
@testable import bitchat
|
||||
|
||||
final class CommandProcessorTests: XCTestCase {
|
||||
|
||||
@MainActor
|
||||
func test_slap_notFoundGrammar() {
|
||||
let processor = CommandProcessor(chatViewModel: nil, meshService: nil)
|
||||
let result = processor.process("/slap @system")
|
||||
switch result {
|
||||
case .error(let message):
|
||||
XCTAssertEqual(message, "cannot slap system: not found")
|
||||
default:
|
||||
XCTFail("Expected error result")
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func test_hug_notFoundGrammar() {
|
||||
let processor = CommandProcessor(chatViewModel: nil, meshService: nil)
|
||||
let result = processor.process("/hug @system")
|
||||
switch result {
|
||||
case .error(let message):
|
||||
XCTAssertEqual(message, "cannot hug system: not found")
|
||||
default:
|
||||
XCTFail("Expected error result")
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func test_slap_usageMessage() {
|
||||
let processor = CommandProcessor(chatViewModel: nil, meshService: nil)
|
||||
let result = processor.process("/slap")
|
||||
switch result {
|
||||
case .error(let message):
|
||||
XCTAssertEqual(message, "usage: /slap <nickname>")
|
||||
default:
|
||||
XCTFail("Expected error result for usage message")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user