diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index ce8aed24d..fae39a2a8 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -55,6 +55,7 @@ 19839F3526CE8C35AAF241AD /* ServerSelectionViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F52BF30D12BA3BD3D3DBB8F /* ServerSelectionViewModelProtocol.swift */; }; 1A70A2199394B5EC660934A5 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = A678E40E917620059695F067 /* MatrixRustSDK */; }; 1AE4AEA0FA8DEF52671832E0 /* RoomTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */; }; + 1CF18DE71D5D23C61BD88852 /* DebugScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9238D3A3A00F45E841FE4EFF /* DebugScreen.swift */; }; 1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05707BF550D770168A406DB /* LoginViewModelTests.swift */; }; 1F3232BD368DF430AB433907 /* DesignKit in Frameworks */ = {isa = PBXBuildFile; productRef = A5A56C4F47C368EBE5C5E870 /* DesignKit */; }; 1FEC0A4EC6E6DF693C16B32A /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CEBCB9676FCD1D0F13188DD /* StringTests.swift */; }; @@ -277,6 +278,7 @@ B3357B00F1AA930E54F76609 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; }; B4AAB3257A83B73F53FB2689 /* StateStoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3DFE5B444F131648066F05 /* StateStoreViewModel.swift */; }; B5111BAF5F601C139EBBD8BB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 01C4C7DB37597D7D8379511A /* Assets.xcassets */; }; + B5903E48CF43259836BF2DBF /* EncryptedRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56C1BCB9E83B09A45387FCA2 /* EncryptedRoomTimelineView.swift */; }; B6DA66EFC13A90846B625836 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 91DE43B8815918E590912DDA /* InfoPlist.strings */; }; B6DF6B6FA8734B70F9BF261E /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */; }; B6F92EBE04D4AABF30B9E73A /* AnalyticsPromptModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8BA82CF99D843FEF680E91 /* AnalyticsPromptModels.swift */; }; @@ -353,6 +355,7 @@ F508683B76EF7B23BB2CBD6D /* TimelineItemPlainStylerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94BCC8A9C73C1F838122C645 /* TimelineItemPlainStylerView.swift */; }; F56261126E368C831B3DE976 /* NavigationRouterType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 752DEC02D93AFF46BC13313A /* NavigationRouterType.swift */; }; F656F92A63D3DC1978D79427 /* AnalyticsEvents in Frameworks */ = {isa = PBXBuildFile; productRef = 2A3F7BCCB18C15B30CCA39A9 /* AnalyticsEvents */; }; + F6E860FF7B18B81DF43B30B8 /* EncryptedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FA7C8D4EF2B1873C180ED7 /* EncryptedRoomTimelineItem.swift */; }; F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */; }; F7567DD6635434E8C563BF85 /* AnalyticsClientProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */; }; F75C4222D52B643214D5E623 /* UITestsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81740EEAFDF0D34C5E10D0DF /* UITestsRootView.swift */; }; @@ -558,6 +561,7 @@ 55BC11560C8A2598964FFA4C /* bs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bs; path = bs.lproj/Localizable.strings; sourceTree = ""; }; 55D7187F6B0C0A651AC3DFFA /* in */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = in; path = in.lproj/Localizable.strings; sourceTree = ""; }; 55F30E764BED111C81739844 /* SoftLogoutUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutUITests.swift; sourceTree = ""; }; + 56C1BCB9E83B09A45387FCA2 /* EncryptedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedRoomTimelineView.swift; sourceTree = ""; }; 56F01DD1BBD4450E18115916 /* LabelledActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelledActivityIndicatorView.swift; sourceTree = ""; }; 5773C86AF04AEF26515AD00C /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/Localizable.strings; sourceTree = ""; }; 5872785B9C7934940146BFBA /* MXLogger.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MXLogger.h; sourceTree = ""; }; @@ -653,6 +657,7 @@ 8FC803282F9268D49F4ABF14 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; 9010EE0CC913D095887EF36E /* OIDCService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCService.swift; sourceTree = ""; }; 90733775209F4D4D366A268F /* RootRouterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootRouterType.swift; sourceTree = ""; }; + 9238D3A3A00F45E841FE4EFF /* DebugScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugScreen.swift; sourceTree = ""; }; 92FCD9116ADDE820E4E30F92 /* UIKitBackgroundTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitBackgroundTask.swift; sourceTree = ""; }; 9349F590E35CE514A71E6764 /* LoginHomeserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginHomeserver.swift; sourceTree = ""; }; 938BD1FCD9E6FF3FCFA7AB4C /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-CN"; path = "zh-CN.lproj/Localizable.stringsdict"; sourceTree = ""; }; @@ -719,6 +724,7 @@ B1183B55FF4B01022DA721CB /* en-GB */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-GB"; path = "en-GB.lproj/Localizable.strings"; sourceTree = ""; }; B1D1532B5D9FB0C8461A1453 /* UserIndicatorDismissal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorDismissal.swift; sourceTree = ""; }; B3069ADED46D063202FE7698 /* SessionVerificationViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationViewModelProtocol.swift; sourceTree = ""; }; + B3FA7C8D4EF2B1873C180ED7 /* EncryptedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedRoomTimelineItem.swift; sourceTree = ""; }; B4173A48FD8542CD4AD3645C /* NavigationRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouter.swift; sourceTree = ""; }; B43AF03660F5FD4FFFA7F1CE /* TimelineItemContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemContextMenu.swift; sourceTree = ""; }; B4C18FAAD59AE7F1462D817E /* SessionVerificationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationViewModel.swift; sourceTree = ""; }; @@ -1375,6 +1381,7 @@ isa = PBXGroup; children = ( F77C060C2ACC4CB7336A29E7 /* EmoteRoomTimelineItem.swift */, + B3FA7C8D4EF2B1873C180ED7 /* EncryptedRoomTimelineItem.swift */, 1A63815AD6A5C306453342F2 /* ImageRoomTimelineItem.swift */, 4F49CDE349C490D617332770 /* NoticeRoomTimelineItem.swift */, 9B577F829C693B8DFB7014FD /* RedactedRoomTimelineItem.swift */, @@ -1410,6 +1417,7 @@ 79023E5904B155E8E2B8B502 /* View */ = { isa = PBXGroup; children = ( + 9238D3A3A00F45E841FE4EFF /* DebugScreen.swift */, E18CF12478983A5EB390FB26 /* MessageComposer.swift */, BE6C10032A77AE7DC5AA4C50 /* MessageComposerTextField.swift */, 422724361B6555364C43281E /* RoomHeaderView.swift */, @@ -1681,6 +1689,7 @@ isa = PBXGroup; children = ( 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */, + 56C1BCB9E83B09A45387FCA2 /* EncryptedRoomTimelineView.swift */, F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */, D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */, B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */, @@ -2411,6 +2420,7 @@ 663E198678778F7426A9B27D /* Collection.swift in Sources */, DCB781BD227CA958809AFADF /* Coordinator.swift in Sources */, C4F69156C31A447FEFF2A47C /* DTHTMLElement+AttributedStringBuilder.swift in Sources */, + 1CF18DE71D5D23C61BD88852 /* DebugScreen.swift in Sources */, EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */, FE4593FC2A02AAF92E089565 /* ElementAnimations.swift in Sources */, 06E93B2E3B32740B40F47CC5 /* ElementNavigationController.swift in Sources */, @@ -2419,6 +2429,8 @@ 7C1A7B594B2F8143F0DD0005 /* ElementXAttributeScope.swift in Sources */, 6647430A45B4A8E692909A8F /* EmoteRoomTimelineItem.swift in Sources */, 68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */, + F6E860FF7B18B81DF43B30B8 /* EncryptedRoomTimelineItem.swift in Sources */, + B5903E48CF43259836BF2DBF /* EncryptedRoomTimelineView.swift in Sources */, 02D8DF8EB7537EB4E9019DDB /* EventBasedTimelineItemProtocol.swift in Sources */, FD4706DC752744A0C91ED6FE /* FileManager.swift in Sources */, A0A0D2A9564BDA3FDE2E360F /* FormattedBodyText.swift in Sources */, diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 064174f40..e09bb206c 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -19,16 +19,6 @@ import UIKit enum RoomScreenViewModelAction { } -enum TimelineItemContextMenuAction: Identifiable, Hashable { - case copy - case quote - case copyPermalink - case redact - case reply - - var id: Self { self } -} - enum RoomScreenComposerMode: Equatable { case `default` case reply(id: String, displayName: String) @@ -67,6 +57,8 @@ struct RoomScreenViewStateBindings { /// Information describing the currently displayed alert. var alertInfo: AlertInfo? + + var debugInfo: DebugScreen.DebugInfo? } enum RoomScreenErrorType: Hashable { diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index a2cba376f..1907eed42 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -161,10 +161,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } } - private func contextMenuActionsForItemId(_ itemId: String) -> [TimelineItemContextMenuAction] { + private func contextMenuActionsForItemId(_ itemId: String) -> TimelineItemContextMenuActions { guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemId }), timelineItem is EventBasedTimelineItemProtocol else { - return [] + return .init(actions: []) } var actions: [TimelineItemContextMenuAction] = [ @@ -175,7 +175,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol actions.append(.redact) } - return actions + return .init(actions: actions) } private func processContentMenuAction(_ action: TimelineItemContextMenuAction, itemId: String) { @@ -202,6 +202,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol case .reply: state.bindings.composerFocused = true state.composerMode = .reply(id: item.id, displayName: item.senderDisplayName ?? item.senderId) + case .viewSource: + let debugDescription = timelineController.debugDescriptionFor(item.id) + MXLog.info(debugDescription) + state.bindings.debugInfo = .init(title: "Timeline item", content: debugDescription) } switch action { diff --git a/ElementX/Sources/Screens/RoomScreen/View/DebugScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/DebugScreen.swift new file mode 100644 index 000000000..2117b8a70 --- /dev/null +++ b/ElementX/Sources/Screens/RoomScreen/View/DebugScreen.swift @@ -0,0 +1,53 @@ +// +// Copyright 2022 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 SwiftUI + +struct DebugScreen: View { + struct DebugInfo: Identifiable { + let id = UUID() + let title: String + var content: String + } + + @Environment(\.presentationMode) private var presentationMode + + let info: DebugInfo + + var body: some View { + NavigationView { + ScrollView { + Text(info.content) + .padding() + .font(.element.footnote) + } + .navigationBarTitleDisplayMode(.inline) + .navigationTitle(info.title) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button(ElementL10n.actionCancel) { + presentationMode.wrappedValue.dismiss() + } + } + ToolbarItem(placement: .secondaryAction) { + Button(ElementL10n.actionCopy) { + UIPasteboard.general.string = info.content + } + } + } + } + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 4e55f40de..55b198457 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -41,6 +41,7 @@ struct RoomScreen: View { } } .alert(item: $context.alertInfo) { $0.alert } + .sheet(item: $context.debugInfo) { DebugScreen(info: $0) } } private func sendMessage() { diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/EncryptedRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/EncryptedRoomTimelineView.swift new file mode 100644 index 000000000..a18f731d1 --- /dev/null +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/EncryptedRoomTimelineView.swift @@ -0,0 +1,91 @@ +// +// Copyright 2022 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 SwiftUI + +struct EncryptedRoomTimelineView: View { + @State private var showEncryptionInfo = false + + let timelineItem: EncryptedRoomTimelineItem + + var body: some View { + TimelineStyler(timelineItem: timelineItem) { + Button { + showEncryptionInfo = !showEncryptionInfo + } label: { + HStack(alignment: .top) { + Image(systemName: "lock.shield") + .foregroundColor(.red) + .padding(.top, 1.0) + if showEncryptionInfo { + FormattedBodyText(text: encryptionDetails) + } else { + FormattedBodyText(text: timelineItem.text) + } + } + .animation(nil, value: showEncryptionInfo) + } + } + .id(timelineItem.id) + } + + private var encryptionDetails: String { + switch timelineItem.encryptionType { + case .unknown: + return "Unknown" + case .megolmV1AesSha2(let sessionId): + return "Megolm session id: \(sessionId)" + case .olmV1Curve25519AesSha2(let senderKey): + return "Olm sender key: \(senderKey)" + } + } +} + +struct EncryptedRoomTimelineView_Previews: PreviewProvider { + static var previews: some View { + body.preferredColorScheme(.light) + body.preferredColorScheme(.dark) + body.preferredColorScheme(.light) + .timelineStyle(.plain) + body.preferredColorScheme(.dark) + .timelineStyle(.plain) + } + + @ViewBuilder + static var body: some View { + VStack(alignment: .leading, spacing: 20.0) { + EncryptedRoomTimelineView(timelineItem: itemWith(text: "Text", + timestamp: "Now", + isOutgoing: false, + senderId: "Bob")) + + EncryptedRoomTimelineView(timelineItem: itemWith(text: "Some other text", + timestamp: "Later", + isOutgoing: true, + senderId: "Anne")) + } + } + + private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> EncryptedRoomTimelineItem { + EncryptedRoomTimelineItem(id: UUID().uuidString, + text: text, + encryptionType: .unknown, + timestamp: timestamp, + inGroupState: .single, + isOutgoing: isOutgoing, + senderId: senderId) + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemContextMenu.swift b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemContextMenu.swift index 3923c64bc..5cffe0797 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemContextMenu.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemContextMenu.swift @@ -16,13 +16,38 @@ import SwiftUI +struct TimelineItemContextMenuActions { + let actions: [TimelineItemContextMenuAction] + let debugActions: [TimelineItemContextMenuAction] = [.viewSource] +} + +enum TimelineItemContextMenuAction: Identifiable, Hashable { + case copy + case quote + case copyPermalink + case redact + case reply + case viewSource + + var id: Self { self } +} + public struct TimelineItemContextMenu: View { - let contextMenuActions: [TimelineItemContextMenuAction] + let contextMenuActions: TimelineItemContextMenuActions let callback: (TimelineItemContextMenuAction) -> Void @ViewBuilder public var body: some View { - ForEach(contextMenuActions, id: \.self) { item in + viewsForActions(contextMenuActions.actions) + Menu { + viewsForActions(contextMenuActions.debugActions) + } label: { + Label("Developer", systemImage: "hammer") + } + } + + private func viewsForActions(_ actions: [TimelineItemContextMenuAction]) -> some View { + ForEach(actions, id: \.self) { item in switch item { case .copy: Button { callback(item) } label: { @@ -44,6 +69,10 @@ public struct TimelineItemContextMenu: View { Button(role: .destructive) { callback(item) } label: { Label(ElementL10n.messageActionItemRedact, systemImage: "trash") } + case .viewSource: + Button { callback(item) } label: { + Label(ElementL10n.viewSource, systemImage: "doc.text.below.ecg") + } } } } diff --git a/ElementX/Sources/Services/Timeline/MockRoomTimelineController.swift b/ElementX/Sources/Services/Timeline/MockRoomTimelineController.swift index 2f7d9c42c..058c21538 100644 --- a/ElementX/Sources/Services/Timeline/MockRoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/MockRoomTimelineController.swift @@ -108,4 +108,8 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol { func sendReply(_ message: String, to itemId: String) async { } func redact(_ eventID: String) async { } + + func debugDescriptionFor(_ itemId: String) -> String { + "Mock debug description" + } } diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/RoomTimelineController.swift index 6adc77566..288718827 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineController.swift @@ -126,6 +126,25 @@ class RoomTimelineController: RoomTimelineControllerProtocol { break } } + + // Handle this paralel to the timeline items so we're not forced + // to bundle the Rust side objects within them + func debugDescriptionFor(_ itemId: String) -> String { + var description = "Unknown item" + timelineProvider.itemsPublisher.value.forEach { timelineItemProxy in + switch timelineItemProxy { + case .event(let item): + if item.id == itemId { + description = item.debugDescription + return + } + default: + break + } + } + + return description + } // MARK: - Private @@ -155,8 +174,6 @@ class RoomTimelineController: RoomTimelineControllerProtocol { switch itemProxy { case .event(let eventItem): - guard eventItem.isMessage || eventItem.isRedacted else { break } // To be handled in the future - newTimelineItems.append(timelineItemFactory.buildTimelineItemFor(eventItemProxy: eventItem, inGroupState: inGroupState)) default: diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineControllerProtocol.swift b/ElementX/Sources/Services/Timeline/RoomTimelineControllerProtocol.swift index 268a90801..9fec4caa5 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineControllerProtocol.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineControllerProtocol.swift @@ -46,4 +46,6 @@ protocol RoomTimelineControllerProtocol { func sendReply(_ message: String, to itemId: String) async func redact(_ eventID: String) async + + func debugDescriptionFor(_ itemId: String) -> String } diff --git a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift index 0d4dc634f..d3d20a400 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift @@ -20,7 +20,7 @@ import MatrixRustSDK enum TimelineItemProxy { case event(EventTimelineItemProxy) case virtual(MatrixRustSDK.VirtualTimelineItem) - case other(MatrixRustSDK.TimelineItem) + case unknown(MatrixRustSDK.TimelineItem) init(item: MatrixRustSDK.TimelineItem) { if let eventItem = item.asEvent() { @@ -28,7 +28,7 @@ enum TimelineItemProxy { } else if let virtualItem = item.asVirtual() { self = .virtual(virtualItem) } else { - self = .other(item) + self = .unknown(item) } } @@ -42,7 +42,7 @@ enum TimelineItemProxy { } /// A light wrapper around event timeline items returned from Rust. -struct EventTimelineItemProxy { +struct EventTimelineItemProxy: CustomDebugStringConvertible { let item: MatrixRustSDK.EventTimelineItem init(item: MatrixRustSDK.EventTimelineItem) { @@ -94,4 +94,10 @@ struct EventTimelineItemProxy { return .now } } + + // MARK: - CustomDebugStringConvertible + + var debugDescription: String { + item.fmtDebug() + } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/EncryptedRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/EncryptedRoomTimelineItem.swift new file mode 100644 index 000000000..381c50415 --- /dev/null +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/EncryptedRoomTimelineItem.swift @@ -0,0 +1,38 @@ +// +// Copyright 2022 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 UIKit + +struct EncryptedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equatable { + enum EncryptionType: Equatable { + case megolmV1AesSha2(sessionId: String) + case olmV1Curve25519AesSha2(senderKey: String) + case unknown + } + + let id: String + let text: String + let encryptionType: EncryptionType + let timestamp: String + let inGroupState: TimelineItemInGroupState + let isOutgoing: Bool + + let senderId: String + var senderDisplayName: String? + var senderAvatar: UIImage? + + var properties = RoomTimelineItemProperties() +} diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift index 2553a9f0c..e7bcf6902 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift @@ -41,6 +41,10 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { let avatarURL = roomProxy.avatarURLStringForUserId(eventItemProxy.sender) let avatarImage = mediaProvider.imageFromURLString(avatarURL, avatarSize: .user(on: .timeline)) let isOutgoing = eventItemProxy.isOwn + + if let encryptedMessage = eventItemProxy.content.asUnableToDecrypt() { + return buildEncryptedTimelineItem(eventItemProxy, encryptedMessage, isOutgoing, inGroupState, displayName, avatarImage) + } if eventItemProxy.isRedacted { return buildRedactedTimelineItem(eventItemProxy, isOutgoing, inGroupState, displayName, avatarImage) @@ -67,7 +71,36 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { } // MARK: - Private - + + // swiftlint:disable:next function_parameter_count + private func buildEncryptedTimelineItem(_ eventItemProxy: EventTimelineItemProxy, + _ encryptedMessage: EncryptedMessage, + _ isOutgoing: Bool, + _ inGroupState: TimelineItemInGroupState, + _ displayName: String?, + _ avatarImage: UIImage?) -> RoomTimelineItemProtocol { + var encryptionType = EncryptedRoomTimelineItem.EncryptionType.unknown + switch encryptedMessage { + case .megolmV1AesSha2(let sessionId): + encryptionType = .megolmV1AesSha2(sessionId: sessionId) + case .olmV1Curve25519AesSha2(let senderKey): + encryptionType = .olmV1Curve25519AesSha2(senderKey: senderKey) + default: + break + } + + return EncryptedRoomTimelineItem(id: eventItemProxy.id, + text: ElementL10n.encryptionInformationDecryptionError, + encryptionType: encryptionType, + timestamp: eventItemProxy.originServerTs.formatted(date: .omitted, time: .shortened), + inGroupState: inGroupState, + isOutgoing: isOutgoing, + senderId: eventItemProxy.sender, + senderDisplayName: displayName, + senderAvatar: avatarImage, + properties: RoomTimelineItemProperties()) + } + private func buildRedactedTimelineItem(_ eventItemProxy: EventTimelineItemProxy, _ isOutgoing: Bool, _ inGroupState: TimelineItemInGroupState, diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewFactory.swift index 82a577860..2bb6c6ef6 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewFactory.swift @@ -31,6 +31,8 @@ struct RoomTimelineViewFactory: RoomTimelineViewFactoryProtocol { return .emote(item) case let item as RedactedRoomTimelineItem: return .redacted(item) + case let item as EncryptedRoomTimelineItem: + return .encrypted(item) default: fatalError("Unknown timeline item") } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewProvider.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewProvider.swift index 327b98fa4..77b14d4aa 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewProvider.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewProvider.swift @@ -24,6 +24,7 @@ enum RoomTimelineViewProvider: Identifiable, Equatable { case emote(EmoteRoomTimelineItem) case notice(NoticeRoomTimelineItem) case redacted(RedactedRoomTimelineItem) + case encrypted(EncryptedRoomTimelineItem) var id: String { switch self { @@ -39,6 +40,8 @@ enum RoomTimelineViewProvider: Identifiable, Equatable { return item.id case .redacted(let item): return item.id + case .encrypted(let item): + return item.id } } } @@ -58,6 +61,8 @@ extension RoomTimelineViewProvider: View { NoticeRoomTimelineView(timelineItem: item) case .redacted(let item): RedactedRoomTimelineView(timelineItem: item) + case .encrypted(let item): + EncryptedRoomTimelineView(timelineItem: item) } } } diff --git a/changelog.d/291.feature b/changelog.d/291.feature new file mode 100644 index 000000000..b6bcd5008 --- /dev/null +++ b/changelog.d/291.feature @@ -0,0 +1 @@ +Added support for non-decryptable timeline items \ No newline at end of file diff --git a/changelog.d/292.feature b/changelog.d/292.feature new file mode 100644 index 000000000..69605b35f --- /dev/null +++ b/changelog.d/292.feature @@ -0,0 +1 @@ +Added a timeline item context menu option for printing and showing their debug description \ No newline at end of file