Model changes for group links.
This commit is contained in:
parent
a0cd13ea38
commit
aa88bb712c
@ -103,11 +103,11 @@ extension ConversationViewController: MessageRequestDelegate {
|
||||
return
|
||||
}
|
||||
let groupMembership = groupThread.groupModel.groupMembership
|
||||
guard groupMembership.isPendingMember(localAddress) else {
|
||||
guard groupMembership.isPendingProfileKeyMember(localAddress) else {
|
||||
owsFailDebug("Can't reject invite if not pending.")
|
||||
return
|
||||
}
|
||||
guard let addedByUuid = groupMembership.addedByUuid(forPendingMember: localAddress) else {
|
||||
guard let addedByUuid = groupMembership.addedByUuid(forPendingProfileKeyMember: localAddress) else {
|
||||
owsFailDebug("Missing addedByUuid.")
|
||||
return
|
||||
}
|
||||
@ -193,7 +193,7 @@ extension ConversationViewController: MessageRequestDelegate {
|
||||
|
||||
var isMemberOfGroup = false
|
||||
if let groupThread = thread as? TSGroupThread {
|
||||
isMemberOfGroup = groupThread.isLocalUserInGroup
|
||||
isMemberOfGroup = groupThread.isLocalUserMemberOfAnyKind
|
||||
}
|
||||
|
||||
if isMemberOfGroup {
|
||||
@ -232,7 +232,7 @@ extension ConversationViewController: MessageRequestDelegate {
|
||||
}
|
||||
|
||||
guard let groupThread = thread as? TSGroupThread,
|
||||
groupThread.isLocalUserPendingOrNonPendingMember else {
|
||||
groupThread.isLocalUserFullOrInvitedMemberOfGroup else {
|
||||
// If we don't need to leave the group, finish up immediately.
|
||||
return completion()
|
||||
}
|
||||
|
||||
@ -557,7 +557,7 @@ typedef enum : NSUInteger {
|
||||
}
|
||||
|
||||
TSGroupThread *groupThread = (TSGroupThread *)self.thread;
|
||||
return !groupThread.isLocalUserInGroup;
|
||||
return !groupThread.isLocalUserFullMemberOfGroup;
|
||||
}
|
||||
|
||||
- (void)updateInputVisibility
|
||||
|
||||
@ -56,7 +56,7 @@ class MessageRequestView: UIStackView {
|
||||
|
||||
if let groupThread = thread as? TSGroupThread,
|
||||
groupThread.isGroupV2Thread {
|
||||
self.mode = (groupThread.isLocalUserPendingMember
|
||||
self.mode = (groupThread.isLocalUserPendingProfileKeyMember
|
||||
? .groupInviteRequest
|
||||
: .contactOrGroupRequest)
|
||||
} else {
|
||||
@ -262,7 +262,7 @@ class MessageRequestView: UIStackView {
|
||||
return UILabel()
|
||||
}
|
||||
let groupMembership = groupThread.groupModel.groupMembership
|
||||
guard let addedByUuid = groupMembership.addedByUuid(forPendingMember: localAddress) else {
|
||||
guard let addedByUuid = groupMembership.addedByUuid(forPendingProfileKeyMember: localAddress) else {
|
||||
owsFailDebug("missing addedByUuid")
|
||||
return UILabel()
|
||||
}
|
||||
|
||||
@ -172,7 +172,7 @@ class DebugUIGroupsV2: DebugUIPage {
|
||||
var defaultModelBuilder = TSGroupModelBuilder()
|
||||
defaultModelBuilder.groupsVersion = groupsVersion
|
||||
var defaultMembershipBuilder = GroupMembership.Builder()
|
||||
defaultMembershipBuilder.addNonPendingMember(localAddress, role: .administrator)
|
||||
defaultMembershipBuilder.addFullMember(localAddress, role: .administrator)
|
||||
defaultModelBuilder.groupMembership = defaultMembershipBuilder.build()
|
||||
let defaultModel = try defaultModelBuilder.build(transaction: transaction)
|
||||
let defaultDMToken = DisappearingMessageToken.disabledToken
|
||||
@ -240,10 +240,10 @@ class DebugUIGroupsV2: DebugUIPage {
|
||||
modelBuilder2.name = "name 2"
|
||||
modelBuilder2.avatarData = "avatar 2".data(using: .utf8)
|
||||
var groupMembershipBuilder1 = model1.groupMembership.asBuilder
|
||||
groupMembershipBuilder1.addNonPendingMember(otherAddress0, role: .normal)
|
||||
groupMembershipBuilder1.addFullMember(otherAddress0, role: .normal)
|
||||
if groupsVersion == .V2,
|
||||
let updaterUuid = updaterAddress?.uuid {
|
||||
groupMembershipBuilder1.addPendingMember(otherAddress1,
|
||||
groupMembershipBuilder1.addPendingProfileKeyMember(otherAddress1,
|
||||
role: .normal,
|
||||
addedByUuid: updaterUuid)
|
||||
}
|
||||
@ -371,7 +371,7 @@ class DebugUIGroupsV2: DebugUIPage {
|
||||
var groupMembershipBuilder1 = defaultModel.groupMembership.asBuilder
|
||||
for member in members {
|
||||
groupMembershipBuilder1.remove(member)
|
||||
groupMembershipBuilder1.addNonPendingMember(member, role: .normal)
|
||||
groupMembershipBuilder1.addFullMember(member, role: .normal)
|
||||
}
|
||||
modelBuilder1.groupMembership = groupMembershipBuilder1.build()
|
||||
let model1 = try modelBuilder1.build(transaction: transaction)
|
||||
@ -380,7 +380,7 @@ class DebugUIGroupsV2: DebugUIPage {
|
||||
var groupMembershipBuilder2 = defaultModel.groupMembership.asBuilder
|
||||
for member in members {
|
||||
groupMembershipBuilder2.remove(member)
|
||||
groupMembershipBuilder2.addNonPendingMember(member, role: .administrator)
|
||||
groupMembershipBuilder2.addFullMember(member, role: .administrator)
|
||||
}
|
||||
modelBuilder2.groupMembership = groupMembershipBuilder2.build()
|
||||
let model2 = try modelBuilder2.build(transaction: transaction)
|
||||
@ -425,7 +425,7 @@ class DebugUIGroupsV2: DebugUIPage {
|
||||
var groupMembershipBuilder2 = defaultModel.groupMembership.asBuilder
|
||||
for member in members {
|
||||
groupMembershipBuilder2.remove(member)
|
||||
groupMembershipBuilder2.addNonPendingMember(member, role: .administrator)
|
||||
groupMembershipBuilder2.addFullMember(member, role: .administrator)
|
||||
}
|
||||
modelBuilder2.groupMembership = groupMembershipBuilder2.build()
|
||||
let model2 = try modelBuilder2.build(transaction: transaction)
|
||||
@ -483,7 +483,7 @@ class DebugUIGroupsV2: DebugUIPage {
|
||||
var groupMembershipBuilder2 = defaultModel.groupMembership.asBuilder
|
||||
for member in members {
|
||||
groupMembershipBuilder2.remove(member)
|
||||
groupMembershipBuilder2.addPendingMember(member, role: .normal, addedByUuid: inviterUuid)
|
||||
groupMembershipBuilder2.addPendingProfileKeyMember(member, role: .normal, addedByUuid: inviterUuid)
|
||||
}
|
||||
modelBuilder2.groupMembership = groupMembershipBuilder2.build()
|
||||
let model2 = try modelBuilder2.build(transaction: transaction)
|
||||
@ -493,7 +493,7 @@ class DebugUIGroupsV2: DebugUIPage {
|
||||
var groupMembershipBuilder3 = defaultModel.groupMembership.asBuilder
|
||||
for member in members {
|
||||
groupMembershipBuilder3.remove(member)
|
||||
groupMembershipBuilder3.addNonPendingMember(member, role: .administrator)
|
||||
groupMembershipBuilder3.addFullMember(member, role: .administrator)
|
||||
}
|
||||
modelBuilder3.groupMembership = groupMembershipBuilder3.build()
|
||||
let model3 = try modelBuilder3.build(transaction: transaction)
|
||||
@ -540,7 +540,7 @@ class DebugUIGroupsV2: DebugUIPage {
|
||||
|
||||
let oldGroupMembership = oldGroupModel.groupMembership
|
||||
var groupMembershipBuilder = oldGroupMembership.asBuilder
|
||||
for address in oldGroupMembership.allUsers {
|
||||
for address in oldGroupMembership.allMembersOfAnyKind {
|
||||
if address != localAddress {
|
||||
groupMembershipBuilder.remove(address)
|
||||
}
|
||||
@ -609,7 +609,7 @@ class DebugUIGroupsV2: DebugUIPage {
|
||||
guard let groupModel = groupThread.groupModel as? TSGroupModelV2 else {
|
||||
throw OWSAssertionError("Invalid groupModel.")
|
||||
}
|
||||
guard groupModel.groupMembership.isNonPendingMember(otherUserAddress) else {
|
||||
guard groupModel.groupMembership.isFullMember(otherUserAddress) else {
|
||||
throw OWSAssertionError("Other user is not a full member.")
|
||||
}
|
||||
// Last admin (local user) can't leave group, so first
|
||||
|
||||
@ -121,12 +121,12 @@ public extension DebugUIStress {
|
||||
var groupMembershipBuilder = oldGroupMembership.asBuilder
|
||||
for address in membersToAdd {
|
||||
assert(address.isValid)
|
||||
guard !oldGroupMembership.isPendingOrNonPendingMember(address) else {
|
||||
guard !oldGroupMembership.isMemberOfAnyKind(address) else {
|
||||
Logger.warn("Recipient is already in group.")
|
||||
continue
|
||||
}
|
||||
// GroupManager will separate out members as pending if necessary.
|
||||
groupMembershipBuilder.addNonPendingMember(address, role: .normal)
|
||||
groupMembershipBuilder.addFullMember(address, role: .normal)
|
||||
}
|
||||
builder.groupMembership = groupMembershipBuilder.build()
|
||||
return try builder.build(transaction: transaction)
|
||||
|
||||
@ -1671,7 +1671,7 @@ NSString *const kArchiveButtonPseudoGroup = @"kArchiveButtonPseudoGroup";
|
||||
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
|
||||
if ([thread isKindOfClass:[TSGroupThread class]]) {
|
||||
TSGroupThread *groupThread = (TSGroupThread *)thread;
|
||||
if (groupThread.isLocalUserInGroup || groupThread.isGroupV2Thread) {
|
||||
if (groupThread.isLocalUserMemberOfAnyKind || groupThread.isGroupV2Thread) {
|
||||
[groupThread softDeleteThreadWithTransaction:transaction];
|
||||
} else {
|
||||
[groupThread anyRemoveWithTransaction:transaction];
|
||||
|
||||
@ -305,7 +305,7 @@ public class NewGroupConfirmViewController: OWSViewController {
|
||||
self.presentingViewController?.dismiss(animated: true, completion: completion)
|
||||
}
|
||||
|
||||
let pendingMembers = groupThread.groupModel.groupMembership.pendingMembers
|
||||
let pendingMembers = groupThread.groupModel.groupMembership.pendingProfileKeyMembers
|
||||
guard let firstPendingMember = pendingMembers.first else {
|
||||
// No pending members.
|
||||
return navigateToNewGroup(nil)
|
||||
|
||||
@ -137,13 +137,13 @@ private extension AddGroupMembersViewController {
|
||||
return nil
|
||||
}
|
||||
for address in addresses {
|
||||
guard !oldGroupMembership.isNonPendingMember(address) else {
|
||||
guard !oldGroupMembership.isFullMember(address) else {
|
||||
owsFailDebug("Recipient is already in group.")
|
||||
continue
|
||||
}
|
||||
// GroupManager will separate out members as pending if necessary.
|
||||
groupMembershipBuilder.remove(address)
|
||||
groupMembershipBuilder.addNonPendingMember(address, role: .normal)
|
||||
groupMembershipBuilder.addFullMember(address, role: .normal)
|
||||
}
|
||||
builder.groupMembership = groupMembershipBuilder.build()
|
||||
return try builder.build(transaction: transaction)
|
||||
@ -248,7 +248,7 @@ extension AddGroupMembersViewController: GroupMemberViewDelegate {
|
||||
}
|
||||
|
||||
func groupMemberViewGroupMemberCountForDisplay() -> Int {
|
||||
return (oldGroupModel.groupMembership.pendingAndNonPendingMemberCount +
|
||||
return (oldGroupModel.groupMembership.allMembersOfAnyKind.count +
|
||||
newRecipientSet.count)
|
||||
}
|
||||
|
||||
@ -270,11 +270,13 @@ extension AddGroupMembersViewController: GroupMemberViewDelegate {
|
||||
owsFailDebug("Invalid recipient.")
|
||||
return false
|
||||
}
|
||||
if oldGroupModel.groupMembership.isNonPendingMember(address) {
|
||||
let groupMembership = oldGroupModel.groupMembership
|
||||
if groupMembership.isFullMember(address) {
|
||||
return true
|
||||
}
|
||||
if oldGroupModel.groupMembership.isPendingMember(address) {
|
||||
// We can "add" pending members if they support gv2
|
||||
if groupMembership.isPendingProfileKeyMember(address) ||
|
||||
groupMembership.isRequestingMember(address) {
|
||||
// We can "add" pending or requesting members if they support gv2
|
||||
// and we know their profile key credential.
|
||||
let canAddMember = databaseStorage.read { transaction -> Bool in
|
||||
guard GroupManager.doesUserSupportGroupsV2(address: address, transaction: transaction) else {
|
||||
|
||||
@ -147,7 +147,7 @@ public class AddToGroupViewController: OWSTableViewController {
|
||||
return Environment.shared.contactsManager.shortDisplayName(for: self.address, transaction: transaction)
|
||||
}
|
||||
|
||||
guard !groupThread.groupModel.groupMembership.isPendingOrNonPendingMember(address) else {
|
||||
guard !groupThread.groupModel.groupMembership.isMemberOfAnyKind(address) else {
|
||||
let toastFormat = NSLocalizedString(
|
||||
"ADD_TO_GROUP_ALREADY_MEMBER_TOAST_FORMAT",
|
||||
comment: "A toast on the 'add to group' view indicating the user is already a member. Embeds {contact name} and {group name}"
|
||||
@ -220,12 +220,12 @@ public class AddToGroupViewController: OWSTableViewController {
|
||||
let oldGroupMembership = oldGroupModel.groupMembership
|
||||
var groupMembershipBuilder = oldGroupMembership.asBuilder
|
||||
|
||||
guard !oldGroupMembership.isPendingOrNonPendingMember(self.address) else {
|
||||
guard !oldGroupMembership.isMemberOfAnyKind(self.address) else {
|
||||
owsFailDebug("Recipient is already in group.")
|
||||
return nil
|
||||
}
|
||||
// GroupManager will separate out members as pending if necessary.
|
||||
groupMembershipBuilder.addNonPendingMember(self.address, role: .normal)
|
||||
groupMembershipBuilder.addFullMember(self.address, role: .normal)
|
||||
|
||||
builder.groupMembership = groupMembershipBuilder.build()
|
||||
return try builder.build(transaction: transaction)
|
||||
|
||||
@ -199,7 +199,7 @@ extension ConversationSettingsViewController {
|
||||
return
|
||||
}
|
||||
|
||||
let isLocalUserInConversation = self.isLocalUserInConversation
|
||||
let isLocalUserFullMemberOfGroup = self.isLocalUserFullMemberOfGroup
|
||||
|
||||
// For pre-message request threads, allow manually sharing your profile if the thread is not whitelisted.
|
||||
let (isPreMessageRequestsThread, isThreadInProfileWhitelist) = databaseStorage.uiRead { transaction -> (Bool, Bool) in
|
||||
@ -225,7 +225,7 @@ extension ConversationSettingsViewController {
|
||||
let cell = OWSTableItem.buildDisclosureCell(name: title,
|
||||
icon: .settingsProfile,
|
||||
accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "share_profile"))
|
||||
cell.isUserInteractionEnabled = isLocalUserInConversation
|
||||
cell.isUserInteractionEnabled = isLocalUserFullMemberOfGroup
|
||||
return cell
|
||||
},
|
||||
actionBlock: { [weak self] in
|
||||
@ -472,7 +472,7 @@ extension ConversationSettingsViewController {
|
||||
: NSLocalizedString("CONVERSATION_SETTINGS_BLOCK_AND_LEAVE_SECTION_CONTACT_FOOTER",
|
||||
comment: "Footer text for the 'block and leave' section of contact conversation settings view.")
|
||||
|
||||
if isGroupThread, isLocalUserInConversation {
|
||||
if isGroupThread, isLocalUserFullOrInvitedMemberOfGroup {
|
||||
section.add(OWSTableItem(customCellBlock: { [weak self] in
|
||||
guard let self = self else {
|
||||
owsFailDebug("Missing self")
|
||||
@ -592,7 +592,7 @@ extension ConversationSettingsViewController {
|
||||
|
||||
private func accessoryLabel(forAccess access: GroupV2Access) -> String {
|
||||
switch access {
|
||||
case .unknown, .any, .member:
|
||||
case .any, .member:
|
||||
if access != .member {
|
||||
owsFailDebug("Invalid attributes access: \(access.rawValue)")
|
||||
}
|
||||
@ -600,7 +600,11 @@ extension ConversationSettingsViewController {
|
||||
comment: "Label indicating that all group members can update the group's attributes: name, avatar, etc.")
|
||||
case .administrator:
|
||||
return NSLocalizedString("CONVERSATION_SETTINGS_ATTRIBUTES_ACCESS_ADMINISTRATOR",
|
||||
comment: "Label indicating that only administrators can update the group's attributes: name, avatar, etc.")
|
||||
comment: "Label indicating that only administrators can update the group's attributes: name, avatar, etc.")
|
||||
case .unknown, .unsatisfiable:
|
||||
owsFailDebug("Invalid access")
|
||||
return NSLocalizedString("CONVERSATION_SETTINGS_ATTRIBUTES_ACCESS_NONE",
|
||||
comment: "Label indicating that no member can update the group's attributes: name, avatar, etc.")
|
||||
}
|
||||
}
|
||||
|
||||
@ -650,7 +654,7 @@ extension ConversationSettingsViewController {
|
||||
}
|
||||
|
||||
let groupMembership = groupModel.groupMembership
|
||||
let allMembers = groupMembership.nonPendingMembers
|
||||
let allMembers = groupMembership.fullMembers
|
||||
var allMembersSorted = [SignalServiceAddress]()
|
||||
var verificationStateMap = [SignalServiceAddress: OWSVerificationState]()
|
||||
databaseStorage.uiRead { transaction in
|
||||
@ -663,15 +667,15 @@ extension ConversationSettingsViewController {
|
||||
}
|
||||
|
||||
var membersToRender = [SignalServiceAddress]()
|
||||
if groupMembership.isNonPendingMember(localAddress) {
|
||||
if groupMembership.isFullMember(localAddress) {
|
||||
// Make sure local user is first.
|
||||
membersToRender.insert(localAddress, at: 0)
|
||||
}
|
||||
// Admin users are second.
|
||||
let adminMembers = allMembersSorted.filter { $0 != localAddress && groupMembership.isAdministrator($0) }
|
||||
let adminMembers = allMembersSorted.filter { $0 != localAddress && groupMembership.isFullMemberAndAdministrator($0) }
|
||||
membersToRender += adminMembers
|
||||
// Non-admin users are third.
|
||||
let nonAdminMembers = allMembersSorted.filter { $0 != localAddress && !groupMembership.isAdministrator($0) }
|
||||
let nonAdminMembers = allMembersSorted.filter { $0 != localAddress && !groupMembership.isFullMemberAndAdministrator($0) }
|
||||
membersToRender += nonAdminMembers
|
||||
|
||||
if membersToRender.count > 1 {
|
||||
@ -708,7 +712,7 @@ extension ConversationSettingsViewController {
|
||||
let cell = ContactTableViewCell()
|
||||
cell.setUseSmallAvatars()
|
||||
|
||||
let isGroupAdmin = groupMembership.isAdministrator(memberAddress)
|
||||
let isGroupAdmin = groupMembership.isFullMemberAndAdministrator(memberAddress)
|
||||
let isVerified = verificationState == .verified
|
||||
let isNoLongerVerified = verificationState == .noLongerVerified
|
||||
let isBlocked = helper.isSignalServiceAddressBlocked(memberAddress)
|
||||
@ -794,6 +798,7 @@ extension ConversationSettingsViewController {
|
||||
return section
|
||||
}
|
||||
|
||||
// TODO: Add requesting members section.
|
||||
private func buildPendingMembersSection(groupModel: TSGroupModel,
|
||||
localAddress: SignalServiceAddress) -> OWSTableSection {
|
||||
let section = OWSTableSection()
|
||||
@ -803,7 +808,7 @@ extension ConversationSettingsViewController {
|
||||
// Only admins can revoke invites.
|
||||
let canRevokeInvites = groupViewHelper.canRevokePendingInvites
|
||||
|
||||
let pendingMembers = groupModel.groupMembership.pendingMembers
|
||||
let pendingMembers = groupModel.groupMembership.pendingProfileKeyMembers
|
||||
let pendingInviteCount = (canRevokeInvites
|
||||
? pendingMembers.count + groupModel.groupMembership.invalidInvites.count
|
||||
: pendingMembers.count)
|
||||
@ -815,7 +820,7 @@ extension ConversationSettingsViewController {
|
||||
accessoryText = NSLocalizedString("CONVERSATION_SETTINGS_PENDING_MEMBER_INVITES_NONE",
|
||||
comment: "Indicates that there are no pending member invites in the group.")
|
||||
}
|
||||
let isLocalUserFullMember = groupModel.groupMembership.isNonPendingMember(localAddress)
|
||||
let isLocalUserFullMember = groupModel.groupMembership.isFullMember(localAddress)
|
||||
|
||||
section.add(OWSTableItem(customCellBlock: { [weak self] in
|
||||
guard let self = self else {
|
||||
|
||||
@ -172,7 +172,7 @@ extension ConversationSettingsViewController {
|
||||
private func buildHeaderForGroup(groupThread: TSGroupThread) -> UIView {
|
||||
var builder = HeaderBuilder(viewController: self)
|
||||
|
||||
let memberCount = groupThread.groupModel.groupMembership.nonPendingMembers.count
|
||||
let memberCount = groupThread.groupModel.groupMembership.fullMembers.count
|
||||
var groupMembersText = GroupViewUtils.formatGroupMembersLabel(memberCount: memberCount)
|
||||
if RemoteConfig.groupsV2GoodCitizen,
|
||||
groupThread.isGroupV1Thread {
|
||||
|
||||
@ -135,8 +135,12 @@ class ConversationSettingsViewController: OWSTableViewController {
|
||||
return groupViewHelper.canEditConversationAccess
|
||||
}
|
||||
|
||||
var isLocalUserInConversation: Bool {
|
||||
return groupViewHelper.isLocalUserInConversation
|
||||
var isLocalUserFullMemberOfGroup: Bool {
|
||||
return groupViewHelper.isLocalUserFullMemberOfGroup
|
||||
}
|
||||
|
||||
var isLocalUserFullOrInvitedMemberOfGroup: Bool {
|
||||
return groupViewHelper.isLocalUserFullOrInvitedMemberOfGroup
|
||||
}
|
||||
|
||||
var isGroupThread: Bool {
|
||||
@ -318,7 +322,7 @@ class ConversationSettingsViewController: OWSTableViewController {
|
||||
// Thread no longer exists.
|
||||
return false
|
||||
}
|
||||
guard latestThread.isLocalUserInGroup else {
|
||||
guard latestThread.isLocalUserMemberOfAnyKind else {
|
||||
// Local user is no longer in group, e.g. perhaps they just blocked it.
|
||||
return false
|
||||
}
|
||||
@ -690,9 +694,8 @@ class ConversationSettingsViewController: OWSTableViewController {
|
||||
return false
|
||||
}
|
||||
let groupMembership = groupModelV2.groupMembership
|
||||
guard groupMembership.isNonPendingMember(localAddress),
|
||||
groupMembership.isAdministrator(localAddress),
|
||||
groupMembership.nonPendingAdministrators.count == 1 else {
|
||||
guard groupMembership.isFullMemberAndAdministrator(localAddress),
|
||||
groupMembership.fullMemberAdministrators.count == 1 else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@ -710,7 +713,7 @@ class ConversationSettingsViewController: OWSTableViewController {
|
||||
owsFailDebug("missing local address")
|
||||
return []
|
||||
}
|
||||
var candidates = groupModelV2.groupMembership.nonPendingMembers
|
||||
var candidates = groupModelV2.groupMembership.fullMembers
|
||||
candidates.remove(localAddress)
|
||||
return candidates
|
||||
}
|
||||
|
||||
@ -50,10 +50,10 @@ extension GroupViewHelper {
|
||||
owsFailDebug("Missing localAddress.")
|
||||
return false
|
||||
}
|
||||
let isLocalUserAdmin = groupThread.groupModel.groupMembership.isAdministrator(localAddress)
|
||||
let isLocalUserAdmin = groupThread.groupModel.groupMembership.isFullMemberAndAdministrator(localAddress)
|
||||
let groupMembership = groupThread.groupModel.groupMembership
|
||||
let canBecomeAdmin = (groupMembership.isNonPendingMember(address) &&
|
||||
!groupMembership.isAdministrator(address))
|
||||
let canBecomeAdmin = (groupMembership.isFullMember(address) &&
|
||||
!groupMembership.isFullMemberAndAdministrator(address))
|
||||
return (canEditConversationMembership && isLocalUserAdmin && canBecomeAdmin)
|
||||
}
|
||||
|
||||
@ -73,7 +73,7 @@ extension GroupViewHelper {
|
||||
guard let oldGroupModel = delegate?.currentGroupModel as? TSGroupModelV2 else {
|
||||
return Promise(error: OWSAssertionError("Missing group model."))
|
||||
}
|
||||
guard oldGroupModel.groupMembership.isPendingOrNonPendingMember(address) else {
|
||||
guard oldGroupModel.groupMembership.isMemberOfAnyKind(address) else {
|
||||
return Promise(error: OWSAssertionError("Not a group member."))
|
||||
}
|
||||
guard let uuid = address.uuid else {
|
||||
@ -98,10 +98,9 @@ extension GroupViewHelper {
|
||||
owsFailDebug("Missing localAddress.")
|
||||
return false
|
||||
}
|
||||
let isLocalUserAdmin = groupThread.groupModel.groupMembership.isAdministrator(localAddress)
|
||||
let groupMembership = groupThread.groupModel.groupMembership
|
||||
let canRevokeAdmin = (groupMembership.isNonPendingMember(address) &&
|
||||
groupMembership.isAdministrator(address))
|
||||
let isLocalUserAdmin = groupMembership.isFullMemberAndAdministrator(localAddress)
|
||||
let canRevokeAdmin = groupMembership.isFullMemberAndAdministrator(address)
|
||||
return (canEditConversationMembership && isLocalUserAdmin && canRevokeAdmin)
|
||||
}
|
||||
|
||||
@ -121,7 +120,7 @@ extension GroupViewHelper {
|
||||
guard let oldGroupModel = delegate?.currentGroupModel as? TSGroupModelV2 else {
|
||||
return Promise(error: OWSAssertionError("Missing group model."))
|
||||
}
|
||||
guard oldGroupModel.groupMembership.isPendingOrNonPendingMember(address) else {
|
||||
guard oldGroupModel.groupMembership.isMemberOfAnyKind(address) else {
|
||||
return Promise(error: OWSAssertionError("Not a group member."))
|
||||
}
|
||||
guard let uuid = address.uuid else {
|
||||
@ -148,9 +147,9 @@ extension GroupViewHelper {
|
||||
return false
|
||||
}
|
||||
// Only admins can kick out other members.
|
||||
let isLocalUserAdmin = groupThread.groupModel.groupMembership.isAdministrator(localAddress)
|
||||
let groupMembership = groupThread.groupModel.groupMembership
|
||||
let isAddressInGroup = groupMembership.isPendingOrNonPendingMember(address)
|
||||
let isLocalUserAdmin = groupMembership.isFullMemberAndAdministrator(localAddress)
|
||||
let isAddressInGroup = groupMembership.isMemberOfAnyKind(address)
|
||||
return canEditConversationMembership && isLocalUserAdmin && isAddressInGroup
|
||||
}
|
||||
|
||||
@ -170,7 +169,7 @@ extension GroupViewHelper {
|
||||
guard let oldGroupModel = delegate?.currentGroupModel as? TSGroupModelV2 else {
|
||||
return Promise(error: OWSAssertionError("Missing group model."))
|
||||
}
|
||||
guard oldGroupModel.groupMembership.isPendingOrNonPendingMember(address) else {
|
||||
guard oldGroupModel.groupMembership.isMemberOfAnyKind(address) else {
|
||||
return Promise(error: OWSAssertionError("Not a group member."))
|
||||
}
|
||||
guard let uuid = address.uuid else {
|
||||
|
||||
@ -61,7 +61,7 @@ class GroupViewHelper: NSObject {
|
||||
if threadViewModel.hasPendingMessageRequest {
|
||||
return false
|
||||
}
|
||||
guard isLocalUserInConversation else {
|
||||
guard isLocalUserFullMemberOfGroup else {
|
||||
return false
|
||||
}
|
||||
guard let groupThread = thread as? TSGroupThread else {
|
||||
@ -85,12 +85,15 @@ class GroupViewHelper: NSObject {
|
||||
case .unknown:
|
||||
owsFailDebug("Unknown access.")
|
||||
return false
|
||||
case .unsatisfiable:
|
||||
owsFailDebug("Invalid access.")
|
||||
return false
|
||||
case .any:
|
||||
return true
|
||||
case .member:
|
||||
return groupModelV2.groupMembership.isNonPendingMember(localAddress)
|
||||
return groupModelV2.groupMembership.isFullMember(localAddress)
|
||||
case .administrator:
|
||||
return (groupModelV2.groupMembership.isNonPendingMember(localAddress) && groupModelV2.groupMembership.isAdministrator(localAddress))
|
||||
return (groupModelV2.groupMembership.isFullMemberAndAdministrator(localAddress))
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,7 +120,7 @@ class GroupViewHelper: NSObject {
|
||||
if threadViewModel.hasPendingMessageRequest {
|
||||
return false
|
||||
}
|
||||
guard isLocalUserInConversation else {
|
||||
guard isLocalUserFullMemberOfGroup else {
|
||||
return false
|
||||
}
|
||||
guard let groupThread = thread as? TSGroupThread else {
|
||||
@ -132,7 +135,7 @@ class GroupViewHelper: NSObject {
|
||||
owsFailDebug("Missing localAddress.")
|
||||
return false
|
||||
}
|
||||
return groupModelV2.groupMembership.isAdministrator(localAddress)
|
||||
return groupModelV2.groupMembership.isFullMemberAndAdministrator(localAddress)
|
||||
}
|
||||
|
||||
var canRevokePendingInvites: Bool {
|
||||
@ -145,20 +148,24 @@ class GroupViewHelper: NSObject {
|
||||
}
|
||||
let groupMembership = groupThread.groupModel.groupMembership
|
||||
return (!threadViewModel.hasPendingMessageRequest &&
|
||||
groupMembership.isPendingOrNonPendingMember(localAddress) &&
|
||||
groupMembership.isAdministrator(localAddress))
|
||||
groupMembership.isFullMemberAndAdministrator(localAddress))
|
||||
}
|
||||
|
||||
var canResendInvites: Bool {
|
||||
return (!threadViewModel.hasPendingMessageRequest &&
|
||||
isLocalUserInConversation)
|
||||
return (!threadViewModel.hasPendingMessageRequest && isLocalUserFullMemberOfGroup)
|
||||
}
|
||||
|
||||
var isLocalUserInConversation: Bool {
|
||||
var isLocalUserFullMemberOfGroup: Bool {
|
||||
guard let groupThread = thread as? TSGroupThread else {
|
||||
return true
|
||||
}
|
||||
return groupThread.isLocalUserFullMemberOfGroup
|
||||
}
|
||||
|
||||
return groupThread.isLocalUserInGroup
|
||||
var isLocalUserFullOrInvitedMemberOfGroup: Bool {
|
||||
guard let groupThread = thread as? TSGroupThread else {
|
||||
return true
|
||||
}
|
||||
return groupThread.isLocalUserFullOrInvitedMemberOfGroup
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,7 +69,7 @@ public class PendingGroupMembersViewController: OWSTableViewController {
|
||||
|
||||
let groupMembership = groupModel.groupMembership
|
||||
let allPendingMembersSorted = databaseStorage.uiRead { transaction in
|
||||
self.contactsManager.sortSignalServiceAddresses(Array(groupMembership.pendingMembers),
|
||||
self.contactsManager.sortSignalServiceAddresses(Array(groupMembership.pendingProfileKeyMembers),
|
||||
transaction: transaction)
|
||||
}
|
||||
|
||||
@ -77,7 +77,7 @@ public class PendingGroupMembersViewController: OWSTableViewController {
|
||||
var membersInvitedByLocalUser = [SignalServiceAddress]()
|
||||
var membersInvitedByOtherUsers = [SignalServiceAddress: [SignalServiceAddress]]()
|
||||
for invitedAddress in allPendingMembersSorted {
|
||||
guard let inviterUuid = groupMembership.addedByUuid(forPendingMember: invitedAddress) else {
|
||||
guard let inviterUuid = groupMembership.addedByUuid(forPendingProfileKeyMember: invitedAddress) else {
|
||||
owsFailDebug("Missing inviter.")
|
||||
continue
|
||||
}
|
||||
|
||||
@ -802,6 +802,9 @@
|
||||
/* Label indicating that all group members can update the group's attributes: name, avatar, etc. */
|
||||
"CONVERSATION_SETTINGS_ATTRIBUTES_ACCESS_MEMBER" = "All";
|
||||
|
||||
/* Label indicating that no member can update the group's attributes: name, avatar, etc. */
|
||||
"CONVERSATION_SETTINGS_ATTRIBUTES_ACCESS_NONE" = "Disabled";
|
||||
|
||||
/* Footer for the 'attributes access' section in conversation settings view. */
|
||||
"CONVERSATION_SETTINGS_ATTRIBUTES_ACCESS_SECTION_FOOTER" = "Choose who can edit the group name, photo, and disappearing messages timer.";
|
||||
|
||||
@ -1480,6 +1483,9 @@
|
||||
/* Description of the 'unknown' access level. */
|
||||
"GROUP_ACCESS_LEVEL_UNKNOWN" = "Unknown";
|
||||
|
||||
/* Description of the 'unsatisfiable' access level. */
|
||||
"GROUP_ACCESS_LEVEL_UNSATISFIABLE" = "Disabled";
|
||||
|
||||
/* Message indicating that the access to the group's members was changed by the local user. Embeds {{new access level}}. */
|
||||
"GROUP_ACCESS_MEMBERS_UPDATED_BY_LOCAL_USER_FORMAT" = "You changed who can edit group membership to “%@“.";
|
||||
|
||||
@ -1591,6 +1597,15 @@
|
||||
/* Message indicating that the local user was removed from the group by an unknown user. */
|
||||
"GROUP_LOCAL_USER_REMOVED_BY_UNKNOWN_USER" = "You were removed from the group.";
|
||||
|
||||
/* Message indicating that the local user's request to join the group was approved. */
|
||||
"GROUP_LOCAL_USER_REQUEST_APPROVED" = "GROUP_LOCAL_USER_REQUEST_APPROVED";
|
||||
|
||||
/* Message indicating that the local user's request to join the group was rejected. */
|
||||
"GROUP_LOCAL_USER_REQUEST_REJECTED" = "GROUP_LOCAL_USER_REQUEST_REJECTED";
|
||||
|
||||
/* Message indicating that the local user requested to join the group. */
|
||||
"GROUP_LOCAL_USER_REQUESTED_TO_JOIN_TO_THE_GROUP" = "GROUP_LOCAL_USER_REQUESTED_TO_JOIN_TO_THE_GROUP";
|
||||
|
||||
/* Message indicating that the local user had their administrator role revoked. */
|
||||
"GROUP_LOCAL_USER_REVOKED_ADMINISTRATOR" = "Your admin privileges were revoked.";
|
||||
|
||||
@ -1714,6 +1729,27 @@
|
||||
/* Message indicating that the remote user was removed from the group. Embeds {{ %1$@ user who removed the user, %2$@ user who was removed}}. */
|
||||
"GROUP_REMOTE_USER_REMOVED_FROM_GROUP_BY_REMOTE_USER_FORMAT" = "%1$@ removed %2$@.";
|
||||
|
||||
/* Message indicating that a remote user's request to join the group was approved by the local user. Embeds {{requesting user name}}. */
|
||||
"GROUP_REMOTE_USER_REQUEST_APPROVED_BY_LOCAL_USER_FORMAT" = "GROUP_REMOTE_USER_REQUEST_APPROVED_BY_LOCAL_USER_FORMAT";
|
||||
|
||||
/* Message indicating that a remote user's request to join the group was approved by another user. Embeds {{ %1$@ requesting user name, %2$@ approving user name }}. */
|
||||
"GROUP_REMOTE_USER_REQUEST_APPROVED_BY_REMOTE_USER_FORMAT" = "GROUP_REMOTE_USER_REQUEST_APPROVED_BY_REMOTE_USER_FORMAT";
|
||||
|
||||
/* Message indicating that a remote user's request to join the group was approved. Embeds {{requesting user name}}. */
|
||||
"GROUP_REMOTE_USER_REQUEST_APPROVED_FORMAT" = "GROUP_REMOTE_USER_REQUEST_APPROVED_FORMAT";
|
||||
|
||||
/* Message indicating that a remote user's request to join the group was rejected by the local user. Embeds {{requesting user name}}. */
|
||||
"GROUP_REMOTE_USER_REQUEST_REJECTED_BY_LOCAL_USER_FORMAT" = "GROUP_REMOTE_USER_REQUEST_REJECTED_BY_LOCAL_USER_FORMAT";
|
||||
|
||||
/* Message indicating that a remote user's request to join the group was rejected by another user. Embeds {{ %1$@ requesting user name, %2$@ approving user name }}. */
|
||||
"GROUP_REMOTE_USER_REQUEST_REJECTED_BY_REMOTE_USER_FORMAT" = "GROUP_REMOTE_USER_REQUEST_REJECTED_BY_REMOTE_USER_FORMAT";
|
||||
|
||||
/* Message indicating that a remote user's request to join the group was rejected. Embeds {{requesting user name}}. */
|
||||
"GROUP_REMOTE_USER_REQUEST_REJECTED_FORMAT" = "GROUP_REMOTE_USER_REQUEST_REJECTED_FORMAT";
|
||||
|
||||
/* Message indicating that a remote user requested to join the group. Embeds {{requesting user name}}. */
|
||||
"GROUP_REMOTE_USER_REQUESTED_TO_JOIN_THE_GROUP_FORMAT" = "GROUP_REMOTE_USER_REQUESTED_TO_JOIN_THE_GROUP_FORMAT";
|
||||
|
||||
/* Message indicating that a remote user had their administrator role revoked. Embeds {{remote user name}}. */
|
||||
"GROUP_REMOTE_USER_REVOKED_ADMINISTRATOR" = "%@ had their admin privileges revoked.";
|
||||
|
||||
|
||||
@ -44,8 +44,9 @@ public struct GroupV2SnapshotImpl: GroupV2Snapshot {
|
||||
private let pendingMembers: [PendingMember]
|
||||
private let invalidInvites: [InvalidInvite]
|
||||
|
||||
public let accessControlForAttributes: GroupsProtoAccessControlAccessRequired
|
||||
public let accessControlForMembers: GroupsProtoAccessControlAccessRequired
|
||||
public let groupAccess: GroupAccess
|
||||
|
||||
public let inviteLinkPassword: Data?
|
||||
|
||||
public let disappearingMessageToken: DisappearingMessageToken
|
||||
|
||||
@ -64,8 +65,8 @@ public struct GroupV2SnapshotImpl: GroupV2Snapshot {
|
||||
members: [Member],
|
||||
pendingMembers: [PendingMember],
|
||||
invalidInvites: [InvalidInvite],
|
||||
accessControlForAttributes: GroupsProtoAccessControlAccessRequired,
|
||||
accessControlForMembers: GroupsProtoAccessControlAccessRequired,
|
||||
groupAccess: GroupAccess,
|
||||
inviteLinkPassword: Data?,
|
||||
disappearingMessageToken: DisappearingMessageToken,
|
||||
profileKeys: [UUID: Data]) {
|
||||
|
||||
@ -78,8 +79,8 @@ public struct GroupV2SnapshotImpl: GroupV2Snapshot {
|
||||
self.members = members
|
||||
self.pendingMembers = pendingMembers
|
||||
self.invalidInvites = invalidInvites
|
||||
self.accessControlForAttributes = accessControlForAttributes
|
||||
self.accessControlForMembers = accessControlForMembers
|
||||
self.groupAccess = groupAccess
|
||||
self.inviteLinkPassword = inviteLinkPassword
|
||||
self.disappearingMessageToken = disappearingMessageToken
|
||||
self.profileKeys = profileKeys
|
||||
}
|
||||
@ -91,7 +92,7 @@ public struct GroupV2SnapshotImpl: GroupV2Snapshot {
|
||||
owsFailDebug("Invalid value: \(member.role.rawValue)")
|
||||
continue
|
||||
}
|
||||
builder.addNonPendingMember(member.address, role: role)
|
||||
builder.addFullMember(member.address, role: role)
|
||||
}
|
||||
|
||||
for member in pendingMembers {
|
||||
@ -99,7 +100,7 @@ public struct GroupV2SnapshotImpl: GroupV2Snapshot {
|
||||
owsFailDebug("Invalid value: \(member.role.rawValue)")
|
||||
continue
|
||||
}
|
||||
builder.addPendingMember(member.address, role: role, addedByUuid: member.addedByUuid)
|
||||
builder.addPendingProfileKeyMember(member.address, role: role, addedByUuid: member.addedByUuid)
|
||||
}
|
||||
|
||||
for invalidInvite in invalidInvites {
|
||||
@ -108,9 +109,4 @@ public struct GroupV2SnapshotImpl: GroupV2Snapshot {
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
public var groupAccess: GroupAccess {
|
||||
return GroupAccess(members: GroupAccess.groupV2Access(forProtoAccess: accessControlForMembers),
|
||||
attributes: GroupAccess.groupV2Access(forProtoAccess: accessControlForAttributes))
|
||||
}
|
||||
}
|
||||
|
||||
@ -262,7 +262,7 @@ public class GroupV2UpdatesImpl: NSObject, GroupV2UpdatesSwift {
|
||||
owsFailDebug("Missing localAddress.")
|
||||
return false
|
||||
}
|
||||
let isLocalUserInGroup = groupThread.groupModel.groupMembership.isPendingOrNonPendingMember(localAddress)
|
||||
let isLocalUserInGroup = groupThread.isLocalUserFullOrInvitedMemberOfGroup
|
||||
// Auth errors are expected if we've left the group,
|
||||
// but we should still try to refresh so we can learn
|
||||
// if we've been re-added.
|
||||
|
||||
@ -58,23 +58,27 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
|
||||
|
||||
// Non-nil if the title changed.
|
||||
// When clearing the title, this will be the empty string.
|
||||
private var title: String?
|
||||
private var newTitle: String?
|
||||
|
||||
public var newAvatarData: Data?
|
||||
public var newAvatarUrlPath: String?
|
||||
private var shouldUpdateAvatar = false
|
||||
|
||||
private var membersToAdd = [UUID: TSGroupMemberRole]()
|
||||
// Pending or non-pending members to remove.
|
||||
// Full, pending profile key or pending request members to remove.
|
||||
private var membersToRemove = [UUID]()
|
||||
private var membersToChangeRole = [UUID: TSGroupMemberRole]()
|
||||
private var pendingMembersToAdd = [UUID: TSGroupMemberRole]()
|
||||
private var pendingProfileKeyMembersToAdd = [UUID: TSGroupMemberRole]()
|
||||
private var invalidInvitesToRemove = [Data: InvalidInvite]()
|
||||
private var pendingMembersToPromote = [UUID]()
|
||||
private var pendingProfileKeyMembersToPromote = [UUID]()
|
||||
|
||||
// These access properties should only be set if the value is changing.
|
||||
private var accessForMembers: GroupV2Access?
|
||||
private var accessForAttributes: GroupV2Access?
|
||||
private var accessForAddFromInviteLink: GroupV2Access?
|
||||
|
||||
public var newInviteLinkPassword: Data?
|
||||
private var shouldUpdateInviteLinkPassword = false
|
||||
|
||||
private var shouldLeaveGroupDeclineInvite = false
|
||||
private var shouldRevokeInvalidInvites = false
|
||||
@ -135,15 +139,19 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
|
||||
let oldGroupMembership = oldGroupModel.groupMembership
|
||||
let newGroupMembership = newGroupModel.groupMembership
|
||||
|
||||
let oldUserUuids = Set(oldGroupMembership.allUsers.compactMap { $0.uuid })
|
||||
let newUserUuids = Set(newGroupMembership.allUsers.compactMap { $0.uuid })
|
||||
let oldUserUuids = oldGroupMembership.allMemberUuidsOfAnyKind
|
||||
let newUserUuids = newGroupMembership.allMemberUuidsOfAnyKind
|
||||
|
||||
for uuid in newUserUuids.subtracting(oldUserUuids) {
|
||||
let isAdministrator = newGroupMembership.isAdministrator(uuid)
|
||||
let isPending = newGroupMembership.isPendingMember(uuid)
|
||||
guard !newGroupMembership.isRequestingMember(uuid) else {
|
||||
owsFailDebug("Pending request members should never be added by diffing models.")
|
||||
continue
|
||||
}
|
||||
let isAdministrator = newGroupMembership.isFullOrInvitedAdministrator(uuid)
|
||||
let isPending = newGroupMembership.isPendingProfileKeyMember(uuid)
|
||||
let role: TSGroupMemberRole = isAdministrator ? .administrator : .normal
|
||||
if isPending {
|
||||
addPendingMember(uuid, role: role)
|
||||
addPendingProfileKeyMember(uuid, role: role)
|
||||
} else {
|
||||
addMember(uuid, role: role)
|
||||
}
|
||||
@ -160,17 +168,22 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
|
||||
}
|
||||
|
||||
for uuid in oldUserUuids.intersection(newUserUuids) {
|
||||
if oldGroupMembership.isPendingMember(uuid),
|
||||
newGroupMembership.isNonPendingMember(uuid) {
|
||||
if oldGroupMembership.isPendingProfileKeyMember(uuid),
|
||||
newGroupMembership.isFullMember(uuid) {
|
||||
addMember(uuid, role: .normal)
|
||||
} else if oldGroupMembership.isRequestingMember(uuid),
|
||||
newGroupMembership.isFullMember(uuid) {
|
||||
// We only currently support accepting join requests
|
||||
// with "normal" role.
|
||||
addMember(uuid, role: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
let oldMemberUuids = Set(oldGroupMembership.nonPendingMembers.compactMap { $0.uuid })
|
||||
let newMemberUuids = Set(newGroupMembership.nonPendingMembers.compactMap { $0.uuid })
|
||||
let oldMemberUuids = Set(oldGroupMembership.fullMembers.compactMap { $0.uuid })
|
||||
let newMemberUuids = Set(newGroupMembership.fullMembers.compactMap { $0.uuid })
|
||||
for uuid in oldMemberUuids.intersection(newMemberUuids) {
|
||||
let oldIsAdministrator = oldGroupMembership.isAdministrator(uuid)
|
||||
let newIsAdministrator = newGroupMembership.isAdministrator(uuid)
|
||||
let oldIsAdministrator = oldGroupMembership.isFullMemberAndAdministrator(uuid)
|
||||
let newIsAdministrator = newGroupMembership.isFullMemberAndAdministrator(uuid)
|
||||
guard oldIsAdministrator != newIsAdministrator else {
|
||||
continue
|
||||
}
|
||||
@ -178,13 +191,20 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
|
||||
changeRoleForMember(uuid, role: role)
|
||||
}
|
||||
|
||||
if oldGroupModel.inviteLinkPassword != newGroupModel.inviteLinkPassword {
|
||||
setInviteLinkPassword(newGroupModel.inviteLinkPassword)
|
||||
}
|
||||
|
||||
let oldAccess = oldGroupModel.access
|
||||
let newAccess = newGroupModel.access
|
||||
if oldAccess.members != newAccess.members {
|
||||
self.accessForMembers = newAccess.members
|
||||
setAccessForMembers(newAccess.members)
|
||||
}
|
||||
if oldAccess.attributes != newAccess.attributes {
|
||||
self.accessForAttributes = newAccess.attributes
|
||||
setAccessForAttributes(newAccess.attributes)
|
||||
}
|
||||
if oldAccess.addFromInviteLink != newAccess.addFromInviteLink {
|
||||
setAccessForAddFromInviteLink(newAccess.addFromInviteLink)
|
||||
}
|
||||
|
||||
let oldDisappearingMessageToken = oldDMConfiguration.asToken
|
||||
@ -196,9 +216,17 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
|
||||
|
||||
@objc
|
||||
public func setTitle(_ value: String?) {
|
||||
assert(self.title == nil)
|
||||
assert(self.newTitle == nil)
|
||||
// Non-nil if the title changed.
|
||||
self.title = value ?? ""
|
||||
self.newTitle = value ?? ""
|
||||
}
|
||||
|
||||
@objc
|
||||
public func setInviteLinkPassword(_ value: Data?) {
|
||||
assert(self.newInviteLinkPassword == nil)
|
||||
assert(!shouldUpdateInviteLinkPassword)
|
||||
newInviteLinkPassword = value
|
||||
shouldUpdateInviteLinkPassword = true
|
||||
}
|
||||
|
||||
@objc
|
||||
@ -234,9 +262,9 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
|
||||
}
|
||||
|
||||
@objc
|
||||
public func promotePendingMember(_ uuid: UUID) {
|
||||
assert(!pendingMembersToPromote.contains(uuid))
|
||||
pendingMembersToPromote.append(uuid)
|
||||
public func promotePendingProfileKeyMember(_ uuid: UUID) {
|
||||
assert(!pendingProfileKeyMembersToPromote.contains(uuid))
|
||||
pendingProfileKeyMembersToPromote.append(uuid)
|
||||
}
|
||||
|
||||
public func changeRoleForMember(_ uuid: UUID, role: TSGroupMemberRole) {
|
||||
@ -244,9 +272,9 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
|
||||
membersToChangeRole[uuid] = role
|
||||
}
|
||||
|
||||
public func addPendingMember(_ uuid: UUID, role: TSGroupMemberRole) {
|
||||
assert(pendingMembersToAdd[uuid] == nil)
|
||||
pendingMembersToAdd[uuid] = role
|
||||
public func addPendingProfileKeyMember(_ uuid: UUID, role: TSGroupMemberRole) {
|
||||
assert(pendingProfileKeyMembersToAdd[uuid] == nil)
|
||||
pendingProfileKeyMembersToAdd[uuid] = role
|
||||
}
|
||||
|
||||
public func setShouldLeaveGroupDeclineInvite() {
|
||||
@ -269,6 +297,11 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
|
||||
accessForAttributes = value
|
||||
}
|
||||
|
||||
public func setAccessForAddFromInviteLink(_ value: GroupV2Access) {
|
||||
assert(accessForAddFromInviteLink == nil)
|
||||
accessForAddFromInviteLink = value
|
||||
}
|
||||
|
||||
public func setNewDisappearingMessageToken(_ newDisappearingMessageToken: DisappearingMessageToken) {
|
||||
assert(self.newDisappearingMessageToken == nil)
|
||||
self.newDisappearingMessageToken = newDisappearingMessageToken
|
||||
@ -313,7 +346,7 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
|
||||
// NOTE: We don't (and can't) gather profile key credentials for pending members.
|
||||
var uuidsForProfileKeyCredentials = Set<UUID>()
|
||||
uuidsForProfileKeyCredentials.formUnion(membersToAdd.keys)
|
||||
uuidsForProfileKeyCredentials.formUnion(pendingMembersToPromote)
|
||||
uuidsForProfileKeyCredentials.formUnion(pendingProfileKeyMembersToPromote)
|
||||
// This should be redundant, but we'll also double-check that we have
|
||||
// the local profile key credential.
|
||||
uuidsForProfileKeyCredentials.insert(localUuid)
|
||||
@ -383,16 +416,16 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
|
||||
Logger.verbose("Revision: \(oldRevision) -> \(newRevision)")
|
||||
actionsBuilder.setRevision(newRevision)
|
||||
|
||||
var nonPendingAdministratorCount: Int = currentGroupModel.groupMembership.nonPendingAdministrators.count
|
||||
var allMemberCount = currentGroupModel.groupMembership.pendingAndNonPendingMemberCount
|
||||
var fullMemberAdministratorCount: Int = currentGroupModel.groupMembership.fullMemberAdministrators.count
|
||||
var allMemberCount = currentGroupModel.groupMembership.allMembersOfAnyKind.count
|
||||
|
||||
var didChange = false
|
||||
|
||||
if let title = self.title {
|
||||
if title == currentGroupModel.groupName {
|
||||
if let newTitle = self.newTitle {
|
||||
if newTitle == currentGroupModel.groupName {
|
||||
// Redundant change, not a conflict.
|
||||
} else {
|
||||
let encryptedData = try groupV2Params.encryptGroupName(title)
|
||||
let encryptedData = try groupV2Params.encryptGroupName(newTitle)
|
||||
let actionBuilder = GroupsProtoGroupChangeActionsModifyTitleAction.builder()
|
||||
actionBuilder.setTitle(encryptedData)
|
||||
actionsBuilder.setModifyTitle(try actionBuilder.build())
|
||||
@ -416,48 +449,76 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
|
||||
}
|
||||
}
|
||||
|
||||
if shouldUpdateInviteLinkPassword {
|
||||
if newInviteLinkPassword == currentGroupModel.inviteLinkPassword {
|
||||
// Redundant change, not a conflict.
|
||||
} else {
|
||||
let actionBuilder = GroupsProtoGroupChangeActionsModifyInviteLinkPasswordAction.builder()
|
||||
if let newInviteLinkPassword = self.newInviteLinkPassword {
|
||||
actionBuilder.setInviteLinkPassword(newInviteLinkPassword)
|
||||
}
|
||||
actionsBuilder.setModifyInviteLinkPassword(try actionBuilder.build())
|
||||
didChange = true
|
||||
}
|
||||
}
|
||||
|
||||
let currentGroupMembership = currentGroupModel.groupMembership
|
||||
for (uuid, role) in self.membersToAdd {
|
||||
guard !currentGroupMembership.isNonPendingMember(uuid) else {
|
||||
for (uuid, role) in membersToAdd {
|
||||
guard !currentGroupMembership.isFullMember(uuid) else {
|
||||
// Another user has already added this member.
|
||||
// They may have been added with a different role.
|
||||
// We don't treat that as a conflict.
|
||||
continue
|
||||
}
|
||||
guard let profileKeyCredential = profileKeyCredentialMap[uuid] else {
|
||||
throw OWSAssertionError("Missing profile key credential: \(uuid)")
|
||||
if currentGroupMembership.isRequestingMember(uuid) {
|
||||
let actionBuilder = GroupsProtoGroupChangeActionsPromoteRequestingMemberAction.builder()
|
||||
let userId = try groupV2Params.userId(forUuid: uuid)
|
||||
actionBuilder.setUserID(userId)
|
||||
actionBuilder.setRole(role.asProtoRole)
|
||||
actionsBuilder.addPromoteRequestingMembers(try actionBuilder.build())
|
||||
} else {
|
||||
guard let profileKeyCredential = profileKeyCredentialMap[uuid] else {
|
||||
throw OWSAssertionError("Missing profile key credential: \(uuid)")
|
||||
}
|
||||
let actionBuilder = GroupsProtoGroupChangeActionsAddMemberAction.builder()
|
||||
actionBuilder.setAdded(try GroupsV2Protos.buildMemberProto(profileKeyCredential: profileKeyCredential,
|
||||
role: role.asProtoRole,
|
||||
groupV2Params: groupV2Params))
|
||||
actionsBuilder.addAddMembers(try actionBuilder.build())
|
||||
}
|
||||
let actionBuilder = GroupsProtoGroupChangeActionsAddMemberAction.builder()
|
||||
actionBuilder.setAdded(try GroupsV2Protos.buildMemberProto(profileKeyCredential: profileKeyCredential,
|
||||
role: role.asProtoRole,
|
||||
groupV2Params: groupV2Params))
|
||||
actionsBuilder.addAddMembers(try actionBuilder.build())
|
||||
didChange = true
|
||||
|
||||
if role == .administrator {
|
||||
nonPendingAdministratorCount += 1
|
||||
fullMemberAdministratorCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
for uuid in self.membersToRemove {
|
||||
if currentGroupMembership.isNonPendingMember(uuid) {
|
||||
if currentGroupMembership.isFullMember(uuid) {
|
||||
let actionBuilder = GroupsProtoGroupChangeActionsDeleteMemberAction.builder()
|
||||
let userId = try groupV2Params.userId(forUuid: uuid)
|
||||
actionBuilder.setDeletedUserID(userId)
|
||||
actionsBuilder.addDeleteMembers(try actionBuilder.build())
|
||||
didChange = true
|
||||
|
||||
if currentGroupMembership.isAdministrator(uuid) {
|
||||
nonPendingAdministratorCount -= 1
|
||||
if currentGroupMembership.isFullMemberAndAdministrator(uuid) {
|
||||
fullMemberAdministratorCount -= 1
|
||||
}
|
||||
allMemberCount -= 1
|
||||
} else if currentGroupMembership.isPendingMember(uuid) {
|
||||
} else if currentGroupMembership.isPendingProfileKeyMember(uuid) {
|
||||
let actionBuilder = GroupsProtoGroupChangeActionsDeletePendingMemberAction.builder()
|
||||
let userId = try groupV2Params.userId(forUuid: uuid)
|
||||
actionBuilder.setDeletedUserID(userId)
|
||||
actionsBuilder.addDeletePendingMembers(try actionBuilder.build())
|
||||
didChange = true
|
||||
allMemberCount -= 1
|
||||
} else if currentGroupMembership.isRequestingMember(uuid) {
|
||||
let actionBuilder = GroupsProtoGroupChangeActionsDeleteRequestingMemberAction.builder()
|
||||
let userId = try groupV2Params.userId(forUuid: uuid)
|
||||
actionBuilder.setDeletedUserID(userId)
|
||||
actionsBuilder.addDeleteRequestingMembers(try actionBuilder.build())
|
||||
didChange = true
|
||||
allMemberCount -= 1
|
||||
} else {
|
||||
// Another user has already removed this member or revoked their
|
||||
// invitation.
|
||||
@ -466,8 +527,8 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
|
||||
}
|
||||
}
|
||||
|
||||
for (uuid, role) in self.pendingMembersToAdd {
|
||||
guard !currentGroupMembership.isPendingOrNonPendingMember(uuid) else {
|
||||
for (uuid, role) in self.pendingProfileKeyMembersToAdd {
|
||||
guard !currentGroupMembership.isMemberOfAnyKind(uuid) else {
|
||||
// Another user has already added or invited this member.
|
||||
// They may have been added with a different role.
|
||||
// We don't treat that as a conflict.
|
||||
@ -516,7 +577,7 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
|
||||
}
|
||||
|
||||
for (uuid, newRole) in self.membersToChangeRole {
|
||||
guard currentGroupMembership.isNonPendingMember(uuid) else {
|
||||
guard currentGroupMembership.isFullMember(uuid) else {
|
||||
// User is no longer a member.
|
||||
throw GroupsV2Error.conflictingChange
|
||||
}
|
||||
@ -534,9 +595,9 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
|
||||
didChange = true
|
||||
|
||||
if currentRole == .administrator {
|
||||
nonPendingAdministratorCount -= 1
|
||||
fullMemberAdministratorCount -= 1
|
||||
} else if newRole == .administrator {
|
||||
nonPendingAdministratorCount += 1
|
||||
fullMemberAdministratorCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
@ -546,7 +607,7 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
|
||||
// Redundant change, not a conflict.
|
||||
} else {
|
||||
let actionBuilder = GroupsProtoGroupChangeActionsModifyMembersAccessControlAction.builder()
|
||||
actionBuilder.setMembersAccess(GroupAccess.protoAccess(forGroupV2Access: access))
|
||||
actionBuilder.setMembersAccess(access.protoAccess)
|
||||
actionsBuilder.setModifyMemberAccess(try actionBuilder.build())
|
||||
didChange = true
|
||||
}
|
||||
@ -556,15 +617,25 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
|
||||
// Redundant change, not a conflict.
|
||||
} else {
|
||||
let actionBuilder = GroupsProtoGroupChangeActionsModifyAttributesAccessControlAction.builder()
|
||||
actionBuilder.setAttributesAccess(GroupAccess.protoAccess(forGroupV2Access: access))
|
||||
actionBuilder.setAttributesAccess(access.protoAccess)
|
||||
actionsBuilder.setModifyAttributesAccess(try actionBuilder.build())
|
||||
didChange = true
|
||||
}
|
||||
}
|
||||
if let access = self.accessForAddFromInviteLink {
|
||||
if currentAccess.addFromInviteLink == access {
|
||||
// Redundant change, not a conflict.
|
||||
} else {
|
||||
let actionBuilder = GroupsProtoGroupChangeActionsModifyAddFromInviteLinkAccessControlAction.builder()
|
||||
actionBuilder.setAddFromInviteLinkAccess(access.protoAccess)
|
||||
actionsBuilder.setModifyAddFromInviteLinkAccess(try actionBuilder.build())
|
||||
didChange = true
|
||||
}
|
||||
}
|
||||
|
||||
for uuid in pendingMembersToPromote {
|
||||
for uuid in pendingProfileKeyMembersToPromote {
|
||||
// Check that pending member is still invited.
|
||||
guard currentGroupMembership.isPendingMember(uuid) else {
|
||||
guard currentGroupMembership.isPendingProfileKeyMember(uuid) else {
|
||||
throw GroupsV2Error.redundantChange
|
||||
}
|
||||
guard let profileKeyCredential = profileKeyCredentialMap[uuid] else {
|
||||
@ -578,9 +649,8 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
|
||||
}
|
||||
|
||||
if self.shouldLeaveGroupDeclineInvite {
|
||||
let isLastAdminInV2Group = (currentGroupMembership.isNonPendingMember(localUuid) &&
|
||||
currentGroupMembership.isAdministrator(localUuid) &&
|
||||
nonPendingAdministratorCount == 1)
|
||||
let isLastAdminInV2Group = (currentGroupMembership.isFullMemberAndAdministrator(localUuid) &&
|
||||
fullMemberAdministratorCount == 1)
|
||||
guard !isLastAdminInV2Group else {
|
||||
// This could happen if the last two admins leave at the same time
|
||||
// and race.
|
||||
@ -588,14 +658,14 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
|
||||
}
|
||||
|
||||
// Check that we are still invited or in group.
|
||||
if currentGroupMembership.isPendingMember(localUuid) {
|
||||
if currentGroupMembership.isPendingProfileKeyMember(localUuid) {
|
||||
// Decline invite
|
||||
let actionBuilder = GroupsProtoGroupChangeActionsDeletePendingMemberAction.builder()
|
||||
let localUserId = try groupV2Params.userId(forUuid: localUuid)
|
||||
actionBuilder.setDeletedUserID(localUserId)
|
||||
actionsBuilder.addDeletePendingMembers(try actionBuilder.build())
|
||||
didChange = true
|
||||
} else if currentGroupMembership.isNonPendingMember(localUuid) {
|
||||
} else if currentGroupMembership.isFullMember(localUuid) {
|
||||
// Leave group
|
||||
let actionBuilder = GroupsProtoGroupChangeActionsDeleteMemberAction.builder()
|
||||
let localUserId = try groupV2Params.userId(forUuid: localUuid)
|
||||
|
||||
@ -29,6 +29,7 @@ public struct ChangedGroupModel {
|
||||
|
||||
// MARK: -
|
||||
|
||||
// TODO: Rename to GroupsV2IncomingChanges
|
||||
public class GroupsV2Changes {
|
||||
|
||||
// GroupsV2Changes has one responsibility: applying incremental
|
||||
@ -75,6 +76,7 @@ public class GroupsV2Changes {
|
||||
var newGroupName: String? = oldGroupModel.groupName
|
||||
var newAvatarData: Data? = oldGroupModel.groupAvatarData
|
||||
var newAvatarUrlPath = oldGroupModel.avatarUrlPath
|
||||
var newInviteLinkPassword: Data? = oldGroupModel.inviteLinkPassword
|
||||
|
||||
let oldGroupMembership = oldGroupModel.groupMembership
|
||||
var groupMembershipBuilder = oldGroupMembership.asBuilder
|
||||
@ -82,13 +84,13 @@ public class GroupsV2Changes {
|
||||
let oldGroupAccess: GroupAccess = oldGroupModel.access
|
||||
var newMembersAccess = oldGroupAccess.members
|
||||
var newAttributesAccess = oldGroupAccess.attributes
|
||||
var newAddFromInviteLinkAccess = oldGroupAccess.addFromInviteLink
|
||||
|
||||
if !oldGroupMembership.isPendingOrNonPendingMember(changeAuthorUuid) {
|
||||
if !oldGroupMembership.isMemberOfAnyKind(changeAuthorUuid) {
|
||||
owsFailDebug("changeAuthorUuid not a full member of the group.")
|
||||
}
|
||||
let isChangeAuthorMember = oldGroupMembership.isNonPendingMember(changeAuthorUuid)
|
||||
let isChangeAuthorAdmin = (oldGroupMembership.isNonPendingMember(changeAuthorUuid) &&
|
||||
oldGroupMembership.isAdministrator(changeAuthorUuid))
|
||||
let isChangeAuthorMember = oldGroupMembership.isFullMember(changeAuthorUuid)
|
||||
let isChangeAuthorAdmin = oldGroupMembership.isFullMemberAndAdministrator(changeAuthorUuid)
|
||||
let canAddMembers: Bool
|
||||
switch oldGroupAccess.members {
|
||||
case .unknown:
|
||||
@ -100,6 +102,8 @@ public class GroupsV2Changes {
|
||||
case .any:
|
||||
// We no longer honor the "any" level.
|
||||
canAddMembers = false
|
||||
case .unsatisfiable:
|
||||
canAddMembers = false
|
||||
}
|
||||
let canRemoveMembers = isChangeAuthorAdmin
|
||||
let canModifyRoles = isChangeAuthorAdmin
|
||||
@ -114,8 +118,11 @@ public class GroupsV2Changes {
|
||||
case .any:
|
||||
// We no longer honor the "any" level.
|
||||
canEditAttributes = false
|
||||
case .unsatisfiable:
|
||||
canEditAttributes = false
|
||||
}
|
||||
let canEditAccess = isChangeAuthorAdmin
|
||||
let canEditInviteLinks = isChangeAuthorAdmin
|
||||
|
||||
// This client can learn of profile keys from parsing group state protos.
|
||||
// After parsing, we should fill in profileKeys in the profile manager.
|
||||
@ -146,12 +153,12 @@ public class GroupsV2Changes {
|
||||
// the service. This is one.
|
||||
let uuid = try groupV2Params.uuid(forUserId: userId)
|
||||
|
||||
guard !oldGroupMembership.isNonPendingMember(uuid) else {
|
||||
guard !oldGroupMembership.isFullMember(uuid) else {
|
||||
throw OWSAssertionError("Invalid membership.")
|
||||
}
|
||||
groupMembershipBuilder.removeInvalidInvite(userId: userId)
|
||||
groupMembershipBuilder.remove(uuid)
|
||||
groupMembershipBuilder.addNonPendingMember(uuid, role: role)
|
||||
groupMembershipBuilder.addFullMember(uuid, role: role)
|
||||
|
||||
guard let profileKeyCiphertextData = member.profileKey else {
|
||||
throw OWSAssertionError("Missing profileKeyCiphertext.")
|
||||
@ -176,7 +183,7 @@ public class GroupsV2Changes {
|
||||
// Any member can leave the group.
|
||||
owsFailDebug("Cannot kick member.")
|
||||
}
|
||||
if !oldGroupMembership.isNonPendingMember(uuid) {
|
||||
if !oldGroupMembership.isFullMember(uuid) {
|
||||
owsFailDebug("Invalid membership.")
|
||||
}
|
||||
groupMembershipBuilder.removeInvalidInvite(userId: userId)
|
||||
@ -206,14 +213,14 @@ public class GroupsV2Changes {
|
||||
// the service. This is one.
|
||||
let uuid = try groupV2Params.uuid(forUserId: userId)
|
||||
|
||||
guard oldGroupMembership.isNonPendingMember(uuid) else {
|
||||
guard oldGroupMembership.isFullMember(uuid) else {
|
||||
throw OWSAssertionError("Invalid membership.")
|
||||
}
|
||||
if oldGroupMembership.role(for: uuid) == role {
|
||||
owsFailDebug("Member already has that role.")
|
||||
}
|
||||
groupMembershipBuilder.remove(uuid)
|
||||
groupMembershipBuilder.addNonPendingMember(uuid, role: role)
|
||||
groupMembershipBuilder.addFullMember(uuid, role: role)
|
||||
}
|
||||
|
||||
for action in changeActionsProto.modifyMemberProfileKeys {
|
||||
@ -224,7 +231,7 @@ public class GroupsV2Changes {
|
||||
let uuidCiphertext = try presentation.getUuidCiphertext()
|
||||
let uuid = try groupV2Params.uuid(forUuidCiphertext: uuidCiphertext)
|
||||
|
||||
guard oldGroupMembership.isNonPendingMember(uuid) else {
|
||||
guard oldGroupMembership.isFullMember(uuid) else {
|
||||
throw OWSAssertionError("Invalid membership.")
|
||||
}
|
||||
|
||||
@ -284,12 +291,12 @@ public class GroupsV2Changes {
|
||||
}
|
||||
continue
|
||||
}
|
||||
guard !oldGroupMembership.isPendingOrNonPendingMember(uuid) else {
|
||||
guard !oldGroupMembership.isMemberOfAnyKind(uuid) else {
|
||||
throw OWSAssertionError("Invalid membership.")
|
||||
}
|
||||
groupMembershipBuilder.removeInvalidInvite(userId: userId)
|
||||
groupMembershipBuilder.remove(uuid)
|
||||
groupMembershipBuilder.addPendingMember(uuid, role: role, addedByUuid: addedByUuid)
|
||||
groupMembershipBuilder.addPendingProfileKeyMember(uuid, role: role, addedByUuid: addedByUuid)
|
||||
}
|
||||
|
||||
for action in changeActionsProto.deletePendingMembers {
|
||||
@ -309,7 +316,7 @@ public class GroupsV2Changes {
|
||||
}
|
||||
|
||||
guard oldGroupMembership.hasInvalidInvite(forUserId: userId) ||
|
||||
oldGroupMembership.isPendingMember(uuid) else {
|
||||
oldGroupMembership.isPendingProfileKeyMember(uuid) else {
|
||||
throw OWSAssertionError("Invalid membership.")
|
||||
}
|
||||
groupMembershipBuilder.removeInvalidInvite(userId: userId)
|
||||
@ -336,10 +343,10 @@ public class GroupsV2Changes {
|
||||
let userId = uuidCiphertext.serialize().asData
|
||||
let uuid = try groupV2Params.uuid(forUuidCiphertext: uuidCiphertext)
|
||||
|
||||
guard oldGroupMembership.isPendingMember(uuid) else {
|
||||
guard oldGroupMembership.isPendingProfileKeyMember(uuid) else {
|
||||
throw OWSAssertionError("Invalid membership.")
|
||||
}
|
||||
guard !oldGroupMembership.isNonPendingMember(uuid) else {
|
||||
guard !oldGroupMembership.isFullMember(uuid) else {
|
||||
throw OWSAssertionError("Invalid membership.")
|
||||
}
|
||||
guard let role = oldGroupMembership.role(for: uuid) else {
|
||||
@ -347,7 +354,7 @@ public class GroupsV2Changes {
|
||||
}
|
||||
groupMembershipBuilder.removeInvalidInvite(userId: userId)
|
||||
groupMembershipBuilder.remove(uuid)
|
||||
groupMembershipBuilder.addNonPendingMember(uuid, role: role)
|
||||
groupMembershipBuilder.addFullMember(uuid, role: role)
|
||||
|
||||
if uuid != changeAuthorUuid {
|
||||
// Only the invitee can accept an invitation.
|
||||
@ -410,7 +417,7 @@ public class GroupsV2Changes {
|
||||
guard let protoAccess = action.attributesAccess else {
|
||||
throw OWSAssertionError("Missing access.")
|
||||
}
|
||||
newAttributesAccess = GroupAccess.groupV2Access(forProtoAccess: protoAccess)
|
||||
newAttributesAccess = GroupV2Access.access(forProtoAccess: protoAccess)
|
||||
|
||||
if newAttributesAccess == .unknown {
|
||||
owsFailDebug("Unknown attributes access.")
|
||||
@ -425,15 +432,99 @@ public class GroupsV2Changes {
|
||||
guard let protoAccess = action.membersAccess else {
|
||||
throw OWSAssertionError("Missing access.")
|
||||
}
|
||||
newMembersAccess = GroupAccess.groupV2Access(forProtoAccess: protoAccess)
|
||||
newMembersAccess = GroupV2Access.access(forProtoAccess: protoAccess)
|
||||
|
||||
if newMembersAccess == .unknown {
|
||||
owsFailDebug("Unknown member access.")
|
||||
}
|
||||
}
|
||||
|
||||
if let action = changeActionsProto.modifyAddFromInviteLinkAccess {
|
||||
if !canEditInviteLinks {
|
||||
owsFailDebug("Cannot edit addFromInviteLink access.")
|
||||
}
|
||||
|
||||
guard let protoAccess = action.addFromInviteLinkAccess else {
|
||||
throw OWSAssertionError("Missing access.")
|
||||
}
|
||||
newAddFromInviteLinkAccess = GroupV2Access.access(forProtoAccess: protoAccess)
|
||||
|
||||
if newAddFromInviteLinkAccess == .unknown {
|
||||
owsFailDebug("Unknown addFromInviteLink access.")
|
||||
}
|
||||
}
|
||||
|
||||
for action in changeActionsProto.addRequestingMembers {
|
||||
if !canAddMembers {
|
||||
owsFailDebug("Cannot add members.")
|
||||
}
|
||||
|
||||
guard let requestingMember = action.added else {
|
||||
throw OWSAssertionError("Missing requestingMember.")
|
||||
}
|
||||
// TODO: Modify this parsing to reflect action contents.
|
||||
guard let userId = requestingMember.userID else {
|
||||
throw OWSAssertionError("Missing userID.")
|
||||
}
|
||||
// Some userIds/uuidCiphertexts can be validated by
|
||||
// the service. This is one.
|
||||
//
|
||||
// TODO: Is this one?
|
||||
let uuid = try groupV2Params.uuid(forUserId: userId)
|
||||
|
||||
guard let profileKeyCiphertextData = requestingMember.profileKey else {
|
||||
throw OWSAssertionError("Missing profileKeyCiphertext.")
|
||||
}
|
||||
let profileKeyCiphertext = try ProfileKeyCiphertext(contents: [UInt8](profileKeyCiphertextData))
|
||||
let profileKey = try groupV2Params.profileKey(forProfileKeyCiphertext: profileKeyCiphertext,
|
||||
uuid: uuid)
|
||||
|
||||
guard let presentationData = requestingMember.presentation else {
|
||||
throw OWSAssertionError("Missing presentation.")
|
||||
}
|
||||
let presentation = try ProfileKeyCredentialPresentation(contents: [UInt8](presentationData))
|
||||
let uuidCiphertext = try presentation.getUuidCiphertext()
|
||||
let uuidFromPresentation = try groupV2Params.uuid(forUuidCiphertext: uuidCiphertext)
|
||||
// TODO:
|
||||
guard uuid == uuidFromPresentation else {
|
||||
throw OWSAssertionError("uuids do not match.")
|
||||
}
|
||||
|
||||
guard oldGroupMembership.isMemberOfAnyKind(uuid) else {
|
||||
throw OWSAssertionError("Invalid membership.")
|
||||
}
|
||||
|
||||
let profileKeyCiphertextFromPresentation = try presentation.getProfileKeyCiphertext()
|
||||
let profileKeyFromPresentation = try groupV2Params.profileKey(forProfileKeyCiphertext: profileKeyCiphertextFromPresentation,
|
||||
uuid: uuid)
|
||||
// TODO:
|
||||
guard profileKey == profileKeyFromPresentation else {
|
||||
throw OWSAssertionError("profileKeys do not match.")
|
||||
}
|
||||
|
||||
guard !oldGroupMembership.isMemberOfAnyKind(uuid) else {
|
||||
throw OWSAssertionError("Invalid membership.")
|
||||
}
|
||||
groupMembershipBuilder.removeInvalidInvite(userId: userId)
|
||||
groupMembershipBuilder.remove(uuid)
|
||||
groupMembershipBuilder.addRequestingMember(uuid)
|
||||
|
||||
profileKeys[uuid] = profileKey
|
||||
}
|
||||
|
||||
if let action = changeActionsProto.modifyInviteLinkPassword {
|
||||
if !canEditInviteLinks {
|
||||
owsFailDebug("Cannot modify inviteLinkPassword.")
|
||||
}
|
||||
|
||||
// Change clears or updates the group inviteLinkPassword.
|
||||
newInviteLinkPassword = action.inviteLinkPassword
|
||||
}
|
||||
|
||||
let newGroupMembership = groupMembershipBuilder.build()
|
||||
let newGroupAccess = GroupAccess(members: newMembersAccess, attributes: newAttributesAccess)
|
||||
let newGroupAccess = GroupAccess(members: newMembersAccess, attributes: newAttributesAccess, addFromInviteLink: newAddFromInviteLinkAccess)
|
||||
|
||||
GroupsV2Protos.validateInviteLinkState(inviteLinkPassword: newInviteLinkPassword, groupAccess: newGroupAccess)
|
||||
|
||||
var builder = oldGroupModel.asBuilder
|
||||
builder.name = newGroupName
|
||||
@ -442,6 +533,7 @@ public class GroupsV2Changes {
|
||||
builder.groupAccess = newGroupAccess
|
||||
builder.groupV2Revision = newRevision
|
||||
builder.avatarUrlPath = newAvatarUrlPath
|
||||
builder.inviteLinkPassword = newInviteLinkPassword
|
||||
let newGroupModel = try builder.buildAsV2(transaction: transaction)
|
||||
|
||||
return ChangedGroupModel(oldGroupModel: oldGroupModel,
|
||||
|
||||
@ -118,7 +118,7 @@ class GroupsV2ProfileKeyUpdater {
|
||||
let groupMembership = groupThread.groupModel.groupMembership
|
||||
// We only need to update v2 groups of which we are a full member.
|
||||
guard groupThread.isGroupV2Thread,
|
||||
groupMembership.isNonPendingMember(localAddress) else {
|
||||
groupMembership.isFullMember(localAddress) else {
|
||||
return
|
||||
}
|
||||
let groupId = groupThread.groupModel.groupId
|
||||
@ -254,7 +254,7 @@ class GroupsV2ProfileKeyUpdater {
|
||||
}
|
||||
return self.groupsV2.fetchCurrentGroupV2Snapshot(groupModel: groupModel)
|
||||
}.map(on: .global()) { (groupV2Snapshot: GroupV2Snapshot) throws -> TSGroupThread in
|
||||
guard groupV2Snapshot.groupMembership.isNonPendingMember(localAddress) else {
|
||||
guard groupV2Snapshot.groupMembership.isFullMember(localAddress) else {
|
||||
// We're not a full member, no need to update profile key.
|
||||
throw GroupsV2Error.redundantChange
|
||||
}
|
||||
|
||||
@ -101,16 +101,20 @@ public class GroupsV2Protos {
|
||||
groupBuilder.setAvatar(avatarUrl)
|
||||
}
|
||||
|
||||
groupBuilder.setAccessControl(try buildAccessProto(groupAccess: groupModel.access))
|
||||
let groupAccess = groupModel.access
|
||||
groupBuilder.setAccessControl(try buildAccessProto(groupAccess: groupAccess))
|
||||
|
||||
if let inviteLinkPassword = groupModel.inviteLinkPassword,
|
||||
!inviteLinkPassword.isEmpty {
|
||||
groupBuilder.setInviteLinkPassword(inviteLinkPassword)
|
||||
}
|
||||
|
||||
// * You will be member 0 and the only admin.
|
||||
// * Other members will be non-admin members.
|
||||
//
|
||||
// Add local user first to ensure that they are user 0.
|
||||
let groupMembership = groupModel.groupMembership
|
||||
let localAddress = SignalServiceAddress(uuid: localUuid)
|
||||
assert(groupMembership.isAdministrator(localAddress))
|
||||
assert(!groupMembership.isPendingMember(localAddress))
|
||||
assert(groupMembership.isFullMemberAndAdministrator(localUuid))
|
||||
groupBuilder.addMembers(try buildMemberProto(profileKeyCredential: localProfileKeyCredential,
|
||||
role: .administrator,
|
||||
groupV2Params: groupV2Params))
|
||||
@ -118,25 +122,24 @@ public class GroupsV2Protos {
|
||||
guard uuid != localUuid else {
|
||||
continue
|
||||
}
|
||||
let address = SignalServiceAddress(uuid: uuid)
|
||||
let isAdministrator = groupMembership.isAdministrator(address)
|
||||
let isPending = groupMembership.isPendingMember(address)
|
||||
let role: GroupsProtoMemberRole = isAdministrator ? .administrator : .`default`
|
||||
guard !isPending else {
|
||||
let isInvited = groupMembership.isPendingProfileKeyMember(uuid)
|
||||
guard !isInvited else {
|
||||
continue
|
||||
}
|
||||
let isAdministrator = groupMembership.isFullMemberAndAdministrator(uuid)
|
||||
let role: GroupsProtoMemberRole = isAdministrator ? .administrator : .`default`
|
||||
groupBuilder.addMembers(try buildMemberProto(profileKeyCredential: profileKeyCredential,
|
||||
role: role,
|
||||
groupV2Params: groupV2Params))
|
||||
}
|
||||
for address in groupMembership.pendingMembers {
|
||||
for address in groupMembership.pendingProfileKeyMembers {
|
||||
guard let uuid = address.uuid else {
|
||||
throw OWSAssertionError("Missing uuid.")
|
||||
}
|
||||
guard uuid != localUuid else {
|
||||
continue
|
||||
}
|
||||
let isAdministrator = groupMembership.isAdministrator(address)
|
||||
let isAdministrator = groupMembership.isFullOrInvitedAdministrator(uuid)
|
||||
let role: GroupsProtoMemberRole = isAdministrator ? .administrator : .`default`
|
||||
groupBuilder.addPendingMembers(try buildPendingMemberProto(uuid: uuid,
|
||||
role: role,
|
||||
@ -144,13 +147,26 @@ public class GroupsV2Protos {
|
||||
groupV2Params: groupV2Params))
|
||||
}
|
||||
|
||||
validateInviteLinkState(inviteLinkPassword: groupModel.inviteLinkPassword, groupAccess: groupAccess)
|
||||
|
||||
return try groupBuilder.build()
|
||||
}
|
||||
|
||||
public class func validateInviteLinkState(inviteLinkPassword: Data?, groupAccess: GroupAccess) {
|
||||
let canJoinFromInviteLink = groupAccess.addFromInviteLink != .unsatisfiable
|
||||
let hasInviteLinkPassword = inviteLinkPassword?.count ?? 0 > 0
|
||||
if canJoinFromInviteLink, !hasInviteLinkPassword {
|
||||
owsFailDebug("Invite links enabled without inviteLinkPassword.")
|
||||
} else if !canJoinFromInviteLink, hasInviteLinkPassword {
|
||||
owsFailDebug("inviteLinkPassword set but invite links not enabled.")
|
||||
}
|
||||
}
|
||||
|
||||
public class func buildAccessProto(groupAccess: GroupAccess) throws -> GroupsProtoAccessControl {
|
||||
let builder = GroupsProtoAccessControl.builder()
|
||||
builder.setAttributes(GroupAccess.protoAccess(forGroupV2Access: groupAccess.attributes))
|
||||
builder.setMembers(GroupAccess.protoAccess(forGroupV2Access: groupAccess.members))
|
||||
builder.setAttributes(groupAccess.attributes.protoAccess)
|
||||
builder.setMembers(groupAccess.members.protoAccess)
|
||||
builder.setAddFromInviteLink(groupAccess.addFromInviteLink.protoAccess)
|
||||
return try builder.build()
|
||||
}
|
||||
|
||||
@ -316,6 +332,8 @@ public class GroupsV2Protos {
|
||||
pendingMembers.append(pendingMember)
|
||||
}
|
||||
|
||||
let inviteLinkPassword = groupProto.inviteLinkPassword
|
||||
|
||||
guard let accessControl = groupProto.accessControl else {
|
||||
throw OWSAssertionError("Missing accessControl.")
|
||||
}
|
||||
@ -325,11 +343,20 @@ public class GroupsV2Protos {
|
||||
guard let accessControlForMembers = accessControl.members else {
|
||||
throw OWSAssertionError("Missing accessControl.members.")
|
||||
}
|
||||
// If group state does not have "invite link" access specified,
|
||||
// assume invite links are disabled.
|
||||
let accessControlForAddFromInviteLink = accessControl.addFromInviteLink ?? .unsatisfiable
|
||||
|
||||
// If the timer blob is not populated or has zero duration,
|
||||
// disappearing messages should be disabled.
|
||||
let disappearingMessageToken = groupV2Params.decryptDisappearingMessagesTimer(groupProto.disappearingMessagesTimer)
|
||||
|
||||
let groupAccess = GroupAccess(members: GroupV2Access.access(forProtoAccess: accessControlForMembers),
|
||||
attributes: GroupV2Access.access(forProtoAccess: accessControlForAttributes),
|
||||
addFromInviteLink: GroupV2Access.access(forProtoAccess: accessControlForAddFromInviteLink))
|
||||
|
||||
validateInviteLinkState(inviteLinkPassword: inviteLinkPassword, groupAccess: groupAccess)
|
||||
|
||||
let revision = groupProto.revision
|
||||
let groupSecretParamsData = groupV2Params.groupSecretParamsData
|
||||
return GroupV2SnapshotImpl(groupSecretParamsData: groupSecretParamsData,
|
||||
@ -341,8 +368,8 @@ public class GroupsV2Protos {
|
||||
members: members,
|
||||
pendingMembers: pendingMembers,
|
||||
invalidInvites: invalidInvites,
|
||||
accessControlForAttributes: accessControlForAttributes,
|
||||
accessControlForMembers: accessControlForMembers,
|
||||
groupAccess: groupAccess,
|
||||
inviteLinkPassword: inviteLinkPassword,
|
||||
disappearingMessageToken: disappearingMessageToken,
|
||||
profileKeys: profileKeys)
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@ public extension GroupManager {
|
||||
replacementAdminUuid: UUID? = nil,
|
||||
success: (() -> Void)?) {
|
||||
|
||||
guard groupThread.isLocalUserPendingOrNonPendingMember else {
|
||||
guard groupThread.isLocalUserMemberOfAnyKind else {
|
||||
owsFailDebug("unexpectedly trying to leave group for which we're not a member.")
|
||||
return
|
||||
}
|
||||
|
||||
@ -6,24 +6,39 @@ import Foundation
|
||||
|
||||
@objc
|
||||
public extension TSGroupThread {
|
||||
var isLocalUserInGroup: Bool {
|
||||
var isLocalUserMemberOfAnyKind: Bool {
|
||||
guard let localAddress = TSAccountManager.localAddress else {
|
||||
return false
|
||||
}
|
||||
return groupModel.groupMembership.isNonPendingMember(localAddress)
|
||||
return groupModel.groupMembership.isMemberOfAnyKind(localAddress)
|
||||
}
|
||||
|
||||
var isLocalUserPendingOrNonPendingMember: Bool {
|
||||
var isLocalUserFullMemberOfGroup: Bool {
|
||||
guard let localAddress = TSAccountManager.localAddress else {
|
||||
return false
|
||||
}
|
||||
return groupModel.groupMembership.isPendingOrNonPendingMember(localAddress)
|
||||
return groupModel.groupMembership.isFullMember(localAddress)
|
||||
}
|
||||
|
||||
var isLocalUserPendingMember: Bool {
|
||||
var isLocalUserPendingProfileKeyMember: Bool {
|
||||
guard let localAddress = TSAccountManager.localAddress else {
|
||||
return false
|
||||
}
|
||||
return groupModel.groupMembership.isPendingMember(localAddress)
|
||||
return groupModel.groupMembership.isPendingProfileKeyMember(localAddress)
|
||||
}
|
||||
|
||||
var isLocalUserRequestingMember: Bool {
|
||||
guard let localAddress = TSAccountManager.localAddress else {
|
||||
return false
|
||||
}
|
||||
return groupModel.groupMembership.isRequestingMember(localAddress)
|
||||
}
|
||||
|
||||
var isLocalUserFullOrInvitedMemberOfGroup: Bool {
|
||||
guard let localAddress = TSAccountManager.localAddress else {
|
||||
return false
|
||||
}
|
||||
return (groupModel.groupMembership.isFullMember(localAddress) ||
|
||||
groupModel.groupMembership.isPendingProfileKeyMember(localAddress))
|
||||
}
|
||||
}
|
||||
|
||||
@ -346,6 +346,10 @@ extension GroupUpdateCopy {
|
||||
case .administrator:
|
||||
return NSLocalizedString("GROUP_ACCESS_LEVEL_ADMINISTRATORS",
|
||||
comment: "Description of the 'admins only' access level.")
|
||||
case .unsatisfiable:
|
||||
// TODO:
|
||||
return NSLocalizedString("GROUP_ACCESS_LEVEL_UNSATISFIABLE",
|
||||
comment: "Description of the 'unsatisfiable' access level.")
|
||||
}
|
||||
}
|
||||
|
||||
@ -415,7 +419,7 @@ extension GroupUpdateCopy {
|
||||
mutating func addMembershipUpdates(oldGroupMembership: GroupMembership) {
|
||||
var membershipCounts = MembershipCounts()
|
||||
|
||||
let allUsersUnsorted = oldGroupMembership.allUsers.union(newGroupMembership.allUsers)
|
||||
let allUsersUnsorted = oldGroupMembership.allMembersOfAnyKind.union(newGroupMembership.allMembersOfAnyKind)
|
||||
var allUsersSorted = allUsersUnsorted.sorted { (left, right) -> Bool in
|
||||
// Use an arbitrary sort to ensure the output is deterministic.
|
||||
return left.stringForDisplay > right.stringForDisplay
|
||||
@ -443,6 +447,9 @@ extension GroupUpdateCopy {
|
||||
addMemberRoleUpdates(for: address, oldGroupMembership: oldGroupMembership)
|
||||
case .invited:
|
||||
addUserLeftOrWasKickedOutOfGroupThenWasInvitedToTheGroup(for: address)
|
||||
case .requesting:
|
||||
owsFailDebug("This transition is unexpected")
|
||||
addUserRequestedToJoinGroup(for: address)
|
||||
case .none:
|
||||
addUserLeftOrWasKickedOutOfGroup(for: address)
|
||||
}
|
||||
@ -468,11 +475,26 @@ extension GroupUpdateCopy {
|
||||
case .invited:
|
||||
// Membership status didn't change.
|
||||
break
|
||||
case .requesting:
|
||||
addUserRequestedToJoinGroup(for: address)
|
||||
case .none:
|
||||
addUserInviteWasDeclinedOrRevoked(for: address,
|
||||
oldGroupMembership: oldGroupMembership,
|
||||
membershipCounts: &membershipCounts)
|
||||
}
|
||||
case .requesting:
|
||||
switch newMembershipStatus {
|
||||
case .normalMember:
|
||||
addUserRequestWasApproved(for: address)
|
||||
case .invited:
|
||||
addUserWasInvitedToTheGroup(for: address,
|
||||
membershipCounts: &membershipCounts)
|
||||
case .requesting:
|
||||
// Membership status didn't change.
|
||||
break
|
||||
case .none:
|
||||
addUserRequestWasRejected(for: address)
|
||||
}
|
||||
case .none:
|
||||
switch newMembershipStatus {
|
||||
case .normalMember:
|
||||
@ -480,6 +502,8 @@ extension GroupUpdateCopy {
|
||||
case .invited:
|
||||
addUserWasInvitedToTheGroup(for: address,
|
||||
membershipCounts: &membershipCounts)
|
||||
case .requesting:
|
||||
addUserRequestedToJoinGroup(for: address)
|
||||
case .none:
|
||||
// Membership status didn't change.
|
||||
break
|
||||
@ -579,8 +603,8 @@ extension GroupUpdateCopy {
|
||||
mutating func addMemberRoleUpdates(for address: SignalServiceAddress,
|
||||
oldGroupMembership: GroupMembership) {
|
||||
|
||||
let oldIsAdministrator = oldGroupMembership.isAdministrator(address)
|
||||
let newIsAdministrator = newGroupMembership.isAdministrator(address)
|
||||
let oldIsAdministrator = oldGroupMembership.isFullMemberAndAdministrator(address)
|
||||
let newIsAdministrator = newGroupMembership.isFullMemberAndAdministrator(address)
|
||||
|
||||
guard oldIsAdministrator != newIsAdministrator else {
|
||||
// Role didn't change.
|
||||
@ -826,7 +850,7 @@ extension GroupUpdateCopy {
|
||||
|
||||
var inviterName: String?
|
||||
var inviterAddress: SignalServiceAddress?
|
||||
if let inviterUuid = oldGroupMembership.addedByUuid(forPendingMember: address) {
|
||||
if let inviterUuid = oldGroupMembership.addedByUuid(forPendingProfileKeyMember: address) {
|
||||
inviterAddress = SignalServiceAddress(uuid: inviterUuid)
|
||||
inviterName = contactsManager.displayName(for: SignalServiceAddress(uuid: inviterUuid),
|
||||
transaction: transaction)
|
||||
@ -924,7 +948,7 @@ extension GroupUpdateCopy {
|
||||
|
||||
var inviterName: String?
|
||||
var inviterAddress: SignalServiceAddress?
|
||||
if let inviterUuid = oldGroupMembership.addedByUuid(forPendingMember: address) {
|
||||
if let inviterUuid = oldGroupMembership.addedByUuid(forPendingProfileKeyMember: address) {
|
||||
inviterAddress = SignalServiceAddress(uuid: inviterUuid)
|
||||
inviterName = contactsManager.displayName(for: SignalServiceAddress(uuid: inviterUuid),
|
||||
transaction: transaction)
|
||||
@ -1198,6 +1222,77 @@ extension GroupUpdateCopy {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Requesting Members
|
||||
|
||||
mutating func addUserRequestedToJoinGroup(for address: SignalServiceAddress) {
|
||||
let isLocalUser = localAddress == address
|
||||
if isLocalUser {
|
||||
addItem(.userMembershipState,
|
||||
address: address,
|
||||
copy: NSLocalizedString("GROUP_LOCAL_USER_REQUESTED_TO_JOIN_TO_THE_GROUP",
|
||||
comment: "Message indicating that the local user requested to join the group."))
|
||||
} else {
|
||||
let requesterName = contactsManager.displayName(for: address, transaction: transaction)
|
||||
let format = NSLocalizedString("GROUP_REMOTE_USER_REQUESTED_TO_JOIN_THE_GROUP_FORMAT",
|
||||
comment: "Message indicating that a remote user requested to join the group. Embeds {{requesting user name}}.")
|
||||
addItem(.userMembershipState, address: address, format: format, requesterName)
|
||||
}
|
||||
}
|
||||
|
||||
mutating func addUserRequestWasApproved(for address: SignalServiceAddress) {
|
||||
let isLocalUser = localAddress == address
|
||||
if isLocalUser {
|
||||
// TODO: Should we embed the name of the user who approved us?
|
||||
addItem(.userMembershipState,
|
||||
address: address,
|
||||
copy: NSLocalizedString("GROUP_LOCAL_USER_REQUEST_APPROVED",
|
||||
comment: "Message indicating that the local user's request to join the group was approved."))
|
||||
} else {
|
||||
let requesterName = contactsManager.displayName(for: address, transaction: transaction)
|
||||
switch updater {
|
||||
case .localUser:
|
||||
let format = NSLocalizedString("GROUP_REMOTE_USER_REQUEST_APPROVED_BY_LOCAL_USER_FORMAT",
|
||||
comment: "Message indicating that a remote user's request to join the group was approved by the local user. Embeds {{requesting user name}}.")
|
||||
addItem(.userMembershipState, address: address, format: format, requesterName)
|
||||
case .otherUser(let updaterName, _):
|
||||
let format = NSLocalizedString("GROUP_REMOTE_USER_REQUEST_APPROVED_BY_REMOTE_USER_FORMAT",
|
||||
comment: "Message indicating that a remote user's request to join the group was approved by another user. Embeds {{ %1$@ requesting user name, %2$@ approving user name }}.")
|
||||
addItem(.userMembershipState, address: address, format: format, requesterName, updaterName)
|
||||
case .unknown:
|
||||
let format = NSLocalizedString("GROUP_REMOTE_USER_REQUEST_APPROVED_FORMAT",
|
||||
comment: "Message indicating that a remote user's request to join the group was approved. Embeds {{requesting user name}}.")
|
||||
addItem(.userMembershipState, address: address, format: format, requesterName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutating func addUserRequestWasRejected(for address: SignalServiceAddress) {
|
||||
let isLocalUser = localAddress == address
|
||||
if isLocalUser {
|
||||
// TODO: Should we embed the name of the user who rejected us?
|
||||
addItem(.userMembershipState,
|
||||
address: address,
|
||||
copy: NSLocalizedString("GROUP_LOCAL_USER_REQUEST_REJECTED",
|
||||
comment: "Message indicating that the local user's request to join the group was rejected."))
|
||||
} else {
|
||||
let requesterName = contactsManager.displayName(for: address, transaction: transaction)
|
||||
switch updater {
|
||||
case .localUser:
|
||||
let format = NSLocalizedString("GROUP_REMOTE_USER_REQUEST_REJECTED_BY_LOCAL_USER_FORMAT",
|
||||
comment: "Message indicating that a remote user's request to join the group was rejected by the local user. Embeds {{requesting user name}}.")
|
||||
addItem(.userMembershipState, address: address, format: format, requesterName)
|
||||
case .otherUser(let updaterName, _):
|
||||
let format = NSLocalizedString("GROUP_REMOTE_USER_REQUEST_REJECTED_BY_REMOTE_USER_FORMAT",
|
||||
comment: "Message indicating that a remote user's request to join the group was rejected by another user. Embeds {{ %1$@ requesting user name, %2$@ approving user name }}.")
|
||||
addItem(.userMembershipState, address: address, format: format, requesterName, updaterName)
|
||||
case .unknown:
|
||||
let format = NSLocalizedString("GROUP_REMOTE_USER_REQUEST_REJECTED_FORMAT",
|
||||
comment: "Message indicating that a remote user's request to join the group was rejected. Embeds {{requesting user name}}.")
|
||||
addItem(.userMembershipState, address: address, format: format, requesterName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Disappearing Messages
|
||||
|
||||
mutating func addDisappearingMessageUpdates(oldToken: DisappearingMessageToken?,
|
||||
@ -1311,7 +1406,7 @@ extension GroupUpdateCopy {
|
||||
comment: "Message indicating that the local user has joined the group."))
|
||||
case .invited:
|
||||
if let localAddress = Self.tsAccountManager.localAddress,
|
||||
let inviterUuid = newGroupMembership.addedByUuid(forPendingMember: localAddress) {
|
||||
let inviterUuid = newGroupMembership.addedByUuid(forPendingProfileKeyMember: localAddress) {
|
||||
let inviterAddress = SignalServiceAddress(uuid: inviterUuid)
|
||||
let inviterName = contactsManager.displayName(for: inviterAddress, transaction: transaction)
|
||||
let format = NSLocalizedString("GROUP_LOCAL_USER_INVITED_BY_REMOTE_USER_FORMAT",
|
||||
@ -1325,6 +1420,12 @@ extension GroupUpdateCopy {
|
||||
copy: NSLocalizedString("GROUP_LOCAL_USER_INVITED_TO_THE_GROUP",
|
||||
comment: "Message indicating that the local user was invited to the group."))
|
||||
}
|
||||
case .requesting:
|
||||
if let localAddress = Self.tsAccountManager.localAddress {
|
||||
addUserRequestedToJoinGroup(for: localAddress)
|
||||
} else {
|
||||
owsFailDebug("Missing localAddress.")
|
||||
}
|
||||
case .none:
|
||||
if !DebugFlags.permissiveGroupUpdateInfoMessages {
|
||||
owsFailDebug("Learned of group without any membership status.")
|
||||
@ -1336,9 +1437,10 @@ extension GroupUpdateCopy {
|
||||
|
||||
// MARK: - Membership Status
|
||||
|
||||
enum MembershipStatus {
|
||||
fileprivate enum MembershipStatus {
|
||||
case normalMember
|
||||
case invited
|
||||
case requesting
|
||||
case none
|
||||
}
|
||||
|
||||
@ -1348,10 +1450,12 @@ extension GroupUpdateCopy {
|
||||
|
||||
func membershipStatus(of address: SignalServiceAddress,
|
||||
in groupMembership: GroupMembership) -> MembershipStatus {
|
||||
if groupMembership.isNonPendingMember(address) {
|
||||
if groupMembership.isFullMember(address) {
|
||||
return .normalMember
|
||||
} else if groupMembership.isPendingMember(address) {
|
||||
} else if groupMembership.isPendingProfileKeyMember(address) {
|
||||
return .invited
|
||||
} else if groupMembership.isRequestingMember(address) {
|
||||
return .requesting
|
||||
} else {
|
||||
return .none
|
||||
}
|
||||
|
||||
@ -486,7 +486,8 @@ NSString *const kOWSBlockingManager_SyncedBlockedGroupIdsKey = @"kOWSBlockingMan
|
||||
TSGroupThread *_Nullable groupThread = [TSGroupThread fetchWithGroupId:groupId transaction:transaction];
|
||||
|
||||
// Quit the group if we're a member
|
||||
if (blockMode == BlockMode_LocalShouldLeaveGroups && groupThread.isLocalUserPendingOrNonPendingMember) {
|
||||
BOOL isInGroup = groupThread.isLocalUserMemberOfAnyKind;
|
||||
if (blockMode == BlockMode_LocalShouldLeaveGroups && isInGroup) {
|
||||
[GroupManager leaveGroupOrDeclineInviteAsyncWithoutUIWithGroupThread:groupThread
|
||||
transaction:transaction
|
||||
success:nil];
|
||||
|
||||
@ -719,7 +719,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
return nil;
|
||||
}
|
||||
if (groupThread) {
|
||||
if (!groupThread.isLocalUserInGroup) {
|
||||
if (!groupThread.isLocalUserFullMemberOfGroup) {
|
||||
OWSLogInfo(@"Ignoring messages for left group.");
|
||||
return nil;
|
||||
}
|
||||
@ -806,21 +806,16 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (!groupThread.isLocalUserInGroup) {
|
||||
// We don't want to process messages for groups in which we are a pending member.
|
||||
OWSLogInfo(@"Ignoring messages for left group.");
|
||||
return nil;
|
||||
}
|
||||
if (!envelope.sourceAddress) {
|
||||
OWSFailDebug(@"Missing sender address.");
|
||||
return nil;
|
||||
}
|
||||
if (!groupThread.isLocalUserInGroup) {
|
||||
if (!groupThread.isLocalUserFullMemberOfGroup) {
|
||||
// We don't want to process messages for groups in which we are a pending member.
|
||||
OWSLogInfo(@"Ignoring messages for left group.");
|
||||
return nil;
|
||||
}
|
||||
if (![groupModel.groupMembership isNonPendingMember:envelope.sourceAddress]) {
|
||||
if (![groupModel.groupMembership isFullMember:envelope.sourceAddress]) {
|
||||
// We don't want to process group messages for non-members.
|
||||
OWSLogInfo(@"Ignoring messages for user not in group: %@.", envelope.sourceAddress);
|
||||
return nil;
|
||||
@ -1138,7 +1133,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
if (typingMessage.hasGroupID) {
|
||||
TSGroupThread *_Nullable groupThread = [TSGroupThread fetchWithGroupId:typingMessage.groupID
|
||||
transaction:transaction];
|
||||
if (groupThread != nil && !groupThread.isLocalUserInGroup) {
|
||||
if (groupThread != nil && !groupThread.isLocalUserFullOrInvitedMemberOfGroup) {
|
||||
OWSLogInfo(@"Ignoring messages for left group.");
|
||||
return;
|
||||
}
|
||||
@ -1243,7 +1238,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
OWSFailDebug(@"Group update for invalid group version.");
|
||||
return;
|
||||
}
|
||||
if (oldGroupThread.isLocalUserInGroup) {
|
||||
if (oldGroupThread.isLocalUserFullMemberOfGroup) {
|
||||
// If the local user had left the group we couldn't trust our local group state - we'd
|
||||
// have to trust the remote membership.
|
||||
//
|
||||
@ -1952,7 +1947,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
}
|
||||
|
||||
// Ensure we are in the group.
|
||||
if (!gThread.isLocalUserInGroup) {
|
||||
if (!gThread.isLocalUserFullOrInvitedMemberOfGroup) {
|
||||
OWSLogWarn(@"Ignoring 'Group Info Request' message for group we no longer belong to.");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -629,13 +629,14 @@ extension MessageSending {
|
||||
// If a member has left the group since this message was enqueued,
|
||||
// they should not receive the message.
|
||||
let groupMembership = groupThread.groupModel.groupMembership
|
||||
var currentValidRecipients = groupMembership.nonPendingMembers
|
||||
var currentValidRecipients = groupMembership.fullMembers
|
||||
|
||||
// ...or latest known list of "additional recipients".
|
||||
//
|
||||
// This is used to send group update messages for v2 groups to
|
||||
// pending members who are not included in .sendingRecipientAddresses().
|
||||
if GroupManager.shouldMessageHaveAdditionalRecipients(message, groupThread: groupThread) {
|
||||
currentValidRecipients.formUnion(groupMembership.pendingMembers)
|
||||
currentValidRecipients.formUnion(groupMembership.pendingProfileKeyOrRequestMembers)
|
||||
}
|
||||
currentValidRecipients.remove(localAddress)
|
||||
recipientAddresses.formIntersection(currentValidRecipients)
|
||||
|
||||
@ -345,14 +345,14 @@ class IncomingGroupsV2MessageQueue: NSObject, MessageProcessingPipelineStage {
|
||||
owsFailDebug("Missing thread.")
|
||||
return true
|
||||
}
|
||||
guard groupThread.groupModel.groupMembership.isNonPendingMember(localAddress) else {
|
||||
guard groupThread.groupModel.groupMembership.isFullMember(localAddress) else {
|
||||
// * Local user might have just left the group.
|
||||
// * Local user may have just learned that we were removed from the group.
|
||||
// * Local user might be a pending member with an invite.
|
||||
Logger.warn("Discarding envelope; local user is not an active group member.")
|
||||
return true
|
||||
}
|
||||
guard groupThread.groupModel.groupMembership.isNonPendingMember(sourceAddress) else {
|
||||
guard groupThread.groupModel.groupMembership.isFullMember(sourceAddress) else {
|
||||
// * The sender might have just left the group.
|
||||
Logger.warn("Discarding envelope; sender is not an active group member.")
|
||||
return true
|
||||
|
||||
@ -162,6 +162,14 @@ struct YAPDBThreadFinder: ThreadFinder {
|
||||
@objc
|
||||
public class GRDBThreadFinder: NSObject, ThreadFinder {
|
||||
|
||||
// MARK: - Dependencies
|
||||
|
||||
private class var tsAccountManager: TSAccountManager {
|
||||
return .sharedInstance()
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
public typealias ReadTransaction = GRDBReadTransaction
|
||||
|
||||
static let cn = ThreadRecord.columnName
|
||||
@ -271,9 +279,10 @@ public class GRDBThreadFinder: NSObject, ThreadFinder {
|
||||
@objc
|
||||
public class func hasPendingMessageRequest(thread: TSThread, transaction: GRDBReadTransaction) -> Bool {
|
||||
|
||||
// TODO: Should we consult isRequestingMember() here?
|
||||
if let groupThread = thread as? TSGroupThread,
|
||||
groupThread.isGroupV2Thread,
|
||||
groupThread.isLocalUserPendingMember {
|
||||
groupThread.isLocalUserPendingProfileKeyMember {
|
||||
return true
|
||||
}
|
||||
|
||||
@ -285,7 +294,7 @@ public class GRDBThreadFinder: NSObject, ThreadFinder {
|
||||
if OWSBlockingManager.shared().isThreadBlocked(thread) { return true }
|
||||
|
||||
let isGroupThread = thread is TSGroupThread
|
||||
let isLocalUserInGroup = (thread as? TSGroupThread)?.isLocalUserInGroup == true
|
||||
let isLocalUserInGroup = (thread as? TSGroupThread)?.isLocalUserFullOrInvitedMemberOfGroup == true
|
||||
|
||||
// If this is a group thread and we're not a member, never show the message request.
|
||||
if isGroupThread, !isLocalUserInGroup { return false }
|
||||
|
||||
@ -10,6 +10,7 @@ public enum GroupV2Access: UInt, Codable {
|
||||
case any
|
||||
case member
|
||||
case administrator
|
||||
case unsatisfiable
|
||||
|
||||
var description: String {
|
||||
get {
|
||||
@ -22,9 +23,39 @@ public enum GroupV2Access: UInt, Codable {
|
||||
return "member"
|
||||
case .administrator:
|
||||
return "administrator"
|
||||
case .unsatisfiable:
|
||||
return "unsatisfiable"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func access(forProtoAccess value: GroupsProtoAccessControlAccessRequired) -> GroupV2Access {
|
||||
switch value {
|
||||
case .member:
|
||||
return .member
|
||||
case .administrator:
|
||||
return .administrator
|
||||
case .unsatisfiable:
|
||||
return .unsatisfiable
|
||||
default:
|
||||
return .unknown
|
||||
}
|
||||
}
|
||||
|
||||
public var protoAccess: GroupsProtoAccessControlAccessRequired {
|
||||
switch self {
|
||||
case .any:
|
||||
return .unknown
|
||||
case .member:
|
||||
return .member
|
||||
case .administrator:
|
||||
return .administrator
|
||||
case .unsatisfiable:
|
||||
return .unsatisfiable
|
||||
default:
|
||||
return .unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
@ -36,11 +67,15 @@ public class GroupAccess: MTLModel {
|
||||
public var members: GroupV2Access = .unknown
|
||||
@objc
|
||||
public var attributes: GroupV2Access = .unknown
|
||||
@objc
|
||||
public var addFromInviteLink: GroupV2Access = .unknown
|
||||
|
||||
public init(members: GroupV2Access,
|
||||
attributes: GroupV2Access) {
|
||||
attributes: GroupV2Access,
|
||||
addFromInviteLink: GroupV2Access) {
|
||||
self.members = members
|
||||
self.attributes = attributes
|
||||
self.addFromInviteLink = addFromInviteLink
|
||||
|
||||
super.init()
|
||||
}
|
||||
@ -62,12 +97,12 @@ public class GroupAccess: MTLModel {
|
||||
|
||||
@objc
|
||||
public static var allAccess: GroupAccess {
|
||||
return GroupAccess(members: .any, attributes: .any)
|
||||
return GroupAccess(members: .any, attributes: .any, addFromInviteLink: .any)
|
||||
}
|
||||
|
||||
@objc
|
||||
public static var adminOnly: GroupAccess {
|
||||
return GroupAccess(members: .administrator, attributes: .administrator)
|
||||
return GroupAccess(members: .administrator, attributes: .administrator, addFromInviteLink: .administrator)
|
||||
}
|
||||
|
||||
@objc
|
||||
@ -77,34 +112,10 @@ public class GroupAccess: MTLModel {
|
||||
|
||||
@objc
|
||||
public static var defaultForV2: GroupAccess {
|
||||
return GroupAccess(members: .member, attributes: .member)
|
||||
}
|
||||
|
||||
public class func groupV2Access(forProtoAccess value: GroupsProtoAccessControlAccessRequired) -> GroupV2Access {
|
||||
switch value {
|
||||
case .member:
|
||||
return .member
|
||||
case .administrator:
|
||||
return .administrator
|
||||
default:
|
||||
return .unknown
|
||||
}
|
||||
}
|
||||
|
||||
public class func protoAccess(forGroupV2Access value: GroupV2Access) -> GroupsProtoAccessControlAccessRequired {
|
||||
switch value {
|
||||
case .any:
|
||||
return .unknown
|
||||
case .member:
|
||||
return .member
|
||||
case .administrator:
|
||||
return .administrator
|
||||
default:
|
||||
return .unknown
|
||||
}
|
||||
return GroupAccess(members: .member, attributes: .member, addFromInviteLink: .unsatisfiable)
|
||||
}
|
||||
|
||||
public override var debugDescription: String {
|
||||
return "[members: \(members.description), attributes: \(attributes.description), ]"
|
||||
return "[members: \(members), attributes: \(attributes), addFromInviteLink: \(addFromInviteLink), ]"
|
||||
}
|
||||
}
|
||||
|
||||
@ -246,9 +246,9 @@ public class GroupManager: NSObject {
|
||||
// The group creator is an administrator;
|
||||
// the other members are normal users.
|
||||
var builder = GroupMembership.Builder()
|
||||
builder.addNonPendingMembers(Set(membersParam), role: .normal)
|
||||
builder.addFullMembers(Set(membersParam), role: .normal)
|
||||
builder.remove(localAddress)
|
||||
builder.addNonPendingMember(localAddress, role: .administrator)
|
||||
builder.addFullMember(localAddress, role: .administrator)
|
||||
return builder.build()
|
||||
}.then(on: .global()) { (groupMembership: GroupMembership) -> Promise<GroupMembership> in
|
||||
// If we might create a v2 group,
|
||||
@ -258,7 +258,7 @@ public class GroupManager: NSObject {
|
||||
return Promise.value(groupMembership)
|
||||
}
|
||||
return firstly { () -> Promise<Void> in
|
||||
self.groupsV2.tryToEnsureProfileKeyCredentials(for: Array(groupMembership.allUsers))
|
||||
self.groupsV2.tryToEnsureProfileKeyCredentials(for: Array(groupMembership.allMembersOfAnyKind))
|
||||
}.map(on: .global()) { (_) -> GroupMembership in
|
||||
return groupMembership
|
||||
}
|
||||
@ -268,11 +268,11 @@ public class GroupManager: NSObject {
|
||||
// Before we create a v2 group, we need to separate out the
|
||||
// pending and non-pending members. If we already know we're
|
||||
// going to create a v1 group, we shouldn't separate them.
|
||||
let groupMembership = self.separatePendingMembers(in: proposedGroupMembership,
|
||||
oldGroupModel: nil,
|
||||
transaction: transaction)
|
||||
let groupMembership = self.separatePendingProfileKeyMembers(in: proposedGroupMembership,
|
||||
oldGroupModel: nil,
|
||||
transaction: transaction)
|
||||
|
||||
guard groupMembership.nonPendingMembers.contains(localAddress) else {
|
||||
guard groupMembership.isFullMember(localAddress) else {
|
||||
throw OWSAssertionError("Missing localAddress.")
|
||||
}
|
||||
|
||||
@ -379,9 +379,9 @@ public class GroupManager: NSObject {
|
||||
// * We have a profile key credential for them.
|
||||
// * Their account has the "groups v2" capability
|
||||
// (e.g. all of their clients support groups v2.
|
||||
private static func separatePendingMembers(in newGroupMembership: GroupMembership,
|
||||
oldGroupModel: TSGroupModel?,
|
||||
transaction: SDSAnyReadTransaction) -> GroupMembership {
|
||||
private static func separatePendingProfileKeyMembers(in newGroupMembership: GroupMembership,
|
||||
oldGroupModel: TSGroupModel?,
|
||||
transaction: SDSAnyReadTransaction) -> GroupMembership {
|
||||
guard let localUuid = tsAccountManager.localUuid else {
|
||||
owsFailDebug("Missing localUuid.")
|
||||
return newGroupMembership
|
||||
@ -396,15 +396,19 @@ public class GroupManager: NSObject {
|
||||
builder.copyInvalidInvites(from: oldGroupMembership)
|
||||
|
||||
assert(oldGroupModel.groupsVersion == .V2)
|
||||
newMembers = newGroupMembership.allUsers.subtracting(oldGroupMembership.allUsers)
|
||||
newMembers = newGroupMembership.allMembersOfAnyKind.subtracting(oldGroupMembership.allMembersOfAnyKind)
|
||||
|
||||
// Carry over existing members as they stand.
|
||||
let existingMembers = oldGroupMembership.allUsers.intersection(newGroupMembership.allUsers)
|
||||
let existingMembers = oldGroupMembership.allMembersOfAnyKind.intersection(newGroupMembership.allMembersOfAnyKind)
|
||||
for address in existingMembers {
|
||||
if oldGroupMembership.isPendingMember(address),
|
||||
newGroupMembership.isNonPendingMember(address) {
|
||||
if oldGroupMembership.isPendingProfileKeyMember(address),
|
||||
newGroupMembership.isFullMember(address) {
|
||||
// If we're adding a pending member, treat them as a new member.
|
||||
newMembers.insert(address)
|
||||
} else if oldGroupMembership.isRequestingMember(address),
|
||||
newGroupMembership.isFullMember(address) {
|
||||
// If we're adding a requesting member, treat them as a new member.
|
||||
newMembers.insert(address)
|
||||
} else {
|
||||
builder.copyMember(address, from: oldGroupMembership)
|
||||
}
|
||||
@ -413,14 +417,14 @@ public class GroupManager: NSObject {
|
||||
// Creating new group
|
||||
|
||||
// First, skip separation when creating v1 groups.
|
||||
guard canUseV2(for: newGroupMembership.allUsers, transaction: transaction) else {
|
||||
guard canUseV2(for: newGroupMembership.allMembersOfAnyKind, transaction: transaction) else {
|
||||
// If any member of a new group doesn't support groups v2,
|
||||
// we're going to create a v1 group. In that case, we
|
||||
// don't want to separate out pending members.
|
||||
return newGroupMembership
|
||||
}
|
||||
|
||||
newMembers = newGroupMembership.allUsers
|
||||
newMembers = newGroupMembership.allMembersOfAnyKind
|
||||
}
|
||||
|
||||
// We only need to separate new members.
|
||||
@ -451,11 +455,11 @@ public class GroupManager: NSObject {
|
||||
// instead of adding them.
|
||||
if address != localAddress &&
|
||||
DebugFlags.groupsV2forceInvites {
|
||||
builder.addPendingMember(address, role: role, addedByUuid: localUuid)
|
||||
builder.addPendingProfileKeyMember(address, role: role, addedByUuid: localUuid)
|
||||
} else if isPending {
|
||||
builder.addPendingMember(address, role: role, addedByUuid: localUuid)
|
||||
builder.addPendingProfileKeyMember(address, role: role, addedByUuid: localUuid)
|
||||
} else {
|
||||
builder.addNonPendingMember(address, role: role)
|
||||
builder.addFullMember(address, role: role)
|
||||
}
|
||||
}
|
||||
return builder.build()
|
||||
@ -574,7 +578,7 @@ public class GroupManager: NSObject {
|
||||
let groupMembership = GroupMembership(v1Members: Set(members))
|
||||
// GroupsV2 TODO: Let tests specify access levels.
|
||||
// GroupsV2 TODO: Fill in avatarUrlPath when we test v2 groups.
|
||||
let groupAccess = GroupAccess.allAccess
|
||||
let groupAccess = GroupAccess.defaultForV1
|
||||
// Use buildGroupModel() to fill in defaults, like it was a new group.
|
||||
|
||||
var builder = TSGroupModelBuilder()
|
||||
@ -654,7 +658,7 @@ public class GroupManager: NSObject {
|
||||
throw OWSAssertionError("Missing localAddress.")
|
||||
}
|
||||
return (groupModel.groupsVersion == .V1 &&
|
||||
groupModel.groupMembership.isPendingOrNonPendingMember(localAddress))
|
||||
groupModel.groupMembership.isMemberOfAnyKind(localAddress))
|
||||
}
|
||||
|
||||
// MARK: - Update Existing Group (Remote)
|
||||
@ -667,10 +671,6 @@ public class GroupManager: NSObject {
|
||||
disappearingMessageToken: DisappearingMessageToken?,
|
||||
groupUpdateSourceAddress: SignalServiceAddress?,
|
||||
transaction: SDSAnyWriteTransaction) throws -> UpsertGroupResult {
|
||||
guard let localAddress = tsAccountManager.localAddress else {
|
||||
throw OWSAssertionError("Missing localAddress.")
|
||||
}
|
||||
|
||||
let groupId = proposedGroupModel.groupId
|
||||
let updateInfo: UpdateInfo
|
||||
do {
|
||||
@ -794,7 +794,7 @@ public class GroupManager: NSObject {
|
||||
guard RemoteConfig.groupsV2GoodCitizen else {
|
||||
return Promise.value(())
|
||||
}
|
||||
return self.tryToEnableGroupsV2(for: Array(proposedGroupModel.groupMembership.allUsers), isBlocking: true, ignoreErrors: true)
|
||||
return self.tryToEnableGroupsV2(for: Array(proposedGroupModel.groupMembership.allMembersOfAnyKind), isBlocking: true, ignoreErrors: true)
|
||||
}.then(on: .global()) { () -> Promise<Void> in
|
||||
return self.ensureLocalProfileHasCommitmentIfNecessary()
|
||||
}.then(on: DispatchQueue.global()) { () -> Promise<String?> in
|
||||
@ -884,7 +884,7 @@ public class GroupManager: NSObject {
|
||||
// Always ensure we're a member of any v1 group we're updating.
|
||||
var builder = proposedGroupModel.groupMembership.asBuilder
|
||||
builder.remove(localAddress)
|
||||
builder.addNonPendingMember(localAddress, role: .normal)
|
||||
builder.addFullMember(localAddress, role: .normal)
|
||||
let groupMembership = builder.build()
|
||||
|
||||
var groupModelBuilder = proposedGroupModel.asBuilder
|
||||
@ -923,23 +923,19 @@ public class GroupManager: NSObject {
|
||||
let oldDMConfiguration = OWSDisappearingMessagesConfiguration.fetchOrBuildDefault(with: thread, transaction: transaction)
|
||||
let newDMConfiguration = dmConfiguration ?? oldDMConfiguration
|
||||
|
||||
for address in proposedGroupMembership.allUsers {
|
||||
for address in proposedGroupMembership.allMembersOfAnyKind {
|
||||
guard address.uuid != nil else {
|
||||
throw OWSAssertionError("Group v2 member missing uuid.")
|
||||
}
|
||||
}
|
||||
// Before we update a v2 group, we need to separate out the
|
||||
// pending and non-pending members.
|
||||
let groupMembership = self.separatePendingMembers(in: proposedGroupMembership,
|
||||
oldGroupModel: oldGroupModel,
|
||||
transaction: transaction)
|
||||
|
||||
guard groupMembership.nonPendingMembers.contains(localAddress) else {
|
||||
throw OWSAssertionError("Missing localAddress.")
|
||||
}
|
||||
let groupMembership = self.separatePendingProfileKeyMembers(in: proposedGroupMembership,
|
||||
oldGroupModel: oldGroupModel,
|
||||
transaction: transaction)
|
||||
|
||||
// Don't try to modify a v2 group if we're not a member.
|
||||
guard groupMembership.nonPendingMembers.contains(localAddress) else {
|
||||
guard groupMembership.isFullMember(localAddress) else {
|
||||
throw OWSAssertionError("Missing localAddress.")
|
||||
}
|
||||
|
||||
@ -1108,7 +1104,7 @@ public class GroupManager: NSObject {
|
||||
}
|
||||
return updateGroupV2(groupModel: groupModel,
|
||||
description: "Accept invite") { groupChangeSet in
|
||||
groupChangeSet.promotePendingMember(localUuid)
|
||||
groupChangeSet.promotePendingProfileKeyMember(localUuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1135,7 +1131,7 @@ public class GroupManager: NSObject {
|
||||
}
|
||||
let oldGroupModel = groupThread.groupModel
|
||||
// Note that we consult allUsers which includes pending members.
|
||||
guard oldGroupModel.groupMembership.allUsers.contains(localAddress) else {
|
||||
guard oldGroupModel.groupMembership.isMemberOfAnyKind(localAddress) else {
|
||||
throw OWSAssertionError("Local user is not a member of the group.")
|
||||
}
|
||||
|
||||
@ -1185,7 +1181,7 @@ public class GroupManager: NSObject {
|
||||
transaction: SDSAnyWriteTransaction,
|
||||
success: (() -> Void)?) {
|
||||
|
||||
guard groupThread.isLocalUserPendingOrNonPendingMember else {
|
||||
guard groupThread.isLocalUserMemberOfAnyKind else {
|
||||
owsFailDebug("unexpectedly trying to leave group for which we're not a member.")
|
||||
return
|
||||
}
|
||||
@ -1535,7 +1531,8 @@ public class GroupManager: NSObject {
|
||||
// We need to send v2 group updates to pending members
|
||||
// as well. Normal group sends only include "full members".
|
||||
assert(messageBuilder.additionalRecipients == nil)
|
||||
let additionalRecipients = groupThread.groupModel.groupMembership.pendingMembers.filter { address in
|
||||
let groupMembership = groupThread.groupModel.groupMembership
|
||||
let additionalRecipients = groupMembership.pendingProfileKeyOrRequestMembers.filter { address in
|
||||
return doesUserSupportGroupsV2(address: address,
|
||||
transaction: transaction)
|
||||
}
|
||||
@ -1585,7 +1582,8 @@ public class GroupManager: NSObject {
|
||||
// We need to send v2 group updates to pending members
|
||||
// as well. Normal group sends only include "full members".
|
||||
assert(messageBuilder.additionalRecipients == nil)
|
||||
let additionalRecipients = groupThread.groupModel.groupMembership.pendingMembers.filter { address in
|
||||
let groupMembership = groupThread.groupModel.groupMembership
|
||||
let additionalRecipients = groupMembership.pendingProfileKeyOrRequestMembers.filter { address in
|
||||
return doesUserSupportGroupsV2(address: address,
|
||||
transaction: transaction)
|
||||
}
|
||||
@ -1931,7 +1929,7 @@ public class GroupManager: NSObject {
|
||||
// if we're a member of the group. We don't want to do this if we're just a
|
||||
// pending member or are leaving/have already left the group.
|
||||
let groupMembership = groupThread.groupModel.groupMembership
|
||||
guard groupMembership.isPendingOrNonPendingMember(localAddress) else {
|
||||
guard groupMembership.isMemberOfAnyKind(localAddress) else {
|
||||
return
|
||||
}
|
||||
profileManager.addThread(toProfileWhitelist: groupThread, transaction: transaction)
|
||||
|
||||
@ -29,251 +29,217 @@ public extension TSGroupMemberRole {
|
||||
|
||||
// MARK: -
|
||||
|
||||
// NOTE: We only use this class for backwards compatibility.
|
||||
@objc(_TtCC16SignalServiceKit15GroupMembership11MemberState)
|
||||
class LegacyMemberState: MTLModel {
|
||||
@objc
|
||||
var role: TSGroupMemberRole = .normal
|
||||
|
||||
@objc
|
||||
var isPending: Bool = false
|
||||
|
||||
// Only applies for pending members.
|
||||
@objc
|
||||
var addedByUuid: UUID?
|
||||
|
||||
@objc
|
||||
public override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
init(role: TSGroupMemberRole,
|
||||
isPending: Bool,
|
||||
addedByUuid: UUID? = nil) {
|
||||
self.role = role
|
||||
self.isPending = isPending
|
||||
self.addedByUuid = addedByUuid
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
@objc
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
}
|
||||
|
||||
@objc
|
||||
public required init(dictionary dictionaryValue: [String: Any]!) throws {
|
||||
try super.init(dictionary: dictionaryValue)
|
||||
}
|
||||
|
||||
@objc
|
||||
public var isAdministrator: Bool {
|
||||
return role == .administrator
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
@objc
|
||||
public enum GroupMemberType: UInt {
|
||||
case fullMember = 0
|
||||
case pendingProfileKey = 1
|
||||
case pendingRequest = 2
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
extension GroupMemberType: CustomStringConvertible {
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .fullMember:
|
||||
return ".fullMember"
|
||||
case .pendingProfileKey:
|
||||
return ".pendingProfileKey"
|
||||
case .pendingRequest:
|
||||
return ".pendingRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
// This class is immutable.
|
||||
@objc(GroupMembershipMemberState)
|
||||
class MemberState: MTLModel {
|
||||
@objc
|
||||
var role: TSGroupMemberRole = .normal
|
||||
|
||||
func memberType() -> GroupMemberType {
|
||||
.fullMember
|
||||
}
|
||||
|
||||
@objc
|
||||
public override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
init(role: TSGroupMemberRole) {
|
||||
self.role = role
|
||||
super.init()
|
||||
}
|
||||
|
||||
@objc
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
}
|
||||
|
||||
@objc
|
||||
public required init(dictionary dictionaryValue: [String: Any]!) throws {
|
||||
try super.init(dictionary: dictionaryValue)
|
||||
}
|
||||
|
||||
@objc
|
||||
public var isAdministrator: Bool {
|
||||
return role == .administrator
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
// This class is immutable.
|
||||
@objc(GroupMembershipFullMemberState)
|
||||
class FullMemberState: MemberState {
|
||||
override func memberType() -> GroupMemberType {
|
||||
.fullMember
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
// This class is immutable.
|
||||
@objc(GroupMembershipPendingProfileKeyMemberState)
|
||||
class PendingProfileKeyMemberState: MemberState {
|
||||
|
||||
override func memberType() -> GroupMemberType {
|
||||
.pendingProfileKey
|
||||
}
|
||||
|
||||
@objc
|
||||
var addedByUuid: UUID?
|
||||
|
||||
init(role: TSGroupMemberRole, addedByUuid: UUID? = nil) {
|
||||
self.addedByUuid = addedByUuid
|
||||
|
||||
super.init(role: role)
|
||||
}
|
||||
|
||||
@objc
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
}
|
||||
|
||||
@objc
|
||||
public required init(dictionary dictionaryValue: [String: Any]!) throws {
|
||||
try super.init(dictionary: dictionaryValue)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
// This class is immutable.
|
||||
@objc(GroupMembershipPendingRequestMemberState)
|
||||
class PendingRequestMemberState: MemberState {
|
||||
|
||||
override func memberType() -> GroupMemberType {
|
||||
.pendingRequest
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
// This class is immutable.
|
||||
@objc(GroupMembershipInvalidInviteModel)
|
||||
class InvalidInviteModel: MTLModel {
|
||||
@objc
|
||||
var userId: Data?
|
||||
|
||||
@objc
|
||||
var addedByUserId: Data?
|
||||
|
||||
@objc
|
||||
public override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
init(userId: Data?, addedByUserId: Data? = nil) {
|
||||
self.userId = userId
|
||||
self.addedByUserId = addedByUserId
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
@objc
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
}
|
||||
|
||||
@objc
|
||||
public required init(dictionary dictionaryValue: [String: Any]!) throws {
|
||||
try super.init(dictionary: dictionaryValue)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
// This class is immutable.
|
||||
@objc
|
||||
public class GroupMembership: MTLModel {
|
||||
// This class is immutable.
|
||||
@objc(_TtCC16SignalServiceKit15GroupMembership11MemberState)
|
||||
class MemberState: MTLModel {
|
||||
@objc
|
||||
var role: TSGroupMemberRole = .normal
|
||||
|
||||
@objc
|
||||
var isPending: Bool = false
|
||||
|
||||
// Only applies for pending members.
|
||||
@objc
|
||||
var addedByUuid: UUID?
|
||||
|
||||
@objc
|
||||
public override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
init(role: TSGroupMemberRole,
|
||||
isPending: Bool,
|
||||
addedByUuid: UUID? = nil) {
|
||||
self.role = role
|
||||
self.isPending = isPending
|
||||
self.addedByUuid = addedByUuid
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
@objc
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
}
|
||||
|
||||
@objc
|
||||
public required init(dictionary dictionaryValue: [String: Any]!) throws {
|
||||
try super.init(dictionary: dictionaryValue)
|
||||
}
|
||||
|
||||
@objc
|
||||
public var isAdministrator: Bool {
|
||||
return role == .administrator
|
||||
}
|
||||
}
|
||||
|
||||
// This class is immutable.
|
||||
@objc(GroupMembershipInvalidInviteModel)
|
||||
class InvalidInviteModel: MTLModel {
|
||||
@objc
|
||||
var userId: Data?
|
||||
|
||||
@objc
|
||||
var addedByUserId: Data?
|
||||
|
||||
@objc
|
||||
public override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
init(userId: Data?, addedByUserId: Data? = nil) {
|
||||
self.userId = userId
|
||||
self.addedByUserId = addedByUserId
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
@objc
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
}
|
||||
|
||||
@objc
|
||||
public required init(dictionary dictionaryValue: [String: Any]!) throws {
|
||||
try super.init(dictionary: dictionaryValue)
|
||||
}
|
||||
}
|
||||
typealias LegacyMemberStateMap = [SignalServiceAddress: LegacyMemberState]
|
||||
|
||||
// By using a single dictionary we ensure that no address has more than one state.
|
||||
typealias MemberStateMap = [SignalServiceAddress: MemberState]
|
||||
@objc
|
||||
var memberStateMap: MemberStateMap
|
||||
var memberStates: MemberStateMap
|
||||
|
||||
typealias InvalidInviteMap = [Data: InvalidInviteModel]
|
||||
@objc
|
||||
var invalidInviteMap: InvalidInviteMap
|
||||
|
||||
@objc
|
||||
public var nonAdminMembers: Set<SignalServiceAddress> {
|
||||
return Set(memberStateMap.filter { !$0.value.isAdministrator && !$0.value.isPending }.keys)
|
||||
}
|
||||
@objc
|
||||
public var nonPendingAdministrators: Set<SignalServiceAddress> {
|
||||
return Set(memberStateMap.filter { $0.value.isAdministrator && !$0.value.isPending }.keys)
|
||||
}
|
||||
@objc
|
||||
public var nonPendingMembers: Set<SignalServiceAddress> {
|
||||
return Set(memberStateMap.filter { !$0.value.isPending }.keys)
|
||||
}
|
||||
|
||||
@objc
|
||||
public var pendingNonAdminMembers: Set<SignalServiceAddress> {
|
||||
return Set(memberStateMap.filter { !$0.value.isAdministrator && $0.value.isPending }.keys)
|
||||
}
|
||||
@objc
|
||||
public var pendingAdministrators: Set<SignalServiceAddress> {
|
||||
return Set(memberStateMap.filter { $0.value.isAdministrator && $0.value.isPending }.keys)
|
||||
}
|
||||
// pendingMembers includes normal and administrator "pending members".
|
||||
@objc
|
||||
public var pendingMembers: Set<SignalServiceAddress> {
|
||||
return Set(memberStateMap.filter { $0.value.isPending }.keys)
|
||||
}
|
||||
|
||||
@objc
|
||||
public var pendingAndNonPendingMemberCount: Int {
|
||||
return memberStateMap.count
|
||||
}
|
||||
|
||||
// allUsers includes _all_ users:
|
||||
//
|
||||
// * Normal and administrator.
|
||||
// * Pending and non-pending.
|
||||
@objc
|
||||
public var allUsers: Set<SignalServiceAddress> {
|
||||
return Set(memberStateMap.keys)
|
||||
}
|
||||
|
||||
public struct Builder {
|
||||
private var memberStateMap = MemberStateMap()
|
||||
private var invalidInviteMap = InvalidInviteMap()
|
||||
|
||||
public init() {}
|
||||
|
||||
internal init(memberStateMap: MemberStateMap, invalidInviteMap: InvalidInviteMap) {
|
||||
self.memberStateMap = memberStateMap
|
||||
self.invalidInviteMap = invalidInviteMap
|
||||
}
|
||||
|
||||
public mutating func remove(_ uuid: UUID) {
|
||||
remove(SignalServiceAddress(uuid: uuid))
|
||||
}
|
||||
|
||||
public mutating func remove(_ address: SignalServiceAddress) {
|
||||
memberStateMap.removeValue(forKey: address)
|
||||
}
|
||||
|
||||
public mutating func remove(_ addresses: Set<SignalServiceAddress>) {
|
||||
for address in addresses {
|
||||
remove(address)
|
||||
}
|
||||
}
|
||||
|
||||
public mutating func addNonPendingMember(_ uuid: UUID,
|
||||
role: TSGroupMemberRole) {
|
||||
addNonPendingMember(SignalServiceAddress(uuid: uuid), role: role)
|
||||
}
|
||||
|
||||
public mutating func addNonPendingMember(_ address: SignalServiceAddress,
|
||||
role: TSGroupMemberRole) {
|
||||
addNonPendingMembers([address], role: role)
|
||||
}
|
||||
|
||||
public mutating func addNonPendingMembers(_ addresses: Set<SignalServiceAddress>,
|
||||
role: TSGroupMemberRole) {
|
||||
for address in addresses {
|
||||
if memberStateMap[address] != nil {
|
||||
owsFailDebug("Duplicate address.")
|
||||
}
|
||||
memberStateMap[address] = MemberState(role: role, isPending: false, addedByUuid: nil)
|
||||
}
|
||||
}
|
||||
|
||||
public mutating func addPendingMember(_ uuid: UUID,
|
||||
role: TSGroupMemberRole,
|
||||
addedByUuid: UUID) {
|
||||
addPendingMember(SignalServiceAddress(uuid: uuid), role: role, addedByUuid: addedByUuid)
|
||||
}
|
||||
|
||||
public mutating func addPendingMember(_ address: SignalServiceAddress,
|
||||
role: TSGroupMemberRole,
|
||||
addedByUuid: UUID) {
|
||||
addPendingMembers([address], role: role, addedByUuid: addedByUuid)
|
||||
}
|
||||
|
||||
public mutating func addPendingMembers(_ addresses: Set<SignalServiceAddress>,
|
||||
role: TSGroupMemberRole,
|
||||
addedByUuid: UUID) {
|
||||
for address in addresses {
|
||||
if memberStateMap[address] != nil {
|
||||
Logger.error("Duplicate address.")
|
||||
continue
|
||||
// owsFailDebug("Duplicate address.")
|
||||
}
|
||||
memberStateMap[address] = MemberState(role: role, isPending: true, addedByUuid: addedByUuid)
|
||||
}
|
||||
}
|
||||
|
||||
public mutating func copyMember(_ address: SignalServiceAddress,
|
||||
from oldGroupMembership: GroupMembership) {
|
||||
guard let memberState = oldGroupMembership.memberStateMap[address] else {
|
||||
owsFailDebug("Unknown address")
|
||||
return
|
||||
}
|
||||
if memberStateMap[address] != nil {
|
||||
owsFailDebug("Duplicate address.")
|
||||
}
|
||||
memberStateMap[address] = memberState
|
||||
}
|
||||
|
||||
public mutating func addInvalidInvite(userId: Data, addedByUserId: Data) {
|
||||
invalidInviteMap[userId] = InvalidInviteModel(userId: userId, addedByUserId: addedByUserId)
|
||||
}
|
||||
|
||||
public mutating func removeInvalidInvite(userId: Data) {
|
||||
invalidInviteMap.removeValue(forKey: userId)
|
||||
}
|
||||
|
||||
public mutating func copyInvalidInvites(from other: GroupMembership) {
|
||||
assert(invalidInviteMap.isEmpty)
|
||||
invalidInviteMap = other.invalidInviteMap
|
||||
}
|
||||
|
||||
internal func asMemberStateMap() -> MemberStateMap {
|
||||
return memberStateMap
|
||||
}
|
||||
|
||||
public func build() -> GroupMembership {
|
||||
var memberStateMap = self.memberStateMap
|
||||
|
||||
let localProfileInvariantAddress = SignalServiceAddress(phoneNumber: kLocalProfileInvariantPhoneNumber)
|
||||
if memberStateMap[localProfileInvariantAddress] != nil {
|
||||
owsFailDebug("Removing localProfileInvariantAddress.")
|
||||
memberStateMap.removeValue(forKey: localProfileInvariantAddress)
|
||||
}
|
||||
|
||||
return GroupMembership(memberStateMap: memberStateMap,
|
||||
invalidInviteMap: invalidInviteMap)
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public override init() {
|
||||
self.memberStateMap = MemberStateMap()
|
||||
self.memberStates = MemberStateMap()
|
||||
self.invalidInviteMap = [:]
|
||||
|
||||
super.init()
|
||||
@ -281,22 +247,47 @@ public class GroupMembership: MTLModel {
|
||||
|
||||
@objc
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
self.memberStateMap = MemberStateMap()
|
||||
self.invalidInviteMap = [:]
|
||||
if let invalidInviteMap = aDecoder.decodeObject(forKey: "invalidInviteMap") as? InvalidInviteMap {
|
||||
self.invalidInviteMap = invalidInviteMap
|
||||
} else {
|
||||
// invalidInviteMap is optional.
|
||||
self.invalidInviteMap = [:]
|
||||
}
|
||||
|
||||
super.init(coder: aDecoder)
|
||||
if let memberStates = aDecoder.decodeObject(forKey: "memberStates") as? MemberStateMap {
|
||||
self.memberStates = memberStates
|
||||
} else if let legacyMemberStateMap = aDecoder.decodeObject(forKey: "memberStateMap") as? LegacyMemberStateMap {
|
||||
self.memberStates = Self.convertLegacyMemberStateMap(legacyMemberStateMap)
|
||||
} else {
|
||||
owsFailDebug("Could not decode.")
|
||||
return nil
|
||||
}
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
@objc
|
||||
public required init(dictionary dictionaryValue: [String: Any]!) throws {
|
||||
self.memberStateMap = MemberStateMap()
|
||||
self.invalidInviteMap = [:]
|
||||
if let invalidInviteMap = dictionaryValue["invalidInviteMap"] as? InvalidInviteMap {
|
||||
self.invalidInviteMap = invalidInviteMap
|
||||
} else {
|
||||
// invalidInviteMap is optional.
|
||||
self.invalidInviteMap = [:]
|
||||
}
|
||||
|
||||
try super.init(dictionary: dictionaryValue)
|
||||
if let memberStates = dictionaryValue["memberStates"] as? MemberStateMap {
|
||||
self.memberStates = memberStates
|
||||
} else if let legacyMemberStateMap = dictionaryValue["memberStateMap"] as? LegacyMemberStateMap {
|
||||
self.memberStates = Self.convertLegacyMemberStateMap(legacyMemberStateMap)
|
||||
} else {
|
||||
throw OWSAssertionError("Could not decode.")
|
||||
}
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
internal init(memberStateMap: MemberStateMap, invalidInviteMap: InvalidInviteMap) {
|
||||
self.memberStateMap = memberStateMap
|
||||
internal init(memberStates: MemberStateMap, invalidInviteMap: InvalidInviteMap) {
|
||||
self.memberStates = memberStates
|
||||
self.invalidInviteMap = invalidInviteMap
|
||||
|
||||
super.init()
|
||||
@ -305,89 +296,38 @@ public class GroupMembership: MTLModel {
|
||||
@objc
|
||||
public init(v1Members: Set<SignalServiceAddress>) {
|
||||
var builder = Builder()
|
||||
builder.addNonPendingMembers(v1Members, role: .normal)
|
||||
self.memberStateMap = builder.asMemberStateMap()
|
||||
builder.addFullMembers(v1Members, role: .normal)
|
||||
self.memberStates = builder.asMemberStateMap()
|
||||
self.invalidInviteMap = [:]
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
private static func convertLegacyMemberStateMap(_ legacyMemberStateMap: LegacyMemberStateMap) -> MemberStateMap {
|
||||
var result = MemberStateMap()
|
||||
for (address, legacyMemberState) in legacyMemberStateMap {
|
||||
let memberState: MemberState
|
||||
if legacyMemberState.isPending {
|
||||
if let addedByUuid = legacyMemberState.addedByUuid {
|
||||
memberState = PendingProfileKeyMemberState(role: legacyMemberState.role,
|
||||
addedByUuid: addedByUuid)
|
||||
} else {
|
||||
owsFailDebug("Missing addedByUuid.")
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
memberState = FullMemberState(role: legacyMemberState.role)
|
||||
}
|
||||
result[address] = memberState
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@objc
|
||||
public static var empty: GroupMembership {
|
||||
return Builder().build()
|
||||
}
|
||||
|
||||
public func role(for uuid: UUID) -> TSGroupMemberRole? {
|
||||
return role(for: SignalServiceAddress(uuid: uuid))
|
||||
}
|
||||
|
||||
public func role(for address: SignalServiceAddress) -> TSGroupMemberRole? {
|
||||
guard let memberState = memberStateMap[address] else {
|
||||
return nil
|
||||
}
|
||||
return memberState.role
|
||||
}
|
||||
|
||||
public func isAdministrator(_ address: SignalServiceAddress) -> Bool {
|
||||
guard let memberState = memberStateMap[address] else {
|
||||
return false
|
||||
}
|
||||
return memberState.isAdministrator
|
||||
}
|
||||
|
||||
public func isAdministrator(_ uuid: UUID) -> Bool {
|
||||
return isAdministrator(SignalServiceAddress(uuid: uuid))
|
||||
}
|
||||
|
||||
@objc
|
||||
public func isNonPendingMember(_ address: SignalServiceAddress) -> Bool {
|
||||
guard let memberState = memberStateMap[address] else {
|
||||
return false
|
||||
}
|
||||
return !memberState.isPending
|
||||
}
|
||||
|
||||
public func isNonPendingMember(_ uuid: UUID) -> Bool {
|
||||
return isNonPendingMember(SignalServiceAddress(uuid: uuid))
|
||||
}
|
||||
|
||||
@objc
|
||||
public func isPendingMember(_ address: SignalServiceAddress) -> Bool {
|
||||
guard let memberState = memberStateMap[address] else {
|
||||
return false
|
||||
}
|
||||
return memberState.isPending
|
||||
}
|
||||
|
||||
public func isPendingMember(_ uuid: UUID) -> Bool {
|
||||
isPendingMember(SignalServiceAddress(uuid: uuid))
|
||||
}
|
||||
|
||||
// When we check "is X a member?" we might mean...
|
||||
//
|
||||
// * Is X a "full" member or a pending member?
|
||||
// * Is X a "full" member and not a pending member?
|
||||
// * Is X a "normal" member and not an administrator member?
|
||||
// * Is X a "normal" member or an administrator member?
|
||||
// * Some combination thereof.
|
||||
//
|
||||
// This method is intended tests the inclusive case: pending
|
||||
// or non-pending, any role.
|
||||
@objc
|
||||
public func isPendingOrNonPendingMember(_ address: SignalServiceAddress) -> Bool {
|
||||
return memberStateMap[address] != nil
|
||||
}
|
||||
|
||||
public func isPendingOrNonPendingMember(_ uuid: UUID) -> Bool {
|
||||
return isPendingOrNonPendingMember(SignalServiceAddress(uuid: uuid))
|
||||
}
|
||||
|
||||
public func addedByUuid(forPendingMember address: SignalServiceAddress) -> UUID? {
|
||||
assert(isPendingMember(address))
|
||||
|
||||
return memberStateMap[address]?.addedByUuid
|
||||
}
|
||||
|
||||
@objc
|
||||
public static func normalize(_ addresses: [SignalServiceAddress]) -> [SignalServiceAddress] {
|
||||
return Array(Set(addresses))
|
||||
@ -415,19 +355,309 @@ public class GroupMembership: MTLModel {
|
||||
}
|
||||
|
||||
public var asBuilder: Builder {
|
||||
return Builder(memberStateMap: memberStateMap, invalidInviteMap: invalidInviteMap)
|
||||
return Builder(memberStates: memberStates, invalidInviteMap: invalidInviteMap)
|
||||
}
|
||||
|
||||
public override var debugDescription: String {
|
||||
var result = "["
|
||||
for address in GroupMembership.normalize(Array(allUsers)) {
|
||||
guard let memberState = memberStateMap[address] else {
|
||||
for address in GroupMembership.normalize(Array(allMembersOfAnyKind)) {
|
||||
guard let memberState = memberStates[address] else {
|
||||
owsFailDebug("Missing memberState.")
|
||||
continue
|
||||
}
|
||||
result += "\(address), isPending: \(memberState.isPending), isAdministrator: \(memberState.isAdministrator)\n"
|
||||
result += "\(address), memberType: \(memberState.memberType()), role: \(memberState.role)\n"
|
||||
}
|
||||
result += "]"
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Swift Accessors
|
||||
|
||||
public extension GroupMembership {
|
||||
|
||||
var fullMemberAdministrators: Set<SignalServiceAddress> {
|
||||
return Set(memberStates.filter { $0.value.isAdministrator && $0.value.memberType() == .fullMember }.keys)
|
||||
}
|
||||
|
||||
var fullMembers: Set<SignalServiceAddress> {
|
||||
return Set(memberStates.filter { $0.value.memberType() == .fullMember }.keys)
|
||||
}
|
||||
|
||||
var pendingProfileKeyMembers: Set<SignalServiceAddress> {
|
||||
return Set(memberStates.filter { $0.value.memberType() == .pendingProfileKey }.keys)
|
||||
}
|
||||
|
||||
var pendingRequestMembers: Set<SignalServiceAddress> {
|
||||
return Set(memberStates.filter { $0.value.memberType() == .pendingRequest }.keys)
|
||||
}
|
||||
|
||||
var fullOrPendingProfileKeyMembers: Set<SignalServiceAddress> {
|
||||
let memberTypes: [GroupMemberType] = [ .fullMember, .pendingProfileKey ]
|
||||
return Set(memberStates.filter { memberTypes.contains($0.value.memberType()) }.keys)
|
||||
}
|
||||
|
||||
var pendingProfileKeyOrRequestMembers: Set<SignalServiceAddress> {
|
||||
let memberTypes: [GroupMemberType] = [ .pendingProfileKey, .pendingRequest ]
|
||||
return Set(memberStates.filter { memberTypes.contains($0.value.memberType()) }.keys)
|
||||
}
|
||||
|
||||
// allMembersOfAnyKind includes _all_ members:
|
||||
//
|
||||
// * Normal and administrator.
|
||||
// * Normal, pending profile key, requesting.
|
||||
var allMembersOfAnyKind: Set<SignalServiceAddress> {
|
||||
return Set(memberStates.keys)
|
||||
}
|
||||
|
||||
// allUsers includes _all_ members:
|
||||
//
|
||||
// * Normal and administrator.
|
||||
// * Normal, pending profile key, requesting.
|
||||
var allMemberUuidsOfAnyKind: Set<UUID> {
|
||||
return Set(memberStates.keys.compactMap { $0.uuid })
|
||||
}
|
||||
|
||||
func role(for uuid: UUID) -> TSGroupMemberRole? {
|
||||
return role(for: SignalServiceAddress(uuid: uuid))
|
||||
}
|
||||
|
||||
func role(for address: SignalServiceAddress) -> TSGroupMemberRole? {
|
||||
guard let memberState = memberStates[address] else {
|
||||
return nil
|
||||
}
|
||||
return memberState.role
|
||||
}
|
||||
|
||||
func isFullOrInvitedAdministrator(_ address: SignalServiceAddress) -> Bool {
|
||||
guard let memberState = memberStates[address] else {
|
||||
return false
|
||||
}
|
||||
guard memberState.isAdministrator else {
|
||||
return false
|
||||
}
|
||||
switch memberState.memberType() {
|
||||
case .fullMember, .pendingProfileKey:
|
||||
return true
|
||||
case .pendingRequest:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isFullOrInvitedAdministrator(_ uuid: UUID) -> Bool {
|
||||
return isFullOrInvitedAdministrator(SignalServiceAddress(uuid: uuid))
|
||||
}
|
||||
|
||||
func isFullMemberAndAdministrator(_ address: SignalServiceAddress) -> Bool {
|
||||
guard let memberState = memberStates[address] else {
|
||||
return false
|
||||
}
|
||||
return memberState.isAdministrator && memberState.memberType() == .fullMember
|
||||
}
|
||||
|
||||
func isFullMemberAndAdministrator(_ uuid: UUID) -> Bool {
|
||||
return isFullMemberAndAdministrator(SignalServiceAddress(uuid: uuid))
|
||||
}
|
||||
|
||||
@objc
|
||||
func isFullMember(_ address: SignalServiceAddress) -> Bool {
|
||||
guard let memberState = memberStates[address] else {
|
||||
return false
|
||||
}
|
||||
return memberState.memberType() == .fullMember
|
||||
}
|
||||
|
||||
func isFullMember(_ uuid: UUID) -> Bool {
|
||||
isFullMember(SignalServiceAddress(uuid: uuid))
|
||||
}
|
||||
|
||||
@objc
|
||||
func isPendingProfileKeyMember(_ address: SignalServiceAddress) -> Bool {
|
||||
guard let memberState = memberStates[address] else {
|
||||
return false
|
||||
}
|
||||
return memberState.memberType() == .pendingProfileKey
|
||||
}
|
||||
|
||||
func isPendingProfileKeyMember(_ uuid: UUID) -> Bool {
|
||||
isPendingProfileKeyMember(SignalServiceAddress(uuid: uuid))
|
||||
}
|
||||
|
||||
func isRequestingMember(_ address: SignalServiceAddress) -> Bool {
|
||||
guard let memberState = memberStates[address] else {
|
||||
return false
|
||||
}
|
||||
return memberState.memberType() == .pendingRequest
|
||||
}
|
||||
|
||||
func isRequestingMember(_ uuid: UUID) -> Bool {
|
||||
isRequestingMember(SignalServiceAddress(uuid: uuid))
|
||||
}
|
||||
|
||||
// Returns true for...
|
||||
//
|
||||
// * Any type: Full members, "pending invite" members, "requesting" members.
|
||||
// * Any role: admin, non-admin.
|
||||
//
|
||||
// This method is intended tests the inclusive case: pending
|
||||
// or non-pending, any role.
|
||||
//
|
||||
// This method does NOT return true for invalid invites,
|
||||
// which don't have a UUID or address associated with them.
|
||||
func isMemberOfAnyKind(_ address: SignalServiceAddress) -> Bool {
|
||||
return memberStates[address] != nil
|
||||
}
|
||||
|
||||
func isMemberOfAnyKind(_ uuid: UUID) -> Bool {
|
||||
return isMemberOfAnyKind(SignalServiceAddress(uuid: uuid))
|
||||
}
|
||||
|
||||
// This method should only be called for "pending profile key" members.
|
||||
func addedByUuid(forPendingProfileKeyMember address: SignalServiceAddress) -> UUID? {
|
||||
assert(isPendingProfileKeyMember(address))
|
||||
|
||||
guard let memberState = memberStates[address] else {
|
||||
return nil
|
||||
}
|
||||
guard let pendingProfileKeyMemberState = memberState as? PendingProfileKeyMemberState else {
|
||||
owsFailDebug("Unexpected member type.")
|
||||
return nil
|
||||
}
|
||||
return pendingProfileKeyMemberState.addedByUuid
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Builder
|
||||
|
||||
@objc
|
||||
public extension GroupMembership {
|
||||
struct Builder {
|
||||
private var memberStates = MemberStateMap()
|
||||
private var invalidInviteMap = InvalidInviteMap()
|
||||
|
||||
public init() {}
|
||||
|
||||
internal init(memberStates: MemberStateMap, invalidInviteMap: InvalidInviteMap) {
|
||||
self.memberStates = memberStates
|
||||
self.invalidInviteMap = invalidInviteMap
|
||||
}
|
||||
|
||||
public mutating func remove(_ uuid: UUID) {
|
||||
remove(SignalServiceAddress(uuid: uuid))
|
||||
}
|
||||
|
||||
public mutating func remove(_ address: SignalServiceAddress) {
|
||||
memberStates.removeValue(forKey: address)
|
||||
}
|
||||
|
||||
public mutating func remove(_ addresses: Set<SignalServiceAddress>) {
|
||||
for address in addresses {
|
||||
remove(address)
|
||||
}
|
||||
}
|
||||
|
||||
public mutating func addFullMember(_ uuid: UUID,
|
||||
role: TSGroupMemberRole) {
|
||||
addFullMember(SignalServiceAddress(uuid: uuid), role: role)
|
||||
}
|
||||
|
||||
public mutating func addFullMember(_ address: SignalServiceAddress,
|
||||
role: TSGroupMemberRole) {
|
||||
addFullMembers([address], role: role)
|
||||
}
|
||||
|
||||
public mutating func addFullMembers(_ addresses: Set<SignalServiceAddress>,
|
||||
role: TSGroupMemberRole) {
|
||||
for address in addresses {
|
||||
if memberStates[address] != nil {
|
||||
owsFailDebug("Duplicate address.")
|
||||
}
|
||||
memberStates[address] = FullMemberState(role: role)
|
||||
}
|
||||
}
|
||||
|
||||
public mutating func addPendingProfileKeyMember(_ uuid: UUID,
|
||||
role: TSGroupMemberRole,
|
||||
addedByUuid: UUID) {
|
||||
addPendingProfileKeyMember(SignalServiceAddress(uuid: uuid), role: role, addedByUuid: addedByUuid)
|
||||
}
|
||||
|
||||
public mutating func addPendingProfileKeyMember(_ address: SignalServiceAddress,
|
||||
role: TSGroupMemberRole,
|
||||
addedByUuid: UUID) {
|
||||
addPendingProfileKeyMembers([address], role: role, addedByUuid: addedByUuid)
|
||||
}
|
||||
|
||||
public mutating func addPendingProfileKeyMembers(_ addresses: Set<SignalServiceAddress>,
|
||||
role: TSGroupMemberRole,
|
||||
addedByUuid: UUID) {
|
||||
for address in addresses {
|
||||
if memberStates[address] != nil {
|
||||
owsFailDebug("Duplicate address.")
|
||||
continue
|
||||
}
|
||||
memberStates[address] = PendingProfileKeyMemberState(role: role, addedByUuid: addedByUuid)
|
||||
}
|
||||
}
|
||||
|
||||
public mutating func addRequestingMember(_ uuid: UUID) {
|
||||
addRequestingMember(SignalServiceAddress(uuid: uuid))
|
||||
}
|
||||
|
||||
public mutating func addRequestingMember(_ address: SignalServiceAddress) {
|
||||
addRequestingMembers([address])
|
||||
}
|
||||
|
||||
public mutating func addRequestingMembers(_ addresses: Set<SignalServiceAddress>) {
|
||||
for address in addresses {
|
||||
if memberStates[address] != nil {
|
||||
owsFailDebug("Duplicate address.")
|
||||
continue
|
||||
}
|
||||
memberStates[address] = PendingRequestMemberState()
|
||||
}
|
||||
}
|
||||
|
||||
public mutating func copyMember(_ address: SignalServiceAddress,
|
||||
from oldGroupMembership: GroupMembership) {
|
||||
guard let memberState = oldGroupMembership.memberStates[address] else {
|
||||
owsFailDebug("Unknown address")
|
||||
return
|
||||
}
|
||||
if memberStates[address] != nil {
|
||||
owsFailDebug("Duplicate address.")
|
||||
}
|
||||
memberStates[address] = memberState
|
||||
}
|
||||
|
||||
public mutating func addInvalidInvite(userId: Data, addedByUserId: Data) {
|
||||
invalidInviteMap[userId] = InvalidInviteModel(userId: userId, addedByUserId: addedByUserId)
|
||||
}
|
||||
|
||||
public mutating func removeInvalidInvite(userId: Data) {
|
||||
invalidInviteMap.removeValue(forKey: userId)
|
||||
}
|
||||
|
||||
public mutating func copyInvalidInvites(from other: GroupMembership) {
|
||||
assert(invalidInviteMap.isEmpty)
|
||||
invalidInviteMap = other.invalidInviteMap
|
||||
}
|
||||
|
||||
internal func asMemberStateMap() -> MemberStateMap {
|
||||
return memberStates
|
||||
}
|
||||
|
||||
public func build() -> GroupMembership {
|
||||
var memberStates = self.memberStates
|
||||
|
||||
let localProfileInvariantAddress = SignalServiceAddress(phoneNumber: kLocalProfileInvariantPhoneNumber)
|
||||
if memberStates[localProfileInvariantAddress] != nil {
|
||||
owsFailDebug("Removing localProfileInvariantAddress.")
|
||||
memberStates.removeValue(forKey: localProfileInvariantAddress)
|
||||
}
|
||||
|
||||
return GroupMembership(memberStates: memberStates,
|
||||
invalidInviteMap: invalidInviteMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,6 +99,7 @@ public protocol GroupsV2Swift: GroupsV2 {
|
||||
|
||||
// MARK: -
|
||||
|
||||
// TODO: Rename to GroupsV2ProposedChanges
|
||||
public protocol GroupsV2ChangeSet: AnyObject {
|
||||
var groupId: Data { get }
|
||||
var groupSecretParamsData: Data { get }
|
||||
@ -116,7 +117,7 @@ public protocol GroupsV2ChangeSet: AnyObject {
|
||||
|
||||
func setAccessForAttributes(_ value: GroupV2Access)
|
||||
|
||||
func promotePendingMember(_ uuid: UUID)
|
||||
func promotePendingProfileKeyMember(_ uuid: UUID)
|
||||
|
||||
func setShouldLeaveGroupDeclineInvite()
|
||||
|
||||
@ -216,12 +217,11 @@ public protocol GroupV2Snapshot {
|
||||
|
||||
var groupAccess: GroupAccess { get }
|
||||
|
||||
var accessControlForAttributes: GroupsProtoAccessControlAccessRequired { get }
|
||||
var accessControlForMembers: GroupsProtoAccessControlAccessRequired { get }
|
||||
|
||||
var disappearingMessageToken: DisappearingMessageToken { get }
|
||||
|
||||
var profileKeys: [UUID: Data] { get }
|
||||
|
||||
var inviteLinkPassword: Data? { get }
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
@ -24,6 +24,8 @@ public class TSGroupModelV2: TSGroupModel {
|
||||
public var revision: UInt32 = 0
|
||||
@objc
|
||||
public var avatarUrlPath: String?
|
||||
@objc
|
||||
public var inviteLinkPassword: Data?
|
||||
|
||||
@objc
|
||||
public required init(groupId: Data,
|
||||
@ -33,7 +35,8 @@ public class TSGroupModelV2: TSGroupModel {
|
||||
groupAccess: GroupAccess,
|
||||
revision: UInt32,
|
||||
secretParamsData: Data,
|
||||
avatarUrlPath: String?) {
|
||||
avatarUrlPath: String?,
|
||||
inviteLinkPassword: Data?) {
|
||||
assert(secretParamsData.count > 0)
|
||||
|
||||
self.membership = groupMembership
|
||||
@ -41,11 +44,12 @@ public class TSGroupModelV2: TSGroupModel {
|
||||
self.access = groupAccess
|
||||
self.revision = revision
|
||||
self.avatarUrlPath = avatarUrlPath
|
||||
self.inviteLinkPassword = inviteLinkPassword
|
||||
|
||||
super.init(groupId: groupId,
|
||||
name: name,
|
||||
avatarData: avatarData,
|
||||
members: Array(groupMembership.nonPendingMembers))
|
||||
members: Array(groupMembership.fullMembers))
|
||||
}
|
||||
|
||||
// MARK: - MTLModel
|
||||
@ -74,7 +78,7 @@ public class TSGroupModelV2: TSGroupModel {
|
||||
|
||||
@objc
|
||||
public override var groupMembers: [SignalServiceAddress] {
|
||||
return Array(groupMembership.nonPendingMembers)
|
||||
return Array(groupMembership.fullMembers)
|
||||
}
|
||||
|
||||
public override func isEqual(to model: TSGroupModel,
|
||||
@ -100,6 +104,9 @@ public class TSGroupModelV2: TSGroupModel {
|
||||
guard other.avatarUrlPath == avatarUrlPath else {
|
||||
return false
|
||||
}
|
||||
guard other.inviteLinkPassword == inviteLinkPassword else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -115,6 +122,7 @@ public class TSGroupModelV2: TSGroupModel {
|
||||
result += "secretParamsData: \(secretParamsData.hexadecimalString.prefix(32)),\n"
|
||||
result += "revision: \(revision),\n"
|
||||
result += "avatarUrlPath: \(String(describing: avatarUrlPath)),\n"
|
||||
result += "inviteLinkPassword: \(inviteLinkPassword?.hexadecimalString ?? "None"),\n"
|
||||
result += "]"
|
||||
return result
|
||||
}
|
||||
|
||||
@ -24,6 +24,7 @@ public struct TSGroupModelBuilder {
|
||||
public var groupSecretParamsData: Data?
|
||||
public var newGroupSeed: NewGroupSeed?
|
||||
public var avatarUrlPath: String?
|
||||
public var inviteLinkPassword: Data?
|
||||
|
||||
public init() {}
|
||||
|
||||
@ -39,11 +40,12 @@ public struct TSGroupModelBuilder {
|
||||
self.groupV2Revision = groupV2Snapshot.revision
|
||||
self.groupSecretParamsData = groupV2Snapshot.groupSecretParamsData
|
||||
self.avatarUrlPath = groupV2Snapshot.avatarUrlPath
|
||||
self.inviteLinkPassword = groupV2Snapshot.inviteLinkPassword
|
||||
}
|
||||
|
||||
public func build(transaction: SDSAnyReadTransaction) throws -> TSGroupModel {
|
||||
|
||||
let allUsers = groupMembership.allUsers
|
||||
let allUsers = groupMembership.allMembersOfAnyKind
|
||||
for recipientAddress in allUsers {
|
||||
guard recipientAddress.isValid else {
|
||||
throw OWSAssertionError("Invalid address.")
|
||||
@ -70,13 +72,16 @@ public struct TSGroupModelBuilder {
|
||||
|
||||
switch groupsVersion {
|
||||
case .V1:
|
||||
if groupMembership.pendingMembers.count > 0 {
|
||||
owsFailDebug("v1 group has pending members.")
|
||||
if !groupMembership.pendingProfileKeyMembers.isEmpty {
|
||||
owsFailDebug("v1 group has pending profile key members.")
|
||||
}
|
||||
if !groupMembership.pendingRequestMembers.isEmpty {
|
||||
owsFailDebug("v1 group has pending request members.")
|
||||
}
|
||||
return TSGroupModel(groupId: groupId,
|
||||
name: name,
|
||||
avatarData: avatarData,
|
||||
members: Array(groupMembership.nonPendingMembers))
|
||||
members: Array(groupMembership.fullMembers))
|
||||
case .V2:
|
||||
let groupAccess = buildGroupAccess(groupsVersion: groupsVersion)
|
||||
let groupSecretParamsData = try buildGroupSecretParamsData(newGroupSeed: newGroupSeed)
|
||||
@ -90,7 +95,8 @@ public struct TSGroupModelBuilder {
|
||||
groupAccess: groupAccess,
|
||||
revision: groupV2Revision,
|
||||
secretParamsData: groupSecretParamsData,
|
||||
avatarUrlPath: avatarUrlPath)
|
||||
avatarUrlPath: avatarUrlPath,
|
||||
inviteLinkPassword: inviteLinkPassword)
|
||||
}
|
||||
}
|
||||
|
||||
@ -182,6 +188,7 @@ public extension TSGroupModel {
|
||||
builder.groupV2Revision = v2.revision
|
||||
builder.groupSecretParamsData = v2.secretParamsData
|
||||
builder.avatarUrlPath = v2.avatarUrlPath
|
||||
builder.inviteLinkPassword = v2.inviteLinkPassword
|
||||
}
|
||||
return builder
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user