From f13aa9c695caae713ca7b65efeb8d73f9e53b86d Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 31 Jan 2024 12:37:18 +1100 Subject: [PATCH] Added a dev setting to invite a group member by AccountID or ONS Added a dev setting to invite a group member by AccountID or ONS Removed the buggy Result autoclosure try init and using the default `Result(catching:)` one instead due to compiler issues --- .../Closed Groups/EditGroupViewModel.swift | 137 +++++++++++++++++- Session/Home/New Conversation/NewDMVC.swift | 2 +- .../Settings/DeveloperSettingsViewModel.swift | 27 +++- .../Sending & Receiving/MessageReceiver.swift | 2 +- .../Sending & Receiving/MessageSender.swift | 20 +-- .../SessionUtil/SessionUtil.swift | 2 +- .../MessageReceiverGroupsSpec.swift | 16 +- SessionUtilitiesKit/Database/Storage.swift | 2 +- SessionUtilitiesKit/General/Feature.swift | 4 + .../Utilities/Result+Utilities.swift | 5 - 10 files changed, 188 insertions(+), 29 deletions(-) diff --git a/Session/Closed Groups/EditGroupViewModel.swift b/Session/Closed Groups/EditGroupViewModel.swift index 11dac58a5..812cfb58f 100644 --- a/Session/Closed Groups/EditGroupViewModel.swift +++ b/Session/Closed Groups/EditGroupViewModel.swift @@ -31,6 +31,7 @@ class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, Editabl fileprivate var newGroupDescription: String? private var editDisplayPictureModal: ConfirmationModal? private var editDisplayPictureModalInfo: ConfirmationModal.Info? + private var inviteByIdValue: String? // MARK: - Initialization @@ -68,6 +69,7 @@ class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, Editabl case groupDescription case invite + case inviteById case member(String) } @@ -301,8 +303,20 @@ class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, Editabl label: "Invite Contacts" ), onTap: { [weak self] in self?.inviteContacts(currentGroupName: state.group.name) } + ), + (!isUpdatedGroup || !dependencies[feature: .updatedGroupsAllowInviteById] ? nil : + SessionCell.Info( + id: .inviteById, + leadingAccessory: .icon(UIImage(named: "ic_plus_24")?.withRenderingMode(.alwaysTemplate)), + title: "Invite Account ID or ONS", // FIXME: Localise this + accessibility: Accessibility( + identifier: "Invite by id", + label: "Invite by id" + ), + onTap: { [weak self] in self?.inviteById() } + ) ) - ] + ].compactMap { $0 } ), SectionModel( model: .members, @@ -777,6 +791,127 @@ class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, Editabl ) } + private func inviteById() { + // Convenience functions to avoid duplicate code + func showError(_ errorString: String) { + let modal: ConfirmationModal = ConfirmationModal( + info: ConfirmationModal.Info( + title: "ALERT_ERROR_TITLE".localized(), + body: .text(errorString), + cancelTitle: "BUTTON_OK".localized(), + cancelStyle: .alert_text, + dismissType: .single + ) + ) + self.transitionToScreen(modal, transitionType: .present) + } + func inviteMember(_ accountId: String, _ modal: UIViewController) { + guard !currentMemberIds.contains(accountId) else { + // FIXME: Localise this + return showError("This Account ID or ONS belongs to an existing member") + } + + MessageSender.addGroupMembers( + groupSessionId: threadId, + members: [(accountId, nil)], + allowAccessToHistoricMessages: dependencies[feature: .updatedGroupsAllowHistoricAccessOnInvite], + using: dependencies + ) + modal.dismiss(animated: true) { [weak self] in + self?.showToast( + text: "GROUP_ACTION_INVITE_SENDING".localized(), + backgroundColor: .backgroundSecondary + ) + } + } + + + let currentMemberIds: Set = (tableData + .first(where: { $0.model == .members })? + .elements + .compactMap { item -> String? in + switch item.id { + case .member(let profileId): return profileId + default: return nil + } + }) + .defaulting(to: []) + .asSet() + + // Make sure inviting another member wouldn't hit the member limit + guard (currentMemberIds.count + 1) <= SessionUtil.sizeMaxGroupMemberCount else { + return showError("vc_create_closed_group_too_many_group_members_error".localized()) + } + + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "Invite Account ID or ONS", // FIXME: Localise this + body: .input( + explanation: nil, + info: ConfirmationModal.Info.Body.InputInfo( + placeholder: "Enter Account ID or ONS" // FIXME: Localise this + ), + onChange: { [weak self] updatedString in self?.inviteByIdValue = updatedString } + ), + confirmTitle: "Invite", // FIXME: Localise this + confirmStyle: .danger, + cancelStyle: .alert_text, + dismissOnConfirm: false, + onConfirm: { [weak self] modal in + // FIXME: Consolidate this with the logic in `NewDMVC` + switch Result(catching: { try SessionId(from: self?.inviteByIdValue) }) { + case .success(let sessionId) where sessionId.prefix == .standard: + inviteMember(sessionId.hexString, modal) + + case .success(let sessionId) where (sessionId.prefix == .blinded15 || sessionId.prefix == .blinded25): + // FIXME: Localise this + return showError("Unable to invite members using their Blinded IDs") + + case .success: + // FIXME: Localise this + return showError("The value entered is not a valid Account ID or ONS") + + case .failure: + guard let inviteByIdValue: String = self?.inviteByIdValue else { + // FIXME: Localise this + return showError("Please enter a valid Account ID or ONS") + } + + // This could be an ONS name + let viewController = ModalActivityIndicatorViewController() { modalActivityIndicator in + SnodeAPI + .getSessionID(for: inviteByIdValue) + .subscribe(on: DispatchQueue.global(qos: .userInitiated)) + .receive(on: DispatchQueue.main) + .sinkUntilComplete( + receiveCompletion: { result in + switch result { + case .finished: break + case .failure: + modalActivityIndicator.dismiss { + // FIXME: Localise this + return showError("Unable to find ONS provided.") + } + } + }, + receiveValue: { sessionIdHexString in + modalActivityIndicator.dismiss { + inviteMember(sessionIdHexString, modal) + } + } + ) + } + self?.transitionToScreen(viewController, transitionType: .present) + } + }, + afterClosed: { [weak self] in self?.inviteByIdValue = nil } + ) + ), + transitionType: .present + ) + } + private func resendInvitation(memberId: String) { MessageSender.resendInvitation( groupSessionId: threadId, diff --git a/Session/Home/New Conversation/NewDMVC.swift b/Session/Home/New Conversation/NewDMVC.swift index 47a1094e9..66e284cd4 100644 --- a/Session/Home/New Conversation/NewDMVC.swift +++ b/Session/Home/New Conversation/NewDMVC.swift @@ -172,7 +172,7 @@ final class NewDMVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControlle } fileprivate func startNewDMIfPossible(with onsNameOrPublicKey: String, onError: (() -> ())?) { - switch Result(try SessionId(from: onsNameOrPublicKey)) { + switch Result(catching: { try SessionId(from: onsNameOrPublicKey) }) { case .success(let sessionId) where sessionId.prefix == .standard: startNewDM(with: onsNameOrPublicKey) case .success(let sessionId) where (sessionId.prefix == .blinded15 || sessionId.prefix == .blinded25): let modal: ConfirmationModal = ConfirmationModal( diff --git a/Session/Settings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettingsViewModel.swift index 7106ae2d7..8985f2831 100644 --- a/Session/Settings/DeveloperSettingsViewModel.swift +++ b/Session/Settings/DeveloperSettingsViewModel.swift @@ -70,6 +70,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case updatedGroupsAllowDisplayPicture case updatedGroupsAllowDescriptionEditing case updatedGroupsAllowPromotions + case updatedGroupsAllowInviteById case exportDatabase } @@ -92,6 +93,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, let updatedGroupsAllowDisplayPicture: Bool let updatedGroupsAllowDescriptionEditing: Bool let updatedGroupsAllowPromotions: Bool + let updatedGroupsAllowInviteById: Bool } let title: String = "Developer Settings" @@ -110,7 +112,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, updatedGroupsAllowHistoricAccessOnInvite: dependencies[feature: .updatedGroupsAllowHistoricAccessOnInvite], updatedGroupsAllowDisplayPicture: dependencies[feature: .updatedGroupsAllowDisplayPicture], updatedGroupsAllowDescriptionEditing: dependencies[feature: .updatedGroupsAllowDescriptionEditing], - updatedGroupsAllowPromotions: dependencies[feature: .updatedGroupsAllowPromotions] + updatedGroupsAllowPromotions: dependencies[feature: .updatedGroupsAllowPromotions], + updatedGroupsAllowInviteById: dependencies[feature: .updatedGroupsAllowInviteById] ) } .compactMapWithPrevious { [weak self] prev, current -> [SectionModel]? in self?.content(prev, current) } @@ -370,7 +373,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, id: .updatedGroupsAllowPromotions, title: "Allow Group Promotions", subtitle: """ - Controls whether the UI allows group admins promote other group members to admin within an updated group. + Controls whether the UI allows group admins to promote other group members to admin within an updated group. Note: In a future release we will offer this functionality but for the initial release it may not be fully supported across platforms so can be controlled via this flag for testing purposes. """, @@ -384,6 +387,25 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, to: !current.updatedGroupsAllowPromotions ) } + ), + SessionCell.Info( + id: .updatedGroupsAllowInviteById, + title: "Allow Invite by ID", + subtitle: """ + Controls whether the UI allows group admins to invlide other group members directly by their Account ID. + + Note: In a future release we will offer this functionality but it's not included in the initial release. + """, + trailingAccessory: .toggle( + current.updatedGroupsAllowInviteById, + oldValue: previous?.updatedGroupsAllowInviteById + ), + onTap: { [weak self] in + self?.updateFlag( + for: .updatedGroupsAllowInviteById, + to: !current.updatedGroupsAllowInviteById + ) + } ) ] ), @@ -435,6 +457,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case .updatedGroupsAllowDescriptionEditing: updateFlag(for: .updatedGroupsAllowDescriptionEditing, to: nil) case .updatedGroupsAllowPromotions: updateFlag(for: .updatedGroupsAllowPromotions, to: nil) + case .updatedGroupsAllowInviteById: updateFlag(for: .updatedGroupsAllowInviteById, to: nil) } } diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index 4b65d1d9d..be5fd375e 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -193,7 +193,7 @@ public enum MessageReceiver { } } - let proto: SNProtoContent = try (customProto ?? Result(SNProtoContent.parseData(plaintext)) + let proto: SNProtoContent = try (customProto ?? Result(catching: { try SNProtoContent.parseData(plaintext) }) .onFailure { SNLog("Couldn't parse proto due to error: \($0).") } .successOrThrow()) let message: Message = try (customMessage ?? Message.createMessageFrom(proto, sender: sender)) diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index fe7bed3ac..c5dbd202e 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -151,7 +151,7 @@ public final class MessageSender { throw MessageSenderError.protoConversionFailed } - return try Result(proto.serializedData()) + return try Result(catching: { try proto.serializedData() }) .map { serialisedData -> Data in switch destination { case .closedGroup(let groupId) where (try? SessionId.Prefix(from: groupId)) == .group: @@ -175,13 +175,13 @@ public final class MessageSender { using: dependencies ) - return try Result( - MessageWrapper.wrap( + return try Result(catching: { + try MessageWrapper.wrap( type: .sessionMessage, timestamp: sentTimestamp, base64EncodedContent: ciphertext.base64EncodedString() ) - ) + }) .mapError { MessageSenderError.other("Couldn't wrap message", $0) } .successOrThrow() .base64EncodedString() @@ -190,14 +190,14 @@ public final class MessageSender { case (.closedGroup(let groupId), .groupMessages) where (try? SessionId.Prefix(from: groupId)) == .group: return try SessionUtil .encrypt( - message: try Result( - MessageWrapper.wrap( + message: try Result(catching: { + try MessageWrapper.wrap( type: .closedGroupMessage, timestamp: sentTimestamp, base64EncodedContent: plaintext.base64EncodedString(), wrapInWebSocketMessage: false ) - ) + }) .mapError { MessageSenderError.other("Couldn't wrap message", $0) } .successOrThrow(), groupSessionId: SessionId(.group, hex: groupId), @@ -227,14 +227,14 @@ public final class MessageSender { using: dependencies ) - return try Result( - MessageWrapper.wrap( + return try Result(catching: { + try MessageWrapper.wrap( type: .closedGroupMessage, timestamp: sentTimestamp, senderPublicKey: groupPublicKey, // Needed for Android base64EncodedContent: ciphertext.base64EncodedString() ) - ) + }) .mapError { MessageSenderError.other("Couldn't wrap message", $0) } .successOrThrow() .base64EncodedString() diff --git a/SessionMessagingKit/SessionUtil/SessionUtil.swift b/SessionMessagingKit/SessionUtil/SessionUtil.swift index b6ea468a5..ec7570a63 100644 --- a/SessionMessagingKit/SessionUtil/SessionUtil.swift +++ b/SessionMessagingKit/SessionUtil/SessionUtil.swift @@ -300,7 +300,7 @@ public enum SessionUtil { // Check if the config needs to be pushed guard config.needsPush else { return nil } - return try Result(config.push(variant: variant)) + return try Result(catching: { try config.push(variant: variant) }) .onFailure { error in let configCountInfo: String = config.count(for: variant) diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift index 57cc716cd..8e710b38f 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift @@ -885,13 +885,15 @@ class MessageReceiverGroupsSpec: QuickSpec { .thenReturn(nil) mockStorage.write { db in - result = Result(try MessageReceiver.handleGroupUpdateMessage( - db, - threadId: groupId.hexString, - threadVariant: .group, - message: promoteMessage, - using: dependencies - )) + result = Result(catching: { + try MessageReceiver.handleGroupUpdateMessage( + db, + threadId: groupId.hexString, + threadVariant: .group, + message: promoteMessage, + using: dependencies + ) + }) } expect(result.failure).to(matchError(MessageReceiverError.invalidMessage)) diff --git a/SessionUtilitiesKit/Database/Storage.swift b/SessionUtilitiesKit/Database/Storage.swift index fb3f3cab9..384ef565a 100644 --- a/SessionUtilitiesKit/Database/Storage.swift +++ b/SessionUtilitiesKit/Database/Storage.swift @@ -348,7 +348,7 @@ open class Storage { } // Note: The non-async migration should only be used for unit tests - guard async else { return migrationCompleted(Result(try migrator.migrate(dbWriter))) } + guard async else { return migrationCompleted(Result(catching: { try migrator.migrate(dbWriter) })) } migrator.asyncMigrate(dbWriter) { result in let finalResult: Result = { diff --git a/SessionUtilitiesKit/General/Feature.swift b/SessionUtilitiesKit/General/Feature.swift index e752f74f9..acfe3cc19 100644 --- a/SessionUtilitiesKit/General/Feature.swift +++ b/SessionUtilitiesKit/General/Feature.swift @@ -53,6 +53,10 @@ public extension FeatureStorage { static let updatedGroupsAllowPromotions: FeatureConfig = Dependencies.create( identifier: "updatedGroupsAllowPromotions" ) + + static let updatedGroupsAllowInviteById: FeatureConfig = Dependencies.create( + identifier: "updatedGroupsAllowInviteById" + ) } // MARK: - FeatureOption diff --git a/SessionUtilitiesKit/Utilities/Result+Utilities.swift b/SessionUtilitiesKit/Utilities/Result+Utilities.swift index 69b3d0944..acfddfb75 100644 --- a/SessionUtilitiesKit/Utilities/Result+Utilities.swift +++ b/SessionUtilitiesKit/Utilities/Result+Utilities.swift @@ -3,11 +3,6 @@ import Foundation public extension Result where Failure == Error { - init(_ closure: @autoclosure () throws -> Success) { - do { self = Result.success(try closure()) } - catch { self = Result.failure(error) } - } - func onFailure(closure: (Failure) -> ()) -> Result { switch self { case .success: break