Fixes element-hq/element-meta/issues/2525 - Display a warning when a user's pinned identity changes

This commit is contained in:
Stefan Ceriu 2024-10-01 08:41:15 +03:00
parent 70652ed7be
commit 794d0eead1
30 changed files with 678 additions and 131 deletions

View File

@ -1035,6 +1035,7 @@
E82E13CC3EB923CCB8F8273C /* TimelineProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E543072DE58E751F028998 /* TimelineProxy.swift */; };
E84ADFE9696936C18C2424B5 /* SecureBackupScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A00BB9CD12CF6AC98D5485 /* SecureBackupScreen.swift */; };
E89536FC8C0E4B79E9842A78 /* RoomTimelineControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C0197EAE9D45A662B8847B6 /* RoomTimelineControllerProtocol.swift */; };
E8C65C19F7C40EE545172DD6 /* RoomScreenFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4137900E28201C314C835C11 /* RoomScreenFooterView.swift */; };
E9347F56CF0683208F4D9249 /* RoomNotificationSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81A9B5225D0881CEFA2CF7C9 /* RoomNotificationSettingsScreenViewModel.swift */; };
E9560744F7B0292E20ECE5F2 /* RoomDetailsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E8A1E8EE094F570573B6E8 /* RoomDetailsScreenViewModelProtocol.swift */; };
E96005321849DBD7C72A28F2 /* UITestsAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C208DA43CE25D13E670F40 /* UITestsAppCoordinator.swift */; };
@ -1488,6 +1489,7 @@
406C90AF8C3E98DF5D4E5430 /* ElementCallServiceConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallServiceConstants.swift; sourceTree = "<group>"; };
40B21E611DADDEF00307E7AC /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
4100DDE6BF3C566AB66B80CC /* MentionSuggestionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionSuggestionItemView.swift; sourceTree = "<group>"; };
4137900E28201C314C835C11 /* RoomScreenFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenFooterView.swift; sourceTree = "<group>"; };
4176C3E20C772DE8D182863C /* LegalInformationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreen.swift; sourceTree = "<group>"; };
419957D7B1C983D7B3B93678 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
41BB37D96C3EA18F3CE8675D /* RoomDirectorySearchScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenModels.swift; sourceTree = "<group>"; };
@ -4018,6 +4020,7 @@
children = (
422724361B6555364C43281E /* RoomHeaderView.swift */,
5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */,
4137900E28201C314C835C11 /* RoomScreenFooterView.swift */,
4552D3466B1453F287223ADA /* SwipeRightAction.swift */,
464C6BFAA853DC755B9C1F60 /* PinnedItemsBanner */,
);
@ -6788,6 +6791,7 @@
F8F47CE757EE656905F01F2C /* RoomRolesAndPermissionsScreenViewModelProtocol.swift in Sources */,
C55A44C99F64A479ABA85B46 /* RoomScreen.swift in Sources */,
A851635B3255C6DC07034A12 /* RoomScreenCoordinator.swift in Sources */,
E8C65C19F7C40EE545172DD6 /* RoomScreenFooterView.swift in Sources */,
352C439BE0F75E101EF11FB1 /* RoomScreenModels.swift in Sources */,
7BB31E67648CF32D2AB5E502 /* RoomScreenViewModel.swift in Sources */,
617624A97BDBB75ED3DD8156 /* RoomScreenViewModelProtocol.swift in Sources */,
@ -7785,7 +7789,7 @@
repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift";
requirement = {
kind = exactVersion;
version = 1.0.53;
version = 1.0.55;
};
};
701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = {

View File

@ -149,8 +149,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/matrix-rust-components-swift",
"state" : {
"revision" : "83abbdc8485340c20f27148153ff62f690ca210b",
"version" : "1.0.53"
"revision" : "8ee63edc76bccd12c17a22eaf4eddae69e5f1303",
"version" : "1.0.55"
}
},
{

View File

@ -124,6 +124,8 @@ final class AppSettings {
let encryptionURL: URL = "https://element.io/help#encryption"
/// A URL where users can go read more about the chat backup.
let chatBackupDetailsURL: URL = "https://element.io/help#encryption5"
/// A URL where users can go read more about identity pinning violations
let identityPinningViolationDetailsURL: URL = "https://element.io/help#18"
/// Any domains that Element web may be hosted on - used for handling links.
let elementWebHosts = ["app.element.io", "staging.element.io", "develop.element.io"]

View File

@ -590,7 +590,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
let composerDraftService = ComposerDraftService(roomProxy: roomProxy, timelineItemfactory: timelineItemFactory)
let parameters = RoomScreenCoordinatorParameters(roomProxy: roomProxy,
let parameters = RoomScreenCoordinatorParameters(clientProxy: userSession.clientProxy,
roomProxy: roomProxy,
focussedEvent: focussedEvent,
timelineController: timelineController,
mediaProvider: userSession.mediaProvider,

View File

@ -4426,6 +4426,76 @@ class ClientProxyMock: ClientProxyProtocol {
return curve25519Base64ReturnValue
}
}
//MARK: - pinUserIdentity
var pinUserIdentityUnderlyingCallsCount = 0
var pinUserIdentityCallsCount: Int {
get {
if Thread.isMainThread {
return pinUserIdentityUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = pinUserIdentityUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
pinUserIdentityUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
pinUserIdentityUnderlyingCallsCount = newValue
}
}
}
}
var pinUserIdentityCalled: Bool {
return pinUserIdentityCallsCount > 0
}
var pinUserIdentityReceivedUserID: String?
var pinUserIdentityReceivedInvocations: [String] = []
var pinUserIdentityUnderlyingReturnValue: Result<Void, ClientProxyError>!
var pinUserIdentityReturnValue: Result<Void, ClientProxyError>! {
get {
if Thread.isMainThread {
return pinUserIdentityUnderlyingReturnValue
} else {
var returnValue: Result<Void, ClientProxyError>? = nil
DispatchQueue.main.sync {
returnValue = pinUserIdentityUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
pinUserIdentityUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
pinUserIdentityUnderlyingReturnValue = newValue
}
}
}
}
var pinUserIdentityClosure: ((String) async -> Result<Void, ClientProxyError>)?
func pinUserIdentity(_ userID: String) async -> Result<Void, ClientProxyError> {
pinUserIdentityCallsCount += 1
pinUserIdentityReceivedUserID = userID
DispatchQueue.main.async {
self.pinUserIdentityReceivedInvocations.append(userID)
}
if let pinUserIdentityClosure = pinUserIdentityClosure {
return await pinUserIdentityClosure(userID)
} else {
return pinUserIdentityReturnValue
}
}
//MARK: - resetIdentity
var resetIdentityUnderlyingCallsCount = 0
@ -5791,6 +5861,11 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol {
set(value) { underlyingTypingMembersPublisher = value }
}
var underlyingTypingMembersPublisher: CurrentValuePublisher<[String], Never>!
var identityStatusChangesPublisher: CurrentValuePublisher<[IdentityStatusChange], Never> {
get { return underlyingIdentityStatusChangesPublisher }
set(value) { underlyingIdentityStatusChangesPublisher = value }
}
var underlyingIdentityStatusChangesPublisher: CurrentValuePublisher<[IdentityStatusChange], Never>!
var actionsPublisher: AnyPublisher<JoinedRoomProxyAction, Never> {
get { return underlyingActionsPublisher }
set(value) { underlyingActionsPublisher = value }
@ -12495,6 +12570,7 @@ class RoomMemberProxyMock: RoomMemberProxyProtocol {
}
var underlyingUserID: String!
var displayName: String?
var disambiguatedDisplayName: String?
var avatarURL: URL?
var membership: MembershipState {
get { return underlyingMembership }

View File

@ -625,6 +625,52 @@ open class ClientSDKMock: MatrixRustSDK.Client {
}
}
//MARK: - customLoginWithJwt
open var customLoginWithJwtJwtInitialDeviceNameDeviceIdThrowableError: Error?
var customLoginWithJwtJwtInitialDeviceNameDeviceIdUnderlyingCallsCount = 0
open var customLoginWithJwtJwtInitialDeviceNameDeviceIdCallsCount: Int {
get {
if Thread.isMainThread {
return customLoginWithJwtJwtInitialDeviceNameDeviceIdUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = customLoginWithJwtJwtInitialDeviceNameDeviceIdUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
customLoginWithJwtJwtInitialDeviceNameDeviceIdUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
customLoginWithJwtJwtInitialDeviceNameDeviceIdUnderlyingCallsCount = newValue
}
}
}
}
open var customLoginWithJwtJwtInitialDeviceNameDeviceIdCalled: Bool {
return customLoginWithJwtJwtInitialDeviceNameDeviceIdCallsCount > 0
}
open var customLoginWithJwtJwtInitialDeviceNameDeviceIdReceivedArguments: (jwt: String, initialDeviceName: String?, deviceId: String?)?
open var customLoginWithJwtJwtInitialDeviceNameDeviceIdReceivedInvocations: [(jwt: String, initialDeviceName: String?, deviceId: String?)] = []
open var customLoginWithJwtJwtInitialDeviceNameDeviceIdClosure: ((String, String?, String?) async throws -> Void)?
open override func customLoginWithJwt(jwt: String, initialDeviceName: String?, deviceId: String?) async throws {
if let error = customLoginWithJwtJwtInitialDeviceNameDeviceIdThrowableError {
throw error
}
customLoginWithJwtJwtInitialDeviceNameDeviceIdCallsCount += 1
customLoginWithJwtJwtInitialDeviceNameDeviceIdReceivedArguments = (jwt: jwt, initialDeviceName: initialDeviceName, deviceId: deviceId)
DispatchQueue.main.async {
self.customLoginWithJwtJwtInitialDeviceNameDeviceIdReceivedInvocations.append((jwt: jwt, initialDeviceName: initialDeviceName, deviceId: deviceId))
}
try await customLoginWithJwtJwtInitialDeviceNameDeviceIdClosure?(jwt, initialDeviceName, deviceId)
}
//MARK: - deactivateAccount
open var deactivateAccountAuthDataEraseDataThrowableError: Error?
@ -4953,6 +4999,77 @@ open class ClientBuilderSDKMock: MatrixRustSDK.ClientBuilder {
}
}
//MARK: - roomDecryptionTrustRequirement
var roomDecryptionTrustRequirementTrustRequirementUnderlyingCallsCount = 0
open var roomDecryptionTrustRequirementTrustRequirementCallsCount: Int {
get {
if Thread.isMainThread {
return roomDecryptionTrustRequirementTrustRequirementUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = roomDecryptionTrustRequirementTrustRequirementUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
roomDecryptionTrustRequirementTrustRequirementUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
roomDecryptionTrustRequirementTrustRequirementUnderlyingCallsCount = newValue
}
}
}
}
open var roomDecryptionTrustRequirementTrustRequirementCalled: Bool {
return roomDecryptionTrustRequirementTrustRequirementCallsCount > 0
}
open var roomDecryptionTrustRequirementTrustRequirementReceivedTrustRequirement: TrustRequirement?
open var roomDecryptionTrustRequirementTrustRequirementReceivedInvocations: [TrustRequirement] = []
var roomDecryptionTrustRequirementTrustRequirementUnderlyingReturnValue: ClientBuilder!
open var roomDecryptionTrustRequirementTrustRequirementReturnValue: ClientBuilder! {
get {
if Thread.isMainThread {
return roomDecryptionTrustRequirementTrustRequirementUnderlyingReturnValue
} else {
var returnValue: ClientBuilder? = nil
DispatchQueue.main.sync {
returnValue = roomDecryptionTrustRequirementTrustRequirementUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
roomDecryptionTrustRequirementTrustRequirementUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
roomDecryptionTrustRequirementTrustRequirementUnderlyingReturnValue = newValue
}
}
}
}
open var roomDecryptionTrustRequirementTrustRequirementClosure: ((TrustRequirement) -> ClientBuilder)?
open override func roomDecryptionTrustRequirement(trustRequirement: TrustRequirement) -> ClientBuilder {
roomDecryptionTrustRequirementTrustRequirementCallsCount += 1
roomDecryptionTrustRequirementTrustRequirementReceivedTrustRequirement = trustRequirement
DispatchQueue.main.async {
self.roomDecryptionTrustRequirementTrustRequirementReceivedInvocations.append(trustRequirement)
}
if let roomDecryptionTrustRequirementTrustRequirementClosure = roomDecryptionTrustRequirementTrustRequirementClosure {
return roomDecryptionTrustRequirementTrustRequirementClosure(trustRequirement)
} else {
return roomDecryptionTrustRequirementTrustRequirementReturnValue
}
}
//MARK: - roomKeyRecipientStrategy
var roomKeyRecipientStrategyStrategyUnderlyingCallsCount = 0
@ -12296,6 +12413,34 @@ open class RoomSDKMock: MatrixRustSDK.Room {
}
}
//MARK: - pinUserIdentity
open var pinUserIdentityUserIdThrowableError: Error?
var pinUserIdentityUserIdUnderlyingCallsCount = 0
open var pinUserIdentityUserIdCallsCount: Int {
get {
if Thread.isMainThread {
return pinUserIdentityUserIdUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = pinUserIdentityUserIdUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
pinUserIdentityUserIdUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
pinUserIdentityUserIdUnderlyingCallsCount = newValue
}
}
}
}
//MARK: - pinnedEventsTimeline
open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsThrowableError: Error?
@ -13068,6 +13213,77 @@ open class RoomSDKMock: MatrixRustSDK.Room {
try await setUnreadFlagNewValueClosure?(newValue)
}
//MARK: - subscribeToIdentityStatusChanges
var subscribeToIdentityStatusChangesListenerUnderlyingCallsCount = 0
open var subscribeToIdentityStatusChangesListenerCallsCount: Int {
get {
if Thread.isMainThread {
return subscribeToIdentityStatusChangesListenerUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = subscribeToIdentityStatusChangesListenerUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
subscribeToIdentityStatusChangesListenerUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
subscribeToIdentityStatusChangesListenerUnderlyingCallsCount = newValue
}
}
}
}
open var subscribeToIdentityStatusChangesListenerCalled: Bool {
return subscribeToIdentityStatusChangesListenerCallsCount > 0
}
open var subscribeToIdentityStatusChangesListenerReceivedListener: IdentityStatusChangeListener?
open var subscribeToIdentityStatusChangesListenerReceivedInvocations: [IdentityStatusChangeListener] = []
var subscribeToIdentityStatusChangesListenerUnderlyingReturnValue: TaskHandle!
open var subscribeToIdentityStatusChangesListenerReturnValue: TaskHandle! {
get {
if Thread.isMainThread {
return subscribeToIdentityStatusChangesListenerUnderlyingReturnValue
} else {
var returnValue: TaskHandle? = nil
DispatchQueue.main.sync {
returnValue = subscribeToIdentityStatusChangesListenerUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
subscribeToIdentityStatusChangesListenerUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
subscribeToIdentityStatusChangesListenerUnderlyingReturnValue = newValue
}
}
}
}
open var subscribeToIdentityStatusChangesListenerClosure: ((IdentityStatusChangeListener) -> TaskHandle)?
open override func subscribeToIdentityStatusChanges(listener: IdentityStatusChangeListener) -> TaskHandle {
subscribeToIdentityStatusChangesListenerCallsCount += 1
subscribeToIdentityStatusChangesListenerReceivedListener = listener
DispatchQueue.main.async {
self.subscribeToIdentityStatusChangesListenerReceivedInvocations.append(listener)
}
if let subscribeToIdentityStatusChangesListenerClosure = subscribeToIdentityStatusChangesListenerClosure {
return subscribeToIdentityStatusChangesListenerClosure(listener)
} else {
return subscribeToIdentityStatusChangesListenerReturnValue
}
}
//MARK: - subscribeToRoomInfoUpdates
var subscribeToRoomInfoUpdatesListenerUnderlyingCallsCount = 0
@ -14087,77 +14303,6 @@ open class RoomListSDKMock: MatrixRustSDK.RoomList {
fileprivate var pointer: UnsafeMutableRawPointer!
//MARK: - entries
var entriesListenerUnderlyingCallsCount = 0
open var entriesListenerCallsCount: Int {
get {
if Thread.isMainThread {
return entriesListenerUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = entriesListenerUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
entriesListenerUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
entriesListenerUnderlyingCallsCount = newValue
}
}
}
}
open var entriesListenerCalled: Bool {
return entriesListenerCallsCount > 0
}
open var entriesListenerReceivedListener: RoomListEntriesListener?
open var entriesListenerReceivedInvocations: [RoomListEntriesListener] = []
var entriesListenerUnderlyingReturnValue: TaskHandle!
open var entriesListenerReturnValue: TaskHandle! {
get {
if Thread.isMainThread {
return entriesListenerUnderlyingReturnValue
} else {
var returnValue: TaskHandle? = nil
DispatchQueue.main.sync {
returnValue = entriesListenerUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
entriesListenerUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
entriesListenerUnderlyingReturnValue = newValue
}
}
}
}
open var entriesListenerClosure: ((RoomListEntriesListener) -> TaskHandle)?
open override func entries(listener: RoomListEntriesListener) -> TaskHandle {
entriesListenerCallsCount += 1
entriesListenerReceivedListener = listener
DispatchQueue.main.async {
self.entriesListenerReceivedInvocations.append(listener)
}
if let entriesListenerClosure = entriesListenerClosure {
return entriesListenerClosure(listener)
} else {
return entriesListenerReturnValue
}
}
//MARK: - entriesWithDynamicAdapters
var entriesWithDynamicAdaptersPageSizeListenerUnderlyingCallsCount = 0

