Update history visible settings (#4861)

* Fixes #4852 - Update history visible settings

* Address PR comments and move attributed strings to the view state
This commit is contained in:
Stefan Ceriu
2025-12-17 15:04:02 +02:00
committed by GitHub
parent aa28ef3f19
commit 8164c88437
34 changed files with 150 additions and 112 deletions
@@ -125,6 +125,7 @@ final class AppSettings {
deviceVerificationURL: URL,
chatBackupDetailsURL: URL,
identityPinningViolationDetailsURL: URL,
historySharingDetailsURL: URL,
elementWebHosts: [String],
accountProvisioningHost: String,
bugReportApplicationID: String,
@@ -144,6 +145,7 @@ final class AppSettings {
self.deviceVerificationURL = deviceVerificationURL
self.chatBackupDetailsURL = chatBackupDetailsURL
self.identityPinningViolationDetailsURL = identityPinningViolationDetailsURL
self.historySharingDetailsURL = historySharingDetailsURL
self.elementWebHosts = elementWebHosts
self.accountProvisioningHost = accountProvisioningHost
self.bugReportApplicationID = bugReportApplicationID
@@ -202,6 +204,9 @@ final class AppSettings {
private(set) var chatBackupDetailsURL: URL = "https://element.io/help#encryption5"
/// A URL where users can go read more about identity pinning violations
private(set) var identityPinningViolationDetailsURL: URL = "https://element.io/help#encryption18"
/// A URL describing how history sharing works
private(set) var historySharingDetailsURL: URL = "https://element.io/en/help#e2ee-history-sharing"
/// Any domains that Element web may be hosted on - used for handling links.
private(set) var elementWebHosts = ["app.element.io", "staging.element.io", "develop.element.io"]
/// The domain that account provisioning links will be hosted on - used for handling the links.
+7 -3
View File
@@ -2906,13 +2906,17 @@ internal enum L10n {
internal static var screenSecurityAndPrivacyRoomDirectoryVisibilityToggleDescription: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_directory_visibility_toggle_description") }
/// Visible in public directory
internal static var screenSecurityAndPrivacyRoomDirectoryVisibilityToggleTitle: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_directory_visibility_toggle_title") }
/// Anyone
/// Anyone (history is public)
internal static var screenSecurityAndPrivacyRoomHistoryAnyoneOptionTitle: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_history_anyone_option_title") }
/// Changes won't affect past messages, only new ones. %@
internal static func screenSecurityAndPrivacyRoomHistorySectionFooter(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_security_and_privacy_room_history_section_footer", String(describing: p1))
}
/// Who can read history
internal static var screenSecurityAndPrivacyRoomHistorySectionHeader: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_history_section_header") }
/// Members only since they were invited
/// Members since invited
internal static var screenSecurityAndPrivacyRoomHistorySinceInviteOptionTitle: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_history_since_invite_option_title") }
/// Members only since selecting this option
/// Members (full history)
internal static var screenSecurityAndPrivacyRoomHistorySinceSelectingOptionTitle: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_history_since_selecting_option_title") }
/// Room addresses are ways to find and access rooms. This also ensures you can easily share your room with others.
/// You can choose to publish your room in your homeserver public room directory.
@@ -15,20 +15,12 @@ enum SecurityAndPrivacyScreenViewModelAction {
}
struct SecurityAndPrivacyScreenViewState: BindableState {
private static let accessSectionFooterAttributedString = {
let linkPlaceholder = "{link}"
var footer = AttributedString(L10n.screenSecurityAndPrivacyRoomAccessFooter(linkPlaceholder))
var linkString = AttributedString(L10n.screenSecurityAndPrivacyRoomAccessFooterManageSpacesAction)
// Doesn't really matter
linkString.link = .init(stringLiteral: "action://manageSpace")
linkString.bold()
footer.replace(linkPlaceholder, with: linkString)
return footer
}()
let serverName: String
var currentSettings: SecurityAndPrivacySettings
var bindings: SecurityAndPrivacyScreenViewStateBindings
let strings: SecurityAndPrivacyScreenStrings
var canonicalAlias: String?
var isKnockingEnabled: Bool
var isSpaceSettingsEnabled: Bool
@@ -38,6 +30,7 @@ struct SecurityAndPrivacyScreenViewState: BindableState {
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: [SpaceRoomProxyProtocol] = []
@@ -58,13 +51,13 @@ struct SecurityAndPrivacyScreenViewState: BindableState {
}
var availableVisibilityOptions: [SecurityAndPrivacyHistoryVisibility] {
var options = [SecurityAndPrivacyHistoryVisibility.sinceSelection]
var options = [SecurityAndPrivacyHistoryVisibility.shared]
if !bindings.desiredSettings.isEncryptionEnabled, bindings.desiredSettings.accessType == .anyone {
options.append(.anyone)
options.append(.worldReadable)
} else {
options.append(.sinceInvite)
options.append(.invited)
}
return options
return options.sorted()
}
var isSpaceMembersOptionAvailable: Bool {
@@ -113,16 +106,14 @@ struct SecurityAndPrivacyScreenViewState: BindableState {
}
}
var accessSectionFooter: AttributedString? {
if (bindings.desiredSettings.accessType.isSpaceMembers &&
isSpaceMembersOptionSelectable) ||
(bindings.desiredSettings.accessType.isAskToJoinWithSpaceMembers &&
isAskToJoinWithSpaceMembersOptionSelectable),
var shouldShowAccessSectionFooter: Bool {
if (bindings.desiredSettings.accessType.isSpaceMembers && isSpaceMembersOptionSelectable) ||
(bindings.desiredSettings.accessType.isAskToJoinWithSpaceMembers && isAskToJoinWithSpaceMembersOptionSelectable),
case .multiple = spaceSelection {
Self.accessSectionFooterAttributedString
} else {
nil
return true
}
return false
}
enum SpaceSelection {
@@ -169,7 +160,8 @@ struct SecurityAndPrivacyScreenViewState: BindableState {
historyVisibility: SecurityAndPrivacyHistoryVisibility,
isSpace: Bool,
isKnockingEnabled: Bool,
isSpaceSettingsEnabled: Bool) {
isSpaceSettingsEnabled: Bool,
historySharingDetailsURL: URL) {
self.serverName = serverName
self.isKnockingEnabled = isKnockingEnabled
self.isSpace = isSpace
@@ -180,6 +172,7 @@ struct SecurityAndPrivacyScreenViewState: BindableState {
historyVisibility: historyVisibility)
currentSettings = settings
bindings = SecurityAndPrivacyScreenViewStateBindings(desiredSettings: settings)
strings = SecurityAndPrivacyScreenStrings(historySharingDetailsURL: historySharingDetailsURL)
}
}
@@ -254,17 +247,44 @@ enum SecurityAndPrivacyScreenViewAction {
case manageSpaces
}
enum SecurityAndPrivacyHistoryVisibility {
case sinceSelection
case sinceInvite
case anyone
enum SecurityAndPrivacyHistoryVisibility: Int, Comparable {
case invited
case shared
case worldReadable
var fallbackOption: Self {
switch self {
case .sinceInvite, .sinceSelection:
return .sinceSelection
case .anyone:
return .sinceInvite
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
}
}
@@ -38,7 +38,8 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType,
historyVisibility: roomProxy.infoPublisher.value.historyVisibility.toSecurityAndPrivacyHistoryVisibility,
isSpace: roomProxy.infoPublisher.value.isSpace,
isKnockingEnabled: appSettings.knockingEnabled,
isSpaceSettingsEnabled: appSettings.spaceSettingsEnabled))
isSpaceSettingsEnabled: appSettings.spaceSettingsEnabled,
historySharingDetailsURL: appSettings.historySharingDetailsURL))
if let powerLevels = roomProxy.infoPublisher.value.powerLevels {
setupPermissions(powerLevels: powerLevels)
@@ -345,11 +346,11 @@ private extension RoomHistoryVisibility {
var toSecurityAndPrivacyHistoryVisibility: SecurityAndPrivacyHistoryVisibility {
switch self {
case .joined, .invited:
return .sinceInvite
return .invited
case .shared, .custom:
return .sinceSelection
return .shared
case .worldReadable:
return .anyone
return .worldReadable
}
}
}
@@ -357,11 +358,11 @@ private extension RoomHistoryVisibility {
private extension SecurityAndPrivacyHistoryVisibility {
var toRoomHistoryVisibility: RoomHistoryVisibility {
switch self {
case .sinceSelection:
case .shared:
return .shared
case .sinceInvite:
case .invited:
return .invited
case .anyone:
case .worldReadable:
return .worldReadable
}
}
@@ -80,8 +80,8 @@ struct SecurityAndPrivacyScreen: View {
Text(L10n.screenSecurityAndPrivacyRoomAccessSectionHeader)
.compoundListSectionHeader()
} footer: {
if let footer = context.viewState.accessSectionFooter {
Text(footer)
if context.viewState.shouldShowAccessSectionFooter {
Text(context.viewState.strings.accessSectionFooterString)
.compoundListSectionFooter()
.environment(\.openURL, OpenURLAction { _ in
context.send(viewAction: .manageSpaces)
@@ -133,20 +133,23 @@ struct SecurityAndPrivacyScreen: View {
Section {
ForEach(context.viewState.availableVisibilityOptions, id: \.self) { option in
switch option {
case .sinceSelection:
ListRow(label: .plain(title: L10n.screenSecurityAndPrivacyRoomHistorySinceSelectingOptionTitle),
kind: .selection(isSelected: context.desiredSettings.historyVisibility == .sinceSelection) { context.desiredSettings.historyVisibility = .sinceSelection })
case .anyone:
ListRow(label: .plain(title: L10n.screenSecurityAndPrivacyRoomHistoryAnyoneOptionTitle),
kind: .selection(isSelected: context.desiredSettings.historyVisibility == .anyone) { context.desiredSettings.historyVisibility = .anyone })
case .sinceInvite:
case .invited:
ListRow(label: .plain(title: L10n.screenSecurityAndPrivacyRoomHistorySinceInviteOptionTitle),
kind: .selection(isSelected: context.desiredSettings.historyVisibility == .sinceInvite) { context.desiredSettings.historyVisibility = .sinceInvite })
kind: .selection(isSelected: context.desiredSettings.historyVisibility == .invited) { context.desiredSettings.historyVisibility = .invited })
case .shared:
ListRow(label: .plain(title: L10n.screenSecurityAndPrivacyRoomHistorySinceSelectingOptionTitle),
kind: .selection(isSelected: context.desiredSettings.historyVisibility == .shared) { context.desiredSettings.historyVisibility = .shared })
case .worldReadable:
ListRow(label: .plain(title: L10n.screenSecurityAndPrivacyRoomHistoryAnyoneOptionTitle),
kind: .selection(isSelected: context.desiredSettings.historyVisibility == .worldReadable) { context.desiredSettings.historyVisibility = .worldReadable })
}
}
} header: {
Text(L10n.screenSecurityAndPrivacyRoomHistorySectionHeader)
.compoundListSectionHeader()
} footer: {
Text(context.viewState.strings.historySectionFooterString)
.compoundListSectionFooter()
}
}
@@ -140,6 +140,7 @@ class MockScreen: Identifiable {
deviceVerificationURL: appSettings.deviceVerificationURL,
chatBackupDetailsURL: appSettings.chatBackupDetailsURL,
identityPinningViolationDetailsURL: appSettings.identityPinningViolationDetailsURL,
historySharingDetailsURL: appSettings.historySharingDetailsURL,
elementWebHosts: appSettings.elementWebHosts,
accountProvisioningHost: appSettings.accountProvisioningHost,
bugReportApplicationID: appSettings.bugReportApplicationID,