Add send and receive support for group member labels.

This commit is contained in:
jeffrey-signal
2026-01-28 12:43:28 -05:00
committed by Greyson Parrelli
parent ce46c44b5d
commit 0a572153f0
21 changed files with 593 additions and 41 deletions

View File

@@ -21,6 +21,7 @@ import org.signal.core.util.exists
import org.signal.core.util.isAbsent
import org.signal.core.util.logging.Log
import org.signal.core.util.optionalString
import org.signal.core.util.orNull
import org.signal.core.util.readToList
import org.signal.core.util.readToMap
import org.signal.core.util.readToSingleInt
@@ -54,6 +55,7 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.groups.BadGroupIdException
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.GroupId.Push
import org.thoughtcrime.securesms.groups.memberlabel.MemberLabel
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -1271,6 +1273,17 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) :
.sortedBy { it.toString() }
.toList()
}
/**
* Gets the member label for a specific member in the group, or null if the member is not found.
*/
fun memberLabel(aci: ACI): MemberLabel? {
return decryptedGroup
.members
.findMemberByAci(aci)
.orNull()
?.let { member -> MemberLabel(member.labelEmoji, member.labelString) }
}
}
@Throws(BadGroupIdException::class)

View File

@@ -6,6 +6,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import org.signal.core.models.ServiceId;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
@@ -22,7 +23,6 @@ import org.thoughtcrime.securesms.groups.v2.processing.GroupUpdateResult;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
import org.signal.core.models.ServiceId;
import java.io.IOException;
import java.util.Collection;
@@ -233,6 +233,19 @@ public final class GroupManager {
}
}
@WorkerThread
public static void updateMemberLabel(@NonNull Context context, @NonNull GroupId.V2 groupId, @NonNull String labelString, @NonNull String labelEmoji)
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException
{
if (!groupId.isV2()) {
throw new GroupChangeFailedException("Not gv2");
}
try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId)) {
editor.updateMemberLabel(labelString, labelEmoji);
}
}
@WorkerThread
public static void revokeInvites(@NonNull Context context,
@NonNull ServiceId authServiceId,

View File

@@ -358,6 +358,14 @@ final class GroupManagerV2 {
}
}
@WorkerThread
@NonNull
GroupManager.GroupActionResult updateMemberLabel(@NonNull String labelString, @NonNull String labelEmoji)
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
{
return commitChangeWithConflictResolution(selfAci, groupOperations.createChangeMemberLabel(selfAci, labelString, labelEmoji));
}
@WorkerThread
@NonNull GroupManager.GroupActionResult revokeInvites(@NonNull ServiceId authServiceId, @NonNull Collection<UuidCiphertext> uuidCipherTexts, boolean sendToMembers)
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.groups.memberlabel
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.signal.core.models.ServiceId
import org.signal.core.util.orNull
import org.thoughtcrime.securesms.database.GroupTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
/**
* Handles the retrieval and modification of group member labels.
*/
class MemberLabelRepository(
private val groupId: GroupId.V2,
private val context: Context = AppDependencies.application,
private val groupsTable: GroupTable = SignalDatabase.groups
) {
/**
* Gets the member label for a specific recipient in the group.
*/
suspend fun getLabel(recipientId: RecipientId): MemberLabel? = withContext(Dispatchers.IO) {
val recipient = Recipient.resolved(recipientId)
val aci = recipient.serviceId.orNull() as? ServiceId.ACI ?: return@withContext null
val groupRecord = groupsTable.getGroup(groupId).orNull() ?: return@withContext null
return@withContext groupRecord.requireV2GroupProperties().memberLabel(aci)
}
/**
* Sets the group member label for the current user.
*/
suspend fun setLabel(label: MemberLabel): Unit = withContext(Dispatchers.IO) {
GroupManager.updateMemberLabel(context, groupId, label.text, label.emoji ?: "")
}
/**
* Clears the group member label for the current user.
*/
suspend fun removeLabel(): Unit = withContext(Dispatchers.IO) {
GroupManager.updateMemberLabel(context, groupId, "", "")
}
}
/**
* A member's custom label within a group.
*/
data class MemberLabel(
val emoji: String?,
val text: String
)