Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@
"soft_logout_clear_data_submit" = "Clear all data";
"soft_logout_clear_data_dialog_title" = "Clear data";
"soft_logout_clear_data_dialog_content" = "Clear all data currently stored on this device?\nSign in again to access your account data and messages.";

// MARK: - Shared history visibility

"crypto_history_visible" = "Messages you send will be shared with new members invited to this room. %1$s";
6 changes: 6 additions & 0 deletions ElementX/Sources/Application/Settings/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@

case elementCallBaseURLOverride

case acknowledgedHistoryVisibleRooms
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super minor nit: Could we put this under hasSeenNewSoundBanner so its position in the keys kind of reflects its position in the settings.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! I did not see these, I think it would be nice to rename this to hasSeenHistoryVisibleBannerRooms. Addressed in c59a88d.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually we would prefix with is/has/can when the value is a Bool, so similarly to seenInvites I think this should drop the has. If I'm honest, I think that acknowledgedHistoryVisibleRooms was probably the clearest name for this anyway 🙃

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eeep, I shall revert!


// Feature flags
case publicSearchEnabled
case fuzzyRoomListSearchEnabled
Expand Down Expand Up @@ -110,7 +112,7 @@
// MARK: - Hooks

// swiftlint:disable:next function_parameter_count
func override(accountProviders: [String],

Check warning on line 115 in ElementX/Sources/Application/Settings/AppSettings.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Function has 19 parameters, which is greater than the 7 authorized.

See more on https://sonarcloud.io/project/issues?id=element-x-ios&issues=AZr-aRlEQ-1SnDX_W8aI&open=AZr-aRlEQ-1SnDX_W8aI&pullRequest=4738
allowOtherAccountProviders: Bool,
hideBrandChrome: Bool,
pushGatewayBaseURL: URL,
Expand Down Expand Up @@ -170,6 +172,10 @@
@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<String>

/// 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
Expand Down
4 changes: 4 additions & 0 deletions ElementX/Sources/Generated/Strings+Untranslated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import Foundation
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
internal enum UntranslatedL10n {
/// Messages you send will be shared with new members invited to this room. %1$s
internal static func cryptoHistoryVisible(_ p1: UnsafePointer<CChar>) -> String {
return UntranslatedL10n.tr("Untranslated", "crypto_history_visible", p1)
}
/// Clear all data currently stored on this device?
/// Sign in again to access your account data and messages.
internal static var softLogoutClearDataDialogContent: String { return UntranslatedL10n.tr("Untranslated", "soft_logout_clear_data_dialog_content") }
Expand Down
2 changes: 2 additions & 0 deletions ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,13 @@ 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 {
Expand Down
10 changes: 10 additions & 0 deletions ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
Task { await resolveIdentityPinningViolation(userID) }
case .resolveVerificationViolation(let userID):
Task { await resolveIdentityVerificationViolation(userID) }
case .dismissHistoryVisibleAlert:
appSettings.acknowledgedHistoryVisibleRooms.insert(roomProxy.id)
state.footerDetails = nil
}
case .acceptKnock(let eventID):
Task { await acceptKnock(eventID: eventID) }
Expand Down Expand Up @@ -342,6 +345,13 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
state.canDeclineKnocks = powerLevels.canOwnUserKick()
state.canBan = powerLevels.canOwnUserBan()
}

if roomInfo.historyVisibility == RoomHistoryVisibility.joined {
appSettings.acknowledgedHistoryVisibleRooms.remove(roomInfo.id)
state.footerDetails = nil
} else if roomInfo.isEncrypted, !appSettings.acknowledgedHistoryVisibleRooms.contains(roomInfo.id) {
state.footerDetails = .historyVisible(learnMoreURL: "https://element.io")
}
}

private func setupPinnedEventsTimelineItemProviderIfNeeded() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
.compound.borderInfoSubtle
case .verificationViolation:
.compound.borderCriticalSubtle
case .historyVisible:
.compound.borderInfoSubtle
case .none:
Color.compound.bgCanvasDefault
}
Expand All @@ -31,6 +33,8 @@
.compound.info
case .verificationViolation:
Gradient(colors: [.compound.bgCriticalSubtle, .clear])
case .historyVisible:
Gradient(colors: [.compound.bgInfoSubtle, .clear])
case .none:
Gradient(colors: [.clear])
}
Expand All @@ -54,6 +58,8 @@
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)
}
}

