mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 09:45:18 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
334c6271b6 | ||
|
|
6f4da62e4e | ||
|
|
d946c01bfc | ||
|
|
ce48720e9a | ||
|
|
e0db8a2bc8 |
@@ -17,6 +17,7 @@
|
|||||||
0475028D2E54171C0083520F /* LocationChannelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0475028B2E54171C0083520F /* LocationChannelManager.swift */; };
|
0475028D2E54171C0083520F /* LocationChannelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0475028B2E54171C0083520F /* LocationChannelManager.swift */; };
|
||||||
0475028F2E5417660083520F /* LocationChannelsSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0475028E2E5417660083520F /* LocationChannelsSheet.swift */; };
|
0475028F2E5417660083520F /* LocationChannelsSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0475028E2E5417660083520F /* LocationChannelsSheet.swift */; };
|
||||||
047502902E5417660083520F /* LocationChannelsSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0475028E2E5417660083520F /* LocationChannelsSheet.swift */; };
|
047502902E5417660083520F /* LocationChannelsSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0475028E2E5417660083520F /* LocationChannelsSheet.swift */; };
|
||||||
|
|
||||||
047502922E547ACC0083520F /* LocationChannelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047502912E547ACC0083520F /* LocationChannelsTests.swift */; };
|
047502922E547ACC0083520F /* LocationChannelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047502912E547ACC0083520F /* LocationChannelsTests.swift */; };
|
||||||
047502932E547ACC0083520F /* LocationChannelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047502912E547ACC0083520F /* LocationChannelsTests.swift */; };
|
047502932E547ACC0083520F /* LocationChannelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047502912E547ACC0083520F /* LocationChannelsTests.swift */; };
|
||||||
047502AC2E55E8360083520F /* BinaryProtocolPaddingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047502AB2E55E8360083520F /* BinaryProtocolPaddingTests.swift */; };
|
047502AC2E55E8360083520F /* BinaryProtocolPaddingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047502AB2E55E8360083520F /* BinaryProtocolPaddingTests.swift */; };
|
||||||
@@ -29,9 +30,6 @@
|
|||||||
047502B72E55FED60083520F /* GeohashPeopleList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047502B22E55FED60083520F /* GeohashPeopleList.swift */; };
|
047502B72E55FED60083520F /* GeohashPeopleList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047502B22E55FED60083520F /* GeohashPeopleList.swift */; };
|
||||||
047502B92E560F690083520F /* RelayController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047502B82E560F690083520F /* RelayController.swift */; };
|
047502B92E560F690083520F /* RelayController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047502B82E560F690083520F /* RelayController.swift */; };
|
||||||
047502BA2E560F690083520F /* RelayController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047502B82E560F690083520F /* RelayController.swift */; };
|
047502BA2E560F690083520F /* RelayController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047502B82E560F690083520F /* RelayController.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 */; };
|
|
||||||
049BD3902E4EC4F0001A566B /* PrivateChatManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD38F2E4EC4F0001A566B /* PrivateChatManager.swift */; };
|
049BD3902E4EC4F0001A566B /* PrivateChatManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD38F2E4EC4F0001A566B /* PrivateChatManager.swift */; };
|
||||||
049BD3912E4EC4F0001A566B /* AutocompleteService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD38C2E4EC4F0001A566B /* AutocompleteService.swift */; };
|
049BD3912E4EC4F0001A566B /* AutocompleteService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD38C2E4EC4F0001A566B /* AutocompleteService.swift */; };
|
||||||
049BD3922E4EC4F0001A566B /* CommandProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD38D2E4EC4F0001A566B /* CommandProcessor.swift */; };
|
049BD3922E4EC4F0001A566B /* CommandProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD38D2E4EC4F0001A566B /* CommandProcessor.swift */; };
|
||||||
@@ -59,8 +57,6 @@
|
|||||||
0AE840940F21AFC07C226636 /* PrivateChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A262EDDC04B7D7B5E31F321 /* PrivateChatE2ETests.swift */; };
|
0AE840940F21AFC07C226636 /* PrivateChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A262EDDC04B7D7B5E31F321 /* PrivateChatE2ETests.swift */; };
|
||||||
0B6F25559A21F8C69C8357C6 /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3CC6FA298729906109F61B /* BinaryProtocolTests.swift */; };
|
0B6F25559A21F8C69C8357C6 /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3CC6FA298729906109F61B /* BinaryProtocolTests.swift */; };
|
||||||
10E68BB889356219189E38EC /* BitchatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF625BB3AD919322C01A46B2 /* BitchatApp.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 */; };
|
|
||||||
132DF1E24B4E9C7DCDAD4376 /* FingerprintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9195CDC7EB236AFBC9A4D41A /* FingerprintView.swift */; };
|
132DF1E24B4E9C7DCDAD4376 /* FingerprintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9195CDC7EB236AFBC9A4D41A /* FingerprintView.swift */; };
|
||||||
17901751FD8010AFC8E750F2 /* bitchatShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
17901751FD8010AFC8E750F2 /* bitchatShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
1D9674FA5F998503831DC281 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A08E03AA0C63E97C91749AEC /* ContentView.swift */; };
|
1D9674FA5F998503831DC281 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A08E03AA0C63E97C91749AEC /* ContentView.swift */; };
|
||||||
@@ -107,17 +103,11 @@
|
|||||||
9C7D287C8E67AAE576A5ECB7 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B378C16594575FCC7F9C75 /* ShareViewController.swift */; };
|
9C7D287C8E67AAE576A5ECB7 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B378C16594575FCC7F9C75 /* ShareViewController.swift */; };
|
||||||
9CCF09F7527EC681A13FC246 /* NoiseSecurityConsiderations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43B4548DAFC9F7AA8873DA53 /* NoiseSecurityConsiderations.swift */; };
|
9CCF09F7527EC681A13FC246 /* NoiseSecurityConsiderations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43B4548DAFC9F7AA8873DA53 /* NoiseSecurityConsiderations.swift */; };
|
||||||
A0A1C26EFBFDD5B8EFEEDE57 /* PublicChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22BF09A49010947CEFE45E2 /* PublicChatE2ETests.swift */; };
|
A0A1C26EFBFDD5B8EFEEDE57 /* PublicChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22BF09A49010947CEFE45E2 /* PublicChatE2ETests.swift */; };
|
||||||
A1B2C3D44E5F60718293A4B5 /* XChaCha20Poly1305Compat.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D44E5F60718293A4B4 /* XChaCha20Poly1305Compat.swift */; };
|
|
||||||
A1B2C3D54E5F60718293A4B6 /* XChaCha20Poly1305Compat.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D44E5F60718293A4B4 /* XChaCha20Poly1305Compat.swift */; };
|
|
||||||
A2977428C1D9EF9944C4BFAF /* BLEServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 980B109CBA72BC996455C62B /* BLEServiceTests.swift */; };
|
A2977428C1D9EF9944C4BFAF /* BLEServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 980B109CBA72BC996455C62B /* BLEServiceTests.swift */; };
|
||||||
A7187D48B07C6857DE01D0ED /* NoiseProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43613045E63D21D429396805 /* NoiseProtocol.swift */; };
|
A7187D48B07C6857DE01D0ED /* NoiseProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43613045E63D21D429396805 /* NoiseProtocol.swift */; };
|
||||||
AA11BB22CC33DD44EE55FF66 /* MessageTextHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA11BB22CC33DD44EE55FF68 /* MessageTextHelpers.swift */; };
|
AA11BB22CC33DD44EE55FF66 /* MessageTextHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA11BB22CC33DD44EE55FF68 /* MessageTextHelpers.swift */; };
|
||||||
AA11BB22CC33DD44EE55FF67 /* MessageTextHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA11BB22CC33DD44EE55FF68 /* MessageTextHelpers.swift */; };
|
AA11BB22CC33DD44EE55FF67 /* MessageTextHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA11BB22CC33DD44EE55FF68 /* MessageTextHelpers.swift */; };
|
||||||
AA6E067DB034FC0FA23C28A9 /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3CC6FA298729906109F61B /* BinaryProtocolTests.swift */; };
|
AA6E067DB034FC0FA23C28A9 /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3CC6FA298729906109F61B /* BinaryProtocolTests.swift */; };
|
||||||
AA77BB11CC22DD33EE44FF55 /* VerificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA77BB10CC22DD33EE44FF55 /* VerificationService.swift */; };
|
|
||||||
AA77BB12CC22DD33EE44FF56 /* VerificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA77BB10CC22DD33EE44FF55 /* VerificationService.swift */; };
|
|
||||||
AA77BB14CC22DD33EE44FF58 /* VerificationViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA77BB13CC22DD33EE44FF57 /* VerificationViews.swift */; };
|
|
||||||
AA77BB15CC22DD33EE44FF59 /* VerificationViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA77BB13CC22DD33EE44FF57 /* VerificationViews.swift */; };
|
|
||||||
ABAF130D88561F4A646F0430 /* AppInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */; };
|
ABAF130D88561F4A646F0430 /* AppInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */; };
|
||||||
ACE2ED172C37F01561E50B71 /* FavoritesPersistenceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419BFFF209EBA93F410E9E9F /* FavoritesPersistenceService.swift */; };
|
ACE2ED172C37F01561E50B71 /* FavoritesPersistenceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419BFFF209EBA93F410E9E9F /* FavoritesPersistenceService.swift */; };
|
||||||
AD11E46940D742AEAF547EB2 /* AppInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */; };
|
AD11E46940D742AEAF547EB2 /* AppInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */; };
|
||||||
@@ -151,6 +141,12 @@
|
|||||||
F455F011B3B648ADA233F998 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; };
|
F455F011B3B648ADA233F998 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; };
|
||||||
FB8819B4C84FAFEF5C36B216 /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136696FC4436A02D98CE6A77 /* KeychainManager.swift */; };
|
FB8819B4C84FAFEF5C36B216 /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136696FC4436A02D98CE6A77 /* KeychainManager.swift */; };
|
||||||
FBC409E105493C491531B59A /* NostrProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5A9FF4AEA8A923317ED26A /* NostrProtocol.swift */; };
|
FBC409E105493C491531B59A /* NostrProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5A9FF4AEA8A923317ED26A /* NostrProtocol.swift */; };
|
||||||
|
A1B2C3D44E5F60718293A4B5 /* XChaCha20Poly1305Compat.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D44E5F60718293A4B4 /* XChaCha20Poly1305Compat.swift */; };
|
||||||
|
A1B2C3D54E5F60718293A4B6 /* XChaCha20Poly1305Compat.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D44E5F60718293A4B4 /* XChaCha20Poly1305Compat.swift */; };
|
||||||
|
AA77BB11CC22DD33EE44FF55 /* VerificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA77BB10CC22DD33EE44FF55 /* VerificationService.swift */; };
|
||||||
|
AA77BB12CC22DD33EE44FF56 /* VerificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA77BB10CC22DD33EE44FF55 /* VerificationService.swift */; };
|
||||||
|
AA77BB14CC22DD33EE44FF58 /* VerificationViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA77BB13CC22DD33EE44FF57 /* VerificationViews.swift */; };
|
||||||
|
AA77BB15CC22DD33EE44FF59 /* VerificationViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA77BB13CC22DD33EE44FF57 /* VerificationViews.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -204,7 +200,6 @@
|
|||||||
047502B22E55FED60083520F /* GeohashPeopleList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeohashPeopleList.swift; sourceTree = "<group>"; };
|
047502B22E55FED60083520F /* GeohashPeopleList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeohashPeopleList.swift; sourceTree = "<group>"; };
|
||||||
047502B32E55FED60083520F /* MeshPeerList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshPeerList.swift; sourceTree = "<group>"; };
|
047502B32E55FED60083520F /* MeshPeerList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshPeerList.swift; sourceTree = "<group>"; };
|
||||||
047502B82E560F690083520F /* RelayController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayController.swift; sourceTree = "<group>"; };
|
047502B82E560F690083520F /* RelayController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayController.swift; sourceTree = "<group>"; };
|
||||||
048A4BE62E5CCCC300162C4A /* TransportConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportConfig.swift; sourceTree = "<group>"; };
|
|
||||||
049BD38C2E4EC4F0001A566B /* AutocompleteService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteService.swift; sourceTree = "<group>"; };
|
049BD38C2E4EC4F0001A566B /* AutocompleteService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteService.swift; sourceTree = "<group>"; };
|
||||||
049BD38D2E4EC4F0001A566B /* CommandProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandProcessor.swift; sourceTree = "<group>"; };
|
049BD38D2E4EC4F0001A566B /* CommandProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandProcessor.swift; sourceTree = "<group>"; };
|
||||||
049BD38F2E4EC4F0001A566B /* PrivateChatManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateChatManager.swift; sourceTree = "<group>"; };
|
049BD38F2E4EC4F0001A566B /* PrivateChatManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateChatManager.swift; sourceTree = "<group>"; };
|
||||||
@@ -220,7 +215,6 @@
|
|||||||
05BA20BC0F123F1507C5C247 /* IdentityModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityModels.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>"; };
|
0B3CC6FA298729906109F61B /* BinaryProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocolTests.swift; sourceTree = "<group>"; };
|
||||||
11186E29A064E8D210880E1B /* BitchatPeer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatPeer.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>"; };
|
136696FC4436A02D98CE6A77 /* KeychainManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainManager.swift; sourceTree = "<group>"; };
|
||||||
229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatProtocol.swift; sourceTree = "<group>"; };
|
229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatProtocol.swift; sourceTree = "<group>"; };
|
||||||
2E346DF8E026FD34EE3DD038 /* TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestHelpers.swift; sourceTree = "<group>"; };
|
2E346DF8E026FD34EE3DD038 /* TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestHelpers.swift; sourceTree = "<group>"; };
|
||||||
@@ -251,11 +245,9 @@
|
|||||||
980B109CBA72BC996455C62B /* BLEServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEServiceTests.swift; sourceTree = "<group>"; };
|
980B109CBA72BC996455C62B /* BLEServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEServiceTests.swift; sourceTree = "<group>"; };
|
||||||
9AB6BE4ABD7F5088E9865E56 /* NoiseSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseSession.swift; sourceTree = "<group>"; };
|
9AB6BE4ABD7F5088E9865E56 /* NoiseSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseSession.swift; sourceTree = "<group>"; };
|
||||||
A08E03AA0C63E97C91749AEC /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
A08E03AA0C63E97C91749AEC /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
A1B2C3D44E5F60718293A4B4 /* XChaCha20Poly1305Compat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XChaCha20Poly1305Compat.swift; sourceTree = "<group>"; };
|
|
||||||
A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocol.swift; sourceTree = "<group>"; };
|
A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocol.swift; sourceTree = "<group>"; };
|
||||||
|
|
||||||
AA11BB22CC33DD44EE55FF68 /* MessageTextHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageTextHelpers.swift; sourceTree = "<group>"; };
|
AA11BB22CC33DD44EE55FF68 /* MessageTextHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageTextHelpers.swift; sourceTree = "<group>"; };
|
||||||
AA77BB10CC22DD33EE44FF55 /* VerificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerificationService.swift; sourceTree = "<group>"; };
|
|
||||||
AA77BB13CC22DD33EE44FF57 /* VerificationViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerificationViews.swift; sourceTree = "<group>"; };
|
|
||||||
B1D6A89B36A3D31E590B94E5 /* NoiseHandshakeCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseHandshakeCoordinator.swift; sourceTree = "<group>"; };
|
B1D6A89B36A3D31E590B94E5 /* NoiseHandshakeCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseHandshakeCoordinator.swift; sourceTree = "<group>"; };
|
||||||
C0DB1DE27F0AAB5092663E8E /* bitchatTests_iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = bitchatTests_iOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
C0DB1DE27F0AAB5092663E8E /* bitchatTests_iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = bitchatTests_iOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
C1B378C16594575FCC7F9C75 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
C1B378C16594575FCC7F9C75 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
||||||
@@ -274,6 +266,9 @@
|
|||||||
FDC18D910D6FF2E8B1B6C885 /* SecureIdentityStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdentityStateManager.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>"; };
|
FE7CCF2BD78A3F3DAE6DA145 /* MockBLEService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBLEService.swift; sourceTree = "<group>"; };
|
||||||
FF7AF93D874001FBD94C8306 /* bitchat-macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "bitchat-macOS.entitlements"; sourceTree = "<group>"; };
|
FF7AF93D874001FBD94C8306 /* bitchat-macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "bitchat-macOS.entitlements"; sourceTree = "<group>"; };
|
||||||
|
A1B2C3D44E5F60718293A4B4 /* XChaCha20Poly1305Compat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XChaCha20Poly1305Compat.swift; sourceTree = "<group>"; };
|
||||||
|
AA77BB10CC22DD33EE44FF55 /* VerificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerificationService.swift; sourceTree = "<group>"; };
|
||||||
|
AA77BB13CC22DD33EE44FF57 /* VerificationViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerificationViews.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -410,7 +405,6 @@
|
|||||||
9A78348821A7D3374607D4E3 /* Utils */ = {
|
9A78348821A7D3374607D4E3 /* Utils */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
1234567890ABCDEFFEDCBA02 /* PeerDisplayNameResolver.swift */,
|
|
||||||
049BD3AA2E51E38E001A566B /* PeerIDResolver.swift */,
|
049BD3AA2E51E38E001A566B /* PeerIDResolver.swift */,
|
||||||
049BD3A42E51DC0E001A566B /* MessageDeduplicator.swift */,
|
049BD3A42E51DC0E001A566B /* MessageDeduplicator.swift */,
|
||||||
32F149C43D1915831B60FE09 /* CompressionUtil.swift */,
|
32F149C43D1915831B60FE09 /* CompressionUtil.swift */,
|
||||||
@@ -449,6 +443,7 @@
|
|||||||
047502B22E55FED60083520F /* GeohashPeopleList.swift */,
|
047502B22E55FED60083520F /* GeohashPeopleList.swift */,
|
||||||
047502B32E55FED60083520F /* MeshPeerList.swift */,
|
047502B32E55FED60083520F /* MeshPeerList.swift */,
|
||||||
0475028E2E5417660083520F /* LocationChannelsSheet.swift */,
|
0475028E2E5417660083520F /* LocationChannelsSheet.swift */,
|
||||||
|
|
||||||
763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */,
|
763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */,
|
||||||
A08E03AA0C63E97C91749AEC /* ContentView.swift */,
|
A08E03AA0C63E97C91749AEC /* ContentView.swift */,
|
||||||
9195CDC7EB236AFBC9A4D41A /* FingerprintView.swift */,
|
9195CDC7EB236AFBC9A4D41A /* FingerprintView.swift */,
|
||||||
@@ -519,7 +514,6 @@
|
|||||||
D98A3186D7E4C72E35BDF7FE /* Services */ = {
|
D98A3186D7E4C72E35BDF7FE /* Services */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
048A4BE62E5CCCC300162C4A /* TransportConfig.swift */,
|
|
||||||
AA77BB10CC22DD33EE44FF55 /* VerificationService.swift */,
|
AA77BB10CC22DD33EE44FF55 /* VerificationService.swift */,
|
||||||
047502B82E560F690083520F /* RelayController.swift */,
|
047502B82E560F690083520F /* RelayController.swift */,
|
||||||
0475028B2E54171C0083520F /* LocationChannelManager.swift */,
|
0475028B2E54171C0083520F /* LocationChannelManager.swift */,
|
||||||
@@ -733,7 +727,6 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
048A4BE92E5CCCC300162C4B /* TransportConfig.swift in Sources */,
|
|
||||||
9C7D287C8E67AAE576A5ECB7 /* ShareViewController.swift in Sources */,
|
9C7D287C8E67AAE576A5ECB7 /* ShareViewController.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -742,8 +735,6 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
048A4BE72E5CCCC300162C4A /* TransportConfig.swift in Sources */,
|
|
||||||
1234567890ABCDEFFEDCBA13 /* PeerDisplayNameResolver.swift in Sources */,
|
|
||||||
AA77BB12CC22DD33EE44FF56 /* VerificationService.swift in Sources */,
|
AA77BB12CC22DD33EE44FF56 /* VerificationService.swift in Sources */,
|
||||||
AA77BB15CC22DD33EE44FF59 /* VerificationViews.swift in Sources */,
|
AA77BB15CC22DD33EE44FF59 /* VerificationViews.swift in Sources */,
|
||||||
A1B2C3D54E5F60718293A4B6 /* XChaCha20Poly1305Compat.swift in Sources */,
|
A1B2C3D54E5F60718293A4B6 /* XChaCha20Poly1305Compat.swift in Sources */,
|
||||||
@@ -767,6 +758,7 @@
|
|||||||
7241FFD6CFFB875B864FA223 /* InputValidator.swift in Sources */,
|
7241FFD6CFFB875B864FA223 /* InputValidator.swift in Sources */,
|
||||||
FB8819B4C84FAFEF5C36B216 /* KeychainManager.swift in Sources */,
|
FB8819B4C84FAFEF5C36B216 /* KeychainManager.swift in Sources */,
|
||||||
0475028F2E5417660083520F /* LocationChannelsSheet.swift in Sources */,
|
0475028F2E5417660083520F /* LocationChannelsSheet.swift in Sources */,
|
||||||
|
|
||||||
501BC56B1A08C0327A09AAF1 /* NoiseEncryptionService.swift in Sources */,
|
501BC56B1A08C0327A09AAF1 /* NoiseEncryptionService.swift in Sources */,
|
||||||
0475028C2E54171C0083520F /* LocationChannelManager.swift in Sources */,
|
0475028C2E54171C0083520F /* LocationChannelManager.swift in Sources */,
|
||||||
AFF33EF44626EF0579D17EB1 /* NoiseHandshakeCoordinator.swift in Sources */,
|
AFF33EF44626EF0579D17EB1 /* NoiseHandshakeCoordinator.swift in Sources */,
|
||||||
@@ -801,8 +793,6 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
048A4BE82E5CCCC300162C4A /* TransportConfig.swift in Sources */,
|
|
||||||
1234567890ABCDEFFEDCBA14 /* PeerDisplayNameResolver.swift in Sources */,
|
|
||||||
AA77BB11CC22DD33EE44FF55 /* VerificationService.swift in Sources */,
|
AA77BB11CC22DD33EE44FF55 /* VerificationService.swift in Sources */,
|
||||||
AA77BB14CC22DD33EE44FF58 /* VerificationViews.swift in Sources */,
|
AA77BB14CC22DD33EE44FF58 /* VerificationViews.swift in Sources */,
|
||||||
A1B2C3D44E5F60718293A4B5 /* XChaCha20Poly1305Compat.swift in Sources */,
|
A1B2C3D44E5F60718293A4B5 /* XChaCha20Poly1305Compat.swift in Sources */,
|
||||||
@@ -826,6 +816,7 @@
|
|||||||
EF49C600C1E464710DD6CA29 /* InputValidator.swift in Sources */,
|
EF49C600C1E464710DD6CA29 /* InputValidator.swift in Sources */,
|
||||||
8F737CE0435792CC2AD65FCB /* KeychainManager.swift in Sources */,
|
8F737CE0435792CC2AD65FCB /* KeychainManager.swift in Sources */,
|
||||||
047502902E5417660083520F /* LocationChannelsSheet.swift in Sources */,
|
047502902E5417660083520F /* LocationChannelsSheet.swift in Sources */,
|
||||||
|
|
||||||
5EE49E150BBF0488E7473687 /* NoiseEncryptionService.swift in Sources */,
|
5EE49E150BBF0488E7473687 /* NoiseEncryptionService.swift in Sources */,
|
||||||
0475028D2E54171C0083520F /* LocationChannelManager.swift in Sources */,
|
0475028D2E54171C0083520F /* LocationChannelManager.swift in Sources */,
|
||||||
6D0D4A0B1D8B659DCBAE7C9C /* NoiseHandshakeCoordinator.swift in Sources */,
|
6D0D4A0B1D8B659DCBAE7C9C /* NoiseHandshakeCoordinator.swift in Sources */,
|
||||||
@@ -1004,7 +995,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.3.3;
|
MARKETING_VERSION = 1.3.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.ShareExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.ShareExtension;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
@@ -1035,7 +1026,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.3.3;
|
MARKETING_VERSION = 1.3.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
|
||||||
PRODUCT_NAME = bitchat;
|
PRODUCT_NAME = bitchat;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -1090,7 +1081,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.3.3;
|
MARKETING_VERSION = 1.3.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
|
||||||
PRODUCT_NAME = bitchat;
|
PRODUCT_NAME = bitchat;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -1122,7 +1113,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
MARKETING_VERSION = 1.3.3;
|
MARKETING_VERSION = 1.3.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
|
||||||
PRODUCT_NAME = bitchat;
|
PRODUCT_NAME = bitchat;
|
||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
@@ -1211,7 +1202,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
MARKETING_VERSION = 1.3.3;
|
MARKETING_VERSION = 1.3.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
|
||||||
PRODUCT_NAME = bitchat;
|
PRODUCT_NAME = bitchat;
|
||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
@@ -1304,7 +1295,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.3.3;
|
MARKETING_VERSION = 1.3.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.ShareExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.ShareExtension;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
|||||||
@@ -99,15 +99,15 @@ struct BitchatApp: App {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only process if shared within configured window
|
// Only process if shared within last 30 seconds
|
||||||
if Date().timeIntervalSince(sharedDate) < TransportConfig.uiShareAcceptWindowSeconds {
|
if Date().timeIntervalSince(sharedDate) < 30 {
|
||||||
let contentType = userDefaults.string(forKey: "sharedContentType") ?? "text"
|
let contentType = userDefaults.string(forKey: "sharedContentType") ?? "text"
|
||||||
|
|
||||||
// Clear the shared content
|
// Clear the shared content
|
||||||
userDefaults.removeObject(forKey: "sharedContent")
|
userDefaults.removeObject(forKey: "sharedContent")
|
||||||
userDefaults.removeObject(forKey: "sharedContentType")
|
userDefaults.removeObject(forKey: "sharedContentType")
|
||||||
userDefaults.removeObject(forKey: "sharedContentDate")
|
userDefaults.removeObject(forKey: "sharedContentDate")
|
||||||
// No need to force synchronize here
|
userDefaults.synchronize()
|
||||||
|
|
||||||
// Send the shared content immediately on the main queue
|
// Send the shared content immediately on the main queue
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
|||||||
@@ -106,8 +106,6 @@ enum HandshakeState {
|
|||||||
struct CryptographicIdentity: Codable {
|
struct CryptographicIdentity: Codable {
|
||||||
let fingerprint: String // SHA256 of public key
|
let fingerprint: String // SHA256 of public key
|
||||||
let publicKey: Data // Noise static public key
|
let publicKey: Data // Noise static public key
|
||||||
// Optional Ed25519 signing public key (used to authenticate public messages)
|
|
||||||
var signingPublicKey: Data? = nil
|
|
||||||
let firstSeen: Date
|
let firstSeen: Date
|
||||||
let lastHandshake: Date?
|
let lastHandshake: Date?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -230,91 +230,6 @@ class SecureIdentityStateManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Cryptographic Identities
|
|
||||||
|
|
||||||
/// Insert or update a cryptographic identity and optionally persist its signing key and claimed nickname.
|
|
||||||
/// - Parameters:
|
|
||||||
/// - fingerprint: SHA-256 hex of the Noise static public key
|
|
||||||
/// - noisePublicKey: Noise static public key data
|
|
||||||
/// - signingPublicKey: Optional Ed25519 signing public key for authenticating public messages
|
|
||||||
/// - claimedNickname: Optional latest claimed nickname to persist into social identity
|
|
||||||
func upsertCryptographicIdentity(fingerprint: String, noisePublicKey: Data, signingPublicKey: Data?, claimedNickname: String? = nil) {
|
|
||||||
queue.async(flags: .barrier) {
|
|
||||||
let now = Date()
|
|
||||||
if var existing = self.cryptographicIdentities[fingerprint] {
|
|
||||||
// Update keys if changed
|
|
||||||
if existing.publicKey != noisePublicKey {
|
|
||||||
existing = CryptographicIdentity(
|
|
||||||
fingerprint: fingerprint,
|
|
||||||
publicKey: noisePublicKey,
|
|
||||||
signingPublicKey: signingPublicKey ?? existing.signingPublicKey,
|
|
||||||
firstSeen: existing.firstSeen,
|
|
||||||
lastHandshake: now
|
|
||||||
)
|
|
||||||
self.cryptographicIdentities[fingerprint] = existing
|
|
||||||
} else {
|
|
||||||
// Update signing key and lastHandshake
|
|
||||||
existing.signingPublicKey = signingPublicKey ?? existing.signingPublicKey
|
|
||||||
let updated = CryptographicIdentity(
|
|
||||||
fingerprint: existing.fingerprint,
|
|
||||||
publicKey: existing.publicKey,
|
|
||||||
signingPublicKey: existing.signingPublicKey,
|
|
||||||
firstSeen: existing.firstSeen,
|
|
||||||
lastHandshake: now
|
|
||||||
)
|
|
||||||
self.cryptographicIdentities[fingerprint] = updated
|
|
||||||
}
|
|
||||||
// Persist updated state (already assigned in branches above)
|
|
||||||
} else {
|
|
||||||
// New entry
|
|
||||||
let entry = CryptographicIdentity(
|
|
||||||
fingerprint: fingerprint,
|
|
||||||
publicKey: noisePublicKey,
|
|
||||||
signingPublicKey: signingPublicKey,
|
|
||||||
firstSeen: now,
|
|
||||||
lastHandshake: now
|
|
||||||
)
|
|
||||||
self.cryptographicIdentities[fingerprint] = entry
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optionally persist claimed nickname into social identity
|
|
||||||
if let claimed = claimedNickname {
|
|
||||||
var identity = self.cache.socialIdentities[fingerprint] ?? SocialIdentity(
|
|
||||||
fingerprint: fingerprint,
|
|
||||||
localPetname: nil,
|
|
||||||
claimedNickname: claimed,
|
|
||||||
trustLevel: .unknown,
|
|
||||||
isFavorite: false,
|
|
||||||
isBlocked: false,
|
|
||||||
notes: nil
|
|
||||||
)
|
|
||||||
// Update claimed nickname if changed
|
|
||||||
if identity.claimedNickname != claimed {
|
|
||||||
identity.claimedNickname = claimed
|
|
||||||
self.cache.socialIdentities[fingerprint] = identity
|
|
||||||
} else if self.cache.socialIdentities[fingerprint] == nil {
|
|
||||||
self.cache.socialIdentities[fingerprint] = identity
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.saveIdentityCache()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Retrieve cryptographic identity by fingerprint
|
|
||||||
func getCryptographicIdentity(for fingerprint: String) -> CryptographicIdentity? {
|
|
||||||
queue.sync { cryptographicIdentities[fingerprint] }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find cryptographic identities whose fingerprint prefix matches a peerID (16-hex) short ID
|
|
||||||
func getCryptoIdentitiesByPeerIDPrefix(_ peerID: String) -> [CryptographicIdentity] {
|
|
||||||
queue.sync {
|
|
||||||
// Defensive: ensure hex and correct length
|
|
||||||
guard peerID.count == 16, peerID.allSatisfy({ $0.isHexDigit }) else { return [] }
|
|
||||||
return cryptographicIdentities.values.filter { $0.fingerprint.hasPrefix(peerID) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAllSocialIdentities() -> [SocialIdentity] {
|
func getAllSocialIdentities() -> [SocialIdentity] {
|
||||||
queue.sync {
|
queue.sync {
|
||||||
return Array(cache.socialIdentities.values)
|
return Array(cache.socialIdentities.values)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ struct BitchatPeer: Identifiable, Equatable {
|
|||||||
let nickname: String
|
let nickname: String
|
||||||
let lastSeen: Date
|
let lastSeen: Date
|
||||||
let isConnected: Bool
|
let isConnected: Bool
|
||||||
let isReachable: Bool
|
|
||||||
|
|
||||||
// Favorite-related properties
|
// Favorite-related properties
|
||||||
var favoriteStatus: FavoritesPersistenceService.FavoriteRelationship?
|
var favoriteStatus: FavoritesPersistenceService.FavoriteRelationship?
|
||||||
@@ -19,7 +18,6 @@ struct BitchatPeer: Identifiable, Equatable {
|
|||||||
// Connection state
|
// Connection state
|
||||||
enum ConnectionState {
|
enum ConnectionState {
|
||||||
case bluetoothConnected
|
case bluetoothConnected
|
||||||
case meshReachable // Seen via mesh recently, not directly connected
|
|
||||||
case nostrAvailable // Mutual favorite, reachable via Nostr
|
case nostrAvailable // Mutual favorite, reachable via Nostr
|
||||||
case offline // Not connected via any transport
|
case offline // Not connected via any transport
|
||||||
}
|
}
|
||||||
@@ -27,8 +25,6 @@ struct BitchatPeer: Identifiable, Equatable {
|
|||||||
var connectionState: ConnectionState {
|
var connectionState: ConnectionState {
|
||||||
if isConnected {
|
if isConnected {
|
||||||
return .bluetoothConnected
|
return .bluetoothConnected
|
||||||
} else if isReachable {
|
|
||||||
return .meshReachable
|
|
||||||
} else if favoriteStatus?.isMutual == true {
|
} else if favoriteStatus?.isMutual == true {
|
||||||
// Mutual favorites can communicate via Nostr when offline
|
// Mutual favorites can communicate via Nostr when offline
|
||||||
return .nostrAvailable
|
return .nostrAvailable
|
||||||
@@ -58,8 +54,6 @@ struct BitchatPeer: Identifiable, Equatable {
|
|||||||
switch connectionState {
|
switch connectionState {
|
||||||
case .bluetoothConnected:
|
case .bluetoothConnected:
|
||||||
return "📻" // Radio icon for mesh connection
|
return "📻" // Radio icon for mesh connection
|
||||||
case .meshReachable:
|
|
||||||
return "📡" // Antenna for mesh reachable
|
|
||||||
case .nostrAvailable:
|
case .nostrAvailable:
|
||||||
return "🌐" // Purple globe for Nostr
|
return "🌐" // Purple globe for Nostr
|
||||||
case .offline:
|
case .offline:
|
||||||
@@ -77,15 +71,13 @@ struct BitchatPeer: Identifiable, Equatable {
|
|||||||
noisePublicKey: Data,
|
noisePublicKey: Data,
|
||||||
nickname: String,
|
nickname: String,
|
||||||
lastSeen: Date = Date(),
|
lastSeen: Date = Date(),
|
||||||
isConnected: Bool = false,
|
isConnected: Bool = false
|
||||||
isReachable: Bool = false
|
|
||||||
) {
|
) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.noisePublicKey = noisePublicKey
|
self.noisePublicKey = noisePublicKey
|
||||||
self.nickname = nickname
|
self.nickname = nickname
|
||||||
self.lastSeen = lastSeen
|
self.lastSeen = lastSeen
|
||||||
self.isConnected = isConnected
|
self.isConnected = isConnected
|
||||||
self.isReachable = isReachable
|
|
||||||
|
|
||||||
// Load favorite status - will be set later by the manager
|
// Load favorite status - will be set later by the manager
|
||||||
self.favoriteStatus = nil
|
self.favoriteStatus = nil
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ final class GeoRelayDirectory {
|
|||||||
private let cacheFileName = "georelays_cache.csv"
|
private let cacheFileName = "georelays_cache.csv"
|
||||||
private let lastFetchKey = "georelay.lastFetchAt"
|
private let lastFetchKey = "georelay.lastFetchAt"
|
||||||
private let remoteURL = URL(string: "https://raw.githubusercontent.com/permissionlesstech/georelays/refs/heads/main/nostr_relays.csv")!
|
private let remoteURL = URL(string: "https://raw.githubusercontent.com/permissionlesstech/georelays/refs/heads/main/nostr_relays.csv")!
|
||||||
private let fetchInterval: TimeInterval = TransportConfig.geoRelayFetchIntervalSeconds // 24h
|
private let fetchInterval: TimeInterval = 60 * 60 * 24 // 24h
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
// Load cached or bundled data synchronously
|
// Load cached or bundled data synchronously
|
||||||
|
|||||||
@@ -122,8 +122,8 @@ struct NostrProtocol {
|
|||||||
tags: tags,
|
tags: tags,
|
||||||
content: content
|
content: content
|
||||||
)
|
)
|
||||||
let schnorrKey = try senderIdentity.schnorrSigningKey()
|
let signingKey = try senderIdentity.signingKey()
|
||||||
return try event.sign(with: schnorrKey)
|
return try event.sign(with: signingKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private Methods
|
// MARK: - Private Methods
|
||||||
@@ -149,8 +149,9 @@ struct NostrProtocol {
|
|||||||
content: encrypted
|
content: encrypted
|
||||||
)
|
)
|
||||||
|
|
||||||
// Sign the seal with the sender's Schnorr private key
|
// Convert to P256K.Signing.PrivateKey for signing (temporary until we update sign method)
|
||||||
return try seal.sign(with: senderKey)
|
let signingKey = try P256K.Signing.PrivateKey(dataRepresentation: senderKey.dataRepresentation)
|
||||||
|
return try seal.sign(with: signingKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func createGiftWrap(
|
private static func createGiftWrap(
|
||||||
@@ -180,8 +181,9 @@ struct NostrProtocol {
|
|||||||
content: encrypted
|
content: encrypted
|
||||||
)
|
)
|
||||||
|
|
||||||
// Sign the gift wrap with the wrap Schnorr private key
|
// Convert to P256K.Signing.PrivateKey for signing (temporary until we update sign method)
|
||||||
return try giftWrap.sign(with: wrapKey)
|
let signingKey = try P256K.Signing.PrivateKey(dataRepresentation: wrapKey.dataRepresentation)
|
||||||
|
return try giftWrap.sign(with: signingKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func unwrapGiftWrap(
|
private static func unwrapGiftWrap(
|
||||||
@@ -470,16 +472,19 @@ struct NostrEvent: Codable {
|
|||||||
self.sig = dict["sig"] as? String
|
self.sig = dict["sig"] as? String
|
||||||
}
|
}
|
||||||
|
|
||||||
func sign(with key: P256K.Schnorr.PrivateKey) throws -> NostrEvent {
|
func sign(with key: P256K.Signing.PrivateKey) throws -> NostrEvent {
|
||||||
let (eventId, eventIdHash) = try calculateEventId()
|
let (eventId, eventIdHash) = try calculateEventId()
|
||||||
|
|
||||||
// Sign with Schnorr (BIP-340)
|
// Convert to Schnorr key for Nostr signing
|
||||||
|
let schnorrKey = try P256K.Schnorr.PrivateKey(dataRepresentation: key.dataRepresentation)
|
||||||
|
|
||||||
|
// Sign with Schnorr
|
||||||
var messageBytes = [UInt8](eventIdHash)
|
var messageBytes = [UInt8](eventIdHash)
|
||||||
var auxRand = [UInt8](repeating: 0, count: 32)
|
var auxRand = [UInt8](repeating: 0, count: 32)
|
||||||
_ = auxRand.withUnsafeMutableBytes { ptr in
|
_ = auxRand.withUnsafeMutableBytes { ptr in
|
||||||
SecRandomCopyBytes(kSecRandomDefault, 32, ptr.baseAddress!)
|
SecRandomCopyBytes(kSecRandomDefault, 32, ptr.baseAddress!)
|
||||||
}
|
}
|
||||||
let schnorrSignature = try key.signature(message: &messageBytes, auxiliaryRand: &auxRand)
|
let schnorrSignature = try schnorrKey.signature(message: &messageBytes, auxiliaryRand: &auxRand)
|
||||||
|
|
||||||
let signatureHex = schnorrSignature.dataRepresentation.hexEncodedString()
|
let signatureHex = schnorrSignature.dataRepresentation.hexEncodedString()
|
||||||
|
|
||||||
|
|||||||
@@ -48,10 +48,10 @@ class NostrRelayManager: ObservableObject {
|
|||||||
private let messageQueueLock = NSLock()
|
private let messageQueueLock = NSLock()
|
||||||
|
|
||||||
// Exponential backoff configuration
|
// Exponential backoff configuration
|
||||||
private let initialBackoffInterval: TimeInterval = TransportConfig.nostrRelayInitialBackoffSeconds
|
private let initialBackoffInterval: TimeInterval = 1.0 // Start with 1 second
|
||||||
private let maxBackoffInterval: TimeInterval = TransportConfig.nostrRelayMaxBackoffSeconds
|
private let maxBackoffInterval: TimeInterval = 300.0 // Max 5 minutes
|
||||||
private let backoffMultiplier: Double = TransportConfig.nostrRelayBackoffMultiplier
|
private let backoffMultiplier: Double = 2.0 // Double each time
|
||||||
private let maxReconnectAttempts = TransportConfig.nostrRelayMaxReconnectAttempts
|
private let maxReconnectAttempts = 10 // Stop after 10 attempts
|
||||||
|
|
||||||
// Reconnection timer
|
// Reconnection timer
|
||||||
private var reconnectionTimer: Timer?
|
private var reconnectionTimer: Timer?
|
||||||
@@ -289,7 +289,7 @@ class NostrRelayManager: ObservableObject {
|
|||||||
|
|
||||||
// Only log non-gift-wrap events to reduce noise
|
// Only log non-gift-wrap events to reduce noise
|
||||||
if event.kind != 1059 {
|
if event.kind != 1059 {
|
||||||
SecureLogger.log("📥 Event kind=\(event.kind) id=\(event.id.prefix(16))… relay=\(relayUrl)",
|
SecureLogger.log("📥 Received Nostr event (kind: \(event.kind)) from relay: \(relayUrl)",
|
||||||
category: SecureLogger.session, level: .debug)
|
category: SecureLogger.session, level: .debug)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,11 +321,11 @@ class NostrRelayManager: ObservableObject {
|
|||||||
let reason = array.count >= 4 ? (array[3] as? String ?? "no reason given") : "no reason given"
|
let reason = array.count >= 4 ? (array[3] as? String ?? "no reason given") : "no reason given"
|
||||||
if success {
|
if success {
|
||||||
_ = Self.pendingGiftWrapIDs.remove(eventId)
|
_ = Self.pendingGiftWrapIDs.remove(eventId)
|
||||||
SecureLogger.log("✅ Accepted id=\(eventId.prefix(16))… relay=\(relayUrl)",
|
SecureLogger.log("✅ Event accepted id=\(eventId.prefix(16))... by relay: \(relayUrl)",
|
||||||
category: SecureLogger.session, level: .debug)
|
category: SecureLogger.session, level: .debug)
|
||||||
} else {
|
} else {
|
||||||
let isGiftWrap = Self.pendingGiftWrapIDs.remove(eventId) != nil
|
let isGiftWrap = Self.pendingGiftWrapIDs.remove(eventId) != nil
|
||||||
SecureLogger.log("📮 Rejected id=\(eventId.prefix(16))… reason=\(reason)",
|
SecureLogger.log("📮 Event \(eventId.prefix(16))... rejected by relay: \(reason)",
|
||||||
category: SecureLogger.session, level: isGiftWrap ? .warning : .error)
|
category: SecureLogger.session, level: isGiftWrap ? .warning : .error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,7 +353,7 @@ class NostrRelayManager: ObservableObject {
|
|||||||
let data = try encoder.encode(req)
|
let data = try encoder.encode(req)
|
||||||
let message = String(data: data, encoding: .utf8) ?? ""
|
let message = String(data: data, encoding: .utf8) ?? ""
|
||||||
|
|
||||||
SecureLogger.log("📤 Send kind=\(event.kind) id=\(event.id.prefix(16))… relay=\(relayUrl)",
|
SecureLogger.log("📤 Sending Nostr event (kind: \(event.kind)) to relay: \(relayUrl)",
|
||||||
category: SecureLogger.session, level: .debug)
|
category: SecureLogger.session, level: .debug)
|
||||||
|
|
||||||
connection.send(.string(message)) { [weak self] error in
|
connection.send(.string(message)) { [weak self] error in
|
||||||
@@ -568,7 +568,7 @@ struct NostrFilter: Encodable {
|
|||||||
filter.kinds = [1059] // Gift wrap kind
|
filter.kinds = [1059] // Gift wrap kind
|
||||||
filter.since = since?.timeIntervalSince1970.toInt()
|
filter.since = since?.timeIntervalSince1970.toInt()
|
||||||
filter.tagFilters = ["p": [pubkey]]
|
filter.tagFilters = ["p": [pubkey]]
|
||||||
filter.limit = TransportConfig.nostrRelayDefaultFetchLimit // reasonable limit
|
filter.limit = 100 // Add a reasonable limit
|
||||||
return filter
|
return filter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+198
-498
File diff suppressed because it is too large
Load Diff
@@ -39,7 +39,7 @@ final class LocationChannelManager: NSObject, CLLocationManagerDelegate, Observa
|
|||||||
super.init()
|
super.init()
|
||||||
cl.delegate = self
|
cl.delegate = self
|
||||||
cl.desiredAccuracy = kCLLocationAccuracyHundredMeters
|
cl.desiredAccuracy = kCLLocationAccuracyHundredMeters
|
||||||
cl.distanceFilter = TransportConfig.locationDistanceFilterMeters // meters; we're not tracking continuously
|
cl.distanceFilter = 1000 // meters; we're not tracking continuously
|
||||||
// Load selection
|
// Load selection
|
||||||
if let data = UserDefaults.standard.data(forKey: userDefaultsKey),
|
if let data = UserDefaults.standard.data(forKey: userDefaultsKey),
|
||||||
let channel = try? JSONDecoder().decode(ChannelID.self, from: data) {
|
let channel = try? JSONDecoder().decode(ChannelID.self, from: data) {
|
||||||
@@ -93,7 +93,7 @@ final class LocationChannelManager: NSObject, CLLocationManagerDelegate, Observa
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Begin periodic one-shot location refreshes while a selector UI is visible.
|
/// Begin periodic one-shot location refreshes while a selector UI is visible.
|
||||||
func beginLiveRefresh(interval: TimeInterval = TransportConfig.locationLiveRefreshInterval) {
|
func beginLiveRefresh(interval: TimeInterval = 5.0) {
|
||||||
guard permissionState == .authorized else { return }
|
guard permissionState == .authorized else { return }
|
||||||
// Switch to a lightweight periodic one-shot request (polling) while the sheet is open
|
// Switch to a lightweight periodic one-shot request (polling) while the sheet is open
|
||||||
refreshTimer?.invalidate()
|
refreshTimer?.invalidate()
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ final class MessageRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sendPrivate(_ content: String, to peerID: String, recipientNickname: String, messageID: String) {
|
func sendPrivate(_ content: String, to peerID: String, recipientNickname: String, messageID: String) {
|
||||||
let reachableMesh = mesh.isPeerReachable(peerID)
|
let hasMesh = mesh.isPeerConnected(peerID)
|
||||||
if reachableMesh {
|
let hasEstablished = mesh.getNoiseService().hasEstablishedSession(with: peerID)
|
||||||
SecureLogger.log("Routing PM via mesh (reachable) to \(peerID.prefix(8))… id=\(messageID.prefix(8))…",
|
if hasMesh && hasEstablished {
|
||||||
|
SecureLogger.log("Routing PM via mesh to \(peerID.prefix(8))… id=\(messageID.prefix(8))…",
|
||||||
category: SecureLogger.session, level: .debug)
|
category: SecureLogger.session, level: .debug)
|
||||||
// BLEService will initiate a handshake if needed and queue the message
|
|
||||||
mesh.sendPrivateMessage(content, to: peerID, recipientNickname: recipientNickname, messageID: messageID)
|
mesh.sendPrivateMessage(content, to: peerID, recipientNickname: recipientNickname, messageID: messageID)
|
||||||
} else if canSendViaNostr(peerID: peerID) {
|
} else if canSendViaNostr(peerID: peerID) {
|
||||||
SecureLogger.log("Routing PM via Nostr to \(peerID.prefix(8))… id=\(messageID.prefix(8))…",
|
SecureLogger.log("Routing PM via Nostr to \(peerID.prefix(8))… id=\(messageID.prefix(8))…",
|
||||||
@@ -57,9 +57,9 @@ final class MessageRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sendReadReceipt(_ receipt: ReadReceipt, to peerID: String) {
|
func sendReadReceipt(_ receipt: ReadReceipt, to peerID: String) {
|
||||||
// Prefer mesh for reachable peers; BLE will queue if handshake is needed
|
// Prefer mesh only if a Noise session is established; else use Nostr to avoid handshakeRequired spam
|
||||||
if mesh.isPeerReachable(peerID) {
|
if mesh.isPeerConnected(peerID) && mesh.getNoiseService().hasEstablishedSession(with: peerID) {
|
||||||
SecureLogger.log("Routing READ ack via mesh (reachable) to \(peerID.prefix(8))… id=\(receipt.originalMessageID.prefix(8))…",
|
SecureLogger.log("Routing READ ack via mesh to \(peerID.prefix(8))… id=\(receipt.originalMessageID.prefix(8))…",
|
||||||
category: SecureLogger.session, level: .debug)
|
category: SecureLogger.session, level: .debug)
|
||||||
mesh.sendReadReceipt(receipt, to: peerID)
|
mesh.sendReadReceipt(receipt, to: peerID)
|
||||||
} else {
|
} else {
|
||||||
@@ -70,9 +70,7 @@ final class MessageRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sendDeliveryAck(_ messageID: String, to peerID: String) {
|
func sendDeliveryAck(_ messageID: String, to peerID: String) {
|
||||||
if mesh.isPeerReachable(peerID) {
|
if mesh.isPeerConnected(peerID) && mesh.getNoiseService().hasEstablishedSession(with: peerID) {
|
||||||
SecureLogger.log("Routing DELIVERED ack via mesh (reachable) to \(peerID.prefix(8))… id=\(messageID.prefix(8))…",
|
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
mesh.sendDeliveryAck(for: messageID, to: peerID)
|
mesh.sendDeliveryAck(for: messageID, to: peerID)
|
||||||
} else {
|
} else {
|
||||||
nostr.sendDeliveryAck(for: messageID, to: peerID)
|
nostr.sendDeliveryAck(for: messageID, to: peerID)
|
||||||
@@ -103,7 +101,7 @@ final class MessageRouter {
|
|||||||
category: SecureLogger.session, level: .debug)
|
category: SecureLogger.session, level: .debug)
|
||||||
// Prefer mesh if connected; else try Nostr if mapping exists
|
// Prefer mesh if connected; else try Nostr if mapping exists
|
||||||
for (content, nickname, messageID) in queued {
|
for (content, nickname, messageID) in queued {
|
||||||
if mesh.isPeerReachable(peerID) {
|
if mesh.isPeerConnected(peerID) {
|
||||||
SecureLogger.log("Outbox -> mesh for \(peerID.prefix(8))… id=\(messageID.prefix(8))…",
|
SecureLogger.log("Outbox -> mesh for \(peerID.prefix(8))… id=\(messageID.prefix(8))…",
|
||||||
category: SecureLogger.session, level: .debug)
|
category: SecureLogger.session, level: .debug)
|
||||||
mesh.sendPrivateMessage(content, to: peerID, recipientNickname: nickname, messageID: messageID)
|
mesh.sendPrivateMessage(content, to: peerID, recipientNickname: nickname, messageID: messageID)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ final class NostrTransport: Transport {
|
|||||||
}
|
}
|
||||||
private var readQueue: [QueuedRead] = []
|
private var readQueue: [QueuedRead] = []
|
||||||
private var isSendingReadAcks = false
|
private var isSendingReadAcks = false
|
||||||
private let readAckInterval: TimeInterval = TransportConfig.nostrReadAckInterval
|
private let readAckInterval: TimeInterval = 0.35 // ~3 per second
|
||||||
|
|
||||||
var myPeerID: String { senderPeerID }
|
var myPeerID: String { senderPeerID }
|
||||||
var myNickname: String { "" }
|
var myNickname: String { "" }
|
||||||
@@ -31,7 +31,6 @@ final class NostrTransport: Transport {
|
|||||||
func emergencyDisconnectAll() { /* no-op */ }
|
func emergencyDisconnectAll() { /* no-op */ }
|
||||||
|
|
||||||
func isPeerConnected(_ peerID: String) -> Bool { false }
|
func isPeerConnected(_ peerID: String) -> Bool { false }
|
||||||
func isPeerReachable(_ peerID: String) -> Bool { false }
|
|
||||||
func peerNickname(peerID: String) -> String? { nil }
|
func peerNickname(peerID: String) -> String? { nil }
|
||||||
func getPeerNicknames() -> [String : String] { [:] }
|
func getPeerNicknames() -> [String : String] { [:] }
|
||||||
|
|
||||||
@@ -49,7 +48,16 @@ final class NostrTransport: Transport {
|
|||||||
|
|
||||||
func sendPrivateMessage(_ content: String, to peerID: String, recipientNickname: String, messageID: String) {
|
func sendPrivateMessage(_ content: String, to peerID: String, recipientNickname: String, messageID: String) {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
guard let recipientNpub = resolveRecipientNpub(for: peerID) else { return }
|
// Resolve favorite by full noise key or by short peerID fallback
|
||||||
|
var recipientNostrPubkey: String?
|
||||||
|
if let noiseKey = Data(hexString: peerID),
|
||||||
|
let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey) {
|
||||||
|
recipientNostrPubkey = fav.peerNostrPublicKey
|
||||||
|
}
|
||||||
|
if recipientNostrPubkey == nil, peerID.count == 16 {
|
||||||
|
recipientNostrPubkey = FavoritesPersistenceService.shared.getFavoriteStatus(forPeerID: peerID)?.peerNostrPublicKey
|
||||||
|
}
|
||||||
|
guard let recipientNpub = recipientNostrPubkey else { return }
|
||||||
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
|
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
|
||||||
SecureLogger.log("NostrTransport: preparing PM to \(recipientNpub.prefix(16))… for peerID \(peerID.prefix(8))… id=\(messageID.prefix(8))…",
|
SecureLogger.log("NostrTransport: preparing PM to \(recipientNpub.prefix(16))… for peerID \(peerID.prefix(8))… id=\(messageID.prefix(8))…",
|
||||||
category: SecureLogger.session, level: .debug)
|
category: SecureLogger.session, level: .debug)
|
||||||
@@ -97,7 +105,15 @@ final class NostrTransport: Transport {
|
|||||||
guard !readQueue.isEmpty else { isSendingReadAcks = false; return }
|
guard !readQueue.isEmpty else { isSendingReadAcks = false; return }
|
||||||
let item = readQueue.removeFirst()
|
let item = readQueue.removeFirst()
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
guard let recipientNpub = resolveRecipientNpub(for: item.peerID) else { scheduleNextReadAck(); return }
|
var recipientNostrPubkey: String?
|
||||||
|
if let noiseKey = Data(hexString: item.peerID),
|
||||||
|
let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey) {
|
||||||
|
recipientNostrPubkey = fav.peerNostrPublicKey
|
||||||
|
}
|
||||||
|
if recipientNostrPubkey == nil, item.peerID.count == 16 {
|
||||||
|
recipientNostrPubkey = FavoritesPersistenceService.shared.getFavoriteStatus(forPeerID: item.peerID)?.peerNostrPublicKey
|
||||||
|
}
|
||||||
|
guard let recipientNpub = recipientNostrPubkey else { scheduleNextReadAck(); return }
|
||||||
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { scheduleNextReadAck(); return }
|
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { scheduleNextReadAck(); return }
|
||||||
SecureLogger.log("NostrTransport: preparing READ ack for id=\(item.receipt.originalMessageID.prefix(8))… to \(recipientNpub.prefix(16))…",
|
SecureLogger.log("NostrTransport: preparing READ ack for id=\(item.receipt.originalMessageID.prefix(8))… to \(recipientNpub.prefix(16))…",
|
||||||
category: SecureLogger.session, level: .debug)
|
category: SecureLogger.session, level: .debug)
|
||||||
@@ -133,7 +149,15 @@ final class NostrTransport: Transport {
|
|||||||
|
|
||||||
func sendFavoriteNotification(to peerID: String, isFavorite: Bool) {
|
func sendFavoriteNotification(to peerID: String, isFavorite: Bool) {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
guard let recipientNpub = resolveRecipientNpub(for: peerID) else { return }
|
var recipientNostrPubkey: String?
|
||||||
|
if let noiseKey = Data(hexString: peerID),
|
||||||
|
let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey) {
|
||||||
|
recipientNostrPubkey = fav.peerNostrPublicKey
|
||||||
|
}
|
||||||
|
if recipientNostrPubkey == nil, peerID.count == 16 {
|
||||||
|
recipientNostrPubkey = FavoritesPersistenceService.shared.getFavoriteStatus(forPeerID: peerID)?.peerNostrPublicKey
|
||||||
|
}
|
||||||
|
guard let recipientNpub = recipientNostrPubkey else { return }
|
||||||
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
|
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
|
||||||
let content = isFavorite ? "[FAVORITED]:\(senderIdentity.npub)" : "[UNFAVORITED]:\(senderIdentity.npub)"
|
let content = isFavorite ? "[FAVORITED]:\(senderIdentity.npub)" : "[UNFAVORITED]:\(senderIdentity.npub)"
|
||||||
SecureLogger.log("NostrTransport: preparing FAVORITE(\(isFavorite)) to \(recipientNpub.prefix(16))…",
|
SecureLogger.log("NostrTransport: preparing FAVORITE(\(isFavorite)) to \(recipientNpub.prefix(16))…",
|
||||||
@@ -159,26 +183,18 @@ final class NostrTransport: Transport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Helpers
|
|
||||||
@MainActor
|
|
||||||
private func resolveRecipientNpub(for peerID: String) -> String? {
|
|
||||||
if let noiseKey = Data(hexString: peerID),
|
|
||||||
let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey),
|
|
||||||
let npub = fav.peerNostrPublicKey {
|
|
||||||
return npub
|
|
||||||
}
|
|
||||||
if peerID.count == 16,
|
|
||||||
let fav = FavoritesPersistenceService.shared.getFavoriteStatus(forPeerID: peerID),
|
|
||||||
let npub = fav.peerNostrPublicKey {
|
|
||||||
return npub
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendBroadcastAnnounce() { /* no-op for Nostr */ }
|
func sendBroadcastAnnounce() { /* no-op for Nostr */ }
|
||||||
func sendDeliveryAck(for messageID: String, to peerID: String) {
|
func sendDeliveryAck(for messageID: String, to peerID: String) {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
guard let recipientNpub = resolveRecipientNpub(for: peerID) else { return }
|
var recipientNostrPubkey: String?
|
||||||
|
if let noiseKey = Data(hexString: peerID),
|
||||||
|
let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey) {
|
||||||
|
recipientNostrPubkey = fav.peerNostrPublicKey
|
||||||
|
}
|
||||||
|
if recipientNostrPubkey == nil, peerID.count == 16 {
|
||||||
|
recipientNostrPubkey = FavoritesPersistenceService.shared.getFavoriteStatus(forPeerID: peerID)?.peerNostrPublicKey
|
||||||
|
}
|
||||||
|
guard let recipientNpub = recipientNostrPubkey else { return }
|
||||||
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
|
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
|
||||||
SecureLogger.log("NostrTransport: preparing DELIVERED ack for id=\(messageID.prefix(8))… to \(recipientNpub.prefix(16))…",
|
SecureLogger.log("NostrTransport: preparing DELIVERED ack for id=\(messageID.prefix(8))… to \(recipientNpub.prefix(16))…",
|
||||||
category: SecureLogger.session, level: .debug)
|
category: SecureLogger.session, level: .debug)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class PrivateChatManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cap for messages stored per private chat
|
// Cap for messages stored per private chat
|
||||||
private let privateChatCap = TransportConfig.privateChatCap
|
private let privateChatCap = 1337
|
||||||
|
|
||||||
/// Start a private chat with a peer
|
/// Start a private chat with a peer
|
||||||
func startChat(with peerID: String) {
|
func startChat(with peerID: String) {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ struct RelayController {
|
|||||||
isDirectedEncrypted: Bool,
|
isDirectedEncrypted: Bool,
|
||||||
isDirectedFragment: Bool,
|
isDirectedFragment: Bool,
|
||||||
isHandshake: Bool,
|
isHandshake: Bool,
|
||||||
isAnnounce: Bool,
|
|
||||||
degree: Int,
|
degree: Int,
|
||||||
highDegreeThreshold: Int) -> RelayDecision {
|
highDegreeThreshold: Int) -> RelayDecision {
|
||||||
// Suppress obvious non-relays
|
// Suppress obvious non-relays
|
||||||
@@ -26,8 +25,7 @@ struct RelayController {
|
|||||||
// Always relay with no TTL cap for these types
|
// Always relay with no TTL cap for these types
|
||||||
let newTTL = (ttl &- 1)
|
let newTTL = (ttl &- 1)
|
||||||
// Slight jitter to desynchronize without adding too much latency
|
// Slight jitter to desynchronize without adding too much latency
|
||||||
// Tighter for faster multi-hop handshakes and directed DMs
|
let delayRange: ClosedRange<Int> = isHandshake ? 20...60 : 40...120
|
||||||
let delayRange: ClosedRange<Int> = isHandshake ? 10...35 : 20...60
|
|
||||||
let delayMs = Int.random(in: delayRange)
|
let delayMs = Int.random(in: delayRange)
|
||||||
return RelayDecision(shouldRelay: true, newTTL: newTTL, delayMs: delayMs)
|
return RelayDecision(shouldRelay: true, newTTL: newTTL, delayMs: delayMs)
|
||||||
}
|
}
|
||||||
@@ -44,22 +42,15 @@ struct RelayController {
|
|||||||
let prob = baseProb
|
let prob = baseProb
|
||||||
let shouldRelay = Double.random(in: 0...1) <= prob
|
let shouldRelay = Double.random(in: 0...1) <= prob
|
||||||
|
|
||||||
// TTL clamping for broadcast
|
// TTL clamping in dense graphs (only for broadcast)
|
||||||
// - Dense graphs: keep very low to avoid floods
|
let ttlCap: UInt8 = degree >= highDegreeThreshold ? 3 : 5
|
||||||
// - Sparse graphs: allow slightly longer reach for multi-hop discovery
|
|
||||||
// - Announces in sparse graphs get a bit more headroom
|
|
||||||
let ttlCap: UInt8 = {
|
|
||||||
if degree >= highDegreeThreshold { return 3 }
|
|
||||||
return isAnnounce ? 7 : 6
|
|
||||||
}()
|
|
||||||
let clamped = max(1, min(ttl, ttlCap))
|
let clamped = max(1, min(ttl, ttlCap))
|
||||||
let newTTL = clamped &- 1
|
let newTTL = clamped &- 1
|
||||||
|
|
||||||
// Wider jitter window to allow duplicate suppression to win more often
|
// Wider jitter window to allow duplicate suppression to win more often
|
||||||
// For sparse graphs (<=2), relay quickly to avoid cancellation races
|
|
||||||
let delayMs: Int
|
let delayMs: Int
|
||||||
switch degree {
|
switch degree {
|
||||||
case 0...2: delayMs = Int.random(in: 10...40)
|
case 0...2: delayMs = Int.random(in: 40...100)
|
||||||
case 3...5: delayMs = Int.random(in: 60...150)
|
case 3...5: delayMs = Int.random(in: 60...150)
|
||||||
case 6...9: delayMs = Int.random(in: 80...180)
|
case 6...9: delayMs = Int.random(in: 80...180)
|
||||||
default: delayMs = Int.random(in: 100...220)
|
default: delayMs = Int.random(in: 100...220)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Combine
|
|||||||
|
|
||||||
/// Abstract transport interface used by ChatViewModel and services.
|
/// Abstract transport interface used by ChatViewModel and services.
|
||||||
/// BLEService implements this protocol; a future Nostr transport can too.
|
/// BLEService implements this protocol; a future Nostr transport can too.
|
||||||
struct TransportPeerSnapshot: Equatable, Hashable {
|
struct TransportPeerSnapshot {
|
||||||
let id: String
|
let id: String
|
||||||
let nickname: String
|
let nickname: String
|
||||||
let isConnected: Bool
|
let isConnected: Bool
|
||||||
@@ -29,7 +29,6 @@ protocol Transport: AnyObject {
|
|||||||
|
|
||||||
// Connectivity and peers
|
// Connectivity and peers
|
||||||
func isPeerConnected(_ peerID: String) -> Bool
|
func isPeerConnected(_ peerID: String) -> Bool
|
||||||
func isPeerReachable(_ peerID: String) -> Bool
|
|
||||||
func peerNickname(peerID: String) -> String?
|
func peerNickname(peerID: String) -> String?
|
||||||
func getPeerNicknames() -> [String: String]
|
func getPeerNicknames() -> [String: String]
|
||||||
|
|
||||||
|
|||||||
@@ -1,182 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
/// Centralized knobs for transport- and UI-related limits.
|
|
||||||
/// Keep values aligned with existing behavior when replacing magic numbers.
|
|
||||||
enum TransportConfig {
|
|
||||||
// BLE / Protocol
|
|
||||||
static let bleDefaultFragmentSize: Int = 469 // ~512 MTU minus protocol overhead
|
|
||||||
static let messageTTLDefault: UInt8 = 7 // Default TTL for mesh flooding
|
|
||||||
static let bleMaxInFlightAssemblies: Int = 128 // Cap concurrent fragment assemblies
|
|
||||||
static let bleHighDegreeThreshold: Int = 6 // For adaptive TTL/probabilistic relays
|
|
||||||
|
|
||||||
// UI / Storage Caps
|
|
||||||
static let privateChatCap: Int = 1337
|
|
||||||
static let meshTimelineCap: Int = 1337
|
|
||||||
static let geoTimelineCap: Int = 1337
|
|
||||||
static let contentLRUCap: Int = 2000
|
|
||||||
|
|
||||||
// Timers
|
|
||||||
static let networkResetGraceSeconds: TimeInterval = 600 // 10 minutes
|
|
||||||
static let basePublicFlushInterval: TimeInterval = 0.08 // ~12.5 fps batching
|
|
||||||
|
|
||||||
// BLE duty/announce/connect
|
|
||||||
static let bleConnectRateLimitInterval: TimeInterval = 0.5
|
|
||||||
static let bleMaxCentralLinks: Int = 6
|
|
||||||
static let bleDutyOnDuration: TimeInterval = 5.0
|
|
||||||
static let bleDutyOffDuration: TimeInterval = 10.0
|
|
||||||
static let bleAnnounceMinInterval: TimeInterval = 1.0
|
|
||||||
|
|
||||||
// BLE discovery/quality thresholds
|
|
||||||
static let bleDynamicRSSIThresholdDefault: Int = -90
|
|
||||||
static let bleConnectionCandidatesMax: Int = 100
|
|
||||||
static let blePendingWriteBufferCapBytes: Int = 1_000_000
|
|
||||||
static let blePendingNotificationsCapCount: Int = 20
|
|
||||||
|
|
||||||
// Nostr
|
|
||||||
static let nostrReadAckInterval: TimeInterval = 0.35 // ~3 per second
|
|
||||||
|
|
||||||
// UI thresholds
|
|
||||||
static let uiLateInsertThreshold: TimeInterval = 15.0
|
|
||||||
static let uiProcessedNostrEventsCap: Int = 2000
|
|
||||||
static let uiChannelInactivityThresholdSeconds: TimeInterval = 9 * 60
|
|
||||||
|
|
||||||
// UI rate limiters (token buckets)
|
|
||||||
static let uiSenderRateBucketCapacity: Double = 5
|
|
||||||
static let uiSenderRateBucketRefillPerSec: Double = 1.0
|
|
||||||
static let uiContentRateBucketCapacity: Double = 3
|
|
||||||
static let uiContentRateBucketRefillPerSec: Double = 0.5
|
|
||||||
|
|
||||||
// UI sleeps/delays
|
|
||||||
static let uiStartupInitialDelaySeconds: TimeInterval = 1.0
|
|
||||||
static let uiStartupShortSleepNs: UInt64 = 200_000_000
|
|
||||||
static let uiStartupPhaseDurationSeconds: TimeInterval = 2.0
|
|
||||||
static let uiAsyncShortSleepNs: UInt64 = 100_000_000
|
|
||||||
static let uiAsyncMediumSleepNs: UInt64 = 500_000_000
|
|
||||||
static let uiReadReceiptRetryShortSeconds: TimeInterval = 0.1
|
|
||||||
static let uiReadReceiptRetryLongSeconds: TimeInterval = 0.5
|
|
||||||
static let uiBatchDispatchStaggerSeconds: TimeInterval = 0.15
|
|
||||||
static let uiScrollThrottleSeconds: TimeInterval = 0.5
|
|
||||||
static let uiAnimationShortSeconds: TimeInterval = 0.15
|
|
||||||
static let uiAnimationMediumSeconds: TimeInterval = 0.2
|
|
||||||
static let uiAnimationSidebarSeconds: TimeInterval = 0.25
|
|
||||||
static let uiRecentCutoffFiveMinutesSeconds: TimeInterval = 5 * 60
|
|
||||||
|
|
||||||
// BLE maintenance & thresholds
|
|
||||||
static let bleMaintenanceInterval: TimeInterval = 5.0
|
|
||||||
static let bleMaintenanceLeewaySeconds: Int = 1
|
|
||||||
static let bleIsolationRelaxThresholdSeconds: TimeInterval = 60
|
|
||||||
static let bleRecentTimeoutWindowSeconds: TimeInterval = 60
|
|
||||||
static let bleRecentTimeoutCountThreshold: Int = 3
|
|
||||||
static let bleRSSIIsolatedBase: Int = -90
|
|
||||||
static let bleRSSIIsolatedRelaxed: Int = -92
|
|
||||||
static let bleRSSIConnectedThreshold: Int = -85
|
|
||||||
static let bleRSSIHighTimeoutThreshold: Int = -80
|
|
||||||
// How long without seeing traffic before we sanity-check the direct link
|
|
||||||
// Lowered to make connected→reachable icon changes react faster when walking out of range
|
|
||||||
static let blePeerInactivityTimeoutSeconds: TimeInterval = 8.0
|
|
||||||
// How long to retain a peer as "reachable" (not directly connected) since lastSeen
|
|
||||||
static let bleReachabilityRetentionVerifiedSeconds: TimeInterval = 21.0 // 21s for verified/favorites
|
|
||||||
static let bleReachabilityRetentionUnverifiedSeconds: TimeInterval = 21.0 // 21s for unknown/unverified
|
|
||||||
static let bleFragmentLifetimeSeconds: TimeInterval = 30.0
|
|
||||||
static let bleIngressRecordLifetimeSeconds: TimeInterval = 3.0
|
|
||||||
static let bleConnectTimeoutBackoffWindowSeconds: TimeInterval = 120.0
|
|
||||||
static let bleRecentPacketWindowSeconds: TimeInterval = 30.0
|
|
||||||
static let bleRecentPacketWindowMaxCount: Int = 100
|
|
||||||
// Keep scanning fully ON when we saw traffic very recently
|
|
||||||
static let bleRecentTrafficForceScanSeconds: TimeInterval = 10.0
|
|
||||||
static let bleThreadSleepWriteShortDelaySeconds: TimeInterval = 0.05
|
|
||||||
static let bleExpectedWritePerFragmentMs: Int = 8
|
|
||||||
static let bleExpectedWriteMaxMs: Int = 2000
|
|
||||||
// Faster fragment pacing; use slightly tighter spacing for directed trains
|
|
||||||
static let bleFragmentSpacingMs: Int = 5
|
|
||||||
static let bleFragmentSpacingDirectedMs: Int = 4
|
|
||||||
static let bleAnnounceIntervalSeconds: TimeInterval = 4.0
|
|
||||||
static let bleDutyOnDurationDense: TimeInterval = 3.0
|
|
||||||
static let bleDutyOffDurationDense: TimeInterval = 15.0
|
|
||||||
static let bleConnectedAnnounceBaseSecondsDense: TimeInterval = 30.0
|
|
||||||
static let bleConnectedAnnounceBaseSecondsSparse: TimeInterval = 15.0
|
|
||||||
static let bleConnectedAnnounceJitterDense: TimeInterval = 8.0
|
|
||||||
static let bleConnectedAnnounceJitterSparse: TimeInterval = 4.0
|
|
||||||
|
|
||||||
// Location
|
|
||||||
static let locationDistanceFilterMeters: Double = 1000
|
|
||||||
static let locationLiveRefreshInterval: TimeInterval = 5.0
|
|
||||||
|
|
||||||
// Nostr geohash
|
|
||||||
static let nostrGeohashInitialLookbackSeconds: TimeInterval = 3600
|
|
||||||
static let nostrGeohashInitialLimit: Int = 200
|
|
||||||
static let nostrGeoRelayCount: Int = 5
|
|
||||||
static let nostrGeohashSampleLookbackSeconds: TimeInterval = 300
|
|
||||||
static let nostrGeohashSampleLimit: Int = 100
|
|
||||||
static let nostrDMSubscribeLookbackSeconds: TimeInterval = 86400
|
|
||||||
|
|
||||||
// Nostr helpers
|
|
||||||
static let nostrShortKeyDisplayLength: Int = 8
|
|
||||||
static let nostrConvKeyPrefixLength: Int = 16
|
|
||||||
|
|
||||||
// Compression
|
|
||||||
static let compressionThresholdBytes: Int = 100
|
|
||||||
|
|
||||||
// Message deduplication
|
|
||||||
static let messageDedupMaxAgeSeconds: TimeInterval = 300
|
|
||||||
static let messageDedupMaxCount: Int = 1000
|
|
||||||
|
|
||||||
// Verification QR
|
|
||||||
static let verificationQRMaxAgeSeconds: TimeInterval = 5 * 60
|
|
||||||
|
|
||||||
// Nostr relay backoff
|
|
||||||
static let nostrRelayInitialBackoffSeconds: TimeInterval = 1.0
|
|
||||||
static let nostrRelayMaxBackoffSeconds: TimeInterval = 300.0
|
|
||||||
static let nostrRelayBackoffMultiplier: Double = 2.0
|
|
||||||
static let nostrRelayMaxReconnectAttempts: Int = 10
|
|
||||||
static let nostrRelayDefaultFetchLimit: Int = 100
|
|
||||||
|
|
||||||
// Geo relay directory
|
|
||||||
static let geoRelayFetchIntervalSeconds: TimeInterval = 60 * 60 * 24
|
|
||||||
|
|
||||||
// BLE operational delays
|
|
||||||
static let bleInitialAnnounceDelaySeconds: TimeInterval = 0.6
|
|
||||||
static let bleConnectTimeoutSeconds: TimeInterval = 8.0
|
|
||||||
static let bleRestartScanDelaySeconds: TimeInterval = 0.1
|
|
||||||
static let blePostSubscribeAnnounceDelaySeconds: TimeInterval = 0.05
|
|
||||||
static let blePostAnnounceDelaySeconds: TimeInterval = 0.4
|
|
||||||
static let bleForceAnnounceMinIntervalSeconds: TimeInterval = 0.15
|
|
||||||
|
|
||||||
// Store-and-forward for directed packets at relays
|
|
||||||
static let bleDirectedSpoolWindowSeconds: TimeInterval = 15.0
|
|
||||||
|
|
||||||
// Log/UI debounce windows
|
|
||||||
// Shorter debounce so UI reacts faster while still suppressing duplicate callbacks
|
|
||||||
static let bleDisconnectNotifyDebounceSeconds: TimeInterval = 0.9
|
|
||||||
static let bleReconnectLogDebounceSeconds: TimeInterval = 2.0
|
|
||||||
|
|
||||||
// Weak-link cooldown after connection timeouts
|
|
||||||
static let bleWeakLinkCooldownSeconds: TimeInterval = 30.0
|
|
||||||
static let bleWeakLinkRSSICutoff: Int = -90
|
|
||||||
|
|
||||||
// Content hashing / formatting
|
|
||||||
static let contentKeyPrefixLength: Int = 256
|
|
||||||
static let uiLongMessageLengthThreshold: Int = 2000
|
|
||||||
static let uiVeryLongTokenThreshold: Int = 512
|
|
||||||
static let uiLongMessageLineLimit: Int = 30
|
|
||||||
static let uiFingerprintSampleCount: Int = 3
|
|
||||||
|
|
||||||
// UI swipe/gesture thresholds
|
|
||||||
static let uiBackSwipeTranslationLarge: CGFloat = 50
|
|
||||||
static let uiBackSwipeTranslationSmall: CGFloat = 30
|
|
||||||
static let uiBackSwipeVelocityThreshold: CGFloat = 300
|
|
||||||
|
|
||||||
// UI color tuning
|
|
||||||
static let uiColorHueAvoidanceDelta: Double = 0.05
|
|
||||||
static let uiColorHueOffset: Double = 0.12
|
|
||||||
|
|
||||||
// UI windowing (infinite scroll)
|
|
||||||
static let uiWindowInitialCountPublic: Int = 300
|
|
||||||
static let uiWindowInitialCountPrivate: Int = 300
|
|
||||||
static let uiWindowStepCount: Int = 200
|
|
||||||
|
|
||||||
// Share extension
|
|
||||||
static let uiShareExtensionDismissDelaySeconds: TimeInterval = 0.3
|
|
||||||
static let uiShareAcceptWindowSeconds: TimeInterval = 30.0
|
|
||||||
static let uiMigrationCutoffSeconds: TimeInterval = 24 * 60 * 60
|
|
||||||
}
|
|
||||||
@@ -68,28 +68,24 @@ class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
|||||||
|
|
||||||
private func updatePeers() {
|
private func updatePeers() {
|
||||||
let meshPeers = meshService.currentPeerSnapshots()
|
let meshPeers = meshService.currentPeerSnapshots()
|
||||||
// If we have no direct links at all, peers should not be marked reachable
|
|
||||||
// "Reachable" means mesh-attached via at least one live link.
|
|
||||||
let hasAnyConnected = meshPeers.contains { $0.isConnected }
|
|
||||||
let favorites = favoritesService.favorites
|
let favorites = favoritesService.favorites
|
||||||
|
|
||||||
var enrichedPeers: [BitchatPeer] = []
|
var enrichedPeers: [BitchatPeer] = []
|
||||||
var connected: Set<String> = []
|
var connected: Set<String> = []
|
||||||
var addedPeerIDs: Set<String> = []
|
var addedPeerIDs: Set<String> = []
|
||||||
|
|
||||||
// Phase 1: Add all mesh peers (connected and reachable)
|
// Phase 1: Add all connected mesh peers
|
||||||
for peerInfo in meshPeers {
|
for peerInfo in meshPeers where peerInfo.isConnected {
|
||||||
let peerID = peerInfo.id
|
let peerID = peerInfo.id
|
||||||
guard peerID != meshService.myPeerID else { continue } // Never add self
|
guard peerID != meshService.myPeerID else { continue } // Never add self
|
||||||
|
|
||||||
let peer = buildPeerFromMesh(
|
let peer = buildPeerFromMesh(
|
||||||
peerInfo: peerInfo,
|
peerInfo: peerInfo,
|
||||||
favorites: favorites,
|
favorites: favorites
|
||||||
meshAttached: hasAnyConnected
|
|
||||||
)
|
)
|
||||||
|
|
||||||
enrichedPeers.append(peer)
|
enrichedPeers.append(peer)
|
||||||
if peer.isConnected { connected.insert(peerID) }
|
connected.insert(peerID)
|
||||||
addedPeerIDs.insert(peerID)
|
addedPeerIDs.insert(peerID)
|
||||||
|
|
||||||
// Update fingerprint cache
|
// Update fingerprint cache
|
||||||
@@ -121,12 +117,14 @@ class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
|||||||
|
|
||||||
// Phase 3: Sort peers
|
// Phase 3: Sort peers
|
||||||
enrichedPeers.sort { lhs, rhs in
|
enrichedPeers.sort { lhs, rhs in
|
||||||
// Connectivity rank: connected > reachable > others
|
// Connected first
|
||||||
func rank(_ p: BitchatPeer) -> Int { p.isConnected ? 2 : (p.isReachable ? 1 : 0) }
|
if lhs.isConnected != rhs.isConnected {
|
||||||
let lr = rank(lhs), rr = rank(rhs)
|
return lhs.isConnected
|
||||||
if lr != rr { return lr > rr }
|
}
|
||||||
// Then favorites inside same rank
|
// Then favorites
|
||||||
if lhs.isFavorite != rhs.isFavorite { return lhs.isFavorite }
|
if lhs.isFavorite != rhs.isFavorite {
|
||||||
|
return lhs.isFavorite
|
||||||
|
}
|
||||||
// Finally alphabetical
|
// Finally alphabetical
|
||||||
return lhs.displayName < rhs.displayName
|
return lhs.displayName < rhs.displayName
|
||||||
}
|
}
|
||||||
@@ -147,11 +145,8 @@ class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 5: Filter out offline non-mutual peers and update published properties
|
// Phase 5: Update published properties
|
||||||
let filtered = enrichedPeers.filter { p in
|
self.peers = enrichedPeers
|
||||||
p.isConnected || p.isReachable || p.isMutualFavorite
|
|
||||||
}
|
|
||||||
self.peers = filtered
|
|
||||||
self.connectedPeerIDs = connected
|
self.connectedPeerIDs = connected
|
||||||
self.favorites = favoritesList
|
self.favorites = favoritesList
|
||||||
self.mutualFavorites = mutualsList
|
self.mutualFavorites = mutualsList
|
||||||
@@ -167,26 +162,14 @@ class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
|||||||
|
|
||||||
private func buildPeerFromMesh(
|
private func buildPeerFromMesh(
|
||||||
peerInfo: TransportPeerSnapshot,
|
peerInfo: TransportPeerSnapshot,
|
||||||
favorites: [Data: FavoritesPersistenceService.FavoriteRelationship],
|
favorites: [Data: FavoritesPersistenceService.FavoriteRelationship]
|
||||||
meshAttached: Bool
|
|
||||||
) -> BitchatPeer {
|
) -> BitchatPeer {
|
||||||
// Determine reachability based on lastSeen and identity trust
|
|
||||||
let now = Date()
|
|
||||||
let fingerprint = peerInfo.noisePublicKey?.sha256Fingerprint()
|
|
||||||
let isVerified = fingerprint.map { SecureIdentityStateManager.shared.isVerified(fingerprint: $0) } ?? false
|
|
||||||
let isFav = peerInfo.noisePublicKey.flatMap { favorites[$0]?.isFavorite } ?? false
|
|
||||||
let retention: TimeInterval = (isVerified || isFav) ? TransportConfig.bleReachabilityRetentionVerifiedSeconds : TransportConfig.bleReachabilityRetentionUnverifiedSeconds
|
|
||||||
// A peer is reachable if we recently saw them AND we are attached to the mesh
|
|
||||||
let withinRetention = now.timeIntervalSince(peerInfo.lastSeen) <= retention
|
|
||||||
let isReachable = peerInfo.isConnected ? true : (withinRetention && meshAttached)
|
|
||||||
|
|
||||||
var peer = BitchatPeer(
|
var peer = BitchatPeer(
|
||||||
id: peerInfo.id,
|
id: peerInfo.id,
|
||||||
noisePublicKey: peerInfo.noisePublicKey ?? Data(),
|
noisePublicKey: peerInfo.noisePublicKey ?? Data(),
|
||||||
nickname: peerInfo.nickname,
|
nickname: peerInfo.nickname,
|
||||||
lastSeen: peerInfo.lastSeen,
|
lastSeen: peerInfo.lastSeen,
|
||||||
isConnected: peerInfo.isConnected,
|
isConnected: true
|
||||||
isReachable: isReachable
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check for favorite status
|
// Check for favorite status
|
||||||
@@ -233,8 +216,7 @@ class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
|||||||
noisePublicKey: favorite.peerNoisePublicKey,
|
noisePublicKey: favorite.peerNoisePublicKey,
|
||||||
nickname: favorite.peerNickname,
|
nickname: favorite.peerNickname,
|
||||||
lastSeen: favorite.lastUpdated,
|
lastSeen: favorite.lastUpdated,
|
||||||
isConnected: false,
|
isConnected: false
|
||||||
isReachable: false
|
|
||||||
)
|
)
|
||||||
|
|
||||||
peer.favoriteStatus = favorite
|
peer.favoriteStatus = favorite
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ final class VerificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Verify a scanned QR and return the parsed payload if valid (signature + freshness checks)
|
/// Verify a scanned QR and return the parsed payload if valid (signature + freshness checks)
|
||||||
func verifyScannedQR(_ urlString: String, maxAge: TimeInterval = TransportConfig.verificationQRMaxAgeSeconds) -> VerificationQR? {
|
func verifyScannedQR(_ urlString: String, maxAge: TimeInterval = 5 * 60) -> VerificationQR? {
|
||||||
guard let url = URL(string: urlString), let qr = VerificationQR.fromURL(url) else { return nil }
|
guard let url = URL(string: urlString), let qr = VerificationQR.fromURL(url) else { return nil }
|
||||||
// Freshness
|
// Freshness
|
||||||
let now = Date().timeIntervalSince1970
|
let now = Date().timeIntervalSince1970
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import Compression
|
|||||||
|
|
||||||
struct CompressionUtil {
|
struct CompressionUtil {
|
||||||
// Compression threshold - don't compress if data is smaller than this
|
// Compression threshold - don't compress if data is smaller than this
|
||||||
static let compressionThreshold = TransportConfig.compressionThresholdBytes // bytes
|
static let compressionThreshold = 100 // bytes
|
||||||
|
|
||||||
// Compress data using zlib algorithm (most compatible)
|
// Compress data using zlib algorithm (most compatible)
|
||||||
static func compress(_ data: Data) -> Data? {
|
static func compress(_ data: Data) -> Data? {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ final class MessageDeduplicator {
|
|||||||
private var entries: [Entry] = []
|
private var entries: [Entry] = []
|
||||||
private var lookup = Set<String>()
|
private var lookup = Set<String>()
|
||||||
private let lock = NSLock()
|
private let lock = NSLock()
|
||||||
private let maxAge: TimeInterval = TransportConfig.messageDedupMaxAgeSeconds // 5 minutes
|
private let maxAge: TimeInterval = 300 // 5 minutes
|
||||||
private let maxCount = TransportConfig.messageDedupMaxCount
|
private let maxCount = 1000
|
||||||
|
|
||||||
/// Check if message is duplicate and add if not
|
/// Check if message is duplicate and add if not
|
||||||
func isDuplicate(_ messageID: String) -> Bool {
|
func isDuplicate(_ messageID: String) -> Bool {
|
||||||
@@ -84,3 +84,4 @@ final class MessageDeduplicator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
/// Resolves a stable display name for peers, adding a short suffix when collisions exist.
|
|
||||||
struct PeerDisplayNameResolver {
|
|
||||||
/// Computes display names with a `#xxxx` suffix for connected peers when nickname collisions occur.
|
|
||||||
/// - Parameters:
|
|
||||||
/// - peers: Array of tuples (id, nickname, isConnected).
|
|
||||||
/// - selfNickname: The local user's current nickname, included in collision counts to suffix remotes matching it.
|
|
||||||
/// - Returns: Map of peerID -> displayName.
|
|
||||||
static func resolve(_ peers: [(id: String, nickname: String, isConnected: Bool)], selfNickname: String) -> [String: String] {
|
|
||||||
// Count collisions among connected peers and include our own nickname
|
|
||||||
var counts: [String: Int] = [:]
|
|
||||||
for p in peers where p.isConnected {
|
|
||||||
counts[p.nickname, default: 0] += 1
|
|
||||||
}
|
|
||||||
counts[selfNickname, default: 0] += 1
|
|
||||||
|
|
||||||
var result: [String: String] = [:]
|
|
||||||
for p in peers {
|
|
||||||
var name = p.nickname
|
|
||||||
if p.isConnected, (counts[p.nickname] ?? 0) > 1 {
|
|
||||||
name += "#" + String(p.id.prefix(4))
|
|
||||||
}
|
|
||||||
result[p.id] = name
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -145,10 +145,10 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
private var rateBucketsBySender: [String: TokenBucket] = [:]
|
private var rateBucketsBySender: [String: TokenBucket] = [:]
|
||||||
private var rateBucketsByContent: [String: TokenBucket] = [:]
|
private var rateBucketsByContent: [String: TokenBucket] = [:]
|
||||||
private let senderBucketCapacity: Double = TransportConfig.uiSenderRateBucketCapacity
|
private let senderBucketCapacity: Double = 5
|
||||||
private let senderBucketRefill: Double = TransportConfig.uiSenderRateBucketRefillPerSec // tokens per second
|
private let senderBucketRefill: Double = 1 // tokens per second
|
||||||
private let contentBucketCapacity: Double = TransportConfig.uiContentRateBucketCapacity
|
private let contentBucketCapacity: Double = 3
|
||||||
private let contentBucketRefill: Double = TransportConfig.uiContentRateBucketRefillPerSec // tokens per second
|
private let contentBucketRefill: Double = 0.5 // tokens per second
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func normalizedSenderKey(for message: BitchatMessage) -> String {
|
private func normalizedSenderKey(for message: BitchatMessage) -> String {
|
||||||
@@ -192,7 +192,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
if last < ns.length { simplified += ns.substring(with: NSRange(location: last, length: ns.length - last)) }
|
if last < ns.length { simplified += ns.substring(with: NSRange(location: last, length: ns.length - last)) }
|
||||||
let trimmed = simplified.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = simplified.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let collapsed = trimmed.replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression)
|
let collapsed = trimmed.replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression)
|
||||||
let prefix = String(collapsed.prefix(TransportConfig.contentKeyPrefixLength))
|
let prefix = String(collapsed.prefix(256))
|
||||||
// Fast djb2 hash
|
// Fast djb2 hash
|
||||||
let h = djb2(prefix)
|
let h = djb2(prefix)
|
||||||
return String(format: "h:%016llx", h)
|
return String(format: "h:%016llx", h)
|
||||||
@@ -201,7 +201,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Persistent recent content map (LRU) to speed near-duplicate checks
|
// Persistent recent content map (LRU) to speed near-duplicate checks
|
||||||
private var contentLRUMap: [String: Date] = [:]
|
private var contentLRUMap: [String: Date] = [:]
|
||||||
private var contentLRUOrder: [String] = []
|
private var contentLRUOrder: [String] = []
|
||||||
private let contentLRUCap = TransportConfig.contentLRUCap
|
private let contentLRUCap = 2000
|
||||||
private func recordContentKey(_ key: String, timestamp: Date) {
|
private func recordContentKey(_ key: String, timestamp: Date) {
|
||||||
if contentLRUMap[key] == nil { contentLRUOrder.append(key) }
|
if contentLRUMap[key] == nil { contentLRUOrder.append(key) }
|
||||||
contentLRUMap[key] = timestamp
|
contentLRUMap[key] = timestamp
|
||||||
@@ -219,13 +219,13 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
@Published var messages: [BitchatMessage] = []
|
@Published var messages: [BitchatMessage] = []
|
||||||
@Published var currentColorScheme: ColorScheme = .light
|
@Published var currentColorScheme: ColorScheme = .light
|
||||||
private let maxMessages = TransportConfig.meshTimelineCap // Maximum messages before oldest are removed
|
private let maxMessages = 1337 // Maximum messages before oldest are removed
|
||||||
@Published var isConnected = false
|
@Published var isConnected = false
|
||||||
private var hasNotifiedNetworkAvailable = false
|
private var hasNotifiedNetworkAvailable = false
|
||||||
private var recentlySeenPeers: Set<String> = []
|
private var recentlySeenPeers: Set<String> = []
|
||||||
private var lastNetworkNotificationTime = Date.distantPast
|
private var lastNetworkNotificationTime = Date.distantPast
|
||||||
private var networkResetTimer: Timer? = nil
|
private var networkResetTimer: Timer? = nil
|
||||||
private let networkResetGraceSeconds: TimeInterval = TransportConfig.networkResetGraceSeconds // avoid refiring on short drops/reconnects
|
private let networkResetGraceSeconds: TimeInterval = 600 // 10 minutes; avoid refiring on short drops/reconnects
|
||||||
@Published var nickname: String = "" {
|
@Published var nickname: String = "" {
|
||||||
didSet {
|
didSet {
|
||||||
// Trim whitespace whenever nickname is set
|
// Trim whitespace whenever nickname is set
|
||||||
@@ -349,7 +349,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// PeerManager replaced by UnifiedPeerService
|
// PeerManager replaced by UnifiedPeerService
|
||||||
private var processedNostrEvents = Set<String>() // Simple deduplication
|
private var processedNostrEvents = Set<String>() // Simple deduplication
|
||||||
private var processedNostrEventOrder: [String] = []
|
private var processedNostrEventOrder: [String] = []
|
||||||
private let maxProcessedNostrEvents = TransportConfig.uiProcessedNostrEventsCap
|
private let maxProcessedNostrEvents = 2000
|
||||||
private let userDefaults = UserDefaults.standard
|
private let userDefaults = UserDefaults.standard
|
||||||
private let nicknameKey = "bitchat.nickname"
|
private let nicknameKey = "bitchat.nickname"
|
||||||
// Location channel state (macOS supports manual geohash selection)
|
// Location channel state (macOS supports manual geohash selection)
|
||||||
@@ -386,14 +386,14 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Messages are naturally ephemeral - no persistent storage
|
// Messages are naturally ephemeral - no persistent storage
|
||||||
// Persist mesh public timeline across channel switches
|
// Persist mesh public timeline across channel switches
|
||||||
private var meshTimeline: [BitchatMessage] = []
|
private var meshTimeline: [BitchatMessage] = []
|
||||||
private let meshTimelineCap = TransportConfig.meshTimelineCap
|
private let meshTimelineCap = 1337
|
||||||
// Persist per-geohash public timelines across switches
|
// Persist per-geohash public timelines across switches
|
||||||
private var geoTimelines: [String: [BitchatMessage]] = [:] // geohash -> messages
|
private var geoTimelines: [String: [BitchatMessage]] = [:] // geohash -> messages
|
||||||
private let geoTimelineCap = TransportConfig.geoTimelineCap
|
private let geoTimelineCap = 1337
|
||||||
// Channel activity tracking for background nudges
|
// Channel activity tracking for background nudges
|
||||||
private var lastPublicActivityAt: [String: Date] = [:] // channelKey -> last activity time
|
private var lastPublicActivityAt: [String: Date] = [:] // channelKey -> last activity time
|
||||||
private var lastPublicActivityNotifyAt: [String: Date] = [:]
|
private var lastPublicActivityNotifyAt: [String: Date] = [:]
|
||||||
private let channelInactivityThreshold: TimeInterval = TransportConfig.uiChannelInactivityThresholdSeconds
|
private let channelInactivityThreshold: TimeInterval = 9 * 60
|
||||||
// Geohash participants (per geohash: pubkey -> lastSeen)
|
// Geohash participants (per geohash: pubkey -> lastSeen)
|
||||||
private var geoParticipants: [String: [String: Date]] = [:]
|
private var geoParticipants: [String: [String: Date]] = [:]
|
||||||
@Published private(set) var geohashPeople: [GeoPerson] = []
|
@Published private(set) var geohashPeople: [GeoPerson] = []
|
||||||
@@ -428,11 +428,11 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Buffer incoming public messages and flush in small batches to reduce UI invalidations
|
// Buffer incoming public messages and flush in small batches to reduce UI invalidations
|
||||||
private var publicBuffer: [BitchatMessage] = []
|
private var publicBuffer: [BitchatMessage] = []
|
||||||
private var publicBufferTimer: Timer? = nil
|
private var publicBufferTimer: Timer? = nil
|
||||||
private let basePublicFlushInterval: TimeInterval = TransportConfig.basePublicFlushInterval
|
private let basePublicFlushInterval: TimeInterval = 0.08 // ~12.5 fps batching
|
||||||
private var dynamicPublicFlushInterval: TimeInterval = TransportConfig.basePublicFlushInterval
|
private var dynamicPublicFlushInterval: TimeInterval = 0.08
|
||||||
private var recentBatchSizes: [Int] = []
|
private var recentBatchSizes: [Int] = []
|
||||||
@Published private(set) var isBatchingPublic: Bool = false
|
@Published private(set) var isBatchingPublic: Bool = false
|
||||||
private let lateInsertThreshold: TimeInterval = TransportConfig.uiLateInsertThreshold
|
private let lateInsertThreshold: TimeInterval = 15.0
|
||||||
|
|
||||||
// Track sent read receipts to avoid duplicates (persisted across launches)
|
// Track sent read receipts to avoid duplicates (persisted across launches)
|
||||||
// Note: Persistence happens automatically in didSet, no lifecycle observers needed
|
// Note: Persistence happens automatically in didSet, no lifecycle observers needed
|
||||||
@@ -441,9 +441,21 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Only persist if there are changes
|
// Only persist if there are changes
|
||||||
guard oldValue != sentReadReceipts else { return }
|
guard oldValue != sentReadReceipts else { return }
|
||||||
|
|
||||||
// Persist to UserDefaults whenever it changes (no manual synchronize/verify re-read)
|
// Persist to UserDefaults whenever it changes
|
||||||
if let data = try? JSONEncoder().encode(Array(sentReadReceipts)) {
|
if let data = try? JSONEncoder().encode(Array(sentReadReceipts)) {
|
||||||
UserDefaults.standard.set(data, forKey: "sentReadReceipts")
|
UserDefaults.standard.set(data, forKey: "sentReadReceipts")
|
||||||
|
// Force synchronization for immediate persistence (ensures data is written to disk)
|
||||||
|
UserDefaults.standard.synchronize()
|
||||||
|
|
||||||
|
// Verify persistence by re-reading
|
||||||
|
if let verifyData = UserDefaults.standard.data(forKey: "sentReadReceipts"),
|
||||||
|
let _ = try? JSONDecoder().decode([String].self, from: verifyData) {
|
||||||
|
// Only log errors, not successful persistence
|
||||||
|
// Successfully persisted
|
||||||
|
} else {
|
||||||
|
SecureLogger.log("⚠️ Failed to verify persistence of read receipts",
|
||||||
|
category: SecureLogger.session, level: .error)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
SecureLogger.log("❌ Failed to encode read receipts for persistence",
|
SecureLogger.log("❌ Failed to encode read receipts for persistence",
|
||||||
category: SecureLogger.session, level: .error)
|
category: SecureLogger.session, level: .error)
|
||||||
@@ -506,7 +518,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Log startup info
|
// Log startup info
|
||||||
|
|
||||||
// Log fingerprint after a delay to ensure encryption service is ready
|
// Log fingerprint after a delay to ensure encryption service is ready
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + TransportConfig.uiStartupInitialDelaySeconds) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
|
||||||
if let self = self {
|
if let self = self {
|
||||||
_ = self.getMyFingerprint()
|
_ = self.getMyFingerprint()
|
||||||
}
|
}
|
||||||
@@ -526,7 +538,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
// Small delay to ensure read receipts are fully loaded
|
// Small delay to ensure read receipts are fully loaded
|
||||||
// This prevents race conditions where messages arrive before initialization completes
|
// This prevents race conditions where messages arrive before initialization completes
|
||||||
try? await Task.sleep(nanoseconds: TransportConfig.uiStartupShortSleepNs) // 0.2 seconds
|
try? await Task.sleep(nanoseconds: 200_000_000) // 0.2 seconds
|
||||||
|
|
||||||
// Set up Nostr message handling directly
|
// Set up Nostr message handling directly
|
||||||
setupNostrMessageHandling()
|
setupNostrMessageHandling()
|
||||||
@@ -539,7 +551,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// 1. Skip cleanup of read receipts
|
// 1. Skip cleanup of read receipts
|
||||||
// 2. Only block OLD messages from being marked as unread
|
// 2. Only block OLD messages from being marked as unread
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
try? await Task.sleep(nanoseconds: UInt64(TransportConfig.uiStartupPhaseDurationSeconds * 1_000_000_000)) // 2 seconds
|
try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds
|
||||||
self.isStartupPhase = false
|
self.isStartupPhase = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -691,7 +703,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// MARK: - Deinitialization
|
// MARK: - Deinitialization
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
// No need to force UserDefaults synchronization
|
// Force immediate save
|
||||||
|
userDefaults.synchronize()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resubscribe to the active geohash channel without clearing timeline
|
// Resubscribe to the active geohash channel without clearing timeline
|
||||||
@@ -707,15 +720,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
startGeoParticipantsTimer()
|
startGeoParticipantsTimer()
|
||||||
// Unsubscribe + resubscribe
|
// Unsubscribe + resubscribe
|
||||||
NostrRelayManager.shared.unsubscribe(id: subID)
|
NostrRelayManager.shared.unsubscribe(id: subID)
|
||||||
let filter = NostrFilter.geohashEphemeral(
|
let filter = NostrFilter.geohashEphemeral(ch.geohash, since: Date().addingTimeInterval(-3600), limit: 200)
|
||||||
ch.geohash,
|
let subRelays = GeoRelayDirectory.shared.closestRelays(toGeohash: ch.geohash, count: 5)
|
||||||
since: Date().addingTimeInterval(-TransportConfig.nostrGeohashInitialLookbackSeconds),
|
|
||||||
limit: TransportConfig.nostrGeohashInitialLimit
|
|
||||||
)
|
|
||||||
let subRelays = GeoRelayDirectory.shared.closestRelays(
|
|
||||||
toGeohash: ch.geohash,
|
|
||||||
count: TransportConfig.nostrGeoRelayCount
|
|
||||||
)
|
|
||||||
NostrRelayManager.shared.subscribe(filter: filter, id: subID, relayUrls: subRelays) { [weak self] event in
|
NostrRelayManager.shared.subscribe(filter: filter, id: subID, relayUrls: subRelays) { [weak self] event in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
guard event.kind == NostrProtocol.EventKind.ephemeralEvent.rawValue else { return }
|
guard event.kind == NostrProtocol.EventKind.ephemeralEvent.rawValue else { return }
|
||||||
@@ -733,9 +739,9 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
self.geoNicknames[event.pubkey.lowercased()] = nick
|
self.geoNicknames[event.pubkey.lowercased()] = nick
|
||||||
}
|
}
|
||||||
// Store mapping for geohash sender IDs used in messages (ensures consistent colors)
|
// Store mapping for geohash sender IDs used in messages (ensures consistent colors)
|
||||||
let key16 = "nostr_" + String(event.pubkey.prefix(TransportConfig.nostrConvKeyPrefixLength))
|
let key16 = "nostr_" + String(event.pubkey.prefix(16))
|
||||||
self.nostrKeyMapping[key16] = event.pubkey
|
self.nostrKeyMapping[key16] = event.pubkey
|
||||||
let key8 = "nostr:" + String(event.pubkey.prefix(TransportConfig.nostrShortKeyDisplayLength))
|
let key8 = "nostr:" + String(event.pubkey.prefix(8))
|
||||||
self.nostrKeyMapping[key8] = event.pubkey
|
self.nostrKeyMapping[key8] = event.pubkey
|
||||||
|
|
||||||
// Update participants last-seen for this pubkey
|
// Update participants last-seen for this pubkey
|
||||||
@@ -762,7 +768,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
originalSender: nil,
|
originalSender: nil,
|
||||||
isPrivate: false,
|
isPrivate: false,
|
||||||
recipientNickname: nil,
|
recipientNickname: nil,
|
||||||
senderPeerID: "nostr:\(event.pubkey.prefix(TransportConfig.nostrShortKeyDisplayLength))",
|
senderPeerID: "nostr:\(event.pubkey.prefix(8))",
|
||||||
mentions: mentions.isEmpty ? nil : mentions
|
mentions: mentions.isEmpty ? nil : mentions
|
||||||
)
|
)
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
@@ -777,7 +783,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
let id = try NostrIdentityBridge.deriveIdentity(forGeohash: ch.geohash)
|
let id = try NostrIdentityBridge.deriveIdentity(forGeohash: ch.geohash)
|
||||||
let dmSub = "geo-dm-\(ch.geohash)"
|
let dmSub = "geo-dm-\(ch.geohash)"
|
||||||
geoDmSubscriptionID = dmSub
|
geoDmSubscriptionID = dmSub
|
||||||
let dmFilter = NostrFilter.giftWrapsFor(pubkey: id.publicKeyHex, since: Date().addingTimeInterval(-TransportConfig.nostrDMSubscribeLookbackSeconds))
|
let dmFilter = NostrFilter.giftWrapsFor(pubkey: id.publicKeyHex, since: Date().addingTimeInterval(-86400))
|
||||||
NostrRelayManager.shared.subscribe(filter: dmFilter, id: dmSub) { [weak self] giftWrap in
|
NostrRelayManager.shared.subscribe(filter: dmFilter, id: dmSub) { [weak self] giftWrap in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
if self.processedNostrEvents.contains(giftWrap.id) { return }
|
if self.processedNostrEvents.contains(giftWrap.id) { return }
|
||||||
@@ -789,7 +795,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
guard packet.type == MessageType.noiseEncrypted.rawValue else { return }
|
guard packet.type == MessageType.noiseEncrypted.rawValue else { return }
|
||||||
guard let noisePayload = NoisePayload.decode(packet.payload) else { return }
|
guard let noisePayload = NoisePayload.decode(packet.payload) else { return }
|
||||||
let messageTimestamp = Date(timeIntervalSince1970: TimeInterval(rumorTs))
|
let messageTimestamp = Date(timeIntervalSince1970: TimeInterval(rumorTs))
|
||||||
let convKey = "nostr_" + String(senderPubkey.prefix(TransportConfig.nostrConvKeyPrefixLength))
|
let convKey = "nostr_" + String(senderPubkey.prefix(16))
|
||||||
self.nostrKeyMapping[convKey] = senderPubkey
|
self.nostrKeyMapping[convKey] = senderPubkey
|
||||||
switch noisePayload.type {
|
switch noisePayload.type {
|
||||||
case .privateMessage:
|
case .privateMessage:
|
||||||
@@ -897,7 +903,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
func saveNickname() {
|
func saveNickname() {
|
||||||
userDefaults.set(nickname, forKey: nicknameKey)
|
userDefaults.set(nickname, forKey: nicknameKey)
|
||||||
// Persist nickname; no need to force synchronize
|
userDefaults.synchronize() // Force immediate save
|
||||||
|
|
||||||
// Send announce with new nickname to all peers
|
// Send announce with new nickname to all peers
|
||||||
meshService.sendBroadcastAnnounce()
|
meshService.sendBroadcastAnnounce()
|
||||||
@@ -935,13 +941,6 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
if unreadPrivateMessages.contains(noiseKeyHex) {
|
if unreadPrivateMessages.contains(noiseKeyHex) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// Also check for geohash (Nostr) DM conv key if this peer has a known Nostr pubkey
|
|
||||||
if let nostrHex = peer.nostrPublicKey {
|
|
||||||
let convKey = "nostr_" + String(nostrHex.prefix(TransportConfig.nostrConvKeyPrefixLength))
|
|
||||||
if unreadPrivateMessages.contains(convKey) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the peer's nickname to check for temporary Nostr peer IDs
|
// Get the peer's nickname to check for temporary Nostr peer IDs
|
||||||
@@ -1096,7 +1095,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
chatFingerprint == fingerprintStr {
|
chatFingerprint == fingerprintStr {
|
||||||
// Send read receipts for any unread messages from this peer
|
// Send read receipts for any unread messages from this peer
|
||||||
// Use a small delay to ensure the connection is fully established
|
// Use a small delay to ensure the connection is fully established
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + TransportConfig.uiReadReceiptRetryLongSeconds) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||||
self?.markPrivateMessagesAsRead(from: peerID)
|
self?.markPrivateMessagesAsRead(from: peerID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1222,7 +1221,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
let myGeoIdentity = try? NostrIdentityBridge.deriveIdentity(forGeohash: ch.geohash) {
|
let myGeoIdentity = try? NostrIdentityBridge.deriveIdentity(forGeohash: ch.geohash) {
|
||||||
let suffix = String(myGeoIdentity.publicKeyHex.suffix(4))
|
let suffix = String(myGeoIdentity.publicKeyHex.suffix(4))
|
||||||
displaySender = nickname + "#" + suffix
|
displaySender = nickname + "#" + suffix
|
||||||
localSenderPeerID = "nostr:\(myGeoIdentity.publicKeyHex.prefix(TransportConfig.nostrShortKeyDisplayLength))"
|
localSenderPeerID = "nostr:\(myGeoIdentity.publicKeyHex.prefix(8))"
|
||||||
}
|
}
|
||||||
|
|
||||||
let message = BitchatMessage(
|
let message = BitchatMessage(
|
||||||
@@ -1278,10 +1277,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
nickname: self.nickname,
|
nickname: self.nickname,
|
||||||
teleported: LocationChannelManager.shared.teleported
|
teleported: LocationChannelManager.shared.teleported
|
||||||
)
|
)
|
||||||
let targetRelays = GeoRelayDirectory.shared.closestRelays(
|
let targetRelays = GeoRelayDirectory.shared.closestRelays(toGeohash: ch.geohash, count: 5)
|
||||||
toGeohash: ch.geohash,
|
|
||||||
count: TransportConfig.nostrGeoRelayCount
|
|
||||||
)
|
|
||||||
if targetRelays.isEmpty {
|
if targetRelays.isEmpty {
|
||||||
SecureLogger.log("Geo: no geohash relays available for \(ch.geohash); not sending", category: SecureLogger.session, level: .warning)
|
SecureLogger.log("Geo: no geohash relays available for \(ch.geohash); not sending", category: SecureLogger.session, level: .warning)
|
||||||
} else {
|
} else {
|
||||||
@@ -1363,11 +1359,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
let subID = "geo-\(ch.geohash)"
|
let subID = "geo-\(ch.geohash)"
|
||||||
geoSubscriptionID = subID
|
geoSubscriptionID = subID
|
||||||
startGeoParticipantsTimer()
|
startGeoParticipantsTimer()
|
||||||
let filter = NostrFilter.geohashEphemeral(
|
let filter = NostrFilter.geohashEphemeral(ch.geohash, since: Date().addingTimeInterval(-3600), limit: 200)
|
||||||
ch.geohash,
|
|
||||||
since: Date().addingTimeInterval(-TransportConfig.nostrGeohashInitialLookbackSeconds),
|
|
||||||
limit: TransportConfig.nostrGeohashInitialLimit
|
|
||||||
)
|
|
||||||
let subRelays = GeoRelayDirectory.shared.closestRelays(toGeohash: ch.geohash, count: 5)
|
let subRelays = GeoRelayDirectory.shared.closestRelays(toGeohash: ch.geohash, count: 5)
|
||||||
NostrRelayManager.shared.subscribe(filter: filter, id: subID, relayUrls: subRelays) { [weak self] event in
|
NostrRelayManager.shared.subscribe(filter: filter, id: subID, relayUrls: subRelays) { [weak self] event in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
@@ -1409,9 +1401,9 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Store mapping for geohash DM initiation
|
// Store mapping for geohash DM initiation
|
||||||
let key16 = "nostr_" + String(event.pubkey.prefix(TransportConfig.nostrConvKeyPrefixLength))
|
let key16 = "nostr_" + String(event.pubkey.prefix(16))
|
||||||
self.nostrKeyMapping[key16] = event.pubkey
|
self.nostrKeyMapping[key16] = event.pubkey
|
||||||
let key8 = "nostr:" + String(event.pubkey.prefix(TransportConfig.nostrShortKeyDisplayLength))
|
let key8 = "nostr:" + String(event.pubkey.prefix(8))
|
||||||
self.nostrKeyMapping[key8] = event.pubkey
|
self.nostrKeyMapping[key8] = event.pubkey
|
||||||
// Update participants last-seen for this pubkey
|
// Update participants last-seen for this pubkey
|
||||||
self.recordGeoParticipant(pubkeyHex: event.pubkey)
|
self.recordGeoParticipant(pubkeyHex: event.pubkey)
|
||||||
@@ -1434,7 +1426,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
originalSender: nil,
|
originalSender: nil,
|
||||||
isPrivate: false,
|
isPrivate: false,
|
||||||
recipientNickname: nil,
|
recipientNickname: nil,
|
||||||
senderPeerID: "nostr:\(event.pubkey.prefix(TransportConfig.nostrShortKeyDisplayLength))",
|
senderPeerID: "nostr:\(event.pubkey.prefix(8))",
|
||||||
mentions: mentions.isEmpty ? nil : mentions
|
mentions: mentions.isEmpty ? nil : mentions
|
||||||
)
|
)
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
@@ -1452,7 +1444,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// pared back logging: subscribe debug only
|
// pared back logging: subscribe debug only
|
||||||
SecureLogger.log("GeoDM: subscribing DMs pub=\(id.publicKeyHex.prefix(8))… sub=\(dmSub)",
|
SecureLogger.log("GeoDM: subscribing DMs pub=\(id.publicKeyHex.prefix(8))… sub=\(dmSub)",
|
||||||
category: SecureLogger.session, level: .debug)
|
category: SecureLogger.session, level: .debug)
|
||||||
let dmFilter = NostrFilter.giftWrapsFor(pubkey: id.publicKeyHex, since: Date().addingTimeInterval(-TransportConfig.nostrDMSubscribeLookbackSeconds))
|
let dmFilter = NostrFilter.giftWrapsFor(pubkey: id.publicKeyHex, since: Date().addingTimeInterval(-86400))
|
||||||
NostrRelayManager.shared.subscribe(filter: dmFilter, id: dmSub) { [weak self] giftWrap in
|
NostrRelayManager.shared.subscribe(filter: dmFilter, id: dmSub) { [weak self] giftWrap in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
// Dedup basic
|
// Dedup basic
|
||||||
@@ -1601,7 +1593,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
private func refreshGeohashPeople() {
|
private func refreshGeohashPeople() {
|
||||||
guard let gh = currentGeohash else { geohashPeople = []; return }
|
guard let gh = currentGeohash else { geohashPeople = []; return }
|
||||||
let cutoff = Date().addingTimeInterval(-TransportConfig.uiRecentCutoffFiveMinutesSeconds)
|
let cutoff = Date().addingTimeInterval(-5 * 60)
|
||||||
var map = geoParticipants[gh] ?? [:]
|
var map = geoParticipants[gh] ?? [:]
|
||||||
// Prune expired entries
|
// Prune expired entries
|
||||||
map = map.filter { $0.value >= cutoff }
|
map = map.filter { $0.value >= cutoff }
|
||||||
@@ -1636,7 +1628,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
@MainActor
|
@MainActor
|
||||||
func visibleGeohashPeople() -> [GeoPerson] {
|
func visibleGeohashPeople() -> [GeoPerson] {
|
||||||
guard let gh = currentGeohash else { return [] }
|
guard let gh = currentGeohash else { return [] }
|
||||||
let cutoff = Date().addingTimeInterval(-TransportConfig.uiRecentCutoffFiveMinutesSeconds)
|
let cutoff = Date().addingTimeInterval(-5 * 60)
|
||||||
let map = (geoParticipants[gh] ?? [:])
|
let map = (geoParticipants[gh] ?? [:])
|
||||||
.filter { $0.value >= cutoff }
|
.filter { $0.value >= cutoff }
|
||||||
.filter { !SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: $0.key) }
|
.filter { !SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: $0.key) }
|
||||||
@@ -1648,7 +1640,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
/// Returns the current participant count for a specific geohash, using the 5-minute activity window.
|
/// Returns the current participant count for a specific geohash, using the 5-minute activity window.
|
||||||
@MainActor
|
@MainActor
|
||||||
func geohashParticipantCount(for geohash: String) -> Int {
|
func geohashParticipantCount(for geohash: String) -> Int {
|
||||||
let cutoff = Date().addingTimeInterval(-TransportConfig.uiRecentCutoffFiveMinutesSeconds)
|
let cutoff = Date().addingTimeInterval(-5 * 60)
|
||||||
let map = geoParticipants[geohash] ?? [:]
|
let map = geoParticipants[geohash] ?? [:]
|
||||||
return map.values.filter { $0 >= cutoff }.count
|
return map.values.filter { $0 >= cutoff }.count
|
||||||
}
|
}
|
||||||
@@ -1696,7 +1688,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove geohash DM conversation if exists
|
// Remove geohash DM conversation if exists
|
||||||
let convKey = "nostr_" + String(hex.prefix(TransportConfig.nostrConvKeyPrefixLength))
|
let convKey = "nostr_" + String(hex.prefix(16))
|
||||||
if privateChats[convKey] != nil {
|
if privateChats[convKey] != nil {
|
||||||
privateChats.removeValue(forKey: convKey)
|
privateChats.removeValue(forKey: convKey)
|
||||||
unreadPrivateMessages.remove(convKey)
|
unreadPrivateMessages.remove(convKey)
|
||||||
@@ -1732,11 +1724,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
for gh in toAdd {
|
for gh in toAdd {
|
||||||
let subID = "geo-sample-\(gh)"
|
let subID = "geo-sample-\(gh)"
|
||||||
geoSamplingSubs[subID] = gh
|
geoSamplingSubs[subID] = gh
|
||||||
let filter = NostrFilter.geohashEphemeral(
|
let filter = NostrFilter.geohashEphemeral(gh, since: Date().addingTimeInterval(-300), limit: 100)
|
||||||
gh,
|
|
||||||
since: Date().addingTimeInterval(-TransportConfig.nostrGeohashSampleLookbackSeconds),
|
|
||||||
limit: TransportConfig.nostrGeohashSampleLimit
|
|
||||||
)
|
|
||||||
let subRelays = GeoRelayDirectory.shared.closestRelays(toGeohash: gh, count: 5)
|
let subRelays = GeoRelayDirectory.shared.closestRelays(toGeohash: gh, count: 5)
|
||||||
NostrRelayManager.shared.subscribe(filter: filter, id: subID, relayUrls: subRelays) { [weak self] event in
|
NostrRelayManager.shared.subscribe(filter: filter, id: subID, relayUrls: subRelays) { [weak self] event in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
@@ -1881,7 +1869,6 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Determine routing method and recipient nickname
|
// Determine routing method and recipient nickname
|
||||||
guard let noiseKey = Data(hexString: peerID) else { return }
|
guard let noiseKey = Data(hexString: peerID) else { return }
|
||||||
let isConnected = meshService.isPeerConnected(peerID)
|
let isConnected = meshService.isPeerConnected(peerID)
|
||||||
let isReachable = meshService.isPeerReachable(peerID)
|
|
||||||
let favoriteStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey)
|
let favoriteStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey)
|
||||||
let isMutualFavorite = favoriteStatus?.isMutual ?? false
|
let isMutualFavorite = favoriteStatus?.isMutual ?? false
|
||||||
let hasNostrKey = favoriteStatus?.peerNostrPublicKey != nil
|
let hasNostrKey = favoriteStatus?.peerNostrPublicKey != nil
|
||||||
@@ -1921,8 +1908,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Trigger UI update for sent message
|
// Trigger UI update for sent message
|
||||||
objectWillChange.send()
|
objectWillChange.send()
|
||||||
|
|
||||||
// Send via appropriate transport (BLE if connected/reachable, else Nostr when possible)
|
// Send via appropriate transport (BLE if connected, else Nostr when possible)
|
||||||
if isConnected || isReachable || (isMutualFavorite && hasNostrKey) {
|
if isConnected || (isMutualFavorite && hasNostrKey) {
|
||||||
messageRouter.sendPrivate(content, to: peerID, recipientNickname: recipientNickname ?? "user", messageID: messageID)
|
messageRouter.sendPrivate(content, to: peerID, recipientNickname: recipientNickname ?? "user", messageID: messageID)
|
||||||
// Optimistically mark as sent for both transports; delivery/read will update subsequently
|
// Optimistically mark as sent for both transports; delivery/read will update subsequently
|
||||||
if let idx = privateChats[peerID]?.firstIndex(where: { $0.id == messageID }) {
|
if let idx = privateChats[peerID]?.firstIndex(where: { $0.id == messageID }) {
|
||||||
@@ -1940,7 +1927,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// MARK: - Geohash DMs initiation
|
// MARK: - Geohash DMs initiation
|
||||||
@MainActor
|
@MainActor
|
||||||
func startGeohashDM(withPubkeyHex hex: String) {
|
func startGeohashDM(withPubkeyHex hex: String) {
|
||||||
let convKey = "nostr_" + String(hex.prefix(TransportConfig.nostrConvKeyPrefixLength))
|
let convKey = "nostr_" + String(hex.prefix(16))
|
||||||
nostrKeyMapping[convKey] = hex
|
nostrKeyMapping[convKey] = hex
|
||||||
selectedPrivateChatPeer = convKey
|
selectedPrivateChatPeer = convKey
|
||||||
}
|
}
|
||||||
@@ -2410,7 +2397,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Try immediately
|
// Try immediately
|
||||||
self.markPrivateMessagesAsRead(from: peerID)
|
self.markPrivateMessagesAsRead(from: peerID)
|
||||||
// And again with a delay
|
// And again with a delay
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + TransportConfig.uiAnimationMediumSeconds) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
||||||
self.markPrivateMessagesAsRead(from: peerID)
|
self.markPrivateMessagesAsRead(from: peerID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2476,7 +2463,12 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
)
|
)
|
||||||
let targetRelays = GeoRelayDirectory.shared.closestRelays(toGeohash: ch.geohash, count: 5)
|
let targetRelays = GeoRelayDirectory.shared.closestRelays(toGeohash: ch.geohash, count: 5)
|
||||||
if targetRelays.isEmpty {
|
if targetRelays.isEmpty {
|
||||||
SecureLogger.log("Geo: no geohash relays available for \(ch.geohash); not sending", category: SecureLogger.session, level: .warning)
|
let targetRelays = GeoRelayDirectory.shared.closestRelays(toGeohash: ch.geohash, count: 5)
|
||||||
|
if targetRelays.isEmpty {
|
||||||
|
SecureLogger.log("Geo: no geohash relays available for \(ch.geohash); not sending", category: SecureLogger.session, level: .warning)
|
||||||
|
} else {
|
||||||
|
NostrRelayManager.shared.sendEvent(event, to: targetRelays)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
NostrRelayManager.shared.sendEvent(event, to: targetRelays)
|
NostrRelayManager.shared.sendEvent(event, to: targetRelays)
|
||||||
}
|
}
|
||||||
@@ -2503,7 +2495,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc private func appWillResignActive() {
|
@objc private func appWillResignActive() {
|
||||||
// No-op; avoid forcing synchronize on resign
|
userDefaults.synchronize()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func applicationWillTerminate() {
|
@objc func applicationWillTerminate() {
|
||||||
@@ -2516,14 +2508,15 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Verify identity key is still there
|
// Verify identity key is still there
|
||||||
_ = KeychainManager.shared.verifyIdentityKeyExists()
|
_ = KeychainManager.shared.verifyIdentityKeyExists()
|
||||||
|
|
||||||
// No need to force synchronize here
|
userDefaults.synchronize()
|
||||||
|
|
||||||
// Verify identity key after save
|
// Verify identity key after save
|
||||||
_ = KeychainManager.shared.verifyIdentityKeyExists()
|
_ = KeychainManager.shared.verifyIdentityKeyExists()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func appWillTerminate() {
|
@objc private func appWillTerminate() {
|
||||||
// No need to force synchronize here
|
|
||||||
|
userDefaults.synchronize()
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -2550,12 +2543,16 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this originated over Nostr, skip (handled by Nostr code paths)
|
// If we know the original transport, use it for the read receipt
|
||||||
if originalTransport == "nostr" {
|
if originalTransport == "nostr" {
|
||||||
return
|
// Skip read receipts for Nostr messages - unnecessary complexity
|
||||||
|
// The radical simplification plan says to accept occasional loss
|
||||||
|
} else if meshService.peerNickname(peerID: actualPeerID) != nil {
|
||||||
|
// Use mesh for connected peers (default behavior)
|
||||||
|
messageRouter.sendReadReceipt(receipt, to: actualPeerID)
|
||||||
|
} else {
|
||||||
|
// Skip read receipts for offline peers - fire and forget principle
|
||||||
}
|
}
|
||||||
// Use router to decide (mesh if reachable, else Nostr if available)
|
|
||||||
messageRouter.sendReadReceipt(receipt, to: actualPeerID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -2685,7 +2682,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}().lowercased()
|
}().lowercased()
|
||||||
// Try exact match against cached geoNicknames (pubkey -> nickname)
|
// Try exact match against cached geoNicknames (pubkey -> nickname)
|
||||||
if let pub = geoNicknames.first(where: { (_, nick) in nick.lowercased() == base })?.key {
|
if let pub = geoNicknames.first(where: { (_, nick) in nick.lowercased() == base })?.key {
|
||||||
let convKey = "nostr_" + String(pub.prefix(TransportConfig.nostrConvKeyPrefixLength))
|
let convKey = "nostr_" + String(pub.prefix(16))
|
||||||
nostrKeyMapping[convKey] = pub
|
nostrKeyMapping[convKey] = pub
|
||||||
return convKey
|
return convKey
|
||||||
}
|
}
|
||||||
@@ -2764,13 +2761,14 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// This will force creation of a new identity (new fingerprint) on next launch
|
// This will force creation of a new identity (new fingerprint) on next launch
|
||||||
meshService.emergencyDisconnectAll()
|
meshService.emergencyDisconnectAll()
|
||||||
|
|
||||||
// No need to force UserDefaults synchronization
|
// Force immediate UserDefaults synchronization
|
||||||
|
userDefaults.synchronize()
|
||||||
|
|
||||||
// Reinitialize Nostr with new identity
|
// Reinitialize Nostr with new identity
|
||||||
// This will generate new Nostr keys derived from new Noise keys
|
// This will generate new Nostr keys derived from new Noise keys
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
// Small delay to ensure cleanup completes
|
// Small delay to ensure cleanup completes
|
||||||
try? await Task.sleep(nanoseconds: TransportConfig.uiAsyncShortSleepNs) // 0.1 seconds
|
try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
|
||||||
|
|
||||||
// Reinitialize Nostr relay manager with new identity
|
// Reinitialize Nostr relay manager with new identity
|
||||||
nostrRelayManager = NostrRelayManager()
|
nostrRelayManager = NostrRelayManager()
|
||||||
@@ -2940,7 +2938,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// In geohash channels, compare against our per-geohash nostr short ID
|
// In geohash channels, compare against our per-geohash nostr short ID
|
||||||
if case .location(let ch) = activeChannel, spid.hasPrefix("nostr:") {
|
if case .location(let ch) = activeChannel, spid.hasPrefix("nostr:") {
|
||||||
if let myGeo = try? NostrIdentityBridge.deriveIdentity(forGeohash: ch.geohash) {
|
if let myGeo = try? NostrIdentityBridge.deriveIdentity(forGeohash: ch.geohash) {
|
||||||
return spid == "nostr:\(myGeo.publicKeyHex.prefix(TransportConfig.nostrShortKeyDisplayLength))"
|
return spid == "nostr:\(myGeo.publicKeyHex.prefix(8))"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return spid == meshService.myPeerID
|
return spid == meshService.myPeerID
|
||||||
@@ -3491,9 +3489,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
var hue = Double(djb2(seed) % 360) / 360.0
|
var hue = Double(djb2(seed) % 360) / 360.0
|
||||||
// Avoid orange (~30°) reserved for self
|
// Avoid orange (~30°) reserved for self
|
||||||
let orange = 30.0 / 360.0
|
let orange = 30.0 / 360.0
|
||||||
if abs(hue - orange) < TransportConfig.uiColorHueAvoidanceDelta {
|
if abs(hue - orange) < 0.05 { hue = fmod(hue + 0.12, 1.0) }
|
||||||
hue = fmod(hue + TransportConfig.uiColorHueOffset, 1.0)
|
|
||||||
}
|
|
||||||
let saturation: Double = isDark ? 0.80 : 0.70
|
let saturation: Double = isDark ? 0.80 : 0.70
|
||||||
let brightness: Double = isDark ? 0.75 : 0.45
|
let brightness: Double = isDark ? 0.75 : 0.45
|
||||||
let c = Color(hue: hue, saturation: saturation, brightness: brightness)
|
let c = Color(hue: hue, saturation: saturation, brightness: brightness)
|
||||||
@@ -3703,7 +3699,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Load verified fingerprints directly from secure storage
|
// Load verified fingerprints directly from secure storage
|
||||||
verifiedFingerprints = SecureIdentityStateManager.shared.getVerifiedFingerprints()
|
verifiedFingerprints = SecureIdentityStateManager.shared.getVerifiedFingerprints()
|
||||||
// Log snapshot for debugging persistence
|
// Log snapshot for debugging persistence
|
||||||
let sample = Array(verifiedFingerprints.prefix(TransportConfig.uiFingerprintSampleCount)).map { $0.prefix(8) }.joined(separator: ", ")
|
let sample = Array(verifiedFingerprints.prefix(3)).map { $0.prefix(8) }.joined(separator: ", ")
|
||||||
SecureLogger.log("🔐 Verified loaded: \(verifiedFingerprints.count) [\(sample)]", category: SecureLogger.security, level: .info)
|
SecureLogger.log("🔐 Verified loaded: \(verifiedFingerprints.count) [\(sample)]", category: SecureLogger.security, level: .info)
|
||||||
// Also log any offline favorites and whether we consider them verified
|
// Also log any offline favorites and whether we consider them verified
|
||||||
let offlineFavorites = unifiedPeerService.favorites.filter { !$0.isConnected }
|
let offlineFavorites = unifiedPeerService.favorites.filter { !$0.isConnected }
|
||||||
@@ -4001,7 +3997,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
let favoriteStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: peer.noisePublicKey),
|
let favoriteStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: peer.noisePublicKey),
|
||||||
favoriteStatus.isFavorite {
|
favoriteStatus.isFavorite {
|
||||||
// Resend favorite notification with our Nostr key after a short delay
|
// Resend favorite notification with our Nostr key after a short delay
|
||||||
try? await Task.sleep(nanoseconds: TransportConfig.uiAsyncMediumSleepNs) // 0.5 seconds
|
try? await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds
|
||||||
meshService.sendFavoriteNotification(to: peerID, isFavorite: true)
|
meshService.sendFavoriteNotification(to: peerID, isFavorite: true)
|
||||||
SecureLogger.log("📤 Resent favorite notification to reconnected peer \(peerID)",
|
SecureLogger.log("📤 Resent favorite notification to reconnected peer \(peerID)",
|
||||||
category: SecureLogger.session, level: .debug)
|
category: SecureLogger.session, level: .debug)
|
||||||
@@ -4415,7 +4411,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Subscribe to Nostr messages
|
// Subscribe to Nostr messages
|
||||||
let filter = NostrFilter.giftWrapsFor(
|
let filter = NostrFilter.giftWrapsFor(
|
||||||
pubkey: currentIdentity.publicKeyHex,
|
pubkey: currentIdentity.publicKeyHex,
|
||||||
since: Date().addingTimeInterval(-TransportConfig.nostrDMSubscribeLookbackSeconds) // Last 24 hours
|
since: Date().addingTimeInterval(-86400) // Last 24 hours
|
||||||
)
|
)
|
||||||
|
|
||||||
nostrRelayManager?.subscribe(filter: filter, id: "chat-messages") { [weak self] event in
|
nostrRelayManager?.subscribe(filter: filter, id: "chat-messages") { [weak self] event in
|
||||||
@@ -4486,7 +4482,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
let messageTimestamp = Date(timeIntervalSince1970: TimeInterval(rumorTimestamp))
|
let messageTimestamp = Date(timeIntervalSince1970: TimeInterval(rumorTimestamp))
|
||||||
let senderNickname = (actualSenderNoiseKey != nil) ? (FavoritesPersistenceService.shared.getFavoriteStatus(for: actualSenderNoiseKey!)?.peerNickname ?? "Unknown") : "Unknown"
|
let senderNickname = (actualSenderNoiseKey != nil) ? (FavoritesPersistenceService.shared.getFavoriteStatus(for: actualSenderNoiseKey!)?.peerNickname ?? "Unknown") : "Unknown"
|
||||||
// Stable target ID if we know Noise key; otherwise temporary Nostr-based peer
|
// Stable target ID if we know Noise key; otherwise temporary Nostr-based peer
|
||||||
let targetPeerID = actualSenderNoiseKey?.hexEncodedString() ?? ("nostr_" + senderPubkey.prefix(TransportConfig.nostrConvKeyPrefixLength))
|
let targetPeerID = actualSenderNoiseKey?.hexEncodedString() ?? ("nostr_" + senderPubkey.prefix(16))
|
||||||
|
|
||||||
switch noisePayload.type {
|
switch noisePayload.type {
|
||||||
case .privateMessage:
|
case .privateMessage:
|
||||||
@@ -4809,7 +4805,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
// For now, create a temporary peer ID based on Nostr pubkey
|
// For now, create a temporary peer ID based on Nostr pubkey
|
||||||
// This allows the message to be displayed even without Noise key mapping
|
// This allows the message to be displayed even without Noise key mapping
|
||||||
let tempPeerID = "nostr_" + senderPubkey.prefix(TransportConfig.nostrConvKeyPrefixLength)
|
let tempPeerID = "nostr_" + senderPubkey.prefix(16)
|
||||||
|
|
||||||
// Check if we're viewing this unknown sender's chat
|
// Check if we're viewing this unknown sender's chat
|
||||||
let isViewingThisChat = selectedPrivateChatPeer == tempPeerID
|
let isViewingThisChat = selectedPrivateChatPeer == tempPeerID
|
||||||
@@ -5037,7 +5033,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
var oldPeerIDsToRemove: [String] = []
|
var oldPeerIDsToRemove: [String] = []
|
||||||
|
|
||||||
// Only migrate messages from the last 24 hours to prevent old messages from flooding
|
// Only migrate messages from the last 24 hours to prevent old messages from flooding
|
||||||
let cutoffTime = Date().addingTimeInterval(-TransportConfig.uiMigrationCutoffSeconds)
|
let cutoffTime = Date().addingTimeInterval(-24 * 60 * 60)
|
||||||
|
|
||||||
for (oldPeerID, messages) in privateChats {
|
for (oldPeerID, messages) in privateChats {
|
||||||
if oldPeerID != peerID {
|
if oldPeerID != peerID {
|
||||||
@@ -5261,7 +5257,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Mark other messages as read
|
// Mark other messages as read
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + TransportConfig.uiReadReceiptRetryShortSeconds) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
||||||
self?.markPrivateMessagesAsRead(from: peerID)
|
self?.markPrivateMessagesAsRead(from: peerID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5497,7 +5493,7 @@ private func checkForMentions(_ message: BitchatMessage) {
|
|||||||
impactFeedback.prepare()
|
impactFeedback.prepare()
|
||||||
|
|
||||||
for i in 0..<8 {
|
for i in 0..<8 {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * TransportConfig.uiBatchDispatchStaggerSeconds) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.15) {
|
||||||
impactFeedback.impactOccurred()
|
impactFeedback.impactOccurred()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,14 +100,14 @@ struct ContentView: View {
|
|||||||
.onEnded { value in
|
.onEnded { value in
|
||||||
let translation = value.translation.width.isNaN ? 0 : value.translation.width
|
let translation = value.translation.width.isNaN ? 0 : value.translation.width
|
||||||
let velocity = value.velocity.width.isNaN ? 0 : value.velocity.width
|
let velocity = value.velocity.width.isNaN ? 0 : value.velocity.width
|
||||||
if translation > TransportConfig.uiBackSwipeTranslationLarge || (translation > TransportConfig.uiBackSwipeTranslationSmall && velocity > TransportConfig.uiBackSwipeVelocityThreshold) {
|
if translation > 50 || (translation > 30 && velocity > 300) {
|
||||||
withAnimation(.easeOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
|
withAnimation(.easeOut(duration: 0.2)) {
|
||||||
showPrivateChat = false
|
showPrivateChat = false
|
||||||
backSwipeOffset = 0
|
backSwipeOffset = 0
|
||||||
viewModel.endPrivateChat()
|
viewModel.endPrivateChat()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
withAnimation(.easeOut(duration: TransportConfig.uiAnimationShortSeconds)) {
|
withAnimation(.easeOut(duration: 0.15)) {
|
||||||
backSwipeOffset = 0
|
backSwipeOffset = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,7 +121,7 @@ struct ContentView: View {
|
|||||||
Color.clear
|
Color.clear
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
showSidebar = false
|
showSidebar = false
|
||||||
sidebarDragOffset = 0
|
sidebarDragOffset = 0
|
||||||
}
|
}
|
||||||
@@ -151,14 +151,14 @@ struct ContentView: View {
|
|||||||
let width = geometry.size.width.isNaN ? 0 : max(0, geometry.size.width)
|
let width = geometry.size.width.isNaN ? 0 : max(0, geometry.size.width)
|
||||||
return showSidebar ? -dragOffset : width - dragOffset
|
return showSidebar ? -dragOffset : width - dragOffset
|
||||||
}())
|
}())
|
||||||
.animation(.easeInOut(duration: TransportConfig.uiAnimationSidebarSeconds), value: showSidebar)
|
.animation(.easeInOut(duration: 0.25), value: showSidebar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.frame(minWidth: 600, minHeight: 400)
|
.frame(minWidth: 600, minHeight: 400)
|
||||||
#endif
|
#endif
|
||||||
.onChange(of: viewModel.selectedPrivateChatPeer) { newValue in
|
.onChange(of: viewModel.selectedPrivateChatPeer) { newValue in
|
||||||
withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
showPrivateChat = newValue != nil
|
showPrivateChat = newValue != nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,7 +195,7 @@ struct ContentView: View {
|
|||||||
} else {
|
} else {
|
||||||
viewModel.startPrivateChat(with: peerID)
|
viewModel.startPrivateChat(with: peerID)
|
||||||
}
|
}
|
||||||
withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
showSidebar = false
|
showSidebar = false
|
||||||
sidebarDragOffset = 0
|
sidebarDragOffset = 0
|
||||||
}
|
}
|
||||||
@@ -264,7 +264,7 @@ struct ContentView: View {
|
|||||||
|
|
||||||
// Implement windowing with adjustable window count per chat
|
// Implement windowing with adjustable window count per chat
|
||||||
let currentWindowCount: Int = {
|
let currentWindowCount: Int = {
|
||||||
if let peer = privatePeer { return windowCountPrivate[peer] ?? TransportConfig.uiWindowInitialCountPrivate }
|
if let peer = privatePeer { return windowCountPrivate[peer] ?? 300 }
|
||||||
return windowCountPublic
|
return windowCountPublic
|
||||||
}()
|
}()
|
||||||
let windowedMessages = messages.suffix(currentWindowCount)
|
let windowedMessages = messages.suffix(currentWindowCount)
|
||||||
@@ -296,11 +296,11 @@ struct ContentView: View {
|
|||||||
let cashuTokens = message.content.extractCashuTokens()
|
let cashuTokens = message.content.extractCashuTokens()
|
||||||
let lightningLinks = message.content.extractLightningLinks()
|
let lightningLinks = message.content.extractLightningLinks()
|
||||||
HStack(alignment: .top, spacing: 0) {
|
HStack(alignment: .top, spacing: 0) {
|
||||||
let isLong = (message.content.count > TransportConfig.uiLongMessageLengthThreshold || message.content.hasVeryLongToken(threshold: TransportConfig.uiVeryLongTokenThreshold)) && cashuTokens.isEmpty
|
let isLong = (message.content.count > 2000 || message.content.hasVeryLongToken(threshold: 512)) && cashuTokens.isEmpty
|
||||||
let isExpanded = expandedMessageIDs.contains(message.id)
|
let isExpanded = expandedMessageIDs.contains(message.id)
|
||||||
Text(viewModel.formatMessageAsText(message, colorScheme: colorScheme))
|
Text(viewModel.formatMessageAsText(message, colorScheme: colorScheme))
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.lineLimit(isLong && !isExpanded ? TransportConfig.uiLongMessageLineLimit : nil)
|
.lineLimit(isLong && !isExpanded ? 30 : nil)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
||||||
// Delivery status indicator for private messages
|
// Delivery status indicator for private messages
|
||||||
@@ -312,7 +312,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Expand/Collapse for very long messages
|
// Expand/Collapse for very long messages
|
||||||
if (message.content.count > TransportConfig.uiLongMessageLengthThreshold || message.content.hasVeryLongToken(threshold: TransportConfig.uiVeryLongTokenThreshold)) && cashuTokens.isEmpty {
|
if (message.content.count > 2000 || message.content.hasVeryLongToken(threshold: 512)) && cashuTokens.isEmpty {
|
||||||
let isExpanded = expandedMessageIDs.contains(message.id)
|
let isExpanded = expandedMessageIDs.contains(message.id)
|
||||||
Button(isExpanded ? "show less" : "show more") {
|
Button(isExpanded ? "show less" : "show more") {
|
||||||
if isExpanded { expandedMessageIDs.remove(message.id) }
|
if isExpanded { expandedMessageIDs.remove(message.id) }
|
||||||
@@ -371,7 +371,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
// Infinite scroll up: when top row appears, increase window and preserve anchor
|
// Infinite scroll up: when top row appears, increase window and preserve anchor
|
||||||
if message.id == windowedMessages.first?.id, messages.count > windowedMessages.count {
|
if message.id == windowedMessages.first?.id, messages.count > windowedMessages.count {
|
||||||
let step = TransportConfig.uiWindowStepCount
|
let step = 200
|
||||||
let contextKey: String = {
|
let contextKey: String = {
|
||||||
if let peer = privatePeer { return "dm:\(peer)" }
|
if let peer = privatePeer { return "dm:\(peer)" }
|
||||||
switch locationManager.selectedChannel {
|
switch locationManager.selectedChannel {
|
||||||
@@ -381,7 +381,7 @@ struct ContentView: View {
|
|||||||
}()
|
}()
|
||||||
let preserveID = "\(contextKey)|\(message.id)"
|
let preserveID = "\(contextKey)|\(message.id)"
|
||||||
if let peer = privatePeer {
|
if let peer = privatePeer {
|
||||||
let current = windowCountPrivate[peer] ?? TransportConfig.uiWindowInitialCountPrivate
|
let current = windowCountPrivate[peer] ?? 300
|
||||||
let newCount = min(messages.count, current + step)
|
let newCount = min(messages.count, current + step)
|
||||||
if newCount != current {
|
if newCount != current {
|
||||||
windowCountPrivate[peer] = newCount
|
windowCountPrivate[peer] = newCount
|
||||||
@@ -540,7 +540,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
// Throttle scroll animations to prevent excessive UI updates
|
// Throttle scroll animations to prevent excessive UI updates
|
||||||
let now = Date()
|
let now = Date()
|
||||||
if now.timeIntervalSince(lastScrollTime) > TransportConfig.uiScrollThrottleSeconds {
|
if now.timeIntervalSince(lastScrollTime) > 0.5 {
|
||||||
// Immediate scroll if enough time has passed
|
// Immediate scroll if enough time has passed
|
||||||
lastScrollTime = now
|
lastScrollTime = now
|
||||||
let contextKey: String = {
|
let contextKey: String = {
|
||||||
@@ -557,7 +557,7 @@ struct ContentView: View {
|
|||||||
} else {
|
} else {
|
||||||
// Schedule a delayed scroll
|
// Schedule a delayed scroll
|
||||||
scrollThrottleTimer?.invalidate()
|
scrollThrottleTimer?.invalidate()
|
||||||
scrollThrottleTimer = Timer.scheduledTimer(withTimeInterval: TransportConfig.uiScrollThrottleSeconds, repeats: false) { _ in
|
scrollThrottleTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in
|
||||||
lastScrollTime = Date()
|
lastScrollTime = Date()
|
||||||
let contextKey: String = {
|
let contextKey: String = {
|
||||||
switch locationManager.selectedChannel {
|
switch locationManager.selectedChannel {
|
||||||
@@ -589,7 +589,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
// Same throttling for private chats
|
// Same throttling for private chats
|
||||||
let now = Date()
|
let now = Date()
|
||||||
if now.timeIntervalSince(lastScrollTime) > TransportConfig.uiScrollThrottleSeconds {
|
if now.timeIntervalSince(lastScrollTime) > 0.5 {
|
||||||
lastScrollTime = now
|
lastScrollTime = now
|
||||||
let contextKey = "dm:\(peerID)"
|
let contextKey = "dm:\(peerID)"
|
||||||
let count = windowCountPrivate[peerID] ?? 300
|
let count = windowCountPrivate[peerID] ?? 300
|
||||||
@@ -599,7 +599,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
scrollThrottleTimer?.invalidate()
|
scrollThrottleTimer?.invalidate()
|
||||||
scrollThrottleTimer = Timer.scheduledTimer(withTimeInterval: TransportConfig.uiScrollThrottleSeconds, repeats: false) { _ in
|
scrollThrottleTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in
|
||||||
lastScrollTime = Date()
|
lastScrollTime = Date()
|
||||||
let contextKey = "dm:\(peerID)"
|
let contextKey = "dm:\(peerID)"
|
||||||
let count = windowCountPrivate[peerID] ?? 300
|
let count = windowCountPrivate[peerID] ?? 300
|
||||||
@@ -619,7 +619,7 @@ struct ContentView: View {
|
|||||||
break
|
break
|
||||||
case .location(let ch):
|
case .location(let ch):
|
||||||
// Reset window size
|
// Reset window size
|
||||||
windowCountPublic = TransportConfig.uiWindowInitialCountPublic
|
windowCountPublic = 300
|
||||||
let contextKey = "geo:\(ch.geohash)"
|
let contextKey = "geo:\(ch.geohash)"
|
||||||
let last = viewModel.messages.suffix(windowCountPublic).last?.id
|
let last = viewModel.messages.suffix(windowCountPublic).last?.id
|
||||||
let target = last.map { "\(contextKey)|\($0)" }
|
let target = last.map { "\(contextKey)|\($0)" }
|
||||||
@@ -635,11 +635,11 @@ struct ContentView: View {
|
|||||||
// Try multiple times to ensure read receipts are sent
|
// Try multiple times to ensure read receipts are sent
|
||||||
viewModel.markPrivateMessagesAsRead(from: peerID)
|
viewModel.markPrivateMessagesAsRead(from: peerID)
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + TransportConfig.uiReadReceiptRetryShortSeconds) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||||
viewModel.markPrivateMessagesAsRead(from: peerID)
|
viewModel.markPrivateMessagesAsRead(from: peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + TransportConfig.uiReadReceiptRetryLongSeconds) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
viewModel.markPrivateMessagesAsRead(from: peerID)
|
viewModel.markPrivateMessagesAsRead(from: peerID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -859,7 +859,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
// Delay keyboard focus to avoid iOS constraint warnings
|
// Delay keyboard focus to avoid iOS constraint warnings
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + TransportConfig.uiReadReceiptRetryShortSeconds) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||||
isTextFieldFocused = true
|
isTextFieldFocused = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -925,7 +925,7 @@ struct ContentView: View {
|
|||||||
secondaryTextColor: secondaryTextColor,
|
secondaryTextColor: secondaryTextColor,
|
||||||
onTapPeer: { peerID in
|
onTapPeer: { peerID in
|
||||||
viewModel.startPrivateChat(with: peerID)
|
viewModel.startPrivateChat(with: peerID)
|
||||||
withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
showSidebar = false
|
showSidebar = false
|
||||||
sidebarDragOffset = 0
|
sidebarDragOffset = 0
|
||||||
}
|
}
|
||||||
@@ -973,7 +973,7 @@ struct ContentView: View {
|
|||||||
.onEnded { value in
|
.onEnded { value in
|
||||||
let translation = value.translation.width.isNaN ? 0 : value.translation.width
|
let translation = value.translation.width.isNaN ? 0 : value.translation.width
|
||||||
let velocity = value.velocity.width.isNaN ? 0 : value.velocity.width
|
let velocity = value.velocity.width.isNaN ? 0 : value.velocity.width
|
||||||
withAnimation(.easeOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
|
withAnimation(.easeOut(duration: 0.2)) {
|
||||||
if !showSidebar {
|
if !showSidebar {
|
||||||
if translation < -100 || (translation < -50 && velocity < -500) {
|
if translation < -100 || (translation < -50 && velocity < -500) {
|
||||||
showSidebar = true
|
showSidebar = true
|
||||||
@@ -1036,8 +1036,9 @@ struct ContentView: View {
|
|||||||
case .mesh:
|
case .mesh:
|
||||||
let counts = viewModel.allPeers.reduce(into: (others: 0, mesh: 0)) { counts, peer in
|
let counts = viewModel.allPeers.reduce(into: (others: 0, mesh: 0)) { counts, peer in
|
||||||
guard peer.id != viewModel.meshService.myPeerID else { return }
|
guard peer.id != viewModel.meshService.myPeerID else { return }
|
||||||
if peer.isConnected { counts.mesh += 1; counts.others += 1 }
|
let isMeshConnected = peer.isConnected
|
||||||
else if peer.isReachable { counts.others += 1 }
|
if isMeshConnected { counts.mesh += 1; counts.others += 1 }
|
||||||
|
else if peer.isMutualFavorite { counts.others += 1 }
|
||||||
}
|
}
|
||||||
let meshBlue = Color(hue: 0.60, saturation: 0.85, brightness: 0.82)
|
let meshBlue = Color(hue: 0.60, saturation: 0.85, brightness: 0.82)
|
||||||
let color: Color = counts.mesh > 0 ? meshBlue : Color.secondary
|
let color: Color = counts.mesh > 0 ? meshBlue : Color.secondary
|
||||||
@@ -1152,7 +1153,7 @@ struct ContentView: View {
|
|||||||
// QR moved to the PEOPLE header in the sidebar when on mesh channel
|
// QR moved to the PEOPLE header in the sidebar when on mesh channel
|
||||||
}
|
}
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
showSidebar.toggle()
|
showSidebar.toggle()
|
||||||
sidebarDragOffset = 0
|
sidebarDragOffset = 0
|
||||||
}
|
}
|
||||||
@@ -1180,11 +1181,20 @@ struct ContentView: View {
|
|||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func privateHeaderContent(for privatePeerID: String) -> some View {
|
private func privateHeaderContent(for privatePeerID: String) -> some View {
|
||||||
// Prefer short (mesh) ID whenever available for encryption/session status; keep stable key for display resolution only.
|
// Prefer short (mesh) ID when mesh-connected (radio). Only use full Noise key when not connected (globe).
|
||||||
let headerPeerID: String = {
|
let headerPeerID: String = {
|
||||||
if privatePeerID.count == 64 {
|
if privatePeerID.count == 16 {
|
||||||
// Map stable Noise key to short ID if we know it (even if not directly connected)
|
let isMeshConnected = viewModel.meshService.isPeerConnected(privatePeerID) || viewModel.connectedPeers.contains(privatePeerID)
|
||||||
if let short = viewModel.getShortIDForNoiseKey(privatePeerID) { return short }
|
if !isMeshConnected, let stable = viewModel.getNoiseKeyForShortID(privatePeerID) {
|
||||||
|
return stable
|
||||||
|
}
|
||||||
|
} else if privatePeerID.count == 64 {
|
||||||
|
// If we have a full Noise key and a corresponding short ID is currently mesh-connected, prefer short ID
|
||||||
|
if let short = viewModel.getShortIDForNoiseKey(privatePeerID) {
|
||||||
|
if viewModel.meshService.isPeerConnected(short) || viewModel.connectedPeers.contains(short) {
|
||||||
|
return short
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return privatePeerID
|
return privatePeerID
|
||||||
}()
|
}()
|
||||||
@@ -1199,29 +1209,10 @@ struct ContentView: View {
|
|||||||
return "#\(ch.geohash)/@\(disp)"
|
return "#\(ch.geohash)/@\(disp)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Try mesh/unified peer display
|
return peer?.displayName ??
|
||||||
if let name = peer?.displayName { return name }
|
viewModel.meshService.peerNickname(peerID: headerPeerID) ??
|
||||||
// Try direct mesh nickname (connected-only)
|
FavoritesPersistenceService.shared.getFavoriteStatus(for: Data(hexString: headerPeerID) ?? Data())?.peerNickname ??
|
||||||
if let name = viewModel.meshService.peerNickname(peerID: headerPeerID) { return name }
|
"Unknown"
|
||||||
// Try favorite nickname by stable Noise key
|
|
||||||
if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: Data(hexString: headerPeerID) ?? Data()),
|
|
||||||
!fav.peerNickname.isEmpty { return fav.peerNickname }
|
|
||||||
// Fallback: resolve from persisted social identity via fingerprint mapping
|
|
||||||
if headerPeerID.count == 16 {
|
|
||||||
let candidates = SecureIdentityStateManager.shared.getCryptoIdentitiesByPeerIDPrefix(headerPeerID)
|
|
||||||
if let id = candidates.first,
|
|
||||||
let social = SecureIdentityStateManager.shared.getSocialIdentity(for: id.fingerprint) {
|
|
||||||
if let pet = social.localPetname, !pet.isEmpty { return pet }
|
|
||||||
if !social.claimedNickname.isEmpty { return social.claimedNickname }
|
|
||||||
}
|
|
||||||
} else if headerPeerID.count == 64, let keyData = Data(hexString: headerPeerID) {
|
|
||||||
let fp = keyData.sha256Fingerprint()
|
|
||||||
if let social = SecureIdentityStateManager.shared.getSocialIdentity(for: fp) {
|
|
||||||
if let pet = social.localPetname, !pet.isEmpty { return pet }
|
|
||||||
if !social.claimedNickname.isEmpty { return social.claimedNickname }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "Unknown"
|
|
||||||
}()
|
}()
|
||||||
let isNostrAvailable: Bool = {
|
let isNostrAvailable: Bool = {
|
||||||
guard let connectionState = peer?.connectionState else {
|
guard let connectionState = peer?.connectionState else {
|
||||||
@@ -1256,12 +1247,6 @@ struct ContentView: View {
|
|||||||
.font(.system(size: 14))
|
.font(.system(size: 14))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
.accessibilityLabel("Connected via mesh")
|
.accessibilityLabel("Connected via mesh")
|
||||||
case .meshReachable:
|
|
||||||
// point.3 filled icon for reachable via mesh (not directly connected)
|
|
||||||
Image(systemName: "point.3.filled.connected.trianglepath.dotted")
|
|
||||||
.font(.system(size: 14))
|
|
||||||
.foregroundColor(textColor)
|
|
||||||
.accessibilityLabel("Reachable via mesh")
|
|
||||||
case .nostrAvailable:
|
case .nostrAvailable:
|
||||||
// Purple globe for Nostr
|
// Purple globe for Nostr
|
||||||
Image(systemName: "globe")
|
Image(systemName: "globe")
|
||||||
@@ -1272,12 +1257,6 @@ struct ContentView: View {
|
|||||||
// Should not happen for PM header, but handle gracefully
|
// Should not happen for PM header, but handle gracefully
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
} else if viewModel.meshService.isPeerReachable(headerPeerID) {
|
|
||||||
// Fallback: reachable via mesh but not in current peer list
|
|
||||||
Image(systemName: "point.3.filled.connected.trianglepath.dotted")
|
|
||||||
.font(.system(size: 14))
|
|
||||||
.foregroundColor(textColor)
|
|
||||||
.accessibilityLabel("Reachable via mesh")
|
|
||||||
} else if isNostrAvailable {
|
} else if isNostrAvailable {
|
||||||
// Fallback to Nostr if peer not in list but is mutual favorite
|
// Fallback to Nostr if peer not in list but is mutual favorite
|
||||||
Image(systemName: "globe")
|
Image(systemName: "globe")
|
||||||
@@ -1296,14 +1275,7 @@ struct ContentView: View {
|
|||||||
.font(.system(size: 16, weight: .medium, design: .monospaced))
|
.font(.system(size: 16, weight: .medium, design: .monospaced))
|
||||||
.foregroundColor(textColor) // Dynamic encryption status icon (hide for geohash DMs)
|
.foregroundColor(textColor) // Dynamic encryption status icon (hide for geohash DMs)
|
||||||
if !privatePeerID.hasPrefix("nostr_") {
|
if !privatePeerID.hasPrefix("nostr_") {
|
||||||
// Use short peer ID if available for encryption status (sessions keyed by short ID)
|
let encryptionStatus = viewModel.getEncryptionStatus(for: headerPeerID)
|
||||||
let statusPeerID: String = {
|
|
||||||
if privatePeerID.count == 64, let short = viewModel.getShortIDForNoiseKey(privatePeerID) {
|
|
||||||
return short
|
|
||||||
}
|
|
||||||
return headerPeerID
|
|
||||||
}()
|
|
||||||
let encryptionStatus = viewModel.getEncryptionStatus(for: statusPeerID)
|
|
||||||
if let icon = encryptionStatus.icon {
|
if let icon = encryptionStatus.icon {
|
||||||
Image(systemName: icon)
|
Image(systemName: icon)
|
||||||
.font(.system(size: 14))
|
.font(.system(size: 14))
|
||||||
@@ -1322,7 +1294,7 @@ struct ContentView: View {
|
|||||||
// Left and right buttons positioned with HStack
|
// Left and right buttons positioned with HStack
|
||||||
HStack {
|
HStack {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
showPrivateChat = false
|
showPrivateChat = false
|
||||||
viewModel.endPrivateChat()
|
viewModel.endPrivateChat()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,27 +41,9 @@ struct FingerprintView: View {
|
|||||||
.padding()
|
.padding()
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
// Prefer short mesh ID for session/encryption status
|
// Peer info
|
||||||
let statusPeerID: String = {
|
let peerNickname = viewModel.meshService.peerNickname(peerID: peerID) ?? "Unknown"
|
||||||
if peerID.count == 64, let short = viewModel.getShortIDForNoiseKey(peerID) { return short }
|
let encryptionStatus = viewModel.getEncryptionStatus(for: peerID)
|
||||||
return peerID
|
|
||||||
}()
|
|
||||||
// Resolve a friendly name
|
|
||||||
let peerNickname: String = {
|
|
||||||
if let p = viewModel.getPeer(byID: statusPeerID) { return p.displayName }
|
|
||||||
if let name = viewModel.meshService.peerNickname(peerID: statusPeerID) { return name }
|
|
||||||
if peerID.count == 64, let data = Data(hexString: peerID) {
|
|
||||||
if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: data), !fav.peerNickname.isEmpty { return fav.peerNickname }
|
|
||||||
let fp = data.sha256Fingerprint()
|
|
||||||
if let social = SecureIdentityStateManager.shared.getSocialIdentity(for: fp) {
|
|
||||||
if let pet = social.localPetname, !pet.isEmpty { return pet }
|
|
||||||
if !social.claimedNickname.isEmpty { return social.claimedNickname }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "Unknown"
|
|
||||||
}()
|
|
||||||
// Accurate encryption state based on short ID session
|
|
||||||
let encryptionStatus = viewModel.getEncryptionStatus(for: statusPeerID)
|
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
if let icon = encryptionStatus.icon {
|
if let icon = encryptionStatus.icon {
|
||||||
@@ -92,7 +74,7 @@ struct FingerprintView: View {
|
|||||||
.font(.system(size: 12, weight: .bold, design: .monospaced))
|
.font(.system(size: 12, weight: .bold, design: .monospaced))
|
||||||
.foregroundColor(textColor.opacity(0.7))
|
.foregroundColor(textColor.opacity(0.7))
|
||||||
|
|
||||||
if let fingerprint = viewModel.getFingerprint(for: statusPeerID) {
|
if let fingerprint = viewModel.getFingerprint(for: peerID) {
|
||||||
Text(formatFingerprint(fingerprint))
|
Text(formatFingerprint(fingerprint))
|
||||||
.font(.system(size: 14, design: .monospaced))
|
.font(.system(size: 14, design: .monospaced))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|||||||
@@ -52,11 +52,6 @@ struct MeshPeerList: View {
|
|||||||
Image(systemName: "antenna.radiowaves.left.and.right")
|
Image(systemName: "antenna.radiowaves.left.and.right")
|
||||||
.font(.system(size: 10))
|
.font(.system(size: 10))
|
||||||
.foregroundColor(baseColor)
|
.foregroundColor(baseColor)
|
||||||
} else if peer.isReachable {
|
|
||||||
// Mesh-reachable (relayed): point.3 icon
|
|
||||||
Image(systemName: "point.3.filled.connected.trianglepath.dotted")
|
|
||||||
.font(.system(size: 10))
|
|
||||||
.foregroundColor(baseColor)
|
|
||||||
} else if peer.isMutualFavorite {
|
} else if peer.isMutualFavorite {
|
||||||
// Mutual favorite reachable via Nostr: globe icon (purple)
|
// Mutual favorite reachable via Nostr: globe icon (purple)
|
||||||
Image(systemName: "globe")
|
Image(systemName: "globe")
|
||||||
@@ -115,14 +110,6 @@ struct MeshPeerList: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
// Unread message indicator for this peer
|
|
||||||
if !isMe, item.hasUnread {
|
|
||||||
Image(systemName: "envelope.fill")
|
|
||||||
.font(.system(size: 10))
|
|
||||||
.foregroundColor(.orange)
|
|
||||||
.help("New messages")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isMe {
|
if !isMe {
|
||||||
Button(action: { onToggleFavorite(peer.id) }) {
|
Button(action: { onToggleFavorite(peer.id) }) {
|
||||||
Image(systemName: (peer.favoriteStatus?.isFavorite ?? false) ? "star.fill" : "star")
|
Image(systemName: (peer.favoriteStatus?.isFavorite ?? false) ? "star.fill" : "star")
|
||||||
|
|||||||
@@ -153,13 +153,13 @@ final class ShareViewController: UIViewController {
|
|||||||
userDefaults.set(content, forKey: "sharedContent")
|
userDefaults.set(content, forKey: "sharedContent")
|
||||||
userDefaults.set(type, forKey: "sharedContentType")
|
userDefaults.set(type, forKey: "sharedContentType")
|
||||||
userDefaults.set(Date(), forKey: "sharedContentDate")
|
userDefaults.set(Date(), forKey: "sharedContentDate")
|
||||||
// No need to force synchronize; the system persists changes
|
userDefaults.synchronize()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func finishWithMessage(_ msg: String) {
|
private func finishWithMessage(_ msg: String) {
|
||||||
statusLabel.text = msg
|
statusLabel.text = msg
|
||||||
// Complete shortly after showing status
|
// Complete shortly after showing status
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + TransportConfig.uiShareExtensionDismissDelaySeconds) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||||
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
|
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user