diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 2489417ebb..d8496bf96c 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C070FD43DC6BF4E50217965A /* LocalizationTests.swift */; }; 004561D297DC8B9786AE136F /* UITestScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FD9D66B75292F2CC11AA4D2 /* UITestScreenIdentifier.swift */; }; + 00EA14F62DCEF62CDE4808D6 /* RedactedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B577F829C693B8DFB7014FD /* RedactedRoomTimelineItem.swift */; }; 00F3059B1E0CFCA019710C3E /* BugReportModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B516212D9FE785DDD5E490D1 /* BugReportModels.swift */; }; 01CB8ACFA5E143E89C168CA8 /* TimelineItemContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = B43AF03660F5FD4FFFA7F1CE /* TimelineItemContextMenu.swift */; }; 01F4A40C1EDCEC8DC4EC9CFA /* WeakDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 109C0201D8CB3F947340DC80 /* WeakDictionary.swift */; }; @@ -39,6 +40,7 @@ 1281625B25371BE53D36CB3A /* SeparatorRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1ED7E89865201EE7D53E6DA /* SeparatorRoomTimelineItem.swift */; }; 12F70C493FB69F4D7E9A37EA /* NavigationRouterStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29EBCBFEC6FD0941749404D /* NavigationRouterStore.swift */; }; 132D241B09F9044711FD70A5 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 91DE43B8815918E590912DDA /* InfoPlist.strings */; }; + 13853973A5E24374FCEDE8A3 /* RedactedRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8F2A7A4E3F5060F52ACFFB0 /* RedactedRoomTimelineView.swift */; }; 13C77FDF17C4C6627CFFC205 /* RoomTimelineItemFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D25A35764C7B3DB78954AB5 /* RoomTimelineItemFactoryProtocol.swift */; }; 149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */; }; 152AE2B8650FB23AFD2E28B9 /* MockAuthenticationServiceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65C2B80DD0BF6F10BB5FA922 /* MockAuthenticationServiceProxy.swift */; }; @@ -650,6 +652,7 @@ 99DE232F24EAD72A3DF7EF1A /* kab */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = kab; path = kab.lproj/Localizable.stringsdict; sourceTree = ""; }; 9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportServiceProtocol.swift; sourceTree = ""; }; 9ABAECB0CA5FF8F8E6F10DD7 /* RoomTimelineProviderItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProviderItem.swift; sourceTree = ""; }; + 9B577F829C693B8DFB7014FD /* RedactedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactedRoomTimelineItem.swift; sourceTree = ""; }; 9C4048041C1A6B20CB97FD18 /* TestMeasurementParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestMeasurementParser.swift; sourceTree = ""; }; 9C5E81214D27A6B898FC397D /* ElementX.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ElementX.entitlements; sourceTree = ""; }; 9C7F7DE62D33C6A26CBFCD72 /* IntegrationTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = IntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -731,6 +734,7 @@ C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportUITests.swift; sourceTree = ""; }; C88508B6F7974CFABEC4B261 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; C888BCD78E2A55DCE364F160 /* MediaProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProviderProtocol.swift; sourceTree = ""; }; + C8F2A7A4E3F5060F52ACFFB0 /* RedactedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactedRoomTimelineView.swift; sourceTree = ""; }; C91A6BC1A54CDB598EE2A81B /* UserIndicatorQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorQueue.swift; sourceTree = ""; }; C95ADE8D9527523572532219 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hu; path = hu.lproj/Localizable.stringsdict; sourceTree = ""; }; C9A86C95340248A8B7BA9A43 /* AnalyticsPromptViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptViewModelProtocol.swift; sourceTree = ""; }; @@ -1325,6 +1329,7 @@ F77C060C2ACC4CB7336A29E7 /* EmoteRoomTimelineItem.swift */, 1A63815AD6A5C306453342F2 /* ImageRoomTimelineItem.swift */, 4F49CDE349C490D617332770 /* NoticeRoomTimelineItem.swift */, + 9B577F829C693B8DFB7014FD /* RedactedRoomTimelineItem.swift */, 289FA233E896FBC5956C67E0 /* RoomTimelineItemProperties.swift */, A1ED7E89865201EE7D53E6DA /* SeparatorRoomTimelineItem.swift */, F6A8C632CEF4600107792899 /* TextRoomTimelineItem.swift */, @@ -1645,6 +1650,7 @@ D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */, B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */, 0950733DD4BA83EEE752E259 /* PlaceholderAvatarImage.swift */, + C8F2A7A4E3F5060F52ACFFB0 /* RedactedRoomTimelineView.swift */, 6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */, F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */, ); @@ -2414,6 +2420,8 @@ BF35062D06888FA80BD139FF /* Presentable.swift in Sources */, C76892321558E75101E68ED6 /* ReadableFrameModifier.swift in Sources */, 53B9C2240C2F5533246EE230 /* RectangleToastView.swift in Sources */, + 00EA14F62DCEF62CDE4808D6 /* RedactedRoomTimelineItem.swift in Sources */, + 13853973A5E24374FCEDE8A3 /* RedactedRoomTimelineView.swift in Sources */, 04A16B45228F7678A027C079 /* RoomHeaderView.swift in Sources */, FE79E2BCCF69E8BF4D21E15A /* RoomMessageFactory.swift in Sources */, 8D9F646387DF656EF91EE4CB /* RoomMessageFactoryProtocol.swift in Sources */, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/RedactedRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/RedactedRoomTimelineView.swift new file mode 100644 index 0000000000..25468aa3e5 --- /dev/null +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/RedactedRoomTimelineView.swift @@ -0,0 +1,59 @@ +// +// 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 Foundation +import SwiftUI + +struct RedactedRoomTimelineView: View { + let timelineItem: RedactedRoomTimelineItem + + var body: some View { + TimelineStyler(timelineItem: timelineItem) { + HStack { + Image(systemName: "trash") + FormattedBodyText(isOutgoing: timelineItem.isOutgoing, text: timelineItem.text) + } + } + .id(timelineItem.id) + } +} + +struct RedactedRoomTimelineView_Previews: PreviewProvider { + static var previews: some View { + body.preferredColorScheme(.light) + body.preferredColorScheme(.dark) + } + + @ViewBuilder + static var body: some View { + VStack(alignment: .leading, spacing: 20.0) { + RedactedRoomTimelineView(timelineItem: itemWith(text: ElementL10n.eventRedacted, + timestamp: "Later", + senderId: "Anne")) + } + .padding() + } + + private static func itemWith(text: String, timestamp: String, senderId: String) -> RedactedRoomTimelineItem { + RedactedRoomTimelineItem(id: UUID().uuidString, + text: text, + timestamp: timestamp, + shouldShowSenderDetails: true, + inGroupState: .single, + isOutgoing: false, + senderId: senderId) + } +} diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/RoomTimelineController.swift index 73ffec8258..0e25b8b174 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineController.swift @@ -146,7 +146,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { switch item { case .event(let eventItem): - guard eventItem.isMessage else { break } // To be handled in the future + guard eventItem.isMessage || eventItem.isRedacted else { break } // To be handled in the future newTimelineItems.append(await timelineItemFactory.buildTimelineItemFor(eventItem: eventItem, showSenderDetails: inGroupState.shouldShowSenderDetails, diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineProviderItem.swift b/ElementX/Sources/Services/Timeline/RoomTimelineProviderItem.swift index 6d7bcc1acf..73d8d90683 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineProviderItem.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineProviderItem.swift @@ -70,6 +70,14 @@ struct EventTimelineItem { var content: TimelineItemContent { item.content() } + + var isRedacted: Bool { + content.isRedactedMessage() + } + + var isOwn: Bool { + item.isOwn() + } var sender: String { item.sender() diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/RedactedRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/RedactedRoomTimelineItem.swift new file mode 100644 index 0000000000..fe43ed458f --- /dev/null +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/RedactedRoomTimelineItem.swift @@ -0,0 +1,33 @@ +// +// 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 Foundation +import UIKit + +struct RedactedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equatable { + let id: String + let text: String + let timestamp: String + let shouldShowSenderDetails: Bool + 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 e8a5675383..cbb2f05efb 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift @@ -38,11 +38,16 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { func buildTimelineItemFor(eventItem: EventTimelineItem, showSenderDetails: Bool, inGroupState: TimelineItemInGroupState) async -> RoomTimelineItemProtocol { - guard let messageContent = eventItem.content.asMessage() else { fatalError("Must be a message for now.") } let displayName = roomProxy.displayNameForUserId(eventItem.sender) let avatarURL = roomProxy.avatarURLStringForUserId(eventItem.sender) let avatarImage = mediaProvider.imageFromURLString(avatarURL, size: MediaProviderDefaultAvatarSize) - let isOutgoing = eventItem.sender == userID + let isOutgoing = eventItem.isOwn + + if eventItem.isRedacted { + return buildRedactedTimelineItemFromEvent(eventItem, isOutgoing, showSenderDetails, inGroupState, displayName, avatarImage) + } + + guard let messageContent = eventItem.content.asMessage() else { fatalError("Must be a message for now.") } switch messageContent.msgtype() { case .text(content: let content): @@ -66,6 +71,24 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { // swiftformat:disable function_parameter_count // swiftlint:disable function_parameter_count + private func buildRedactedTimelineItemFromEvent(_ event: EventTimelineItem, + _ isOutgoing: Bool, + _ showSenderDetails: Bool, + _ inGroupState: TimelineItemInGroupState, + _ displayName: String?, + _ avatarImage: UIImage?) -> RoomTimelineItemProtocol { + RedactedRoomTimelineItem(id: event.id, + text: ElementL10n.eventRedacted, + timestamp: event.originServerTs.formatted(date: .omitted, time: .shortened), + shouldShowSenderDetails: showSenderDetails, + inGroupState: inGroupState, + isOutgoing: isOutgoing, + senderId: event.sender, + senderDisplayName: displayName, + senderAvatar: avatarImage, + properties: RoomTimelineItemProperties()) + } + private func buildFallbackTimelineItem(_ item: EventTimelineItem, _ isOutgoing: Bool, _ showSenderDetails: Bool, diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewFactory.swift index f0ae752e3b..82a5778607 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewFactory.swift @@ -29,6 +29,8 @@ struct RoomTimelineViewFactory: RoomTimelineViewFactoryProtocol { return .notice(item) case let item as EmoteRoomTimelineItem: return .emote(item) + case let item as RedactedRoomTimelineItem: + return .redacted(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 8720d5050d..327b98fa41 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewProvider.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewProvider.swift @@ -23,6 +23,7 @@ enum RoomTimelineViewProvider: Identifiable, Equatable { case image(ImageRoomTimelineItem) case emote(EmoteRoomTimelineItem) case notice(NoticeRoomTimelineItem) + case redacted(RedactedRoomTimelineItem) var id: String { switch self { @@ -36,6 +37,8 @@ enum RoomTimelineViewProvider: Identifiable, Equatable { return item.id case .notice(let item): return item.id + case .redacted(let item): + return item.id } } } @@ -53,6 +56,8 @@ extension RoomTimelineViewProvider: View { EmoteRoomTimelineView(timelineItem: item) case .notice(let item): NoticeRoomTimelineView(timelineItem: item) + case .redacted(let item): + RedactedRoomTimelineView(timelineItem: item) } } } diff --git a/UnitTests/Sources/SettingsViewModelTests.swift b/UnitTests/Sources/SettingsViewModelTests.swift index de48ef1080..0ce792c228 100644 --- a/UnitTests/Sources/SettingsViewModelTests.swift +++ b/UnitTests/Sources/SettingsViewModelTests.swift @@ -24,7 +24,9 @@ class SettingsViewModelTests: XCTestCase { var context: SettingsViewModelType.Context! @MainActor override func setUpWithError() throws { - viewModel = SettingsViewModel() + let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: ""), + mediaProvider: MockMediaProvider()) + viewModel = SettingsViewModel(withUserSession: userSession) context = viewModel.context } diff --git a/changelog.d/pr-199.change b/changelog.d/pr-199.change new file mode 100644 index 0000000000..827b937a26 --- /dev/null +++ b/changelog.d/pr-199.change @@ -0,0 +1 @@ +Include redacted events in the timeline.