/* Copyright 2019-2024 New Vector Ltd. Copyright (c) 2021 BWI GmbH SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ 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: Notification Times func fetchNotificationTimes() { guard let dict = getAccountData(forEventType: "de.bwi.notification_times") else { return } NotificationTimes.shared.isEnabled = (dict["is_enabled"] as? Bool) ?? false if let entries = dict["entries"] as? [[String: Any]], entries.count == 7 { for i in 0...6 { let weekday = NotificationTimes.shared.weekday(index: i) weekday.isEnabled = (entries[i]["is_enabled"] as? Bool) ?? false if let fromHour = entries[i]["from_hour"] as? Int, let fromMinute = entries[i]["from_minute"] as? Int { weekday.startTime = Date.from(hour: fromHour, minute: fromMinute) } if let toHour = entries[i]["to_hour"] as? Int, let toMinute = entries[i]["to_minute"] as? Int { weekday.endTime = Date.from(hour: toHour, minute: toMinute) } } } if let rooms = dict["rooms"] as? [[String: Any]] { NotificationTimes.shared.rooms = [:] for room in rooms { if let roomID = room["room_id"] as? String, let isEnabled = room["is_enabled"] as? Bool { NotificationTimes.shared.rooms[roomID] = isEnabled } } } } func storeNotificationTimes(success: @escaping () -> Void, failure: @escaping (Error?) -> Void) -> MXHTTPOperation? { let eventType = "de.bwi.notification_times" var entries: [[String: Any]] = [] for i in 0...6 { let weekday = NotificationTimes.shared.weekday(index: i) let calender = Calendar.current let fromHour = calender.component(.hour, from: weekday.startTime) let fromMinute = calender.component(.minute, from: weekday.startTime) let toHour = calender.component(.hour, from: weekday.endTime) let toMinute = calender.component(.minute, from: weekday.endTime) let entry: [String: Any] = ["is_enabled": weekday.isEnabled, "from_hour": fromHour, "from_minute": fromMinute, "to_hour": toHour, "to_minute": toMinute] entries.append(entry) } var rooms: [[String: Any]] = [] for room in NotificationTimes.shared.rooms { rooms.append(["room_id": room.key, "is_enabled": room.value]) } let dict: [String: Any] = ["is_enabled": NotificationTimes.shared.isEnabled, "entries": entries, "rooms": rooms] return session.setAccountData(dict, forType: eventType, success: success, failure: failure) } // MARK: Top Banner Features func topBanner(for feature: String) -> Bool { guard let featuresDict = getAccountData(forEventType: "de.bwi.top_banner_features") else { return true } return (featuresDict[feature] as? Bool) ?? true } @discardableResult func setTopBannerFeature(_ feature: String, enabled: Bool, success: @escaping () -> Void, failure: @escaping (Error?) -> Void) -> MXHTTPOperation? { let eventType = "de.bwi.top_banner_features" var featuresDict = getAccountData(forEventType: eventType) ?? [:] featuresDict[feature] = enabled return session.setAccountData(featuresDict, forType: eventType, success: success, failure: failure) } // MARK: Top Banner Features func happyBirthdayCampaign(for campaign: String) -> Bool { guard let notificationsDict = getAccountData(forEventType: "de.bwi.notifications") else { return true } guard let birthdayCampaignDict = notificationsDict["should_show_ios_birthday_campaign"] as? [String : Any] else { return true } return (birthdayCampaignDict["2022"] as? Bool) ?? true } @discardableResult func setHappyBirthdayCampaign(_ campaign: String, enabled: Bool, success: @escaping () -> Void, failure: @escaping (Error?) -> Void) -> MXHTTPOperation? { var notificationsDict = getAccountData(forEventType: "de.bwi.notifications") ?? [:] var birthdayCampaignDict = notificationsDict["should_show_ios_birthday_campaign"] as? [String : Any] ?? [String: Any]() birthdayCampaignDict[campaign] = enabled notificationsDict["should_show_ios_birthday_campaign"] = birthdayCampaignDict return session.setAccountData(notificationsDict, forType: "de.bwi.notifications", 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 } }