diff --git a/Riot.xcodeproj/project.pbxproj b/Riot.xcodeproj/project.pbxproj index fd3d52b2d..8623ef6d6 100644 --- a/Riot.xcodeproj/project.pbxproj +++ b/Riot.xcodeproj/project.pbxproj @@ -115,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 */; }; @@ -765,6 +766,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 = ""; }; @@ -1702,6 +1704,7 @@ isa = PBXGroup; children = ( 32863A5B2384074C00D07C4A /* RiotSettingAllowedWidgets.swift */, + 32FDC1CC2386CD390084717A /* RiotSettingIntegrationProvisioning.swift */, ); path = JSONModels; sourceTree = ""; @@ -4580,6 +4583,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 */, diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index a870b2f15..60cd59440 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -378,6 +378,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 +432,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"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 2e9fd6fb7..5ba4b674b 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2810,6 +2810,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") diff --git a/Riot/Managers/Settings/Shared/JSONModels/RiotSettingAllowedWidgets.swift b/Riot/Managers/Settings/Shared/JSONModels/RiotSettingAllowedWidgets.swift index 700ac8424..ffe7d3417 100644 --- a/Riot/Managers/Settings/Shared/JSONModels/RiotSettingAllowedWidgets.swift +++ b/Riot/Managers/Settings/Shared/JSONModels/RiotSettingAllowedWidgets.swift @@ -26,11 +26,4 @@ extension RiotSettingAllowedWidgets: Decodable { enum CodingKeys: String, CodingKey { case widgets } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let widgets = try container.decode([String: Bool].self, forKey: .widgets) - - self.init(widgets: widgets) - } } diff --git a/Riot/Managers/Settings/Shared/JSONModels/RiotSettingIntegrationProvisioning.swift b/Riot/Managers/Settings/Shared/JSONModels/RiotSettingIntegrationProvisioning.swift new file mode 100644 index 000000000..4c707a6b2 --- /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 index b09232be8..42b69d07a 100644 --- a/Riot/Managers/Settings/Shared/RiotSharedSettings.swift +++ b/Riot/Managers/Settings/Shared/RiotSharedSettings.swift @@ -52,6 +52,39 @@ class RiotSharedSettings: NSObject { // MARK: - Public + // MARK: Integration provisioning + + func hasIntegrationProvisioningEnabled() -> Bool { + return getIntegrationProvisioning()?.enabled ?? true + } + + func getIntegrationProvisioning() -> RiotSettingIntegrationProvisioning? { + guard let integrationProvisioningDict = getAccountData(forEventType: Settings.integrationProvisioning) else { + return nil + } + + do { + let integrationProvisioning: RiotSettingIntegrationProvisioning = try serializationService.deserialize(integrationProvisioningDict) + return integrationProvisioning + } catch { + return nil + } + } + + @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 { @@ -74,7 +107,8 @@ class RiotSharedSettings: NSObject { } } - @discardableResult func setPermission(_ permission: WidgetPermission, + @discardableResult + func setPermission(_ permission: WidgetPermission, for widget: Widget, success: @escaping () -> Void, failure: @escaping (Error?) -> Void) diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index fd0eb5b0a..75c8203bc 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -57,6 +57,7 @@ enum SETTINGS_SECTION_IDENTITY_SERVER_INDEX, SETTINGS_SECTION_CONTACTS_INDEX, SETTINGS_SECTION_IGNORED_USERS_INDEX, + SETTINGS_SECTION_INTEGRATIONS_INDEX, SETTINGS_SECTION_USER_INTERFACE_INDEX, SETTINGS_SECTION_ADVANCED_INDEX, SETTINGS_SECTION_OTHER_INDEX, @@ -94,6 +95,13 @@ enum CALLS_COUNT }; +enum +{ + INTEGRATIONS_INDEX, + INTEGRATIONS_DESCRIPTION_INDEX, + INTEGRATIONS_COUNT +}; + enum { USER_INTERFACE_LANGUAGE_INDEX = 0, @@ -1390,6 +1398,10 @@ SettingsIdentityServerCoordinatorBridgePresenterDelegate> { count = IDENTITY_SERVER_COUNT; } + else if (section == SETTINGS_SECTION_INTEGRATIONS_INDEX) + { + count = INTEGRATIONS_COUNT; + } else if (section == SETTINGS_SECTION_USER_INTERFACE_INDEX) { count = USER_INTERFACE_COUNT; @@ -2057,6 +2069,44 @@ SettingsIdentityServerCoordinatorBridgePresenterDelegate> break; } } + else if (section == SETTINGS_SECTION_INTEGRATIONS_INDEX) + { + switch (row) { + case INTEGRATIONS_INDEX: + { + RiotSharedSettings *sharedSettings = [[RiotSharedSettings alloc] initWithSession:session]; + + MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; + labelAndSwitchCell.mxkLabel.text = NSLocalizedStringFromTable(@"settings_integrations_allow_button", @"Vector", nil); + labelAndSwitchCell.mxkSwitch.on = sharedSettings.hasIntegrationProvisioningEnabled; + labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; + labelAndSwitchCell.mxkSwitch.enabled = YES; + [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleAllowIntegrations:) forControlEvents:UIControlEventTouchUpInside]; + + cell = labelAndSwitchCell; + break; + } + + case INTEGRATIONS_DESCRIPTION_INDEX: + { + MXKTableViewCell *descriptionCell = [self getDefaultTableViewCell:tableView]; + + NSString *integrationManager = [WidgetManager.sharedManager configForUser:session.myUser.userId].apiUrl; + NSString *integrationManagerDomain = [NSURL URLWithString:integrationManager].host; + + NSString *description = [NSString stringWithFormat:NSLocalizedStringFromTable(@"settings_integrations_allow_description", @"Vector", nil), integrationManagerDomain]; + descriptionCell.textLabel.text = description; + descriptionCell.textLabel.numberOfLines = 0; + descriptionCell.selectionStyle = UITableViewCellSelectionStyleNone; + + cell = descriptionCell; + break; + } + + default: + break; + } + } else if (section == SETTINGS_SECTION_USER_INTERFACE_INDEX) { if (row == USER_INTERFACE_LANGUAGE_INDEX) @@ -2564,6 +2614,10 @@ SettingsIdentityServerCoordinatorBridgePresenterDelegate> { return NSLocalizedStringFromTable(@"settings_identity_server_settings", @"Vector", nil); } + else if (section == SETTINGS_SECTION_INTEGRATIONS_INDEX) + { + return NSLocalizedStringFromTable(@"settings_integrations", @"Vector", nil); + } else if (section == SETTINGS_SECTION_USER_INTERFACE_INDEX) { return NSLocalizedStringFromTable(@"settings_user_interface", @"Vector", nil); @@ -3187,6 +3241,24 @@ SettingsIdentityServerCoordinatorBridgePresenterDelegate> self.mainSession.callManager.fallbackSTUNServer = RiotSettings.shared.allowStunServerFallback ? RiotSettings.shared.stunServerFallback : nil; } +- (void)toggleAllowIntegrations:(id)sender +{ + UISwitch *switchButton = (UISwitch*)sender; + + MXSession *session = self.mainSession; + [self startActivityIndicator]; + + __block RiotSharedSettings *sharedSettings = [[RiotSharedSettings alloc] initWithSession:session]; + [sharedSettings setIntegrationProvisioningWithEnabled:switchButton.on success:^{ + sharedSettings = nil; + [self stopActivityIndicator]; + } failure:^(NSError * _Nullable error) { + sharedSettings = nil; + [switchButton setOn:!switchButton.on animated:YES]; + [self stopActivityIndicator]; + }]; +} + - (void)toggleShowDecodedContent:(id)sender { UISwitch *switchButton = (UISwitch*)sender;