feat: add some basic interactive animations #46
@@ -86,6 +86,7 @@ import coop.composeapp.generated.resources.ic_new_chat
|
|||||||
import coop.composeapp.generated.resources.ic_qr
|
import coop.composeapp.generated.resources.ic_qr
|
||||||
import coop.composeapp.generated.resources.ic_request
|
import coop.composeapp.generated.resources.ic_request
|
||||||
import coop.composeapp.generated.resources.ic_scanner
|
import coop.composeapp.generated.resources.ic_scanner
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jetbrains.compose.resources.painterResource
|
import org.jetbrains.compose.resources.painterResource
|
||||||
import rust.nostr.sdk.PublicKey
|
import rust.nostr.sdk.PublicKey
|
||||||
@@ -372,6 +373,9 @@ fun HomeScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
items(ongoing, key = { it.id }) { room ->
|
items(ongoing, key = { it.id }) { room ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.animateItem()
|
||||||
|
) {
|
||||||
ChatRoom(
|
ChatRoom(
|
||||||
room = room,
|
room = room,
|
||||||
onClick = { navigator.navigate(Screen.Chat(room.id)) }
|
onClick = { navigator.navigate(Screen.Chat(room.id)) }
|
||||||
@@ -382,6 +386,7 @@ fun HomeScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -620,13 +625,16 @@ fun NewRequests(requests: List<Room>) {
|
|||||||
val firstRoom = requests.getOrNull(0)
|
val firstRoom = requests.getOrNull(0)
|
||||||
val secondRoom = requests.getOrNull(1)
|
val secondRoom = requests.getOrNull(1)
|
||||||
|
|
||||||
val firstRoomState by (firstRoom as Room).uiStateFlow(profileCache)
|
val firstRoomState by remember(firstRoom?.id) {
|
||||||
.collectAsStateWithLifecycle(RoomUiState())
|
firstRoom?.uiStateFlow(profileCache) ?: flowOf(RoomUiState())
|
||||||
val secondRoomState by (secondRoom ?: firstRoom).uiStateFlow(profileCache)
|
}.collectAsStateWithLifecycle(RoomUiState())
|
||||||
.collectAsStateWithLifecycle(RoomUiState())
|
|
||||||
|
val secondRoomState by remember(secondRoom?.id) {
|
||||||
|
(secondRoom ?: firstRoom)?.uiStateFlow(profileCache) ?: flowOf(RoomUiState())
|
||||||
|
}.collectAsStateWithLifecycle(RoomUiState())
|
||||||
|
|
||||||
val supportingText = when {
|
val supportingText = when {
|
||||||
total == 1 -> {
|
total == 1 && firstRoom != null -> {
|
||||||
val message = firstRoom.lastMessage ?: ""
|
val message = firstRoom.lastMessage ?: ""
|
||||||
"${firstRoomState.name}: $message"
|
"${firstRoomState.name}: $message"
|
||||||
}
|
}
|
||||||
@@ -695,7 +703,8 @@ fun NewRequests(requests: List<Room>) {
|
|||||||
@Composable
|
@Composable
|
||||||
fun ChatRoom(room: Room, onClick: () -> Unit) {
|
fun ChatRoom(room: Room, onClick: () -> Unit) {
|
||||||
val profileCache = LocalProfileCache.current
|
val profileCache = LocalProfileCache.current
|
||||||
val roomState by room.uiStateFlow(profileCache).collectAsStateWithLifecycle(RoomUiState())
|
val roomState by remember(room.id) { room.uiStateFlow(profileCache) }
|
||||||
|
.collectAsStateWithLifecycle(RoomUiState())
|
||||||
|
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable(onClick = onClick),
|
modifier = Modifier.clickable(onClick = onClick),
|
||||||
|
|||||||
@@ -185,8 +185,8 @@ fun NewChatScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
text = { Text("Next") },
|
text = { Text("Next") },
|
||||||
containerColor = MaterialTheme.colorScheme.tertiary,
|
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
|
||||||
contentColor = MaterialTheme.colorScheme.onTertiary,
|
contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,8 +146,9 @@ fun ChatScreen(
|
|||||||
val groupedMessages =
|
val groupedMessages =
|
||||||
remember { derivedStateOf { messages.groupBy { it.createdAt().formatAsGroup() } } }
|
remember { derivedStateOf { messages.groupBy { it.createdAt().formatAsGroup() } } }
|
||||||
|
|
||||||
val roomState by (room as Room).uiStateFlow(profileCache, currentUser?.publicKey)
|
val roomState by remember(id, currentUser?.publicKey) {
|
||||||
.collectAsStateWithLifecycle(RoomUiState())
|
(room as Room).uiStateFlow(profileCache, currentUser?.publicKey)
|
||||||
|
}.collectAsStateWithLifecycle(RoomUiState())
|
||||||
|
|
||||||
var text by remember { mutableStateOf("") }
|
var text by remember { mutableStateOf("") }
|
||||||
var selectedMessage by remember { mutableStateOf<Pair<MessageModel, Rect>?>(null) }
|
var selectedMessage by remember { mutableStateOf<Pair<MessageModel, Rect>?>(null) }
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
@@ -21,6 +22,7 @@ import rust.nostr.sdk.PublicKey
|
|||||||
import rust.nostr.sdk.Tag
|
import rust.nostr.sdk.Tag
|
||||||
import rust.nostr.sdk.UnsignedEvent
|
import rust.nostr.sdk.UnsignedEvent
|
||||||
import su.reya.coop.Room
|
import su.reya.coop.Room
|
||||||
|
import su.reya.coop.RoomKind
|
||||||
import su.reya.coop.nostr.Nostr
|
import su.reya.coop.nostr.Nostr
|
||||||
import su.reya.coop.roomId
|
import su.reya.coop.roomId
|
||||||
import su.reya.coop.viewmodel.ErrorHost
|
import su.reya.coop.viewmodel.ErrorHost
|
||||||
@@ -38,11 +40,7 @@ class ChatRepository(
|
|||||||
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
|
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||||
) : ErrorHost by createErrorHost() {
|
) : ErrorHost by createErrorHost() {
|
||||||
private val _state = MutableStateFlow(ChatState())
|
private val _state = MutableStateFlow(ChatState())
|
||||||
val state = _state.stateIn(
|
val state = _state.asStateFlow()
|
||||||
scope,
|
|
||||||
SharingStarted.WhileSubscribed(5000),
|
|
||||||
ChatState()
|
|
||||||
)
|
|
||||||
|
|
||||||
private val _newEvents = MutableSharedFlow<UnsignedEvent>(
|
private val _newEvents = MutableSharedFlow<UnsignedEvent>(
|
||||||
replay = 0,
|
replay = 0,
|
||||||
@@ -134,10 +132,19 @@ class ChatRepository(
|
|||||||
fun refreshChatRooms() {
|
fun refreshChatRooms() {
|
||||||
scope.launch(defaultDispatcher) {
|
scope.launch(defaultDispatcher) {
|
||||||
try {
|
try {
|
||||||
val rooms = nostr.messages.getChatRooms() ?: emptySet()
|
val dbRooms = nostr.messages.getChatRooms() ?: emptySet()
|
||||||
_state.update { currentState ->
|
_state.update { currentState ->
|
||||||
val newMap = currentState.rooms.toMutableMap()
|
val newMap = currentState.rooms.toMutableMap()
|
||||||
rooms.forEach { room -> newMap[room.id] = room }
|
dbRooms.forEach { dbRoom ->
|
||||||
|
val existing = newMap[dbRoom.id]
|
||||||
|
// Only update if the database version is newer or equal
|
||||||
|
if (existing == null || dbRoom.createdAt.asSecs() >= existing.createdAt.asSecs()) {
|
||||||
|
// Preserve Ongoing kind if already marked as such in memory
|
||||||
|
val mergedKind =
|
||||||
|
if (existing?.kind == RoomKind.Ongoing) RoomKind.Ongoing else dbRoom.kind
|
||||||
|
newMap[dbRoom.id] = dbRoom.copy(kind = mergedKind)
|
||||||
|
}
|
||||||
|
}
|
||||||
currentState.copy(rooms = newMap)
|
currentState.copy(rooms = newMap)
|
||||||
}
|
}
|
||||||
} catch (e: CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
@@ -223,16 +230,24 @@ class ChatRepository(
|
|||||||
val rooms = currentState.rooms.toMutableMap()
|
val rooms = currentState.rooms.toMutableMap()
|
||||||
val existingRoom = rooms[roomId]
|
val existingRoom = rooms[roomId]
|
||||||
|
|
||||||
|
val isFromMe = event.author() == currentUser
|
||||||
|
val newKind =
|
||||||
|
if (isFromMe) RoomKind.Ongoing else (existingRoom?.kind ?: RoomKind.Request)
|
||||||
|
|
||||||
if (existingRoom == null) {
|
if (existingRoom == null) {
|
||||||
// New room discovery
|
// New room discovery
|
||||||
val newRoom = Room.new(event, currentUser, roomId)
|
val newRoom = Room.new(event, currentUser, roomId).copy(kind = newKind)
|
||||||
rooms[newRoom.id] = newRoom
|
rooms[newRoom.id] = newRoom
|
||||||
} else if (event.createdAt().asSecs() >= existingRoom.createdAt.asSecs()) {
|
} else if (event.createdAt().asSecs() >= existingRoom.createdAt.asSecs()) {
|
||||||
// Only update preview if message is newer (handles sync/late arrivals)
|
// Only update preview if message is newer (handles sync/late arrivals)
|
||||||
rooms[roomId] = existingRoom.copy(
|
rooms[roomId] = existingRoom.copy(
|
||||||
lastMessage = event.content(),
|
lastMessage = event.content(),
|
||||||
createdAt = event.createdAt()
|
createdAt = event.createdAt(),
|
||||||
|
kind = newKind
|
||||||
)
|
)
|
||||||
|
} else if (isFromMe && existingRoom.kind != RoomKind.Ongoing) {
|
||||||
|
// Even if it's an older message, if it's from me, the room is ongoing
|
||||||
|
rooms[roomId] = existingRoom.copy(kind = RoomKind.Ongoing)
|
||||||
} else {
|
} else {
|
||||||
// Don't update the room list state for older messages
|
// Don't update the room list state for older messages
|
||||||
return@update currentState
|
return@update currentState
|
||||||
|
|||||||
Reference in New Issue
Block a user