add chat viewmodel

This commit is contained in:
2026-07-02 15:09:01 +07:00
parent e5f3078c9c
commit 830375f562
9 changed files with 305 additions and 259 deletions

View File

@@ -52,6 +52,10 @@ val LocalNostrViewModel = staticCompositionLocalOf<NostrViewModel> {
error("No NostrViewModel provided")
}
val LocalChatViewModel = staticCompositionLocalOf<ChatViewModel> {
error("No ChatViewModel provided")
}
val LocalAuthViewModel = staticCompositionLocalOf<AuthViewModel> {
error("No AuthViewModel provided")
}
@@ -72,6 +76,7 @@ val LocalScanResult = staticCompositionLocalOf<QrScanResult> {
@Composable
fun App(
nostrViewModel: NostrViewModel,
chatViewModel: ChatViewModel,
authViewModel: AuthViewModel,
) {
val context = LocalContext.current
@@ -153,6 +158,7 @@ fun App(
) {
CompositionLocalProvider(
LocalNostrViewModel provides nostrViewModel,
LocalChatViewModel provides chatViewModel,
LocalAuthViewModel provides authViewModel,
LocalSnackbarHostState provides snackbarHostState,
LocalNavigator provides navigator,

View File

@@ -28,12 +28,15 @@ class MainActivity : ComponentActivity() {
private val secretStore = SecretStore(this@MainActivity)
private val nostrViewModel =
NostrViewModel(NostrManager.instance)
private val chatViewModel =
ChatViewModel(NostrManager.instance)
private val authViewModel =
AuthViewModel(NostrManager.instance, secretStore, androidSigner)
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return when {
modelClass.isAssignableFrom(NostrViewModel::class.java) -> nostrViewModel
modelClass.isAssignableFrom(ChatViewModel::class.java) -> chatViewModel
modelClass.isAssignableFrom(AuthViewModel::class.java) -> authViewModel
else -> throw IllegalArgumentException("Unknown ViewModel class")
} as T
@@ -42,6 +45,7 @@ class MainActivity : ComponentActivity() {
}
private val nostrViewModel: NostrViewModel by viewModels { factory }
private val chatViewModel: ChatViewModel by viewModels { factory }
private val authViewModel: AuthViewModel by viewModels { factory }
override fun onCreate(savedInstanceState: Bundle?) {
@@ -87,6 +91,7 @@ class MainActivity : ComponentActivity() {
setContent {
App(
nostrViewModel = nostrViewModel,
chatViewModel = chatViewModel,
authViewModel = authViewModel,
)
}

View File

@@ -76,6 +76,7 @@ import org.jetbrains.compose.resources.painterResource
import rust.nostr.sdk.PublicKey
import rust.nostr.sdk.Timestamp
import rust.nostr.sdk.UnsignedEvent
import su.reya.coop.LocalChatViewModel
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalSnackbarHostState
@@ -96,6 +97,7 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
val snackbarHostState = LocalSnackbarHostState.current
val navigator = LocalNavigator.current
val nostrViewModel = LocalNostrViewModel.current
val chatViewModel = LocalChatViewModel.current
val scope = rememberCoroutineScope()
val listState = rememberLazyListState()
@@ -103,7 +105,7 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
val currentUser by nostrViewModel.currentUserProfile.collectAsStateWithLifecycle()
// Get chat room by ID
val chatRooms by nostrViewModel.chatRooms.collectAsStateWithLifecycle()
val chatRooms by chatViewModel.chatRooms.collectAsStateWithLifecycle()
val room by remember(id) { derivedStateOf { chatRooms.firstOrNull { it.id == id } } }
// Show empty screen
@@ -144,7 +146,7 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
val type = context.contentResolver.getType(uri)
// Send message
nostrViewModel.sendFileMessage(id, file, type)
chatViewModel.sendFileMessage(id, file, type)
} catch (e: Exception) {
snackbarHostState.showSnackbar("Error: ${e.message}")
}
@@ -157,7 +159,7 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
LaunchedEffect(id) {
// Get messages
val initialMessages = nostrViewModel.getChatRoomMessages(id)
val initialMessages = chatViewModel.getChatRoomMessages(id)
messages.clear()
messages.addAll(initialMessages)
@@ -165,10 +167,10 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
loading = false
// Get msg relays for each member
nostrViewModel.chatRoomConnect(id)
chatViewModel.chatRoomConnect(id)
// Handle new messages
nostrViewModel.newEvents.collect { event ->
chatViewModel.newEvents.collect { event ->
if (event.roomId() == id) {
if (event.id() !in messages.map { it.id() }) {
messages.add(0, event)
@@ -350,7 +352,7 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
value = text,
onValueChange = { text = it },
onSend = {
nostrViewModel.sendMessage(id, text)
chatViewModel.sendMessage(id, text)
text = ""
},
onUpload = {

View File

@@ -91,6 +91,7 @@ import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.painterResource
import rust.nostr.sdk.PublicKey
import su.reya.coop.LocalAuthViewModel
import su.reya.coop.LocalChatViewModel
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalScanResult
@@ -112,6 +113,7 @@ fun HomeScreen() {
val snackbarHostState = LocalSnackbarHostState.current
val clipboardManager = LocalClipboard.current
val nostrViewModel = LocalNostrViewModel.current
val chatViewModel = LocalChatViewModel.current
val authViewModel = LocalAuthViewModel.current
val scope = rememberCoroutineScope()
@@ -120,11 +122,11 @@ fun HomeScreen() {
val pullToRefreshState = rememberPullToRefreshState()
val userProfile by nostrViewModel.currentUserProfile.collectAsStateWithLifecycle()
val chatRooms by nostrViewModel.chatRooms.collectAsStateWithLifecycle()
val chatRooms by chatViewModel.chatRooms.collectAsStateWithLifecycle()
val isRelayListEmpty by nostrViewModel.isRelayListEmpty.collectAsStateWithLifecycle()
val isSyncing by nostrViewModel.isSyncing.collectAsStateWithLifecycle()
val isPartialProcessedGiftWrap by nostrViewModel.isPartialProcessedGiftWrap.collectAsStateWithLifecycle()
val isSyncing by chatViewModel.isSyncing.collectAsStateWithLifecycle()
val isPartialProcessedGiftWrap by chatViewModel.isPartialProcessedGiftWrap.collectAsStateWithLifecycle()
val authState by authViewModel.state.collectAsStateWithLifecycle()
val isBannerDismissed = authState.isNotificationBannerDismissed
@@ -155,7 +157,7 @@ fun HomeScreen() {
}
LaunchedEffect(Unit) {
nostrViewModel.getChatRooms()
chatViewModel.refreshChatRooms()
}
LaunchedEffect(qrScanResult.content) {
@@ -163,7 +165,7 @@ fun HomeScreen() {
runCatching { PublicKey.parse(result) }
.onSuccess { pubkey ->
try {
val roomId = nostrViewModel.createChatRoom(listOf(pubkey))
val roomId = chatViewModel.createChatRoom(listOf(pubkey))
navigator.navigate(Screen.Chat(roomId))
} catch (e: Exception) {
e.message?.let { snackbarHostState.showSnackbar(it) }
@@ -320,7 +322,7 @@ fun HomeScreen() {
onRefresh = {
scope.launch {
isRefreshing = true
nostrViewModel.refreshChatRooms()
chatViewModel.refreshChatRooms()
isRefreshing = false
}
},
@@ -746,6 +748,7 @@ fun BottomMenuList(
) {
val navigator = LocalNavigator.current
val nostrViewModel = LocalNostrViewModel.current
val chatViewModel = LocalChatViewModel.current
val authViewModel = LocalAuthViewModel.current
val defaultMenuList = listOf(
@@ -776,7 +779,14 @@ fun BottomMenuList(
}
Spacer(modifier = Modifier.size(16.dp))
FilledTonalButton(
onClick = { onDismiss { authViewModel.logout(onLogout = nostrViewModel::resetInternalState) } },
onClick = {
onDismiss {
authViewModel.logout(onLogout = {
nostrViewModel.resetInternalState()
chatViewModel.resetInternalState()
})
}
},
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = MaterialTheme.colorScheme.error,
contentColor = MaterialTheme.colorScheme.onError

View File

@@ -55,6 +55,7 @@ import coop.composeapp.generated.resources.ic_scanner
import kotlinx.coroutines.delay
import org.jetbrains.compose.resources.painterResource
import rust.nostr.sdk.PublicKey
import su.reya.coop.LocalChatViewModel
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalScanResult
@@ -71,6 +72,7 @@ fun NewChatScreen() {
val navigator = LocalNavigator.current
val qrScanResult = LocalScanResult.current
val nostrViewModel = LocalNostrViewModel.current
val chatViewModel = LocalChatViewModel.current
val contactList by nostrViewModel.contactList.collectAsStateWithLifecycle()
var query by remember { mutableStateOf("") }
@@ -169,7 +171,7 @@ fun NewChatScreen() {
) {
ExtendedFloatingActionButton(
onClick = {
val roomId = nostrViewModel.createChatRoom(selectedReceivers.toList())
val roomId = chatViewModel.createChatRoom(selectedReceivers.toList())
navigator.navigate(Screen.Chat(roomId))
},
expanded = false,
@@ -260,7 +262,7 @@ fun NewChatScreen() {
items = searchResults,
selectedReceivers = selectedReceivers,
onContactClick = { pubkey ->
val roomId = nostrViewModel.createChatRoom(listOf(pubkey))
val roomId = chatViewModel.createChatRoom(listOf(pubkey))
navigator.navigate(Screen.Chat(roomId))
},
)
@@ -271,7 +273,7 @@ fun NewChatScreen() {
items = contactList.toList(),
selectedReceivers = selectedReceivers,
onContactClick = { pubkey ->
val roomId = nostrViewModel.createChatRoom(listOf(pubkey))
val roomId = chatViewModel.createChatRoom(listOf(pubkey))
navigator.navigate(Screen.Chat(roomId))
}
)

View File

@@ -44,6 +44,7 @@ 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.LocalChatViewModel
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalSnackbarHostState
@@ -55,19 +56,20 @@ import su.reya.coop.short
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ProfileScreen(pubkey: String) {
val pubkey = runCatching { PublicKey.parse(pubkey) }.getOrNull() ?: return
val context = LocalContext.current
val snackbarHostState = LocalSnackbarHostState.current
val navigator = LocalNavigator.current
val nostrViewModel = LocalNostrViewModel.current
val chatViewModel = LocalChatViewModel.current
val scope = rememberCoroutineScope()
val pubkeyObj = runCatching { PublicKey.parse(pubkey) }.getOrNull() ?: return
val profileFlow = remember(pubkeyObj) { nostrViewModel.getMetadata(pubkeyObj) }
val profileFlow = remember(pubkey) { nostrViewModel.getMetadata(pubkey) }
val profile by profileFlow.collectAsStateWithLifecycle()
val metadata = profile?.metadata?.asRecord()
val nip05 = metadata?.nip05 ?: pubkeyObj.short()
val nip05 = metadata?.nip05 ?: pubkey.short()
val picture = metadata?.picture
val details = remember(profile) {
@@ -159,7 +161,7 @@ fun ProfileScreen(pubkey: String) {
scope.launch {
try {
val roomId =
nostrViewModel.createChatRoom(listOf(pubkeyObj))
chatViewModel.createChatRoom(listOf(pubkey))
navigator.navigate(Screen.Chat(roomId))
} catch (e: Exception) {
e.message?.let { snackbarHostState.showSnackbar(it) }
@@ -189,7 +191,7 @@ fun ProfileScreen(pubkey: String) {
onClick = {
val sendIntent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, pubkeyObj.toBech32())
putExtra(Intent.EXTRA_TEXT, pubkey.toBech32())
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)

View File

@@ -37,6 +37,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coop.composeapp.generated.resources.Res
import coop.composeapp.generated.resources.ic_arrow_back
import kotlinx.coroutines.launch
import su.reya.coop.LocalChatViewModel
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalSnackbarHostState
@@ -49,13 +50,14 @@ fun RequestListScreen() {
val navigator = LocalNavigator.current
val snackbarHostState = LocalSnackbarHostState.current
val nostrViewModel = LocalNostrViewModel.current
val chatViewModel = LocalChatViewModel.current
val scope = rememberCoroutineScope()
val listState = rememberLazyListState()
val pullToRefreshState = rememberPullToRefreshState()
var isRefreshing by remember { mutableStateOf(false) }
val chatRooms by nostrViewModel.chatRooms.collectAsStateWithLifecycle()
val chatRooms by chatViewModel.chatRooms.collectAsStateWithLifecycle()
// Get all request rooms
val requests = remember(chatRooms) {
@@ -103,7 +105,7 @@ fun RequestListScreen() {
onRefresh = {
scope.launch {
isRefreshing = true
nostrViewModel.refreshChatRooms()
chatViewModel.refreshChatRooms()
isRefreshing = false
}
},