Merge pull request #4291 from vitorpamplona/ccr-740792fe-ptnqe9

feat(cordn): fold "Review invitations" into the Cordn card on the new-conversation screen
This commit is contained in:
Vitor Pamplona
2026-09-30 22:23:27 -04:00
committed by GitHub
10 changed files with 52 additions and 57 deletions
@@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.commons.resources.cordn_groups_none
import com.vitorpamplona.amethyst.commons.resources.cordn_groups_none_detail
import com.vitorpamplona.amethyst.commons.resources.cordn_groups_start
import com.vitorpamplona.amethyst.commons.resources.cordn_groups_title
import com.vitorpamplona.amethyst.commons.resources.cordn_invitations_title
import com.vitorpamplona.amethyst.commons.ui.components.EmptyState
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
@@ -110,6 +111,16 @@ fun CordnGroupListScreen(
Scaffold(
topBar = {
TopBarWithBackButton(stringRes(Res.string.cordn_groups_title), nav) {
// Invitations are fetched only when someone opens that screen
// (every call to a coordinator is metadata, spec/00.md §8), so
// this is a plain way in, never a badge.
IconButton(onClick = { nav.nav(Route.CordnInvitations) }) {
Icon(
symbol = MaterialSymbols.MoveToInbox,
contentDescription = stringRes(Res.string.cordn_invitations_title),
modifier = Modifier.size(22.dp),
)
}
IconButton(onClick = { nav.nav(Route.CordnCoordinators) }) {
Icon(
symbol = MaterialSymbols.Dns,
@@ -5591,7 +5591,6 @@
<string name="new_conversation_cordn_pro_1">End-to-end šifrování pomocí MLS — koordinátor nikdy nepřečte žádnou zprávu.</string>
<string name="new_conversation_cordn_pro_2">Jedno dohodnuté pořadí pro všechny, takže historii nemohou přeházet špatně nastavené hodiny.</string>
<string name="new_conversation_cordn_con_1">Koordinátor zjistí, kdo je ve skupině a kdy spolu mluvíte, i když nemůže číst, co píšete.</string>
<string name="cordn_invitations_entry">Máte pozvánku do skupiny cordn?</string>
<string name="cordn_invitations_entry_action">Zobrazit pozvánky</string>
<!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a
one-shot handoff rather than continuous sync. The exposure strings say
@@ -5361,7 +5361,6 @@
<string name="new_conversation_cordn_pro_1">Ende-zu-Ende-verschlüsselt mit MLS — der Koordinator kann nie eine Nachricht lesen.</string>
<string name="new_conversation_cordn_pro_2">Eine gemeinsame Reihenfolge für alle, sodass der Verlauf nicht durch eine falsch gehende Uhr durcheinandergeraten kann.</string>
<string name="new_conversation_cordn_con_1">Der Koordinator erfährt, wer in der Gruppe ist und wann ihr schreibt, auch wenn er nicht lesen kann, was ihr sagt.</string>
<string name="cordn_invitations_entry">In eine cordn-Gruppe eingeladen?</string>
<string name="cordn_invitations_entry_action">Einladungen ansehen</string>
<!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a
one-shot handoff rather than continuous sync. The exposure strings say
@@ -5455,7 +5455,6 @@
<string name="new_conversation_cordn_pro_1">आरम्भ से अन्त तक रहस्यीकृत एमएलएस॰ के साथ। समायोजक सन्देश कभी पढ नहीं सकता।</string>
<string name="new_conversation_cordn_pro_2">एक स्वीकृत अनुक्रमण सभी के लिए। जिससे इतिहास को अस्तव्यस्त नहीं किया जा सकता एक भ्रष्ट घडी से।</string>
<string name="new_conversation_cordn_con_1">समायोजक जान लेता है समूह में कौन हैं तथा आप कब बात करते है। यद्यपि वह पढ नहीं सकता कि आप ने क्या कहा।</string>
<string name="cordn_invitations_entry">क्या आप एक कोर्डन समूह में आमन्त्रित हैं।</string>
<string name="cordn_invitations_entry_action">आमन्त्रण पुनरीक्षण</string>
<!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a
one-shot handoff rather than continuous sync. The exposure strings say
@@ -5455,7 +5455,6 @@
<string name="new_conversation_cordn_pro_1">Végponttól végpontig titkosított MLS-sel – a koordinátor soha nem olvashat el egyetlen üzenetet sem.</string>
<string name="new_conversation_cordn_pro_2">Egy mindenki által elfogadott sorrend, így az előzményeket egy hibás óra sem keverheti össze.</string>
<string name="new_conversation_cordn_con_1">A koordinátor tudja, ki van a csoportban és mikor beszélnek, még ha nem is tudja elolvasni, mit mondanak.</string>
<string name="cordn_invitations_entry">Meghívták egy cordn csoportba?</string>
<string name="cordn_invitations_entry_action">Meghívók áttekintése</string>
<!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a
one-shot handoff rather than continuous sync. The exposure strings say
@@ -5660,7 +5660,6 @@
<string name="new_conversation_cordn_pro_1">Szyfrowanie typu „end-to-end” z wykorzystaniem MLS — koordynator nigdy nie ma możliwości odczytania wiadomości.</string>
<string name="new_conversation_cordn_pro_2">Jedno uzgodnione postanowienie dla wszystkich, aby historia nie mogła zostać zmieniona przez błędny zegar.</string>
<string name="new_conversation_cordn_con_1">Koordynator wie, kto należy do grupy i kiedy ktoś zabiera głos, mimo że nie potrafi odczytać treści wypowiedzi.</string>
<string name="cordn_invitations_entry">Zostałeś zaproszony do grupy „cordn”?</string>
<string name="cordn_invitations_entry_action">Przejrzyj zaproszenia</string>
<!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a
one-shot handoff rather than continuous sync. The exposure strings say
@@ -5359,7 +5359,6 @@
<string name="new_conversation_cordn_pro_1">Criptografia de ponta a ponta com MLS — o coordenador nunca consegue ler uma mensagem.</string>
<string name="new_conversation_cordn_pro_2">Uma única ordem acordada para todos, então o histórico não pode ser embaralhado por um relógio errado.</string>
<string name="new_conversation_cordn_con_1">O coordenador fica sabendo quem está no grupo e quando vocês conversam, mesmo sem conseguir ler o que é dito.</string>
<string name="cordn_invitations_entry">Convidado para um grupo cordn?</string>
<string name="cordn_invitations_entry_action">Ver convites</string>
<!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a
one-shot handoff rather than continuous sync. The exposure strings say
@@ -5359,7 +5359,6 @@
<string name="new_conversation_cordn_pro_1">End-to-end-krypterad med MLS — koordinatorn kan aldrig läsa ett meddelande.</string>
<string name="new_conversation_cordn_pro_2">En gemensam ordning för alla, så att historiken inte kan kastas om av en felaktig klocka.</string>
<string name="new_conversation_cordn_con_1">Koordinatorn får veta vilka som är med i gruppen och när ni pratar, även om den inte kan läsa vad ni säger.</string>
<string name="cordn_invitations_entry">Inbjuden till en cordn-grupp?</string>
<string name="cordn_invitations_entry_action">Granska inbjudningar</string>
<!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a
one-shot handoff rather than continuous sync. The exposure strings say
@@ -5543,15 +5543,14 @@
<string name="cordn_preview_photo">Photo</string>
<string name="cordn_preview_video">Video</string>
<string name="cordn_preview_file">File</string>
<string name="new_conversation_cordn_title">Cordn group</string>
<string name="new_conversation_cordn_tagline">Encrypted group chat ordered by a coordinator you pick.</string>
<string name="new_conversation_cordn_title">Cordn Group</string>
<string name="new_conversation_cordn_tagline">Encrypted groups on a coordinator you pick</string>
<string name="new_conversation_cordn_chip">Coordinated</string>
<string name="new_conversation_cordn_best">Best for teams that want one reliable ordering of the conversation.</string>
<string name="new_conversation_cordn_best">Teams that want one reliable ordering of the conversation.</string>
<string name="new_conversation_cordn_cta">Create Cordn group</string>
<string name="new_conversation_cordn_pro_1">End-to-end encrypted with MLS — the coordinator can never read a message.</string>
<string name="new_conversation_cordn_pro_2">One agreed order for everyone, so history cannot be reshuffled by a bad clock.</string>
<string name="new_conversation_cordn_con_1">The coordinator learns who is in the group and when you talk, even though it cannot read what you say.</string>
<string name="cordn_invitations_entry">Invited to a cordn group?</string>
<string name="new_conversation_cordn_pro_1">Coordinator can't read messages</string>
<string name="new_conversation_cordn_pro_2">One agreed message order</string>
<string name="new_conversation_cordn_con_1">Coordinator sees who talks, and when</string>
<string name="cordn_invitations_entry_action">Review invitations</string>
<!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a
@@ -21,7 +21,7 @@
package com.vitorpamplona.amethyst.commons.chats.ui
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.clickable
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -42,6 +42,7 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@@ -64,7 +65,6 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.cordn_invitations_entry
import com.vitorpamplona.amethyst.commons.resources.cordn_invitations_entry_action
import com.vitorpamplona.amethyst.commons.resources.new_conversation_best_for
import com.vitorpamplona.amethyst.commons.resources.new_conversation_concord_best
@@ -164,7 +164,9 @@ private val ColorLocation = Color(0xFF0891B2)
* One selectable conversation type. Collapsed, a row shows only the icon, name, a short tagline, and
* a one-word [chip] naming its deciding axis (scale / device-bound / moderation / live). Tapping the
* row expands it to reveal [bestFor] and the [pros]/[cons] before the [cta] button routes to that
* type's existing creation (or browse) flow.
* type's existing creation (or browse) flow. A type whose joining is its own flow (rather than
* something that just shows up in the inbox) adds it as [secondary], an outlined button under
* the main one, so "join" sits on the same card as "create" instead of floating elsewhere.
*/
@Immutable
private class ConversationType(
@@ -178,6 +180,13 @@ private class ConversationType(
val pros: List<StringResource>,
val cons: List<StringResource>,
val route: Route,
val secondary: SecondaryAction? = null,
)
@Immutable
private class SecondaryAction(
val label: StringResource,
val route: Route,
)
@Immutable
@@ -253,6 +262,13 @@ private val conversationSections =
// to it, and this is the first place it can be said.
cons = listOf(Res.string.new_conversation_cordn_con_1),
route = Route.CordnCreateGroup,
// Being invited is the other half of "start a
// conversation", so it sits on the same card as
// creating one. It is a button you press, never a
// count: a count would take a call to every
// coordinator, and every call to a coordinator is
// metadata (spec/00.md §8).
secondary = SecondaryAction(Res.string.cordn_invitations_entry_action, Route.CordnInvitations),
),
),
),
@@ -351,50 +367,11 @@ fun NewConversationScreen(nav: INav) {
expanded = expandedId == type.title.key,
onToggle = { expandedId = if (expandedId == type.title.key) "" else type.title.key },
onCreate = { nav.nav(type.route) },
onSecondary = { type.secondary?.let { nav.nav(it.route) } },
)
}
}
}
// Being invited is the other half of "start a conversation", so it
// belongs on the screen people reach for when they want one --
// not buried in settings. The count is deliberately absent: it
// would take a call to every coordinator, and every call to a
// coordinator is metadata (spec/00.md §8).
item(key = "cordn-invitations") {
CordnInvitationsEntry(onClick = { nav.nav(Route.CordnInvitations) })
}
}
}
}
@Composable
private fun CordnInvitationsEntry(onClick: () -> Unit) {
Row(
modifier =
Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 6.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
Icon(
symbol = MaterialSymbols.Dns,
contentDescription = null,
tint = ColorCordn,
modifier = Modifier.size(20.dp),
)
Column(Modifier.weight(1f)) {
Text(
text = stringRes(Res.string.cordn_invitations_entry),
style = MaterialTheme.typography.bodyMedium,
)
Text(
text = stringRes(Res.string.cordn_invitations_entry_action),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.grayText,
)
}
}
}
@@ -405,6 +382,7 @@ private fun ConversationRow(
expanded: Boolean,
onToggle: () -> Unit,
onCreate: () -> Unit,
onSecondary: () -> Unit,
) {
// Lighten the accent in dark mode so chip/label/checkmark text stays legible on the dark ground;
// the solid icon tile keeps the saturated base color in both themes.
@@ -493,6 +471,20 @@ private fun ConversationRow(
) {
Text(stringRes(type.cta), fontWeight = FontWeight.Bold)
}
// Stacked rather than side by side: at phone width each half
// would be ~150dp and the bold primary label would wrap.
type.secondary?.let { secondary ->
OutlinedButton(
onClick = onSecondary,
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
shape = RoundedCornerShape(12.dp),
border = BorderStroke(1.dp, accent),
colors = ButtonDefaults.outlinedButtonColors(contentColor = accent),
) {
Text(stringRes(secondary.label), fontWeight = FontWeight.Bold)
}
}
}
}
}