diff --git a/ElementX/Sources/Application/Settings/AppSettings.swift b/ElementX/Sources/Application/Settings/AppSettings.swift index 0125b2f48..55c016b58 100644 --- a/ElementX/Sources/Application/Settings/AppSettings.swift +++ b/ElementX/Sources/Application/Settings/AppSettings.swift @@ -32,7 +32,6 @@ final class AppSettings { case seenInvites case hasSeenSpacesAnnouncement case hasSeenNewSoundBanner - case acknowledgedHistoryVisibleRooms case appLockNumberOfPINAttempts case appLockNumberOfBiometricAttempts case timelineStyle @@ -178,10 +177,6 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.hasSeenNewSoundBanner, defaultValue: true, storageType: .userDefaults(store)) var hasSeenNewSoundBanner - /// The Set of room identifiers that the user has acknowledged have visible history. - @UserPreference(key: UserDefaultsKeys.acknowledgedHistoryVisibleRooms, defaultValue: [], storageType: .userDefaults(store)) - var acknowledgedHistoryVisibleRooms: Set - /// The initial set of account providers shown to the user in the authentication flow. /// /// Account provider is the friendly term for the server name. It should not contain an `https` prefix and should @@ -215,6 +210,7 @@ final class AppSettings { 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. diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index a970868f4..6f70de9a2 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -80,18 +80,7 @@ struct RoomScreenViewState: BindableState { (canAcceptKnocks || canDeclineKnocks || canBan) } - var identityViolationDetails: RoomScreenFooterViewDetails? - var historyVisibleDetails: RoomScreenFooterViewDetails? - - var footerDetails: RoomScreenFooterViewDetails? { - if let identityViolationDetails { - return identityViolationDetails - } - guard canSendMessage else { - return nil - } - return historyVisibleDetails - } + var footerDetails: RoomScreenFooterViewDetails? var bindings = RoomScreenViewStateBindings() } @@ -104,13 +93,11 @@ struct RoomScreenViewStateBindings { enum RoomScreenFooterViewAction { case resolvePinViolation(userID: String) case resolveVerificationViolation(userID: String) - case dismissHistoryVisibleAlert } enum RoomScreenFooterViewDetails { case pinViolation(member: RoomMemberProxyProtocol, learnMoreURL: URL) case verificationViolation(member: RoomMemberProxyProtocol, learnMoreURL: URL) - case historyVisible(learnMoreURL: URL) } enum PinnedEventsBannerState: Equatable { diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 524da2f2b..92ad22650 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -101,9 +101,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol Task { await resolveIdentityPinningViolation(userID) } case .resolveVerificationViolation(let userID): Task { await resolveIdentityVerificationViolation(userID) } - case .dismissHistoryVisibleAlert: - appSettings.acknowledgedHistoryVisibleRooms.insert(roomProxy.id) - state.historyVisibleDetails = nil } case .acceptKnock(let eventID): Task { await acceptKnock(eventID: eventID) } @@ -247,13 +244,13 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } if let member = identityVerificationViolations.values.first { - state.identityViolationDetails = .verificationViolation(member: member, - learnMoreURL: appSettings.identityPinningViolationDetailsURL) + state.footerDetails = .verificationViolation(member: member, + learnMoreURL: appSettings.identityPinningViolationDetailsURL) } else if let member = identityPinningViolations.values.first { - state.identityViolationDetails = .pinViolation(member: member, - learnMoreURL: appSettings.identityPinningViolationDetailsURL) + state.footerDetails = .pinViolation(member: member, + learnMoreURL: appSettings.identityPinningViolationDetailsURL) } else { - state.identityViolationDetails = nil + state.footerDetails = nil } } @@ -345,20 +342,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol state.canDeclineKnocks = powerLevels.canOwnUserKick() state.canBan = powerLevels.canOwnUserBan() } - - let isHistoryVisible = roomInfo.historyVisibility == .shared || roomInfo.historyVisibility == .worldReadable - let isHistoryVisibleBannerAcknowledged = appSettings.acknowledgedHistoryVisibleRooms.contains(roomInfo.id) - - if appSettings.enableKeyShareOnInvite, roomInfo.isEncrypted { - if isHistoryVisible, !isHistoryVisibleBannerAcknowledged { - // Whenever the user opens an encrypted room with shared/world-readable history visbility, we show them a warning banner if they have not already dismissed it. - state.historyVisibleDetails = .historyVisible(learnMoreURL: appSettings.historySharingDetailsURL) - } else if !isHistoryVisible, isHistoryVisibleBannerAcknowledged { - // Whenever the user opens a room with non-shared history visibility, we clear the dismiss flag to ensure that the banner is displayed again if the history is made visible in the future. - appSettings.acknowledgedHistoryVisibleRooms.remove(roomInfo.id) - state.historyVisibleDetails = nil - } - } } private func setupPinnedEventsTimelineItemProviderIfNeeded() { diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreenFooterView.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreenFooterView.swift index 1702a7d64..b9574fed7 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreenFooterView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreenFooterView.swift @@ -16,7 +16,7 @@ struct RoomScreenFooterView: View { private var borderColor: Color { switch details { - case .pinViolation, .historyVisible: + case .pinViolation: .compound.borderInfoSubtle case .verificationViolation: .compound.borderCriticalSubtle @@ -27,7 +27,7 @@ struct RoomScreenFooterView: View { private var gradient: Gradient { switch details { - case .pinViolation, .historyVisible: + case .pinViolation: .compound.info case .verificationViolation: Gradient(colors: [.compound.bgCriticalSubtle, .clear]) @@ -54,8 +54,6 @@ struct RoomScreenFooterView: View { pinViolation(member: member, learnMoreURL: learnMoreURL) case .verificationViolation(member: let member, learnMoreURL: let learnMoreURL): verificationViolation(member: member, learnMoreURL: learnMoreURL) - case .historyVisible(learnMoreURL: let learnMoreURL): - historyVisibleAlert(learnMoreURL: learnMoreURL) } } @@ -153,43 +151,10 @@ struct RoomScreenFooterView: View { return description } - private func historyVisibleAlertDescriptionWithLearnMoreLink(learnMoreURL: URL) -> AttributedString { - let linkPlaceholder = "{link}" - var description = AttributedString(L10n.cryptoHistoryVisible(linkPlaceholder)) - var linkString = AttributedString(L10n.actionLearnMore) - linkString.link = learnMoreURL - linkString.bold() - description.replace(linkPlaceholder, with: linkString) - return description - } - private func fallbackDisplayName(_ userID: String) -> String { guard let localpart = userID.components(separatedBy: ":").first else { return userID } return String(localpart.trimmingPrefix("@")) } - - private func historyVisibleAlert(learnMoreURL: URL) -> some View { - let description = historyVisibleAlertDescriptionWithLearnMoreLink(learnMoreURL: learnMoreURL) - - return VStack(spacing: 16) { - HStack(spacing: 16) { - CompoundIcon(\.info).foregroundColor(.compound.iconInfoPrimary) - Text(description) - .font(.compound.bodyMD) - .foregroundColor(.compound.textInfoPrimary) - } - Button { - callback(.dismissHistoryVisibleAlert) - } label: { - Text(L10n.actionDismiss) - .frame(maxWidth: .infinity) - } - .buttonStyle(.compound(.primary, size: .medium)) - } - .padding(.top, 16) - .padding(.horizontal, 16) - .padding(.bottom, 8) - } } struct RoomScreenFooterView_Previews: PreviewProvider, TestablePreview { @@ -201,8 +166,6 @@ struct RoomScreenFooterView_Previews: PreviewProvider, TestablePreview { static let verificationViolationDetails: RoomScreenFooterViewDetails = .verificationViolation(member: RoomMemberProxyMock.mockBob, learnMoreURL: "https://element.io/") - static let historyVisibleDetails: RoomScreenFooterViewDetails = .historyVisible(learnMoreURL: "https://element.io") - static var previews: some View { RoomScreenFooterView(details: bobDetails, mediaProvider: MediaProviderMock(configuration: .init())) { _ in } .previewDisplayName("With displayname") @@ -210,7 +173,5 @@ struct RoomScreenFooterView_Previews: PreviewProvider, TestablePreview { .previewDisplayName("Without displayname") RoomScreenFooterView(details: verificationViolationDetails, mediaProvider: MediaProviderMock(configuration: .init())) { _ in } .previewDisplayName("Verification Violation") - RoomScreenFooterView(details: historyVisibleDetails, mediaProvider: MediaProviderMock(configuration: .init())) { _ in } - .previewDisplayName("History Visible") } } diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomScreenFooterView.History-Visible-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomScreenFooterView.History-Visible-iPad-en-GB.png deleted file mode 100644 index d14e45ee9..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomScreenFooterView.History-Visible-iPad-en-GB.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:79e30c512ac42d208dc253b969812b6d3f2d85be8676befde4dc67d8287aeebc -size 150023 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomScreenFooterView.History-Visible-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomScreenFooterView.History-Visible-iPad-pseudo.png deleted file mode 100644 index 2fcc2645c..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomScreenFooterView.History-Visible-iPad-pseudo.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ce717389949a6c0d853d27fdecbfc09b80dc239eae801c89ea1b0ba338bf6a61 -size 176311 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomScreenFooterView.History-Visible-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomScreenFooterView.History-Visible-iPhone-en-GB.png deleted file mode 100644 index 834708271..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomScreenFooterView.History-Visible-iPhone-en-GB.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ef89b3e0c89f13371380a6f38e350c1b885c79a7c07b6fd215d7c56dbb71f6ef -size 78568 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomScreenFooterView.History-Visible-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomScreenFooterView.History-Visible-iPhone-pseudo.png deleted file mode 100644 index 15e3042c1..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomScreenFooterView.History-Visible-iPhone-pseudo.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:09b99546030959ee3a87cda4dfda37b43bc537f7eb22725c4057c68f4a0d8ab9 -size 109019 diff --git a/UnitTests/Sources/RoomScreenViewModelTests.swift b/UnitTests/Sources/RoomScreenViewModelTests.swift index fe500e3b2..b798b4041 100644 --- a/UnitTests/Sources/RoomScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomScreenViewModelTests.swift @@ -439,245 +439,4 @@ class RoomScreenViewModelTests: XCTestCase { } try await deferred.fulfill() } - - // MARK: History Sharing - - func testHistoryVisibleBannerDoesNotAppearIfFeatureDisabled() async throws { - ServiceLocator.shared.settings.enableKeyShareOnInvite = false - ServiceLocator.shared.settings.acknowledgedHistoryVisibleRooms = Set() - - let configuration = JoinedRoomProxyMockConfiguration(isEncrypted: true) - let roomProxyMock = JoinedRoomProxyMock(configuration) - - let roomInfoProxyMock = RoomInfoProxyMock(configuration) - roomInfoProxyMock.historyVisibility = .shared - - let infoSubject = CurrentValueSubject(roomInfoProxyMock) - roomProxyMock.underlyingInfoPublisher = infoSubject.asCurrentValuePublisher() - - let viewModel = RoomScreenViewModel(userSession: UserSessionMock(.init()), - roomProxy: roomProxyMock, - initialSelectedPinnedEventID: nil, - ongoingCallRoomIDPublisher: .init(.init(nil)), - appSettings: ServiceLocator.shared.settings, - appHooks: AppHooks(), - analyticsService: ServiceLocator.shared.analytics, - userIndicatorController: ServiceLocator.shared.userIndicatorController) - - self.viewModel = viewModel - - let deferred = deferFailure(viewModel.context.$viewState, timeout: 1) { $0.footerDetails != nil } - try await deferred.fulfill() - } - - func testHistoryVisibleBannerDoesNotAppearIfNotEncrypted() async throws { - ServiceLocator.shared.settings.enableKeyShareOnInvite = true - ServiceLocator.shared.settings.acknowledgedHistoryVisibleRooms = Set() - - let roomProxyMock = JoinedRoomProxyMock(.init(isEncrypted: false)) - - let viewModel = RoomScreenViewModel(userSession: UserSessionMock(.init()), - roomProxy: roomProxyMock, - initialSelectedPinnedEventID: nil, - ongoingCallRoomIDPublisher: .init(.init(nil)), - appSettings: ServiceLocator.shared.settings, - appHooks: AppHooks(), - analyticsService: ServiceLocator.shared.analytics, - userIndicatorController: ServiceLocator.shared.userIndicatorController) - - self.viewModel = viewModel - - let deferred = deferFailure(viewModel.context.$viewState, timeout: 1) { $0.footerDetails != nil } - try await deferred.fulfill() - } - - func testHistoryVisibleBannerDoesNotAppearIfJoinedOrInvited() async throws { - ServiceLocator.shared.settings.enableKeyShareOnInvite = true - ServiceLocator.shared.settings.acknowledgedHistoryVisibleRooms = Set() - - let configuration = JoinedRoomProxyMockConfiguration(isEncrypted: true) - let roomProxyMock = JoinedRoomProxyMock(configuration) - - let roomInfoProxyMock = RoomInfoProxyMock(configuration) - roomInfoProxyMock.historyVisibility = .joined - - let infoSubject = CurrentValueSubject(roomInfoProxyMock) - roomProxyMock.underlyingInfoPublisher = infoSubject.asCurrentValuePublisher() - - let viewModel = RoomScreenViewModel(userSession: UserSessionMock(.init()), - roomProxy: roomProxyMock, - initialSelectedPinnedEventID: nil, - ongoingCallRoomIDPublisher: .init(.init(nil)), - appSettings: ServiceLocator.shared.settings, - appHooks: AppHooks(), - analyticsService: ServiceLocator.shared.analytics, - userIndicatorController: ServiceLocator.shared.userIndicatorController) - - self.viewModel = viewModel - - var deferred = deferFailure(viewModel.context.$viewState, timeout: 1) { $0.footerDetails != nil } - try await deferred.fulfill() - - // Update visibility to invited - roomInfoProxyMock.historyVisibility = .invited - infoSubject.send(roomInfoProxyMock) - - deferred = deferFailure(viewModel.context.$viewState, timeout: 1) { $0.footerDetails != nil } - try await deferred.fulfill() - } - - func testHistoryVisibleBannerDoesNotAppearIfAcknowledged() async throws { - ServiceLocator.shared.settings.enableKeyShareOnInvite = true - ServiceLocator.shared.settings.acknowledgedHistoryVisibleRooms = Set() - ServiceLocator.shared.settings.acknowledgedHistoryVisibleRooms.insert("$room:example.com") - - let configuration = JoinedRoomProxyMockConfiguration(id: "$room:example.com", isEncrypted: true) - let roomProxyMock = JoinedRoomProxyMock(configuration) - - let roomInfoProxyMock = RoomInfoProxyMock(configuration) - roomInfoProxyMock.historyVisibility = .shared - - let infoSubject = CurrentValueSubject(roomInfoProxyMock) - roomProxyMock.underlyingInfoPublisher = infoSubject.asCurrentValuePublisher() - - let viewModel = RoomScreenViewModel(userSession: UserSessionMock(.init()), - roomProxy: roomProxyMock, - initialSelectedPinnedEventID: nil, - ongoingCallRoomIDPublisher: .init(.init(nil)), - appSettings: ServiceLocator.shared.settings, - appHooks: AppHooks(), - analyticsService: ServiceLocator.shared.analytics, - userIndicatorController: ServiceLocator.shared.userIndicatorController) - - self.viewModel = viewModel - - let deferred = deferFailure(viewModel.context.$viewState, timeout: 1) { $0.footerDetails != nil } - try await deferred.fulfill() - } - - func testHistoryVisibleBannerDoesNotAppearIfCannotSendMessages() async throws { - ServiceLocator.shared.settings.enableKeyShareOnInvite = true - ServiceLocator.shared.settings.acknowledgedHistoryVisibleRooms = Set() - - let powerlevels = RoomPowerLevelsProxyMockConfiguration( - canUserSendMessage: false - ) - - let configuration = JoinedRoomProxyMockConfiguration(id: "$room:example.com", isEncrypted: true, powerLevelsConfiguration: powerlevels) - let roomProxyMock = JoinedRoomProxyMock(configuration) - - let roomInfoProxyMock = RoomInfoProxyMock(configuration) - roomInfoProxyMock.historyVisibility = .shared - - let infoSubject = CurrentValueSubject(roomInfoProxyMock) - roomProxyMock.underlyingInfoPublisher = infoSubject.asCurrentValuePublisher() - - let viewModel = RoomScreenViewModel(userSession: UserSessionMock(.init()), - roomProxy: roomProxyMock, - initialSelectedPinnedEventID: nil, - ongoingCallRoomIDPublisher: .init(.init(nil)), - appSettings: ServiceLocator.shared.settings, - appHooks: AppHooks(), - analyticsService: ServiceLocator.shared.analytics, - userIndicatorController: ServiceLocator.shared.userIndicatorController) - - self.viewModel = viewModel - - let deferred = deferFailure(viewModel.context.$viewState, timeout: 1) { $0.footerDetails != nil } - try await deferred.fulfill() - } - - func testHistoryVisibleBannerAppearsThenDisappearsOnAcknowledge() async throws { - ServiceLocator.shared.settings.enableKeyShareOnInvite = true - ServiceLocator.shared.settings.acknowledgedHistoryVisibleRooms = Set() - - let configuration = JoinedRoomProxyMockConfiguration(id: "$room:example.com", isEncrypted: true) - let roomProxyMock = JoinedRoomProxyMock(configuration) - - let roomInfoProxyMock = RoomInfoProxyMock(configuration) - roomInfoProxyMock.historyVisibility = .shared - - let infoSubject = CurrentValueSubject(roomInfoProxyMock) - roomProxyMock.underlyingInfoPublisher = infoSubject.asCurrentValuePublisher() - - let viewModel = RoomScreenViewModel(userSession: UserSessionMock(.init()), - roomProxy: roomProxyMock, - initialSelectedPinnedEventID: nil, - ongoingCallRoomIDPublisher: .init(.init(nil)), - appSettings: ServiceLocator.shared.settings, - appHooks: AppHooks(), - analyticsService: ServiceLocator.shared.analytics, - userIndicatorController: ServiceLocator.shared.userIndicatorController) - - self.viewModel = viewModel - - var deferred = deferFulfillment(viewModel.context.$viewState) { state in - state.footerDetails != nil - } - try await deferred.fulfill() - - deferred = deferFulfillment(viewModel.context.$viewState) { state in - state.footerDetails == nil - } - - ServiceLocator.shared.settings.acknowledgedHistoryVisibleRooms.insert("$room:example.com") - viewModel.context.send(viewAction: .footerViewAction(RoomScreenFooterViewAction.dismissHistoryVisibleAlert)) - - try await deferred.fulfill() - } - - func testHistoryVisibleBannerAppearsFullFlow() async throws { - ServiceLocator.shared.settings.enableKeyShareOnInvite = false - ServiceLocator.shared.settings.acknowledgedHistoryVisibleRooms = Set() - - let configuration = JoinedRoomProxyMockConfiguration(id: "$room:example.com", isEncrypted: true) - let roomProxyMock = JoinedRoomProxyMock(configuration) - - let roomInfoProxyMock = RoomInfoProxyMock(configuration) - roomInfoProxyMock.historyVisibility = .joined - - let infoSubject = CurrentValueSubject(roomInfoProxyMock) - roomProxyMock.underlyingInfoPublisher = infoSubject.asCurrentValuePublisher() - - var viewModel = RoomScreenViewModel(userSession: UserSessionMock(.init()), - roomProxy: roomProxyMock, - initialSelectedPinnedEventID: nil, - ongoingCallRoomIDPublisher: .init(.init(nil)), - appSettings: ServiceLocator.shared.settings, - appHooks: AppHooks(), - analyticsService: ServiceLocator.shared.analytics, - userIndicatorController: ServiceLocator.shared.userIndicatorController) - - self.viewModel = viewModel - - // When the history is not shared, the banner should not be visible. - var deferred = deferFulfillment(viewModel.context.$viewState) { state in - state.footerDetails == nil - } - try await deferred.fulfill() - - roomInfoProxyMock.historyVisibility = .shared - infoSubject.send(roomInfoProxyMock) - - // When the feature is off, the banner should not be visible. - deferred = deferFulfillment(viewModel.context.$viewState) { state in - state.footerDetails == nil - } - try await deferred.fulfill() - - // When the history is shared, and the feature is on, the banner should be visible. - ServiceLocator.shared.settings.enableKeyShareOnInvite = true - viewModel = RoomScreenViewModel(userSession: UserSessionMock(.init()), - roomProxy: roomProxyMock, - initialSelectedPinnedEventID: nil, - ongoingCallRoomIDPublisher: .init(.init(nil)), - appSettings: ServiceLocator.shared.settings, - appHooks: AppHooks(), - analyticsService: ServiceLocator.shared.analytics, - userIndicatorController: ServiceLocator.shared.userIndicatorController) - deferred = deferFulfillment(viewModel.context.$viewState) { state in - state.footerDetails != nil - } - try await deferred.fulfill() - } }