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)
}