This commit is contained in:
2026-07-12 15:45:06 +07:00
parent 77ff35fbf7
commit 2d9bcab946
21 changed files with 1195 additions and 1017 deletions

View File

@@ -26,7 +26,10 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.platform.LocalContext
import androidx.core.util.Consumer
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
@@ -35,6 +38,8 @@ import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import kotlinx.coroutines.launch
import su.reya.coop.repository.AccountRepository
import su.reya.coop.repository.ChatRepository
import su.reya.coop.screens.ContactListScreen
import su.reya.coop.screens.HomeScreen
import su.reya.coop.screens.ImportScreen
@@ -49,6 +54,7 @@ import su.reya.coop.screens.ScanScreen
import su.reya.coop.screens.UpdateProfileScreen
import su.reya.coop.screens.chat.ChatScreen
import su.reya.coop.viewmodel.AccountViewModel
import su.reya.coop.viewmodel.ChatScreenViewModel
import su.reya.coop.viewmodel.ChatViewModel
import su.reya.coop.viewmodel.ProfileCache
@@ -56,15 +62,6 @@ val LocalProfileCache = staticCompositionLocalOf<ProfileCache> {
error("No ProfileCache provided")
}
val LocalChatViewModel = staticCompositionLocalOf<ChatViewModel> {
error("No ChatViewModel provided")
}
val LocalAccountViewModel = staticCompositionLocalOf<AccountViewModel> {
error("No AccountViewModel provided")
}
val LocalSnackbarHostState = staticCompositionLocalOf<SnackbarHostState> {
error("No SnackbarHostState provided")
}
@@ -81,9 +78,32 @@ val LocalScanResult = staticCompositionLocalOf<QrScanResult> {
@Composable
fun App(
profileCache: ProfileCache,
chatViewModel: ChatViewModel,
accountViewModel: AccountViewModel,
accountRepository: AccountRepository,
chatRepository: ChatRepository,
) {
val viewModelFactory = remember {
object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val result = when {
modelClass.isAssignableFrom(ChatViewModel::class.java) -> ChatViewModel(
chatRepository
)
modelClass.isAssignableFrom(AccountViewModel::class.java) -> AccountViewModel(
accountRepository
)
else -> throw IllegalArgumentException("Unknown ViewModel class")
}
@Suppress("UNCHECKED_CAST")
return result as T
}
}
}
val accountViewModel: AccountViewModel = viewModel(factory = viewModelFactory)
val chatViewModel: ChatViewModel = viewModel(factory = viewModelFactory)
val context = LocalContext.current
val activity = context as? ComponentActivity
val backStack = rememberNavBackStack(Screen.Home)
@@ -141,7 +161,7 @@ fun App(
fun handleIntent(intent: Intent) {
val screen = Screen.fromIntent(intent)
// Prevent pushing the same screen
if (screen != null && backStack.lastOrNull() != screen) {
if ((screen != null) && (backStack.lastOrNull() != screen)) {
navigator.navigate(screen)
}
}
@@ -175,8 +195,6 @@ fun App(
) {
CompositionLocalProvider(
LocalProfileCache provides profileCache,
LocalChatViewModel provides chatViewModel,
LocalAccountViewModel provides accountViewModel,
LocalSnackbarHostState provides snackbarHostState,
LocalNavigator provides navigator,
LocalScanResult provides qrScanResult,
@@ -197,43 +215,62 @@ fun App(
),
entryProvider = entryProvider {
entry<Screen.Home> {
HomeScreen()
HomeScreen(accountViewModel, chatViewModel)
}
entry<Screen.RequestList> {
RequestListScreen()
RequestListScreen(chatViewModel)
}
entry<Screen.Onboarding> {
OnboardingScreen()
OnboardingScreen(accountViewModel)
}
entry<Screen.Import> {
ImportScreen()
ImportScreen(accountViewModel)
}
entry<Screen.NewIdentity> {
NewIdentityScreen()
NewIdentityScreen(accountViewModel)
}
entry<Screen.Chat> { key ->
ChatScreen(id = key.id, screening = key.screening)
val factory = remember(key) {
object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return ChatScreenViewModel(
key.id,
key.screening,
accountRepository,
chatRepository
) as T
}
}
}
ChatScreen(
viewModel<ChatScreenViewModel>(
key = key.id.toString(),
factory = factory
),
accountViewModel
)
}
entry<Screen.NewChat> {
NewChatScreen()
NewChatScreen(accountViewModel, chatViewModel)
}
entry<Screen.Profile> { key ->
ProfileScreen(pubkey = key.pubkey)
ProfileScreen(pubkey = key.pubkey, chatViewModel = chatViewModel)
}
entry<Screen.UpdateProfile> {
UpdateProfileScreen()
UpdateProfileScreen(accountViewModel)
}
entry<Screen.Scan> {
ScanScreen()
}
entry<Screen.MyQr> {
MyQrScreen()
MyQrScreen(accountViewModel)
}
entry<Screen.ContactList> {
ContactListScreen()
ContactListScreen(accountViewModel, chatViewModel)
}
entry<Screen.Relay> {
RelayScreen()
RelayScreen(accountViewModel)
}
}
)

View File

@@ -12,7 +12,10 @@ import androidx.activity.viewModels
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import kotlinx.coroutines.MainScope
import su.reya.coop.nostr.NostrManager
import su.reya.coop.repository.AccountRepository
import su.reya.coop.repository.ChatRepository
import su.reya.coop.repository.MediaRepository
import su.reya.coop.viewmodel.AccountViewModel
import su.reya.coop.viewmodel.ChatViewModel
@@ -25,28 +28,40 @@ class MainActivity : ComponentActivity() {
}
private val profileCache by lazy { ProfileCache(NostrManager.instance) }
private val scope = MainScope()
private val accountRepository by lazy {
val storage = AppStore(this@MainActivity)
val mediaRepository = MediaRepository()
val androidSigner = AndroidExternalSigner(this@MainActivity, externalSignerLauncher)
AccountRepository(NostrManager.instance, storage, mediaRepository, scope, androidSigner)
}
private val chatRepository by lazy {
val mediaRepository = MediaRepository()
ChatRepository(NostrManager.instance, mediaRepository, scope)
}
private val factory by lazy {
object : ViewModelProvider.Factory {
private val storage = AppStore(this@MainActivity)
private val mediaRepository = MediaRepository()
private val chatViewModel = ChatViewModel(NostrManager.instance, mediaRepository)
private val androidSigner =
AndroidExternalSigner(this@MainActivity, externalSignerLauncher)
private val accountViewModel =
AccountViewModel(NostrManager.instance, storage, mediaRepository, androidSigner)
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return when {
modelClass.isAssignableFrom(ChatViewModel::class.java) -> chatViewModel
modelClass.isAssignableFrom(AccountViewModel::class.java) -> accountViewModel
val result = when {
modelClass.isAssignableFrom(ChatViewModel::class.java) -> ChatViewModel(
chatRepository
)
modelClass.isAssignableFrom(AccountViewModel::class.java) -> AccountViewModel(
accountRepository
)
else -> throw IllegalArgumentException("Unknown ViewModel class")
} as T
}
@Suppress("UNCHECKED_CAST")
return result as T
}
}
}
private val chatViewModel: ChatViewModel by viewModels { factory }
private val accountViewModel: AccountViewModel by viewModels { factory }
override fun onCreate(savedInstanceState: Bundle?) {
@@ -92,8 +107,8 @@ class MainActivity : ComponentActivity() {
setContent {
App(
profileCache = profileCache,
chatViewModel = chatViewModel,
accountViewModel = accountViewModel,
accountRepository = accountRepository,
chatRepository = chatRepository,
)
}
}

View File

@@ -36,7 +36,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
@@ -54,28 +53,26 @@ import coop.composeapp.generated.resources.ic_check
import coop.composeapp.generated.resources.ic_close
import coop.composeapp.generated.resources.ic_plus
import coop.composeapp.generated.resources.ic_scanner
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.LocalProfileCache
import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.Screen
import su.reya.coop.shared.Avatar
import su.reya.coop.short
import su.reya.coop.viewmodel.AccountViewModel
import su.reya.coop.viewmodel.ChatViewModel
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ContactListScreen() {
fun ContactListScreen(
accountViewModel: AccountViewModel,
chatViewModel: ChatViewModel
) {
val navigator = LocalNavigator.current
val snackbarHostState = LocalSnackbarHostState.current
val profileCache = LocalProfileCache.current
val accountViewModel = LocalAccountViewModel.current
val chatViewModel = LocalChatViewModel.current
val contactList by accountViewModel.contactList.collectAsStateWithLifecycle()
var openAddContactDialog by remember { mutableStateOf(false) }
var contactToDelete by remember { mutableStateOf<PublicKey?>(null) }
@@ -192,7 +189,10 @@ fun ContactListScreen() {
)
if (openAddContactDialog) {
AddContactDialog(onDismissRequest = { openAddContactDialog = false })
AddContactDialog(
accountViewModel = accountViewModel,
onDismissRequest = { openAddContactDialog = false }
)
}
if (contactToDelete != null) {
@@ -221,16 +221,15 @@ fun ContactListScreen() {
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun AddContactDialog(onDismissRequest: () -> Unit) {
fun AddContactDialog(
accountViewModel: AccountViewModel,
onDismissRequest: () -> Unit,
) {
val snackbarHostState = LocalSnackbarHostState.current
val profileCache = LocalProfileCache.current
val accountViewModel = LocalAccountViewModel.current
val focusRequester = remember { FocusRequester() }
var contact by remember { mutableStateOf("") }
var isError by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}

View File

@@ -89,8 +89,6 @@ 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.LocalAccountViewModel
import su.reya.coop.LocalChatViewModel
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalProfileCache
import su.reya.coop.LocalScanResult
@@ -103,17 +101,20 @@ import su.reya.coop.ago
import su.reya.coop.shared.Avatar
import su.reya.coop.shared.getExpressiveFontFamily
import su.reya.coop.uiStateFlow
import su.reya.coop.viewmodel.AccountViewModel
import su.reya.coop.viewmodel.ChatViewModel
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen() {
fun HomeScreen(
accountViewModel: AccountViewModel,
chatViewModel: ChatViewModel
) {
val context = LocalContext.current
val navigator = LocalNavigator.current
val qrScanResult = LocalScanResult.current
val snackbarHostState = LocalSnackbarHostState.current
val clipboardManager = LocalClipboard.current
val chatViewModel = LocalChatViewModel.current
val accountViewModel = LocalAccountViewModel.current
val scope = rememberCoroutineScope()
val sheetState = rememberModalBottomSheetState(true)
@@ -459,7 +460,11 @@ fun HomeScreen() {
}
}
Spacer(modifier = Modifier.size(16.dp))
BottomMenuList(onDismiss = dismissAndRun)
BottomMenuList(
onDismiss = dismissAndRun,
accountViewModel = accountViewModel,
chatViewModel = chatViewModel
)
}
}
}
@@ -734,12 +739,11 @@ fun ChatRoom(room: Room, onClick: () -> Unit) {
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun BottomMenuList(
onDismiss: (suspend () -> Unit) -> Unit
onDismiss: (suspend () -> Unit) -> Unit,
accountViewModel: AccountViewModel,
chatViewModel: ChatViewModel,
) {
val navigator = LocalNavigator.current
val profileCache = LocalProfileCache.current
val chatViewModel = LocalChatViewModel.current
val accountViewModel = LocalAccountViewModel.current
val defaultMenuList = listOf(
"Update Profile" to { navigator.navigate(Screen.UpdateProfile) },

View File

@@ -50,23 +50,20 @@ 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.LocalAccountViewModel
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalScanResult
import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.Screen
import su.reya.coop.viewmodel.AccountViewModel
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ImportScreen() {
fun ImportScreen(viewModel: AccountViewModel) {
val snackbarHostState = LocalSnackbarHostState.current
val navigator = LocalNavigator.current
val qrScanResult = LocalScanResult.current
val focusManager = LocalFocusManager.current
val accountViewModel = LocalAccountViewModel.current
val accountState by accountViewModel.state.collectAsStateWithLifecycle()
val accountState by viewModel.state.collectAsStateWithLifecycle()
var secret by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var requirePassword by remember { mutableStateOf(false) }
@@ -250,7 +247,7 @@ fun ImportScreen() {
Spacer(modifier = Modifier.size(16.dp))
Button(
onClick = {
accountViewModel.importIdentity(secret, password)
viewModel.importIdentity(secret, password)
},
modifier = Modifier
.fillMaxWidth()

View File

@@ -21,16 +21,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.LocalSnackbarHostState
import su.reya.coop.viewmodel.AccountViewModel
@Composable
fun MyQrScreen() {
fun MyQrScreen(viewModel: AccountViewModel) {
val navigator = LocalNavigator.current
val snackbarHostState = LocalSnackbarHostState.current
val accountViewModel = LocalAccountViewModel.current
val currentUser by accountViewModel.currentUserProfile.collectAsStateWithLifecycle()
val currentUser by viewModel.currentUserProfile.collectAsStateWithLifecycle()
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },

View File

@@ -55,8 +55,6 @@ 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.LocalProfileCache
import su.reya.coop.LocalScanResult
@@ -64,18 +62,19 @@ import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.Screen
import su.reya.coop.shared.Avatar
import su.reya.coop.short
import su.reya.coop.viewmodel.AccountViewModel
import su.reya.coop.viewmodel.ChatViewModel
import kotlin.time.Duration.Companion.milliseconds
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun NewChatScreen() {
fun NewChatScreen(
accountViewModel: AccountViewModel,
chatViewModel: ChatViewModel
) {
val snackbarHostState = LocalSnackbarHostState.current
val navigator = LocalNavigator.current
val qrScanResult = LocalScanResult.current
val profileCache = LocalProfileCache.current
val accountViewModel = LocalAccountViewModel.current
val chatViewModel = LocalChatViewModel.current
val contactList by accountViewModel.contactList.collectAsStateWithLifecycle()
var query by remember { mutableStateOf("") }

View File

@@ -3,16 +3,15 @@ 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.Screen
import su.reya.coop.shared.ProfileEditor
import su.reya.coop.viewmodel.AccountViewModel
@Composable
fun NewIdentityScreen() {
val accountViewModel = LocalAccountViewModel.current
fun NewIdentityScreen(viewModel: AccountViewModel) {
val navigator = LocalNavigator.current
val accountState by accountViewModel.state.collectAsStateWithLifecycle()
val accountState by viewModel.state.collectAsStateWithLifecycle()
ProfileEditor(
title = "Create a new identity",
@@ -20,7 +19,7 @@ fun NewIdentityScreen() {
isBusy = accountState.isImporting,
onBack = { navigator.goBack() },
onConfirm = { name, bio, bytes, type ->
accountViewModel.createIdentity(name, bio, bytes, type)
viewModel.createIdentity(name, bio, bytes, type)
navigator.navigate(Screen.Home)
}
)

View File

@@ -43,26 +43,24 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle
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.LocalAccountViewModel
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.Screen
import su.reya.coop.shared.getExpressiveFontFamily
import su.reya.coop.viewmodel.AccountViewModel
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun OnboardingScreen() {
fun OnboardingScreen(viewModel: AccountViewModel) {
val context = LocalContext.current
val snackbarHostState = LocalSnackbarHostState.current
val navigator = LocalNavigator.current
val accountViewModel = LocalAccountViewModel.current
val accountState by accountViewModel.state.collectAsStateWithLifecycle()
val accountState by viewModel.state.collectAsStateWithLifecycle()
val scope = rememberCoroutineScope()
// Navigate to Home on successful external signer connection
@@ -176,10 +174,10 @@ fun OnboardingScreen() {
Spacer(modifier = Modifier.size(8.dp))
FilledTonalButton(
onClick = {
if (accountViewModel.isExternalSignerAvailable()) {
if (viewModel.isExternalSignerAvailable()) {
// Connect to the external signer
// TODO: show all available signers?
accountViewModel.connectExternalSigner()
viewModel.connectExternalSigner()
} else {
scope.launch {
val result = snackbarHostState.showSnackbar(

View File

@@ -44,7 +44,6 @@ 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.LocalProfileCache
import su.reya.coop.LocalSnackbarHostState
@@ -52,17 +51,20 @@ import su.reya.coop.Screen
import su.reya.coop.shared.Avatar
import su.reya.coop.shared.getExpressiveFontFamily
import su.reya.coop.short
import su.reya.coop.viewmodel.ChatViewModel
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ProfileScreen(pubkey: String) {
fun ProfileScreen(
pubkey: String,
chatViewModel: ChatViewModel
) {
val pubkey = runCatching { PublicKey.parse(pubkey) }.getOrNull() ?: return
val context = LocalContext.current
val snackbarHostState = LocalSnackbarHostState.current
val navigator = LocalNavigator.current
val profileCache = LocalProfileCache.current
val chatViewModel = LocalChatViewModel.current
val scope = rememberCoroutineScope()
val profileFlow = remember(pubkey) { profileCache.getMetadata(pubkey) }

View File

@@ -67,16 +67,13 @@ 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.LocalProfileCache
import su.reya.coop.LocalAccountViewModel
import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.viewmodel.AccountViewModel
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun RelayScreen() {
fun RelayScreen(viewModel: AccountViewModel) {
val navigator = LocalNavigator.current
val profileCache = LocalProfileCache.current
val accountViewModel = LocalAccountViewModel.current
val snackbarHostState = LocalSnackbarHostState.current
val scope = rememberCoroutineScope()
@@ -99,12 +96,12 @@ fun RelayScreen() {
var relayToDelete by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
accountViewModel.loadCurrentUserRelayList()
accountViewModel.loadCurrentUserMsgRelayList()
viewModel.loadCurrentUserRelayList()
viewModel.loadCurrentUserMsgRelayList()
}
val loadedRelayList by accountViewModel.currentUserRelayList.collectAsStateWithLifecycle()
val loadedMsgRelayList by accountViewModel.currentUserMsgRelayList.collectAsStateWithLifecycle()
val loadedRelayList by viewModel.currentUserRelayList.collectAsStateWithLifecycle()
val loadedMsgRelayList by viewModel.currentUserMsgRelayList.collectAsStateWithLifecycle()
LaunchedEffect(loadedRelayList) {
if (loadedRelayList.isNotEmpty()) {
@@ -316,6 +313,7 @@ fun RelayScreen() {
if (openAddRelayDialog) {
AddRelayDialog(
viewModel = viewModel,
onDismissRequest = { openAddRelayDialog = false },
onMsgRelayAdded = { newRelay ->
msgRelayList.add(RelayUrl.parse(newRelay))
@@ -341,7 +339,7 @@ fun RelayScreen() {
relayToDelete = null
return@TextButton
}
accountViewModel.removeMsgRelay(relayToDelete!!)
viewModel.removeMsgRelay(relayToDelete!!)
msgRelayList.removeIf { it.toString() == relayToDelete }
relayToDelete = null
}
@@ -361,14 +359,12 @@ fun RelayScreen() {
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun AddRelayDialog(
viewModel: AccountViewModel,
onDismissRequest: () -> Unit,
onMsgRelayAdded: (newRelay: String) -> Unit,
onRelayAdded: (newRelay: String, metadata: RelayMetadata?) -> Unit,
) {
val accountViewModel = LocalAccountViewModel.current
val snackbarHostState = LocalSnackbarHostState.current
val scope = rememberCoroutineScope()
val focusRequester = remember { FocusRequester() }
var relayAddress by remember { mutableStateOf("") }
@@ -416,17 +412,17 @@ fun AddRelayDialog(
if (!isError) {
when (selected) {
"Messaging" -> {
accountViewModel.addMsgRelay(relayAddress)
viewModel.addMsgRelay(relayAddress)
onMsgRelayAdded(relayAddress)
}
"Inbox" -> {
accountViewModel.addInboxRelay(relayAddress)
viewModel.addInboxRelay(relayAddress)
onRelayAdded(relayAddress, RelayMetadata.WRITE)
}
"Outbox" -> {
accountViewModel.addOutboxRelay(relayAddress)
viewModel.addOutboxRelay(relayAddress)
onRelayAdded(relayAddress, RelayMetadata.READ)
}
}

View File

@@ -27,7 +27,6 @@ import androidx.compose.runtime.Composable
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
@@ -36,26 +35,23 @@ 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 kotlinx.coroutines.launch
import su.reya.coop.LocalChatViewModel
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.RoomKind
import su.reya.coop.Screen
import su.reya.coop.viewmodel.ChatViewModel
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Composable
fun RequestListScreen() {
fun RequestListScreen(viewModel: ChatViewModel) {
val navigator = LocalNavigator.current
val snackbarHostState = LocalSnackbarHostState.current
val chatViewModel = LocalChatViewModel.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 viewModel.chatRooms.collectAsStateWithLifecycle()
// Get all request rooms
val requests = remember(chatRooms) {
@@ -102,7 +98,7 @@ fun RequestListScreen() {
state = pullToRefreshState,
onRefresh = {
isRefreshing = true
chatViewModel.refreshChatRooms()
viewModel.refreshChatRooms()
isRefreshing = false
},
indicator = {

View File

@@ -3,18 +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.shared.ProfileEditor
import su.reya.coop.viewmodel.AccountViewModel
@Composable
fun UpdateProfileScreen() {
val accountViewModel = LocalAccountViewModel.current
fun UpdateProfileScreen(viewModel: AccountViewModel) {
val navigator = LocalNavigator.current
val currentUser by accountViewModel.currentUserProfile.collectAsStateWithLifecycle()
val currentUser by viewModel.currentUserProfile.collectAsStateWithLifecycle()
val profile = currentUser?.metadata?.asRecord()
val isUpdatingProfile by accountViewModel.isUpdatingProfile.collectAsStateWithLifecycle()
val isUpdatingProfile by viewModel.isUpdatingProfile.collectAsStateWithLifecycle()
ProfileEditor(
title = "Update profile",
@@ -25,7 +23,7 @@ fun UpdateProfileScreen() {
isBusy = isUpdatingProfile,
onBack = { navigator.goBack() },
onConfirm = { name, bio, bytes, type ->
accountViewModel.updateProfile(name, bio, bytes, type)
viewModel.updateProfile(name, bio, bytes, type)
navigator.goBack()
}
)

View File

@@ -21,7 +21,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -31,21 +30,20 @@ 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.LocalProfileCache
import su.reya.coop.Room
import su.reya.coop.humanReadable
import su.reya.coop.shared.Avatar
import su.reya.coop.shared.getExpressiveFontFamily
import su.reya.coop.short
import su.reya.coop.viewmodel.AccountViewModel
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ScreenerCard(room: Room) {
fun ScreenerCard(viewModel: AccountViewModel, room: Room) {
val pubkey = room.members.firstOrNull() ?: return
val profileCache = LocalProfileCache.current
val accountViewModel = LocalAccountViewModel.current
var isContact by remember { mutableStateOf(false) }
var mutualContacts by remember { mutableStateOf<Set<PublicKey>>(emptySet()) }
@@ -56,11 +54,11 @@ fun ScreenerCard(room: Room) {
LaunchedEffect(pubkey) {
// Check contact
accountViewModel.verifyContact(pubkey) { isContact = it }
viewModel.verifyContact(pubkey) { isContact = it }
// Get mutual contacts
accountViewModel.mutualContacts(pubkey) { mutualContacts = it }
viewModel.mutualContacts(pubkey) { mutualContacts = it }
// Get the last activity
accountViewModel.verifyActivity(pubkey) { lastActivity = it }
viewModel.verifyActivity(pubkey) { lastActivity = it }
}
Column(

View File

@@ -45,8 +45,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -64,9 +62,6 @@ import kotlinx.coroutines.Dispatchers
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.LocalProfileCache
import su.reya.coop.LocalSnackbarHostState
@@ -74,31 +69,28 @@ import su.reya.coop.Room
import su.reya.coop.RoomUiState
import su.reya.coop.Screen
import su.reya.coop.formatAsGroupHeader
import su.reya.coop.uiStateFlow
import su.reya.coop.roomId
import su.reya.coop.shared.Avatar
import su.reya.coop.uiStateFlow
import su.reya.coop.viewmodel.AccountViewModel
import su.reya.coop.viewmodel.ChatScreenViewModel
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ChatScreen(
id: Long,
screening: Boolean = false,
viewModel: ChatScreenViewModel,
accountViewModel: AccountViewModel,
coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO,
) {
val context = LocalContext.current
val snackbarHostState = LocalSnackbarHostState.current
val navigator = LocalNavigator.current
val profileCache = LocalProfileCache.current
val chatViewModel = LocalChatViewModel.current
val scope = rememberCoroutineScope()
val listState = rememberLazyListState()
// Get current user
val accountViewModel = LocalAccountViewModel.current
val currentUser by accountViewModel.currentUserProfile.collectAsStateWithLifecycle()
// Get chat room by ID
val chatRooms by chatViewModel.chatRooms.collectAsStateWithLifecycle()
val id = viewModel.id
val currentUser by viewModel.currentUser.collectAsStateWithLifecycle()
val chatRooms by viewModel.chatRooms.collectAsStateWithLifecycle()
val room by remember(id) { derivedStateOf { chatRooms.firstOrNull { it.id == id } } }
// Show empty screen
@@ -118,12 +110,13 @@ fun ChatScreen(
val roomState by (room as Room).uiStateFlow(profileCache, currentUser?.publicKey)
.collectAsStateWithLifecycle(RoomUiState())
var text by remember { mutableStateOf("") }
var loading by remember { mutableStateOf(true) }
var newOtherMessages by remember { mutableIntStateOf(0) }
var requireScreening by remember { mutableStateOf(screening) }
val messages = remember { mutableStateListOf<UnsignedEvent>() }
var text by remember { mutableStateOf("") }
val loading = viewModel.loading
val newOtherMessages = viewModel.newOtherMessages
val requireScreening = viewModel.requireScreening
val messages = viewModel.messages
val groupedMessages =
remember { derivedStateOf { messages.groupBy { it.createdAt().formatAsGroupHeader() } } }
@@ -138,7 +131,7 @@ fun ChatScreen(
val type = context.contentResolver.getType(uri)
// Send message (handles errors internally via ViewModel)
chatViewModel.sendFileMessage(id, file, type)
viewModel.sendFileMessage(file, type)
}
}
@@ -157,29 +150,6 @@ fun ChatScreen(
}
LaunchedEffect(id) {
// Get messages
chatViewModel.loadChatRoomMessages(id) { initialMessages ->
messages.clear()
messages.addAll(initialMessages)
// Stop loading spinner
loading = false
}
// Get msg relays for each member
chatViewModel.chatRoomConnect(id)
// Handle new messages
chatViewModel.newEvents.collect { event ->
if (event.roomId() == id) {
if (event.id() !in messages.map { it.id() }) {
messages.add(0, event)
}
} else {
// If the event is not in the current room, it's a new message from another user
newOtherMessages++
}
}
}
LaunchedEffect(messages.size) {
@@ -256,7 +226,7 @@ fun ChatScreen(
.padding(bottom = innerPadding.calculateBottomPadding())
) {
if (requireScreening) {
room?.let { ScreenerCard(it) }
room?.let { ScreenerCard(accountViewModel, it) }
}
val mineColor = MaterialTheme.colorScheme.onPrimaryContainer
@@ -340,7 +310,7 @@ fun ChatScreen(
)
}
FilledTonalButton(
onClick = { requireScreening = false },
onClick = { viewModel.requireScreening = false },
modifier = Modifier
.weight(1f)
.size(ButtonDefaults.MediumContainerHeight)
@@ -358,7 +328,7 @@ fun ChatScreen(
value = text,
onValueChange = { text = it },
onSend = {
chatViewModel.sendMessage(id, text)
viewModel.sendMessage(text)
text = ""
},
onUpload = {