diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt index 76e98fa168..fa289044dc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt @@ -21,10 +21,13 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.AlertDialog @@ -42,6 +45,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.text.font.FontWeight @@ -116,18 +120,43 @@ fun ConcordChannelListScreen( ?.entries ?.toList() .orEmpty() - LazyColumn(Modifier.fillMaxSize().padding(padding)) { - items(channels, key = { it.key }) { entry -> - val name = entry.value.definition?.name ?: entry.key - Column( - Modifier - .fillMaxWidth() - .clickable { nav.nav(Route.Concord(communityId, entry.key)) } - .padding(horizontal = 16.dp, vertical = 14.dp), - ) { - Text("# $name", style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Medium) + if (channels.isEmpty()) { + Box(Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.Center) { + Text( + stringRes(com.vitorpamplona.amethyst.R.string.concord_channels_empty), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } else { + LazyColumn(Modifier.fillMaxSize().padding(padding)) { + items(channels, key = { it.key }) { entry -> + val def = entry.value.definition + val name = def?.name ?: entry.key + val icon = + when { + def?.voice == true -> MaterialSymbols.Mic + def?.private == true -> MaterialSymbols.Lock + else -> MaterialSymbols.Tag + } + Row( + Modifier + .fillMaxWidth() + .clickable { nav.nav(Route.Concord(communityId, entry.key)) } + .padding(horizontal = 16.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + SymbolIcon( + symbol = icon, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text(name, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Medium, maxLines = 1) + } + HorizontalDivider(thickness = 0.25.dp, color = MaterialTheme.colorScheme.outlineVariant) } - HorizontalDivider() } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt index 4df6224b67..48a9926176 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt @@ -23,11 +23,14 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.conco import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.HorizontalDivider @@ -38,12 +41,18 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -62,52 +71,97 @@ fun ConcordHomeScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val communities by accountViewModel.account.concordChannelList.liveCommunities - .collectAsStateWithLifecycle() + val account = accountViewModel.account + val communities by account.concordChannelList.liveCommunities.collectAsStateWithLifecycle() + // Re-read folded metadata (icon / channel count) whenever a Control Plane folds. + val revision by account.concordSessions.revision.collectAsStateWithLifecycle() Scaffold( topBar = { TopAppBar( - title = { Text(stringRes(com.vitorpamplona.amethyst.R.string.concord_home_title), fontWeight = FontWeight.Bold) }, + title = { Text(stringRes(R.string.concord_home_title), fontWeight = FontWeight.Bold) }, navigationIcon = { IconButton(onClick = { nav.popBack() }) { - SymbolIcon(symbol = MaterialSymbols.AutoMirrored.ArrowBack, contentDescription = stringRes(com.vitorpamplona.amethyst.R.string.back)) + SymbolIcon(symbol = MaterialSymbols.AutoMirrored.ArrowBack, contentDescription = stringRes(R.string.back)) } }, ) }, floatingActionButton = { FloatingActionButton(onClick = { nav.nav(Route.ConcordCreate) }) { - SymbolIcon(symbol = MaterialSymbols.Add, contentDescription = stringRes(com.vitorpamplona.amethyst.R.string.concord_create_title)) + SymbolIcon(symbol = MaterialSymbols.Add, contentDescription = stringRes(R.string.concord_create_title)) } }, ) { padding -> if (communities.isEmpty()) { Box(Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.Center) { Text( - stringRes(com.vitorpamplona.amethyst.R.string.concord_home_empty), + stringRes(R.string.concord_home_empty), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 32.dp), ) } } else { LazyColumn(Modifier.fillMaxSize().padding(padding)) { items(communities, key = { it.id }) { entry -> - Column( - Modifier - .fillMaxWidth() - .clickable { nav.nav(Route.ConcordServer(entry.id)) } - .padding(horizontal = 16.dp, vertical = 14.dp), - ) { - Text( - entry.name.ifBlank { stringRes(com.vitorpamplona.amethyst.R.string.concord_home_title) }, - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium, - ) - } - HorizontalDivider() + val state = + remember(entry.id, revision) { + account.concordSessions + .sessionFor(entry.id) + ?.state + ?.value + } + CommunityRow( + communityId = entry.id, + name = state?.metadata?.name?.takeIf { it.isNotBlank() } ?: entry.name.ifBlank { stringRes(R.string.concord_home_title) }, + iconUrl = state?.metadata?.icon, + channelCount = state?.channels?.size ?: 0, + accountViewModel = accountViewModel, + onClick = { nav.nav(Route.ConcordServer(entry.id)) }, + ) + HorizontalDivider(thickness = 0.25.dp, color = MaterialTheme.colorScheme.outlineVariant) } } } } } + +@Composable +private fun CommunityRow( + communityId: String, + name: String, + iconUrl: String?, + channelCount: Int, + accountViewModel: AccountViewModel, + onClick: () -> Unit, +) { + val autoPlayGif by accountViewModel.settings.autoPlayVideosFlow.collectAsStateWithLifecycle() + Row( + modifier = Modifier.fillMaxWidth().clickable(onClick = onClick).padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = + androidx.compose.foundation.layout.Arrangement + .spacedBy(12.dp), + ) { + RobohashFallbackAsyncImage( + robot = communityId, + model = iconUrl, + contentDescription = name, + modifier = Modifier.size(40.dp).clip(CircleShape), + loadProfilePicture = accountViewModel.settings.showProfilePictures(), + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + autoPlayGif = autoPlayGif, + ) + Column(Modifier.weight(1f)) { + Text(name, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Medium, maxLines = 1, overflow = TextOverflow.Ellipsis) + if (channelCount > 0) { + Text( + pluralStringResource(R.plurals.concord_channel_count, channelCount, channelCount), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index 3fa55bed44..383bb3254a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -436,7 +436,7 @@ private fun ConcordRoomCompose( ChannelName( channelIdHex = channel.channelId.channelId, - channelPicture = null, + channelPicture = channel.communityIcon, channelTitle = { modifier -> Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) { Text( diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 138573f3dd..d3b7d8eda3 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -309,11 +309,16 @@ Could not fetch this invite. The link may be expired or its relays unreachable. Concord Channels You haven\'t joined any Concord Channels yet. Create one, or open an invite link. + No channels yet. New Concord Channel Name About (optional) Relays Icon URL (optional) + + %1$d channel + %1$d channels + Create Invite people Invite link diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/concord/ConcordChannel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/concord/ConcordChannel.kt index a19bbe71fb..5854f1fc0c 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/concord/ConcordChannel.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/concord/ConcordChannel.kt @@ -60,6 +60,10 @@ class ConcordChannel( var communityName: String? = null private set + /** The parent community's icon URL, from its folded metadata (null if unset). */ + var communityIcon: String? = null + private set + /** The community's bootstrap relays — a channel plane may be mirrored on all of them. */ var communityRelays: Set = emptySet() private set @@ -84,6 +88,7 @@ class ConcordChannel( isPrivate = it.private } communityName = state.metadata?.name + communityIcon = state.metadata?.icon communityRelays = relays membership = ConcordMembership.of(state.authority, myPubKey) }