mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
Start implementing the UI
Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
This commit is contained in:
@@ -28,7 +28,9 @@ import ru.fromchat.ui.auth.LoginScreen
|
|||||||
import ru.fromchat.ui.auth.RegisterScreen
|
import ru.fromchat.ui.auth.RegisterScreen
|
||||||
import ru.fromchat.ui.chat.PublicChatScreen
|
import ru.fromchat.ui.chat.PublicChatScreen
|
||||||
import ru.fromchat.ui.debug.DebugApiScreen
|
import ru.fromchat.ui.debug.DebugApiScreen
|
||||||
|
import ru.fromchat.ui.dm.DmScreen
|
||||||
import ru.fromchat.ui.main.MainScreen
|
import ru.fromchat.ui.main.MainScreen
|
||||||
|
import ru.fromchat.ui.profile.ProfileScreen
|
||||||
import ru.fromchat.ui.setup.ServerConfigScreen
|
import ru.fromchat.ui.setup.ServerConfigScreen
|
||||||
|
|
||||||
val LocalNavController = compositionLocalOf<NavController> { error("NavController not provided") }
|
val LocalNavController = compositionLocalOf<NavController> { error("NavController not provided") }
|
||||||
@@ -174,6 +176,23 @@ fun App(scrollToMessageId: Int? = null, startAtPublicChat: Boolean = false) {
|
|||||||
DebugApiScreen()
|
DebugApiScreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable("profile/{userId}") { backStackEntry ->
|
||||||
|
val userId = backStackEntry.arguments?.getString("userId")?.toIntOrNull()
|
||||||
|
ProfileScreen(
|
||||||
|
userId = userId,
|
||||||
|
onBack = { navController.navigateUp() },
|
||||||
|
onChat = { navController.navigate("dm/$it") }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable("dm/{otherUserId}") { backStackEntry ->
|
||||||
|
val otherUserId = backStackEntry.arguments?.getString("otherUserId")?.toIntOrNull() ?: 0
|
||||||
|
DmScreen(
|
||||||
|
otherUserId = otherUserId,
|
||||||
|
onBack = { navController.navigateUp() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
composable("about") {
|
composable("about") {
|
||||||
AboutScreen()
|
AboutScreen()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,276 @@
|
|||||||
|
package ru.fromchat.ui.dm
|
||||||
|
|
||||||
|
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.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Divider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
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.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonArray
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.serializer
|
||||||
|
import ru.fromchat.api.ApiClient
|
||||||
|
import ru.fromchat.api.DmEnvelope
|
||||||
|
import ru.fromchat.api.WebSocketManager
|
||||||
|
import ru.fromchat.api.WebSocketMessage
|
||||||
|
import ru.fromchat.crypto.decryptEnvelope
|
||||||
|
|
||||||
|
private data class DecryptedDmMessage(
|
||||||
|
val id: Int,
|
||||||
|
val text: String,
|
||||||
|
val timestamp: String,
|
||||||
|
val isOutgoing: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun DmScreen(
|
||||||
|
otherUserId: Int,
|
||||||
|
onBack: () -> Unit
|
||||||
|
) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val messages = remember { mutableStateListOf<DecryptedDmMessage>() }
|
||||||
|
var isLoading by remember { mutableStateOf(false) }
|
||||||
|
var status by remember { mutableStateOf<String?>(null) }
|
||||||
|
var input by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
val currentUserId = ApiClient.user?.id
|
||||||
|
val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
|
suspend fun loadHistory() {
|
||||||
|
isLoading = true
|
||||||
|
runCatching {
|
||||||
|
ApiClient.getDmHistory(otherUserId)
|
||||||
|
}.onSuccess { response ->
|
||||||
|
val decrypted = response.messages.mapNotNull { envelope ->
|
||||||
|
val plaintext = runCatching { decryptEnvelope(envelope, currentUserId) }.getOrNull()
|
||||||
|
plaintext?.let {
|
||||||
|
DecryptedDmMessage(
|
||||||
|
id = envelope.id,
|
||||||
|
text = it,
|
||||||
|
timestamp = envelope.timestamp,
|
||||||
|
isOutgoing = currentUserId != null && envelope.senderId == currentUserId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
messages.clear()
|
||||||
|
messages.addAll(decrypted)
|
||||||
|
status = null
|
||||||
|
}.onFailure {
|
||||||
|
status = it.message ?: "Failed to load DMs"
|
||||||
|
}
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(otherUserId) {
|
||||||
|
loadHistory()
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(otherUserId) {
|
||||||
|
val handler: (WebSocketMessage) -> Unit = handler@ { msg ->
|
||||||
|
val payloads = extractDmPayloads(msg)
|
||||||
|
for (payload in payloads) {
|
||||||
|
val envelope = runCatching {
|
||||||
|
json.decodeFromJsonElement(DmEnvelope.serializer(), payload)
|
||||||
|
}.getOrNull() ?: continue
|
||||||
|
if (envelope.senderId != otherUserId && envelope.recipientId != otherUserId) continue
|
||||||
|
coroutineScope.launch {
|
||||||
|
val plaintext = runCatching { decryptEnvelope(envelope, currentUserId) }.getOrNull()
|
||||||
|
if (plaintext == null) return@launch
|
||||||
|
val newMessage = DecryptedDmMessage(
|
||||||
|
id = envelope.id,
|
||||||
|
text = plaintext,
|
||||||
|
timestamp = envelope.timestamp,
|
||||||
|
isOutgoing = currentUserId != null && envelope.senderId == currentUserId
|
||||||
|
)
|
||||||
|
if (messages.none { it.id == newMessage.id }) {
|
||||||
|
messages.add(newMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WebSocketManager.addGlobalMessageHandler(handler)
|
||||||
|
onDispose {
|
||||||
|
WebSocketManager.removeGlobalMessageHandler(handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(text = "DM with user $otherUserId") },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = "Back")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(innerPadding)
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
if (isLoading) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
if (messages.isEmpty()) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = "No messages yet.",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items(messages, key = { it.id }) { message ->
|
||||||
|
MessageBubble(message = message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
status?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.Bottom
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = input,
|
||||||
|
onValueChange = { input = it },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
label = { Text("Message") },
|
||||||
|
singleLine = false
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (input.isBlank()) return@Button
|
||||||
|
coroutineScope.launch {
|
||||||
|
runCatching {
|
||||||
|
ApiClient.sendDm(recipientId = otherUserId, plaintext = input.trim())
|
||||||
|
}.onSuccess {
|
||||||
|
input = ""
|
||||||
|
loadHistory()
|
||||||
|
}.onFailure {
|
||||||
|
status = it.message ?: "Failed to send DM"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(text = "Send")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MessageBubble(message: DecryptedDmMessage) {
|
||||||
|
val alignment = if (message.isOutgoing) Alignment.End else Alignment.Start
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = alignment
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.85f),
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(12.dp)) {
|
||||||
|
Text(text = message.text, style = MaterialTheme.typography.bodyMedium)
|
||||||
|
Spacer(modifier = Modifier.size(4.dp))
|
||||||
|
Text(
|
||||||
|
text = message.timestamp,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractDmPayloads(msg: WebSocketMessage): List<JsonElement> {
|
||||||
|
val results = mutableListOf<JsonElement>()
|
||||||
|
when (msg.type) {
|
||||||
|
"dmNew" -> msg.data?.let { results.add(it) }
|
||||||
|
"updates" -> {
|
||||||
|
val updatesArray = msg.data
|
||||||
|
?.jsonObject
|
||||||
|
?.get("updates")
|
||||||
|
?.jsonArray
|
||||||
|
?: JsonArray(emptyList())
|
||||||
|
for (item in updatesArray) {
|
||||||
|
val obj = item.jsonObject
|
||||||
|
if (obj["type"]?.jsonPrimitive?.content == "dmNew") {
|
||||||
|
obj["data"]?.let { results.add(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
@@ -214,6 +214,31 @@ fun SettingsTab(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val currentUserId = ApiClient.user?.id ?: 0
|
||||||
|
ListItem(
|
||||||
|
headline = "Profile screen",
|
||||||
|
supportingText = "Show your profile with chat/link actions",
|
||||||
|
onClick = {
|
||||||
|
if (currentUserId != 0) {
|
||||||
|
navController.navigate("profile/$currentUserId")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
divider = true,
|
||||||
|
dividerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
dividerThickness = 2.dp
|
||||||
|
)
|
||||||
|
|
||||||
|
ListItem(
|
||||||
|
headline = "DM screen (user 2)",
|
||||||
|
supportingText = "Open DM flow with another user",
|
||||||
|
onClick = {
|
||||||
|
navController.navigate("dm/2")
|
||||||
|
},
|
||||||
|
divider = true,
|
||||||
|
dividerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
dividerThickness = 2.dp
|
||||||
|
)
|
||||||
|
|
||||||
ListItem(
|
ListItem(
|
||||||
headline = stringResource(Res.string.logout),
|
headline = stringResource(Res.string.logout),
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
package ru.fromchat.ui.profile
|
||||||
|
|
||||||
|
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.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Divider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.ClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import ru.fromchat.api.ApiClient
|
||||||
|
import ru.fromchat.api.UserProfile
|
||||||
|
|
||||||
|
private data class ProfileUiState(
|
||||||
|
val profile: UserProfile? = null,
|
||||||
|
val isLoading: Boolean = true,
|
||||||
|
val error: String? = null,
|
||||||
|
val linkStatus: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ProfileScreen(
|
||||||
|
userId: Int?,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onChat: (Int) -> Unit
|
||||||
|
) {
|
||||||
|
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||||
|
var state by remember { mutableStateOf(ProfileUiState()) }
|
||||||
|
|
||||||
|
val targetUserId = userId.takeIf { it != null && it > 0 }
|
||||||
|
val fetchKey = targetUserId ?: 0
|
||||||
|
|
||||||
|
LaunchedEffect(fetchKey) {
|
||||||
|
state = state.copy(isLoading = true, error = null)
|
||||||
|
runCatching {
|
||||||
|
if (targetUserId == null) {
|
||||||
|
ApiClient.getOwnProfile()
|
||||||
|
} else {
|
||||||
|
ApiClient.getProfileById(targetUserId)
|
||||||
|
}
|
||||||
|
}.onSuccess { profile ->
|
||||||
|
state = state.copy(profile = profile, isLoading = false)
|
||||||
|
}.onFailure {
|
||||||
|
state = state.copy(error = it.message ?: "Unable to load profile", isLoading = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(text = "Profile") },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = "Back")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(innerPadding)
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
val errorMessage = state.error
|
||||||
|
when {
|
||||||
|
state.isLoading -> {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
||||||
|
}
|
||||||
|
errorMessage != null -> {
|
||||||
|
Text(
|
||||||
|
text = errorMessage,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
state.profile != null -> {
|
||||||
|
val profile = state.profile!!
|
||||||
|
val profileLink = profile.username.takeIf { it.isNotBlank() }
|
||||||
|
?.let { "https://fromchat.ru/@$it" }
|
||||||
|
?: "https://fromchat.ru/?u=${profile.id}"
|
||||||
|
val displayName = profile.displayName?.takeIf { it.isNotBlank() } ?: profile.username
|
||||||
|
val initials = profile.displayName?.firstOrNull()
|
||||||
|
?: profile.username.firstOrNull()
|
||||||
|
?: '?'
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Top
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(128.dp)
|
||||||
|
.padding(top = 16.dp),
|
||||||
|
shape = CircleShape,
|
||||||
|
color = MaterialTheme.colorScheme.primaryContainer
|
||||||
|
) {
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
Text(
|
||||||
|
text = initials.toString(),
|
||||||
|
style = MaterialTheme.typography.headlineLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Text(
|
||||||
|
text = displayName,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = "@${profile.username}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = { onChat(profile.id) },
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(text = "Chat")
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
clipboardManager.setText(AnnotatedString(profileLink))
|
||||||
|
state = state.copy(linkStatus = "Link copied!")
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(text = "Link")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.linkStatus?.let {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(text = it, style = MaterialTheme.typography.bodySmall)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
|
ProfileField(label = "Username", value = profile.username)
|
||||||
|
Divider(modifier = Modifier.padding(vertical = 8.dp))
|
||||||
|
ProfileField(label = "Bio", value = profile.bio ?: "No bio")
|
||||||
|
Divider(modifier = Modifier.padding(vertical = 8.dp))
|
||||||
|
ProfileField(label = "Member since", value = profile.createdAt ?: "Unknown")
|
||||||
|
Divider(modifier = Modifier.padding(vertical = 8.dp))
|
||||||
|
ProfileField(
|
||||||
|
label = "Status",
|
||||||
|
value = if ((profile.deleted == true) || (profile.suspended == true)) "Suspended/Deleted" else "Active"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ProfileField(label: String, value: String) {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
Text(
|
||||||
|
text = value,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user