add chat viewmodel
This commit is contained in:
@@ -52,6 +52,10 @@ val LocalNostrViewModel = staticCompositionLocalOf<NostrViewModel> {
|
|||||||
error("No NostrViewModel provided")
|
error("No NostrViewModel provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val LocalChatViewModel = staticCompositionLocalOf<ChatViewModel> {
|
||||||
|
error("No ChatViewModel provided")
|
||||||
|
}
|
||||||
|
|
||||||
val LocalAuthViewModel = staticCompositionLocalOf<AuthViewModel> {
|
val LocalAuthViewModel = staticCompositionLocalOf<AuthViewModel> {
|
||||||
error("No AuthViewModel provided")
|
error("No AuthViewModel provided")
|
||||||
}
|
}
|
||||||
@@ -72,6 +76,7 @@ val LocalScanResult = staticCompositionLocalOf<QrScanResult> {
|
|||||||
@Composable
|
@Composable
|
||||||
fun App(
|
fun App(
|
||||||
nostrViewModel: NostrViewModel,
|
nostrViewModel: NostrViewModel,
|
||||||
|
chatViewModel: ChatViewModel,
|
||||||
authViewModel: AuthViewModel,
|
authViewModel: AuthViewModel,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -153,6 +158,7 @@ fun App(
|
|||||||
) {
|
) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalNostrViewModel provides nostrViewModel,
|
LocalNostrViewModel provides nostrViewModel,
|
||||||
|
LocalChatViewModel provides chatViewModel,
|
||||||
LocalAuthViewModel provides authViewModel,
|
LocalAuthViewModel provides authViewModel,
|
||||||
LocalSnackbarHostState provides snackbarHostState,
|
LocalSnackbarHostState provides snackbarHostState,
|
||||||
LocalNavigator provides navigator,
|
LocalNavigator provides navigator,
|
||||||
|
|||||||
@@ -28,12 +28,15 @@ class MainActivity : ComponentActivity() {
|
|||||||
private val secretStore = SecretStore(this@MainActivity)
|
private val secretStore = SecretStore(this@MainActivity)
|
||||||
private val nostrViewModel =
|
private val nostrViewModel =
|
||||||
NostrViewModel(NostrManager.instance)
|
NostrViewModel(NostrManager.instance)
|
||||||
|
private val chatViewModel =
|
||||||
|
ChatViewModel(NostrManager.instance)
|
||||||
private val authViewModel =
|
private val authViewModel =
|
||||||
AuthViewModel(NostrManager.instance, secretStore, androidSigner)
|
AuthViewModel(NostrManager.instance, secretStore, androidSigner)
|
||||||
|
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
return when {
|
return when {
|
||||||
modelClass.isAssignableFrom(NostrViewModel::class.java) -> nostrViewModel
|
modelClass.isAssignableFrom(NostrViewModel::class.java) -> nostrViewModel
|
||||||
|
modelClass.isAssignableFrom(ChatViewModel::class.java) -> chatViewModel
|
||||||
modelClass.isAssignableFrom(AuthViewModel::class.java) -> authViewModel
|
modelClass.isAssignableFrom(AuthViewModel::class.java) -> authViewModel
|
||||||
else -> throw IllegalArgumentException("Unknown ViewModel class")
|
else -> throw IllegalArgumentException("Unknown ViewModel class")
|
||||||
} as T
|
} as T
|
||||||
@@ -42,6 +45,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val nostrViewModel: NostrViewModel by viewModels { factory }
|
private val nostrViewModel: NostrViewModel by viewModels { factory }
|
||||||
|
private val chatViewModel: ChatViewModel by viewModels { factory }
|
||||||
private val authViewModel: AuthViewModel by viewModels { factory }
|
private val authViewModel: AuthViewModel by viewModels { factory }
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -87,6 +91,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
setContent {
|
setContent {
|
||||||
App(
|
App(
|
||||||
nostrViewModel = nostrViewModel,
|
nostrViewModel = nostrViewModel,
|
||||||
|
chatViewModel = chatViewModel,
|
||||||
authViewModel = authViewModel,
|
authViewModel = authViewModel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ import org.jetbrains.compose.resources.painterResource
|
|||||||
import rust.nostr.sdk.PublicKey
|
import rust.nostr.sdk.PublicKey
|
||||||
import rust.nostr.sdk.Timestamp
|
import rust.nostr.sdk.Timestamp
|
||||||
import rust.nostr.sdk.UnsignedEvent
|
import rust.nostr.sdk.UnsignedEvent
|
||||||
|
import su.reya.coop.LocalChatViewModel
|
||||||
import su.reya.coop.LocalNavigator
|
import su.reya.coop.LocalNavigator
|
||||||
import su.reya.coop.LocalNostrViewModel
|
import su.reya.coop.LocalNostrViewModel
|
||||||
import su.reya.coop.LocalSnackbarHostState
|
import su.reya.coop.LocalSnackbarHostState
|
||||||
@@ -96,6 +97,7 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
|
|||||||
val snackbarHostState = LocalSnackbarHostState.current
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
val navigator = LocalNavigator.current
|
val navigator = LocalNavigator.current
|
||||||
val nostrViewModel = LocalNostrViewModel.current
|
val nostrViewModel = LocalNostrViewModel.current
|
||||||
|
val chatViewModel = LocalChatViewModel.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
@@ -103,7 +105,7 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
|
|||||||
val currentUser by nostrViewModel.currentUserProfile.collectAsStateWithLifecycle()
|
val currentUser by nostrViewModel.currentUserProfile.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
// Get chat room by ID
|
// 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 } } }
|
val room by remember(id) { derivedStateOf { chatRooms.firstOrNull { it.id == id } } }
|
||||||
|
|
||||||
// Show empty screen
|
// Show empty screen
|
||||||
@@ -144,7 +146,7 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
|
|||||||
val type = context.contentResolver.getType(uri)
|
val type = context.contentResolver.getType(uri)
|
||||||
|
|
||||||
// Send message
|
// Send message
|
||||||
nostrViewModel.sendFileMessage(id, file, type)
|
chatViewModel.sendFileMessage(id, file, type)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
snackbarHostState.showSnackbar("Error: ${e.message}")
|
snackbarHostState.showSnackbar("Error: ${e.message}")
|
||||||
}
|
}
|
||||||
@@ -157,7 +159,7 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
|
|||||||
|
|
||||||
LaunchedEffect(id) {
|
LaunchedEffect(id) {
|
||||||
// Get messages
|
// Get messages
|
||||||
val initialMessages = nostrViewModel.getChatRoomMessages(id)
|
val initialMessages = chatViewModel.getChatRoomMessages(id)
|
||||||
messages.clear()
|
messages.clear()
|
||||||
messages.addAll(initialMessages)
|
messages.addAll(initialMessages)
|
||||||
|
|
||||||
@@ -165,10 +167,10 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
|
|||||||
loading = false
|
loading = false
|
||||||
|
|
||||||
// Get msg relays for each member
|
// Get msg relays for each member
|
||||||
nostrViewModel.chatRoomConnect(id)
|
chatViewModel.chatRoomConnect(id)
|
||||||
|
|
||||||
// Handle new messages
|
// Handle new messages
|
||||||
nostrViewModel.newEvents.collect { event ->
|
chatViewModel.newEvents.collect { event ->
|
||||||
if (event.roomId() == id) {
|
if (event.roomId() == id) {
|
||||||
if (event.id() !in messages.map { it.id() }) {
|
if (event.id() !in messages.map { it.id() }) {
|
||||||
messages.add(0, event)
|
messages.add(0, event)
|
||||||
@@ -350,7 +352,7 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
|
|||||||
value = text,
|
value = text,
|
||||||
onValueChange = { text = it },
|
onValueChange = { text = it },
|
||||||
onSend = {
|
onSend = {
|
||||||
nostrViewModel.sendMessage(id, text)
|
chatViewModel.sendMessage(id, text)
|
||||||
text = ""
|
text = ""
|
||||||
},
|
},
|
||||||
onUpload = {
|
onUpload = {
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ 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
|
||||||
import su.reya.coop.LocalAuthViewModel
|
import su.reya.coop.LocalAuthViewModel
|
||||||
|
import su.reya.coop.LocalChatViewModel
|
||||||
import su.reya.coop.LocalNavigator
|
import su.reya.coop.LocalNavigator
|
||||||
import su.reya.coop.LocalNostrViewModel
|
import su.reya.coop.LocalNostrViewModel
|
||||||
import su.reya.coop.LocalScanResult
|
import su.reya.coop.LocalScanResult
|
||||||
@@ -112,6 +113,7 @@ fun HomeScreen() {
|
|||||||
val snackbarHostState = LocalSnackbarHostState.current
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
val clipboardManager = LocalClipboard.current
|
val clipboardManager = LocalClipboard.current
|
||||||
val nostrViewModel = LocalNostrViewModel.current
|
val nostrViewModel = LocalNostrViewModel.current
|
||||||
|
val chatViewModel = LocalChatViewModel.current
|
||||||
val authViewModel = LocalAuthViewModel.current
|
val authViewModel = LocalAuthViewModel.current
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -120,11 +122,11 @@ fun HomeScreen() {
|
|||||||
val pullToRefreshState = rememberPullToRefreshState()
|
val pullToRefreshState = rememberPullToRefreshState()
|
||||||
|
|
||||||
val userProfile by nostrViewModel.currentUserProfile.collectAsStateWithLifecycle()
|
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 isRelayListEmpty by nostrViewModel.isRelayListEmpty.collectAsStateWithLifecycle()
|
||||||
val isSyncing by nostrViewModel.isSyncing.collectAsStateWithLifecycle()
|
val isSyncing by chatViewModel.isSyncing.collectAsStateWithLifecycle()
|
||||||
val isPartialProcessedGiftWrap by nostrViewModel.isPartialProcessedGiftWrap.collectAsStateWithLifecycle()
|
val isPartialProcessedGiftWrap by chatViewModel.isPartialProcessedGiftWrap.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val authState by authViewModel.state.collectAsStateWithLifecycle()
|
val authState by authViewModel.state.collectAsStateWithLifecycle()
|
||||||
val isBannerDismissed = authState.isNotificationBannerDismissed
|
val isBannerDismissed = authState.isNotificationBannerDismissed
|
||||||
@@ -155,7 +157,7 @@ fun HomeScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
nostrViewModel.getChatRooms()
|
chatViewModel.refreshChatRooms()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(qrScanResult.content) {
|
LaunchedEffect(qrScanResult.content) {
|
||||||
@@ -163,7 +165,7 @@ fun HomeScreen() {
|
|||||||
runCatching { PublicKey.parse(result) }
|
runCatching { PublicKey.parse(result) }
|
||||||
.onSuccess { pubkey ->
|
.onSuccess { pubkey ->
|
||||||
try {
|
try {
|
||||||
val roomId = nostrViewModel.createChatRoom(listOf(pubkey))
|
val roomId = chatViewModel.createChatRoom(listOf(pubkey))
|
||||||
navigator.navigate(Screen.Chat(roomId))
|
navigator.navigate(Screen.Chat(roomId))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.message?.let { snackbarHostState.showSnackbar(it) }
|
e.message?.let { snackbarHostState.showSnackbar(it) }
|
||||||
@@ -320,7 +322,7 @@ fun HomeScreen() {
|
|||||||
onRefresh = {
|
onRefresh = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
isRefreshing = true
|
isRefreshing = true
|
||||||
nostrViewModel.refreshChatRooms()
|
chatViewModel.refreshChatRooms()
|
||||||
isRefreshing = false
|
isRefreshing = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -746,6 +748,7 @@ fun BottomMenuList(
|
|||||||
) {
|
) {
|
||||||
val navigator = LocalNavigator.current
|
val navigator = LocalNavigator.current
|
||||||
val nostrViewModel = LocalNostrViewModel.current
|
val nostrViewModel = LocalNostrViewModel.current
|
||||||
|
val chatViewModel = LocalChatViewModel.current
|
||||||
val authViewModel = LocalAuthViewModel.current
|
val authViewModel = LocalAuthViewModel.current
|
||||||
|
|
||||||
val defaultMenuList = listOf(
|
val defaultMenuList = listOf(
|
||||||
@@ -776,7 +779,14 @@ fun BottomMenuList(
|
|||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.size(16.dp))
|
Spacer(modifier = Modifier.size(16.dp))
|
||||||
FilledTonalButton(
|
FilledTonalButton(
|
||||||
onClick = { onDismiss { authViewModel.logout(onLogout = nostrViewModel::resetInternalState) } },
|
onClick = {
|
||||||
|
onDismiss {
|
||||||
|
authViewModel.logout(onLogout = {
|
||||||
|
nostrViewModel.resetInternalState()
|
||||||
|
chatViewModel.resetInternalState()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
colors = ButtonDefaults.filledTonalButtonColors(
|
colors = ButtonDefaults.filledTonalButtonColors(
|
||||||
containerColor = MaterialTheme.colorScheme.error,
|
containerColor = MaterialTheme.colorScheme.error,
|
||||||
contentColor = MaterialTheme.colorScheme.onError
|
contentColor = MaterialTheme.colorScheme.onError
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import coop.composeapp.generated.resources.ic_scanner
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import org.jetbrains.compose.resources.painterResource
|
import org.jetbrains.compose.resources.painterResource
|
||||||
import rust.nostr.sdk.PublicKey
|
import rust.nostr.sdk.PublicKey
|
||||||
|
import su.reya.coop.LocalChatViewModel
|
||||||
import su.reya.coop.LocalNavigator
|
import su.reya.coop.LocalNavigator
|
||||||
import su.reya.coop.LocalNostrViewModel
|
import su.reya.coop.LocalNostrViewModel
|
||||||
import su.reya.coop.LocalScanResult
|
import su.reya.coop.LocalScanResult
|
||||||
@@ -71,6 +72,7 @@ fun NewChatScreen() {
|
|||||||
val navigator = LocalNavigator.current
|
val navigator = LocalNavigator.current
|
||||||
val qrScanResult = LocalScanResult.current
|
val qrScanResult = LocalScanResult.current
|
||||||
val nostrViewModel = LocalNostrViewModel.current
|
val nostrViewModel = LocalNostrViewModel.current
|
||||||
|
val chatViewModel = LocalChatViewModel.current
|
||||||
|
|
||||||
val contactList by nostrViewModel.contactList.collectAsStateWithLifecycle()
|
val contactList by nostrViewModel.contactList.collectAsStateWithLifecycle()
|
||||||
var query by remember { mutableStateOf("") }
|
var query by remember { mutableStateOf("") }
|
||||||
@@ -169,7 +171,7 @@ fun NewChatScreen() {
|
|||||||
) {
|
) {
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
val roomId = nostrViewModel.createChatRoom(selectedReceivers.toList())
|
val roomId = chatViewModel.createChatRoom(selectedReceivers.toList())
|
||||||
navigator.navigate(Screen.Chat(roomId))
|
navigator.navigate(Screen.Chat(roomId))
|
||||||
},
|
},
|
||||||
expanded = false,
|
expanded = false,
|
||||||
@@ -260,7 +262,7 @@ fun NewChatScreen() {
|
|||||||
items = searchResults,
|
items = searchResults,
|
||||||
selectedReceivers = selectedReceivers,
|
selectedReceivers = selectedReceivers,
|
||||||
onContactClick = { pubkey ->
|
onContactClick = { pubkey ->
|
||||||
val roomId = nostrViewModel.createChatRoom(listOf(pubkey))
|
val roomId = chatViewModel.createChatRoom(listOf(pubkey))
|
||||||
navigator.navigate(Screen.Chat(roomId))
|
navigator.navigate(Screen.Chat(roomId))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -271,7 +273,7 @@ fun NewChatScreen() {
|
|||||||
items = contactList.toList(),
|
items = contactList.toList(),
|
||||||
selectedReceivers = selectedReceivers,
|
selectedReceivers = selectedReceivers,
|
||||||
onContactClick = { pubkey ->
|
onContactClick = { pubkey ->
|
||||||
val roomId = nostrViewModel.createChatRoom(listOf(pubkey))
|
val roomId = chatViewModel.createChatRoom(listOf(pubkey))
|
||||||
navigator.navigate(Screen.Chat(roomId))
|
navigator.navigate(Screen.Chat(roomId))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import coop.composeapp.generated.resources.ic_share
|
|||||||
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
|
||||||
|
import su.reya.coop.LocalChatViewModel
|
||||||
import su.reya.coop.LocalNavigator
|
import su.reya.coop.LocalNavigator
|
||||||
import su.reya.coop.LocalNostrViewModel
|
import su.reya.coop.LocalNostrViewModel
|
||||||
import su.reya.coop.LocalSnackbarHostState
|
import su.reya.coop.LocalSnackbarHostState
|
||||||
@@ -55,19 +56,20 @@ import su.reya.coop.short
|
|||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ProfileScreen(pubkey: String) {
|
fun ProfileScreen(pubkey: String) {
|
||||||
|
val pubkey = runCatching { PublicKey.parse(pubkey) }.getOrNull() ?: return
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val snackbarHostState = LocalSnackbarHostState.current
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
val navigator = LocalNavigator.current
|
val navigator = LocalNavigator.current
|
||||||
val nostrViewModel = LocalNostrViewModel.current
|
val nostrViewModel = LocalNostrViewModel.current
|
||||||
|
val chatViewModel = LocalChatViewModel.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
val pubkeyObj = runCatching { PublicKey.parse(pubkey) }.getOrNull() ?: return
|
val profileFlow = remember(pubkey) { nostrViewModel.getMetadata(pubkey) }
|
||||||
|
|
||||||
val profileFlow = remember(pubkeyObj) { nostrViewModel.getMetadata(pubkeyObj) }
|
|
||||||
val profile by profileFlow.collectAsStateWithLifecycle()
|
val profile by profileFlow.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val metadata = profile?.metadata?.asRecord()
|
val metadata = profile?.metadata?.asRecord()
|
||||||
val nip05 = metadata?.nip05 ?: pubkeyObj.short()
|
val nip05 = metadata?.nip05 ?: pubkey.short()
|
||||||
val picture = metadata?.picture
|
val picture = metadata?.picture
|
||||||
|
|
||||||
val details = remember(profile) {
|
val details = remember(profile) {
|
||||||
@@ -159,7 +161,7 @@ fun ProfileScreen(pubkey: String) {
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
val roomId =
|
val roomId =
|
||||||
nostrViewModel.createChatRoom(listOf(pubkeyObj))
|
chatViewModel.createChatRoom(listOf(pubkey))
|
||||||
navigator.navigate(Screen.Chat(roomId))
|
navigator.navigate(Screen.Chat(roomId))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.message?.let { snackbarHostState.showSnackbar(it) }
|
e.message?.let { snackbarHostState.showSnackbar(it) }
|
||||||
@@ -189,7 +191,7 @@ fun ProfileScreen(pubkey: String) {
|
|||||||
onClick = {
|
onClick = {
|
||||||
val sendIntent = Intent().apply {
|
val sendIntent = Intent().apply {
|
||||||
action = Intent.ACTION_SEND
|
action = Intent.ACTION_SEND
|
||||||
putExtra(Intent.EXTRA_TEXT, pubkeyObj.toBech32())
|
putExtra(Intent.EXTRA_TEXT, pubkey.toBech32())
|
||||||
type = "text/plain"
|
type = "text/plain"
|
||||||
}
|
}
|
||||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
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 kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import su.reya.coop.LocalChatViewModel
|
||||||
import su.reya.coop.LocalNavigator
|
import su.reya.coop.LocalNavigator
|
||||||
import su.reya.coop.LocalNostrViewModel
|
import su.reya.coop.LocalNostrViewModel
|
||||||
import su.reya.coop.LocalSnackbarHostState
|
import su.reya.coop.LocalSnackbarHostState
|
||||||
@@ -49,13 +50,14 @@ fun RequestListScreen() {
|
|||||||
val navigator = LocalNavigator.current
|
val navigator = LocalNavigator.current
|
||||||
val snackbarHostState = LocalSnackbarHostState.current
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
val nostrViewModel = LocalNostrViewModel.current
|
val nostrViewModel = LocalNostrViewModel.current
|
||||||
|
val chatViewModel = LocalChatViewModel.current
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val pullToRefreshState = rememberPullToRefreshState()
|
val pullToRefreshState = rememberPullToRefreshState()
|
||||||
|
|
||||||
var isRefreshing by remember { mutableStateOf(false) }
|
var isRefreshing by remember { mutableStateOf(false) }
|
||||||
val chatRooms by nostrViewModel.chatRooms.collectAsStateWithLifecycle()
|
val chatRooms by chatViewModel.chatRooms.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
// Get all request rooms
|
// Get all request rooms
|
||||||
val requests = remember(chatRooms) {
|
val requests = remember(chatRooms) {
|
||||||
@@ -103,7 +105,7 @@ fun RequestListScreen() {
|
|||||||
onRefresh = {
|
onRefresh = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
isRefreshing = true
|
isRefreshing = true
|
||||||
nostrViewModel.refreshChatRooms()
|
chatViewModel.refreshChatRooms()
|
||||||
isRefreshing = false
|
isRefreshing = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
245
shared/src/commonMain/kotlin/su/reya/coop/ChatViewModel.kt
Normal file
245
shared/src/commonMain/kotlin/su/reya/coop/ChatViewModel.kt
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
package su.reya.coop
|
||||||
|
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import rust.nostr.sdk.EventBuilder
|
||||||
|
import rust.nostr.sdk.EventId
|
||||||
|
import rust.nostr.sdk.Kind
|
||||||
|
import rust.nostr.sdk.KindStandard
|
||||||
|
import rust.nostr.sdk.PublicKey
|
||||||
|
import rust.nostr.sdk.RelayUrl
|
||||||
|
import rust.nostr.sdk.Tag
|
||||||
|
import rust.nostr.sdk.UnsignedEvent
|
||||||
|
import su.reya.coop.nostr.Nostr
|
||||||
|
|
||||||
|
data class ChatState(
|
||||||
|
val isSyncing: Boolean = false,
|
||||||
|
val isPartialProcessedGiftWrap: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
class ChatViewModel(private val nostr: Nostr) : BaseViewModel() {
|
||||||
|
private val _state = MutableStateFlow(ChatState())
|
||||||
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
private val _chatRooms = MutableStateFlow<Set<Room>>(emptySet())
|
||||||
|
val chatRooms = _chatRooms.asStateFlow()
|
||||||
|
|
||||||
|
private val _newEvents = MutableSharedFlow<UnsignedEvent>(extraBufferCapacity = 100)
|
||||||
|
val newEvents = _newEvents.asSharedFlow()
|
||||||
|
|
||||||
|
private val _sentReports = MutableSharedFlow<Map<EventId, List<RelayUrl>>>()
|
||||||
|
val sentReport = _sentReports.asSharedFlow()
|
||||||
|
|
||||||
|
val isSyncing = nostr.messages.messageSyncState.map { it.isSyncing }
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
|
||||||
|
|
||||||
|
val isPartialProcessedGiftWrap = _state.map { it.isPartialProcessedGiftWrap }
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
nostr.waitUntilInitialized()
|
||||||
|
|
||||||
|
// Observe message sync progress
|
||||||
|
launch {
|
||||||
|
nostr.messages.messageSyncState.collect { state ->
|
||||||
|
// When at least some messages are processed, allow UI to show the list
|
||||||
|
if (state.processedCount > 0) {
|
||||||
|
_state.update { it.copy(isPartialProcessedGiftWrap = true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh UI every 10 messages OR when sync is fully done
|
||||||
|
if (state.processedCount % 10 == 0 || !state.isSyncing) {
|
||||||
|
refreshChatRooms()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observe new messages
|
||||||
|
launch {
|
||||||
|
nostr.newEvents.collect { event ->
|
||||||
|
val roomId = event.roomId()
|
||||||
|
val existingRoom = _chatRooms.value.firstOrNull { it.id == roomId }
|
||||||
|
|
||||||
|
if (existingRoom == null) {
|
||||||
|
val currentUser = nostr.signer.getPublicKeyAsync() ?: return@collect
|
||||||
|
val newRoom = Room.new(event, currentUser)
|
||||||
|
_chatRooms.update { (it + newRoom).sortedDescending().toSet() }
|
||||||
|
} else {
|
||||||
|
updateRoomList(roomId, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
_newEvents.emit(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial load of rooms
|
||||||
|
refreshChatRooms()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createChatRoom(to: List<PublicKey>): Long {
|
||||||
|
try {
|
||||||
|
if (to.isEmpty()) {
|
||||||
|
throw IllegalArgumentException("At least one recipient is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current user
|
||||||
|
val currentUser = nostr.signer.publicKeyFlow.value
|
||||||
|
?: throw IllegalStateException("User not signed in")
|
||||||
|
|
||||||
|
// Construct the rumor event
|
||||||
|
val rumor = EventBuilder(Kind.fromStd(KindStandard.PRIVATE_DIRECT_MESSAGE), "")
|
||||||
|
.tags(to.map { Tag.publicKey(it) })
|
||||||
|
.finalizeUnsigned(currentUser)
|
||||||
|
|
||||||
|
// Check if the room already exists
|
||||||
|
val id = rumor.roomId()
|
||||||
|
val existingRoom = _chatRooms.value.firstOrNull { it.id == id }
|
||||||
|
|
||||||
|
// If the room already exists, return its ID
|
||||||
|
if (existingRoom != null) {
|
||||||
|
return existingRoom.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a room from the rumor event
|
||||||
|
val room = Room.new(rumor, currentUser)
|
||||||
|
|
||||||
|
// Update the chat rooms state
|
||||||
|
_chatRooms.update { currentRooms ->
|
||||||
|
(currentRooms + room).sortedDescending().toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
return room.id
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw IllegalArgumentException("Failed to create room: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getChatRoom(id: Long): Room? {
|
||||||
|
return chatRooms.value.firstOrNull { it.id == id }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun refreshChatRooms() {
|
||||||
|
try {
|
||||||
|
val rooms = nostr.messages.getChatRooms() ?: emptySet()
|
||||||
|
_chatRooms.update { currentRooms ->
|
||||||
|
val merged = currentRooms.associateBy { it.id }.toMutableMap()
|
||||||
|
// Add or update rooms from the database
|
||||||
|
rooms.forEach { room ->
|
||||||
|
merged[room.id] = room
|
||||||
|
}
|
||||||
|
// Return as a sorted set to maintain UI consistency
|
||||||
|
merged.values.sortedDescending().toSet()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getChatRoomMessages(roomId: Long): List<UnsignedEvent> {
|
||||||
|
try {
|
||||||
|
return nostr.messages.getChatRoomMessages(roomId)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Error: ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun chatRoomConnect(roomId: Long) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val room = getChatRoom(roomId) ?: throw IllegalArgumentException("Room not found")
|
||||||
|
val members = room.members
|
||||||
|
|
||||||
|
nostr.messages.chatRoomConnect(members.toList())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendMessage(roomId: Long, message: String, replies: List<EventId> = emptyList()) {
|
||||||
|
if (message.isEmpty()) {
|
||||||
|
showError("Message cannot be empty")
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val room = getChatRoom(roomId) ?: throw IllegalArgumentException("Room not found")
|
||||||
|
nostr.messages.sendMessage(
|
||||||
|
to = room.members,
|
||||||
|
content = message,
|
||||||
|
subject = room.subject,
|
||||||
|
replies = replies,
|
||||||
|
onRumorCreated = { event ->
|
||||||
|
updateRoomList(roomId, event)
|
||||||
|
viewModelScope.launch { _newEvents.emit(event) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun sendFileMessage(
|
||||||
|
roomId: Long,
|
||||||
|
file: ByteArray?,
|
||||||
|
contentType: String? = "image/jpeg",
|
||||||
|
replies: List<EventId> = emptyList()
|
||||||
|
) {
|
||||||
|
if (file == null) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
val uri = blossomUpload(nostr.signer.get(), file, contentType)
|
||||||
|
if (uri != null) sendMessage(roomId, uri, replies)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw IllegalArgumentException("Error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isMessageSent(id: EventId): Boolean {
|
||||||
|
val giftWrapId = nostr.messages.rumorMap[id]
|
||||||
|
|
||||||
|
if (giftWrapId != null) {
|
||||||
|
val isSent = nostr.messages.sentEvents[giftWrapId]?.isNotEmpty() ?: false
|
||||||
|
return isSent
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateRoomList(roomId: Long, newMessage: UnsignedEvent) {
|
||||||
|
_chatRooms.update { currentRooms ->
|
||||||
|
currentRooms.map { room ->
|
||||||
|
if (room.id == roomId) {
|
||||||
|
room.copy(
|
||||||
|
lastMessage = newMessage.content(),
|
||||||
|
createdAt = newMessage.createdAt()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
room
|
||||||
|
}
|
||||||
|
}.sortedDescending().toSet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetInternalState() {
|
||||||
|
_chatRooms.value = emptySet()
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isPartialProcessedGiftWrap = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,11 +7,9 @@ import kotlinx.coroutines.awaitCancellation
|
|||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
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.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
@@ -25,16 +23,10 @@ import kotlinx.coroutines.sync.Mutex
|
|||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import rust.nostr.sdk.EventBuilder
|
|
||||||
import rust.nostr.sdk.EventId
|
|
||||||
import rust.nostr.sdk.Kind
|
|
||||||
import rust.nostr.sdk.KindStandard
|
|
||||||
import rust.nostr.sdk.PublicKey
|
import rust.nostr.sdk.PublicKey
|
||||||
import rust.nostr.sdk.RelayMetadata
|
import rust.nostr.sdk.RelayMetadata
|
||||||
import rust.nostr.sdk.RelayUrl
|
import rust.nostr.sdk.RelayUrl
|
||||||
import rust.nostr.sdk.Tag
|
|
||||||
import rust.nostr.sdk.Timestamp
|
import rust.nostr.sdk.Timestamp
|
||||||
import rust.nostr.sdk.UnsignedEvent
|
|
||||||
import su.reya.coop.nostr.Nostr
|
import su.reya.coop.nostr.Nostr
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
@@ -43,12 +35,10 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
data class NostrAppState(
|
data class NostrAppState(
|
||||||
val isBusy: Boolean = false,
|
val isBusy: Boolean = false,
|
||||||
val isRelayListEmpty: Boolean = false,
|
val isRelayListEmpty: Boolean = false,
|
||||||
val isSyncing: Boolean = false,
|
|
||||||
val isPartialProcessedGiftWrap: Boolean = false,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class NostrViewModel(private val nostr: Nostr) : BaseViewModel() {
|
class NostrViewModel(private val nostr: Nostr) : BaseViewModel() {
|
||||||
private val backgroundWorkTrigger = flow {
|
private val alwaysRunTasks = flow {
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
val observerJob = launch { runObserver() }
|
val observerJob = launch { runObserver() }
|
||||||
val batchingJob = launch { runMetadataBatching() }
|
val batchingJob = launch { runMetadataBatching() }
|
||||||
@@ -63,30 +53,16 @@ class NostrViewModel(private val nostr: Nostr) : BaseViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val _appState = MutableStateFlow(NostrAppState())
|
private val _appState = MutableStateFlow(NostrAppState())
|
||||||
val appState: StateFlow<NostrAppState> = combine(
|
val appState: StateFlow<NostrAppState> =
|
||||||
_appState,
|
combine(_appState, alwaysRunTasks) { state, _ -> state }.stateIn(
|
||||||
nostr.messages.messageSyncState.map { it.isSyncing },
|
scope = viewModelScope,
|
||||||
backgroundWorkTrigger
|
started = SharingStarted.WhileSubscribed(5000),
|
||||||
) { state, isSyncing, _ ->
|
initialValue = NostrAppState()
|
||||||
state.copy(isSyncing = isSyncing)
|
)
|
||||||
}.stateIn(
|
|
||||||
scope = viewModelScope,
|
|
||||||
started = SharingStarted.WhileSubscribed(5000),
|
|
||||||
initialValue = NostrAppState()
|
|
||||||
)
|
|
||||||
|
|
||||||
private val _chatRooms = MutableStateFlow<Set<Room>>(emptySet())
|
|
||||||
val chatRooms = _chatRooms.asStateFlow()
|
|
||||||
|
|
||||||
private val _contactList = MutableStateFlow<Set<PublicKey>>(emptySet())
|
private val _contactList = MutableStateFlow<Set<PublicKey>>(emptySet())
|
||||||
val contactList = _contactList.asStateFlow()
|
val contactList = _contactList.asStateFlow()
|
||||||
|
|
||||||
private val _newEvents = MutableSharedFlow<UnsignedEvent>(extraBufferCapacity = 100)
|
|
||||||
val newEvents = _newEvents.asSharedFlow()
|
|
||||||
|
|
||||||
private val _sentReports = MutableSharedFlow<Map<EventId, List<RelayUrl>>>()
|
|
||||||
val sentReport = _sentReports.asSharedFlow()
|
|
||||||
|
|
||||||
private val profilesMutex = Mutex()
|
private val profilesMutex = Mutex()
|
||||||
private val profiles = mutableMapOf<PublicKey, MutableStateFlow<Profile?>>()
|
private val profiles = mutableMapOf<PublicKey, MutableStateFlow<Profile?>>()
|
||||||
private val metadataRequestChannel = Channel<PublicKey>(Channel.UNLIMITED)
|
private val metadataRequestChannel = Channel<PublicKey>(Channel.UNLIMITED)
|
||||||
@@ -94,12 +70,8 @@ class NostrViewModel(private val nostr: Nostr) : BaseViewModel() {
|
|||||||
|
|
||||||
val isBusy = appState.map { it.isBusy }
|
val isBusy = appState.map { it.isBusy }
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
|
||||||
val isPartialProcessedGiftWrap = appState.map { it.isPartialProcessedGiftWrap }
|
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
|
|
||||||
val isRelayListEmpty = appState.map { it.isRelayListEmpty }
|
val isRelayListEmpty = appState.map { it.isRelayListEmpty }
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
|
||||||
val isSyncing = appState.map { it.isSyncing }
|
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val currentUserProfile = nostr.signer.publicKeyFlow
|
val currentUserProfile = nostr.signer.publicKeyFlow
|
||||||
@@ -138,39 +110,6 @@ class NostrViewModel(private val nostr: Nostr) : BaseViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun runObserver() = coroutineScope {
|
private suspend fun runObserver() = coroutineScope {
|
||||||
// Observe message sync progress
|
|
||||||
launch {
|
|
||||||
nostr.messages.messageSyncState.collect { state ->
|
|
||||||
// When at least some messages are processed, allow UI to show the list
|
|
||||||
if (state.processedCount > 0) {
|
|
||||||
_appState.update { it.copy(isPartialProcessedGiftWrap = true) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh UI every 10 messages OR when sync is fully done
|
|
||||||
if (state.processedCount % 10 == 0 || !state.isSyncing) {
|
|
||||||
refreshChatRooms()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Observe new messages
|
|
||||||
launch {
|
|
||||||
nostr.newEvents.collect { event ->
|
|
||||||
val roomId = event.roomId()
|
|
||||||
val existingRoom = _chatRooms.value.firstOrNull { it.id == roomId }
|
|
||||||
|
|
||||||
if (existingRoom == null) {
|
|
||||||
val currentUser = nostr.signer.getPublicKeyAsync() ?: return@collect
|
|
||||||
val newRoom = Room.new(event, currentUser)
|
|
||||||
_chatRooms.update { (it + newRoom).sortedDescending().toSet() }
|
|
||||||
} else {
|
|
||||||
updateRoomList(roomId, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
_newEvents.emit(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Observe contact list updates
|
// Observe contact list updates
|
||||||
launch {
|
launch {
|
||||||
nostr.profiles.contactListUpdates.collect { contacts ->
|
nostr.profiles.contactListUpdates.collect { contacts ->
|
||||||
@@ -245,13 +184,6 @@ class NostrViewModel(private val nostr: Nostr) : BaseViewModel() {
|
|||||||
val currentUser = nostr.signer.getPublicKeyAsync()
|
val currentUser = nostr.signer.getPublicKeyAsync()
|
||||||
|
|
||||||
if (currentUser != null) {
|
if (currentUser != null) {
|
||||||
// Get chat rooms
|
|
||||||
val rooms = nostr.messages.getChatRooms() ?: emptySet()
|
|
||||||
if (rooms.isNotEmpty()) {
|
|
||||||
mergeChatRooms(rooms)
|
|
||||||
_appState.update { it.copy(isPartialProcessedGiftWrap = true) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all metadata for the current user
|
// Get all metadata for the current user
|
||||||
nostr.profiles.getUserMetadata()
|
nostr.profiles.getUserMetadata()
|
||||||
|
|
||||||
@@ -294,11 +226,9 @@ class NostrViewModel(private val nostr: Nostr) : BaseViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun resetInternalState() {
|
fun resetInternalState() {
|
||||||
_chatRooms.value = emptySet()
|
|
||||||
_contactList.value = emptySet()
|
_contactList.value = emptySet()
|
||||||
_appState.update {
|
_appState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isPartialProcessedGiftWrap = false,
|
|
||||||
isRelayListEmpty = false,
|
isRelayListEmpty = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -475,164 +405,6 @@ class NostrViewModel(private val nostr: Nostr) : BaseViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createChatRoom(to: List<PublicKey>): Long {
|
|
||||||
try {
|
|
||||||
if (to.isEmpty()) {
|
|
||||||
throw IllegalArgumentException("At least one recipient is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current user
|
|
||||||
val currentUser = nostr.signer.publicKeyFlow.value
|
|
||||||
?: throw IllegalStateException("User not signed in")
|
|
||||||
|
|
||||||
// Construct the rumor event
|
|
||||||
val rumor = EventBuilder(Kind.fromStd(KindStandard.PRIVATE_DIRECT_MESSAGE), "")
|
|
||||||
.tags(to.map { Tag.publicKey(it) })
|
|
||||||
.finalizeUnsigned(currentUser)
|
|
||||||
|
|
||||||
// Check if the room already exists
|
|
||||||
val id = rumor.roomId()
|
|
||||||
val existingRoom = _chatRooms.value.firstOrNull { it.id == id }
|
|
||||||
|
|
||||||
// If the room already exists, return its ID
|
|
||||||
if (existingRoom != null) {
|
|
||||||
return existingRoom.id
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a room from the rumor event
|
|
||||||
val room = Room.new(rumor, currentUser)
|
|
||||||
|
|
||||||
// Update the chat rooms state
|
|
||||||
_chatRooms.update { currentRooms ->
|
|
||||||
(currentRooms + room).sortedDescending().toSet()
|
|
||||||
}
|
|
||||||
|
|
||||||
return room.id
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw IllegalArgumentException("Failed to create room: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getChatRoom(id: Long): Room? {
|
|
||||||
return chatRooms.value.firstOrNull { it.id == id }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mergeChatRooms(rooms: Set<Room>) {
|
|
||||||
_chatRooms.update { currentRooms ->
|
|
||||||
val merged = currentRooms.associateBy { it.id }.toMutableMap()
|
|
||||||
// Add or update rooms from the database
|
|
||||||
rooms.forEach { room ->
|
|
||||||
merged[room.id] = room
|
|
||||||
}
|
|
||||||
// Return as a sorted set to maintain UI consistency
|
|
||||||
merged.values.sortedDescending().toSet()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getChatRooms() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
val rooms = nostr.messages.getChatRooms() ?: emptySet()
|
|
||||||
mergeChatRooms(rooms)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun refreshChatRooms() {
|
|
||||||
try {
|
|
||||||
val rooms = nostr.messages.getChatRooms() ?: emptySet()
|
|
||||||
mergeChatRooms(rooms)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showError("Error: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getChatRoomMessages(roomId: Long): List<UnsignedEvent> {
|
|
||||||
try {
|
|
||||||
return nostr.messages.getChatRoomMessages(roomId)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showError("Error: ${e.message}")
|
|
||||||
}
|
|
||||||
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun chatRoomConnect(roomId: Long) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
val room = getChatRoom(roomId) ?: throw IllegalArgumentException("Room not found")
|
|
||||||
val members = room.members
|
|
||||||
|
|
||||||
nostr.messages.chatRoomConnect(members.toList())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showError("Error: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendMessage(roomId: Long, message: String, replies: List<EventId> = emptyList()) {
|
|
||||||
if (message.isEmpty()) {
|
|
||||||
showError("Message cannot be empty")
|
|
||||||
}
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
val room = getChatRoom(roomId) ?: throw IllegalArgumentException("Room not found")
|
|
||||||
nostr.messages.sendMessage(
|
|
||||||
to = room.members,
|
|
||||||
content = message,
|
|
||||||
subject = room.subject,
|
|
||||||
replies = replies,
|
|
||||||
onRumorCreated = { event ->
|
|
||||||
updateRoomList(roomId, event)
|
|
||||||
viewModelScope.launch { _newEvents.emit(event) }
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showError("Error: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun sendFileMessage(
|
|
||||||
roomId: Long,
|
|
||||||
file: ByteArray?,
|
|
||||||
contentType: String? = "image/jpeg",
|
|
||||||
replies: List<EventId> = emptyList()
|
|
||||||
) {
|
|
||||||
if (file == null) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
val uri = blossomUpload(nostr.signer.get(), file, contentType)
|
|
||||||
if (uri != null) sendMessage(roomId, uri, replies)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw IllegalArgumentException("Error: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isMessageSent(id: EventId): Boolean {
|
|
||||||
val giftWrapId = nostr.messages.rumorMap[id]
|
|
||||||
|
|
||||||
if (giftWrapId != null) {
|
|
||||||
val isSent = nostr.messages.sentEvents[giftWrapId]?.isNotEmpty() ?: false
|
|
||||||
return isSent
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateRoomList(roomId: Long, newMessage: UnsignedEvent) {
|
|
||||||
_chatRooms.update { currentRooms ->
|
|
||||||
currentRooms.map { room ->
|
|
||||||
if (room.id == roomId) {
|
|
||||||
room.copy(
|
|
||||||
lastMessage = newMessage.content(),
|
|
||||||
createdAt = newMessage.createdAt()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
room
|
|
||||||
}
|
|
||||||
}.sortedDescending().toSet()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun searchByAddress(query: String): PublicKey? {
|
suspend fun searchByAddress(query: String): PublicKey? {
|
||||||
try {
|
try {
|
||||||
return nostr.profiles.searchByAddress(query)
|
return nostr.profiles.searchByAddress(query)
|
||||||
|
|||||||
Reference in New Issue
Block a user