mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 21:45:22 +00:00
Location notes: Matrix loader with 1–3s minimum display; allow multiple notes; wire notes UI from mesh toolbar
This commit is contained in:
@@ -33,14 +33,16 @@
|
|||||||
0481A3472E6D869F00FC845E /* TorURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3442E6D869F00FC845E /* TorURLSession.swift */; };
|
0481A3472E6D869F00FC845E /* TorURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3442E6D869F00FC845E /* TorURLSession.swift */; };
|
||||||
0481A3482E6D869F00FC845E /* TorManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3432E6D869F00FC845E /* TorManager.swift */; };
|
0481A3482E6D869F00FC845E /* TorManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3432E6D869F00FC845E /* TorManager.swift */; };
|
||||||
0481A3492E6D869F00FC845E /* TorURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3442E6D869F00FC845E /* TorURLSession.swift */; };
|
0481A3492E6D869F00FC845E /* TorURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3442E6D869F00FC845E /* TorURLSession.swift */; };
|
||||||
0481A3552E6D877600FC845E /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 0481A3532E6D877600FC845E /* README.md */; };
|
|
||||||
0481A3562E6D877600FC845E /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 0481A3532E6D877600FC845E /* README.md */; };
|
|
||||||
0481A3582E6D929E00FC845E /* tor-nolzma.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481A3572E6D929E00FC845E /* tor-nolzma.xcframework */; };
|
0481A3582E6D929E00FC845E /* tor-nolzma.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481A3572E6D929E00FC845E /* tor-nolzma.xcframework */; };
|
||||||
0481A3592E6D929E00FC845E /* tor-nolzma.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481A3572E6D929E00FC845E /* tor-nolzma.xcframework */; };
|
0481A3592E6D929E00FC845E /* tor-nolzma.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481A3572E6D929E00FC845E /* tor-nolzma.xcframework */; };
|
||||||
0481A35B2E6D9BEF00FC845E /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481A35A2E6D9BEF00FC845E /* libz.tbd */; };
|
0481A35B2E6D9BEF00FC845E /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481A35A2E6D9BEF00FC845E /* libz.tbd */; };
|
||||||
0481A35D2E6DA18600FC845E /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481A35C2E6DA18600FC845E /* libz.tbd */; };
|
0481A35D2E6DA18600FC845E /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481A35C2E6DA18600FC845E /* libz.tbd */; };
|
||||||
0481A3902E734CAE00FC845E /* CommandProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A38F2E734CAE00FC845E /* CommandProcessorTests.swift */; };
|
0481A3902E734CAE00FC845E /* CommandProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A38F2E734CAE00FC845E /* CommandProcessorTests.swift */; };
|
||||||
0481A3912E734CAE00FC845E /* CommandProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A38F2E734CAE00FC845E /* CommandProcessorTests.swift */; };
|
0481A3912E734CAE00FC845E /* CommandProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A38F2E734CAE00FC845E /* CommandProcessorTests.swift */; };
|
||||||
|
0481A3A92E74D28800FC845E /* LocationNotesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3A82E74D28800FC845E /* LocationNotesView.swift */; };
|
||||||
|
0481A3AA2E74D28800FC845E /* LocationNotesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3A82E74D28800FC845E /* LocationNotesView.swift */; };
|
||||||
|
0481A3AC2E74D29400FC845E /* LocationNotesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3AB2E74D29400FC845E /* LocationNotesManager.swift */; };
|
||||||
|
0481A3AD2E74D29400FC845E /* LocationNotesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3AB2E74D29400FC845E /* LocationNotesManager.swift */; };
|
||||||
048A4BE72E5CCCC300162C4A /* TransportConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 048A4BE62E5CCCC300162C4A /* TransportConfig.swift */; };
|
048A4BE72E5CCCC300162C4A /* TransportConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 048A4BE62E5CCCC300162C4A /* TransportConfig.swift */; };
|
||||||
048A4BE82E5CCCC300162C4A /* 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 */; };
|
048A4BE92E5CCCC300162C4B /* TransportConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 048A4BE62E5CCCC300162C4A /* TransportConfig.swift */; };
|
||||||
@@ -128,6 +130,10 @@
|
|||||||
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 */; };
|
A1B2C3D44E5F60718293A4B5 /* XChaCha20Poly1305Compat.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D44E5F60718293A4B4 /* XChaCha20Poly1305Compat.swift */; };
|
||||||
|
A1B2C3D4E5F60123456789BA /* MockKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60123456789AA /* MockKeychain.swift */; };
|
||||||
|
A1B2C3D4E5F60123456789BB /* MockKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60123456789AA /* MockKeychain.swift */; };
|
||||||
|
A1B2C3D4E5F60123456789BC /* MockIdentityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60123456789AB /* MockIdentityManager.swift */; };
|
||||||
|
A1B2C3D4E5F60123456789BD /* MockIdentityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60123456789AB /* MockIdentityManager.swift */; };
|
||||||
A1B2C3D54E5F60718293A4B6 /* 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 */; };
|
||||||
@@ -173,10 +179,6 @@
|
|||||||
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 */; };
|
||||||
A1B2C3D4E5F60123456789BA /* MockKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60123456789AA /* MockKeychain.swift */; };
|
|
||||||
A1B2C3D4E5F60123456789BB /* MockKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60123456789AA /* MockKeychain.swift */; };
|
|
||||||
A1B2C3D4E5F60123456789BC /* MockIdentityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60123456789AB /* MockIdentityManager.swift */; };
|
|
||||||
A1B2C3D4E5F60123456789BD /* MockIdentityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60123456789AB /* MockIdentityManager.swift */; };
|
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -232,11 +234,12 @@
|
|||||||
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>"; };
|
||||||
0481A3432E6D869F00FC845E /* TorManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorManager.swift; sourceTree = "<group>"; };
|
0481A3432E6D869F00FC845E /* TorManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorManager.swift; sourceTree = "<group>"; };
|
||||||
0481A3442E6D869F00FC845E /* TorURLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorURLSession.swift; sourceTree = "<group>"; };
|
0481A3442E6D869F00FC845E /* TorURLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorURLSession.swift; sourceTree = "<group>"; };
|
||||||
0481A3532E6D877600FC845E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
|
||||||
0481A3572E6D929E00FC845E /* tor-nolzma.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = "tor-nolzma.xcframework"; sourceTree = "<group>"; };
|
0481A3572E6D929E00FC845E /* tor-nolzma.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = "tor-nolzma.xcframework"; sourceTree = "<group>"; };
|
||||||
0481A35A2E6D9BEF00FC845E /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; };
|
0481A35A2E6D9BEF00FC845E /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; };
|
||||||
0481A35C2E6DA18600FC845E /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; };
|
0481A35C2E6DA18600FC845E /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; };
|
||||||
0481A38F2E734CAE00FC845E /* CommandProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandProcessorTests.swift; sourceTree = "<group>"; };
|
0481A38F2E734CAE00FC845E /* CommandProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandProcessorTests.swift; sourceTree = "<group>"; };
|
||||||
|
0481A3A82E74D28800FC845E /* LocationNotesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationNotesView.swift; sourceTree = "<group>"; };
|
||||||
|
0481A3AB2E74D29400FC845E /* LocationNotesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationNotesManager.swift; sourceTree = "<group>"; };
|
||||||
048A4BE62E5CCCC300162C4A /* TransportConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportConfig.swift; sourceTree = "<group>"; };
|
048A4BE62E5CCCC300162C4A /* TransportConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportConfig.swift; sourceTree = "<group>"; };
|
||||||
048A4C272E5FCD6600162C4A /* GeohashBookmarksStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeohashBookmarksStore.swift; sourceTree = "<group>"; };
|
048A4C272E5FCD6600162C4A /* GeohashBookmarksStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeohashBookmarksStore.swift; sourceTree = "<group>"; };
|
||||||
048A4C2A2E5FCE0300162C4A /* GeohashBookmarksStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeohashBookmarksStoreTests.swift; sourceTree = "<group>"; };
|
048A4C2A2E5FCE0300162C4A /* GeohashBookmarksStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeohashBookmarksStoreTests.swift; sourceTree = "<group>"; };
|
||||||
@@ -289,6 +292,8 @@
|
|||||||
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>"; };
|
A1B2C3D44E5F60718293A4B4 /* XChaCha20Poly1305Compat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XChaCha20Poly1305Compat.swift; sourceTree = "<group>"; };
|
||||||
|
A1B2C3D4E5F60123456789AA /* MockKeychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockKeychain.swift; sourceTree = "<group>"; };
|
||||||
|
A1B2C3D4E5F60123456789AB /* MockIdentityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockIdentityManager.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>"; };
|
AA77BB10CC22DD33EE44FF55 /* VerificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerificationService.swift; sourceTree = "<group>"; };
|
||||||
@@ -311,8 +316,6 @@
|
|||||||
FC75901A0F0073B5BB8356E7 /* TestConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConstants.swift; sourceTree = "<group>"; };
|
FC75901A0F0073B5BB8356E7 /* TestConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConstants.swift; sourceTree = "<group>"; };
|
||||||
FDC18D910D6FF2E8B1B6C885 /* SecureIdentityStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdentityStateManager.swift; sourceTree = "<group>"; };
|
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>"; };
|
||||||
A1B2C3D4E5F60123456789AA /* MockKeychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockKeychain.swift; sourceTree = "<group>"; };
|
|
||||||
A1B2C3D4E5F60123456789AB /* MockIdentityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockIdentityManager.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>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
@@ -372,7 +375,6 @@
|
|||||||
children = (
|
children = (
|
||||||
0481A35A2E6D9BEF00FC845E /* libz.tbd */,
|
0481A35A2E6D9BEF00FC845E /* libz.tbd */,
|
||||||
0481A35C2E6DA18600FC845E /* libz.tbd */,
|
0481A35C2E6DA18600FC845E /* libz.tbd */,
|
||||||
0481A3532E6D877600FC845E /* README.md */,
|
|
||||||
0481A3572E6D929E00FC845E /* tor-nolzma.xcframework */,
|
0481A3572E6D929E00FC845E /* tor-nolzma.xcframework */,
|
||||||
);
|
);
|
||||||
path = Frameworks;
|
path = Frameworks;
|
||||||
@@ -515,6 +517,7 @@
|
|||||||
A55126E93155456CAA8D6656 /* Views */ = {
|
A55126E93155456CAA8D6656 /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
0481A3A82E74D28800FC845E /* LocationNotesView.swift */,
|
||||||
AA77BB13CC22DD33EE44FF57 /* VerificationViews.swift */,
|
AA77BB13CC22DD33EE44FF57 /* VerificationViews.swift */,
|
||||||
047502B22E55FED60083520F /* GeohashPeopleList.swift */,
|
047502B22E55FED60083520F /* GeohashPeopleList.swift */,
|
||||||
047502B32E55FED60083520F /* MeshPeerList.swift */,
|
047502B32E55FED60083520F /* MeshPeerList.swift */,
|
||||||
@@ -591,6 +594,7 @@
|
|||||||
D98A3186D7E4C72E35BDF7FE /* Services */ = {
|
D98A3186D7E4C72E35BDF7FE /* Services */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
0481A3AB2E74D29400FC845E /* LocationNotesManager.swift */,
|
||||||
0481A3452E6D869F00FC845E /* Tor */,
|
0481A3452E6D869F00FC845E /* Tor */,
|
||||||
048A4C272E5FCD6600162C4A /* GeohashBookmarksStore.swift */,
|
048A4C272E5FCD6600162C4A /* GeohashBookmarksStore.swift */,
|
||||||
048A4BE62E5CCCC300162C4A /* TransportConfig.swift */,
|
048A4BE62E5CCCC300162C4A /* TransportConfig.swift */,
|
||||||
@@ -785,7 +789,6 @@
|
|||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
0481A3552E6D877600FC845E /* README.md in Resources */,
|
|
||||||
7DD72D928FF9DD3CA81B46B0 /* Assets.xcassets in Resources */,
|
7DD72D928FF9DD3CA81B46B0 /* Assets.xcassets in Resources */,
|
||||||
E0A1B2C3D4E5F6012345678D /* relays/online_relays_gps.csv in Resources */,
|
E0A1B2C3D4E5F6012345678D /* relays/online_relays_gps.csv in Resources */,
|
||||||
);
|
);
|
||||||
@@ -797,7 +800,6 @@
|
|||||||
files = (
|
files = (
|
||||||
BCCFEDC1EBE59323C3C470BF /* Assets.xcassets in Resources */,
|
BCCFEDC1EBE59323C3C470BF /* Assets.xcassets in Resources */,
|
||||||
E65BBB6544FE0159F3C6C3A8 /* LaunchScreen.storyboard in Resources */,
|
E65BBB6544FE0159F3C6C3A8 /* LaunchScreen.storyboard in Resources */,
|
||||||
0481A3562E6D877600FC845E /* README.md in Resources */,
|
|
||||||
E0A1B2C3D4E5F6012345678E /* relays/online_relays_gps.csv in Resources */,
|
E0A1B2C3D4E5F6012345678E /* relays/online_relays_gps.csv in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -864,6 +866,8 @@
|
|||||||
049BD3AF2E51ED60001A566B /* Transport.swift in Sources */,
|
049BD3AF2E51ED60001A566B /* Transport.swift in Sources */,
|
||||||
E2DCF7817344F1CCDB8B7B2F /* SecureIdentityStateManager.swift in Sources */,
|
E2DCF7817344F1CCDB8B7B2F /* SecureIdentityStateManager.swift in Sources */,
|
||||||
049BD3A02E51DBF4001A566B /* Packets.swift in Sources */,
|
049BD3A02E51DBF4001A566B /* Packets.swift in Sources */,
|
||||||
|
0481A3AD2E74D29400FC845E /* LocationNotesManager.swift in Sources */,
|
||||||
|
0481A3AA2E74D28800FC845E /* LocationNotesView.swift in Sources */,
|
||||||
047502892E5416250083520F /* Geohash.swift in Sources */,
|
047502892E5416250083520F /* Geohash.swift in Sources */,
|
||||||
0475028A2E5416250083520F /* LocationChannel.swift in Sources */,
|
0475028A2E5416250083520F /* LocationChannel.swift in Sources */,
|
||||||
049BD3A12E51DBF4001A566B /* PeerID.swift in Sources */,
|
049BD3A12E51DBF4001A566B /* PeerID.swift in Sources */,
|
||||||
@@ -929,6 +933,8 @@
|
|||||||
049BD3AE2E51ED60001A566B /* Transport.swift in Sources */,
|
049BD3AE2E51ED60001A566B /* Transport.swift in Sources */,
|
||||||
68C4BE564735F6E7915274A2 /* SecureIdentityStateManager.swift in Sources */,
|
68C4BE564735F6E7915274A2 /* SecureIdentityStateManager.swift in Sources */,
|
||||||
049BD3A22E51DBF4001A566B /* Packets.swift in Sources */,
|
049BD3A22E51DBF4001A566B /* Packets.swift in Sources */,
|
||||||
|
0481A3AC2E74D29400FC845E /* LocationNotesManager.swift in Sources */,
|
||||||
|
0481A3A92E74D28800FC845E /* LocationNotesView.swift in Sources */,
|
||||||
047502872E5416250083520F /* Geohash.swift in Sources */,
|
047502872E5416250083520F /* Geohash.swift in Sources */,
|
||||||
047502882E5416250083520F /* LocationChannel.swift in Sources */,
|
047502882E5416250083520F /* LocationChannel.swift in Sources */,
|
||||||
049BD3A32E51DBF4001A566B /* PeerID.swift in Sources */,
|
049BD3A32E51DBF4001A566B /* PeerID.swift in Sources */,
|
||||||
@@ -984,7 +990,6 @@
|
|||||||
048A4C2C2E5FCE0300162C4A /* GeohashBookmarksStoreTests.swift in Sources */,
|
048A4C2C2E5FCE0300162C4A /* GeohashBookmarksStoreTests.swift in Sources */,
|
||||||
3849CA6D99B2D536636DF4A6 /* MockBLEService.swift in Sources */,
|
3849CA6D99B2D536636DF4A6 /* MockBLEService.swift in Sources */,
|
||||||
A1B2C3D4E5F60123456789BA /* MockKeychain.swift in Sources */,
|
A1B2C3D4E5F60123456789BA /* MockKeychain.swift in Sources */,
|
||||||
|
|
||||||
A1B2C3D4E5F60123456789BC /* MockIdentityManager.swift in Sources */,
|
A1B2C3D4E5F60123456789BC /* MockIdentityManager.swift in Sources */,
|
||||||
BC4DC75F4FB823FF40569676 /* NoiseProtocolTests.swift in Sources */,
|
BC4DC75F4FB823FF40569676 /* NoiseProtocolTests.swift in Sources */,
|
||||||
EE8C3ECADAB3083A2687D50B /* NostrProtocolTests.swift in Sources */,
|
EE8C3ECADAB3083A2687D50B /* NostrProtocolTests.swift in Sources */,
|
||||||
|
|||||||
@@ -124,6 +124,32 @@ struct NostrProtocol {
|
|||||||
return try event.sign(with: schnorrKey)
|
return try event.sign(with: schnorrKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a persistent location note (kind 1: text note) tagged to a street-level geohash.
|
||||||
|
static func createGeohashTextNote(
|
||||||
|
content: String,
|
||||||
|
geohash: String,
|
||||||
|
senderIdentity: NostrIdentity,
|
||||||
|
nickname: String? = nil,
|
||||||
|
teleported: Bool = false
|
||||||
|
) throws -> NostrEvent {
|
||||||
|
var tags = [["g", geohash]]
|
||||||
|
if let nickname = nickname?.trimmingCharacters(in: .whitespacesAndNewlines), !nickname.isEmpty {
|
||||||
|
tags.append(["n", nickname])
|
||||||
|
}
|
||||||
|
if teleported {
|
||||||
|
tags.append(["t", "teleport"])
|
||||||
|
}
|
||||||
|
let event = NostrEvent(
|
||||||
|
pubkey: senderIdentity.publicKeyHex,
|
||||||
|
createdAt: Date(),
|
||||||
|
kind: .textNote,
|
||||||
|
tags: tags,
|
||||||
|
content: content
|
||||||
|
)
|
||||||
|
let schnorrKey = try senderIdentity.schnorrSigningKey()
|
||||||
|
return try event.sign(with: schnorrKey)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Private Methods
|
// MARK: - Private Methods
|
||||||
|
|
||||||
private static func createSeal(
|
private static func createSeal(
|
||||||
|
|||||||
@@ -755,6 +755,16 @@ struct NostrFilter: Encodable {
|
|||||||
filter.limit = limit
|
filter.limit = limit
|
||||||
return filter
|
return filter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For location notes: persistent text notes (kind 1) tagged with geohash
|
||||||
|
static func geohashNotes(_ geohash: String, since: Date? = nil, limit: Int = 200) -> NostrFilter {
|
||||||
|
var filter = NostrFilter()
|
||||||
|
filter.kinds = [1]
|
||||||
|
filter.since = since?.timeIntervalSince1970.toInt()
|
||||||
|
filter.tagFilters = ["g": [geohash]]
|
||||||
|
filter.limit = limit
|
||||||
|
return filter
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dynamic coding key for tag filters
|
// Dynamic coding key for tag filters
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Persistent location notes (Nostr kind 1) scoped to a street-level geohash (precision 7).
|
||||||
|
/// Subscribes to and publishes notes for a given geohash and provides a send API.
|
||||||
|
@MainActor
|
||||||
|
final class LocationNotesManager: ObservableObject {
|
||||||
|
struct Note: Identifiable, Equatable {
|
||||||
|
let id: String
|
||||||
|
let pubkey: String
|
||||||
|
let content: String
|
||||||
|
let createdAt: Date
|
||||||
|
let nickname: String?
|
||||||
|
|
||||||
|
var displayName: String {
|
||||||
|
let suffix = String(pubkey.suffix(4))
|
||||||
|
if let nick = nickname, !nick.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
|
return "\(nick)#\(suffix)"
|
||||||
|
}
|
||||||
|
return "anon#\(suffix)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published private(set) var notes: [Note] = [] // reverse-chron sorted
|
||||||
|
@Published private(set) var isLoading: Bool = true
|
||||||
|
@Published private(set) var geohash: String
|
||||||
|
private var subscriptionID: String?
|
||||||
|
private var loadingWorkItem: DispatchWorkItem?
|
||||||
|
private var loadingStartedAt: Date?
|
||||||
|
private var loadingMinDuration: TimeInterval = 0
|
||||||
|
|
||||||
|
init(geohash: String) {
|
||||||
|
self.geohash = geohash.lowercased()
|
||||||
|
subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setGeohash(_ newGeohash: String) {
|
||||||
|
let norm = newGeohash.lowercased()
|
||||||
|
guard norm != geohash else { return }
|
||||||
|
if let sub = subscriptionID {
|
||||||
|
NostrRelayManager.shared.unsubscribe(id: sub)
|
||||||
|
}
|
||||||
|
geohash = norm
|
||||||
|
notes.removeAll()
|
||||||
|
subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func subscribe() {
|
||||||
|
// Begin loading: always display Matrix for a randomized 1–3 seconds
|
||||||
|
isLoading = true
|
||||||
|
loadingStartedAt = Date()
|
||||||
|
loadingMinDuration = Double.random(in: 1.0...3.0)
|
||||||
|
loadingWorkItem?.cancel()
|
||||||
|
let subID = "locnotes-\(geohash)-\(UUID().uuidString.prefix(8))"
|
||||||
|
subscriptionID = subID
|
||||||
|
let lookback = Date().addingTimeInterval(-TransportConfig.nostrGeohashInitialLookbackSeconds)
|
||||||
|
let filter = NostrFilter.geohashNotes(geohash, since: lookback, limit: 200)
|
||||||
|
let relays = GeoRelayDirectory.shared.closestRelays(toGeohash: geohash, count: TransportConfig.nostrGeoRelayCount)
|
||||||
|
NostrRelayManager.shared.subscribe(filter: filter, id: subID, relayUrls: relays) { [weak self] event in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard event.kind == NostrProtocol.EventKind.textNote.rawValue else { return }
|
||||||
|
// Ensure matching tag
|
||||||
|
guard event.tags.contains(where: { $0.count >= 2 && $0[0].lowercased() == "g" && $0[1].lowercased() == self.geohash }) else { return }
|
||||||
|
if self.notes.contains(where: { $0.id == event.id }) { return }
|
||||||
|
let nick = event.tags.first(where: { $0.first?.lowercased() == "n" && $0.count >= 2 })?.dropFirst().first
|
||||||
|
let ts = Date(timeIntervalSince1970: TimeInterval(event.created_at))
|
||||||
|
let note = Note(id: event.id, pubkey: event.pubkey, content: event.content, createdAt: ts, nickname: nick)
|
||||||
|
self.notes.append(note)
|
||||||
|
self.notes.sort { $0.createdAt > $1.createdAt }
|
||||||
|
// Respect minimum loader time
|
||||||
|
if self.isLoading {
|
||||||
|
let elapsed = Date().timeIntervalSince(self.loadingStartedAt ?? Date())
|
||||||
|
if elapsed >= self.loadingMinDuration {
|
||||||
|
self.isLoading = false
|
||||||
|
} else {
|
||||||
|
self.loadingWorkItem?.cancel()
|
||||||
|
let remaining = self.loadingMinDuration - elapsed
|
||||||
|
let wi = DispatchWorkItem { [weak self] in self?.isLoading = false }
|
||||||
|
self.loadingWorkItem = wi
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + remaining, execute: wi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Ensure we hide after the minimum loader duration even if no events arrive
|
||||||
|
let wi = DispatchWorkItem { [weak self] in self?.isLoading = false }
|
||||||
|
loadingWorkItem = wi
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + loadingMinDuration, execute: wi)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a location note for the current geohash using the per-geohash identity.
|
||||||
|
func send(content: String, nickname: String) {
|
||||||
|
let trimmed = content.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !trimmed.isEmpty else { return }
|
||||||
|
do {
|
||||||
|
let id = try NostrIdentityBridge.deriveIdentity(forGeohash: geohash)
|
||||||
|
let event = try NostrProtocol.createGeohashTextNote(
|
||||||
|
content: trimmed,
|
||||||
|
geohash: geohash,
|
||||||
|
senderIdentity: id,
|
||||||
|
nickname: nickname,
|
||||||
|
teleported: LocationChannelManager.shared.teleported
|
||||||
|
)
|
||||||
|
let relays = GeoRelayDirectory.shared.closestRelays(toGeohash: geohash, count: TransportConfig.nostrGeoRelayCount)
|
||||||
|
NostrRelayManager.shared.sendEvent(event, to: relays)
|
||||||
|
// Optimistic local-echo
|
||||||
|
let echo = Note(id: event.id, pubkey: id.publicKeyHex, content: trimmed, createdAt: Date(), nickname: nickname)
|
||||||
|
self.notes.insert(echo, at: 0)
|
||||||
|
if self.isLoading {
|
||||||
|
let elapsed = Date().timeIntervalSince(self.loadingStartedAt ?? Date())
|
||||||
|
if elapsed >= self.loadingMinDuration {
|
||||||
|
self.isLoading = false
|
||||||
|
} else {
|
||||||
|
self.loadingWorkItem?.cancel()
|
||||||
|
let remaining = self.loadingMinDuration - elapsed
|
||||||
|
let wi = DispatchWorkItem { [weak self] in self?.isLoading = false }
|
||||||
|
self.loadingWorkItem = wi
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + remaining, execute: wi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
SecureLogger.error("LocationNotesManager: failed to send note: \(error)", category: .session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Explicitly cancel subscription and release resources.
|
||||||
|
func cancel() {
|
||||||
|
if let sub = subscriptionID {
|
||||||
|
NostrRelayManager.shared.unsubscribe(id: sub)
|
||||||
|
subscriptionID = nil
|
||||||
|
}
|
||||||
|
loadingWorkItem?.cancel()
|
||||||
|
loadingWorkItem = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,6 +49,7 @@ struct ContentView: View {
|
|||||||
@State private var showLocationChannelsSheet = false
|
@State private var showLocationChannelsSheet = false
|
||||||
@State private var showVerifySheet = false
|
@State private var showVerifySheet = false
|
||||||
@State private var expandedMessageIDs: Set<String> = []
|
@State private var expandedMessageIDs: Set<String> = []
|
||||||
|
@State private var showLocationNotes = false
|
||||||
// Window sizes for rendering (infinite scroll up)
|
// Window sizes for rendering (infinite scroll up)
|
||||||
@State private var windowCountPublic: Int = 300
|
@State private var windowCountPublic: Int = 300
|
||||||
@State private var windowCountPrivate: [String: Int] = [:]
|
@State private var windowCountPrivate: [String: Int] = [:]
|
||||||
@@ -1144,6 +1145,16 @@ struct ContentView: View {
|
|||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.accessibilityLabel("Toggle bookmark for #\(ch.geohash)")
|
.accessibilityLabel("Toggle bookmark for #\(ch.geohash)")
|
||||||
}
|
}
|
||||||
|
// Notes icon (mesh only), left of channel badge
|
||||||
|
if case .mesh = locationManager.selectedChannel {
|
||||||
|
Button(action: { showLocationNotes = true }) {
|
||||||
|
Image(systemName: "note.text")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundColor(Color(hue: 0.60, saturation: 0.85, brightness: 0.82))
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.accessibilityLabel("Location notes for this place")
|
||||||
|
}
|
||||||
// Location channels button '#'
|
// Location channels button '#'
|
||||||
Button(action: { showLocationChannelsSheet = true }) {
|
Button(action: { showLocationChannelsSheet = true }) {
|
||||||
let badgeText: String = {
|
let badgeText: String = {
|
||||||
@@ -1201,6 +1212,28 @@ struct ContentView: View {
|
|||||||
.onAppear { viewModel.isLocationChannelsSheetPresented = true }
|
.onAppear { viewModel.isLocationChannelsSheetPresented = true }
|
||||||
.onDisappear { viewModel.isLocationChannelsSheetPresented = false }
|
.onDisappear { viewModel.isLocationChannelsSheetPresented = false }
|
||||||
}
|
}
|
||||||
|
.sheet(isPresented: $showLocationNotes) {
|
||||||
|
// Determine current block-level geohash for this place
|
||||||
|
if let block = LocationChannelManager.shared.availableChannels.first(where: { $0.level == .block })?.geohash {
|
||||||
|
LocationNotesView(geohash: block)
|
||||||
|
.environmentObject(viewModel)
|
||||||
|
} else {
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
Text("location unavailable")
|
||||||
|
.font(.system(size: 16, weight: .bold, design: .monospaced))
|
||||||
|
Text("enable location to view notes for your street-level area")
|
||||||
|
.font(.system(size: 12, design: .monospaced))
|
||||||
|
.foregroundColor(secondaryTextColor)
|
||||||
|
Button("enable location") {
|
||||||
|
LocationChannelManager.shared.enableLocationChannels()
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
}
|
||||||
|
.padding(20)
|
||||||
|
.background(backgroundColor)
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
.alert("heads up", isPresented: $viewModel.showScreenshotPrivacyWarning) {
|
.alert("heads up", isPresented: $viewModel.showScreenshotPrivacyWarning) {
|
||||||
Button("ok", role: .cancel) {}
|
Button("ok", role: .cancel) {}
|
||||||
} message: {
|
} message: {
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct LocationNotesView: View {
|
||||||
|
@EnvironmentObject var viewModel: ChatViewModel
|
||||||
|
@ObservedObject var manager: LocationNotesManager
|
||||||
|
let geohash: String
|
||||||
|
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
@State private var draft: String = ""
|
||||||
|
|
||||||
|
init(geohash: String) {
|
||||||
|
self.geohash = geohash.lowercased()
|
||||||
|
self.manager = LocationNotesManager(geohash: self.geohash)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var backgroundColor: Color {
|
||||||
|
colorScheme == .dark ? Color.black : Color.white
|
||||||
|
}
|
||||||
|
private var textColor: Color {
|
||||||
|
colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
|
||||||
|
}
|
||||||
|
private var secondaryTextColor: Color {
|
||||||
|
colorScheme == .dark ? Color.green.opacity(0.8) : Color(red: 0, green: 0.5, blue: 0).opacity(0.8)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
header
|
||||||
|
Divider()
|
||||||
|
list
|
||||||
|
Divider()
|
||||||
|
input
|
||||||
|
}
|
||||||
|
if manager.isLoading {
|
||||||
|
MatrixRainView()
|
||||||
|
.transition(.opacity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(backgroundColor)
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
.onDisappear { manager.cancel() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private var header: some View {
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text("notes @ #\(geohash)")
|
||||||
|
.font(.system(size: 16, weight: .bold, design: .monospaced))
|
||||||
|
Text("street-level notes")
|
||||||
|
.font(.system(size: 12, design: .monospaced))
|
||||||
|
.foregroundColor(secondaryTextColor)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.frame(height: 44)
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.background(backgroundColor.opacity(0.95))
|
||||||
|
}
|
||||||
|
|
||||||
|
private var list: some View {
|
||||||
|
ScrollView {
|
||||||
|
LazyVStack(alignment: .leading, spacing: 8) {
|
||||||
|
ForEach(manager.notes) { note in
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Text(note.displayName)
|
||||||
|
.font(.system(size: 12, weight: .semibold, design: .monospaced))
|
||||||
|
.foregroundColor(secondaryTextColor)
|
||||||
|
Text(note.createdAt, style: .relative)
|
||||||
|
.font(.system(size: 11, design: .monospaced))
|
||||||
|
.foregroundColor(secondaryTextColor.opacity(0.8))
|
||||||
|
}
|
||||||
|
Text(note.content)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
.background(backgroundColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var input: some View {
|
||||||
|
HStack(alignment: .center, spacing: 8) {
|
||||||
|
TextField("add a note for this place", text: $draft, axis: .vertical)
|
||||||
|
.textFieldStyle(.plain)
|
||||||
|
.font(.system(size: 14, design: .monospaced))
|
||||||
|
.lineLimit(3, reservesSpace: true)
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
|
||||||
|
Button(action: send) {
|
||||||
|
Image(systemName: "arrow.up.circle.fill")
|
||||||
|
.font(.system(size: 20))
|
||||||
|
.foregroundColor(draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? Color.gray : textColor)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.disabled(draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
||||||
|
.padding(.trailing, 12)
|
||||||
|
}
|
||||||
|
.frame(minHeight: 44)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.background(backgroundColor.opacity(0.95))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func send() {
|
||||||
|
let content = draft.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !content.isEmpty else { return }
|
||||||
|
manager.send(content: content, nickname: viewModel.nickname)
|
||||||
|
draft = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Matrix Rain Loader
|
||||||
|
private struct MatrixRainView: View {
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
private var fg: Color { colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0) }
|
||||||
|
private let charset = Array("01abcdefghijklmnopqrstuvwxyzアイウエオカキクケコハヒフヘホ0123456789")
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
GeometryReader { geo in
|
||||||
|
let w = max(geo.size.width, 1)
|
||||||
|
let h = max(geo.size.height, 1)
|
||||||
|
let colWidth: CGFloat = 14
|
||||||
|
let cols = max(Int(w / colWidth), 6)
|
||||||
|
ZStack {
|
||||||
|
ForEach(0..<cols, id: \.self) { i in
|
||||||
|
RainColumn(charset: charset,
|
||||||
|
columnWidth: colWidth,
|
||||||
|
height: h,
|
||||||
|
speed: Double.random(in: 2.8...4.8),
|
||||||
|
delay: Double.random(in: 0...1.2),
|
||||||
|
color: fg)
|
||||||
|
.frame(width: colWidth)
|
||||||
|
.position(x: CGFloat(i) * colWidth + colWidth/2, y: h/2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(Color.black.opacity(colorScheme == .dark ? 0.75 : 0.65))
|
||||||
|
.overlay(
|
||||||
|
VStack(spacing: 6) {
|
||||||
|
Text("loading notes…")
|
||||||
|
.font(.system(size: 12, weight: .semibold, design: .monospaced))
|
||||||
|
.foregroundColor(fg)
|
||||||
|
.padding(.top, 12)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.ignoresSafeArea(edges: .bottom)
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct RainColumn: View {
|
||||||
|
let charset: [Character]
|
||||||
|
let columnWidth: CGFloat
|
||||||
|
let height: CGFloat
|
||||||
|
let speed: Double
|
||||||
|
let delay: Double
|
||||||
|
let color: Color
|
||||||
|
@State private var y: CGFloat = 0
|
||||||
|
@State private var glyphs: [String] = []
|
||||||
|
@State private var timer: Timer?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let font = Font.system(size: 12, weight: .regular, design: .monospaced)
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
ForEach(Array(glyphs.enumerated()), id: \.offset) { idx, ch in
|
||||||
|
Text(ch)
|
||||||
|
.font(font)
|
||||||
|
.foregroundColor(color.opacity(opacity(for: idx)))
|
||||||
|
.frame(width: columnWidth, alignment: .center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.offset(y: y)
|
||||||
|
.onAppear {
|
||||||
|
let count = max(Int(height / 14) + 12, 24)
|
||||||
|
glyphs = randomGlyphs(count)
|
||||||
|
startGlyphTimer()
|
||||||
|
y = -height
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
||||||
|
withAnimation(.linear(duration: speed).repeatForever(autoreverses: false)) {
|
||||||
|
y = height * 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDisappear { stopGlyphTimer() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func opacity(for idx: Int) -> Double {
|
||||||
|
let p = Double(idx) / Double(max(glyphs.count, 1))
|
||||||
|
return 0.15 + (1.0 - p) * 0.85
|
||||||
|
}
|
||||||
|
|
||||||
|
private func randomGlyphs(_ n: Int) -> [String] {
|
||||||
|
(0..<n).map { _ in String(charset.randomElement() ?? "0") }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startGlyphTimer() {
|
||||||
|
stopGlyphTimer()
|
||||||
|
timer = Timer.scheduledTimer(withTimeInterval: 0.20, repeats: true) { _ in
|
||||||
|
if glyphs.isEmpty { return }
|
||||||
|
let changes = Int.random(in: 1...3)
|
||||||
|
for _ in 0..<changes {
|
||||||
|
let idx = Int.random(in: 0..<glyphs.count)
|
||||||
|
glyphs[idx] = String(charset.randomElement() ?? "1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopGlyphTimer() {
|
||||||
|
timer?.invalidate()
|
||||||
|
timer = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user