Expand Down Expand Up @@ -119,7 +125,7 @@

private func pinViolationDescriptionWithLearnMoreLink(displayName: String?, userID: String, url: URL) -> AttributedString {
let userIDPlaceholder = "{mxid}"
let linkPlaceholder = "{link}"

Check failure on line 128 in ElementX/Sources/Screens/RoomScreen/View/RoomScreenFooterView.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal 3 times.

See more on https://sonarcloud.io/project/issues?id=element-x-ios&issues=AZr-aRiCQ-1SnDX_W8aH&open=AZr-aRiCQ-1SnDX_W8aH&pullRequest=4738
let displayName = displayName ?? fallbackDisplayName(userID)
var description = AttributedString(L10n.cryptoIdentityChangePinViolationNew(displayName, userIDPlaceholder, linkPlaceholder))

Expand Down Expand Up @@ -155,6 +161,34 @@
guard let localpart = userID.components(separatedBy: ":").first else { return userID }
return String(localpart.trimmingPrefix("@"))
}

private func historyVisibleAlert(learnMoreUrl: URL) -> some View {
let linkPlaceholder = "{link}"
var description = AttributedString(UntranslatedL10n.cryptoHistoryVisible(linkPlaceholder))
var linkString = AttributedString(L10n.actionLearnMore)
linkString.link = learnMoreUrl
linkString.bold()
description.replace(linkPlaceholder, with: linkString)

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 {
Expand All @@ -166,12 +200,15 @@
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")
RoomScreenFooterView(details: noNameDetails, mediaProvider: MediaProviderMock(configuration: .init())) { _ in }
.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")
}
}
114 changes: 114 additions & 0 deletions UnitTests/Sources/RoomScreenViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -438,4 +438,118 @@ class RoomScreenViewModelTests: XCTestCase {
}
try await deferred.fulfill()
}

// MARK: History Sharing

func testHistoryVisibleBannerDoesNotAppearIfNotEncrypted() async throws {
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 = deferFulfillment(viewModel.context.$viewState) { state in
state.footerDetails == nil
}
try await deferred.fulfill()
}

func testHistoryVisibleBannerDoesNotAppearIfJoined() async throws {
let configuration = JoinedRoomProxyMockConfiguration(isEncrypted: false)
let roomProxyMock = JoinedRoomProxyMock(configuration)

let roomInfoProxyMock = RoomInfoProxyMock(configuration)
roomInfoProxyMock.historyVisibility = RoomHistoryVisibility.joined

let infoSubject = CurrentValueSubject<RoomInfoProxyProtocol, Never>(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 = deferFulfillment(viewModel.context.$viewState) { state in
state.footerDetails == nil
}
try await deferred.fulfill()
}

func testHistoryVisibleBannerDoesNotAppearIfAcknowledged() async throws {
ServiceLocator.shared.settings.acknowledgedHistoryVisibleRooms.insert("$room:example.com")

let configuration = JoinedRoomProxyMockConfiguration(id: "$room:example.com", isEncrypted: false)
let roomProxyMock = JoinedRoomProxyMock(configuration)

let roomInfoProxyMock = RoomInfoProxyMock(configuration)
roomInfoProxyMock.historyVisibility = RoomHistoryVisibility.shared

let infoSubject = CurrentValueSubject<RoomInfoProxyProtocol, Never>(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 = deferFulfillment(viewModel.context.$viewState) { state in
state.footerDetails == nil
}
try await deferred.fulfill()
}

func testHistoryVisibleBannerAppearsThenDisappearsOnAcknowledge() async throws {
let configuration = JoinedRoomProxyMockConfiguration(id: "$room:example.com", isEncrypted: true)
let roomProxyMock = JoinedRoomProxyMock(configuration)

let roomInfoProxyMock = RoomInfoProxyMock(configuration)
roomInfoProxyMock.historyVisibility = RoomHistoryVisibility.shared

let infoSubject = CurrentValueSubject<RoomInfoProxyProtocol, Never>(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()

ServiceLocator.shared.settings.acknowledgedHistoryVisibleRooms.insert("$room:example.com")

viewModel.context.send(viewAction: RoomScreenViewAction.footerViewAction(RoomScreenFooterViewAction.dismissHistoryVisibleAlert))

deferred = deferFulfillment(viewModel.context.$viewState) { state in
state.footerDetails == nil
}
try await deferred.fulfill()
}
}
Loading