Files
bundesmessenger-ios/Riot/Modules/Room/ParticipantsInviteModal/ContactsPicker/ContactsPickerViewModel.swift
T
JanNiklas Grabowski b298dedc22 chore: update from foss 1.11.19 (MESSENGER-6656)
Merge commit 'f823ab9aae70e8d15ed7cc079210dd9bbbb6c8e1' into feature/foss_update_1_11_19

* commit 'f823ab9aae70e8d15ed7cc079210dd9bbbb6c8e1':
  finish version++
  version++
  comments
  update submodule
  remove obsolete tests
  removed unused code
  update submodule
  fix
  Libolm removal
  update license macro
  update license
  Prepare for new sprint

# Conflicts:
#	Config/AppVersion.xcconfig
#	IDETemplateMacros.plist
#	LICENSE
#	README.md
#	Riot/Categories/MXSession+Riot.m
#	Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift
#	Riot/Managers/KeyValueStorage/Extensions/Keychain.swift
#	Riot/Managers/KeyValueStorage/KeyValueStore.swift
#	Riot/Managers/KeyValueStorage/KeychainStore.swift
#	Riot/Managers/KeyValueStorage/MemoryStore.swift
#	Riot/Managers/PushNotification/PushNotificationService.m
#	Riot/Managers/Settings/RiotSettings.swift
#	Riot/Managers/Settings/Shared/RiotSharedSettings.swift
#	Riot/Modules/Analytics/AnalyticsUIElement.swift
#	Riot/Modules/Application/AppCoordinator.swift
#	Riot/Modules/Application/LegacyAppDelegate.h
#	Riot/Modules/Application/LegacyAppDelegate.m
#	Riot/Modules/Authentication/Legacy/AuthenticationViewController.h
#	Riot/Modules/Authentication/Legacy/AuthenticationViewController.m
#	Riot/Modules/Authentication/Legacy/Views/AuthInputsView.h
#	Riot/Modules/Authentication/Legacy/Views/AuthInputsView.m
#	Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m
#	Riot/Modules/Common/Recents/RecentsViewController.m
#	Riot/Modules/Common/WebViewController/WebViewViewController.m
#	Riot/Modules/Contacts/Details/ContactDetailsViewController.m
#	Riot/Modules/Contacts/Views/ContactTableViewCell.m
#	Riot/Modules/Favorites/FavouritesViewController.h
#	Riot/Modules/Favorites/FavouritesViewController.m
#	Riot/Modules/GlobalSearch/UnifiedSearchViewController.m
#	Riot/Modules/People/PeopleViewController.h
#	Riot/Modules/People/PeopleViewController.m
#	Riot/Modules/Room/ContextualMenu/ReactionsMenu/ReactionsMenuViewModel.swift
#	Riot/Modules/Room/DataSources/RoomDataSource.m
#	Riot/Modules/Room/Files/RoomFilesViewController.m
#	Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m
#	Riot/Modules/Room/Members/RoomParticipantsViewController.m
#	Riot/Modules/Room/RoomViewController.m
#	Riot/Modules/Room/Settings/RoomSettingsViewController.m
#	Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCell.swift
#	Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCellContentView.swift
#	Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroViewData.swift
#	Riot/Modules/Room/TimelineCells/RoomTimelineCellIdentifier.h
#	Riot/Modules/Rooms/RoomsViewController.h
#	Riot/Modules/Rooms/ShowDirectory/Cells/Network/DirectoryNetworkTableHeaderFooterView.swift
#	Riot/Modules/Rooms/ShowDirectory/Cells/Room/DirectoryRoomTableViewCell.swift
#	Riot/Modules/Rooms/ShowDirectory/PublicRoomsDirectoryViewModel.swift
#	Riot/Modules/Secrets/Recover/RecoverWithKey/SecretsRecoveryWithKeyCoordinator.swift
#	Riot/Modules/Secrets/Recover/RecoverWithKey/SecretsRecoveryWithKeyViewController.swift
#	Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseCoordinator.swift
#	Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseViewController.swift
#	Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinator.swift
#	Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewController.swift
#	Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewModel.swift
#	Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewModelType.swift
#	Riot/Modules/SetPinCode/PinCodePreferences.swift
#	Riot/Modules/SetPinCode/SetupBiometrics/BiometricsAuthenticationPresenter.swift
#	Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m
#	Riot/Modules/Settings/Security/SecurityViewController.m
#	Riot/Modules/Settings/SettingsViewController.m
#	Riot/Modules/SplitView/SplitViewCoordinator.swift
#	Riot/Modules/SplitView/SplitViewCoordinatorType.swift
#	Riot/Modules/StartChat/StartChatViewController.m
#	Riot/Modules/TabBar/MasterTabBarController.h
#	Riot/Modules/TabBar/MasterTabBarController.m
#	Riot/Utils/EventFormatter.m
#	Riot/Utils/HTMLFormatter.swift
#	Riot/Utils/Tools.m
#	RiotNSE/NotificationService.swift
2024-10-18 15:45:54 +02:00

