chore: merge the develop branch into master #1

Merged
reya merged 43 commits from develop into master 2026-05-23 00:50:13 +00:00
5 changed files with 147 additions and 56 deletions
Showing only changes of commit e6dff5277d - Show all commits

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="M440,520L200,520L200,440L440,440L440,200L520,200L520,440L760,440L760,520L520,520L520,760L440,760L440,520Z" />
</vector>

View File

@@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -68,6 +69,8 @@ fun ChatScreen(
val viewModel = LocalNostrViewModel.current val viewModel = LocalNostrViewModel.current
val room = viewModel.getChatRoom(id) val room = viewModel.getChatRoom(id)
val listState = rememberLazyListState()
val displayName by remember(room) { room.displayNameFlow(viewModel) }.collectAsState("Loading...") val displayName by remember(room) { room.displayNameFlow(viewModel) }.collectAsState("Loading...")
val picture by remember(room) { room.pictureFlow(viewModel) }.collectAsState(null) val picture by remember(room) { room.pictureFlow(viewModel) }.collectAsState(null)
@@ -117,6 +120,12 @@ fun ChatScreen(
} }
} }
LaunchedEffect(messages.size) {
if (messages.isNotEmpty()) {
listState.animateScrollToItem(0)
}
}
Scaffold( Scaffold(
containerColor = MaterialTheme.colorScheme.surfaceContainer, containerColor = MaterialTheme.colorScheme.surfaceContainer,
snackbarHost = { SnackbarHost(snackbarHostState) }, snackbarHost = { SnackbarHost(snackbarHostState) },
@@ -174,7 +183,8 @@ fun ChatScreen(
.weight(1f) .weight(1f)
.fillMaxWidth(), .fillMaxWidth(),
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(16.dp),
reverseLayout = true reverseLayout = true,
state = listState,
) { ) {
groupedMessages.forEach { (dateHeader, messagesInGroup) -> groupedMessages.forEach { (dateHeader, messagesInGroup) ->
items( items(

View File

@@ -14,8 +14,8 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
@@ -23,14 +23,15 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.LoadingIndicator
import androidx.compose.material3.MaterialShapes
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.toShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -39,12 +40,14 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import coop.composeapp.generated.resources.Res import coop.composeapp.generated.resources.Res
import coop.composeapp.generated.resources.ic_arrow_back import coop.composeapp.generated.resources.ic_arrow_back
import coop.composeapp.generated.resources.ic_avatar import coop.composeapp.generated.resources.ic_plus
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
import su.reya.coop.LocalSnackbarHostState import su.reya.coop.LocalSnackbarHostState
@@ -53,7 +56,7 @@ import su.reya.coop.LocalSnackbarHostState
fun NewIdentityScreen( fun NewIdentityScreen(
isLoading: Boolean, isLoading: Boolean,
onBack: () -> Unit, onBack: () -> Unit,
onSave: (name: String, bio: String, picture: Uri?) -> Unit onSave: (name: String, bio: String?, picture: Uri?) -> Unit
) { ) {
val snackbarHostState = LocalSnackbarHostState.current val snackbarHostState = LocalSnackbarHostState.current
var name by remember { mutableStateOf("") } var name by remember { mutableStateOf("") }
@@ -90,25 +93,21 @@ fun NewIdentityScreen(
) )
}, },
content = { innerPadding -> content = { innerPadding ->
Surface( Column(
modifier = Modifier modifier = Modifier.fillMaxSize(),
.fillMaxSize()
.padding(top = innerPadding.calculateTopPadding()),
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp),
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .weight(1f)
.padding(24.dp) .fillMaxWidth()
.verticalScroll(rememberScrollState()), .padding(top = innerPadding.calculateTopPadding()),
horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center,
verticalArrangement = Arrangement.spacedBy(16.dp) horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.size(120.dp) .size(120.dp)
.clip(CircleShape) .clip(MaterialShapes.Pentagon.toShape())
.clickable { launcher.launch("image/*") }, .clickable { launcher.launch("image/*") },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
@@ -127,7 +126,7 @@ fun NewIdentityScreen(
) { ) {
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
Icon( Icon(
painter = painterResource(Res.drawable.ic_avatar), painter = painterResource(Res.drawable.ic_plus),
contentDescription = "Pick avatar", contentDescription = "Pick avatar",
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant tint = MaterialTheme.colorScheme.onSurfaceVariant
@@ -136,21 +135,87 @@ fun NewIdentityScreen(
} }
} }
} }
OutlinedTextField( }
Surface(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp),
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "What others should call you?",
style = MaterialTheme.typography.titleLargeEmphasized.copy(
fontWeight = FontWeight.SemiBold,
),
)
BasicTextField(
value = name, value = name,
onValueChange = { name = it }, onValueChange = { name = it },
label = { Text("Name") },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true, maxLines = 1,
textStyle = MaterialTheme.typography.headlineLargeEmphasized.copy(
color = MaterialTheme.colorScheme.primaryFixed,
fontWeight = FontWeight.SemiBold,
),
cursorBrush = SolidColor(MaterialTheme.colorScheme.secondary),
decorationBox = { innerTextField ->
Box(contentAlignment = Alignment.CenterStart) {
if (name.isEmpty()) {
Text(
"Alice",
style = MaterialTheme.typography.headlineLargeEmphasized.copy(
fontWeight = FontWeight.SemiBold,
),
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(
alpha = 0.5f
) )
OutlinedTextField( )
}
innerTextField()
}
}
)
Spacer(modifier = Modifier.size(8.dp))
Text(
text = "Your bio (optional)",
style = MaterialTheme.typography.titleLargeEmphasized.copy(
fontWeight = FontWeight.SemiBold,
),
)
BasicTextField(
value = bio, value = bio,
onValueChange = { bio = it }, onValueChange = { bio = it },
label = { Text("Bio:") }, modifier = Modifier.fillMaxWidth(),
modifier = Modifier maxLines = 3,
.fillMaxWidth() textStyle = MaterialTheme.typography.bodyLarge.copy(
.height(150.dp), color = MaterialTheme.colorScheme.primaryFixed,
minLines = 3, fontWeight = FontWeight.SemiBold,
),
cursorBrush = SolidColor(MaterialTheme.colorScheme.secondary),
decorationBox = { innerTextField ->
Box(contentAlignment = Alignment.CenterStart) {
if (bio.isEmpty()) {
Text(
"I love cat",
style = MaterialTheme.typography.headlineLargeEmphasized.copy(
fontWeight = FontWeight.SemiBold,
),
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(
alpha = 0.5f
)
)
}
innerTextField()
}
}
) )
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Button( Button(
@@ -166,7 +231,7 @@ fun NewIdentityScreen(
LoadingIndicator() LoadingIndicator()
} else { } else {
Text( Text(
text = "Save & Continue", text = "Continue",
style = MaterialTheme.typography.titleLargeEmphasized, style = MaterialTheme.typography.titleLargeEmphasized,
) )
} }
@@ -174,5 +239,6 @@ fun NewIdentityScreen(
} }
} }
} }
}
) )
} }

