chore: clean up codebase (#42)

Reviewed-on: #42
This commit was merged in pull request #42.
This commit is contained in:
2026-07-13 01:04:15 +00:00
parent a5951e03cf
commit 410615bd89
32 changed files with 1360 additions and 1466 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
@@ -48,23 +53,15 @@ import su.reya.coop.screens.RequestListScreen
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.NostrViewModel
import su.reya.coop.viewmodel.account.AccountViewModel
import su.reya.coop.viewmodel.ProfileCache
val LocalNostrViewModel = staticCompositionLocalOf<NostrViewModel> {
error("No NostrViewModel provided")
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")
}
@@ -80,10 +77,33 @@ val LocalScanResult = staticCompositionLocalOf<QrScanResult> {
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Composable
fun App(
nostrViewModel: NostrViewModel,
chatViewModel: ChatViewModel,
accountViewModel: AccountViewModel,
profileCache: ProfileCache,
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)
@@ -130,7 +150,7 @@ fun App(
}
}
launch {
nostrViewModel.errorEvents.collect { message ->
profileCache.errorEvents.collect { message ->
snackbarHostState.showSnackbar(message)
}
}
@@ -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)
}
}
@@ -174,9 +194,7 @@ fun App(
motionScheme = MotionScheme.expressive(),
) {
CompositionLocalProvider(
LocalNostrViewModel provides nostrViewModel,
LocalChatViewModel provides chatViewModel,
LocalAccountViewModel provides accountViewModel,
LocalProfileCache provides profileCache,
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

@@ -8,15 +8,13 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
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.ChatViewModel
import su.reya.coop.viewmodel.NostrViewModel
import su.reya.coop.viewmodel.account.AccountViewModel
import su.reya.coop.viewmodel.ProfileCache
import kotlin.system.exitProcess
class MainActivity : ComponentActivity() {
@@ -24,31 +22,20 @@ class MainActivity : ComponentActivity() {
val externalSignerLauncher = ExternalSignerLauncher()
}
private val factory by lazy {
object : ViewModelProvider.Factory {
private val storage = AppStore(this@MainActivity)
private val mediaRepository = MediaRepository()
private val nostrViewModel = NostrViewModel(NostrManager.instance)
private val chatViewModel = ChatViewModel(NostrManager.instance, mediaRepository)
private val androidSigner =
AndroidExternalSigner(this@MainActivity, externalSignerLauncher)
private val accountViewModel =
AccountViewModel(NostrManager.instance, storage, mediaRepository, androidSigner)
private val profileCache by lazy { ProfileCache(NostrManager.instance) }
private val scope = MainScope()
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return when {
modelClass.isAssignableFrom(NostrViewModel::class.java) -> nostrViewModel
modelClass.isAssignableFrom(ChatViewModel::class.java) -> chatViewModel
modelClass.isAssignableFrom(AccountViewModel::class.java) -> accountViewModel
else -> throw IllegalArgumentException("Unknown ViewModel class")
} as T
}
}
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 nostrViewModel: NostrViewModel by viewModels { factory }
private val chatViewModel: ChatViewModel by viewModels { factory }
private val accountViewModel: AccountViewModel by viewModels { factory }
private val chatRepository by lazy {
val mediaRepository = MediaRepository()
ChatRepository(NostrManager.instance, mediaRepository, scope)
}
override fun onCreate(savedInstanceState: Bundle?) {
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
@@ -87,14 +74,14 @@ class MainActivity : ComponentActivity() {
// Keep the splash screen visible until the signer check is complete
splashScreen.setKeepOnScreenCondition {
accountViewModel.state.value.signerRequired == null
accountRepository.state.value.signerRequired == null
}
setContent {
App(
nostrViewModel = nostrViewModel,
chatViewModel = chatViewModel,
accountViewModel = accountViewModel,
profileCache = profileCache,
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.LocalNostrViewModel
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 nostrViewModel = LocalNostrViewModel.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 nostrViewModel = LocalNostrViewModel.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()
}
@@ -326,8 +325,8 @@ fun ContactListItem(
onClick: () -> Unit,
onLongClick: () -> Unit,
) {
val nostrViewModel = LocalNostrViewModel.current
val profileFlow = remember(pubkey) { nostrViewModel.getMetadata(pubkey) }
val profileCache = LocalProfileCache.current
val profileFlow = remember(pubkey) { profileCache.getMetadata(pubkey) }
val profile by profileFlow.collectAsStateWithLifecycle(initialValue = null)
SegmentedListItem(

View File

@@ -89,31 +89,32 @@ 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.LocalNostrViewModel
import su.reya.coop.LocalProfileCache
import su.reya.coop.LocalScanResult
import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.Room
import su.reya.coop.RoomKind
import su.reya.coop.RoomUiState
import su.reya.coop.Screen
import su.reya.coop.ago
import su.reya.coop.rememberUiState
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 nostrViewModel = LocalNostrViewModel.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
)
}
}
}
@@ -609,14 +614,16 @@ fun HomeScreen() {
@Composable
fun NewRequests(requests: List<Room>) {
val navigator = LocalNavigator.current
val nostrViewModel = LocalNostrViewModel.current
val profileCache = LocalProfileCache.current
val total = requests.size
val firstRoom = requests.getOrNull(0)
val secondRoom = requests.getOrNull(1)
val firstRoomState by (firstRoom as Room).rememberUiState(nostrViewModel)
val secondRoomState by (secondRoom ?: firstRoom).rememberUiState(nostrViewModel)
val firstRoomState by (firstRoom as Room).uiStateFlow(profileCache)
.collectAsStateWithLifecycle(RoomUiState())
val secondRoomState by (secondRoom ?: firstRoom).uiStateFlow(profileCache)
.collectAsStateWithLifecycle(RoomUiState())
val supportingText = when {
total == 1 -> {
@@ -687,8 +694,9 @@ fun NewRequests(requests: List<Room>) {
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ChatRoom(room: Room, onClick: () -> Unit) {
val nostrViewModel = LocalNostrViewModel.current
val roomState by room.rememberUiState(nostrViewModel)
val profileCache = LocalProfileCache.current
val roomState by room.uiStateFlow(profileCache)
.collectAsStateWithLifecycle(RoomUiState())
ListItem(
modifier = Modifier.clickable(onClick = onClick),
@@ -731,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 nostrViewModel = LocalNostrViewModel.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,27 +55,26 @@ 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
import su.reya.coop.LocalProfileCache
import su.reya.coop.LocalScanResult
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 nostrViewModel = LocalNostrViewModel.current
val accountViewModel = LocalAccountViewModel.current
val chatViewModel = LocalChatViewModel.current
val contactList by accountViewModel.contactList.collectAsStateWithLifecycle()
var query by remember { mutableStateOf("") }
@@ -294,8 +293,8 @@ fun ReceiverChip(
pubkey: PublicKey,
onRemove: () -> Unit
) {
val nostrViewModel = LocalNostrViewModel.current
val profileFlow = remember(pubkey) { nostrViewModel.getMetadata(pubkey) }
val profileCache = LocalProfileCache.current
val profileFlow = remember(pubkey) { profileCache.getMetadata(pubkey) }
val profile by profileFlow.collectAsState(initial = null)
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
@@ -376,8 +375,8 @@ fun ContactListItem(
onClick: () -> Unit,
onLongClick: () -> Unit
) {
val nostrViewModel = LocalNostrViewModel.current
val profileFlow = remember(pubkey) { nostrViewModel.getMetadata(pubkey) }
val profileCache = LocalProfileCache.current
val profileFlow = remember(pubkey) { profileCache.getMetadata(pubkey) }
val profile by profileFlow.collectAsState(initial = null)
SegmentedListItem(

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,28 +44,30 @@ 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.LocalNostrViewModel
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.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 nostrViewModel = LocalNostrViewModel.current
val chatViewModel = LocalChatViewModel.current
val profileCache = LocalProfileCache.current
val scope = rememberCoroutineScope()
val profileFlow = remember(pubkey) { nostrViewModel.getMetadata(pubkey) }
val profileFlow = remember(pubkey) { profileCache.getMetadata(pubkey) }
val profile by profileFlow.collectAsStateWithLifecycle()
val metadata = profile?.metadata?.asRecord()

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.LocalNostrViewModel
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 nostrViewModel = LocalNostrViewModel.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.userRelayList.collectAsStateWithLifecycle()
val loadedMsgRelayList by viewModel.userMsgRelayList.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,36 +30,35 @@ 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.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 nostrViewModel = LocalNostrViewModel.current
val accountViewModel = LocalAccountViewModel.current
val profileCache = LocalProfileCache.current
var isContact by remember { mutableStateOf(false) }
var mutualContacts by remember { mutableStateOf<Set<PublicKey>>(emptySet()) }
var lastActivity by remember { mutableStateOf<Timestamp?>(null) }
val profileFlow = remember(pubkey) { nostrViewModel.getMetadata(pubkey) }
val profileFlow = remember(pubkey) { profileCache.getMetadata(pubkey) }
val profile by profileFlow.collectAsStateWithLifecycle()
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,40 +62,35 @@ 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.LocalNostrViewModel
import su.reya.coop.LocalProfileCache
import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.Room
import su.reya.coop.RoomUiState
import su.reya.coop.Screen
import su.reya.coop.formatAsGroupHeader
import su.reya.coop.rememberUiState
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 nostrViewModel = LocalNostrViewModel.current
val chatViewModel = LocalChatViewModel.current
val profileCache = LocalProfileCache.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
@@ -115,13 +108,15 @@ fun ChatScreen(
return
}
val roomState by (room as Room).rememberUiState(nostrViewModel, currentUser?.publicKey)
var text by remember { mutableStateOf("") }
var loading by remember { mutableStateOf(true) }
var newOtherMessages by remember { mutableIntStateOf(0) }
var requireScreening by remember { mutableStateOf(screening) }
val roomState by (room as Room).uiStateFlow(profileCache, currentUser?.publicKey)
.collectAsStateWithLifecycle(RoomUiState())
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() } } }
@@ -136,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)
}
}
@@ -155,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) {
@@ -254,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
@@ -338,7 +310,7 @@ fun ChatScreen(
)
}
FilledTonalButton(
onClick = { requireScreening = false },
onClick = { viewModel.requireScreening = false },
modifier = Modifier
.weight(1f)
.size(ButtonDefaults.MediumContainerHeight)
@@ -356,7 +328,7 @@ fun ChatScreen(
value = text,
onValueChange = { text = it },
onSend = {
chatViewModel.sendMessage(id, text)
viewModel.sendMessage(text)
text = ""
},
onUpload = {