diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 4705d05ca7..742e491716 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -31,6 +31,7 @@ 02D8DF8EB7537EB4E9019DDB /* EventBasedTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 218AB05B4E3889731959C5F1 /* EventBasedTimelineItemProtocol.swift */; }; 02F4FAE40AF63A1941FD3BBA /* NotificationCenterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10B7F8EE25775DE2A305CBB5 /* NotificationCenterProtocol.swift */; }; 037006FB6DF1374F94E4058D /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDCAC6CAAD65A2C24EA9C4B /* Dictionary.swift */; }; + 03CDCA6243F89B194E3FAD17 /* EncryptionAuthenticity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955336CBD5ED73C792D1F580 /* EncryptionAuthenticity.swift */; }; 0437765FF480249486893CC7 /* ScreenTrackerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 196004E7695FBA292A7944AF /* ScreenTrackerViewModifier.swift */; }; 044DD8F80231BC30570F7965 /* UserDiscoveryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65AAD845E53B0C8B5E0812C2 /* UserDiscoveryService.swift */; }; 04A16B45228F7678A027C079 /* RoomHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 422724361B6555364C43281E /* RoomHeaderView.swift */; }; @@ -1771,6 +1772,7 @@ 94028A227645FA880B966211 /* WaveformSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveformSource.swift; sourceTree = ""; }; 94D670124FC3E84F23A62CCF /* APNSPayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNSPayload.swift; sourceTree = ""; }; 9501D11B4258DFA33BA3B40F /* ServerSelectionScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenModels.swift; sourceTree = ""; }; + 955336CBD5ED73C792D1F580 /* EncryptionAuthenticity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionAuthenticity.swift; sourceTree = ""; }; 95BAC0F6C9644336E9567EE6 /* NSRegularExpresion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSRegularExpresion.swift; sourceTree = ""; }; 969694F67E844FCA51F7E051 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/InfoPlist.strings; sourceTree = ""; }; 96C4762F8D6112E43117DB2F /* CustomStringConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomStringConvertible.swift; sourceTree = ""; }; @@ -3516,6 +3518,7 @@ children = ( B858A61F2A570DFB8DE570A7 /* AggregratedReaction.swift */, 96C4762F8D6112E43117DB2F /* CustomStringConvertible.swift */, + 955336CBD5ED73C792D1F580 /* EncryptionAuthenticity.swift */, 314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */, 5DE8D25D6A91030175D52A20 /* RoomTimelineItemProperties.swift */, BE89A8BD65CCE3FCC925CA14 /* TimelineItemReplyDetails.swift */, @@ -6198,6 +6201,7 @@ 8B1D5CE017EEC734CF5FE130 /* Encodable.swift in Sources */, 4C5A638DAA8AF64565BA4866 /* EncryptedRoomTimelineItem.swift in Sources */, B5903E48CF43259836BF2DBF /* EncryptedRoomTimelineView.swift in Sources */, + 03CDCA6243F89B194E3FAD17 /* EncryptionAuthenticity.swift in Sources */, FBD402E3170EB1ED0D1AA672 /* EncryptionKeyProvider.swift in Sources */, 46A6DB0F78FB399BD59E2D41 /* EncryptionKeyProviderProtocol.swift in Sources */, 0C6DF318E9C8F6461E6ABDE7 /* EncryptionResetPasswordScreen.swift in Sources */, @@ -7553,7 +7557,7 @@ repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift"; requirement = { kind = exactVersion; - version = 1.0.30; + version = 1.0.31; }; }; 701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = { diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 19114cbe0e..3d793291f8 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -149,8 +149,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/element-hq/matrix-rust-components-swift", "state" : { - "revision" : "bc534e15fa0749d668b201b923ee57204afb868a", - "version" : "1.0.30" + "revision" : "8e2b4049fb492dcf5b0c796784b7aa7a3c099943", + "version" : "1.0.31" } }, { diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 53e7c97daa..bc19676b60 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -256,6 +256,7 @@ "error_some_messages_have_not_been_sent" = "Some messages have not been sent"; "error_unknown" = "Sorry, an error occurred"; "event_shield_reason_authenticity_not_guaranteed" = "The authenticity of this encrypted message can't be guaranteed on this device."; +"event_shield_reason_sent_in_clear" = "Sent in clear."; "event_shield_reason_unknown_device" = "Encrypted by an unknown or deleted device."; "event_shield_reason_unsigned_device" = "Encrypted by a device not verified by its owner."; "event_shield_reason_unverified_identity" = "Encrypted by an unverified user."; diff --git a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings index 3cf57ed4b7..2b31da559c 100644 --- a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings +++ b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings @@ -4,10 +4,6 @@ /* Used for testing */ "untranslated" = "Untranslated"; -// MARK: - Shields - -"send_info_not_encrypted" = "Not encrypted"; - // MARK: - Soft logout "soft_logout_signin_title" = "Sign in"; diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index 2ac15cdd76..8e49a42c93 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -47,6 +47,7 @@ final class AppSettings { case publicSearchEnabled case fuzzyRoomListSearchEnabled case pinningEnabled + case timelineItemAuthenticityEnabled } private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier @@ -284,6 +285,9 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.pinningEnabled, defaultValue: false, storageType: .userDefaults(store)) var pinningEnabled + + @UserPreference(key: UserDefaultsKeys.timelineItemAuthenticityEnabled, defaultValue: false, storageType: .userDefaults(store)) + var timelineItemAuthenticityEnabled #endif diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 9e3cdcb954..81619663ca 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -540,6 +540,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { let userID = userSession.clientProxy.userID let timelineItemFactory = RoomTimelineItemFactory(userID: userID, + encryptionAuthenticityEnabled: appSettings.timelineItemAuthenticityEnabled, attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID)) @@ -1033,6 +1034,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { let userID = userSession.clientProxy.userID let timelineItemFactory = RoomTimelineItemFactory(userID: userID, + encryptionAuthenticityEnabled: appSettings.timelineItemAuthenticityEnabled, attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID)) diff --git a/ElementX/Sources/Generated/Strings+Untranslated.swift b/ElementX/Sources/Generated/Strings+Untranslated.swift index b9029e983d..55cfabb3ea 100644 --- a/ElementX/Sources/Generated/Strings+Untranslated.swift +++ b/ElementX/Sources/Generated/Strings+Untranslated.swift @@ -10,8 +10,6 @@ import Foundation // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces internal enum UntranslatedL10n { - /// Not encrypted - internal static var sendInfoNotEncrypted: String { return UntranslatedL10n.tr("Untranslated", "send_info_not_encrypted") } /// Clear all data currently stored on this device? /// Sign in again to access your account data and messages. internal static var softLogoutClearDataDialogContent: String { return UntranslatedL10n.tr("Untranslated", "soft_logout_clear_data_dialog_content") } diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index cfee57a6cf..24b644e5a2 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -570,6 +570,8 @@ internal enum L10n { internal static var errorUnknown: String { return L10n.tr("Localizable", "error_unknown") } /// The authenticity of this encrypted message can't be guaranteed on this device. internal static var eventShieldReasonAuthenticityNotGuaranteed: String { return L10n.tr("Localizable", "event_shield_reason_authenticity_not_guaranteed") } + /// Sent in clear. + internal static var eventShieldReasonSentInClear: String { return L10n.tr("Localizable", "event_shield_reason_sent_in_clear") } /// Encrypted by an unknown or deleted device. internal static var eventShieldReasonUnknownDevice: String { return L10n.tr("Localizable", "event_shield_reason_unknown_device") } /// Encrypted by a device not verified by its owner. diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift index b719c36c2f..06ddbd5098 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift @@ -330,6 +330,8 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview .previewDisplayName("Replies") threads .previewDisplayName("Thread decorator") + encryptionAuthenticity + .previewDisplayName("Encryption Indicators") } // These akwats include a reply @@ -477,4 +479,80 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview } .environmentObject(viewModel.context) } + + static var encryptionAuthenticity: some View { + VStack(spacing: 0) { + RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""), + timestamp: "10:42", + isOutgoing: true, + isEditable: false, + canBeRepliedTo: true, + isThreaded: false, + sender: .init(id: "whoever"), + content: .init(body: "A long message that should be on multiple lines."), + properties: RoomTimelineItemProperties(encryptionAuthenticity: .unsignedDevice(color: .red))), + groupStyle: .single)) + + RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""), + timestamp: "10:42", + isOutgoing: true, + isEditable: false, + canBeRepliedTo: true, + isThreaded: false, + sender: .init(id: "whoever"), + content: .init(body: "A long message that should be on multiple lines."), + properties: RoomTimelineItemProperties(isEdited: true, + encryptionAuthenticity: .unsignedDevice(color: .red))), + groupStyle: .single)) + + RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""), + timestamp: "10:42", + isOutgoing: false, + isEditable: false, + canBeRepliedTo: true, + isThreaded: false, + sender: .init(id: "whoever"), + content: .init(body: "Short message"), + properties: RoomTimelineItemProperties(encryptionAuthenticity: .unknownDevice(color: .red))), + groupStyle: .first)) + + RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""), + timestamp: "10:42", + isOutgoing: false, + isEditable: false, + canBeRepliedTo: true, + isThreaded: false, + sender: .init(id: "whoever"), + content: .init(body: "Message goes Here"), + properties: RoomTimelineItemProperties(encryptionAuthenticity: .notGuaranteed(color: .gray))), + groupStyle: .last)) + + ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .random, + timestamp: "Now", + isOutgoing: false, + isEditable: false, + canBeRepliedTo: true, + isThreaded: false, + sender: .init(id: "Bob"), + content: .init(body: "Some other image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil), + + properties: RoomTimelineItemProperties(encryptionAuthenticity: .notGuaranteed(color: .gray)))) + + VoiceMessageRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""), + timestamp: "10:42", + isOutgoing: true, + isEditable: false, + canBeRepliedTo: true, + isThreaded: true, + sender: .init(id: ""), + content: .init(body: "audio.ogg", + duration: 100, + waveform: EstimatedWaveform.mockWaveform, + source: nil, + contentType: nil), + properties: RoomTimelineItemProperties(encryptionAuthenticity: .notGuaranteed(color: .gray))), + playerState: AudioPlayerState(id: .timelineItemIdentifier(.random), duration: 10, waveform: EstimatedWaveform.mockWaveform)) + } + .environmentObject(viewModel.context) + } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemSendInfoLabel.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemSendInfoLabel.swift index 0c020d4ec8..834fc0d62e 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemSendInfoLabel.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemSendInfoLabel.swift @@ -55,10 +55,17 @@ private struct TimelineItemSendInfoLabel: View { var statusIcon: KeyPath? { switch sendInfo.status { - case .sendingFailed: \.error - case .unverifiedSession, .authenticityUnknown: \.admin - case .unencrypted: \.keyOff - case .none: nil + case .sendingFailed: + \.error + case .encryptionAuthenticity(.notGuaranteed): + \.infoSolid + case .encryptionAuthenticity(.unknownDevice), + .encryptionAuthenticity(.unsignedDevice), + .encryptionAuthenticity(.unverifiedIdentity), + .encryptionAuthenticity(.sentInClear): + \.lockOff + case .none: + nil } } @@ -67,9 +74,7 @@ private struct TimelineItemSendInfoLabel: View { case .sendingFailed: L10n.commonSendingFailed case .none: nil // Temporary testing strings. - case .unverifiedSession: L10n.eventShieldReasonUnsignedDevice - case .authenticityUnknown: L10n.eventShieldReasonAuthenticityNotGuaranteed - case .unencrypted: UntranslatedL10n.sendInfoNotEncrypted + case .encryptionAuthenticity(let authenticity): authenticity.message } } @@ -111,7 +116,7 @@ private struct TimelineItemSendInfoLabel: View { /// All the data needed to render a timeline item's send info label. private struct TimelineItemSendInfo { - enum Status { case sendingFailed, unverifiedSession, authenticityUnknown, unencrypted } + enum Status { case sendingFailed, encryptionAuthenticity(EncryptionAuthenticity) } /// Describes how the content and the send info should be arranged inside a bubble enum LayoutType { @@ -126,9 +131,11 @@ private struct TimelineItemSendInfo { var foregroundStyle: Color { switch status { - case .sendingFailed, .unverifiedSession: + case .sendingFailed: .compound.textCriticalPrimary - case .authenticityUnknown, .unencrypted, .none: + case .encryptionAuthenticity(let authenticity): + authenticity.foregroundStyle + case .none: .compound.textSecondary } } @@ -140,6 +147,8 @@ private extension TimelineItemSendInfo { status = if adjustedDeliveryStatus == .sendingFailed { .sendingFailed + } else if let authenticity = timelineItem.properties.encryptionAuthenticity { + .encryptionAuthenticity(authenticity) } else { nil } @@ -161,6 +170,24 @@ private extension TimelineItemSendInfo { } } +private extension EncryptionAuthenticity { + var foregroundStyle: SwiftUI.Color { + switch self { + case .notGuaranteed(let color), + .unknownDevice(let color), + .unsignedDevice(let color), + .unverifiedIdentity(let color), + .sentInClear(let color): + switch color { + case .red: + .compound.textCriticalPrimary + case .gray: + .compound.textSecondary + } + } + } +} + // MARK: - Previews struct TimelineItemSendInfoLabel_Previews: PreviewProvider, TestablePreview { @@ -172,14 +199,14 @@ struct TimelineItemSendInfoLabel_Previews: PreviewProvider, TestablePreview { status: .sendingFailed, layoutType: .horizontal())) TimelineItemSendInfoLabel(sendInfo: .init(localizedString: "09:47 AM", - status: .unverifiedSession, - layoutType: .horizontal())) - TimelineItemSendInfoLabel(sendInfo: .init(localizedString: "09:47 AM", - status: .authenticityUnknown, + status: .encryptionAuthenticity(.unsignedDevice(color: .red)), layoutType: .horizontal())) TimelineItemSendInfoLabel(sendInfo: .init(localizedString: "09:47 AM", - status: .unencrypted, + status: .encryptionAuthenticity(.notGuaranteed(color: .gray)), layoutType: .horizontal())) +// TimelineItemSendInfoLabel(sendInfo: .init(localizedString: "09:47 AM", +// status: .unencrypted, +// layoutType: .horizontal())) } } } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift index ba618ad297..a51b15b96b 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift @@ -53,6 +53,7 @@ protocol DeveloperOptionsProtocol: AnyObject { var elementCallBaseURLOverride: URL? { get set } var fuzzyRoomListSearchEnabled: Bool { get set } var pinningEnabled: Bool { get set } + var timelineItemAuthenticityEnabled: Bool { get set } } extension AppSettings: DeveloperOptionsProtocol { } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift index 6462998924..18be31e8be 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -50,6 +50,13 @@ struct DeveloperOptionsScreen: View { Text("Fuzzy searching") } } + + Section("Encryption") { + Toggle(isOn: $context.timelineItemAuthenticityEnabled) { + Text("Message authenticity warnings") + Text("Requires app reboot") + } + } Section("Element Call") { TextField(context.viewState.elementCallBaseURL.absoluteString, text: $elementCallBaseURLString) diff --git a/ElementX/Sources/Services/Timeline/TimelineItemContent/EncryptionAuthenticity.swift b/ElementX/Sources/Services/Timeline/TimelineItemContent/EncryptionAuthenticity.swift new file mode 100644 index 0000000000..fef91f098e --- /dev/null +++ b/ElementX/Sources/Services/Timeline/TimelineItemContent/EncryptionAuthenticity.swift @@ -0,0 +1,74 @@ +// +// Copyright 2024 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import MatrixRustSDK + +/// Represents and issue with a timeline item's authenticity such as coming from an +/// unsigned session or being sent unencrypted in an encrypted room. See Rust's +/// `ShieldStateCode` for more information about the meaning of the cases. +enum EncryptionAuthenticity: Hashable { + enum Color { case red, gray } + + case notGuaranteed(color: Color) + case unknownDevice(color: Color) + case unsignedDevice(color: Color) + case unverifiedIdentity(color: Color) + case sentInClear(color: Color) + + var message: String { + switch self { + case .notGuaranteed: + L10n.eventShieldReasonAuthenticityNotGuaranteed + case .unknownDevice: + L10n.eventShieldReasonUnknownDevice + case .unsignedDevice: + L10n.eventShieldReasonUnsignedDevice + case .unverifiedIdentity: + L10n.eventShieldReasonUnverifiedIdentity + case .sentInClear: + L10n.eventShieldReasonSentInClear + } + } +} + +extension EncryptionAuthenticity { + init?(shieldState: ShieldState) { + switch shieldState { + case .red(let code, _): + self.init(shieldStateCode: code, color: .red) + case .grey(let code, _): + self.init(shieldStateCode: code, color: .gray) + case .none: + return nil + } + } + + init(shieldStateCode: ShieldStateCode, color: EncryptionAuthenticity.Color) { + switch shieldStateCode { + case .authenticityNotGuaranteed: + self = .notGuaranteed(color: color) + case .unknownDevice: + self = .unknownDevice(color: color) + case .unsignedDevice: + self = .unsignedDevice(color: color) + case .unverifiedIdentity: + self = .unverifiedIdentity(color: color) + case .sentInClear: + self = .sentInClear(color: color) + } + } +} diff --git a/ElementX/Sources/Services/Timeline/TimelineItemContent/RoomTimelineItemProperties.swift b/ElementX/Sources/Services/Timeline/TimelineItemContent/RoomTimelineItemProperties.swift index ddbe6a044b..07b971bdbf 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemContent/RoomTimelineItemProperties.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemContent/RoomTimelineItemProperties.swift @@ -24,6 +24,8 @@ struct RoomTimelineItemProperties: Hashable { var reactions: [AggregatedReaction] = [] /// The delivery status for this item. If a sent message is echoed the value is nil. var deliveryStatus: TimelineItemDeliveryStatus? - /// The read receipts of the item, ordered from newest to oldest + /// The read receipts of the item, ordered from newest to oldest. var orderedReadReceipts: [ReadReceipt] = [] + /// Authenticity warnings for item's sent in encrypted rooms. + var encryptionAuthenticity: EncryptionAuthenticity? } diff --git a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift index af68b34ade..f162ae6c45 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift @@ -120,7 +120,9 @@ class EventTimelineItemProxy { let debugInfo = item.debugInfo() return TimelineItemDebugInfo(model: debugInfo.model, originalJSON: debugInfo.originalJson, latestEditJSON: debugInfo.latestEditJson) }() - + + lazy var shieldState = item.getShield(strict: false) + lazy var readReceipts = item.readReceipts() } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift index 22e4291e6a..d017409f54 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift @@ -50,7 +50,11 @@ extension EventBasedTimelineItemProtocol { var pollIfAvailable: Poll? { (self as? PollRoomTimelineItem)?.poll } - + + var hasStatusIcon: Bool { + hasFailedToSend || properties.encryptionAuthenticity != nil + } + var hasFailedToSend: Bool { properties.deliveryStatus == .sendingFailed } @@ -74,8 +78,8 @@ extension EventBasedTimelineItemProtocol { whiteSpaces += 1 } - // To account for the extra spacing created by the alert icon - if hasFailedToSend { + // To account for the extra spacing created by the status icon + if hasStatusIcon { whiteSpaces += 3 } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift index 1ef15d3229..f48a273e99 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift @@ -24,11 +24,14 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { /// The Matrix ID of the current user. private let userID: String + private let encryptionAuthenticityEnabled: Bool init(userID: String, + encryptionAuthenticityEnabled: Bool, attributedStringBuilder: AttributedStringBuilderProtocol, stateEventStringBuilder: RoomStateEventStringBuilder) { self.userID = userID + self.encryptionAuthenticityEnabled = encryptionAuthenticityEnabled self.attributedStringBuilder = attributedStringBuilder self.stateEventStringBuilder = stateEventStringBuilder } @@ -156,7 +159,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { blurhash: imageInfo.blurhash, properties: RoomTimelineItemProperties(reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, - orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), + encryptionAuthenticity: authenticity(eventItemProxy.shieldState))) } private func buildEncryptedTimelineItem(_ eventItemProxy: EventTimelineItemProxy, @@ -220,7 +224,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, - orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), + encryptionAuthenticity: authenticity(eventItemProxy.shieldState))) } private func buildImageTimelineItem(for eventItemProxy: EventTimelineItemProxy, @@ -240,7 +245,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, - orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), + encryptionAuthenticity: authenticity(eventItemProxy.shieldState))) } private func buildVideoTimelineItem(for eventItemProxy: EventTimelineItemProxy, @@ -260,7 +266,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, - orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), + encryptionAuthenticity: authenticity(eventItemProxy.shieldState))) } private func buildAudioTimelineItem(for eventItemProxy: EventTimelineItemProxy, @@ -280,7 +287,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, - orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), + encryptionAuthenticity: authenticity(eventItemProxy.shieldState))) } private func buildVoiceTimelineItem(for eventItemProxy: EventTimelineItemProxy, @@ -300,7 +308,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, - orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), + encryptionAuthenticity: authenticity(eventItemProxy.shieldState))) } private func buildFileTimelineItem(for eventItemProxy: EventTimelineItemProxy, @@ -320,7 +329,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, - orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), + encryptionAuthenticity: authenticity(eventItemProxy.shieldState))) } private func buildNoticeTimelineItem(for eventItemProxy: EventTimelineItemProxy, @@ -340,7 +350,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, - orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), + encryptionAuthenticity: authenticity(eventItemProxy.shieldState))) } private func buildEmoteTimelineItem(for eventItemProxy: EventTimelineItemProxy, @@ -360,7 +371,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, - orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), + encryptionAuthenticity: authenticity(eventItemProxy.shieldState))) } private func buildLocationTimelineItem(for eventItemProxy: EventTimelineItemProxy, @@ -380,7 +392,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, - orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), + encryptionAuthenticity: authenticity(eventItemProxy.shieldState))) } // swiftlint:disable:next function_parameter_count @@ -429,7 +442,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { properties: RoomTimelineItemProperties(isEdited: edited, reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, - orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), + encryptionAuthenticity: authenticity(eventItemProxy.shieldState))) } private func buildCallInviteTimelineItem(for eventItemProxy: EventTimelineItemProxy) -> RoomTimelineItemProtocol { @@ -488,6 +502,11 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { } } + private func authenticity(_ shieldState: ShieldState?) -> EncryptionAuthenticity? { + guard encryptionAuthenticityEnabled else { return nil } + return shieldState.flatMap(EncryptionAuthenticity.init) + } + // MARK: - Message events content private func buildTextTimelineItemContent(_ messageContent: TextMessageContent) -> TextRoomTimelineItemContent { diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 27d2bd2bb9..e138a50843 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -636,6 +636,7 @@ class MockScreen: Identifiable { let timelineController = RoomTimelineController(roomProxy: roomProxy, initialFocussedEventID: nil, timelineItemFactory: RoomTimelineItemFactory(userID: "@alice:matrix.org", + encryptionAuthenticityEnabled: true, attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), stateEventStringBuilder: RoomStateEventStringBuilder(userID: "@alice:matrix.org")), appSettings: ServiceLocator.shared.settings) diff --git a/Package.resolved b/Package.resolved index 2c7d7e93e9..8ff67e001a 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser", "state" : { - "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", - "version" : "1.4.0" + "revision" : "41982a3656a71c768319979febd796c6fd111d5c", + "version" : "1.5.0" } }, { @@ -22,8 +22,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/jpsim/Yams", "state" : { - "revision" : "9234124cff5e22e178988c18d8b95a8ae8007f76", - "version" : "5.1.2" + "revision" : "3036ba9d69cf1fd04d433527bc339dc0dc75433d", + "version" : "5.1.3" } } ], diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView-iPad-en-GB.Encryption-Indicators.png b/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView-iPad-en-GB.Encryption-Indicators.png new file mode 100644 index 0000000000..489f113889 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView-iPad-en-GB.Encryption-Indicators.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cfcaff9f0d5680fc4dbb30c906b8ce84213b6a41f6b4fb52c2ec256902fce350 +size 437852 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView-iPad-pseudo.Encryption-Indicators.png b/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView-iPad-pseudo.Encryption-Indicators.png new file mode 100644 index 0000000000..e97cd1fe45 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView-iPad-pseudo.Encryption-Indicators.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec285cfbd7793fa8feca3e8869b776f64214b3cfd116bb6e77d9f4149732625a +size 440028 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView-iPhone-15-en-GB.Encryption-Indicators.png b/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView-iPhone-15-en-GB.Encryption-Indicators.png new file mode 100644 index 0000000000..39476b5fa5 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView-iPhone-15-en-GB.Encryption-Indicators.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb717b5c2f4c5e5dc9f0a473934bb237eebad19e401ea08353a62d5567d99fc3 +size 343142 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView-iPhone-15-pseudo.Encryption-Indicators.png b/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView-iPhone-15-pseudo.Encryption-Indicators.png new file mode 100644 index 0000000000..2334be92ff --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView-iPhone-15-pseudo.Encryption-Indicators.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f8613ae1955fd57f8536f51e46b87913b0c6f156d8cff7db6a3e6f7f18846896 +size 344422 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemSendInfoLabel-iPad-en-GB.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemSendInfoLabel-iPad-en-GB.1.png index 6736fd1f16..47a1d61850 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemSendInfoLabel-iPad-en-GB.1.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemSendInfoLabel-iPad-en-GB.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f04ababd10ae20834830511223418d25b49030ed0be58cb649c437acd4ed7b9 -size 81479 +oid sha256:d8a4a63fd7a948b134d02a405070c038116405d6b19f7ab45930d8d8fe25d66c +size 78530 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemSendInfoLabel-iPad-pseudo.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemSendInfoLabel-iPad-pseudo.1.png index 6736fd1f16..47a1d61850 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemSendInfoLabel-iPad-pseudo.1.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemSendInfoLabel-iPad-pseudo.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f04ababd10ae20834830511223418d25b49030ed0be58cb649c437acd4ed7b9 -size 81479 +oid sha256:d8a4a63fd7a948b134d02a405070c038116405d6b19f7ab45930d8d8fe25d66c +size 78530 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemSendInfoLabel-iPhone-15-en-GB.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemSendInfoLabel-iPhone-15-en-GB.1.png index c5054a9f11..132c59a02e 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemSendInfoLabel-iPhone-15-en-GB.1.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemSendInfoLabel-iPhone-15-en-GB.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f1b0ecbae3765e60c67f4a468d61586652c31a5d0f4dcdc32dd35de886694288 -size 38900 +oid sha256:67533013d6b66f5256e00fbb7ee684f4ec364f640d10a9209dc14f067aa31905 +size 36782 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemSendInfoLabel-iPhone-15-pseudo.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemSendInfoLabel-iPhone-15-pseudo.1.png index c5054a9f11..132c59a02e 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemSendInfoLabel-iPhone-15-pseudo.1.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_timelineItemSendInfoLabel-iPhone-15-pseudo.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f1b0ecbae3765e60c67f4a468d61586652c31a5d0f4dcdc32dd35de886694288 -size 38900 +oid sha256:67533013d6b66f5256e00fbb7ee684f4ec364f640d10a9209dc14f067aa31905 +size 36782 diff --git a/UnitTests/Sources/TimelineItemFactoryTests.swift b/UnitTests/Sources/TimelineItemFactoryTests.swift index c7b8404106..9b75a6516a 100644 --- a/UnitTests/Sources/TimelineItemFactoryTests.swift +++ b/UnitTests/Sources/TimelineItemFactoryTests.swift @@ -24,6 +24,7 @@ class TimelineItemFactoryTests: XCTestCase { let senderUserID = "@bob:matrix.org" let factory = RoomTimelineItemFactory(userID: ownUserID, + encryptionAuthenticityEnabled: true, attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), stateEventStringBuilder: RoomStateEventStringBuilder(userID: ownUserID)) diff --git a/project.yml b/project.yml index 8b3ec05f93..91f61e8ea4 100644 --- a/project.yml +++ b/project.yml @@ -60,7 +60,7 @@ packages: # Element/Matrix dependencies MatrixRustSDK: url: https://github.com/element-hq/matrix-rust-components-swift - exactVersion: 1.0.30 + exactVersion: 1.0.31 # path: ../matrix-rust-sdk Compound: url: https://github.com/element-hq/compound-ios