diff --git a/CHANGES.rst b/CHANGES.rst index 400f4e41d..2882cb3e8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -92,6 +92,7 @@ Changes in 1.1.4 (2021-01-15) * Bug report: Add "Continue in background" button (#3816). * Show user id in the room invite preview screen (#3839) * AuthVC: SSO authentication now use redirect URL instead of fallback page (#3846). + * VoIP: Implement call tiles on timeline (#3955). 🐛 Bugfix * Crash report cannot be submitted (on small phones) (#3819) diff --git a/Podfile b/Podfile index 38349e222..46a8ae841 100644 --- a/Podfile +++ b/Podfile @@ -11,9 +11,9 @@ use_frameworks! # - `{ {kit spec hash} => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for each repo. Used by Fastfile during CI # # Warning: our internal tooling depends on the name of this variable name, so be sure not to change it -$matrixKitVersion = '= 0.13.8' +#$matrixKitVersion = '= 0.13.8' # $matrixKitVersion = :local -# $matrixKitVersion = {'develop' => 'develop'} + $matrixKitVersion = {'voip_2746' => 'voip_2746'} ######################################## diff --git a/Podfile.lock b/Podfile.lock index 7f785686f..c36755b5c 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -127,10 +127,10 @@ DEPENDENCIES: - KeychainAccess (~> 4.2.1) - KTCenterFlowLayout (~> 1.3.1) - MatomoTracker (~> 7.2.2) - - MatrixKit (= 0.13.8) - - MatrixKit/AppExtension (= 0.13.8) - - MatrixSDK - - MatrixSDK/JingleCallStack + - MatrixKit (from `https://github.com/matrix-org/matrix-ios-kit.git`, branch `voip_2746`) + - MatrixKit/AppExtension (from `https://github.com/matrix-org/matrix-ios-kit.git`, branch `voip_2746`) + - MatrixSDK (from `https://github.com/matrix-org/matrix-ios-sdk.git`, branch `voip_2746`) + - MatrixSDK/JingleCallStack (from `https://github.com/matrix-org/matrix-ios-sdk.git`, branch `voip_2746`) - OLMKit - ReadMoreTextView (~> 3.0.1) - Reusable (~> 4.1) @@ -164,8 +164,6 @@ SPEC REPOS: - LoggerAPI - Logging - MatomoTracker - - MatrixKit - - MatrixSDK - OLMKit - ReadMoreTextView - Realm @@ -177,6 +175,22 @@ SPEC REPOS: - zxcvbn-ios - ZXingObjC +EXTERNAL SOURCES: + MatrixKit: + :branch: voip_2746 + :git: https://github.com/matrix-org/matrix-ios-kit.git + MatrixSDK: + :branch: voip_2746 + :git: https://github.com/matrix-org/matrix-ios-sdk.git + +CHECKOUT OPTIONS: + MatrixKit: + :commit: dd4a397a95ff2e2b9a0d9b75a097ef63d428f683 + :git: https://github.com/matrix-org/matrix-ios-kit.git + MatrixSDK: + :commit: 7afc4c109249ab6a69da1c9f460a30f756cf6324 + :git: https://github.com/matrix-org/matrix-ios-sdk.git + SPEC CHECKSUMS: AFNetworking: 7864c38297c79aaca1500c33288e429c3451fdce BlueCryptor: b0aee3d9b8f367b49b30de11cda90e1735571c24 @@ -212,6 +226,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 376eeb13ecb078f5d76166cd2982477f181e815f +PODFILE CHECKSUM: 736d0c773f36aceb3334fdda58933e81360d5774 COCOAPODS: 1.10.0 diff --git a/Riot.xcodeproj/project.pbxproj b/Riot.xcodeproj/project.pbxproj index 47c72d15c..807bc3489 100644 --- a/Riot.xcodeproj/project.pbxproj +++ b/Riot.xcodeproj/project.pbxproj @@ -963,6 +963,12 @@ ECB5D98F255420F8000AD89C /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECB5D98E255420F8000AD89C /* Keychain.swift */; }; ECB5D9902554221F000AD89C /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECB5D98E255420F8000AD89C /* Keychain.swift */; }; ECC4C82F256FA7520010BA44 /* CallService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECC4C82E256FA7520010BA44 /* CallService.swift */; }; + ECD95ACF25CAB5C300E92D6F /* RoomBubbleCellData.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD95ACE25CAB5C300E92D6F /* RoomBubbleCellData.swift */; }; + ECD95AD725CAB5D100E92D6F /* CallBubbleCellBaseContentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = ECD95AD125CAB5D100E92D6F /* CallBubbleCellBaseContentView.xib */; }; + ECD95AD825CAB5D100E92D6F /* RoomDirectCallStatusBubbleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD95AD325CAB5D100E92D6F /* RoomDirectCallStatusBubbleCell.swift */; }; + ECD95AD925CAB5D100E92D6F /* RoomBaseCallBubbleCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = ECD95AD425CAB5D100E92D6F /* RoomBaseCallBubbleCell.xib */; }; + ECD95ADA25CAB5D100E92D6F /* RoomBaseCallBubbleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD95AD525CAB5D100E92D6F /* RoomBaseCallBubbleCell.swift */; }; + ECD95ADB25CAB5D100E92D6F /* CallBubbleCellBaseContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD95AD625CAB5D100E92D6F /* CallBubbleCellBaseContentView.swift */; }; ECDC15F224AF41D2003437CF /* FormattedBodyParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECDC15F124AF41D2003437CF /* FormattedBodyParser.swift */; }; ECF57A4425090C23004BBF9D /* CreateRoomCoordinatorBridgePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF57A3825090C23004BBF9D /* CreateRoomCoordinatorBridgePresenter.swift */; }; ECF57A4525090C23004BBF9D /* CreateRoomCoordinatorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF57A3925090C23004BBF9D /* CreateRoomCoordinatorType.swift */; }; @@ -2252,6 +2258,12 @@ ECB101352477D00700CF8C11 /* UniversalLink.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UniversalLink.m; sourceTree = ""; }; ECB5D98E255420F8000AD89C /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = ""; }; ECC4C82E256FA7520010BA44 /* CallService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallService.swift; sourceTree = ""; }; + ECD95ACE25CAB5C300E92D6F /* RoomBubbleCellData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomBubbleCellData.swift; sourceTree = ""; }; + ECD95AD125CAB5D100E92D6F /* CallBubbleCellBaseContentView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CallBubbleCellBaseContentView.xib; sourceTree = ""; }; + ECD95AD325CAB5D100E92D6F /* RoomDirectCallStatusBubbleCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomDirectCallStatusBubbleCell.swift; sourceTree = ""; }; + ECD95AD425CAB5D100E92D6F /* RoomBaseCallBubbleCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = RoomBaseCallBubbleCell.xib; sourceTree = ""; }; + ECD95AD525CAB5D100E92D6F /* RoomBaseCallBubbleCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomBaseCallBubbleCell.swift; sourceTree = ""; }; + ECD95AD625CAB5D100E92D6F /* CallBubbleCellBaseContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallBubbleCellBaseContentView.swift; sourceTree = ""; }; ECDC15F124AF41D2003437CF /* FormattedBodyParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattedBodyParser.swift; sourceTree = ""; }; ECF57A3825090C23004BBF9D /* CreateRoomCoordinatorBridgePresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateRoomCoordinatorBridgePresenter.swift; sourceTree = ""; }; ECF57A3925090C23004BBF9D /* CreateRoomCoordinatorType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateRoomCoordinatorType.swift; sourceTree = ""; }; @@ -4431,6 +4443,7 @@ B1B5583F20EF768E00210D55 /* BubbleCells */ = { isa = PBXGroup; children = ( + ECD95AD025CAB5D100E92D6F /* Call */, ECFBD5E6250F97FC00DD5F5A /* RoomCreationCollapsedBubbleCell.h */, ECFBD5E9250F97FC00DD5F5A /* RoomCreationCollapsedBubbleCell.m */, ECFBD5E4250F97FC00DD5F5A /* RoomCreationCollapsedBubbleCell.xib */, @@ -5461,6 +5474,26 @@ path = Call; sourceTree = ""; }; + ECD95AD025CAB5D100E92D6F /* Call */ = { + isa = PBXGroup; + children = ( + ECD95AD125CAB5D100E92D6F /* CallBubbleCellBaseContentView.xib */, + ECD95AD225CAB5D100E92D6F /* Direct */, + ECD95AD425CAB5D100E92D6F /* RoomBaseCallBubbleCell.xib */, + ECD95AD525CAB5D100E92D6F /* RoomBaseCallBubbleCell.swift */, + ECD95AD625CAB5D100E92D6F /* CallBubbleCellBaseContentView.swift */, + ); + path = Call; + sourceTree = ""; + }; + ECD95AD225CAB5D100E92D6F /* Direct */ = { + isa = PBXGroup; + children = ( + ECD95AD325CAB5D100E92D6F /* RoomDirectCallStatusBubbleCell.swift */, + ); + path = Direct; + sourceTree = ""; + }; ECF57A3725090C23004BBF9D /* CreateRoom */ = { isa = PBXGroup; children = ( @@ -5666,6 +5699,7 @@ F083BBE41E7009EC00A9B29C /* Categories */ = { isa = PBXGroup; children = ( + ECD95ACE25CAB5C300E92D6F /* RoomBubbleCellData.swift */, ECFBD5D9250A7ABD00DD5F5A /* MXThirdPartyProtocolInstance.swift */, EC711B4524A63B13008F830C /* MXRecoveryService.swift */, ECB1012E2477CFDB00CF8C11 /* UIDevice.swift */, @@ -6088,6 +6122,7 @@ B1B558C220EF768F00210D55 /* RoomIncomingEncryptedTextMsgBubbleCell.xib in Resources */, B1B558F620EF768F00210D55 /* RoomOutgoingTextMsgBubbleCell.xib in Resources */, B1B5574D20EE6C4D00210D55 /* MediaPickerViewController.xib in Resources */, + ECD95AD925CAB5D100E92D6F /* RoomBaseCallBubbleCell.xib in Resources */, B1B5575020EE6C4D00210D55 /* AuthenticationViewController.xib in Resources */, B1C45A85232A8C2600165425 /* SettingsIdentityServerViewController.storyboard in Resources */, B14F142E22144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyViewController.storyboard in Resources */, @@ -6137,6 +6172,7 @@ 3232AB1522564D9100AD6A5C /* flat-swift4-vector.stencil in Resources */, F083BDE61E7009ED00A9B29C /* busy.mp3 in Resources */, EC711BB324A63B58008F830C /* SecureBackupBannerCell.xib in Resources */, + ECD95AD725CAB5D100E92D6F /* CallBubbleCellBaseContentView.xib in Resources */, B1B5574C20EE6C4D00210D55 /* MediaAlbumContentViewController.xib in Resources */, 32607D6F243E0A55006674CC /* KeyBackupRecoverFromPrivateKeyViewController.storyboard in Resources */, EC757B2625B85C7F00DF5787 /* DialpadViewController.storyboard in Resources */, @@ -6613,6 +6649,7 @@ B1C3361C22F32B4A0021BA8D /* SingleImagePickerPresenter.swift in Sources */, B1B5572F20EE6C4D00210D55 /* ReadReceiptsViewController.m in Sources */, B1BD71BC238E8F9600BA92E2 /* WidgetPermissionViewController.swift in Sources */, + ECD95AD825CAB5D100E92D6F /* RoomDirectCallStatusBubbleCell.swift in Sources */, B1B558CB20EF768F00210D55 /* RoomIncomingEncryptedTextMsgWithoutSenderInfoBubbleCell.m in Sources */, B10A3E9824FE86AF007C380F /* SplitViewCoordinatorType.swift in Sources */, B11291EA238D35590077B478 /* SlidingModalPresentable.swift in Sources */, @@ -6885,6 +6922,7 @@ B1C7822F2500EAF500337EB9 /* TabBarCoordinator.swift in Sources */, B1B557DE20EF5FBB00210D55 /* FilesSearchTableViewCell.m in Sources */, B1B5574020EE6C4D00210D55 /* SegmentedViewController.m in Sources */, + ECD95ACF25CAB5C300E92D6F /* RoomBubbleCellData.swift in Sources */, ECF57A4C25090C23004BBF9D /* EnterNewRoomDetailsViewAction.swift in Sources */, EC711BAE24A63B58008F830C /* SecureBackupSetupCoordinator.swift in Sources */, B1B5599320EFC5E400210D55 /* DecryptionFailure.m in Sources */, @@ -7092,6 +7130,7 @@ EC85D6AE2477DC89002C44C9 /* RoundedButton.swift in Sources */, B1CE83D72422817200D07506 /* KeyVerificationVerifyByScanningViewModelType.swift in Sources */, 3232ABA1225730E100AD6A5C /* KeyVerificationCoordinatorType.swift in Sources */, + ECD95ADA25CAB5D100E92D6F /* RoomBaseCallBubbleCell.swift in Sources */, B1C562D9228C0B760037F12A /* RoomContextualMenuItem.swift in Sources */, B1C543B223A2913F00DCA1FA /* KeyVerificationConclusionBubbleCell.swift in Sources */, 323AB947232BD74600C1451F /* AuthFallBackViewController.m in Sources */, @@ -7210,6 +7249,7 @@ B109D6F1222D8C400061B6D9 /* UIApplication.swift in Sources */, ECFBD5D6250A7AAF00DD5F5A /* ShowDirectoryViewAction.swift in Sources */, EC711BB124A63B58008F830C /* SecureBackupBannerPreferences.swift in Sources */, + ECD95ADB25CAB5D100E92D6F /* CallBubbleCellBaseContentView.swift in Sources */, ECF57A4625090C23004BBF9D /* CreateRoomCoordinator.swift in Sources */, ECFBD5DC250A82B200DD5F5A /* TextViewTableViewHeaderFooterView.swift in Sources */, B1BEE73723DF44A60003A4CB /* UserVerificationSessionsStatusViewState.swift in Sources */, diff --git a/Riot/Assets/Images.xcassets/Call/call_video_icon.imageset/24.png b/Riot/Assets/Images.xcassets/Call/call_video_icon.imageset/24.png new file mode 100644 index 000000000..aa4d94502 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Call/call_video_icon.imageset/24.png differ diff --git a/Riot/Assets/Images.xcassets/Call/call_video_icon.imageset/24@2x.png b/Riot/Assets/Images.xcassets/Call/call_video_icon.imageset/24@2x.png new file mode 100644 index 000000000..b359725c4 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Call/call_video_icon.imageset/24@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Call/call_video_icon.imageset/24@3x.png b/Riot/Assets/Images.xcassets/Call/call_video_icon.imageset/24@3x.png new file mode 100644 index 000000000..2c487f4aa Binary files /dev/null and b/Riot/Assets/Images.xcassets/Call/call_video_icon.imageset/24@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Call/call_video_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Call/call_video_icon.imageset/Contents.json new file mode 100644 index 000000000..f69f28085 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Call/call_video_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "24.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "24@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "24@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 223cbf657..00a2532d8 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -809,6 +809,12 @@ Tap the + to start adding people."; "event_formatter_rerequest_keys_part1_link" = "Re-request encryption keys"; "event_formatter_rerequest_keys_part2" = " from your other sessions."; "event_formatter_message_edited_mention" = "(edited)"; +"event_formatter_call_voice" = "Voice call"; +"event_formatter_call_video" = "Video call"; +"event_formatter_call_has_ended" = "This call has ended"; +"event_formatter_call_you_currently_in" = "You're currently in this call"; +"event_formatter_call_you_declined" = "You declined this call"; +"event_formatter_call_back" = "Call back"; // Events formatter with you "event_formatter_widget_added_by_you" = "You added the widget: %@"; diff --git a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.h b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.h index ce1282065..ac04ef63f 100644 --- a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.h +++ b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.h @@ -56,6 +56,13 @@ extern NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestAcceptPre */ extern NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed; +/** + Action identifier used when the user pressed "Call back" button for a declined call. + + The `userInfo` dictionary contains an `MXEvent` object under the `kMXKRoomBubbleCellEventKey` key, representing the invite event of the declined call. + */ +extern NSString *const kMXKRoomBubbleCellCallBackButtonPressed; + /** Define a `MXKRoomBubbleTableViewCell` category at Riot level to handle bubble customisation. */ diff --git a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m index 6723f2dc0..f48054f7f 100644 --- a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m +++ b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m @@ -33,6 +33,7 @@ NSString *const kMXKRoomBubbleCellLongPressOnReactionView = @"kMXKRoomBubbleCell NSString *const kMXKRoomBubbleCellEventIdKey = @"kMXKRoomBubbleCellEventIdKey"; NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestAcceptPressed = @"kMXKRoomBubbleCellKeyVerificationAcceptPressed"; NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed = @"kMXKRoomBubbleCellKeyVerificationDeclinePressed"; +NSString *const kMXKRoomBubbleCellCallBackButtonPressed = @"kMXKRoomBubbleCellCallBackButtonPressed"; @implementation MXKRoomBubbleTableViewCell (Riot) diff --git a/Riot/Categories/RoomBubbleCellData.swift b/Riot/Categories/RoomBubbleCellData.swift new file mode 100644 index 000000000..d35681dd4 --- /dev/null +++ b/Riot/Categories/RoomBubbleCellData.swift @@ -0,0 +1,50 @@ +// +// Copyright 2020 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 + +@objc extension RoomBubbleCellData { + + /// Gathers all collapsable events in both directions (previous and next) + /// - Returns: Array of events containing collapsable events in both sides. + func allLinkedEvents() -> [MXEvent] { + var result: [MXEvent] = [] + + // add prev linked events + var prevBubbleData = prevCollapsableCellData + while prevBubbleData != nil { + // swiftlint:disable force_unwrapping + result.append(contentsOf: prevBubbleData!.events) + // swiftlint:enable force_unwrapping + prevBubbleData = prevBubbleData?.prevCollapsableCellData + } + + // add self events + result.append(contentsOf: events) + + // add next linked events + var nextBubbleData = nextCollapsableCellData + while nextBubbleData != nil { + // swiftlint:disable force_unwrapping + result.append(contentsOf: nextBubbleData!.events) + // swiftlint:enable force_unwrapping + nextBubbleData = nextBubbleData?.nextCollapsableCellData + } + + return result + } + +} diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index f28b92c92..90a35d7ac 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -38,6 +38,7 @@ internal enum Asset { internal static let callPipIcon = ImageAsset(name: "call_pip_icon") internal static let callSpeakerOffIcon = ImageAsset(name: "call_speaker_off_icon") internal static let callSpeakerOnIcon = ImageAsset(name: "call_speaker_on_icon") + internal static let callVideoIcon = ImageAsset(name: "call_video_icon") internal static let callVideoMuteOffIcon = ImageAsset(name: "call_video_mute_off_icon") internal static let callVideoMuteOnIcon = ImageAsset(name: "call_video_mute_on_icon") internal static let callkitIcon = ImageAsset(name: "callkit_icon") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 4c8cf695d..66383bff8 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -1238,6 +1238,30 @@ internal enum VectorL10n { internal static var errorUserAlreadyLoggedIn: String { return VectorL10n.tr("Vector", "error_user_already_logged_in") } + /// Call back + internal static var eventFormatterCallBack: String { + return VectorL10n.tr("Vector", "event_formatter_call_back") + } + /// This call has ended + internal static var eventFormatterCallHasEnded: String { + return VectorL10n.tr("Vector", "event_formatter_call_has_ended") + } + /// Video call + internal static var eventFormatterCallVideo: String { + return VectorL10n.tr("Vector", "event_formatter_call_video") + } + /// Voice call + internal static var eventFormatterCallVoice: String { + return VectorL10n.tr("Vector", "event_formatter_call_voice") + } + /// You're currently in this call + internal static var eventFormatterCallYouCurrentlyIn: String { + return VectorL10n.tr("Vector", "event_formatter_call_you_currently_in") + } + /// You declined this call + internal static var eventFormatterCallYouDeclined: String { + return VectorL10n.tr("Vector", "event_formatter_call_you_declined") + } /// VoIP conference added by %@ internal static func eventFormatterJitsiWidgetAdded(_ p1: String) -> String { return VectorL10n.tr("Vector", "event_formatter_jitsi_widget_added", p1) diff --git a/Riot/Managers/Call/CallService.swift b/Riot/Managers/Call/CallService.swift index de06b8d79..80a8996ae 100644 --- a/Riot/Managers/Call/CallService.swift +++ b/Riot/Managers/Call/CallService.swift @@ -25,6 +25,7 @@ class CallService: NSObject { static let pipAnimationDuration: TimeInterval = 0.25 } + private var sessions: [MXSession] = [] private var callVCs: [String: CallViewController] = [:] private var callBackgroundTasks: [String: MXBackgroundTask] = [:] private weak var presentedCallVC: CallViewController? { @@ -77,6 +78,16 @@ class CallService: NSObject { /// Delegate object weak var delegate: CallServiceDelegate? + func addMatrixSession(_ session: MXSession) { + sessions.append(session) + } + + func removeMatrixSession(_ session: MXSession) { + if let index = sessions.firstIndex(of: session) { + sessions.remove(at: index) + } + } + /// Start the service func start() { addCallObservers() @@ -217,6 +228,10 @@ class CallService: NSObject { selector: #selector(callStateChanged(_:)), name: NSNotification.Name(rawValue: kMXCallStateDidChange), object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(callTileTapped(_:)), + name: NSNotification.Name(rawValue: kRoomCallTileTapped), + object: nil) isStarted = true } @@ -232,6 +247,9 @@ class CallService: NSObject { NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: kMXCallStateDidChange), object: nil) + NotificationCenter.default.removeObserver(self, + name: NSNotification.Name(rawValue: kRoomCallTileTapped), + object: nil) isStarted = false } @@ -301,6 +319,40 @@ class CallService: NSObject { } } + @objc + private func callTileTapped(_ notification: Notification) { + NSLog("[CallService] callTileTapped") + guard let bubbleData = notification.object as? RoomBubbleCellData else { + return + } + + guard let randomEvent = bubbleData.allLinkedEvents().randomElement() else { + return + } + + guard let callEventContent = MXCallEventContent(fromJSON: randomEvent.content) else { + return + } + + NSLog("[CallService] callTileTapped: for call: \(callEventContent.callId)") + + guard let session = sessions.first else { return } + + guard let call = session.callManager.call(withCallId: callEventContent.callId) else { + return + } + + if call.state == .ended { + return + } + + guard let callVC = callVCs[call.callId] else { + return + } + + presentCallVC(callVC) + } + // MARK: - Call Screens private func presentCallVC(_ callVC: CallViewController, completion: (() -> Void)? = nil) { diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index c8fdf6219..bf356d821 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -1961,6 +1961,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Register the session to the widgets manager [[WidgetManager sharedManager] addMatrixSession:mxSession]; + // register the session to the call service + [_callService addMatrixSession:mxSession]; + [mxSessionArray addObject:mxSession]; // Do the one time check on device id @@ -1974,6 +1977,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Update home data sources [_masterTabBarController removeMatrixSession:mxSession]; + + // remove session from the call service + [_callService removeMatrixSession:mxSession]; // Update the widgets manager [[WidgetManager sharedManager] removeMatrixSession:mxSession]; diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.h b/Riot/Modules/Room/CellData/RoomBubbleCellData.h index 305371c75..6c2f729d3 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.h +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.h @@ -26,7 +26,8 @@ typedef NS_ENUM(NSInteger, RoomBubbleCellDataTag) RoomBubbleCellDataTagKeyVerificationNoDisplay, RoomBubbleCellDataTagKeyVerificationRequestIncomingApproval, RoomBubbleCellDataTagKeyVerificationRequest, - RoomBubbleCellDataTagKeyVerificationConclusion + RoomBubbleCellDataTagKeyVerificationConclusion, + RoomBubbleCellDataTagCall }; /** diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index dee71aec9..35bd209f7 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -125,6 +125,18 @@ static NSAttributedString *timestampVerticalWhitespace = nil; // Membership events can be collapsed together self.collapsable = YES; + // Collapse them by default + self.collapsed = YES; + } + break; + case MXEventTypeCallInvite: + case MXEventTypeCallReject: + { + self.tag = RoomBubbleCellDataTagCall; + + // Call events can be collapsed together + self.collapsable = YES; + // Collapse them by default self.collapsed = YES; } @@ -233,6 +245,17 @@ static NSAttributedString *timestampVerticalWhitespace = nil; { return YES; } + else if (self.tag == RoomBubbleCellDataTagCall && cellData.tag == RoomBubbleCellDataTagCall) + { + // Check if the same call + MXEvent * event1 = self.events.firstObject; + MXCallEventContent *eventContent1 = [MXCallEventContent modelFromJSON:event1.content]; + + MXEvent * event2 = cellData.events.firstObject; + MXCallEventContent *eventContent2 = [MXCallEventContent modelFromJSON:event2.content]; + + return [eventContent1.callId isEqualToString:eventContent2.callId]; + } if (self.tag == RoomBubbleCellDataTagRoomCreateWithPredecessor || cellData.tag == RoomBubbleCellDataTagRoomCreateWithPredecessor) { @@ -711,6 +734,9 @@ static NSAttributedString *timestampVerticalWhitespace = nil; // One single bubble per membership event shouldAddEvent = NO; break; + case RoomBubbleCellDataTagCall: + shouldAddEvent = NO; + break; case RoomBubbleCellDataTagRoomCreateConfiguration: shouldAddEvent = NO; break; @@ -755,6 +781,10 @@ static NSAttributedString *timestampVerticalWhitespace = nil; case MXEventTypeRoomJoinRules: shouldAddEvent = NO; break; + case MXEventTypeCallInvite: + case MXEventTypeCallReject: + shouldAddEvent = NO; + break; default: break; } diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index 72b7c2ce5..99ee17445 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -27,6 +27,11 @@ #import "UIViewController+RiotSearch.h" +/** + Notification string used to indicate call tile tapped in a room. Notification object will be the `RoomBubbleCellData` object. + */ +extern NSString *const kRoomCallTileTapped; + @interface RoomViewController : MXKRoomViewController // The preview header diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index c474f2c09..ec7957dd8 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -123,6 +123,8 @@ #import "Riot-Swift.h" +NSString *const kRoomCallTileTapped = @"RoomCallTileTapped"; + @interface RoomViewController () + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/Views/BubbleCells/Call/Direct/RoomDirectCallStatusBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Call/Direct/RoomDirectCallStatusBubbleCell.swift new file mode 100644 index 000000000..8a5391c8b --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Call/Direct/RoomDirectCallStatusBubbleCell.swift @@ -0,0 +1,207 @@ +// +// Copyright 2020 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 + +class RoomDirectCallStatusBubbleCell: RoomBaseCallBubbleCell { + + private enum Constants { + static let statusTextFontSize: CGFloat = 14 + static let statusTextInsets: UIEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 12, right: 8) + // swiftlint:disable force_unwrapping + static let statusCallBackURL: URL = URL(string: "element://call")! + // swiftlint:enable force_unwrapping + } + + private lazy var statusTextView: UITextView = { + let textView = UITextView() + textView.font = .systemFont(ofSize: Constants.statusTextFontSize) + textView.backgroundColor = .clear + textView.textColor = ThemeService.shared().theme.noticeSecondaryColor + textView.linkTextAttributes = [ + .font: UIFont.systemFont(ofSize: Constants.statusTextFontSize), + .foregroundColor: ThemeService.shared().theme.tintColor + ] + textView.textAlignment = .center + textView.contentInset = .zero + textView.isEditable = false + textView.isSelectable = false + textView.isScrollEnabled = false + textView.scrollsToTop = false + textView.textContainerInset = Constants.statusTextInsets + textView.textContainer.lineFragmentPadding = 0 + textView.delegate = self + return textView + }() + + override var bottomContentView: UIView? { + return statusTextView + } + + override func update(theme: Theme) { + super.update(theme: theme) + statusTextView.textColor = theme.noticeSecondaryColor + statusTextView.linkTextAttributes = [ + .font: UIFont.systemFont(ofSize: Constants.statusTextFontSize), + .foregroundColor: theme.tintColor + ] + } + + private func configure(withCall call: MXCall) { + switch call.state { + case .connected, + .fledgling, + .waitLocalMedia, + .createOffer, + .inviteSent, + .createAnswer, + .connecting, + .onHold, + .remotelyOnHold: + statusTextView.text = VectorL10n.eventFormatterCallYouCurrentlyIn + case .ringing: + if call.isIncoming { + // should not be here + statusTextView.text = nil + } else { + statusTextView.text = VectorL10n.eventFormatterCallYouCurrentlyIn + } + case .ended: + switch call.endReason { + case .unknown, + .hangup, + .hangupElsewhere, + .remoteHangup, + .missed, + .answeredElseWhere: + statusTextView.text = VectorL10n.eventFormatterCallHasEnded + case .busy: + configureForRejectedCall(call: call) + @unknown default: + statusTextView.text = VectorL10n.eventFormatterCallHasEnded + } + case .inviteExpired, + .answeredElseWhere: + statusTextView.text = VectorL10n.eventFormatterCallHasEnded + @unknown default: + statusTextView.text = VectorL10n.eventFormatterCallHasEnded + } + } + + private func configureForRejectedCall(withEvent event: MXEvent? = nil, call: MXCall? = nil, bubbleCellData: RoomBubbleCellData? = nil) { + + let isMyReject: Bool + + if let call = call, call.isIncoming { + isMyReject = true + } else if let event = event, let bubbleCellData = bubbleCellData, event.sender == bubbleCellData.mxSession.myUserId { + isMyReject = true + } else { + isMyReject = false + } + + if isMyReject { + + let centerParagraphStyle = NSMutableParagraphStyle() + centerParagraphStyle.alignment = .center + + let mutableAttrString = NSMutableAttributedString(string: VectorL10n.eventFormatterCallYouDeclined + " " + VectorL10n.eventFormatterCallBack, attributes: [ + .font: UIFont.systemFont(ofSize: Constants.statusTextFontSize), + .foregroundColor: ThemeService.shared().theme.noticeSecondaryColor, + .paragraphStyle: centerParagraphStyle + ]) + + let range = mutableAttrString.mutableString.range(of: VectorL10n.eventFormatterCallBack) + if range.location != NSNotFound { + mutableAttrString.addAttribute(.link, value: Constants.statusCallBackURL, range: range) + } + + statusTextView.attributedText = mutableAttrString + statusTextView.isSelectable = true + } else { + statusTextView.text = VectorL10n.eventFormatterCallHasEnded + } + } + + // MARK: - MXKCellRendering + + override func render(_ cellData: MXKCellData!) { + super.render(cellData) + + guard let bubbleCellData = cellData as? RoomBubbleCellData else { + return + } + + let events = bubbleCellData.allLinkedEvents() + + // getting a random event for call id is enough + guard let randomEvent = bubbleCellData.events.randomElement() else { + return + } + + guard let callEventContent = MXCallEventContent(fromJSON: randomEvent.content) else { return } + let callId = callEventContent.callId + guard let call = bubbleCellData.mxSession.callManager.call(withCallId: callId) else { + + // check events include a reject event + if let rejectEvent = events.first(where: { $0.eventType == .callReject }) { + configureForRejectedCall(withEvent: rejectEvent, bubbleCellData: bubbleCellData) + return + } + + // there is no reject event, we can just say this call has ended + statusTextView.text = VectorL10n.eventFormatterCallHasEnded + return + } + + configure(withCall: call) + } + + override func prepareForReuse() { + statusTextView.isSelectable = false + statusTextView.text = nil + statusTextView.attributedText = nil + + super.prepareForReuse() + } + +} + +// MARK: - UITextViewDelegate + +extension RoomDirectCallStatusBubbleCell { + + override func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { + if URL == Constants.statusCallBackURL && interaction == .invokeDefaultAction { + let userInfo: [AnyHashable: Any]? + + guard let bubbleCellData = bubbleData as? RoomBubbleCellData else { + return false + } + let events = bubbleCellData.allLinkedEvents() + if let callInviteEvent = events.first(where: { $0.eventType == .callInvite }) { + userInfo = [kMXKRoomBubbleCellEventKey: callInviteEvent] + } else { + userInfo = nil + } + + self.delegate?.cell(self, didRecognizeAction: kMXKRoomBubbleCellCallBackButtonPressed, userInfo: userInfo) + return true + } + return false + } + +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Call/RoomBaseCallBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Call/RoomBaseCallBubbleCell.swift new file mode 100644 index 000000000..fed709feb --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Call/RoomBaseCallBubbleCell.swift @@ -0,0 +1,113 @@ +// +// Copyright 2020 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 +import Reusable + +class RoomBaseCallBubbleCell: MXKRoomBubbleTableViewCell { + + fileprivate lazy var innerContentView: CallBubbleCellBaseContentView = { + return CallBubbleCellBaseContentView.loadFromNib() + }() + + override required init!(style: UITableViewCell.CellStyle, reuseIdentifier: String!) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupViews() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupViews() + } + + override func setupViews() { + super.setupViews() + + self.contentView.vc_removeAllSubviews() + self.contentView.vc_addSubViewMatchingParent(innerContentView) + + updateBottomContentView() + } + + // Properties to override + private(set) var bottomContentView: UIView? + + func updateBottomContentView() { + innerContentView.bottomContainerView.vc_removeAllSubviews() + + guard let bottomContentView = bottomContentView else { return } + innerContentView.bottomContainerView.vc_addSubViewMatchingParent(bottomContentView) + } + + class func createSizingView() -> RoomBaseCallBubbleCell { + return self.init(style: .default, reuseIdentifier: self.defaultReuseIdentifier()) + } + + // MARK: - Overrides + + override var bubbleOverlayContainer: UIView! { + get { + guard let overlayContainer = innerContentView.bubbleOverlayContainer else { + fatalError("[RoomBaseCallBubbleCell] bubbleOverlayContainer should not be used before set") + } + return overlayContainer + } + set { + super.bubbleOverlayContainer = newValue + } + } + + // MARK: - MXKCellRendering + + override func render(_ cellData: MXKCellData!) { + super.render(cellData) + + update(theme: ThemeService.shared().theme) + + guard let cellData = cellData else { + return + } + + innerContentView.render(cellData) + } + + override class func height(for cellData: MXKCellData!, withMaximumWidth maxWidth: CGFloat) -> CGFloat { + guard let cellData = cellData else { + return 0 + } + + let fittingSize = CGSize(width: maxWidth, height: UIView.layoutFittingCompressedSize.height) + guard let cell = self.init(style: .default, reuseIdentifier: self.defaultReuseIdentifier()) else { + return 0 + } + cell.render(cellData) + + return cell.contentView.systemLayoutSizeFitting(fittingSize).height + } + +} + +extension RoomBaseCallBubbleCell: Themable { + + func update(theme: Theme) { + innerContentView.update(theme: theme) + } + +} + +extension RoomBaseCallBubbleCell: NibLoadable, Reusable { + +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Call/RoomBaseCallBubbleCell.xib b/Riot/Modules/Room/Views/BubbleCells/Call/RoomBaseCallBubbleCell.xib new file mode 100644 index 000000000..b7b585fca --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Call/RoomBaseCallBubbleCell.xib @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Utils/EventFormatter.m b/Riot/Utils/EventFormatter.m index b6ce37b81..6dd3aa6ce 100644 --- a/Riot/Utils/EventFormatter.m +++ b/Riot/Utils/EventFormatter.m @@ -26,6 +26,7 @@ #import "DecryptionFailureTracker.h" #import "EventFormatter+DTCoreTextFix.h" +#import #pragma mark - Constants definitions @@ -168,30 +169,55 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; } } - if (event.eventType == MXEventTypeRoomCreate) + switch (event.eventType) { - MXRoomCreateContent *createContent = [MXRoomCreateContent modelFromJSON:event.content]; - - NSString *roomPredecessorId = createContent.roomPredecessorInfo.roomId; - - if (roomPredecessorId) + case MXEventTypeRoomCreate: { - return [self roomCreatePredecessorAttributedStringWithPredecessorRoomId:roomPredecessorId]; + MXRoomCreateContent *createContent = [MXRoomCreateContent modelFromJSON:event.content]; + + NSString *roomPredecessorId = createContent.roomPredecessorInfo.roomId; + + if (roomPredecessorId) + { + return [self roomCreatePredecessorAttributedStringWithPredecessorRoomId:roomPredecessorId]; + } + else + { + NSAttributedString *string = [super attributedStringFromEvent:event withRoomState:roomState error:error]; + NSMutableAttributedString *result = [[NSMutableAttributedString alloc] initWithString:@"· "]; + [result appendAttributedString:string]; + return result; + } } - else + break; + case MXEventTypeCallCandidates: + case MXEventTypeCallAnswer: + case MXEventTypeCallSelectAnswer: + case MXEventTypeCallHangup: + case MXEventTypeCallNegotiate: + case MXEventTypeCallReplaces: + case MXEventTypeCallRejectReplacement: + // Do not show call events except invite and reject in timeline + return nil; + case MXEventTypeCallInvite: { - NSAttributedString *string = [super attributedStringFromEvent:event withRoomState:roomState error:error]; - NSMutableAttributedString *result = [[NSMutableAttributedString alloc] initWithString:@"· "]; - [result appendAttributedString:string]; - return result; + MXCallInviteEventContent *content = [MXCallInviteEventContent modelFromJSON:event.content]; + MXCall *call = [mxSession.callManager callWithCallId:content.callId]; + if (call && call.isIncoming && call.state == MXCallStateRinging) + { + // incoming call UI will be handled by CallKit (or incoming call screen if CallKit disabled) + // do not show a bubble for this case + return nil; + } } - } - - // Make event types MXEventTypeKeyVerificationCancel and MXEventTypeKeyVerificationDone visible in timeline. - // TODO: Find another way to keep them visible and avoid instantiate empty NSMutableAttributedString. - if (event.eventType == MXEventTypeKeyVerificationCancel || event.eventType == MXEventTypeKeyVerificationDone) - { - return [NSMutableAttributedString new]; + break; + case MXEventTypeKeyVerificationCancel: + case MXEventTypeKeyVerificationDone: + // Make event types MXEventTypeKeyVerificationCancel and MXEventTypeKeyVerificationDone visible in timeline. + // TODO: Find another way to keep them visible and avoid instantiate empty NSMutableAttributedString. + return [NSMutableAttributedString new]; + default: + break; } NSAttributedString *attributedString = [super attributedStringFromEvent:event withRoomState:roomState error:error]; @@ -265,6 +291,8 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; { MXEvent *roomCreateEvent = [events filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"type == %@", kMXEventTypeStringRoomCreate]].firstObject; + MXEvent *callInviteEvent = [events filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"type == %@", kMXEventTypeStringCallInvite]].firstObject; + if (roomCreateEvent) { MXKEventFormatterError tmpError; @@ -287,6 +315,11 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; [result appendAttributedString:linkMore]; return result; } + else if (callInviteEvent) + { + // return a non-nil value + return [NSMutableAttributedString new]; + } else if (events[0].eventType == MXEventTypeRoomMember) { // This is a series for cells tagged with RoomBubbleCellDataTagMembership