mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 08:15:24 +00:00
fix: remove rlement endpoints and links from the HealthChat
* fix(healthchat): switch off analytics, crash and bug reporting endpoints Upstream ships *.localhost placeholders in Secrets.swift and marks the file assume-unchanged; with analytics on, the Element Call widget was handed Element's own PostHog and Sentry. nil hides the analytics prompt, the Analytics and Report a problem entries until HealthChat runs its own services. * feat(healthchat): hide the Labs entry in settings * fix(healthchat): point Copyright and the analytics terms at healthchat.ch * docs(healthchat): add the 26.11.1 release notes * feat(healthchat): drop the Element help links * feat(healthchat): switch off location sharing * test(healthchat): skip the analytics suite without PostHog
This commit is contained in:
@@ -43,7 +43,13 @@ Upstream files the fork edits — conflicts live here every time:
|
|||||||
migration contract"), `AppHooks/UserHooks.swift` (two HealthChat hook fields),
|
migration contract"), `AppHooks/UserHooks.swift` (two HealthChat hook fields),
|
||||||
`AppSettings.swift` (VoIP push properties for the NSE), `ElementCallService.swift` (PushKit
|
`AppSettings.swift` (VoIP push properties for the NSE), `ElementCallService.swift` (PushKit
|
||||||
token, expiration parsing), `NSE/Sources/NotificationHandler.swift` (VoIP push to Sygnal),
|
token, expiration parsing), `NSE/Sources/NotificationHandler.swift` (VoIP push to Sygnal),
|
||||||
`UserSessionFlowCoordinator` (Members tab), `SettingsScreen*` (account status section),
|
`UserSessionFlowCoordinator` (Members tab), `SettingsScreen*` (account status section, Labs
|
||||||
|
gate), the five help URLs are `URL?` (`AppSettings`, `SessionVerificationScreen*`,
|
||||||
|
`IdentityConfirmationScreen*`, `SecureBackupScreen*`, `RoomScreenModels` + `RoomScreenFooterView`,
|
||||||
|
`SecurityAndPrivacyScreenModels`, `TimelineViewModel` alert — nil hides the "Learn more" link), `UnitTests/Sources/AnalyticsTests.swift` (suite skipped without PostHog secrets),
|
||||||
|
`Components/Secrets/Secrets.swift` (analytics/sentry/rageshake nil, hand-edited; upstream's setup
|
||||||
|
marks it `assume-unchanged`, so run `git update-index --no-assume-unchanged` on it before an
|
||||||
|
edit shows up in `git status` or a commit),
|
||||||
`RoomMemberDetailsScreen*` + `UserProfileScreen*` (extended profile section), the five fork-gated
|
`RoomMemberDetailsScreen*` + `UserProfileScreen*` (extended profile section), the five fork-gated
|
||||||
CI workflows, `.gitignore`, `project.pbxproj` (never resolve by hand — `xcodegen`).
|
CI workflows, `.gitignore`, `project.pbxproj` (never resolve by hand — `xcodegen`).
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
nonisolated enum Secrets {
|
nonisolated enum Secrets {
|
||||||
static let sentryDSN: String? = "https://username@sentry.localhost/project_id"
|
static let sentryDSN: String? = nil
|
||||||
static let sentryRustDSN: String? = "https://username@sentry.localhost/project_id"
|
static let sentryRustDSN: String? = nil
|
||||||
static let postHogHost: String? = "https://posthog.localhost"
|
static let postHogHost: String? = nil
|
||||||
static let postHogAPIKey: String? = "your_key"
|
static let postHogAPIKey: String? = nil
|
||||||
static let rageshakeURL: String? = "https://rageshake.localhost/submit"
|
static let rageshakeURL: String? = nil
|
||||||
static let mapLibreAPIKey: String? = "your_key"
|
static let mapLibreAPIKey: String? = "your_key"
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,8 @@ import Foundation
|
|||||||
/// - `accountProviders` / `allowOtherAccountProviders` lock every user to `healthchat.ch` (the server name;
|
/// - `accountProviders` / `allowOtherAccountProviders` lock every user to `healthchat.ch` (the server name;
|
||||||
/// `.well-known` delegates to `matrix.healthchat.ch`) and hide the change-server / create-account UI.
|
/// `.well-known` delegates to `matrix.healthchat.ch`) and hide the change-server / create-account UI.
|
||||||
/// - `oAuthRedirectURL` uses the app's own URL scheme so the OIDC/MAS callback needs no associated domain.
|
/// - `oAuthRedirectURL` uses the app's own URL scheme so the OIDC/MAS callback needs no associated domain.
|
||||||
/// - Legacy `element.io` help/encryption/copyright URLs are passed through unchanged for now.
|
/// - Legal links point at healthchat.ch. The encryption/verification/backup help links still go to
|
||||||
|
/// Element's help pages because no HealthChat equivalents exist yet.
|
||||||
struct HealthChatAppSettingsHook: AppSettingsHookProtocol {
|
struct HealthChatAppSettingsHook: AppSettingsHookProtocol {
|
||||||
func configure(_ appSettings: AppSettings) -> AppSettings {
|
func configure(_ appSettings: AppSettings) -> AppSettings {
|
||||||
appSettings.override(accountProviders: [.generic("healthchat.ch")],
|
appSettings.override(accountProviders: [.generic("healthchat.ch")],
|
||||||
@@ -24,19 +25,25 @@ struct HealthChatAppSettingsHook: AppSettingsHookProtocol {
|
|||||||
oAuthClientURIPath: nil,
|
oAuthClientURIPath: nil,
|
||||||
websiteURL: "https://healthchat.ch",
|
websiteURL: "https://healthchat.ch",
|
||||||
logoURL: "https://app.healthchat.ch/themes/element/img/logos/element-logo.png",
|
logoURL: "https://app.healthchat.ch/themes/element/img/logos/element-logo.png",
|
||||||
copyrightURL: appSettings.copyrightURL,
|
copyrightURL: "https://www.healthchat.ch/impressum",
|
||||||
acceptableUseURL: "https://www.healthchat.ch/datenschutz",
|
acceptableUseURL: "https://www.healthchat.ch/datenschutz",
|
||||||
privacyURL: "https://www.healthchat.ch/datenschutz",
|
privacyURL: "https://www.healthchat.ch/datenschutz",
|
||||||
encryptionURL: appSettings.encryptionURL,
|
encryptionURL: nil,
|
||||||
deviceVerificationURL: appSettings.deviceVerificationURL,
|
deviceVerificationURL: nil,
|
||||||
chatBackupDetailsURL: appSettings.chatBackupDetailsURL,
|
chatBackupDetailsURL: nil,
|
||||||
identityPinningViolationDetailsURL: appSettings.identityPinningViolationDetailsURL,
|
identityPinningViolationDetailsURL: nil,
|
||||||
historySharingDetailsURL: appSettings.historySharingDetailsURL,
|
historySharingDetailsURL: nil,
|
||||||
elementWebHosts: ["app.healthchat.ch"],
|
elementWebHosts: ["app.healthchat.ch"],
|
||||||
accountProvisioningHost: appSettings.accountProvisioningHost,
|
accountProvisioningHost: appSettings.accountProvisioningHost,
|
||||||
bugReportApplicationID: appSettings.bugReportApplicationID,
|
bugReportApplicationID: "healthchat-ios",
|
||||||
analyticsTermsURL: appSettings.analyticsTermsURL,
|
analyticsTermsURL: "https://www.healthchat.ch/datenschutz",
|
||||||
mapTilerConfiguration: AppSettings.bundledMapTilerConfiguration)
|
mapTilerConfiguration: Self.mapTilerConfiguration)
|
||||||
return appSettings
|
return appSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// No MapTiler account yet: a nil key hides location sharing and shows received locations as a placeholder.
|
||||||
|
private static let mapTilerConfiguration = MapTilerConfiguration(baseURL: AppSettings.bundledMapTilerConfiguration.baseURL,
|
||||||
|
apiKey: nil,
|
||||||
|
lightStyleID: AppSettings.bundledMapTilerConfiguration.lightStyleID,
|
||||||
|
darkStyleID: AppSettings.bundledMapTilerConfiguration.darkStyleID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,5 +28,10 @@ final class HealthChatSettingsScreenHook: SettingsScreenHookProtocol {
|
|||||||
func refreshAccountStatus() {
|
func refreshAccountStatus() {
|
||||||
statusLoader.load()
|
statusLoader.load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Upstream's experiments are not supported by the HealthChat server setup and never reviewed for it.
|
||||||
|
nonisolated var showsLabs: Bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ nonisolated protocol SettingsScreenHookProtocol: Sendable {
|
|||||||
|
|
||||||
/// Called whenever the settings screen appears, so the section reflects what the user just did.
|
/// Called whenever the settings screen appears, so the section reflects what the user just did.
|
||||||
@MainActor func refreshAccountStatus()
|
@MainActor func refreshAccountStatus()
|
||||||
|
|
||||||
|
/// Whether the Labs entry (experimental upstream features) is offered at all.
|
||||||
|
var showsLabs: Bool { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DefaultSettingsScreenHook: SettingsScreenHookProtocol {
|
struct DefaultSettingsScreenHook: SettingsScreenHookProtocol {
|
||||||
@@ -23,4 +26,8 @@ struct DefaultSettingsScreenHook: SettingsScreenHookProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func refreshAccountStatus() { }
|
func refreshAccountStatus() { }
|
||||||
|
|
||||||
|
var showsLabs: Bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,11 +65,11 @@ final nonisolated class AppSettings: @unchecked Sendable {
|
|||||||
copyrightURL: URL,
|
copyrightURL: URL,
|
||||||
acceptableUseURL: URL,
|
acceptableUseURL: URL,
|
||||||
privacyURL: URL,
|
privacyURL: URL,
|
||||||
encryptionURL: URL,
|
encryptionURL: URL?,
|
||||||
deviceVerificationURL: URL,
|
deviceVerificationURL: URL?,
|
||||||
chatBackupDetailsURL: URL,
|
chatBackupDetailsURL: URL?,
|
||||||
identityPinningViolationDetailsURL: URL,
|
identityPinningViolationDetailsURL: URL?,
|
||||||
historySharingDetailsURL: URL,
|
historySharingDetailsURL: URL?,
|
||||||
elementWebHosts: [String],
|
elementWebHosts: [String],
|
||||||
accountProvisioningHost: String,
|
accountProvisioningHost: String,
|
||||||
bugReportApplicationID: String,
|
bugReportApplicationID: String,
|
||||||
@@ -145,15 +145,15 @@ final nonisolated class AppSettings: @unchecked Sendable {
|
|||||||
/// A URL that contains the app's Privacy Policy.
|
/// A URL that contains the app's Privacy Policy.
|
||||||
private(set) var privacyURL: URL = "https://element.io/privacy"
|
private(set) var privacyURL: URL = "https://element.io/privacy"
|
||||||
/// A URL where users can go read more about encryption in general.
|
/// A URL where users can go read more about encryption in general.
|
||||||
private(set) var encryptionURL: URL = "https://element.io/help#encryption"
|
private(set) var encryptionURL: URL? = "https://element.io/help#encryption"
|
||||||
/// A URL where users can go read more about device verification..
|
/// A URL where users can go read more about device verification..
|
||||||
private(set) var deviceVerificationURL: URL = "https://element.io/help#encryption-device-verification"
|
private(set) var deviceVerificationURL: URL? = "https://element.io/help#encryption-device-verification"
|
||||||
/// A URL where users can go read more about the chat backup.
|
/// A URL where users can go read more about the chat backup.
|
||||||
private(set) var chatBackupDetailsURL: URL = "https://element.io/help#encryption5"
|
private(set) var chatBackupDetailsURL: URL? = "https://element.io/help#encryption5"
|
||||||
/// A URL where users can go read more about identity pinning violations
|
/// A URL where users can go read more about identity pinning violations
|
||||||
private(set) var identityPinningViolationDetailsURL: URL = "https://element.io/help#encryption18"
|
private(set) var identityPinningViolationDetailsURL: URL? = "https://element.io/help#encryption18"
|
||||||
/// A URL describing how history sharing works
|
/// A URL describing how history sharing works
|
||||||
private(set) var historySharingDetailsURL: URL = "https://element.io/en/help#e2ee-history-sharing"
|
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.
|
/// 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"]
|
private(set) var elementWebHosts = ["app.element.io", "staging.element.io", "develop.element.io"]
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@ struct IdentityConfirmationScreenViewState: BindableState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var availableActions: [AvailableActions]?
|
var availableActions: [AvailableActions]?
|
||||||
let learnMoreURL: URL
|
let learnMoreURL: URL?
|
||||||
|
|
||||||
var bindings = IdentityConfirmationScreenBindings()
|
var bindings = IdentityConfirmationScreenBindings()
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -53,11 +53,13 @@ struct IdentityConfirmationScreen: View {
|
|||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.foregroundColor(.compound.textSecondary)
|
.foregroundColor(.compound.textSecondary)
|
||||||
|
|
||||||
Button(L10n.actionLearnMore) {
|
if let learnMoreURL = context.viewState.learnMoreURL {
|
||||||
UIApplication.shared.open(context.viewState.learnMoreURL)
|
Button(L10n.actionLearnMore) {
|
||||||
|
UIApplication.shared.open(learnMoreURL)
|
||||||
|
}
|
||||||
|
.buttonStyle(.compound(.tertiary, size: .small))
|
||||||
|
.padding(.top, 16)
|
||||||
}
|
}
|
||||||
.buttonStyle(.compound(.tertiary, size: .small))
|
|
||||||
.padding(.top, 16)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ enum SessionVerificationScreenViewAction {
|
|||||||
|
|
||||||
struct SessionVerificationScreenViewState: BindableState {
|
struct SessionVerificationScreenViewState: BindableState {
|
||||||
let flow: SessionVerificationScreenFlow
|
let flow: SessionVerificationScreenFlow
|
||||||
let learnMoreURL: URL
|
let learnMoreURL: URL?
|
||||||
|
|
||||||
var verificationState: SessionVerificationScreenStateMachine.State
|
var verificationState: SessionVerificationScreenStateMachine.State
|
||||||
|
|
||||||
|
|||||||
+5
-3
@@ -92,10 +92,12 @@ struct SessionVerificationScreen: View {
|
|||||||
isUserVerification: true,
|
isUserVerification: true,
|
||||||
mediaProvider: context.mediaProvider)
|
mediaProvider: context.mediaProvider)
|
||||||
case .userInitiator:
|
case .userInitiator:
|
||||||
Button(L10n.actionLearnMore) {
|
if let learnMoreURL = context.viewState.learnMoreURL {
|
||||||
UIApplication.shared.open(context.viewState.learnMoreURL)
|
Button(L10n.actionLearnMore) {
|
||||||
|
UIApplication.shared.open(learnMoreURL)
|
||||||
|
}
|
||||||
|
.buttonStyle(.compound(.tertiary, size: .small))
|
||||||
}
|
}
|
||||||
.buttonStyle(.compound(.tertiary, size: .small))
|
|
||||||
default:
|
default:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,8 +118,8 @@ enum RoomScreenFooterViewAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum RoomScreenFooterViewDetails {
|
enum RoomScreenFooterViewDetails {
|
||||||
case pinViolation(member: RoomMemberProxyProtocol, learnMoreURL: URL)
|
case pinViolation(member: RoomMemberProxyProtocol, learnMoreURL: URL?)
|
||||||
case verificationViolation(member: RoomMemberProxyProtocol, learnMoreURL: URL)
|
case verificationViolation(member: RoomMemberProxyProtocol, learnMoreURL: URL?)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PinnedEventsBannerState: Equatable {
|
enum PinnedEventsBannerState: Equatable {
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ struct RoomScreenFooterView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func pinViolation(member: RoomMemberProxyProtocol,
|
private func pinViolation(member: RoomMemberProxyProtocol,
|
||||||
learnMoreURL: URL) -> some View {
|
learnMoreURL: URL?) -> some View {
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: 16) {
|
||||||
HStack(spacing: 16) {
|
HStack(spacing: 16) {
|
||||||
LoadableAvatarImage(url: member.avatarURL,
|
LoadableAvatarImage(url: member.avatarURL,
|
||||||
@@ -86,7 +86,7 @@ struct RoomScreenFooterView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func verificationViolation(member: RoomMemberProxyProtocol,
|
private func verificationViolation(member: RoomMemberProxyProtocol,
|
||||||
learnMoreURL: URL) -> some View {
|
learnMoreURL: URL?) -> some View {
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: 16) {
|
||||||
HStack(spacing: 16) {
|
HStack(spacing: 16) {
|
||||||
LoadableAvatarImage(url: member.avatarURL,
|
LoadableAvatarImage(url: member.avatarURL,
|
||||||
@@ -115,7 +115,7 @@ struct RoomScreenFooterView: View {
|
|||||||
.padding(.bottom, 8)
|
.padding(.bottom, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func pinViolationDescriptionWithLearnMoreLink(displayName: String?, userID: String, url: URL) -> AttributedString {
|
private func pinViolationDescriptionWithLearnMoreLink(displayName: String?, userID: String, url: URL?) -> AttributedString {
|
||||||
let userIDPlaceholder = "{mxid}"
|
let userIDPlaceholder = "{mxid}"
|
||||||
let linkPlaceholder = "{link}"
|
let linkPlaceholder = "{link}"
|
||||||
let displayName = displayName ?? fallbackDisplayName(userID)
|
let displayName = displayName ?? fallbackDisplayName(userID)
|
||||||
@@ -125,14 +125,14 @@ struct RoomScreenFooterView: View {
|
|||||||
userIDString.bold()
|
userIDString.bold()
|
||||||
description.replace(userIDPlaceholder, with: userIDString)
|
description.replace(userIDPlaceholder, with: userIDString)
|
||||||
|
|
||||||
var linkString = AttributedString(L10n.actionLearnMore)
|
var linkString = AttributedString(url == nil ? "" : L10n.actionLearnMore)
|
||||||
linkString.link = url
|
linkString.link = url
|
||||||
linkString.bold()
|
linkString.bold()
|
||||||
description.replace(linkPlaceholder, with: linkString)
|
description.replace(linkPlaceholder, with: linkString)
|
||||||
return description
|
return description
|
||||||
}
|
}
|
||||||
|
|
||||||
private func verificationViolationDescriptionWithLearnMoreLink(displayName: String?, userID: String, url: URL) -> AttributedString {
|
private func verificationViolationDescriptionWithLearnMoreLink(displayName: String?, userID: String, url: URL?) -> AttributedString {
|
||||||
let userIDPlaceholder = "{mxid}"
|
let userIDPlaceholder = "{mxid}"
|
||||||
let linkPlaceholder = "{link}"
|
let linkPlaceholder = "{link}"
|
||||||
let displayName = displayName ?? fallbackDisplayName(userID)
|
let displayName = displayName ?? fallbackDisplayName(userID)
|
||||||
@@ -142,7 +142,7 @@ struct RoomScreenFooterView: View {
|
|||||||
userIDString.bold()
|
userIDString.bold()
|
||||||
description.replace(userIDPlaceholder, with: userIDString)
|
description.replace(userIDPlaceholder, with: userIDString)
|
||||||
|
|
||||||
var linkString = AttributedString(L10n.actionLearnMore)
|
var linkString = AttributedString(url == nil ? "" : L10n.actionLearnMore)
|
||||||
linkString.link = url
|
linkString.link = url
|
||||||
linkString.bold()
|
linkString.bold()
|
||||||
description.replace(linkPlaceholder, with: linkString)
|
description.replace(linkPlaceholder, with: linkString)
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ enum SecureBackupScreenViewModelAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct SecureBackupScreenViewState: BindableState {
|
struct SecureBackupScreenViewState: BindableState {
|
||||||
let chatBackupDetailsURL: URL
|
let chatBackupDetailsURL: URL?
|
||||||
var recoveryState = SecureBackupRecoveryState.unknown
|
var recoveryState = SecureBackupRecoveryState.unknown
|
||||||
var keyBackupState = SecureBackupKeyBackupState.unknown
|
var keyBackupState = SecureBackupKeyBackupState.unknown
|
||||||
var bindings: SecureBackupScreenViewStateBindings
|
var bindings: SecureBackupScreenViewStateBindings
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ class SecureBackupScreenViewModel: SecureBackupScreenViewModelType, SecureBackup
|
|||||||
|
|
||||||
init(secureBackupController: SecureBackupControllerProtocol,
|
init(secureBackupController: SecureBackupControllerProtocol,
|
||||||
userIndicatorController: UserIndicatorControllerProtocol,
|
userIndicatorController: UserIndicatorControllerProtocol,
|
||||||
chatBackupDetailsURL: URL) {
|
chatBackupDetailsURL: URL?) {
|
||||||
self.secureBackupController = secureBackupController
|
self.secureBackupController = secureBackupController
|
||||||
self.userIndicatorController = userIndicatorController
|
self.userIndicatorController = userIndicatorController
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -64,10 +64,15 @@ struct SecureBackupScreen: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var keyBackupDescriptionWithLearnMoreLink: AttributedString {
|
private var keyBackupDescriptionWithLearnMoreLink: AttributedString {
|
||||||
|
guard let chatBackupDetailsURL = context.viewState.chatBackupDetailsURL else {
|
||||||
|
// The link is its own sentence, so the full stop goes with it.
|
||||||
|
return AttributedString(L10n.screenChatBackupKeyBackupDescription("").replacing(/\s*\.\s*$/, with: ""))
|
||||||
|
}
|
||||||
|
|
||||||
let linkPlaceholder = "{link}"
|
let linkPlaceholder = "{link}"
|
||||||
var description = AttributedString(L10n.screenChatBackupKeyBackupDescription(linkPlaceholder))
|
var description = AttributedString(L10n.screenChatBackupKeyBackupDescription(linkPlaceholder))
|
||||||
var linkString = AttributedString(L10n.actionLearnMore)
|
var linkString = AttributedString(L10n.actionLearnMore)
|
||||||
linkString.link = context.viewState.chatBackupDetailsURL
|
linkString.link = chatBackupDetailsURL
|
||||||
linkString.bold()
|
linkString.bold()
|
||||||
description.replace(linkPlaceholder, with: linkString)
|
description.replace(linkPlaceholder, with: linkString)
|
||||||
return description
|
return description
|
||||||
|
|||||||
+3
-3
@@ -162,7 +162,7 @@ struct SecurityAndPrivacyScreenViewState: BindableState {
|
|||||||
historyVisibility: SecurityAndPrivacyHistoryVisibility,
|
historyVisibility: SecurityAndPrivacyHistoryVisibility,
|
||||||
isSpace: Bool,
|
isSpace: Bool,
|
||||||
isKnockingEnabled: Bool,
|
isKnockingEnabled: Bool,
|
||||||
historySharingDetailsURL: URL) {
|
historySharingDetailsURL: URL?) {
|
||||||
self.isKnockingEnabled = isKnockingEnabled
|
self.isKnockingEnabled = isKnockingEnabled
|
||||||
self.isSpace = isSpace
|
self.isSpace = isSpace
|
||||||
|
|
||||||
@@ -269,7 +269,7 @@ struct SecurityAndPrivacyScreenStrings {
|
|||||||
let accessSectionFooterString: AttributedString
|
let accessSectionFooterString: AttributedString
|
||||||
let historySectionFooterString: AttributedString
|
let historySectionFooterString: AttributedString
|
||||||
|
|
||||||
init(historySharingDetailsURL: URL) {
|
init(historySharingDetailsURL: URL?) {
|
||||||
let linkPlaceholder = "{link}"
|
let linkPlaceholder = "{link}"
|
||||||
|
|
||||||
var accessFooterString = AttributedString(L10n.screenSecurityAndPrivacyRoomAccessFooter(linkPlaceholder))
|
var accessFooterString = AttributedString(L10n.screenSecurityAndPrivacyRoomAccessFooter(linkPlaceholder))
|
||||||
@@ -280,7 +280,7 @@ struct SecurityAndPrivacyScreenStrings {
|
|||||||
accessSectionFooterString = accessFooterString
|
accessSectionFooterString = accessFooterString
|
||||||
|
|
||||||
var historyFooterString = AttributedString(L10n.screenSecurityAndPrivacyRoomHistorySectionFooter(linkPlaceholder))
|
var historyFooterString = AttributedString(L10n.screenSecurityAndPrivacyRoomHistorySectionFooter(linkPlaceholder))
|
||||||
var historyLinkString = AttributedString(L10n.actionLearnMore)
|
var historyLinkString = AttributedString(historySharingDetailsURL == nil ? "" : L10n.actionLearnMore)
|
||||||
historyLinkString.link = historySharingDetailsURL
|
historyLinkString.link = historySharingDetailsURL
|
||||||
historyLinkString.bold()
|
historyLinkString.bold()
|
||||||
historyFooterString.replace(linkPlaceholder, with: historyLinkString)
|
historyFooterString.replace(linkPlaceholder, with: historyLinkString)
|
||||||
|
|||||||
@@ -211,11 +211,13 @@ struct SettingsScreen: View {
|
|||||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.analytics)
|
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.analytics)
|
||||||
}
|
}
|
||||||
|
|
||||||
ListRow(label: .default(title: L10n.screenAdvancedSettingsLabs,
|
if context.viewState.settingsScreenHook.showsLabs {
|
||||||
icon: \.labs),
|
ListRow(label: .default(title: L10n.screenAdvancedSettingsLabs,
|
||||||
kind: .navigationLink {
|
icon: \.labs),
|
||||||
context.send(viewAction: .labs)
|
kind: .navigationLink {
|
||||||
})
|
context.send(viewAction: .labs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
ListRow(label: .default(title: L10n.commonAbout,
|
ListRow(label: .default(title: L10n.commonAbout,
|
||||||
icon: \.info),
|
icon: \.info),
|
||||||
|
|||||||
@@ -1143,9 +1143,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
state.bindings.alertInfo = .init(id: type,
|
state.bindings.alertInfo = .init(id: type,
|
||||||
title: message,
|
title: message,
|
||||||
primaryButton: .init(title: L10n.actionOk, action: nil),
|
primaryButton: .init(title: L10n.actionOk, action: nil),
|
||||||
secondaryButton: .init(title: L10n.actionLearnMore) { [weak self] in
|
secondaryButton: userSettings.app.historySharingDetailsURL.map { url in
|
||||||
guard let self else { return }
|
.init(title: L10n.actionLearnMore) { [weak self] in self?.appMediator.open(url) }
|
||||||
appMediator.open(userSettings.app.historySharingDetailsURL)
|
|
||||||
})
|
})
|
||||||
case .inviteAgain:
|
case .inviteAgain:
|
||||||
state.bindings.alertInfo = .init(id: .inviteAgain,
|
state.bindings.alertInfo = .init(id: .inviteAgain,
|
||||||
|
|||||||
@@ -9,6 +9,27 @@ matches `## Changes in <version>` exactly.
|
|||||||
|
|
||||||
Upstream's `CHANGES.md` tracks element-hq's own releases and must not be edited here.
|
Upstream's `CHANGES.md` tracks element-hq's own releases and must not be edited here.
|
||||||
|
|
||||||
|
## Changes in 26.11.1 (2026-10-19)
|
||||||
|
|
||||||
|
🙌 Improvements
|
||||||
|
|
||||||
|
- Copyright in "About" now opens the HealthChat Impressum.
|
||||||
|
- The Labs entry with Element's experimental features is no longer shown.
|
||||||
|
- Analytics and crash reporting are switched off entirely: the "Analytics" settings and the
|
||||||
|
onboarding prompt are gone, and calls no longer send usage data to Element.
|
||||||
|
- "Report a problem" is hidden until HealthChat runs its own report server.
|
||||||
|
- The "Learn more" links on identity confirmation, device verification, chat backup, identity
|
||||||
|
changes and history sharing are gone; they led to Element's documentation.
|
||||||
|
- Location sharing is switched off: the attachment menu no longer offers it, and received
|
||||||
|
locations show a placeholder instead of a map that could not load.
|
||||||
|
|
||||||
|
⚠️ Known issues
|
||||||
|
|
||||||
|
- Incoming calls are labelled as video calls on the call screen; voice calls still connect
|
||||||
|
as voice calls.
|
||||||
|
- An "Incoming call" notification is shown next to the call screen, and a few notifications
|
||||||
|
may show generic text, while Apple reviews the notification filtering entitlement.
|
||||||
|
|
||||||
## Changes in 26.11.0 (2026-10-19)
|
## Changes in 26.11.0 (2026-10-19)
|
||||||
|
|
||||||
✨ Features
|
✨ Features
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import AnalyticsEvents
|
|||||||
import PostHog
|
import PostHog
|
||||||
import Testing
|
import Testing
|
||||||
|
|
||||||
|
@Suite(.enabled(if: Secrets.postHogHost != nil && Secrets.postHogAPIKey != nil, "HealthChat ships without a PostHog configuration."))
|
||||||
@MainActor
|
@MainActor
|
||||||
final class AnalyticsTests {
|
final class AnalyticsTests {
|
||||||
private let appSettings: AppSettings
|
private let appSettings: AppSettings
|
||||||
|
|||||||
@@ -104,9 +104,12 @@ final class BugReportServiceTests {
|
|||||||
@Test
|
@Test
|
||||||
@MainActor
|
@MainActor
|
||||||
func configurations() async throws {
|
func configurations() async throws {
|
||||||
guard case let .url(initialURL) = userSettings.app.bugReportRageshakeURL.publisher.value else {
|
// HealthChat ships without a rageshake server (Secrets.rageshakeURL is nil), so the default
|
||||||
Issue.record("Unexpected initial configuration.")
|
// configuration is `.disabled`; the transitions below are exercised from remote values instead.
|
||||||
return
|
let initialURL: URL? = if case let .url(url) = userSettings.app.bugReportRageshakeURL.publisher.value {
|
||||||
|
url
|
||||||
|
} else {
|
||||||
|
nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let service = BugReportService(rageshakeURLPublisher: userSettings.app.bugReportRageshakeURL.publisher,
|
let service = BugReportService(rageshakeURLPublisher: userSettings.app.bugReportRageshakeURL.publisher,
|
||||||
@@ -114,7 +117,7 @@ final class BugReportServiceTests {
|
|||||||
sdkGitSHA: "1234",
|
sdkGitSHA: "1234",
|
||||||
session: .mock,
|
session: .mock,
|
||||||
appHooks: AppHooks())
|
appHooks: AppHooks())
|
||||||
#expect(service.isEnabled)
|
#expect(service.isEnabled == (initialURL != nil))
|
||||||
|
|
||||||
userSettings.app.bugReportRageshakeURL.applyRemoteValue(.disabled)
|
userSettings.app.bugReportRageshakeURL.applyRemoteValue(.disabled)
|
||||||
#expect(!service.isEnabled)
|
#expect(!service.isEnabled)
|
||||||
@@ -137,6 +140,10 @@ final class BugReportServiceTests {
|
|||||||
#expect(customConfigurationResponse.reportURL == "https://bugs.server.net/123")
|
#expect(customConfigurationResponse.reportURL == "https://bugs.server.net/123")
|
||||||
|
|
||||||
userSettings.app.bugReportRageshakeURL.reset()
|
userSettings.app.bugReportRageshakeURL.reset()
|
||||||
|
guard let initialURL else {
|
||||||
|
#expect(!service.isEnabled)
|
||||||
|
return
|
||||||
|
}
|
||||||
#expect(service.isEnabled)
|
#expect(service.isEnabled)
|
||||||
|
|
||||||
let defaultConfigurationResponse = try await service.submitBugReport(bugReport, progressListener: progressSubject).get()
|
let defaultConfigurationResponse = try await service.submitBugReport(bugReport, progressListener: progressSubject).get()
|
||||||
|
|||||||
Reference in New Issue
Block a user