diff --git a/CHANGES.rst b/CHANGES.rst index add1f37257..9927db0191 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,9 +1,35 @@ +Changes in 0.10.4 (2019-xx-xx) +=============================================== + + +Changes in 0.10.3 (2019-12-05) +=============================================== + +Improvements: + * Upgrade MatrixKit version ([v0.11.3](https://github.com/matrix-org/matrix-ios-kit/releases/tag/v0.11.3)). + * Integrations: Use the integrations manager provided by the homeserver admin via .well-known (#2815). + * i18n: Add Welsh (cy). + * i18n: Add Italian (it). + * SerializationService: Add deserialisation of Any. + * RiotSharedSettings: New class to handle user settings shared accross Riot apps. + * Widgets: Check user permission before opening a widget (#2833). + * Widgets: Check user permission before opening jitsi (#2842). + * Widgets: Add a contextual menu to refresh, open outside, remove and revoke the permission (#2834). + * Settings: Add an option for disabling use of the integration manager (#2843). + * Jitsi: Display room name, user name and user avatar in the conference screen. + * Improve UNNotificationSound compatibility with MA4 (IMA/ADPCM) file, thanks to @pixlwave (PR #2847). + +Bug fix: + * Accessibility: Make checkboxes accessible in terms of service screen. + * RoomVC: Tapping on location links gives 'unable to open link' (#2803). + * RoomVC: Reply to links fail with 'unable to open link' (#2804). + Changes in 0.10.2 (2019-11-15) =============================================== Bug fix: * Integrations: Fix terms consent display when they are required. - + Changes in 0.10.1 (2019-11-06) =============================================== diff --git a/Podfile b/Podfile index 65620847e8..08e25c09e5 100644 --- a/Podfile +++ b/Podfile @@ -7,7 +7,7 @@ use_frameworks! # Different flavours of pods to MatrixKit # The current MatrixKit pod version -$matrixKitVersion = '0.11.2' +$matrixKitVersion = '0.11.3' # The develop branch version #$matrixKitVersion = 'develop' @@ -64,7 +64,7 @@ abstract_target 'RiotPods' do pod 'SwiftUTI', :git => 'https://github.com/speramusinc/SwiftUTI.git', :branch => 'master' # Piwik for analytics - pod 'MatomoTracker', '~> 6.0.1' + pod 'MatomoTracker', '~> 7.2.0' # Remove warnings from "bad" pods pod 'OLMKit', :inhibit_warnings => true diff --git a/Podfile.lock b/Podfile.lock index adeb4d2525..bc0650dbad 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -46,44 +46,44 @@ PODS: - JitsiMeetSDK (2.3.1) - libbase58 (0.1.4) - libPhoneNumber-iOS (0.9.15) - - MatomoTracker (6.0.1): - - MatomoTracker/Core (= 6.0.1) - - MatomoTracker/Core (6.0.1) - - MatrixKit (0.11.2): + - MatomoTracker (7.2.0): + - MatomoTracker/Core (= 7.2.0) + - MatomoTracker/Core (7.2.0) + - MatrixKit (0.11.3): - cmark (~> 0.24.1) - DTCoreText (~> 1.6.21) - HPGrowingTextView (~> 1.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixKit/Core (= 0.11.2) - - MatrixSDK (= 0.15.0) + - MatrixKit/Core (= 0.11.3) + - MatrixSDK (= 0.15.2) - SwiftUTI (~> 1.0.6) - - MatrixKit/AppExtension (0.11.2): + - MatrixKit/AppExtension (0.11.3): - cmark (~> 0.24.1) - DTCoreText (~> 1.6.21) - DTCoreText/Extension - HPGrowingTextView (~> 1.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.15.0) + - MatrixSDK (= 0.15.2) - SwiftUTI (~> 1.0.6) - - MatrixKit/Core (0.11.2): + - MatrixKit/Core (0.11.3): - cmark (~> 0.24.1) - DTCoreText (~> 1.6.21) - HPGrowingTextView (~> 1.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.15.0) + - MatrixSDK (= 0.15.2) - SwiftUTI (~> 1.0.6) - - MatrixSDK (0.15.0): - - MatrixSDK/Core (= 0.15.0) - - MatrixSDK/Core (0.15.0): + - MatrixSDK (0.15.2): + - MatrixSDK/Core (= 0.15.2) + - MatrixSDK/Core (0.15.2): - AFNetworking (~> 3.2.0) - GZIP (~> 1.2.2) - libbase58 (~> 0.1.4) - OLMKit (~> 3.1.0) - Realm (~> 3.17.3) - - MatrixSDK/JingleCallStack (0.15.0): + - MatrixSDK/JingleCallStack (0.15.2): - JitsiMeetSDK (~> 2.3.1) - MatrixSDK/Core - - MatrixSDK/SwiftSupport (0.15.0): + - MatrixSDK/SwiftSupport (0.15.2): - MatrixSDK/Core - OLMKit (3.1.0): - OLMKit/olmc (= 3.1.0) @@ -107,9 +107,9 @@ DEPENDENCIES: - cmark - DGCollectionViewLeftAlignFlowLayout (~> 1.0.4) - GBDeviceInfo (~> 6.3.0) - - MatomoTracker (~> 6.0.1) - - MatrixKit (= 0.11.2) - - MatrixKit/AppExtension (= 0.11.2) + - MatomoTracker (~> 7.2.0) + - MatrixKit (= 0.11.3) + - MatrixKit/AppExtension (= 0.11.3) - MatrixSDK/JingleCallStack - MatrixSDK/SwiftSupport - OLMKit @@ -164,9 +164,9 @@ SPEC CHECKSUMS: JitsiMeetSDK: 69e4978fbab21f9a535d1bec3b8d43721a4c72b2 libbase58: 7c040313537b8c44b6e2d15586af8e21f7354efd libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75 - MatomoTracker: 3ae4f65a1f5ace8043bda7244888fee28a734de5 - MatrixKit: 7f681c9086509a4f5e6e7172ce00f552cba1f8bc - MatrixSDK: 342384d62bac5d95a31a7dab79e5f687bd87ddca + MatomoTracker: 6f89e2561083685a360e223fb663e9ccd57c1d1a + MatrixKit: 5a20025b05490a70694b701e607ec75e0988af21 + MatrixSDK: f83bd3c5519c1cb9ac3998f6423574cf528f0250 OLMKit: 4ee0159d63feeb86d836fdcfefe418e163511639 Realm: 5a1d9d47bfc101dd597668b1a8af4288a2557f6d Reusable: 82be188f29d96dc5eff0db7b2393bcc08d2cdd5b @@ -175,6 +175,6 @@ SPEC CHECKSUMS: SwiftUTI: 917993c124f8eac25e88ced0202fc58d7eb50fa8 zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c -PODFILE CHECKSUM: 32e1f5dcb15429b0ecff04da8ebce193f145edb5 +PODFILE CHECKSUM: 881048fb17d68dd834b18e23929482600daca7f3 COCOAPODS: 1.8.4 diff --git a/Riot.xcodeproj/project.pbxproj b/Riot.xcodeproj/project.pbxproj index 453ca730d1..fd3d454aa7 100644 --- a/Riot.xcodeproj/project.pbxproj +++ b/Riot.xcodeproj/project.pbxproj @@ -70,6 +70,8 @@ 3275FD8C21A5A2C500B9C13D /* TermsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3275FD8B21A5A2C500B9C13D /* TermsView.swift */; }; 3281BCF72201FA4200F4A383 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3281BCF62201FA4200F4A383 /* UIControl.swift */; }; 3284A35120A07C210044F922 /* postMessageAPI.js in Resources */ = {isa = PBXBuildFile; fileRef = 3284A35020A07C210044F922 /* postMessageAPI.js */; }; + 32863A5A2384070300D07C4A /* RiotSharedSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32863A592384070300D07C4A /* RiotSharedSettings.swift */; }; + 32863A5C2384074C00D07C4A /* RiotSettingAllowedWidgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32863A5B2384074C00D07C4A /* RiotSettingAllowedWidgets.swift */; }; 32891D6B2264CBA300C82226 /* SimpleScreenTemplateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32891D692264CBA300C82226 /* SimpleScreenTemplateViewController.swift */; }; 32891D6C2264CBA300C82226 /* SimpleScreenTemplateViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 32891D6A2264CBA300C82226 /* SimpleScreenTemplateViewController.storyboard */; }; 32891D702264DF7B00C82226 /* DeviceVerificationVerifiedViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 32891D6E2264DF7B00C82226 /* DeviceVerificationVerifiedViewController.storyboard */; }; @@ -113,6 +115,7 @@ 32F6B96C2270623100BBA352 /* DeviceVerificationDataLoadingCoordinatorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F6B9662270623100BBA352 /* DeviceVerificationDataLoadingCoordinatorType.swift */; }; 32F6B96D2270623100BBA352 /* DeviceVerificationDataLoadingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F6B9672270623100BBA352 /* DeviceVerificationDataLoadingViewModel.swift */; }; 32F6B96E2270623100BBA352 /* DeviceVerificationDataLoadingViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F6B9682270623100BBA352 /* DeviceVerificationDataLoadingViewModelType.swift */; }; + 32FDC1CD2386CD390084717A /* RiotSettingIntegrationProvisioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32FDC1CC2386CD390084717A /* RiotSettingIntegrationProvisioning.swift */; }; 3AF393339D2D566CE14AC200 /* Pods_RiotTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 129EB7E27E7E4AC3F5F098F5 /* Pods_RiotTests.framework */; }; 405FD41D306133A48D9B5AA1 /* Pods_RiotPods_Riot.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1ACF09217ADF1D7E7A35BC02 /* Pods_RiotPods_Riot.framework */; }; 670966FEFE120D865FD8A5B6 /* Pods_RiotPods_SiriIntents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51187E952D5CECF6D6F5A28E /* Pods_RiotPods_SiriIntents.framework */; }; @@ -157,12 +160,15 @@ B110872421F098F0003554A5 /* ActivityIndicatorView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B110872021F098EF003554A5 /* ActivityIndicatorView.xib */; }; B110872521F098F0003554A5 /* ActivityIndicatorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B110872121F098EF003554A5 /* ActivityIndicatorPresenter.swift */; }; B110872621F098F0003554A5 /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B110872221F098F0003554A5 /* ActivityIndicatorView.swift */; }; + B11291EA238D35590077B478 /* SlidingModalPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B11291E9238D35590077B478 /* SlidingModalPresentable.swift */; }; + B11291EC238D704C0077B478 /* FloatingPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = B11291EB238D704C0077B478 /* FloatingPoint.swift */; }; B120863722EF375F001F89E0 /* ReactionHistoryBridgeCoordinatorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B120863622EF375F001F89E0 /* ReactionHistoryBridgeCoordinatorPresenter.swift */; }; B125FE1B231D5BF200B72806 /* SettingsDiscoveryTableViewSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B125FE1A231D5BF200B72806 /* SettingsDiscoveryTableViewSection.swift */; }; B125FE1D231D5DE400B72806 /* SettingsDiscoveryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B125FE1C231D5DE400B72806 /* SettingsDiscoveryViewModel.swift */; }; B125FE1F231D5DF700B72806 /* SettingsDiscoveryViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B125FE1E231D5DF700B72806 /* SettingsDiscoveryViewModelType.swift */; }; B125FE21231D5E1D00B72806 /* SettingsDiscoveryViewAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B125FE20231D5E1D00B72806 /* SettingsDiscoveryViewAction.swift */; }; B125FE23231D5E4300B72806 /* SettingsDiscoveryViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B125FE22231D5E4300B72806 /* SettingsDiscoveryViewState.swift */; }; + B12C56EF2396CB5E00FAC6DE /* RoomMessageURLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B12C56EE2396CB5E00FAC6DE /* RoomMessageURLParser.swift */; }; B139C21B21FE5B9200BB68EC /* KeyBackupRecoverFromPassphraseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B139C21A21FE5B9100BB68EC /* KeyBackupRecoverFromPassphraseViewModel.swift */; }; B139C21D21FE5BF500BB68EC /* KeyBackupRecoverFromPassphraseViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B139C21C21FE5BF500BB68EC /* KeyBackupRecoverFromPassphraseViewModelType.swift */; }; B139C21F21FE5D6600BB68EC /* KeyBackupRecoverFromPassphraseViewAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B139C21E21FE5D6600BB68EC /* KeyBackupRecoverFromPassphraseViewAction.swift */; }; @@ -249,6 +255,12 @@ B19EFA3B21F8BB4100FC070E /* KeyBackupRecoverCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B19EFA3A21F8BB4100FC070E /* KeyBackupRecoverCoordinator.swift */; }; B1A5B33E227ADF2A004CBA85 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A5B33D227ADF2A004CBA85 /* UIImage.swift */; }; B1A68593229E807A00D6C09A /* RoomBubbleCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A68592229E807800D6C09A /* RoomBubbleCellLayout.swift */; }; + B1A6C10723881EF2002882FD /* SlidingModalPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A6C10623881EF2002882FD /* SlidingModalPresenter.swift */; }; + B1A6C109238828A6002882FD /* SlidingModalPresentationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A6C108238828A6002882FD /* SlidingModalPresentationDelegate.swift */; }; + B1A6C10B23882B6C002882FD /* SlidingModalPresentationAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A6C10A23882B6C002882FD /* SlidingModalPresentationAnimator.swift */; }; + B1A6C10D23882D1D002882FD /* SlidingModalPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A6C10C23882D1D002882FD /* SlidingModalPresentationController.swift */; }; + B1A6C111238BD236002882FD /* SlidingModalContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A6C110238BD236002882FD /* SlidingModalContainerView.swift */; }; + B1A6C113238BD245002882FD /* SlidingModalContainerView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B1A6C112238BD245002882FD /* SlidingModalContainerView.xib */; }; B1B12B2922942315002CB419 /* UITouch.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B12B2822942315002CB419 /* UITouch.swift */; }; B1B5571820EE6C4D00210D55 /* CountryPickerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = B1B5567A20EE6C4C00210D55 /* CountryPickerViewController.m */; }; B1B5571920EE6C4D00210D55 /* LanguagePickerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = B1B5567C20EE6C4C00210D55 /* LanguagePickerViewController.m */; }; @@ -501,6 +513,10 @@ B1B9DEF122EB396B0065E677 /* ReactionHistoryViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B9DEF022EB396B0065E677 /* ReactionHistoryViewData.swift */; }; B1B9DEF422EB426D0065E677 /* ReactionHistoryViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B9DEF222EB426D0065E677 /* ReactionHistoryViewCell.swift */; }; B1B9DEF522EB426D0065E677 /* ReactionHistoryViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = B1B9DEF322EB426D0065E677 /* ReactionHistoryViewCell.xib */; }; + B1BD71B5238DCBF700BA92E2 /* SlidingModalEmptyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1BD71B4238DCBF700BA92E2 /* SlidingModalEmptyViewController.swift */; }; + B1BD71BC238E8F9600BA92E2 /* WidgetPermissionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1BD71BA238E8F9600BA92E2 /* WidgetPermissionViewController.swift */; }; + B1BD71BF238EA56700BA92E2 /* WidgetPermissionViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B1BD71BE238EA56700BA92E2 /* WidgetPermissionViewController.storyboard */; }; + B1BD71C1238EA92100BA92E2 /* WidgetPermissionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1BD71C0238EA92000BA92E2 /* WidgetPermissionViewModel.swift */; }; B1C335CD22F1C1320021BA8D /* CameraPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C335CC22F1C1320021BA8D /* CameraPresenter.swift */; }; B1C3360122F1ED600021BA8D /* MediaPickerCoordinatorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C335FE22F1ED5F0021BA8D /* MediaPickerCoordinatorType.swift */; }; B1C3360222F1ED600021BA8D /* MediaPickerCoordinatorBridgePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C335FF22F1ED5F0021BA8D /* MediaPickerCoordinatorBridgePresenter.swift */; }; @@ -579,7 +595,7 @@ F083BD1E1E7009ED00A9B29C /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = F083BB0D1E7009EC00A9B29C /* AppDelegate.m */; }; F083BDE61E7009ED00A9B29C /* busy.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = F083BBDB1E7009EC00A9B29C /* busy.mp3 */; }; F083BDE71E7009ED00A9B29C /* callend.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = F083BBDC1E7009EC00A9B29C /* callend.mp3 */; }; - F083BDE81E7009ED00A9B29C /* message.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = F083BBDD1E7009EC00A9B29C /* message.mp3 */; }; + F083BDE81E7009ED00A9B29C /* message.caf in Resources */ = {isa = PBXBuildFile; fileRef = F083BBDD1E7009EC00A9B29C /* message.caf */; }; F083BDE91E7009ED00A9B29C /* ring.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = F083BBDE1E7009EC00A9B29C /* ring.mp3 */; }; F083BDEA1E7009ED00A9B29C /* ringback.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = F083BBDF1E7009EC00A9B29C /* ringback.mp3 */; }; F083BDED1E7009ED00A9B29C /* MXKRoomBubbleTableViewCell+Riot.m in Sources */ = {isa = PBXBuildFile; fileRef = F083BBE61E7009EC00A9B29C /* MXKRoomBubbleTableViewCell+Riot.m */; }; @@ -696,6 +712,9 @@ 324A204C225FC571004FE8B0 /* DeviceVerificationIncomingCoordinatorType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceVerificationIncomingCoordinatorType.swift; sourceTree = ""; }; 324A204D225FC571004FE8B0 /* DeviceVerificationIncomingViewModelType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceVerificationIncomingViewModelType.swift; sourceTree = ""; }; 324A204E225FC571004FE8B0 /* DeviceVerificationIncomingCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceVerificationIncomingCoordinator.swift; sourceTree = ""; }; + 325789A5237AB241009388E6 /* cy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cy; path = cy.lproj/InfoPlist.strings; sourceTree = ""; }; + 325789A6237AB27F009388E6 /* cy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cy; path = cy.lproj/Localizable.strings; sourceTree = ""; }; + 325789A7237AB297009388E6 /* cy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cy; path = cy.lproj/Vector.strings; sourceTree = ""; }; 3267EFB320E379FD00FF1CAA /* CHANGES.rst */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = CHANGES.rst; sourceTree = ""; }; 3267EFB420E379FD00FF1CAA /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; fileEncoding = 4; path = Podfile; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; 3267EFB520E379FD00FF1CAA /* AUTHORS.rst */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = AUTHORS.rst; sourceTree = ""; }; @@ -703,6 +722,8 @@ 3275FD8B21A5A2C500B9C13D /* TermsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsView.swift; sourceTree = ""; }; 3281BCF62201FA4200F4A383 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = ""; }; 3284A35020A07C210044F922 /* postMessageAPI.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = postMessageAPI.js; sourceTree = ""; }; + 32863A592384070300D07C4A /* RiotSharedSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RiotSharedSettings.swift; sourceTree = ""; }; + 32863A5B2384074C00D07C4A /* RiotSettingAllowedWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RiotSettingAllowedWidgets.swift; sourceTree = ""; }; 32891D692264CBA300C82226 /* SimpleScreenTemplateViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleScreenTemplateViewController.swift; sourceTree = ""; }; 32891D6A2264CBA300C82226 /* SimpleScreenTemplateViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = SimpleScreenTemplateViewController.storyboard; sourceTree = ""; }; 32891D6E2264DF7B00C82226 /* DeviceVerificationVerifiedViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = DeviceVerificationVerifiedViewController.storyboard; sourceTree = ""; }; @@ -758,6 +779,7 @@ 32F6B9662270623100BBA352 /* DeviceVerificationDataLoadingCoordinatorType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceVerificationDataLoadingCoordinatorType.swift; sourceTree = ""; }; 32F6B9672270623100BBA352 /* DeviceVerificationDataLoadingViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceVerificationDataLoadingViewModel.swift; sourceTree = ""; }; 32F6B9682270623100BBA352 /* DeviceVerificationDataLoadingViewModelType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceVerificationDataLoadingViewModelType.swift; sourceTree = ""; }; + 32FDC1CC2386CD390084717A /* RiotSettingIntegrationProvisioning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RiotSettingIntegrationProvisioning.swift; sourceTree = ""; }; 3942DD65EBEB7AE647C6392A /* Pods-RiotPods-SiriIntents.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RiotPods-SiriIntents.debug.xcconfig"; path = "Target Support Files/Pods-RiotPods-SiriIntents/Pods-RiotPods-SiriIntents.debug.xcconfig"; sourceTree = ""; }; 3D78489021AC9E6400B98A7D /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = ""; }; 3D78489121AC9E6500B98A7D /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; @@ -815,12 +837,15 @@ B110872021F098EF003554A5 /* ActivityIndicatorView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ActivityIndicatorView.xib; sourceTree = ""; }; B110872121F098EF003554A5 /* ActivityIndicatorPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorPresenter.swift; sourceTree = ""; }; B110872221F098F0003554A5 /* ActivityIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorView.swift; sourceTree = ""; }; + B11291E9238D35590077B478 /* SlidingModalPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlidingModalPresentable.swift; sourceTree = ""; }; + B11291EB238D704C0077B478 /* FloatingPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPoint.swift; sourceTree = ""; }; B120863622EF375F001F89E0 /* ReactionHistoryBridgeCoordinatorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionHistoryBridgeCoordinatorPresenter.swift; sourceTree = ""; }; B125FE1A231D5BF200B72806 /* SettingsDiscoveryTableViewSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDiscoveryTableViewSection.swift; sourceTree = ""; }; B125FE1C231D5DE400B72806 /* SettingsDiscoveryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDiscoveryViewModel.swift; sourceTree = ""; }; B125FE1E231D5DF700B72806 /* SettingsDiscoveryViewModelType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDiscoveryViewModelType.swift; sourceTree = ""; }; B125FE20231D5E1D00B72806 /* SettingsDiscoveryViewAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDiscoveryViewAction.swift; sourceTree = ""; }; B125FE22231D5E4300B72806 /* SettingsDiscoveryViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDiscoveryViewState.swift; sourceTree = ""; }; + B12C56EE2396CB5E00FAC6DE /* RoomMessageURLParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageURLParser.swift; sourceTree = ""; }; B139C21A21FE5B9100BB68EC /* KeyBackupRecoverFromPassphraseViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBackupRecoverFromPassphraseViewModel.swift; sourceTree = ""; }; B139C21C21FE5BF500BB68EC /* KeyBackupRecoverFromPassphraseViewModelType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBackupRecoverFromPassphraseViewModelType.swift; sourceTree = ""; }; B139C21E21FE5D6600BB68EC /* KeyBackupRecoverFromPassphraseViewAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBackupRecoverFromPassphraseViewAction.swift; sourceTree = ""; }; @@ -956,6 +981,12 @@ B19EFA3A21F8BB4100FC070E /* KeyBackupRecoverCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBackupRecoverCoordinator.swift; sourceTree = ""; }; B1A5B33D227ADF2A004CBA85 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; B1A68592229E807800D6C09A /* RoomBubbleCellLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomBubbleCellLayout.swift; sourceTree = ""; }; + B1A6C10623881EF2002882FD /* SlidingModalPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlidingModalPresenter.swift; sourceTree = ""; }; + B1A6C108238828A6002882FD /* SlidingModalPresentationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlidingModalPresentationDelegate.swift; sourceTree = ""; }; + B1A6C10A23882B6C002882FD /* SlidingModalPresentationAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlidingModalPresentationAnimator.swift; sourceTree = ""; }; + B1A6C10C23882D1D002882FD /* SlidingModalPresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlidingModalPresentationController.swift; sourceTree = ""; }; + B1A6C110238BD236002882FD /* SlidingModalContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlidingModalContainerView.swift; sourceTree = ""; }; + B1A6C112238BD245002882FD /* SlidingModalContainerView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SlidingModalContainerView.xib; sourceTree = ""; }; B1B12B2822942315002CB419 /* UITouch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITouch.swift; sourceTree = ""; }; B1B5567920EE6C4C00210D55 /* CountryPickerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CountryPickerViewController.h; sourceTree = ""; }; B1B5567A20EE6C4C00210D55 /* CountryPickerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountryPickerViewController.m; sourceTree = ""; }; @@ -1344,6 +1375,10 @@ B1B9DEF022EB396B0065E677 /* ReactionHistoryViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionHistoryViewData.swift; sourceTree = ""; }; B1B9DEF222EB426D0065E677 /* ReactionHistoryViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionHistoryViewCell.swift; sourceTree = ""; }; B1B9DEF322EB426D0065E677 /* ReactionHistoryViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ReactionHistoryViewCell.xib; sourceTree = ""; }; + B1BD71B4238DCBF700BA92E2 /* SlidingModalEmptyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlidingModalEmptyViewController.swift; sourceTree = ""; }; + B1BD71BA238E8F9600BA92E2 /* WidgetPermissionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetPermissionViewController.swift; sourceTree = ""; }; + B1BD71BE238EA56700BA92E2 /* WidgetPermissionViewController.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = WidgetPermissionViewController.storyboard; sourceTree = ""; }; + B1BD71C0238EA92000BA92E2 /* WidgetPermissionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetPermissionViewModel.swift; sourceTree = ""; }; B1C335CC22F1C1320021BA8D /* CameraPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPresenter.swift; sourceTree = ""; }; B1C335FE22F1ED5F0021BA8D /* MediaPickerCoordinatorType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPickerCoordinatorType.swift; sourceTree = ""; }; B1C335FF22F1ED5F0021BA8D /* MediaPickerCoordinatorBridgePresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPickerCoordinatorBridgePresenter.swift; sourceTree = ""; }; @@ -1369,6 +1404,9 @@ B1C562E0228C7C8C0037F12A /* RoomContextualMenuViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = RoomContextualMenuViewController.storyboard; sourceTree = ""; }; B1C562E6228C7CF10037F12A /* ContextualMenuItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextualMenuItemView.swift; sourceTree = ""; }; B1C562E7228C7CF20037F12A /* ContextualMenuItemView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ContextualMenuItemView.xib; sourceTree = ""; }; + B1C6FFE723954CE70055347B /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = ""; }; + B1C6FFE823954D3B0055347B /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; + B1C6FFE923954D4B0055347B /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Vector.strings; sourceTree = ""; }; B1CA3A2621EF6913000D1D89 /* UIViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = ""; }; B1CA3A2821EF692B000D1D89 /* UIView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; }; B1CE9EFC22148703000FAE6A /* SignOutAlertPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignOutAlertPresenter.swift; sourceTree = ""; }; @@ -1426,7 +1464,7 @@ F083BB0D1E7009EC00A9B29C /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; F083BBDB1E7009EC00A9B29C /* busy.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = busy.mp3; sourceTree = ""; }; F083BBDC1E7009EC00A9B29C /* callend.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = callend.mp3; sourceTree = ""; }; - F083BBDD1E7009EC00A9B29C /* message.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = message.mp3; sourceTree = ""; }; + F083BBDD1E7009EC00A9B29C /* message.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = message.caf; sourceTree = ""; }; F083BBDE1E7009EC00A9B29C /* ring.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = ring.mp3; sourceTree = ""; }; F083BBDF1E7009EC00A9B29C /* ringback.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = ringback.mp3; sourceTree = ""; }; F083BBE51E7009EC00A9B29C /* MXKRoomBubbleTableViewCell+Riot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MXKRoomBubbleTableViewCell+Riot.h"; sourceTree = ""; }; @@ -1682,6 +1720,24 @@ path = Incoming; sourceTree = ""; }; + 32863A572384070300D07C4A /* Shared */ = { + isa = PBXGroup; + children = ( + 32863A582384070300D07C4A /* JSONModels */, + 32863A592384070300D07C4A /* RiotSharedSettings.swift */, + ); + path = Shared; + sourceTree = ""; + }; + 32863A582384070300D07C4A /* JSONModels */ = { + isa = PBXGroup; + children = ( + 32863A5B2384074C00D07C4A /* RiotSettingAllowedWidgets.swift */, + 32FDC1CC2386CD390084717A /* RiotSettingIntegrationProvisioning.swift */, + ); + path = JSONModels; + sourceTree = ""; + }; 32891D682264C6A000C82226 /* SimpleScreenTemplate */ = { isa = PBXGroup; children = ( @@ -1964,6 +2020,16 @@ path = ActivityIndicator; sourceTree = ""; }; + B11291ED238DC8C80077B478 /* WidgetPermission */ = { + isa = PBXGroup; + children = ( + B1BD71C0238EA92000BA92E2 /* WidgetPermissionViewModel.swift */, + B1BD71BA238E8F9600BA92E2 /* WidgetPermissionViewController.swift */, + B1BD71BE238EA56700BA92E2 /* WidgetPermissionViewController.storyboard */, + ); + path = WidgetPermission; + sourceTree = ""; + }; B125FE19231D5B5600B72806 /* Discovery */ = { isa = PBXGroup; children = ( @@ -1977,6 +2043,14 @@ path = Discovery; sourceTree = ""; }; + B12C56ED2396CB0100FAC6DE /* RoomMessageLinkParser */ = { + isa = PBXGroup; + children = ( + B12C56EE2396CB5E00FAC6DE /* RoomMessageURLParser.swift */, + ); + path = RoomMessageLinkParser; + sourceTree = ""; + }; B14F142522144F6400FA0595 /* RecoveryKey */ = { isa = PBXGroup; children = ( @@ -2321,9 +2395,25 @@ path = Riot/Modules/Common/CollectionView; sourceTree = SOURCE_ROOT; }; + B1A6C10523881ECB002882FD /* SlidingModal */ = { + isa = PBXGroup; + children = ( + B1A6C10623881EF2002882FD /* SlidingModalPresenter.swift */, + B11291E9238D35590077B478 /* SlidingModalPresentable.swift */, + B1A6C108238828A6002882FD /* SlidingModalPresentationDelegate.swift */, + B1A6C10A23882B6C002882FD /* SlidingModalPresentationAnimator.swift */, + B1A6C10C23882D1D002882FD /* SlidingModalPresentationController.swift */, + B1A6C110238BD236002882FD /* SlidingModalContainerView.swift */, + B1A6C112238BD245002882FD /* SlidingModalContainerView.xib */, + B1BD71B4238DCBF700BA92E2 /* SlidingModalEmptyViewController.swift */, + ); + path = SlidingModal; + sourceTree = ""; + }; B1B5567620EE6C4C00210D55 /* Modules */ = { isa = PBXGroup; children = ( + B1A6C10523881ECB002882FD /* SlidingModal */, 32DB556722FDADE50016329E /* ServiceTerms */, 3232AB94225730E100AD6A5C /* DeviceVerification */, B1B556EA20EE6C4C00210D55 /* Main */, @@ -2824,6 +2914,7 @@ children = ( B1B5576420EE702800210D55 /* IntegrationManagerViewController.h */, B1B5576020EE702800210D55 /* IntegrationManagerViewController.m */, + B11291ED238DC8C80077B478 /* WidgetPermission */, B1B5576120EE702800210D55 /* WidgetPicker */, B1B5576520EE702800210D55 /* Widgets */, ); @@ -3378,6 +3469,7 @@ B1B5597C20EFC3DF00210D55 /* Managers */ = { isa = PBXGroup; children = ( + B12C56ED2396CB0100FAC6DE /* RoomMessageLinkParser */, B1B9DED822E9B7120065E677 /* Serialization */, B1FDF56321F68C0700BA3834 /* PasswordStrength */, B1798300211B137B001FD722 /* OnBoarding */, @@ -3414,6 +3506,7 @@ B1B5598A20EFC42100210D55 /* Settings */ = { isa = PBXGroup; children = ( + 32863A572384070300D07C4A /* Shared */, B1B5597F20EFC3DF00210D55 /* RiotSettings.swift */, ); path = Settings; @@ -3646,7 +3739,7 @@ children = ( F083BBDB1E7009EC00A9B29C /* busy.mp3 */, F083BBDC1E7009EC00A9B29C /* callend.mp3 */, - F083BBDD1E7009EC00A9B29C /* message.mp3 */, + F083BBDD1E7009EC00A9B29C /* message.caf */, F083BBDE1E7009EC00A9B29C /* ring.mp3 */, F083BBDF1E7009EC00A9B29C /* ringback.mp3 */, ); @@ -3684,6 +3777,7 @@ B1C562CB228AB3510037F12A /* UIStackView.swift */, B1B12B2822942315002CB419 /* UITouch.swift */, B1DCC63322E72C1B00625807 /* UISearchBar.swift */, + B11291EB238D704C0077B478 /* FloatingPoint.swift */, ); path = Categories; sourceTree = ""; @@ -3913,6 +4007,8 @@ ja, hu, pl, + cy, + it, ); mainGroup = F094A9991B78D8F000B1FBBF; productRefGroup = F094A9A31B78D8F000B1FBBF /* Products */; @@ -3962,6 +4058,7 @@ B1B5592B20EF7A5D00210D55 /* TableViewCellWithButton.xib in Resources */, B1B5574620EE6C4D00210D55 /* StartChatViewController.xib in Resources */, B1B5590A20EF768F00210D55 /* RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.xib in Resources */, + B1A6C113238BD245002882FD /* SlidingModalContainerView.xib in Resources */, B1B5594420EF7BD000210D55 /* TableViewCellWithCollectionView.xib in Resources */, B1B558D520EF768F00210D55 /* RoomIncomingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.xib in Resources */, B1B5590020EF768F00210D55 /* RoomOutgoingTextMsgWithoutSenderNameBubbleCell.xib in Resources */, @@ -4015,7 +4112,7 @@ B1B558EC20EF768F00210D55 /* RoomMembershipCollapsedBubbleCell.xib in Resources */, B1B558D720EF768F00210D55 /* RoomOutgoingEncryptedTextMsgWithPaginationTitleBubbleCell.xib in Resources */, B1B5590820EF768F00210D55 /* RoomMembershipWithPaginationTitleBubbleCell.xib in Resources */, - F083BDE81E7009ED00A9B29C /* message.mp3 in Resources */, + F083BDE81E7009ED00A9B29C /* message.caf in Resources */, B1107ECA2200B09F0038014B /* KeyBackupRecoverSuccessViewController.storyboard in Resources */, B1B9DEF522EB426D0065E677 /* ReactionHistoryViewCell.xib in Resources */, B1B5579C20EF575B00210D55 /* ForgotPasswordInputsView.xib in Resources */, @@ -4045,6 +4142,7 @@ 3232AB4B2256558300AD6A5C /* TemplateScreenViewController.storyboard in Resources */, B1D1BDA822BBAFC900831367 /* ReactionsMenuView.xib in Resources */, 32B1FEDB21A46F2C00637127 /* TermsView.xib in Resources */, + B1BD71BF238EA56700BA92E2 /* WidgetPermissionViewController.storyboard in Resources */, B1DCC63222E7026F00625807 /* EmojiPickerHeaderView.xib in Resources */, B1B5578E20EF568D00210D55 /* GroupInviteTableViewCell.xib in Resources */, B1B5582020EF625800210D55 /* SimpleRoomTitleView.xib in Resources */, @@ -4345,6 +4443,7 @@ B1B558DE20EF768F00210D55 /* RoomIncomingAttachmentBubbleCell.m in Sources */, B1B5574820EE6C4D00210D55 /* PeopleViewController.m in Sources */, B1B5598720EFC3E000210D55 /* Widget.m in Sources */, + B1BD71C1238EA92100BA92E2 /* WidgetPermissionViewModel.swift in Sources */, B1B557E320EF60B900210D55 /* MessagesSearchResultAttachmentBubbleCell.m in Sources */, B1CE9F062216FB09000FAE6A /* EncryptionKeysExportPresenter.swift in Sources */, 3232ABAA225730E100AD6A5C /* DeviceVerificationCoordinatorBridgePresenter.swift in Sources */, @@ -4375,7 +4474,9 @@ B1963B2E228F1C4900CBA17F /* BubbleReactionViewData.swift in Sources */, B1C3361C22F32B4A0021BA8D /* SingleImagePickerPresenter.swift in Sources */, B1B5572F20EE6C4D00210D55 /* ReadReceiptsViewController.m in Sources */, + B1BD71BC238E8F9600BA92E2 /* WidgetPermissionViewController.swift in Sources */, B1B558CB20EF768F00210D55 /* RoomIncomingEncryptedTextMsgWithoutSenderInfoBubbleCell.m in Sources */, + B11291EA238D35590077B478 /* SlidingModalPresentable.swift in Sources */, B157FAA823264BED00EBFBD4 /* SettingsDiscoveryThreePidDetailsCoordinatorBridgePresenter.swift in Sources */, B169330B20F3CA3A00746532 /* Contact.m in Sources */, B1A5B33E227ADF2A004CBA85 /* UIImage.swift in Sources */, @@ -4396,6 +4497,7 @@ 32A6001E22C661100042C1D9 /* EditHistoryCoordinatorBridgePresenter.swift in Sources */, B1B5574A20EE6C4D00210D55 /* MediaPickerViewController.m in Sources */, B1B5598520EFC3E000210D55 /* RageShakeManager.m in Sources */, + B1A6C111238BD236002882FD /* SlidingModalContainerView.swift in Sources */, B1DCC62D22E61EAF00625807 /* EmojiPickerViewCell.swift in Sources */, 3232ABA8225730E100AD6A5C /* DeviceVerificationStartViewState.swift in Sources */, B1B558D420EF768F00210D55 /* RoomOutgoingEncryptedTextMsgWithoutSenderInfoBubbleCell.m in Sources */, @@ -4409,8 +4511,10 @@ B1CE9EFD22148703000FAE6A /* SignOutAlertPresenter.swift in Sources */, 32F6B9692270623100BBA352 /* DeviceVerificationDataLoadingCoordinator.swift in Sources */, B125FE1D231D5DE400B72806 /* SettingsDiscoveryViewModel.swift in Sources */, + 32863A5A2384070300D07C4A /* RiotSharedSettings.swift in Sources */, B1B5594720EF7BD000210D55 /* RoomCollectionViewCell.m in Sources */, B10CFBC32268D99D00A5842E /* JitsiService.swift in Sources */, + B1A6C10B23882B6C002882FD /* SlidingModalPresentationAnimator.swift in Sources */, B1B558C120EF768F00210D55 /* RoomIncomingEncryptedAttachmentWithPaginationTitleBubbleCell.m in Sources */, B1B5573E20EE6C4D00210D55 /* RiotNavigationController.m in Sources */, B1B5593B20EF7BAC00210D55 /* TableViewCellWithCheckBoxAndLabel.m in Sources */, @@ -4433,6 +4537,7 @@ 3232ABBC2257BE6500AD6A5C /* DeviceVerificationVerifyViewAction.swift in Sources */, F05927C91FDED836009F2A68 /* MXGroup+Riot.m in Sources */, B1B5594520EF7BD000210D55 /* TableViewCellWithCollectionView.m in Sources */, + B1A6C109238828A6002882FD /* SlidingModalPresentationDelegate.swift in Sources */, 32DB557722FDADE50016329E /* ServiceTermsModalCoordinator.swift in Sources */, 32DB557922FDADE50016329E /* ServiceTermsModalScreenViewModel.swift in Sources */, 32891D75226728EE00C82226 /* DeviceVerificationDataLoadingViewController.swift in Sources */, @@ -4483,6 +4588,7 @@ B139C21D21FE5BF500BB68EC /* KeyBackupRecoverFromPassphraseViewModelType.swift in Sources */, B157FA9F23264AE900EBFBD4 /* SettingsDiscoveryThreePidDetailsCoordinator.swift in Sources */, B1C45A8B232A8C2600165425 /* SettingsIdentityServerViewModel.swift in Sources */, + B12C56EF2396CB5E00FAC6DE /* RoomMessageURLParser.swift in Sources */, B1C45A86232A8C2600165425 /* SettingsIdentityServerViewModelType.swift in Sources */, F083BE031E7009ED00A9B29C /* EventFormatter.m in Sources */, B157FAA623264AE900EBFBD4 /* SettingsDiscoveryThreePidDetailsViewController.swift in Sources */, @@ -4510,6 +4616,7 @@ B14F143522144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyViewController.swift in Sources */, B1DCC61E22E5E17100625807 /* EmojiPickerViewModel.swift in Sources */, B1B5574F20EE6C4D00210D55 /* RoomsViewController.m in Sources */, + 32863A5C2384074C00D07C4A /* RiotSettingAllowedWidgets.swift in Sources */, B1B9DEDA22E9B7350065E677 /* SerializationService.swift in Sources */, B1B5572520EE6C4D00210D55 /* RoomMessagesSearchViewController.m in Sources */, B139C22121FE5D9D00BB68EC /* KeyBackupRecoverFromPassphraseViewState.swift in Sources */, @@ -4539,6 +4646,7 @@ B1B5572620EE6C4D00210D55 /* RoomFilesSearchViewController.m in Sources */, B1B5583120EF66BA00210D55 /* RoomIdOrAliasTableViewCell.m in Sources */, B1CA3A2921EF692B000D1D89 /* UIView.swift in Sources */, + B1A6C10D23882D1D002882FD /* SlidingModalPresentationController.swift in Sources */, 32A6001D22C661100042C1D9 /* EditHistoryCoordinatorType.swift in Sources */, F083BDFA1E7009ED00A9B29C /* RoomPreviewData.m in Sources */, B1B557B420EF5AEF00210D55 /* EventDetailsView.m in Sources */, @@ -4552,6 +4660,7 @@ B1B5574120EE6C4D00210D55 /* RecentsViewController.m in Sources */, B1D250D82118AA0A000F4E93 /* RoomPredecessorBubbleCell.m in Sources */, B1B5577120EE702800210D55 /* StickerPickerViewController.m in Sources */, + 32FDC1CD2386CD390084717A /* RiotSettingIntegrationProvisioning.swift in Sources */, B104C2942203773C00D9F496 /* KeyBackupBannerPreferences.swift in Sources */, B1B5572020EE6C4D00210D55 /* ContactsTableViewController.m in Sources */, B1B5581920EF625800210D55 /* RoomTitleView.m in Sources */, @@ -4566,6 +4675,7 @@ B1B5597520EFB02A00210D55 /* InviteRecentTableViewCell.m in Sources */, B1B5571E20EE6C4D00210D55 /* ContactDetailsViewController.m in Sources */, B1798302211B13B3001FD722 /* OnBoardingManager.swift in Sources */, + B1A6C10723881EF2002882FD /* SlidingModalPresenter.swift in Sources */, B1B5573520EE6C4D00210D55 /* GroupDetailsViewController.m in Sources */, B10B3B5B2201DD740072C76B /* KeyBackupBannerCell.swift in Sources */, B1DCC61A22E5E17100625807 /* EmojiPickerViewController.swift in Sources */, @@ -4629,6 +4739,7 @@ B1B557BF20EF5B4500210D55 /* DisabledRoomInputToolbarView.m in Sources */, B1B5578620EF564900210D55 /* GroupTableViewCellWithSwitch.m in Sources */, B1098BE821ECFE52000DDA48 /* Coordinator.swift in Sources */, + B11291EC238D704C0077B478 /* FloatingPoint.swift in Sources */, B1B557E920EF60F500210D55 /* MessagesSearchResultTextMsgBubbleCell.m in Sources */, 324A2050225FC571004FE8B0 /* DeviceVerificationIncomingViewController.swift in Sources */, B1098C0D21ED07E4000DDA48 /* NavigationRouter.swift in Sources */, @@ -4683,6 +4794,7 @@ 3232AB482256558300AD6A5C /* FlowTemplateCoordinatorType.swift in Sources */, B1B9DEF122EB396B0065E677 /* ReactionHistoryViewData.swift in Sources */, B1B558F820EF768F00210D55 /* RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m in Sources */, + B1BD71B5238DCBF700BA92E2 /* SlidingModalEmptyViewController.swift in Sources */, B125FE1B231D5BF200B72806 /* SettingsDiscoveryTableViewSection.swift in Sources */, 32242F0921E8B05F00725742 /* UIColor.swift in Sources */, B16932E720F3C37100746532 /* HomeMessagesSearchDataSource.m in Sources */, @@ -4756,6 +4868,8 @@ 3D78489221AC9E6500B98A7D /* ja */, 3D78489521ACA25300B98A7D /* hu */, 32DAF8DD231813E100654A44 /* pl */, + 325789A7237AB297009388E6 /* cy */, + B1C6FFE923954D4B0055347B /* it */, ); name = Vector.strings; sourceTree = ""; @@ -4779,6 +4893,8 @@ 3D78489021AC9E6400B98A7D /* ja */, 3D78489321ACA25200B98A7D /* hu */, 32DAF8DB231813C800654A44 /* pl */, + 325789A5237AB241009388E6 /* cy */, + B1C6FFE723954CE70055347B /* it */, ); name = InfoPlist.strings; sourceTree = ""; @@ -4802,6 +4918,8 @@ 3D78489121AC9E6500B98A7D /* ja */, 3D78489421ACA25300B98A7D /* hu */, 32DAF8DC231813D500654A44 /* pl */, + 325789A6237AB27F009388E6 /* cy */, + B1C6FFE823954D3B0055347B /* it */, ); name = Localizable.strings; sourceTree = ""; @@ -4829,6 +4947,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 9C3242E3FE95BCDA9562C75D /* Pods-RiotPods-RiotShareExtension.debug.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; APPLICATION_EXTENSION_API_ONLY = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ANALYZER_NONNULL = YES; @@ -4868,6 +4987,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 4FC6A5D63FAD1B27C2F57AFA /* Pods-RiotPods-RiotShareExtension.release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; APPLICATION_EXTENSION_API_ONLY = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ANALYZER_NONNULL = YES; @@ -4908,6 +5028,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 3942DD65EBEB7AE647C6392A /* Pods-RiotPods-SiriIntents.debug.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; @@ -4940,6 +5061,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = E2599D0ECB8DD206624E450B /* Pods-RiotPods-SiriIntents.release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; @@ -5079,6 +5201,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 43C2962BE367F59220F517FA /* Pods-RiotPods-Riot.debug.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Riot/SupportingFiles/Riot.entitlements; @@ -5112,6 +5235,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = B43DC75D1590BB8A4243BD4D /* Pods-RiotPods-Riot.release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Riot/SupportingFiles/Riot.entitlements; @@ -5143,7 +5267,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = BABB6681FBD79219B1213D6C /* Pods-RiotTests.debug.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ENABLE_MODULES = YES; DEVELOPMENT_TEAM = 7J4U792NQT; @@ -5174,7 +5298,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = AC34BF67FD21A9D01C16AE5D /* Pods-RiotTests.release.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ENABLE_MODULES = YES; DEVELOPMENT_TEAM = 7J4U792NQT; diff --git a/Riot/AppDelegate.m b/Riot/AppDelegate.m index 662c33b938..517395fb02 100644 --- a/Riot/AppDelegate.m +++ b/Riot/AppDelegate.m @@ -238,6 +238,7 @@ The current call view controller (if any). @property (weak, nonatomic) UIViewController *gdprConsentController; @property (nonatomic, strong) ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter; +@property (nonatomic, strong) SlidingModalPresenter *slidingModalPresenter; /** Used to manage on boarding steps, like create DM with riot bot @@ -374,7 +375,7 @@ - (UINavigationController*)secondaryNavigationController - (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(nullable NSDictionary *)launchOptions { // Create message sound - NSURL *messageSoundURL = [[NSBundle mainBundle] URLForResource:@"message" withExtension:@"mp3"]; + NSURL *messageSoundURL = [[NSBundle mainBundle] URLForResource:@"message" withExtension:@"caf"]; AudioServicesCreateSystemSoundID((__bridge CFURLRef)messageSoundURL, &_messageSound); NSLog(@"[AppDelegate] willFinishLaunchingWithOptions: Done"); @@ -1565,7 +1566,7 @@ - (NSString*)notificationSoundNameFromPushRule:(MXPushRule*)pushRule soundName = action.parameters[@"value"]; if ([soundName isEqualToString:@"default"]) { - soundName = @"message.mp3"; + soundName = @"message.caf"; } } } @@ -4099,18 +4100,27 @@ - (void)displayJitsiViewControllerWithWidget:(Widget*)jitsiWidget andVideo:(BOOL { if (!_jitsiViewController && !currentCallViewController) { - _jitsiViewController = [JitsiViewController jitsiViewController]; + MXWeakify(self); + [self checkPermissionForNativeWidget:jitsiWidget fromUrl:JitsiService.shared.serverURL completion:^(BOOL granted) { + MXStrongifyAndReturnIfNil(self); + if (!granted) + { + return; + } - [_jitsiViewController openWidget:jitsiWidget withVideo:video success:^{ + self->_jitsiViewController = [JitsiViewController jitsiViewController]; - _jitsiViewController.delegate = self; - [self presentJitsiViewController:nil]; - - } failure:^(NSError *error) { + [self->_jitsiViewController openWidget:jitsiWidget withVideo:video success:^{ - _jitsiViewController = nil; + self->_jitsiViewController.delegate = self; + [self presentJitsiViewController:nil]; - [self showAlertWithTitle:nil message:NSLocalizedStringFromTable(@"call_jitsi_error", @"Vector", nil)]; + } failure:^(NSError *error) { + + self->_jitsiViewController = nil; + + [self showAlertWithTitle:nil message:NSLocalizedStringFromTable(@"call_jitsi_error", @"Vector", nil)]; + }]; }]; } else @@ -4166,6 +4176,123 @@ - (void)jitsiViewController:(JitsiViewController *)jitsiViewController goBackToA } +#pragma mark - Native Widget Permission + +- (void)checkPermissionForNativeWidget:(Widget*)widget fromUrl:(NSURL*)url completion:(void (^)(BOOL granted))completion +{ + MXSession *session = widget.mxSession; + + if ([widget.widgetEvent.sender isEqualToString:session.myUser.userId]) + { + // No need of more permission check if the user created the widget + completion(YES); + return; + } + + // Check permission in user Riot settings + __block RiotSharedSettings *sharedSettings = [[RiotSharedSettings alloc] initWithSession:session]; + + WidgetPermission permission = [sharedSettings permissionForNative:widget fromUrl:url]; + if (permission == WidgetPermissionGranted) + { + completion(YES); + } + else + { + // Note: ask permission again if the user previously declined it + [self askNativeWidgetPermissionWithWidget:widget completion:^(BOOL granted) { + // Update the settings in user account data in parallel + [sharedSettings setPermission:granted ? WidgetPermissionGranted : WidgetPermissionDeclined + forNative:widget fromUrl:url + success:^ + { + sharedSettings = nil; + } + failure:^(NSError * _Nullable error) + { + NSLog(@"[WidgetVC] setPermissionForWidget failed. Error: %@", error); + sharedSettings = nil; + }]; + + completion(granted); + }]; + } +} + +- (void)askNativeWidgetPermissionWithWidget:(Widget*)widget completion:(void (^)(BOOL granted))completion +{ + if (!self.slidingModalPresenter) + { + self.slidingModalPresenter = [SlidingModalPresenter new]; + } + + [self.slidingModalPresenter dismissWithAnimated:NO completion:nil]; + + NSString *widgetCreatorUserId = widget.widgetEvent.sender ?: NSLocalizedStringFromTable(@"room_participants_unknown", @"Vector", nil); + + MXSession *session = widget.mxSession; + MXRoom *room = [session roomWithRoomId:widget.widgetEvent.roomId]; + MXRoomState *roomState = room.dangerousSyncState; + MXRoomMember *widgetCreatorRoomMember = [roomState.members memberWithUserId:widgetCreatorUserId]; + + NSString *widgetDomain = @""; + + if (widget.url) + { + NSString *host = [[NSURL alloc] initWithString:widget.url].host; + if (host) + { + widgetDomain = host; + } + } + + MXMediaManager *mediaManager = widget.mxSession.mediaManager; + NSString *widgetCreatorDisplayName = widgetCreatorRoomMember.displayname; + NSString *widgetCreatorAvatarURL = widgetCreatorRoomMember.avatarUrl; + + NSArray *permissionStrings = @[ + NSLocalizedStringFromTable(@"room_widget_permission_display_name_permission", @"Vector", nil), + NSLocalizedStringFromTable(@"room_widget_permission_avatar_url_permission", @"Vector", nil) + ]; + + WidgetPermissionViewModel *widgetPermissionViewModel = [[WidgetPermissionViewModel alloc] initWithCreatorUserId:widgetCreatorUserId + creatorDisplayName:widgetCreatorDisplayName creatorAvatarUrl:widgetCreatorAvatarURL widgetDomain:widgetDomain + isWebviewWidget:NO + widgetPermissions:permissionStrings + mediaManager:mediaManager]; + + + WidgetPermissionViewController *widgetPermissionViewController = [WidgetPermissionViewController instantiateWith:widgetPermissionViewModel]; + + MXWeakify(self); + + widgetPermissionViewController.didTapContinueButton = ^{ + + MXStrongifyAndReturnIfNil(self); + + [self.slidingModalPresenter dismissWithAnimated:YES completion:^{ + completion(YES); + }]; + }; + + widgetPermissionViewController.didTapCloseButton = ^{ + + MXStrongifyAndReturnIfNil(self); + + [self.slidingModalPresenter dismissWithAnimated:YES completion:^{ + completion(NO); + }]; + }; + + UIViewController *presentingViewController = self.window.rootViewController.presentedViewController ?: self.window.rootViewController; + + [self.slidingModalPresenter present:widgetPermissionViewController + from:presentingViewController + animated:YES + completion:nil]; +} + + #pragma mark - Call status handling - (void)addCallStatusBar:(NSString*)buttonTitle diff --git a/Riot/Assets/Images.xcassets/Common/close_button.imageset/Contents.json b/Riot/Assets/Images.xcassets/Common/close_button.imageset/Contents.json new file mode 100644 index 0000000000..4b71754569 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Common/close_button.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "close_button.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "close_button@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "close_button@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Riot/Assets/Images.xcassets/Common/close_button.imageset/close_button.png b/Riot/Assets/Images.xcassets/Common/close_button.imageset/close_button.png new file mode 100644 index 0000000000..4fddab59c0 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Common/close_button.imageset/close_button.png differ diff --git a/Riot/Assets/Images.xcassets/Common/close_button.imageset/close_button@2x.png b/Riot/Assets/Images.xcassets/Common/close_button.imageset/close_button@2x.png new file mode 100644 index 0000000000..07557612b5 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Common/close_button.imageset/close_button@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Common/close_button.imageset/close_button@3x.png b/Riot/Assets/Images.xcassets/Common/close_button.imageset/close_button@3x.png new file mode 100644 index 0000000000..cfb00b59e9 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Common/close_button.imageset/close_button@3x.png differ diff --git a/Riot/Assets/Sounds/message.caf b/Riot/Assets/Sounds/message.caf new file mode 100644 index 0000000000..4c241b5866 Binary files /dev/null and b/Riot/Assets/Sounds/message.caf differ diff --git a/Riot/Assets/Sounds/message.mp3 b/Riot/Assets/Sounds/message.mp3 deleted file mode 100644 index c857ee7b16..0000000000 Binary files a/Riot/Assets/Sounds/message.mp3 and /dev/null differ diff --git a/Riot/Assets/bg.lproj/Vector.strings b/Riot/Assets/bg.lproj/Vector.strings index 539218872f..9246d1c2c3 100644 --- a/Riot/Assets/bg.lproj/Vector.strings +++ b/Riot/Assets/bg.lproj/Vector.strings @@ -825,8 +825,8 @@ "room_participants_start_new_chat_error_using_user_email_without_identity_server" = "Не е конфигуриран сървър за самоличност, така че не можете да започнете чат с контакт посредством имейл адрес."; // Service terms "service_terms_modal_title" = "Условия за ползване"; -"service_terms_modal_message" = "За да продължите е необходимо да приемете Условията за ползване."; -"service_terms_modal_accept_button" = "Приемам"; +"service_terms_modal_message" = "За да продължите, трябва да приемете Условията за ползване на тази услуга (%@)."; +"service_terms_modal_accept_button" = "Приеми"; "service_terms_modal_description_for_identity_server" = "Бъдете откриваеми от потребители"; "service_terms_modal_description_for_integration_manager" = "Използвайте ботове, връзки към други мрежи и стикери"; "room_participants_remove_third_party_invite_prompt_msg" = "Сигурни ли сте, че искате да оттеглите тази покана?"; @@ -867,3 +867,59 @@ "settings_devices_description" = "Публичното име на устройството е видимо за хората, с които общувате"; "settings_discovery_no_identity_server" = "В момента не използвате сървър за самоличност. Добавете такъв, за да бъдете откриваеми от съществуващи познати контакти."; "settings_discovery_terms_not_signed" = "Съгласете се с условията за ползване на сървъра за самоличност (%@) за да бъдете откриваеми по имейл адрес или телефон."; +"settings_discovery_three_pids_management_information_part1" = "Управлявайте кои имейл адреси и телефонни номера други потребители могат да използват за да ви открият или поканят в стаи. Добавяйте и премахвайте имейл адреси и телефонни номера от този списък в "; +"settings_discovery_three_pids_management_information_part2" = "Потребителски настройки"; +"settings_discovery_three_pids_management_information_part3" = "."; +"settings_discovery_error_message" = "Възникна грешка. Опитайте пак."; +"settings_discovery_three_pid_details_title_email" = "Управление на имейл"; +"settings_discovery_three_pid_details_information_email" = "Управлявайте настройките за този имейл адрес, който други потребители могат да използват за да ви открият или поканят в стая. Добавяйте и премахвайте имейл адреси в Профил."; +"settings_discovery_three_pid_details_title_phone_number" = "Управлявай телефонен номер"; +"settings_discovery_three_pid_details_information_phone_number" = "Управлявайте настройките за този телефонен номер, който други потребители могат да използват за да ви открият или поканят в стая. Добавяйте и премахвайте телефонни номера в Профил."; +"settings_discovery_three_pid_details_share_action" = "Сподели"; +"settings_discovery_three_pid_details_revoke_action" = "Оттегли"; +"settings_discovery_three_pid_details_cancel_email_validation_action" = "Откажи потвърждението на имейл"; +"settings_discovery_three_pid_details_enter_sms_code_action" = "Въведи SMS код за активация"; +"settings_identity_server_description" = "Използвайки сървъра за самоличност по-горе, може да откривате и да бъдете открити от съществуващи познати ваши контакти."; +"settings_identity_server_no_is" = "Не е настроен сървър за самоличност"; +"settings_identity_server_no_is_description" = "В момента не използвате сървър за самоличност. Добавете такъв по-горе, за да откривате и бъдете откриваеми от съществуващи ваши контакти."; +// Identity server settings +"identity_server_settings_title" = "Сървър за самоличност"; +"identity_server_settings_description" = "В момента използвате %2 за да откривате и да бъдете открити от съществуващи ваши контакти."; +"identity_server_settings_no_is_description" = "В момента не използвате сървър за самоличност. Добавете такъв по-горе, за да откривате и бъдете открити от съществуващи ваши контакти."; +"identity_server_settings_place_holder" = "Въведете сървър за самоличност"; +"identity_server_settings_add" = "Добави"; +"identity_server_settings_change" = "Промени"; +"identity_server_settings_disconnect_info" = "Ако прекъснете връзката със сървъра за самоличност, няма да можете да бъдете открити от други потребители, както и да ви канят в стаи по имейл или телефонен номер."; +"identity_server_settings_disconnect" = "Прекъсни"; +"identity_server_settings_alert_no_terms_title" = "Сървъра за самоличност няма условия за ползване"; +"identity_server_settings_alert_no_terms" = "Сървъра за самоличност, който сте избрали няма условия за ползване на услугата. Продължете, само ако вярвате на собственика на сървъра."; +"identity_server_settings_alert_change_title" = "Промяна на сървър за самоличност"; +"identity_server_settings_alert_change" = "Прекъсване от сървър за самоличност %1$@ и вместо това свързване с %2$@?"; +"identity_server_settings_alert_disconnect_title" = "Прекъсване на връзка със сървър за самоличност"; +"identity_server_settings_alert_disconnect" = "Прекъсване на връзката със сървър за самоличност %@?"; +"identity_server_settings_alert_disconnect_button" = "Прекъсни"; +"identity_server_settings_alert_disconnect_still_sharing_3pid" = "Личните ви данни все още са споделени със сървър за самоличност %@.\n\nПрепоръчваме да премахнете имейл адресите и телефонните номера от сървъра за самоличност преди прекъсване на връзката."; +"identity_server_settings_alert_disconnect_still_sharing_3pid_button" = "Прекъсни въпреки това"; +"identity_server_settings_alert_error_terms_not_accepted" = "Трябва да приемете условията на %@ за да го използвате за сървър за самоличност."; +"identity_server_settings_alert_error_invalid_identity_server" = "%@ не е валиден сървър за самоличност."; +"call_no_stun_server_error_title" = "Обаждането се провали поради грешно конфигуриран сървър"; +"call_no_stun_server_error_message_1" = "Попитайте администратора на сървъра %@ да конфигурира TURN сървър за да може разговорите да работят надеждно."; +"call_no_stun_server_error_message_2" = "Като алтернатива, също може да използвате публичния сървър %@, но това няма да е толкова надеждно, а и ще сподели IP адреса ви със сървъра. Може да управлявате това в Настройки"; +"call_no_stun_server_error_use_fallback_button" = "Опитай с %@"; +"service_terms_modal_decline_button" = "Откажи"; +"service_terms_modal_description_for_identity_server_1" = "Открийте други по телефон или имейл"; +"service_terms_modal_description_for_identity_server_2" = "Бъдете откриваеми по телефон или имейл"; +// Service terms - Variant for identity server when displayed out of a context +"service_terms_modal_title_identity_server" = "Откриване на контакти"; +"service_terms_modal_message_identity_server" = "Приемете условията на сървъра за самоличност (%@) за да откривате контакти."; +// Generic errors +"error_invite_3pid_with_no_identity_server" = "Добавете сървър за самоличност в настройки за да каните по имейл."; +"error_not_supported_on_mobile" = "Не може да правите това от %@ мобилен телефон."; +"settings_integrations" = "ИНТЕГРАЦИИ"; +"settings_integrations_allow_button" = "Управлявай интеграциите"; +"settings_integrations_allow_description" = "Използвайте мениджър на интеграции (%@) за да управлявате ботове, мостове към други мрежи, приспособления и стикери.\n\nМениджърите на интеграции получават данни за конфигурация, могат да модифицират приспособления, да пращат покани в стаи и да контролират нивата на достъп вместо вас."; +"widget_menu_refresh" = "Опресни"; +"widget_menu_open_outside" = "Отвори в браузър"; +"widget_menu_revoke_permission" = "Премахни достъпа за мен"; +"widget_menu_remove" = "Премахни за всички"; +"widget_integration_manager_disabled" = "Необходимо е да включите мениджър на интеграции от настройки"; diff --git a/Riot/Assets/cy.lproj/InfoPlist.strings b/Riot/Assets/cy.lproj/InfoPlist.strings new file mode 100644 index 0000000000..dce0c67266 --- /dev/null +++ b/Riot/Assets/cy.lproj/InfoPlist.strings @@ -0,0 +1,6 @@ +// Permissions usage explanations +"NSCameraUsageDescription" = "Defnyddir y camera i dynnu lluniau a fideos, gwneud galwadau fideo."; +"NSPhotoLibraryUsageDescription" = "Defnyddir y llyfrgell ffotograffau i anfon lluniau a fideos."; +"NSMicrophoneUsageDescription" = "Defnyddir y meicroffon i gymryd fideos, gwneud galwadau."; +"NSContactsUsageDescription" = "I ddarganfod cysylltiadau sydd eisoes yn defnyddio Matrix, gall Riot anfon cyfeiriadau e-bost a rhifau ffôn yn eich llyfr cyfeiriadau at y gweinydd adnabod Matrix o'ch dewis. Pan gânt eu cefnogi, mae data personol yn cael ei amgodio cyn ei anfon - gwiriwch bolisi preifatrwydd eich gweinydd adnabod i gael mwy o fanylion."; +"NSCalendarsUsageDescription" = "Gweler eich cyfarfodydd a drefnwyd yn yr ap."; diff --git a/Riot/Assets/cy.lproj/Localizable.strings b/Riot/Assets/cy.lproj/Localizable.strings new file mode 100644 index 0000000000..4e00dae820 --- /dev/null +++ b/Riot/Assets/cy.lproj/Localizable.strings @@ -0,0 +1,56 @@ +/* Message title for a specific person in a named room */ +"MSG_FROM_USER_IN_ROOM_TITLE" = "%@ yn %@"; +/* New message from a specific person, not referencing a room */ +"MSG_FROM_USER" = "Anfonwyd %@ neges"; +/* New message from a specific person in a named room */ +"MSG_FROM_USER_IN_ROOM" = "Postiodd %@ yn %@"; +/* New message from a specific person, not referencing a room. Content included. */ +"MSG_FROM_USER_WITH_CONTENT" = "%@: %@"; +/* New message from a specific person in a named room. Content included. */ +"MSG_FROM_USER_IN_ROOM_WITH_CONTENT" = "%@ yn %@: %@"; +/* New action message from a specific person, not referencing a room. */ +"ACTION_FROM_USER" = "* %@ %@"; +/* New action message from a specific person in a named room. */ +"ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; +/* New action message from a specific person, not referencing a room. */ +"IMAGE_FROM_USER" = "Anfonwyd %@ lun %@"; +/* New action message from a specific person in a named room. */ +"IMAGE_FROM_USER_IN_ROOM" = "Postiodd %@ lun %@ yn %@"; +/* A single unread message in a room */ +"SINGLE_UNREAD_IN_ROOM" = "Cawsoch neges yn %@"; +/* A single unread message */ +"SINGLE_UNREAD" = "Cawsoch neges"; +/* Sticker from a specific person, not referencing a room. */ +"STICKER_FROM_USER" = "Anfonodd %@ sticer"; +/* Multiple unread messages in a room */ +"UNREAD_IN_ROOM" = "%@ neges newydd yn %@"; +/* Multiple unread messages from a specific person, not referencing a room */ +"MSGS_FROM_USER" = "%@ neges newydd yn %@"; +/* Multiple unread messages from two people */ +"MSGS_FROM_TWO_USERS" = "%@ neges newydd gan %@ a %@"; +/* Multiple unread messages from three people */ +"MSGS_FROM_THREE_USERS" = "%@ neges newydd gan %@, %@ a %@"; +/* Multiple unread messages from two plus people (ie. for 4+ people: 'others' replaces the third person) */ +"MSGS_FROM_TWO_PLUS_USERS" = "%@ neges newydd gan %@, %@ ac eraill"; +/* Multiple messages in two rooms */ +"MSGS_IN_TWO_ROOMS" = "%@ neges newydd yn %@ a %@"; +/* Look, stuff's happened, alright? Just open the app. */ +"MSGS_IN_TWO_PLUS_ROOMS" = "%@ neges newydd yn %@, %@ ac eraill"; +/* A user has invited you to a chat */ +"USER_INVITE_TO_CHAT" = "Mae %@ wedi eich gwahodd chi i sgwrsio"; +/* A user has invited you to an (unamed) group chat */ +"USER_INVITE_TO_CHAT_GROUP_CHAT" = "Mae %@ wedi eich gwahodd chi i sgwrsio mewn grŵp"; +/* A user has invited you to a named room */ +"USER_INVITE_TO_NAMED_ROOM" = "Mae %@ wedi eich gwahodd chi i %@"; +/* Incoming one-to-one voice call */ +"VOICE_CALL_FROM_USER" = "Galwad gan %@"; +/* Incoming one-to-one video call */ +"VIDEO_CALL_FROM_USER" = "Galwad fideo gan %@"; +/* Incoming unnamed voice conference invite from a specific person */ +"VOICE_CONF_FROM_USER" = "Galwad grŵp gan %@"; +/* Incoming unnamed video conference invite from a specific person */ +"VIDEO_CONF_FROM_USER" = "Galwad fideo grŵp gan %@"; +/* Incoming named voice conference invite from a specific person */ +"VOICE_CONF_NAMED_FROM_USER" = "Galwad grŵp gan %@: '%@'"; +/* Incoming named video conference invite from a specific person */ +"VIDEO_CONF_NAMED_FROM_USER" = "Galwad fideo grŵp gan %@: '%@'"; diff --git a/Riot/Assets/cy.lproj/Vector.strings b/Riot/Assets/cy.lproj/Vector.strings index 454563f700..2ef812cdb7 100644 --- a/Riot/Assets/cy.lproj/Vector.strings +++ b/Riot/Assets/cy.lproj/Vector.strings @@ -172,7 +172,7 @@ // Contacts "contacts_address_book_section" = "CYSYLLTIADAU LLEOL"; "contacts_address_book_matrix_users_toggle" = "Defnyddwyr Matrix yn unig"; -"contacts_address_book_no_identity_server" = "Dim gweinyddwr adnabod wedi'i osod"; +"contacts_address_book_no_identity_server" = "Dim gweinydd adnabod wedi'i osod"; "contacts_address_book_no_contact" = "Dim cysylltiadau lleol"; "contacts_address_book_permission_required" = "Mae angen caniatâd i gael mynediad at gysylltiadau lleol"; "contacts_address_book_permission_denied" = "Gwrthodwyd caniatâd i Riot gael mynediad i'ch cysylltiadau lleol"; @@ -425,23 +425,23 @@ "settings_key_backup_info" = "Sicrheir negeseuon wedi'u hamgryptio gydag amgryptio o'r dechrau i'r diwedd. Dim ond chi a'r derbynnydd / derbynwyr sydd â'r allweddi i ddarllen y negeseuon hyn."; "settings_key_backup_info_checking" = "Gwirio..."; "settings_key_backup_info_none" = "Nid yw'ch allweddi yn cael eu cadw wrth gefn o'r ddyfais hon."; -"settings_key_backup_info_signout_warning" = "Cysylltwch y ddyfais hon â Key Backup cyn arwyddo allan er mwyn osgoi colli unrhyw allweddi a allai fod ar y ddyfais hon yn unig."; -"settings_key_backup_info_version" = "Fersiwn Key Backup: %@"; +"settings_key_backup_info_signout_warning" = "Cysylltwch y ddyfais hon â Allweddi Wrth Gefn cyn arwyddo allan er mwyn osgoi colli unrhyw allweddi a allai fod ar y ddyfais hon yn unig."; +"settings_key_backup_info_version" = "Fersiwn Allweddi Wrth Gefn: %@"; "settings_key_backup_info_algorithm" = "Algorithm: %@"; "settings_key_backup_info_valid" = "Mae'r ddyfais hon yn gwneud copi wrth gefn o'ch allweddi."; "settings_key_backup_info_not_valid" = "Nid yw'r ddyfais hon yn gwneud copi wrth gefn o'ch allweddi, ond mae gennych gopi wrth gefn y gallwch ei adfer ac ychwanegu ato wrth symud ymlaen."; "settings_key_backup_info_progress" = "Creu copi wrth gefn o allweddi %@..."; "settings_key_backup_info_progress_done" = "Pob allwedd â copi wrth gefn"; -"settings_key_backup_info_trust_signature_unknown" = "Mae gan Key Backup lofnod o'r ddyfais gydag ID: %@"; -"settings_key_backup_info_trust_signature_valid" = "Mae gan Key Backup lofnod dilys o'r ddyfais hon"; -"settings_key_backup_info_trust_signature_valid_device_verified" = "Mae gan Key Backup lofnod dilys o %@"; -"settings_key_backup_info_trust_signature_valid_device_unverified" = "Mae gan Key Backup lofnod o %@"; -"settings_key_backup_info_trust_signature_invalid_device_verified" = "Mae gan Key Backup lofnod annilys o %@"; -"settings_key_backup_info_trust_signature_invalid_device_unverified" = "Mae gan Key Backup lofnod annilys o %@"; -"settings_key_backup_button_create" = "Dechrau defnyddio Key Backup"; +"settings_key_backup_info_trust_signature_unknown" = "Mae gan Allweddi Wrth Gefn lofnod o'r ddyfais gydag ID: %@"; +"settings_key_backup_info_trust_signature_valid" = "Mae gan Allweddi Wrth Gefn lofnod dilys o'r ddyfais hon"; +"settings_key_backup_info_trust_signature_valid_device_verified" = "Mae gan Allweddi Wrth Gefn lofnod dilys o %@"; +"settings_key_backup_info_trust_signature_valid_device_unverified" = "Mae gan Allweddi Wrth Gefn lofnod o %@"; +"settings_key_backup_info_trust_signature_invalid_device_verified" = "Mae gan Allweddi Wrth Gefn lofnod annilys o %@"; +"settings_key_backup_info_trust_signature_invalid_device_unverified" = "Mae gan Allweddi Wrth Gefn lofnod annilys o %@"; +"settings_key_backup_button_create" = "Dechrau defnyddio Allweddi Wrth Gefn"; "settings_key_backup_button_restore" = "Adfer o'r copi wrth gefn"; "settings_key_backup_button_delete" = "Dileu copi wrth gefn"; -"settings_key_backup_button_connect" = "Cysylltwch y ddyfais hon i Key Backup"; +"settings_key_backup_button_connect" = "Cysylltwch y ddyfais hon i Allweddi Wrth Gefn"; "settings_key_backup_delete_confirmation_prompt_title" = "Dileu copi wrth gefn"; "settings_key_backup_delete_confirmation_prompt_msg" = "Ydych chi'n siwr? Byddwch yn colli'ch negeseuon wedi'u hamgryptio os nad yw'ch allweddi wedi'u cadw wrth gefn yn gywir."; "settings_devices_description" = "Mae enw cyhoeddus dyfais yn weladwy i'r bobl rydych chi'n cyfathrebu â nhw"; @@ -460,7 +460,7 @@ "settings_discovery_three_pid_details_cancel_email_validation_action" = "Canslo gwiro e-bost"; "settings_discovery_three_pid_details_enter_sms_code_action" = "Rhowch côd actifadu neges destyn"; "settings_identity_server_description" = "Gan ddefnyddio'r gweinydd adnabod a osodir uchod, gallwch ddarganfod a bod yn ddarganfyddadwy gan gysylltiadau presennol rydych chi'n eu hadnabod."; -"settings_identity_server_no_is" = "Dim gweinyddwr adnabod wedi'i osod"; +"settings_identity_server_no_is" = "Dim gweinydd adnabod wedi'i osod"; "settings_identity_server_no_is_description" = "Ar hyn o bryd nid ydych yn defnyddio gweinydd adnabod. I ddarganfod a bod yn ddarganfyddadwy gan gysylltiadau presennol rydych chi'n eu hadnabod, ychwanegwch un uchod."; // Identity server settings "identity_server_settings_title" = "Gweinydd Adnabod"; @@ -538,3 +538,348 @@ "room_details_fail_to_update_room_canonical_alias" = "Methwyd diweddaru'r prif gyfeiriad"; "room_details_fail_to_update_room_communities" = "Methwyd diweddaru'r cymunedau cysylltiedig"; "room_details_fail_to_update_room_direct" = "Methwyd diweddaru baner uniongyrchol yr ystafell hon"; +"room_details_fail_to_enable_encryption" = "Methwyd galluogi amgryptio yn yr ystafell hon"; +"room_details_save_changes_prompt" = "Hoffech chi gadw'r newidiadau?"; +"room_details_set_main_address" = "Gosod fel Prif Gyfeiriad"; +"room_details_unset_main_address" = "Dad-osod fel Prif Gyfeiriad"; +"room_details_copy_room_id" = "Copio ID Ystafell"; +"room_details_copy_room_address" = "Copio Cyfeiriad Ystafell"; +"room_details_copy_room_url" = "Copio URL Ystafell"; +// Group Details +"group_details_title" = "Manylion Cymuned"; +"group_details_home" = "Hafan"; +"group_details_people" = "Pobl"; +"group_details_rooms" = "Ystafelloedd"; +// Group Home +"group_home_one_member_format" = "1 aelod"; +"group_home_multi_members_format" = "%tu aelod"; +"group_home_one_room_format" = "1 ystafell"; +"group_home_multi_rooms_format" = "%tu ystafell"; +"group_invitation_format" = "Mae %@ wedi eich gwahodd i ymuno â'r gymuned hon"; +// Group participants +"group_participants_add_participant" = "Ychwanegu cyfranogwr"; +"group_participants_leave_prompt_title" = "Gadael grŵp"; +"group_participants_leave_prompt_msg" = "Ydych chi'n siŵr eich bod chi eisiau gadael y grŵp?"; +"group_participants_remove_prompt_title" = "Cadarnhad"; +"group_participants_remove_prompt_msg" = "Ydych chi'n siwr eich bod chi eisiau tynnu %@ o'r grŵp?"; +"group_participants_invite_prompt_title" = "Cadarnhad"; +"group_participants_invite_prompt_msg" = "Ydych chi'n siwr eich bod eisau gwahodd %@ i'r grŵp?"; +"group_participants_filter_members" = "Hidlo aelodau'r cymuned"; +"group_participants_invite_another_user" = "Chwilio / gwahodd yn ôl ID Defnyddiwr neu Enw"; +"group_participants_invite_malformed_id_title" = "Gwall gwahoddiad"; +"group_participants_invite_malformed_id" = "ID camffurfiedig. Dylai fod yn ID Matrix fel '@localpart:domain'"; +"group_participants_invited_section" = "GWAHODDWYD"; +// Group rooms +"group_rooms_filter_rooms" = "Hidlo ystafelloedd y gymuned"; +// Read Receipts +"read_receipts_list" = "Rhestr Derbynebau Darllen"; +"receipt_status_read" = "Wedi darllen: "; +// Media picker +"media_picker_title" = "Llyfrgell cyfryngau"; +"media_picker_library" = "Llyfrgell"; +"media_picker_select" = "Dewis"; +// Image picker +"image_picker_action_camera" = "Tynnu llun"; +"image_picker_action_library" = "Dewis o'r llyfrgell"; +// Directory +"directory_title" = "Cyfeiriadur"; +"directory_server_picker_title" = "Dewisiwch gyfeiriadur"; +"directory_server_all_rooms" = "Pob ystafell ar weinydd %@"; +"directory_server_all_native_rooms" = "Pob ystafell Matrix frodorol"; +"directory_server_type_homeserver" = "Teipiwch hafanweinydd i restru ystafelloedd cyhoeddus o"; +"directory_server_placeholder" = "matrix.org"; +// Events formatter +"event_formatter_member_updates" = "%tu newidiadau aelodaeth"; +"event_formatter_widget_added" = "Ychwanegwyd teclyn %@ gan %@"; +"event_formatter_widget_removed" = "Tynnwyd teclyn %@ gan %@"; +"event_formatter_jitsi_widget_added" = "Ychwanegwyd cynhadledd VoIP gan %@"; +"event_formatter_jitsi_widget_removed" = "Tynnwyd cynhadledd VoIP gan %@"; +"event_formatter_rerequest_keys_part1_link" = "Ail-ofyn am allweddi amgryptio"; +"event_formatter_rerequest_keys_part2" = " o'ch dyfeisiau eraill."; +"event_formatter_message_edited_mention" = "(adolygwyd)"; +// Others +"or" = "neu"; +"you" = "Chi"; +"today" = "Heddiw"; +"yesterday" = "Ddoe"; +"network_offline_prompt" = "Mae'n ymddangos bod y cysylltiad Rhyngrwyd yn all-lein."; +"homeserver_connection_lost" = "Methu cysylltu â'r hafanweinydd."; +"public_room_section_title" = "Ystafelloedd Cyhoeddus (yn %@):"; +"bug_report_prompt" = "Daeth yr app ar draws nam pall y tro diwethaf iddo redeg. Hoffech chi gyflwyno adroddiad pall?"; +"rage_shake_prompt" = "Mae'n ymddangos eich bod yn ysgwyd y ffôn mewn rhwystredigaeth. Hoffech chi gyflwyno adroddiad nam?"; +"do_not_ask_again" = "Peidio â gofyn eto"; +"camera_access_not_granted" = "Nid oes gan %@ ganiatâd i ddefnyddio'r Camera, newidiwch y gosodiadau preifatrwydd"; +"camera_unavailable" = "Nid yw'r camera ar gael ar eich dyfais"; +"photo_library_access_not_granted" = "Nid oes gan %@ ganiatâd i gael mynediad i'r llyfrgell ffotograffau, newidiwch osodiadau preifatrwydd"; +"large_badge_value_k_format" = "%.1fK"; +"room_does_not_exist" = "Nid yw %@ yn bodoli"; +// Call +"call_incoming_voice_prompt" = "Galwad llais sy'n dod i mewn gan %@"; +"call_incoming_video_prompt" = "Galwad fideo sy'n dod i mewn gan %@"; +"call_incoming_voice" = "Galwad sy'n dod i mewn..."; +"call_incoming_video" = "Galwad fideo sy'n dod i mewn..."; +"call_already_displayed" = "Mae galwad ar y gweill eisoes."; +"call_jitsi_error" = "Methwyd ymuno â'r alwad cynhadledd."; +"call_no_stun_server_error_title" = "Methodd yr alwad oherwydd gweinydd wedi'i gamosod"; +"call_no_stun_server_error_message_1" = "Gofynnwch i weinyddwr eich hafanweinydd %@ i osod gweinydd TURN er mwyn i alwadau weithio'n ddibynadwy."; +"call_no_stun_server_error_message_2" = "Fel arall, gallwch geisio defnyddio'r gweinydd cyhoeddus yn %@, ond ni fydd hyn mor ddibynadwy, a bydd yn rhannu eich cyfeiriad IP gyda'r gweinydd hwnnw. Gallwch hefyd reoli hyn yn Gosodiadau"; +"call_no_stun_server_error_use_fallback_button" = "Rhowch gynnig ar ddefnyddio %@"; +// No VoIP support +"no_voip_title" = "Galwad sy'n dod i mewn"; +"no_voip" = "Mae %@ yn eich ffonio ond nid yw %@ yn cefnogi galwadau eto.\nGallwch anwybyddu'r hysbysiad hwn ac ateb yr alwad o ddyfais arall neu gallwch ei wrthod."; +// Crash report +"google_analytics_use_prompt" = "Hoffech chi helpu i wella %@ trwy gyrru adroddiadau pall a data defnydd dienw yn awtomatig?"; +// Crypto +"e2e_enabling_on_app_update" = "Mae Riot bellach yn cefnogi amgryptio o'r dechrau i'r diwedd ond mae angen i chi fewngofnodi eto i'w alluogi.\n\nGallwch ei wneud nawr neu'n hwyrach o'r gosodiadau."; +"e2e_need_log_in_again" = "Mae angen i chi fewngofnodi i gynhyrchu allweddi amgryptio o'r dechrau i'r diwedd ar gyfer y ddyfais hon a chyflwyno'r allwedd gyhoeddus i'ch hafanweinydd\nDim ond unwaith fydd rhaid gwneud hyn; sori am yr anghyfleustra."; +// Key backup wrong version +"e2e_key_backup_wrong_version_title" = "Copi Allwedd Wrth Gefn Newydd"; +"e2e_key_backup_wrong_version" = "Mae copi allwedd wrth gefn newydd neges ddiogel wedi'i ganfod.\n\nOs nad chi oedd hyn, gosodwch gyfrinair newydd yn Gosodiadau."; +"e2e_key_backup_wrong_version_button_settings" = "Gosodiadau"; +"e2e_key_backup_wrong_version_button_wasme" = "Fi oedd e"; +// Bug report +"bug_report_title" = "Adroddiad Nam"; +"bug_report_description" = "Disgrifiwch y nam. Beth wnaethoch chi? Beth oeddech chi'n disgwyl iddo ddigwydd? Beth ddigwyddodd mewn gwirionedd?"; +"bug_crash_report_title" = "Adroddiad Pall"; +"bug_crash_report_description" = "Disgrifiwch yr hyn a wnaethoch cyn y pall:"; +"bug_report_logs_description" = "Er mwyn canfod problemau, anfonir logiau gan y cleient hwn gyda'r adroddiad nam hwn. Os byddai'n well gennych anfon y testun uchod yn unig, dad-gliciwch:"; +"bug_report_send_logs" = "Anfon logiau"; +"bug_report_send_screenshot" = "Anfon sgrinlun"; +"bug_report_progress_zipping" = "Casglu logiau"; +"bug_report_progress_uploading" = "Uwchlwytho adroddiad"; +"bug_report_send" = "Anfon"; +// Widget +"widget_no_integrations_server_configured" = "Dim gweinydd integreiddiadau wedi'i osod"; +"widget_integrations_server_failed_to_connect" = "Methwyd cysylltu â'r gweinydd integreiddiadau"; +"widget_no_power_to_manage" = "Mae angen caniatâd arnoch i reoli teclynnau yn yr ystafell hon"; +"widget_creation_failure" = "Methwyd creu teclyn"; +"widget_sticker_picker_no_stickerpacks_alert" = "Ar hyn o bryd nid oes gennych unrhyw becyn sticeri wedi'u galluogi."; +"widget_sticker_picker_no_stickerpacks_alert_add_now" = "Ychwanegu rhai rwan?"; +// Widget Integration Manager +"widget_integration_need_to_be_able_to_invite" = "Mae angen i chi allu gwahodd defnyddwyr i wneud hynny."; +"widget_integration_unable_to_create" = "Methu creu teclyn."; +"widget_integration_failed_to_send_request" = "Methwyd i anfon cais."; +"widget_integration_room_not_recognised" = "Ni gydnabyddir yr ystafell hon."; +"widget_integration_positive_power_level" = "Rhaid i lefel pŵer fod yn gyfanrif positif."; +"widget_integration_must_be_in_room" = "Nid ydych yn yr ystafell hon."; +"widget_integration_no_permission_in_room" = "Nid oes gennych ganiatâd i wneud hynny yn yr ystafell hon."; +"widget_integration_missing_room_id" = "room_id ar goll yn y cais."; +"widget_integration_missing_user_id" = "user_id ar goll yn y cais."; +"widget_integration_room_not_visible" = "Nid yw ystafell %@ yn weladwy."; +// Widget Picker +"widget_picker_title" = "Integreiddiadau"; +// Share extension +"share_extension_auth_prompt" = "Mewngofnodwch yn y prif app i rannu cynnwys"; +"share_extension_failed_to_encrypt" = "Methwyd anfon. Gwiriwch y gosodiadau amgryptio ar gyfer yr ystafell hon yn y prif app"; +// Room key request dialog +"e2e_room_key_request_title" = "Cais allwedd amgryptio"; +"e2e_room_key_request_message_new_device" = "Fe wnaethoch ychwanegu dyfais newydd '%@', sy'n gofyn am allweddi amgryptio."; +"e2e_room_key_request_message" = "Mae'ch dyfais anwiriedig '%@' yn gofyn am allweddi amgryptio."; +"e2e_room_key_request_start_verification" = "Dechrau gwirio..."; +"e2e_room_key_request_share_without_verifying" = "Rhannwch heb wirio"; +"e2e_room_key_request_ignore_request" = "Anwybyddu cais"; +// GDPR +"gdpr_consent_not_given_alert_message" = "Er mwyn parhau i ddefnyddio'r hafanweinydd %@ rhaid i chi adolygu a chytuno i'r telerau ac amodau."; +"gdpr_consent_not_given_alert_review_now_action" = "Adolygu rwan"; +// Service terms +"service_terms_modal_title" = "Telerau Gwasanaeth"; +"service_terms_modal_message" = "I barhau maen' rhaid i chi dderbyn telerau y gwasanaeth hwn (%@)."; +"service_terms_modal_accept_button" = "Derbyn"; +"service_terms_modal_decline_button" = "Gwrthod"; +"service_terms_modal_description_for_identity_server_1" = "Dod o hyd i eraill dros y ffôn neu e-bost"; +"service_terms_modal_description_for_identity_server_2" = "Byddwch i'w gweld dros ffôn neu e-bost"; +"service_terms_modal_description_for_integration_manager" = "Defnyddiwch Botiau, pontydd, teclynnau a phecynnau sticeri"; +// Service terms - Variant for identity server when displayed out of a context +"service_terms_modal_title_identity_server" = "Darganfod Cysylltiadau"; +"service_terms_modal_message_identity_server" = "Derbyn telerau'r gweinydd adnabod (%@) i ddarganfod cysylltiadau."; +"deactivate_account_title" = "Dad-actifadu Cyfrif"; +"deactivate_account_informations_part1" = "Bydd hyn yn golygu na ellir defnyddio'ch cyfrif yn barhaol. Ni fyddwch yn gallu mewngofnodi, ac ni fydd unrhyw un yn gallu ailgofrestru'r un ID defnyddiwr. Bydd hyn yn achosi i'ch cyfrif adael yr holl ystafelloedd y mae'n cymryd rhan ynddynt, a bydd yn tynnu manylion eich cyfrif o'ch gweinydd adnabod. "; +"deactivate_account_informations_part2_emphasize" = "Ni ellir gwrthdroi'r weithred hon."; +"deactivate_account_informations_part3" = "\n\nYn dad-actifadu eich cyfrif "; +"deactivate_account_informations_part4_emphasize" = "nid yw yn ddiofyn yn achosi inni anghofio negeseuon yr ydych wedi'u hanfon. "; +"deactivate_account_informations_part5" = "Os hoffech i ni anghofio'ch negeseuon, ticiwch y blwch isod\n\nMae gwelededd neges yn Matrix yn debyg i e-bost. Mae anghofio'ch negeseuon yn golygu na fydd negeseuon rydych chi wedi'u hanfon yn cael eu rhannu ag unrhyw ddefnyddwyr newydd neu anghofrestredig, ond bydd defnyddwyr cofrestredig sydd eisoes â mynediad at y negeseuon hyn yn dal i gael mynediad i'w copi."; +"deactivate_account_forget_messages_information_part1" = "Anghofiwch yr holl negeseuon yr wyf wedi'u hanfon pan fydd fy nghyfrif yn cael ei ddad-actifadu ("; +"deactivate_account_forget_messages_information_part2_emphasize" = "Rhybudd"; +"deactivate_account_forget_messages_information_part3" = ": bydd hyn yn achosi i ddefnyddwyr y dyfodol weld golwg anghyflawn o sgyrsiau)"; +"deactivate_account_validate_action" = "Dad-actifadu cyfrif"; +"deactivate_account_password_alert_title" = "Dad-actifadu Cyfrif"; +"deactivate_account_password_alert_message" = "I barhau, rhowch eich cyfrinair os gwelwch yn dda"; +// Re-request confirmation dialog +"rerequest_keys_alert_title" = "Anfonwyd y Cais"; +"rerequest_keys_alert_message" = "Lansiwch Riot ar ddyfais arall a all ddadgryptio'r neges fel y gall anfon yr allweddi i'r ddyfais hon."; +"key_backup_setup_title" = "Allweddi Wrth Gefn"; +"key_backup_setup_skip_alert_title" = "Ydych chi'n siwr?"; +"key_backup_setup_skip_alert_message" = "Efallai y byddwch chi'n colli negeseuon diogel os byddwch chi'n allgofnodi neu'n colli'ch dyfais."; +"key_backup_setup_skip_alert_skip_action" = "Sgipio"; +"key_backup_setup_intro_title" = "Peidiwch byth â cholli negeseuon wedi'u hamgryptio"; +"key_backup_setup_intro_info" = "Diogelir negeseuon mewn ystafelloedd amgryptiedig gydag amgryptio o'r dechrau i'r diwedd. Dim ond chi a'r derbynnydd / derbynwyr sydd â'r allweddi i ddarllen y negeseuon hyn.\n\nGwnewch copi wrth gefn o'ch allweddi yn ddiogel er mwyn osgoi eu colli."; +"key_backup_setup_intro_setup_action_without_existing_backup" = "Dechrau defnyddio Allweddi Wrth Gefn"; +"key_backup_setup_intro_setup_connect_action_with_existing_backup" = "Cysylltwch y ddyfais hon i Allweddi Wrth Gefn"; +"key_backup_setup_intro_manual_export_info" = "(Uwch)"; +"key_backup_setup_intro_manual_export_action" = "Allfudo allweddi â llaw"; +"key_backup_setup_passphrase_title" = "Diogelwch eich copi wrth gefn gyda chyfrinair"; +"key_backup_setup_passphrase_info" = "Byddwn yn storio copi wedi'i amgryptio o'ch allweddi ar ein gweinydd. Amddiffynwch eich copi wrth gefn gyda chyfrinair i'w gadw'n ddiogel.\n\nEr mwyn sicrhau'r diogelwch mwyaf, dylai hyn fod yn wahanol i gyfrinair eich cyfrif."; +"key_backup_setup_passphrase_passphrase_title" = "Cyfrinair"; +"key_backup_setup_passphrase_passphrase_placeholder" = "Rhowch cyfrinair"; +"key_backup_setup_passphrase_passphrase_valid" = "Gwych!"; +"key_backup_setup_passphrase_passphrase_invalid" = "Ceisiwch ychwanegu gair"; +"key_backup_setup_passphrase_confirm_passphrase_title" = "Cadarnhau"; +"key_backup_setup_passphrase_confirm_passphrase_placeholder" = "Cadarnhau cyfrinair"; +"key_backup_setup_passphrase_confirm_passphrase_valid" = "Gwych!"; +"key_backup_setup_passphrase_confirm_passphrase_invalid" = "Nid yw'r cyfrinair yn cyfateb"; +"key_backup_setup_passphrase_set_passphrase_action" = "Cadw Cyfrinair"; +"key_backup_setup_passphrase_setup_recovery_key_info" = "Neu, diogelwch eich copi wrth gefn gydag Allwedd Adfer, gan ei arbed yn rhywle diogel."; +"key_backup_setup_passphrase_setup_recovery_key_action" = "(Uwch) Gosod gydag Allwedd Adfer"; +"key_backup_setup_success_title" = "Llwyddiant!"; +// Success from passphrase +"key_backup_setup_success_from_passphrase_info" = "Mae'ch allweddi yn cael eu cadw wrth gefn.\n\nRhwyd ddiogelwch yw eich allwedd adfer - gallwch ei defnyddio i adfer mynediad i'ch negeseuon amgryptiedig os byddwch chi'n anghofio'ch cyfrinair.\n\nCadwch eich allwedd adfer yn rhywle diogel iawn, fel rheolwr cyfrinair (neu dan glo)."; +"key_backup_setup_success_from_passphrase_save_recovery_key_action" = "Cadw Allwedd Adfer"; +"key_backup_setup_success_from_passphrase_done_action" = "Wedi Gorffen"; +// Success from recovery key +"key_backup_setup_success_from_recovery_key_info" = "Mae'ch allweddi yn cael eu cadw wrth gefn.\n\nGwnewch gopi o'r allwedd adfer hon a'i chadw'n ddiogel."; +"key_backup_setup_success_from_recovery_key_recovery_key_title" = "Allwedd Adfer"; +"key_backup_setup_success_from_recovery_key_make_copy_action" = "Gwneud Copi"; +"key_backup_setup_success_from_recovery_key_made_copy_action" = "Dwi wedi gwneud copi"; +"key_backup_recover_title" = "Negeseuon Diogel"; +"key_backup_recover_invalid_passphrase_title" = "Cyfrinair Adfer Anghywir"; +"key_backup_recover_invalid_passphrase" = "Ni ellid dadgryptio copi wrth gefn gyda'r cyfrinair hwn: gwiriwch eich bod wedi nodi'r cyfrinair adfer cywir."; +"key_backup_recover_invalid_recovery_key_title" = "Camgymhariad Allwedd Adfer"; +"key_backup_recover_invalid_recovery_key" = "Ni ellid dadgryptio copi wrth gefn gyda'r allwedd hon: gwiriwch eich bod wedi nodi'r allwedd adfer gywir."; +"key_backup_recover_from_passphrase_info" = "Defnyddiwch eich cyfrinair adfer i ddatgloi eich hanes neges ddiogel"; +"key_backup_recover_from_passphrase_passphrase_title" = "Cyfrinair"; +"key_backup_recover_from_passphrase_passphrase_placeholder" = "Rhowch cyfrinair"; +"key_backup_recover_from_passphrase_recover_action" = "Datgloi Hanes"; +"key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "Ddim yn gwybod eich cyfrinair adfer? Gallwch chi "; +"key_backup_recover_from_passphrase_lost_passphrase_action_part2" = "ddefnyddio eich allwedd adfer"; +"key_backup_recover_from_passphrase_lost_passphrase_action_part3" = "."; +"key_backup_recover_from_recovery_key_info" = "Defnyddiwch eich allwedd adfer i ddatgloi eich hanes neges ddiogel"; +"key_backup_recover_from_recovery_key_recovery_key_title" = "Allwedd Adfer"; +"key_backup_recover_from_recovery_key_recovery_key_placeholder" = "Rhowch Allwedd Adfer"; +"key_backup_recover_from_recovery_key_recover_action" = "Datgloi Hanes"; +"key_backup_recover_from_recovery_key_lost_recovery_key_action" = "Wedi colli'ch allwedd adfer? Gallwch chi sefydlu un newydd yn gosodiadau."; +"key_backup_recover_success_info" = "Copi wrth gefn wedi'i Adfer!"; +"key_backup_recover_done_action" = "Wedi Gorffen"; +"key_backup_setup_banner_title" = "Peidiwch byth â cholli negeseuon wedi'u hamgryptio"; +"key_backup_setup_banner_subtitle" = "Dechrau defnyddio Allweddi Wrth Gefn"; +"key_backup_recover_banner_title" = "Peidiwch byth â cholli negeseuon wedi'u hamgryptio"; +"key_backup_recover_connent_banner_subtitle" = "Cysylltwch y ddyfais hon i Allweddi Wrth Gefn"; +"sign_out_existing_key_backup_alert_title" = "Ydych chi'n siŵr eich bod chi am allgofnodi?"; +"sign_out_existing_key_backup_alert_sign_out_action" = "Allgofnodi"; +"sign_out_non_existing_key_backup_alert_title" = "Byddwch yn colli mynediad i'ch negeseuon amgryptiedig os byddwch chi'n allgofnodi rwan"; +"sign_out_non_existing_key_backup_alert_setup_key_backup_action" = "Dechrau defnyddio Allweddi Wrth Gefn"; +"sign_out_non_existing_key_backup_alert_discard_key_backup_action" = "Dydw i ddim eisiau i fy negeseuon amgryptiedig"; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_title" = "Byddwch yn colli ei'ch negeseuon amgryptiedig"; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_message" = "Byddwch yn colli mynediad i'ch negeseuon amgryptiedig oni bai eich bod yn gwneud copi wrth gefn o'ch allweddi cyn allgofnodi."; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_sign_out_action" = "Allgofnodi"; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_backup_action" = "Creu Copi Wrth Gefn"; +"sign_out_key_backup_in_progress_alert_title" = "Creu copi allwedd wrth gefn ar y gweill. Os byddwch chi'n allgofnodi nawr byddwch chi'n colli mynediad i'ch negeseuon wedi'u hamgryptio."; +"sign_out_key_backup_in_progress_alert_discard_key_backup_action" = "Dydw i ddim eisiau i fy negeseuon amgryptiedig"; +"sign_out_key_backup_in_progress_alert_cancel_action" = "Arosaf"; +// MARK: - Device Verification +"device_verification_title" = "Gwirio dyfais"; +"device_verification_security_advice" = "Er mwyn sicrhau'r diogelwch mwyaf, rydym yn argymell eich bod yn gwneud hyn yn bersonol neu'n defnyddio dull cyfathrebu dibynadwy arall"; +"device_verification_cancelled" = "Canslodd y parti arall y gwiriad."; +"device_verification_cancelled_by_me" = "Mae'r gwiriad wedi'i ganslo. Rheswm: %@"; +"device_verification_error_cannot_load_device" = "Methu llwytho gwybodaeth am ddyfeisiau."; +// Mark: Incoming +"device_verification_incoming_title" = "Cais Gwirio sy'n Dod i Mewn"; +"device_verification_incoming_description_1" = "Gwiriwch y ddyfais hon i'w nodi fel un y gellir ymddiried ynddo. Mae dyfeisiau ymddiriedol partneriaid yn rhoi tawelwch meddwl ychwanegol i chi wrth ddefnyddio negeseuon wedi'u hamgryptio o'r dechrau i'r diwedd."; +"device_verification_incoming_description_2" = "Bydd gwirio'r ddyfais hon yn ei nodi fel un y gellir ymddiried ynddo, a hefyd yn marcio'ch dyfais fel un y gellir ymddiried ynddo i'r partner."; +// MARK: Start +"device_verification_start_title" = "Gwirio trwy gymharu testun byr"; +"device_verification_start_wait_partner" = "Aros i'r partner dderbyn ..."; +"device_verification_start_use_legacy" = "Dim byd yn ymddangos? Nid yw pob cleient yn cefnogi gwirio rhyngweithiol eto. Defnyddiwch yr hen fodd o wirio."; +"device_verification_start_verify_button" = "Dechrau Gwirio"; +"device_verification_start_use_legacy_action" = "Defnyddio'r Hen Fodd o Wirio"; +// MARK: Verify +"device_verification_verify_title_emoji" = "Gwiriwch y ddyfais hon trwy gadarnhau'r emoji canlynol sy'n ymddangos ar sgrin y partner"; +"device_verification_verify_title_number" = "Gwiriwch y ddyfais hon trwy gadarnhau'r rhifau canlynol sy'n ymddangos ar sgrin y partner"; +"device_verification_verify_wait_partner" = "Aros i'r partner gadarnhau..."; +// MARK: Verified +"device_verification_verified_title" = "Wedi Gwirio!"; +"device_verification_verified_description_1" = "Rydych chi wedi gwirio'r ddyfais hon yn llwyddiannus."; +"device_verification_verified_description_2" = "Mae negeseuon diogel gyda'r defnyddiwr hwn wedi'u hamgryptio o'r dechrau i'r diwedd ac ni all unrhyw drydydd parti eu darllen."; +"device_verification_verified_got_it_button" = "Iawn"; +// MARK: Emoji +"device_verification_emoji_dog" = "Ci"; +"device_verification_emoji_cat" = "Cath"; +"device_verification_emoji_lion" = "Llew"; +"device_verification_emoji_horse" = "Ceffyl"; +"device_verification_emoji_unicorn" = "Ungorn"; +"device_verification_emoji_pig" = "Mochyn"; +"device_verification_emoji_elephant" = "Eliffant"; +"device_verification_emoji_rabbit" = "Cwningen"; +"device_verification_emoji_panda" = "Panda"; +"device_verification_emoji_rooster" = "Ceiliog"; +"device_verification_emoji_penguin" = "Pengwin"; +"device_verification_emoji_turtle" = "Crwban"; +"device_verification_emoji_fish" = "Pysgodyn"; +"device_verification_emoji_octopus" = "Octopws"; +"device_verification_emoji_butterfly" = "Pilipala"; +"device_verification_emoji_flower" = "Blodyn"; +"device_verification_emoji_tree" = "Coeden"; +"device_verification_emoji_cactus" = "Cactws"; +"device_verification_emoji_mushroom" = "Madarchen"; +"device_verification_emoji_globe" = "Glôb"; +"device_verification_emoji_moon" = "Lleuad"; +"device_verification_emoji_cloud" = "Cwmwl"; +"device_verification_emoji_fire" = "Tân"; +"device_verification_emoji_banana" = "Banana"; +"device_verification_emoji_apple" = "Afal"; +"device_verification_emoji_strawberry" = "Mefusen"; +"device_verification_emoji_corn" = "Ŷd"; +"device_verification_emoji_pizza" = "Pizza"; +"device_verification_emoji_cake" = "Cacen"; +"device_verification_emoji_heart" = "Calon"; +"device_verification_emoji_smiley" = "Gwenoglun"; +"device_verification_emoji_robot" = "Robot"; +"device_verification_emoji_hat" = "Het"; +"device_verification_emoji_glasses" = "Sbectol"; +"device_verification_emoji_spanner" = "Sbaner"; +"device_verification_emoji_santa" = "Siôn Corn"; +"device_verification_emoji_thumbs up" = "Codi bawd"; +"device_verification_emoji_umbrella" = "Ymbarél"; +"device_verification_emoji_hourglass" = "Awrwydr"; +"device_verification_emoji_clock" = "Cloc"; +"device_verification_emoji_gift" = "Rhodd"; +"device_verification_emoji_light bulb" = "Bwlb Golau"; +"device_verification_emoji_book" = "Llyfr"; +"device_verification_emoji_pencil" = "Pensil"; +"device_verification_emoji_paperclip" = "Clip Papur"; +"device_verification_emoji_scissors" = "Siswrn"; +"device_verification_emoji_lock" = "Clo"; +"device_verification_emoji_key" = "Allwedd"; +"device_verification_emoji_hammer" = "Mwrthwl"; +"device_verification_emoji_telephone" = "Ffôn"; +"device_verification_emoji_flag" = "Baner"; +"device_verification_emoji_train" = "Trên"; +"device_verification_emoji_bicycle" = "Beic"; +"device_verification_emoji_aeroplane" = "Awyren"; +"device_verification_emoji_rocket" = "Roced"; +"device_verification_emoji_trophy" = "Tlws"; +"device_verification_emoji_ball" = "Pêl"; +"device_verification_emoji_guitar" = "Gitâr"; +"device_verification_emoji_trumpet" = "Trwmped"; +"device_verification_emoji_bell" = "Cloch"; +"device_verification_emoji_anchor" = "Angor"; +"device_verification_emoji_headphones" = "Clustffonau"; +"device_verification_emoji_folder" = "Plygell"; +"device_verification_emoji_pin" = "Pin"; +// MARK: File upload +"file_upload_error_title" = "Uwchlwythiad Ffeil"; +"file_upload_error_unsupported_file_type_message" = "Ni gefnogir y yma math o ffeil."; +// MARK: Emoji picker +"emoji_picker_title" = "Ymatebion"; +"emoji_picker_people_category" = "Gwenogluniau & Pobl"; +"emoji_picker_nature_category" = "Anifeiliaid & Natur"; +"emoji_picker_foods_category" = "Bwyd & Diod"; +"emoji_picker_activity_category" = "Gweithgareddau"; +"emoji_picker_places_category" = "Teithio & Llefydd"; +"emoji_picker_objects_category" = "Gwrthrychau"; +"emoji_picker_symbols_category" = "Symbolau"; +"emoji_picker_flags_category" = "Baneri"; +// MARK: Reaction history +"reaction_history_title" = "Ymatebion"; +// Generic errors +"error_invite_3pid_with_no_identity_server" = "Ychwanegwch weinydd adnabod yn eich gosodiadau i wahodd trwy e-bost."; +"error_not_supported_on_mobile" = "Ni allwch wneud hyn o %@ ffôn symudol."; diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 5c6ca7b393..3f9f78ddfe 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -897,3 +897,10 @@ "settings_add_3pid_invalid_password_message" = "Ungültiges Passwort"; "identity_server_settings_disconnect_info" = "Wenn Sie die Verbindung zu Ihrem Identitätsserver trennen, werden Sie von anderen Benutzern nicht erkannt und können andere per E-Mail oder Telefon einladen."; "error_not_supported_on_mobile" = "Dies ist in %@ mobile nicht möglich."; +"settings_integrations" = "INTEGRATIONEN"; +"settings_integrations_allow_button" = "Integrationen verwalten"; +"widget_menu_refresh" = "Aktualisierung"; +"widget_menu_open_outside" = "Im Browser öffnen"; +"widget_menu_revoke_permission" = "Zugriff für mich widerrufen"; +"widget_menu_remove" = "Für alle entfernen"; +"widget_integration_manager_disabled" = "Sie müssen den Integration Manager in den Einstellungen aktivieren"; diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 59b4a686b4..4266432f66 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -58,6 +58,9 @@ "sending" = "Sending"; "close" = "Close"; +// Accessibility +"accessibility_checkbox_label" = "checkbox"; + // Authentication "auth_login" = "Log in"; "auth_register" = "Register"; @@ -378,6 +381,7 @@ "settings_calls_settings" = "CALLS"; "settings_discovery_settings" = "DISCOVERY"; "settings_identity_server_settings" = "IDENTITY SERVER"; +"settings_integrations" = "INTEGRATIONS"; "settings_user_interface" = "USER INTERFACE"; "settings_ignored_users" = "IGNORED USERS"; "settings_contacts" = "LOCAL CONTACTS"; @@ -431,6 +435,9 @@ "settings_calls_stun_server_fallback_button" = "Allow fallback call assist server"; "settings_calls_stun_server_fallback_description" = "Allow fallback call assist server %@ when your homeserver does not offer one (your IP address would be shared during a call)."; +"settings_integrations_allow_button" = "Manage integrations"; +"settings_integrations_allow_description" = "Use an Integration Manager (%@) to manage bots, bridges, widgets and sticker packs.\n\nIntegration Managers receive configuration data, and can modify widgets, send room invites and set power levels on your behalf."; + "settings_ui_language" = "Language"; "settings_ui_theme" = "Theme"; "settings_ui_theme_auto" = "Auto"; @@ -755,6 +762,10 @@ "widget_creation_failure" = "Widget creation has failed"; "widget_sticker_picker_no_stickerpacks_alert" = "You don't currently have any stickerpacks enabled."; "widget_sticker_picker_no_stickerpacks_alert_add_now" = "Add some now?"; +"widget_menu_refresh" = "Refresh"; +"widget_menu_open_outside" = "Open in browser"; +"widget_menu_revoke_permission" = "Revoke access for me"; +"widget_menu_remove" = "Remove for everyone"; // Widget Integration Manager "widget_integration_need_to_be_able_to_invite" = "You need to be able to invite users to do that."; @@ -767,9 +778,26 @@ "widget_integration_missing_room_id" = "Missing room_id in request."; "widget_integration_missing_user_id" = "Missing user_id in request."; "widget_integration_room_not_visible" = "Room %@ is not visible."; +"widget_integration_manager_disabled" = "You need to enable Integration Manager in settings"; // Widget Picker "widget_picker_title" = "Integrations"; +"widget_picker_manage_integrations" = "Manage integrations..."; + +// Room widget permissions +"room_widget_permission_title" = "Load Widget"; +"room_widget_permission_creator_info_title" = "This widget was added by:"; + +"room_widget_permission_webview_information_title" = "Using it may set cookies and share data with %@:\n"; + +"room_widget_permission_information_title" = "Using it may share data with %@:\n"; + +"room_widget_permission_display_name_permission" = "Your display name"; +"room_widget_permission_avatar_url_permission" = "Your avatar URL"; +"room_widget_permission_user_id_permission" = "Your user ID"; +"room_widget_permission_theme_permission" = "Your theme"; +"room_widget_permission_widget_id_permission" = "Widget ID"; +"room_widget_permission_room_id_permission" = "Room ID"; // Share extension "share_extension_auth_prompt" = "Login in the main app to share content"; @@ -801,6 +829,7 @@ "service_terms_modal_title_identity_server" = "Contact discovery"; "service_terms_modal_message_identity_server" = "Accept the terms of the identity server (%@) to discover contacts."; +"service_terms_modal_policy_checkbox_accessibility_hint" = "Check to accept %@"; // Deactivate account diff --git a/Riot/Assets/eu.lproj/Vector.strings b/Riot/Assets/eu.lproj/Vector.strings index 435cd11357..0328db0124 100644 --- a/Riot/Assets/eu.lproj/Vector.strings +++ b/Riot/Assets/eu.lproj/Vector.strings @@ -904,3 +904,22 @@ "settings_add_3pid_password_message" = "Jarraitzeko sartu zure pasahitza"; "settings_add_3pid_invalid_password_message" = "Pasahitz baliogabea"; "error_not_supported_on_mobile" = "Ezin duzu hau %@ mugikorretik egin."; +"settings_integrations" = "INTEGRAZIOAK"; +"settings_integrations_allow_button" = "Kudeatu integrazioak"; +"settings_integrations_allow_description" = "Erabili integrazio kudeatzaileren bat botak, zubiak, trepetak eta eranskailu multzoak kudeatzeko.\n\nIntegrazio kudeatzaileek konfigurazio datuak jasotzen dituzte, eta trepetak aldatu ditzakete, gelarako gonbidapenak bidali, eta botere mailak zure izenean ezarri."; +"widget_menu_refresh" = "Freskatu"; +"widget_menu_open_outside" = "Ireki nabigatzailean"; +"widget_menu_revoke_permission" = "Indargabetu sarbidea niretzat"; +"widget_menu_remove" = "Kendu denentzat"; +"widget_integration_manager_disabled" = "Integrazio kudeatzaileak gaitu behar dituzu ezarpenetan"; +// Room widget permissions +"room_widget_permission_title" = "Kargatu trepeta"; +"room_widget_permission_creator_info_title" = "Trepeta hau honek gehitu du:"; +"room_widget_permission_webview_information_title" = "Hau erabiltzean cookieak ezarri litezke eta %@ zerbitzariarekin datuak partekatu:\n"; +"room_widget_permission_information_title" = "Hau erabiltzean %@ zerbitzariarekin datuak partekatu litezke:\n"; +"room_widget_permission_display_name_permission" = "Zure pantaila-izena"; +"room_widget_permission_avatar_url_permission" = "Zure abatarraren URL-a"; +"room_widget_permission_user_id_permission" = "Zure erabiltzaile ID-a"; +"room_widget_permission_theme_permission" = "Zure gaia"; +"room_widget_permission_widget_id_permission" = "Trepetaren ID-a"; +"room_widget_permission_room_id_permission" = "Gelaren ID-a"; diff --git a/Riot/Assets/fr.lproj/Vector.strings b/Riot/Assets/fr.lproj/Vector.strings index 4f466ccdab..ce173345ae 100644 --- a/Riot/Assets/fr.lproj/Vector.strings +++ b/Riot/Assets/fr.lproj/Vector.strings @@ -916,3 +916,25 @@ "settings_add_3pid_password_message" = "Pour continuer, saisissez votre mot de passe"; "settings_add_3pid_invalid_password_message" = "Mot de passe non valide"; "error_not_supported_on_mobile" = "Vous ne pouvez pas faire cela depuis %@ mobile."; +"widget_menu_refresh" = "Actualiser"; +"widget_menu_open_outside" = "Ouvrir dans le navigateur"; +"widget_menu_revoke_permission" = "Révoquer l’accès pour moi"; +"widget_menu_remove" = "Supprimer pour tout le monde"; +"settings_integrations" = "INTÉGRATIONS"; +"settings_integrations_allow_button" = "Gérer les intégrations"; +"settings_integrations_allow_description" = "Utilisez un gestionnaire d’intégrations (%@) pour gérer les bots, les passerelles, les widgets et les packs de stickers.\n\nLes gestionnaires d’intégration reçoivent des données de configuration et peuvent modifier les widgets, envoyer des invitations de salon et définir des rangs à votre place."; +"widget_integration_manager_disabled" = "Vous devez activer le gestionnaire d’intégrations dans les paramètres"; +"widget_room_permission_title" = "Charger le widget"; +"widget_room_permission_creator_info_title" = "Ce widget a été ajouté par :"; +"widget_room_permission_information" = "Son utilisation peut utiliser des cookies et partager des données avec %@ :\n\n• Votre nom affiché\n• L’URL de votre avatar\n• Votre identifiant d’utilisateur\n• Votre thème\n• L’identifiant du salon\n• L’identifiant du widget"; +// Room widget permissions +"room_widget_permission_title" = "Charger un widget"; +"room_widget_permission_creator_info_title" = "Ce widget a été ajouté par :"; +"room_widget_permission_webview_information_title" = "Son utilisation peut entraîner l’utilisation de cookies et le partage de données avec %@ :\n"; +"room_widget_permission_information_title" = "Son utilisation peut entraîner le partage de données avec %@ :\n"; +"room_widget_permission_display_name_permission" = "Votre nom affiché"; +"room_widget_permission_avatar_url_permission" = "L’URL de votre avatar"; +"room_widget_permission_user_id_permission" = "Votre identifiant d’utilisateur"; +"room_widget_permission_theme_permission" = "Votre thème"; +"room_widget_permission_widget_id_permission" = "L’identifiant du widget"; +"room_widget_permission_room_id_permission" = "L’identifiant du salon"; diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index 9c0ce9f952..4035eae136 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -921,3 +921,22 @@ "settings_add_3pid_password_message" = "A folytatáshoz add meg a jelszavadat"; "settings_add_3pid_invalid_password_message" = "Érvénytelen jelszó"; "error_not_supported_on_mobile" = "%@ mobilról ezt nem teheted meg."; +"widget_menu_refresh" = "Frissítés"; +"widget_menu_open_outside" = "Megnyitás böngészőben"; +"widget_menu_revoke_permission" = "Hozzáférés megvonása magamtól"; +"widget_menu_remove" = "Visszavonás mindenkitől"; +"settings_integrations" = "INTEGRÁCIÓK"; +"settings_integrations_allow_button" = "Integrációk kezelése"; +"settings_integrations_allow_description" = "Botok, hidak, kisalkalmazások és matrica csomagok kezeléséhez használj Integrációs Menedzsert (%@).\n\nIntegrációs Menedzser megkapja a konfigurációt, módosíthat kisalkalmazásokat, szobához meghívót küldhet és a hozzáférési szintet beállíthatja helyetted."; +"widget_integration_manager_disabled" = "Az integrációs menedzsert engedélyezned kell a beállításokban"; +// Room widget permissions +"room_widget_permission_title" = "Kisalkalmazás betöltése"; +"room_widget_permission_creator_info_title" = "Ezt a kisalkalmazást hozzáadta:"; +"room_widget_permission_webview_information_title" = "A használatához lehet, hogy sütiket kell használni és adat lesz megosztva ezzel: %@:\n"; +"room_widget_permission_information_title" = "A használatához lehet, hogy adat lesz megosztva ezzel: %@:\n"; +"room_widget_permission_display_name_permission" = "Megjelenítési neved"; +"room_widget_permission_avatar_url_permission" = "Profilképed URL-je"; +"room_widget_permission_user_id_permission" = "Felhasználói azonosítód"; +"room_widget_permission_theme_permission" = "Témád"; +"room_widget_permission_widget_id_permission" = "Kisalkalmazás azon."; +"room_widget_permission_room_id_permission" = "Szoba azonosító"; diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index f1666242db..0a553b952f 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -7,7 +7,7 @@ "title_groups" = "Comunità"; "warning" = "Attenzione"; "next" = "Prossimo"; -"leave" = "Lascia"; +"leave" = "Esci"; "remove" = "Rimuovi"; "invite" = "Invita"; "cancel" = "Annulla"; @@ -61,7 +61,7 @@ // String for App Store "store_short_description" = "Conversazioni sicure e decentralizzate"; // Actions -"view" = "Vedi"; +"view" = "Visualizza"; "back" = "Indietro"; "continue" = "Continua"; "create" = "Crea"; @@ -391,7 +391,7 @@ "settings_key_backup_delete_confirmation_prompt_title" = "Elimina backup"; "settings_key_backup_delete_confirmation_prompt_msg" = "Sei sicuro? Se non hai eseguito un Backup delle Chiavi perderai i tuoi messaggi crittografati."; // Room Details -"room_details_title" = "Dettagli stanza"; +"room_details_title" = "Dettagli canale"; "room_details_people" = "Membri"; "room_details_files" = "File"; "room_details_settings" = "Impostazioni"; @@ -891,3 +891,22 @@ "settings_add_3pid_password_message" = "Per continuare, inserisci la tua password"; "settings_add_3pid_invalid_password_message" = "Password non valida"; "error_not_supported_on_mobile" = "Non puoi farlo da %@ mobile."; +"widget_menu_refresh" = "Ricarica"; +"widget_menu_open_outside" = "Apri nel browser"; +"widget_menu_revoke_permission" = "Revoca l'accesso a me"; +"widget_menu_remove" = "Rimuovi per tutti"; +"settings_integrations" = "INTEGRAZIONI"; +"settings_integrations_allow_button" = "Gestisci le integrazioni"; +"settings_integrations_allow_description" = "Usa un gestore di integrazioni (%@) per gestire bot, bridge, widget e pacchetti di adesivi.\n\nI gestori di integrazione ricevono dati di configurazione e possono modificare widget, inviare inviti alla stanza, assegnare permessi a tuo nome."; +"widget_integration_manager_disabled" = "Devi attivare il gestore di integrazioni nelle impostazioni"; +// Room widget permissions +"room_widget_permission_title" = "Carica widget"; +"room_widget_permission_creator_info_title" = "Questo widget è stato aggiunto da:"; +"room_widget_permission_webview_information_title" = "Usarlo potrebbe impostare cookie e condividere dati con %@:\n"; +"room_widget_permission_information_title" = "Usarlo potrebbe condividere dati con %@:\n"; +"room_widget_permission_display_name_permission" = "Il tuo nome visualizzato"; +"room_widget_permission_avatar_url_permission" = "Il tuo URL dell'avatar"; +"room_widget_permission_user_id_permission" = "Il tuo ID utente"; +"room_widget_permission_theme_permission" = "Il tuo tema"; +"room_widget_permission_widget_id_permission" = "ID widget"; +"room_widget_permission_room_id_permission" = "ID stanza"; diff --git a/Riot/Assets/ko.lproj/Vector.strings b/Riot/Assets/ko.lproj/Vector.strings index 88c5df6fd4..90d153d913 100644 --- a/Riot/Assets/ko.lproj/Vector.strings +++ b/Riot/Assets/ko.lproj/Vector.strings @@ -265,7 +265,7 @@ "room_event_action_reaction_show_all" = "모두 보이기"; "room_event_action_reaction_show_less" = "적게 보이기"; "room_event_action_reaction_history" = "리액션 기록"; -"room_warning_about_encryption" = "종단간 암호화는 베타 버전이고 신뢰하지 못할 수 있습니다.\n\n아직 데이터를 보호한다고 신뢰하지 마세요.\n\n기기가 방에 참가하기 전에 아직 기록을 해독할 수 없습니다.\n\n아직 암호화를 구현하지 않았기 때문에 암호화된 메시지는 클라이언트에 나타나지 않습니다."; +"room_warning_about_encryption" = "종단간 암호화는 베타 버전이고 신뢰하지 못할 수 있습니다.\n\n아직 데이터를 보호한다고 신뢰하지 마세요.\n\n기기가 방에 참가하기 전에 아직 기록을 복호화할 수 없습니다.\n\n아직 암호화를 구현하지 않았기 때문에 암호화된 메시지는 클라이언트에 나타나지 않습니다."; "room_event_failed_to_send" = "보내기에 실패함"; "room_action_camera" = "사진 또는 영상 찍기"; "room_action_send_photo_or_video" = "사진 또는 영상 보내기"; @@ -629,7 +629,7 @@ "deactivate_account_password_alert_message" = "계속하려면, 비밀번호를 입력해주세요"; // Re-request confirmation dialog "rerequest_keys_alert_title" = "요청을 보냈습니다"; -"rerequest_keys_alert_message" = "메시지를 해독해서 이 기기로 키를 보낼 수 있도록 Riot을 다른 기기에 설치해주세요."; +"rerequest_keys_alert_message" = "메시지를 복호화해서 이 기기로 키를 보낼 수 있도록 Riot을 다른 기기에 설치해주세요."; "key_backup_setup_title" = "키 백업"; "key_backup_setup_skip_alert_title" = "확신합니까?"; "key_backup_setup_skip_alert_message" = "로그아웃하거나 기기를 잃어버리면 보안 메시지를 잃게 됩니다."; @@ -665,9 +665,9 @@ "key_backup_setup_success_from_recovery_key_made_copy_action" = "사본을 만들었습니다"; "key_backup_recover_title" = "보안 메시지"; "key_backup_recover_invalid_passphrase_title" = "맞지 않는 복구 암호"; -"key_backup_recover_invalid_passphrase" = "이 암호로 백업을 해독할 수 없습니다: 올바른 복구 암호를 입력해서 확인해주세요."; +"key_backup_recover_invalid_passphrase" = "이 암호로 백업을 복호화할 수 없습니다: 올바른 복구 암호를 입력해서 확인해주세요."; "key_backup_recover_invalid_recovery_key_title" = "복구 키가 맞지 않음"; -"key_backup_recover_invalid_recovery_key" = "이 키로 백업을 해독할 수 없습니다: 올바른 복구 키를 입력해서 확인해주세요."; +"key_backup_recover_invalid_recovery_key" = "이 키로 백업을 복호화할 수 없습니다: 올바른 복구 키를 입력해서 확인해주세요."; "key_backup_recover_from_passphrase_info" = "복구 암호를 사용해 보안 메시지 기록을 푸세요"; "key_backup_recover_from_passphrase_passphrase_title" = "입력"; "key_backup_recover_from_passphrase_passphrase_placeholder" = "암호 입력"; @@ -889,3 +889,11 @@ "settings_add_3pid_password_message" = "계속하려면 비밀번호를 입력해주세요"; "settings_add_3pid_invalid_password_message" = "잘못된 비밀번호"; "error_not_supported_on_mobile" = "%@ 모바일에서 할 수 없습니다."; +"settings_integrations" = "통합"; +"settings_integrations_allow_button" = "통합 관리"; +"settings_integrations_allow_description" = "통합 관리자 (%@)를 사용해 봇, 브릿지, 위젯과 스티커 팩을 관리하세요.\n\n통합 관리자는 설정 데이터를 받고 위젯을 수정하거나, 방 초대를 보내고 권한 등급을 설정할 수 있습니다."; +"widget_menu_refresh" = "새로고침"; +"widget_menu_open_outside" = "브라우저에서 열기"; +"widget_menu_revoke_permission" = "액세스 취소"; +"widget_menu_remove" = "모두를 위해 제거"; +"widget_integration_manager_disabled" = "설정에서 통합 관리자를 켜야 합니다"; diff --git a/Riot/Assets/sq.lproj/Vector.strings b/Riot/Assets/sq.lproj/Vector.strings index 9c93a1877f..e706080d1f 100644 --- a/Riot/Assets/sq.lproj/Vector.strings +++ b/Riot/Assets/sq.lproj/Vector.strings @@ -850,3 +850,63 @@ "contacts_address_book_no_identity_server" = "S’ka të formësuar shërbyes identitetesh"; "settings_discovery_settings" = "ZBULIM"; "settings_identity_server_settings" = "SHËRBYES IDENTITETESH"; +"settings_three_pids_management_information_part1" = "Administroni cilat adresa email apo numra telefonash mund të përdorni për të bërë hyrjen ose për të rimarrë llogarinë tuaj këtu. Kontrolloni cilët mund t’ju gjejnë "; +"settings_three_pids_management_information_part2" = "Zbulim"; +"settings_three_pids_management_information_part3" = "."; +"settings_add_3pid_password_title_email" = "Shtoni adresë email"; +"settings_add_3pid_password_title_msidsn" = "Shtoni numër telefoni"; +"settings_add_3pid_password_message" = "Që të vazhdohet, ju lutemi, jepni fjalëkalimin tuaj"; +"settings_add_3pid_invalid_password_message" = "Fjalëkalim i pavlefshëm"; +"settings_devices_description" = "Emri publik i një pajisjeje është i dukshëm për persona me të cilët komunikoni"; +"settings_discovery_no_identity_server" = "S’po përdorni ndonjë shërbyes identitetesh. Që të jeni i zbulueshëm nga kontakte ekzistuese që njihni, shtoni një të tillë."; +"settings_discovery_terms_not_signed" = "Pajtohuni me Kushtet e Shërbimit të Shërbyesit të Identiteteve që t’i lejoni vetes të jeni i zbulueshëm përmes adrese email ose numri telefoni."; +"settings_discovery_three_pids_management_information_part1" = "Administroni cilat adresa email ose numra telefonash mund të përdorin përdoruesit e tjerë për t’ju zbuluar dhe ftuar në dhoma. Shtoni ose hiqni prej kësaj liste adresa email ose numra telefonash "; +"settings_discovery_three_pids_management_information_part2" = "Rregullime Përdoruesi"; +"settings_discovery_three_pids_management_information_part3" = "."; +"settings_discovery_error_message" = "Ndodhi një gabim. Ju lutemi, riprovoni."; +"settings_discovery_three_pid_details_title_email" = "Administroni email"; +"settings_discovery_three_pid_details_information_email" = "Administroni parapëlqime për këtë adresë email, të cilët përdorues të tjerë mund ta përdorin për t’ju zbuluar dhe ftuar në dhoma. Shtoni ose hiqni adresa email te Llogaritë."; +"settings_discovery_three_pid_details_title_phone_number" = "Administroni numër telefoni"; +"settings_discovery_three_pid_details_information_phone_number" = "Administroni parapëlqime për këtë numër telefoni, të cilin mund ta përdorin përdorues të tjerë për t’ju zbuluar dhe ftuar në dhoma. Shtoni ose hiqni numra telefonash te Llogaritë."; +"settings_discovery_three_pid_details_share_action" = "Ndajeni me të tjerë"; +"settings_discovery_three_pid_details_revoke_action" = "Shfuqizoje"; +"settings_discovery_three_pid_details_cancel_email_validation_action" = "Anuloni vlerësim email-i"; +"settings_discovery_three_pid_details_enter_sms_code_action" = "Jepni kod SMS aktivizimi"; +"settings_identity_server_description" = "Duke përdorur shërbyesin e identiteteve më sipër mund të zbuloni dhe të jeni i zbulueshëm nga kontakte ekzistuese që njihni."; +"settings_identity_server_no_is" = "S’ka të formësuar shërbyes identitetesh"; +"settings_identity_server_no_is_description" = "S’po përdorni ndonjë shërbyes identitetesh. Që të zbuloni dhe të jeni i zbulueshëm nga kontakte ekzistuese që njihni, shtoni një më sipër."; +// Identity server settings +"identity_server_settings_title" = "Shërbyes Identitetesh"; +"identity_server_settings_description" = "Po përdorni %@ që të zbuloni dhe të jeni i zbulueshëm nga kontakte ekzistuese që dini."; +"identity_server_settings_no_is_description" = "S’po përdorni ndonjë shërbyes identitetesh. Që të zbuloni dhe të jeni i zbulueshëm nga kontakte ekzistuese, shtoni një më sipër."; +"identity_server_settings_place_holder" = "Jepni një shërbyes identitetesh"; +"identity_server_settings_add" = "Shtoje"; +"identity_server_settings_change" = "Ndryshojeni"; +"identity_server_settings_disconnect_info" = "Shkëputja nga shërbyesi juaj i identiteteve do të thotë se s’do të jeni të zbulueshëm nga përdorues të tjerë dhe as të jeni në gjendje të ftoni të tjerë përmes email-i ose telefoni."; +"identity_server_settings_disconnect" = "Shkëputu"; +"identity_server_settings_alert_no_terms_title" = "Shërbyesi i identiteteve s’ka kushte shërbimi"; +"identity_server_settings_alert_no_terms" = "Shërbyesi i identiteteve që keni zgjedhur nuk ka ndonjë kusht shërbimesh. Vazhdoni vetëm nëse i zini besë të zotit të shërbyesit."; +"identity_server_settings_alert_change_title" = "Ndryshoni shërbyes identitetesh"; +"identity_server_settings_alert_change" = "Të bëhet shkëputja nga shërbyesi i identiteteve %1$@ dhe të lidhet me %2$@?"; +"identity_server_settings_alert_disconnect_title" = "Shkëpute shërbyesin e identiteteve"; +"identity_server_settings_alert_disconnect" = "Të bëhet shkëputja nga shërbyesi i identiteteve %@?"; +"identity_server_settings_alert_disconnect_button" = "Shkëpute"; +"identity_server_settings_alert_disconnect_still_sharing_3pid" = "Ende ndani me të tjerët të dhëna tuajat personale në shërbyesin e identiteteve %@.\n\nKëshillojmë që të hiqni prej shërbyesit të identiteteve adresat tuaj email dhe numrat tuaj të telefonave përpara se të bëni shkëputjen."; +"identity_server_settings_alert_disconnect_still_sharing_3pid_button" = "Shkëputu, sido qoftë"; +"identity_server_settings_alert_error_terms_not_accepted" = "Duhet të pranoni termat e %@ që ta caktoni si shërbyes identitetesh."; +"identity_server_settings_alert_error_invalid_identity_server" = "%@ s’është shërbyes i vlershëm identitetesh."; +"call_no_stun_server_error_title" = "Thirrja dështoi për shkak shërbyesi të keqformësuar"; +"call_no_stun_server_error_message_1" = "Që thirrjet të funksionojnë pa probleme, ju lutemi, kërkojini përgjegjësit të shërbyesit tuaj Home %@ të formësojë një shërbyes TURN."; +"call_no_stun_server_error_message_2" = "Ndryshe, mund të provoni të përdorni shërbyesin publik te %@, por kjo s’do të jetë edhe aq e qëndrueshme, dhe adresa juaj IP do t’i bëhet e njohur atij shërbyesi. Këtë mund ta bëni edhe që nga Rregullimet"; +"call_no_stun_server_error_use_fallback_button" = "Provoni të përdorni %@"; +// Widget Picker +"widget_picker_title" = "Integrime"; +"service_terms_modal_decline_button" = "Hidhe poshtë"; +"service_terms_modal_description_for_identity_server_1" = "Gjeni të tjerë përmes telefoni ose email-i"; +"service_terms_modal_description_for_identity_server_2" = "Bëhuni i gjetshëm përmes telefoni ose email-i"; +// Service terms - Variant for identity server when displayed out of a context +"service_terms_modal_title_identity_server" = "Zbulim kontaktesh"; +"service_terms_modal_message_identity_server" = "Që të zbuloni kontakte, pranoni kushtet e shërbyesit të identiteteve (%@)."; +// Generic errors +"error_invite_3pid_with_no_identity_server" = "Që të ftoni me email, shtoni një shërbyes identitetesh, që nga rregullimet tuaja."; +"error_not_supported_on_mobile" = "Këtë s’mund ta bëni nga %@ për celular."; diff --git a/Riot/Categories/FloatingPoint.swift b/Riot/Categories/FloatingPoint.swift new file mode 100644 index 0000000000..3d7e5639f0 --- /dev/null +++ b/Riot/Categories/FloatingPoint.swift @@ -0,0 +1,29 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +extension FloatingPoint { + + /// Returns clamped `self` value. + /// https://gist.github.com/laevandus/6fd35992157fcc9b5660bcbc82ebfb52#file-clampfloatingpoint-swift + /// + /// - Parameter range: The closed range in which `self` should be clamped (`0.2...3.3` for example). + /// - Returns: A FloatingPoint clamped value. + func clamped(to range: ClosedRange) -> Self { + return max(min(self, range.upperBound), range.lowerBound) + } +} diff --git a/Riot/Categories/UIButton.swift b/Riot/Categories/UIButton.swift index 01c1095f44..0e3af78ef3 100644 --- a/Riot/Categories/UIButton.swift +++ b/Riot/Categories/UIButton.swift @@ -29,4 +29,15 @@ extension UIButton { titleLabel.numberOfLines = 0 titleLabel.textAlignment = textAlignment } + + /// Set background color as an image. + /// Useful to automatically adjust highlighted background if `adjustsImageWhenHighlighted` property is set to true or disabled background when `adjustsImageWhenDisabled`is set to true. + /// + /// - Parameters: + /// - color: The background color to set as an image. + /// - state: The control state for wich to apply this color. + func vc_setBackgroundColor(_ color: UIColor, for state: UIControl.State) { + let image = UIImage.vc_image(from: color) + self.setBackgroundImage(image, for: state) + } } diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 6953898f63..5daa4047a2 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -32,6 +32,7 @@ internal enum Asset { internal static let adminIcon = ImageAsset(name: "admin_icon") internal static let backIcon = ImageAsset(name: "back_icon") internal static let chevron = ImageAsset(name: "chevron") + internal static let closeButton = ImageAsset(name: "close_button") internal static let disclosureIcon = ImageAsset(name: "disclosure_icon") internal static let group = ImageAsset(name: "group") internal static let logo = ImageAsset(name: "logo") diff --git a/Riot/Generated/Storyboards.swift b/Riot/Generated/Storyboards.swift index da0ccfc9f3..7ec5424ed2 100644 --- a/Riot/Generated/Storyboards.swift +++ b/Riot/Generated/Storyboards.swift @@ -117,6 +117,11 @@ internal enum StoryboardScene { internal static let initialScene = InitialSceneType(storyboard: TemplateScreenViewController.self) } + internal enum WidgetPermissionViewController: StoryboardType { + internal static let storyboardName = "WidgetPermissionViewController" + + internal static let initialScene = InitialSceneType(storyboard: WidgetPermissionViewController.self) + } } // swiftlint:enable explicit_type_interface identifier_name line_length type_body_length type_name diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 12b5d5b6b7..65f8cc1cce 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -14,6 +14,10 @@ internal enum VectorL10n { internal static var accept: String { return VectorL10n.tr("Vector", "accept") } + /// checkbox + internal static var accessibilityCheckboxLabel: String { + return VectorL10n.tr("Vector", "accessibility_checkbox_label") + } /// Logout all accounts internal static var accountLogoutAll: String { return VectorL10n.tr("Vector", "account_logout_all") @@ -2474,6 +2478,46 @@ internal enum VectorL10n { internal static var roomWarningAboutEncryption: String { return VectorL10n.tr("Vector", "room_warning_about_encryption") } + /// Your avatar URL + internal static var roomWidgetPermissionAvatarUrlPermission: String { + return VectorL10n.tr("Vector", "room_widget_permission_avatar_url_permission") + } + /// This widget was added by: + internal static var roomWidgetPermissionCreatorInfoTitle: String { + return VectorL10n.tr("Vector", "room_widget_permission_creator_info_title") + } + /// Your display name + internal static var roomWidgetPermissionDisplayNamePermission: String { + return VectorL10n.tr("Vector", "room_widget_permission_display_name_permission") + } + /// Using it may share data with %@:\n + internal static func roomWidgetPermissionInformationTitle(_ p1: String) -> String { + return VectorL10n.tr("Vector", "room_widget_permission_information_title", p1) + } + /// Room ID + internal static var roomWidgetPermissionRoomIdPermission: String { + return VectorL10n.tr("Vector", "room_widget_permission_room_id_permission") + } + /// Your theme + internal static var roomWidgetPermissionThemePermission: String { + return VectorL10n.tr("Vector", "room_widget_permission_theme_permission") + } + /// Load Widget + internal static var roomWidgetPermissionTitle: String { + return VectorL10n.tr("Vector", "room_widget_permission_title") + } + /// Your user ID + internal static var roomWidgetPermissionUserIdPermission: String { + return VectorL10n.tr("Vector", "room_widget_permission_user_id_permission") + } + /// Using it may set cookies and share data with %@:\n + internal static func roomWidgetPermissionWebviewInformationTitle(_ p1: String) -> String { + return VectorL10n.tr("Vector", "room_widget_permission_webview_information_title", p1) + } + /// Widget ID + internal static var roomWidgetPermissionWidgetIdPermission: String { + return VectorL10n.tr("Vector", "room_widget_permission_widget_id_permission") + } /// Save internal static var save: String { return VectorL10n.tr("Vector", "save") @@ -2546,6 +2590,10 @@ internal enum VectorL10n { internal static func serviceTermsModalMessageIdentityServer(_ p1: String) -> String { return VectorL10n.tr("Vector", "service_terms_modal_message_identity_server", p1) } + /// Check to accept %@ + internal static func serviceTermsModalPolicyCheckboxAccessibilityHint(_ p1: String) -> String { + return VectorL10n.tr("Vector", "service_terms_modal_policy_checkbox_accessibility_hint", p1) + } /// Terms Of Service internal static var serviceTermsModalTitle: String { return VectorL10n.tr("Vector", "service_terms_modal_title") @@ -2810,6 +2858,18 @@ internal enum VectorL10n { internal static var settingsIgnoredUsers: String { return VectorL10n.tr("Vector", "settings_ignored_users") } + /// INTEGRATIONS + internal static var settingsIntegrations: String { + return VectorL10n.tr("Vector", "settings_integrations") + } + /// Manage integrations + internal static var settingsIntegrationsAllowButton: String { + return VectorL10n.tr("Vector", "settings_integrations_allow_button") + } + /// Use an Integration Manager (%@) to manage bots, bridges, widgets and sticker packs.\n\nIntegration Managers receive configuration data, and can modify widgets, send room invites and set power levels on your behalf. + internal static func settingsIntegrationsAllowDescription(_ p1: String) -> String { + return VectorL10n.tr("Vector", "settings_integrations_allow_description", p1) + } /// KEY BACKUP internal static var settingsKeyBackup: String { return VectorL10n.tr("Vector", "settings_key_backup") @@ -3250,6 +3310,10 @@ internal enum VectorL10n { internal static var widgetIntegrationFailedToSendRequest: String { return VectorL10n.tr("Vector", "widget_integration_failed_to_send_request") } + /// You need to enable Integration Manager in settings + internal static var widgetIntegrationManagerDisabled: String { + return VectorL10n.tr("Vector", "widget_integration_manager_disabled") + } /// Missing room_id in request. internal static var widgetIntegrationMissingRoomId: String { return VectorL10n.tr("Vector", "widget_integration_missing_room_id") @@ -3290,6 +3354,22 @@ internal enum VectorL10n { internal static var widgetIntegrationsServerFailedToConnect: String { return VectorL10n.tr("Vector", "widget_integrations_server_failed_to_connect") } + /// Open in browser + internal static var widgetMenuOpenOutside: String { + return VectorL10n.tr("Vector", "widget_menu_open_outside") + } + /// Refresh + internal static var widgetMenuRefresh: String { + return VectorL10n.tr("Vector", "widget_menu_refresh") + } + /// Remove for everyone + internal static var widgetMenuRemove: String { + return VectorL10n.tr("Vector", "widget_menu_remove") + } + /// Revoke access for me + internal static var widgetMenuRevokePermission: String { + return VectorL10n.tr("Vector", "widget_menu_revoke_permission") + } /// No integrations server configured internal static var widgetNoIntegrationsServerConfigured: String { return VectorL10n.tr("Vector", "widget_no_integrations_server_configured") @@ -3298,6 +3378,10 @@ internal enum VectorL10n { internal static var widgetNoPowerToManage: String { return VectorL10n.tr("Vector", "widget_no_power_to_manage") } + /// Manage integrations... + internal static var widgetPickerManageIntegrations: String { + return VectorL10n.tr("Vector", "widget_picker_manage_integrations") + } /// Integrations internal static var widgetPickerTitle: String { return VectorL10n.tr("Vector", "widget_picker_title") diff --git a/Riot/Managers/RoomMessageLinkParser/RoomMessageURLParser.swift b/Riot/Managers/RoomMessageLinkParser/RoomMessageURLParser.swift new file mode 100644 index 0000000000..758cf72c6e --- /dev/null +++ b/Riot/Managers/RoomMessageLinkParser/RoomMessageURLParser.swift @@ -0,0 +1,65 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +@objc enum RoomMessageURLType: Int { + case appleDataDetector + case http + case dummy + case unknown +} + +/// URL parser for room messages. +@objcMembers +final class RoomMessageURLParser: NSObject { + + // MARK: - Constants + + private enum Scheme { + static let appleDataDetector = "x-apple-data-detectors" + static let http = "http" + static let https = "https" + } + + private enum Constants { + static let dummyURL = "#" + } + + // MARK: - Public + + func parseURL(_ url: URL) -> RoomMessageURLType { + + let roomMessageLink: RoomMessageURLType + + if let scheme = url.scheme?.lowercased() { + switch scheme { + case Scheme.appleDataDetector: + roomMessageLink = .appleDataDetector + case Scheme.http, Scheme.https: + roomMessageLink = .http + default: + roomMessageLink = .unknown + } + } else if url.absoluteString == Constants.dummyURL { + roomMessageLink = .dummy + } else { + roomMessageLink = .unknown + } + + return roomMessageLink + } +} diff --git a/Riot/Managers/Serialization/SerializationService.swift b/Riot/Managers/Serialization/SerializationService.swift index ca0dae0dd6..da083fc939 100644 --- a/Riot/Managers/Serialization/SerializationService.swift +++ b/Riot/Managers/Serialization/SerializationService.swift @@ -28,6 +28,18 @@ final class SerializationService: SerializationServiceType { func deserialize(_ data: Data) throws -> T { return try decoder.decode(T.self, from: data) } + + func deserialize(_ object: Any) throws -> T { + let jsonData: Data + + if let data = object as? Data { + jsonData = data + } else { + jsonData = try JSONSerialization.data(withJSONObject: object, options: []) + } + return try decoder.decode(T.self, from: jsonData) + } + func serialize(_ object: T) throws -> Data { return try encoder.encode(object) diff --git a/Riot/Managers/Serialization/SerializationServiceType.swift b/Riot/Managers/Serialization/SerializationServiceType.swift index 2a0d77e04c..f9911e9401 100644 --- a/Riot/Managers/Serialization/SerializationServiceType.swift +++ b/Riot/Managers/Serialization/SerializationServiceType.swift @@ -18,5 +18,7 @@ import Foundation protocol SerializationServiceType { func deserialize(_ data: Data) throws -> T + func deserialize(_ object: Any) throws -> T + func serialize(_ object: T) throws -> Data } diff --git a/Riot/Managers/Settings/Shared/JSONModels/RiotSettingAllowedWidgets.swift b/Riot/Managers/Settings/Shared/JSONModels/RiotSettingAllowedWidgets.swift new file mode 100644 index 0000000000..a63624edab --- /dev/null +++ b/Riot/Managers/Settings/Shared/JSONModels/RiotSettingAllowedWidgets.swift @@ -0,0 +1,33 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +/// Model for "im.vector.setting.allowed_widgets" +/// https://github.com/vector-im/riot-meta/blob/master/spec/settings.md#tracking-which-widgets-the-user-has-allowed-to-load +struct RiotSettingAllowedWidgets { + let widgets: [String: Bool] + + // Widget type -> Server domain -> Bool + let nativeWidgets: [String: [String: Bool]] +} + +extension RiotSettingAllowedWidgets: Decodable { + enum CodingKeys: String, CodingKey { + case widgets + case nativeWidgets = "native_widgets" + } +} diff --git a/Riot/Managers/Settings/Shared/JSONModels/RiotSettingIntegrationProvisioning.swift b/Riot/Managers/Settings/Shared/JSONModels/RiotSettingIntegrationProvisioning.swift new file mode 100644 index 0000000000..4c707a6b28 --- /dev/null +++ b/Riot/Managers/Settings/Shared/JSONModels/RiotSettingIntegrationProvisioning.swift @@ -0,0 +1,29 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +/// Model for "im.vector.setting.integration_provisioning" +/// https://github.com/vector-im/riot-meta/blob/master/spec/settings.md#selecting-no-provisioning-for-integration-managers +struct RiotSettingIntegrationProvisioning { + let enabled: Bool +} + +extension RiotSettingIntegrationProvisioning: Decodable { + enum CodingKeys: String, CodingKey { + case enabled + } +} diff --git a/Riot/Managers/Settings/Shared/RiotSharedSettings.swift b/Riot/Managers/Settings/Shared/RiotSharedSettings.swift new file mode 100644 index 0000000000..df70ad310a --- /dev/null +++ b/Riot/Managers/Settings/Shared/RiotSharedSettings.swift @@ -0,0 +1,219 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +import MatrixSDK + +@objc enum WidgetPermission: Int { + case undefined + case granted + case declined +} + +/// Shared user settings across all Riot clients. +/// It implements https://github.com/vector-im/riot-meta/blob/master/spec/settings.md +@objcMembers +class RiotSharedSettings: NSObject { + + // MARK: - Constants + private enum Settings { + static let breadcrumbs = "im.vector.setting.breadcrumbs" + static let integrationProvisioning = "im.vector.setting.integration_provisioning" + static let allowedWidgets = "im.vector.setting.allowed_widgets" + } + + + // MARK: - Properties + // MARK: Private + private let session: MXSession + private lazy var serializationService: SerializationServiceType = SerializationService() + + + // MARK: - Setup + + init(session: MXSession) { + self.session = session + } + + + // MARK: - Public + + // MARK: Integration provisioning + + var hasIntegrationProvisioningEnabled: Bool { + return getIntegrationProvisioning()?.enabled ?? true + } + + func getIntegrationProvisioning() -> RiotSettingIntegrationProvisioning? { + guard let integrationProvisioningDict = getAccountData(forEventType: Settings.integrationProvisioning) else { + return nil + } + + return try? serializationService.deserialize(integrationProvisioningDict) + } + + @discardableResult + func setIntegrationProvisioning(enabled: Bool, + success: @escaping () -> Void, + failure: @escaping (Error?) -> Void) + -> MXHTTPOperation? { + + // Update only the "widgets" field in the account data + var integrationProvisioningDict = getAccountData(forEventType: Settings.integrationProvisioning) ?? [:] + integrationProvisioningDict[RiotSettingIntegrationProvisioning.CodingKeys.enabled.rawValue] = enabled + + return session.setAccountData(integrationProvisioningDict, forType: Settings.integrationProvisioning, success: success, failure: failure) + } + + + // MARK: Allowed widgets + func permission(for widget: Widget) -> WidgetPermission { + guard let allowedWidgets = getAllowedWidgets() else { + return .undefined + } + + if let value = allowedWidgets.widgets[widget.widgetEvent.eventId] { + return value == true ? .granted : .declined + } else { + return .undefined + } + } + + func getAllowedWidgets() -> RiotSettingAllowedWidgets? { + guard let allowedWidgetsDict = getAccountData(forEventType: Settings.allowedWidgets) else { + return nil + } + + return try? serializationService.deserialize(allowedWidgetsDict) + } + + @discardableResult + func setPermission(_ permission: WidgetPermission, + for widget: Widget, + success: @escaping () -> Void, + failure: @escaping (Error?) -> Void) + -> MXHTTPOperation? { + + guard let widgetEventId = widget.widgetEvent.eventId else { + return nil + } + + var widgets = getAllowedWidgets()?.widgets ?? [:] + + switch permission { + case .undefined: + widgets.removeValue(forKey: widgetEventId) + case .granted: + widgets[widgetEventId] = true + case .declined: + widgets[widgetEventId] = false + } + + // Update only the "widgets" field in the account data + var allowedWidgetsDict = getAccountData(forEventType: Settings.allowedWidgets) ?? [:] + allowedWidgetsDict[RiotSettingAllowedWidgets.CodingKeys.widgets.rawValue] = widgets + + return session.setAccountData(allowedWidgetsDict, forType: Settings.allowedWidgets, success: success, failure: failure) + } + + + // MARK: Allowed native widgets + + /// Get the permission for widget that will be displayed natively instead within + /// a webview. + /// + /// - Parameters: + /// - widget: the widget + /// - url: the url the native implementation will open. Nil will use the url declared in the widget + /// - Returns: the permission + func permission(forNative widget: Widget, fromUrl url: URL? = nil) -> WidgetPermission { + guard let allowedWidgets = getAllowedWidgets() else { + return .undefined + } + + guard let type = widget.type, let domain = domainForNativeWidget(widget, fromUrl: url) else { + return .undefined + } + + if let value = allowedWidgets.nativeWidgets[type]?[domain] { + return value == true ? .granted : .declined + } else { + return .undefined + } + } + + /// Set the permission for widget that is displayed natively. + /// + /// - Parameters: + /// - permission: the permission to set + /// - widget: the widget + /// - url: the url the native implementation opens. Nil will use the url declared in the widget + /// - success: the success block + /// - failure: the failure block + /// - Returns: a `MXHTTPOperation` instance. + @discardableResult + func setPermission(_ permission: WidgetPermission, + forNative widget: Widget, + fromUrl url: URL?, + success: @escaping () -> Void, + failure: @escaping (Error?) -> Void) + -> MXHTTPOperation? { + + guard let type = widget.type, let domain = domainForNativeWidget(widget, fromUrl: url) else { + return nil + } + + var nativeWidgets = getAllowedWidgets()?.nativeWidgets ?? [String: [String: Bool]]() + var nativeWidgetsType = nativeWidgets[type] ?? [String: Bool]() + + switch permission { + case .undefined: + nativeWidgetsType.removeValue(forKey: domain) + case .granted: + nativeWidgetsType[domain] = true + case .declined: + nativeWidgetsType[domain] = false + } + + nativeWidgets[type] = nativeWidgetsType + + // Update only the "native_widgets" field in the account data + var allowedWidgetsDict = getAccountData(forEventType: Settings.allowedWidgets) ?? [:] + allowedWidgetsDict[RiotSettingAllowedWidgets.CodingKeys.nativeWidgets.rawValue] = nativeWidgets + + return session.setAccountData(allowedWidgetsDict, forType: Settings.allowedWidgets, success: success, failure: failure) + } + + + // MARK: - Private + private func getAccountData(forEventType eventType: String) -> [String: Any]? { + return session.accountData.accountData(forEventType: eventType) as? [String: Any] + } + + private func domainForNativeWidget(_ widget: Widget, fromUrl url: URL? = nil) -> String? { + var widgetUrl: URL? + if let widgetUrlString = widget.url { + widgetUrl = URL(string: widgetUrlString) + } + + guard let url = url ?? widgetUrl, let domain = url.host else { + return nil + } + + return domain + } +} diff --git a/Riot/Managers/Widgets/Widget.m b/Riot/Managers/Widgets/Widget.m index 1438701b3b..7a6f00b9fa 100644 --- a/Riot/Managers/Widgets/Widget.m +++ b/Riot/Managers/Widgets/Widget.m @@ -103,7 +103,7 @@ - (MXHTTPOperation *)widgetUrl:(void (^)(NSString * _Nonnull))success failure:(v // Check if their scalar token must added if ([[WidgetManager sharedManager] isScalarUrl:widgetUrl forUser:userId]) { - return [[WidgetManager sharedManager] getScalarTokenForMXSession:_mxSession validate:NO success:^(NSString *scalarToken) { + return [[WidgetManager sharedManager] getScalarTokenForMXSession:_mxSession validate:YES success:^(NSString *scalarToken) { // Add the user scalar token widgetUrl = [widgetUrl stringByAppendingString:[NSString stringWithFormat:@"&scalar_token=%@", scalarToken]]; diff --git a/Riot/Managers/Widgets/WidgetManager.h b/Riot/Managers/Widgets/WidgetManager.h index f8d027185b..c808de9d60 100644 --- a/Riot/Managers/Widgets/WidgetManager.h +++ b/Riot/Managers/Widgets/WidgetManager.h @@ -56,6 +56,7 @@ typedef enum : NSUInteger WidgetManagerErrorCodeNotEnoughPower, WidgetManagerErrorCodeCreationFailed, WidgetManagerErrorCodeNoIntegrationsServerConfigured, + WidgetManagerErrorCodeDisabledIntegrationsServer, WidgetManagerErrorCodeFailedToConnectToIntegrationsServer, WidgetManagerErrorCodeTermsNotSigned } diff --git a/Riot/Managers/Widgets/WidgetManager.m b/Riot/Managers/Widgets/WidgetManager.m index 2d7ee3e42c..e7b4704b57 100644 --- a/Riot/Managers/Widgets/WidgetManager.m +++ b/Riot/Managers/Widgets/WidgetManager.m @@ -50,6 +50,9 @@ @interface WidgetManager () // User id -> scalar token NSMutableDictionary *configs; + + // User id -> MXSession + NSMutableDictionary *matrixSessions; } @end @@ -73,6 +76,7 @@ - (instancetype)init self = [super init]; if (self) { + matrixSessions = [NSMutableDictionary dictionary]; widgetEventListener = [NSMutableDictionary dictionary]; successBlockForWidgetCreation = [NSMutableDictionary dictionary]; failureBlockForWidgetCreation = [NSMutableDictionary dictionary]; @@ -265,6 +269,14 @@ - (MXHTTPOperation *)createJitsiWidgetInRoom:(MXRoom*)room return nil; } + RiotSharedSettings *sharedSettings = [[RiotSharedSettings alloc] initWithSession:room.mxSession]; + if (!sharedSettings.hasIntegrationProvisioningEnabled) + { + NSLog(@"[WidgetManager] createJitsiWidgetInRoom: Error: Disabled integration manager for user %@", userId); + failure(self.errorForDisabledIntegrationManager); + return nil; + } + // Build data for a jitsi widget NSString *widgetId = [NSString stringWithFormat:@"%@_%@_%@", kWidgetTypeJitsi, room.mxSession.myUser.userId, @((uint64_t)([[NSDate date] timeIntervalSince1970] * 1000))]; @@ -367,6 +379,8 @@ - (void)addMatrixSession:(MXSession *)mxSession { __weak __typeof__(self) weakSelf = self; + matrixSessions[mxSession.matrixRestClient.credentials.userId] = mxSession; + NSString *hash = [NSString stringWithFormat:@"%p", mxSession]; id listener = [mxSession listenToEventsOfTypes:@[kWidgetMatrixEventTypeString, kWidgetModularEventTypeString] onEvent:^(MXEvent *event, MXTimelineDirection direction, id customObject) { @@ -421,6 +435,12 @@ - (void)addMatrixSession:(MXSession *)mxSession - (void)removeMatrixSession:(MXSession *)mxSession { + // Remove by value in a dict + for (NSString *key in [matrixSessions allKeysForObject:mxSession]) + { + [matrixSessions removeObjectForKey:key]; + } + // mxSession.myUser.userId and mxSession.matrixRestClient.credentials.userId may be nil here // So, use a kind of hash value instead NSString *hash = [NSString stringWithFormat:@"%p", mxSession]; @@ -433,18 +453,59 @@ - (void)removeMatrixSession:(MXSession *)mxSession [failureBlockForWidgetCreation removeObjectForKey:hash]; } +- (MXSession*)matrixSessionForUser:(NSString*)userId +{ + return matrixSessions[userId]; +} + - (void)deleteDataForUser:(NSString *)userId { [configs removeObjectForKey:userId]; [self saveConfigs]; } +#pragma mark - User integrations configuration + +- (WidgetManagerConfig*)createWidgetManagerConfigForUser:(NSString*)userId +{ + WidgetManagerConfig *config; + + MXSession *session = [self matrixSessionForUser:userId]; + + // Find the integrations settings for the user + + // First, look at matrix account + // TODO in another user story + + // Then, try to the homeserver configuration + MXWellknownIntegrationsManager *integrationsManager = session.homeserverWellknown.integrations.managers.firstObject; + if (integrationsManager) + { + config = [[WidgetManagerConfig alloc] initWithApiUrl:integrationsManager.apiUrl uiUrl:integrationsManager.uiUrl]; + } + else + { + // Fallback on app settings + config = [self createWidgetManagerConfigWithAppSettings]; + } + + return config; +} + +- (WidgetManagerConfig*)createWidgetManagerConfigWithAppSettings +{ + NSString *apiUrl = [[NSUserDefaults standardUserDefaults] objectForKey:@"integrationsRestUrl"]; + NSString *uiUrl = [[NSUserDefaults standardUserDefaults] objectForKey:@"integrationsUiUrl"]; + + return [[WidgetManagerConfig alloc] initWithApiUrl:apiUrl uiUrl:uiUrl]; +} + #pragma mark - Modular interface - (WidgetManagerConfig*)configForUser:(NSString*)userId { // Return a default config by default - return configs[userId] ? configs[userId] : [WidgetManagerConfig new]; + return configs[userId] ? configs[userId] : [self createWidgetManagerConfigForUser:userId]; } - (BOOL)hasIntegrationManagerForUser:(NSString*)userId @@ -697,7 +758,7 @@ - (void)loadConfigs NSLog(@"[WidgetManager] migrate scalarTokens to integrationManagerConfigs for %@", userId); - WidgetManagerConfig *config = [WidgetManagerConfig new]; + WidgetManagerConfig *config = [self createWidgetManagerConfigWithAppSettings]; config.scalarToken = scalarToken; configs[userId] = config; @@ -738,4 +799,11 @@ - (NSError*)errorForNonConfiguredIntegrationManager userInfo:@{NSLocalizedDescriptionKey: NSLocalizedStringFromTable(@"widget_no_integrations_server_configured", @"Vector", nil)}]; } +- (NSError*)errorForDisabledIntegrationManager +{ + return [NSError errorWithDomain:WidgetManagerErrorDomain + code:WidgetManagerErrorCodeDisabledIntegrationsServer + userInfo:@{NSLocalizedDescriptionKey: NSLocalizedStringFromTable(@"widget_integration_manager_disabled", @"Vector", nil)}]; +} + @end diff --git a/Riot/Managers/Widgets/WidgetManagerConfig.swift b/Riot/Managers/Widgets/WidgetManagerConfig.swift index 3bd8287479..05b48be7df 100644 --- a/Riot/Managers/Widgets/WidgetManagerConfig.swift +++ b/Riot/Managers/Widgets/WidgetManagerConfig.swift @@ -75,15 +75,6 @@ class WidgetManagerConfig: NSObject, NSCoding { super.init() } - override convenience init () { - // Use app settings as default - let apiUrl = UserDefaults.standard.object(forKey: "integrationsRestUrl") as? NSString - let uiUrl = UserDefaults.standard.object(forKey: "integrationsUiUrl") as? NSString - - self.init(apiUrl: apiUrl, uiUrl: uiUrl) - } - - /// MARK: - NSCoding enum CodingKeys: String { diff --git a/Riot/Modules/Integrations/IntegrationManagerViewController.m b/Riot/Modules/Integrations/IntegrationManagerViewController.m index 6a412fd245..ea29bdb7d5 100644 --- a/Riot/Modules/Integrations/IntegrationManagerViewController.m +++ b/Riot/Modules/Integrations/IntegrationManagerViewController.m @@ -38,6 +38,7 @@ @interface IntegrationManagerViewController () + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Integrations/WidgetPermission/WidgetPermissionViewController.swift b/Riot/Modules/Integrations/WidgetPermission/WidgetPermissionViewController.swift new file mode 100644 index 0000000000..2bb3a48bdf --- /dev/null +++ b/Riot/Modules/Integrations/WidgetPermission/WidgetPermissionViewController.swift @@ -0,0 +1,229 @@ +// File created from ScreenTemplate +// $ createScreen.sh Modal/Show ServiceTermsModalScreen +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import UIKit + +@objc +final class WidgetPermissionViewController: UIViewController { + + // MARK: - Constants + + private enum Constants { + static let continueButtonCornerRadius: CGFloat = 8.0 + } + + private enum Sizing { + static var viewController: WidgetPermissionViewController? + static var widthConstraint: NSLayoutConstraint? + } + + // MARK: - Properties + + // MARK: Outlets + + @IBOutlet private weak var scrollView: UIScrollView! + + @IBOutlet private weak var titleLabel: UILabel! + @IBOutlet private weak var closeButton: UIButton! + + @IBOutlet private weak var creatorInfoTitleLabel: UILabel! + @IBOutlet private weak var creatorAvatarImageView: MXKImageView! + @IBOutlet private weak var creatorDisplayNameLabel: UILabel! + @IBOutlet private weak var creatorUserIDLabel: UILabel! + + @IBOutlet private weak var informationLabel: UILabel! + + @IBOutlet private weak var continueButton: UIButton! + + // MARK: Private + + private var viewModel: WidgetPermissionViewModel! { + didSet { + self.updateViews() + } + } + private var theme: Theme! + + // MARK: Public + + @objc var didTapCloseButton: (() -> Void)? + @objc var didTapContinueButton: (() -> Void)? + + // MARK: - Setup + + @objc class func instantiate(with viewModel: WidgetPermissionViewModel) -> WidgetPermissionViewController { + let viewController = StoryboardScene.WidgetPermissionViewController.initialScene.instantiate() + viewController.viewModel = viewModel + viewController.theme = ThemeService.shared().theme + return viewController + } + + // MARK: - Life cycle + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + + self.setupViews() + self.updateViews() + + self.registerThemeServiceDidChangeThemeNotification() + self.update(theme: self.theme) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.scrollView.flashScrollIndicators() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + self.creatorAvatarImageView.layer.cornerRadius = self.creatorAvatarImageView.frame.size.width/2 + self.continueButton.layer.cornerRadius = Constants.continueButtonCornerRadius + self.closeButton.layer.cornerRadius = self.closeButton.frame.size.width/2 + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return self.theme.statusBarStyle + } + + // MARK: - Private + + private func update(theme: Theme) { + self.theme = theme + + self.view.backgroundColor = theme.headerBackgroundColor + + self.titleLabel.textColor = theme.textPrimaryColor + + self.closeButton.vc_setBackgroundColor(theme.headerTextSecondaryColor, for: .normal) + + self.creatorInfoTitleLabel.textColor = theme.textSecondaryColor + self.creatorDisplayNameLabel.textColor = theme.textSecondaryColor + self.creatorUserIDLabel.textColor = theme.textSecondaryColor + + self.informationLabel.textColor = theme.textSecondaryColor + + self.continueButton.vc_setBackgroundColor(theme.tintColor, for: .normal) + } + + private func registerThemeServiceDidChangeThemeNotification() { + NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil) + } + + @objc private func themeDidChange() { + self.update(theme: ThemeService.shared().theme) + } + + private func setupViews() { + self.closeButton.layer.masksToBounds = true + + self.setupCreatorAvatarImageView() + + self.titleLabel.text = VectorL10n.roomWidgetPermissionTitle + self.creatorInfoTitleLabel.text = VectorL10n.roomWidgetPermissionCreatorInfoTitle + self.informationLabel.text = "" + + self.setupContinueButton() + } + + private func updateViews() { + + if let avatarImageView = self.creatorAvatarImageView { + let defaultavatarImage = AvatarGenerator.generateAvatar(forMatrixItem: self.viewModel.creatorUserId, withDisplayName: self.viewModel.creatorDisplayName) + avatarImageView.setImageURI(self.viewModel.creatorAvatarUrl, withType: nil, andImageOrientation: .up, previewImage: defaultavatarImage, mediaManager: self.viewModel.mediaManager) + } + + if let creatorDisplayNameLabel = self.creatorDisplayNameLabel { + if let creatorDisplayName = self.viewModel.creatorDisplayName { + creatorDisplayNameLabel.text = creatorDisplayName + } else { + creatorDisplayNameLabel.isHidden = true + } + } + + if let creatorUserIDLabel = self.creatorUserIDLabel { + creatorUserIDLabel.text = self.viewModel.creatorUserId + } + + if let informationLabel = self.informationLabel { + informationLabel.text = self.viewModel.permissionsInformationText + } + } + + private func setupCreatorAvatarImageView() { + self.creatorAvatarImageView.defaultBackgroundColor = UIColor.clear + self.creatorAvatarImageView.enableInMemoryCache = true + self.creatorAvatarImageView.clipsToBounds = true + } + + private func setupContinueButton() { + self.continueButton.layer.masksToBounds = true + self.continueButton.setTitle(VectorL10n.continue, for: .normal) + } + + // MARK: - Actions + + @IBAction private func closeButtonAction(_ sender: Any) { + self.didTapCloseButton?() + } + + @IBAction private func continueButtonAction(_ sender: Any) { + self.didTapContinueButton?() + } +} + +// MARK: - SlidingModalPresentable +extension WidgetPermissionViewController: SlidingModalPresentable { + + func allowsDismissOnBackgroundTap() -> Bool { + return false + } + + func layoutHeightFittingWidth(_ width: CGFloat) -> CGFloat { + + let sizingViewContoller: WidgetPermissionViewController + + if let viewController = WidgetPermissionViewController.Sizing.viewController { + viewController.viewModel = self.viewModel + sizingViewContoller = viewController + } else { + sizingViewContoller = WidgetPermissionViewController.instantiate(with: self.viewModel) + WidgetPermissionViewController.Sizing.viewController = sizingViewContoller + } + + let sizingViewContollerView: UIView = sizingViewContoller.view + + if let widthConstraint = WidgetPermissionViewController.Sizing.widthConstraint { + widthConstraint.constant = width + } else { + let widthConstraint = sizingViewContollerView.widthAnchor.constraint(equalToConstant: width) + widthConstraint.isActive = true + WidgetPermissionViewController.Sizing.widthConstraint = widthConstraint + + sizingViewContollerView.heightAnchor.constraint(equalToConstant: 0) + } + + sizingViewContollerView.layoutIfNeeded() + + return sizingViewContoller.scrollView.contentSize.height + } +} diff --git a/Riot/Modules/Integrations/WidgetPermission/WidgetPermissionViewModel.swift b/Riot/Modules/Integrations/WidgetPermission/WidgetPermissionViewModel.swift new file mode 100644 index 0000000000..55f52ade23 --- /dev/null +++ b/Riot/Modules/Integrations/WidgetPermission/WidgetPermissionViewModel.swift @@ -0,0 +1,68 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +/// View model used by `WidgetPermissionViewController` +@objcMembers +final class WidgetPermissionViewModel: NSObject { + + // MARK: - Properties + + let creatorUserId: String + let creatorDisplayName: String? + let creatorAvatarUrl: String? + let widgetDomain: String? + let isWebviewWidget: Bool + let widgetPermissions: [String] + let mediaManager: MXMediaManager + + lazy var permissionsInformationText: String = { + return self.buildPermissionsInformationText() + }() + + // MARK: - Setup + + init(creatorUserId: String, creatorDisplayName: String?, creatorAvatarUrl: String?, widgetDomain: String?, isWebviewWidget: Bool, widgetPermissions: [String], mediaManager: MXMediaManager) { + self.creatorUserId = creatorUserId + self.creatorDisplayName = creatorDisplayName + self.creatorAvatarUrl = creatorAvatarUrl + self.widgetDomain = widgetDomain + self.isWebviewWidget = isWebviewWidget + self.widgetPermissions = widgetPermissions + self.mediaManager = mediaManager + } + + // MARK: - Private + + private func buildPermissionsInformationText() -> String { + + let informationTitle: String + let widgetDomain = self.widgetDomain ?? "" + + if self.isWebviewWidget { + informationTitle = VectorL10n.roomWidgetPermissionWebviewInformationTitle(widgetDomain) + } else { + informationTitle = VectorL10n.roomWidgetPermissionInformationTitle(widgetDomain) + } + + let permissionsList = self.widgetPermissions.reduce("") { (accumulatedPermissions, permission) -> String in + return accumulatedPermissions + "\n• \(permission)" + } + + return informationTitle + permissionsList + } +} diff --git a/Riot/Modules/Integrations/WidgetPicker/WidgetPickerViewController.m b/Riot/Modules/Integrations/WidgetPicker/WidgetPickerViewController.m index d0077f87c8..164fcbbd3d 100644 --- a/Riot/Modules/Integrations/WidgetPicker/WidgetPickerViewController.m +++ b/Riot/Modules/Integrations/WidgetPicker/WidgetPickerViewController.m @@ -21,13 +21,19 @@ #import "WidgetManager.h" #import "WidgetViewController.h" #import "IntegrationManagerViewController.h" +#import "Riot-Swift.h" -@interface WidgetPickerViewController () +@interface WidgetPickerViewController () { MXSession *mxSession; NSString *roomId; } +@property (nonatomic, weak) UIViewController *presentingViewController; +@property (nonatomic, strong) ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter; +@property (nonatomic, strong) MXKRoomDataSource *roomDataSource; +@property (nonatomic, strong) Widget *selectedWidget; + @end @implementation WidgetPickerViewController @@ -67,27 +73,14 @@ - (void)showInViewController:(MXKViewController *)mxkViewController { // Hide back button title mxkViewController.navigationItem.backBarButtonItem =[[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil]; - - // Display the widget - [widget widgetUrl:^(NSString * _Nonnull widgetUrl) { - - WidgetViewController *widgetVC = [[WidgetViewController alloc] initWithUrl:widgetUrl forWidget:widget]; - - widgetVC.roomDataSource = roomDataSource; - - [mxkViewController.navigationController pushViewController:widgetVC animated:YES]; - - } failure:^(NSError * _Nonnull error) { - - NSLog(@"[WidgetPickerVC] Cannot display widget %@", widget); - [[AppDelegate theDelegate] showErrorAsAlert:error]; - }]; + + [self fetchWidgetURLAndDisplayUsingWidget:widget canPresentServiceTerms:YES]; }]; [self.alertController addAction:alertAction]; } // Link to the integration manager - alertAction = [UIAlertAction actionWithTitle:@"Manage integrations..." + alertAction = [UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"widget_picker_manage_integrations", @"Vector", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { @@ -107,8 +100,88 @@ - (void)showInViewController:(MXKViewController *)mxkViewController [self.alertController addAction:alertAction]; // And show it - [mxkViewController presentViewController:_alertController animated:YES completion:nil]; + [mxkViewController presentViewController:self.alertController animated:YES completion:nil]; + + self.presentingViewController = mxkViewController; }]; } +- (void)fetchWidgetURLAndDisplayUsingWidget:(Widget*)widget canPresentServiceTerms:(BOOL)canPresentServiceTerms +{ + [widget widgetUrl:^(NSString * _Nonnull widgetUrl) { + + // Display the widget + + WidgetViewController *widgetVC = [[WidgetViewController alloc] initWithUrl:widgetUrl forWidget:widget]; + + widgetVC.roomDataSource = self.roomDataSource; + + [self.presentingViewController.navigationController pushViewController:widgetVC animated:YES]; + + } failure:^(NSError * _Nonnull error) { + + NSLog(@"[WidgetPickerVC] Get widget URL failed with error: %@", error); + + if (canPresentServiceTerms + && [error.domain isEqualToString:WidgetManagerErrorDomain] + && error.code == WidgetManagerErrorCodeTermsNotSigned) + { + [self presentTermsForWidget:widget]; + } + else + { + [[AppDelegate theDelegate] showErrorAsAlert:error]; + } + }]; +} + +#pragma mark - Service terms + +- (void)presentTermsForWidget:(Widget*)widget +{ + if (self.serviceTermsModalCoordinatorBridgePresenter) + { + return; + } + + WidgetManagerConfig *config = [[WidgetManager sharedManager] configForUser:widget.mxSession.myUser.userId]; + + NSLog(@"[WidgetVC] presentTerms for %@", config.baseUrl); + + ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter = [[ServiceTermsModalCoordinatorBridgePresenter alloc] initWithSession:widget.mxSession baseUrl:config.baseUrl + serviceType:MXServiceTypeIntegrationManager + outOfContext:NO + accessToken:config.scalarToken]; + serviceTermsModalCoordinatorBridgePresenter.delegate = self; + + [serviceTermsModalCoordinatorBridgePresenter presentFrom:self.presentingViewController animated:YES]; + self.serviceTermsModalCoordinatorBridgePresenter = serviceTermsModalCoordinatorBridgePresenter; +} + +- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidAccept:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter +{ + MXWeakify(self); + [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ + MXStrongifyAndReturnIfNil(self); + + if (self.selectedWidget) + { + [self fetchWidgetURLAndDisplayUsingWidget:self.selectedWidget canPresentServiceTerms:NO]; + } + }]; + self.serviceTermsModalCoordinatorBridgePresenter = nil; +} + +- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidCancel:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter +{ + [coordinatorBridgePresenter dismissWithAnimated:YES completion:nil]; + self.serviceTermsModalCoordinatorBridgePresenter = nil; +} + +- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidDecline:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter session:(MXSession * _Nonnull)session +{ + [coordinatorBridgePresenter dismissWithAnimated:YES completion:nil]; + self.serviceTermsModalCoordinatorBridgePresenter = nil; +} + @end diff --git a/Riot/Modules/Integrations/Widgets/Jitsi/JitsiService.swift b/Riot/Modules/Integrations/Widgets/Jitsi/JitsiService.swift index f551fea412..2c9cb30b41 100644 --- a/Riot/Modules/Integrations/Widgets/Jitsi/JitsiService.swift +++ b/Riot/Modules/Integrations/Widgets/Jitsi/JitsiService.swift @@ -30,7 +30,11 @@ final class JitsiService: NSObject { JMCallKitProxy.enabled = enableCallKit } } - + + var serverURL: URL? { + return self.jitsiMeet.defaultConferenceOptions?.serverURL + } + private let jitsiMeet = JitsiMeet.sharedInstance() // MARK: - Setup diff --git a/Riot/Modules/Integrations/Widgets/Jitsi/JitsiViewController.m b/Riot/Modules/Integrations/Widgets/Jitsi/JitsiViewController.m index 0208e6db48..c2bb739f1f 100644 --- a/Riot/Modules/Integrations/Widgets/Jitsi/JitsiViewController.m +++ b/Riot/Modules/Integrations/Widgets/Jitsi/JitsiViewController.m @@ -146,12 +146,26 @@ - (void)joinConferenceWithId:(NSString*)conferenceId { if (conferenceId) { - // TODO: Set up user info but it is not yet available in the jitsi-meet iOS SDK - // See https://github.com/jitsi/jitsi-meet/issues/1880 - - JitsiMeetConferenceOptions *jitsiMeetConferenceOptions = [JitsiMeetConferenceOptions fromBuilder:^(JitsiMeetConferenceOptionsBuilder * _Nonnull jitsiMeetConferenceOptionsBuilder) { - jitsiMeetConferenceOptionsBuilder.room = conferenceId; - jitsiMeetConferenceOptionsBuilder.videoMuted = !self.startWithVideo; + // Get info about the room and our user + MXSession *session = self.widget.mxSession; + MXRoomSummary *roomSummary = [session roomSummaryWithRoomId:self.widget.roomId]; + + MXRoom *room = [session roomWithRoomId:self.widget.roomId]; + MXRoomMember *roomMember = [room.dangerousSyncState.members memberWithUserId:session.myUser.userId]; + + NSString *userDisplayName = roomMember.displayname; + NSString *avatar = [session.mediaManager urlOfContent:roomMember.avatarUrl]; + NSURL *avatarUrl = [NSURL URLWithString:avatar]; + + JitsiMeetConferenceOptions *jitsiMeetConferenceOptions = [JitsiMeetConferenceOptions fromBuilder:^(JitsiMeetConferenceOptionsBuilder * _Nonnull builder) { + + builder.room = conferenceId; + builder.videoMuted = !self.startWithVideo; + + builder.subject = roomSummary.displayname; + builder.userInfo = [[JitsiMeetUserInfo alloc] initWithDisplayName:userDisplayName + andEmail:nil + andAvatar:avatarUrl]; }]; [self.jitsiMeetView join:jitsiMeetConferenceOptions]; diff --git a/Riot/Modules/Integrations/Widgets/WidgetViewController.m b/Riot/Modules/Integrations/Widgets/WidgetViewController.m index 81f8881843..ba6cc68ebc 100644 --- a/Riot/Modules/Integrations/Widgets/WidgetViewController.m +++ b/Riot/Modules/Integrations/Widgets/WidgetViewController.m @@ -26,6 +26,9 @@ @interface WidgetViewController () @property (nonatomic, strong) ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter; +@property (nonatomic, strong) NSString *widgetUrl; + +@property (nonatomic, strong) SlidingModalPresenter *slidingModalPresenter; @end @@ -34,9 +37,12 @@ @implementation WidgetViewController - (instancetype)initWithUrl:(NSString*)widgetUrl forWidget:(Widget*)theWidget { - self = [super initWithURL:widgetUrl]; + // The opening of the url is delayed in viewWillAppear where we will check + // the widget permission + self = [super initWithURL:nil]; if (self) { + self.widgetUrl = widgetUrl; widget = theWidget; } return self; @@ -54,6 +60,74 @@ - (void)viewDidLoad if (widget) { self.navigationItem.title = widget.name ? widget.name : widget.type; + + UIBarButtonItem *menuButton = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"room_context_menu_more"] style:UIBarButtonItemStylePlain target:self action:@selector(onMenuButtonPressed:)]; + self.navigationItem.rightBarButtonItem = menuButton; + } + + self.slidingModalPresenter = [SlidingModalPresenter new]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + // Check widget permission before opening the widget + [self checkWidgetPermissionWithCompletion:^(BOOL granted) { + + [self.slidingModalPresenter dismissWithAnimated:YES completion:nil]; + + if (granted) + { + self.URL = self.widgetUrl; + } + else + { + [self withdrawViewControllerAnimated:YES completion:nil]; + } + }]; +} + +- (void)reloadWidget +{ + self.URL = self.widgetUrl; +} + +- (BOOL)hasUserEnoughPowerToManageCurrentWidget +{ + BOOL hasUserEnoughPower = NO; + + MXSession *session = widget.mxSession; + MXRoom *room = [session roomWithRoomId:self.widget.roomId]; + MXRoomState *roomState = room.dangerousSyncState; + if (roomState) + { + // Check user's power in the room + MXRoomPowerLevels *powerLevels = roomState.powerLevels; + NSInteger oneSelfPowerLevel = [powerLevels powerLevelOfUserWithUserID:session.myUser.userId]; + + // The user must be able to send state events to manage widgets + if (oneSelfPowerLevel >= powerLevels.stateDefault) + { + hasUserEnoughPower = YES; + } + } + + return hasUserEnoughPower; +} + +- (void)removeCurrentWidget +{ + WidgetManager *widgetManager = [WidgetManager sharedManager]; + + MXRoom *room = [self.widget.mxSession roomWithRoomId:self.widget.roomId]; + NSString *widgetId = self.widget.widgetId; + if (room && widgetId) + { + [widgetManager closeWidget:widgetId inRoom:room success:^{ + } failure:^(NSError *error) { + NSLog(@"[WidgetVC] removeCurrentWidget failed. Error: %@", error); + }]; } } @@ -94,6 +168,182 @@ - (void)showErrorAsAlert:(NSError*)error [self presentViewController:alert animated:YES completion:nil]; } + +#pragma mark - Widget Permission + +- (void)checkWidgetPermissionWithCompletion:(void (^)(BOOL granted))completion +{ + MXSession *session = widget.mxSession; + + if ([widget.widgetEvent.sender isEqualToString:session.myUser.userId]) + { + // No need of more permission check if the user created the widget + completion(YES); + return; + } + + // Check permission in user Riot settings + __block RiotSharedSettings *sharedSettings = [[RiotSharedSettings alloc] initWithSession:session]; + + WidgetPermission permission = [sharedSettings permissionFor:widget]; + if (permission == WidgetPermissionGranted) + { + completion(YES); + } + else + { + // Note: ask permission again if the user previously declined it + [self askPermissionWithCompletion:^(BOOL granted) { + // Update the settings in user account data in parallel + [sharedSettings setPermission:granted ? WidgetPermissionGranted : WidgetPermissionDeclined + for:self.widget + success:^ + { + sharedSettings = nil; + } + failure:^(NSError * _Nullable error) + { + NSLog(@"[WidgetVC] setPermissionForWidget failed. Error: %@", error); + sharedSettings = nil; + }]; + + completion(granted); + }]; + } +} + +- (void)askPermissionWithCompletion:(void (^)(BOOL granted))completion +{ + NSString *widgetCreatorUserId = self.widget.widgetEvent.sender ?: NSLocalizedStringFromTable(@"room_participants_unknown", @"Vector", nil); + + MXSession *session = widget.mxSession; + MXRoom *room = [session roomWithRoomId:self.widget.widgetEvent.roomId]; + MXRoomState *roomState = room.dangerousSyncState; + MXRoomMember *widgetCreatorRoomMember = [roomState.members memberWithUserId:widgetCreatorUserId]; + + NSString *widgetDomain = @""; + + if (widget.url) + { + NSString *host = [[NSURL alloc] initWithString:widget.url].host; + if (host) + { + widgetDomain = host; + } + } + + MXMediaManager *mediaManager = widget.mxSession.mediaManager; + NSString *widgetCreatorDisplayName = widgetCreatorRoomMember.displayname; + NSString *widgetCreatorAvatarURL = widgetCreatorRoomMember.avatarUrl; + + NSArray *permissionStrings = @[ + NSLocalizedStringFromTable(@"room_widget_permission_display_name_permission", @"Vector", nil), + NSLocalizedStringFromTable(@"room_widget_permission_avatar_url_permission", @"Vector", nil), + NSLocalizedStringFromTable(@"room_widget_permission_user_id_permission", @"Vector", nil), + NSLocalizedStringFromTable(@"room_widget_permission_theme_permission", @"Vector", nil), + NSLocalizedStringFromTable(@"room_widget_permission_widget_id_permission", @"Vector", nil), + NSLocalizedStringFromTable(@"room_widget_permission_room_id_permission", @"Vector", nil) + ]; + + + WidgetPermissionViewModel *widgetPermissionViewModel = [[WidgetPermissionViewModel alloc] initWithCreatorUserId:widgetCreatorUserId + creatorDisplayName:widgetCreatorDisplayName creatorAvatarUrl:widgetCreatorAvatarURL widgetDomain:widgetDomain + isWebviewWidget:YES + widgetPermissions:permissionStrings + mediaManager:mediaManager]; + + + WidgetPermissionViewController *widgetPermissionViewController = [WidgetPermissionViewController instantiateWith:widgetPermissionViewModel]; + + widgetPermissionViewController.didTapContinueButton = ^{ + completion(YES); + }; + + widgetPermissionViewController.didTapCloseButton = ^{ + completion(NO); + }; + + + [self.slidingModalPresenter present:widgetPermissionViewController from:self animated:YES completion:nil]; +} + +- (void)revokePermissionForCurrentWidget +{ + MXSession *session = widget.mxSession; + __block RiotSharedSettings *sharedSettings = [[RiotSharedSettings alloc] initWithSession:session]; + + [sharedSettings setPermission:WidgetPermissionDeclined for:widget success:^{ + sharedSettings = nil; + } failure:^(NSError * _Nullable error) { + NSLog(@"[WidgetVC] revokePermissionForCurrentWidget failed. Error: %@", error); + sharedSettings = nil; + }]; +} + + +#pragma mark - Contextual Menu + +- (IBAction)onMenuButtonPressed:(id)sender +{ + [self showMenu]; +} + +-(void)showMenu +{ + MXSession *session = widget.mxSession; + + UIAlertController *menu = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet]; + + [menu addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"widget_menu_refresh", @"Vector", nil) + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) + { + [self reloadWidget]; + }]]; + + NSURL *url = [NSURL URLWithString:self.widgetUrl]; + if (url && [[UIApplication sharedApplication] canOpenURL:url]) + { + [menu addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"widget_menu_open_outside", @"Vector", nil) + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) + { + [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:^(BOOL success) { + }]; + }]]; + } + + if (![widget.widgetEvent.sender isEqualToString:session.myUser.userId]) + { + [menu addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"widget_menu_revoke_permission", @"Vector", nil) + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) + { + [self revokePermissionForCurrentWidget]; + [self withdrawViewControllerAnimated:YES completion:nil]; + }]]; + } + + if ([self hasUserEnoughPowerToManageCurrentWidget]) + { + [menu addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"widget_menu_remove", @"Vector", nil) + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) + { + [self removeCurrentWidget]; + [self withdrawViewControllerAnimated:YES completion:nil]; + }]]; + } + + [menu addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"cancel"] + style:UIAlertActionStyleCancel + handler:^(UIAlertAction * action) { + }]]; + + [self presentViewController:menu animated:YES completion:nil]; +} + + #pragma mark - WKNavigationDelegate - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation @@ -435,4 +685,12 @@ - (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidCancel:(ServiceTer self.serviceTermsModalCoordinatorBridgePresenter = nil; } +- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidDecline:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter session:(MXSession * _Nonnull)session +{ + [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ + [self withdrawViewControllerAnimated:YES completion:nil]; + }]; + self.serviceTermsModalCoordinatorBridgePresenter = nil; +} + @end diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 47f8f3e3d4..2c4fac7951 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -228,6 +228,7 @@ @interface RoomViewController () SlidingModalContainerView { + return SlidingModalContainerView.loadFromNib() + } + + // MARK: - Life cycle + + override func awakeFromNib() { + super.awakeFromNib() + + self.contentView.layer.masksToBounds = true + self.dimmingView.backgroundColor = UIColor.black.withAlphaComponent(Constants.dimmingColorAlpha) + + self.setupBackgroundTapGestureRecognizer() + + self.update(theme: ThemeService.shared().theme) + } + + override func layoutSubviews() { + super.layoutSubviews() + + self.contentView.layer.cornerRadius = Constants.cornerRadius + } + + // MARK: - Public + + func preparePresentAnimation() { + self.contentViewBottomConstraint.constant = 0 + } + + func prepareDismissAnimation() { + self.contentViewBottomConstraint.constant = self.dismissContentViewBottomConstant + } + + func update(theme: Theme) { + self.contentView.backgroundColor = theme.headerBackgroundColor + } + + func updateContentViewMaxHeight(_ maxHeight: CGFloat) { + self.contentViewHeightConstraint.constant = maxHeight + } + + func updateContentViewLayout() { + self.layoutIfNeeded() + } + + func setContentView(_ contentView: UIView) { + for subView in self.contentView.subviews { + subView.removeFromSuperview() + } + self.contentView.vc_addSubViewMatchingParent(contentView) + } + + func updateDimmingViewAlpha(_ alpha: CGFloat) { + self.dimmingView.alpha = alpha + } + + func contentViewWidthFittingSize(_ size: CGSize) -> CGFloat { + let sizingView = SlidingModalContainerView.Sizing.view + + if let widthConstraint = SlidingModalContainerView.Sizing.widthConstraint { + widthConstraint.constant = size.width + } else { + let widthConstraint = sizingView.widthAnchor.constraint(equalToConstant: size.width) + widthConstraint.isActive = true + SlidingModalContainerView.Sizing.widthConstraint = widthConstraint + } + + if let heightConstraint = SlidingModalContainerView.Sizing.heightConstraint { + heightConstraint.constant = size.height + } else { + let heightConstraint = sizingView.heightAnchor.constraint(equalToConstant: size.width) + heightConstraint.isActive = true + SlidingModalContainerView.Sizing.heightConstraint = heightConstraint + } + + sizingView.setNeedsLayout() + sizingView.layoutIfNeeded() + + return sizingView.contentViewFrame.width + } + + // MARK: - Private + + private func setupBackgroundTapGestureRecognizer() { + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleBackgroundTap(_:))) + self.dimmingView.addGestureRecognizer(tapGestureRecognizer) + } + + @objc private func handleBackgroundTap(_ gestureRecognizer: UITapGestureRecognizer) { + self.delegate?.slidingModalContainerViewDidTapBackground(self) + } +} diff --git a/Riot/Modules/SlidingModal/SlidingModalContainerView.xib b/Riot/Modules/SlidingModal/SlidingModalContainerView.xib new file mode 100644 index 0000000000..c14b00ddd2 --- /dev/null +++ b/Riot/Modules/SlidingModal/SlidingModalContainerView.xib @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/SlidingModal/SlidingModalEmptyViewController.swift b/Riot/Modules/SlidingModal/SlidingModalEmptyViewController.swift new file mode 100644 index 0000000000..7b81187622 --- /dev/null +++ b/Riot/Modules/SlidingModal/SlidingModalEmptyViewController.swift @@ -0,0 +1,51 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +/// Empty view controller used to embed a view conforming to `SlidingModalPresentable`. +final class SlidingModalEmptyViewController: UIViewController { + + // MARK: - Properties + + private var modalView: SlidingModalPresentable.ViewType! + + // MARK: - Setup + + static func instantiate(with view: SlidingModalPresentable.ViewType) -> SlidingModalEmptyViewController { + let viewController = SlidingModalEmptyViewController() + viewController.modalView = view + return viewController + } + + // MARK: - Life cycle + + override func loadView() { + self.view = self.modalView + } +} + +// MARK: - SlidingModalPresentable +extension SlidingModalEmptyViewController: SlidingModalPresentable { + + func allowsDismissOnBackgroundTap() -> Bool { + return self.modalView.allowsDismissOnBackgroundTap() + } + + func layoutHeightFittingWidth(_ width: CGFloat) -> CGFloat { + return self.modalView.layoutHeightFittingWidth(width) + } +} diff --git a/Riot/Modules/SlidingModal/SlidingModalPresentable.swift b/Riot/Modules/SlidingModal/SlidingModalPresentable.swift new file mode 100644 index 0000000000..106d2d32c5 --- /dev/null +++ b/Riot/Modules/SlidingModal/SlidingModalPresentable.swift @@ -0,0 +1,29 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +/// `SlidingModalPresentable` is a protocol describing a UI element to present modally using `SlidingModalPresenter`. +@objc protocol SlidingModalPresentable { + + typealias ViewType = UIView & SlidingModalPresentable + + typealias ViewControllerType = UIViewController & SlidingModalPresentable + + func allowsDismissOnBackgroundTap() -> Bool + + func layoutHeightFittingWidth(_ width: CGFloat) -> CGFloat +} diff --git a/Riot/Modules/SlidingModal/SlidingModalPresentationAnimator.swift b/Riot/Modules/SlidingModal/SlidingModalPresentationAnimator.swift new file mode 100644 index 0000000000..db7e769fee --- /dev/null +++ b/Riot/Modules/SlidingModal/SlidingModalPresentationAnimator.swift @@ -0,0 +1,135 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import UIKit + +/// `SlidingModalPresentationAnimator` handles the animations for a custom sliding view controller transition. +final class SlidingModalPresentationAnimator: NSObject { + + // MARK: - Constants + + private enum AnimationDuration { + static let presentation: TimeInterval = 0.2 + static let dismissal: TimeInterval = 0.3 + } + + // MARK: - Properties + + private let isPresenting: Bool + + // MARK: - Setup + + /// Instantiate a SlidingModalPresentationAnimator object. + /// + /// - Parameter isPresenting: true to animate presentation or false to animate dismissal + required public init(isPresenting: Bool) { + self.isPresenting = isPresenting + super.init() + } + + // MARK: - Private + + // Animate presented view controller presentation + private func animatePresentation(using transitionContext: UIViewControllerContextTransitioning) { + guard let presentedViewController = transitionContext.viewController(forKey: .to), + let sourceViewController = transitionContext.viewController(forKey: .from) else { + return + } + + guard let presentedViewControllerView = presentedViewController.view else { + return + } + + let containerView = transitionContext.containerView + + let slidingModalContainerView = SlidingModalContainerView.instantiate() + slidingModalContainerView.alpha = 0 + slidingModalContainerView.updateDimmingViewAlpha(0.0) + + // Add presented view controller view to slidingModalContainerView content view + slidingModalContainerView.setContentView(presentedViewControllerView) + + // Add slidingModalContainerView to container view + containerView.vc_addSubViewMatchingParent(slidingModalContainerView) + containerView.layoutIfNeeded() + + // Adapt slidingModalContainerView content view height from presentedViewControllerView height + if let slidingModalPresentable = presentedViewController as? SlidingModalPresentable { + let slidingModalContainerViewContentViewWidth = slidingModalContainerView.contentViewFrame.width + let presentableHeight = slidingModalPresentable.layoutHeightFittingWidth(slidingModalContainerViewContentViewWidth) + slidingModalContainerView.updateContentViewMaxHeight(presentableHeight) + slidingModalContainerView.updateContentViewLayout() + } + + // Hide slidingModalContainerView content view + slidingModalContainerView.prepareDismissAnimation() + containerView.layoutIfNeeded() + + let animationDuration = self.transitionDuration(using: transitionContext) + + slidingModalContainerView.preparePresentAnimation() + slidingModalContainerView.alpha = 1 + + UIView.animate(withDuration: animationDuration, animations: { + containerView.layoutIfNeeded() + slidingModalContainerView.updateDimmingViewAlpha(1.0) + }, completion: { completed in + transitionContext.completeTransition(completed) + }) + } + + // Animate presented view controller dismissal + private func animateDismissal(using transitionContext: UIViewControllerContextTransitioning) { + + let containerView = transitionContext.containerView + + let slidingModalContainerView = self.slidingModalContainerView(from: transitionContext) + + let animationDuration = self.transitionDuration(using: transitionContext) + + slidingModalContainerView?.prepareDismissAnimation() + + UIView.animate(withDuration: animationDuration, animations: { + containerView.layoutIfNeeded() + slidingModalContainerView?.updateDimmingViewAlpha(0.0) + }, completion: { completed in + transitionContext.completeTransition(completed) + }) + } + + private func slidingModalContainerView(from transitionContext: UIViewControllerContextTransitioning) -> SlidingModalContainerView? { + let modalContentView = transitionContext.containerView.subviews.first(where: { view -> Bool in + view is SlidingModalContainerView + }) as? SlidingModalContainerView + return modalContentView + } +} + +// MARK: - UIViewControllerAnimatedTransitioning +extension SlidingModalPresentationAnimator: UIViewControllerAnimatedTransitioning { + + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + return self.isPresenting ? AnimationDuration.presentation : AnimationDuration.dismissal + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + if self.isPresenting { + self.animatePresentation(using: transitionContext) + } else { + self.animateDismissal(using: transitionContext) + } + } +} diff --git a/Riot/Modules/SlidingModal/SlidingModalPresentationController.swift b/Riot/Modules/SlidingModal/SlidingModalPresentationController.swift new file mode 100644 index 0000000000..c29940d9cd --- /dev/null +++ b/Riot/Modules/SlidingModal/SlidingModalPresentationController.swift @@ -0,0 +1,106 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import UIKit + +/// `SlidingModalPresentationController` handles sliding transition presentation life cycle. +final class SlidingModalPresentationController: UIPresentationController { + + // MARK: - Properties + + private var slidingModalContainerView: SlidingModalContainerView? { + return self.containerView?.subviews.first(where: { (view) -> Bool in + view is SlidingModalContainerView + }) as? SlidingModalContainerView + } + + // MARK: - Setup + + override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) { + super.init(presentedViewController: presentedViewController, presenting: presentingViewController) + } + + // MARK: - Life cycle + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + if let slidingModalPresentable = self.presentedViewController as? SlidingModalPresentable, let slidingModalContainerView = self.slidingModalContainerView { + + let slidingModalContainerViewContentViewWidth = slidingModalContainerView.contentViewWidthFittingSize(size) + + let presentableHeight = slidingModalPresentable.layoutHeightFittingWidth(slidingModalContainerViewContentViewWidth) + slidingModalContainerView.updateContentViewMaxHeight(presentableHeight) + } + + coordinator.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) -> Void in + self.slidingModalContainerView?.updateContentViewLayout() + }, completion: { _ in + + }) + } + + override func presentationTransitionWillBegin() { + guard let coordinator = presentedViewController.transitionCoordinator else { + return + } + + coordinator.animate(alongsideTransition: { [weak self] _ in + // Update status bar appearance of presented view controller (if presenting view controller allowed it, see `modalPresentationCapturesStatusBarAppearance` property) + self?.presentedViewController.setNeedsStatusBarAppearanceUpdate() + }, completion: nil) + } + + override func presentationTransitionDidEnd(_ completed: Bool) { + self.slidingModalContainerView?.delegate = self + } + + override func dismissalTransitionWillBegin() { + guard let coordinator = presentedViewController.transitionCoordinator else { + return + } + + coordinator.animate(alongsideTransition: { [weak self] _ -> Void in + // Update status bar appearance of presenting view controller + self?.presentingViewController.setNeedsStatusBarAppearanceUpdate() + }, completion: nil) + } + + override func dismissalTransitionDidEnd(_ completed: Bool) { + if completed { + self.slidingModalContainerView?.removeFromSuperview() + } + } +} + +// MARK: - SlidingModalContainerViewDelegate +extension SlidingModalPresentationController: SlidingModalContainerViewDelegate { + + func slidingModalContainerViewDidTapBackground(_ view: SlidingModalContainerView) { + + let isDismissOnBackgroundTapAllowed: Bool + + if let slidingModalPresentable = self.presentedViewController as? SlidingModalPresentable { + isDismissOnBackgroundTapAllowed = slidingModalPresentable.allowsDismissOnBackgroundTap() + } else { + isDismissOnBackgroundTapAllowed = true + } + + if isDismissOnBackgroundTapAllowed { + self.presentedViewController.dismiss(animated: true, completion: nil) + } + } +} diff --git a/Riot/Modules/SlidingModal/SlidingModalPresentationDelegate.swift b/Riot/Modules/SlidingModal/SlidingModalPresentationDelegate.swift new file mode 100644 index 0000000000..da025fb35b --- /dev/null +++ b/Riot/Modules/SlidingModal/SlidingModalPresentationDelegate.swift @@ -0,0 +1,48 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +/// `SlidingModalPresentationDelegate` handle a custom sliding UIViewController transition. +public class SlidingModalPresentationDelegate: NSObject { +} + +// MARK: - UIViewControllerTransitioningDelegate +extension SlidingModalPresentationDelegate: UIViewControllerTransitioningDelegate { + + public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { + return SlidingModalPresentationAnimator(isPresenting: true) + } + + public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { + return SlidingModalPresentationAnimator(isPresenting: false) + } + + public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { + let controller = SlidingModalPresentationController(presentedViewController: presented, presenting: presenting) + controller.delegate = self + return controller + } +} + +// MARK: - UIAdaptivePresentationControllerDelegate +extension SlidingModalPresentationDelegate: UIAdaptivePresentationControllerDelegate { + + // Do not adapt to size classes + public func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { + return .none + } +} diff --git a/Riot/Modules/SlidingModal/SlidingModalPresenter.swift b/Riot/Modules/SlidingModal/SlidingModalPresenter.swift new file mode 100644 index 0000000000..2b0c63e3a9 --- /dev/null +++ b/Riot/Modules/SlidingModal/SlidingModalPresenter.swift @@ -0,0 +1,72 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import UIKit + +/// `SlidingModalPresenter` allows to present a custom UIViewController or UIView conforming to `SlidingModalPresentable` as a modal with a vertical sliding animation from a UIViewController. +final class SlidingModalPresenter: NSObject { + + // MARK: - Constants + + private enum TabletContentSize { + static let preferred = CGSize(width: 400.0, height: 400.0) + static let minHeight: CGFloat = 0.0 + static let maxHeight: CGFloat = 600.0 + } + + // MARK: - Properties + + // swiftlint:disable weak_delegate + private var transitionDelegate: SlidingModalPresentationDelegate? + // swiftlint:enable weak_delegate + private weak var presentingViewController: UIViewController? + + // MARK: - Public + + @objc func present(_ viewController: SlidingModalPresentable.ViewControllerType, from presentingViewController: UIViewController, animated: Bool, completion: (() -> Void)?) { + + if UIDevice.current.userInterfaceIdiom == .pad { + viewController.modalPresentationStyle = .formSheet + + let preferredHeight = viewController.layoutHeightFittingWidth(TabletContentSize.preferred.width).clamped(to: TabletContentSize.minHeight...TabletContentSize.maxHeight) + + viewController.preferredContentSize = CGSize(width: TabletContentSize.preferred.width, height: preferredHeight) + } else { + let transitionDelegate = SlidingModalPresentationDelegate() + + viewController.modalPresentationStyle = .custom + viewController.transitioningDelegate = transitionDelegate + + // Presented view controller does not affect the statusbar appearance + viewController.modalPresentationCapturesStatusBarAppearance = false + + self.transitionDelegate = transitionDelegate + } + + presentingViewController.present(viewController, animated: animated, completion: completion) + + self.presentingViewController = presentingViewController + } + + @objc func presentView(_ view: SlidingModalPresentable.ViewType, from viewControllerPresenter: UIViewController, animated: Bool, completion: (() -> Void)?) { + let viewController = SlidingModalEmptyViewController.instantiate(with: view) + self.present(viewController, from: viewControllerPresenter, animated: animated, completion: completion) + } + + @objc func dismiss(animated: Bool, completion: (() -> Void)?) { + self.presentingViewController?.dismiss(animated: animated, completion: completion) + } +} diff --git a/Riot/SupportingFiles/Info.plist b/Riot/SupportingFiles/Info.plist index b0069f21e9..4ecb0184be 100644 --- a/Riot/SupportingFiles/Info.plist +++ b/Riot/SupportingFiles/Info.plist @@ -17,11 +17,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.10.2 + 0.10.3 CFBundleSignature ???? CFBundleVersion - 0.10.2 + 0.10.3 ITSAppUsesNonExemptEncryption ITSEncryptionExportComplianceCode diff --git a/RiotShareExtension/SupportingFiles/Info.plist b/RiotShareExtension/SupportingFiles/Info.plist index 258bb8efca..1abe5091c2 100644 --- a/RiotShareExtension/SupportingFiles/Info.plist +++ b/RiotShareExtension/SupportingFiles/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 0.10.2 + 0.10.3 CFBundleVersion - 0.10.2 + 0.10.3 NSExtension NSExtensionAttributes diff --git a/SiriIntents/Info.plist b/SiriIntents/Info.plist index 2896423c56..687e177521 100644 --- a/SiriIntents/Info.plist +++ b/SiriIntents/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 0.10.2 + 0.10.3 CFBundleVersion - 0.10.2 + 0.10.3 NSExtension NSExtensionAttributes