mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
Prompt user when inviting users with uncached identities (#5331)
If the user attempts to invite someone (to a room or creating a DM) whose identity is not cached, we prompt them to make sure this was their intention.
This commit is contained in:
@@ -239,6 +239,10 @@ extension AccessibilityTests {
|
|||||||
try await performAccessibilityAudit(named: "ImageRoomTimelineView_Previews")
|
try await performAccessibilityAudit(named: "ImageRoomTimelineView_Previews")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testInviteUsersConfirmationSheetView() async throws {
|
||||||
|
try await performAccessibilityAudit(named: "InviteUsersConfirmationSheetView_Previews")
|
||||||
|
}
|
||||||
|
|
||||||
func testInviteUsersScreenSelectedItem() async throws {
|
func testInviteUsersScreenSelectedItem() async throws {
|
||||||
try await performAccessibilityAudit(named: "InviteUsersScreenSelectedItem_Previews")
|
try await performAccessibilityAudit(named: "InviteUsersScreenSelectedItem_Previews")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -432,6 +432,7 @@
|
|||||||
4949C8C12669D1B5E082366E /* QRCodeLoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA9EA59D5C0DA1BFC7B3621 /* QRCodeLoginScreen.swift */; };
|
4949C8C12669D1B5E082366E /* QRCodeLoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA9EA59D5C0DA1BFC7B3621 /* QRCodeLoginScreen.swift */; };
|
||||||
49500BBA1CD65A5AE252D970 /* RoomDirectorySearchScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41BB37D96C3EA18F3CE8675D /* RoomDirectorySearchScreenModels.swift */; };
|
49500BBA1CD65A5AE252D970 /* RoomDirectorySearchScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41BB37D96C3EA18F3CE8675D /* RoomDirectorySearchScreenModels.swift */; };
|
||||||
49BBEC46D523BF6A41400048 /* URLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB34956C87731AB094DB33A /* URLTests.swift */; };
|
49BBEC46D523BF6A41400048 /* URLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB34956C87731AB094DB33A /* URLTests.swift */; };
|
||||||
|
4A361E8BAFFA20B9719B47F5 /* InviteUsersConfirmationSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E97CF050B0168F3D605F0E9 /* InviteUsersConfirmationSheetView.swift */; };
|
||||||
4A4110369DBB79E4A314F415 /* ComposerToolbarViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0618820D26F9871A4BBB40E /* ComposerToolbarViewModelProtocol.swift */; };
|
4A4110369DBB79E4A314F415 /* ComposerToolbarViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0618820D26F9871A4BBB40E /* ComposerToolbarViewModelProtocol.swift */; };
|
||||||
4A618590DEB72C4F186BFED4 /* UserSessionFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99FDEEB71173C4C6FA2734C /* UserSessionFlowCoordinator.swift */; };
|
4A618590DEB72C4F186BFED4 /* UserSessionFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99FDEEB71173C4C6FA2734C /* UserSessionFlowCoordinator.swift */; };
|
||||||
4A8287E5281B44A8754BE509 /* SessionVerificationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED33988DA4FD4FC666800106 /* SessionVerificationScreenViewModel.swift */; };
|
4A8287E5281B44A8754BE509 /* SessionVerificationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED33988DA4FD4FC666800106 /* SessionVerificationScreenViewModel.swift */; };
|
||||||
@@ -897,6 +898,7 @@
|
|||||||
964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B902EA6CD3296B0E10EE432B /* HomeScreen.swift */; };
|
964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B902EA6CD3296B0E10EE432B /* HomeScreen.swift */; };
|
||||||
9696ECAFB4F0C079C5C2A526 /* AppLockSetupPINScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAF8C2226A57B9AB7446B31 /* AppLockSetupPINScreenCoordinator.swift */; };
|
9696ECAFB4F0C079C5C2A526 /* AppLockSetupPINScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAF8C2226A57B9AB7446B31 /* AppLockSetupPINScreenCoordinator.swift */; };
|
||||||
96B3606E30F824095B1DD022 /* NetworkMonitorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DA1E8F287680C8ED25EDBAC /* NetworkMonitorMock.swift */; };
|
96B3606E30F824095B1DD022 /* NetworkMonitorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DA1E8F287680C8ED25EDBAC /* NetworkMonitorMock.swift */; };
|
||||||
|
9707AF8D41667FA9B35E8953 /* UserToInvite.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED25719E19B205B668FDACFF /* UserToInvite.swift */; };
|
||||||
97189E495F0E47805D1868DB /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 2B43F2AF7456567FE37270A7 /* KeychainAccess */; };
|
97189E495F0E47805D1868DB /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 2B43F2AF7456567FE37270A7 /* KeychainAccess */; };
|
||||||
973C48F9E4EFB808F61BE401 /* LocationRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED49073BB1C1FC649DAC2CCD /* LocationRoomTimelineView.swift */; };
|
973C48F9E4EFB808F61BE401 /* LocationRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED49073BB1C1FC649DAC2CCD /* LocationRoomTimelineView.swift */; };
|
||||||
978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */; };
|
978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */; };
|
||||||
@@ -2387,6 +2389,7 @@
|
|||||||
8DA1E8F287680C8ED25EDBAC /* NetworkMonitorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitorMock.swift; sourceTree = "<group>"; };
|
8DA1E8F287680C8ED25EDBAC /* NetworkMonitorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitorMock.swift; sourceTree = "<group>"; };
|
||||||
8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = "<group>"; };
|
8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = "<group>"; };
|
||||||
8E1584F8BCF407BB94F48F04 /* EncryptionResetPasswordScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreen.swift; sourceTree = "<group>"; };
|
8E1584F8BCF407BB94F48F04 /* EncryptionResetPasswordScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreen.swift; sourceTree = "<group>"; };
|
||||||
|
8E97CF050B0168F3D605F0E9 /* InviteUsersConfirmationSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersConfirmationSheetView.swift; sourceTree = "<group>"; };
|
||||||
8EAF4A49F3ACD8BB8B0D2371 /* ClientSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientSDKMock.swift; sourceTree = "<group>"; };
|
8EAF4A49F3ACD8BB8B0D2371 /* ClientSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientSDKMock.swift; sourceTree = "<group>"; };
|
||||||
8F062DD2CCD95DC33528A16F /* KnockRequestProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockRequestProxy.swift; sourceTree = "<group>"; };
|
8F062DD2CCD95DC33528A16F /* KnockRequestProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockRequestProxy.swift; sourceTree = "<group>"; };
|
||||||
8F21ED7205048668BEB44A38 /* AppActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppActivityView.swift; sourceTree = "<group>"; };
|
8F21ED7205048668BEB44A38 /* AppActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppActivityView.swift; sourceTree = "<group>"; };
|
||||||
@@ -2921,6 +2924,7 @@
|
|||||||
ED0AD0C652385F69FA90FAF5 /* TimelineMediaPreviewDataSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewDataSourceTests.swift; sourceTree = "<group>"; };
|
ED0AD0C652385F69FA90FAF5 /* TimelineMediaPreviewDataSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewDataSourceTests.swift; sourceTree = "<group>"; };
|
||||||
ED0CBEAB5F796BEFBAF7BB6A /* VideoRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineView.swift; sourceTree = "<group>"; };
|
ED0CBEAB5F796BEFBAF7BB6A /* VideoRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||||
ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = "<group>"; };
|
ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = "<group>"; };
|
||||||
|
ED25719E19B205B668FDACFF /* UserToInvite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserToInvite.swift; sourceTree = "<group>"; };
|
||||||
ED33988DA4FD4FC666800106 /* SessionVerificationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenViewModel.swift; sourceTree = "<group>"; };
|
ED33988DA4FD4FC666800106 /* SessionVerificationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenViewModel.swift; sourceTree = "<group>"; };
|
||||||
ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = "<group>"; };
|
ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = "<group>"; };
|
||||||
ED49073BB1C1FC649DAC2CCD /* LocationRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineView.swift; sourceTree = "<group>"; };
|
ED49073BB1C1FC649DAC2CCD /* LocationRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||||
@@ -4134,6 +4138,7 @@
|
|||||||
578AF9CE60816069536C0953 /* RoomRole.swift */,
|
578AF9CE60816069536C0953 /* RoomRole.swift */,
|
||||||
CCE06F4A71FD46C9D8CD432E /* RoomThreadListServiceProxy.swift */,
|
CCE06F4A71FD46C9D8CD432E /* RoomThreadListServiceProxy.swift */,
|
||||||
04EB6035C1F33F25F1EBFB7D /* RoomThreadListServiceProxyProtocol.swift */,
|
04EB6035C1F33F25F1EBFB7D /* RoomThreadListServiceProxyProtocol.swift */,
|
||||||
|
ED25719E19B205B668FDACFF /* UserToInvite.swift */,
|
||||||
2C0F49BD446849654C0D24E0 /* RoomMember */,
|
2C0F49BD446849654C0D24E0 /* RoomMember */,
|
||||||
4DC0344D2EBD0AE5D71754A9 /* RoomMembershipDetails */,
|
4DC0344D2EBD0AE5D71754A9 /* RoomMembershipDetails */,
|
||||||
7FC3F8FA5EA765AC3B000F55 /* RoomPreview */,
|
7FC3F8FA5EA765AC3B000F55 /* RoomPreview */,
|
||||||
@@ -4296,6 +4301,7 @@
|
|||||||
493225D61FED2DA3D3B26104 /* View */ = {
|
493225D61FED2DA3D3B26104 /* View */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
8E97CF050B0168F3D605F0E9 /* InviteUsersConfirmationSheetView.swift */,
|
||||||
C2E9B841EE4878283ECDB554 /* InviteUsersScreen.swift */,
|
C2E9B841EE4878283ECDB554 /* InviteUsersScreen.swift */,
|
||||||
10F32E0B4B83D2A11EE8D011 /* InviteUsersScreenSelectedItem.swift */,
|
10F32E0B4B83D2A11EE8D011 /* InviteUsersScreenSelectedItem.swift */,
|
||||||
);
|
);
|
||||||
@@ -7325,6 +7331,7 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
buildConfigurationList = 7AE41FCCF9D1352E2770D1F9 /* Build configuration list for PBXProject "ElementX" */;
|
buildConfigurationList = 7AE41FCCF9D1352E2770D1F9 /* Build configuration list for PBXProject "ElementX" */;
|
||||||
|
compatibilityVersion = "Xcode 14.0";
|
||||||
developmentRegion = en;
|
developmentRegion = en;
|
||||||
hasScannedForEncodings = 0;
|
hasScannedForEncodings = 0;
|
||||||
knownRegions = (
|
knownRegions = (
|
||||||
@@ -7402,7 +7409,6 @@
|
|||||||
C89CF7729E028671C5DC461E /* XCLocalSwiftPackageReference "compound-ios" */,
|
C89CF7729E028671C5DC461E /* XCLocalSwiftPackageReference "compound-ios" */,
|
||||||
);
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = 681566846AF307E9BA4C72C6 /* Products */;
|
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
@@ -8328,6 +8334,7 @@
|
|||||||
B6048166B4AA4CEFEA9B77A6 /* InfoPlistReader.swift in Sources */,
|
B6048166B4AA4CEFEA9B77A6 /* InfoPlistReader.swift in Sources */,
|
||||||
BFEB24336DFD5F196E6F3456 /* IntentionalMentions.swift in Sources */,
|
BFEB24336DFD5F196E6F3456 /* IntentionalMentions.swift in Sources */,
|
||||||
2AAB2A77F1762A2648078A30 /* InteractiveQuickLook.swift in Sources */,
|
2AAB2A77F1762A2648078A30 /* InteractiveQuickLook.swift in Sources */,
|
||||||
|
4A361E8BAFFA20B9719B47F5 /* InviteUsersConfirmationSheetView.swift in Sources */,
|
||||||
C4D2BCAA54E2C62B94B24AF4 /* InviteUsersScreen.swift in Sources */,
|
C4D2BCAA54E2C62B94B24AF4 /* InviteUsersScreen.swift in Sources */,
|
||||||
E27C4D1A1F8BB77CA790B403 /* InviteUsersScreenCoordinator.swift in Sources */,
|
E27C4D1A1F8BB77CA790B403 /* InviteUsersScreenCoordinator.swift in Sources */,
|
||||||
C26DB49C06C00B5DF1A991A5 /* InviteUsersScreenModels.swift in Sources */,
|
C26DB49C06C00B5DF1A991A5 /* InviteUsersScreenModels.swift in Sources */,
|
||||||
@@ -9022,6 +9029,7 @@
|
|||||||
7E91BAC17963ED41208F489B /* UserSessionStore.swift in Sources */,
|
7E91BAC17963ED41208F489B /* UserSessionStore.swift in Sources */,
|
||||||
79D57E9AE03A2DC689D14EA2 /* UserSessionStoreMock.swift in Sources */,
|
79D57E9AE03A2DC689D14EA2 /* UserSessionStoreMock.swift in Sources */,
|
||||||
AC69B6DF15FC451AB2945036 /* UserSessionStoreProtocol.swift in Sources */,
|
AC69B6DF15FC451AB2945036 /* UserSessionStoreProtocol.swift in Sources */,
|
||||||
|
9707AF8D41667FA9B35E8953 /* UserToInvite.swift in Sources */,
|
||||||
2447FADEF13225BB6227B977 /* VerificationBadge.swift in Sources */,
|
2447FADEF13225BB6227B977 /* VerificationBadge.swift in Sources */,
|
||||||
5C33976A720B64094CBC56B1 /* VideoMediaEventsTimelineView.swift in Sources */,
|
5C33976A720B64094CBC56B1 /* VideoMediaEventsTimelineView.swift in Sources */,
|
||||||
F07D88421A9BC4D03D4A5055 /* VideoRoomTimelineItem.swift in Sources */,
|
F07D88421A9BC4D03D4A5055 /* VideoRoomTimelineItem.swift in Sources */,
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
"crypto_history_sharing_confirm_invite_dialog_content" = "You currently don’t have any chats with these contacts. Confirm inviting them to this room before continuing.";
|
||||||
|
"crypto_history_sharing_confirm_invite_dialog_title" = "Invite new contacts to this room?";
|
||||||
|
"crypto_history_sharing_confirm_start_chat_dialog_content" = "You currently don’t have any chats with this person. Confirm inviting them before continuing.";
|
||||||
|
"crypto_history_sharing_confirm_start_chat_dialog_title" = "Start a chat with this new contact?";
|
||||||
"soft_logout_clear_data_dialog_content" = "Clear all data currently stored on this device?\nSign in again to access your account data and messages.";
|
"soft_logout_clear_data_dialog_content" = "Clear all data currently stored on this device?\nSign in again to access your account data and messages.";
|
||||||
"soft_logout_clear_data_dialog_title" = "Clear data";
|
"soft_logout_clear_data_dialog_title" = "Clear data";
|
||||||
"soft_logout_clear_data_notice" = "Warning: Your personal data (including encryption keys) is still stored on this device.\n\nClear it if you’re finished using this device, or want to sign in to another account.";
|
"soft_logout_clear_data_notice" = "Warning: Your personal data (including encryption keys) is still stored on this device.\n\nClear it if you’re finished using this device, or want to sign in to another account.";
|
||||||
|
|||||||
@@ -791,7 +791,8 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
isPresentedModally: true,
|
isPresentedModally: true,
|
||||||
userSession: userSession,
|
userSession: userSession,
|
||||||
userIndicatorController: flowParameters.userIndicatorController,
|
userIndicatorController: flowParameters.userIndicatorController,
|
||||||
analytics: flowParameters.analytics)
|
analytics: flowParameters.analytics,
|
||||||
|
appSettings: flowParameters.appSettings)
|
||||||
let coordinator = UserProfileScreenCoordinator(parameters: parameters)
|
let coordinator = UserProfileScreenCoordinator(parameters: parameters)
|
||||||
coordinator.actionsPublisher.sink { [weak self] action in
|
coordinator.actionsPublisher.sink { [weak self] action in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|||||||
@@ -225,7 +225,8 @@ final class RoomMembersFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
roomProxy: roomProxy,
|
roomProxy: roomProxy,
|
||||||
userSession: flowParameters.userSession,
|
userSession: flowParameters.userSession,
|
||||||
userIndicatorController: flowParameters.userIndicatorController,
|
userIndicatorController: flowParameters.userIndicatorController,
|
||||||
analytics: flowParameters.analytics)
|
analytics: flowParameters.analytics,
|
||||||
|
appSettings: flowParameters.appSettings)
|
||||||
let coordinator = RoomMemberDetailsScreenCoordinator(parameters: params)
|
let coordinator = RoomMemberDetailsScreenCoordinator(parameters: params)
|
||||||
|
|
||||||
coordinator.actions.sink { [weak self] action in
|
coordinator.actions.sink { [weak self] action in
|
||||||
@@ -286,7 +287,8 @@ final class RoomMembersFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
isPresentedModally: false,
|
isPresentedModally: false,
|
||||||
userSession: flowParameters.userSession,
|
userSession: flowParameters.userSession,
|
||||||
userIndicatorController: flowParameters.userIndicatorController,
|
userIndicatorController: flowParameters.userIndicatorController,
|
||||||
analytics: flowParameters.analytics)
|
analytics: flowParameters.analytics,
|
||||||
|
appSettings: flowParameters.appSettings)
|
||||||
let coordinator = UserProfileScreenCoordinator(parameters: parameters)
|
let coordinator = UserProfileScreenCoordinator(parameters: parameters)
|
||||||
coordinator.actionsPublisher.sink { [weak self] action in
|
coordinator.actionsPublisher.sink { [weak self] action in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ import Foundation
|
|||||||
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
|
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
|
||||||
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
|
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
|
||||||
internal enum UntranslatedL10n {
|
internal enum UntranslatedL10n {
|
||||||
|
/// You currently don’t have any chats with these contacts. Confirm inviting them to this room before continuing.
|
||||||
|
internal static var cryptoHistorySharingConfirmInviteDialogContent: String { return UntranslatedL10n.tr("Untranslated", "crypto_history_sharing_confirm_invite_dialog_content") }
|
||||||
|
/// Invite new contacts to this room?
|
||||||
|
internal static var cryptoHistorySharingConfirmInviteDialogTitle: String { return UntranslatedL10n.tr("Untranslated", "crypto_history_sharing_confirm_invite_dialog_title") }
|
||||||
|
/// You currently don’t have any chats with this person. Confirm inviting them before continuing.
|
||||||
|
internal static var cryptoHistorySharingConfirmStartChatDialogContent: String { return UntranslatedL10n.tr("Untranslated", "crypto_history_sharing_confirm_start_chat_dialog_content") }
|
||||||
|
/// Start a chat with this new contact?
|
||||||
|
internal static var cryptoHistorySharingConfirmStartChatDialogTitle: String { return UntranslatedL10n.tr("Untranslated", "crypto_history_sharing_confirm_start_chat_dialog_title") }
|
||||||
/// Clear all data currently stored on this device?
|
/// Clear all data currently stored on this device?
|
||||||
/// Sign in again to access your account data and messages.
|
/// 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") }
|
internal static var softLogoutClearDataDialogContent: String { return UntranslatedL10n.tr("Untranslated", "soft_logout_clear_data_dialog_content") }
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ enum TestablePreviewsDictionary {
|
|||||||
"IdentityConfirmedScreen_Previews" : IdentityConfirmedScreen_Previews.self,
|
"IdentityConfirmedScreen_Previews" : IdentityConfirmedScreen_Previews.self,
|
||||||
"ImageMediaEventsTimelineView_Previews" : ImageMediaEventsTimelineView_Previews.self,
|
"ImageMediaEventsTimelineView_Previews" : ImageMediaEventsTimelineView_Previews.self,
|
||||||
"ImageRoomTimelineView_Previews" : ImageRoomTimelineView_Previews.self,
|
"ImageRoomTimelineView_Previews" : ImageRoomTimelineView_Previews.self,
|
||||||
|
"InviteUsersConfirmationSheetView_Previews" : InviteUsersConfirmationSheetView_Previews.self,
|
||||||
"InviteUsersScreenSelectedItem_Previews" : InviteUsersScreenSelectedItem_Previews.self,
|
"InviteUsersScreenSelectedItem_Previews" : InviteUsersScreenSelectedItem_Previews.self,
|
||||||
"InviteUsersScreen_Previews" : InviteUsersScreen_Previews.self,
|
"InviteUsersScreen_Previews" : InviteUsersScreen_Previews.self,
|
||||||
"JoinRoomByAddressView_Previews" : JoinRoomByAddressView_Previews.self,
|
"JoinRoomByAddressView_Previews" : JoinRoomByAddressView_Previews.self,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ struct InviteUsersScreenViewState: BindableState {
|
|||||||
|
|
||||||
var selectedUsers: [UserProfileProxy] = []
|
var selectedUsers: [UserProfileProxy] = []
|
||||||
var membershipState: [String: MembershipState] = .init()
|
var membershipState: [String: MembershipState] = .init()
|
||||||
|
var usersToConfirm: [UserProfileProxy] = []
|
||||||
|
|
||||||
var isSearching = false
|
var isSearching = false
|
||||||
|
|
||||||
@@ -69,6 +70,9 @@ struct InviteUsersScreenViewStateBindings {
|
|||||||
var searchQuery = ""
|
var searchQuery = ""
|
||||||
var selectedUsersPosition: String?
|
var selectedUsersPosition: String?
|
||||||
|
|
||||||
|
/// Whether we are showing the confirmation dialog.
|
||||||
|
var presentConfirmationDialog = false
|
||||||
|
|
||||||
/// Information describing the currently displayed alert.
|
/// Information describing the currently displayed alert.
|
||||||
var alertInfo: AlertInfo<InviteUsersScreenErrorType>?
|
var alertInfo: AlertInfo<InviteUsersScreenErrorType>?
|
||||||
}
|
}
|
||||||
@@ -76,5 +80,7 @@ struct InviteUsersScreenViewStateBindings {
|
|||||||
enum InviteUsersScreenViewAction {
|
enum InviteUsersScreenViewAction {
|
||||||
case cancel
|
case cancel
|
||||||
case proceed
|
case proceed
|
||||||
|
case removeUnknownUsers
|
||||||
|
case confirmUnknownUsers
|
||||||
case toggleUser(UserProfileProxy)
|
case toggleUser(UserProfileProxy)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import SwiftUI
|
|||||||
typealias InviteUsersScreenViewModelType = StateStoreViewModel<InviteUsersScreenViewState, InviteUsersScreenViewAction>
|
typealias InviteUsersScreenViewModelType = StateStoreViewModel<InviteUsersScreenViewState, InviteUsersScreenViewAction>
|
||||||
|
|
||||||
class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScreenViewModelProtocol {
|
class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScreenViewModelProtocol {
|
||||||
|
private let clientProxy: ClientProxyProtocol
|
||||||
private let roomProxy: JoinedRoomProxyProtocol
|
private let roomProxy: JoinedRoomProxyProtocol
|
||||||
private let userDiscoveryService: UserDiscoveryServiceProtocol
|
private let userDiscoveryService: UserDiscoveryServiceProtocol
|
||||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
@@ -31,6 +32,7 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
|||||||
userDiscoveryService: UserDiscoveryServiceProtocol,
|
userDiscoveryService: UserDiscoveryServiceProtocol,
|
||||||
userIndicatorController: UserIndicatorControllerProtocol,
|
userIndicatorController: UserIndicatorControllerProtocol,
|
||||||
appSettings: AppSettings) {
|
appSettings: AppSettings) {
|
||||||
|
clientProxy = userSession.clientProxy
|
||||||
self.roomProxy = roomProxy
|
self.roomProxy = roomProxy
|
||||||
self.userDiscoveryService = userDiscoveryService
|
self.userDiscoveryService = userDiscoveryService
|
||||||
self.userIndicatorController = userIndicatorController
|
self.userIndicatorController = userIndicatorController
|
||||||
@@ -59,6 +61,23 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
|||||||
case .cancel:
|
case .cancel:
|
||||||
actionsSubject.send(.dismiss)
|
actionsSubject.send(.dismiss)
|
||||||
case .proceed:
|
case .proceed:
|
||||||
|
guard appSettings.enableKeyShareOnInvite,
|
||||||
|
roomProxy.details.historySharingState != RoomHistorySharingState.hidden,
|
||||||
|
!state.usersToConfirm.isEmpty,
|
||||||
|
!state.isSkippable else {
|
||||||
|
inviteUsers(state.selectedUsers.map(\.userID), roomProxy: roomProxy)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state.bindings.presentConfirmationDialog = true
|
||||||
|
case .removeUnknownUsers:
|
||||||
|
state.bindings.presentConfirmationDialog = false
|
||||||
|
state.selectedUsers.removeAll { user in
|
||||||
|
state.usersToConfirm.contains { $0.userID == user.userID }
|
||||||
|
}
|
||||||
|
state.usersToConfirm = []
|
||||||
|
case .confirmUnknownUsers:
|
||||||
|
state.bindings.presentConfirmationDialog = false
|
||||||
|
state.usersToConfirm = []
|
||||||
inviteUsers(state.selectedUsers.map(\.userID), roomProxy: roomProxy)
|
inviteUsers(state.selectedUsers.map(\.userID), roomProxy: roomProxy)
|
||||||
case .toggleUser(let user):
|
case .toggleUser(let user):
|
||||||
toggleUser(user)
|
toggleUser(user)
|
||||||
@@ -73,6 +92,17 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
|||||||
} else {
|
} else {
|
||||||
state.selectedUsers.append(user)
|
state.selectedUsers.append(user)
|
||||||
withElementAnimation(.easeInOut) { state.bindings.selectedUsersPosition = user.userID }
|
withElementAnimation(.easeInOut) { state.bindings.selectedUsersPosition = user.userID }
|
||||||
|
Task {
|
||||||
|
let identityUnknown = if case .success(let identity) = await self.clientProxy.userIdentity(for: user.userID, fallBackToServer: false) {
|
||||||
|
identity == nil
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
if identityUnknown {
|
||||||
|
// If we do not have the identity cached, we will prompt the user to confirm they meant to invite them.
|
||||||
|
self.state.usersToConfirm.append(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2026 Element Creations Ltd.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
// Please see LICENSE files in the repository root for full details.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Compound
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct InviteUsersConfirmationSheetView: View {
|
||||||
|
@ObservedObject var context: InviteUsersScreenViewModel.Context
|
||||||
|
|
||||||
|
/// The users whose identities we wish the user to confirm.
|
||||||
|
var users: [UserProfileProxy]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
FullscreenDialog(topPadding: 24, horizontalPadding: 24) {
|
||||||
|
VStack(spacing: 32) {
|
||||||
|
TitleAndIcon(title: L10n.screenInviteUsersConfirmDialogTitle(users.count),
|
||||||
|
subtitle: L10n.screenInviteUsersConfirmDialogSubtitle(users.count),
|
||||||
|
icon: \.userAddSolid,
|
||||||
|
iconStyle: .defaultSolid)
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
ForEach(users, id: \.userID) { user in
|
||||||
|
UserProfileListRow(user: user,
|
||||||
|
membership: nil,
|
||||||
|
mediaProvider: context.mediaProvider,
|
||||||
|
kind: .label)
|
||||||
|
.rowDivider(alignment: .top)
|
||||||
|
.accessibilityIdentifier(A11yIdentifiers.inviteUsersScreen.userProfile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} bottomContent: {
|
||||||
|
HStack(spacing: 32) {
|
||||||
|
Button(L10n.actionRemove, role: .cancel) {
|
||||||
|
context.send(viewAction: .removeUnknownUsers)
|
||||||
|
}
|
||||||
|
.buttonStyle(.compound(.secondary))
|
||||||
|
|
||||||
|
Button(L10n.actionInvite) {
|
||||||
|
context.send(viewAction: .confirmUnknownUsers)
|
||||||
|
}
|
||||||
|
.buttonStyle(.compound(.primary))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background()
|
||||||
|
.backgroundStyle(.compound.bgCanvasDefault)
|
||||||
|
.interactiveDismissDisabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InviteUsersConfirmationSheetView_Previews: PreviewProvider, TestablePreview {
|
||||||
|
static var viewModel = makeViewModel()
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
InviteUsersConfirmationSheetView(context: viewModel.context, users: [.mockAlice, .mockCharlie, .mockBob, .mockDan])
|
||||||
|
.previewDisplayName("Default")
|
||||||
|
}
|
||||||
|
|
||||||
|
static func makeViewModel() -> InviteUsersScreenViewModel {
|
||||||
|
let viewModel = InviteUsersScreenViewModel(userSession: UserSessionMock(.init(clientProxy: ClientProxyMock(.init()))),
|
||||||
|
roomProxy: JoinedRoomProxyMock(.init(members: [])),
|
||||||
|
isSkippable: true,
|
||||||
|
userDiscoveryService: UserDiscoveryServiceMock(),
|
||||||
|
userIndicatorController: UserIndicatorControllerMock(),
|
||||||
|
appSettings: ServiceLocator.shared.settings)
|
||||||
|
|
||||||
|
viewModel.state.usersToConfirm = [.mockAlice, .mockCharlie, .mockBob, .mockDan]
|
||||||
|
|
||||||
|
return viewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,6 +31,9 @@ struct InviteUsersScreen: View {
|
|||||||
disablesInteractiveDismiss: true,
|
disablesInteractiveDismiss: true,
|
||||||
accessibilityFocusOnStart: true)
|
accessibilityFocusOnStart: true)
|
||||||
.compoundSearchField()
|
.compoundSearchField()
|
||||||
|
.sheet(isPresented: $context.presentConfirmationDialog) {
|
||||||
|
InviteUsersConfirmationSheetView(context: context, users: context.viewState.usersToConfirm)
|
||||||
|
}
|
||||||
.alert(item: $context.alertInfo)
|
.alert(item: $context.alertInfo)
|
||||||
.navigationBarBackButtonHidden(context.viewState.isSkippable)
|
.navigationBarBackButtonHidden(context.viewState.isSkippable)
|
||||||
}
|
}
|
||||||
@@ -149,6 +152,7 @@ struct InviteUsersScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
static let viewModel = makeViewModel()
|
static let viewModel = makeViewModel()
|
||||||
static let searchingViewModel = makeViewModel(searchQuery: "Alice")
|
static let searchingViewModel = makeViewModel(searchQuery: "Alice")
|
||||||
static let selectedViewModel = makeViewModel(hasSelection: true)
|
static let selectedViewModel = makeViewModel(hasSelection: true)
|
||||||
|
static let confirmSelectedViewModel = makeViewModel(shouldConfirm: true)
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ElementNavigationStack {
|
ElementNavigationStack {
|
||||||
@@ -170,9 +174,14 @@ struct InviteUsersScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
}
|
}
|
||||||
.previewDisplayName("Selected")
|
.previewDisplayName("Selected")
|
||||||
.snapshotPreferences(expect: selectedViewModel.context.$viewState.map { !$0.selectedUsers.isEmpty })
|
.snapshotPreferences(expect: selectedViewModel.context.$viewState.map { !$0.selectedUsers.isEmpty })
|
||||||
|
|
||||||
|
ElementNavigationStack {
|
||||||
|
InviteUsersScreen(context: confirmSelectedViewModel.context)
|
||||||
|
}
|
||||||
|
.previewDisplayName("Confirm Selected")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func makeViewModel(searchQuery: String? = nil, hasSelection: Bool = false) -> InviteUsersScreenViewModel {
|
static func makeViewModel(searchQuery: String? = nil, hasSelection: Bool = false, shouldConfirm: Bool = false) -> InviteUsersScreenViewModel {
|
||||||
let clientProxy = ClientProxyMock(.init())
|
let clientProxy = ClientProxyMock(.init())
|
||||||
clientProxy.recentConversationCounterpartsReturnValue = [.mockAlice, .mockBob, .mockCharlie, .mockDan, .mockVerbose]
|
clientProxy.recentConversationCounterpartsReturnValue = [.mockAlice, .mockBob, .mockCharlie, .mockDan, .mockVerbose]
|
||||||
|
|
||||||
@@ -194,6 +203,11 @@ struct InviteUsersScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
viewModel.state.selectedUsers = [.mockAlice]
|
viewModel.state.selectedUsers = [.mockAlice]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if shouldConfirm {
|
||||||
|
viewModel.state.usersToConfirm = [.mockAlice, .mockAlice, .mockAlice, .mockAlice, .mockAlice, .mockAlice, .mockAlice, .mockAlice, .mockAlice]
|
||||||
|
viewModel.state.bindings.presentConfirmationDialog = true
|
||||||
|
}
|
||||||
|
|
||||||
return viewModel
|
return viewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -15,6 +15,7 @@ struct RoomMemberDetailsScreenCoordinatorParameters {
|
|||||||
let userSession: UserSessionProtocol
|
let userSession: UserSessionProtocol
|
||||||
let userIndicatorController: UserIndicatorControllerProtocol
|
let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
let analytics: AnalyticsService
|
let analytics: AnalyticsService
|
||||||
|
let appSettings: AppSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RoomMemberDetailsScreenCoordinatorAction {
|
enum RoomMemberDetailsScreenCoordinatorAction {
|
||||||
@@ -39,7 +40,8 @@ final class RoomMemberDetailsScreenCoordinator: CoordinatorProtocol {
|
|||||||
roomProxy: parameters.roomProxy,
|
roomProxy: parameters.roomProxy,
|
||||||
userSession: parameters.userSession,
|
userSession: parameters.userSession,
|
||||||
userIndicatorController: parameters.userIndicatorController,
|
userIndicatorController: parameters.userIndicatorController,
|
||||||
analytics: parameters.analytics)
|
analytics: parameters.analytics,
|
||||||
|
appSettings: parameters.appSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() {
|
func start() {
|
||||||
|
|||||||
+1
-1
@@ -79,7 +79,7 @@ struct RoomMemberDetailsScreenViewStateBindings {
|
|||||||
|
|
||||||
var ignoreUserAlert: IgnoreUserAlertItem?
|
var ignoreUserAlert: IgnoreUserAlertItem?
|
||||||
var alertInfo: AlertInfo<RoomMemberDetailsScreenAlertType>?
|
var alertInfo: AlertInfo<RoomMemberDetailsScreenAlertType>?
|
||||||
var inviteConfirmationUser: UserProfileProxy?
|
var inviteConfirmationUser: UserToInvite?
|
||||||
|
|
||||||
/// A media item that will be previewed with QuickLook.
|
/// A media item that will be previewed with QuickLook.
|
||||||
var mediaPreviewItem: MediaPreviewItem?
|
var mediaPreviewItem: MediaPreviewItem?
|
||||||
|
|||||||
+16
-2
@@ -16,6 +16,7 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
|
|||||||
private let userSession: UserSessionProtocol
|
private let userSession: UserSessionProtocol
|
||||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
private let analytics: AnalyticsService
|
private let analytics: AnalyticsService
|
||||||
|
private let appSettings: AppSettings
|
||||||
|
|
||||||
private var actionsSubject: PassthroughSubject<RoomMemberDetailsScreenViewModelAction, Never> = .init()
|
private var actionsSubject: PassthroughSubject<RoomMemberDetailsScreenViewModelAction, Never> = .init()
|
||||||
|
|
||||||
@@ -29,11 +30,13 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
|
|||||||
roomProxy: JoinedRoomProxyProtocol,
|
roomProxy: JoinedRoomProxyProtocol,
|
||||||
userSession: UserSessionProtocol,
|
userSession: UserSessionProtocol,
|
||||||
userIndicatorController: UserIndicatorControllerProtocol,
|
userIndicatorController: UserIndicatorControllerProtocol,
|
||||||
analytics: AnalyticsService) {
|
analytics: AnalyticsService,
|
||||||
|
appSettings: AppSettings) {
|
||||||
self.roomProxy = roomProxy
|
self.roomProxy = roomProxy
|
||||||
self.userSession = userSession
|
self.userSession = userSession
|
||||||
self.userIndicatorController = userIndicatorController
|
self.userIndicatorController = userIndicatorController
|
||||||
self.analytics = analytics
|
self.analytics = analytics
|
||||||
|
self.appSettings = appSettings
|
||||||
|
|
||||||
let initialViewState = RoomMemberDetailsScreenViewState(userID: userID, bindings: .init())
|
let initialViewState = RoomMemberDetailsScreenViewState(userID: userID, bindings: .init())
|
||||||
|
|
||||||
@@ -197,8 +200,19 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
|
|||||||
case .success(let roomID):
|
case .success(let roomID):
|
||||||
if let roomID {
|
if let roomID {
|
||||||
actionsSubject.send(.openDirectChat(roomID: roomID))
|
actionsSubject.send(.openDirectChat(roomID: roomID))
|
||||||
|
} else if appSettings.enableKeyShareOnInvite, roomProxy.details.historySharingState != RoomHistorySharingState.hidden {
|
||||||
|
Task {
|
||||||
|
let identity = await self.userSession.clientProxy.userIdentity(for: roomMemberProxy.userID, fallBackToServer: false)
|
||||||
|
let user: UserProfileProxy = .init(userID: roomMemberProxy.userID, displayName: roomMemberProxy.displayName, avatarURL: roomMemberProxy.avatarURL)
|
||||||
|
let isUnknown = if case .success(let identity) = identity {
|
||||||
|
identity == nil
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
self.state.bindings.inviteConfirmationUser = .init(user: user, isUnknown: isUnknown)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
state.bindings.inviteConfirmationUser = .init(userID: roomMemberProxy.userID, displayName: roomMemberProxy.displayName, avatarURL: roomMemberProxy.avatarURL)
|
state.bindings.inviteConfirmationUser = .init(user: .init(userID: roomMemberProxy.userID, displayName: roomMemberProxy.displayName, avatarURL: roomMemberProxy.avatarURL), isUnknown: false)
|
||||||
}
|
}
|
||||||
case .failure:
|
case .failure:
|
||||||
state.bindings.alertInfo = .init(id: .failedOpeningDirectChat)
|
state.bindings.alertInfo = .init(id: .failedOpeningDirectChat)
|
||||||
|
|||||||
@@ -240,6 +240,7 @@ struct RoomMemberDetailsScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
roomProxy: roomProxyMock,
|
roomProxy: roomProxyMock,
|
||||||
userSession: UserSessionMock(.init(clientProxy: clientProxyMock)),
|
userSession: UserSessionMock(.init(clientProxy: clientProxyMock)),
|
||||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||||
analytics: ServiceLocator.shared.analytics)
|
analytics: ServiceLocator.shared.analytics,
|
||||||
|
appSettings: ServiceLocator.shared.settings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ struct StartChatScreenViewStateBindings {
|
|||||||
/// Information describing the currently displayed alert.
|
/// Information describing the currently displayed alert.
|
||||||
var alertInfo: AlertInfo<StartChatScreenErrorType>?
|
var alertInfo: AlertInfo<StartChatScreenErrorType>?
|
||||||
|
|
||||||
var selectedUserToInvite: UserProfileProxy?
|
var selectedUserToInvite: UserToInvite?
|
||||||
var isJoinRoomByAddressSheetPresented = false
|
var isJoinRoomByAddressSheetPresented = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,8 +67,21 @@ class StartChatScreenViewModel: StartChatScreenViewModelType, StartChatScreenVie
|
|||||||
hideLoadingIndicator()
|
hideLoadingIndicator()
|
||||||
actionsSubject.send(.showRoom(roomID: roomId))
|
actionsSubject.send(.showRoom(roomID: roomId))
|
||||||
case .success:
|
case .success:
|
||||||
hideLoadingIndicator()
|
if appSettings.enableKeyShareOnInvite {
|
||||||
state.bindings.selectedUserToInvite = user
|
Task {
|
||||||
|
// If an error occured while fetching the identity, assume they are unknown.
|
||||||
|
let isUnknown = if case .success(let identity) = await self.userSession.clientProxy.userIdentity(for: user.userID, fallBackToServer: false) {
|
||||||
|
identity == nil
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
self.state.bindings.selectedUserToInvite = UserToInvite(user: user, isUnknown: isUnknown)
|
||||||
|
hideLoadingIndicator()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hideLoadingIndicator()
|
||||||
|
state.bindings.selectedUserToInvite = UserToInvite(user: user, isUnknown: false)
|
||||||
|
}
|
||||||
case .failure:
|
case .failure:
|
||||||
hideLoadingIndicator()
|
hideLoadingIndicator()
|
||||||
displayError()
|
displayError()
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import Compound
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct SendInviteConfirmationView: View {
|
struct SendInviteConfirmationView: View {
|
||||||
let userToInvite: UserProfileProxy
|
let userToInvite: UserToInvite
|
||||||
let mediaProvider: MediaProviderProtocol?
|
let mediaProvider: MediaProviderProtocol?
|
||||||
let onInvite: () -> Void
|
let onInvite: () -> Void
|
||||||
|
|
||||||
@@ -19,14 +19,26 @@ struct SendInviteConfirmationView: View {
|
|||||||
@State private var sheetHeight: CGFloat = .zero
|
@State private var sheetHeight: CGFloat = .zero
|
||||||
private let topPadding: CGFloat = 24
|
private let topPadding: CGFloat = 24
|
||||||
|
|
||||||
|
private var title: String {
|
||||||
|
if userToInvite.isUnknown {
|
||||||
|
L10n.screenBottomSheetCreateDmUnknownUserTitle
|
||||||
|
} else {
|
||||||
|
L10n.screenBottomSheetCreateDmTitle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var subtitle: String {
|
private var subtitle: String {
|
||||||
let string: String
|
let string: String
|
||||||
if let displayName = userToInvite.displayName {
|
if let displayName = userToInvite.displayName {
|
||||||
string = L10n.commonNameAndId(displayName, userToInvite.userID)
|
string = L10n.commonNameAndId(displayName, userToInvite.id)
|
||||||
} else {
|
} else {
|
||||||
string = userToInvite.userID
|
string = userToInvite.id
|
||||||
|
}
|
||||||
|
return if userToInvite.isUnknown {
|
||||||
|
L10n.screenBottomSheetCreateDmUnknownUserContent
|
||||||
|
} else {
|
||||||
|
L10n.screenBottomSheetCreateDmMessage(string)
|
||||||
}
|
}
|
||||||
return L10n.screenBottomSheetCreateDmMessage(string)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -48,11 +60,11 @@ struct SendInviteConfirmationView: View {
|
|||||||
VStack(spacing: 16) {
|
VStack(spacing: 16) {
|
||||||
LoadableAvatarImage(url: userToInvite.avatarURL,
|
LoadableAvatarImage(url: userToInvite.avatarURL,
|
||||||
name: userToInvite.displayName,
|
name: userToInvite.displayName,
|
||||||
contentID: userToInvite.userID,
|
contentID: userToInvite.id,
|
||||||
avatarSize: .user(on: .sendInviteConfirmation),
|
avatarSize: .user(on: .sendInviteConfirmation),
|
||||||
mediaProvider: mediaProvider)
|
mediaProvider: mediaProvider)
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
Text(L10n.screenBottomSheetCreateDmTitle)
|
Text(title)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.font(.compound.headingMDBold)
|
.font(.compound.headingMDBold)
|
||||||
.foregroundStyle(.compound.textPrimary)
|
.foregroundStyle(.compound.textPrimary)
|
||||||
@@ -91,7 +103,22 @@ struct SendInviteConfirmationView: View {
|
|||||||
|
|
||||||
struct SendInviteConfirmationView_Previews: PreviewProvider, TestablePreview {
|
struct SendInviteConfirmationView_Previews: PreviewProvider, TestablePreview {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
SendInviteConfirmationView(userToInvite: .mockBob,
|
SendInviteConfirmationView(userToInvite: .mockKnownBob,
|
||||||
mediaProvider: nil) { }
|
mediaProvider: nil) { }
|
||||||
|
.previewDisplayName("With Known Identity")
|
||||||
|
|
||||||
|
SendInviteConfirmationView(userToInvite: .mockUnknownBob,
|
||||||
|
mediaProvider: nil) { }
|
||||||
|
.previewDisplayName("With Unknown Identity")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension UserToInvite {
|
||||||
|
static var mockKnownBob: Self {
|
||||||
|
.init(user: .mockBob, isUnknown: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
static var mockUnknownBob: Self {
|
||||||
|
.init(user: .mockBob, isUnknown: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,9 +32,10 @@ struct StartChatScreen: View {
|
|||||||
disablesInteractiveDismiss: true)
|
disablesInteractiveDismiss: true)
|
||||||
.compoundSearchField()
|
.compoundSearchField()
|
||||||
.alert(item: $context.alertInfo)
|
.alert(item: $context.alertInfo)
|
||||||
.sheet(item: $context.selectedUserToInvite) { user in
|
.sheet(item: $context.selectedUserToInvite) { userToInvite in
|
||||||
SendInviteConfirmationView(userToInvite: user, mediaProvider: context.mediaProvider) {
|
SendInviteConfirmationView(userToInvite: userToInvite,
|
||||||
context.send(viewAction: .createDM(user: user))
|
mediaProvider: context.mediaProvider) {
|
||||||
|
context.send(viewAction: .createDM(user: userToInvite.user))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $context.isJoinRoomByAddressSheetPresented) {
|
.sheet(isPresented: $context.isJoinRoomByAddressSheetPresented) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ struct UserProfileScreenCoordinatorParameters {
|
|||||||
let userSession: UserSessionProtocol
|
let userSession: UserSessionProtocol
|
||||||
let userIndicatorController: UserIndicatorControllerProtocol
|
let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
let analytics: AnalyticsService
|
let analytics: AnalyticsService
|
||||||
|
let appSettings: AppSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
enum UserProfileScreenCoordinatorAction {
|
enum UserProfileScreenCoordinatorAction {
|
||||||
@@ -38,7 +39,8 @@ final class UserProfileScreenCoordinator: CoordinatorProtocol {
|
|||||||
isPresentedModally: parameters.isPresentedModally,
|
isPresentedModally: parameters.isPresentedModally,
|
||||||
userSession: parameters.userSession,
|
userSession: parameters.userSession,
|
||||||
userIndicatorController: parameters.userIndicatorController,
|
userIndicatorController: parameters.userIndicatorController,
|
||||||
analytics: parameters.analytics)
|
analytics: parameters.analytics,
|
||||||
|
appSettings: parameters.appSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() {
|
func start() {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ struct UserProfileScreenViewState: BindableState {
|
|||||||
let isPresentedModally: Bool
|
let isPresentedModally: Bool
|
||||||
|
|
||||||
var userProfile: UserProfileProxy?
|
var userProfile: UserProfileProxy?
|
||||||
|
var isIdentityKnown = false
|
||||||
var isVerified: Bool?
|
var isVerified: Bool?
|
||||||
var permalink: URL?
|
var permalink: URL?
|
||||||
var dmRoomID: String?
|
var dmRoomID: String?
|
||||||
@@ -33,7 +34,7 @@ struct UserProfileScreenViewState: BindableState {
|
|||||||
|
|
||||||
struct UserProfileScreenViewStateBindings {
|
struct UserProfileScreenViewStateBindings {
|
||||||
var alertInfo: AlertInfo<UserProfileScreenAlertType>?
|
var alertInfo: AlertInfo<UserProfileScreenAlertType>?
|
||||||
var inviteConfirmationUser: UserProfileProxy?
|
var inviteConfirmationUser: UserToInvite?
|
||||||
|
|
||||||
/// A media item that will be previewed with QuickLook.
|
/// A media item that will be previewed with QuickLook.
|
||||||
var mediaPreviewItem: MediaPreviewItem?
|
var mediaPreviewItem: MediaPreviewItem?
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr
|
|||||||
private let userSession: UserSessionProtocol
|
private let userSession: UserSessionProtocol
|
||||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
private let analytics: AnalyticsService
|
private let analytics: AnalyticsService
|
||||||
|
private let appSettings: AppSettings
|
||||||
|
|
||||||
private var actionsSubject: PassthroughSubject<UserProfileScreenViewModelAction, Never> = .init()
|
private var actionsSubject: PassthroughSubject<UserProfileScreenViewModelAction, Never> = .init()
|
||||||
var actionsPublisher: AnyPublisher<UserProfileScreenViewModelAction, Never> {
|
var actionsPublisher: AnyPublisher<UserProfileScreenViewModelAction, Never> {
|
||||||
@@ -26,10 +27,12 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr
|
|||||||
isPresentedModally: Bool,
|
isPresentedModally: Bool,
|
||||||
userSession: UserSessionProtocol,
|
userSession: UserSessionProtocol,
|
||||||
userIndicatorController: UserIndicatorControllerProtocol,
|
userIndicatorController: UserIndicatorControllerProtocol,
|
||||||
analytics: AnalyticsService) {
|
analytics: AnalyticsService,
|
||||||
|
appSettings: AppSettings) {
|
||||||
self.userSession = userSession
|
self.userSession = userSession
|
||||||
self.userIndicatorController = userIndicatorController
|
self.userIndicatorController = userIndicatorController
|
||||||
self.analytics = analytics
|
self.analytics = analytics
|
||||||
|
self.appSettings = appSettings
|
||||||
|
|
||||||
let initialViewState = UserProfileScreenViewState(userID: userID,
|
let initialViewState = UserProfileScreenViewState(userID: userID,
|
||||||
isOwnUser: userID == userSession.clientProxy.userID,
|
isOwnUser: userID == userSession.clientProxy.userID,
|
||||||
@@ -92,8 +95,10 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr
|
|||||||
}
|
}
|
||||||
|
|
||||||
if case let .success(.some(identity)) = await identityResult {
|
if case let .success(.some(identity)) = await identityResult {
|
||||||
|
state.isIdentityKnown = true
|
||||||
state.isVerified = identity.verificationState == .verified
|
state.isVerified = identity.verificationState == .verified
|
||||||
} else {
|
} else {
|
||||||
|
state.isIdentityKnown = false
|
||||||
MXLog.error("Failed to find the user's identity.")
|
MXLog.error("Failed to find the user's identity.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,7 +127,18 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr
|
|||||||
if let roomID {
|
if let roomID {
|
||||||
actionsSubject.send(.openDirectChat(roomID: roomID))
|
actionsSubject.send(.openDirectChat(roomID: roomID))
|
||||||
} else {
|
} else {
|
||||||
state.bindings.inviteConfirmationUser = userProfile
|
if appSettings.enableKeyShareOnInvite {
|
||||||
|
Task {
|
||||||
|
let isUnknown = if case let .success(identity) = await userSession.clientProxy.userIdentity(for: userProfile.userID, fallBackToServer: false) {
|
||||||
|
identity == nil
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
state.bindings.inviteConfirmationUser = .init(user: userProfile, isUnknown: isUnknown)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.bindings.inviteConfirmationUser = .init(user: userProfile, isUnknown: false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case .failure:
|
case .failure:
|
||||||
state.bindings.alertInfo = .init(id: .failedOpeningDirectChat)
|
state.bindings.alertInfo = .init(id: .failedOpeningDirectChat)
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ struct UserProfileScreen: View {
|
|||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar { toolbar }
|
.toolbar { toolbar }
|
||||||
.alert(item: $context.alertInfo)
|
.alert(item: $context.alertInfo)
|
||||||
.sheet(item: $context.inviteConfirmationUser) { user in
|
.sheet(item: $context.inviteConfirmationUser) { userToInvite in
|
||||||
SendInviteConfirmationView(userToInvite: user,
|
SendInviteConfirmationView(userToInvite: userToInvite,
|
||||||
mediaProvider: context.mediaProvider) {
|
mediaProvider: context.mediaProvider) {
|
||||||
context.send(viewAction: .createDirectChat)
|
context.send(viewAction: .createDirectChat)
|
||||||
}
|
}
|
||||||
@@ -147,6 +147,7 @@ struct UserProfileScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
isPresentedModally: false,
|
isPresentedModally: false,
|
||||||
userSession: UserSessionMock(.init(clientProxy: clientProxyMock)),
|
userSession: UserSessionMock(.init(clientProxy: clientProxyMock)),
|
||||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||||
analytics: ServiceLocator.shared.analytics)
|
analytics: ServiceLocator.shared.analytics,
|
||||||
|
appSettings: ServiceLocator.shared.settings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2026 Element Creations Ltd.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
// Please see LICENSE files in the repository root for full details.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct UserToInvite: Identifiable {
|
||||||
|
/// The profile of the user being invited.
|
||||||
|
var user: UserProfileProxy
|
||||||
|
|
||||||
|
/// The ID of the user being invited.
|
||||||
|
var id: String {
|
||||||
|
user.id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether we have the cryptographic identity of this user cached locally.
|
||||||
|
var isUnknown: Bool
|
||||||
|
|
||||||
|
/// The display name of the user being invited
|
||||||
|
var displayName: String? {
|
||||||
|
user.displayName
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The avatar URL of the user's profile, if available.
|
||||||
|
var avatarURL: URL? {
|
||||||
|
user.avatarURL
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -475,6 +475,14 @@ extension PreviewTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func inviteUsersConfirmationSheetView() async throws {
|
||||||
|
AppSettings.resetAllSettings() // Ensure this test's previews start with fresh settings.
|
||||||
|
for (index, preview) in InviteUsersConfirmationSheetView_Previews._allPreviews.enumerated() {
|
||||||
|
try await assertSnapshots(matching: preview, step: index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func inviteUsersScreenSelectedItem() async throws {
|
func inviteUsersScreenSelectedItem() async throws {
|
||||||
AppSettings.resetAllSettings() // Ensure this test's previews start with fresh settings.
|
AppSettings.resetAllSettings() // Ensure this test's previews start with fresh settings.
|
||||||
|
|||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:b1f7bbc91732d5bc2846a22b9c992705ba023ace7c89f038e69211010bf2830f
|
||||||
|
size 155722
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:1d873a384efccce301a79b01f5d549c39724a2b7798315ab0334eeaf2d73f947
|
||||||
|
size 144978
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:58cfabb92619e5d5f716d5299975ca4f2a5a38ba578a241c1fa29ef226e36a42
|
||||||
|
size 103461
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:0f219f191ab96ba0dc142114ea796a0fb0f9af30cc2ce8f29b99b0bb6718d7c3
|
||||||
|
size 96500
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:2b96b85d9a091da9515212b02ecdac23b23f2f4c9e5a1ee8e9bc68378a796cb3
|
||||||
|
size 152293
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:117598d6b54d6bd68492fd14f091ff3a436ec920d4e8ac62f83ebb899a66a334
|
||||||
|
size 156696
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:62680a39445805013d14963a26abfd65d192b96fedb321f5052c53e771575e0e
|
||||||
|
size 96073
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:507a257e16587137397a716f62f466d934416f1349b1f39de213c5e59b947beb
|
||||||
|
size 98954
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:1fa0aca5dc6c594440ac0992afb83cad577f002abc84b63aa753d0b5e0e4f78d
|
||||||
|
size 103351
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:26fac79b2439a30f6f4a3850e2105a7b396c82b63bac87c7147e0de85210844e
|
||||||
|
size 116096
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:b8235d8f2ce0f4f1187534209d3ab6ff7a21dcbd49f62b6ac2b8e6d19984c9af
|
||||||
|
size 61539
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:9deea49513c330d3605f484f16b87e525dc49ec9892980be73740ca14d69305f
|
||||||
|
size 84901
|
||||||
@@ -11,16 +11,28 @@ import Combine
|
|||||||
import Testing
|
import Testing
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
struct InviteUsersScreenViewModelTests {
|
final class InviteUsersScreenViewModelTests {
|
||||||
var viewModel: InviteUsersScreenViewModelProtocol!
|
var viewModel: InviteUsersScreenViewModelProtocol!
|
||||||
var userDiscoveryService: UserDiscoveryServiceMock!
|
var userDiscoveryService: UserDiscoveryServiceMock!
|
||||||
|
var clientProxy: ClientProxyMock!
|
||||||
|
var appSettings: AppSettings!
|
||||||
|
|
||||||
|
init() {
|
||||||
|
AppSettings.resetAllSettings()
|
||||||
|
appSettings = AppSettings()
|
||||||
|
ServiceLocator.shared.register(appSettings: appSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
AppSettings.resetAllSettings()
|
||||||
|
}
|
||||||
|
|
||||||
var context: InviteUsersScreenViewModel.Context {
|
var context: InviteUsersScreenViewModel.Context {
|
||||||
viewModel.context
|
viewModel.context
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
mutating func selectUser() {
|
func selectUser() {
|
||||||
let roomProxy = JoinedRoomProxyMock(.init(name: "newroom", members: []))
|
let roomProxy = JoinedRoomProxyMock(.init(name: "newroom", members: []))
|
||||||
roomProxy.inviteUserIDReturnValue = .success(())
|
roomProxy.inviteUserIDReturnValue = .success(())
|
||||||
setupViewModel(roomProxy: roomProxy, isSkippable: true)
|
setupViewModel(roomProxy: roomProxy, isSkippable: true)
|
||||||
@@ -32,7 +44,7 @@ struct InviteUsersScreenViewModelTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
mutating func reselectUser() {
|
func reselectUser() {
|
||||||
let roomProxy = JoinedRoomProxyMock(.init(name: "newroom", members: []))
|
let roomProxy = JoinedRoomProxyMock(.init(name: "newroom", members: []))
|
||||||
roomProxy.inviteUserIDReturnValue = .success(())
|
roomProxy.inviteUserIDReturnValue = .success(())
|
||||||
setupViewModel(roomProxy: roomProxy, isSkippable: true)
|
setupViewModel(roomProxy: roomProxy, isSkippable: true)
|
||||||
@@ -46,7 +58,7 @@ struct InviteUsersScreenViewModelTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
mutating func deselectUser() {
|
func deselectUser() {
|
||||||
let roomProxy = JoinedRoomProxyMock(.init(name: "newroom", members: []))
|
let roomProxy = JoinedRoomProxyMock(.init(name: "newroom", members: []))
|
||||||
roomProxy.inviteUserIDReturnValue = .success(())
|
roomProxy.inviteUserIDReturnValue = .success(())
|
||||||
setupViewModel(roomProxy: roomProxy, isSkippable: true)
|
setupViewModel(roomProxy: roomProxy, isSkippable: true)
|
||||||
@@ -60,7 +72,7 @@ struct InviteUsersScreenViewModelTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
mutating func inviteButton() async throws {
|
func inviteButton() async throws {
|
||||||
let mockedMembers: [RoomMemberProxyMock] = [.mockAlice, .mockBob]
|
let mockedMembers: [RoomMemberProxyMock] = [.mockAlice, .mockBob]
|
||||||
let roomProxy = JoinedRoomProxyMock(.init(name: "test", members: mockedMembers))
|
let roomProxy = JoinedRoomProxyMock(.init(name: "test", members: mockedMembers))
|
||||||
roomProxy.inviteUserIDReturnValue = .success(())
|
roomProxy.inviteUserIDReturnValue = .success(())
|
||||||
@@ -87,10 +99,82 @@ struct InviteUsersScreenViewModelTests {
|
|||||||
#expect(roomProxy.inviteUserIDReceivedInvocations == [RoomMemberProxyMock.mockAlice.userID])
|
#expect(roomProxy.inviteUserIDReceivedInvocations == [RoomMemberProxyMock.mockAlice.userID])
|
||||||
}
|
}
|
||||||
|
|
||||||
private mutating func setupViewModel(roomProxy: JoinedRoomProxyProtocol, isSkippable: Bool) {
|
// MARK: - History Sharing
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func invitingUnknownUsersOpensConfirmationDialog() async throws {
|
||||||
|
appSettings.enableKeyShareOnInvite = true
|
||||||
|
|
||||||
|
let mockedMembers: [RoomMemberProxyMock] = [.mockAlice, .mockBob]
|
||||||
|
let roomProxy = JoinedRoomProxyMock(.init(name: "test", members: mockedMembers))
|
||||||
|
roomProxy.inviteUserIDReturnValue = .success(())
|
||||||
|
setupViewModel(roomProxy: roomProxy, isSkippable: false)
|
||||||
|
|
||||||
|
// Mock the lack of cached user identity
|
||||||
|
clientProxy.userIdentityForFallBackToServerReturnValue = .success(nil)
|
||||||
|
|
||||||
|
let deferredState = deferFulfillment(viewModel.context.$viewState) { state in
|
||||||
|
state.isUserSelected(.mockAlice) && state.usersToConfirm.contains(.mockAlice)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.send(viewAction: .toggleUser(.mockAlice))
|
||||||
|
try await deferredState.fulfill()
|
||||||
|
|
||||||
|
context.send(viewAction: .proceed)
|
||||||
|
#expect(context.presentConfirmationDialog)
|
||||||
|
|
||||||
|
let deferredAction = deferFulfillment(viewModel.actions) { action in
|
||||||
|
switch action {
|
||||||
|
case .dismiss:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.send(viewAction: .confirmUnknownUsers)
|
||||||
|
|
||||||
|
try await deferredAction.fulfill()
|
||||||
|
#expect(roomProxy.inviteUserIDReceivedInvocations == [RoomMemberProxyMock.mockAlice.userID])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func removeButtonRemovesUnknownUsers() async throws {
|
||||||
|
appSettings.enableKeyShareOnInvite = true
|
||||||
|
|
||||||
|
let mockedMembers: [RoomMemberProxyMock] = [.mockAlice, .mockBob]
|
||||||
|
let roomProxy = JoinedRoomProxyMock(.init(name: "test", members: mockedMembers))
|
||||||
|
roomProxy.inviteUserIDReturnValue = .success(())
|
||||||
|
setupViewModel(roomProxy: roomProxy, isSkippable: false)
|
||||||
|
|
||||||
|
// Mock the lack of cached user identity
|
||||||
|
clientProxy.userIdentityForFallBackToServerReturnValue = .success(nil)
|
||||||
|
|
||||||
|
var deferredState = deferFulfillment(viewModel.context.$viewState) { state in
|
||||||
|
state.isUserSelected(.mockAlice) && state.usersToConfirm.contains(.mockAlice)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.send(viewAction: .toggleUser(.mockAlice))
|
||||||
|
try await deferredState.fulfill()
|
||||||
|
|
||||||
|
context.send(viewAction: .proceed)
|
||||||
|
#expect(context.presentConfirmationDialog)
|
||||||
|
|
||||||
|
deferredState = deferFulfillment(viewModel.context.$viewState) { state in
|
||||||
|
!state.usersToConfirm.contains(.mockAlice) && !state.selectedUsers.contains(.mockAlice)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.send(viewAction: .removeUnknownUsers)
|
||||||
|
try await deferredState.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
|
||||||
|
private func setupViewModel(roomProxy: JoinedRoomProxyProtocol, isSkippable: Bool) {
|
||||||
userDiscoveryService = UserDiscoveryServiceMock()
|
userDiscoveryService = UserDiscoveryServiceMock()
|
||||||
userDiscoveryService.searchProfilesWithReturnValue = .success([])
|
userDiscoveryService.searchProfilesWithReturnValue = .success([])
|
||||||
let viewModel = InviteUsersScreenViewModel(userSession: UserSessionMock(.init()),
|
|
||||||
|
clientProxy = ClientProxyMock(.init(userID: "@mock:client.com"))
|
||||||
|
|
||||||
|
let viewModel = InviteUsersScreenViewModel(userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
||||||
roomProxy: roomProxy,
|
roomProxy: roomProxy,
|
||||||
isSkippable: isSkippable,
|
isSkippable: isSkippable,
|
||||||
userDiscoveryService: userDiscoveryService,
|
userDiscoveryService: userDiscoveryService,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import Testing
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
struct RoomMemberDetailsViewModelTests {
|
struct RoomMemberDetailsViewModelTests {
|
||||||
|
var clientProxy: ClientProxy!
|
||||||
var viewModel: RoomMemberDetailsScreenViewModelProtocol!
|
var viewModel: RoomMemberDetailsScreenViewModelProtocol!
|
||||||
var roomProxyMock: JoinedRoomProxyMock!
|
var roomProxyMock: JoinedRoomProxyMock!
|
||||||
var roomMemberProxyMock: RoomMemberProxyMock!
|
var roomMemberProxyMock: RoomMemberProxyMock!
|
||||||
@@ -151,6 +152,52 @@ struct RoomMemberDetailsViewModelTests {
|
|||||||
#expect(context.ignoreUserAlert == nil)
|
#expect(context.ignoreUserAlert == nil)
|
||||||
#expect(context.alertInfo == nil)
|
#expect(context.alertInfo == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - History Sharing
|
||||||
|
|
||||||
|
@Test
|
||||||
|
mutating func inviteConfirmationFetchesIdentity() async throws {
|
||||||
|
let clientProxy = ClientProxyMock(.init())
|
||||||
|
setup(roomMemberProxyMock: .mockBob, clientProxy: clientProxy)
|
||||||
|
|
||||||
|
let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil }
|
||||||
|
try await waitForMemberToLoad.fulfill()
|
||||||
|
|
||||||
|
ServiceLocator.shared.settings.enableKeyShareOnInvite = true
|
||||||
|
clientProxy.directRoomForUserIDReturnValue = .success(nil)
|
||||||
|
clientProxy.userIdentityForFallBackToServerReturnValue = .success(UserIdentityProxyMock(configuration: .init(verificationState: .notVerified)))
|
||||||
|
|
||||||
|
// The user identity becomes known, i.e. not unknown.
|
||||||
|
let deferred = deferFulfillment(viewModel.context.$viewState.compactMap(\.bindings.inviteConfirmationUser)) {
|
||||||
|
!$0.isUnknown
|
||||||
|
}
|
||||||
|
context.send(viewAction: .openDirectChat)
|
||||||
|
try await deferred.fulfill()
|
||||||
|
|
||||||
|
#expect(clientProxy.userIdentityForFallBackToServerCalled)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
mutating func inviteConfirmationFallsBackToUnknownIdentityOnFailure() async throws {
|
||||||
|
let clientProxy = ClientProxyMock(.init())
|
||||||
|
setup(roomMemberProxyMock: .mockBob, clientProxy: clientProxy)
|
||||||
|
|
||||||
|
let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil }
|
||||||
|
try await waitForMemberToLoad.fulfill()
|
||||||
|
|
||||||
|
ServiceLocator.shared.settings.enableKeyShareOnInvite = true
|
||||||
|
clientProxy.directRoomForUserIDReturnValue = .success(nil)
|
||||||
|
clientProxy.userIdentityForFallBackToServerReturnValue = .failure(.forbiddenAccess)
|
||||||
|
|
||||||
|
// The user identity is always unknown, i.e. never not unknown.
|
||||||
|
let deferred = deferFailure(viewModel.context.$viewState.compactMap(\.bindings.inviteConfirmationUser), timeout: .seconds(5)) {
|
||||||
|
!$0.isUnknown
|
||||||
|
}
|
||||||
|
context.send(viewAction: .openDirectChat)
|
||||||
|
try await deferred.fulfill()
|
||||||
|
|
||||||
|
#expect(clientProxy.userIdentityForFallBackToServerCalled)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Helpers
|
// MARK: - Helpers
|
||||||
|
|
||||||
@@ -166,6 +213,7 @@ struct RoomMemberDetailsViewModelTests {
|
|||||||
roomProxy: roomProxyMock,
|
roomProxy: roomProxyMock,
|
||||||
userSession: userSession,
|
userSession: userSession,
|
||||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||||
analytics: ServiceLocator.shared.analytics)
|
analytics: ServiceLocator.shared.analytics,
|
||||||
|
appSettings: ServiceLocator.shared.settings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,40 @@ struct StartChatScreenViewModelTests {
|
|||||||
try await deferred.fulfill()
|
try await deferred.fulfill()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - History Sharing
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func inviteConfirmationFetchesIdentity() async throws {
|
||||||
|
ServiceLocator.shared.settings.enableKeyShareOnInvite = true
|
||||||
|
clientProxy.directRoomForUserIDReturnValue = .success(nil)
|
||||||
|
clientProxy.userIdentityForFallBackToServerReturnValue = .success(UserIdentityProxyMock(configuration: .init(verificationState: .notVerified)))
|
||||||
|
|
||||||
|
// User identity becomes known, i.e. not unknown
|
||||||
|
let deferred = deferFulfillment(viewModel.context.$viewState.compactMap(\.bindings.selectedUserToInvite)) {
|
||||||
|
!$0.isUnknown
|
||||||
|
}
|
||||||
|
context.send(viewAction: .selectUser(.mockBob))
|
||||||
|
try await deferred.fulfill()
|
||||||
|
|
||||||
|
#expect(clientProxy.userIdentityForFallBackToServerCalled)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func inviteConfirmationFallsBackToUnknownIdentityOnFailure() async throws {
|
||||||
|
ServiceLocator.shared.settings.enableKeyShareOnInvite = true
|
||||||
|
clientProxy.directRoomForUserIDReturnValue = .success(nil)
|
||||||
|
clientProxy.userIdentityForFallBackToServerReturnValue = .failure(.forbiddenAccess)
|
||||||
|
|
||||||
|
// User identity never becomes known, i.e. is never not unknown
|
||||||
|
let deferred = deferFailure(viewModel.context.$viewState.compactMap(\.bindings.selectedUserToInvite), timeout: .seconds(5)) {
|
||||||
|
!$0.isUnknown
|
||||||
|
}
|
||||||
|
context.send(viewAction: .selectUser(.mockBob))
|
||||||
|
try await deferred.fulfill()
|
||||||
|
|
||||||
|
#expect(clientProxy.userIdentityForFallBackToServerCalled)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func assertSearchResults(toBe count: Int) {
|
private func assertSearchResults(toBe count: Int) {
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ struct UserProfileScreenViewModelTests {
|
|||||||
isPresentedModally: false,
|
isPresentedModally: false,
|
||||||
userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
||||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||||
analytics: ServiceLocator.shared.analytics)
|
analytics: ServiceLocator.shared.analytics,
|
||||||
|
appSettings: ServiceLocator.shared.settings)
|
||||||
let context = viewModel.context
|
let context = viewModel.context
|
||||||
|
|
||||||
let waitForMemberToLoad = deferFulfillment(context.observe(\.viewState.userProfile)) { $0 != nil }
|
let waitForMemberToLoad = deferFulfillment(context.observe(\.viewState.userProfile)) { $0 != nil }
|
||||||
@@ -42,7 +43,8 @@ struct UserProfileScreenViewModelTests {
|
|||||||
isPresentedModally: false,
|
isPresentedModally: false,
|
||||||
userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
||||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||||
analytics: ServiceLocator.shared.analytics)
|
analytics: ServiceLocator.shared.analytics,
|
||||||
|
appSettings: ServiceLocator.shared.settings)
|
||||||
let context = viewModel.context
|
let context = viewModel.context
|
||||||
|
|
||||||
let waitForMemberToLoad = deferFulfillment(context.observe(\.viewState.userProfile)) { $0 != nil }
|
let waitForMemberToLoad = deferFulfillment(context.observe(\.viewState.userProfile)) { $0 != nil }
|
||||||
@@ -52,4 +54,32 @@ struct UserProfileScreenViewModelTests {
|
|||||||
#expect(context.viewState.userProfile == profile)
|
#expect(context.viewState.userProfile == profile)
|
||||||
#expect(context.viewState.permalink != nil)
|
#expect(context.viewState.permalink != nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func startingDmWithUnknownUserFetchesIdentity() async throws {
|
||||||
|
ServiceLocator.shared.settings.enableKeyShareOnInvite = true
|
||||||
|
|
||||||
|
let profile = UserProfileProxy.mockAlice
|
||||||
|
|
||||||
|
let clientProxy = ClientProxyMock(.init())
|
||||||
|
clientProxy.directRoomForUserIDReturnValue = .success(nil)
|
||||||
|
clientProxy.userIdentityForFallBackToServerReturnValue = .success(nil)
|
||||||
|
|
||||||
|
let viewModel = UserProfileScreenViewModel(userID: profile.userID,
|
||||||
|
isPresentedModally: false,
|
||||||
|
userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
||||||
|
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||||
|
analytics: ServiceLocator.shared.analytics,
|
||||||
|
appSettings: ServiceLocator.shared.settings)
|
||||||
|
|
||||||
|
let context = viewModel.context
|
||||||
|
|
||||||
|
let waitForMemberToLoad = deferFulfillment(context.observe(\.viewState.userProfile)) { $0 != nil }
|
||||||
|
try await waitForMemberToLoad.fulfill()
|
||||||
|
|
||||||
|
let deferred = deferFulfillment(context.observe(\.viewState.bindings).compactMap(\.inviteConfirmationUser), timeout: .seconds(5)) { $0.isUnknown }
|
||||||
|
|
||||||
|
context.send(viewAction: .openDirectChat)
|
||||||
|
try await deferred.fulfill()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user