From 063b3732b18aea3d436b4e4033c0c42dbd800b38 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Sat, 2 Mar 2024 19:12:02 +0200 Subject: [PATCH] Fixes #1837 - Add support for `m.call.invite` events in the timeline, room list and notifications --- ElementX.xcodeproj/project.pbxproj | 8 ++++ .../en.lproj/Localizable.strings | 1 + ElementX/Sources/Generated/Strings.swift | 2 + .../Timeline/CallInviteRoomTimelineView.swift | 48 +++++++++++++++++++ .../RoomSummary/RoomEventStringBuilder.swift | 2 +- .../Other/CallInviteRoomTimelineItem.swift | 28 +++++++++++ .../RoomTimelineItemFactory.swift | 10 +++- .../TimelineItems/RoomTimelineItemView.swift | 2 + .../RoomTimelineItemViewState.swift | 6 ++- NSE/Sources/NotificationContentBuilder.swift | 8 ++++ .../test_callInviteRoomTimelineView.1.png | 3 ++ changelog.d/1837.feature | 1 + 12 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 ElementX/Sources/Screens/RoomScreen/View/Timeline/CallInviteRoomTimelineView.swift create mode 100644 ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/CallInviteRoomTimelineItem.swift create mode 100644 PreviewTests/__Snapshots__/PreviewTests/test_callInviteRoomTimelineView.1.png create mode 100644 changelog.d/1837.feature diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index dec9142da..8d7a530bd 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ /* Begin PBXBuildFile section */ 0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C070FD43DC6BF4E50217965A /* LocalizationTests.swift */; }; 0180C44B997EDA8D21F883AC /* RoomNotificationSettingsCustomSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B746EFA112532A7B701FB914 /* RoomNotificationSettingsCustomSectionView.swift */; }; + 01B63F1A04A276B39AC17014 /* CallInviteRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A3D3CFA199FA7897364547 /* CallInviteRoomTimelineItem.swift */; }; 020C530986D7B97631877FEF /* TimelineItemMacContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A4AD793D50748F8997E5B15 /* TimelineItemMacContextMenu.swift */; }; 020F7E70167FB2833266F2F0 /* AnalyticsSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D39D7F513A36C9C1951DB44C /* AnalyticsSettingsScreen.swift */; }; 024E70451A7CD9E4E034D8A9 /* VoiceMessageRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D529B976F8B2AA654D923422 /* VoiceMessageRoomTimelineItem.swift */; }; @@ -1003,6 +1004,7 @@ F697284B9B5F2C00CFEA3B12 /* EmojiDetectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58E93D91DE3288010390DEE /* EmojiDetectionTests.swift */; }; F6DFA23885980118AD7359C5 /* NotificationSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2389732B0E115A999A069083 /* NotificationSettingsScreenCoordinator.swift */; }; F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */; }; + F7048AD79361405AA95F2B3B /* CallInviteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA8F098AE48D958B4257EB24 /* CallInviteRoomTimelineView.swift */; }; F7567DD6635434E8C563BF85 /* AnalyticsClientProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */; }; F777C6FEE7D106136E2ED2B2 /* MessageForwardingScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F6E6EDC4BBF962B2ED595A4 /* MessageForwardingScreenViewModelTests.swift */; }; F78BAD28482A467287A9A5A3 /* EventBasedMessageTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0900BBF0A5D5D775E917C70 /* EventBasedMessageTimelineItemProtocol.swift */; }; @@ -1869,6 +1871,7 @@ CA29952595B804DA221A0C1D /* ComposerToolbarViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbarViewModelTests.swift; sourceTree = ""; }; CA2A71915C1F075E403F559C /* InvitesScreenCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenCell.swift; sourceTree = ""; }; CA89A2DD51B6BBE1DA55E263 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; + CA8F098AE48D958B4257EB24 /* CallInviteRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallInviteRoomTimelineView.swift; sourceTree = ""; }; CA90BD288E5AE6BC643AFDDF /* TemplateScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenCoordinator.swift; sourceTree = ""; }; CACA846B3E3E9A521D98B178 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; CAD9547E47C58930E2CE8306 /* CallScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenViewModelTests.swift; sourceTree = ""; }; @@ -1995,6 +1998,7 @@ E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIFont+AttributedStringBuilder.m"; sourceTree = ""; }; E96ED747FF90332EA1333C22 /* RoomTimelineItemFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemFixtures.swift; sourceTree = ""; }; E992D7B8BE54B2AB454613AF /* XCUIElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCUIElement.swift; sourceTree = ""; }; + E9A3D3CFA199FA7897364547 /* CallInviteRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallInviteRoomTimelineItem.swift; sourceTree = ""; }; E9D059BFE329BE09B6D96A9F /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ro; path = ro.lproj/Localizable.stringsdict; sourceTree = ""; }; EA4D639E27D5882A6A71AECF /* GlobalSearchScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreenViewModelTests.swift; sourceTree = ""; }; EA880E78AF4BD24E45A7808C /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -4259,6 +4263,7 @@ B1FC81662045E2369B0C4A0E /* Other */ = { isa = PBXGroup; children = ( + E9A3D3CFA199FA7897364547 /* CallInviteRoomTimelineItem.swift */, A7C4EA55DA62F9D0F984A2AE /* CollapsibleTimelineItem.swift */, 5351EBD7A0B9610548E4B7B2 /* EncryptedRoomTimelineItem.swift */, 5281C5CDC4A712265A0B5FBF /* PollRoomTimelineItem.swift */, @@ -4332,6 +4337,7 @@ isa = PBXGroup; children = ( FC2D505742FDA21FCDC4C18A /* AudioRoomTimelineView.swift */, + CA8F098AE48D958B4257EB24 /* CallInviteRoomTimelineView.swift */, 6E2656184491C505700D2405 /* CollapsibleRoomTimelineView.swift */, 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */, 75697AB5E64A12F1F069F511 /* EncryptedHistoryRoomTimelineView.swift */, @@ -5691,6 +5697,8 @@ 172E6E9A612ADCF10A62CF13 /* BugReportServiceProtocol.swift in Sources */, E1DF24D085572A55C9758A2D /* Bundle.swift in Sources */, 6BAD956B909A6E29F6CC6E7C /* ButtonStyle.swift in Sources */, + 01B63F1A04A276B39AC17014 /* CallInviteRoomTimelineItem.swift in Sources */, + F7048AD79361405AA95F2B3B /* CallInviteRoomTimelineView.swift in Sources */, D19A748E95E2FAB2940570F0 /* CallScreen.swift in Sources */, 763D69741D58D2B650BC1FC9 /* CallScreenCoordinator.swift in Sources */, B7C9E07F4F9CCC8DD7156A20 /* CallScreenModels.swift in Sources */, diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index d39b46238..7e03be90c 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -105,6 +105,7 @@ "common_audio" = "Audio"; "common_blocked_users" = "Blocked users"; "common_bubbles" = "Bubbles"; +"common_call_invite" = "Call in progress (unsupported)"; "common_chat_backup" = "Chat backup"; "common_copyright" = "Copyright"; "common_creating_room" = "Creating room…"; diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 487f71273..00de74155 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -242,6 +242,8 @@ internal enum L10n { internal static var commonBlockedUsers: String { return L10n.tr("Localizable", "common_blocked_users") } /// Bubbles internal static var commonBubbles: String { return L10n.tr("Localizable", "common_bubbles") } + /// Call in progress (unsupported) + internal static var commonCallInvite: String { return L10n.tr("Localizable", "common_call_invite") } /// Chat backup internal static var commonChatBackup: String { return L10n.tr("Localizable", "common_chat_backup") } /// Copyright diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/CallInviteRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/CallInviteRoomTimelineView.swift new file mode 100644 index 000000000..ec28835fc --- /dev/null +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/CallInviteRoomTimelineView.swift @@ -0,0 +1,48 @@ +// +// Copyright 2024 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Compound +import Foundation +import SwiftUI + +struct CallInviteRoomTimelineView: View { + let timelineItem: CallInviteRoomTimelineItem + + var body: some View { + Label(title: { Text(L10n.commonCallInvite) }, + icon: { CompoundIcon(\.voiceCall, size: .medium, relativeTo: .compound.bodyMD) }) + .font(.compound.bodyMD) + .foregroundColor(.compound.textSecondary) + .frame(maxWidth: .infinity, alignment: .center) + .padding() + } +} + +struct CallInviteRoomTimelineView_Previews: PreviewProvider, TestablePreview { + static let viewModel = RoomScreenViewModel.mock + + static var previews: some View { + body.environmentObject(viewModel.context) + } + + static var body: some View { + CallInviteRoomTimelineView(timelineItem: .init(id: .random, + timestamp: "Now", + isEditable: false, + canBeRepliedTo: false, + sender: .init(id: "Bob"))) + } +} diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift index dc18827af..ebafd9bba 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift @@ -68,7 +68,7 @@ struct RoomEventStringBuilder { case .poll(let question, _, _, _, _, _, _): return prefix(L10n.commonPollSummary(question), with: senderDisplayName) case .callInvite: - return nil + return prefix(L10n.commonCallInvite, with: senderDisplayName) } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/CallInviteRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/CallInviteRoomTimelineItem.swift new file mode 100644 index 000000000..66e135c92 --- /dev/null +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/CallInviteRoomTimelineItem.swift @@ -0,0 +1,28 @@ +// +// Copyright 2024 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +struct CallInviteRoomTimelineItem: RoomTimelineItemProtocol, Equatable { + let id: TimelineItemIdentifier + let timestamp: String + let isEditable: Bool + let canBeRepliedTo: Bool + + let sender: TimelineItemSender + + var properties = RoomTimelineItemProperties() +} diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift index 4f8521f8e..2fed26eb5 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift @@ -74,7 +74,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { case .poll(question: let question, kind: let kind, maxSelections: let maxSelections, answers: let answers, votes: let votes, endTime: let endTime, let edited): return buildPollTimelineItem(question, kind, maxSelections, answers, votes, endTime, eventItemProxy, isOutgoing, edited) case .callInvite: - return nil + return buildCallInviteTimelineItem(for: eventItemProxy) } } @@ -422,6 +422,14 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) } + private func buildCallInviteTimelineItem(for eventItemProxy: EventTimelineItemProxy) -> RoomTimelineItemProtocol { + CallInviteRoomTimelineItem(id: eventItemProxy.id, + timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened), + isEditable: eventItemProxy.isEditable, + canBeRepliedTo: eventItemProxy.canBeRepliedTo, + sender: eventItemProxy.sender) + } + private func aggregateReactions(_ reactions: [Reaction]) -> [AggregatedReaction] { reactions.map { reaction in let senders = reaction.senders diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemView.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemView.swift index bdfa6dc81..0d999ca3a 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemView.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemView.swift @@ -76,6 +76,8 @@ struct RoomTimelineItemView: View { PollRoomTimelineView(timelineItem: item) case .voice(let item): VoiceMessageRoomTimelineView(timelineItem: item, playerState: context.viewState.audioPlayerStateProvider?(item.id) ?? AudioPlayerState(id: .timelineItemIdentifier(item.id), duration: 0)) + case .callInvite(let item): + CallInviteRoomTimelineView(timelineItem: item) } } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemViewState.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemViewState.swift index 5e2fde3f4..c9a564327 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemViewState.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemViewState.swift @@ -66,6 +66,7 @@ enum RoomTimelineItemType: Equatable { case location(LocationRoomTimelineItem) case poll(PollRoomTimelineItem) case voice(VoiceMessageRoomTimelineItem) + case callInvite(CallInviteRoomTimelineItem) init(item: RoomTimelineItemProtocol) { switch item { @@ -111,6 +112,8 @@ enum RoomTimelineItemType: Equatable { self = .poll(item) case let item as VoiceMessageRoomTimelineItem: self = .voice(item) + case let item as CallInviteRoomTimelineItem: + self = .callInvite(item) default: fatalError("Unknown timeline item") } @@ -138,7 +141,8 @@ enum RoomTimelineItemType: Equatable { .group(let item as RoomTimelineItemProtocol), .location(let item as RoomTimelineItemProtocol), .poll(let item as RoomTimelineItemProtocol), - .voice(let item as RoomTimelineItemProtocol): + .voice(let item as RoomTimelineItemProtocol), + .callInvite(let item as RoomTimelineItemProtocol): return item.id } } diff --git a/NSE/Sources/NotificationContentBuilder.swift b/NSE/Sources/NotificationContentBuilder.swift index 45c629b6c..cc9bf1af5 100644 --- a/NSE/Sources/NotificationContentBuilder.swift +++ b/NSE/Sources/NotificationContentBuilder.swift @@ -44,6 +44,8 @@ struct NotificationContentBuilder { return try await processRoomMessage(notificationItem: notificationItem, messageType: messageType, mediaProvider: mediaProvider) case .poll(let question): return try await processPollStartEvent(notificationItem: notificationItem, pollQuestion: question, mediaProvider: mediaProvider) + case .callInvite: + return try await processCallInviteEvent(notificationItem: notificationItem, mediaProvider: mediaProvider) default: return processEmpty(notificationItem: notificationItem) } @@ -128,6 +130,12 @@ struct NotificationContentBuilder { notification.body = L10n.commonPollSummary(pollQuestion) return notification } + + private func processCallInviteEvent(notificationItem: NotificationItemProxyProtocol, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { + let notification = try await processCommonRoomMessage(notificationItem: notificationItem, mediaProvider: mediaProvider) + notification.body = L10n.commonCallInvite + return notification + } private func processCommonRoomMessage(notificationItem: NotificationItemProxyProtocol, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { var notification = baseMutableContent(for: notificationItem) diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_callInviteRoomTimelineView.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_callInviteRoomTimelineView.1.png new file mode 100644 index 000000000..107490725 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_callInviteRoomTimelineView.1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:132a9734cb8b511826add67c7db6bb72211f320d47a16c545ddca5abac4f8cd7 +size 64066 diff --git a/changelog.d/1837.feature b/changelog.d/1837.feature new file mode 100644 index 000000000..43503bc3f --- /dev/null +++ b/changelog.d/1837.feature @@ -0,0 +1 @@ +Added support for `m.call.invite` events in the timeline, room list and notifications \ No newline at end of file