Move “can local user leave” check to membership

This commit is contained in:
Max Radermacher 2026-04-13 14:16:48 -05:00 committed by GitHub
parent 3b60ab1530
commit 0632af7391
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 61 additions and 38 deletions

View File

@ -777,6 +777,7 @@
509085BC2C498D3600409B85 /* LinkPreviewFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 509085BB2C498D3500409B85 /* LinkPreviewFetcher.swift */; };
509085BE2C49C29400409B85 /* PaddingBucket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 509085BD2C49C29400409B85 /* PaddingBucket.swift */; };
509085C02C49C2A500409B85 /* PaddingBucketTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 509085BF2C49C2A500409B85 /* PaddingBucketTest.swift */; };
5090B1A12F8D7239003F029D /* GroupMembershipTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5090B1A02F8D7239003F029D /* GroupMembershipTest.swift */; };
50925DEA2DA86E3A00DAB484 /* GroupMessageProcessorJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50925DE92DA86E3A00DAB484 /* GroupMessageProcessorJob.swift */; };
50925DEC2DA87AEF00DAB484 /* GroupMessageProcessorJobTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50925DEB2DA87AEF00DAB484 /* GroupMessageProcessorJobTest.swift */; };
5094D5052EE3A6780041F402 /* AttachmentLimits.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5094D5042EE3A6780041F402 /* AttachmentLimits.swift */; };
@ -5052,6 +5053,7 @@
509085BB2C498D3500409B85 /* LinkPreviewFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewFetcher.swift; sourceTree = "<group>"; };
509085BD2C49C29400409B85 /* PaddingBucket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaddingBucket.swift; sourceTree = "<group>"; };
509085BF2C49C2A500409B85 /* PaddingBucketTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaddingBucketTest.swift; sourceTree = "<group>"; };
5090B1A02F8D7239003F029D /* GroupMembershipTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMembershipTest.swift; sourceTree = "<group>"; };
50925DE92DA86E3A00DAB484 /* GroupMessageProcessorJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMessageProcessorJob.swift; sourceTree = "<group>"; };
50925DEB2DA87AEF00DAB484 /* GroupMessageProcessorJobTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMessageProcessorJobTest.swift; sourceTree = "<group>"; };
5094D5042EE3A6780041F402 /* AttachmentLimits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentLimits.swift; sourceTree = "<group>"; };
@ -14334,6 +14336,7 @@
F94261E2289B1B5400460798 /* Groups */ = {
isa = PBXGroup;
children = (
5090B1A02F8D7239003F029D /* GroupMembershipTest.swift */,
F94261E3289B1B5400460798 /* GroupModelsTest.swift */,
);
name = Groups;
@ -20202,6 +20205,7 @@
F97217FE28DCBC5100113D9F /* GRDBSchemaMigratorTest.swift in Sources */,
D979CC5E2AD618EA006AAC49 /* GroupCallRecordManagerTest.swift in Sources */,
D91F0B4F2B193A7A0086DB30 /* GroupCallRecordRingUpdateDelegateTest.swift in Sources */,
5090B1A12F8D7239003F029D /* GroupMembershipTest.swift in Sources */,
5075C21729CA1EE700A260D2 /* GroupMemberUpdaterTest.swift in Sources */,
50925DEC2DA87AEF00DAB484 /* GroupMessageProcessorJobTest.swift in Sources */,
F9426251289B1B5500460798 /* GroupModelsTest.swift in Sources */,

View File

@ -687,10 +687,7 @@ class ConversationSettingsViewController: OWSTableViewController2, BadgeCollecti
let groupThread = thread as? TSGroupThread,
let groupModel = groupThread.groupModel as? TSGroupModelV2,
let localAci = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction?.aci,
GroupManager.canLocalUserLeaveGroupWithoutChoosingNewAdmin(
localAci: localAci,
groupMembership: groupModel.groupMembership,
)
groupModel.groupMembership.canLocalUserLeaveGroupWithoutChoosingNewAdmin(localAci: localAci)
{
LeaveGroupCoordinator(
groupThread: groupThread,

View File

@ -30,12 +30,7 @@ class LeaveGroupCoordinator: ReplaceAdminViewControllerDelegate {
// Retain self for the lifetime of rootViewController.
ObjectRetainer.retainObject(self, forLifetimeOf: rootViewController)
if
GroupManager.canLocalUserLeaveGroupWithoutChoosingNewAdmin(
localAci: localAci,
groupMembership: groupModel.groupMembership,
)
{
if groupModel.groupMembership.canLocalUserLeaveGroupWithoutChoosingNewAdmin(localAci: localAci) {
showLeaveGroupConfirmAlert(
fromViewController: rootViewController,
replacementAdminAci: nil,

View File

@ -70,34 +70,6 @@ public class GroupManager: NSObject {
return isV1GroupId(groupId) || isV2GroupId(groupId)
}
// MARK: -
public static func canLocalUserLeaveGroupWithoutChoosingNewAdmin(
localAci: Aci,
groupMembership: GroupMembership,
) -> Bool {
let fullMembers = Set(groupMembership.fullMembers.compactMap { $0.serviceId as? Aci })
let fullMemberAdmins = Set(groupMembership.fullMemberAdministrators.compactMap { $0.serviceId as? Aci })
return canLocalUserLeaveGroupWithoutChoosingNewAdmin(
localAci: localAci,
fullMembers: fullMembers,
admins: fullMemberAdmins,
)
}
public static func canLocalUserLeaveGroupWithoutChoosingNewAdmin(
localAci: Aci,
fullMembers: Set<Aci>,
admins: Set<Aci>,
) -> Bool {
// If the current user is the only admin and they're not the only member of
// the group, then they must select a new admin.
if Set([localAci]) == admins, Set([localAci]) != fullMembers {
return false
}
return true
}
// MARK: - Group Models
/// Confirms that a given address supports V2 groups.

View File

@ -585,6 +585,28 @@ public class GroupMembership: NSObject, NSSecureCoding {
// MARK: -
public func canLocalUserLeaveGroupWithoutChoosingNewAdmin(localAci: Aci) -> Bool {
let fullMembers = Set(self.fullMembers.compactMap { $0.serviceId as? Aci })
let fullMemberAdmins = Set(self.fullMemberAdministrators.compactMap { $0.serviceId as? Aci })
return Self.canLocalUserLeaveGroupWithoutChoosingNewAdmin(
localAci: localAci,
fullMembers: fullMembers,
admins: fullMemberAdmins,
)
}
static func canLocalUserLeaveGroupWithoutChoosingNewAdmin(
localAci: Aci,
fullMembers: Set<Aci>,
admins: Set<Aci>,
) -> Bool {
// If there's already another admin or we're the only member, we can leave
// without selecting a new admin.
return Set([localAci]) != admins || Set([localAci]) == fullMembers
}
// MARK: -
/// Is this user's profile key exposed to the group?
public func hasProfileKeyInGroup(serviceId: ServiceId) -> Bool {
guard let memberState = memberStates[SignalServiceAddress(serviceId)] else {

View File

@ -0,0 +1,33 @@
//
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import LibSignalClient
import Testing
@testable import SignalServiceKit
struct GroupMembershipTest {
private static let localAci = LocalIdentifiers.forUnitTests.aci
private static let otherAci = Aci.constantForTesting("00000000-0000-4000-8000-000000000000")
@Test(arguments: [
(true, [], []),
(true, [Self.localAci], [Self.localAci]),
(false, [Self.localAci], [Self.localAci, Self.otherAci]),
(true, [Self.localAci, Self.otherAci], [Self.localAci, Self.otherAci]),
(true, [], [Self.localAci]),
(true, [], [Self.localAci, Self.otherAci]),
(true, [Self.otherAci], [Self.localAci, Self.otherAci]),
])
func testCanLocalUserLeaveGroup(testCase: (canLeave: Bool, admins: [Aci], members: [Aci])) {
let localAci = Self.localAci
let canLeave = GroupMembership.canLocalUserLeaveGroupWithoutChoosingNewAdmin(
localAci: localAci,
fullMembers: Set(testCase.members),
admins: Set(testCase.admins),
)
#expect(canLeave == testCase.canLeave)
}
}