clean up view models
This commit is contained in:
@@ -34,7 +34,7 @@ import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import su.reya.coop.repository.ErrorRepository
|
||||
import kotlinx.coroutines.launch
|
||||
import su.reya.coop.screens.chat.ChatScreen
|
||||
import su.reya.coop.screens.ContactListScreen
|
||||
import su.reya.coop.screens.HomeScreen
|
||||
@@ -118,8 +118,20 @@ fun App(
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
ErrorRepository.errors.collect { message ->
|
||||
snackbarHostState.showSnackbar(message)
|
||||
launch {
|
||||
authViewModel.errorEvents.collect { message ->
|
||||
snackbarHostState.showSnackbar(message)
|
||||
}
|
||||
}
|
||||
launch {
|
||||
chatViewModel.errorEvents.collect { message ->
|
||||
snackbarHostState.showSnackbar(message)
|
||||
}
|
||||
}
|
||||
launch {
|
||||
nostrViewModel.errorEvents.collect { message ->
|
||||
snackbarHostState.showSnackbar(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
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
|
||||
@@ -26,12 +27,13 @@ class MainActivity : ComponentActivity() {
|
||||
private val factory by lazy {
|
||||
object : ViewModelProvider.Factory {
|
||||
private val storage = AppStore(this@MainActivity)
|
||||
private val nostrViewModel = NostrViewModel(NostrManager.instance)
|
||||
private val chatViewModel = ChatViewModel(NostrManager.instance)
|
||||
private val mediaRepository = MediaRepository()
|
||||
private val nostrViewModel = NostrViewModel(NostrManager.instance, mediaRepository)
|
||||
private val chatViewModel = ChatViewModel(NostrManager.instance, mediaRepository)
|
||||
private val androidSigner =
|
||||
AndroidExternalSigner(this@MainActivity, externalSignerLauncher)
|
||||
private val authViewModel =
|
||||
AuthViewModel(NostrManager.instance, storage, androidSigner)
|
||||
AuthViewModel(NostrManager.instance, storage, mediaRepository, androidSigner)
|
||||
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return when {
|
||||
|
||||
@@ -34,7 +34,6 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -44,10 +43,10 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coop.composeapp.generated.resources.Res
|
||||
import coop.composeapp.generated.resources.ic_arrow_back
|
||||
import coop.composeapp.generated.resources.ic_scanner
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import rust.nostr.sdk.Keys
|
||||
import rust.nostr.sdk.NostrConnectUri
|
||||
@@ -65,12 +64,12 @@ fun ImportScreen() {
|
||||
val qrScanResult = LocalScanResult.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val authViewModel = LocalAuthViewModel.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val authState by authViewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
var secret by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
var requirePassword by remember { mutableStateOf(false) }
|
||||
var loading by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(qrScanResult.content) {
|
||||
qrScanResult.content?.let { result ->
|
||||
@@ -98,6 +97,20 @@ fun ImportScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate to Home on successful import (signerRequired becomes false)
|
||||
LaunchedEffect(authState.signerRequired) {
|
||||
if (authState.signerRequired == false) {
|
||||
navigator.navigate(Screen.Home)
|
||||
}
|
||||
}
|
||||
|
||||
// Show import errors via snackbar
|
||||
LaunchedEffect(authState.importError) {
|
||||
authState.importError?.let {
|
||||
snackbarHostState.showSnackbar(it)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
@@ -164,7 +177,7 @@ fun ImportScreen() {
|
||||
BasicTextField(
|
||||
value = secret,
|
||||
onValueChange = { secret = it },
|
||||
enabled = !loading,
|
||||
enabled = !authState.isImporting,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
@@ -209,7 +222,7 @@ fun ImportScreen() {
|
||||
BasicTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
enabled = !loading && requirePassword,
|
||||
enabled = !authState.isImporting && requirePassword,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
@@ -237,25 +250,14 @@ fun ImportScreen() {
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
loading = true
|
||||
try {
|
||||
// Import the identity
|
||||
authViewModel.importIdentity(secret, password)
|
||||
// Navigate to the home screen
|
||||
navigator.navigate(Screen.Home)
|
||||
} catch (e: Exception) {
|
||||
snackbarHostState.showSnackbar(e.message ?: "Error")
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
authViewModel.importIdentity(secret, password)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(ButtonDefaults.MediumContainerHeight),
|
||||
enabled = secret.isNotBlank() && !loading,
|
||||
enabled = secret.isNotBlank() && !authState.isImporting,
|
||||
) {
|
||||
if (loading) {
|
||||
if (authState.isImporting) {
|
||||
LoadingIndicator()
|
||||
} else {
|
||||
Text(
|
||||
|
||||
@@ -24,6 +24,8 @@ import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
@@ -44,6 +46,7 @@ import androidx.core.net.toUri
|
||||
import coop.composeapp.generated.resources.Res
|
||||
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.LocalNavigator
|
||||
@@ -58,8 +61,24 @@ fun OnboardingScreen() {
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
val navigator = LocalNavigator.current
|
||||
val authViewModel = LocalAuthViewModel.current
|
||||
|
||||
val authState by authViewModel.state.collectAsStateWithLifecycle()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// Navigate to Home on successful external signer connection
|
||||
LaunchedEffect(authState.signerRequired) {
|
||||
if (authState.signerRequired == false) {
|
||||
navigator.navigate(Screen.Home)
|
||||
}
|
||||
}
|
||||
|
||||
// Show connection errors
|
||||
LaunchedEffect(authState.importError) {
|
||||
authState.importError?.let {
|
||||
snackbarHostState.showSnackbar(it)
|
||||
}
|
||||
}
|
||||
|
||||
val logoPainter = painterResource(Res.drawable.coop)
|
||||
val expressiveFont = getExpressiveFontFamily()
|
||||
|
||||
@@ -157,18 +176,12 @@ fun OnboardingScreen() {
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
FilledTonalButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
if (authViewModel.isExternalSignerAvailable()) {
|
||||
try {
|
||||
// Connect to the external signer
|
||||
// TODO: show all available signers?
|
||||
authViewModel.connectExternalSigner()
|
||||
// Navigate to the home screen
|
||||
navigator.navigate(Screen.Home)
|
||||
} catch (e: Exception) {
|
||||
e.message?.let { snackbarHostState.showSnackbar(it) }
|
||||
}
|
||||
} else {
|
||||
if (authViewModel.isExternalSignerAvailable()) {
|
||||
// Connect to the external signer
|
||||
// TODO: show all available signers?
|
||||
authViewModel.connectExternalSigner()
|
||||
} else {
|
||||
scope.launch {
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
message = "External signer not installed. Please install Amber or alternatives.",
|
||||
actionLabel = "Install",
|
||||
|
||||
@@ -120,20 +120,16 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
|
||||
|
||||
val sendFile = { uri: Uri ->
|
||||
scope.launch {
|
||||
try {
|
||||
// Read file
|
||||
val file = withContext(Dispatchers.IO) {
|
||||
context.contentResolver.openInputStream(uri)?.use { it.readBytes() }
|
||||
}
|
||||
|
||||
// Parse the file content type
|
||||
val type = context.contentResolver.getType(uri)
|
||||
|
||||
// Send message
|
||||
chatViewModel.sendFileMessage(id, file, type)
|
||||
} catch (e: Exception) {
|
||||
snackbarHostState.showSnackbar("Error: ${e.message}")
|
||||
// Read file on IO dispatcher
|
||||
val file = withContext(Dispatchers.IO) {
|
||||
context.contentResolver.openInputStream(uri)?.use { it.readBytes() }
|
||||
}
|
||||
|
||||
// Parse the file content type
|
||||
val type = context.contentResolver.getType(uri)
|
||||
|
||||
// Send message (handles errors internally via ViewModel)
|
||||
chatViewModel.sendFileMessage(id, file, type)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ fun ProfileEditor(
|
||||
initialBio: String = "",
|
||||
initialPicture: Any? = null, // Accepts Uri (picked) or String (current URL)
|
||||
onBack: () -> Unit,
|
||||
onConfirm: suspend (name: String, bio: String, pictureBytes: ByteArray?, contentType: String?) -> Unit
|
||||
onConfirm: (name: String, bio: String, pictureBytes: ByteArray?, contentType: String?) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
|
||||
Reference in New Issue
Block a user