View File

@ -80,6 +80,7 @@ extension JoinedRoomProxyMock {
membersPublisher = CurrentValueSubject(configuration.members).asCurrentValuePublisher()
typingMembersPublisher = CurrentValueSubject([]).asCurrentValuePublisher()
identityStatusChangesPublisher = CurrentValueSubject([]).asCurrentValuePublisher()
joinedMembersCount = configuration.members.filter { $0.membership == .join }.count
activeMembersCount = configuration.members.filter { $0.membership == .join || $0.membership == .invite }.count

View File

@ -25,6 +25,11 @@ extension RoomMemberProxyMock {
self.init()
userID = configuration.userID
displayName = configuration.displayName
if let displayName = configuration.displayName {
disambiguatedDisplayName = "\(displayName) (\(userID))"
}
avatarURL = configuration.avatarURL
membership = configuration.membership

View File

@ -12,6 +12,7 @@ import SwiftUI
import WysiwygComposer
struct RoomScreenCoordinatorParameters {
let clientProxy: ClientProxyProtocol
let roomProxy: JoinedRoomProxyProtocol
var focussedEvent: FocusEvent?
let timelineController: RoomTimelineControllerProtocol
@ -61,13 +62,15 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
selectedPinnedEventID = focussedEvent.shouldSetPin ? focussedEvent.eventID : nil
}
roomViewModel = RoomScreenViewModel(roomProxy: parameters.roomProxy,
roomViewModel = RoomScreenViewModel(clientProxy: parameters.clientProxy,
roomProxy: parameters.roomProxy,
initialSelectedPinnedEventID: selectedPinnedEventID,
mediaProvider: parameters.mediaProvider,
ongoingCallRoomIDPublisher: parameters.ongoingCallRoomIDPublisher,
appMediator: parameters.appMediator,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics)
analyticsService: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
timelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy,
focussedEventID: parameters.focussedEvent?.eventID,
@ -149,10 +152,10 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
.store(in: &cancellables)
roomViewModel.actions
.sink { [weak self] actions in
.sink { [weak self] action in
guard let self else { return }
switch actions {
switch action {
case .focusEvent(eventID: let eventID):
focusOnEvent(FocusEvent(eventID: eventID, shouldSetPin: false))
case .displayPinnedEventsTimeline:

View File

@ -21,6 +21,7 @@ enum RoomScreenViewAction {
case viewAllPins
case displayRoomDetails
case displayCall
case footerViewAction(RoomScreenFooterViewAction)
}
struct RoomScreenViewState: BindableState {
@ -38,11 +39,21 @@ struct RoomScreenViewState: BindableState {
var hasOngoingCall: Bool
var shouldShowCallButton = true
var footerDetails: RoomScreenFooterViewDetails?
var bindings: RoomScreenViewStateBindings
}
struct RoomScreenViewStateBindings { }
enum RoomScreenFooterViewAction {
case resolvePinViolation(userID: String)
}
enum RoomScreenFooterViewDetails {
case pinViolation(member: RoomMemberProxyProtocol, learnMoreURL: URL)
}
enum PinnedEventsBannerState: Equatable {
case loading(numbersOfEvents: Int)
case loaded(state: PinnedEventsState)

View File

@ -7,18 +7,24 @@
import Combine
import Foundation
import MatrixRustSDK
import OrderedCollections
import SwiftUI
typealias RoomScreenViewModelType = StateStoreViewModel<RoomScreenViewState, RoomScreenViewAction>
class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol {
private let clientProxy: ClientProxyProtocol
private let roomProxy: JoinedRoomProxyProtocol
private let appMediator: AppMediatorProtocol
private let appSettings: AppSettings
private let analyticsService: AnalyticsService
private let pinnedEventStringBuilder: RoomEventStringBuilder
private let userIndicatorController: UserIndicatorControllerProtocol
private var initialSelectedPinnedEventID: String?
private let pinnedEventStringBuilder: RoomEventStringBuilder
private var identityPinningViolations = [String: RoomMemberProxyProtocol]()
private let actionsSubject: PassthroughSubject<RoomScreenViewModelAction, Never> = .init()
var actions: AnyPublisher<RoomScreenViewModelAction, Never> {
@ -43,17 +49,22 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
}
}
init(roomProxy: JoinedRoomProxyProtocol,
init(clientProxy: ClientProxyProtocol,
roomProxy: JoinedRoomProxyProtocol,
initialSelectedPinnedEventID: String?,
mediaProvider: MediaProviderProtocol,
ongoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never>,
appMediator: AppMediatorProtocol,
appSettings: AppSettings,
analyticsService: AnalyticsService) {
analyticsService: AnalyticsService,
userIndicatorController: UserIndicatorControllerProtocol) {
self.clientProxy = clientProxy
self.roomProxy = roomProxy
self.appMediator = appMediator
self.appSettings = appSettings
self.analyticsService = analyticsService
self.userIndicatorController = userIndicatorController
self.initialSelectedPinnedEventID = initialSelectedPinnedEventID
pinnedEventStringBuilder = .pinnedEventStringBuilder(userID: roomProxy.ownUserID)
@ -87,6 +98,11 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
actionsSubject.send(.displayCall)
actionsSubject.send(.removeComposerFocus)
analyticsService.trackInteraction(name: .MobileRoomCallButton)
case .footerViewAction(let action):
switch action {
case .resolvePinViolation(let userID):
Task { await resolveIdentityPinningViolation(userID) }
}
}
}
@ -98,6 +114,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
state.pinnedEventsBannerState.setSelectedPinnedEventID(eventID)
}
// MARK: - Private
private func setupSubscriptions(ongoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never>) {
let roomInfoSubscription = roomProxy
.actionsPublisher
@ -124,6 +142,19 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
}
.store(in: &cancellables)
let identityStatusChangesPublisher = roomProxy.identityStatusChangesPublisher.receive(on: DispatchQueue.main)
Task { [weak self] in
for await changes in identityStatusChangesPublisher.values {
guard !Task.isCancelled else {
return
}
await self?.processIdentityStatusChanges(changes)
}
}
.store(in: &cancellables)
appMediator.networkMonitor.reachabilityPublisher
.filter { $0 == .reachable }
.receive(on: DispatchQueue.main)
@ -141,6 +172,43 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
.store(in: &cancellables)
}
private func processIdentityStatusChanges(_ changes: [IdentityStatusChange]) async {
for change in changes {
switch change.changedTo {
case .pinned:
identityPinningViolations[change.userId] = nil
case .pinViolation:
guard case let .success(member) = await roomProxy.getMember(userID: change.userId) else {
MXLog.error("Failed retrieving room member for identity status change: \(change)")
continue
}
identityPinningViolations[change.userId] = member
default:
break
}
}
if let member = identityPinningViolations.values.first {
state.footerDetails = .pinViolation(member: member,
learnMoreURL: appSettings.identityPinningViolationDetailsURL)
} else {
state.footerDetails = nil
}
}
private func resolveIdentityPinningViolation(_ userID: String) async {
defer {
hideLoadingIndicator()
}
showLoadingIndicator()
if case .failure = await clientProxy.pinUserIdentity(userID) {
userIndicatorController.alertInfo = .init(id: .init(), title: L10n.commonError)
}
}
private func buildPinnedEventContents(timelineItems: [TimelineItemProxy]) {
var pinnedEventContents = OrderedDictionary<String, AttributedString>()
@ -190,16 +258,30 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
}
}
}
// MARK: Loading indicators
private static let loadingIndicatorIdentifier = "\(RoomScreenViewModel.self)-Loading"
private func showLoadingIndicator() {
userIndicatorController.submitIndicator(.init(id: Self.loadingIndicatorIdentifier, type: .toast, title: L10n.commonLoading))
}
private func hideLoadingIndicator() {
userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
}
}
extension RoomScreenViewModel {
static func mock(roomProxyMock: JoinedRoomProxyMock) -> RoomScreenViewModel {
RoomScreenViewModel(roomProxy: roomProxyMock,
RoomScreenViewModel(clientProxy: ClientProxyMock(),
roomProxy: roomProxyMock,
initialSelectedPinnedEventID: nil,
mediaProvider: MediaProviderMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics)
analyticsService: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
}
}

View File

@ -29,21 +29,28 @@ struct RoomScreen: View {
timeline
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
.safeAreaInset(edge: .bottom, spacing: 0) {
composerToolbar
.padding(.bottom, composerToolbarContext.composerFormattingEnabled ? 8 : 12)
.background {
if composerToolbarContext.composerFormattingEnabled {
RoundedRectangle(cornerRadius: 20)
.stroke(Color.compound.borderInteractiveSecondary, lineWidth: 0.5)
.ignoresSafeArea()
}
VStack(spacing: 0) {
RoomScreenFooterView(details: roomContext.viewState.footerDetails,
mediaProvider: roomContext.mediaProvider) { action in
roomContext.send(viewAction: .footerViewAction(action))
}
.padding(.top, 8)
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
.environmentObject(timelineContext)
.environment(\.timelineContext, timelineContext)
// Make sure the reply header honours the hideTimelineMedia setting too.
.environment(\.shouldAutomaticallyLoadImages, !timelineContext.viewState.hideTimelineMedia)
composerToolbar
.padding(.bottom, composerToolbarContext.composerFormattingEnabled ? 8 : 12)
.background {
if composerToolbarContext.composerFormattingEnabled {
RoundedRectangle(cornerRadius: 20)
.stroke(Color.compound.borderInteractiveSecondary, lineWidth: 0.5)
.ignoresSafeArea()
}
}
.padding(.top, 8)
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
.environmentObject(timelineContext)
.environment(\.timelineContext, timelineContext)
// Make sure the reply header honours the hideTimelineMedia setting too.
.environment(\.shouldAutomaticallyLoadImages, !timelineContext.viewState.hideTimelineMedia)
}
}
.overlay(alignment: .top) {
Group {

View File

@ -0,0 +1,78 @@
//
// Copyright 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import SwiftUI
struct RoomScreenFooterView: View {
let details: RoomScreenFooterViewDetails?
let mediaProvider: MediaProviderProtocol?
let callback: (RoomScreenFooterViewAction) -> Void
var body: some View {
if let details {
ZStack(alignment: .top) {
VStack(spacing: 0) {
Color.compound.borderInfoSubtle
.frame(height: 1)
LinearGradient(colors: [.compound.bgInfoSubtle, .compound.bgCanvasDefault],
startPoint: .top,
endPoint: .bottom)
}
switch details {
case .pinViolation(let member, let learnMoreURL):
pinViolation(member: member, learnMoreURL: learnMoreURL)
}
}
.padding(.top, 8)
.fixedSize(horizontal: false, vertical: true)
}
}
private func pinViolation(member: RoomMemberProxyProtocol,
learnMoreURL: URL) -> some View {
VStack(spacing: 16) {
HStack(spacing: 16) {
LoadableAvatarImage(url: member.avatarURL,
name: member.disambiguatedDisplayName,
contentID: member.userID,
avatarSize: .user(on: .timeline),
mediaProvider: mediaProvider)
Text(pinViolationDescriptionWithLearnMoreLink(displayName: member.disambiguatedDisplayName ?? member.userID,
url: learnMoreURL))
.font(.compound.bodyMD)
.foregroundColor(.compound.textPrimary)
}
Button(L10n.actionOk) {
callback(.resolvePinViolation(userID: member.userID))
}
.buttonStyle(.compound(.primary, size: .medium))
}
.padding(.top, 16)
.padding(.horizontal, 16)
.padding(.bottom, 8)
}
private func pinViolationDescriptionWithLearnMoreLink(displayName: String, url: URL) -> AttributedString {
let linkPlaceholder = "{link}"
var description = AttributedString(L10n.cryptoIdentityChangePinViolation(displayName, linkPlaceholder))
var linkString = AttributedString(L10n.actionLearnMore)
linkString.link = url
linkString.bold()
description.replace(linkPlaceholder, with: linkString)
return description
}
}
struct RoomScreenFooterView_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
RoomScreenFooterView(details: .pinViolation(member: RoomMemberProxyMock.mockBob, learnMoreURL: "https://element.io/"),
mediaProvider: MediaProviderMock(configuration: .init())) { _ in }
}
}

View File

@ -895,6 +895,22 @@ class ClientProxy: ClientProxyProtocol {
await client.encryption().curve25519Key()
}
func pinUserIdentity(_ userID: String) async -> Result<Void, ClientProxyError> {
MXLog.info("Pinning current identity for user: \(userID)")
do {
guard let userIdentity = try await client.encryption().getUserIdentity(userId: userID) else {
MXLog.error("Failed retrieving identity for user: \(userID)")
return .failure(.failedRetrievingUserIdentity)
}
return try await .success(userIdentity.pin())
} catch {
MXLog.error("Failed pinning current identity for user: \(error)")
return .failure(.sdkError(error))
}
}
func resetIdentity() async -> Result<IdentityResetHandle?, ClientProxyError> {
do {
return try await .success(client.encryption().resetIdentity())

View File

@ -35,6 +35,7 @@ enum ClientProxyError: Error {
case invalidServerName
case failedUploadingMedia(Error, MatrixErrorCode)
case roomPreviewIsPrivate
case failedRetrievingUserIdentity
}
enum SlidingSyncConstants {
@ -196,5 +197,6 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
func ed25519Base64() async -> String?
func curve25519Base64() async -> String?
func pinUserIdentity(_ userID: String) async -> Result<Void, ClientProxyError>
func resetIdentity() async -> Result<IdentityResetHandle?, ClientProxyError>
}

View File

@ -295,10 +295,12 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
roomProxy
.actionsPublisher
.map { action -> (Bool, [String]) in
.compactMap { action -> (Bool, [String])? in
switch action {
case .roomInfoUpdate:
return (roomProxy.hasOngoingCall, roomProxy.activeRoomCallParticipants)
default:
return nil
}
}
.removeDuplicates { $0 == $1 }

View File

@ -58,6 +58,8 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
private var roomInfoObservationToken: TaskHandle?
// periphery:ignore - required for instance retention in the rust codebase
private var typingNotificationObservationToken: TaskHandle?
// periphery:ignore - required for instance retention in the rust codebase
private var identityStatusChangesObservationToken: TaskHandle?
private var subscribedForUpdates = false
@ -70,6 +72,11 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
var typingMembersPublisher: CurrentValuePublisher<[String], Never> {
typingMembersSubject.asCurrentValuePublisher()
}
private let identityStatusChangesSubject = CurrentValueSubject<[IdentityStatusChange], Never>([])
var identityStatusChangesPublisher: CurrentValuePublisher<[IdentityStatusChange], Never> {
identityStatusChangesSubject.asCurrentValuePublisher()
}
private let actionsSubject = PassthroughSubject<JoinedRoomProxyAction, Never>()
var actionsPublisher: AnyPublisher<JoinedRoomProxyAction, Never> {
@ -193,6 +200,8 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
subscribeToRoomInfoUpdates()
subscribeToIdentityStatusChanges()
subscribeToTypingNotifications()
}
@ -708,6 +717,16 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
typingMembersSubject.send(typingMembers)
})
}
private func subscribeToIdentityStatusChanges() {
identityStatusChangesObservationToken = room.subscribeToIdentityStatusChanges(listener: RoomIdentityStatusChangeListener { [weak self] changes in
guard let self else { return }
MXLog.info("Received identity status changes: \(changes)")
identityStatusChangesSubject.send(changes)
})
}
}
private final class RoomInfoUpdateListener: RoomInfoListener {
@ -733,3 +752,15 @@ private final class RoomTypingNotificationUpdateListener: TypingNotificationsLis
onUpdateClosure(typingUserIds)
}
}
private final class RoomIdentityStatusChangeListener: IdentityStatusChangeListener {
private let onUpdateClosure: ([IdentityStatusChange]) -> Void
init(_ onUpdateClosure: @escaping ([IdentityStatusChange]) -> Void) {
self.onUpdateClosure = onUpdateClosure
}
func call(identityStatusChange: [IdentityStatusChange]) {
onUpdateClosure(identityStatusChange)
}
}

View File

@ -16,12 +16,24 @@ final class RoomMemberProxy: RoomMemberProxyProtocol {
}
var userID: String { member.userId }
var displayName: String? { member.displayName }
var disambiguatedDisplayName: String? {
guard let displayName else {
return nil
}
return member.isNameAmbiguous ? "\(displayName) (\(userID))" : displayName
}
var avatarURL: URL? { member.avatarUrl.flatMap(URL.init(string:)) }
var membership: MembershipState { member.membership }
var isIgnored: Bool { member.isIgnored }
var powerLevel: Int { Int(member.powerLevel) }
var role: RoomMemberRole { member.suggestedRoleForPowerLevel }
}

View File

@ -11,13 +11,18 @@ import MatrixRustSDK
// sourcery: AutoMockable
protocol RoomMemberProxyProtocol: AnyObject {
var userID: String { get }
var displayName: String? { get }
var disambiguatedDisplayName: String? { get }
var avatarURL: URL? { get }
var membership: MembershipState { get }
var isIgnored: Bool { get }
var powerLevel: Int { get }
var role: RoomMemberRole { get }
}

View File

@ -56,7 +56,7 @@ protocol InvitedRoomProxyProtocol: RoomProxyProtocol {
func acceptInvitation() async -> Result<Void, RoomProxyError>
}
enum JoinedRoomProxyAction {
enum JoinedRoomProxyAction: Equatable {
case roomInfoUpdate
}
@ -73,6 +73,8 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
var typingMembersPublisher: CurrentValuePublisher<[String], Never> { get }
var identityStatusChangesPublisher: CurrentValuePublisher<[IdentityStatusChange], Never> { get }
var actionsPublisher: AnyPublisher<JoinedRoomProxyAction, Never> { get }
var timeline: TimelineProxyProtocol { get }

View File

@ -19,7 +19,7 @@ enum EncryptionAuthenticity: Hashable {
case unknownDevice(color: Color)
case unsignedDevice(color: Color)
case unverifiedIdentity(color: Color)
case previouslyVerified(color: Color)
case verificationViolation(color: Color)
case sentInClear(color: Color)
var message: String {
@ -32,7 +32,7 @@ enum EncryptionAuthenticity: Hashable {
L10n.eventShieldReasonUnsignedDevice
case .unverifiedIdentity:
L10n.eventShieldReasonUnverifiedIdentity
case .previouslyVerified:
case .verificationViolation:
L10n.eventShieldReasonPreviouslyVerified
case .sentInClear:
L10n.eventShieldReasonSentInClear
@ -45,7 +45,7 @@ enum EncryptionAuthenticity: Hashable {
.unknownDevice(let color),
.unsignedDevice(let color),
.unverifiedIdentity(let color),
.previouslyVerified(let color),
.verificationViolation(let color),
.sentInClear(let color):
color
}
@ -54,7 +54,7 @@ enum EncryptionAuthenticity: Hashable {
var icon: KeyPath<CompoundIcons, Image> {
switch self {
case .notGuaranteed: \.info
case .unknownDevice, .unsignedDevice, .unverifiedIdentity, .previouslyVerified: \.helpSolid
case .unknownDevice, .unsignedDevice, .unverifiedIdentity, .verificationViolation: \.helpSolid
case .sentInClear: \.lockOff
}
}
@ -82,8 +82,8 @@ extension EncryptionAuthenticity {
self = .unsignedDevice(color: color)
case .unverifiedIdentity:
self = .unverifiedIdentity(color: color)
case .previouslyVerified:
self = .previouslyVerified(color: color)
case .verificationViolation:
self = .verificationViolation(color: color)
case .sentInClear:
self = .sentInClear(color: color)
}

View File

@ -233,7 +233,8 @@ class MockScreen: Identifiable {
return navigationStackCoordinator
case .roomPlainNoAvatar:
let navigationStackCoordinator = NavigationStackCoordinator()
let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Some room name", avatarURL: nil)),
let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(),
roomProxy: JoinedRoomProxyMock(.init(name: "Some room name", avatarURL: nil)),
timelineController: MockRoomTimelineController(),
mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(),
@ -251,7 +252,8 @@ class MockScreen: Identifiable {
let navigationStackCoordinator = NavigationStackCoordinator()
let timelineController = MockRoomTimelineController()
timelineController.timelineItems = RoomTimelineItemFixtures.smallChunk
let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)),
let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(),
roomProxy: JoinedRoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)),
timelineController: timelineController,
mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(),
@ -269,7 +271,8 @@ class MockScreen: Identifiable {
let navigationStackCoordinator = NavigationStackCoordinator()
let timelineController = MockRoomTimelineController()
timelineController.timelineItems = RoomTimelineItemFixtures.default
let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)),
let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(),
roomProxy: JoinedRoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)),
timelineController: timelineController,
mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(),
@ -287,7 +290,8 @@ class MockScreen: Identifiable {
let navigationStackCoordinator = NavigationStackCoordinator()
let timelineController = MockRoomTimelineController()
timelineController.timelineItems = RoomTimelineItemFixtures.smallChunkWithReadReceipts
let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)),
let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(),
roomProxy: JoinedRoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)),
timelineController: timelineController,
mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(),
@ -308,7 +312,8 @@ class MockScreen: Identifiable {
timelineController.timelineItems = RoomTimelineItemFixtures.smallChunk
timelineController.backPaginationResponses = [RoomTimelineItemFixtures.singleMessageChunk]
timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage]
let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Small timeline", avatarURL: URL.picturesDirectory)),
let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(),
roomProxy: JoinedRoomProxyMock(.init(name: "Small timeline", avatarURL: URL.picturesDirectory)),
timelineController: timelineController,
mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(),
@ -329,7 +334,8 @@ class MockScreen: Identifiable {
let timelineController = MockRoomTimelineController(listenForSignals: true)
timelineController.timelineItems = RoomTimelineItemFixtures.smallChunk
timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk]
let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Small timeline, paginating", avatarURL: URL.picturesDirectory)),
let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(),
roomProxy: JoinedRoomProxyMock(.init(name: "Small timeline, paginating", avatarURL: URL.picturesDirectory)),
timelineController: timelineController,
mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(),
@ -350,7 +356,8 @@ class MockScreen: Identifiable {
let timelineController = MockRoomTimelineController(listenForSignals: true)
timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk
timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk]
let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)),
let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(),
roomProxy: JoinedRoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)),
timelineController: timelineController,
mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(),
@ -372,7 +379,8 @@ class MockScreen: Identifiable {
timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk
timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk]
timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage]
let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)),
let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(),
roomProxy: JoinedRoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)),
timelineController: timelineController,
mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(),
@ -393,7 +401,8 @@ class MockScreen: Identifiable {
let timelineController = MockRoomTimelineController(listenForSignals: true)
timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk
timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage]
let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)),
let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(),
roomProxy: JoinedRoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)),
timelineController: timelineController,
mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(),
@ -413,7 +422,8 @@ class MockScreen: Identifiable {
let timelineController = MockRoomTimelineController()
timelineController.timelineItems = RoomTimelineItemFixtures.permalinkChunk
let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Timeline highlight", avatarURL: URL.picturesDirectory)),
let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(),
roomProxy: JoinedRoomProxyMock(.init(name: "Timeline highlight", avatarURL: URL.picturesDirectory)),
timelineController: timelineController,
mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(),
@ -447,7 +457,8 @@ class MockScreen: Identifiable {
let timelineController = MockRoomTimelineController()
timelineController.timelineItems = RoomTimelineItemFixtures.disclosedPolls
timelineController.incomingItems = []
let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)),
let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(),
roomProxy: JoinedRoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)),
timelineController: timelineController,
mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(),
@ -468,7 +479,8 @@ class MockScreen: Identifiable {
let timelineController = MockRoomTimelineController()
timelineController.timelineItems = RoomTimelineItemFixtures.undisclosedPolls
timelineController.incomingItems = []
let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)),
let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(),
roomProxy: JoinedRoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)),
timelineController: timelineController,
mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(),
@ -489,7 +501,8 @@ class MockScreen: Identifiable {
let timelineController = MockRoomTimelineController()
timelineController.timelineItems = RoomTimelineItemFixtures.outgoingPolls
timelineController.incomingItems = []
let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)),
let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(),
roomProxy: JoinedRoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)),
timelineController: timelineController,
mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(),

