diff --git a/.agents/skills/syncing-upstream-healthchat/SKILL.md b/.agents/skills/syncing-upstream-healthchat/SKILL.md index 9628a527b..9be06d81a 100644 --- a/.agents/skills/syncing-upstream-healthchat/SKILL.md +++ b/.agents/skills/syncing-upstream-healthchat/SKILL.md @@ -43,7 +43,13 @@ Upstream files the fork edits — conflicts live here every time: migration contract"), `AppHooks/UserHooks.swift` (two HealthChat hook fields), `AppSettings.swift` (VoIP push properties for the NSE), `ElementCallService.swift` (PushKit 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 CI workflows, `.gitignore`, `project.pbxproj` (never resolve by hand — `xcodegen`). diff --git a/Components/Secrets/Secrets.swift b/Components/Secrets/Secrets.swift index d8b096c38..a8d04228c 100644 --- a/Components/Secrets/Secrets.swift +++ b/Components/Secrets/Secrets.swift @@ -1,9 +1,9 @@ nonisolated enum Secrets { - static let sentryDSN: String? = "https://username@sentry.localhost/project_id" - static let sentryRustDSN: String? = "https://username@sentry.localhost/project_id" - static let postHogHost: String? = "https://posthog.localhost" - static let postHogAPIKey: String? = "your_key" - static let rageshakeURL: String? = "https://rageshake.localhost/submit" + static let sentryDSN: String? = nil + static let sentryRustDSN: String? = nil + static let postHogHost: String? = nil + static let postHogAPIKey: String? = nil + static let rageshakeURL: String? = nil static let mapLibreAPIKey: String? = "your_key" } \ No newline at end of file diff --git a/ElementX/Sources/AppHooks/HealthChat/HealthChatAppSettingsHook.swift b/ElementX/Sources/AppHooks/HealthChat/HealthChatAppSettingsHook.swift index 0bcbcbd75..c2ac4d043 100644 --- a/ElementX/Sources/AppHooks/HealthChat/HealthChatAppSettingsHook.swift +++ b/ElementX/Sources/AppHooks/HealthChat/HealthChatAppSettingsHook.swift @@ -12,7 +12,8 @@ import Foundation /// - `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. /// - `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 { func configure(_ appSettings: AppSettings) -> AppSettings { appSettings.override(accountProviders: [.generic("healthchat.ch")], @@ -24,19 +25,25 @@ struct HealthChatAppSettingsHook: AppSettingsHookProtocol { oAuthClientURIPath: nil, websiteURL: "https://healthchat.ch", 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", privacyURL: "https://www.healthchat.ch/datenschutz", - encryptionURL: appSettings.encryptionURL, - deviceVerificationURL: appSettings.deviceVerificationURL, - chatBackupDetailsURL: appSettings.chatBackupDetailsURL, - identityPinningViolationDetailsURL: appSettings.identityPinningViolationDetailsURL, - historySharingDetailsURL: appSettings.historySharingDetailsURL, + encryptionURL: nil, + deviceVerificationURL: nil, + chatBackupDetailsURL: nil, + identityPinningViolationDetailsURL: nil, + historySharingDetailsURL: nil, elementWebHosts: ["app.healthchat.ch"], accountProvisioningHost: appSettings.accountProvisioningHost, - bugReportApplicationID: appSettings.bugReportApplicationID, - analyticsTermsURL: appSettings.analyticsTermsURL, - mapTilerConfiguration: AppSettings.bundledMapTilerConfiguration) + bugReportApplicationID: "healthchat-ios", + analyticsTermsURL: "https://www.healthchat.ch/datenschutz", + mapTilerConfiguration: Self.mapTilerConfiguration) 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) } diff --git a/ElementX/Sources/AppHooks/HealthChat/HealthChatSettingsScreenHook.swift b/ElementX/Sources/AppHooks/HealthChat/HealthChatSettingsScreenHook.swift index 89b4d7c57..3a5d8e12d 100644 --- a/ElementX/Sources/AppHooks/HealthChat/HealthChatSettingsScreenHook.swift +++ b/ElementX/Sources/AppHooks/HealthChat/HealthChatSettingsScreenHook.swift @@ -28,5 +28,10 @@ final class HealthChatSettingsScreenHook: SettingsScreenHookProtocol { func refreshAccountStatus() { statusLoader.load() } + + // Upstream's experiments are not supported by the HealthChat server setup and never reviewed for it. + nonisolated var showsLabs: Bool { + false + } } #endif diff --git a/ElementX/Sources/AppHooks/Hooks/SettingsScreenHook.swift b/ElementX/Sources/AppHooks/Hooks/SettingsScreenHook.swift index ea28d1d42..2bacee335 100644 --- a/ElementX/Sources/AppHooks/Hooks/SettingsScreenHook.swift +++ b/ElementX/Sources/AppHooks/Hooks/SettingsScreenHook.swift @@ -15,6 +15,9 @@ nonisolated protocol SettingsScreenHookProtocol: Sendable { /// Called whenever the settings screen appears, so the section reflects what the user just did. @MainActor func refreshAccountStatus() + + /// Whether the Labs entry (experimental upstream features) is offered at all. + var showsLabs: Bool { get } } struct DefaultSettingsScreenHook: SettingsScreenHookProtocol { @@ -23,4 +26,8 @@ struct DefaultSettingsScreenHook: SettingsScreenHookProtocol { } func refreshAccountStatus() { } + + var showsLabs: Bool { + true + } } diff --git a/ElementX/Sources/Application/Settings/AppSettings.swift b/ElementX/Sources/Application/Settings/AppSettings.swift index 271aff8d3..d49e1067c 100644 --- a/ElementX/Sources/Application/Settings/AppSettings.swift +++ b/ElementX/Sources/Application/Settings/AppSettings.swift @@ -65,11 +65,11 @@ final nonisolated class AppSettings: @unchecked Sendable { copyrightURL: URL, acceptableUseURL: URL, privacyURL: URL, - encryptionURL: URL, - deviceVerificationURL: URL, - chatBackupDetailsURL: URL, - identityPinningViolationDetailsURL: URL, - historySharingDetailsURL: URL, + encryptionURL: URL?, + deviceVerificationURL: URL?, + chatBackupDetailsURL: URL?, + identityPinningViolationDetailsURL: URL?, + historySharingDetailsURL: URL?, elementWebHosts: [String], accountProvisioningHost: String, bugReportApplicationID: String, @@ -145,15 +145,15 @@ final nonisolated class AppSettings: @unchecked Sendable { /// A URL that contains the app's Privacy Policy. private(set) var privacyURL: URL = "https://element.io/privacy" /// 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.. - 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. - 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 - 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 - 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. private(set) var elementWebHosts = ["app.element.io", "staging.element.io", "develop.element.io"] diff --git a/ElementX/Sources/Screens/Onboarding/IdentityConfirmationScreen/IdentityConfirmationScreenModels.swift b/ElementX/Sources/Screens/Onboarding/IdentityConfirmationScreen/IdentityConfirmationScreenModels.swift index 8d2ef073a..2586122ea 100644 --- a/ElementX/Sources/Screens/Onboarding/IdentityConfirmationScreen/IdentityConfirmationScreenModels.swift +++ b/ElementX/Sources/Screens/Onboarding/IdentityConfirmationScreen/IdentityConfirmationScreenModels.swift @@ -24,7 +24,7 @@ struct IdentityConfirmationScreenViewState: BindableState { } var availableActions: [AvailableActions]? - let learnMoreURL: URL + let learnMoreURL: URL? var bindings = IdentityConfirmationScreenBindings() } diff --git a/ElementX/Sources/Screens/Onboarding/IdentityConfirmationScreen/View/IdentityConfirmationScreen.swift b/ElementX/Sources/Screens/Onboarding/IdentityConfirmationScreen/View/IdentityConfirmationScreen.swift index 073669f5c..d8f524a3f 100644 --- a/ElementX/Sources/Screens/Onboarding/IdentityConfirmationScreen/View/IdentityConfirmationScreen.swift +++ b/ElementX/Sources/Screens/Onboarding/IdentityConfirmationScreen/View/IdentityConfirmationScreen.swift @@ -53,11 +53,13 @@ struct IdentityConfirmationScreen: View { .multilineTextAlignment(.center) .foregroundColor(.compound.textSecondary) - Button(L10n.actionLearnMore) { - UIApplication.shared.open(context.viewState.learnMoreURL) + if let learnMoreURL = 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) } } diff --git a/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenModels.swift b/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenModels.swift index c0e15f945..93fc31297 100644 --- a/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenModels.swift +++ b/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenModels.swift @@ -26,7 +26,7 @@ enum SessionVerificationScreenViewAction { struct SessionVerificationScreenViewState: BindableState { let flow: SessionVerificationScreenFlow - let learnMoreURL: URL + let learnMoreURL: URL? var verificationState: SessionVerificationScreenStateMachine.State diff --git a/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/View/SessionVerificationScreen.swift b/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/View/SessionVerificationScreen.swift index 3bbdfa6f3..5b0295a37 100644 --- a/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/View/SessionVerificationScreen.swift +++ b/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/View/SessionVerificationScreen.swift @@ -92,10 +92,12 @@ struct SessionVerificationScreen: View { isUserVerification: true, mediaProvider: context.mediaProvider) case .userInitiator: - Button(L10n.actionLearnMore) { - UIApplication.shared.open(context.viewState.learnMoreURL) + if let learnMoreURL = context.viewState.learnMoreURL { + Button(L10n.actionLearnMore) { + UIApplication.shared.open(learnMoreURL) + } + .buttonStyle(.compound(.tertiary, size: .small)) } - .buttonStyle(.compound(.tertiary, size: .small)) default: EmptyView() } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 306ef7b81..821096c6e 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -118,8 +118,8 @@ enum RoomScreenFooterViewAction { } enum RoomScreenFooterViewDetails { - case pinViolation(member: RoomMemberProxyProtocol, learnMoreURL: URL) - case verificationViolation(member: RoomMemberProxyProtocol, learnMoreURL: URL) + case pinViolation(member: RoomMemberProxyProtocol, learnMoreURL: URL?) + case verificationViolation(member: RoomMemberProxyProtocol, learnMoreURL: URL?) } enum PinnedEventsBannerState: Equatable { diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreenFooterView.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreenFooterView.swift index e71267722..5c6eb4820 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreenFooterView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreenFooterView.swift @@ -56,7 +56,7 @@ struct RoomScreenFooterView: View { } private func pinViolation(member: RoomMemberProxyProtocol, - learnMoreURL: URL) -> some View { + learnMoreURL: URL?) -> some View { VStack(spacing: 16) { HStack(spacing: 16) { LoadableAvatarImage(url: member.avatarURL, @@ -86,7 +86,7 @@ struct RoomScreenFooterView: View { } private func verificationViolation(member: RoomMemberProxyProtocol, - learnMoreURL: URL) -> some View { + learnMoreURL: URL?) -> some View { VStack(spacing: 16) { HStack(spacing: 16) { LoadableAvatarImage(url: member.avatarURL, @@ -115,7 +115,7 @@ struct RoomScreenFooterView: View { .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 linkPlaceholder = "{link}" let displayName = displayName ?? fallbackDisplayName(userID) @@ -125,14 +125,14 @@ struct RoomScreenFooterView: View { userIDString.bold() description.replace(userIDPlaceholder, with: userIDString) - var linkString = AttributedString(L10n.actionLearnMore) + var linkString = AttributedString(url == nil ? "" : L10n.actionLearnMore) linkString.link = url linkString.bold() description.replace(linkPlaceholder, with: linkString) 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 linkPlaceholder = "{link}" let displayName = displayName ?? fallbackDisplayName(userID) @@ -142,7 +142,7 @@ struct RoomScreenFooterView: View { userIDString.bold() description.replace(userIDPlaceholder, with: userIDString) - var linkString = AttributedString(L10n.actionLearnMore) + var linkString = AttributedString(url == nil ? "" : L10n.actionLearnMore) linkString.link = url linkString.bold() description.replace(linkPlaceholder, with: linkString) diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenModels.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenModels.swift index 030a8df03..1aae191f9 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenModels.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenModels.swift @@ -14,7 +14,7 @@ enum SecureBackupScreenViewModelAction { } struct SecureBackupScreenViewState: BindableState { - let chatBackupDetailsURL: URL + let chatBackupDetailsURL: URL? var recoveryState = SecureBackupRecoveryState.unknown var keyBackupState = SecureBackupKeyBackupState.unknown var bindings: SecureBackupScreenViewStateBindings diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModel.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModel.swift index 6baba5dd9..0c34e27fd 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModel.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModel.swift @@ -22,7 +22,7 @@ class SecureBackupScreenViewModel: SecureBackupScreenViewModelType, SecureBackup init(secureBackupController: SecureBackupControllerProtocol, userIndicatorController: UserIndicatorControllerProtocol, - chatBackupDetailsURL: URL) { + chatBackupDetailsURL: URL?) { self.secureBackupController = secureBackupController self.userIndicatorController = userIndicatorController diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/View/SecureBackupScreen.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/View/SecureBackupScreen.swift index 190bba8d0..d0635b39d 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/View/SecureBackupScreen.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/View/SecureBackupScreen.swift @@ -64,10 +64,15 @@ struct SecureBackupScreen: View { } 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}" var description = AttributedString(L10n.screenChatBackupKeyBackupDescription(linkPlaceholder)) var linkString = AttributedString(L10n.actionLearnMore) - linkString.link = context.viewState.chatBackupDetailsURL + linkString.link = chatBackupDetailsURL linkString.bold() description.replace(linkPlaceholder, with: linkString) return description diff --git a/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenModels.swift b/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenModels.swift index 8af1012cb..ac76e54c8 100644 --- a/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenModels.swift +++ b/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenModels.swift @@ -162,7 +162,7 @@ struct SecurityAndPrivacyScreenViewState: BindableState { historyVisibility: SecurityAndPrivacyHistoryVisibility, isSpace: Bool, isKnockingEnabled: Bool, - historySharingDetailsURL: URL) { + historySharingDetailsURL: URL?) { self.isKnockingEnabled = isKnockingEnabled self.isSpace = isSpace @@ -269,7 +269,7 @@ struct SecurityAndPrivacyScreenStrings { let accessSectionFooterString: AttributedString let historySectionFooterString: AttributedString - init(historySharingDetailsURL: URL) { + init(historySharingDetailsURL: URL?) { let linkPlaceholder = "{link}" var accessFooterString = AttributedString(L10n.screenSecurityAndPrivacyRoomAccessFooter(linkPlaceholder)) @@ -280,7 +280,7 @@ struct SecurityAndPrivacyScreenStrings { accessSectionFooterString = accessFooterString var historyFooterString = AttributedString(L10n.screenSecurityAndPrivacyRoomHistorySectionFooter(linkPlaceholder)) - var historyLinkString = AttributedString(L10n.actionLearnMore) + var historyLinkString = AttributedString(historySharingDetailsURL == nil ? "" : L10n.actionLearnMore) historyLinkString.link = historySharingDetailsURL historyLinkString.bold() historyFooterString.replace(linkPlaceholder, with: historyLinkString) diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift index c162f4bc8..f70999122 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift @@ -211,11 +211,13 @@ struct SettingsScreen: View { .accessibilityIdentifier(A11yIdentifiers.settingsScreen.analytics) } - ListRow(label: .default(title: L10n.screenAdvancedSettingsLabs, - icon: \.labs), - kind: .navigationLink { - context.send(viewAction: .labs) - }) + if context.viewState.settingsScreenHook.showsLabs { + ListRow(label: .default(title: L10n.screenAdvancedSettingsLabs, + icon: \.labs), + kind: .navigationLink { + context.send(viewAction: .labs) + }) + } ListRow(label: .default(title: L10n.commonAbout, icon: \.info), diff --git a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift index 0ced31ea2..2e3822b92 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -1143,9 +1143,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { state.bindings.alertInfo = .init(id: type, title: message, primaryButton: .init(title: L10n.actionOk, action: nil), - secondaryButton: .init(title: L10n.actionLearnMore) { [weak self] in - guard let self else { return } - appMediator.open(userSettings.app.historySharingDetailsURL) + secondaryButton: userSettings.app.historySharingDetailsURL.map { url in + .init(title: L10n.actionLearnMore) { [weak self] in self?.appMediator.open(url) } }) case .inviteAgain: state.bindings.alertInfo = .init(id: .inviteAgain, diff --git a/HEALTHCHAT_CHANGES.md b/HEALTHCHAT_CHANGES.md index dcd65a4cf..3686825e9 100644 --- a/HEALTHCHAT_CHANGES.md +++ b/HEALTHCHAT_CHANGES.md @@ -9,6 +9,27 @@ matches `## Changes in ` exactly. 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) ✨ Features diff --git a/UnitTests/Sources/AnalyticsTests.swift b/UnitTests/Sources/AnalyticsTests.swift index c0e17601f..c1c06746f 100644 --- a/UnitTests/Sources/AnalyticsTests.swift +++ b/UnitTests/Sources/AnalyticsTests.swift @@ -11,6 +11,7 @@ import AnalyticsEvents import PostHog import Testing +@Suite(.enabled(if: Secrets.postHogHost != nil && Secrets.postHogAPIKey != nil, "HealthChat ships without a PostHog configuration.")) @MainActor final class AnalyticsTests { private let appSettings: AppSettings diff --git a/UnitTests/Sources/BugReportServiceTests.swift b/UnitTests/Sources/BugReportServiceTests.swift index 2d9c30820..58ef68b15 100644 --- a/UnitTests/Sources/BugReportServiceTests.swift +++ b/UnitTests/Sources/BugReportServiceTests.swift @@ -104,9 +104,12 @@ final class BugReportServiceTests { @Test @MainActor func configurations() async throws { - guard case let .url(initialURL) = userSettings.app.bugReportRageshakeURL.publisher.value else { - Issue.record("Unexpected initial configuration.") - return + // HealthChat ships without a rageshake server (Secrets.rageshakeURL is nil), so the default + // configuration is `.disabled`; the transitions below are exercised from remote values instead. + let initialURL: URL? = if case let .url(url) = userSettings.app.bugReportRageshakeURL.publisher.value { + url + } else { + nil } let service = BugReportService(rageshakeURLPublisher: userSettings.app.bugReportRageshakeURL.publisher, @@ -114,7 +117,7 @@ final class BugReportServiceTests { sdkGitSHA: "1234", session: .mock, appHooks: AppHooks()) - #expect(service.isEnabled) + #expect(service.isEnabled == (initialURL != nil)) userSettings.app.bugReportRageshakeURL.applyRemoteValue(.disabled) #expect(!service.isEnabled) @@ -137,6 +140,10 @@ final class BugReportServiceTests { #expect(customConfigurationResponse.reportURL == "https://bugs.server.net/123") userSettings.app.bugReportRageshakeURL.reset() + guard let initialURL else { + #expect(!service.isEnabled) + return + } #expect(service.isEnabled) let defaultConfigurationResponse = try await service.submitBugReport(bugReport, progressListener: progressSubject).get()