359 lines
17 KiB
Swift

//
// Copyright 2021-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Foundation
class ContactsPickerViewModel: NSObject, ContactsPickerViewModelProtocol {
private class RoomMembers {
var actualParticipants: [Contact] = []
var invitedParticipants: [Contact] = []
var userParticipant: Contact?
}
// MARK: - Properties
weak var coordinatorDelegate: ContactsPickerViewModelCoordinatorDelegate?
private(set) var areParticipantsLoaded: Bool = false
// MARK: - Private
private let room: MXRoom
private var actualParticipants: [Contact]?
private var invitedParticipants: [Contact]?
private var userParticipant: Contact?
// MARK: - Setup
init(room: MXRoom, actualParticipants: [Contact]?, invitedParticipants: [Contact]?, userParticipant: Contact?) {
self.room = room
self.actualParticipants = actualParticipants
self.invitedParticipants = invitedParticipants
self.userParticipant = userParticipant
areParticipantsLoaded = actualParticipants != nil && invitedParticipants != nil && userParticipant != nil
super.init()
}
// MARK: - Public
func loadParticipants() {
coordinatorDelegate?.contactsPickerViewModelDidStartLoading(self)
let roomMembers = RoomMembers()
// Retrieve the current members from the room state
room.state { [weak self] roomState in
guard let self = self else {
return
}
guard let roomState = roomState, let members = roomState.members.membersWithoutConferenceUser(), let session = self.room.mxSession, let myUserId = session.myUserId, let roomThirdPartyInvites = roomState.thirdPartyInvites else {
self.finalize(participants: roomMembers)
return
}
for member in members {
if member.userId == myUserId {
if member.membership == .join || member.membership == .invite {
let displayName = VectorL10n.you
if let participant = Contact(matrixContactWithDisplayName: displayName, andMatrixID: myUserId) {
participant.mxMember = roomState.members.member(withUserId: myUserId)
roomMembers.userParticipant = participant
}
}
} else {
self.handle(roomMember: member, session: session, members: roomMembers)
}
}
for invite in roomThirdPartyInvites {
self.add(thirdPartyParticipant: invite, roomState: roomState, members: roomMembers)
}
self.finalize(participants: roomMembers)
}
}
func prepare(contactsViewController: RoomInviteViewController, currentSearchText: String?) -> Bool {
contactsViewController.room = self.room
// Set delegate to handle action on member (start chat, mention)
contactsViewController.contactsTableViewControllerDelegate = self
// Prepare its data source
guard let contactsDataSource = ContactsDataSource(matrixSession: room.mxSession) else {
MXLog.error("[ContactsPickerViewModel] prepare: failed to instantiate ContactsDataSource")
return false
}
contactsDataSource.areSectionsShrinkable = true
contactsDataSource.displaySearchInputInContactsList = true
contactsDataSource.forceMatrixIdInDisplayName = true
// Add a plus icon to the contact cell in the contacts picker, in order to make it more understandable for the end user.
contactsDataSource.contactCellAccessoryImage = Asset.Images.plusIcon.image.vc_tintedImage(usingColor: ThemeService.shared().theme.textPrimaryColor)
// List all the participants matrix user id to ignore them during the contacts search.
for contact in actualParticipants ?? [] {
if let userId = contact.mxMember.userId {
contactsDataSource.ignoredContactsByMatrixId[userId] = contact
}
}
for contact in invitedParticipants ?? [] {
if let userId = contact.mxMember?.userId {
contactsDataSource.ignoredContactsByMatrixId[userId] = contact
}
}
if let userParticipantId = self.userParticipant?.mxMember.userId {
contactsDataSource.ignoredContactsByMatrixId[userParticipantId] = userParticipant
}
contactsViewController.showSearch(true)
contactsViewController.searchBar.placeholder = BWIL10n.roomParticipantsInviteAnotherUser
contactsViewController.searchBar.resignFirstResponder()
// Apply the search pattern if any
if currentSearchText != nil {
contactsViewController.searchBar.text = currentSearchText
contactsDataSource.search(withPattern: currentSearchText, forceReset: true)
}
contactsViewController.displayList(contactsDataSource)
return true
}
// MARK: - Private
private func handle(roomMember: MXRoomMember, session: MXSession, members: RoomMembers) {
// Add this member after checking his status
guard roomMember.membership == .join || roomMember.membership == .invite else {
return
}
// Prepare the display name of this member
var displayName = roomMember.displayname
if displayName.isEmptyOrNil {
// Look for the corresponding MXUser in matrix session
if let user = session.user(withUserId: roomMember.userId) {
displayName = user.displayname.isEmptyOrNil ? user.userId : user.displayname
} else {
displayName = roomMember.userId
}
}
// Create the contact related to this member
if let contact = Contact(matrixContactWithDisplayName: displayName, andMatrixID: roomMember.userId) {
contact.mxMember = roomMember
if roomMember.membership == .invite {
members.invitedParticipants.append(contact)
} else {
members.actualParticipants.append(contact)
}
}
}
private func add(thirdPartyParticipant invite: MXRoomThirdPartyInvite, roomState: MXRoomState, members: RoomMembers) {
// If the homeserver has converted the 3pid invite into a room member, do no show it
// If the invite has been revoked (null display name), do not show it too.
guard let displayName = invite.displayname, roomState.member(withThirdPartyInviteToken: invite.token) == nil else {
return
}
if let contact = Contact(matrixContactWithDisplayName: displayName, andMatrixID: nil) {
contact.isThirdPartyInvite = true
contact.mxThirdPartyInvite = invite
members.invitedParticipants.append(contact)
}
}
private func finalize(participants roomMembers: RoomMembers) {
self.actualParticipants = roomMembers.actualParticipants
self.invitedParticipants = roomMembers.invitedParticipants
self.userParticipant = roomMembers.userParticipant
self.coordinatorDelegate?.contactsPickerViewModelDidEndLoading(self)
}
}
// MARK: - ContactsTableViewControllerDelegate
extension ContactsPickerViewModel: ContactsTableViewControllerDelegate {
func contactsTableViewController(_ contactsTableViewController: ContactsTableViewController!, didSelect contact: MXKContact?) {
guard let contact = contact else {
MXLog.error("[ContactsPickerViewModel] contactsTableViewController: nil contact found")
return
}
// bwi: #5386
if checkRoomFederationStatusForInvite(contact: contact) {
// Check for user
if MXTools.isMatrixUserIdentifier(contact.displayName) {
let user = MXUser(userId: contact.displayName)
coordinatorDelegate?.contactsPickerViewModelDidStartValidatingUser(self)
user?.update(fromHomeserverOfMatrixSession: self.room.mxSession, success: { [weak self] in
guard let self = self else { return }
self.coordinatorDelegate?.contactsPickerViewModelDidEndValidatingUser(self)
self.displayInvitePrompt(contact: contact)
}, failure: { [weak self] error in
guard let self = self else { return }
self.coordinatorDelegate?.contactsPickerViewModelDidEndValidatingUser(self)
self.displayInvitePrompt(contact: contact, isUnknownUser: true)
})
} else {
displayInvitePrompt(contact: contact)
}
}
}
private func displayInvitePrompt(contact: MXKContact, isUnknownUser: Bool = false) {
let roomName = room.displayName ?? VectorL10n.spaceTag
let message = isUnknownUser ? VectorL10n.roomParticipantsInviteUnknownParticipantPromptToMsg(contact.displayName, roomName) : VectorL10n.roomParticipantsInvitePromptToMsg(contact.displayName, roomName)
let inviteActionTitle = isUnknownUser ? VectorL10n.roomParticipantsInviteAnyway : VectorL10n.invite
coordinatorDelegate?.contactsPickerViewModel(self, display: message, title: VectorL10n.roomParticipantsInvitePromptTitle, actions: [
UIAlertAction(title: VectorL10n.cancel, style: .cancel),
UIAlertAction(title: VectorL10n.invite, style: .default, handler: { [weak self] _ in
self?.invite(contact: contact)
})
])
}
private func invite(contact: MXKContact) {
if let identifiers = contact.matrixIdentifiers as? [String], let participantId = identifiers.first {
// Invite this user if a room is defined
self.coordinatorDelegate?.contactsPickerViewModelDidStartInvite(self)
room.invite(.userId(participantId)) { [weak self] response in
guard let self = self else { return }
switch response {
case .success:
self.coordinatorDelegate?.contactsPickerViewModelDidEndInvite(self)
case .failure:
MXLog.error("[ContactsPickerViewModel] Failed to invite participant", context: response.error)
self.coordinatorDelegate?.contactsPickerViewModel(self, inviteFailedWithError: response.error)
}
}
} else {
let _participantId: String?
if let emailAddresses = contact.emailAddresses as? [MXKEmail], let email = emailAddresses.first {
// This is a local contact, consider the first email by default.
// TODO: Prompt the user to select the right email.
_participantId = email.emailAddress
} else {
// This is the text filled by the user.
_participantId = contact.displayName
}
guard let participantId = _participantId else {
MXLog.error("[ContactsPickerViewModel] invite: unexpectedly found participantId nil")
return
}
self.coordinatorDelegate?.contactsPickerViewModelDidStartInvite(self)
// Is it an email or a Matrix user ID?
if MXTools.isEmailAddress(participantId) {
room.invite(.email(participantId)) { [weak self] response in
guard let self = self else { return }
switch response {
case .success:
self.coordinatorDelegate?.contactsPickerViewModelDidEndInvite(self)
case .failure:
MXLog.error("[ContactsPickerViewModel] Failed to invite participant by email", context: response.error)
if let error = response.error as NSError?, error.domain == kMXRestClientErrorDomain, error.code == MXRestClientErrorMissingIdentityServer {
self.coordinatorDelegate?.contactsPickerViewModel(self, inviteFailedWithError: nil)
AppDelegate.theDelegate().showAlert(withTitle: VectorL10n.errorInvite3pidWithNoIdentityServer, message: nil)
} else {
self.coordinatorDelegate?.contactsPickerViewModel(self, inviteFailedWithError: response.error)
}
}
}
} else {
room.invite(.userId(participantId)) { [weak self] response in
guard let self = self else { return }
switch response {
case .success:
self.coordinatorDelegate?.contactsPickerViewModelDidEndInvite(self)
case .failure:
MXLog.error("[ContactsPickerViewModel] Failed to invite participant", context: response.error)
self.coordinatorDelegate?.contactsPickerViewModel(self, inviteFailedWithError: response.error)
}
}
}
}
}
/*
bwi: #5386 show error msg if federation is not configured or deactivated for this room
- if user is not federated -> display invite prompt
- if user federated -> Check:
- if server acl is configured and room is federated / serverACL = "*" -> display invite prompt
- if server acl is configured and room is not federated -> show error prompt
- if server acl is not configured -> show error prompt
*/
private func checkRoomFederationStatusForInvite(contact: MXKContact) -> Bool {
var canInvite: Bool = false
if BWIBuildSettings.shared.isFederationEnabled {
if let identifieres = contact.matrixIdentifiers {
if let identifiere = identifieres.first as? String {
// Check if user is federated
if room.isRoomMemberFederated(identifiere) {
// Check if room federation flag "isFederated" is true
room.getFederatedFlag { isFederated in
if isFederated {
// Get current serverACL settings for room
self.room.getCurrentRoomServerACLSettings { serverACL in
if let serverACL = serverACL {
if serverACL.elementsEqual("*") {
// Federation is active
canInvite = true
} else {
// Federation is deactivated
self.coordinatorDelegate?.contactsPickerViewModel(self, display: "", title: BWIL10n.roomParticipantsInvitePromptFederationForRoomNotAllowedText, actions:
[UIAlertAction(title: VectorL10n.ok, style: .cancel)])
}
} else {
// ServerACL not configured
self.coordinatorDelegate?.contactsPickerViewModel(self, display: "", title: BWIL10n.roomParticipantsInvitePromptServerAclForRoomNotConfiguredText, actions:
[UIAlertAction(title: VectorL10n.ok, style: .cancel)])
}
}
} else {
// Federation is deactivated
self.coordinatorDelegate?.contactsPickerViewModel(self, display: "", title: BWIL10n.roomParticipantsInvitePromptFederationForRoomNotAllowedText, actions:
[UIAlertAction(title: VectorL10n.ok, style: .cancel)])
}
}
} else {
canInvite = true
}
} else {
// Show error if federation cannot be determined
coordinatorDelegate?.contactsPickerViewModel(self, display: "", title: BWIL10n.roomParticipantsInvitePromptServerAclLoadingErrorText, actions: [
UIAlertAction(title: VectorL10n.ok, style: .cancel)
])
}
} else {
// Show error if federation cannot be determined
coordinatorDelegate?.contactsPickerViewModel(self, display: "", title: BWIL10n.roomParticipantsInvitePromptServerAclLoadingErrorText, actions: [
UIAlertAction(title: VectorL10n.ok, style: .cancel)
])
}
} else {
canInvite = true
}
return canInvite
}
}