Use locale-aware collation when sorting recipients in message details.

This commit is contained in:
Greyson Parrelli
2026-08-19 19:05:47 -04:00
committed by Cody Henthorne
parent 4b795a8aa6
commit bef261e9dd
3 changed files with 79 additions and 4 deletions
@@ -29,6 +29,18 @@ object GroupMemberOrder {
getDisplayName: (T) -> String
): Comparator<T> = compareBy<T> { !isSelf(it) }
.thenBy { !isAdmin(it) }
.thenBy { !hasDisplayName(it) }
.then(displayNameComparator(hasDisplayName, getDisplayName))
/**
* Creates a [Comparator] for just the display-name portion of the canonical order: members with a user-set display name
* first, then alphabetically by display name using locale-aware collation.
*
* Useful for member lists that have no notion of self-first or admin-first ordering.
*/
@JvmStatic
fun <T> displayNameComparator(
hasDisplayName: (T) -> Boolean,
getDisplayName: (T) -> String
): Comparator<T> = compareBy<T> { !hasDisplayName(it) }
.thenBy { collator.getCollationKey(getDisplayName(it)) }
}
@@ -5,6 +5,7 @@ import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.conversation.ConversationMessage;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.groups.ui.GroupMemberOrder;
import java.util.Collection;
import java.util.Comparator;
@@ -12,9 +13,9 @@ import java.util.List;
import java.util.TreeSet;
public final class MessageDetails {
private static final Comparator<RecipientDeliveryStatus> HAS_DISPLAY_NAME = (r1, r2) -> Boolean.compare(r2.getRecipient().hasAUserSetDisplayName(AppDependencies.getApplication()), r1.getRecipient().hasAUserSetDisplayName(AppDependencies.getApplication()));
private static final Comparator<RecipientDeliveryStatus> ALPHABETICAL = (r1, r2) -> r1.getRecipient().getDisplayName(AppDependencies.getApplication()).compareToIgnoreCase(r2.getRecipient().getDisplayName(AppDependencies.getApplication()));
private static final Comparator<RecipientDeliveryStatus> RECIPIENT_COMPARATOR = HAS_DISPLAY_NAME.thenComparing(ALPHABETICAL);
private static final Comparator<RecipientDeliveryStatus> RECIPIENT_COMPARATOR = GroupMemberOrder.<RecipientDeliveryStatus>displayNameComparator(r -> r.getRecipient().hasAUserSetDisplayName(AppDependencies.getApplication()),
r -> r.getRecipient().getDisplayName(AppDependencies.getApplication()))
.thenComparing(r -> r.getRecipient().getId());
private final ConversationMessage conversationMessage;
@@ -0,0 +1,62 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.groups.ui
import assertk.assertThat
import assertk.assertions.isEqualTo
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.util.Locale
class GroupMemberOrderTest {
private lateinit var previousLocale: Locale
@Before
fun setUp() {
previousLocale = Locale.getDefault()
Locale.setDefault(Locale.GERMANY)
}
@After
fun tearDown() {
Locale.setDefault(previousLocale)
}
@Test
fun `displayNameComparator - accented names sort alongside their unaccented equivalents`() {
val names = listOf("Zeta", "Ärzte", "Anna", "Émile", "Bernd", "Ökonom", "Oskar")
val sorted = names.sortedWith(GroupMemberOrder.displayNameComparator({ true }, { it }))
assertThat(sorted).isEqualTo(listOf("Anna", "Ärzte", "Bernd", "Émile", "Ökonom", "Oskar", "Zeta"))
}
@Test
fun `comparator - self first, then admins, then named members alphabetically`() {
val self = Member("Zoe", isSelf = true)
val admin = Member("Yannick", isAdmin = true)
val bernd = Member("Bernd")
val arzte = Member("Ärzte")
val unnamed = Member("+15551234567", hasDisplayName = false)
val sorted = listOf(bernd, unnamed, arzte, admin, self).sortedWith(MEMBER_ORDER)
assertThat(sorted).isEqualTo(listOf(self, admin, arzte, bernd, unnamed))
}
private data class Member(
val name: String,
val isSelf: Boolean = false,
val isAdmin: Boolean = false,
val hasDisplayName: Boolean = true
)
companion object {
private val MEMBER_ORDER = GroupMemberOrder.comparator<Member>({ it.isSelf }, { it.isAdmin }, { it.hasDisplayName }, { it.name })
}
}