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
pull/894/head
Morgan Pretty 1 year ago
parent 2fe48033fd
commit f13aa9c695

@ -31,6 +31,7 @@ class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, Editabl
fileprivate var newGroupDescription: String? fileprivate var newGroupDescription: String?
private var editDisplayPictureModal: ConfirmationModal? private var editDisplayPictureModal: ConfirmationModal?
private var editDisplayPictureModalInfo: ConfirmationModal.Info? private var editDisplayPictureModalInfo: ConfirmationModal.Info?
private var inviteByIdValue: String?
// MARK: - Initialization // MARK: - Initialization
@ -68,6 +69,7 @@ class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, Editabl
case groupDescription case groupDescription
case invite case invite
case inviteById
case member(String) case member(String)
} }
@ -301,8 +303,20 @@ class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, Editabl
label: "Invite Contacts" label: "Invite Contacts"
), ),
onTap: { [weak self] in self?.inviteContacts(currentGroupName: state.group.name) } 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( SectionModel(
model: .members, 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<String> = (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) { private func resendInvitation(memberId: String) {
MessageSender.resendInvitation( MessageSender.resendInvitation(
groupSessionId: threadId, groupSessionId: threadId,

@ -172,7 +172,7 @@ final class NewDMVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControlle
} }
fileprivate func startNewDMIfPossible(with onsNameOrPublicKey: String, onError: (() -> ())?) { 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 == .standard: startNewDM(with: onsNameOrPublicKey)
case .success(let sessionId) where (sessionId.prefix == .blinded15 || sessionId.prefix == .blinded25): case .success(let sessionId) where (sessionId.prefix == .blinded15 || sessionId.prefix == .blinded25):
let modal: ConfirmationModal = ConfirmationModal( let modal: ConfirmationModal = ConfirmationModal(

@ -70,6 +70,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
case updatedGroupsAllowDisplayPicture case updatedGroupsAllowDisplayPicture
case updatedGroupsAllowDescriptionEditing case updatedGroupsAllowDescriptionEditing
case updatedGroupsAllowPromotions case updatedGroupsAllowPromotions
case updatedGroupsAllowInviteById
case exportDatabase case exportDatabase
} }
@ -92,6 +93,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
let updatedGroupsAllowDisplayPicture: Bool let updatedGroupsAllowDisplayPicture: Bool
let updatedGroupsAllowDescriptionEditing: Bool let updatedGroupsAllowDescriptionEditing: Bool
let updatedGroupsAllowPromotions: Bool let updatedGroupsAllowPromotions: Bool
let updatedGroupsAllowInviteById: Bool
} }
let title: String = "Developer Settings" let title: String = "Developer Settings"
@ -110,7 +112,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
updatedGroupsAllowHistoricAccessOnInvite: dependencies[feature: .updatedGroupsAllowHistoricAccessOnInvite], updatedGroupsAllowHistoricAccessOnInvite: dependencies[feature: .updatedGroupsAllowHistoricAccessOnInvite],
updatedGroupsAllowDisplayPicture: dependencies[feature: .updatedGroupsAllowDisplayPicture], updatedGroupsAllowDisplayPicture: dependencies[feature: .updatedGroupsAllowDisplayPicture],
updatedGroupsAllowDescriptionEditing: dependencies[feature: .updatedGroupsAllowDescriptionEditing], 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) } .compactMapWithPrevious { [weak self] prev, current -> [SectionModel]? in self?.content(prev, current) }
@ -370,7 +373,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
id: .updatedGroupsAllowPromotions, id: .updatedGroupsAllowPromotions,
title: "Allow Group Promotions", title: "Allow Group Promotions",
subtitle: """ 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.
<b>Note:</b> 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. <b>Note:</b> 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 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.
<b>Note:</b> 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: case .updatedGroupsAllowDescriptionEditing:
updateFlag(for: .updatedGroupsAllowDescriptionEditing, to: nil) updateFlag(for: .updatedGroupsAllowDescriptionEditing, to: nil)
case .updatedGroupsAllowPromotions: updateFlag(for: .updatedGroupsAllowPromotions, to: nil) case .updatedGroupsAllowPromotions: updateFlag(for: .updatedGroupsAllowPromotions, to: nil)
case .updatedGroupsAllowInviteById: updateFlag(for: .updatedGroupsAllowInviteById, to: nil)
} }
} }

@ -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).") } .onFailure { SNLog("Couldn't parse proto due to error: \($0).") }
.successOrThrow()) .successOrThrow())
let message: Message = try (customMessage ?? Message.createMessageFrom(proto, sender: sender)) let message: Message = try (customMessage ?? Message.createMessageFrom(proto, sender: sender))

@ -151,7 +151,7 @@ public final class MessageSender {
throw MessageSenderError.protoConversionFailed throw MessageSenderError.protoConversionFailed
} }
return try Result(proto.serializedData()) return try Result(catching: { try proto.serializedData() })
.map { serialisedData -> Data in .map { serialisedData -> Data in
switch destination { switch destination {
case .closedGroup(let groupId) where (try? SessionId.Prefix(from: groupId)) == .group: case .closedGroup(let groupId) where (try? SessionId.Prefix(from: groupId)) == .group:
@ -175,13 +175,13 @@ public final class MessageSender {
using: dependencies using: dependencies
) )
return try Result( return try Result(catching: {
MessageWrapper.wrap( try MessageWrapper.wrap(
type: .sessionMessage, type: .sessionMessage,
timestamp: sentTimestamp, timestamp: sentTimestamp,
base64EncodedContent: ciphertext.base64EncodedString() base64EncodedContent: ciphertext.base64EncodedString()
) )
) })
.mapError { MessageSenderError.other("Couldn't wrap message", $0) } .mapError { MessageSenderError.other("Couldn't wrap message", $0) }
.successOrThrow() .successOrThrow()
.base64EncodedString() .base64EncodedString()
@ -190,14 +190,14 @@ public final class MessageSender {
case (.closedGroup(let groupId), .groupMessages) where (try? SessionId.Prefix(from: groupId)) == .group: case (.closedGroup(let groupId), .groupMessages) where (try? SessionId.Prefix(from: groupId)) == .group:
return try SessionUtil return try SessionUtil
.encrypt( .encrypt(
message: try Result( message: try Result(catching: {
MessageWrapper.wrap( try MessageWrapper.wrap(
type: .closedGroupMessage, type: .closedGroupMessage,
timestamp: sentTimestamp, timestamp: sentTimestamp,
base64EncodedContent: plaintext.base64EncodedString(), base64EncodedContent: plaintext.base64EncodedString(),
wrapInWebSocketMessage: false wrapInWebSocketMessage: false
) )
) })
.mapError { MessageSenderError.other("Couldn't wrap message", $0) } .mapError { MessageSenderError.other("Couldn't wrap message", $0) }
.successOrThrow(), .successOrThrow(),
groupSessionId: SessionId(.group, hex: groupId), groupSessionId: SessionId(.group, hex: groupId),
@ -227,14 +227,14 @@ public final class MessageSender {
using: dependencies using: dependencies
) )
return try Result( return try Result(catching: {
MessageWrapper.wrap( try MessageWrapper.wrap(
type: .closedGroupMessage, type: .closedGroupMessage,
timestamp: sentTimestamp, timestamp: sentTimestamp,
senderPublicKey: groupPublicKey, // Needed for Android senderPublicKey: groupPublicKey, // Needed for Android
base64EncodedContent: ciphertext.base64EncodedString() base64EncodedContent: ciphertext.base64EncodedString()
) )
) })
.mapError { MessageSenderError.other("Couldn't wrap message", $0) } .mapError { MessageSenderError.other("Couldn't wrap message", $0) }
.successOrThrow() .successOrThrow()
.base64EncodedString() .base64EncodedString()

@ -300,7 +300,7 @@ public enum SessionUtil {
// Check if the config needs to be pushed // Check if the config needs to be pushed
guard config.needsPush else { return nil } 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 .onFailure { error in
let configCountInfo: String = config.count(for: variant) let configCountInfo: String = config.count(for: variant)

@ -885,13 +885,15 @@ class MessageReceiverGroupsSpec: QuickSpec {
.thenReturn(nil) .thenReturn(nil)
mockStorage.write { db in mockStorage.write { db in
result = Result(try MessageReceiver.handleGroupUpdateMessage( result = Result(catching: {
try MessageReceiver.handleGroupUpdateMessage(
db, db,
threadId: groupId.hexString, threadId: groupId.hexString,
threadVariant: .group, threadVariant: .group,
message: promoteMessage, message: promoteMessage,
using: dependencies using: dependencies
)) )
})
} }
expect(result.failure).to(matchError(MessageReceiverError.invalidMessage)) expect(result.failure).to(matchError(MessageReceiverError.invalidMessage))

@ -348,7 +348,7 @@ open class Storage {
} }
// Note: The non-async migration should only be used for unit tests // 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 migrator.asyncMigrate(dbWriter) { result in
let finalResult: Result<Void, Error> = { let finalResult: Result<Void, Error> = {

@ -53,6 +53,10 @@ public extension FeatureStorage {
static let updatedGroupsAllowPromotions: FeatureConfig<Bool> = Dependencies.create( static let updatedGroupsAllowPromotions: FeatureConfig<Bool> = Dependencies.create(
identifier: "updatedGroupsAllowPromotions" identifier: "updatedGroupsAllowPromotions"
) )
static let updatedGroupsAllowInviteById: FeatureConfig<Bool> = Dependencies.create(
identifier: "updatedGroupsAllowInviteById"
)
} }
// MARK: - FeatureOption // MARK: - FeatureOption

@ -3,11 +3,6 @@
import Foundation import Foundation
public extension Result where Failure == Error { 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<Success, Failure> { func onFailure(closure: (Failure) -> ()) -> Result<Success, Failure> {
switch self { switch self {
case .success: break case .success: break

Loading…
Cancel
Save