diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberOrder.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberOrder.kt index 93ea254e05..4ff1181575 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberOrder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberOrder.kt @@ -29,6 +29,18 @@ object GroupMemberOrder { getDisplayName: (T) -> String ): Comparator = compareBy { !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 displayNameComparator( + hasDisplayName: (T) -> Boolean, + getDisplayName: (T) -> String + ): Comparator = compareBy { !hasDisplayName(it) } .thenBy { collator.getCollationKey(getDisplayName(it)) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetails.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetails.java index cfe2c267bd..711381c6f0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetails.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetails.java @@ -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 HAS_DISPLAY_NAME = (r1, r2) -> Boolean.compare(r2.getRecipient().hasAUserSetDisplayName(AppDependencies.getApplication()), r1.getRecipient().hasAUserSetDisplayName(AppDependencies.getApplication())); - private static final Comparator ALPHABETICAL = (r1, r2) -> r1.getRecipient().getDisplayName(AppDependencies.getApplication()).compareToIgnoreCase(r2.getRecipient().getDisplayName(AppDependencies.getApplication())); - private static final Comparator RECIPIENT_COMPARATOR = HAS_DISPLAY_NAME.thenComparing(ALPHABETICAL); + private static final Comparator RECIPIENT_COMPARATOR = GroupMemberOrder.displayNameComparator(r -> r.getRecipient().hasAUserSetDisplayName(AppDependencies.getApplication()), + r -> r.getRecipient().getDisplayName(AppDependencies.getApplication())) + .thenComparing(r -> r.getRecipient().getId()); private final ConversationMessage conversationMessage; diff --git a/app/src/test/java/org/thoughtcrime/securesms/groups/ui/GroupMemberOrderTest.kt b/app/src/test/java/org/thoughtcrime/securesms/groups/ui/GroupMemberOrderTest.kt new file mode 100644 index 0000000000..e9825e11da --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/groups/ui/GroupMemberOrderTest.kt @@ -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({ it.isSelf }, { it.isAdmin }, { it.hasDisplayName }, { it.name }) + } +}