mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
feat(cordn): fold "Review invitations" into the Cordn card on the new-conversation screen
The invitations entry was a flat row appended after the Relay-aware section, styled unlike every other card and sharing the relay group's icon, so it read as part of the wrong group. It now sits inside the Cordn card as an outlined secondary button under "Create Cordn group" (a new optional SecondaryAction on ConversationType). Still a button, never a count: fetching invitations is a call to every coordinator (spec/00.md §8). Also: - Add an invitations button to the Cordn groups list top bar, which was the other natural place to look and had no way in. - Tighten the Cordn card copy to match the other types: title case, short pros/cons, and drop the "Best for" prefix that duplicated the BEST FOR header. - Remove the now-unused cordn_invitations_entry string from all locales. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DUVZKGPbRB9gWBUrBMU5x5
This commit is contained in:
+11
@@ -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
|
||||
|
||||
+35
-43
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user