From 5a14384f71047b77a95024837a99379fa8a80b2e Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 18 Jan 2023 10:46:58 +0200 Subject: [PATCH 1/5] State events in the timeline WIP # Conflicts: # ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift --- ElementX.xcodeproj/project.pbxproj | 20 +++- .../xcshareddata/xcschemes/ElementX.xcscheme | 37 +++---- .../View/Timeline/StateRoomTimelineView.swift | 37 +++++++ .../Items/Other/StateRoomTimelineItem.swift | 34 +++++++ .../RoomStateTimelineItemFactory.swift | 96 +++++++++++++++++++ .../RoomTimelineItemFactory.swift | 8 +- .../RoomTimelineViewFactory.swift | 2 + .../RoomTimelineViewProvider.swift | 5 + 8 files changed, 209 insertions(+), 30 deletions(-) create mode 100644 ElementX/Sources/Screens/RoomScreen/View/Timeline/StateRoomTimelineView.swift create mode 100644 ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StateRoomTimelineItem.swift create mode 100644 ElementX/Sources/Services/Timeline/TimelineItems/RoomStateTimelineItemFactory.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index ffaf8f0f15..155e6193f1 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -58,6 +58,9 @@ 1702981A8085BE4FB0EC001B /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33116993D54FADC0C721C1F /* Application.swift */; }; 172E6E9A612ADCF10A62CF13 /* BugReportServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */; }; 187E18F21EF4DA244E436E58 /* BugReportViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28959C7DB36C7688A01D4045 /* BugReportViewModelProtocol.swift */; }; + 18E674DB2977DBD60055EA9F /* StateRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E674DA2977DBD60055EA9F /* StateRoomTimelineItem.swift */; }; + 18E674DD2977DC2B0055EA9F /* StateRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E674DC2977DC2B0055EA9F /* StateRoomTimelineView.swift */; }; + 18E674DF2977DD9B0055EA9F /* RoomStateTimelineItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E674DE2977DD9B0055EA9F /* RoomStateTimelineItemFactory.swift */; }; 191161FE9E0DA89704301F37 /* Untranslated.strings in Resources */ = {isa = PBXBuildFile; fileRef = D2F7194F440375338F8E2487 /* Untranslated.strings */; }; 1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */; }; 19839F3526CE8C35AAF241AD /* ServerSelectionViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F52BF30D12BA3BD3D3DBB8F /* ServerSelectionViewModelProtocol.swift */; }; @@ -580,6 +583,9 @@ 1734A445A58ED855B977A0A8 /* TracingConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingConfigurationTests.swift; sourceTree = ""; }; 179423E34EE846E048E49CBF /* MediaSourceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaSourceProxy.swift; sourceTree = ""; }; 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecorationTimelineItemProtocol.swift; sourceTree = ""; }; + 18E674DA2977DBD60055EA9F /* StateRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRoomTimelineItem.swift; sourceTree = ""; }; + 18E674DC2977DC2B0055EA9F /* StateRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRoomTimelineView.swift; sourceTree = ""; }; + 18E674DE2977DD9B0055EA9F /* RoomStateTimelineItemFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateTimelineItemFactory.swift; sourceTree = ""; }; 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxy.swift; sourceTree = ""; }; 18FE0CDF1FFA92EA7EE17B0B /* RoomTimelineControllerFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineControllerFactoryProtocol.swift; sourceTree = ""; }; 1941C8817E6B6971BA4415F5 /* VideoRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineView.swift; sourceTree = ""; }; @@ -679,7 +685,7 @@ 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxyProtocol.swift; sourceTree = ""; }; 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineView.swift; sourceTree = ""; }; 475EB595D7527E9A8A14043E /* uz */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uz; path = uz.lproj/Localizable.strings; sourceTree = ""; }; - 478BE8591BD13E908EF70C0C /* DesignKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = DesignKit; path = DesignKit; sourceTree = SOURCE_ROOT; }; + 478BE8591BD13E908EF70C0C /* DesignKit */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DesignKit; sourceTree = SOURCE_ROOT; }; 4798B3B7A1E8AE3901CEE8C6 /* FramePreferenceKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramePreferenceKey.swift; sourceTree = ""; }; 47EBB5D698CE9A25BB553A2D /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; 48CE6BF18E542B32FA52CE06 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fa; path = fa.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -806,7 +812,7 @@ 8D6094DEAAEB388E1AE118C6 /* MockRoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineProvider.swift; sourceTree = ""; }; 8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = ""; }; - 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = ""; }; + 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UITests.xctestplan; sourceTree = ""; }; 8ED2D2F6A137A95EA50413BE /* UserNotificationControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationControllerProtocol.swift; sourceTree = ""; }; 8F7D42E66E939B709C1EC390 /* MockRoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomSummaryProvider.swift; sourceTree = ""; }; 8FC26871038FB0E4AAE22605 /* apple_emojis_data.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = apple_emojis_data.json; sourceTree = ""; }; @@ -1019,7 +1025,7 @@ EC589E641AE46EFB2962534D /* RoomMemberDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsViewModelTests.swift; sourceTree = ""; }; ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomCell.swift; sourceTree = ""; }; ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = ""; }; - ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = ""; }; + ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = message.caf; sourceTree = ""; }; EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModelTests.swift; sourceTree = ""; }; EDB6E40BAD4504D899FAAC9A /* TemplateViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateViewModel.swift; sourceTree = ""; }; EE8BCD14EFED23459A43FDFF /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; @@ -1983,6 +1989,7 @@ EEE384418EB1FEDFA62C9CD0 /* RoomTimelineViewFactoryProtocol.swift */, ACB6C5E4950B6C9842F35A38 /* RoomTimelineViewProvider.swift */, 75D1D02F7F3AC1122FCFB4F3 /* Items */, + 18E674DE2977DD9B0055EA9F /* RoomStateTimelineItemFactory.swift */, ); path = TimelineItems; sourceTree = ""; @@ -2116,6 +2123,7 @@ E6E6BDF9D26DB05C88901416 /* RedactedRoomTimelineItem.swift */, 818695BED971753243FEF897 /* StickerRoomTimelineItem.swift */, F1B8500C152BC59445647DA8 /* UnsupportedRoomTimelineItem.swift */, + 18E674DA2977DBD60055EA9F /* StateRoomTimelineItem.swift */, ); path = Other; sourceTree = ""; @@ -2163,6 +2171,7 @@ F9ED8E731E21055F728E5FED /* TimelineStartRoomTimelineView.swift */, A2AC3C656E960E15B5905E05 /* UnsupportedRoomTimelineView.swift */, 1941C8817E6B6971BA4415F5 /* VideoRoomTimelineView.swift */, + 18E674DC2977DC2B0055EA9F /* StateRoomTimelineView.swift */, ); path = Timeline; sourceTree = ""; @@ -2965,6 +2974,7 @@ 64FF5CB4E35971255872E1BB /* AuthenticationServiceProxyProtocol.swift in Sources */, D876EC0FED3B6D46C806912A /* AvatarSize.swift in Sources */, E0A4DCA633D174EB43AD599F /* BackgroundTaskProtocol.swift in Sources */, + 18E674DF2977DD9B0055EA9F /* RoomStateTimelineItemFactory.swift in Sources */, 6D046D653DA28ADF1E6E59A4 /* BackgroundTaskServiceProtocol.swift in Sources */, 38546A6010A2CF240EC9AF73 /* BindableState.swift in Sources */, B6DF6B6FA8734B70F9BF261E /* BlurHashDecode.swift in Sources */, @@ -2995,6 +3005,7 @@ 3910D3A2EF98587C0E7B9CCB /* EmojiMartEmoji.swift in Sources */, 7E3C34BC10936AD4F77975F4 /* EmojiMartJSONLoader.swift in Sources */, 92B95779840CD749117B3615 /* EmojiMartStore.swift in Sources */, + 18E674DB2977DBD60055EA9F /* StateRoomTimelineItem.swift in Sources */, 1A8BDEB96C3B2F033FA563F8 /* EmojiPickerHeaderView.swift in Sources */, 340D39DB87F3800D53A6A621 /* EmojiPickerScreen.swift in Sources */, C1910A16BDF131FECA77BE22 /* EmojiPickerScreenCoordinator.swift in Sources */, @@ -3039,6 +3050,7 @@ CEB8FB1269DE20536608B957 /* LoginMode.swift in Sources */, 38C76D586404C1FDED095F3A /* LoginModels.swift in Sources */, 5375902175B2FEA2949D7D74 /* LoginScreen.swift in Sources */, + 18E674DD2977DC2B0055EA9F /* StateRoomTimelineView.swift in Sources */, BCEC41FB1F2BB663183863E4 /* LoginServerInfoSection.swift in Sources */, 49E9B99CB6A275C7744351F0 /* LoginViewModel.swift in Sources */, 2F30EFEB7BD39242D1AD96F3 /* LoginViewModelProtocol.swift in Sources */, diff --git a/ElementX.xcodeproj/xcshareddata/xcschemes/ElementX.xcscheme b/ElementX.xcodeproj/xcshareddata/xcschemes/ElementX.xcscheme index 3b14dd2e5c..b04048625a 100644 --- a/ElementX.xcodeproj/xcshareddata/xcschemes/ElementX.xcscheme +++ b/ElementX.xcodeproj/xcshareddata/xcschemes/ElementX.xcscheme @@ -1,11 +1,10 @@ + version = "1.3"> + buildImplicitDependencies = "YES"> - - - - - - - - + + + + + + - - + isEnabled = "YES"> @@ -121,8 +116,6 @@ ReferencedContainer = "container:ElementX.xcodeproj"> - - diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/StateRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/StateRoomTimelineView.swift new file mode 100644 index 0000000000..cac5ca5210 --- /dev/null +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/StateRoomTimelineView.swift @@ -0,0 +1,37 @@ +// +// Copyright 2023 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 StateRoomTimelineView: View { + let timelineItem: StateRoomTimelineItem + + var body: some View { + Text(timelineItem.text) + } +} + +struct StateRoomTimelineView_Previews: PreviewProvider { + static var previews: some View { + body + body.timelineStyle(.plain) + } + + static var body: some View { + let item = StateRoomTimelineItem(id: UUID().uuidString, text: "Alice joined", timestamp: "Now", groupState: .beginning, isOutgoing: false, isEditable: false, senderId: "") + return StateRoomTimelineView(timelineItem: item) + } +} diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StateRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StateRoomTimelineItem.swift new file mode 100644 index 0000000000..958e1c6556 --- /dev/null +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StateRoomTimelineItem.swift @@ -0,0 +1,34 @@ +// +// Copyright 2023 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 StateRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable { + let id: String + let text: String + let timestamp: String + let groupState: TimelineItemGroupState + let isOutgoing: Bool + let isEditable: Bool + + let senderId: String + var senderDisplayName: String? + + var senderAvatarURL: URL? + var senderAvatar: UIImage? + + var properties = RoomTimelineItemProperties() +} diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomStateTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomStateTimelineItemFactory.swift new file mode 100644 index 0000000000..345d785149 --- /dev/null +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomStateTimelineItemFactory.swift @@ -0,0 +1,96 @@ +// +// Copyright 2023 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 MatrixRustSDK +import UIKit + +struct RoomStateTimelineItemFactory { + static func buildStateTimelineItemFor(eventItemProxy: EventTimelineItemProxy, + isOutgoing: Bool, + avatarImage: UIImage?, + stateKey: String, + content: OtherState) -> RoomTimelineItemProtocol { + buildDefault(eventItemProxy: eventItemProxy, text: UUID().uuidString, isOutgoing: isOutgoing, avatarImage: avatarImage) + } + + static func buildStateMembershipChangeTimelineItemFor(eventItemProxy: EventTimelineItemProxy, + isOutgoing: Bool, + avatarImage: UIImage?, + change: MembershipChange) -> RoomTimelineItemProtocol { + let text = textForMembershipChange(change, member: eventItemProxy.senderDisplayName ?? eventItemProxy.sender) + return buildDefault(eventItemProxy: eventItemProxy, text: text, isOutgoing: isOutgoing, avatarImage: avatarImage) + } + + // MARK: - Private + + private static func buildDefault(eventItemProxy: EventTimelineItemProxy, text: String, isOutgoing: Bool, avatarImage: UIImage?) -> RoomTimelineItemProtocol { + StateRoomTimelineItem(id: eventItemProxy.id, + text: text, + timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened), + groupState: .single, + isOutgoing: isOutgoing, + isEditable: false, + senderId: eventItemProxy.sender, + senderDisplayName: eventItemProxy.senderDisplayName, + senderAvatarURL: eventItemProxy.senderAvatarURL, + senderAvatar: avatarImage) + } + + private static func textForMembershipChange(_ change: MembershipChange, member: String) -> String { + switch change { + case .none: + break + case .error: + break + case .joined: + return ElementL10n.noticeRoomJoin(member) + case .left: + return ElementL10n.noticeRoomLeave(member) + case .banned: + break + case .unbanned: + break + case .kicked: + break + case .invited: + break + case .kickedAndBanned: + break + case .invitationAccepted: + break + case .invitationRejected: + break + case .invitationRevoked: + break + case .knocked: + break + case .knockAccepted: + break + case .knockRetracted: + break + case .knockDenied: + break + case .profileChanged(let displayName, let previousDisplayName, let avatarURL, let previousAvatarURL): + break + case .notImplemented: + break + case .unknown(let membershipState, let displayName, let avatarURLString): + break + } + + return "" + } +} diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift index be579ab75e..7b3dc1fe95 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift @@ -79,10 +79,10 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { case .none: return buildFallbackTimelineItem(eventItemProxy, sender, isOutgoing, groupState) } - case .state: - return buildFallbackTimelineItem(eventItemProxy, sender, isOutgoing, groupState) - case .roomMembership: - return buildFallbackTimelineItem(eventItemProxy, sender, isOutgoing, groupState) + case .state(let stateKey, let content): + return RoomStateTimelineItemFactory.buildStateTimelineItemFor(eventItemProxy: eventItemProxy, isOutgoing: isOutgoing, avatarImage: avatarImage, stateKey: stateKey, content: content) + case .roomMembership(let change): + return RoomStateTimelineItemFactory.buildStateMembershipChangeTimelineItemFor(eventItemProxy: eventItemProxy, isOutgoing: isOutgoing, avatarImage: avatarImage, change: change) } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewFactory.swift index 44505a2566..008f17ab71 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewFactory.swift @@ -48,6 +48,8 @@ struct RoomTimelineViewFactory: RoomTimelineViewFactoryProtocol { return .unsupported(item) case let item as TimelineStartRoomTimelineItem: return .timelineStart(item) + case let item as StateRoomTimelineItem: + return .state(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 185307f717..a73509cd92 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewProvider.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewProvider.swift @@ -32,6 +32,7 @@ enum RoomTimelineViewProvider: Identifiable, Hashable { case sticker(StickerRoomTimelineItem) case unsupported(UnsupportedRoomTimelineItem) case timelineStart(TimelineStartRoomTimelineItem) + case state(StateRoomTimelineItem) var id: String { switch self { @@ -63,6 +64,8 @@ enum RoomTimelineViewProvider: Identifiable, Hashable { return item.id case .timelineStart(let item): return item.id + case .state(let item): + return item.id } } } @@ -98,6 +101,8 @@ extension RoomTimelineViewProvider: View { UnsupportedRoomTimelineView(timelineItem: item) case .timelineStart(let item): TimelineStartRoomTimelineView(timelineItem: item) + case .state(let item): + StateRoomTimelineView(timelineItem: item) } } } From 39ea97f0a7c1337ebeb96f48fe993fc85b01b131 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 18 Jan 2023 18:43:51 +0000 Subject: [PATCH 2/5] Implement membership change strings. --- .../en.lproj/Untranslated.strings | 15 ++ .../Generated/Strings+Untranslated.swift | 50 ++++++ .../View/Timeline/StateRoomTimelineView.swift | 2 +- .../Items/Other/StateRoomTimelineItem.swift | 6 +- .../RoomStateTimelineItemFactory.swift | 155 +++++++++++++----- .../RoomTimelineItemFactory.swift | 9 +- 6 files changed, 187 insertions(+), 50 deletions(-) diff --git a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings index 8c5997399d..b3c7311f98 100644 --- a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings +++ b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings @@ -24,6 +24,21 @@ "room_timeline_item_unsupported" = "Unsupported event"; +"noticeRoomInviteAccepted" = "%1$@ accepted the invite"; +"noticeRoomInviteAcceptedByYou" = "You accepted the invite"; +"noticeRoomKnock" = "%1$@ requested to join"; +"noticeRoomKnockByYou" = "You requested to join"; +"noticeRoomKnockAccepted" = "%1$@ allowed %2$@ to join"; +"noticeRoomKnockAcceptedByYou" = "%1$@ allowed you to join"; +"noticeRoomKnockRetracted" = "%1$@ is no longer interested in joining"; +"noticeRoomKnockRetractedByYou" = "You cancelled your request to join"; +"noticeRoomKnockDenied" = "%1$@ rejected %2$@'s request to join"; +"noticeRoomKnockDeniedByYou" = "You rejected %1$@'s request to join"; +"noticeRoomKnockDeniedYou" = "%1$@ rejected your request to join"; +"noticeRoomUnknownChange" = "%1$@ made an unknown change in the room"; +"noticeRoomUnknownMembershipChange" = "%1$@ made an unknown change to their membership"; +"noticeRoomMembershipError" = "Membership error for %1$@"; + "session_verification_banner_title" = "Help keep your messages secure"; "session_verification_banner_message" = "Looks like you’re using a new device. Verify its you."; diff --git a/ElementX/Sources/Generated/Strings+Untranslated.swift b/ElementX/Sources/Generated/Strings+Untranslated.swift index bdb639f280..b44c9bad9f 100644 --- a/ElementX/Sources/Generated/Strings+Untranslated.swift +++ b/ElementX/Sources/Generated/Strings+Untranslated.swift @@ -24,6 +24,56 @@ extension ElementL10n { public static let loginMobileDevice = ElementL10n.tr("Untranslated", "login_mobile_device") /// Tablet public static let loginTabletDevice = ElementL10n.tr("Untranslated", "login_tablet_device") + /// %1$@ accepted the invite + public static func noticeRoomInviteAccepted(_ p1: Any) -> String { + return ElementL10n.tr("Untranslated", "noticeRoomInviteAccepted", String(describing: p1)) + } + /// You accepted the invite + public static let noticeRoomInviteAcceptedByYou = ElementL10n.tr("Untranslated", "noticeRoomInviteAcceptedByYou") + /// %1$@ requested to join + public static func noticeRoomKnock(_ p1: Any) -> String { + return ElementL10n.tr("Untranslated", "noticeRoomKnock", String(describing: p1)) + } + /// %1$@ allowed %2$@ to join + public static func noticeRoomKnockAccepted(_ p1: Any, _ p2: Any) -> String { + return ElementL10n.tr("Untranslated", "noticeRoomKnockAccepted", String(describing: p1), String(describing: p2)) + } + /// %1$@ allowed you to join + public static func noticeRoomKnockAcceptedByYou(_ p1: Any) -> String { + return ElementL10n.tr("Untranslated", "noticeRoomKnockAcceptedByYou", String(describing: p1)) + } + /// You requested to join + public static let noticeRoomKnockByYou = ElementL10n.tr("Untranslated", "noticeRoomKnockByYou") + /// %1$@ rejected %2$@'s request to join + public static func noticeRoomKnockDenied(_ p1: Any, _ p2: Any) -> String { + return ElementL10n.tr("Untranslated", "noticeRoomKnockDenied", String(describing: p1), String(describing: p2)) + } + /// You rejected %1$@'s request to join + public static func noticeRoomKnockDeniedByYou(_ p1: Any) -> String { + return ElementL10n.tr("Untranslated", "noticeRoomKnockDeniedByYou", String(describing: p1)) + } + /// %1$@ rejected your request to join + public static func noticeRoomKnockDeniedYou(_ p1: Any) -> String { + return ElementL10n.tr("Untranslated", "noticeRoomKnockDeniedYou", String(describing: p1)) + } + /// %1$@ is no longer interested in joining + public static func noticeRoomKnockRetracted(_ p1: Any) -> String { + return ElementL10n.tr("Untranslated", "noticeRoomKnockRetracted", String(describing: p1)) + } + /// You cancelled your request to join + public static let noticeRoomKnockRetractedByYou = ElementL10n.tr("Untranslated", "noticeRoomKnockRetractedByYou") + /// Membership error for %1$@ + public static func noticeRoomMembershipError(_ p1: Any) -> String { + return ElementL10n.tr("Untranslated", "noticeRoomMembershipError", String(describing: p1)) + } + /// %1$@ made an unknown change in the room + public static func noticeRoomUnknownChange(_ p1: Any) -> String { + return ElementL10n.tr("Untranslated", "noticeRoomUnknownChange", String(describing: p1)) + } + /// %1$@ made an unknown change to their membership + public static func noticeRoomUnknownMembershipChange(_ p1: Any) -> String { + return ElementL10n.tr("Untranslated", "noticeRoomUnknownMembershipChange", String(describing: p1)) + } /// Notification public static let notification = ElementL10n.tr("Untranslated", "Notification") /// About diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/StateRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/StateRoomTimelineView.swift index cac5ca5210..ff1fe58585 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/StateRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/StateRoomTimelineView.swift @@ -31,7 +31,7 @@ struct StateRoomTimelineView_Previews: PreviewProvider { } static var body: some View { - let item = StateRoomTimelineItem(id: UUID().uuidString, text: "Alice joined", timestamp: "Now", groupState: .beginning, isOutgoing: false, isEditable: false, senderId: "") + let item = StateRoomTimelineItem(id: UUID().uuidString, text: "Alice joined", timestamp: "Now", groupState: .beginning, isOutgoing: false, isEditable: false, sender: .init(id: "")) return StateRoomTimelineView(timelineItem: item) } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StateRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StateRoomTimelineItem.swift index 958e1c6556..d53b2ab85e 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StateRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StateRoomTimelineItem.swift @@ -24,11 +24,7 @@ struct StateRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hash let isOutgoing: Bool let isEditable: Bool - let senderId: String - var senderDisplayName: String? - - var senderAvatarURL: URL? - var senderAvatar: UIImage? + var sender: TimelineItemSender var properties = RoomTimelineItemProperties() } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomStateTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomStateTimelineItemFactory.swift index 345d785149..9c2eb4d7d8 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomStateTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomStateTimelineItemFactory.swift @@ -18,79 +18,152 @@ import MatrixRustSDK import UIKit struct RoomStateTimelineItemFactory { - static func buildStateTimelineItemFor(eventItemProxy: EventTimelineItemProxy, - isOutgoing: Bool, - avatarImage: UIImage?, - stateKey: String, - content: OtherState) -> RoomTimelineItemProtocol { - buildDefault(eventItemProxy: eventItemProxy, text: UUID().uuidString, isOutgoing: isOutgoing, avatarImage: avatarImage) + let userID: String + + func buildStateTimelineItemFor(eventItemProxy: EventTimelineItemProxy, + content: OtherState, + stateKey: String, + sender: TimelineItemSender, + isOutgoing: Bool) -> RoomTimelineItemProtocol { + let text = textForOtherState(content, stateKey: stateKey, sender: sender, isOutgoing: isOutgoing) + return buildStateTimelineItem(eventItemProxy: eventItemProxy, text: text, sender: sender, isOutgoing: isOutgoing) } - static func buildStateMembershipChangeTimelineItemFor(eventItemProxy: EventTimelineItemProxy, - isOutgoing: Bool, - avatarImage: UIImage?, - change: MembershipChange) -> RoomTimelineItemProtocol { - let text = textForMembershipChange(change, member: eventItemProxy.senderDisplayName ?? eventItemProxy.sender) - return buildDefault(eventItemProxy: eventItemProxy, text: text, isOutgoing: isOutgoing, avatarImage: avatarImage) + func buildStateMembershipChangeTimelineItemFor(eventItemProxy: EventTimelineItemProxy, + member: String, + change: MembershipChange, + sender: TimelineItemSender, + isOutgoing: Bool) -> RoomTimelineItemProtocol { + let text = textForMembershipChange(change, member: member, sender: eventItemProxy.sender, isOutgoing: isOutgoing) + return buildStateTimelineItem(eventItemProxy: eventItemProxy, text: text, sender: sender, isOutgoing: isOutgoing) } // MARK: - Private - private static func buildDefault(eventItemProxy: EventTimelineItemProxy, text: String, isOutgoing: Bool, avatarImage: UIImage?) -> RoomTimelineItemProtocol { + private func buildStateTimelineItem(eventItemProxy: EventTimelineItemProxy, text: String, sender: TimelineItemSender, isOutgoing: Bool) -> RoomTimelineItemProtocol { StateRoomTimelineItem(id: eventItemProxy.id, text: text, timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened), groupState: .single, isOutgoing: isOutgoing, isEditable: false, - senderId: eventItemProxy.sender, - senderDisplayName: eventItemProxy.senderDisplayName, - senderAvatarURL: eventItemProxy.senderAvatarURL, - senderAvatar: avatarImage) + sender: sender) } - private static func textForMembershipChange(_ change: MembershipChange, member: String) -> String { + // swiftlint:disable:next cyclomatic_complexity function_body_length + private func textForMembershipChange(_ change: MembershipChange, member: String, sender: TimelineItemSender, isOutgoing: Bool) -> String { + let senderName = sender.displayName ?? sender.id + let senderIsYou = isOutgoing + let memberIsYou = member == userID + switch change { case .none: - break + return senderIsYou ? ElementL10n.noticeMemberNoChangesByYou : ElementL10n.noticeMemberNoChanges(member) case .error: - break + return ElementL10n.noticeRoomMembershipError(member) case .joined: - return ElementL10n.noticeRoomJoin(member) + return memberIsYou ? ElementL10n.noticeRoomJoinByYou : ElementL10n.noticeRoomJoin(member) case .left: - return ElementL10n.noticeRoomLeave(member) - case .banned: - break + return memberIsYou ? ElementL10n.noticeRoomLeaveByYou : ElementL10n.noticeRoomLeave(member) + case .banned, .kickedAndBanned: + return senderIsYou ? ElementL10n.noticeRoomBanByYou(member) : ElementL10n.noticeRoomBan(senderName, member) case .unbanned: - break + return senderIsYou ? ElementL10n.noticeRoomUnbanByYou(member) : ElementL10n.noticeRoomUnban(senderName, member) case .kicked: - break + return senderIsYou ? ElementL10n.noticeRoomRemoveByYou(member) : ElementL10n.noticeRoomRemove(senderName, member) case .invited: - break - case .kickedAndBanned: - break + if senderIsYou { + return ElementL10n.noticeRoomInviteByYou(member) + } else if memberIsYou { + return ElementL10n.noticeRoomInviteYou(senderName) + } else { + return ElementL10n.noticeRoomInvite(senderName, member) + } case .invitationAccepted: - break + return memberIsYou ? ElementL10n.noticeRoomInviteAcceptedByYou : ElementL10n.noticeRoomInviteAccepted(member) case .invitationRejected: - break + return memberIsYou ? ElementL10n.noticeRoomRejectByYou : ElementL10n.noticeRoomReject(member) case .invitationRevoked: - break + return senderIsYou ? ElementL10n.noticeRoomThirdPartyRevokedInviteByYou(member) : ElementL10n.noticeRoomThirdPartyRevokedInvite(sender, member) case .knocked: - break + return memberIsYou ? ElementL10n.noticeRoomKnockByYou : ElementL10n.noticeRoomKnock(member) case .knockAccepted: - break + return senderIsYou ? ElementL10n.noticeRoomKnockAcceptedByYou(senderName) : ElementL10n.noticeRoomKnockAccepted(senderName, member) case .knockRetracted: - break + return memberIsYou ? ElementL10n.noticeRoomKnockRetractedByYou : ElementL10n.noticeRoomKnockRetracted(member) case .knockDenied: - break - case .profileChanged(let displayName, let previousDisplayName, let avatarURL, let previousAvatarURL): - break + if senderIsYou { + return ElementL10n.noticeRoomKnockDeniedByYou(member) + } else if memberIsYou { + return ElementL10n.noticeRoomKnockDeniedYou(senderName) + } else { + return ElementL10n.noticeRoomKnockDenied(senderName, member) + } + case .profileChanged(let displayName, let previousDisplayName, let avatarURLString, let previousAvatarURLString): + return profileChangedString(displayName: displayName, previousDisplayName: previousDisplayName, + avatarURLString: avatarURLString, previousAvatarURLString: previousAvatarURLString, + member: member, memberIsYou: memberIsYou, + sender: sender, senderIsYou: senderIsYou) case .notImplemented: - break - case .unknown(let membershipState, let displayName, let avatarURLString): - break + return ElementL10n.noticeRoomUnknownChange(sender) + case .unknown(_, let displayName, _): + return ElementL10n.noticeRoomUnknownMembershipChange(displayName ?? member) } return "" } + + // swiftlint:disable:next cyclomatic_complexity function_parameter_count + private func profileChangedString(displayName: String?, previousDisplayName: String?, + avatarURLString: String?, previousAvatarURLString: String?, + member: String, memberIsYou: Bool, + sender: TimelineItemSender, senderIsYou: Bool) -> String { + let displayNameChanged = displayName != previousDisplayName + let avatarChanged = avatarURLString != previousAvatarURLString + + switch (displayNameChanged, avatarChanged, memberIsYou) { + case (true, false, false): + if let displayName, let previousDisplayName { + return ElementL10n.noticeDisplayNameChangedFrom(member, displayName, previousDisplayName) + } else if let displayName { + return ElementL10n.noticeDisplayNameSet(member, displayName) + } else if let previousDisplayName { + return ElementL10n.noticeDisplayNameRemoved(member, previousDisplayName) + } else { + return ElementL10n.noticeMemberNoChanges(member) + } + case (false, true, false): + return ElementL10n.noticeAvatarUrlChanged(member) + case (true, true, false): + return profileChangedString(displayName: displayName, previousDisplayName: previousDisplayName, + avatarURLString: nil, previousAvatarURLString: nil, + member: member, memberIsYou: memberIsYou, + sender: sender, senderIsYou: senderIsYou) + "\n" + ElementL10n.noticeAvatarChangedToo + case (false, false, false): + return ElementL10n.noticeMemberNoChanges(member) + case (true, false, true): + if let displayName, let previousDisplayName { + return ElementL10n.noticeDisplayNameChangedFromByYou(displayName, previousDisplayName) + } else if let displayName { + return ElementL10n.noticeDisplayNameSetByYou(displayName) + } else if let previousDisplayName { + return ElementL10n.noticeDisplayNameRemovedByYou(previousDisplayName) + } else { + return ElementL10n.noticeMemberNoChangesByYou + } + case (false, true, true): + return ElementL10n.noticeAvatarUrlChangedByYou + case (true, true, true): + return profileChangedString(displayName: displayName, previousDisplayName: previousDisplayName, + avatarURLString: nil, previousAvatarURLString: nil, + member: member, memberIsYou: memberIsYou, + sender: sender, senderIsYou: senderIsYou) + "\n" + ElementL10n.noticeAvatarChangedToo + case (false, false, true): + return ElementL10n.noticeMemberNoChangesByYou + } + } + + private func textForOtherState(_ content: OtherState, stateKey: String?, sender: TimelineItemSender, isOutgoing: Bool) -> String { + return "State change" + } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift index 7b3dc1fe95..15e2ea4e7d 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift @@ -20,6 +20,7 @@ import UIKit struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { private let mediaProvider: MediaProviderProtocol private let attributedStringBuilder: AttributedStringBuilderProtocol + private let roomStateTimelineItemFactory: RoomStateTimelineItemFactory /// The Matrix ID of the current user. private let userID: String @@ -30,6 +31,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { self.userID = userID self.mediaProvider = mediaProvider self.attributedStringBuilder = attributedStringBuilder + #warning("Add dependency injection") + self.roomStateTimelineItemFactory = RoomStateTimelineItemFactory(userID: userID) } // swiftlint:disable:next cyclomatic_complexity @@ -80,9 +83,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { return buildFallbackTimelineItem(eventItemProxy, sender, isOutgoing, groupState) } case .state(let stateKey, let content): - return RoomStateTimelineItemFactory.buildStateTimelineItemFor(eventItemProxy: eventItemProxy, isOutgoing: isOutgoing, avatarImage: avatarImage, stateKey: stateKey, content: content) - case .roomMembership(let change): - return RoomStateTimelineItemFactory.buildStateMembershipChangeTimelineItemFor(eventItemProxy: eventItemProxy, isOutgoing: isOutgoing, avatarImage: avatarImage, change: change) + return roomStateTimelineItemFactory.buildStateTimelineItemFor(eventItemProxy: eventItemProxy, content: content, stateKey: stateKey, sender: sender, isOutgoing: isOutgoing) + case .roomMembership(userId: let userID, change: let change): + return roomStateTimelineItemFactory.buildStateMembershipChangeTimelineItemFor(eventItemProxy: eventItemProxy, member: userID, change: change, sender: sender, isOutgoing: isOutgoing) } } From ecbe80c9958d96591150b71fa2cb849b1442bd39 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 19 Jan 2023 14:54:02 +0000 Subject: [PATCH 3/5] Add other state event types and use in RoomSummaryProvider. --- ElementX.xcodeproj/project.pbxproj | 4 + .../en.lproj/Untranslated.strings | 3 +- .../Generated/Strings+Untranslated.swift | 8 -- .../View/Timeline/StateRoomTimelineView.swift | 6 + .../Sources/Services/Client/ClientProxy.swift | 10 +- .../Services/Client/ClientProxyProtocol.swift | 2 +- .../Services/Client/MockClientProxy.swift | 4 +- .../EventTimelineItemSummaryFactory.swift | 84 +++++++++++ .../RoomSummary/RoomSummaryProvider.swift | 14 +- .../Services/Session/MockUserSession.swift | 2 +- .../Services/Session/UserSession.swift | 2 +- .../RoomTimelineController.swift | 7 +- .../RoomStateTimelineItemFactory.swift | 132 +++++++++++------- .../RoomTimelineItemFactory.swift | 44 +++++- .../RoomTimelineItemFactoryProtocol.swift | 2 +- .../UserSessionFlowCoordinator.swift | 5 +- .../UserSession/UserSessionStore.swift | 4 +- UnitTests/Sources/LoggingTests.swift | 4 +- 18 files changed, 241 insertions(+), 96 deletions(-) create mode 100644 ElementX/Sources/Services/Room/RoomSummary/EventTimelineItemSummaryFactory.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 155e6193f1..4e47f44cd6 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -286,6 +286,7 @@ 8F2FAA98457750D9D664136F /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 7731767AE437BA3BD2CC14A8 /* Sentry */; }; 90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */; }; 90EB25D13AE6EEF034BDE9D2 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; }; + 910D302D29795F110093B842 /* EventTimelineItemSummaryFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 910D302C29795F110093B842 /* EventTimelineItemSummaryFactory.swift */; }; 91DFCB641FBA03EE2DA0189E /* FilePreviewScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FB27E1BE894F9F9F0134372 /* FilePreviewScreen.swift */; }; 9219640F4D980CFC5FE855AD /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 536E72DCBEEC4A1FE66CFDCE /* target.yml */; }; 92B95779840CD749117B3615 /* EmojiMartStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38AE3617D7619EF30CDD229 /* EmojiMartStore.swift */; }; @@ -820,6 +821,7 @@ 9010EE0CC913D095887EF36E /* OIDCService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCService.swift; sourceTree = ""; }; 9080CDD3881D0D1B2F280A7C /* MockUserNotificationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserNotificationController.swift; sourceTree = ""; }; 90A55430639712CFACA34F43 /* TextRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItem.swift; sourceTree = ""; }; + 910D302C29795F110093B842 /* EventTimelineItemSummaryFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTimelineItemSummaryFactory.swift; sourceTree = ""; }; 91FB6F5ECCF51ECE98ACFEEC /* RoomDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsViewModel.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 = ""; }; @@ -1654,6 +1656,7 @@ 8F7D42E66E939B709C1EC390 /* MockRoomSummaryProvider.swift */, 142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */, CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */, + 910D302C29795F110093B842 /* EventTimelineItemSummaryFactory.swift */, 10CC626F97AD70FF0420C115 /* RoomSummaryProviderProtocol.swift */, ); path = RoomSummary; @@ -3191,6 +3194,7 @@ 44AE0752E001D1D10605CD88 /* Swipe.swift in Sources */, E290C78E7F09F47FD2662986 /* Task.swift in Sources */, 43FD77998F33C32718C51450 /* TemplateCoordinator.swift in Sources */, + 910D302D29795F110093B842 /* EventTimelineItemSummaryFactory.swift in Sources */, 63C9AF0FB8278AF1C0388A0C /* TemplateModels.swift in Sources */, 1555A7643D85187D4851040C /* TemplateScreen.swift in Sources */, 75EA4ABBFAA810AFF289D6F4 /* TemplateViewModel.swift in Sources */, diff --git a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings index b3c7311f98..40523ae51a 100644 --- a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings +++ b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings @@ -35,9 +35,8 @@ "noticeRoomKnockDenied" = "%1$@ rejected %2$@'s request to join"; "noticeRoomKnockDeniedByYou" = "You rejected %1$@'s request to join"; "noticeRoomKnockDeniedYou" = "%1$@ rejected your request to join"; -"noticeRoomUnknownChange" = "%1$@ made an unknown change in the room"; "noticeRoomUnknownMembershipChange" = "%1$@ made an unknown change to their membership"; -"noticeRoomMembershipError" = "Membership error for %1$@"; + "session_verification_banner_title" = "Help keep your messages secure"; "session_verification_banner_message" = "Looks like you’re using a new device. Verify its you."; diff --git a/ElementX/Sources/Generated/Strings+Untranslated.swift b/ElementX/Sources/Generated/Strings+Untranslated.swift index b44c9bad9f..0d0c604d22 100644 --- a/ElementX/Sources/Generated/Strings+Untranslated.swift +++ b/ElementX/Sources/Generated/Strings+Untranslated.swift @@ -62,14 +62,6 @@ extension ElementL10n { } /// You cancelled your request to join public static let noticeRoomKnockRetractedByYou = ElementL10n.tr("Untranslated", "noticeRoomKnockRetractedByYou") - /// Membership error for %1$@ - public static func noticeRoomMembershipError(_ p1: Any) -> String { - return ElementL10n.tr("Untranslated", "noticeRoomMembershipError", String(describing: p1)) - } - /// %1$@ made an unknown change in the room - public static func noticeRoomUnknownChange(_ p1: Any) -> String { - return ElementL10n.tr("Untranslated", "noticeRoomUnknownChange", String(describing: p1)) - } /// %1$@ made an unknown change to their membership public static func noticeRoomUnknownMembershipChange(_ p1: Any) -> String { return ElementL10n.tr("Untranslated", "noticeRoomUnknownMembershipChange", String(describing: p1)) diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/StateRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/StateRoomTimelineView.swift index ff1fe58585..d558bf4e47 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/StateRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/StateRoomTimelineView.swift @@ -21,6 +21,12 @@ struct StateRoomTimelineView: View { var body: some View { Text(timelineItem.text) + .font(.element.caption1Bold) + .multilineTextAlignment(.center) + .foregroundColor(.element.secondaryContent) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.horizontal, 16) + .padding(.vertical, 4) } } diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 6c2ce4807f..5ddd265336 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -93,7 +93,7 @@ class ClientProxy: ClientProxyProtocol { configureSlidingSync() } - var userIdentifier: String { + var userID: String { do { return try client.userId() } catch { @@ -291,7 +291,9 @@ class ClientProxy: ClientProxyProtocol { private func buildAndConfigureVisibleRoomsSlidingSyncView(slidingSync: SlidingSyncProtocol, visibleRoomsView: SlidingSyncView) { let visibleRoomsViewProxy = SlidingSyncViewProxy(slidingSync: slidingSync, slidingSyncView: visibleRoomsView) - visibleRoomsSummaryProvider = RoomSummaryProvider(slidingSyncViewProxy: visibleRoomsViewProxy) + visibleRoomsSummaryProvider = RoomSummaryProvider(slidingSyncViewProxy: visibleRoomsViewProxy, + eventSummaryFactory: EventTimelineItemSummaryFactory(userID: userID, + roomStateTimelineItemFactory: RoomStateTimelineItemFactory(userID: userID))) visibleRoomsSlidingSyncView = visibleRoomsView @@ -327,7 +329,9 @@ class ClientProxy: ClientProxyProtocol { let allRoomsViewProxy = SlidingSyncViewProxy(slidingSync: slidingSync, slidingSyncView: allRoomsView) - allRoomsSummaryProvider = RoomSummaryProvider(slidingSyncViewProxy: allRoomsViewProxy) + allRoomsSummaryProvider = RoomSummaryProvider(slidingSyncViewProxy: allRoomsViewProxy, + eventSummaryFactory: EventTimelineItemSummaryFactory(userID: userID, + roomStateTimelineItemFactory: RoomStateTimelineItemFactory(userID: userID))) allRoomsSlidingSyncView = allRoomsView diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index ec37f90c57..d9597c4c3d 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -61,7 +61,7 @@ enum PushFormat { protocol ClientProxyProtocol: AnyObject, MediaProxyProtocol { var callbacks: PassthroughSubject { get } - var userIdentifier: String { get } + var userID: String { get } var isSoftLogout: Bool { get } diff --git a/ElementX/Sources/Services/Client/MockClientProxy.swift b/ElementX/Sources/Services/Client/MockClientProxy.swift index cdac27380c..d7b30d06aa 100644 --- a/ElementX/Sources/Services/Client/MockClientProxy.swift +++ b/ElementX/Sources/Services/Client/MockClientProxy.swift @@ -21,7 +21,7 @@ import MatrixRustSDK class MockClientProxy: ClientProxyProtocol { let callbacks = PassthroughSubject() - let userIdentifier: String + let userID: String let isSoftLogout = false let deviceId: String? = nil let homeserver = "" @@ -32,7 +32,7 @@ class MockClientProxy: ClientProxyProtocol { var allRoomsSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider() internal init(userIdentifier: String, roomSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider()) { - self.userIdentifier = userIdentifier + self.userID = userIdentifier visibleRoomsSummaryProvider = roomSummaryProvider } diff --git a/ElementX/Sources/Services/Room/RoomSummary/EventTimelineItemSummaryFactory.swift b/ElementX/Sources/Services/Room/RoomSummary/EventTimelineItemSummaryFactory.swift new file mode 100644 index 0000000000..8821d0c410 --- /dev/null +++ b/ElementX/Sources/Services/Room/RoomSummary/EventTimelineItemSummaryFactory.swift @@ -0,0 +1,84 @@ +// +// Copyright 2023 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 + +struct EventTimelineItemSummaryFactory { + private let roomStateTimelineItemFactory: RoomStateTimelineItemFactory + + /// The Matrix ID of the current user. + private let userID: String + + init(userID: String, roomStateTimelineItemFactory: RoomStateTimelineItemFactory) { + self.userID = userID + self.roomStateTimelineItemFactory = roomStateTimelineItemFactory + } + + // swiftlint:disable:next cyclomatic_complexity + func buildAttributedString(for eventItemProxy: EventTimelineItemProxy) -> AttributedString? { + let sender = eventItemProxy.sender + let isOutgoing = eventItemProxy.isOwn + + switch eventItemProxy.content.kind() { + case .unableToDecrypt: + return prefix(ElementL10n.encryptionInformationDecryptionError, with: sender) + case .redactedMessage: + return prefix(ElementL10n.eventRedacted, with: sender) + case .sticker: + return prefix(ElementL10n.sendASticker, with: sender) + case .failedToParseMessageLike, .failedToParseState: + return prefix(ElementL10n.roomTimelineItemUnsupported, with: sender) + case .message: + guard let messageContent = eventItemProxy.content.asMessage() else { fatalError("Invalid message timeline item: \(eventItemProxy)") } + + let message: String + switch messageContent.msgtype() { + // Message types that don't need a prefix. + case .emote(content: let content): + let senderDisplayName = sender.displayName ?? sender.id + return AttributedString("* \(senderDisplayName) \(content.body)") + // Message types that should be prefixed with the sender's name. + case .image: + message = ElementL10n.sentAnImage + case .video: + message = ElementL10n.sentAVideo + case .file: + message = ElementL10n.sentAFile + default: + message = messageContent.body() + } + return prefix(message, with: sender) + case .state(let stateKey, let content): + return roomStateTimelineItemFactory + .textForOtherState(content, stateKey: stateKey, sender: sender, isOutgoing: isOutgoing) + .map(AttributedString.init) + case .roomMembership(userId: let userID, change: let change): + return roomStateTimelineItemFactory + .textForMembershipChange(change, member: userID, sender: sender, isOutgoing: isOutgoing) + .map(AttributedString.init) + } + } + + func prefix(_ eventSummary: String, with sender: TimelineItemSender) -> AttributedString { + if let senderDisplayName = sender.displayName, + let attributedSenderDisplayName = try? AttributedString(markdown: "**\(senderDisplayName)**") { + // Don't include the message body in the markdown otherwise it makes tappable links. + return attributedSenderDisplayName + ": " + AttributedString(eventSummary) + } else { + return AttributedString(eventSummary) + } + } +} diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift index f38b6cc38b..b61852498f 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift @@ -21,6 +21,7 @@ import MatrixRustSDK class RoomSummaryProvider: RoomSummaryProviderProtocol { private let slidingSyncViewProxy: SlidingSyncViewProxy private let serialDispatchQueue: DispatchQueue + private let eventSummaryFactory: EventTimelineItemSummaryFactory private var cancellables = Set() @@ -34,9 +35,10 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { } } - init(slidingSyncViewProxy: SlidingSyncViewProxy) { + init(slidingSyncViewProxy: SlidingSyncViewProxy, eventSummaryFactory: EventTimelineItemSummaryFactory) { self.slidingSyncViewProxy = slidingSyncViewProxy serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomsummaryprovider") + self.eventSummaryFactory = eventSummaryFactory rooms = slidingSyncViewProxy.currentRoomsList().map { roomListEntry in buildSummaryForRoomListEntry(roomListEntry) @@ -151,16 +153,8 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { DispatchQueue.global(qos: .default).sync { if let latestRoomMessage = room.latestRoomMessage() { let lastMessage = EventTimelineItemProxy(item: latestRoomMessage) - lastMessageTimestamp = lastMessage.timestamp - - if let senderDisplayName = lastMessage.sender.displayName, - let attributedSenderDisplayName = try? AttributedString(markdown: "**\(senderDisplayName)**") { - // Don't include the message body in the markdown otherwise it makes tappable links. - attributedLastMessage = attributedSenderDisplayName + ": " + AttributedString(lastMessage.body ?? "") - } else if let body = lastMessage.body { - attributedLastMessage = AttributedString(body) - } + attributedLastMessage = eventSummaryFactory.buildAttributedString(for: lastMessage) } } diff --git a/ElementX/Sources/Services/Session/MockUserSession.swift b/ElementX/Sources/Services/Session/MockUserSession.swift index 231a4a9659..c7eccdf1a9 100644 --- a/ElementX/Sources/Services/Session/MockUserSession.swift +++ b/ElementX/Sources/Services/Session/MockUserSession.swift @@ -19,7 +19,7 @@ import Combine struct MockUserSession: UserSessionProtocol { let callbacks = PassthroughSubject() let sessionVerificationController: SessionVerificationControllerProxyProtocol? = nil - var userID: String { clientProxy.userIdentifier } + var userID: String { clientProxy.userID } var isSoftLogout: Bool { clientProxy.isSoftLogout } var deviceId: String? { clientProxy.deviceId } var homeserver: String { clientProxy.homeserver } diff --git a/ElementX/Sources/Services/Session/UserSession.swift b/ElementX/Sources/Services/Session/UserSession.swift index 0332984082..f1fdd7d6d2 100644 --- a/ElementX/Sources/Services/Session/UserSession.swift +++ b/ElementX/Sources/Services/Session/UserSession.swift @@ -23,7 +23,7 @@ class UserSession: UserSessionProtocol { private var authErrorCancellable: AnyCancellable? private var restoreTokenUpdateCancellable: AnyCancellable? - var userID: String { clientProxy.userIdentifier } + var userID: String { clientProxy.userID } var isSoftLogout: Bool { clientProxy.isSoftLogout } var deviceId: String? { clientProxy.deviceId } var homeserver: String { clientProxy.homeserver } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift index ec9fd750fb..7a5dc1ba0b 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift @@ -259,9 +259,10 @@ class RoomTimelineController: RoomTimelineControllerProtocol { let groupState = computeGroupState(for: itemProxy, previousItemProxy: previousItemProxy, nextItemProxy: nextItemProxy) switch itemProxy { - case .event(let eventItem): - newTimelineItems.append(timelineItemFactory.buildTimelineItemFor(eventItemProxy: eventItem, - groupState: groupState)) + case .event(let eventItemProxy): + if let timelineItem = timelineItemFactory.buildTimelineItemFor(eventItemProxy: eventItemProxy, groupState: groupState) { + newTimelineItems.append(timelineItem) + } case .virtual(let virtualItem): switch virtualItem { case .dayDivider(let year, let month, let day): diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomStateTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomStateTimelineItemFactory.swift index 9c2eb4d7d8..dcba810831 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomStateTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomStateTimelineItemFactory.swift @@ -20,47 +20,13 @@ import UIKit struct RoomStateTimelineItemFactory { let userID: String - func buildStateTimelineItemFor(eventItemProxy: EventTimelineItemProxy, - content: OtherState, - stateKey: String, - sender: TimelineItemSender, - isOutgoing: Bool) -> RoomTimelineItemProtocol { - let text = textForOtherState(content, stateKey: stateKey, sender: sender, isOutgoing: isOutgoing) - return buildStateTimelineItem(eventItemProxy: eventItemProxy, text: text, sender: sender, isOutgoing: isOutgoing) - } - - func buildStateMembershipChangeTimelineItemFor(eventItemProxy: EventTimelineItemProxy, - member: String, - change: MembershipChange, - sender: TimelineItemSender, - isOutgoing: Bool) -> RoomTimelineItemProtocol { - let text = textForMembershipChange(change, member: member, sender: eventItemProxy.sender, isOutgoing: isOutgoing) - return buildStateTimelineItem(eventItemProxy: eventItemProxy, text: text, sender: sender, isOutgoing: isOutgoing) - } - - // MARK: - Private - - private func buildStateTimelineItem(eventItemProxy: EventTimelineItemProxy, text: String, sender: TimelineItemSender, isOutgoing: Bool) -> RoomTimelineItemProtocol { - StateRoomTimelineItem(id: eventItemProxy.id, - text: text, - timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened), - groupState: .single, - isOutgoing: isOutgoing, - isEditable: false, - sender: sender) - } - // swiftlint:disable:next cyclomatic_complexity function_body_length - private func textForMembershipChange(_ change: MembershipChange, member: String, sender: TimelineItemSender, isOutgoing: Bool) -> String { + func textForMembershipChange(_ change: MembershipChange, member: String, sender: TimelineItemSender, isOutgoing: Bool) -> String? { let senderName = sender.displayName ?? sender.id let senderIsYou = isOutgoing let memberIsYou = member == userID switch change { - case .none: - return senderIsYou ? ElementL10n.noticeMemberNoChangesByYou : ElementL10n.noticeMemberNoChanges(member) - case .error: - return ElementL10n.noticeRoomMembershipError(member) case .joined: return memberIsYou ? ElementL10n.noticeRoomJoinByYou : ElementL10n.noticeRoomJoin(member) case .left: @@ -104,13 +70,10 @@ struct RoomStateTimelineItemFactory { avatarURLString: avatarURLString, previousAvatarURLString: previousAvatarURLString, member: member, memberIsYou: memberIsYou, sender: sender, senderIsYou: senderIsYou) - case .notImplemented: - return ElementL10n.noticeRoomUnknownChange(sender) - case .unknown(_, let displayName, _): - return ElementL10n.noticeRoomUnknownMembershipChange(displayName ?? member) + case .none, .error, .notImplemented, .unknown: // Not useful information for the user. + MXLog.verbose("Filtering timeline item for membership change: \(change)") + return nil } - - return "" } // swiftlint:disable:next cyclomatic_complexity function_parameter_count @@ -130,17 +93,15 @@ struct RoomStateTimelineItemFactory { } else if let previousDisplayName { return ElementL10n.noticeDisplayNameRemoved(member, previousDisplayName) } else { - return ElementL10n.noticeMemberNoChanges(member) + return ElementL10n.noticeMemberNoChanges(member); #warning("Filter these") } case (false, true, false): - return ElementL10n.noticeAvatarUrlChanged(member) + return ElementL10n.noticeAvatarUrlChanged(displayName ?? member) case (true, true, false): return profileChangedString(displayName: displayName, previousDisplayName: previousDisplayName, avatarURLString: nil, previousAvatarURLString: nil, member: member, memberIsYou: memberIsYou, sender: sender, senderIsYou: senderIsYou) + "\n" + ElementL10n.noticeAvatarChangedToo - case (false, false, false): - return ElementL10n.noticeMemberNoChanges(member) case (true, false, true): if let displayName, let previousDisplayName { return ElementL10n.noticeDisplayNameChangedFromByYou(displayName, previousDisplayName) @@ -149,7 +110,7 @@ struct RoomStateTimelineItemFactory { } else if let previousDisplayName { return ElementL10n.noticeDisplayNameRemovedByYou(previousDisplayName) } else { - return ElementL10n.noticeMemberNoChangesByYou + return ElementL10n.noticeMemberNoChangesByYou; #warning("Filter these") } case (false, true, true): return ElementL10n.noticeAvatarUrlChangedByYou @@ -158,12 +119,83 @@ struct RoomStateTimelineItemFactory { avatarURLString: nil, previousAvatarURLString: nil, member: member, memberIsYou: memberIsYou, sender: sender, senderIsYou: senderIsYou) + "\n" + ElementL10n.noticeAvatarChangedToo - case (false, false, true): - return ElementL10n.noticeMemberNoChangesByYou + case (false, false, _): + return ElementL10n.noticeMemberNoChangesByYou; #warning("Filter these") } } - private func textForOtherState(_ content: OtherState, stateKey: String?, sender: TimelineItemSender, isOutgoing: Bool) -> String { - return "State change" + // swiftlint:disable:next cyclomatic_complexity function_body_length + func textForOtherState(_ content: OtherState, stateKey: String?, sender: TimelineItemSender, isOutgoing: Bool) -> String? { + let senderName = sender.displayName ?? sender.id + + switch content { + case .roomAvatar(let url): + switch (url, isOutgoing) { + case (.some, false): + return ElementL10n.noticeRoomAvatarChanged(senderName) + case (nil, false): + return ElementL10n.noticeRoomAvatarRemoved(senderName) + case (.some, true): + return ElementL10n.noticeRoomAvatarRemovedByYou + case (nil, true): + return ElementL10n.noticeRoomAvatarRemovedByYou + } + case .roomCreate: + return isOutgoing ? ElementL10n.noticeRoomCreatedByYou : ElementL10n.noticeRoomCreated(senderName) + case .roomEncryption: + return ElementL10n.encryptionEnabled + case .roomName(let name): + switch (name, isOutgoing) { + case (.some(let name), false): + return ElementL10n.noticeRoomNameChanged(senderName, name) + case (nil, false): + return ElementL10n.noticeRoomNameRemoved(senderName) + case (.some(let name), true): + return ElementL10n.noticeRoomNameChangedByYou(name) + case (nil, true): + return ElementL10n.noticeRoomNameRemovedByYou + } + case .roomThirdPartyInvite(let displayName): + guard let displayName else { + MXLog.error("roomThirdPartyInvite undisplayable due to missing name.") + return nil + } + + if isOutgoing { + return ElementL10n.noticeRoomThirdPartyInviteByYou(displayName) + } else { + return ElementL10n.noticeRoomThirdPartyInvite(senderName, displayName) + } + case .roomTopic(let topic): + switch (topic, isOutgoing) { + case (.some(let topic), false): + return ElementL10n.noticeRoomTopicChanged(senderName, topic) + case (nil, false): + return ElementL10n.noticeRoomTopicRemoved(senderName) + case (.some(let name), true): + return ElementL10n.noticeRoomTopicChangedByYou(name) + case (nil, true): + return ElementL10n.noticeRoomTopicRemovedByYou + } + case .policyRuleRoom, .policyRuleServer, .policyRuleUser: // No strings available. + break + case .roomAliases, .roomCanonicalAlias: // Doesn't provide the alias. + break + case .roomGuestAccess, .roomHistoryVisibility: // Doesn't provide information about the change. + break + case .roomJoinRules: // Doesn't provide information about the change. + break + case .roomPinnedEvents, .roomPowerLevels, .roomServerAcl: // Doesn't provide information about the change. + break + case .roomTombstone: // Handle as a virtual timeline item with a link to the upgraded room. + break + case .spaceChild, .spaceParent: // Users shouldn't see the timeline of a Space. + break + case .custom: // Won't provide actionable information to the user. + break + } + + MXLog.verbose("Filtering timeline item for state: \(content)") + return nil } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift index 15e2ea4e7d..2f0541f332 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift @@ -27,17 +27,17 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { init(userID: String, mediaProvider: MediaProviderProtocol, - attributedStringBuilder: AttributedStringBuilderProtocol) { + attributedStringBuilder: AttributedStringBuilderProtocol, + roomStateTimelineItemFactory: RoomStateTimelineItemFactory) { self.userID = userID self.mediaProvider = mediaProvider self.attributedStringBuilder = attributedStringBuilder - #warning("Add dependency injection") - self.roomStateTimelineItemFactory = RoomStateTimelineItemFactory(userID: userID) + self.roomStateTimelineItemFactory = roomStateTimelineItemFactory } // swiftlint:disable:next cyclomatic_complexity func buildTimelineItemFor(eventItemProxy: EventTimelineItemProxy, - groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol { + groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol? { var sender = eventItemProxy.sender if let senderAvatarURL = eventItemProxy.sender.avatarURL, let image = mediaProvider.imageFromURL(senderAvatarURL, avatarSize: .user(on: .timeline)) { @@ -83,13 +83,13 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { return buildFallbackTimelineItem(eventItemProxy, sender, isOutgoing, groupState) } case .state(let stateKey, let content): - return roomStateTimelineItemFactory.buildStateTimelineItemFor(eventItemProxy: eventItemProxy, content: content, stateKey: stateKey, sender: sender, isOutgoing: isOutgoing) + return buildStateTimelineItemFor(eventItemProxy: eventItemProxy, content: content, stateKey: stateKey, sender: sender, isOutgoing: isOutgoing) case .roomMembership(userId: let userID, change: let change): - return roomStateTimelineItemFactory.buildStateMembershipChangeTimelineItemFor(eventItemProxy: eventItemProxy, member: userID, change: change, sender: sender, isOutgoing: isOutgoing) + return buildStateMembershipChangeTimelineItemFor(eventItemProxy: eventItemProxy, member: userID, change: change, sender: sender, isOutgoing: isOutgoing) } } - // MARK: - Private + // MARK: - Message Events // swiftlint:disable:next function_parameter_count private func buildUnsupportedTimelineItem(_ eventItemProxy: EventTimelineItemProxy, @@ -362,4 +362,34 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { return AggregatedReaction(key: reaction.key, count: Int(reaction.count), isHighlighted: isHighlighted) } } + + // MARK: - State Events + + private func buildStateTimelineItemFor(eventItemProxy: EventTimelineItemProxy, + content: OtherState, + stateKey: String, + sender: TimelineItemSender, + isOutgoing: Bool) -> RoomTimelineItemProtocol? { + guard let text = roomStateTimelineItemFactory.textForOtherState(content, stateKey: stateKey, sender: sender, isOutgoing: isOutgoing) else { return nil } + return buildStateTimelineItem(eventItemProxy: eventItemProxy, text: text, sender: sender, isOutgoing: isOutgoing) + } + + private func buildStateMembershipChangeTimelineItemFor(eventItemProxy: EventTimelineItemProxy, + member: String, + change: MembershipChange, + sender: TimelineItemSender, + isOutgoing: Bool) -> RoomTimelineItemProtocol? { + guard let text = roomStateTimelineItemFactory.textForMembershipChange(change, member: member, sender: eventItemProxy.sender, isOutgoing: isOutgoing) else { return nil } + return buildStateTimelineItem(eventItemProxy: eventItemProxy, text: text, sender: sender, isOutgoing: isOutgoing) + } + + private func buildStateTimelineItem(eventItemProxy: EventTimelineItemProxy, text: String, sender: TimelineItemSender, isOutgoing: Bool) -> RoomTimelineItemProtocol { + StateRoomTimelineItem(id: eventItemProxy.id, + text: text, + timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened), + groupState: .single, + isOutgoing: isOutgoing, + isEditable: false, + sender: sender) + } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactoryProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactoryProtocol.swift index 81c73e4081..1aaac253df 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactoryProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactoryProtocol.swift @@ -19,5 +19,5 @@ import Foundation @MainActor protocol RoomTimelineItemFactoryProtocol { func buildTimelineItemFor(eventItemProxy: EventTimelineItemProxy, - groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol + groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol? } diff --git a/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift b/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift index c88f55dd5b..dc7344dddc 100644 --- a/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift @@ -149,11 +149,12 @@ class UserSessionFlowCoordinator: CoordinatorProtocol { MXLog.error("Invalid room identifier: \(roomIdentifier)") return } - let userId = userSession.clientProxy.userIdentifier + let userId = userSession.clientProxy.userID let timelineItemFactory = RoomTimelineItemFactory(userID: userId, mediaProvider: userSession.mediaProvider, - attributedStringBuilder: AttributedStringBuilder()) + attributedStringBuilder: AttributedStringBuilder(), + roomStateTimelineItemFactory: RoomStateTimelineItemFactory(userID: userId)) let timelineController = roomTimelineControllerFactory.buildRoomTimelineController(userId: userId, roomProxy: roomProxy, diff --git a/ElementX/Sources/Services/UserSession/UserSessionStore.swift b/ElementX/Sources/Services/UserSession/UserSessionStore.swift index ccc01e725e..73459d7635 100644 --- a/ElementX/Sources/Services/UserSession/UserSessionStore.swift +++ b/ElementX/Sources/Services/UserSession/UserSessionStore.swift @@ -78,13 +78,13 @@ class UserSessionStore: UserSessionStoreProtocol { return .failure(.failedRefreshingRestoreToken) } - keychainController.setRestorationToken(restorationToken, forUsername: userSession.clientProxy.userIdentifier) + keychainController.setRestorationToken(restorationToken, forUsername: userSession.clientProxy.userID) return .success(()) } func logout(userSession: UserSessionProtocol) { - let userID = userSession.clientProxy.userIdentifier + let userID = userSession.clientProxy.userID keychainController.removeRestorationTokenForUsername(userID) deleteSessionDirectory(for: userID) } diff --git a/UnitTests/Sources/LoggingTests.swift b/UnitTests/Sources/LoggingTests.swift index a5afb9efb6..c4acc43f4d 100644 --- a/UnitTests/Sources/LoggingTests.swift +++ b/UnitTests/Sources/LoggingTests.swift @@ -206,7 +206,7 @@ class LoggingTests: XCTestCase { XCTAssertFalse(content.contains(lastMessage)) } - // swiftlint:disable function_body_length + // swiftlint:disable:next function_body_length func testTimelineContentIsRedacted() throws { // Given timeline items that contain text let textAttributedString = "TextAttributed" @@ -275,8 +275,6 @@ class LoggingTests: XCTestCase { XCTAssertTrue(content.contains(fileMessage.id)) XCTAssertFalse(content.contains(fileMessage.text)) } - - // swiftlint:enable function_body_length func testRustMessageContentIsRedacted() throws { // Given message content that contain text From d3f5bc120503b1e6f85297f69fc2ac8945bc67ad Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 19 Jan 2023 14:57:56 +0000 Subject: [PATCH 4/5] Tidy up --- ElementX.xcodeproj/project.pbxproj | 16 ++++++++-------- .../Screens/HomeScreen/View/HomeScreen.swift | 2 +- .../HomeScreen/View/HomeScreenRoomCell.swift | 2 +- .../Settings/View/SettingsScreen.swift | 2 +- .../MockAuthenticationServiceProxy.swift | 2 +- .../Sources/Services/Client/ClientProxy.swift | 8 ++++---- .../Services/Client/MockClientProxy.swift | 4 ++-- ...ory.swift => RoomEventStringBuilder.swift} | 18 +++++++++--------- .../RoomSummary/RoomSummaryProvider.swift | 8 ++++---- ...ory.swift => RoomStateStringBuilder.swift} | 19 +++++++++++-------- .../RoomTimelineItemFactory.swift | 18 +++++++++--------- .../UserSessionFlowCoordinator.swift | 2 +- .../UITests/UITestsAppCoordinator.swift | 8 ++++---- .../Sources/HomeScreenViewModelTests.swift | 2 +- .../NotificationManagerTests.swift | 2 +- .../Sources/SettingsViewModelTests.swift | 2 +- .../UserSession/UserSessionTests.swift | 2 +- changelog.d/pr-473.feature | 1 + 18 files changed, 61 insertions(+), 57 deletions(-) rename ElementX/Sources/Services/Room/RoomSummary/{EventTimelineItemSummaryFactory.swift => RoomEventStringBuilder.swift} (83%) rename ElementX/Sources/Services/Timeline/TimelineItems/{RoomStateTimelineItemFactory.swift => RoomStateStringBuilder.swift} (93%) create mode 100644 changelog.d/pr-473.feature diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 4e47f44cd6..c20bcda3eb 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -60,7 +60,7 @@ 187E18F21EF4DA244E436E58 /* BugReportViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28959C7DB36C7688A01D4045 /* BugReportViewModelProtocol.swift */; }; 18E674DB2977DBD60055EA9F /* StateRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E674DA2977DBD60055EA9F /* StateRoomTimelineItem.swift */; }; 18E674DD2977DC2B0055EA9F /* StateRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E674DC2977DC2B0055EA9F /* StateRoomTimelineView.swift */; }; - 18E674DF2977DD9B0055EA9F /* RoomStateTimelineItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E674DE2977DD9B0055EA9F /* RoomStateTimelineItemFactory.swift */; }; + 18E674DF2977DD9B0055EA9F /* RoomStateStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E674DE2977DD9B0055EA9F /* RoomStateStringBuilder.swift */; }; 191161FE9E0DA89704301F37 /* Untranslated.strings in Resources */ = {isa = PBXBuildFile; fileRef = D2F7194F440375338F8E2487 /* Untranslated.strings */; }; 1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */; }; 19839F3526CE8C35AAF241AD /* ServerSelectionViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F52BF30D12BA3BD3D3DBB8F /* ServerSelectionViewModelProtocol.swift */; }; @@ -286,7 +286,7 @@ 8F2FAA98457750D9D664136F /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 7731767AE437BA3BD2CC14A8 /* Sentry */; }; 90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */; }; 90EB25D13AE6EEF034BDE9D2 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; }; - 910D302D29795F110093B842 /* EventTimelineItemSummaryFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 910D302C29795F110093B842 /* EventTimelineItemSummaryFactory.swift */; }; + 910D302D29795F110093B842 /* RoomEventStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 910D302C29795F110093B842 /* RoomEventStringBuilder.swift */; }; 91DFCB641FBA03EE2DA0189E /* FilePreviewScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FB27E1BE894F9F9F0134372 /* FilePreviewScreen.swift */; }; 9219640F4D980CFC5FE855AD /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 536E72DCBEEC4A1FE66CFDCE /* target.yml */; }; 92B95779840CD749117B3615 /* EmojiMartStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38AE3617D7619EF30CDD229 /* EmojiMartStore.swift */; }; @@ -586,7 +586,7 @@ 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecorationTimelineItemProtocol.swift; sourceTree = ""; }; 18E674DA2977DBD60055EA9F /* StateRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRoomTimelineItem.swift; sourceTree = ""; }; 18E674DC2977DC2B0055EA9F /* StateRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRoomTimelineView.swift; sourceTree = ""; }; - 18E674DE2977DD9B0055EA9F /* RoomStateTimelineItemFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateTimelineItemFactory.swift; sourceTree = ""; }; + 18E674DE2977DD9B0055EA9F /* RoomStateStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateStringBuilder.swift; sourceTree = ""; }; 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxy.swift; sourceTree = ""; }; 18FE0CDF1FFA92EA7EE17B0B /* RoomTimelineControllerFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineControllerFactoryProtocol.swift; sourceTree = ""; }; 1941C8817E6B6971BA4415F5 /* VideoRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineView.swift; sourceTree = ""; }; @@ -821,7 +821,7 @@ 9010EE0CC913D095887EF36E /* OIDCService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCService.swift; sourceTree = ""; }; 9080CDD3881D0D1B2F280A7C /* MockUserNotificationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserNotificationController.swift; sourceTree = ""; }; 90A55430639712CFACA34F43 /* TextRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItem.swift; sourceTree = ""; }; - 910D302C29795F110093B842 /* EventTimelineItemSummaryFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTimelineItemSummaryFactory.swift; sourceTree = ""; }; + 910D302C29795F110093B842 /* RoomEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomEventStringBuilder.swift; sourceTree = ""; }; 91FB6F5ECCF51ECE98ACFEEC /* RoomDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsViewModel.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 = ""; }; @@ -1656,7 +1656,7 @@ 8F7D42E66E939B709C1EC390 /* MockRoomSummaryProvider.swift */, 142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */, CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */, - 910D302C29795F110093B842 /* EventTimelineItemSummaryFactory.swift */, + 910D302C29795F110093B842 /* RoomEventStringBuilder.swift */, 10CC626F97AD70FF0420C115 /* RoomSummaryProviderProtocol.swift */, ); path = RoomSummary; @@ -1992,7 +1992,7 @@ EEE384418EB1FEDFA62C9CD0 /* RoomTimelineViewFactoryProtocol.swift */, ACB6C5E4950B6C9842F35A38 /* RoomTimelineViewProvider.swift */, 75D1D02F7F3AC1122FCFB4F3 /* Items */, - 18E674DE2977DD9B0055EA9F /* RoomStateTimelineItemFactory.swift */, + 18E674DE2977DD9B0055EA9F /* RoomStateStringBuilder.swift */, ); path = TimelineItems; sourceTree = ""; @@ -2977,7 +2977,7 @@ 64FF5CB4E35971255872E1BB /* AuthenticationServiceProxyProtocol.swift in Sources */, D876EC0FED3B6D46C806912A /* AvatarSize.swift in Sources */, E0A4DCA633D174EB43AD599F /* BackgroundTaskProtocol.swift in Sources */, - 18E674DF2977DD9B0055EA9F /* RoomStateTimelineItemFactory.swift in Sources */, + 18E674DF2977DD9B0055EA9F /* RoomStateStringBuilder.swift in Sources */, 6D046D653DA28ADF1E6E59A4 /* BackgroundTaskServiceProtocol.swift in Sources */, 38546A6010A2CF240EC9AF73 /* BindableState.swift in Sources */, B6DF6B6FA8734B70F9BF261E /* BlurHashDecode.swift in Sources */, @@ -3194,7 +3194,7 @@ 44AE0752E001D1D10605CD88 /* Swipe.swift in Sources */, E290C78E7F09F47FD2662986 /* Task.swift in Sources */, 43FD77998F33C32718C51450 /* TemplateCoordinator.swift in Sources */, - 910D302D29795F110093B842 /* EventTimelineItemSummaryFactory.swift in Sources */, + 910D302D29795F110093B842 /* RoomEventStringBuilder.swift in Sources */, 63C9AF0FB8278AF1C0388A0C /* TemplateModels.swift in Sources */, 1555A7643D85187D4851040C /* TemplateScreen.swift in Sources */, 75EA4ABBFAA810AFF289D6F4 /* TemplateViewModel.swift in Sources */, diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift index 0992f81c33..823063ca86 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift @@ -213,7 +213,7 @@ struct HomeScreen_Previews: PreviewProvider { } static func body(_ state: MockRoomSummaryProviderState) -> some View { - let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: "John Doe", + let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "John Doe", roomSummaryProvider: MockRoomSummaryProvider(state: state)), mediaProvider: MockMediaProvider()) diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift index ebf1e39c3e..aa3293a28a 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift @@ -121,7 +121,7 @@ struct HomeScreenRoomCell_Previews: PreviewProvider { static var body: some View { let summaryProvider = MockRoomSummaryProvider(state: .loaded) - let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: "John Doe", roomSummaryProvider: summaryProvider), + let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "John Doe", roomSummaryProvider: summaryProvider), mediaProvider: MockMediaProvider()) let viewModel = HomeScreenViewModel(userSession: userSession, diff --git a/ElementX/Sources/Screens/Settings/View/SettingsScreen.swift b/ElementX/Sources/Screens/Settings/View/SettingsScreen.swift index 341b4aceb0..16bf765714 100644 --- a/ElementX/Sources/Screens/Settings/View/SettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/View/SettingsScreen.swift @@ -200,7 +200,7 @@ extension TimelineStyle: CustomStringConvertible { struct Settings_Previews: PreviewProvider { static var previews: some View { - let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: "@userid:example.com"), + let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@userid:example.com"), mediaProvider: MockMediaProvider()) let viewModel = SettingsViewModel(withUserSession: userSession) diff --git a/ElementX/Sources/Services/Authentication/MockAuthenticationServiceProxy.swift b/ElementX/Sources/Services/Authentication/MockAuthenticationServiceProxy.swift index b19d0f1d05..bffb55be6e 100644 --- a/ElementX/Sources/Services/Authentication/MockAuthenticationServiceProxy.swift +++ b/ElementX/Sources/Services/Authentication/MockAuthenticationServiceProxy.swift @@ -52,7 +52,7 @@ class MockAuthenticationServiceProxy: AuthenticationServiceProxyProtocol { return .failure(.invalidCredentials) } - let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: username), + let userSession = MockUserSession(clientProxy: MockClientProxy(userID: username), mediaProvider: MockMediaProvider()) return .success(userSession) } diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 5ddd265336..8baee58beb 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -292,8 +292,8 @@ class ClientProxy: ClientProxyProtocol { let visibleRoomsViewProxy = SlidingSyncViewProxy(slidingSync: slidingSync, slidingSyncView: visibleRoomsView) visibleRoomsSummaryProvider = RoomSummaryProvider(slidingSyncViewProxy: visibleRoomsViewProxy, - eventSummaryFactory: EventTimelineItemSummaryFactory(userID: userID, - roomStateTimelineItemFactory: RoomStateTimelineItemFactory(userID: userID))) + eventStringBuilder: RoomEventStringBuilder(userID: userID, + roomStateStringBuilder: RoomStateStringBuilder(userID: userID))) visibleRoomsSlidingSyncView = visibleRoomsView @@ -330,8 +330,8 @@ class ClientProxy: ClientProxyProtocol { let allRoomsViewProxy = SlidingSyncViewProxy(slidingSync: slidingSync, slidingSyncView: allRoomsView) allRoomsSummaryProvider = RoomSummaryProvider(slidingSyncViewProxy: allRoomsViewProxy, - eventSummaryFactory: EventTimelineItemSummaryFactory(userID: userID, - roomStateTimelineItemFactory: RoomStateTimelineItemFactory(userID: userID))) + eventStringBuilder: RoomEventStringBuilder(userID: userID, + roomStateStringBuilder: RoomStateStringBuilder(userID: userID))) allRoomsSlidingSyncView = allRoomsView diff --git a/ElementX/Sources/Services/Client/MockClientProxy.swift b/ElementX/Sources/Services/Client/MockClientProxy.swift index d7b30d06aa..d672ed449b 100644 --- a/ElementX/Sources/Services/Client/MockClientProxy.swift +++ b/ElementX/Sources/Services/Client/MockClientProxy.swift @@ -31,8 +31,8 @@ class MockClientProxy: ClientProxyProtocol { var allRoomsSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider() - internal init(userIdentifier: String, roomSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider()) { - self.userID = userIdentifier + internal init(userID: String, roomSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider()) { + self.userID = userID visibleRoomsSummaryProvider = roomSummaryProvider } diff --git a/ElementX/Sources/Services/Room/RoomSummary/EventTimelineItemSummaryFactory.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift similarity index 83% rename from ElementX/Sources/Services/Room/RoomSummary/EventTimelineItemSummaryFactory.swift rename to ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift index 8821d0c410..193bc2394c 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/EventTimelineItemSummaryFactory.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift @@ -16,15 +16,15 @@ import Foundation -struct EventTimelineItemSummaryFactory { - private let roomStateTimelineItemFactory: RoomStateTimelineItemFactory +struct RoomEventStringBuilder { + private let roomStateStringBuilder: RoomStateStringBuilder /// The Matrix ID of the current user. private let userID: String - init(userID: String, roomStateTimelineItemFactory: RoomStateTimelineItemFactory) { + init(userID: String, roomStateStringBuilder: RoomStateStringBuilder) { self.userID = userID - self.roomStateTimelineItemFactory = roomStateTimelineItemFactory + self.roomStateStringBuilder = roomStateStringBuilder } // swiftlint:disable:next cyclomatic_complexity @@ -61,13 +61,13 @@ struct EventTimelineItemSummaryFactory { message = messageContent.body() } return prefix(message, with: sender) - case .state(let stateKey, let content): - return roomStateTimelineItemFactory - .textForOtherState(content, stateKey: stateKey, sender: sender, isOutgoing: isOutgoing) + case .state(let stateKey, let state): + return roomStateStringBuilder + .buildString(for: state, stateKey: stateKey, sender: sender, isOutgoing: isOutgoing) .map(AttributedString.init) case .roomMembership(userId: let userID, change: let change): - return roomStateTimelineItemFactory - .textForMembershipChange(change, member: userID, sender: sender, isOutgoing: isOutgoing) + return roomStateStringBuilder + .buildString(for: change, member: userID, sender: sender, isOutgoing: isOutgoing) .map(AttributedString.init) } } diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift index b61852498f..332f3fba22 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift @@ -21,7 +21,7 @@ import MatrixRustSDK class RoomSummaryProvider: RoomSummaryProviderProtocol { private let slidingSyncViewProxy: SlidingSyncViewProxy private let serialDispatchQueue: DispatchQueue - private let eventSummaryFactory: EventTimelineItemSummaryFactory + private let eventStringBuilder: RoomEventStringBuilder private var cancellables = Set() @@ -35,10 +35,10 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { } } - init(slidingSyncViewProxy: SlidingSyncViewProxy, eventSummaryFactory: EventTimelineItemSummaryFactory) { + init(slidingSyncViewProxy: SlidingSyncViewProxy, eventStringBuilder: RoomEventStringBuilder) { self.slidingSyncViewProxy = slidingSyncViewProxy serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomsummaryprovider") - self.eventSummaryFactory = eventSummaryFactory + self.eventStringBuilder = eventStringBuilder rooms = slidingSyncViewProxy.currentRoomsList().map { roomListEntry in buildSummaryForRoomListEntry(roomListEntry) @@ -154,7 +154,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { if let latestRoomMessage = room.latestRoomMessage() { let lastMessage = EventTimelineItemProxy(item: latestRoomMessage) lastMessageTimestamp = lastMessage.timestamp - attributedLastMessage = eventSummaryFactory.buildAttributedString(for: lastMessage) + attributedLastMessage = eventStringBuilder.buildAttributedString(for: lastMessage) } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomStateTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomStateStringBuilder.swift similarity index 93% rename from ElementX/Sources/Services/Timeline/TimelineItems/RoomStateTimelineItemFactory.swift rename to ElementX/Sources/Services/Timeline/TimelineItems/RoomStateStringBuilder.swift index dcba810831..662aa919f4 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomStateTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomStateStringBuilder.swift @@ -17,11 +17,11 @@ import MatrixRustSDK import UIKit -struct RoomStateTimelineItemFactory { +struct RoomStateStringBuilder { let userID: String // swiftlint:disable:next cyclomatic_complexity function_body_length - func textForMembershipChange(_ change: MembershipChange, member: String, sender: TimelineItemSender, isOutgoing: Bool) -> String? { + func buildString(for change: MembershipChange, member: String, sender: TimelineItemSender, isOutgoing: Bool) -> String? { let senderName = sender.displayName ?? sender.id let senderIsYou = isOutgoing let memberIsYou = member == userID @@ -93,7 +93,8 @@ struct RoomStateTimelineItemFactory { } else if let previousDisplayName { return ElementL10n.noticeDisplayNameRemoved(member, previousDisplayName) } else { - return ElementL10n.noticeMemberNoChanges(member); #warning("Filter these") + MXLog.error("The display name changed from nil to nil, shouldn't be possible.") + return ElementL10n.noticeMemberNoChanges(member) } case (false, true, false): return ElementL10n.noticeAvatarUrlChanged(displayName ?? member) @@ -110,7 +111,8 @@ struct RoomStateTimelineItemFactory { } else if let previousDisplayName { return ElementL10n.noticeDisplayNameRemovedByYou(previousDisplayName) } else { - return ElementL10n.noticeMemberNoChangesByYou; #warning("Filter these") + MXLog.error("The display name changed from nil to nil, shouldn't be possible.") + return ElementL10n.noticeMemberNoChangesByYou } case (false, true, true): return ElementL10n.noticeAvatarUrlChangedByYou @@ -120,15 +122,16 @@ struct RoomStateTimelineItemFactory { member: member, memberIsYou: memberIsYou, sender: sender, senderIsYou: senderIsYou) + "\n" + ElementL10n.noticeAvatarChangedToo case (false, false, _): - return ElementL10n.noticeMemberNoChangesByYou; #warning("Filter these") + MXLog.error("Nothing changed, shouldn't be possible.") + return ElementL10n.noticeMemberNoChangesByYou } } // swiftlint:disable:next cyclomatic_complexity function_body_length - func textForOtherState(_ content: OtherState, stateKey: String?, sender: TimelineItemSender, isOutgoing: Bool) -> String? { + func buildString(for state: OtherState, stateKey: String?, sender: TimelineItemSender, isOutgoing: Bool) -> String? { let senderName = sender.displayName ?? sender.id - switch content { + switch state { case .roomAvatar(let url): switch (url, isOutgoing) { case (.some, false): @@ -195,7 +198,7 @@ struct RoomStateTimelineItemFactory { break } - MXLog.verbose("Filtering timeline item for state: \(content)") + MXLog.verbose("Filtering timeline item for state: \(state)") return nil } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift index 2f0541f332..016cf3ba64 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift @@ -20,7 +20,7 @@ import UIKit struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { private let mediaProvider: MediaProviderProtocol private let attributedStringBuilder: AttributedStringBuilderProtocol - private let roomStateTimelineItemFactory: RoomStateTimelineItemFactory + private let roomStateStringBuilder: RoomStateStringBuilder /// The Matrix ID of the current user. private let userID: String @@ -28,11 +28,11 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { init(userID: String, mediaProvider: MediaProviderProtocol, attributedStringBuilder: AttributedStringBuilderProtocol, - roomStateTimelineItemFactory: RoomStateTimelineItemFactory) { + roomStateStringBuilder: RoomStateStringBuilder) { self.userID = userID self.mediaProvider = mediaProvider self.attributedStringBuilder = attributedStringBuilder - self.roomStateTimelineItemFactory = roomStateTimelineItemFactory + self.roomStateStringBuilder = roomStateStringBuilder } // swiftlint:disable:next cyclomatic_complexity @@ -83,9 +83,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { return buildFallbackTimelineItem(eventItemProxy, sender, isOutgoing, groupState) } case .state(let stateKey, let content): - return buildStateTimelineItemFor(eventItemProxy: eventItemProxy, content: content, stateKey: stateKey, sender: sender, isOutgoing: isOutgoing) + return buildStateTimelineItemFor(eventItemProxy: eventItemProxy, state: content, stateKey: stateKey, sender: sender, isOutgoing: isOutgoing) case .roomMembership(userId: let userID, change: let change): - return buildStateMembershipChangeTimelineItemFor(eventItemProxy: eventItemProxy, member: userID, change: change, sender: sender, isOutgoing: isOutgoing) + return buildStateMembershipChangeTimelineItemFor(eventItemProxy: eventItemProxy, member: userID, membershipChange: change, sender: sender, isOutgoing: isOutgoing) } } @@ -366,20 +366,20 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { // MARK: - State Events private func buildStateTimelineItemFor(eventItemProxy: EventTimelineItemProxy, - content: OtherState, + state: OtherState, stateKey: String, sender: TimelineItemSender, isOutgoing: Bool) -> RoomTimelineItemProtocol? { - guard let text = roomStateTimelineItemFactory.textForOtherState(content, stateKey: stateKey, sender: sender, isOutgoing: isOutgoing) else { return nil } + guard let text = roomStateStringBuilder.buildString(for: state, stateKey: stateKey, sender: sender, isOutgoing: isOutgoing) else { return nil } return buildStateTimelineItem(eventItemProxy: eventItemProxy, text: text, sender: sender, isOutgoing: isOutgoing) } private func buildStateMembershipChangeTimelineItemFor(eventItemProxy: EventTimelineItemProxy, member: String, - change: MembershipChange, + membershipChange: MembershipChange, sender: TimelineItemSender, isOutgoing: Bool) -> RoomTimelineItemProtocol? { - guard let text = roomStateTimelineItemFactory.textForMembershipChange(change, member: member, sender: eventItemProxy.sender, isOutgoing: isOutgoing) else { return nil } + guard let text = roomStateStringBuilder.buildString(for: membershipChange, member: member, sender: eventItemProxy.sender, isOutgoing: isOutgoing) else { return nil } return buildStateTimelineItem(eventItemProxy: eventItemProxy, text: text, sender: sender, isOutgoing: isOutgoing) } diff --git a/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift b/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift index dc7344dddc..98696110b4 100644 --- a/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift @@ -154,7 +154,7 @@ class UserSessionFlowCoordinator: CoordinatorProtocol { let timelineItemFactory = RoomTimelineItemFactory(userID: userId, mediaProvider: userSession.mediaProvider, attributedStringBuilder: AttributedStringBuilder(), - roomStateTimelineItemFactory: RoomStateTimelineItemFactory(userID: userId)) + roomStateStringBuilder: RoomStateStringBuilder(userID: userId)) let timelineController = roomTimelineControllerFactory.buildRoomTimelineController(userId: userId, roomProxy: roomProxy, diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index e8180828f5..50a9fa0008 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -85,7 +85,7 @@ class MockScreen: Identifiable { userNotificationController: MockUserNotificationController(), isModallyPresented: false)) case .analyticsPrompt: - return AnalyticsPromptCoordinator(parameters: .init(userSession: MockUserSession(clientProxy: MockClientProxy(userIdentifier: "@mock:client.com"), + return AnalyticsPromptCoordinator(parameters: .init(userSession: MockUserSession(clientProxy: MockClientProxy(userID: "@mock:client.com"), mediaProvider: MockMediaProvider()))) case .authenticationFlow: let navigationStackCoordinator = NavigationStackCoordinator() @@ -108,7 +108,7 @@ class MockScreen: Identifiable { return TemplateCoordinator(parameters: .init(promptType: .upgrade)) case .home: let navigationStackCoordinator = NavigationStackCoordinator() - let session = MockUserSession(clientProxy: MockClientProxy(userIdentifier: "@mock:matrix.org"), + let session = MockUserSession(clientProxy: MockClientProxy(userID: "@mock:matrix.org"), mediaProvider: MockMediaProvider()) let coordinator = HomeScreenCoordinator(parameters: .init(userSession: session, attributedStringBuilder: AttributedStringBuilder(), @@ -120,7 +120,7 @@ class MockScreen: Identifiable { let navigationStackCoordinator = NavigationStackCoordinator() let coordinator = SettingsCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator, userNotificationController: MockUserNotificationController(), - userSession: MockUserSession(clientProxy: MockClientProxy(userIdentifier: "@mock:client.com"), + userSession: MockUserSession(clientProxy: MockClientProxy(userID: "@mock:client.com"), mediaProvider: MockMediaProvider()), bugReportService: MockBugReportService())) navigationStackCoordinator.setRootCoordinator(coordinator) @@ -260,7 +260,7 @@ class MockScreen: Identifiable { case .userSessionScreen: let navigationSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: SplashScreenCoordinator()) - let clientProxy = MockClientProxy(userIdentifier: "@mock:client.com", roomSummaryProvider: MockRoomSummaryProvider(state: .loaded)) + let clientProxy = MockClientProxy(userID: "@mock:client.com", roomSummaryProvider: MockRoomSummaryProvider(state: .loaded)) let coordinator = UserSessionFlowCoordinator(userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider()), navigationSplitCoordinator: navigationSplitCoordinator, diff --git a/UnitTests/Sources/HomeScreenViewModelTests.swift b/UnitTests/Sources/HomeScreenViewModelTests.swift index 3304aeeae5..5dc466f39a 100644 --- a/UnitTests/Sources/HomeScreenViewModelTests.swift +++ b/UnitTests/Sources/HomeScreenViewModelTests.swift @@ -23,7 +23,7 @@ class HomeScreenViewModelTests: XCTestCase { var context: HomeScreenViewModelType.Context! @MainActor override func setUpWithError() throws { - viewModel = HomeScreenViewModel(userSession: MockUserSession(clientProxy: MockClientProxy(userIdentifier: "@mock:client.com"), + viewModel = HomeScreenViewModel(userSession: MockUserSession(clientProxy: MockClientProxy(userID: "@mock:client.com"), mediaProvider: MockMediaProvider()), attributedStringBuilder: AttributedStringBuilder()) context = viewModel.context diff --git a/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift b/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift index 82f6f84ff2..65ace8360c 100644 --- a/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift +++ b/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift @@ -21,7 +21,7 @@ import Combine final class NotificationManagerTests: XCTestCase { var notificationManager: NotificationManager! - private let clientProxy = MockClientProxy(userIdentifier: "@test:user.net") + private let clientProxy = MockClientProxy(userID: "@test:user.net") private let notificationCenter = UserNotificationCenterSpy() private var authorizationStatusWasGranted = false private var shouldDisplayInAppNotificationReturnValue = false diff --git a/UnitTests/Sources/SettingsViewModelTests.swift b/UnitTests/Sources/SettingsViewModelTests.swift index 6460e5f813..b9f070d3a0 100644 --- a/UnitTests/Sources/SettingsViewModelTests.swift +++ b/UnitTests/Sources/SettingsViewModelTests.swift @@ -24,7 +24,7 @@ class SettingsViewModelTests: XCTestCase { var context: SettingsViewModelType.Context! @MainActor override func setUpWithError() throws { - let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: ""), + let userSession = MockUserSession(clientProxy: MockClientProxy(userID: ""), mediaProvider: MockMediaProvider()) viewModel = SettingsViewModel(withUserSession: userSession) context = viewModel.context diff --git a/UnitTests/Sources/UserSession/UserSessionTests.swift b/UnitTests/Sources/UserSession/UserSessionTests.swift index 49732c34fa..ab65529501 100644 --- a/UnitTests/Sources/UserSession/UserSessionTests.swift +++ b/UnitTests/Sources/UserSession/UserSessionTests.swift @@ -19,7 +19,7 @@ import XCTest final class UserSessionTests: XCTestCase { var userSession: UserSession! - let clientProxy = MockClientProxy(userIdentifier: "@test:user.net") + let clientProxy = MockClientProxy(userID: "@test:user.net") private var cancellables: Set = [] diff --git a/changelog.d/pr-473.feature b/changelog.d/pr-473.feature new file mode 100644 index 0000000000..33b2d88bfd --- /dev/null +++ b/changelog.d/pr-473.feature @@ -0,0 +1 @@ +Show state events in the timeline and (at least temporarily) on the home screen. \ No newline at end of file From 80bb331ef4dae1cc2a1e5b8a972d676ab4337cf6 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 19 Jan 2023 17:13:02 +0000 Subject: [PATCH 5/5] SonarCloud --- .../Timeline/TimelineItems/RoomStateStringBuilder.swift | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomStateStringBuilder.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomStateStringBuilder.swift index 662aa919f4..7e40c36ea7 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomStateStringBuilder.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomStateStringBuilder.swift @@ -98,11 +98,6 @@ struct RoomStateStringBuilder { } case (false, true, false): return ElementL10n.noticeAvatarUrlChanged(displayName ?? member) - case (true, true, false): - return profileChangedString(displayName: displayName, previousDisplayName: previousDisplayName, - avatarURLString: nil, previousAvatarURLString: nil, - member: member, memberIsYou: memberIsYou, - sender: sender, senderIsYou: senderIsYou) + "\n" + ElementL10n.noticeAvatarChangedToo case (true, false, true): if let displayName, let previousDisplayName { return ElementL10n.noticeDisplayNameChangedFromByYou(displayName, previousDisplayName) @@ -116,7 +111,8 @@ struct RoomStateStringBuilder { } case (false, true, true): return ElementL10n.noticeAvatarUrlChangedByYou - case (true, true, true): + case (true, true, _): + // When both have changed, get the string for the display name and tack on that the avatar changed too. return profileChangedString(displayName: displayName, previousDisplayName: previousDisplayName, avatarURLString: nil, previousAvatarURLString: nil, member: member, memberIsYou: memberIsYou,