View File

@@ -95,7 +95,13 @@ class Nostr {
.websocketTransport(CoopWebSocketClient(httpClient)) .websocketTransport(CoopWebSocketClient(httpClient))
.database(lmdb) .database(lmdb)
.gossip(gossip) .gossip(gossip)
.gossipConfig(GossipConfig().noBackgroundRefresh()) .gossipConfig(
GossipConfig()
.noBackgroundRefresh()
.fetchTimeout(Duration.parse("2s"))
.syncIdleTimeout(Duration.parse("100ms"))
.syncInitialTimeout(Duration.parse("100ms"))
)
.verifySubscriptions(false) .verifySubscriptions(false)
.automaticAuthentication(true) .automaticAuthentication(true)
.sleepWhenIdle(SleepWhenIdle.Enabled(idleTimeout)) .sleepWhenIdle(SleepWhenIdle.Enabled(idleTimeout))
@@ -481,7 +487,7 @@ class Nostr {
return msgRelayList return msgRelayList
} }
suspend fun createIdentity(keys: Keys, name: String, bio: String, picture: String?) { suspend fun createIdentity(keys: Keys, name: String, bio: String?, picture: String?) {
// Send relay list event // Send relay list event
val relayList = getDefaultRelayList() val relayList = getDefaultRelayList()
val relayListEvent = EventBuilder.relayList(relayList).signWithKeys(keys); val relayListEvent = EventBuilder.relayList(relayList).signWithKeys(keys);
@@ -505,7 +511,7 @@ class Nostr {
// Send metadata event // Send metadata event
val metadata = val metadata =
Metadata.fromRecord(MetadataRecord(name = name, about = bio, picture = picture)) Metadata.fromRecord(MetadataRecord(displayName = name, about = bio, picture = picture))
val metadataEvent = EventBuilder.metadata(metadata).signWithKeys(keys) val metadataEvent = EventBuilder.metadata(metadata).signWithKeys(keys)
client?.sendEvent( client?.sendEvent(
@@ -608,7 +614,7 @@ class Nostr {
} }
} }
return roomsMap.values.toSet() return roomsMap.values.sortedByDescending { it.createdAt.asSecs() }.toSet()
} catch (e: Exception) { } catch (e: Exception) {
println("Failed to get chat rooms: ${e.message}") println("Failed to get chat rooms: ${e.message}")
return null return null

View File

@@ -244,9 +244,9 @@ class NostrViewModel(
fun createIdentity( fun createIdentity(
name: String, name: String,
bio: String, bio: String?,
picture: ByteArray?, picture: ByteArray?,
contentType: String? contentType: String? = null
) { ) {
viewModelScope.launch { viewModelScope.launch {
try { try {
@@ -282,7 +282,7 @@ class NostrViewModel(
} }
// Create identity // Create identity
nostr.createIdentity(keys = keys, name = name, bio = bio, picture = avatarUrl) nostr.createIdentity(keys = keys, name = name, bio, picture = avatarUrl)
// Save secret to the secret storage // Save secret to the secret storage
secretStore.set("user_signer", secret) secretStore.set("user_signer", secret)