From 95a1c62b5456231cf3ca3a19f740a948bfa95674 Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Sun, 27 Sep 2026 21:43:08 +0700 Subject: [PATCH] some fixes --- Config/Luma-Info.plist | 13 +++++ Luma.xcodeproj/project.pbxproj | 26 +++++----- .../xcshareddata/xcschemes/LumaWatch.xcscheme | 2 - README.md | 1 + Sources/Shared/Models/AppModel.swift | 42 +++++++++++----- .../Models/VideoNoteRotationPolicy.swift | 3 +- .../Shared/Services/PhoneWatchBridge.swift | 4 +- .../Shared/Services/VideoNoteRecorder.swift | 10 ++-- Sources/Shared/UI/ChatView.swift | 49 +++++++++++++++++-- .../Components/LocationMessagePreview.swift | 2 +- Sources/Shared/XMPP/LumaOMEMO2Module.swift | 7 +-- .../Shared/XMPP/LumaSaslFailureModule.swift | 2 +- .../Shared/XMPP/LumaScramPlusMechanism.swift | 2 +- .../Shared/XMPP/LumaTLSNetworkProcessor.swift | 6 +-- ThirdParty/MartinOMEMO/Package.swift | 7 ++- project.yml | 12 +++++ 16 files changed, 137 insertions(+), 51 deletions(-) diff --git a/Config/Luma-Info.plist b/Config/Luma-Info.plist index 68eb2c3..8914c70 100644 --- a/Config/Luma-Info.plist +++ b/Config/Luma-Info.plist @@ -44,6 +44,19 @@ UILaunchScreen + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + UISupportsDocumentBrowser diff --git a/Luma.xcodeproj/project.pbxproj b/Luma.xcodeproj/project.pbxproj index 06aac64..5b3fee5 100644 --- a/Luma.xcodeproj/project.pbxproj +++ b/Luma.xcodeproj/project.pbxproj @@ -7,10 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - AB12CD34EF56010203040503 /* ChatMessageEntrancePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB12CD34EF56010203040501 /* ChatMessageEntrancePolicy.swift */; }; - AB12CD34EF56010203040504 /* ChatMessageEntrancePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB12CD34EF56010203040501 /* ChatMessageEntrancePolicy.swift */; }; - AB12CD34EF56010203040505 /* MessageBubbleEntrance.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB12CD34EF56010203040502 /* MessageBubbleEntrance.swift */; }; - AB12CD34EF56010203040506 /* MessageBubbleEntrance.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB12CD34EF56010203040502 /* MessageBubbleEntrance.swift */; }; 00FCAB7210D005D2C30747CB /* AccountConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2006464EAC88C6E4E2B08AC8 /* AccountConfiguration.swift */; }; 01039E013AB84FA338B2A5CF /* MessageCorrectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 096748EF4DC92889BCAB3260 /* MessageCorrectionTests.swift */; }; 01E4131ED4DBED0BEAD465A2 /* ServerInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBA20B768A9C675FABCDC821 /* ServerInfoView.swift */; }; @@ -79,6 +75,7 @@ 32790D95A577DCE99937B539 /* AccountPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 235CA66C5DFB8B6F9472D0C3 /* AccountPreferences.swift */; }; 32E78EE5035DD08181B50B86 /* VideoNoteRotationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C986DC0BAE0D11816C25E782 /* VideoNoteRotationPolicy.swift */; }; 3375874DFCE22B22879112DD /* SystemPhotoCameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5198FF1E47790E498CC8C6F3 /* SystemPhotoCameraView.swift */; }; + 33EF454D77366A0E9137D824 /* MessageBubbleEntrance.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1180AA3C5AC03BB48393D97 /* MessageBubbleEntrance.swift */; }; 39C3E7AC663AB6C8DACFFAA8 /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = A99B4C1B1459E1C34F214164 /* OpenSSL */; }; 3BA375F1E0934FB4DD2148F9 /* SASLprep.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2446F6A1D00ADAD261D4CCD /* SASLprep.swift */; }; 3BEF3895B4C84880B9103058 /* NewChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43A4CE3E678096BA76F2988C /* NewChatView.swift */; }; @@ -104,12 +101,14 @@ 49DC659401787CF44A33B620 /* LocationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C67A8A07F9DC9DFB7FB35DA /* LocationProvider.swift */; }; 4A3844CC51129576741753E7 /* MediaPlaybackCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26ADAEFB7CB8441CA1A50C44 /* MediaPlaybackCoordinator.swift */; }; 4B2CFA4008D1234DCE35ADDE /* MediaSendActivityTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 247FBE680C61D12AFB583862 /* MediaSendActivityTrackerTests.swift */; }; + 4B65093C19D148E04A675440 /* ChatMessageEntrancePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B311D6C0234E1C910457FCE0 /* ChatMessageEntrancePolicy.swift */; }; 4CD33DF0D193A7EED84A673F /* CallSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6079CE75BAB995515A8D5460 /* CallSnapshot.swift */; }; 4DE3B27CFA3D524590FB751A /* AppLockPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFFA3AEE3D9FB450B77AEC10 /* AppLockPolicyTests.swift */; }; 4E1B116771922F92FEEB079B /* ArchiveStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BA0E25A2B52BDED1C8B3B1B /* ArchiveStore.swift */; }; 4EFADFE085B42E0B0593E501 /* ArchiveSyncCheckpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9BBE412EAF54DFE968B2E54 /* ArchiveSyncCheckpoint.swift */; }; 51AE33BE56ED4543014119D0 /* ConversationRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 758B448979C845F56168A51F /* ConversationRow.swift */; }; 523D54621DD4DDD54EA8D510 /* MediaDownloadService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB613D3331D85529B5F989D0 /* MediaDownloadService.swift */; }; + 52CA5DEE0CF7697D1B2D1FEA /* MessageBubbleEntrance.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1180AA3C5AC03BB48393D97 /* MessageBubbleEntrance.swift */; }; 52F75F60459DA50E1815479A /* MediaPreviewProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0E3C6F60C3D4D6C565CA0A /* MediaPreviewProcessor.swift */; }; 5409F5C90FBE0817F4AC8176 /* ChatTimelineEntryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90C426133ECC0319483A084 /* ChatTimelineEntryTests.swift */; }; 5415AF453AB8FCDA55FB59AD /* LumaCallEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72EC7DC2FE873C0BA4321496 /* LumaCallEngine.swift */; }; @@ -128,6 +127,7 @@ 64C8B9876278B711CC6479E0 /* LocationMessagePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = B96E7970930AB7F09CB5DA9C /* LocationMessagePreview.swift */; }; 64C9A8E42046AC0FCB701613 /* TLSVersionProbe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 186BB225C4C5E528B3D7C9CE /* TLSVersionProbe.swift */; }; 64F4AD83D86B8900DE69F117 /* CredentialVault.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCE381301A55B1C4F333B523 /* CredentialVault.swift */; }; + 65D2B572D508EF10931420FD /* ChatMessageEntrancePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B311D6C0234E1C910457FCE0 /* ChatMessageEntrancePolicy.swift */; }; 662B8E72F00F5A77DE28819A /* SCRAMPlusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21D03F8C05843DD139C1A4CE /* SCRAMPlusTests.swift */; }; 66489D4D15366683D3095B3D /* ChatMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12C6A6B83DA076867481CCF3 /* ChatMessage.swift */; }; 66613FF7E056788222552D6C /* NotificationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E25F876C75CC5FCDDCB5906 /* NotificationPolicy.swift */; }; @@ -336,8 +336,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - AB12CD34EF56010203040501 /* ChatMessageEntrancePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageEntrancePolicy.swift; sourceTree = ""; }; - AB12CD34EF56010203040502 /* MessageBubbleEntrance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBubbleEntrance.swift; sourceTree = ""; }; 0142FF4A1B05C373DAB4CDBE /* VideoNoteCaptureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoNoteCaptureView.swift; sourceTree = ""; }; 0386C2687C1C60EC734FDBB0 /* BubbleShapes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleShapes.swift; sourceTree = ""; }; 03AC6BC7FC7CFFD69A20DA17 /* MessageBubble.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBubble.swift; sourceTree = ""; }; @@ -459,6 +457,7 @@ B1E6875B522254FFB5FA880E /* MediaPickerSelectionPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPickerSelectionPolicy.swift; sourceTree = ""; }; B2446F6A1D00ADAD261D4CCD /* SASLprep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SASLprep.swift; sourceTree = ""; }; B2DC8B629E8D4F078B192FAB /* MartinOMEMO */ = {isa = PBXFileReference; lastKnownFileType = folder; name = MartinOMEMO; path = ThirdParty/MartinOMEMO; sourceTree = SOURCE_ROOT; }; + B311D6C0234E1C910457FCE0 /* ChatMessageEntrancePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageEntrancePolicy.swift; sourceTree = ""; }; B38702A403DA3E943525FB62 /* MediaPickerSelectionPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPickerSelectionPolicyTests.swift; sourceTree = ""; }; B8F7B01C59E9667ADAFABBAC /* MediaViewerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaViewerItem.swift; sourceTree = ""; }; B96E7970930AB7F09CB5DA9C /* LocationMessagePreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationMessagePreview.swift; sourceTree = ""; }; @@ -493,6 +492,7 @@ D9FCE089ABBF05EF9F55D451 /* VideoNoteRecordingLifecycleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoNoteRecordingLifecycleTests.swift; sourceTree = ""; }; DCE53C6D5BAE984F218BFCFB /* ReadStateSync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadStateSync.swift; sourceTree = ""; }; DF8C531E464C9C4EDE2648C6 /* Luma.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Luma.app; sourceTree = BUILT_PRODUCTS_DIR; }; + E1180AA3C5AC03BB48393D97 /* MessageBubbleEntrance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBubbleEntrance.swift; sourceTree = ""; }; E619B2C85B0DD2E80653D6D3 /* VideoNoteRecordingCompletionPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoNoteRecordingCompletionPolicy.swift; sourceTree = ""; }; E6A5C32B4DACD56DA733A0DB /* ServerInformation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerInformation.swift; sourceTree = ""; }; E9F226A1F8D82B39630F9CEB /* LumaRoomStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LumaRoomStore.swift; sourceTree = ""; }; @@ -575,7 +575,7 @@ C04D749B7CA93CB7C59127D6 /* AttachmentDraft.swift */, 6079CE75BAB995515A8D5460 /* CallSnapshot.swift */, 12C6A6B83DA076867481CCF3 /* ChatMessage.swift */, - AB12CD34EF56010203040501 /* ChatMessageEntrancePolicy.swift */, + B311D6C0234E1C910457FCE0 /* ChatMessageEntrancePolicy.swift */, C37AAE886AA3B8782B89E1A0 /* ChatScrollPositionPolicy.swift */, 13E35F7F0CB0243311FCDD61 /* ChatTimelineEntry.swift */, 1C3E51BB890F05FD02C71F6E /* ChatTypingPolicy.swift */, @@ -823,7 +823,7 @@ B96E7970930AB7F09CB5DA9C /* LocationMessagePreview.swift */, 4380219D3AF1D75EDF4D3167 /* MediaViewer.swift */, 03AC6BC7FC7CFFD69A20DA17 /* MessageBubble.swift */, - AB12CD34EF56010203040502 /* MessageBubbleEntrance.swift */, + E1180AA3C5AC03BB48393D97 /* MessageBubbleEntrance.swift */, 8D2294F0FE44B2268A0620BB /* PhotoAttachmentPreview.swift */, 19C556A41E640592AF6376B6 /* ReplyThreadOverlay.swift */, 5003B4F9DF154543555E9825 /* RTCVideoRendererView.swift */, @@ -1062,7 +1062,7 @@ 97A3A0635ADA8A8227C94216 /* ChatBubbleShape.swift in Sources */, 7AC93485DA12BC90717739E9 /* ChatMediaImageCache.swift in Sources */, 66489D4D15366683D3095B3D /* ChatMessage.swift in Sources */, - AB12CD34EF56010203040503 /* ChatMessageEntrancePolicy.swift in Sources */, + 65D2B572D508EF10931420FD /* ChatMessageEntrancePolicy.swift in Sources */, D5E6B1859101ABBD33D90C3C /* ChatScrollPositionPolicy.swift in Sources */, 1E239B9338CEEA7EA704626C /* ChatTimelineEntry.swift in Sources */, 433A7B65E635FB59021F7D87 /* ChatTypingPolicy.swift in Sources */, @@ -1109,7 +1109,7 @@ B33870CD89289E44882750C3 /* MediaViewer.swift in Sources */, EBAF4861DD93FBEAA4B37D1C /* MediaViewerItem.swift in Sources */, 230E0DAA5396EA507D6F6D47 /* MessageBubble.swift in Sources */, - AB12CD34EF56010203040505 /* MessageBubbleEntrance.swift in Sources */, + 33EF454D77366A0E9137D824 /* MessageBubbleEntrance.swift in Sources */, FD9F8640841236BB78563573 /* MessageReaction.swift in Sources */, 30BE19F459FDDAD640F66C68 /* MessageReplyFallback.swift in Sources */, 94FB3597E01FC508F872861E /* MessageReplySwipePolicy.swift in Sources */, @@ -1190,7 +1190,7 @@ 2A7611650AB52EDABB978362 /* ChatBubbleShape.swift in Sources */, 6FCF6A4EFF8AE1FBFC2D81E1 /* ChatMediaImageCache.swift in Sources */, 8714A5A67956D6A78D4D3E4B /* ChatMessage.swift in Sources */, - AB12CD34EF56010203040504 /* ChatMessageEntrancePolicy.swift in Sources */, + 4B65093C19D148E04A675440 /* ChatMessageEntrancePolicy.swift in Sources */, 8B98B818AC4379F03907B0E0 /* ChatScrollPositionPolicy.swift in Sources */, 7F7BA6805C1D1EBCEAFDD3AD /* ChatTimelineEntry.swift in Sources */, B4AA654DF3A53AAAAC23915D /* ChatTypingPolicy.swift in Sources */, @@ -1237,7 +1237,7 @@ CC5297F90A3DBD45C5E41A3A /* MediaViewer.swift in Sources */, 2C5BAAEF4EC7AAE5FE0FEEDB /* MediaViewerItem.swift in Sources */, A90142509385DE6E83818953 /* MessageBubble.swift in Sources */, - AB12CD34EF56010203040506 /* MessageBubbleEntrance.swift in Sources */, + 52CA5DEE0CF7697D1B2D1FEA /* MessageBubbleEntrance.swift in Sources */, ECCD95DD4E16E0B393C707A9 /* MessageReaction.swift in Sources */, C2CC9BABAE68FA9258694766 /* MessageReplyFallback.swift in Sources */, D4E1CF1F7D948B2E72C063BE /* MessageReplySwipePolicy.swift in Sources */, @@ -1423,6 +1423,7 @@ PRODUCT_NAME = Luma; SDKROOT = iphoneos; SUPPORTS_MACCATALYST = NO; + SWIFT_SUPPRESS_WARNINGS = YES; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -1682,6 +1683,7 @@ PRODUCT_NAME = Luma; SDKROOT = iphoneos; SUPPORTS_MACCATALYST = NO; + SWIFT_SUPPRESS_WARNINGS = YES; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; diff --git a/Luma.xcodeproj/xcshareddata/xcschemes/LumaWatch.xcscheme b/Luma.xcodeproj/xcshareddata/xcschemes/LumaWatch.xcscheme index 59cd919..05effce 100644 --- a/Luma.xcodeproj/xcshareddata/xcschemes/LumaWatch.xcscheme +++ b/Luma.xcodeproj/xcshareddata/xcschemes/LumaWatch.xcscheme @@ -55,7 +55,6 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "BAB78463D7DE07A8F7B3D27D" BuildableName = "LumaWatch.app" - BlueprintName = "LumaWatch" ReferencedContainer = "container:Luma.xcodeproj"> @@ -72,7 +71,6 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "BAB78463D7DE07A8F7B3D27D" BuildableName = "LumaWatch.app" - BlueprintName = "LumaWatch" ReferencedContainer = "container:Luma.xcodeproj"> diff --git a/README.md b/README.md index 9f55990..1255f8a 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ Luma — рабочий MVP XMPP-клиента для iOS, iPadOS, macOS и App iPhone, iPad, Mac и Apple Watch; один и тот же catalog явно подключён ко всем app-target’ам, а имя закреплено в build settings и Info.plist; - адаптивный SwiftUI-интерфейс для iPhone, iPad и Mac; +- поддержка портретной и альбомной ориентации на iPhone и всех четырёх ориентаций на iPad; - companion-приложение watchOS: последние чаты, диктовка/ответы, а также запись голосового до 60 секунд с отменой и предпрослушиванием; `.m4a` передаётся на iPhone фоновой файловой очередью WatchConnectivity, после чего iPhone diff --git a/Sources/Shared/Models/AppModel.swift b/Sources/Shared/Models/AppModel.swift index 7c05709..dc822c8 100644 --- a/Sources/Shared/Models/AppModel.swift +++ b/Sources/Shared/Models/AppModel.swift @@ -2485,26 +2485,44 @@ final class AppModel: ObservableObject { stashPendingReadMarker(envelope, cameFromPeer: cameFromPeer) } - /// Promotes a matching outgoing message to `.read` and, when the receipt - /// came from the peer (not from another own device), broadcasts the state - /// to the user's other devices via Carbons/MAM. Failed messages are never + /// Promotes the matching outgoing message and every earlier outgoing + /// message in the same conversation to `.read`. XEP-0333 markers are + /// cumulative in practice: a marker for message N means the peer has + /// displayed the whole conversation through N. Failed messages are never /// promoted by a receipt. private func markRead(at index: Int, cameFromPeer: Bool) { - let message = messages[index] - guard message.direction == .outgoing, - message.delivery != .read, - message.delivery != .failed, - !message.isRetracted + guard messages.indices.contains(index) else { return } + let target = messages[index] + guard target.direction == .outgoing, + target.delivery != .failed, + !target.isRetracted else { return } - messages[index].delivery = .read + + let targetConversation = target.conversationID + var changed = false + for candidateIndex in messages.indices { + let candidate = messages[candidateIndex] + guard candidate.conversationID == targetConversation, + candidate.direction == .outgoing, + candidate.delivery != .read, + candidate.delivery != .failed, + !candidate.isRetracted, + candidate.timestamp < target.timestamp + || (candidate.timestamp == target.timestamp + && candidate.clientID <= target.clientID) + else { continue } + messages[candidateIndex].delivery = .read + changed = true + } + guard changed else { return } schedulePersist() guard cameFromPeer else { return } xmpp.syncReadState( ReadStateSync.Envelope( id: UUID().uuidString, - conversationJID: message.conversationID, - messageID: message.originID ?? message.clientID, - stanzaID: message.stanzaID, + conversationJID: target.conversationID, + messageID: target.originID ?? target.clientID, + stanzaID: target.stanzaID, timestamp: Date() )) } diff --git a/Sources/Shared/Models/VideoNoteRotationPolicy.swift b/Sources/Shared/Models/VideoNoteRotationPolicy.swift index 78e438e..d811a41 100644 --- a/Sources/Shared/Models/VideoNoteRotationPolicy.swift +++ b/Sources/Shared/Models/VideoNoteRotationPolicy.swift @@ -31,11 +31,12 @@ enum VideoNoteRotationPolicy { /// sensor (UIDevice.current.orientation) can report landscape or /// face-up while the app stays portrait, which is exactly how a /// portrait circle ended up encoded sideways. + @MainActor static var currentInterfaceOrientation: InterfaceOrientation { let orientation = UIApplication.shared.connectedScenes .compactMap { $0 as? UIWindowScene } .first { $0.activationState == .foregroundActive }? - .interfaceOrientation + .effectiveGeometry.interfaceOrientation switch orientation { case .portraitUpsideDown: return .portraitUpsideDown case .landscapeLeft: return .landscapeLeft diff --git a/Sources/Shared/Services/PhoneWatchBridge.swift b/Sources/Shared/Services/PhoneWatchBridge.swift index f84e98c..a23c26b 100644 --- a/Sources/Shared/Services/PhoneWatchBridge.swift +++ b/Sources/Shared/Services/PhoneWatchBridge.swift @@ -50,9 +50,9 @@ struct WatchVoiceMessage: Sendable { } #if os(iOS) && canImport(WatchConnectivity) -import WatchConnectivity +@preconcurrency import WatchConnectivity -final class PhoneWatchBridge: NSObject, WCSessionDelegate { +final class PhoneWatchBridge: NSObject, WCSessionDelegate, @unchecked Sendable { var onReply: ((String, String) -> Void)? var onVoiceMessage: ((WatchVoiceMessage) -> Void)? { didSet { deliverPendingVoiceMessages() } diff --git a/Sources/Shared/Services/VideoNoteRecorder.swift b/Sources/Shared/Services/VideoNoteRecorder.swift index 2a17958..777159f 100644 --- a/Sources/Shared/Services/VideoNoteRecorder.swift +++ b/Sources/Shared/Services/VideoNoteRecorder.swift @@ -1033,7 +1033,7 @@ final class VideoNoteRecorder: NSObject, ObservableObject, AVCaptureFileOutputRe // consistent no matter which camera is recording. let microphone = AVCaptureDevice.default( - .builtInMicrophone, + .microphone, for: .audio, position: .back ) @@ -1194,7 +1194,7 @@ private actor VideoNoteFileInspector { ] ) videoInput.expectsMediaDataInRealTime = false - videoInput.transform = videoTrack.preferredTransform + videoInput.transform = (try? await videoTrack.load(.preferredTransform)) ?? .identity let adaptor = AVAssetWriterInputPixelBufferAdaptor( assetWriterInput: videoInput, sourcePixelBufferAttributes: [ @@ -1397,7 +1397,7 @@ private actor VideoNoteFileInspector { return nil } if !hasVideo { - transform = source.preferredTransform + transform = (try? await source.load(.preferredTransform)) ?? .identity hasVideo = true } cursor = cursor + duration @@ -1431,10 +1431,6 @@ private actor VideoNoteFileInspector { try? FileManager.default.removeItem(at: outputURL) return nil } - guard exporter.status == .completed else { - try? FileManager.default.removeItem(at: outputURL) - return nil - } return Finalization(url: outputURL, duration: composition.duration) } diff --git a/Sources/Shared/UI/ChatView.swift b/Sources/Shared/UI/ChatView.swift index 4af8fb4..6bbc99d 100644 --- a/Sources/Shared/UI/ChatView.swift +++ b/Sources/Shared/UI/ChatView.swift @@ -27,6 +27,7 @@ private enum EmojiPickerPresentation: Identifiable { struct ChatView: View { private static let bottomAnchorID = "luma-chat-timeline-bottom" private static let timelineCoordinateSpace = "luma-chat-timeline-space" + private static let localHistoryPageSize = 60 @ObservedObject var model: AppModel let conversation: Conversation @@ -75,6 +76,8 @@ struct ChatView: View { @State private var captureSuspendsArchiveSync = false @State private var pickerResetToken = UUID() @State private var timelineEntries: [ChatTimelineEntry] = [] + @State private var visibleLocalMessageCount = 0 + @State private var localHistoryExhausted = false /// Messages-style entrance state: which rows currently animate, which IDs /// the timeline already showed, and whether the opening cascade of this /// conversation has played. @@ -150,6 +153,8 @@ struct ChatView: View { _captureSuspendsArchiveSync = State(initialValue: false) _pickerResetToken = State(initialValue: UUID()) _timelineEntries = State<[ChatTimelineEntry]>(initialValue: []) + _visibleLocalMessageCount = State(initialValue: 0) + _localHistoryExhausted = State(initialValue: false) _entranceModes = State<[String: MessageBubbleEntrance.Mode]>( initialValue: [:] ) @@ -389,6 +394,8 @@ struct ChatView: View { historyTopTriggerVisible = false historyAutoContinueCount = 0 emptyHistoryRetryCount = 0 + visibleLocalMessageCount = 0 + localHistoryExhausted = false entranceModes = [:] knownEntranceIDs = [] hasPlayedOpeningEntrance = false @@ -495,8 +502,21 @@ struct ChatView: View { private func rebuildTimelineEntries(from newMessages: [ChatMessage]? = nil) { let source = newMessages ?? messages - timelineEntries = ChatTimelineEntry.make(from: source) - updateEntranceModes(for: source) + if visibleLocalMessageCount == 0 { + visibleLocalMessageCount = min(Self.localHistoryPageSize, source.count) + } else if localHistoryExhausted { + visibleLocalMessageCount = source.count + } else { + visibleLocalMessageCount = min(visibleLocalMessageCount, source.count) + } + localHistoryExhausted = visibleLocalMessageCount >= source.count + let visible = Array(source.suffix(visibleLocalMessageCount)) + timelineEntries = ChatTimelineEntry.make(from: visible) + updateEntranceModes(for: visible) + } + + private var hasMoreLocalHistory: Bool { + visibleLocalMessageCount < messages.count } /// Decides which rows play a Messages-style entrance after a rebuild. @@ -588,7 +608,7 @@ struct ChatView: View { ScrollView { LazyVStack(spacing: 5) { if hasCompletedInitialScroll, - model.hasMoreOlderHistory, + (hasMoreLocalHistory || model.hasMoreOlderHistory), !timelineEntries.isEmpty { Color.clear @@ -625,7 +645,7 @@ struct ChatView: View { .onAppear { // Empty timeline has no scroll gesture, so load // the first page directly when it appears. - guard model.hasMoreOlderHistory, + guard hasMoreLocalHistory || model.hasMoreOlderHistory, !model.isLoadingOlderHistory else { return } model @@ -815,6 +835,17 @@ struct ChatView: View { selectedMessageIDs.formIntersection( Set(timelineEntries.map(\.id)) ) + guard historyLoadAnchorID != nil, + !model.isLoadingOlderHistory, + historyTopTriggerVisible, + let anchor = historyLoadAnchorID + else { return } + Task { @MainActor in + await Task.yield() + proxy.scrollTo(anchor, anchor: .top) + historyLoadAnchorID = nil + historyTopTriggerArmed = true + } } if hasCompletedInitialScroll, @@ -1995,6 +2026,16 @@ struct ChatView: View { historyTopTriggerArmed = false historyLoadAnchorID = timelineEntries.first?.id historyLoadEntryCount = timelineEntries.count + if hasMoreLocalHistory { + visibleLocalMessageCount = min( + messages.count, + visibleLocalMessageCount + Self.localHistoryPageSize + ) + localHistoryExhausted = visibleLocalMessageCount >= messages.count + rebuildTimelineEntries(from: messages) + historyTopTriggerArmed = true + return + } model.loadOlderHistoryForSelectedConversation() } diff --git a/Sources/Shared/UI/Components/LocationMessagePreview.swift b/Sources/Shared/UI/Components/LocationMessagePreview.swift index c795771..c64f979 100644 --- a/Sources/Shared/UI/Components/LocationMessagePreview.swift +++ b/Sources/Shared/UI/Components/LocationMessagePreview.swift @@ -56,7 +56,7 @@ struct LocationMessagePreview: View { } private func openInMaps() { - let item = MKMapItem(placemark: MKPlacemark(coordinate: coordinate)) + let item = MKMapItem(location: CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude), address: nil) item.name = "Геопозиция из Luma" item.openInMaps() } diff --git a/Sources/Shared/XMPP/LumaOMEMO2Module.swift b/Sources/Shared/XMPP/LumaOMEMO2Module.swift index 61c365b..509f0fb 100644 --- a/Sources/Shared/XMPP/LumaOMEMO2Module.swift +++ b/Sources/Shared/XMPP/LumaOMEMO2Module.swift @@ -3,7 +3,7 @@ import Combine import CommonCrypto import CryptoKit import os -import Martin +@preconcurrency import Martin import MartinOMEMO extension XmppModuleIdentifier { @@ -22,7 +22,7 @@ extension XmppModuleIdentifier { /// `` with `///`). /// The Double Ratchet sessions are shared with the legacy OMEMO module /// through the same SignalStorage. -final class LumaOMEMO2Module: AbstractPEPModule, XmppModule { +final class LumaOMEMO2Module: AbstractPEPModule, XmppModule, @unchecked Sendable { public static let ID = "omemo2" public static let IDENTIFIER = XmppModuleIdentifier() public static let XMLNS = "urn:xmpp:omemo:2" @@ -98,7 +98,8 @@ final class LumaOMEMO2Module: AbstractPEPModule, XmppModule { return } let known = listEl.mapChildren(transform: { $0.getAttribute("id").flatMap(Int32.init) }) - self.devicesQueue.async { self.devices[jid] = known } + let jidString = jid.stringValue + self.devicesQueue.async { self.devices[BareJID(jidString)] = known } continuation.resume(returning: known) } } diff --git a/Sources/Shared/XMPP/LumaSaslFailureModule.swift b/Sources/Shared/XMPP/LumaSaslFailureModule.swift index 911c6e4..a04d855 100644 --- a/Sources/Shared/XMPP/LumaSaslFailureModule.swift +++ b/Sources/Shared/XMPP/LumaSaslFailureModule.swift @@ -28,7 +28,7 @@ final class LumaSaslFailureModule: XmppModuleBase, XmppModule { } func process(stanza: Stanza) throws { - let condition = stanza.findChild()?.name + let condition = stanza.firstChild(where: { _ in true })?.name let text = stanza.firstChild(name: "text")?.value lock.lock() stored = Failure(condition: condition, text: text) diff --git a/Sources/Shared/XMPP/LumaScramPlusMechanism.swift b/Sources/Shared/XMPP/LumaScramPlusMechanism.swift index a1a3b05..6b494c7 100644 --- a/Sources/Shared/XMPP/LumaScramPlusMechanism.swift +++ b/Sources/Shared/XMPP/LumaScramPlusMechanism.swift @@ -61,7 +61,7 @@ enum SCRAMHash { func hash(data: Data) -> Data { var output = [UInt8](repeating: 0, count: digestLength) - data.withUnsafeBytes { bytes in + _ = data.withUnsafeBytes { bytes in switch self { case .sha1: CC_SHA1(bytes.baseAddress, CC_LONG(bytes.count), &output) diff --git a/Sources/Shared/XMPP/LumaTLSNetworkProcessor.swift b/Sources/Shared/XMPP/LumaTLSNetworkProcessor.swift index dfd2b77..ba959ce 100644 --- a/Sources/Shared/XMPP/LumaTLSNetworkProcessor.swift +++ b/Sources/Shared/XMPP/LumaTLSNetworkProcessor.swift @@ -201,7 +201,7 @@ final class LumaTLSNetworkProcessor: SocketConnector.NetworkProcessor, SSLNetwor override func read(data: Data) { guard !handshakeFailed else { return } ensureTLS() - guard let ssl, let readBIO else { return } + guard ssl != nil, let readBIO else { return } data.withUnsafeBytes { raw in _ = BIO_write(readBIO, raw.baseAddress, Int32(raw.count)) } @@ -415,9 +415,7 @@ final class LumaTLSNetworkProcessor: SocketConnector.NetworkProcessor, SSLNetwor switch certificateValidation { case .default: SecTrustSetPolicies(trust, SecPolicyCreateSSL(true, domain as CFString)) - var result = SecTrustResultType.invalid - SecTrustEvaluate(trust, &result) - validated = result == .proceed || result == .unspecified + validated = SecTrustEvaluateWithError(trust, nil) case .fingerprint(let fingerprint): validated = Self.matchesFingerprint(leafDER, fingerprint: fingerprint) case .customValidator(let validator): diff --git a/ThirdParty/MartinOMEMO/Package.swift b/ThirdParty/MartinOMEMO/Package.swift index c0c5c71..b81beeb 100644 --- a/ThirdParty/MartinOMEMO/Package.swift +++ b/ThirdParty/MartinOMEMO/Package.swift @@ -23,7 +23,12 @@ let package = Package( // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "MartinOMEMO", - dependencies: ["Martin", "libsignal"]), + dependencies: ["Martin", "libsignal"], + swiftSettings: [ + // Martin 3.2 still exposes deprecated event APIs that are + // required for compatibility with older clients. + .unsafeFlags(["-suppress-warnings"]) + ]), .testTarget( name: "MartinOMEMOTests", dependencies: ["MartinOMEMO"]), diff --git a/project.yml b/project.yml index da094b3..7720d4d 100644 --- a/project.yml +++ b/project.yml @@ -69,6 +69,15 @@ targets: CFBundleShortVersionString: $(MARKETING_VERSION) CFBundleVersion: $(CURRENT_PROJECT_VERSION) UILaunchScreen: {} + UISupportedInterfaceOrientations: + - UIInterfaceOrientationPortrait + - UIInterfaceOrientationLandscapeLeft + - UIInterfaceOrientationLandscapeRight + UISupportedInterfaceOrientations~ipad: + - UIInterfaceOrientationPortrait + - UIInterfaceOrientationPortraitUpsideDown + - UIInterfaceOrientationLandscapeLeft + - UIInterfaceOrientationLandscapeRight UIBackgroundModes: - audio - fetch @@ -89,6 +98,9 @@ targets: PRODUCT_BUNDLE_IDENTIFIER: $(COMPANION_BUNDLE_IDENTIFIER) PRODUCT_NAME: Luma TARGETED_DEVICE_FAMILY: "1,2" + # The remaining diagnostics come from APIs that Martin still requires + # for XMPP/TLS compatibility (QueueDispatcher and SecureTransport). + SWIFT_SUPPRESS_WARNINGS: true SUPPORTS_MACCATALYST: false DEVELOPMENT_TEAM: K3AS449A74 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor