mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +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:
@@ -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_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.";
|
||||
|
||||
@@ -791,7 +791,8 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol {
|
||||
isPresentedModally: true,
|
||||
userSession: userSession,
|
||||
userIndicatorController: flowParameters.userIndicatorController,
|
||||
analytics: flowParameters.analytics)
|
||||
analytics: flowParameters.analytics,
|
||||
appSettings: flowParameters.appSettings)
|
||||
let coordinator = UserProfileScreenCoordinator(parameters: parameters)
|
||||
coordinator.actionsPublisher.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
@@ -225,7 +225,8 @@ final class RoomMembersFlowCoordinator: FlowCoordinatorProtocol {
|
||||
roomProxy: roomProxy,
|
||||
userSession: flowParameters.userSession,
|
||||
userIndicatorController: flowParameters.userIndicatorController,
|
||||
analytics: flowParameters.analytics)
|
||||
analytics: flowParameters.analytics,
|
||||
appSettings: flowParameters.appSettings)
|
||||
let coordinator = RoomMemberDetailsScreenCoordinator(parameters: params)
|
||||
|
||||
coordinator.actions.sink { [weak self] action in
|
||||
@@ -286,7 +287,8 @@ final class RoomMembersFlowCoordinator: FlowCoordinatorProtocol {
|
||||
isPresentedModally: false,
|
||||
userSession: flowParameters.userSession,
|
||||
userIndicatorController: flowParameters.userIndicatorController,
|
||||
analytics: flowParameters.analytics)
|
||||
analytics: flowParameters.analytics,
|
||||
appSettings: flowParameters.appSettings)
|
||||
let coordinator = UserProfileScreenCoordinator(parameters: parameters)
|
||||
coordinator.actionsPublisher.sink { [weak self] action in
|
||||
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 nesting type_body_length type_name vertical_whitespace_opening_braces
|
||||
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?
|
||||
/// 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") }
|
||||
|
||||
@@ -67,6 +67,7 @@ enum TestablePreviewsDictionary {
|
||||
"IdentityConfirmedScreen_Previews" : IdentityConfirmedScreen_Previews.self,
|
||||
"ImageMediaEventsTimelineView_Previews" : ImageMediaEventsTimelineView_Previews.self,
|
||||
"ImageRoomTimelineView_Previews" : ImageRoomTimelineView_Previews.self,
|
||||
"InviteUsersConfirmationSheetView_Previews" : InviteUsersConfirmationSheetView_Previews.self,
|
||||
"InviteUsersScreenSelectedItem_Previews" : InviteUsersScreenSelectedItem_Previews.self,
|
||||
"InviteUsersScreen_Previews" : InviteUsersScreen_Previews.self,
|
||||
"JoinRoomByAddressView_Previews" : JoinRoomByAddressView_Previews.self,
|
||||
|
||||
@@ -30,6 +30,7 @@ struct InviteUsersScreenViewState: BindableState {
|
||||
|
||||
var selectedUsers: [UserProfileProxy] = []
|
||||
var membershipState: [String: MembershipState] = .init()
|
||||
var usersToConfirm: [UserProfileProxy] = []
|
||||
|
||||
var isSearching = false
|
||||
|
||||
@@ -69,6 +70,9 @@ struct InviteUsersScreenViewStateBindings {
|
||||
var searchQuery = ""
|
||||
var selectedUsersPosition: String?
|
||||
|
||||
/// Whether we are showing the confirmation dialog.
|
||||
var presentConfirmationDialog = false
|
||||
|
||||
/// Information describing the currently displayed alert.
|
||||
var alertInfo: AlertInfo<InviteUsersScreenErrorType>?
|
||||
}
|
||||
@@ -76,5 +80,7 @@ struct InviteUsersScreenViewStateBindings {
|
||||
enum InviteUsersScreenViewAction {
|
||||
case cancel
|
||||
case proceed
|
||||
case removeUnknownUsers
|
||||
case confirmUnknownUsers
|
||||
case toggleUser(UserProfileProxy)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import SwiftUI
|
||||
typealias InviteUsersScreenViewModelType = StateStoreViewModel<InviteUsersScreenViewState, InviteUsersScreenViewAction>
|
||||
|
||||
class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScreenViewModelProtocol {
|
||||
private let clientProxy: ClientProxyProtocol
|
||||
private let roomProxy: JoinedRoomProxyProtocol
|
||||
private let userDiscoveryService: UserDiscoveryServiceProtocol
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
@@ -31,6 +32,7 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
||||
userDiscoveryService: UserDiscoveryServiceProtocol,
|
||||
userIndicatorController: UserIndicatorControllerProtocol,
|
||||
appSettings: AppSettings) {
|
||||
clientProxy = userSession.clientProxy
|
||||
self.roomProxy = roomProxy
|
||||
self.userDiscoveryService = userDiscoveryService
|
||||
self.userIndicatorController = userIndicatorController
|
||||
@@ -59,6 +61,23 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
||||
case .cancel:
|
||||
actionsSubject.send(.dismiss)
|
||||
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)
|
||||
case .toggleUser(let user):
|
||||
toggleUser(user)
|
||||
@@ -73,6 +92,17 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
||||
} else {
|
||||
state.selectedUsers.append(user)
|
||||
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,
|
||||
accessibilityFocusOnStart: true)
|
||||
.compoundSearchField()
|
||||
.sheet(isPresented: $context.presentConfirmationDialog) {
|
||||
InviteUsersConfirmationSheetView(context: context, users: context.viewState.usersToConfirm)
|
||||
}
|
||||
.alert(item: $context.alertInfo)
|
||||
.navigationBarBackButtonHidden(context.viewState.isSkippable)
|
||||
}
|
||||
@@ -149,6 +152,7 @@ struct InviteUsersScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = makeViewModel()
|
||||
static let searchingViewModel = makeViewModel(searchQuery: "Alice")
|
||||
static let selectedViewModel = makeViewModel(hasSelection: true)
|
||||
static let confirmSelectedViewModel = makeViewModel(shouldConfirm: true)
|
||||
|
||||
static var previews: some View {
|
||||
ElementNavigationStack {
|
||||
@@ -170,9 +174,14 @@ struct InviteUsersScreen_Previews: PreviewProvider, TestablePreview {
|
||||
}
|
||||
.previewDisplayName("Selected")
|
||||
.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())
|
||||
clientProxy.recentConversationCounterpartsReturnValue = [.mockAlice, .mockBob, .mockCharlie, .mockDan, .mockVerbose]
|
||||
|
||||
@@ -194,6 +203,11 @@ struct InviteUsersScreen_Previews: PreviewProvider, TestablePreview {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -15,6 +15,7 @@ struct RoomMemberDetailsScreenCoordinatorParameters {
|
||||
let userSession: UserSessionProtocol
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
let analytics: AnalyticsService
|
||||
let appSettings: AppSettings
|
||||
}
|
||||
|
||||
enum RoomMemberDetailsScreenCoordinatorAction {
|
||||
@@ -39,7 +40,8 @@ final class RoomMemberDetailsScreenCoordinator: CoordinatorProtocol {
|
||||
roomProxy: parameters.roomProxy,
|
||||
userSession: parameters.userSession,
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
analytics: parameters.analytics)
|
||||
analytics: parameters.analytics,
|
||||
appSettings: parameters.appSettings)
|
||||
}
|
||||
|
||||
func start() {
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ struct RoomMemberDetailsScreenViewStateBindings {
|
||||
|
||||
var ignoreUserAlert: IgnoreUserAlertItem?
|
||||
var alertInfo: AlertInfo<RoomMemberDetailsScreenAlertType>?
|
||||
var inviteConfirmationUser: UserProfileProxy?
|
||||
var inviteConfirmationUser: UserToInvite?
|
||||
|
||||
/// A media item that will be previewed with QuickLook.
|
||||
var mediaPreviewItem: MediaPreviewItem?
|
||||
|
||||
+16
-2
@@ -16,6 +16,7 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
|
||||
private let userSession: UserSessionProtocol
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
private let analytics: AnalyticsService
|
||||
private let appSettings: AppSettings
|
||||
|
||||
private var actionsSubject: PassthroughSubject<RoomMemberDetailsScreenViewModelAction, Never> = .init()
|
||||
|
||||
@@ -29,11 +30,13 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
|
||||
roomProxy: JoinedRoomProxyProtocol,
|
||||
userSession: UserSessionProtocol,
|
||||
userIndicatorController: UserIndicatorControllerProtocol,
|
||||
analytics: AnalyticsService) {
|
||||
analytics: AnalyticsService,
|
||||
appSettings: AppSettings) {
|
||||
self.roomProxy = roomProxy
|
||||
self.userSession = userSession
|
||||
self.userIndicatorController = userIndicatorController
|
||||
self.analytics = analytics
|
||||
self.appSettings = appSettings
|
||||
|
||||
let initialViewState = RoomMemberDetailsScreenViewState(userID: userID, bindings: .init())
|
||||
|
||||
@@ -197,8 +200,19 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
|
||||
case .success(let roomID):
|
||||
if let 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 {
|
||||
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:
|
||||
state.bindings.alertInfo = .init(id: .failedOpeningDirectChat)
|
||||
|
||||
@@ -240,6 +240,7 @@ struct RoomMemberDetailsScreen_Previews: PreviewProvider, TestablePreview {
|
||||
roomProxy: roomProxyMock,
|
||||
userSession: UserSessionMock(.init(clientProxy: clientProxyMock)),
|
||||
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.
|
||||
var alertInfo: AlertInfo<StartChatScreenErrorType>?
|
||||
|
||||
var selectedUserToInvite: UserProfileProxy?
|
||||
var selectedUserToInvite: UserToInvite?
|
||||
var isJoinRoomByAddressSheetPresented = false
|
||||
}
|
||||
|
||||
|
||||
@@ -67,8 +67,21 @@ class StartChatScreenViewModel: StartChatScreenViewModelType, StartChatScreenVie
|
||||
hideLoadingIndicator()
|
||||
actionsSubject.send(.showRoom(roomID: roomId))
|
||||
case .success:
|
||||
hideLoadingIndicator()
|
||||
state.bindings.selectedUserToInvite = user
|
||||
if appSettings.enableKeyShareOnInvite {
|
||||
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:
|
||||
hideLoadingIndicator()
|
||||
displayError()
|
||||
|
||||
@@ -10,7 +10,7 @@ import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct SendInviteConfirmationView: View {
|
||||
let userToInvite: UserProfileProxy
|
||||
let userToInvite: UserToInvite
|
||||
let mediaProvider: MediaProviderProtocol?
|
||||
let onInvite: () -> Void
|
||||
|
||||
@@ -19,14 +19,26 @@ struct SendInviteConfirmationView: View {
|
||||
@State private var sheetHeight: CGFloat = .zero
|
||||
private let topPadding: CGFloat = 24
|
||||
|
||||
private var title: String {
|
||||
if userToInvite.isUnknown {
|
||||
L10n.screenBottomSheetCreateDmUnknownUserTitle
|
||||
} else {
|
||||
L10n.screenBottomSheetCreateDmTitle
|
||||
}
|
||||
}
|
||||
|
||||
private var subtitle: String {
|
||||
let string: String
|
||||
if let displayName = userToInvite.displayName {
|
||||
string = L10n.commonNameAndId(displayName, userToInvite.userID)
|
||||
string = L10n.commonNameAndId(displayName, userToInvite.id)
|
||||
} 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 {
|
||||
@@ -48,11 +60,11 @@ struct SendInviteConfirmationView: View {
|
||||
VStack(spacing: 16) {
|
||||
LoadableAvatarImage(url: userToInvite.avatarURL,
|
||||
name: userToInvite.displayName,
|
||||
contentID: userToInvite.userID,
|
||||
contentID: userToInvite.id,
|
||||
avatarSize: .user(on: .sendInviteConfirmation),
|
||||
mediaProvider: mediaProvider)
|
||||
VStack(spacing: 8) {
|
||||
Text(L10n.screenBottomSheetCreateDmTitle)
|
||||
Text(title)
|
||||
.multilineTextAlignment(.center)
|
||||
.font(.compound.headingMDBold)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
@@ -91,7 +103,22 @@ struct SendInviteConfirmationView: View {
|
||||
|
||||
struct SendInviteConfirmationView_Previews: PreviewProvider, TestablePreview {
|
||||
static var previews: some View {
|
||||
SendInviteConfirmationView(userToInvite: .mockBob,
|
||||
SendInviteConfirmationView(userToInvite: .mockKnownBob,
|
||||
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)
|
||||
.compoundSearchField()
|
||||
.alert(item: $context.alertInfo)
|
||||
.sheet(item: $context.selectedUserToInvite) { user in
|
||||
SendInviteConfirmationView(userToInvite: user, mediaProvider: context.mediaProvider) {
|
||||
context.send(viewAction: .createDM(user: user))
|
||||
.sheet(item: $context.selectedUserToInvite) { userToInvite in
|
||||
SendInviteConfirmationView(userToInvite: userToInvite,
|
||||
mediaProvider: context.mediaProvider) {
|
||||
context.send(viewAction: .createDM(user: userToInvite.user))
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $context.isJoinRoomByAddressSheetPresented) {
|
||||
|
||||
@@ -15,6 +15,7 @@ struct UserProfileScreenCoordinatorParameters {
|
||||
let userSession: UserSessionProtocol
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
let analytics: AnalyticsService
|
||||
let appSettings: AppSettings
|
||||
}
|
||||
|
||||
enum UserProfileScreenCoordinatorAction {
|
||||
@@ -38,7 +39,8 @@ final class UserProfileScreenCoordinator: CoordinatorProtocol {
|
||||
isPresentedModally: parameters.isPresentedModally,
|
||||
userSession: parameters.userSession,
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
analytics: parameters.analytics)
|
||||
analytics: parameters.analytics,
|
||||
appSettings: parameters.appSettings)
|
||||
}
|
||||
|
||||
func start() {
|
||||
|
||||
@@ -20,6 +20,7 @@ struct UserProfileScreenViewState: BindableState {
|
||||
let isPresentedModally: Bool
|
||||
|
||||
var userProfile: UserProfileProxy?
|
||||
var isIdentityKnown = false
|
||||
var isVerified: Bool?
|
||||
var permalink: URL?
|
||||
var dmRoomID: String?
|
||||
@@ -33,7 +34,7 @@ struct UserProfileScreenViewState: BindableState {
|
||||
|
||||
struct UserProfileScreenViewStateBindings {
|
||||
var alertInfo: AlertInfo<UserProfileScreenAlertType>?
|
||||
var inviteConfirmationUser: UserProfileProxy?
|
||||
var inviteConfirmationUser: UserToInvite?
|
||||
|
||||
/// A media item that will be previewed with QuickLook.
|
||||
var mediaPreviewItem: MediaPreviewItem?
|
||||
|
||||
@@ -16,6 +16,7 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr
|
||||
private let userSession: UserSessionProtocol
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
private let analytics: AnalyticsService
|
||||
private let appSettings: AppSettings
|
||||
|
||||
private var actionsSubject: PassthroughSubject<UserProfileScreenViewModelAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<UserProfileScreenViewModelAction, Never> {
|
||||
@@ -26,10 +27,12 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr
|
||||
isPresentedModally: Bool,
|
||||
userSession: UserSessionProtocol,
|
||||
userIndicatorController: UserIndicatorControllerProtocol,
|
||||
analytics: AnalyticsService) {
|
||||
analytics: AnalyticsService,
|
||||
appSettings: AppSettings) {
|
||||
self.userSession = userSession
|
||||
self.userIndicatorController = userIndicatorController
|
||||
self.analytics = analytics
|
||||
self.appSettings = appSettings
|
||||
|
||||
let initialViewState = UserProfileScreenViewState(userID: userID,
|
||||
isOwnUser: userID == userSession.clientProxy.userID,
|
||||
@@ -92,8 +95,10 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr
|
||||
}
|
||||
|
||||
if case let .success(.some(identity)) = await identityResult {
|
||||
state.isIdentityKnown = true
|
||||
state.isVerified = identity.verificationState == .verified
|
||||
} else {
|
||||
state.isIdentityKnown = false
|
||||
MXLog.error("Failed to find the user's identity.")
|
||||
}
|
||||
}
|
||||
@@ -122,7 +127,18 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr
|
||||
if let roomID {
|
||||
actionsSubject.send(.openDirectChat(roomID: roomID))
|
||||
} 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:
|
||||
state.bindings.alertInfo = .init(id: .failedOpeningDirectChat)
|
||||
|
||||
@@ -21,8 +21,8 @@ struct UserProfileScreen: View {
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar { toolbar }
|
||||
.alert(item: $context.alertInfo)
|
||||
.sheet(item: $context.inviteConfirmationUser) { user in
|
||||
SendInviteConfirmationView(userToInvite: user,
|
||||
.sheet(item: $context.inviteConfirmationUser) { userToInvite in
|
||||
SendInviteConfirmationView(userToInvite: userToInvite,
|
||||
mediaProvider: context.mediaProvider) {
|
||||
context.send(viewAction: .createDirectChat)
|
||||
}
|
||||
@@ -147,6 +147,7 @@ struct UserProfileScreen_Previews: PreviewProvider, TestablePreview {
|
||||
isPresentedModally: false,
|
||||
userSession: UserSessionMock(.init(clientProxy: clientProxyMock)),
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user