refactor
This commit is contained in:
@@ -35,7 +35,6 @@ import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import kotlinx.coroutines.launch
|
||||
import su.reya.coop.screens.chat.ChatScreen
|
||||
import su.reya.coop.screens.ContactListScreen
|
||||
import su.reya.coop.screens.HomeScreen
|
||||
import su.reya.coop.screens.ImportScreen
|
||||
@@ -48,10 +47,10 @@ 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.viewmodel.AuthViewModel
|
||||
import su.reya.coop.screens.chat.ChatScreen
|
||||
import su.reya.coop.viewmodel.ChatViewModel
|
||||
import su.reya.coop.viewmodel.NostrViewModel
|
||||
import su.reya.coop.viewmodel.RelayViewModel
|
||||
import su.reya.coop.viewmodel.account.AccountViewModel
|
||||
|
||||
val LocalNostrViewModel = staticCompositionLocalOf<NostrViewModel> {
|
||||
error("No NostrViewModel provided")
|
||||
@@ -61,13 +60,10 @@ val LocalChatViewModel = staticCompositionLocalOf<ChatViewModel> {
|
||||
error("No ChatViewModel provided")
|
||||
}
|
||||
|
||||
val LocalAuthViewModel = staticCompositionLocalOf<AuthViewModel> {
|
||||
error("No AuthViewModel provided")
|
||||
val LocalAccountViewModel = staticCompositionLocalOf<AccountViewModel> {
|
||||
error("No AccountViewModel provided")
|
||||
}
|
||||
|
||||
val LocalRelayViewModel = staticCompositionLocalOf<RelayViewModel> {
|
||||
error("No RelayViewModel provided")
|
||||
}
|
||||
|
||||
val LocalSnackbarHostState = staticCompositionLocalOf<SnackbarHostState> {
|
||||
error("No SnackbarHostState provided")
|
||||
@@ -85,9 +81,8 @@ val LocalScanResult = staticCompositionLocalOf<QrScanResult> {
|
||||
@Composable
|
||||
fun App(
|
||||
nostrViewModel: NostrViewModel,
|
||||
relayViewModel: RelayViewModel,
|
||||
chatViewModel: ChatViewModel,
|
||||
authViewModel: AuthViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val activity = context as? ComponentActivity
|
||||
@@ -96,8 +91,8 @@ fun App(
|
||||
val qrScanResult = remember { QrScanResult() }
|
||||
|
||||
// Get the signer required state
|
||||
val authState by authViewModel.state.collectAsStateWithLifecycle()
|
||||
val signerRequired = authState.signerRequired
|
||||
val accountState by accountViewModel.state.collectAsStateWithLifecycle()
|
||||
val signerRequired = accountState.signerRequired
|
||||
|
||||
// Snackbar
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
@@ -125,7 +120,7 @@ fun App(
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
launch {
|
||||
authViewModel.errorEvents.collect { message ->
|
||||
accountViewModel.errorEvents.collect { message ->
|
||||
snackbarHostState.showSnackbar(message)
|
||||
}
|
||||
}
|
||||
@@ -139,11 +134,6 @@ fun App(
|
||||
snackbarHostState.showSnackbar(message)
|
||||
}
|
||||
}
|
||||
launch {
|
||||
relayViewModel.errorEvents.collect { message ->
|
||||
snackbarHostState.showSnackbar(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(activity) {
|
||||
@@ -185,9 +175,8 @@ fun App(
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalNostrViewModel provides nostrViewModel,
|
||||
LocalRelayViewModel provides relayViewModel,
|
||||
LocalChatViewModel provides chatViewModel,
|
||||
LocalAuthViewModel provides authViewModel,
|
||||
LocalAccountViewModel provides accountViewModel,
|
||||
LocalSnackbarHostState provides snackbarHostState,
|
||||
LocalNavigator provides navigator,
|
||||
LocalScanResult provides qrScanResult,
|
||||
|
||||
@@ -14,10 +14,9 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import su.reya.coop.nostr.NostrManager
|
||||
import su.reya.coop.repository.MediaRepository
|
||||
import su.reya.coop.viewmodel.AuthViewModel
|
||||
import su.reya.coop.viewmodel.ChatViewModel
|
||||
import su.reya.coop.viewmodel.NostrViewModel
|
||||
import su.reya.coop.viewmodel.RelayViewModel
|
||||
import su.reya.coop.viewmodel.account.AccountViewModel
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
@@ -29,20 +28,18 @@ class MainActivity : ComponentActivity() {
|
||||
object : ViewModelProvider.Factory {
|
||||
private val storage = AppStore(this@MainActivity)
|
||||
private val mediaRepository = MediaRepository()
|
||||
private val nostrViewModel = NostrViewModel(NostrManager.instance, mediaRepository)
|
||||
private val relayViewModel = RelayViewModel(NostrManager.instance)
|
||||
private val nostrViewModel = NostrViewModel(NostrManager.instance)
|
||||
private val chatViewModel = ChatViewModel(NostrManager.instance, mediaRepository)
|
||||
private val androidSigner =
|
||||
AndroidExternalSigner(this@MainActivity, externalSignerLauncher)
|
||||
private val authViewModel =
|
||||
AuthViewModel(NostrManager.instance, storage, mediaRepository, androidSigner)
|
||||
private val accountViewModel =
|
||||
AccountViewModel(NostrManager.instance, storage, mediaRepository, androidSigner)
|
||||
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return when {
|
||||
modelClass.isAssignableFrom(NostrViewModel::class.java) -> nostrViewModel
|
||||
modelClass.isAssignableFrom(RelayViewModel::class.java) -> relayViewModel
|
||||
modelClass.isAssignableFrom(ChatViewModel::class.java) -> chatViewModel
|
||||
modelClass.isAssignableFrom(AuthViewModel::class.java) -> authViewModel
|
||||
modelClass.isAssignableFrom(AccountViewModel::class.java) -> accountViewModel
|
||||
else -> throw IllegalArgumentException("Unknown ViewModel class")
|
||||
} as T
|
||||
}
|
||||
@@ -50,9 +47,8 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
private val nostrViewModel: NostrViewModel by viewModels { factory }
|
||||
private val relayViewModel: RelayViewModel by viewModels { factory }
|
||||
private val chatViewModel: ChatViewModel by viewModels { factory }
|
||||
private val authViewModel: AuthViewModel by viewModels { factory }
|
||||
private val accountViewModel: AccountViewModel by viewModels { factory }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
||||
@@ -91,15 +87,14 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
// Keep the splash screen visible until the signer check is complete
|
||||
splashScreen.setKeepOnScreenCondition {
|
||||
authViewModel.state.value.signerRequired == null
|
||||
accountViewModel.state.value.signerRequired == null
|
||||
}
|
||||
|
||||
setContent {
|
||||
App(
|
||||
nostrViewModel = nostrViewModel,
|
||||
relayViewModel = relayViewModel,
|
||||
chatViewModel = chatViewModel,
|
||||
authViewModel = authViewModel,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import rust.nostr.sdk.Nip05Address
|
||||
import rust.nostr.sdk.PublicKey
|
||||
import su.reya.coop.LocalAccountViewModel
|
||||
import su.reya.coop.LocalChatViewModel
|
||||
import su.reya.coop.LocalNavigator
|
||||
import su.reya.coop.LocalNostrViewModel
|
||||
@@ -72,9 +73,10 @@ fun ContactListScreen() {
|
||||
val navigator = LocalNavigator.current
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
val nostrViewModel = LocalNostrViewModel.current
|
||||
val accountViewModel = LocalAccountViewModel.current
|
||||
val chatViewModel = LocalChatViewModel.current
|
||||
|
||||
val contactList by nostrViewModel.contactList.collectAsStateWithLifecycle()
|
||||
val contactList by accountViewModel.contactList.collectAsStateWithLifecycle()
|
||||
var openAddContactDialog by remember { mutableStateOf(false) }
|
||||
var contactToDelete by remember { mutableStateOf<PublicKey?>(null) }
|
||||
|
||||
@@ -201,7 +203,7 @@ fun ContactListScreen() {
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
contactToDelete?.let { nostrViewModel.removeContact(it) }
|
||||
contactToDelete?.let { accountViewModel.removeContact(it) }
|
||||
contactToDelete = null
|
||||
}
|
||||
) {
|
||||
@@ -222,6 +224,7 @@ fun ContactListScreen() {
|
||||
fun AddContactDialog(onDismissRequest: () -> Unit) {
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
val nostrViewModel = LocalNostrViewModel.current
|
||||
val accountViewModel = LocalAccountViewModel.current
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
var contact by remember { mutableStateOf("") }
|
||||
var isError by remember { mutableStateOf(false) }
|
||||
@@ -265,7 +268,7 @@ fun AddContactDialog(onDismissRequest: () -> Unit) {
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
nostrViewModel.addContact(contact)
|
||||
accountViewModel.addContact(contact)
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Icon(
|
||||
|
||||
@@ -89,11 +89,10 @@ import coop.composeapp.generated.resources.ic_scanner
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import rust.nostr.sdk.PublicKey
|
||||
import su.reya.coop.LocalAuthViewModel
|
||||
import su.reya.coop.LocalAccountViewModel
|
||||
import su.reya.coop.LocalChatViewModel
|
||||
import su.reya.coop.LocalNavigator
|
||||
import su.reya.coop.LocalNostrViewModel
|
||||
import su.reya.coop.LocalRelayViewModel
|
||||
import su.reya.coop.LocalScanResult
|
||||
import su.reya.coop.LocalSnackbarHostState
|
||||
import su.reya.coop.Room
|
||||
@@ -114,23 +113,22 @@ fun HomeScreen() {
|
||||
val clipboardManager = LocalClipboard.current
|
||||
val nostrViewModel = LocalNostrViewModel.current
|
||||
val chatViewModel = LocalChatViewModel.current
|
||||
val authViewModel = LocalAuthViewModel.current
|
||||
val relayViewModel = LocalRelayViewModel.current
|
||||
val accountViewModel = LocalAccountViewModel.current
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val sheetState = rememberModalBottomSheetState(true)
|
||||
val listState = rememberLazyListState()
|
||||
val pullToRefreshState = rememberPullToRefreshState()
|
||||
|
||||
val userProfile by nostrViewModel.currentUserProfile.collectAsStateWithLifecycle()
|
||||
val userProfile by accountViewModel.currentUserProfile.collectAsStateWithLifecycle()
|
||||
val chatRooms by chatViewModel.chatRooms.collectAsStateWithLifecycle()
|
||||
|
||||
val isRelayListEmpty by relayViewModel.isRelayListEmpty.collectAsStateWithLifecycle()
|
||||
val isRelayListEmpty by accountViewModel.isRelayListEmpty.collectAsStateWithLifecycle()
|
||||
val isSyncing by chatViewModel.isSyncing.collectAsStateWithLifecycle()
|
||||
val isPartialProcessedGiftWrap by chatViewModel.isPartialProcessedGiftWrap.collectAsStateWithLifecycle()
|
||||
|
||||
val authState by authViewModel.state.collectAsStateWithLifecycle()
|
||||
val isBannerDismissed = authState.isNotificationBannerDismissed
|
||||
val accountState by accountViewModel.state.collectAsStateWithLifecycle()
|
||||
val isBannerDismissed = accountState.isNotificationBannerDismissed
|
||||
|
||||
val expandedFab by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } }
|
||||
var showBottomSheet by remember { mutableStateOf(false) }
|
||||
@@ -157,7 +155,7 @@ fun HomeScreen() {
|
||||
onPauseOrDispose { }
|
||||
}
|
||||
|
||||
LaunchedEffect(authState.signerRequired) {
|
||||
LaunchedEffect(accountState.signerRequired) {
|
||||
chatViewModel.refreshChatRooms()
|
||||
}
|
||||
|
||||
@@ -282,7 +280,7 @@ fun HomeScreen() {
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
TextButton(
|
||||
onClick = { authViewModel.dismissNotificationBanner() },
|
||||
onClick = { accountViewModel.dismissNotificationBanner() },
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text(text = "Maybe later")
|
||||
@@ -469,7 +467,7 @@ fun HomeScreen() {
|
||||
// Show the relay setup dialog if the msg relay list is empty
|
||||
if (isRelayListEmpty) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { relayViewModel.dismissRelayWarning() },
|
||||
onDismissRequest = { accountViewModel.dismissRelayWarning() },
|
||||
sheetState = sheetState,
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
) {
|
||||
@@ -574,7 +572,7 @@ fun HomeScreen() {
|
||||
enabled = !isBusy,
|
||||
onClick = {
|
||||
isBusy = true
|
||||
relayViewModel.refetchMsgRelays()
|
||||
accountViewModel.refetchMsgRelays()
|
||||
isBusy = false
|
||||
},
|
||||
modifier = Modifier
|
||||
@@ -589,7 +587,7 @@ fun HomeScreen() {
|
||||
Button(
|
||||
enabled = !isBusy,
|
||||
onClick = {
|
||||
relayViewModel.useDefaultMsgRelayList()
|
||||
accountViewModel.useDefaultMsgRelayList()
|
||||
scope.launch { sheetState.hide() }
|
||||
},
|
||||
modifier = Modifier
|
||||
@@ -738,7 +736,7 @@ fun BottomMenuList(
|
||||
val navigator = LocalNavigator.current
|
||||
val nostrViewModel = LocalNostrViewModel.current
|
||||
val chatViewModel = LocalChatViewModel.current
|
||||
val authViewModel = LocalAuthViewModel.current
|
||||
val accountViewModel = LocalAccountViewModel.current
|
||||
|
||||
val defaultMenuList = listOf(
|
||||
"Update Profile" to { navigator.navigate(Screen.UpdateProfile) },
|
||||
@@ -770,8 +768,8 @@ fun BottomMenuList(
|
||||
FilledTonalButton(
|
||||
onClick = {
|
||||
onDismiss {
|
||||
authViewModel.logout(onLogout = {
|
||||
nostrViewModel.resetInternalState()
|
||||
accountViewModel.logout(onLogout = {
|
||||
accountViewModel.resetInternalState()
|
||||
chatViewModel.resetInternalState()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ import coop.composeapp.generated.resources.ic_scanner
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import rust.nostr.sdk.Keys
|
||||
import rust.nostr.sdk.NostrConnectUri
|
||||
import su.reya.coop.LocalAuthViewModel
|
||||
import su.reya.coop.LocalAccountViewModel
|
||||
import su.reya.coop.LocalNavigator
|
||||
import su.reya.coop.LocalScanResult
|
||||
import su.reya.coop.LocalSnackbarHostState
|
||||
@@ -63,9 +63,9 @@ fun ImportScreen() {
|
||||
val navigator = LocalNavigator.current
|
||||
val qrScanResult = LocalScanResult.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val authViewModel = LocalAuthViewModel.current
|
||||
val accountViewModel = LocalAccountViewModel.current
|
||||
|
||||
val authState by authViewModel.state.collectAsStateWithLifecycle()
|
||||
val accountState by accountViewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
var secret by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
@@ -98,15 +98,15 @@ fun ImportScreen() {
|
||||
}
|
||||
|
||||
// Navigate to Home on successful import (signerRequired becomes false)
|
||||
LaunchedEffect(authState.signerRequired) {
|
||||
if (authState.signerRequired == false) {
|
||||
LaunchedEffect(accountState.signerRequired) {
|
||||
if (accountState.signerRequired == false) {
|
||||
navigator.navigate(Screen.Home)
|
||||
}
|
||||
}
|
||||
|
||||
// Show import errors via snackbar
|
||||
LaunchedEffect(authState.importError) {
|
||||
authState.importError?.let {
|
||||
LaunchedEffect(accountState.importError) {
|
||||
accountState.importError?.let {
|
||||
snackbarHostState.showSnackbar(it)
|
||||
}
|
||||
}
|
||||
@@ -177,7 +177,7 @@ fun ImportScreen() {
|
||||
BasicTextField(
|
||||
value = secret,
|
||||
onValueChange = { secret = it },
|
||||
enabled = !authState.isImporting,
|
||||
enabled = !accountState.isImporting,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
@@ -222,7 +222,7 @@ fun ImportScreen() {
|
||||
BasicTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
enabled = !authState.isImporting && requirePassword,
|
||||
enabled = !accountState.isImporting && requirePassword,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
@@ -250,14 +250,14 @@ fun ImportScreen() {
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
authViewModel.importIdentity(secret, password)
|
||||
accountViewModel.importIdentity(secret, password)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(ButtonDefaults.MediumContainerHeight),
|
||||
enabled = secret.isNotBlank() && !authState.isImporting,
|
||||
enabled = secret.isNotBlank() && !accountState.isImporting,
|
||||
) {
|
||||
if (authState.isImporting) {
|
||||
if (accountState.isImporting) {
|
||||
LoadingIndicator()
|
||||
} else {
|
||||
Text(
|
||||
|
||||
@@ -21,16 +21,16 @@ 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 nostrViewModel = LocalNostrViewModel.current
|
||||
val currentUser by nostrViewModel.currentUserProfile.collectAsStateWithLifecycle()
|
||||
val accountViewModel = LocalAccountViewModel.current
|
||||
val currentUser by accountViewModel.currentUserProfile.collectAsStateWithLifecycle()
|
||||
|
||||
Scaffold(
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
|
||||
@@ -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.LocalAccountViewModel
|
||||
import su.reya.coop.LocalChatViewModel
|
||||
import su.reya.coop.LocalNavigator
|
||||
import su.reya.coop.LocalNostrViewModel
|
||||
@@ -72,9 +73,10 @@ fun NewChatScreen() {
|
||||
val navigator = LocalNavigator.current
|
||||
val qrScanResult = LocalScanResult.current
|
||||
val nostrViewModel = LocalNostrViewModel.current
|
||||
val accountViewModel = LocalAccountViewModel.current
|
||||
val chatViewModel = LocalChatViewModel.current
|
||||
|
||||
val contactList by nostrViewModel.contactList.collectAsStateWithLifecycle()
|
||||
val contactList by accountViewModel.contactList.collectAsStateWithLifecycle()
|
||||
var query by remember { mutableStateOf("") }
|
||||
|
||||
val createGroup = remember { mutableStateOf(false) }
|
||||
@@ -96,13 +98,13 @@ fun NewChatScreen() {
|
||||
selectedReceivers.add(pubkey)
|
||||
}
|
||||
} else if (query.contains("@")) {
|
||||
nostrViewModel.searchByAddress(query) { pubkey ->
|
||||
accountViewModel.searchByAddress(query) { pubkey ->
|
||||
if (pubkey != null) {
|
||||
selectedReceivers.add(pubkey)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
nostrViewModel.searchByNostr(query) { results ->
|
||||
accountViewModel.searchByNostr(query) { results ->
|
||||
searchResults.clear()
|
||||
searchResults.addAll(results)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package su.reya.coop.screens
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import su.reya.coop.LocalAuthViewModel
|
||||
import su.reya.coop.LocalAccountViewModel
|
||||
import su.reya.coop.LocalNavigator
|
||||
import su.reya.coop.Screen
|
||||
import su.reya.coop.shared.ProfileEditor
|
||||
|
||||
@Composable
|
||||
fun NewIdentityScreen() {
|
||||
val authViewModel = LocalAuthViewModel.current
|
||||
val accountViewModel = LocalAccountViewModel.current
|
||||
val navigator = LocalNavigator.current
|
||||
|
||||
ProfileEditor(
|
||||
@@ -16,7 +16,7 @@ fun NewIdentityScreen() {
|
||||
buttonLabel = "Continue",
|
||||
onBack = { navigator.goBack() },
|
||||
onConfirm = { name, bio, bytes, type ->
|
||||
authViewModel.createIdentity(name, bio, bytes, type)
|
||||
accountViewModel.createIdentity(name, bio, bytes, type)
|
||||
navigator.navigate(Screen.Home)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -48,7 +48,7 @@ import coop.composeapp.generated.resources.coop
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import su.reya.coop.LocalAuthViewModel
|
||||
import su.reya.coop.LocalAccountViewModel
|
||||
import su.reya.coop.LocalNavigator
|
||||
import su.reya.coop.LocalSnackbarHostState
|
||||
import su.reya.coop.Screen
|
||||
@@ -60,21 +60,21 @@ fun OnboardingScreen() {
|
||||
val context = LocalContext.current
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
val navigator = LocalNavigator.current
|
||||
val authViewModel = LocalAuthViewModel.current
|
||||
val accountViewModel = LocalAccountViewModel.current
|
||||
|
||||
val authState by authViewModel.state.collectAsStateWithLifecycle()
|
||||
val accountState by accountViewModel.state.collectAsStateWithLifecycle()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// Navigate to Home on successful external signer connection
|
||||
LaunchedEffect(authState.signerRequired) {
|
||||
if (authState.signerRequired == false) {
|
||||
LaunchedEffect(accountState.signerRequired) {
|
||||
if (accountState.signerRequired == false) {
|
||||
navigator.navigate(Screen.Home)
|
||||
}
|
||||
}
|
||||
|
||||
// Show connection errors
|
||||
LaunchedEffect(authState.importError) {
|
||||
authState.importError?.let {
|
||||
LaunchedEffect(accountState.importError) {
|
||||
accountState.importError?.let {
|
||||
snackbarHostState.showSnackbar(it)
|
||||
}
|
||||
}
|
||||
@@ -176,10 +176,10 @@ fun OnboardingScreen() {
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
FilledTonalButton(
|
||||
onClick = {
|
||||
if (authViewModel.isExternalSignerAvailable()) {
|
||||
if (accountViewModel.isExternalSignerAvailable()) {
|
||||
// Connect to the external signer
|
||||
// TODO: show all available signers?
|
||||
authViewModel.connectExternalSigner()
|
||||
accountViewModel.connectExternalSigner()
|
||||
} else {
|
||||
scope.launch {
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
|
||||
@@ -68,7 +68,7 @@ import rust.nostr.sdk.RelayMetadata
|
||||
import rust.nostr.sdk.RelayUrl
|
||||
import su.reya.coop.LocalNavigator
|
||||
import su.reya.coop.LocalNostrViewModel
|
||||
import su.reya.coop.LocalRelayViewModel
|
||||
import su.reya.coop.LocalAccountViewModel
|
||||
import su.reya.coop.LocalSnackbarHostState
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@@ -76,7 +76,7 @@ import su.reya.coop.LocalSnackbarHostState
|
||||
fun RelayScreen() {
|
||||
val navigator = LocalNavigator.current
|
||||
val nostrViewModel = LocalNostrViewModel.current
|
||||
val relayViewModel = LocalRelayViewModel.current
|
||||
val accountViewModel = LocalAccountViewModel.current
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -99,12 +99,12 @@ fun RelayScreen() {
|
||||
var relayToDelete by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
relayViewModel.loadCurrentUserRelayList()
|
||||
relayViewModel.loadCurrentUserMsgRelayList()
|
||||
accountViewModel.loadCurrentUserRelayList()
|
||||
accountViewModel.loadCurrentUserMsgRelayList()
|
||||
}
|
||||
|
||||
val loadedRelayList by relayViewModel.currentUserRelayList.collectAsStateWithLifecycle()
|
||||
val loadedMsgRelayList by relayViewModel.currentUserMsgRelayList.collectAsStateWithLifecycle()
|
||||
val loadedRelayList by accountViewModel.currentUserRelayList.collectAsStateWithLifecycle()
|
||||
val loadedMsgRelayList by accountViewModel.currentUserMsgRelayList.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(loadedRelayList) {
|
||||
if (loadedRelayList.isNotEmpty()) {
|
||||
@@ -341,7 +341,7 @@ fun RelayScreen() {
|
||||
relayToDelete = null
|
||||
return@TextButton
|
||||
}
|
||||
relayViewModel.removeMsgRelay(relayToDelete!!)
|
||||
accountViewModel.removeMsgRelay(relayToDelete!!)
|
||||
msgRelayList.removeIf { it.toString() == relayToDelete }
|
||||
relayToDelete = null
|
||||
}
|
||||
@@ -365,7 +365,7 @@ fun AddRelayDialog(
|
||||
onMsgRelayAdded: (newRelay: String) -> Unit,
|
||||
onRelayAdded: (newRelay: String, metadata: RelayMetadata?) -> Unit,
|
||||
) {
|
||||
val relayViewModel = LocalRelayViewModel.current
|
||||
val accountViewModel = LocalAccountViewModel.current
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -416,17 +416,17 @@ fun AddRelayDialog(
|
||||
if (!isError) {
|
||||
when (selected) {
|
||||
"Messaging" -> {
|
||||
relayViewModel.addMsgRelay(relayAddress)
|
||||
accountViewModel.addMsgRelay(relayAddress)
|
||||
onMsgRelayAdded(relayAddress)
|
||||
}
|
||||
|
||||
"Inbox" -> {
|
||||
relayViewModel.addInboxRelay(relayAddress)
|
||||
accountViewModel.addInboxRelay(relayAddress)
|
||||
onRelayAdded(relayAddress, RelayMetadata.WRITE)
|
||||
}
|
||||
|
||||
"Outbox" -> {
|
||||
relayViewModel.addOutboxRelay(relayAddress)
|
||||
accountViewModel.addOutboxRelay(relayAddress)
|
||||
onRelayAdded(relayAddress, RelayMetadata.READ)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,16 @@ package su.reya.coop.screens
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import su.reya.coop.LocalAccountViewModel
|
||||
import su.reya.coop.LocalNavigator
|
||||
import su.reya.coop.LocalNostrViewModel
|
||||
import su.reya.coop.shared.ProfileEditor
|
||||
|
||||
@Composable
|
||||
fun UpdateProfileScreen() {
|
||||
val nostrViewModel = LocalNostrViewModel.current
|
||||
val accountViewModel = LocalAccountViewModel.current
|
||||
val navigator = LocalNavigator.current
|
||||
|
||||
val currentUser by nostrViewModel.currentUserProfile.collectAsStateWithLifecycle()
|
||||
val currentUser by accountViewModel.currentUserProfile.collectAsStateWithLifecycle()
|
||||
val profile = currentUser?.metadata?.asRecord()
|
||||
|
||||
ProfileEditor(
|
||||
@@ -23,7 +23,7 @@ fun UpdateProfileScreen() {
|
||||
initialPicture = profile?.picture,
|
||||
onBack = { navigator.goBack() },
|
||||
onConfirm = { name, bio, bytes, type ->
|
||||
nostrViewModel.updateProfile(name, bio, bytes, type)
|
||||
accountViewModel.updateProfile(name, bio, bytes, type)
|
||||
navigator.goBack()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -31,6 +31,7 @@ import coop.composeapp.generated.resources.ic_check_circle
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import rust.nostr.sdk.PublicKey
|
||||
import rust.nostr.sdk.Timestamp
|
||||
import su.reya.coop.LocalAccountViewModel
|
||||
import su.reya.coop.LocalNostrViewModel
|
||||
import su.reya.coop.Room
|
||||
import su.reya.coop.humanReadable
|
||||
@@ -44,6 +45,7 @@ fun ScreenerCard(room: Room) {
|
||||
val pubkey = room.members.firstOrNull() ?: return
|
||||
|
||||
val nostrViewModel = LocalNostrViewModel.current
|
||||
val accountViewModel = LocalAccountViewModel.current
|
||||
|
||||
var isContact by remember { mutableStateOf(false) }
|
||||
var mutualContacts by remember { mutableStateOf<Set<PublicKey>>(emptySet()) }
|
||||
@@ -54,11 +56,11 @@ fun ScreenerCard(room: Room) {
|
||||
|
||||
LaunchedEffect(pubkey) {
|
||||
// Check contact
|
||||
nostrViewModel.verifyContact(pubkey) { isContact = it }
|
||||
accountViewModel.verifyContact(pubkey) { isContact = it }
|
||||
// Get mutual contacts
|
||||
nostrViewModel.mutualContacts(pubkey) { mutualContacts = it }
|
||||
accountViewModel.mutualContacts(pubkey) { mutualContacts = it }
|
||||
// Get the last activity
|
||||
nostrViewModel.verifyActivity(pubkey) { lastActivity = it }
|
||||
accountViewModel.verifyActivity(pubkey) { lastActivity = it }
|
||||
}
|
||||
|
||||
Column(
|
||||
|
||||
@@ -65,6 +65,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import rust.nostr.sdk.UnsignedEvent
|
||||
import su.reya.coop.LocalAccountViewModel
|
||||
import su.reya.coop.LocalChatViewModel
|
||||
import su.reya.coop.LocalNavigator
|
||||
import su.reya.coop.LocalNostrViewModel
|
||||
@@ -92,7 +93,8 @@ fun ChatScreen(
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
// Get current user
|
||||
val currentUser by nostrViewModel.currentUserProfile.collectAsStateWithLifecycle()
|
||||
val accountViewModel = LocalAccountViewModel.current
|
||||
val currentUser by accountViewModel.currentUserProfile.collectAsStateWithLifecycle()
|
||||
|
||||
// Get chat room by ID
|
||||
val chatRooms by chatViewModel.chatRooms.collectAsStateWithLifecycle()
|
||||
|
||||
@@ -1,57 +1,27 @@
|
||||
package su.reya.coop.viewmodel
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import rust.nostr.sdk.PublicKey
|
||||
import rust.nostr.sdk.Timestamp
|
||||
import su.reya.coop.Profile
|
||||
import su.reya.coop.nostr.Nostr
|
||||
import su.reya.coop.repository.MediaRepository
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
data class NostrAppState(
|
||||
val isBusy: Boolean = false,
|
||||
)
|
||||
|
||||
class NostrViewModel(
|
||||
private val nostr: Nostr,
|
||||
private val mediaRepository: MediaRepository,
|
||||
) : BaseViewModel() {
|
||||
private val _appState = MutableStateFlow(NostrAppState())
|
||||
val appState: StateFlow<NostrAppState> = _appState.asStateFlow()
|
||||
|
||||
private val _contactList = MutableStateFlow<Set<PublicKey>>(emptySet())
|
||||
val contactList = _contactList.asStateFlow()
|
||||
|
||||
class NostrViewModel(private val nostr: Nostr) : BaseViewModel() {
|
||||
private val profilesMutex = Mutex()
|
||||
private val profiles = mutableMapOf<PublicKey, MutableStateFlow<Profile?>>()
|
||||
private val metadataRequestChannel = Channel<PublicKey>(Channel.UNLIMITED)
|
||||
private val seenPublicKeys = mutableSetOf<PublicKey>()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val currentUserProfile = nostr.signer.publicKeyFlow
|
||||
.flatMapLatest { pubkey ->
|
||||
if (pubkey != null) getMetadata(pubkey) else flowOf(null)
|
||||
}
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
|
||||
|
||||
init {
|
||||
// Launch continuous background observers
|
||||
viewModelScope.launch { runObserver() }
|
||||
@@ -60,9 +30,6 @@ class NostrViewModel(
|
||||
// Automatically reconnect bootstrap relays
|
||||
reconnect()
|
||||
|
||||
// Fetch metadata for the current user
|
||||
fetchUserMetadata()
|
||||
|
||||
// Get all local stored metadata
|
||||
getCacheMetadata()
|
||||
}
|
||||
@@ -75,13 +42,6 @@ class NostrViewModel(
|
||||
}
|
||||
|
||||
private suspend fun runObserver() = coroutineScope {
|
||||
// Observe contact list updates
|
||||
launch {
|
||||
nostr.profiles.contactListUpdates.collect { contacts ->
|
||||
_contactList.value = contacts.toSet()
|
||||
}
|
||||
}
|
||||
|
||||
// Observe metadata updates
|
||||
launch {
|
||||
nostr.profiles.metadataUpdates.collect { (pubkey, metadata) ->
|
||||
@@ -143,20 +103,7 @@ class NostrViewModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchUserMetadata() {
|
||||
viewModelScope.launch {
|
||||
// Wait until the client is ready
|
||||
nostr.waitUntilInitialized()
|
||||
|
||||
// Wait until a signer is explicitly set (which updates publicKeyFlow)
|
||||
val currentUser = nostr.signer.publicKeyFlow.filterNotNull().first()
|
||||
|
||||
// Get all metadata for the current user
|
||||
nostr.profiles.getUserMetadata()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun requestMetadata(pubkey: PublicKey) {
|
||||
if (seenPublicKeys.add(pubkey)) {
|
||||
metadataRequestChannel.trySend(pubkey)
|
||||
@@ -176,144 +123,4 @@ class NostrViewModel(
|
||||
return flow.asStateFlow()
|
||||
}
|
||||
|
||||
fun resetInternalState() {
|
||||
_contactList.value = emptySet()
|
||||
}
|
||||
|
||||
fun updateProfile(
|
||||
name: String? = null,
|
||||
bio: String? = null,
|
||||
picture: ByteArray? = null,
|
||||
contentType: String? = null
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
_appState.update { it.copy(isBusy = true) }
|
||||
try {
|
||||
val avatarUrl =
|
||||
picture?.let {
|
||||
mediaRepository.blossomUpload(
|
||||
nostr.signer.get(),
|
||||
it,
|
||||
contentType ?: "image/jpeg"
|
||||
)
|
||||
}
|
||||
val newMetadata = nostr.profiles.updateProfile(name, bio, avatarUrl)
|
||||
val currentUser =
|
||||
nostr.signer.getPublicKeyAsync() ?: throw Exception("User not found")
|
||||
|
||||
// Update the metadata state after successfully published
|
||||
updateMetadata(currentUser, Profile(currentUser, newMetadata))
|
||||
|
||||
// Update local state
|
||||
_appState.update { it.copy(isBusy = false) }
|
||||
} catch (e: Exception) {
|
||||
showError("Error: ${e.message}")
|
||||
_appState.update { it.copy(isBusy = false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun newContact(publicKey: PublicKey) {
|
||||
if (publicKey in contactList.value) return
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val updated = contactList.value + publicKey
|
||||
// Publish new event
|
||||
nostr.profiles.setContactList(updated.toList())
|
||||
// Optimistic local update
|
||||
_contactList.update { it + publicKey }
|
||||
} catch (e: Exception) {
|
||||
showError("Error: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addContact(address: String) {
|
||||
viewModelScope.launch {
|
||||
val pubkey = try {
|
||||
if (address.contains("@")) {
|
||||
nostr.profiles.searchByAddress(address)
|
||||
} else {
|
||||
PublicKey.parse(address)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
showError("Invalid contact address: ${e.message}")
|
||||
return@launch
|
||||
}
|
||||
|
||||
newContact(pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeContact(publicKey: PublicKey) {
|
||||
viewModelScope.launch {
|
||||
if (publicKey !in contactList.value) return@launch
|
||||
|
||||
try {
|
||||
val updated = contactList.value - publicKey
|
||||
// Publish new event
|
||||
nostr.profiles.setContactList(updated.toList())
|
||||
// Optimistic local update
|
||||
_contactList.update { it - publicKey }
|
||||
} catch (e: Exception) {
|
||||
showError("Error: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun searchByAddress(query: String, onResult: (PublicKey?) -> Unit) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
onResult(nostr.profiles.searchByAddress(query))
|
||||
} catch (e: Exception) {
|
||||
showError("Error: ${e.message}")
|
||||
onResult(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun searchByNostr(query: String, onResult: (List<PublicKey>) -> Unit) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
onResult(nostr.profiles.searchByNostr(query))
|
||||
} catch (e: Exception) {
|
||||
showError("Error: ${e.message}")
|
||||
onResult(emptyList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun verifyActivity(pubkey: PublicKey, onResult: (Timestamp?) -> Unit) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
onResult(nostr.profiles.verifyActivity(pubkey))
|
||||
} catch (e: Exception) {
|
||||
showError("Error: ${e.message}")
|
||||
onResult(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun verifyContact(pubkey: PublicKey, onResult: (Boolean) -> Unit) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
onResult(nostr.profiles.verifyContact(pubkey))
|
||||
} catch (e: Exception) {
|
||||
showError("Error: ${e.message}")
|
||||
onResult(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun mutualContacts(pubkey: PublicKey, onResult: (Set<PublicKey>) -> Unit) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
onResult(nostr.profiles.mutualContacts(pubkey))
|
||||
} catch (e: Exception) {
|
||||
showError("Error: ${e.message}")
|
||||
onResult(emptySet())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package su.reya.coop.viewmodel
|
||||
package su.reya.coop.viewmodel.account
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -23,46 +24,46 @@ import su.reya.coop.nostr.SignerPermissions
|
||||
import su.reya.coop.repository.MediaRepository
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
data class AuthState(
|
||||
data class AccountState(
|
||||
val signerRequired: Boolean? = null,
|
||||
val isNotificationBannerDismissed: Boolean = false,
|
||||
val isImporting: Boolean = false,
|
||||
val importError: String? = null,
|
||||
)
|
||||
|
||||
class AuthViewModel(
|
||||
class AccountAuthDelegate(
|
||||
private val nostr: Nostr,
|
||||
private val storage: AppStorage,
|
||||
private val mediaRepository: MediaRepository,
|
||||
private val externalSignerHandler: ExternalSignerHandler? = null,
|
||||
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||
) : BaseViewModel() {
|
||||
private val onError: (String) -> Unit,
|
||||
private val onSignerReady: () -> Unit,
|
||||
private val scope: CoroutineScope,
|
||||
) {
|
||||
companion object {
|
||||
private const val KEY_USER_SIGNER = "user_signer"
|
||||
private const val KEY_APP_KEYS = "app_keys"
|
||||
private const val KEY_BANNER_DISMISSED = "notification_banner_dismissed"
|
||||
}
|
||||
|
||||
private val _state = MutableStateFlow(AuthState())
|
||||
val state = _state.asStateFlow()
|
||||
private val _state = MutableStateFlow(AccountState())
|
||||
val state: StateFlow<AccountState> = _state.asStateFlow()
|
||||
|
||||
init {
|
||||
// Check if the notification banner has been dismissed
|
||||
fun init() {
|
||||
checkNotificationBannerDismissedStatus()
|
||||
|
||||
// Check local stored secret (secret key or bunker)
|
||||
login()
|
||||
}
|
||||
|
||||
private fun checkNotificationBannerDismissedStatus() {
|
||||
viewModelScope.launch {
|
||||
scope.launch {
|
||||
val dismissed = storage.get(KEY_BANNER_DISMISSED) == "true"
|
||||
_state.update { it.copy(isNotificationBannerDismissed = dismissed) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun login() {
|
||||
viewModelScope.launch {
|
||||
scope.launch {
|
||||
try {
|
||||
val secret = withTimeoutOrNull(5.seconds) {
|
||||
storage.getSecret(KEY_USER_SIGNER)
|
||||
@@ -78,39 +79,36 @@ class AuthViewModel(
|
||||
nostr.setSigner(signer)
|
||||
}.onSuccess {
|
||||
_state.update { it.copy(signerRequired = false) }
|
||||
onSignerReady()
|
||||
}.onFailure { e ->
|
||||
showError("Login failed: ${e.message}")
|
||||
onError("Login failed: ${e.message}")
|
||||
_state.update { it.copy(signerRequired = true) }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
showError("Login failed: ${e.message}")
|
||||
onError("Login failed: ${e.message}")
|
||||
_state.update { it.copy(signerRequired = true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun logout(onLogout: () -> Unit = {}) {
|
||||
viewModelScope.launch {
|
||||
scope.launch {
|
||||
try {
|
||||
// Reset the nostr signer and prune the database
|
||||
nostr.signer.switch(Keys.generate())
|
||||
nostr.prune()
|
||||
} catch (e: Exception) {
|
||||
showError("Logout encountered an error: ${e.message}")
|
||||
onError("Logout encountered an error: ${e.message}")
|
||||
} finally {
|
||||
// Clear credentials from persistent storage
|
||||
storage.clear(KEY_USER_SIGNER)
|
||||
storage.clear(KEY_BANNER_DISMISSED)
|
||||
// Call cleanup callback (e.g. to reset other ViewModels)
|
||||
onLogout()
|
||||
// Reset local states
|
||||
_state.update { it.copy(signerRequired = true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun dismissNotificationBanner() {
|
||||
viewModelScope.launch {
|
||||
scope.launch {
|
||||
storage.set(KEY_BANNER_DISMISSED, "true")
|
||||
_state.update { it.copy(isNotificationBannerDismissed = true) }
|
||||
}
|
||||
@@ -118,9 +116,7 @@ class AuthViewModel(
|
||||
|
||||
private suspend fun getOrInitAppKeys(): Keys = withContext(defaultDispatcher) {
|
||||
val secret = storage.getSecret(KEY_APP_KEYS)
|
||||
// If app keys are already stored, use them
|
||||
if (secret != null) return@withContext Keys.parse(secret)
|
||||
// Generate new app keys and save to the secret storage
|
||||
val keys = Keys.generate()
|
||||
storage.setSecret(KEY_APP_KEYS, keys.secretKey().toBech32())
|
||||
keys
|
||||
@@ -136,9 +132,8 @@ class AuthViewModel(
|
||||
secret.startsWith("ncryptsec1") -> {
|
||||
if (password == null) throw IllegalArgumentException("Password is required")
|
||||
val enc = EncryptedSecretKey.fromBech32(secret)
|
||||
val secret = enc.decrypt(password)
|
||||
val keys = Keys(secret)
|
||||
|
||||
val decrypted = enc.decrypt(password)
|
||||
val keys = Keys(decrypted)
|
||||
keys to keys.secretKey().toBech32()
|
||||
}
|
||||
|
||||
@@ -153,7 +148,6 @@ class AuthViewModel(
|
||||
val handler = externalSignerHandler
|
||||
?: throw IllegalStateException("External signer not available on this platform")
|
||||
|
||||
// Format: nip55://packageName/hexPubkey
|
||||
val parts = secret.removePrefix("nip55://").split("/", limit = 2)
|
||||
val packageName = parts[0]
|
||||
val pubkey = PublicKey.parse(parts[1])
|
||||
@@ -167,25 +161,23 @@ class AuthViewModel(
|
||||
}
|
||||
|
||||
fun importIdentity(secret: String, password: String? = null) {
|
||||
viewModelScope.launch {
|
||||
scope.launch {
|
||||
_state.update { it.copy(isImporting = true, importError = null) }
|
||||
try {
|
||||
val (signer, decryptedSecret) = createSigner(secret, password)
|
||||
// Update signer
|
||||
nostr.setSigner(signer)
|
||||
// Persist the secret in the secret storage
|
||||
storage.setSecret(KEY_USER_SIGNER, decryptedSecret ?: secret)
|
||||
// Update local states
|
||||
_state.update { it.copy(signerRequired = false, isImporting = false) }
|
||||
onSignerReady()
|
||||
} catch (e: Exception) {
|
||||
showError("Import failed: ${e.message}")
|
||||
onError("Import failed: ${e.message}")
|
||||
_state.update { it.copy(isImporting = false, importError = e.message) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun connectExternalSigner() {
|
||||
viewModelScope.launch {
|
||||
scope.launch {
|
||||
_state.update { it.copy(isImporting = true, importError = null) }
|
||||
try {
|
||||
val handler = externalSignerHandler
|
||||
@@ -209,17 +201,15 @@ class AuthViewModel(
|
||||
val result = handler.getPublicKey(permissions) ?: throw Exception("Rejected")
|
||||
val signer = ExternalSignerProxy(handler, result.pubkey)
|
||||
|
||||
// Update signer
|
||||
nostr.setSigner(signer)
|
||||
// Store the signer in the secret storage
|
||||
storage.setSecret(
|
||||
KEY_USER_SIGNER,
|
||||
"nip55://${result.packageName}/${result.pubkey.toHex()}"
|
||||
)
|
||||
// Update local states
|
||||
_state.update { it.copy(signerRequired = false, isImporting = false) }
|
||||
onSignerReady()
|
||||
} catch (e: Exception) {
|
||||
showError("External signer connection failed: ${e.message}")
|
||||
onError("External signer connection failed: ${e.message}")
|
||||
_state.update { it.copy(isImporting = false, importError = e.message) }
|
||||
}
|
||||
}
|
||||
@@ -235,7 +225,7 @@ class AuthViewModel(
|
||||
picture: ByteArray?,
|
||||
contentType: String? = null
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
scope.launch {
|
||||
_state.update { it.copy(isImporting = true, importError = null) }
|
||||
try {
|
||||
val keys = Keys.generate()
|
||||
@@ -243,19 +233,17 @@ class AuthViewModel(
|
||||
val avatarUrl = picture?.let {
|
||||
mediaRepository.blossomUpload(keys, it, contentType ?: "image/jpeg")
|
||||
}
|
||||
// Create identity
|
||||
nostr.profiles.createIdentity(
|
||||
keys = keys,
|
||||
name = name,
|
||||
bio = bio,
|
||||
picture = avatarUrl
|
||||
)
|
||||
// Persist the secret in the secret storage
|
||||
storage.setSecret(KEY_USER_SIGNER, secret)
|
||||
// Update local states
|
||||
_state.update { it.copy(signerRequired = false, isImporting = false) }
|
||||
onSignerReady()
|
||||
} catch (e: Exception) {
|
||||
showError("Identity creation failed: ${e.message}")
|
||||
onError("Identity creation failed: ${e.message}")
|
||||
_state.update { it.copy(isImporting = false, importError = e.message) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package su.reya.coop.viewmodel.account
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import rust.nostr.sdk.PublicKey
|
||||
import rust.nostr.sdk.Timestamp
|
||||
import su.reya.coop.nostr.Nostr
|
||||
|
||||
class AccountContactDelegate(
|
||||
private val nostr: Nostr,
|
||||
private val scope: CoroutineScope,
|
||||
private val onError: (String) -> Unit,
|
||||
) {
|
||||
private val _contactList = MutableStateFlow<Set<PublicKey>>(emptySet())
|
||||
val contactList: StateFlow<Set<PublicKey>> = _contactList.asStateFlow()
|
||||
|
||||
fun init() {
|
||||
observeContactList()
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
_contactList.value = emptySet()
|
||||
}
|
||||
|
||||
private fun observeContactList() {
|
||||
scope.launch {
|
||||
nostr.waitUntilInitialized()
|
||||
nostr.profiles.contactListUpdates.collect { contacts ->
|
||||
_contactList.value = contacts.toSet()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun newContact(publicKey: PublicKey) {
|
||||
if (publicKey in contactList.value) return
|
||||
|
||||
scope.launch {
|
||||
try {
|
||||
val updated = contactList.value + publicKey
|
||||
nostr.profiles.setContactList(updated.toList())
|
||||
_contactList.update { it + publicKey }
|
||||
} catch (e: Exception) {
|
||||
onError("Error: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addContact(address: String) {
|
||||
scope.launch {
|
||||
val pubkey = try {
|
||||
if (address.contains("@")) {
|
||||
nostr.profiles.searchByAddress(address)
|
||||
} else {
|
||||
PublicKey.parse(address)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
onError("Invalid contact address: ${e.message}")
|
||||
return@launch
|
||||
}
|
||||
|
||||
newContact(pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeContact(publicKey: PublicKey) {
|
||||
scope.launch {
|
||||
if (publicKey !in contactList.value) return@launch
|
||||
|
||||
try {
|
||||
val updated = contactList.value - publicKey
|
||||
nostr.profiles.setContactList(updated.toList())
|
||||
_contactList.update { it - publicKey }
|
||||
} catch (e: Exception) {
|
||||
onError("Error: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun searchByAddress(query: String, onResult: (PublicKey?) -> Unit) {
|
||||
scope.launch {
|
||||
try {
|
||||
onResult(nostr.profiles.searchByAddress(query))
|
||||
} catch (e: Exception) {
|
||||
onError("Error: ${e.message}")
|
||||
onResult(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun searchByNostr(query: String, onResult: (List<PublicKey>) -> Unit) {
|
||||
scope.launch {
|
||||
try {
|
||||
onResult(nostr.profiles.searchByNostr(query))
|
||||
} catch (e: Exception) {
|
||||
onError("Error: ${e.message}")
|
||||
onResult(emptyList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun verifyActivity(pubkey: PublicKey, onResult: (Timestamp?) -> Unit) {
|
||||
scope.launch {
|
||||
try {
|
||||
onResult(nostr.profiles.verifyActivity(pubkey))
|
||||
} catch (e: Exception) {
|
||||
onError("Error: ${e.message}")
|
||||
onResult(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun verifyContact(pubkey: PublicKey, onResult: (Boolean) -> Unit) {
|
||||
scope.launch {
|
||||
try {
|
||||
onResult(nostr.profiles.verifyContact(pubkey))
|
||||
} catch (e: Exception) {
|
||||
onError("Error: ${e.message}")
|
||||
onResult(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun mutualContacts(pubkey: PublicKey, onResult: (Set<PublicKey>) -> Unit) {
|
||||
scope.launch {
|
||||
try {
|
||||
onResult(nostr.profiles.mutualContacts(pubkey))
|
||||
} catch (e: Exception) {
|
||||
onError("Error: ${e.message}")
|
||||
onResult(emptySet())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package su.reya.coop.viewmodel.account
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import rust.nostr.sdk.PublicKey
|
||||
import su.reya.coop.Profile
|
||||
import su.reya.coop.nostr.Nostr
|
||||
import su.reya.coop.repository.MediaRepository
|
||||
|
||||
class AccountProfileDelegate(
|
||||
private val nostr: Nostr,
|
||||
private val mediaRepository: MediaRepository,
|
||||
private val onError: (String) -> Unit,
|
||||
private val scope: CoroutineScope,
|
||||
) {
|
||||
private val _isUpdatingProfile = MutableStateFlow(false)
|
||||
val isUpdatingProfile: StateFlow<Boolean> = _isUpdatingProfile.asStateFlow()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val currentUserProfile: StateFlow<Profile?> = nostr.signer.publicKeyFlow
|
||||
.flatMapLatest { pubkey ->
|
||||
if (pubkey != null) currentUserProfileFlow(pubkey) else flowOf(null)
|
||||
}
|
||||
.stateIn(scope, SharingStarted.WhileSubscribed(5000), null)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun currentUserProfileFlow(pubkey: PublicKey) = merge(
|
||||
flow {
|
||||
nostr.waitUntilInitialized()
|
||||
val cached = nostr.profiles.getAllCacheMetadata()[pubkey]
|
||||
if (cached != null) emit(Profile(pubkey, cached))
|
||||
nostr.profiles.fetchMetadataBatch(listOf(pubkey))
|
||||
},
|
||||
nostr.profiles.metadataUpdates
|
||||
.filter { (p, _) -> p == pubkey }
|
||||
.map { (p, m) -> Profile(p, m) }
|
||||
)
|
||||
|
||||
fun getUserMetadata() {
|
||||
scope.launch {
|
||||
nostr.profiles.getUserMetadata()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateProfile(
|
||||
name: String? = null,
|
||||
bio: String? = null,
|
||||
picture: ByteArray? = null,
|
||||
contentType: String? = null
|
||||
) {
|
||||
scope.launch {
|
||||
_isUpdatingProfile.value = true
|
||||
try {
|
||||
val avatarUrl = picture?.let {
|
||||
mediaRepository.blossomUpload(
|
||||
nostr.signer.get(),
|
||||
it,
|
||||
contentType ?: "image/jpeg"
|
||||
)
|
||||
}
|
||||
nostr.profiles.updateProfile(name, bio, avatarUrl)
|
||||
_isUpdatingProfile.value = false
|
||||
} catch (e: Exception) {
|
||||
onError("Error: ${e.message}")
|
||||
_isUpdatingProfile.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package su.reya.coop.viewmodel
|
||||
package su.reya.coop.viewmodel.account
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
@@ -13,11 +13,13 @@ import rust.nostr.sdk.RelayUrl
|
||||
import su.reya.coop.nostr.Nostr
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class RelayViewModel(
|
||||
class AccountRelayDelegate(
|
||||
private val nostr: Nostr,
|
||||
) : BaseViewModel() {
|
||||
private val onError: (String) -> Unit,
|
||||
private val scope: CoroutineScope,
|
||||
) {
|
||||
private val _isRelayListEmpty = MutableStateFlow(false)
|
||||
val isRelayListEmpty: StateFlow<Boolean> = _isRelayListEmpty.asStateFlow()
|
||||
val isRelayListEmpty = _isRelayListEmpty.asStateFlow()
|
||||
|
||||
private val _currentUserRelayList = MutableStateFlow<Map<RelayUrl, RelayMetadata?>>(emptyMap())
|
||||
val currentUserRelayList = _currentUserRelayList.asStateFlow()
|
||||
@@ -25,22 +27,19 @@ class RelayViewModel(
|
||||
private val _currentUserMsgRelayList = MutableStateFlow<List<RelayUrl>>(emptyList())
|
||||
val currentUserMsgRelayList = _currentUserMsgRelayList.asStateFlow()
|
||||
|
||||
init {
|
||||
checkRelayList()
|
||||
fun reset() {
|
||||
_isRelayListEmpty.value = false
|
||||
}
|
||||
|
||||
private fun checkRelayList() {
|
||||
viewModelScope.launch {
|
||||
// Wait until the client is ready
|
||||
nostr.waitUntilInitialized()
|
||||
|
||||
// Wait until a signer is explicitly set (which updates publicKeyFlow)
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun checkRelayList() {
|
||||
scope.launch {
|
||||
val currentUser = nostr.signer.publicKeyFlow.filterNotNull().first()
|
||||
println("user: ${currentUser.toBech32()}")
|
||||
|
||||
// Small delay to ensure all relays are connected
|
||||
delay(2.seconds)
|
||||
// Small delay to ensure subscription is ready
|
||||
delay(6.seconds)
|
||||
|
||||
// Check if the relay list is empty
|
||||
val relays = nostr.relays.getMsgRelays(currentUser)
|
||||
if (relays.isEmpty()) _isRelayListEmpty.value = true
|
||||
}
|
||||
@@ -50,12 +49,8 @@ class RelayViewModel(
|
||||
_isRelayListEmpty.value = false
|
||||
}
|
||||
|
||||
fun resetInternalState() {
|
||||
_isRelayListEmpty.value = false
|
||||
}
|
||||
|
||||
fun refetchMsgRelays() {
|
||||
viewModelScope.launch {
|
||||
scope.launch {
|
||||
val currentUser = nostr.signer.getPublicKeyAsync() ?: return@launch
|
||||
val relays = nostr.relays.fetchMsgRelays(currentUser)
|
||||
|
||||
@@ -64,23 +59,24 @@ class RelayViewModel(
|
||||
}
|
||||
|
||||
fun useDefaultMsgRelayList() {
|
||||
viewModelScope.launch {
|
||||
scope.launch {
|
||||
try {
|
||||
val defaultRelays = nostr.relays.getDefaultMsgRelayList()
|
||||
nostr.relays.setMsgRelays(defaultRelays)
|
||||
} catch (e: Exception) {
|
||||
showError("Error: ${e.message}")
|
||||
onError("Error: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadCurrentUserRelayList() {
|
||||
viewModelScope.launch {
|
||||
scope.launch {
|
||||
try {
|
||||
val currentUser = nostr.signer.getPublicKeyAsync() ?: throw Exception("User not found")
|
||||
val currentUser =
|
||||
nostr.signer.getPublicKeyAsync() ?: throw Exception("User not found")
|
||||
_currentUserRelayList.value = nostr.relays.getRelayList(currentUser)
|
||||
} catch (e: Exception) {
|
||||
showError("Error: ${e.message}")
|
||||
onError("Error: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,13 +86,13 @@ class RelayViewModel(
|
||||
val currentUser = nostr.signer.getPublicKeyAsync() ?: throw Exception("User not found")
|
||||
return nostr.relays.getRelayList(currentUser)
|
||||
} catch (e: Exception) {
|
||||
showError("Error: ${e.message}")
|
||||
onError("Error: ${e.message}")
|
||||
return emptyMap()
|
||||
}
|
||||
}
|
||||
|
||||
fun addInboxRelay(relay: String) {
|
||||
viewModelScope.launch {
|
||||
scope.launch {
|
||||
try {
|
||||
val relayUrl = RelayUrl.parse(relay)
|
||||
val relays = currentUserRelayListInternal().toMutableMap()
|
||||
@@ -104,13 +100,13 @@ class RelayViewModel(
|
||||
|
||||
nostr.relays.setRelaylist(relays)
|
||||
} catch (e: Exception) {
|
||||
showError("Error: ${e.message}")
|
||||
onError("Error: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addOutboxRelay(relay: String) {
|
||||
viewModelScope.launch {
|
||||
scope.launch {
|
||||
try {
|
||||
val relayUrl = RelayUrl.parse(relay)
|
||||
val relays = currentUserRelayListInternal().toMutableMap()
|
||||
@@ -118,13 +114,13 @@ class RelayViewModel(
|
||||
|
||||
nostr.relays.setRelaylist(relays)
|
||||
} catch (e: Exception) {
|
||||
showError("Error: ${e.message}")
|
||||
onError("Error: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeRelay(relay: String) {
|
||||
viewModelScope.launch {
|
||||
scope.launch {
|
||||
try {
|
||||
val relayUrl = RelayUrl.parse(relay)
|
||||
val relays = currentUserRelayListInternal().toMutableMap()
|
||||
@@ -132,18 +128,19 @@ class RelayViewModel(
|
||||
|
||||
nostr.relays.setRelaylist(relays)
|
||||
} catch (e: Exception) {
|
||||
showError("Error: ${e.message}")
|
||||
onError("Error: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadCurrentUserMsgRelayList() {
|
||||
viewModelScope.launch {
|
||||
scope.launch {
|
||||
try {
|
||||
val currentUser = nostr.signer.getPublicKeyAsync() ?: throw Exception("User not found")
|
||||
val currentUser =
|
||||
nostr.signer.getPublicKeyAsync() ?: throw Exception("User not found")
|
||||
_currentUserMsgRelayList.value = nostr.relays.getMsgRelays(currentUser)
|
||||
} catch (e: Exception) {
|
||||
showError("Error: ${e.message}")
|
||||
onError("Error: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,13 +150,13 @@ class RelayViewModel(
|
||||
val currentUser = nostr.signer.getPublicKeyAsync() ?: throw Exception("User not found")
|
||||
return nostr.relays.getMsgRelays(currentUser)
|
||||
} catch (e: Exception) {
|
||||
showError("Error: ${e.message}")
|
||||
onError("Error: ${e.message}")
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun addMsgRelay(relay: String) {
|
||||
viewModelScope.launch {
|
||||
scope.launch {
|
||||
try {
|
||||
val relayUrl = RelayUrl.parse(relay)
|
||||
val relays = currentUserMsgRelayListInternal().toMutableSet()
|
||||
@@ -167,13 +164,13 @@ class RelayViewModel(
|
||||
|
||||
nostr.relays.setMsgRelays(relays.toList())
|
||||
} catch (e: Exception) {
|
||||
showError("Error: ${e.message}")
|
||||
onError("Error: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeMsgRelay(relay: String) {
|
||||
viewModelScope.launch {
|
||||
scope.launch {
|
||||
try {
|
||||
val relayUrl = RelayUrl.parse(relay)
|
||||
val relays = currentUserMsgRelayListInternal().toMutableSet()
|
||||
@@ -181,7 +178,7 @@ class RelayViewModel(
|
||||
|
||||
nostr.relays.setMsgRelays(relays.toList())
|
||||
} catch (e: Exception) {
|
||||
showError("Error: ${e.message}")
|
||||
onError("Error: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package su.reya.coop.viewmodel.account
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import rust.nostr.sdk.PublicKey
|
||||
import rust.nostr.sdk.RelayMetadata
|
||||
import rust.nostr.sdk.RelayUrl
|
||||
import rust.nostr.sdk.Timestamp
|
||||
import su.reya.coop.AppStorage
|
||||
import su.reya.coop.Profile
|
||||
import su.reya.coop.nostr.ExternalSignerHandler
|
||||
import su.reya.coop.nostr.Nostr
|
||||
import su.reya.coop.repository.MediaRepository
|
||||
import su.reya.coop.viewmodel.BaseViewModel
|
||||
|
||||
class AccountViewModel(
|
||||
nostr: Nostr,
|
||||
storage: AppStorage,
|
||||
mediaRepository: MediaRepository,
|
||||
externalSignerHandler: ExternalSignerHandler? = null,
|
||||
defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||
) : BaseViewModel() {
|
||||
private val relays = AccountRelayDelegate(
|
||||
nostr = nostr,
|
||||
onError = ::showError,
|
||||
scope = viewModelScope,
|
||||
)
|
||||
|
||||
private val auth = AccountAuthDelegate(
|
||||
nostr = nostr,
|
||||
storage = storage,
|
||||
mediaRepository = mediaRepository,
|
||||
externalSignerHandler = externalSignerHandler,
|
||||
defaultDispatcher = defaultDispatcher,
|
||||
onError = ::showError,
|
||||
onSignerReady = {
|
||||
profile.getUserMetadata()
|
||||
relays.checkRelayList()
|
||||
},
|
||||
scope = viewModelScope,
|
||||
)
|
||||
|
||||
private val profile = AccountProfileDelegate(
|
||||
nostr = nostr,
|
||||
mediaRepository = mediaRepository,
|
||||
onError = ::showError,
|
||||
scope = viewModelScope,
|
||||
)
|
||||
|
||||
private val contacts = AccountContactDelegate(
|
||||
nostr = nostr,
|
||||
onError = ::showError,
|
||||
scope = viewModelScope,
|
||||
)
|
||||
|
||||
|
||||
val state = auth.state
|
||||
|
||||
val currentUserProfile: StateFlow<Profile?> = profile.currentUserProfile
|
||||
|
||||
val isUpdatingProfile: StateFlow<Boolean> = profile.isUpdatingProfile
|
||||
|
||||
val contactList: StateFlow<Set<PublicKey>> = contacts.contactList
|
||||
|
||||
val isRelayListEmpty: StateFlow<Boolean> = relays.isRelayListEmpty
|
||||
|
||||
val currentUserRelayList: StateFlow<Map<RelayUrl, RelayMetadata?>> = relays.currentUserRelayList
|
||||
|
||||
val currentUserMsgRelayList: StateFlow<List<RelayUrl>> = relays.currentUserMsgRelayList
|
||||
|
||||
init {
|
||||
auth.init()
|
||||
contacts.init()
|
||||
}
|
||||
|
||||
fun logout(onLogout: () -> Unit = {}) = auth.logout(onLogout)
|
||||
|
||||
fun dismissNotificationBanner() = auth.dismissNotificationBanner()
|
||||
|
||||
fun connectExternalSigner() = auth.connectExternalSigner()
|
||||
|
||||
fun isExternalSignerAvailable(): Boolean = auth.isExternalSignerAvailable()
|
||||
|
||||
fun importIdentity(secret: String, password: String? = null) =
|
||||
auth.importIdentity(secret, password)
|
||||
|
||||
fun createIdentity(
|
||||
name: String,
|
||||
bio: String? = null,
|
||||
picture: ByteArray? = null,
|
||||
contentType: String? = null,
|
||||
) = auth.createIdentity(name, bio, picture, contentType)
|
||||
|
||||
fun updateProfile(
|
||||
name: String? = null,
|
||||
bio: String? = null,
|
||||
picture: ByteArray? = null,
|
||||
contentType: String? = null,
|
||||
) {
|
||||
profile.updateProfile(name, bio, picture, contentType)
|
||||
}
|
||||
|
||||
fun resetInternalState() {
|
||||
contacts.reset()
|
||||
relays.reset()
|
||||
}
|
||||
|
||||
fun addContact(address: String) = contacts.addContact(address)
|
||||
|
||||
fun removeContact(publicKey: PublicKey) = contacts.removeContact(publicKey)
|
||||
|
||||
fun searchByAddress(query: String, onResult: (PublicKey?) -> Unit) =
|
||||
contacts.searchByAddress(query, onResult)
|
||||
|
||||
fun searchByNostr(query: String, onResult: (List<PublicKey>) -> Unit) =
|
||||
contacts.searchByNostr(query, onResult)
|
||||
|
||||
fun verifyActivity(pubkey: PublicKey, onResult: (Timestamp?) -> Unit) =
|
||||
contacts.verifyActivity(pubkey, onResult)
|
||||
|
||||
fun verifyContact(pubkey: PublicKey, onResult: (Boolean) -> Unit) =
|
||||
contacts.verifyContact(pubkey, onResult)
|
||||
|
||||
fun mutualContacts(pubkey: PublicKey, onResult: (Set<PublicKey>) -> Unit) =
|
||||
contacts.mutualContacts(pubkey, onResult)
|
||||
|
||||
fun dismissRelayWarning() = relays.dismissRelayWarning()
|
||||
|
||||
fun refetchMsgRelays() = relays.refetchMsgRelays()
|
||||
|
||||
fun useDefaultMsgRelayList() = relays.useDefaultMsgRelayList()
|
||||
|
||||
fun loadCurrentUserRelayList() = relays.loadCurrentUserRelayList()
|
||||
|
||||
fun addInboxRelay(relay: String) = relays.addInboxRelay(relay)
|
||||
|
||||
fun addOutboxRelay(relay: String) = relays.addOutboxRelay(relay)
|
||||
|
||||
fun loadCurrentUserMsgRelayList() = relays.loadCurrentUserMsgRelayList()
|
||||
|
||||
fun addMsgRelay(relay: String) = relays.addMsgRelay(relay)
|
||||
|
||||
fun removeMsgRelay(relay: String) = relays.removeMsgRelay(relay)
|
||||
}
|
||||
Reference in New Issue
Block a user