revert to single viewmodel

This commit is contained in:
2026-06-30 16:46:02 +07:00
parent cd5a393a01
commit c3ef341f5d
21 changed files with 1048 additions and 1106 deletions

View File

@@ -47,30 +47,9 @@ import su.reya.coop.screens.RelayScreen
import su.reya.coop.screens.RequestListScreen
import su.reya.coop.screens.ScanScreen
import su.reya.coop.screens.UpdateProfileScreen
import su.reya.coop.viewmodels.AccountViewModel
import su.reya.coop.viewmodels.AppViewModel
import su.reya.coop.viewmodels.ChatViewModel
import su.reya.coop.viewmodels.ProfileViewModel
import su.reya.coop.viewmodels.RelayViewModel
val LocalAppViewModel = staticCompositionLocalOf<AppViewModel> {
error("No AppViewModel provided")
}
val LocalAccountViewModel = staticCompositionLocalOf<AccountViewModel> {
error("No AccountViewModel provided")
}
val LocalChatViewModel = staticCompositionLocalOf<ChatViewModel> {
error("No ChatViewModel provided")
}
val LocalProfileViewModel = staticCompositionLocalOf<ProfileViewModel> {
error("No ProfileViewModel provided")
}
val LocalRelayViewModel = staticCompositionLocalOf<RelayViewModel> {
error("No RelayViewModel provided")
val LocalNostrViewModel = staticCompositionLocalOf<NostrViewModel> {
error("No NostrViewModel provided")
}
val LocalSnackbarHostState = staticCompositionLocalOf<SnackbarHostState> {
@@ -88,11 +67,7 @@ val LocalScanResult = staticCompositionLocalOf<QrScanResult> {
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Composable
fun App(
appViewModel: AppViewModel,
accountViewModel: AccountViewModel,
chatViewModel: ChatViewModel,
profileViewModel: ProfileViewModel,
relayViewModel: RelayViewModel,
nostrViewModel: NostrViewModel,
) {
val context = LocalContext.current
val activity = context as? ComponentActivity
@@ -101,7 +76,7 @@ fun App(
val qrScanResult = remember { QrScanResult() }
// Get the signer required state
val signerRequired by accountViewModel.signerRequired.collectAsStateWithLifecycle()
val signerRequired by nostrViewModel.signerRequired.collectAsStateWithLifecycle()
// Snackbar
val snackbarHostState = remember { SnackbarHostState() }
@@ -128,7 +103,7 @@ fun App(
}
LaunchedEffect(Unit) {
appViewModel.errorEvents.collect { message ->
nostrViewModel.errorEvents.collect { message ->
snackbarHostState.showSnackbar(message)
}
}
@@ -171,15 +146,12 @@ fun App(
motionScheme = MotionScheme.expressive(),
) {
CompositionLocalProvider(
LocalAppViewModel provides appViewModel,
LocalAccountViewModel provides accountViewModel,
LocalChatViewModel provides chatViewModel,
LocalProfileViewModel provides profileViewModel,
LocalRelayViewModel provides relayViewModel,
LocalNostrViewModel provides nostrViewModel,
LocalSnackbarHostState provides snackbarHostState,
LocalNavigator provides navigator,
LocalScanResult provides qrScanResult,
) {
NavDisplay(
backStack = backStack,
onBack = {

View File

@@ -22,11 +22,6 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import su.reya.coop.coop.storage.SecretStore
import su.reya.coop.nostr.NostrManager
import su.reya.coop.viewmodels.AccountViewModel
import su.reya.coop.viewmodels.AppViewModel
import su.reya.coop.viewmodels.ChatViewModel
import su.reya.coop.viewmodels.ProfileViewModel
import su.reya.coop.viewmodels.RelayViewModel
import kotlin.system.exitProcess
class MainActivity : ComponentActivity() {
@@ -39,31 +34,18 @@ class MainActivity : ComponentActivity() {
private val secretStore = SecretStore(this@MainActivity)
private val androidSigner =
AndroidExternalSigner(this@MainActivity, externalSignerLauncher)
private val appVM = AppViewModel(NostrManager.instance, secretStore)
private val profileVM = ProfileViewModel(NostrManager.instance, appVM)
private val chatVM = ChatViewModel(NostrManager.instance, appVM)
private val accountVM =
AccountViewModel(NostrManager.instance, secretStore, appVM, androidSigner)
private val relayVM = RelayViewModel(NostrManager.instance, appVM, chatVM, profileVM)
private val nostrVM = NostrViewModel(NostrManager.instance, secretStore, androidSigner)
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return when {
modelClass.isAssignableFrom(AppViewModel::class.java) -> appVM
modelClass.isAssignableFrom(AccountViewModel::class.java) -> accountVM
modelClass.isAssignableFrom(ChatViewModel::class.java) -> chatVM
modelClass.isAssignableFrom(ProfileViewModel::class.java) -> profileVM
modelClass.isAssignableFrom(RelayViewModel::class.java) -> relayVM
modelClass.isAssignableFrom(NostrViewModel::class.java) -> nostrVM
else -> throw IllegalArgumentException("Unknown ViewModel class")
} as T
}
}
}
private val appViewModel: AppViewModel by viewModels { factory }
private val accountViewModel: AccountViewModel by viewModels { factory }
private val chatViewModel: ChatViewModel by viewModels { factory }
private val profileViewModel: ProfileViewModel by viewModels { factory }
private val relayViewModel: RelayViewModel by viewModels { factory }
private val nostrViewModel: NostrViewModel by viewModels { factory }
override fun onCreate(savedInstanceState: Bundle?) {
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
@@ -102,36 +84,20 @@ class MainActivity : ComponentActivity() {
// Keep the splash screen visible until the signer check is complete
splashScreen.setKeepOnScreenCondition {
accountViewModel.signerRequired.value == null
nostrViewModel.signerRequired.value == null
}
// Bind the lifecycle of the ViewModels
val lifecycle = ProcessLifecycleOwner.get().lifecycle
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
appViewModel.viewModelScope.launch {
owner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
coroutineScope {
launch { chatViewModel.bindObservers() }
launch { profileViewModel.bindObservers() }
}
}
}
}
})
relayViewModel.observeSignerAndCheckRelays()
nostrViewModel.bindLifecycle(ProcessLifecycleOwner.get().lifecycle)
setContent {
App(
appViewModel = appViewModel,
accountViewModel = accountViewModel,
chatViewModel = chatViewModel,
profileViewModel = profileViewModel,
relayViewModel = relayViewModel,
nostrViewModel = nostrViewModel,
)
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)

View File

@@ -77,10 +77,8 @@ 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.LocalAccountViewModel
import su.reya.coop.LocalChatViewModel
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalProfileViewModel
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.Screen
import su.reya.coop.nostr.Room
@@ -99,13 +97,12 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
val context = LocalContext.current
val snackbarHostState = LocalSnackbarHostState.current
val navigator = LocalNavigator.current
val chatViewModel = LocalChatViewModel.current
val profileViewModel = LocalProfileViewModel.current
val nostrViewModel = LocalNostrViewModel.current
val scope = rememberCoroutineScope()
val listState = rememberLazyListState()
// Get chat room by ID
val chatRooms by chatViewModel.chatRooms.collectAsStateWithLifecycle()
val chatRooms by nostrViewModel.chatRooms.collectAsStateWithLifecycle()
val room by remember(id) { derivedStateOf { chatRooms.firstOrNull { it.id == id } } }
// Show empty screen
@@ -125,11 +122,11 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
val displayName by remember(room) {
room?.nameFlow(profileViewModel) ?: flowOf("Loading...")
room?.nameFlow(nostrViewModel) ?: flowOf("Loading...")
}.collectAsStateWithLifecycle("Loading...")
val picture by remember(room) {
room?.pictureFlow(profileViewModel) ?: flowOf(null)
room?.pictureFlow(nostrViewModel) ?: flowOf(null)
}.collectAsStateWithLifecycle(null)
var text by remember { mutableStateOf("") }
@@ -154,7 +151,7 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
val type = context.contentResolver.getType(uri)
// Send message
chatViewModel.sendFileMessage(id, file, type)
nostrViewModel.sendFileMessage(id, file, type)
} catch (e: Exception) {
snackbarHostState.showSnackbar("Error: ${e.message}")
}
@@ -167,7 +164,7 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
LaunchedEffect(id) {
// Get messages
val initialMessages = chatViewModel.getChatRoomMessages(id)
val initialMessages = nostrViewModel.getChatRoomMessages(id)
messages.clear()
messages.addAll(initialMessages)
@@ -175,10 +172,10 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
loading = false
// Get msg relays for each member
chatViewModel.chatRoomConnect(id)
nostrViewModel.chatRoomConnect(id)
// Handle new messages
chatViewModel.newEvents.collect { event ->
nostrViewModel.newEvents.collect { event ->
if (event.roomId() == id) {
if (event.id() !in messages.map { it.id() }) {
messages.add(0, event)
@@ -357,7 +354,7 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
value = text,
onValueChange = { text = it },
onSend = {
chatViewModel.sendMessage(id, text)
nostrViewModel.sendMessage(id, text)
text = ""
},
onUpload = {
@@ -377,14 +374,14 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
fun ScreenerCard(room: Room) {
val pubkey = room.members.firstOrNull() ?: return
val profileViewModel = LocalProfileViewModel.current
val nostrViewModel = LocalNostrViewModel.current
val scope = rememberCoroutineScope()
var isContact by remember { mutableStateOf(false) }
var mutualContacts by remember { mutableStateOf<Set<PublicKey>>(emptySet()) }
var lastActivity by remember { mutableStateOf<Timestamp?>(null) }
val metadataFlow = remember(pubkey) { profileViewModel.getMetadata(pubkey) }
val metadataFlow = remember(pubkey) { nostrViewModel.getMetadata(pubkey) }
val metadata by metadataFlow.collectAsStateWithLifecycle()
val profile = metadata?.asRecord()
@@ -394,11 +391,11 @@ fun ScreenerCard(room: Room) {
LaunchedEffect(pubkey) {
scope.launch {
// Check contact
profileViewModel.verifyContact(pubkey).let { isContact = it }
nostrViewModel.verifyContact(pubkey).let { isContact = it }
// Get mutual contacts
profileViewModel.mutualContacts(pubkey).let { mutualContacts = it }
nostrViewModel.mutualContacts(pubkey).let { mutualContacts = it }
// Get the last activity
profileViewModel.verifyActivity(pubkey)?.let { lastActivity = it }
nostrViewModel.verifyActivity(pubkey)?.let { lastActivity = it }
}
}
@@ -516,9 +513,8 @@ fun DateSeparator(date: String) {
fun ChatMessage(
rumor: UnsignedEvent
) {
val accountViewModel = LocalAccountViewModel.current
val chatViewModel = LocalChatViewModel.current
val currentUser = accountViewModel.nostr.signer.currentUser
val nostrViewModel = LocalNostrViewModel.current
val currentUser = nostrViewModel.nostr.signer.currentUser
val isMine = rumor.author() == currentUser
val bubbleShape = if (isMine) {
@@ -552,7 +548,7 @@ fun ChatMessage(
onClick = {
val id = rumor.id()
if (id != null) {
val sent = chatViewModel.isMessageSent(id)
val sent = nostrViewModel.isMessageSent(id)
println("Sent: $sent")
}
}

View File

@@ -55,7 +55,7 @@ import org.jetbrains.compose.resources.painterResource
import rust.nostr.sdk.Nip05Address
import rust.nostr.sdk.PublicKey
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalProfileViewModel
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.Screen
import su.reya.coop.shared.Avatar
@@ -66,9 +66,9 @@ import su.reya.coop.short
fun ContactListScreen() {
val navigator = LocalNavigator.current
val snackbarHostState = LocalSnackbarHostState.current
val profileViewModel = LocalProfileViewModel.current
val nostrViewModel = LocalNostrViewModel.current
val contactList by profileViewModel.contactList.collectAsStateWithLifecycle()
val contactList by nostrViewModel.contactList.collectAsStateWithLifecycle()
var openAddContactDialog by remember { mutableStateOf(false) }
Scaffold(
@@ -180,7 +180,7 @@ fun ContactListScreen() {
@Composable
fun AddContactDialog(onDismissRequest: () -> Unit) {
val snackbarHostState = LocalSnackbarHostState.current
val profileViewModel = LocalProfileViewModel.current
val nostrViewModel = LocalNostrViewModel.current
val focusRequester = remember { FocusRequester() }
var contact by remember { mutableStateOf("") }
var isError by remember { mutableStateOf(false) }
@@ -225,7 +225,7 @@ fun AddContactDialog(onDismissRequest: () -> Unit) {
actions = {
IconButton(onClick = {
scope.launch {
val success = profileViewModel.addContact(contact)
val success = nostrViewModel.addContact(contact)
if (success) onDismissRequest()
}
}) {
@@ -283,8 +283,8 @@ fun ContactListItem(
total: Int = 0,
onClick: () -> Unit,
) {
val profileViewModel = LocalProfileViewModel.current
val metadataFlow = remember(pubkey) { profileViewModel.getMetadata(pubkey) }
val nostrViewModel = LocalNostrViewModel.current
val metadataFlow = remember(pubkey) { nostrViewModel.getMetadata(pubkey) }
val metadata by metadataFlow.collectAsState(initial = null)
val profile = metadata?.asRecord()
@@ -293,7 +293,7 @@ fun ContactListItem(
SegmentedListItem(
onClick = onClick,
onLongClick = { profileViewModel.removeContact(pubkey) },
onLongClick = { nostrViewModel.removeContact(pubkey) },
shapes = ListItemDefaults.segmentedShapes(
index = index,
count = total

View File

@@ -91,12 +91,8 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.painterResource
import rust.nostr.sdk.PublicKey
import su.reya.coop.LocalAccountViewModel
import su.reya.coop.LocalAppViewModel
import su.reya.coop.LocalChatViewModel
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalProfileViewModel
import su.reya.coop.LocalRelayViewModel
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalScanResult
import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.Screen
@@ -117,27 +113,22 @@ fun HomeScreen() {
val qrScanResult = LocalScanResult.current
val snackbarHostState = LocalSnackbarHostState.current
val clipboardManager = LocalClipboard.current
val appViewModel = LocalAppViewModel.current
val accountViewModel = LocalAccountViewModel.current
val chatViewModel = LocalChatViewModel.current
val profileViewModel = LocalProfileViewModel.current
val relayViewModel = LocalRelayViewModel.current
val nostrViewModel = LocalNostrViewModel.current
val scope = rememberCoroutineScope()
val sheetState = rememberModalBottomSheetState(true)
val listState = rememberLazyListState()
val pullToRefreshState = rememberPullToRefreshState()
val currentUser = accountViewModel.nostr.signer.currentUser ?: return
val currentUserProfile = profileViewModel.getMetadata(currentUser)
val currentUser = nostrViewModel.nostr.signer.currentUser ?: return
val currentUserProfile = nostrViewModel.getMetadata(currentUser)
val userProfile by currentUserProfile.collectAsStateWithLifecycle()
val chatRooms by chatViewModel.chatRooms.collectAsStateWithLifecycle()
val isRelayListEmpty by relayViewModel.isRelayListEmpty.collectAsStateWithLifecycle()
val isSyncing by chatViewModel.isSyncing.collectAsStateWithLifecycle()
val isPartialProcessedGiftWrap by chatViewModel.isPartialProcessedGiftWrap.collectAsStateWithLifecycle()
val isBannerDismissed by appViewModel.isNotificationBannerDismissed.collectAsState()
val chatRooms by nostrViewModel.chatRooms.collectAsStateWithLifecycle()
val isRelayListEmpty by nostrViewModel.isRelayListEmpty.collectAsStateWithLifecycle()
val isSyncing by nostrViewModel.isSyncing.collectAsStateWithLifecycle()
val isPartialProcessedGiftWrap by nostrViewModel.isPartialProcessedGiftWrap.collectAsStateWithLifecycle()
val isBannerDismissed by nostrViewModel.isNotificationBannerDismissed.collectAsState()
val expandedFab by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } }
var showBottomSheet by remember { mutableStateOf(false) }
@@ -165,7 +156,7 @@ fun HomeScreen() {
}
LaunchedEffect(Unit) {
chatViewModel.getChatRooms()
nostrViewModel.getChatRooms()
}
LaunchedEffect(qrScanResult.content) {
@@ -173,7 +164,7 @@ fun HomeScreen() {
runCatching { PublicKey.parse(result) }
.onSuccess { pubkey ->
try {
val roomId = chatViewModel.createChatRoom(listOf(pubkey))
val roomId = nostrViewModel.createChatRoom(listOf(pubkey))
navigator.navigate(Screen.Chat(roomId))
} catch (e: Exception) {
e.message?.let { snackbarHostState.showSnackbar(it) }
@@ -289,7 +280,7 @@ fun HomeScreen() {
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
TextButton(
onClick = { appViewModel.dismissNotificationBanner() },
onClick = { nostrViewModel.dismissNotificationBanner() },
modifier = Modifier.weight(1f),
) {
Text(text = "Maybe later")
@@ -330,7 +321,7 @@ fun HomeScreen() {
onRefresh = {
scope.launch {
isRefreshing = true
chatViewModel.refreshChatRooms()
nostrViewModel.refreshChatRooms()
isRefreshing = false
}
},
@@ -400,7 +391,7 @@ fun HomeScreen() {
onDismissRequest = { showBottomSheet = false },
sheetState = sheetState,
) {
val pubkey = accountViewModel.nostr.signer.currentUser
val pubkey = nostrViewModel.nostr.signer.currentUser
val shortPubkey = pubkey?.short() ?: "Not available"
val userName =
@@ -487,7 +478,7 @@ fun HomeScreen() {
// Show the relay setup dialog if the msg relay list is empty
if (isRelayListEmpty) {
ModalBottomSheet(
onDismissRequest = { relayViewModel.dismissRelayWarning() },
onDismissRequest = { nostrViewModel.dismissRelayWarning() },
sheetState = sheetState,
containerColor = MaterialTheme.colorScheme.surfaceContainer,
) {
@@ -594,7 +585,7 @@ fun HomeScreen() {
scope.launch {
isBusy = true
try {
relayViewModel.refetchMsgRelays(currentUser)
nostrViewModel.refetchMsgRelays(currentUser)
} catch (e: Exception) {
snackbarHostState.showSnackbar("Failed to refresh metadata: ${e.message}")
}
@@ -614,7 +605,7 @@ fun HomeScreen() {
enabled = !isBusy,
onClick = {
scope.launch {
relayViewModel.useDefaultMsgRelayList()
nostrViewModel.useDefaultMsgRelayList()
sheetState.hide()
}
},
@@ -637,18 +628,18 @@ fun HomeScreen() {
@Composable
fun NewRequests(requests: List<Room>) {
val navigator = LocalNavigator.current
val profileViewModel = LocalProfileViewModel.current
val nostrViewModel = LocalNostrViewModel.current
val total = requests.size
val firstRoom = requests.getOrNull(0)
val secondRoom = requests.getOrNull(1)
val firstName by remember(firstRoom) {
firstRoom?.nameFlow(profileViewModel) ?: flowOf("")
firstRoom?.nameFlow(nostrViewModel) ?: flowOf("")
}.collectAsStateWithLifecycle("Loading...")
val secondName by remember(secondRoom) {
secondRoom?.nameFlow(profileViewModel) ?: flowOf("")
secondRoom?.nameFlow(nostrViewModel) ?: flowOf("")
}.collectAsStateWithLifecycle("")
val supportingText = when {
@@ -720,9 +711,9 @@ fun NewRequests(requests: List<Room>) {
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ChatRoom(room: Room, onClick: () -> Unit) {
val profileViewModel = LocalProfileViewModel.current
val displayName by remember(room) { room.nameFlow(profileViewModel) }.collectAsStateWithLifecycle("Loading...")
val picture by remember(room) { room.pictureFlow(profileViewModel) }.collectAsStateWithLifecycle(null)
val nostrViewModel = LocalNostrViewModel.current
val displayName by remember(room) { room.nameFlow(nostrViewModel) }.collectAsStateWithLifecycle("Loading...")
val picture by remember(room) { room.pictureFlow(nostrViewModel) }.collectAsStateWithLifecycle(null)
ListItem(
modifier = Modifier.clickable(onClick = onClick),
@@ -768,7 +759,7 @@ fun BottomMenuList(
onDismiss: (suspend () -> Unit) -> Unit
) {
val navigator = LocalNavigator.current
val accountViewModel = LocalAccountViewModel.current
val nostrViewModel = LocalNostrViewModel.current
val defaultMenuList = listOf(
"Update Profile" to { navigator.navigate(Screen.UpdateProfile) },
@@ -798,7 +789,7 @@ fun BottomMenuList(
}
Spacer(modifier = Modifier.size(16.dp))
FilledTonalButton(
onClick = { onDismiss { accountViewModel.logout() } },
onClick = { onDismiss { nostrViewModel.logout() } },
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = MaterialTheme.colorScheme.error,
contentColor = MaterialTheme.colorScheme.onError

View File

@@ -58,10 +58,8 @@ import org.jetbrains.compose.resources.painterResource
import rust.nostr.sdk.Keys
import rust.nostr.sdk.NostrConnectUri
import rust.nostr.sdk.PublicKey
import su.reya.coop.LocalAccountViewModel
import su.reya.coop.LocalAppViewModel
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalProfileViewModel
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalScanResult
import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.Screen
@@ -76,19 +74,17 @@ fun ImportScreen() {
val navigator = LocalNavigator.current
val qrScanResult = LocalScanResult.current
val focusManager = LocalFocusManager.current
val appViewModel = LocalAppViewModel.current
val accountViewModel = LocalAccountViewModel.current
val profileViewModel = LocalProfileViewModel.current
val nostrViewModel = LocalNostrViewModel.current
val scope = rememberCoroutineScope()
val isBusy by appViewModel.isBusy.collectAsStateWithLifecycle(false)
val isBusy by nostrViewModel.isBusy.collectAsStateWithLifecycle(false)
var secret by remember { mutableStateOf("") }
var pubkey by remember { mutableStateOf<PublicKey?>(null) }
// Get metadata when pubkey changes
val metadata by remember(pubkey) {
pubkey?.let(profileViewModel::getMetadata) ?: flowOf(null)
pubkey?.let(nostrViewModel::getMetadata) ?: flowOf(null)
}.collectAsStateWithLifecycle(null)
val profile = metadata?.asRecord()
@@ -250,10 +246,10 @@ fun ImportScreen() {
onClick = {
scope.launch {
if (pubkey == null) {
accountViewModel.verifyIdentity(secret).let { pubkey = it }
nostrViewModel.verifyIdentity(secret).let { pubkey = it }
} else {
// Import the identity
accountViewModel.importIdentity(secret)
nostrViewModel.importIdentity(secret)
// Navigate to the home screen
navigator.navigate(Screen.Home)
}

View File

@@ -19,15 +19,15 @@ import coop.composeapp.generated.resources.Res
import coop.composeapp.generated.resources.ic_arrow_back
import io.github.alexzhirkevich.qrose.rememberQrCodePainter
import org.jetbrains.compose.resources.painterResource
import su.reya.coop.LocalAccountViewModel
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalSnackbarHostState
@Composable
fun MyQrScreen() {
val navigator = LocalNavigator.current
val snackbarHostState = LocalSnackbarHostState.current
val accountViewModel = LocalAccountViewModel.current
val accountViewModel = LocalNostrViewModel.current
val currentUser = accountViewModel.nostr.signer.currentUser ?: return
Scaffold(

View File

@@ -55,9 +55,8 @@ 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.LocalProfileViewModel
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalScanResult
import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.Screen
@@ -71,10 +70,9 @@ fun NewChatScreen() {
val snackbarHostState = LocalSnackbarHostState.current
val navigator = LocalNavigator.current
val qrScanResult = LocalScanResult.current
val chatViewModel = LocalChatViewModel.current
val profileViewModel = LocalProfileViewModel.current
val nostrViewModel = LocalNostrViewModel.current
val contactList by profileViewModel.contactList.collectAsStateWithLifecycle()
val contactList by nostrViewModel.contactList.collectAsStateWithLifecycle()
var query by remember { mutableStateOf("") }
val createGroup = remember { mutableStateOf(false) }
@@ -96,12 +94,12 @@ fun NewChatScreen() {
selectedReceivers.add(pubkey)
}
} else if (query.contains("@")) {
val pubkey = profileViewModel.searchByAddress(query)
val pubkey = nostrViewModel.searchByAddress(query)
if (pubkey != null) {
selectedReceivers.add(pubkey)
}
} else {
val results = profileViewModel.searchByNostr(query)
val results = nostrViewModel.searchByNostr(query)
searchResults.clear()
searchResults.addAll(results)
}
@@ -171,7 +169,7 @@ fun NewChatScreen() {
) {
ExtendedFloatingActionButton(
onClick = {
val roomId = chatViewModel.createChatRoom(selectedReceivers.toList())
val roomId = nostrViewModel.createChatRoom(selectedReceivers.toList())
navigator.navigate(Screen.Chat(roomId))
},
expanded = false,
@@ -262,7 +260,7 @@ fun NewChatScreen() {
items = searchResults,
selectedReceivers = selectedReceivers,
onContactClick = { pubkey ->
val roomId = chatViewModel.createChatRoom(listOf(pubkey))
val roomId = nostrViewModel.createChatRoom(listOf(pubkey))
navigator.navigate(Screen.Chat(roomId))
},
)
@@ -273,7 +271,7 @@ fun NewChatScreen() {
items = contactList.toList(),
selectedReceivers = selectedReceivers,
onContactClick = { pubkey ->
val roomId = chatViewModel.createChatRoom(listOf(pubkey))
val roomId = nostrViewModel.createChatRoom(listOf(pubkey))
navigator.navigate(Screen.Chat(roomId))
}
)
@@ -290,8 +288,8 @@ fun ReceiverChip(
pubkey: PublicKey,
onRemove: () -> Unit
) {
val profileViewModel = LocalProfileViewModel.current
val metadataFlow = remember(pubkey) { profileViewModel.getMetadata(pubkey) }
val nostrViewModel = LocalNostrViewModel.current
val metadataFlow = remember(pubkey) { nostrViewModel.getMetadata(pubkey) }
val metadata by metadataFlow.collectAsState(initial = null)
val profile = metadata?.asRecord()
@@ -376,8 +374,8 @@ fun ContactListItem(
onClick: () -> Unit,
onLongClick: () -> Unit
) {
val profileViewModel = LocalProfileViewModel.current
val metadataFlow = remember(pubkey) { profileViewModel.getMetadata(pubkey) }
val nostrViewModel = LocalNostrViewModel.current
val metadataFlow = remember(pubkey) { nostrViewModel.getMetadata(pubkey) }
val metadata by metadataFlow.collectAsState(initial = null)
val profile = metadata?.asRecord()

View File

@@ -5,21 +5,17 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.launch
import su.reya.coop.LocalAccountViewModel
import su.reya.coop.LocalAppViewModel
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalProfileViewModel
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.Screen
import su.reya.coop.shared.ProfileEditor
@Composable
fun NewIdentityScreen() {
val appViewModel = LocalAppViewModel.current
val accountViewModel = LocalAccountViewModel.current
val profileViewModel = LocalProfileViewModel.current
val nostrViewModel = LocalNostrViewModel.current
val navigator = LocalNavigator.current
val scope = rememberCoroutineScope()
val isBusy by appViewModel.isBusy.collectAsStateWithLifecycle(false)
val isBusy by nostrViewModel.isBusy.collectAsStateWithLifecycle(false)
ProfileEditor(
title = "Create a new identity",
@@ -28,7 +24,7 @@ fun NewIdentityScreen() {
onBack = { navigator.goBack() },
onConfirm = { name, bio, bytes, type ->
scope.launch {
accountViewModel.createIdentity(name, bio, bytes, type)
nostrViewModel.createIdentity(name, bio, bytes, type)
navigator.navigate(Screen.Home)
}
}

View File

@@ -45,8 +45,8 @@ import coop.composeapp.generated.resources.Res
import coop.composeapp.generated.resources.coop
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.painterResource
import su.reya.coop.LocalAccountViewModel
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.Screen
import su.reya.coop.shared.getExpressiveFontFamily
@@ -57,7 +57,7 @@ fun OnboardingScreen() {
val context = LocalContext.current
val snackbarHostState = LocalSnackbarHostState.current
val navigator = LocalNavigator.current
val accountViewModel = LocalAccountViewModel.current
val nostrViewModel = LocalNostrViewModel.current
val scope = rememberCoroutineScope()
val logoPainter = painterResource(Res.drawable.coop)
@@ -158,9 +158,9 @@ fun OnboardingScreen() {
FilledTonalButton(
onClick = {
scope.launch {
if (accountViewModel.isExternalSignerAvailable()) {
if (nostrViewModel.isExternalSignerAvailable()) {
try {
accountViewModel.connectExternalSigner()
nostrViewModel.connectExternalSigner()
navigator.navigate(Screen.Home)
} catch (e: Exception) {
e.message?.let { snackbarHostState.showSnackbar(it) }

View File

@@ -44,9 +44,8 @@ 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.LocalProfileViewModel
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.Screen
import su.reya.coop.shared.Avatar
@@ -59,18 +58,17 @@ fun ProfileScreen(pubkey: String) {
val context = LocalContext.current
val snackbarHostState = LocalSnackbarHostState.current
val navigator = LocalNavigator.current
val chatViewModel = LocalChatViewModel.current
val profileViewModel = LocalProfileViewModel.current
val nostrViewModel = LocalNostrViewModel.current
val scope = rememberCoroutineScope()
val pubkey = runCatching { PublicKey.parse(pubkey) }.getOrNull() ?: return
val pubkeyObj = runCatching { PublicKey.parse(pubkey) }.getOrNull() ?: return
val metadataFlow = remember(pubkey) { profileViewModel.getMetadata(pubkey) }
val metadataFlow = remember(pubkeyObj) { nostrViewModel.getMetadata(pubkeyObj) }
val metadata by metadataFlow.collectAsStateWithLifecycle()
val profile = metadata?.asRecord()
val displayName = profile?.displayName ?: profile?.name ?: "No name"
val nip05 = profile?.nip05 ?: pubkey.short()
val nip05 = profile?.nip05 ?: pubkeyObj.short()
val picture = profile?.picture
val details = remember(profile) {
@@ -161,7 +159,7 @@ fun ProfileScreen(pubkey: String) {
onClick = {
scope.launch {
try {
val roomId = chatViewModel.createChatRoom(listOf(pubkey))
val roomId = nostrViewModel.createChatRoom(listOf(pubkeyObj))
navigator.navigate(Screen.Chat(roomId))
} catch (e: Exception) {
e.message?.let { snackbarHostState.showSnackbar(it) }
@@ -191,7 +189,7 @@ fun ProfileScreen(pubkey: String) {
onClick = {
val sendIntent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, pubkey.toBech32())
putExtra(Intent.EXTRA_TEXT, pubkeyObj.toBech32())
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
@@ -240,4 +238,4 @@ fun ProfileScreen(pubkey: String) {
}
}
)
}
}

View File

@@ -66,7 +66,7 @@ import org.jetbrains.compose.resources.painterResource
import rust.nostr.sdk.RelayMetadata
import rust.nostr.sdk.RelayUrl
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalRelayViewModel
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalSnackbarHostState
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@@ -74,7 +74,7 @@ import su.reya.coop.LocalSnackbarHostState
fun RelayScreen() {
val navigator = LocalNavigator.current
val snackbarHostState = LocalSnackbarHostState.current
val relayViewModel = LocalRelayViewModel.current
val nostrViewModel = LocalNostrViewModel.current
val scope = rememberCoroutineScope()
val msgRelayList = remember { mutableStateListOf<RelayUrl>() }
@@ -96,8 +96,8 @@ fun RelayScreen() {
var relayToDelete by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
relayList.putAll(relayViewModel.currentUserRelayList())
msgRelayList.addAll(relayViewModel.currentUserMsgRelayList())
relayList.putAll(nostrViewModel.currentUserRelayList())
msgRelayList.addAll(nostrViewModel.currentUserMsgRelayList())
}
Scaffold(
@@ -321,7 +321,7 @@ fun RelayScreen() {
return@launch
}
try {
relayViewModel.removeMsgRelay(relayToDelete!!)
nostrViewModel.removeMsgRelay(relayToDelete!!)
msgRelayList.removeIf { it.toString() == relayToDelete }
relayToDelete = null
} catch (e: Exception) {
@@ -349,7 +349,7 @@ fun AddRelayDialog(
onMsgRelayAdded: (newRelay: String) -> Unit,
onRelayAdded: (newRelay: String, metadata: RelayMetadata?) -> Unit,
) {
val relayViewModel = LocalRelayViewModel.current
val nostrViewModel = LocalNostrViewModel.current
val snackbarHostState = LocalSnackbarHostState.current
val scope = rememberCoroutineScope()
@@ -401,17 +401,17 @@ fun AddRelayDialog(
if (!isError) {
when (selected) {
"Messaging" -> {
relayViewModel.addMsgRelay(relayAddress)
nostrViewModel.addMsgRelay(relayAddress)
onMsgRelayAdded(relayAddress)
}
"Inbox" -> {
relayViewModel.addInboxRelay(relayAddress)
nostrViewModel.addInboxRelay(relayAddress)
onRelayAdded(relayAddress, RelayMetadata.WRITE)
}
"Outbox" -> {
relayViewModel.addOutboxRelay(relayAddress)
nostrViewModel.addOutboxRelay(relayAddress)
onRelayAdded(relayAddress, RelayMetadata.READ)
}
}

View File

@@ -37,8 +37,8 @@ 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
import su.reya.coop.Screen
import su.reya.coop.nostr.RoomKind
@@ -48,14 +48,14 @@ import su.reya.coop.nostr.RoomKind
fun RequestListScreen() {
val navigator = LocalNavigator.current
val snackbarHostState = LocalSnackbarHostState.current
val chatViewModel = LocalChatViewModel.current
val nostrViewModel = LocalNostrViewModel.current
val scope = rememberCoroutineScope()
val listState = rememberLazyListState()
val pullToRefreshState = rememberPullToRefreshState()
var isRefreshing by remember { mutableStateOf(false) }
val chatRooms by chatViewModel.chatRooms.collectAsStateWithLifecycle()
val chatRooms by nostrViewModel.chatRooms.collectAsStateWithLifecycle()
// Get all request rooms
val requests = remember(chatRooms) {
@@ -103,7 +103,7 @@ fun RequestListScreen() {
onRefresh = {
scope.launch {
isRefreshing = true
chatViewModel.refreshChatRooms()
nostrViewModel.refreshChatRooms()
isRefreshing = false
}
},
@@ -155,4 +155,4 @@ fun RequestListScreen() {
}
}
}
}
}

View File

@@ -5,23 +5,19 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.launch
import su.reya.coop.LocalAccountViewModel
import su.reya.coop.LocalAppViewModel
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalProfileViewModel
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.shared.ProfileEditor
@Composable
fun UpdateProfileScreen() {
val appViewModel = LocalAppViewModel.current
val accountViewModel = LocalAccountViewModel.current
val profileViewModel = LocalProfileViewModel.current
val nostrViewModel = LocalNostrViewModel.current
val navigator = LocalNavigator.current
val scope = rememberCoroutineScope()
val currentUser = accountViewModel.nostr.signer.currentUser ?: return
val metadata by profileViewModel.getMetadata(currentUser).collectAsStateWithLifecycle()
val isBusy by appViewModel.isBusy.collectAsStateWithLifecycle(false)
val currentUser = nostrViewModel.nostr.signer.currentUser ?: return
val metadata by nostrViewModel.getMetadata(currentUser).collectAsStateWithLifecycle()
val isBusy by nostrViewModel.isBusy.collectAsStateWithLifecycle(false)
val profile = metadata?.asRecord()
@@ -35,9 +31,9 @@ fun UpdateProfileScreen() {
onBack = { navigator.goBack() },
onConfirm = { name, bio, bytes, type ->
scope.launch {
profileViewModel.updateProfile(name, bio, bytes, type)
nostrViewModel.updateProfile(name, bio, bytes, type)
navigator.goBack()
}
}
)
}
}

View File

@@ -7,10 +7,10 @@ import kotlinx.coroutines.flow.map
import rust.nostr.sdk.PublicKey
import su.reya.coop.nostr.Room
import su.reya.coop.short
import su.reya.coop.viewmodels.ProfileViewModel
import su.reya.coop.NostrViewModel
fun Room.nameFlow(
profileViewModel: ProfileViewModel,
nostrViewModel: NostrViewModel,
currentUser: PublicKey? = null
): Flow<String> {
// Return early if there's a custom subject/room name
@@ -19,7 +19,7 @@ fun Room.nameFlow(
val displayMembers = if (isGroup()) members.take(2) else members.take(1)
if (displayMembers.isEmpty()) return flowOf("Unknown")
return combine(displayMembers.map { profileViewModel.getMetadata(it) }) { metadataArray ->
return combine(displayMembers.map { nostrViewModel.getMetadata(it) }) { metadataArray ->
val names = metadataArray.mapIndexed { i, metadata ->
val profile = metadata?.asRecord()
profile?.displayName?.takeIf { it.isNotBlank() }
@@ -38,7 +38,8 @@ fun Room.nameFlow(
}
}
fun Room.pictureFlow(profileViewModel: ProfileViewModel): Flow<String?> {
fun Room.pictureFlow(nostrViewModel: NostrViewModel): Flow<String?> {
val firstMember = members.firstOrNull() ?: return flowOf(null)
return profileViewModel.getMetadata(firstMember).map { it?.asRecord()?.picture }
return nostrViewModel.getMetadata(firstMember).map { it?.asRecord()?.picture }
}