diff --git a/composeApp/src/androidMain/composeResources/drawable/ic_badge.xml b/composeApp/src/androidMain/composeResources/drawable/ic_badge.xml
new file mode 100644
index 0000000..f2d834b
--- /dev/null
+++ b/composeApp/src/androidMain/composeResources/drawable/ic_badge.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/composeApp/src/androidMain/composeResources/drawable/ic_bitcoin.xml b/composeApp/src/androidMain/composeResources/drawable/ic_bitcoin.xml
new file mode 100644
index 0000000..b190e5e
--- /dev/null
+++ b/composeApp/src/androidMain/composeResources/drawable/ic_bitcoin.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/composeApp/src/androidMain/composeResources/drawable/ic_chat.xml b/composeApp/src/androidMain/composeResources/drawable/ic_chat.xml
new file mode 100644
index 0000000..df11a25
--- /dev/null
+++ b/composeApp/src/androidMain/composeResources/drawable/ic_chat.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/composeApp/src/androidMain/composeResources/drawable/ic_globe.xml b/composeApp/src/androidMain/composeResources/drawable/ic_globe.xml
new file mode 100644
index 0000000..57104ef
--- /dev/null
+++ b/composeApp/src/androidMain/composeResources/drawable/ic_globe.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/composeApp/src/androidMain/composeResources/drawable/ic_share.xml b/composeApp/src/androidMain/composeResources/drawable/ic_share.xml
new file mode 100644
index 0000000..d4e9c12
--- /dev/null
+++ b/composeApp/src/androidMain/composeResources/drawable/ic_share.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/App.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/App.kt
index 4cba833..e8628c0 100644
--- a/composeApp/src/androidMain/kotlin/su/reya/coop/App.kt
+++ b/composeApp/src/androidMain/kotlin/su/reya/coop/App.kt
@@ -54,6 +54,7 @@ import su.reya.coop.screens.MyQrScreen
import su.reya.coop.screens.NewChatScreen
import su.reya.coop.screens.NewIdentityScreen
import su.reya.coop.screens.OnboardingScreen
+import su.reya.coop.screens.ProfileScreen
import su.reya.coop.screens.RelayScreen
import su.reya.coop.screens.ScanScreen
@@ -232,6 +233,13 @@ fun App() {
onBack = { navController.popBackStack() },
)
}
+ composable { backStackEntry ->
+ val profile: Screen.Profile = backStackEntry.toRoute()
+ ProfileScreen(
+ pubkey = profile.pubkey,
+ onBack = { navController.popBackStack() },
+ )
+ }
composable { backStackEntry ->
NewChatScreen(
onBack = { navController.popBackStack() },
diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/Navigation.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/Navigation.kt
index 1b4ddb4..dbf5bc3 100644
--- a/composeApp/src/androidMain/kotlin/su/reya/coop/Navigation.kt
+++ b/composeApp/src/androidMain/kotlin/su/reya/coop/Navigation.kt
@@ -9,6 +9,9 @@ sealed interface Screen {
@Serializable
data class Chat(val id: Long) : Screen
+ @Serializable
+ data class Profile(val pubkey: String) : Screen
+
@Serializable
data object NewChat : Screen
diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt
index f315106..c7f8f87 100644
--- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt
+++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt
@@ -51,8 +51,10 @@ import coop.composeapp.generated.resources.ic_send
import kotlinx.coroutines.flow.first
import org.jetbrains.compose.resources.painterResource
import rust.nostr.sdk.UnsignedEvent
+import su.reya.coop.LocalNavController
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalSnackbarHostState
+import su.reya.coop.Screen
import su.reya.coop.formatAsGroupHeader
import su.reya.coop.roomId
import su.reya.coop.shared.Avatar
@@ -66,6 +68,7 @@ fun ChatScreen(
onBack: () -> Unit,
) {
val snackbarHostState = LocalSnackbarHostState.current
+ val navController = LocalNavController.current
val viewModel = LocalNostrViewModel.current
val listState = rememberLazyListState()
@@ -143,7 +146,14 @@ fun ChatScreen(
topBar = {
TopAppBar(
title = {
- Row(verticalAlignment = Alignment.CenterVertically) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.clickable {
+ room.members.firstOrNull()?.let { pubkey ->
+ navController.navigate(Screen.Profile(pubkey.toBech32()))
+ }
+ }
+ ) {
if (loading) {
LoadingIndicator(
modifier = Modifier.size(32.dp),
diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ProfileScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ProfileScreen.kt
new file mode 100644
index 0000000..5c0f924
--- /dev/null
+++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ProfileScreen.kt
@@ -0,0 +1,243 @@
+package su.reya.coop.screens
+
+import android.content.Intent
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.FilledTonalIconButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.ListItemDefaults
+import androidx.compose.material3.MaterialShapes
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SegmentedListItem
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.toShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import coop.composeapp.generated.resources.Res
+import coop.composeapp.generated.resources.ic_arrow_back
+import coop.composeapp.generated.resources.ic_chat
+import coop.composeapp.generated.resources.ic_share
+import kotlinx.coroutines.launch
+import org.jetbrains.compose.resources.painterResource
+import rust.nostr.sdk.PublicKey
+import su.reya.coop.LocalNavController
+import su.reya.coop.LocalNostrViewModel
+import su.reya.coop.LocalSnackbarHostState
+import su.reya.coop.Screen
+import su.reya.coop.shared.Avatar
+import su.reya.coop.shared.getExpressiveFontFamily
+import su.reya.coop.short
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+fun ProfileScreen(
+ onBack: () -> Unit,
+ pubkey: String
+) {
+ val pubkey = runCatching { PublicKey.parse(pubkey) }.getOrNull() ?: return
+
+ val context = LocalContext.current
+ val snackbarHostState = LocalSnackbarHostState.current
+ val navController = LocalNavController.current
+ val viewModel = LocalNostrViewModel.current
+
+ val scope = rememberCoroutineScope()
+ val metadataFlow = remember(pubkey) { viewModel.getMetadata(pubkey) }
+ val metadata by metadataFlow.collectAsState(initial = null)
+
+ val profile = metadata?.asRecord()
+ val displayName = profile?.displayName ?: profile?.name ?: "No name"
+ val nip05 = profile?.nip05 ?: pubkey.short()
+ val picture = profile?.picture
+ val details = remember(profile) {
+ listOf(
+ "Username:" to (profile?.name ?: "None"),
+ "Website:" to (profile?.website ?: "None"),
+ "Lightning Address:" to (profile?.lud16 ?: "None"),
+ )
+ }
+
+ Scaffold(
+ containerColor = MaterialTheme.colorScheme.surfaceContainer,
+ snackbarHost = { SnackbarHost(snackbarHostState) },
+ topBar = {
+ TopAppBar(
+ title = { },
+ navigationIcon = {
+ IconButton(onClick = onBack) {
+ Icon(
+ painter = painterResource(Res.drawable.ic_arrow_back),
+ contentDescription = "Back"
+ )
+ }
+ },
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainer,
+ )
+ )
+ },
+ content = { innerPadding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp)
+ .weight(1f),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Box(
+ modifier = Modifier
+ .size(120.dp)
+ .clip(MaterialShapes.Cookie9Sided.toShape()),
+ contentAlignment = Alignment.Center
+ ) {
+ Avatar(
+ picture = picture,
+ description = "Profile picture",
+ modifier = Modifier.fillMaxSize(),
+ shape = MaterialShapes.Cookie9Sided.toShape(),
+ )
+ }
+ Spacer(modifier = Modifier.size(8.dp))
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ ) {
+ Text(
+ text = displayName,
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.titleLargeEmphasized.copy(
+ fontFamily = getExpressiveFontFamily()
+ ),
+ )
+ Text(
+ text = nip05,
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.titleSmall
+ )
+ }
+ Spacer(modifier = Modifier.size(24.dp))
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ Column(
+ modifier = Modifier.weight(1f),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ ) {
+ FilledTonalIconButton(
+ onClick = {
+ scope.launch {
+ try {
+ val roomId = viewModel.createChatRoom(listOf(pubkey))
+ navController.navigate(Screen.Chat(roomId))
+ } catch (e: Exception) {
+ e.message?.let { snackbarHostState.showSnackbar(it) }
+ }
+ }
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(ButtonDefaults.MediumContainerHeight),
+ ) {
+ Icon(
+ painter = painterResource(Res.drawable.ic_chat),
+ contentDescription = "New Chat"
+ )
+ }
+ Text(
+ text = "Message",
+ style = MaterialTheme.typography.labelSmall
+ )
+ }
+ Column(
+ modifier = Modifier.weight(1f),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ ) {
+ FilledTonalIconButton(
+ onClick = {
+ val sendIntent = Intent().apply {
+ action = Intent.ACTION_SEND
+ putExtra(Intent.EXTRA_TEXT, pubkey.toBech32())
+ type = "text/plain"
+ }
+ val shareIntent = Intent.createChooser(sendIntent, null)
+ context.startActivity(shareIntent)
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(ButtonDefaults.MediumContainerHeight),
+ ) {
+ Icon(
+ painter = painterResource(Res.drawable.ic_share),
+ contentDescription = "Share"
+ )
+ }
+ Text(
+ text = "Share",
+ style = MaterialTheme.typography.labelMedium
+ )
+ }
+ }
+ }
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp)
+ .weight(1.5f),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap),
+ ) {
+ details.forEachIndexed { index, (label, value) ->
+ SegmentedListItem(
+ onClick = { },
+ shapes = ListItemDefaults.segmentedShapes(
+ index = index,
+ count = details.size
+ ),
+ content = { Text(label) },
+ supportingContent = { Text(value) },
+ )
+ }
+ }
+ }
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt b/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt
index 4f2d0a8..a33c00e 100644
--- a/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt
+++ b/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt
@@ -477,6 +477,9 @@ class NostrViewModel(
}
fun sendMessage(roomId: Long, message: String, replies: List = emptyList()) {
+ if (message.isEmpty()) {
+ showError("Message cannot be empty")
+ }
viewModelScope.launch {
try {
val room = getChatRoom(roomId)