diff --git a/CHANGES.rst b/CHANGES.rst index 492903ce7..2c72c1345 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,20 @@ +Changes in 0.5.3 (2017-08-25) +=============================================== + +Improvements: + * Upgrade MatrixKit version (v0.6.2). + * Support dark theme (vector-im/riot-meta#22). + * Set the application group identifier to be able to share userDefaults object. + +Bug Fixes: + * SettingsViewController: Release correctly the pushed view controller. + * App have crashed whilst uploading photos (#1445). + * Register for remote notifications only if user provides access to notification feature, thanks to @aramsargsyan (#1467). + * Improvements in notification registration flow, thanks to @aramsargsyan (#1472). + +Translations: + * Enable Russian. + Changes in 0.5.2 (2017-08-01) =============================================== diff --git a/Podfile b/Podfile index ba5a9c5e6..8c8b975d8 100644 --- a/Podfile +++ b/Podfile @@ -5,10 +5,9 @@ source 'https://github.com/CocoaPods/Specs.git' target "Riot" do - # Different flavours of pods to MatrixKit # The tagged version on which this version of Riot has been built -pod 'MatrixKit', '0.6.1' +pod 'MatrixKit', '0.6.2' # The lastest release available on the CocoaPods repository #pod 'MatrixKit' @@ -52,8 +51,8 @@ pod 'OLMKit' #pod 'OLMKit', :path => '../olm/OLMKit.podspec' pod 'Realm', '~> 2.8.1' -# The tagged version on which this version of Riot has been built -pod 'MatrixKit', '0.6.1' +# The tagged version on which this version of Riot share extension has been built +pod 'MatrixKit/AppExtension', '0.6.2' # The lastest release available on the CocoaPods repository #pod 'MatrixKit/AppExtension' @@ -70,10 +69,38 @@ pod 'MatrixKit', '0.6.1' # Remove warnings from "bad" pods pod 'OLMKit', :inhibit_warnings => true pod 'cmark', :inhibit_warnings => true -pod 'DTCoreText', :inhibit_warnings => true end +target "SiriIntents" do +pod 'GoogleAnalytics' +# The Google WebRTC stack +pod 'WebRTC', '58.17.16937' +# OLMKit for crypto +pod 'OLMKit' +#pod 'OLMKit', :path => '../olm/OLMKit.podspec' +pod 'Realm', '~> 2.8.1' + +# The tagged version on which this version of Riot share extension has been built +pod 'MatrixKit/AppExtension', '0.6.2' + +# The lastest release available on the CocoaPods repository +#pod 'MatrixKit/AppExtension' + +# The develop branch version +#pod 'MatrixSDK', :git => 'https://github.com/matrix-org/matrix-ios-sdk.git', :branch => 'develop' +#pod 'MatrixKit/AppExtension', :git => 'https://github.com/matrix-org/matrix-ios-kit.git', :branch => 'develop' + +# The one used for developing both MatrixSDK and MatrixKit +# Note that MatrixSDK must be cloned into a folder called matrix-ios-sdk next to the MatrixKit folder +#pod 'MatrixSDK', :path => '../matrix-ios-sdk/MatrixSDK.podspec' +#pod 'MatrixKit/AppExtension', :path => '../matrix-ios-kit/MatrixKit.podspec' + +# Remove warnings from "bad" pods +pod 'OLMKit', :inhibit_warnings => true +pod 'cmark', :inhibit_warnings => true + +end diff --git a/Podfile.lock b/Podfile.lock index 3e0d6acf9..7264c3ab8 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -26,6 +26,11 @@ PODS: - DTFoundation/DTAnimatedGIF (~> 1.7.5) - DTFoundation/DTHTMLParser (~> 1.7.5) - DTFoundation/UIKit (~> 1.7.5) + - DTCoreText/Extension (1.6.20): + - DTFoundation/Core (~> 1.7.5) + - DTFoundation/DTAnimatedGIF (~> 1.7.5) + - DTFoundation/DTHTMLParser (~> 1.7.5) + - DTFoundation/UIKit (~> 1.7.5) - DTFoundation/Core (1.7.12) - DTFoundation/DTAnimatedGIF (1.7.12) - DTFoundation/DTHTMLParser (1.7.12): @@ -39,13 +44,27 @@ PODS: - GZIP (1.1.1) - HPGrowingTextView (1.1) - libPhoneNumber-iOS (0.9.10) - - MatrixKit (0.6.1): + - MatrixKit (0.6.2): - cmark (~> 0.24.1) - DTCoreText (~> 1.6.17) - HPGrowingTextView (~> 1.1) - libPhoneNumber-iOS (~> 0.9.10) - - MatrixSDK (= 0.9.1) - - MatrixSDK (0.9.1): + - MatrixKit/Core (= 0.6.2) + - MatrixSDK (= 0.9.2) + - MatrixKit/AppExtension (0.6.2): + - cmark (~> 0.24.1) + - DTCoreText (~> 1.6.17) + - DTCoreText/Extension + - HPGrowingTextView (~> 1.1) + - libPhoneNumber-iOS (~> 0.9.10) + - MatrixSDK (= 0.9.2) + - MatrixKit/Core (0.6.2): + - cmark (~> 0.24.1) + - DTCoreText (~> 1.6.17) + - HPGrowingTextView (~> 1.1) + - libPhoneNumber-iOS (~> 0.9.10) + - MatrixSDK (= 0.9.2) + - MatrixSDK (0.9.2): - AFNetworking (~> 3.1.0) - GZIP (~> 1.1.1) - OLMKit (2.2.2): @@ -63,7 +82,8 @@ DEPENDENCIES: - DTCoreText - GBDeviceInfo (~> 4.3.0) - GoogleAnalytics - - MatrixKit (= 0.6.1) + - MatrixKit (= 0.6.2) + - MatrixKit/AppExtension (= 0.6.2) - OLMKit - Realm (~> 2.8.1) - WebRTC (= 58.17.16937) @@ -78,12 +98,12 @@ SPEC CHECKSUMS: GZIP: f8beb59597f651e6970a45b816508a9c6d700b77 HPGrowingTextView: 88a716d97fb853bcb08a4a08e4727da17efc9b19 libPhoneNumber-iOS: f721ae4d5854bce60934f9fb9b0b28e8e68913cb - MatrixKit: fbf30823fb64604b5f8bb8ef8cbb8d949189525b - MatrixSDK: c5d9b84da5e32b305c5def410f239a7d94c72105 + MatrixKit: 8552ee8abf935b08ae08fc0000f53ab3218ea5a0 + MatrixSDK: 0499dd3dbe293ce1e743a7cda181dcf50eda7f10 OLMKit: b9d8c0ffee9ea8c45bc0aaa9afb47f93fba7efbd Realm: 3601ef091c8c499a31101d8563b991e75546cdce WebRTC: 1e9a85bf75509eec44be6478c64e9de65ac82332 -PODFILE CHECKSUM: 76ecd40dee0ff2c3007141b353b385be7e863d6c +PODFILE CHECKSUM: db0ae7d6037f7768feb2adf17119ba69d9e1a77b -COCOAPODS: 1.2.1 +COCOAPODS: 1.3.1 diff --git a/Riot.xcodeproj/project.pbxproj b/Riot.xcodeproj/project.pbxproj old mode 100644 new mode 100755 index 5f4941ee2..a20a1b659 --- a/Riot.xcodeproj/project.pbxproj +++ b/Riot.xcodeproj/project.pbxproj @@ -27,7 +27,7 @@ 24EEE5A11F23A09A00B3C705 /* RiotDesignValues.m in Sources */ = {isa = PBXBuildFile; fileRef = F083BC171E7009EC00A9B29C /* RiotDesignValues.m */; }; 24EEE5A21F23A8B400B3C705 /* MXRoom+Riot.m in Sources */ = {isa = PBXBuildFile; fileRef = F083BBE81E7009EC00A9B29C /* MXRoom+Riot.m */; }; 24EEE5A31F23A8C300B3C705 /* AvatarGenerator.m in Sources */ = {isa = PBXBuildFile; fileRef = F083BC111E7009EC00A9B29C /* AvatarGenerator.m */; }; - 24EEE5A41F24C06E00B3C705 /* (null) in Resources */ = {isa = PBXBuildFile; }; + 24EEE5A41F24C06E00B3C705 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; 24EEE5A81F25529600B3C705 /* cancel@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0614A121EDEE65000F5DC9A /* cancel@3x.png */; }; 24EEE5A91F25529900B3C705 /* cancel@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0614A111EDEE65000F5DC9A /* cancel@2x.png */; }; 24EEE5AA1F25529C00B3C705 /* cancel.png in Resources */ = {isa = PBXBuildFile; fileRef = F0614A101EDEE65000F5DC9A /* cancel.png */; }; @@ -76,6 +76,13 @@ 32D392191EB9B7AB009A2BAF /* DirectoryServerDetailTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 32D392171EB9B7AB009A2BAF /* DirectoryServerDetailTableViewCell.xib */; }; 32FD0A3D1EB0CD9B0072B066 /* BugReportViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 32FD0A3B1EB0CD9B0072B066 /* BugReportViewController.m */; }; 32FD0A3E1EB0CD9B0072B066 /* BugReportViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 32FD0A3C1EB0CD9B0072B066 /* BugReportViewController.xib */; }; + 714F6391AC0AA86C0AEB3F43 /* libPods-SiriIntents.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5666C1236223F54D4C635C54 /* libPods-SiriIntents.a */; }; + 92324BE31F4F66D3009DE194 /* IncomingCallView.m in Sources */ = {isa = PBXBuildFile; fileRef = 92324BE21F4F66D3009DE194 /* IncomingCallView.m */; }; + 92324BE61F4F6A60009DE194 /* CircleButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 92324BE51F4F6A60009DE194 /* CircleButton.m */; }; + 926FA53F1F4C132000F826C2 /* MXSession+Riot.m in Sources */ = {isa = PBXBuildFile; fileRef = 926FA53E1F4C132000F826C2 /* MXSession+Riot.m */; }; + 92726A471F58737A004AD26F /* IntentHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 92726A461F58737A004AD26F /* IntentHandler.m */; }; + 92726A4B1F58737A004AD26F /* SiriIntents.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 92726A431F58737A004AD26F /* SiriIntents.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 92726A511F587410004AD26F /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 92726A501F587410004AD26F /* Intents.framework */; }; A27ECCE3FC4971745D2CB78D /* libPods-RiotShareExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7246451C668D6782166E22EC /* libPods-RiotShareExtension.a */; }; F0131DE51F2200D600CBF707 /* RiotSplitViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F0131DE41F2200D600CBF707 /* RiotSplitViewController.m */; }; F02C1A861E8EB04C0045A404 /* PeopleViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F02C1A841E8EB04C0045A404 /* PeopleViewController.m */; }; @@ -532,6 +539,13 @@ remoteGlobalIDString = 24CBEC4D1F0EAD310093EABB; remoteInfo = "Riot Share Extension"; }; + 92726A491F58737A004AD26F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F094A99A1B78D8F000B1FBBF /* Project object */; + proxyType = 1; + remoteGlobalIDString = 92726A421F58737A004AD26F; + remoteInfo = SiriIntents; + }; F094A9BF1B78D8F000B1FBBF /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = F094A99A1B78D8F000B1FBBF /* Project object */; @@ -549,6 +563,7 @@ dstSubfolderSpec = 13; files = ( 24CBEC591F0EAD310093EABB /* RiotShareExtension.appex in Embed App Extensions */, + 92726A4B1F58737A004AD26F /* SiriIntents.appex in Embed App Extensions */, ); name = "Embed App Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -643,9 +658,24 @@ 32FD0A3A1EB0CD9B0072B066 /* BugReportViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BugReportViewController.h; sourceTree = ""; }; 32FD0A3B1EB0CD9B0072B066 /* BugReportViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugReportViewController.m; sourceTree = ""; }; 32FD0A3C1EB0CD9B0072B066 /* BugReportViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = BugReportViewController.xib; sourceTree = ""; }; + 397BCA987893439918EBF330 /* Pods-SiriIntents.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SiriIntents.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SiriIntents/Pods-SiriIntents.debug.xcconfig"; sourceTree = ""; }; + 4D1164C2F07EF74950DCDA7A /* Pods-SiriIntents.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SiriIntents.release.xcconfig"; path = "Pods/Target Support Files/Pods-SiriIntents/Pods-SiriIntents.release.xcconfig"; sourceTree = ""; }; + 5666C1236223F54D4C635C54 /* libPods-SiriIntents.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-SiriIntents.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 7246451C668D6782166E22EC /* libPods-RiotShareExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RiotShareExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 765F5104DB3EC39713DEB3A4 /* Pods-RiotShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RiotShareExtension.release.xcconfig"; path = "Pods/Target Support Files/Pods-RiotShareExtension/Pods-RiotShareExtension.release.xcconfig"; sourceTree = ""; }; 839BB91240D350D5607D55BA /* Pods-Riot.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Riot.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Riot/Pods-Riot.debug.xcconfig"; sourceTree = ""; }; + 92324BE11F4F66D3009DE194 /* IncomingCallView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IncomingCallView.h; sourceTree = ""; }; + 92324BE21F4F66D3009DE194 /* IncomingCallView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IncomingCallView.m; sourceTree = ""; }; + 92324BE41F4F6A60009DE194 /* CircleButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CircleButton.h; sourceTree = ""; }; + 92324BE51F4F6A60009DE194 /* CircleButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CircleButton.m; sourceTree = ""; }; + 926FA53D1F4C132000F826C2 /* MXSession+Riot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MXSession+Riot.h"; sourceTree = ""; }; + 926FA53E1F4C132000F826C2 /* MXSession+Riot.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MXSession+Riot.m"; sourceTree = ""; }; + 92726A431F58737A004AD26F /* SiriIntents.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SiriIntents.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 92726A451F58737A004AD26F /* IntentHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IntentHandler.h; sourceTree = ""; }; + 92726A461F58737A004AD26F /* IntentHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IntentHandler.m; sourceTree = ""; }; + 92726A481F58737A004AD26F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 92726A4F1F587393004AD26F /* SiriIntents.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = SiriIntents.entitlements; sourceTree = ""; }; + 92726A501F587410004AD26F /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; }; C195C53961EA28E6900AEB68 /* Pods-Riot.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Riot.release.xcconfig"; path = "Pods/Target Support Files/Pods-Riot/Pods-Riot.release.xcconfig"; sourceTree = ""; }; F0131DE31F2200D600CBF707 /* RiotSplitViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RiotSplitViewController.h; sourceTree = ""; }; F0131DE41F2200D600CBF707 /* RiotSplitViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RiotSplitViewController.m; sourceTree = ""; }; @@ -1231,6 +1261,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 92726A401F58737A004AD26F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 92726A511F587410004AD26F /* Intents.framework in Frameworks */, + 714F6391AC0AA86C0AEB3F43 /* libPods-SiriIntents.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; F094A99F1B78D8F000B1FBBF /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1395,8 +1434,10 @@ 5FC42FA41F5186AFFB6A2404 /* Frameworks */ = { isa = PBXGroup; children = ( + 92726A501F587410004AD26F /* Intents.framework */, FD9D0BDE9232898950554DD5 /* libPods-Riot.a */, 7246451C668D6782166E22EC /* libPods-RiotShareExtension.a */, + 5666C1236223F54D4C635C54 /* libPods-SiriIntents.a */, ); name = Frameworks; sourceTree = ""; @@ -1408,10 +1449,34 @@ C195C53961EA28E6900AEB68 /* Pods-Riot.release.xcconfig */, 12AA0005C8B3D8D8162584C5 /* Pods-RiotShareExtension.debug.xcconfig */, 765F5104DB3EC39713DEB3A4 /* Pods-RiotShareExtension.release.xcconfig */, + 397BCA987893439918EBF330 /* Pods-SiriIntents.debug.xcconfig */, + 4D1164C2F07EF74950DCDA7A /* Pods-SiriIntents.release.xcconfig */, ); name = Pods; sourceTree = ""; }; + 92324BE01F4F668F009DE194 /* Calls */ = { + isa = PBXGroup; + children = ( + 92324BE11F4F66D3009DE194 /* IncomingCallView.h */, + 92324BE21F4F66D3009DE194 /* IncomingCallView.m */, + 92324BE41F4F6A60009DE194 /* CircleButton.h */, + 92324BE51F4F6A60009DE194 /* CircleButton.m */, + ); + path = Calls; + sourceTree = ""; + }; + 92726A441F58737A004AD26F /* SiriIntents */ = { + isa = PBXGroup; + children = ( + 92726A4F1F587393004AD26F /* SiriIntents.entitlements */, + 92726A451F58737A004AD26F /* IntentHandler.h */, + 92726A461F58737A004AD26F /* IntentHandler.m */, + 92726A481F58737A004AD26F /* Info.plist */, + ); + path = SiriIntents; + sourceTree = ""; + }; F083BB021E7005FD00A9B29C /* RiotTests */ = { isa = PBXGroup; children = ( @@ -1736,6 +1801,8 @@ F083BBEA1E7009EC00A9B29C /* UINavigationController+Riot.m */, F083BBEB1E7009EC00A9B29C /* UIViewController+RiotSearch.h */, F083BBEC1E7009EC00A9B29C /* UIViewController+RiotSearch.m */, + 926FA53D1F4C132000F826C2 /* MXSession+Riot.h */, + 926FA53E1F4C132000F826C2 /* MXSession+Riot.m */, ); path = Categories; sourceTree = ""; @@ -1920,6 +1987,7 @@ F083BC571E7009EC00A9B29C /* Views */ = { isa = PBXGroup; children = ( + 92324BE01F4F668F009DE194 /* Calls */, F0B4CBAD1F4215E3008E99C5 /* Event */, F083BC581E7009EC00A9B29C /* Authentication */, F083BC5F1E7009EC00A9B29C /* Contact */, @@ -2270,6 +2338,7 @@ F083BB081E7009EC00A9B29C /* Riot */, F083BB021E7005FD00A9B29C /* RiotTests */, 24CBEC4F1F0EAD310093EABB /* RiotShareExtension */, + 92726A441F58737A004AD26F /* SiriIntents */, F094A9A31B78D8F000B1FBBF /* Products */, 7471DF3720D498384A068DA7 /* Pods */, 5FC42FA41F5186AFFB6A2404 /* Frameworks */, @@ -2282,6 +2351,7 @@ F094A9A21B78D8F000B1FBBF /* Riot.app */, F094A9BE1B78D8F000B1FBBF /* RiotTests.xctest */, 24CBEC4E1F0EAD310093EABB /* RiotShareExtension.appex */, + 92726A431F58737A004AD26F /* SiriIntents.appex */, ); name = Products; sourceTree = ""; @@ -2318,6 +2388,25 @@ productReference = 24CBEC4E1F0EAD310093EABB /* RiotShareExtension.appex */; productType = "com.apple.product-type.app-extension"; }; + 92726A421F58737A004AD26F /* SiriIntents */ = { + isa = PBXNativeTarget; + buildConfigurationList = 92726A4E1F58737A004AD26F /* Build configuration list for PBXNativeTarget "SiriIntents" */; + buildPhases = ( + 6AA0024D4D5FAE30C2E1F311 /* [CP] Check Pods Manifest.lock */, + 92726A3F1F58737A004AD26F /* Sources */, + 92726A401F58737A004AD26F /* Frameworks */, + 92726A411F58737A004AD26F /* Resources */, + 807A0ABF153A23C2FC22F977 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SiriIntents; + productName = SiriIntents; + productReference = 92726A431F58737A004AD26F /* SiriIntents.appex */; + productType = "com.apple.product-type.app-extension"; + }; F094A9A11B78D8F000B1FBBF /* Riot */ = { isa = PBXNativeTarget; buildConfigurationList = F094A9C81B78D8F000B1FBBF /* Build configuration list for PBXNativeTarget "Riot" */; @@ -2335,6 +2424,7 @@ ); dependencies = ( 242661F61F12B1BA00D3FC08 /* PBXTargetDependency */, + 92726A4A1F58737A004AD26F /* PBXTargetDependency */, ); name = Riot; productName = Vector; @@ -2378,6 +2468,16 @@ }; }; }; + 92726A421F58737A004AD26F = { + CreatedOnToolsVersion = 8.3.3; + DevelopmentTeam = 7J4U792NQT; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.ApplicationGroups.iOS = { + enabled = 1; + }; + }; + }; F094A9A11B78D8F000B1FBBF = { CreatedOnToolsVersion = 6.2; DevelopmentTeam = 7J4U792NQT; @@ -2418,6 +2518,7 @@ F094A9A11B78D8F000B1FBBF /* Riot */, F094A9BD1B78D8F000B1FBBF /* RiotTests */, 24CBEC4D1F0EAD310093EABB /* RiotShareExtension */, + 92726A421F58737A004AD26F /* SiriIntents */, ); }; /* End PBXProject section */ @@ -2438,7 +2539,14 @@ 24EEE5AF1F25F0F500B3C705 /* Images.xcassets in Resources */, 24EEE5AA1F25529C00B3C705 /* cancel.png in Resources */, 24D6B35E1F3CA03E00FC7A71 /* FallbackViewController.xib in Resources */, - 24EEE5A41F24C06E00B3C705 /* (null) in Resources */, + 24EEE5A41F24C06E00B3C705 /* BuildFile in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 92726A411F58737A004AD26F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2811,13 +2919,16 @@ files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Riot-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 3051A9ED306E05F8AE91C906 /* [CP] Check Pods Manifest.lock */ = { @@ -2826,13 +2937,16 @@ files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RiotShareExtension-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 68D6013FA64A4507DC9DB95B /* [CP] Copy Pods Resources */ = { @@ -2841,39 +2955,145 @@ files = ( ); inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-Riot/Pods-Riot-resources.sh", + "${PODS_ROOT}/MatrixKit/MatrixKit/Controllers/MXKAccountDetailsViewController.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Controllers/MXKAttachmentsViewController.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Controllers/MXKAuthenticationViewController.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Controllers/MXKCallViewController.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Controllers/MXKContactDetailsViewController.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Controllers/MXKContactListViewController.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Controllers/MXKCountryPickerViewController.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Controllers/MXKLanguagePickerViewController.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Controllers/MXKRecentListViewController.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Controllers/MXKRoomMemberDetailsViewController.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Controllers/MXKRoomMemberListViewController.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Controllers/MXKRoomSettingsViewController.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Controllers/MXKRoomViewController.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Controllers/MXKSearchViewController.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/Account/MXKAccountTableViewCell.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/Authentication/MXKAuthInputsEmailCodeBasedView.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/Authentication/MXKAuthInputsPasswordBasedView.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/Contact/MXKContactTableCell.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/DeviceView/MXKDeviceView.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/EncryptionInfoView/MXKEncryptionInfoView.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/MXKCollectionViewCell/MXKMediaCollectionViewCell.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/MXKEventDetailsView.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/MXKPieChartHUD.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/MXKRoomCreationView.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/MXKTableViewCell/MXKTableViewCellWithButton.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/MXKTableViewCell/MXKTableViewCellWithLabelAndButton.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/MXKTableViewCell/MXKTableViewCellWithLabelAndImageView.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/MXKTableViewCell/MXKTableViewCellWithLabelAndMXKImageView.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/MXKTableViewCell/MXKTableViewCellWithLabelAndSlider.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/MXKTableViewCell/MXKTableViewCellWithLabelAndSubLabel.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/MXKTableViewCell/MXKTableViewCellWithLabelAndSwitch.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/MXKTableViewCell/MXKTableViewCellWithLabelAndTextField.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/MXKTableViewCell/MXKTableViewCellWithLabelTextFieldAndButton.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/MXKTableViewCell/MXKTableViewCellWithPicker.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/MXKTableViewCell/MXKTableViewCellWithSearchBar.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/MXKTableViewCell/MXKTableViewCellWithTextFieldAndButton.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/MXKTableViewCell/MXKTableViewCellWithTextView.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/PushRule/MXKPushRuleCreationTableViewCell.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/PushRule/MXKPushRuleTableViewCell.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/ReadReceipts/MXKReadReceiptTableViewCell.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/RoomBubbleList/MXKRoomEmptyBubbleTableViewCell.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/RoomBubbleList/MXKRoomIncomingAttachmentBubbleCell.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/RoomBubbleList/MXKRoomIncomingAttachmentWithoutSenderInfoBubbleCell.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/RoomBubbleList/MXKRoomIncomingTextMsgBubbleCell.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/RoomBubbleList/MXKRoomIncomingTextMsgWithoutSenderInfoBubbleCell.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/RoomBubbleList/MXKRoomIOSOutgoingBubbleTableViewCell.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/RoomBubbleList/MXKRoomOutgoingAttachmentBubbleCell.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/RoomBubbleList/MXKRoomOutgoingAttachmentWithoutSenderInfoBubbleCell.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/RoomBubbleList/MXKRoomOutgoingTextMsgBubbleCell.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/RoomBubbleList/MXKRoomOutgoingTextMsgWithoutSenderInfoBubbleCell.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarViewWithHPGrowingText.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarViewWithSimpleTextView.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/RoomList/MXKInterleavedRecentTableViewCell.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/RoomList/MXKPublicRoomTableViewCell.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/RoomList/MXKRecentTableViewCell.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/RoomMemberList/MXKRoomMemberTableViewCell.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/RoomTitle/MXKRoomTitleView.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/RoomTitle/MXKRoomTitleViewWithTopic.xib", + "${PODS_ROOT}/MatrixKit/MatrixKit/Views/Search/MXKSearchTableViewCell.xib", + $PODS_CONFIGURATION_BUILD_DIR/MatrixKit/MatrixKit.bundle, + "${PODS_ROOT}/MatrixSDK/MatrixSDK/Data/Store/MXCoreDataStore/MXCoreDataStore.xcdatamodeld", ); name = "[CP] Copy Pods Resources"; outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Riot/Pods-Riot-resources.sh\"\n"; showEnvVarsInLog = 0; }; + 6AA0024D4D5FAE30C2E1F311 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-SiriIntents-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 7FFD40AA75DB32D83350D225 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-Riot/Pods-Riot-frameworks.sh", + "${PODS_ROOT}/WebRTC/WebRTC.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/WebRTC.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Riot/Pods-Riot-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; + 807A0ABF153A23C2FC22F977 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-SiriIntents/Pods-SiriIntents-resources.sh", + "${PODS_ROOT}/MatrixSDK/MatrixSDK/Data/Store/MXCoreDataStore/MXCoreDataStore.xcdatamodeld", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-SiriIntents/Pods-SiriIntents-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 8EA19F5011654D3BD5EDAC33 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-RiotShareExtension/Pods-RiotShareExtension-resources.sh", + "${PODS_ROOT}/MatrixSDK/MatrixSDK/Data/Store/MXCoreDataStore/MXCoreDataStore.xcdatamodeld", ); name = "[CP] Copy Pods Resources"; outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -2902,6 +3122,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 92726A3F1F58737A004AD26F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 92726A471F58737A004AD26F /* IntentHandler.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; F094A99E1B78D8F000B1FBBF /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -2917,6 +3145,7 @@ F083BE9C1E7009ED00A9B29C /* TableViewCellWithCheckBoxAndLabel.m in Sources */, 32471CE11F13AC1500BDF50A /* RoomMembershipExpandedWithPaginationTitleBubbleCell.m in Sources */, F083BDFE1E7009ED00A9B29C /* RecentCellData.m in Sources */, + 92324BE61F4F6A60009DE194 /* CircleButton.m in Sources */, F083BE3A1E7009ED00A9B29C /* RoomIncomingEncryptedAttachmentBubbleCell.m in Sources */, 3205ED841E97725E003D65FA /* DirectoryServerTableViewCell.m in Sources */, F083BEA21E7009ED00A9B29C /* TableViewCellWithPhoneNumberTextField.m in Sources */, @@ -2950,6 +3179,7 @@ F083BE481E7009ED00A9B29C /* RoomIncomingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m in Sources */, F06CDD691EF01E3900870B75 /* RoomEmptyBubbleCell.m in Sources */, F083BE041E7009ED00A9B29C /* Tools.m in Sources */, + 92324BE31F4F66D3009DE194 /* IncomingCallView.m in Sources */, F083BE6A1E7009ED00A9B29C /* RoomOutgoingAttachmentBubbleCell.m in Sources */, F083BDEF1E7009ED00A9B29C /* UINavigationController+Riot.m in Sources */, F083BE761E7009ED00A9B29C /* RoomOutgoingTextMsgWithPaginationTitleBubbleCell.m in Sources */, @@ -3021,6 +3251,7 @@ F083BDF81E7009ED00A9B29C /* RoomDataSource.m in Sources */, F083BE371E7009ED00A9B29C /* RoomActivitiesView.m in Sources */, F083BE131E7009ED00A9B29C /* HomeMessagesSearchViewController.m in Sources */, + 926FA53F1F4C132000F826C2 /* MXSession+Riot.m in Sources */, F083BE8C1E7009ED00A9B29C /* PreviewRoomTitleView.m in Sources */, F083BE271E7009ED00A9B29C /* SettingsViewController.m in Sources */, F083BE9A1E7009ED00A9B29C /* TableViewCellWithButton.m in Sources */, @@ -3064,6 +3295,11 @@ target = 24CBEC4D1F0EAD310093EABB /* RiotShareExtension */; targetProxy = 242661F51F12B1BA00D3FC08 /* PBXContainerItemProxy */; }; + 92726A4A1F58737A004AD26F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 92726A421F58737A004AD26F /* SiriIntents */; + targetProxy = 92726A491F58737A004AD26F /* PBXContainerItemProxy */; + }; F094A9C01B78D8F000B1FBBF /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = F094A9A11B78D8F000B1FBBF /* Riot */; @@ -3219,7 +3455,7 @@ INFOPLIST_FILE = RiotShareExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = im.vector.app.share.extension; + PRODUCT_BUNDLE_IDENTIFIER = im.vector.app.shareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; }; @@ -3242,7 +3478,48 @@ INFOPLIST_FILE = RiotShareExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = im.vector.app.share.extension; + PRODUCT_BUNDLE_IDENTIFIER = im.vector.app.shareExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Release; + }; + 92726A4C1F58737A004AD26F /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 397BCA987893439918EBF330 /* Pods-SiriIntents.debug.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CODE_SIGN_ENTITLEMENTS = SiriIntents/SiriIntents.entitlements; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 7J4U792NQT; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = SiriIntents/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = im.vector.app.SiriIntents; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 92726A4D1F58737A004AD26F /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4D1164C2F07EF74950DCDA7A /* Pods-SiriIntents.release.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CODE_SIGN_ENTITLEMENTS = SiriIntents/SiriIntents.entitlements; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 7J4U792NQT; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = SiriIntents/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = im.vector.app.SiriIntents; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; }; @@ -3428,6 +3705,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 92726A4E1F58737A004AD26F /* Build configuration list for PBXNativeTarget "SiriIntents" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 92726A4C1F58737A004AD26F /* Debug */, + 92726A4D1F58737A004AD26F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; F094A99D1B78D8F000B1FBBF /* Build configuration list for PBXProject "Riot" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Riot/AppDelegate.h b/Riot/AppDelegate.h index 863f96b08..dd33b3579 100644 --- a/Riot/AppDelegate.h +++ b/Riot/AppDelegate.h @@ -116,6 +116,13 @@ extern NSString *const kAppDelegateNetworkStatusDidChangeNotification; - (void)registerUserNotificationSettings; +/** + Perform registration for remote notifications. + + @param completion the block to be executed when registration finished. + */ +- (void)registerForRemoteNotificationsWithCompletion:(void (^)(NSError *))completion; + #pragma mark - Matrix Room handling - (void)showRoom:(NSString*)roomId andEventId:(NSString*)eventId withMatrixSession:(MXSession*)mxSession; diff --git a/Riot/AppDelegate.m b/Riot/AppDelegate.m index 37989a831..e0a2b2e14 100644 --- a/Riot/AppDelegate.m +++ b/Riot/AppDelegate.m @@ -17,6 +17,9 @@ #import "AppDelegate.h" +#import +#import + #import "RecentsDataSource.h" #import "RoomDataSource.h" @@ -41,8 +44,16 @@ #import +#include + +// Calls #import "CallViewController.h" +#import +#import + +#import "MXSession+Riot.h" + //#define MX_CALL_STACK_OPENWEBRTC #ifdef MX_CALL_STACK_OPENWEBRTC #import @@ -52,9 +63,10 @@ #import #endif -#include - -#include +#ifdef MX_CALL_STACK_JINGLE +#import +#import +#endif #define CALL_STATUS_BAR_HEIGHT 44 @@ -64,7 +76,7 @@ NSString *const kAppDelegateDidTapStatusBarNotification = @"kAppDelegateDidTapStatusBarNotification"; NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateNetworkStatusDidChangeNotification"; -@interface AppDelegate () +@interface AppDelegate () { /** Reachability observer @@ -107,11 +119,6 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN */ NSMutableArray *mxSessionArray; - /** - The room id of the current handled remote notification (if any) - */ - NSString *remoteNotificationRoomId; - /** The fragment of the universal link being processing. Only one fragment is handled at a time. @@ -168,6 +175,18 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN @property (strong, nonatomic) UIAlertController *mxInAppNotification; @property (strong, nonatomic) UIAlertController *incomingCallNotification; +@property (nonatomic, nullable, copy) void (^registrationForRemoteNotificationsCompletion)(NSError *); + + +@property (nonatomic, strong) PKPushRegistry *pushRegistry; + +@property (nonatomic, getter=isHandlingPushNotification) BOOL handlingPushNotification; +@property (nonatomic) NSUInteger pushNotificationHandlingTaskIdentifier; + +@property (nonatomic, nullable) MXOnNotification notificationListenerBlock; +@property (nonatomic, nullable) dispatch_block_t pushNotificationHandlingCompletionBlock; +@property (nonatomic, nullable) dispatch_block_t pushNotificationHandlingTimeoutBlock; + @end @implementation AppDelegate @@ -443,10 +462,6 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. - // cancel any background sync before resuming - // i.e. warn IOS that there is no new data with any received push. - [self cancelBackgroundSync]; - // Open account session(s) if this is not already done (see [initMatrixSessions] in case of background launch). [[MXKAccountManager sharedManager] prepareSessionForActiveAccounts]; @@ -460,8 +475,6 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN { NSLog(@"[AppDelegate] applicationDidBecomeActive"); - remoteNotificationRoomId = nil; - // Check if there is crash log to send if ([[NSUserDefaults standardUserDefaults] boolForKey:@"enableCrashReport"]) { @@ -536,6 +549,67 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN { continueUserActivity = [self handleUniversalLink:userActivity]; } + else if ([userActivity.activityType isEqualToString:INStartAudioCallIntentIdentifier] || + [userActivity.activityType isEqualToString:INStartVideoCallIntentIdentifier]) + { + INInteraction *interaction = userActivity.interaction; + + // roomID provided by Siri intent + NSString *roomID = userActivity.userInfo[@"roomID"]; + + // We've launched from calls history list + if (!roomID) + { + INPerson *person; + + if ([interaction.intent isKindOfClass:INStartAudioCallIntent.class]) + { + person = [[(INStartAudioCallIntent *)(interaction.intent) contacts] firstObject]; + } + else if ([interaction.intent isKindOfClass:INStartVideoCallIntent.class]) + { + person = [[(INStartVideoCallIntent *)(interaction.intent) contacts] firstObject]; + } + + roomID = person.personHandle.value; + } + + BOOL isVideoCall = [userActivity.activityType isEqualToString:INStartVideoCallIntentIdentifier]; + + UIApplication *application = UIApplication.sharedApplication; + NSNumber *backgroundTaskIdentifier; + + // Start background task since we need time for MXSession preparasion because our app can be launched in the background + if (application.applicationState == UIApplicationStateBackground) + backgroundTaskIdentifier = @([application beginBackgroundTaskWithExpirationHandler:^{}]); + + MXSession *session = mxSessionArray.firstObject; + [session.callManager placeCallInRoom:roomID + withVideo:isVideoCall + success:^(MXCall *call) { + if (application.applicationState == UIApplicationStateBackground) + { + __weak NSNotificationCenter *center = NSNotificationCenter.defaultCenter; + __block id token = + [center addObserverForName:kMXCallStateDidChange + object:call + queue:nil + usingBlock:^(NSNotification * _Nonnull note) { + if (call.state == MXCallStateEnded) + { + [application endBackgroundTask:backgroundTaskIdentifier.unsignedIntegerValue]; + [center removeObserver:token]; + } + }]; + } + } + failure:^(NSError *error) { + if (backgroundTaskIdentifier) + [application endBackgroundTask:backgroundTaskIdentifier.unsignedIntegerValue]; + }]; + + continueUserActivity = YES; + } return continueUserActivity; } @@ -563,13 +637,7 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN // Enable error notifications isErrorNotificationSuspended = NO; - // Restore call alert if any - if (_incomingCallNotification) - { - NSLog(@"[AppDelegate] restoreInitialDisplay: keep visible incoming call alert"); - [self showNotificationAlert:_incomingCallNotification]; - } - else if (noCallSupportAlert) + if (noCallSupportAlert) { NSLog(@"[AppDelegate] restoreInitialDisplay: keep visible noCall support alert"); [self showNotificationAlert:noCallSupportAlert]; @@ -897,51 +965,25 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN } } +- (void)registerForRemoteNotificationsWithCompletion:(nullable void (^)(NSError *))completion +{ + self.registrationForRemoteNotificationsCompletion = completion; + [[UIApplication sharedApplication] registerForRemoteNotifications]; +} + - (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings { - // Register for remote notifications only if user provide access to notification feature - if (notificationSettings.types != UIUserNotificationTypeNone) - { - [application registerForRemoteNotifications]; - } -} - -- (void)application:(UIApplication*)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken -{ - NSUInteger len = ((deviceToken.length > 8) ? 8 : deviceToken.length / 2); - NSLog(@"[AppDelegate] Got APNS token! (%@ ...)", [deviceToken subdataWithRange:NSMakeRange(0, len)]); + if (notificationSettings.types == UIUserNotificationTypeNone) + return; - MXKAccountManager* accountManager = [MXKAccountManager sharedManager]; - [accountManager setApnsDeviceToken:deviceToken]; - - isAPNSRegistered = YES; + self.pushRegistry = [[PKPushRegistry alloc] initWithQueue:nil]; + self.pushRegistry.delegate = self; + self.pushRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP]; } -- (void)application:(UIApplication*)app didFailToRegisterForRemoteNotificationsWithError:(NSError*)error +- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification { - NSLog(@"[AppDelegate] Failed to register for APNS: %@", error); -} - -- (void)cancelBackgroundSync -{ - if (_completionHandler) - { - _completionHandler(UIBackgroundFetchResultNoData); - _completionHandler = nil; - } -} - -- (void)application:(UIApplication*)application didReceiveRemoteNotification:(NSDictionary*)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler -{ -#ifdef DEBUG - // log the full userInfo only in DEBUG - NSLog(@"[AppDelegate] didReceiveRemoteNotification: %@", userInfo); -#else - NSLog(@"[AppDelegate] didReceiveRemoteNotification"); -#endif - - // Look for the room id - NSString* roomId = [userInfo objectForKey:@"room_id"]; + NSString* roomId = notification.userInfo[@"room_id"]; if (roomId.length) { // TODO retrieve the right matrix session @@ -971,71 +1013,266 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN // sanity checks if (dedicatedAccount && dedicatedAccount.mxSession) { - UIApplicationState state = [UIApplication sharedApplication].applicationState; + NSLog(@"[AppDelegate] didReceiveLocalNotification: open the roomViewController %@", roomId); - // Jump to the concerned room only if the app is transitioning from the background - if (state == UIApplicationStateInactive) - { - // Check whether another remote notification is not already processed - if (!remoteNotificationRoomId) - { - remoteNotificationRoomId = roomId; - - NSLog(@"[AppDelegate] didReceiveRemoteNotification: open the roomViewController %@", roomId); - - [self showRoom:roomId andEventId:nil withMatrixSession:dedicatedAccount.mxSession]; - } - else - { - NSLog(@"[AppDelegate] didReceiveRemoteNotification: busy"); - } - } - else if (!_completionHandler && (state == UIApplicationStateBackground)) - { - _completionHandler = completionHandler; - - NSLog(@"[AppDelegate] didReceiveRemoteNotification: starts a background sync"); - - [dedicatedAccount backgroundSync:20000 success:^{ - NSLog(@"[AppDelegate] didReceiveRemoteNotification: the background sync succeeds"); - - if (_completionHandler) - { - _completionHandler(UIBackgroundFetchResultNewData); - _completionHandler = nil; - } - } failure:^(NSError *error) { - NSLog(@"[AppDelegate] didReceiveRemoteNotification: the background sync fails"); - - if (_completionHandler) - { - _completionHandler(UIBackgroundFetchResultNoData); - _completionHandler = nil; - } - }]; - - // wait that the background sync is done - return; - } + [self showRoom:roomId andEventId:nil withMatrixSession:dedicatedAccount.mxSession]; } else { - NSLog(@"[AppDelegate] didReceiveRemoteNotification : no linked session / account has been found."); + NSLog(@"[AppDelegate] didReceiveLocalNotification : no linked session / account has been found."); } } - completionHandler(UIBackgroundFetchResultNoData); } -- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo +- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(PKPushType)type { - // iOS 10 (at least up to GM beta release) does not call application:didReceiveRemoteNotification:fetchCompletionHandler: - // when the user clicks on a notification but it calls this deprecated version - // of didReceiveRemoteNotification. - // Use this method as a workaround as adviced at http://stackoverflow.com/a/39419245 - NSLog(@"[AppDelegate] didReceiveRemoteNotification (deprecated version)"); + NSData *token = credentials.token; - [self application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:^(UIBackgroundFetchResult result) { + NSUInteger len = ((token.length > 8) ? 8 : token.length / 2); + NSLog(@"[AppDelegate] Got APNS token! (%@ ...)", [token subdataWithRange:NSMakeRange(0, len)]); + + MXKAccountManager* accountManager = [MXKAccountManager sharedManager]; + [accountManager setApnsDeviceToken:token]; + + isAPNSRegistered = YES; +} + +- (void)pushRegistry:(PKPushRegistry *)registry didInvalidatePushTokenForType:(PKPushType)type +{ + MXKAccountManager* accountManager = [MXKAccountManager sharedManager]; + [accountManager setApnsDeviceToken:nil]; +} + +- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type +{ + // If it isn't a first push + if (self.isHandlingPushNotification) + { + // If we have processed push notification and we're waiting to complete after a timeout + if (self.pushNotificationHandlingCompletionBlock) + { + // Cancel completion of previous push notification. + // The new completion block will be created when this push notification will be processed if timeout block wouldn't be called + dispatch_block_cancel(self.pushNotificationHandlingCompletionBlock); + self.pushNotificationHandlingCompletionBlock = nil; + } + + // On every new push cancel timeout block if any. + // This situation is possible when we sequentially receive a series of push notifications in a short amount of time. + if (self.pushNotificationHandlingTimeoutBlock) + { + dispatch_block_cancel(self.pushNotificationHandlingTimeoutBlock); + self.pushNotificationHandlingTimeoutBlock = nil; + } + + // Create and run timeout block since we don't know how much time will take a new sync request + // There is also a case when we receive push event earlier than push and we need to stop execution somehow + // so timeoutBlock will help us in this situation + dispatch_block_t timeoutBlock = [self createPushNotificationHandlingStateCleaningBlock]; + self.pushNotificationHandlingTimeoutBlock = timeoutBlock; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(120 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + timeoutBlock(); + }); + + return; + } + + UIApplication *application = [UIApplication sharedApplication]; + + if (application.applicationState == UIApplicationStateActive || application.applicationState == UIApplicationStateInactive) + return; + + MXSession *session = mxSessionArray.firstObject; + + if (session.state == MXSessionStateHomeserverNotReachable) + return; + + if (session.state == MXSessionStatePaused) + [session resume:^{}]; + + self.handlingPushNotification = YES; + + id handler = [MXSDKOptions sharedInstance].backgroundModeHandler; + self.pushNotificationHandlingTaskIdentifier = [handler startBackgroundTaskWithName:nil completion:^{ + // Maybe do smth here... }]; + + // Listen events from MXNotificationCenter + __block MXOnNotification notificationsListenerBlock = nil; + + // This variable will be point to notificationsListenerBlock but won't retain it. + // It's very important since notificationsListenerBlock is required in two another blocks which also + // are being referenced inside notificationsListenerBlock's body. + // So this weak variable allows us to avoid retain cycle. + __weak __block MXOnNotification weakNotificationsListenerBlock = nil; + + // The block which is called when a sync with server takes a lot of time + dispatch_block_t timeoutBlock = [self createPushNotificationHandlingStateCleaningBlock]; + self.pushNotificationHandlingTimeoutBlock = timeoutBlock; + + __weak typeof(self) weakSelf = self; + + notificationsListenerBlock = ^(MXEvent *event, MXRoomState *roomState, MXPushRule *rule) { + if (weakSelf.pushNotificationHandlingTimeoutBlock) + { + dispatch_block_cancel(weakSelf.pushNotificationHandlingTimeoutBlock); + weakSelf.pushNotificationHandlingTimeoutBlock = nil; + } + + if (weakSelf.pushNotificationHandlingCompletionBlock) + { + dispatch_block_cancel(weakSelf.pushNotificationHandlingCompletionBlock); + weakSelf.pushNotificationHandlingCompletionBlock = nil; + } + + // For all type of event show local notifications besides the situation + // when the type of event is call invite and we have CallKit support + BOOL isCallKitActive = [MXCallKitAdapter callKitAvailable] && [MXKAppSettings standardAppSettings].isCallKitEnabled; + if (!(event.eventType == MXEventTypeCallInvite && isCallKitActive)) + { + BOOL isEventRoomDirect = [session roomWithRoomId:event.roomId].isDirect; + NSString *notificationBody = [weakSelf notificationBodyForEvent:event inDirectRoom:isEventRoomDirect withRoomState:roomState]; + if (notificationBody) + { + UILocalNotification *eventNotification = [[UILocalNotification alloc] init]; + eventNotification.fireDate = [NSDate date]; + eventNotification.alertBody = notificationBody; + eventNotification.userInfo = @{ @"room_id" : event.roomId }; + + // Set sound name based on the value provided in action of MXPushRule + for (MXPushRuleAction *action in rule.actions) + { + if (action.actionType == MXPushRuleActionTypeSetTweak) + { + if ([action.parameters[@"set_tweak"] isEqualToString:@"sound"]) + { + NSString *soundName = action.parameters[@"value"]; + if ([soundName isEqualToString:@"default"]) + soundName = UILocalNotificationDefaultSoundName; + + eventNotification.soundName = soundName; + } + } + } + + [[UIApplication sharedApplication] scheduleLocalNotification:eventNotification]; + } + } + + dispatch_block_t completionBlock = [weakSelf createPushNotificationHandlingStateCleaningBlock]; + weakSelf.pushNotificationHandlingCompletionBlock = completionBlock; + + // This delay will help us to process push events which we receive earlier than push notifications associated with them + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + completionBlock(); + }); + }; + + weakNotificationsListenerBlock = notificationsListenerBlock; + self.notificationListenerBlock = notificationsListenerBlock; + + [session.notificationCenter listenToNotifications:notificationsListenerBlock]; + + // Run timeout block + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(120.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + timeoutBlock(); + }); +} + +/** + Create and return a block which clean all state associated wiht push notifications handling + */ +- (dispatch_block_t)createPushNotificationHandlingStateCleaningBlock +{ + __weak typeof(self) weakSelf = self; + MXSession *session = mxSessionArray.firstObject; + + return dispatch_block_create(0, ^{ + [session.notificationCenter removeListener:weakSelf.notificationListenerBlock]; + + // Update icon badge number + [UIApplication sharedApplication].applicationIconBadgeNumber = [session riot_missedDiscussionsCount]; + + id handler = [MXSDKOptions sharedInstance].backgroundModeHandler; + + NSUInteger identifier = weakSelf.pushNotificationHandlingTaskIdentifier; + + weakSelf.handlingPushNotification = NO; + weakSelf.pushNotificationHandlingTaskIdentifier = [handler invalidIdentifier]; + + weakSelf.notificationListenerBlock = nil; + weakSelf.pushNotificationHandlingTimeoutBlock = nil; + weakSelf.pushNotificationHandlingCompletionBlock = nil; + + [handler endBackgrounTaskWithIdentifier:identifier]; + }); +} + +- (nullable NSString *)notificationBodyForEvent:(MXEvent *)event inDirectRoom:(BOOL)isDirect withRoomState:(MXRoomState *)roomState +{ + if (!event.content || !event.content.count) + return nil; + + NSString *notificationBody; + NSString *eventSenderName = [roomState memberName:event.sender]; + + if (event.eventType == MXEventTypeRoomMessage || event.eventType == MXEventTypeRoomEncrypted) + { + NSString *msgType = event.content[@"msgtype"]; + NSString *content = event.content[@"body"]; + + if (!isDirect) + { + NSString *roomDisplayName = roomState.displayname; + + if ([msgType isEqualToString:@"m.text"]) + notificationBody = [NSString stringWithFormat:NSLocalizedString(@"MSG_FROM_USER_IN_ROOM_WITH_CONTENT", nil), eventSenderName,roomDisplayName, content]; + else if ([msgType isEqualToString:@"m.emote"]) + notificationBody = [NSString stringWithFormat:NSLocalizedString(@"ACTION_FROM_USER_IN_ROOM", nil), roomDisplayName, eventSenderName, content]; + else if ([msgType isEqualToString:@"m.image"]) + notificationBody = [NSString stringWithFormat:NSLocalizedString(@"IMAGE_FROM_USER_IN_ROOM", nil), eventSenderName, content, roomDisplayName]; + else + // Unencrypted messages falls here + notificationBody = [NSString stringWithFormat:NSLocalizedString(@"MSG_FROM_USER_IN_ROOM", nil), eventSenderName, roomDisplayName]; + } + else + { + if ([msgType isEqualToString:@"m.text"]) + notificationBody = [NSString stringWithFormat:NSLocalizedString(@"MSG_FROM_USER_WITH_CONTENT", nil), eventSenderName, content]; + else if ([msgType isEqualToString:@"m.emote"]) + notificationBody = [NSString stringWithFormat:NSLocalizedString(@"ACTION_FROM_USER", nil), eventSenderName, content]; + else if ([msgType isEqualToString:@"m.image"]) + notificationBody = [NSString stringWithFormat:NSLocalizedString(@"IMAGE_FROM_USER", nil), eventSenderName, content]; + else + // Unencrypted messages falls here + notificationBody = [NSString stringWithFormat:NSLocalizedString(@"MSG_FROM_USER", nil), eventSenderName]; + } + } + else if (event.eventType == MXEventTypeCallInvite) + { + NSString *sdp = event.content[@"offer"][@"sdp"]; + BOOL isVideoCall = [sdp rangeOfString:@"m=video"].location != NSNotFound; + + if (!isVideoCall) + notificationBody = [NSString stringWithFormat:NSLocalizedString(@"VOICE_CALL_FROM_USER", nil), eventSenderName]; + else + notificationBody = [NSString stringWithFormat:NSLocalizedString(@"VIDEO_CALL_FROM_USER", nil), eventSenderName]; + } + else if (event.eventType == MXEventTypeRoomMember) + { + NSString *roomName = roomState.name; + NSString *roomAlias = roomState.aliases.firstObject; + + if (roomName) + notificationBody = [NSString stringWithFormat:NSLocalizedString(@"USER_INVITE_TO_NAMED_ROOM", nil), eventSenderName, roomName]; + else if (roomAlias) + notificationBody = [NSString stringWithFormat:NSLocalizedString(@"USER_INVITE_TO_NAMED_ROOM", nil), eventSenderName, roomAlias]; + else + notificationBody = [NSString stringWithFormat:NSLocalizedString(@"USER_INVITE_TO_CHAT", nil), eventSenderName]; + } + + return notificationBody; } - (void)refreshApplicationIconBadgeNumber @@ -1446,6 +1683,9 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN MXSDKOptions *sdkOptions = [MXSDKOptions sharedInstance]; + // Set the App Group identifier. + sdkOptions.applicationGroupIdentifier = @"group.im.vector"; + // Define the media cache version sdkOptions.mediaCacheAppVersion = 0; @@ -1460,6 +1700,12 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN // Use UIKit BackgroundTask for handling background tasks in the SDK sdkOptions.backgroundModeHandler = [[MXUIKitBackgroundModeHandler alloc] init]; + + // Get modular widget events in rooms histories + [[MXKAppSettings standardAppSettings] addSupportedEventTypes:@[kWidgetEventTypeString]]; + + // Use shared container to share data with app extensions + sdkOptions.applicationGroupIdentifier = @"group.im.vector"; // Disable long press on event in bubble cells [MXKRoomBubbleTableViewCell disableLongPressGestureOnEvent:YES]; @@ -1499,6 +1745,19 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN if (callStack) { [mxSession enableVoIPWithCallStack:callStack]; + + // Setup CallKit + if ([MXCallKitAdapter callKitAvailable]) + { + BOOL isCallKitEnabled = [MXKAppSettings standardAppSettings].isCallKitEnabled; + [self enableCallKit:isCallKitEnabled forCallManager:mxSession.callManager]; + + // Register for changes performed by the user + [[MXKAppSettings standardAppSettings] addObserver:self + forKeyPath:@"enableCallKit" + options:NSKeyValueObservingOptionNew + context:NULL]; + } } else { @@ -1508,6 +1767,11 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN // Each room member will be considered as a potential contact. [MXKContactManager sharedManager].contactManagerMXRoomSource = MXKContactManagerMXRoomSourceAll; + + // Send read receipts for modular widgets events too + NSMutableArray *acknowledgableEventTypes = [NSMutableArray arrayWithArray:mxSession.acknowledgableEventTypes]; + [acknowledgableEventTypes addObject:kWidgetEventTypeString]; + mxSession.acknowledgableEventTypes = acknowledgableEventTypes; } else if (mxSession.state == MXSessionStateStoreDataReady) { @@ -1621,8 +1885,7 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN }]; - // Apply the application group name, and add observer on settings changes. - [MXKAppSettings standardAppSettings].applicationGroup = @"group.im.vector"; + // Add observer on settings changes. [[MXKAppSettings standardAppSettings] addObserver:self forKeyPath:@"showAllEventsInRoomHistory" options:0 context:nil]; // Prepare account manager @@ -1751,7 +2014,7 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN - (void)logout { - [[UIApplication sharedApplication] unregisterForRemoteNotifications]; + self.pushRegistry = nil; isAPNSRegistered = NO; // Clear cache @@ -1791,6 +2054,12 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN { [self enableInAppNotificationsForAccount:(MXKAccount*)object]; } + else if (object == [MXKAppSettings standardAppSettings] && [keyPath isEqualToString:@"enableCallKit"]) + { + BOOL isCallKitEnabled = [MXKAppSettings standardAppSettings].isCallKitEnabled; + MXCallManager *callManager = [[[[[MXKAccountManager sharedManager] activeAccounts] firstObject] mxSession] callManager]; + [self enableCallKit:isCallKitEnabled forCallManager:callManager]; + } } - (void)addMatrixCallObserver @@ -1801,18 +2070,55 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN } // Register call observer in order to handle new opened session - matrixCallObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXCallManagerNewCall object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - + matrixCallObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXCallManagerNewCall + object:nil + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification *notif) + { // Ignore the call if a call is already in progress if (!currentCallViewController && !_jitsiViewController) { MXCall *mxCall = (MXCall*)notif.object; - // Prepare the call view controller - currentCallViewController = [CallViewController callViewController:mxCall]; - currentCallViewController.delegate = self; + BOOL isCallKitAvailable = [MXCallKitAdapter callKitAvailable] && [MXKAppSettings standardAppSettings].isCallKitEnabled; - if (mxCall.isIncoming) + // Prepare the call view controller + currentCallViewController = [CallViewController callViewController:nil]; + currentCallViewController.playRingtone = !isCallKitAvailable; + currentCallViewController.mxCall = mxCall; + currentCallViewController.delegate = self; + + UIApplicationState applicationState = UIApplication.sharedApplication.applicationState; + + // App has been woken by PushKit notification in the background + if (applicationState == UIApplicationStateBackground && mxCall.isIncoming) + { + // Create backgound task. + // Without CallKit this will allow us to play vibro until the call was ended + // With CallKit we'll inform the system when the call is ended to let the system terminate our app to save resources + id handler = [MXSDKOptions sharedInstance].backgroundModeHandler; + NSUInteger callTaskIdentifier = [handler startBackgroundTaskWithName:nil completion:^{}]; + + // Start listening for call state change notifications + __weak NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + __block id token = [[NSNotificationCenter defaultCenter] addObserverForName:kMXCallStateDidChange + object:mxCall + queue:nil + usingBlock:^(NSNotification * _Nonnull note) { + MXCall *call = (MXCall *)note.object; + + if (call.state == MXCallStateEnded) + { + // Set call vc to nil to let our app handle new incoming calls even it wasn't killed by the system + currentCallViewController = nil; + [notificationCenter removeObserver:token]; + + [handler endBackgrounTaskWithIdentifier:callTaskIdentifier]; + } + }]; + } + + if (mxCall.isIncoming && !isCallKitAvailable) { // Prompt user before presenting the call view controller NSString *callPromptFormat = mxCall.isVideoCall ? NSLocalizedStringFromTable(@"call_incoming_video_prompt", @"Vector", nil) : NSLocalizedStringFromTable(@"call_incoming_voice_prompt", @"Vector", nil); @@ -1823,16 +2129,14 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN } NSString *callPrompt = [NSString stringWithFormat:callPromptFormat, callerName]; - __weak typeof(self) weakSelf = self; - // Removing existing notification (if any) [_incomingCallNotification dismissViewControllerAnimated:NO completion:nil]; - - _incomingCallNotification = [UIAlertController alertControllerWithTitle:callPrompt message:nil preferredStyle:UIAlertControllerStyleAlert]; + + __weak typeof(self) weakSelf = self; [_incomingCallNotification addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"decline", @"Vector", nil) style:UIAlertActionStyleDefault @@ -1886,7 +2190,6 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN [self presentCallViewController:nil]; } } - }]; } @@ -2046,6 +2349,29 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN } } +- (void)enableCallKit:(BOOL)enable forCallManager:(MXCallManager *)callManager +{ + if (enable) + { + // Create adapter with default configuration for a while + MXCallKitAdapter *callKitAdapter = [[MXCallKitAdapter alloc] init]; + + id audioSessionConfigurator; + +#ifdef MX_CALL_STACK_JINGLE + audioSessionConfigurator = [[MXJingleCallAudioSessionConfigurator alloc] init]; +#endif + + callKitAdapter.audioSessionConfigurator = audioSessionConfigurator; + + callManager.callKitAdapter = callKitAdapter; + } + else + { + callManager.callKitAdapter = nil; + } +} + #pragma mark - /** @@ -2441,6 +2767,7 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN // Release properly [currentCallViewController destroy]; + currentCallViewController = nil; if (completion) { diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 33f2aece5..4d6cc4b1e 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -298,6 +298,7 @@ "settings_user_settings" = "USER SETTINGS"; "settings_notifications_settings" = "NOTIFICATION SETTINGS"; +"settings_calls_settings" = "CALLS"; "settings_user_interface" = "USER INTERFACE"; "settings_ignored_users" = "IGNORED USERS"; "settings_contacts" = "LOCAL CONTACTS"; @@ -340,6 +341,8 @@ //"settings_join_leave_rooms" = "When people join or leave rooms"; //"settings_call_invitations" = "Call invitations"; +"settings_enable_callkit" = "Integrated calling"; +"settings_callkit_info" = "Receive incoming calls on your lock screen. See your Riot calls in the system's call history. If iCloud is enabled, this call history will be shared with Apple."; "settings_ui_language" = "Language"; "settings_ui_light_theme" = "Light theme"; "settings_ui_dark_theme" = "Dark theme"; @@ -454,6 +457,8 @@ // Events formatter "event_formatter_member_updates" = "%tu membership changes"; +"event_formatter_jitsi_widget_added" = "VoIP conference added by %@"; +"event_formatter_jitsi_widget_removed" = "VoIP conference removed by %@"; // Others "or" = "or"; @@ -477,6 +482,8 @@ // Call "call_incoming_voice_prompt" = "Incoming voice call from %@"; "call_incoming_video_prompt" = "Incoming video call from %@"; +"call_incoming_voice" = "Incoming call..."; +"call_incoming_video" = "Incoming video call..."; "call_already_displayed" = "There is already a call in progress."; "call_jitsi_error" = "Failed to join the conference call."; diff --git a/Riot/Categories/MXSession+Riot.h b/Riot/Categories/MXSession+Riot.h new file mode 100644 index 000000000..9bc296a98 --- /dev/null +++ b/Riot/Categories/MXSession+Riot.h @@ -0,0 +1,28 @@ +/* + Copyright 2017 Vector Creations 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 + +#import + +@interface MXSession (Riot) + +/** + The current number of rooms with missed notifications, including the invites. + */ +- (NSUInteger)riot_missedDiscussionsCount; + +@end diff --git a/Riot/Categories/MXSession+Riot.m b/Riot/Categories/MXSession+Riot.m new file mode 100644 index 000000000..3d8805b5e --- /dev/null +++ b/Riot/Categories/MXSession+Riot.m @@ -0,0 +1,51 @@ +/* + Copyright 2017 Vector Creations 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 "MXSession+Riot.h" + +#import "MXRoom+Riot.h" + +@implementation MXSession (Riot) + +- (NSUInteger)riot_missedDiscussionsCount +{ + NSUInteger missedDiscussionsCount = 0; + + // Sum all the rooms with missed notifications. + for (MXRoomSummary *roomSummary in self.roomsSummaries) + { + NSUInteger notificationCount = roomSummary.notificationCount; + + // Ignore the regular notification count if the room is in 'mentions only" mode at the Riot level. + if (roomSummary.room.isMentionsOnly) + { + // Only the highlighted missed messages must be considered here. + notificationCount = roomSummary.highlightCount; + } + + if (notificationCount) + { + missedDiscussionsCount++; + } + } + + // Add the invites count + missedDiscussionsCount += [self invitedRooms].count; + + return missedDiscussionsCount; +} + +@end diff --git a/Riot/Categories/UIViewController+RiotSearch.m b/Riot/Categories/UIViewController+RiotSearch.m index 7f53a9556..9db702f76 100644 --- a/Riot/Categories/UIViewController+RiotSearch.m +++ b/Riot/Categories/UIViewController+RiotSearch.m @@ -19,6 +19,8 @@ #import +#import "RiotDesignValues.h" + /** `UIViewControllerRiotSearchInternals` is the internal single point storage for the search feature. @@ -91,6 +93,10 @@ // Reset searches self.searchBar.text = @""; + // Customize search bar + self.searchBar.barStyle = kRiotDesignSearchBarStyle; + self.searchBar.tintColor = kRiotDesignSearchBarTintColor; + // Remove navigation buttons self.navigationItem.hidesBackButton = YES; self.navigationItem.rightBarButtonItem = nil; diff --git a/Riot/Info.plist b/Riot/Info.plist index 67e088a49..75db3fd4b 100644 --- a/Riot/Info.plist +++ b/Riot/Info.plist @@ -17,11 +17,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.5.2 + 0.5.3 CFBundleSignature ???? CFBundleVersion - 0.5.2 + 0.5.3 ITSAppUsesNonExemptEncryption ITSEncryptionExportComplianceCode @@ -49,6 +49,8 @@ The microphone is used to take videos, make calls. NSPhotoLibraryUsageDescription The photo library is used to send photos and videos. + NSSiriUsageDescription + Siri is used to perform calls even from the lock screen. UIBackgroundModes audio diff --git a/Riot/Riot.entitlements b/Riot/Riot.entitlements index bb2281e16..edee34b65 100644 --- a/Riot/Riot.entitlements +++ b/Riot/Riot.entitlements @@ -11,6 +11,8 @@ applinks:riot.im applinks:www.riot.im + com.apple.developer.siri + com.apple.security.application-groups group.im.vector diff --git a/Riot/Utils/EventFormatter.m b/Riot/Utils/EventFormatter.m index 91370a3be..6fb500e2d 100644 --- a/Riot/Utils/EventFormatter.m +++ b/Riot/Utils/EventFormatter.m @@ -19,6 +19,8 @@ #import "RiotDesignValues.h" +#import "WidgetManager.h" + @interface EventFormatter () { /** @@ -35,6 +37,53 @@ @implementation EventFormatter +- (NSAttributedString *)attributedStringFromEvent:(MXEvent *)event withRoomState:(MXRoomState *)roomState error:(MXKEventFormatterError *)error +{ + // Build strings for modular widget events + // TODO: At the moment, we support only jitsi widgets + if (event.eventType == MXEventTypeCustom + && [event.type isEqualToString:kWidgetEventTypeString]) + { + NSString *displayText; + + // Prepare the display name of the sender + NSString *senderDisplayName = roomState ? [self senderDisplayNameForEvent:event withRoomState:roomState] : event.sender; + + if ([event.content[@"type"] isEqualToString:kWidgetTypeJitsi]) + { + // This is an alive jitsi widget + displayText = [NSString stringWithFormat:NSLocalizedStringFromTable(@"event_formatter_jitsi_widget_added", @"Vector", nil), senderDisplayName]; + } + else if (event.content.count == 0) + { + // This is a closed widget + // Check if it corresponds to a jitsi widget by looking at other state events for + // this jitsi widget (widget id = event.stateKey). + for (MXEvent *widgetStateEvent in [roomState stateEventsWithType:kWidgetEventTypeString]) + { + if ([widgetStateEvent.stateKey isEqualToString:event.stateKey] && [widgetStateEvent.content[@"type"] isEqualToString:kWidgetTypeJitsi]) + { + displayText = [NSString stringWithFormat:NSLocalizedStringFromTable(@"event_formatter_jitsi_widget_removed", @"Vector", nil), senderDisplayName]; + break; + } + } + } + + if (displayText) + { + if (error) + { + *error = MXKEventFormatterErrorNone; + } + + // Build the attributed string with the right font and color for the events + return [self renderString:displayText forEvent:event]; + } + } + + return [super attributedStringFromEvent:event withRoomState:roomState error:error]; +} + - (NSAttributedString*)attributedStringFromEvents:(NSArray*)events withRoomState:(MXRoomState*)roomState error:(MXKEventFormatterError*)error { NSString *displayText; @@ -79,7 +128,7 @@ white-space: pre; \ -coretext-fontname: Menlo-Regular; \ font-size: small; \ - }", bgColor]; + }", (unsigned long)bgColor]; self.defaultTextColor = kRiotPrimaryTextColor; self.subTitleTextColor = kRiotSecondaryTextColor; diff --git a/Riot/Utils/Widgets/Widget.h b/Riot/Utils/Widgets/Widget.h index c645e7e7a..9cc1e89ec 100644 --- a/Riot/Utils/Widgets/Widget.h +++ b/Riot/Utils/Widgets/Widget.h @@ -19,7 +19,7 @@ #import /** - The `Widget` class represents scalar widget information. + The `Widget` class represents modular widget information. */ @interface Widget : NSObject diff --git a/Riot/Utils/Widgets/Widget.m b/Riot/Utils/Widgets/Widget.m index 8bfc1574a..381e23c48 100644 --- a/Riot/Utils/Widgets/Widget.m +++ b/Riot/Utils/Widgets/Widget.m @@ -24,7 +24,7 @@ { if (![widgetEvent.type isEqualToString:kWidgetEventTypeString]) { - // The Widget class works only with scalar, aka "im.vector.modular.widgets", widgets + // The Widget class works only with modular, aka "im.vector.modular.widgets", widgets return nil; } diff --git a/Riot/Utils/Widgets/WidgetManager.h b/Riot/Utils/Widgets/WidgetManager.h index 3e20e5a32..4a0833e02 100644 --- a/Riot/Utils/Widgets/WidgetManager.h +++ b/Riot/Utils/Widgets/WidgetManager.h @@ -21,7 +21,7 @@ #import "Widget.h" /** - The type of matrix event used for scalar widgets. + The type of matrix event used for modular widgets. */ FOUNDATION_EXPORT NSString *const kWidgetEventTypeString; @@ -50,7 +50,7 @@ WidgetManagerErrorCode; /** - The `WidgetManager` helps to handle scalar widgets. + The `WidgetManager` helps to handle modular widgets. */ @interface WidgetManager : NSObject @@ -80,7 +80,7 @@ WidgetManagerErrorCode; /** - Add a scalar widget to a room. + Add a modular widget to a room. @param widgetId the id of the widget. @param widgetContent the widget content. diff --git a/Riot/Utils/Widgets/WidgetManager.m b/Riot/Utils/Widgets/WidgetManager.m index 19cdf3df1..ce390a05c 100644 --- a/Riot/Utils/Widgets/WidgetManager.m +++ b/Riot/Utils/Widgets/WidgetManager.m @@ -158,7 +158,7 @@ NSString *const WidgetManagerErrorDomain = @"WidgetManagerErrorDomain"; } // Send a state event with the widget data - // TODO: This API will be shortly replaced by a pure scalar API + // TODO: This API will be shortly replaced by a pure modular API return [room sendStateEventOfType:kWidgetEventTypeString content:widgetContent stateKey:widgetId @@ -181,7 +181,7 @@ NSString *const WidgetManagerErrorDomain = @"WidgetManagerErrorDomain"; NSString *confId = [room.roomId substringWithRange:NSMakeRange(1, [room.roomId rangeOfString:@":"].location - 1)]; confId = [confId stringByAppendingString:widgetSessionId]; - // TODO: This url may come from scalar API + // TODO: This url may come from modular API // Note: this url can be used as is inside a web container (like iframe for Riot-web) // Riot-iOS does not directly use it but extracts params from it (see `[JitsiViewController openWidget:withVideo:]`) NSString *url = [NSString stringWithFormat:@"https://scalar-staging.riot.im/scalar/api/widgets/jitsi.html?confId=%@&isAudioConf=%@&displayName=$matrix_display_name&avatarUrl=$matrix_avatar_url&email=$matrix_user_id@", confId, video ? @"false" : @"true"]; @@ -216,7 +216,7 @@ NSString *const WidgetManagerErrorDomain = @"WidgetManagerErrorDomain"; } // Send a state event with an empty content to disable the widget - // TODO: This API will be shortly replaced by a pure scalar API + // TODO: This API will be shortly replaced by a pure modular API return [room sendStateEventOfType:kWidgetEventTypeString content:@{} stateKey:widgetId diff --git a/Riot/ViewController/CallViewController.m b/Riot/ViewController/CallViewController.m index 830fa7281..558039dfc 100644 --- a/Riot/ViewController/CallViewController.m +++ b/Riot/ViewController/CallViewController.m @@ -27,6 +27,8 @@ #import "RiotNavigationController.h" +#import "IncomingCallView.h" + @interface CallViewController () { // Display a gradient view above the screen @@ -196,6 +198,37 @@ gradientMaskLayer = nil; } +- (UIView *)createIncomingCallView +{ + NSString *avatarThumbURL = [self.mainSession.matrixRestClient urlOfContentThumbnail:self.peer.avatarUrl + toFitViewSize:IncomingCallView.callerAvatarSize + withMethod:MXThumbnailingMethodCrop]; + + NSString *callInfo; + if (self.mxCall.isVideoCall) + callInfo = NSLocalizedStringFromTable(@"call_incoming_video", @"Vector", nil); + else + callInfo = NSLocalizedStringFromTable(@"call_incoming_voice", @"Vector", nil); + + IncomingCallView *incomingCallView = [[IncomingCallView alloc] initWithCallerAvatarURL:avatarThumbURL + placeholderImage:self.picturePlaceholder + callerName:self.peer.displayname + callInfo:callInfo]; + + // Incoming call is retained by call vc so use weak to avoid retain cycle + __weak typeof(self) weakSelf = self; + + incomingCallView.onAnswer = ^{ + [weakSelf onButtonPressed:weakSelf.answerCallButton]; + }; + + incomingCallView.onReject = ^{ + [weakSelf onButtonPressed:weakSelf.rejectCallButton]; + }; + + return incomingCallView; +} + #pragma mark - MXCallDelegate - (void)call:(MXCall *)call didEncounterError:(NSError *)error diff --git a/Riot/ViewController/MasterTabBarController.m b/Riot/ViewController/MasterTabBarController.m index 8c02e2c81..fb5d451d3 100644 --- a/Riot/ViewController/MasterTabBarController.m +++ b/Riot/ViewController/MasterTabBarController.m @@ -23,6 +23,7 @@ #import "AppDelegate.h" #import "MXRoom+Riot.h" +#import "MXSession+Riot.h" @interface MasterTabBarController () { @@ -454,26 +455,7 @@ // Considering all the current sessions. for (MXSession *session in mxSessionArray) { - // Sum all the rooms with missed notifications. - for (MXRoomSummary *roomSummary in session.roomsSummaries) - { - NSUInteger notificationCount = roomSummary.notificationCount; - - // Ignore the regular notification count if the room is in 'mentions only" mode at the Riot level. - if (roomSummary.room.isMentionsOnly) - { - // Only the highlighted missed messages must be considered here. - notificationCount = roomSummary.highlightCount; - } - - if (notificationCount) - { - roomCount ++; - } - } - - // Add the invites count - roomCount += [session invitedRooms].count; + roomCount += [session riot_missedDiscussionsCount]; } return roomCount; diff --git a/Riot/ViewController/SettingsViewController.m b/Riot/ViewController/SettingsViewController.m index 7ec1ad9b3..b3ed78ad3 100644 --- a/Riot/ViewController/SettingsViewController.m +++ b/Riot/ViewController/SettingsViewController.m @@ -17,13 +17,14 @@ #import "SettingsViewController.h" -#import "AppDelegate.h" - -#import "AvatarGenerator.h" - -#import +#import #import #import +#import +#import + +#import "AppDelegate.h" +#import "AvatarGenerator.h" #import "MXKEncryptionKeysExportView.h" #import "BugReportViewController.h" @@ -32,22 +33,20 @@ #import "CountryPickerViewController.h" #import "LanguagePickerViewController.h" -#import "TableViewCellWithPhoneNumberTextField.h" #import "NBPhoneNumberUtil.h" +#import "RageShakeManager.h" +#import "RiotDesignValues.h" +#import "TableViewCellWithPhoneNumberTextField.h" -#import "AvatarGenerator.h" - -#import "OLMKit/OLMKit.h" - - -NSString* const kSettingsViewControllerPhoneBookCountryCellId = @"kSettingsViewControllerPhoneBookCountryCellId"; +static NSString* const kSettingsViewControllerPhoneBookCountryCellId = @"kSettingsViewControllerPhoneBookCountryCellId"; enum { SETTINGS_SECTION_SIGN_OUT_INDEX = 0, SETTINGS_SECTION_USER_SETTINGS_INDEX, SETTINGS_SECTION_NOTIFICATIONS_SETTINGS_INDEX, + SETTINGS_SECTION_CALLS_INDEX, SETTINGS_SECTION_USER_INTERFACE_INDEX, SETTINGS_SECTION_IGNORED_USERS_INDEX, SETTINGS_SECTION_CONTACTS_INDEX, @@ -75,6 +74,13 @@ enum NOTIFICATION_SETTINGS_COUNT }; +enum +{ + CALLS_ENABLE_CALLKIT_INDEX = 0, + CALLS_DESCRIPTION_INDEX, + CALLS_COUNT +}; + enum { USER_INTERFACE_LANGUAGE_INDEX = 0, @@ -1177,6 +1183,13 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(); { count = NOTIFICATION_SETTINGS_COUNT; } + else if (section == SETTINGS_SECTION_CALLS_INDEX) + { + if ([MXCallKitAdapter callKitAvailable]) + { + count = CALLS_COUNT; + } + } else if (section == SETTINGS_SECTION_USER_INTERFACE_INDEX) { count = USER_INTERFACE_COUNT; @@ -1664,6 +1677,29 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(); cell = labelAndSwitchCell; } } + else if (section == SETTINGS_SECTION_CALLS_INDEX) + { + if (row == CALLS_ENABLE_CALLKIT_INDEX) + { + MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; + labelAndSwitchCell.mxkLabel.text = NSLocalizedStringFromTable(@"settings_enable_callkit", @"Vector", nil); + labelAndSwitchCell.mxkSwitch.on = [MXKAppSettings standardAppSettings].isCallKitEnabled; + labelAndSwitchCell.mxkSwitch.enabled = YES; + [labelAndSwitchCell.mxkSwitch removeTarget:self action:nil forControlEvents:UIControlEventTouchUpInside]; + [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleCallKit:) forControlEvents:UIControlEventTouchUpInside]; + + cell = labelAndSwitchCell; + } + else if (row == CALLS_DESCRIPTION_INDEX) + { + MXKTableViewCell *globalInfoCell = [self getDefaultTableViewCell:tableView]; + globalInfoCell.textLabel.text = NSLocalizedStringFromTable(@"settings_callkit_info", @"Vector", nil); + globalInfoCell.textLabel.numberOfLines = 0; + globalInfoCell.selectionStyle = UITableViewCellSelectionStyleNone; + + cell = globalInfoCell; + } + } else if (section == SETTINGS_SECTION_USER_INTERFACE_INDEX) { if (row == USER_INTERFACE_LANGUAGE_INDEX) @@ -2044,6 +2080,13 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(); { return NSLocalizedStringFromTable(@"settings_notifications_settings", @"Vector", nil); } + else if (section == SETTINGS_SECTION_CALLS_INDEX) + { + if ([MXCallKitAdapter callKitAvailable]) + { + return NSLocalizedStringFromTable(@"settings_calls_settings", @"Vector", nil); + } + } else if (section == SETTINGS_SECTION_USER_INTERFACE_INDEX) { return NSLocalizedStringFromTable(@"settings_user_interface", @"Vector", nil); @@ -2168,6 +2211,13 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(); } } } + else if (section == SETTINGS_SECTION_CALLS_INDEX) + { + if (![MXCallKitAdapter callKitAvailable]) + { + return SECTION_TITLE_PADDING_WHEN_HIDDEN; + } + } return 24; } @@ -2186,6 +2236,13 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(); } } } + else if (section == SETTINGS_SECTION_CALLS_INDEX) + { + if (![MXCallKitAdapter callKitAvailable]) + { + return SECTION_TITLE_PADDING_WHEN_HIDDEN; + } + } return 24; } @@ -2575,7 +2632,8 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(); - (void)togglePushNotifications:(id)sender { // Check first whether the user allow notification from device settings - if ([[MXKAccountManager sharedManager] isAPNSAvailable] == NO) + UIUserNotificationType currentUserNotificationTypes = UIApplication.sharedApplication.currentUserNotificationSettings.types; + if (currentUserNotificationTypes == UIUserNotificationTypeNone) { [currentAlert dismissViewControllerAnimated:NO completion:nil]; @@ -2607,13 +2665,37 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(); { [self startActivityIndicator]; - MXKAccount* account = [MXKAccountManager sharedManager].activeAccounts.firstObject; + MXKAccountManager *accountManager = [MXKAccountManager sharedManager]; + MXKAccount* account = accountManager.activeAccounts.firstObject; - // toggle the pushes - [account setEnablePushNotifications:!account.pushNotificationServiceIsActive]; + if (accountManager.apnsDeviceToken) + { + [account setEnablePushNotifications:!account.pushNotificationServiceIsActive]; + } + else + { + // Obtain device token when user has just enabled access to notifications from system settings + [[AppDelegate theDelegate] registerForRemoteNotificationsWithCompletion:^(NSError * error) { + if (error) + { + [(UISwitch *)sender setOn:NO animated:YES]; + [self stopActivityIndicator]; + } + else + { + [account setEnablePushNotifications:YES]; + } + }]; + } } } +- (void)toggleCallKit:(id)sender +{ + UISwitch *switchButton = (UISwitch*)sender; + [MXKAppSettings standardAppSettings].enableCallKit = switchButton.isOn; +} + - (void)toggleShowDecodedContent:(id)sender { MXKAccount* account = [MXKAccountManager sharedManager].activeAccounts.firstObject; diff --git a/Riot/ViewController/Widgets/JitsiViewController.h b/Riot/ViewController/Widgets/JitsiViewController.h index f6368bc18..0d175de00 100644 --- a/Riot/ViewController/Widgets/JitsiViewController.h +++ b/Riot/ViewController/Widgets/JitsiViewController.h @@ -24,7 +24,7 @@ /** The `JitsiViewController` is a VC for specifically handling a jitsi widget using the - jitsi-meet iOS SDK instead of displaying it in a webview like other scalar widgets. + jitsi-meet iOS SDK instead of displaying it in a webview like other modular widgets. https://github.com/jitsi/jitsi-meet/tree/master/ios */ diff --git a/Riot/Views/Calls/CircleButton.h b/Riot/Views/Calls/CircleButton.h new file mode 100644 index 000000000..61780be30 --- /dev/null +++ b/Riot/Views/Calls/CircleButton.h @@ -0,0 +1,55 @@ +/* + Copyright 2017 Vector Creations 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface CircleButton : UIButton + +/** + Background color that is used for highlighted state + + By default the same as borderColor + */ +@property (nonatomic) UIColor *highlightBackgroundColor; + +/** + Background color that is used for normal state + + By default white + */ +@property (nonatomic) UIColor *defaultBackgroundColor; + +/** + Tint color that is used for highlighted state + + By default is white + */ +@property (nonatomic) UIColor *highlightTintColor; + +/** + Tint color that is used for normal state + + By default is the same as borderColor + */ +@property (nonatomic) UIColor *defaultTintColor; + +- (instancetype)initWithImage:(UIImage *)image borderColor:(UIColor *)borderColor; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Riot/Views/Calls/CircleButton.m b/Riot/Views/Calls/CircleButton.m new file mode 100644 index 000000000..d277935ea --- /dev/null +++ b/Riot/Views/Calls/CircleButton.m @@ -0,0 +1,86 @@ +/* + Copyright 2017 Vector Creations 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 "CircleButton.h" + +@implementation CircleButton + +- (instancetype)initWithImage:(UIImage *)image borderColor:(UIColor *)borderColor +{ + self = [[super class] buttonWithType:UIButtonTypeCustom]; + + self.adjustsImageWhenDisabled = NO; + self.adjustsImageWhenHighlighted = NO; + + self.layer.borderWidth = 1.0; + self.layer.borderColor = borderColor.CGColor; + + self.defaultBackgroundColor = [UIColor whiteColor]; + self.highlightTintColor = [UIColor whiteColor]; + + self.highlightBackgroundColor = borderColor; + self.defaultTintColor = borderColor; + self.tintColor = borderColor; + + [self setImage:[image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateNormal]; + + return self; +} + +- (void)setDefaultBackgroundColor:(UIColor *)defaultBackgroundColor +{ + _defaultBackgroundColor = defaultBackgroundColor; + self.backgroundColor = defaultBackgroundColor; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + self.layer.cornerRadius = CGRectGetWidth(self.bounds) / 2.0; +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesBegan:touches withEvent:event]; + [self animateBackgroundColor:self.highlightBackgroundColor tintColor:self.highlightTintColor]; +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesEnded:touches withEvent:event]; + [self animateBackgroundColor:self.defaultBackgroundColor tintColor:self.defaultTintColor]; +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesCancelled:touches withEvent:event]; + [self animateBackgroundColor:self.defaultBackgroundColor tintColor:self.defaultTintColor]; +} + +- (void)animateBackgroundColor:(UIColor *)color tintColor:(UIColor *)tintColor +{ + [UIView animateWithDuration:0.25 + delay:0.0 + options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction + animations:^{ + self.backgroundColor = color; + self.tintColor = tintColor; + } + completion:nil]; +} + +@end diff --git a/Riot/Views/Calls/IncomingCallView.h b/Riot/Views/Calls/IncomingCallView.h new file mode 100644 index 000000000..9b437c1ee --- /dev/null +++ b/Riot/Views/Calls/IncomingCallView.h @@ -0,0 +1,47 @@ +/* + Copyright 2017 Vector Creations 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 + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^IncomingCallViewAction)(); + +@interface IncomingCallView : UIView + +/** + Size that is applied to displayed user avatar + */ +@property (class, readonly) CGSize callerAvatarSize; + +/** + Block which is performed on call answer action + */ +@property (nonatomic, nullable, copy) IncomingCallViewAction onAnswer; + +/** + Block which is performed on call reject + */ +@property (nonatomic, nullable, copy) IncomingCallViewAction onReject; + +- (instancetype)initWithCallerAvatarURL:(NSString *)callerAvatarURL + placeholderImage:(UIImage *)placeholderImage + callerName:(NSString *)callerName + callInfo:(NSString *)callInfo; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Riot/Views/Calls/IncomingCallView.m b/Riot/Views/Calls/IncomingCallView.m new file mode 100644 index 000000000..0d442232c --- /dev/null +++ b/Riot/Views/Calls/IncomingCallView.m @@ -0,0 +1,326 @@ +/* + Copyright 2017 Vector Creations 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 "IncomingCallView.h" + +#import +#import + +#import "CircleButton.h" +#import "RiotDesignValues.h" + +static const CGFloat kAvatarSize = 100.0; +static const CGFloat kButtonSize = 80.0; + +@interface IncomingCallView () + +@property (nonatomic) MXKImageView *callerImageView; +@property (nonatomic) UILabel *callerNameLabel; +@property (nonatomic) UILabel *callInfoLabel; + +@property (nonatomic) CircleButton *answerButton; +@property (nonatomic) UILabel *answerTitleLabel; + +@property (nonatomic) CircleButton *rejectButton; +@property (nonatomic) UILabel *rejectTitleLabel; + +@end + +@implementation IncomingCallView + ++ (CGSize)callerAvatarSize +{ + return CGSizeMake(kAvatarSize, kAvatarSize); +} + +- (instancetype)initWithCallerAvatarURL:(NSString *)callerAvatarURL placeholderImage:(UIImage *)placeholderImage callerName:(NSString *)callerName callInfo:(NSString *)callInfo +{ + self = [super initWithFrame:CGRectZero]; + if (self) + { + self.backgroundColor = kRiotPrimaryBgColor; + self.opaque = YES; + + self.callerImageView = [[MXKImageView alloc] init]; + self.callerImageView.backgroundColor = kRiotPrimaryBgColor; + self.callerImageView.clipsToBounds = YES; + self.callerImageView.mediaFolder = kMXMediaManagerAvatarThumbnailFolder; + self.callerImageView.enableInMemoryCache = YES; + [self.callerImageView setImageURL:callerAvatarURL + withType:nil + andImageOrientation:UIImageOrientationUp + previewImage:placeholderImage]; + + self.callerNameLabel = [[UILabel alloc] init]; + self.callerNameLabel.backgroundColor = kRiotPrimaryBgColor; + self.callerNameLabel.textColor = kRiotPrimaryTextColor; + self.callerNameLabel.font = [UIFont systemFontOfSize:24.0 weight:UIFontWeightMedium]; + self.callerNameLabel.text = callerName; + self.callerNameLabel.textAlignment = NSTextAlignmentCenter; + + self.callInfoLabel = [[UILabel alloc] init]; + self.callInfoLabel.backgroundColor = kRiotPrimaryBgColor; + self.callInfoLabel.textColor = kRiotSecondaryTextColor; + self.callInfoLabel.font = [UIFont systemFontOfSize:18.0 weight:UIFontWeightRegular]; + self.callInfoLabel.text = callInfo; + self.callInfoLabel.textAlignment = NSTextAlignmentCenter; + + UIColor *answerButtonBorderColor = kRiotColorGreen; + + self.answerButton = [[CircleButton alloc] initWithImage:[UIImage imageNamed:@"voice_call_icon"] + borderColor:answerButtonBorderColor]; + self.answerButton.defaultBackgroundColor = kRiotPrimaryBgColor; + [self.answerButton addTarget:self action:@selector(didTapAnswerButton) forControlEvents:UIControlEventTouchUpInside]; + + self.answerTitleLabel = [[UILabel alloc] init]; + self.answerTitleLabel.backgroundColor = kRiotPrimaryBgColor; + self.answerTitleLabel.textColor = answerButtonBorderColor; + self.answerTitleLabel.font = [UIFont systemFontOfSize:18.0 weight:UIFontWeightRegular]; + self.answerTitleLabel.text = NSLocalizedStringFromTable(@"accept", @"Vector", nil); + + UIColor *rejectButtonBorderColor = kRiotColorPinkRed; + + self.rejectButton = [[CircleButton alloc] initWithImage:[UIImage imageNamed:@"call_hangup_icon"] + borderColor:rejectButtonBorderColor]; + self.rejectButton.defaultBackgroundColor = kRiotPrimaryBgColor; + [self.rejectButton addTarget:self action:@selector(didTapRejectButton) forControlEvents:UIControlEventTouchUpInside]; + + self.rejectTitleLabel = [[UILabel alloc] init]; + self.rejectTitleLabel.backgroundColor = kRiotPrimaryBgColor; + self.rejectTitleLabel.textColor = rejectButtonBorderColor; + self.rejectTitleLabel.font = [UIFont systemFontOfSize:18.0 weight:UIFontWeightRegular]; + self.rejectTitleLabel.text = NSLocalizedStringFromTable(@"decline", @"Vector", nil); + + [self setupLayout]; + } + + return self; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + self.callerImageView.layer.cornerRadius = CGRectGetWidth(self.callerImageView.bounds) / 2.0; +} + +- (void)setupLayout +{ + NSArray *views = @[self.callerImageView, self.callerNameLabel, self.callInfoLabel, self.answerButton, self.answerTitleLabel, self.rejectButton, self.rejectTitleLabel]; + for (UIView *view in views) + { + view.translatesAutoresizingMaskIntoConstraints = NO; + [self addSubview:view]; + } + + [NSLayoutConstraint activateConstraints:@[ + [NSLayoutConstraint constraintWithItem:self.callerImageView + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeTop + multiplier:1.0 + constant:62.0], + + [NSLayoutConstraint constraintWithItem:self.callerImageView + attribute:NSLayoutAttributeCenterX + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeCenterX + multiplier:1.0 + constant:0.0], + + [NSLayoutConstraint constraintWithItem:self.callerImageView + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1.0 + constant:kAvatarSize], + + [NSLayoutConstraint constraintWithItem:self.callerImageView + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:self.callerImageView + attribute:NSLayoutAttributeWidth + multiplier:1.0 + constant:0.0], + + [NSLayoutConstraint constraintWithItem:self.callerNameLabel + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:self.callerImageView + attribute:NSLayoutAttributeBottom + multiplier:1.0 + constant:18.0], + + [NSLayoutConstraint constraintWithItem:self.callerNameLabel + attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeLeading + multiplier:1.0 + constant:15.0], + + [NSLayoutConstraint constraintWithItem:self.callerNameLabel + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeTrailing + multiplier:1.0 + constant:-15.0], + + [NSLayoutConstraint constraintWithItem:self.callInfoLabel + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:self.callerNameLabel + attribute:NSLayoutAttributeBottom + multiplier:1.0 + constant:7.0], + + [NSLayoutConstraint constraintWithItem:self.callInfoLabel + attribute:NSLayoutAttributeCenterX + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeCenterX + multiplier:1.0 + constant:0.0], + + [NSLayoutConstraint constraintWithItem:self.callInfoLabel + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:self.callerNameLabel + attribute:NSLayoutAttributeWidth + multiplier:1.0 + constant:0.0], + + [NSLayoutConstraint constraintWithItem:self.rejectButton + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1.0 + constant:kButtonSize], + + [NSLayoutConstraint constraintWithItem:self.rejectButton + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:self.rejectButton + attribute:NSLayoutAttributeWidth + multiplier:1.0 + constant:0.0], + + [NSLayoutConstraint constraintWithItem:self.rejectButton + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeCenterX + multiplier:1.0 + constant:-22.5], + + [NSLayoutConstraint constraintWithItem:self.rejectTitleLabel + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:self.rejectButton + attribute:NSLayoutAttributeBottom + multiplier:1.0 + constant:8.0], + + [NSLayoutConstraint constraintWithItem:self.rejectTitleLabel + attribute:NSLayoutAttributeCenterX + relatedBy:NSLayoutRelationEqual + toItem:self.rejectButton + attribute:NSLayoutAttributeCenterX + multiplier:1.0 + constant:0.0], + + [NSLayoutConstraint constraintWithItem:self.rejectTitleLabel + attribute:NSLayoutAttributeBottom + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeBottom + multiplier:1.0 + constant:-16.0], + + [NSLayoutConstraint constraintWithItem:self.answerButton + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1.0 + constant:kButtonSize], + + [NSLayoutConstraint constraintWithItem:self.answerButton + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:self.answerButton + attribute:NSLayoutAttributeWidth + multiplier:1.0 + constant:0.0], + + [NSLayoutConstraint constraintWithItem:self.answerButton + attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeCenterX + multiplier:1.0 + constant:22.5], + + [NSLayoutConstraint constraintWithItem:self.answerTitleLabel + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:self.answerButton + attribute:NSLayoutAttributeBottom + multiplier:1.0 + constant:8.0], + + [NSLayoutConstraint constraintWithItem:self.answerTitleLabel + attribute:NSLayoutAttributeCenterX + relatedBy:NSLayoutRelationEqual + toItem:self.answerButton + attribute:NSLayoutAttributeCenterX + multiplier:1.0 + constant:0.0], + + [NSLayoutConstraint constraintWithItem:self.answerTitleLabel + attribute:NSLayoutAttributeBottom + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeBottom + multiplier:1.0 + constant:-16.0] + ]]; +} + +// MARK: - Actions + +- (void)didTapAnswerButton +{ + if (self.onAnswer) + { + self.onAnswer(); + } +} + +- (void)didTapRejectButton +{ + if (self.onReject) + { + self.onReject(); + } +} + +@end diff --git a/RiotShareExtension/Info.plist b/RiotShareExtension/Info.plist index 251562e64..0889a1471 100644 --- a/RiotShareExtension/Info.plist +++ b/RiotShareExtension/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.0 + 0.5.3 CFBundleVersion 1 NSExtension @@ -29,13 +29,13 @@ NSExtensionActivationRule NSExtensionActivationSupportsMovieWithMaxCount - 1 + 0 NSExtensionActivationSupportsImageWithMaxCount - 1 + 0 NSExtensionActivationSupportsWebURLWithMaxCount - 1 + 0 NSExtensionActivationSupportsText - + NSExtensionPointIdentifier diff --git a/RiotShareExtension/Model/ShareExtensionManager.h b/RiotShareExtension/Model/ShareExtensionManager.h index 1c54e4bd7..065b59b2e 100644 --- a/RiotShareExtension/Model/ShareExtensionManager.h +++ b/RiotShareExtension/Model/ShareExtensionManager.h @@ -18,6 +18,7 @@ #import @class ShareExtensionManager; +@class SharePresentingViewController; /** Posted when the matrix session has been changed. @@ -64,6 +65,11 @@ extern NSString *const kShareExtensionManagerDidChangeMXSessionNotification; */ @property (nonatomic) NSExtensionContext *shareExtensionContext; +/** + The share app extension’s primary view controller. + */ +@property (nonatomic) SharePresentingViewController *primaryViewController; + /** The associated matrix session (nil by default). */ diff --git a/RiotShareExtension/Model/ShareExtensionManager.m b/RiotShareExtension/Model/ShareExtensionManager.m index b6c65b24f..47a679a0b 100644 --- a/RiotShareExtension/Model/ShareExtensionManager.m +++ b/RiotShareExtension/Model/ShareExtensionManager.m @@ -15,6 +15,7 @@ */ #import "ShareExtensionManager.h" +#import "SharePresentingViewController.h" #import "MXKPieChartHUD.h" @import MobileCoreServices; @@ -60,8 +61,12 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) [[NSNotificationCenter defaultCenter] addObserver:sharedInstance selector:@selector(checkUserAccount) name:NSExtensionHostWillEnterForegroundNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:sharedInstance selector:@selector(suspendSession) name:NSExtensionHostDidEnterBackgroundNotification object:nil]; + MXSDKOptions *sdkOptions = [MXSDKOptions sharedInstance]; + // Apply the application group - [MXKAppSettings standardAppSettings].applicationGroup = @"group.im.vector"; + sdkOptions.applicationGroupIdentifier = @"group.im.vector"; + // Disable identicon use + sdkOptions.disableIdenticonUseForUserAvatar = YES; }); return sharedInstance; } @@ -152,68 +157,114 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) if ([itemProvider hasItemConformingToTypeIdentifier:UTTypeFileUrl]) { [itemProvider loadItemForTypeIdentifier:UTTypeFileUrl options:nil completionHandler:^(NSURL *fileUrl, NSError * _Null_unspecified error) { - if (weakSelf) - { - typeof(self) self = weakSelf; - [self sendFileWithUrl:fileUrl toRoom:room extensionItem:item failureBlock:failureBlock]; - } + + // Switch back on the main thread to handle correctly the UI change + dispatch_async(dispatch_get_main_queue(), ^{ + + if (weakSelf) + { + typeof(self) self = weakSelf; + [self sendFileWithUrl:fileUrl toRoom:room extensionItem:item failureBlock:failureBlock]; + } + + }); + }]; } else if ([itemProvider hasItemConformingToTypeIdentifier:UTTypeText]) { [itemProvider loadItemForTypeIdentifier:UTTypeText options:nil completionHandler:^(NSString *text, NSError * _Null_unspecified error) { - if (weakSelf) - { - typeof(self) self = weakSelf; - [self sendText:text toRoom:room extensionItem:item failureBlock:failureBlock]; - } + + // Switch back on the main thread to handle correctly the UI change + dispatch_async(dispatch_get_main_queue(), ^{ + + if (weakSelf) + { + typeof(self) self = weakSelf; + [self sendText:text toRoom:room extensionItem:item failureBlock:failureBlock]; + } + + }); + }]; } else if ([itemProvider hasItemConformingToTypeIdentifier:UTTypeURL]) { [itemProvider loadItemForTypeIdentifier:UTTypeURL options:nil completionHandler:^(NSURL *url, NSError * _Null_unspecified error) { - if (weakSelf) - { - typeof(self) self = weakSelf; - [self sendText:url.absoluteString toRoom:room extensionItem:item failureBlock:failureBlock]; - } + + // Switch back on the main thread to handle correctly the UI change + dispatch_async(dispatch_get_main_queue(), ^{ + + if (weakSelf) + { + typeof(self) self = weakSelf; + [self sendText:url.absoluteString toRoom:room extensionItem:item failureBlock:failureBlock]; + } + + }); + }]; } else if ([itemProvider hasItemConformingToTypeIdentifier:UTTypeImage]) { - [itemProvider loadItemForTypeIdentifier:UTTypeImage options:nil completionHandler:^(NSData *imageData, NSError * _Null_unspecified error) - { - if (weakSelf) - { - typeof(self) self = weakSelf; - UIImage *image = [[UIImage alloc] initWithData:imageData]; - UIAlertController *compressionPrompt = [self compressionPromptForImage:image shareBlock:^{ - [self sendImage:imageData withProvider:itemProvider toRoom:room extensionItem:item failureBlock:failureBlock]; - }]; - [self.delegate shareExtensionManager:self showImageCompressionPrompt:compressionPrompt]; - } + [itemProvider loadItemForTypeIdentifier:UTTypeImage options:nil completionHandler:^(NSData *imageData, NSError * _Null_unspecified error) { + + // Switch back on the main thread to handle correctly the UI change + dispatch_async(dispatch_get_main_queue(), ^{ + + if (weakSelf) + { + typeof(self) self = weakSelf; + UIImage *image = [[UIImage alloc] initWithData:imageData]; + + UIAlertController *compressionPrompt = [self compressionPromptForImage:image shareBlock:^{ + + [self sendImage:image withProvider:itemProvider toRoom:room extensionItem:item failureBlock:failureBlock]; + + }]; + + if (compressionPrompt) + { + [self.delegate shareExtensionManager:self showImageCompressionPrompt:compressionPrompt]; + } + } + + }); + }]; } else if ([itemProvider hasItemConformingToTypeIdentifier:UTTypeVideo]) { - [itemProvider loadItemForTypeIdentifier:UTTypeVideo options:nil completionHandler:^(NSURL *videoLocalUrl, NSError * _Null_unspecified error) - { - if (weakSelf) - { - typeof(self) self = weakSelf; - [self sendVideo:videoLocalUrl toRoom:room extensionItem:item failureBlock:failureBlock]; - } + [itemProvider loadItemForTypeIdentifier:UTTypeVideo options:nil completionHandler:^(NSURL *videoLocalUrl, NSError * _Null_unspecified error) { + + // Switch back on the main thread to handle correctly the UI change + dispatch_async(dispatch_get_main_queue(), ^{ + + if (weakSelf) + { + typeof(self) self = weakSelf; + [self sendVideo:videoLocalUrl toRoom:room extensionItem:item failureBlock:failureBlock]; + } + + }); + }]; } else if ([itemProvider hasItemConformingToTypeIdentifier:UTTypeMovie]) { - [itemProvider loadItemForTypeIdentifier:UTTypeMovie options:nil completionHandler:^(NSURL *videoLocalUrl, NSError * _Null_unspecified error) - { - if (weakSelf) - { - typeof(self) self = weakSelf; - [self sendVideo:videoLocalUrl toRoom:room extensionItem:item failureBlock:failureBlock]; - } + [itemProvider loadItemForTypeIdentifier:UTTypeMovie options:nil completionHandler:^(NSURL *videoLocalUrl, NSError * _Null_unspecified error) { + + // Switch back on the main thread to handle correctly the UI change + dispatch_async(dispatch_get_main_queue(), ^{ + + if (weakSelf) + { + typeof(self) self = weakSelf; + [self sendVideo:videoLocalUrl toRoom:room extensionItem:item failureBlock:failureBlock]; + } + + }); + }]; } } @@ -247,15 +298,28 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) { [self.shareExtensionContext cancelRequestWithError:[NSError errorWithDomain:@"MXFailureErrorDomain" code:500 userInfo:nil]]; } + + [self.primaryViewController destroy]; + self.primaryViewController = nil; } #pragma mark - Private +- (void)completeRequestReturningItems:(nullable NSArray *)items completionHandler:(void(^ __nullable)(BOOL expired))completionHandler; +{ + [self suspendSession]; + + [self.shareExtensionContext completeRequestReturningItems:items completionHandler:completionHandler]; + + [self.primaryViewController destroy]; + self.primaryViewController = nil; +} + - (UIAlertController *)compressionPromptForImage:(UIImage *)image shareBlock:(void(^)())shareBlock { UIAlertController *compressionPrompt; - // Get availabe sizes for this image + // Get available sizes for this image MXKImageCompressionSizes compressionSizes = [MXKTools availableCompressionSizesForImage:image]; // Apply the compression mode @@ -347,28 +411,32 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) }]]; } - NSString *resolution = [NSString stringWithFormat:@"%@ (%d x %d)", [MXTools fileSizeToString:compressionSizes.original.fileSize round:NO], (int)compressionSizes.original.imageSize.width, (int)compressionSizes.original.imageSize.height]; - - NSString *title = [NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"attachment_original"], resolution]; - - [compressionPrompt addAction:[UIAlertAction actionWithTitle:title - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; + // To limit memory consumption, we suggest the original resolution only if the image orientation is up, or if the image size is moderate + if (image.imageOrientation == UIImageOrientationUp || !compressionSizes.large.fileSize) + { + NSString *resolution = [NSString stringWithFormat:@"%@ (%d x %d)", [MXTools fileSizeToString:compressionSizes.original.fileSize round:NO], (int)compressionSizes.original.imageSize.width, (int)compressionSizes.original.imageSize.height]; + + NSString *title = [NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"attachment_original"], resolution]; + + [compressionPrompt addAction:[UIAlertAction actionWithTitle:title + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { - self.imageCompressionMode = ImageCompressionModeNone; - if (shareBlock) + if (weakSelf) { - shareBlock(); + typeof(self) self = weakSelf; + + self.imageCompressionMode = ImageCompressionModeNone; + if (shareBlock) + { + shareBlock(); + } + + [compressionPrompt dismissViewControllerAnimated:YES completion:nil]; } - [compressionPrompt dismissViewControllerAnimated:YES completion:nil]; - } - - }]]; + }]]; + } [compressionPrompt addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"cancel"] style:UIAlertActionStyleDefault @@ -383,6 +451,14 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) } + else + { + self.imageCompressionMode = ImageCompressionModeNone; + if (shareBlock) + { + shareBlock(); + } + } return compressionPrompt; } @@ -426,8 +502,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) if (weakSelf) { typeof(self) self = weakSelf; - [self suspendSession]; - [self.shareExtensionContext completeRequestReturningItems:@[extensionItem] completionHandler:nil]; + [self completeRequestReturningItems:@[extensionItem] completionHandler:nil]; } } failure:^(NSError *error) { NSLog(@"[ShareExtensionManager] sendTextMessage failed."); @@ -462,8 +537,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) if (weakSelf) { typeof(self) self = weakSelf; - [self suspendSession]; - [self.shareExtensionContext completeRequestReturningItems:@[extensionItem] completionHandler:nil]; + [self completeRequestReturningItems:@[extensionItem] completionHandler:nil]; } } failure:^(NSError *error) { NSLog(@"[ShareExtensionManager] sendFile failed."); @@ -474,10 +548,10 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) } keepActualFilename:YES]; } -- (void)sendImage:(NSData *)imageData withProvider:(NSItemProvider*)itemProvider toRoom:(MXRoom *)room extensionItem:(NSExtensionItem *)extensionItem failureBlock:(void(^)())failureBlock +- (void)sendImage:(UIImage *)image withProvider:(NSItemProvider*)itemProvider toRoom:(MXRoom *)room extensionItem:(NSExtensionItem *)extensionItem failureBlock:(void(^)())failureBlock { [self didStartSendingToRoom:room]; - if (!imageData) + if (!image) { NSLog(@"[ShareExtensionManager] loadItemForTypeIdentifier: failed."); if (failureBlock) @@ -488,15 +562,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) } // Prepare the image - BOOL rotated = NO; - UIImage *image = [[UIImage alloc] initWithData:imageData]; - - // Make sure the uploaded image orientation is up - if (image.imageOrientation != UIImageOrientationUp) - { - image = [MXKTools forceImageOrientationUp:image]; - rotated = YES; - } + NSData *imageData; if (self.imageCompressionMode == ImageCompressionModeSmall) { @@ -511,32 +577,20 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) image = [MXKTools reduceImage:image toFitInSize:CGSizeMake(self.actualLargeSize, self.actualLargeSize)]; } + // Make sure the uploaded image orientation is up + image = [MXKTools forceImageOrientationUp:image]; + NSString *mimeType; if ([itemProvider hasItemConformingToTypeIdentifier:(__bridge NSString *)kUTTypePNG]) { mimeType = @"image/png"; - - if (rotated) - { - // Update imageData - imageData = UIImagePNGRepresentation(image); - } - } - else if ([itemProvider hasItemConformingToTypeIdentifier:(__bridge NSString *)kUTTypeJPEG]) - { - mimeType = @"image/jpeg"; - - if (rotated) - { - // Update imageData - imageData = UIImageJPEGRepresentation(image, 1.0); - } + imageData = UIImagePNGRepresentation(image); } else { - imageData = UIImageJPEGRepresentation(image, 1.0); - image = [[UIImage alloc] initWithData:imageData]; + // Use jpeg format by default. mimeType = @"image/jpeg"; + imageData = UIImageJPEGRepresentation(image, 0.9); } UIImage *thumbnail = nil; @@ -556,8 +610,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) if (weakSelf) { typeof(self) self = weakSelf; - [self suspendSession]; - [self.shareExtensionContext completeRequestReturningItems:@[extensionItem] completionHandler:nil]; + [self completeRequestReturningItems:@[extensionItem] completionHandler:nil]; } } failure:^(NSError *error) { NSLog(@"[ShareExtensionManager] sendImage failed."); @@ -597,8 +650,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) if (weakSelf) { typeof(self) self = weakSelf; - [self suspendSession]; - [self.shareExtensionContext completeRequestReturningItems:@[extensionItem] completionHandler:nil]; + [self completeRequestReturningItems:@[extensionItem] completionHandler:nil]; } } failure:^(NSError *error) { NSLog(@"[ShareExtensionManager] sendVideo failed."); diff --git a/RiotShareExtension/ViewController/SharePresentingViewController.h b/RiotShareExtension/ViewController/SharePresentingViewController.h index 430f568ba..c9eff12d0 100644 --- a/RiotShareExtension/ViewController/SharePresentingViewController.h +++ b/RiotShareExtension/ViewController/SharePresentingViewController.h @@ -18,4 +18,6 @@ @interface SharePresentingViewController : UIViewController +- (void)destroy; + @end diff --git a/RiotShareExtension/ViewController/SharePresentingViewController.m b/RiotShareExtension/ViewController/SharePresentingViewController.m index 081c328b7..13c7390e8 100644 --- a/RiotShareExtension/ViewController/SharePresentingViewController.m +++ b/RiotShareExtension/ViewController/SharePresentingViewController.m @@ -20,6 +20,8 @@ @interface SharePresentingViewController () +@property (nonatomic) ShareViewController *shareViewController; + @end @implementation SharePresentingViewController @@ -28,21 +30,33 @@ { [super viewDidLoad]; - [ShareExtensionManager sharedManager].shareExtensionContext = self.extensionContext; + ShareExtensionManager *sharedManager = [ShareExtensionManager sharedManager]; + + sharedManager.primaryViewController = self; + sharedManager.shareExtensionContext = self.extensionContext; [self presentShareViewController]; } +- (void)destroy +{ + if (self.shareViewController) + { + [self.shareViewController destroy]; + self.shareViewController = nil; + } +} + - (void)presentShareViewController { - ShareViewController *shareViewController = [[ShareViewController alloc] init]; + self.shareViewController = [[ShareViewController alloc] init]; - shareViewController.providesPresentationContextTransitionStyle = YES; - shareViewController.definesPresentationContext = YES; - shareViewController.modalPresentationStyle = UIModalPresentationOverFullScreen; - shareViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; + self.shareViewController.providesPresentationContextTransitionStyle = YES; + self.shareViewController.definesPresentationContext = YES; + self.shareViewController.modalPresentationStyle = UIModalPresentationOverFullScreen; + self.shareViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; - [self presentViewController:shareViewController animated:YES completion:nil]; + [self presentViewController:self.shareViewController animated:YES completion:nil]; } - (void)didReceiveMemoryWarning diff --git a/RiotShareExtension/ViewController/ShareViewController.h b/RiotShareExtension/ViewController/ShareViewController.h index a7ce75c4d..74e7af78a 100644 --- a/RiotShareExtension/ViewController/ShareViewController.h +++ b/RiotShareExtension/ViewController/ShareViewController.h @@ -14,7 +14,6 @@ limitations under the License. */ -#import #import #import diff --git a/RiotShareExtension/ViewController/ShareViewController.m b/RiotShareExtension/ViewController/ShareViewController.m index 781a51525..e02961a28 100644 --- a/RiotShareExtension/ViewController/ShareViewController.m +++ b/RiotShareExtension/ViewController/ShareViewController.m @@ -30,6 +30,8 @@ @property (nonatomic) SegmentedViewController *segmentedViewController; +@property (nonatomic) id shareExtensionManagerDidChangeMXSessionObserver; + @end @@ -42,7 +44,7 @@ { [super viewDidLoad]; - [[NSNotificationCenter defaultCenter] addObserverForName:kShareExtensionManagerDidChangeMXSessionNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { + self.shareExtensionManagerDidChangeMXSessionObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kShareExtensionManagerDidChangeMXSessionNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { [self configureViews]; @@ -51,12 +53,21 @@ [self configureViews]; } -#pragma mark - Private - -- (void)configureViews +- (void)destroy { - self.masterContainerView.layer.cornerRadius = 7; + [super destroy]; + if (self.shareExtensionManagerDidChangeMXSessionObserver) + { + [[NSNotificationCenter defaultCenter] removeObserver:self.shareExtensionManagerDidChangeMXSessionObserver]; + self.shareExtensionManagerDidChangeMXSessionObserver = nil; + } + + [self resetContentView]; +} + +- (void)resetContentView +{ // Empty the content view NSArray *subviews = self.contentView.subviews; for (UIView *subview in subviews) @@ -67,10 +78,21 @@ // Release the current segmented view controller if any if (self.segmentedViewController) { + [self.segmentedViewController removeFromParentViewController]; + // Release correctly all the existing data source and view controllers. [self.segmentedViewController destroy]; self.segmentedViewController = nil; } +} + +#pragma mark - Private + +- (void)configureViews +{ + self.masterContainerView.layer.cornerRadius = 7; + + [self resetContentView]; if ([ShareExtensionManager sharedManager].mxSession) { diff --git a/SiriIntents/Info.plist b/SiriIntents/Info.plist new file mode 100644 index 000000000..9fa5f075e --- /dev/null +++ b/SiriIntents/Info.plist @@ -0,0 +1,41 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + SiriIntents + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSExtension + + NSExtensionAttributes + + IntentsRestrictedWhileLocked + + IntentsSupported + + INStartAudioCallIntent + INStartVideoCallIntent + + + NSExtensionPointIdentifier + com.apple.intents-service + NSExtensionPrincipalClass + IntentHandler + + + diff --git a/SiriIntents/IntentHandler.h b/SiriIntents/IntentHandler.h new file mode 100644 index 000000000..66da341a6 --- /dev/null +++ b/SiriIntents/IntentHandler.h @@ -0,0 +1,21 @@ +/* + Copyright 2017 Vector Creations 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 + +@interface IntentHandler : INExtension + +@end diff --git a/SiriIntents/IntentHandler.m b/SiriIntents/IntentHandler.m new file mode 100644 index 000000000..5031ac70a --- /dev/null +++ b/SiriIntents/IntentHandler.m @@ -0,0 +1,302 @@ +/* + Copyright 2017 Vector Creations 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 "IntentHandler.h" + +#import "MXKAccount.h" +#import "MXKAccountManager.h" +#import "MXFileStore.h" +#import "MXSession.h" + +@interface IntentHandler () + +@end + +@implementation IntentHandler + +- (instancetype)init +{ + self = [super init]; + if (self) + { + [MXSDKOptions sharedInstance].applicationGroupIdentifier = @"group.im.vector"; + } + return self; +} + +- (id)handlerForIntent:(INIntent *)intent +{ + return self; +} + +#pragma mark - INStartAudioCallIntentHandling + +- (void)resolveContactsForStartAudioCall:(INStartAudioCallIntent *)intent withCompletion:(void (^)(NSArray * _Nonnull))completion +{ + [self resolveContacts:intent.contacts withCompletion:completion]; +} + +- (void)confirmStartAudioCall:(INStartAudioCallIntent *)intent completion:(void (^)(INStartAudioCallIntentResponse * _Nonnull))completion +{ + INStartAudioCallIntentResponse *response = nil; + + MXKAccount *account = [MXKAccountManager sharedManager].activeAccounts.firstObject; + if (account) + { +#if defined MX_CALL_STACK_OPENWEBRTC || defined MX_CALL_STACK_ENDPOINT || defined MX_CALL_STACK_JINGLE + NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass(INStartAudioCallIntent.class)]; + response = [[INStartAudioCallIntentResponse alloc] initWithCode:INStartAudioCallIntentResponseCodeReady userActivity:userActivity]; +#else + response = [[INStartAudioCallIntentResponse alloc] initWithCode:INStartAudioCallIntentResponseCodeFailureCallingServiceNotAvailable userActivity:nil]; +#endif + } + else + { + // User hasn't logged in + response = [[INStartAudioCallIntentResponse alloc] initWithCode:INStartAudioCallIntentResponseCodeFailureAppConfigurationRequired userActivity:nil]; + } + + completion(response); +} + +- (void)handleStartAudioCall:(INStartAudioCallIntent *)intent completion:(void (^)(INStartAudioCallIntentResponse * _Nonnull))completion +{ + INStartAudioCallIntentResponse *response = nil; + + INPerson *person = intent.contacts.firstObject; + if (person && person.customIdentifier) + { + NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass(INStartAudioCallIntent.class)]; + userActivity.userInfo = @{ @"roomID" : person.customIdentifier }; + + response = [[INStartAudioCallIntentResponse alloc] initWithCode:INStartAudioCallIntentResponseCodeContinueInApp + userActivity:userActivity]; + } + else + { + response = [[INStartAudioCallIntentResponse alloc] initWithCode:INStartAudioCallIntentResponseCodeFailure userActivity:nil]; + } + + completion(response); +} + +#pragma mark - INStartVideoCallIntentHandling + +- (void)resolveContactsForStartVideoCall:(INStartVideoCallIntent *)intent withCompletion:(void (^)(NSArray * _Nonnull))completion +{ + [self resolveContacts:intent.contacts withCompletion:completion]; +} + +- (void)confirmStartVideoCall:(INStartVideoCallIntent *)intent completion:(void (^)(INStartVideoCallIntentResponse * _Nonnull))completion +{ + INStartVideoCallIntentResponse *response = nil; + + MXKAccount *account = [MXKAccountManager sharedManager].activeAccounts.firstObject; + if (account) + { +#if defined MX_CALL_STACK_OPENWEBRTC || defined MX_CALL_STACK_ENDPOINT || defined MX_CALL_STACK_JINGLE + NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass(INStartVideoCallIntent.class)]; + response = [[INStartVideoCallIntentResponse alloc] initWithCode:INStartVideoCallIntentResponseCodeReady userActivity:userActivity]; +#else + response = [[INStartVideoCallIntentResponse alloc] initWithCode:INStartVideoCallIntentResponseCodeFailureCallingServiceNotAvailable userActivity:nil]; +#endif + } + else + { + // User hasn't logged in + response = [[INStartVideoCallIntentResponse alloc] initWithCode:INStartVideoCallIntentResponseCodeFailureRequiringAppLaunch userActivity:nil]; + } + + completion(response); +} + +- (void)handleStartVideoCall:(INStartVideoCallIntent *)intent completion:(void (^)(INStartVideoCallIntentResponse * _Nonnull))completion +{ + INStartVideoCallIntentResponse *response = nil; + + INPerson *person = intent.contacts.firstObject; + if (person && person.customIdentifier) + { + NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass(INStartVideoCallIntent.class)]; + userActivity.userInfo = @{ @"roomID" : person.customIdentifier }; + + response = [[INStartVideoCallIntentResponse alloc] initWithCode:INStartVideoCallIntentResponseCodeContinueInApp + userActivity:userActivity]; + } + else + { + response = [[INStartVideoCallIntentResponse alloc] initWithCode:INStartVideoCallIntentResponseCodeFailure userActivity:nil]; + } + + completion(response); +} + +#pragma mark - Private + +- (void)resolveContacts:(nullable NSArray *)contacts withCompletion:(void (^)(NSArray * _Nonnull))completion +{ + if (contacts.count == 0) + { + completion(@[[INPersonResolutionResult needsValue]]); + return; + } + else + { + // We don't iterate over array of contacts from passed intent + // since it's hard to imagine scenario with several callee + // so we just extract the first one + INPerson *callee = contacts.firstObject; + + // If this method is called after selection of the appropriate user, it will hold userId of an user to whom we must call + NSString *selectedUserId; + + // Check if the user has selected right room among several direct rooms from previous resolution process run + if (callee.customIdentifier.length) + { + // If callee will have the same name as one of the contact in the system contacts app + // Siri will pass us this contact in the intent.contacts array and we must provide the same count of + // resolution results as elements count in the intent.contact. + // So we just pass the same result at all iterations + NSMutableArray *resolutionResults = [NSMutableArray array]; + for (NSInteger i = 0; i < contacts.count; ++i) + [resolutionResults addObject:[INPersonResolutionResult successWithResolvedPerson:callee]]; + completion(resolutionResults); + return; + } + else + { + // This resolution process run after selecting appropriate user among suggested user list + selectedUserId = callee.personHandle.value; + } + + MXKAccount *account = [MXKAccountManager sharedManager].activeAccounts.firstObject; + if (account) + { + MXFileStore *fileStore = [[MXFileStore alloc] initWithCredentials:account.mxCredentials]; + [fileStore asyncRoomsSummaries:^(NSArray * _Nonnull roomsSummaries) { + + // Contains userIds of all users with whom the current user has direct chats + // Use set to avoid duplicates + NSMutableSet *directUserIds = [NSMutableSet set]; + + // Contains room summaries for all direct rooms connected with particular userId + NSMutableDictionary *> *roomSummaries = [NSMutableDictionary dictionary]; + + for (MXRoomSummary *summary in roomsSummaries) + { + // TODO: We also need to check if joined room members count equals 2 + // It is pointlessly to save rooms with 1 joined member or room with more than 2 joined members + if (summary.isDirect) + { + NSString *diretUserId = summary.directUserId; + + // Collect room summaries only for specified user + if (selectedUserId && ![diretUserId isEqualToString:selectedUserId]) + continue; + + // Save userId + [directUserIds addObject:diretUserId]; + + // Save associated with diretUserId room summary + NSMutableArray *userRoomSummaries = roomSummaries[diretUserId]; + if (userRoomSummaries) + [userRoomSummaries addObject:summary]; + else + roomSummaries[diretUserId] = [NSMutableArray arrayWithObject:summary]; + } + } + + [fileStore asyncUsersWithUserIds:directUserIds.allObjects success:^(NSArray * _Nonnull users) { + + // Find users whose display name contains string presented us by Siri + NSMutableArray *matchingUsers = [NSMutableArray array]; + for (MXUser *user in users) + { + if (!user.displayname) + continue; + + if (!NSEqualRanges([callee.displayName rangeOfString:user.displayname options:NSCaseInsensitiveSearch], (NSRange){NSNotFound,0})) + { + [matchingUsers addObject:user]; + } + } + + NSMutableArray *persons = [NSMutableArray array]; + + if (matchingUsers.count == 1) + { + MXUser *user = matchingUsers.firstObject; + + // Provide to the user a list of direct rooms to choose from + NSArray *summaries = roomSummaries[user.userId]; + for (MXRoomSummary *summary in summaries) + { + INPersonHandle *personHandle = [[INPersonHandle alloc] initWithValue:user.userId type:INPersonHandleTypeUnknown]; + + // For rooms we try to use room display name + NSString *displayName = summary.displayname ? summary.displayname : user.displayname; + + INPerson *person = [[INPerson alloc] initWithPersonHandle:personHandle + nameComponents:nil + displayName:displayName + image:nil + contactIdentifier:nil + customIdentifier:summary.roomId]; + + [persons addObject:person]; + } + } + else if (matchingUsers.count > 1) + { + // Provide to the user a list of users to choose from + // This is the case when there are several users with the same name + for (MXUser *user in matchingUsers) + { + INPersonHandle *personHandle = [[INPersonHandle alloc] initWithValue:user.userId type:INPersonHandleTypeUnknown]; + INPerson *person = [[INPerson alloc] initWithPersonHandle:personHandle + nameComponents:nil + displayName:user.displayname + image:nil + contactIdentifier:nil + customIdentifier:nil]; + + [persons addObject:person]; + } + } + + if (persons.count == 0) + { + completion(@[[INPersonResolutionResult unsupported]]); + } + else if (persons.count == 1) + { + completion(@[[INPersonResolutionResult successWithResolvedPerson:persons.firstObject]]); + } + else + { + completion(@[[INPersonResolutionResult disambiguationWithPeopleToDisambiguate:persons]]); + } + } failure:nil]; + } failure:nil]; + } + else + { + completion(@[[INPersonResolutionResult notRequired]]); + } + } +} + +@end diff --git a/SiriIntents/SiriIntents.entitlements b/SiriIntents/SiriIntents.entitlements new file mode 100644 index 000000000..e540aaaec --- /dev/null +++ b/SiriIntents/SiriIntents.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.im.vector + + +