View File

@ -689,6 +689,12 @@ extension PreviewTests {
}
}
func test_roomScreenFooterView() {
for preview in RoomScreenFooterView_Previews._allPreviews {
assertSnapshots(matching: preview)
}
}
func test_roomScreen() {
for preview in RoomScreen_Previews._allPreviews {
assertSnapshots(matching: preview)

View File

@ -218,9 +218,32 @@ class LoggingTests: XCTestCase {
let rustEmoteMessage = EmoteMessageContent(body: emoteString,
formatted: FormattedBody(format: .html, body: "<b>\(emoteString)</b>"))
let rustImageMessage = ImageMessageContent(body: "ImageString", formatted: nil, filename: nil, source: MediaSource(noPointer: .init()), info: nil)
let rustVideoMessage = VideoMessageContent(body: "VideoString", formatted: nil, filename: nil, source: MediaSource(noPointer: .init()), info: nil)
let rustFileMessage = FileMessageContent(body: "FileString", formatted: nil, filename: "FileName", source: MediaSource(noPointer: .init()), info: nil)
let rustImageMessage = ImageMessageContent(body: "ImageString",
formatted: nil,
rawFilename: "ImageString",
filename: "ImageString",
caption: "ImageString",
formattedCaption: nil,
source: MediaSource(noPointer: .init()),
info: nil)
let rustVideoMessage = VideoMessageContent(body: "VideoString",
formatted: nil,
rawFilename: "VideoString",
filename: "VideoString",
caption: "VideoString",
formattedCaption: nil,
source: MediaSource(noPointer: .init()),
info: nil)
let rustFileMessage = FileMessageContent(body: "FileString",
formatted: nil,
rawFilename: "FileString",
filename: "FileString",
caption: "FileString",
formattedCaption: nil,
source: MediaSource(noPointer: .init()),
info: nil)
// When logging that value
MXLog.info(rustTextMessage)

View File

@ -33,13 +33,15 @@ class RoomScreenViewModelTests: XCTestCase {
}
// setup the room proxy actions publisher
roomProxyMock.underlyingActionsPublisher = updateSubject.eraseToAnyPublisher()
let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock,
let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(),
roomProxy: roomProxyMock,
initialSelectedPinnedEventID: nil,
mediaProvider: MediaProviderMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics)
analyticsService: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
self.viewModel = viewModel
// check if in the default state is not showing but is indeed loading
@ -111,13 +113,15 @@ class RoomScreenViewModelTests: XCTestCase {
.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test2")), uniqueID: "2")),
.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test3")), uniqueID: "3"))]
roomProxyMock.underlyingPinnedEventsTimeline = pinnedTimelineMock
let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock,
let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(),
roomProxy: roomProxyMock,
initialSelectedPinnedEventID: "test1",
mediaProvider: MediaProviderMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics)
analyticsService: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
self.viewModel = viewModel
// check if the banner is now in a loaded state and is showing the counter
@ -158,13 +162,15 @@ class RoomScreenViewModelTests: XCTestCase {
// setup the room proxy actions publisher
roomProxyMock.canUserJoinCallUserIDReturnValue = .success(false)
roomProxyMock.underlyingActionsPublisher = updateSubject.eraseToAnyPublisher()
let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock,
let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(),
roomProxy: roomProxyMock,
initialSelectedPinnedEventID: nil,
mediaProvider: MediaProviderMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics)
analyticsService: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
self.viewModel = viewModel
var deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
@ -195,13 +201,15 @@ class RoomScreenViewModelTests: XCTestCase {
// Given a room screen with no ongoing call.
let ongoingCallRoomIDSubject = CurrentValueSubject<String?, Never>(nil)
let roomProxyMock = JoinedRoomProxyMock(.init(id: "MyRoomID"))
let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock,
let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(),
roomProxy: roomProxyMock,
initialSelectedPinnedEventID: nil,
mediaProvider: MediaProviderMock(configuration: .init()),
ongoingCallRoomIDPublisher: ongoingCallRoomIDSubject.asCurrentValuePublisher(),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics)
analyticsService: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
self.viewModel = viewModel
XCTAssertTrue(viewModel.state.shouldShowCallButton)

View File

@ -60,7 +60,7 @@ packages:
# Element/Matrix dependencies
MatrixRustSDK:
url: https://github.com/element-hq/matrix-rust-components-swift
exactVersion: 1.0.53
exactVersion: 1.0.55
# path: ../matrix-rust-sdk
Compound:
url: https://github.com/element-hq/compound-ios