// // Copyright 2025 Element Creations Ltd. // Copyright 2022-2025 New Vector Ltd. // // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial // Please see LICENSE files in the repository root for full details. // import Foundation enum SecurityAndPrivacyScreenViewModelAction { case displayEditAddressScreen case dismiss case displayManageAuthorizedSpacesScreen(AuthorizedSpacesSelection) } struct SecurityAndPrivacyScreenViewState: BindableState { let serverName: String var currentSettings: SecurityAndPrivacySettings var bindings: SecurityAndPrivacyScreenViewStateBindings let strings: SecurityAndPrivacyScreenStrings var canonicalAlias: String? var isKnockingEnabled: Bool var isSpace: Bool var canEditAddress = false var canEditJoinRule = false var canEnableEncryption = false var canEditHistoryVisibility = false /// The union of joined parent spaces and the joined spaces in the current access type var selectableJoinedSpaces: [SpaceServiceRoom] = [] /// The count of the intersection between the set of joined parent spaces and the set of spaces in the current access type var selectableSpacesCount: Int { Set(selectableJoinedSpaces.map(\.id) + currentSettings.accessType.spaceIDs).count } private var hasChanges: Bool { currentSettings != bindings.desiredSettings } var isSaveDisabled: Bool { !hasChanges || (currentSettings.isVisibileInRoomDirectory == nil && bindings.desiredSettings.accessType != .inviteOnly && canonicalAlias != nil) } var availableVisibilityOptions: [SecurityAndPrivacyHistoryVisibility] { var options = [SecurityAndPrivacyHistoryVisibility.shared] if !bindings.desiredSettings.isEncryptionEnabled, bindings.desiredSettings.accessType == .anyone { options.append(.worldReadable) } else { options.append(.invited) } return options.sorted() } var isSpaceMembersOptionAvailable: Bool { currentSettings.accessType.isSpaceMembers || isSpaceMembersOptionSelectable } var isSpaceMembersOptionSelectable: Bool { selectableSpacesCount > 0 } var isAskToJoinWithSpaceMembersOptionAvailable: Bool { currentSettings.accessType.isAskToJoinWithSpaceMembers || isAskToJoinWithSpaceMembersOptionSelectable } var isAskToJoinWithSpaceMembersOptionSelectable: Bool { isSpaceMembersOptionSelectable && isKnockingEnabled } var spaceMembersDescription: String { if isSpaceMembersOptionSelectable { switch spaceSelection { case .singleJoined(let joinedSpace): L10n.screenSecurityAndPrivacyRoomAccessSpaceMembersOptionSingleParentDescription(joinedSpace.name) case .singleUnknown(let id): L10n.screenSecurityAndPrivacyRoomAccessSpaceMembersOptionSingleParentDescription(id) case .multiple, .empty: L10n.screenSecurityAndPrivacyRoomAccessSpaceMembersOptionMultipleParentsDescription } } else { L10n.screenSecurityAndPrivacyRoomAccessSpaceMembersOptionUnavailableDescription } } var askToJoinWithSpaceMembersDescription: String { if isAskToJoinWithSpaceMembersOptionSelectable { switch spaceSelection { case .singleJoined(let joinedSpace): L10n.screenSecurityAndPrivacyAskToJoinSingleSpaceMembersOptionDescription(joinedSpace.name) case .singleUnknown(let id): L10n.screenSecurityAndPrivacyAskToJoinSingleSpaceMembersOptionDescription(id) case .multiple, .empty: L10n.screenSecurityAndPrivacyAskToJoinMultipleSpacesMembersOptionDescription } } else { L10n.screenSecurityAndPrivacyRoomAccessSpaceMembersOptionUnavailableDescription } } var shouldShowAccessSectionFooter: Bool { if (bindings.desiredSettings.accessType.isSpaceMembers && isSpaceMembersOptionSelectable) || (bindings.desiredSettings.accessType.isAskToJoinWithSpaceMembers && isAskToJoinWithSpaceMembersOptionSelectable), case .multiple = spaceSelection { return true } return false } enum SpaceSelection { /// There is only one available parent space for selection and is joined by the user case singleJoined(SpaceServiceRoom) /// There is only one available space for selection and is unknown to the user case singleUnknown(id: String) /// Multiple spaces are available for selection case multiple /// Edge case where the space members access type was found but it did not contain any space case empty } var spaceSelection: SpaceSelection { if selectableSpacesCount == 0 { .empty } else if selectableSpacesCount > 1 { .multiple } else if let joinedSpace = selectableJoinedSpaces.first { if currentSettings.accessType.isSpaceMembers || currentSettings.accessType.isAskToJoinWithSpaceMembers { if currentSettings.accessType.spaceIDs.isEmpty { // Edge case where the access type is already space members, but it does not contain any id // So if the user wants to add their own parent they need to do it from the selection menu .multiple } else { .singleJoined(joinedSpace) } } else { .singleJoined(joinedSpace) } } else if let unknownSpaceID = currentSettings.accessType.spaceIDs.first { // The space is not joined by the user but is currently selected .singleUnknown(id: unknownSpaceID) } else { // Not reachable because it would mean the selectable spaces are more than 1 // but are neither selected and/or joined parents. fatalError("Not reachable") } } init(serverName: String, accessType: SecurityAndPrivacyRoomAccessType, isEncryptionEnabled: Bool, historyVisibility: SecurityAndPrivacyHistoryVisibility, isSpace: Bool, isKnockingEnabled: Bool, historySharingDetailsURL: URL) { self.serverName = serverName self.isKnockingEnabled = isKnockingEnabled self.isSpace = isSpace let settings = SecurityAndPrivacySettings(accessType: accessType, isEncryptionEnabled: isEncryptionEnabled, historyVisibility: historyVisibility) currentSettings = settings bindings = SecurityAndPrivacyScreenViewStateBindings(desiredSettings: settings) strings = SecurityAndPrivacyScreenStrings(historySharingDetailsURL: historySharingDetailsURL) } } struct SecurityAndPrivacyScreenViewStateBindings { var desiredSettings: SecurityAndPrivacySettings var alertInfo: AlertInfo? } struct SecurityAndPrivacySettings: Equatable { var accessType: SecurityAndPrivacyRoomAccessType var isEncryptionEnabled: Bool var historyVisibility: SecurityAndPrivacyHistoryVisibility var isVisibileInRoomDirectory: Bool? } enum SecurityAndPrivacyRoomAccessType: Equatable { case inviteOnly case askToJoin case askToJoinWithSpaceMembers(spaceIDs: [String]) case anyone case spaceMembers(spaceIDs: [String]) var isSpaceMembers: Bool { switch self { case .spaceMembers: true default: false } } var isAskToJoinWithSpaceMembers: Bool { switch self { case .askToJoinWithSpaceMembers: true default: false } } var isAddressRequired: Bool { switch self { case .inviteOnly, .spaceMembers: false case .anyone, .askToJoin, .askToJoinWithSpaceMembers: true } } var spaceIDs: [String] { switch self { case .spaceMembers(let spaceIDs), .askToJoinWithSpaceMembers(let spaceIDs): return spaceIDs case .inviteOnly, .askToJoin, .anyone: return [] } } } enum SecurityAndPrivacyAlertType { case enableEncryption case unsavedChanges } enum SecurityAndPrivacyScreenViewAction { case cancel case save case tryUpdatingEncryption(Bool) case editAddress case selectedSpaceMembersAccess case selectedAskToJoinWithSpaceMembersAccess case manageSpaces } enum SecurityAndPrivacyHistoryVisibility: Int, Comparable { case invited case shared case worldReadable var fallbackOption: Self { switch self { case .invited, .shared: return .shared case .worldReadable: return .invited } } static func < (lhs: SecurityAndPrivacyHistoryVisibility, rhs: SecurityAndPrivacyHistoryVisibility) -> Bool { lhs.rawValue < rhs.rawValue } } struct SecurityAndPrivacyScreenStrings { let accessSectionFooterString: AttributedString let historySectionFooterString: AttributedString init(historySharingDetailsURL: URL) { let linkPlaceholder = "{link}" var accessFooterString = AttributedString(L10n.screenSecurityAndPrivacyRoomAccessFooter(linkPlaceholder)) var accessLinkString = AttributedString(L10n.screenSecurityAndPrivacyRoomAccessFooterManageSpacesAction) accessLinkString.link = .init(stringLiteral: "action://manageSpace") // The link address doesn't matter accessLinkString.bold() accessFooterString.replace(linkPlaceholder, with: accessLinkString) accessSectionFooterString = accessFooterString var historyFooterString = AttributedString(L10n.screenSecurityAndPrivacyRoomHistorySectionFooter(linkPlaceholder)) var historyLinkString = AttributedString(L10n.actionLearnMore) historyLinkString.link = historySharingDetailsURL historyLinkString.bold() historyFooterString.replace(linkPlaceholder, with: historyLinkString) historySectionFooterString = historyFooterString } }