Compare commits
4 Commits
v0.2.5
...
6bb4ef2805
| Author | SHA1 | Date | |
|---|---|---|---|
| 6bb4ef2805 | |||
| e4e73da229 | |||
| 255c840847 | |||
| 9defad522c |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,3 +17,4 @@ captures
|
|||||||
!*.xcworkspace/contents.xcworkspacedata
|
!*.xcworkspace/contents.xcworkspacedata
|
||||||
**/xcshareddata/WorkspaceSettings.xcsettings
|
**/xcshareddata/WorkspaceSettings.xcsettings
|
||||||
node_modules/
|
node_modules/
|
||||||
|
.artifacts/
|
||||||
@@ -4,7 +4,6 @@ import android.app.Activity
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
@@ -40,6 +39,7 @@ import androidx.navigation3.ui.NavDisplay
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import su.reya.coop.repository.AccountRepository
|
import su.reya.coop.repository.AccountRepository
|
||||||
import su.reya.coop.repository.ChatRepository
|
import su.reya.coop.repository.ChatRepository
|
||||||
|
import su.reya.coop.repository.SettingsRepository
|
||||||
import su.reya.coop.screens.ContactListScreen
|
import su.reya.coop.screens.ContactListScreen
|
||||||
import su.reya.coop.screens.HomeScreen
|
import su.reya.coop.screens.HomeScreen
|
||||||
import su.reya.coop.screens.ImportScreen
|
import su.reya.coop.screens.ImportScreen
|
||||||
@@ -57,11 +57,16 @@ import su.reya.coop.viewmodel.AccountViewModel
|
|||||||
import su.reya.coop.viewmodel.ChatScreenViewModel
|
import su.reya.coop.viewmodel.ChatScreenViewModel
|
||||||
import su.reya.coop.viewmodel.ChatViewModel
|
import su.reya.coop.viewmodel.ChatViewModel
|
||||||
import su.reya.coop.viewmodel.ProfileCache
|
import su.reya.coop.viewmodel.ProfileCache
|
||||||
|
import su.reya.coop.viewmodel.SettingsViewModel
|
||||||
|
|
||||||
val LocalProfileCache = staticCompositionLocalOf<ProfileCache> {
|
val LocalProfileCache = staticCompositionLocalOf<ProfileCache> {
|
||||||
error("No ProfileCache provided")
|
error("No ProfileCache provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val LocalSettings = staticCompositionLocalOf<Settings> {
|
||||||
|
error("No Settings provided")
|
||||||
|
}
|
||||||
|
|
||||||
val LocalSnackbarHostState = staticCompositionLocalOf<SnackbarHostState> {
|
val LocalSnackbarHostState = staticCompositionLocalOf<SnackbarHostState> {
|
||||||
error("No SnackbarHostState provided")
|
error("No SnackbarHostState provided")
|
||||||
}
|
}
|
||||||
@@ -80,6 +85,7 @@ fun App(
|
|||||||
profileCache: ProfileCache,
|
profileCache: ProfileCache,
|
||||||
accountRepository: AccountRepository,
|
accountRepository: AccountRepository,
|
||||||
chatRepository: ChatRepository,
|
chatRepository: ChatRepository,
|
||||||
|
settingsRepository: SettingsRepository,
|
||||||
) {
|
) {
|
||||||
val viewModelFactory = remember {
|
val viewModelFactory = remember {
|
||||||
object : ViewModelProvider.Factory {
|
object : ViewModelProvider.Factory {
|
||||||
@@ -93,6 +99,10 @@ fun App(
|
|||||||
accountRepository
|
accountRepository
|
||||||
)
|
)
|
||||||
|
|
||||||
|
modelClass.isAssignableFrom(SettingsViewModel::class.java) -> SettingsViewModel(
|
||||||
|
settingsRepository
|
||||||
|
)
|
||||||
|
|
||||||
else -> throw IllegalArgumentException("Unknown ViewModel class")
|
else -> throw IllegalArgumentException("Unknown ViewModel class")
|
||||||
}
|
}
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
@@ -103,6 +113,7 @@ fun App(
|
|||||||
|
|
||||||
val accountViewModel: AccountViewModel = viewModel(factory = viewModelFactory)
|
val accountViewModel: AccountViewModel = viewModel(factory = viewModelFactory)
|
||||||
val chatViewModel: ChatViewModel = viewModel(factory = viewModelFactory)
|
val chatViewModel: ChatViewModel = viewModel(factory = viewModelFactory)
|
||||||
|
val settingsViewModel: SettingsViewModel = viewModel(factory = viewModelFactory)
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val activity = context as? ComponentActivity
|
val activity = context as? ComponentActivity
|
||||||
@@ -114,19 +125,24 @@ fun App(
|
|||||||
val accountState by accountViewModel.state.collectAsStateWithLifecycle()
|
val accountState by accountViewModel.state.collectAsStateWithLifecycle()
|
||||||
val signerRequired = accountState.signerRequired
|
val signerRequired = accountState.signerRequired
|
||||||
|
|
||||||
|
// Get the settings
|
||||||
|
val settings by settingsViewModel.settings.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
// Snackbar
|
// Snackbar
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
// Check if dark theme enabled
|
// Check if dark theme enabled
|
||||||
val darkMode = isSystemInDarkTheme()
|
val darkMode = when (settings.theme) {
|
||||||
|
Theme.Light -> false
|
||||||
|
Theme.Dark -> true
|
||||||
|
Theme.System -> isSystemInDarkTheme()
|
||||||
|
}
|
||||||
|
|
||||||
// Enabled the dynamic color scheme
|
// Enabled the dynamic color scheme
|
||||||
val colorScheme = when {
|
val colorScheme = when {
|
||||||
// Enable the dynamic color scheme for Android 12+
|
// Enable the dynamic color scheme for Android 12+
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
settings.dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||||
if (isSystemInDarkTheme()) dynamicDarkColorScheme(context) else dynamicLightColorScheme(
|
if (darkMode) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||||
context
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
// When dark mode is enabled, use the dark color scheme
|
// When dark mode is enabled, use the dark color scheme
|
||||||
darkMode -> darkColorScheme()
|
darkMode -> darkColorScheme()
|
||||||
@@ -134,10 +150,6 @@ fun App(
|
|||||||
else -> expressiveLightColorScheme()
|
else -> expressiveLightColorScheme()
|
||||||
}
|
}
|
||||||
|
|
||||||
BackHandler(enabled = backStack.size > 1) {
|
|
||||||
navigator.goBack()
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
launch {
|
launch {
|
||||||
accountViewModel.errorEvents.collect { message ->
|
accountViewModel.errorEvents.collect { message ->
|
||||||
@@ -195,11 +207,11 @@ fun App(
|
|||||||
) {
|
) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalProfileCache provides profileCache,
|
LocalProfileCache provides profileCache,
|
||||||
|
LocalSettings provides settings,
|
||||||
LocalSnackbarHostState provides snackbarHostState,
|
LocalSnackbarHostState provides snackbarHostState,
|
||||||
LocalNavigator provides navigator,
|
LocalNavigator provides navigator,
|
||||||
LocalScanResult provides qrScanResult,
|
LocalScanResult provides qrScanResult,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
NavDisplay(
|
NavDisplay(
|
||||||
backStack = backStack,
|
backStack = backStack,
|
||||||
onBack = {
|
onBack = {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import su.reya.coop.nostr.NostrManager
|
|||||||
import su.reya.coop.repository.AccountRepository
|
import su.reya.coop.repository.AccountRepository
|
||||||
import su.reya.coop.repository.ChatRepository
|
import su.reya.coop.repository.ChatRepository
|
||||||
import su.reya.coop.repository.MediaRepository
|
import su.reya.coop.repository.MediaRepository
|
||||||
|
import su.reya.coop.repository.SettingsRepository
|
||||||
import su.reya.coop.viewmodel.ProfileCache
|
import su.reya.coop.viewmodel.ProfileCache
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
@@ -25,16 +26,28 @@ class MainActivity : ComponentActivity() {
|
|||||||
private val profileCache by lazy { ProfileCache(NostrManager.instance) }
|
private val profileCache by lazy { ProfileCache(NostrManager.instance) }
|
||||||
private val scope = MainScope()
|
private val scope = MainScope()
|
||||||
|
|
||||||
|
private val settingsRepository by lazy {
|
||||||
|
val storage = AppStore(this@MainActivity)
|
||||||
|
SettingsRepository(storage, scope)
|
||||||
|
}
|
||||||
|
|
||||||
private val accountRepository by lazy {
|
private val accountRepository by lazy {
|
||||||
val storage = AppStore(this@MainActivity)
|
val storage = AppStore(this@MainActivity)
|
||||||
val mediaRepository = MediaRepository()
|
val mediaRepository = MediaRepository(settingsRepository)
|
||||||
val androidSigner = AndroidExternalSigner(this@MainActivity, externalSignerLauncher)
|
val androidSigner = AndroidExternalSigner(this@MainActivity, externalSignerLauncher)
|
||||||
AccountRepository(NostrManager.instance, storage, mediaRepository, scope, androidSigner)
|
AccountRepository(
|
||||||
|
NostrManager.instance,
|
||||||
|
storage,
|
||||||
|
mediaRepository,
|
||||||
|
settingsRepository,
|
||||||
|
scope,
|
||||||
|
androidSigner
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val chatRepository by lazy {
|
private val chatRepository by lazy {
|
||||||
val mediaRepository = MediaRepository()
|
val mediaRepository = MediaRepository(settingsRepository)
|
||||||
ChatRepository(NostrManager.instance, mediaRepository, scope)
|
ChatRepository(NostrManager.instance, mediaRepository, settingsRepository, scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -82,6 +95,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
profileCache = profileCache,
|
profileCache = profileCache,
|
||||||
accountRepository = accountRepository,
|
accountRepository = accountRepository,
|
||||||
chatRepository = chatRepository,
|
chatRepository = chatRepository,
|
||||||
|
settingsRepository = settingsRepository,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,8 @@ fun ContactListScreen(
|
|||||||
) {
|
) {
|
||||||
val navigator = LocalNavigator.current
|
val navigator = LocalNavigator.current
|
||||||
val snackbarHostState = LocalSnackbarHostState.current
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
val contactList by accountViewModel.contactList.collectAsStateWithLifecycle()
|
val accountState by accountViewModel.state.collectAsStateWithLifecycle()
|
||||||
|
val contactList = accountState.contactList
|
||||||
var openAddContactDialog by remember { mutableStateOf(false) }
|
var openAddContactDialog by remember { mutableStateOf(false) }
|
||||||
var contactToDelete by remember { mutableStateOf<PublicKey?>(null) }
|
var contactToDelete by remember { mutableStateOf<PublicKey?>(null) }
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.os.Build
|
|||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -86,6 +87,7 @@ import coop.composeapp.generated.resources.ic_new_chat
|
|||||||
import coop.composeapp.generated.resources.ic_qr
|
import coop.composeapp.generated.resources.ic_qr
|
||||||
import coop.composeapp.generated.resources.ic_request
|
import coop.composeapp.generated.resources.ic_request
|
||||||
import coop.composeapp.generated.resources.ic_scanner
|
import coop.composeapp.generated.resources.ic_scanner
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jetbrains.compose.resources.painterResource
|
import org.jetbrains.compose.resources.painterResource
|
||||||
import rust.nostr.sdk.PublicKey
|
import rust.nostr.sdk.PublicKey
|
||||||
@@ -124,13 +126,13 @@ fun HomeScreen(
|
|||||||
val userProfile by accountViewModel.currentUserProfile.collectAsStateWithLifecycle()
|
val userProfile by accountViewModel.currentUserProfile.collectAsStateWithLifecycle()
|
||||||
val chatRooms by chatViewModel.chatRooms.collectAsStateWithLifecycle()
|
val chatRooms by chatViewModel.chatRooms.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val isRelayListEmpty by accountViewModel.isRelayListEmpty.collectAsStateWithLifecycle()
|
val accountState by accountViewModel.state.collectAsStateWithLifecycle()
|
||||||
|
val isRelayListEmpty = accountState.isRelayListEmpty
|
||||||
|
val isBannerDismissed = accountState.isNotificationBannerDismissed
|
||||||
|
|
||||||
val isSyncing by chatViewModel.isSyncing.collectAsStateWithLifecycle()
|
val isSyncing by chatViewModel.isSyncing.collectAsStateWithLifecycle()
|
||||||
val isPartialProcessedGiftWrap by chatViewModel.isPartialProcessedGiftWrap.collectAsStateWithLifecycle()
|
val isPartialProcessedGiftWrap by chatViewModel.isPartialProcessedGiftWrap.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val accountState by accountViewModel.state.collectAsStateWithLifecycle()
|
|
||||||
val isBannerDismissed = accountState.isNotificationBannerDismissed
|
|
||||||
|
|
||||||
val expandedFab by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } }
|
val expandedFab by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } }
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
var isRefreshing by remember { mutableStateOf(false) }
|
var isRefreshing by remember { mutableStateOf(false) }
|
||||||
@@ -249,7 +251,9 @@ fun HomeScreen(
|
|||||||
modifier = Modifier.padding(top = innerPadding.calculateTopPadding()),
|
modifier = Modifier.padding(top = innerPadding.calculateTopPadding()),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
if (!isNotificationEnabled && !isBannerDismissed) {
|
AnimatedVisibility(
|
||||||
|
visible = !isNotificationEnabled && !isBannerDismissed,
|
||||||
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -372,6 +376,9 @@ fun HomeScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
items(ongoing, key = { it.id }) { room ->
|
items(ongoing, key = { it.id }) { room ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.animateItem()
|
||||||
|
) {
|
||||||
ChatRoom(
|
ChatRoom(
|
||||||
room = room,
|
room = room,
|
||||||
onClick = { navigator.navigate(Screen.Chat(room.id)) }
|
onClick = { navigator.navigate(Screen.Chat(room.id)) }
|
||||||
@@ -382,6 +389,7 @@ fun HomeScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -620,13 +628,16 @@ fun NewRequests(requests: List<Room>) {
|
|||||||
val firstRoom = requests.getOrNull(0)
|
val firstRoom = requests.getOrNull(0)
|
||||||
val secondRoom = requests.getOrNull(1)
|
val secondRoom = requests.getOrNull(1)
|
||||||
|
|
||||||
val firstRoomState by (firstRoom as Room).uiStateFlow(profileCache)
|
val firstRoomState by remember(firstRoom?.id) {
|
||||||
.collectAsStateWithLifecycle(RoomUiState())
|
firstRoom?.uiStateFlow(profileCache) ?: flowOf(RoomUiState())
|
||||||
val secondRoomState by (secondRoom ?: firstRoom).uiStateFlow(profileCache)
|
}.collectAsStateWithLifecycle(RoomUiState())
|
||||||
.collectAsStateWithLifecycle(RoomUiState())
|
|
||||||
|
val secondRoomState by remember(secondRoom?.id) {
|
||||||
|
(secondRoom ?: firstRoom)?.uiStateFlow(profileCache) ?: flowOf(RoomUiState())
|
||||||
|
}.collectAsStateWithLifecycle(RoomUiState())
|
||||||
|
|
||||||
val supportingText = when {
|
val supportingText = when {
|
||||||
total == 1 -> {
|
total == 1 && firstRoom != null -> {
|
||||||
val message = firstRoom.lastMessage ?: ""
|
val message = firstRoom.lastMessage ?: ""
|
||||||
"${firstRoomState.name}: $message"
|
"${firstRoomState.name}: $message"
|
||||||
}
|
}
|
||||||
@@ -695,7 +706,8 @@ fun NewRequests(requests: List<Room>) {
|
|||||||
@Composable
|
@Composable
|
||||||
fun ChatRoom(room: Room, onClick: () -> Unit) {
|
fun ChatRoom(room: Room, onClick: () -> Unit) {
|
||||||
val profileCache = LocalProfileCache.current
|
val profileCache = LocalProfileCache.current
|
||||||
val roomState by room.uiStateFlow(profileCache).collectAsStateWithLifecycle(RoomUiState())
|
val roomState by remember(room.id) { room.uiStateFlow(profileCache) }
|
||||||
|
.collectAsStateWithLifecycle(RoomUiState())
|
||||||
|
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable(onClick = onClick),
|
modifier = Modifier.clickable(onClick = onClick),
|
||||||
|
|||||||
@@ -101,13 +101,6 @@ fun ImportScreen(viewModel: AccountViewModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show import errors via snackbar
|
|
||||||
LaunchedEffect(accountState.importError) {
|
|
||||||
accountState.importError?.let {
|
|
||||||
snackbarHostState.showSnackbar(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
|
|||||||
@@ -75,7 +75,8 @@ fun NewChatScreen(
|
|||||||
val snackbarHostState = LocalSnackbarHostState.current
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
val navigator = LocalNavigator.current
|
val navigator = LocalNavigator.current
|
||||||
val qrScanResult = LocalScanResult.current
|
val qrScanResult = LocalScanResult.current
|
||||||
val contactList by accountViewModel.contactList.collectAsStateWithLifecycle()
|
val accountState by accountViewModel.state.collectAsStateWithLifecycle()
|
||||||
|
val contactList = accountState.contactList
|
||||||
var query by remember { mutableStateOf("") }
|
var query by remember { mutableStateOf("") }
|
||||||
|
|
||||||
val createGroup = remember { mutableStateOf(false) }
|
val createGroup = remember { mutableStateOf(false) }
|
||||||
@@ -185,8 +186,8 @@ fun NewChatScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
text = { Text("Next") },
|
text = { Text("Next") },
|
||||||
containerColor = MaterialTheme.colorScheme.tertiary,
|
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
|
||||||
contentColor = MaterialTheme.colorScheme.onTertiary,
|
contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,13 +70,6 @@ fun OnboardingScreen(viewModel: AccountViewModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show connection errors
|
|
||||||
LaunchedEffect(accountState.importError) {
|
|
||||||
accountState.importError?.let {
|
|
||||||
snackbarHostState.showSnackbar(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val logoPainter = painterResource(Res.drawable.coop)
|
val logoPainter = painterResource(Res.drawable.coop)
|
||||||
val expressiveFont = getExpressiveFontFamily()
|
val expressiveFont = getExpressiveFontFamily()
|
||||||
|
|
||||||
|
|||||||
@@ -100,8 +100,9 @@ fun RelayScreen(viewModel: AccountViewModel) {
|
|||||||
viewModel.loadCurrentUserMsgRelayList()
|
viewModel.loadCurrentUserMsgRelayList()
|
||||||
}
|
}
|
||||||
|
|
||||||
val loadedRelayList by viewModel.userRelayList.collectAsStateWithLifecycle()
|
val accountState by viewModel.state.collectAsStateWithLifecycle()
|
||||||
val loadedMsgRelayList by viewModel.userMsgRelayList.collectAsStateWithLifecycle()
|
val loadedRelayList = accountState.userRelayList
|
||||||
|
val loadedMsgRelayList = accountState.userMsgRelayList
|
||||||
|
|
||||||
LaunchedEffect(loadedRelayList) {
|
LaunchedEffect(loadedRelayList) {
|
||||||
if (loadedRelayList.isNotEmpty()) {
|
if (loadedRelayList.isNotEmpty()) {
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
package su.reya.coop.screens.chat
|
package su.reya.coop.screens.chat
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.expandVertically
|
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.animation.shrinkVertically
|
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -177,11 +172,7 @@ fun ChatMessage(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AnimatedVisibility(
|
if (isMessageClicked) {
|
||||||
visible = isMessageClicked,
|
|
||||||
enter = fadeIn() + expandVertically(),
|
|
||||||
exit = fadeOut() + shrinkVertically()
|
|
||||||
) {
|
|
||||||
Text(
|
Text(
|
||||||
text = model.timestamp,
|
text = model.timestamp,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
|||||||
@@ -146,8 +146,9 @@ fun ChatScreen(
|
|||||||
val groupedMessages =
|
val groupedMessages =
|
||||||
remember { derivedStateOf { messages.groupBy { it.createdAt().formatAsGroup() } } }
|
remember { derivedStateOf { messages.groupBy { it.createdAt().formatAsGroup() } } }
|
||||||
|
|
||||||
val roomState by (room as Room).uiStateFlow(profileCache, currentUser?.publicKey)
|
val roomState by remember(id, currentUser?.publicKey) {
|
||||||
.collectAsStateWithLifecycle(RoomUiState())
|
(room as Room).uiStateFlow(profileCache, currentUser?.publicKey)
|
||||||
|
}.collectAsStateWithLifecycle(RoomUiState())
|
||||||
|
|
||||||
var text by remember { mutableStateOf("") }
|
var text by remember { mutableStateOf("") }
|
||||||
var selectedMessage by remember { mutableStateOf<Pair<MessageModel, Rect>?>(null) }
|
var selectedMessage by remember { mutableStateOf<Pair<MessageModel, Rect>?>(null) }
|
||||||
@@ -295,7 +296,9 @@ fun ChatScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.animateItem(),
|
||||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
) {
|
) {
|
||||||
replyPreview?.let { ReplyPreview(it, model.isMine) }
|
replyPreview?.let { ReplyPreview(it, model.isMine) }
|
||||||
@@ -380,9 +383,11 @@ fun ChatScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
|
AnimatedVisibility(visible = replyingTo != null) {
|
||||||
replyingTo?.let {
|
replyingTo?.let {
|
||||||
ReplyBox(it) { replyingTo = null }
|
ReplyBox(it) { replyingTo = null }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
ChatInput(
|
ChatInput(
|
||||||
value = text,
|
value = text,
|
||||||
onValueChange = { text = it },
|
onValueChange = { text = it },
|
||||||
@@ -420,7 +425,6 @@ fun ChatScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = selectedMessage != null,
|
visible = selectedMessage != null,
|
||||||
enter = fadeIn(),
|
enter = fadeIn(),
|
||||||
|
|||||||
22
shared/src/commonMain/kotlin/su/reya/coop/Settings.kt
Normal file
22
shared/src/commonMain/kotlin/su/reya/coop/Settings.kt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package su.reya.coop
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Settings(
|
||||||
|
val theme: Theme = Theme.System,
|
||||||
|
val dynamicColor: Boolean = true,
|
||||||
|
val media: Media = Media.AlwaysEnabled,
|
||||||
|
val screening: Boolean = true,
|
||||||
|
val blossomServer: String? = "https://blossom.band",
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class Theme {
|
||||||
|
Light, Dark, System
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class Media {
|
||||||
|
Disabled, DisabledForMobileData, AlwaysEnabled
|
||||||
|
}
|
||||||
@@ -47,13 +47,17 @@ data class AccountState(
|
|||||||
val signerRequired: Boolean? = null,
|
val signerRequired: Boolean? = null,
|
||||||
val isNotificationBannerDismissed: Boolean = false,
|
val isNotificationBannerDismissed: Boolean = false,
|
||||||
val isImporting: Boolean = false,
|
val isImporting: Boolean = false,
|
||||||
val importError: String? = null,
|
val isRelayListEmpty: Boolean = false,
|
||||||
|
val contactList: Set<PublicKey> = emptySet(),
|
||||||
|
val userRelayList: Map<RelayUrl, RelayMetadata?> = emptyMap(),
|
||||||
|
val userMsgRelayList: List<RelayUrl> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
class AccountRepository(
|
class AccountRepository(
|
||||||
private val nostr: Nostr,
|
private val nostr: Nostr,
|
||||||
private val storage: AppStorage,
|
private val storage: AppStorage,
|
||||||
private val mediaRepository: MediaRepository,
|
private val mediaRepository: MediaRepository,
|
||||||
|
private val settingsRepository: SettingsRepository,
|
||||||
private val scope: CoroutineScope,
|
private val scope: CoroutineScope,
|
||||||
private val externalSignerHandler: ExternalSignerHandler? = null,
|
private val externalSignerHandler: ExternalSignerHandler? = null,
|
||||||
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
|
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||||
@@ -72,23 +76,9 @@ class AccountRepository(
|
|||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val currentUserProfile: StateFlow<Profile?> = nostr.signer.publicKeyFlow
|
val currentUserProfile: StateFlow<Profile?> = nostr.signer.publicKeyFlow
|
||||||
.flatMapLatest { pubkey ->
|
.flatMapLatest { if (it != null) currentUserProfileFlow(it) else flowOf(null) }
|
||||||
if (pubkey != null) currentUserProfileFlow(pubkey) else flowOf(null)
|
|
||||||
}
|
|
||||||
.stateIn(scope, SharingStarted.WhileSubscribed(5000), null)
|
.stateIn(scope, SharingStarted.WhileSubscribed(5000), null)
|
||||||
|
|
||||||
private val _contactList = MutableStateFlow<Set<PublicKey>>(emptySet())
|
|
||||||
val contactList: StateFlow<Set<PublicKey>> = _contactList.asStateFlow()
|
|
||||||
|
|
||||||
private val _isRelayListEmpty = MutableStateFlow(false)
|
|
||||||
val isRelayListEmpty: StateFlow<Boolean> = _isRelayListEmpty.asStateFlow()
|
|
||||||
|
|
||||||
private val _userRelayList = MutableStateFlow<Map<RelayUrl, RelayMetadata?>>(emptyMap())
|
|
||||||
val userRelayList: StateFlow<Map<RelayUrl, RelayMetadata?>> = _userRelayList.asStateFlow()
|
|
||||||
|
|
||||||
private val _userMsgRelayList = MutableStateFlow<List<RelayUrl>>(emptyList())
|
|
||||||
val userMsgRelayList: StateFlow<List<RelayUrl>> = _userMsgRelayList.asStateFlow()
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
checkNotificationBannerDismissedStatus()
|
checkNotificationBannerDismissedStatus()
|
||||||
login()
|
login()
|
||||||
@@ -116,7 +106,7 @@ class AccountRepository(
|
|||||||
} ?: emptyList()
|
} ?: emptyList()
|
||||||
|
|
||||||
// Automatically update the warning state
|
// Automatically update the warning state
|
||||||
_isRelayListEmpty.value = relays.isEmpty()
|
_state.update { it.copy(isRelayListEmpty = relays.isEmpty()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,7 +219,7 @@ class AccountRepository(
|
|||||||
|
|
||||||
fun importIdentity(secret: String, password: String? = null) {
|
fun importIdentity(secret: String, password: String? = null) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
_state.update { it.copy(isImporting = true, importError = null) }
|
_state.update { it.copy(isImporting = true) }
|
||||||
try {
|
try {
|
||||||
val (signer, decryptedSecret) = createSigner(secret, password)
|
val (signer, decryptedSecret) = createSigner(secret, password)
|
||||||
|
|
||||||
@@ -239,14 +229,14 @@ class AccountRepository(
|
|||||||
_state.update { it.copy(signerRequired = false, isImporting = false) }
|
_state.update { it.copy(signerRequired = false, isImporting = false) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
showError("Import failed: ${e.message}")
|
showError("Import failed: ${e.message}")
|
||||||
_state.update { it.copy(isImporting = false, importError = e.message) }
|
_state.update { it.copy(isImporting = false) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun connectExternalSigner() {
|
fun connectExternalSigner() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
_state.update { it.copy(isImporting = true, importError = null) }
|
_state.update { it.copy(isImporting = true) }
|
||||||
try {
|
try {
|
||||||
val handler =
|
val handler =
|
||||||
externalSignerHandler ?: throw IllegalStateException("Signer not available")
|
externalSignerHandler ?: throw IllegalStateException("Signer not available")
|
||||||
@@ -276,7 +266,7 @@ class AccountRepository(
|
|||||||
_state.update { it.copy(signerRequired = false, isImporting = false) }
|
_state.update { it.copy(signerRequired = false, isImporting = false) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
showError("External signer connection failed: ${e.message}")
|
showError("External signer connection failed: ${e.message}")
|
||||||
_state.update { it.copy(isImporting = false, importError = e.message) }
|
_state.update { it.copy(isImporting = false) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,7 +278,7 @@ class AccountRepository(
|
|||||||
contentType: String? = null
|
contentType: String? = null
|
||||||
) {
|
) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
_state.update { it.copy(isImporting = true, importError = null) }
|
_state.update { it.copy(isImporting = true) }
|
||||||
try {
|
try {
|
||||||
val keys = Keys.generate()
|
val keys = Keys.generate()
|
||||||
val secret = keys.secretKey().toBech32()
|
val secret = keys.secretKey().toBech32()
|
||||||
@@ -307,7 +297,7 @@ class AccountRepository(
|
|||||||
_state.update { it.copy(signerRequired = false, isImporting = false) }
|
_state.update { it.copy(signerRequired = false, isImporting = false) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
showError("Identity creation failed: ${e.message}")
|
showError("Identity creation failed: ${e.message}")
|
||||||
_state.update { it.copy(isImporting = false, importError = e.message) }
|
_state.update { it.copy(isImporting = false) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -355,14 +345,13 @@ class AccountRepository(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
nostr.waitUntilInitialized()
|
nostr.waitUntilInitialized()
|
||||||
nostr.profiles.contactListUpdates.collect { contacts ->
|
nostr.profiles.contactListUpdates.collect { contacts ->
|
||||||
_contactList.value = contacts.toSet()
|
_state.update { it.copy(contactList = contacts.toSet()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetInternalState() {
|
fun resetInternalState() {
|
||||||
_contactList.value = emptySet()
|
_state.update { it.copy(contactList = emptySet(), isRelayListEmpty = false) }
|
||||||
_isRelayListEmpty.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addContact(address: String) {
|
fun addContact(address: String) {
|
||||||
@@ -378,12 +367,12 @@ class AccountRepository(
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pubkey in _contactList.value) return@launch
|
if (pubkey in _state.value.contactList) return@launch
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val updated = _contactList.value + pubkey
|
val updated = _state.value.contactList + pubkey
|
||||||
nostr.profiles.setContactList(updated.toList())
|
nostr.profiles.setContactList(updated.toList())
|
||||||
_contactList.update { it + pubkey }
|
_state.update { it.copy(contactList = it.contactList + pubkey) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
showError("Error: ${e.message}")
|
showError("Error: ${e.message}")
|
||||||
}
|
}
|
||||||
@@ -392,12 +381,12 @@ class AccountRepository(
|
|||||||
|
|
||||||
fun removeContact(publicKey: PublicKey) {
|
fun removeContact(publicKey: PublicKey) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
if (publicKey !in _contactList.value) return@launch
|
if (publicKey !in _state.value.contactList) return@launch
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val updated = _contactList.value - publicKey
|
val updated = _state.value.contactList - publicKey
|
||||||
nostr.profiles.setContactList(updated.toList())
|
nostr.profiles.setContactList(updated.toList())
|
||||||
_contactList.update { it - publicKey }
|
_state.update { it.copy(contactList = it.contactList - publicKey) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
showError("Error: ${e.message}")
|
showError("Error: ${e.message}")
|
||||||
}
|
}
|
||||||
@@ -460,7 +449,7 @@ class AccountRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun dismissRelayWarning() {
|
fun dismissRelayWarning() {
|
||||||
_isRelayListEmpty.value = false
|
_state.update { it.copy(isRelayListEmpty = false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refetchMsgRelays() {
|
fun refetchMsgRelays() {
|
||||||
@@ -487,7 +476,8 @@ class AccountRepository(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
val user = nostr.signer.getPublicKeyAsync() ?: throw Exception("User not found")
|
val user = nostr.signer.getPublicKeyAsync() ?: throw Exception("User not found")
|
||||||
_userRelayList.value = nostr.relays.getRelayList(user)
|
val relayList = nostr.relays.getRelayList(user)
|
||||||
|
_state.update { it.copy(userRelayList = relayList) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
showError("Error: ${e.message}")
|
showError("Error: ${e.message}")
|
||||||
}
|
}
|
||||||
@@ -550,7 +540,8 @@ class AccountRepository(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
val user = nostr.signer.getPublicKeyAsync() ?: throw Exception("User not found")
|
val user = nostr.signer.getPublicKeyAsync() ?: throw Exception("User not found")
|
||||||
_userMsgRelayList.value = nostr.relays.getMsgRelays(user)
|
val msgRelays = nostr.relays.getMsgRelays(user)
|
||||||
|
_state.update { it.copy(userMsgRelayList = msgRelays) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
showError("Error: ${e.message}")
|
showError("Error: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
@@ -21,6 +22,7 @@ import rust.nostr.sdk.PublicKey
|
|||||||
import rust.nostr.sdk.Tag
|
import rust.nostr.sdk.Tag
|
||||||
import rust.nostr.sdk.UnsignedEvent
|
import rust.nostr.sdk.UnsignedEvent
|
||||||
import su.reya.coop.Room
|
import su.reya.coop.Room
|
||||||
|
import su.reya.coop.RoomKind
|
||||||
import su.reya.coop.nostr.Nostr
|
import su.reya.coop.nostr.Nostr
|
||||||
import su.reya.coop.roomId
|
import su.reya.coop.roomId
|
||||||
import su.reya.coop.viewmodel.ErrorHost
|
import su.reya.coop.viewmodel.ErrorHost
|
||||||
@@ -34,15 +36,12 @@ data class ChatState(
|
|||||||
class ChatRepository(
|
class ChatRepository(
|
||||||
private val nostr: Nostr,
|
private val nostr: Nostr,
|
||||||
private val mediaRepository: MediaRepository,
|
private val mediaRepository: MediaRepository,
|
||||||
|
private val settingsRepository: SettingsRepository,
|
||||||
private val scope: CoroutineScope,
|
private val scope: CoroutineScope,
|
||||||
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
|
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||||
) : ErrorHost by createErrorHost() {
|
) : ErrorHost by createErrorHost() {
|
||||||
private val _state = MutableStateFlow(ChatState())
|
private val _state = MutableStateFlow(ChatState())
|
||||||
val state = _state.stateIn(
|
val state = _state.asStateFlow()
|
||||||
scope,
|
|
||||||
SharingStarted.WhileSubscribed(5000),
|
|
||||||
ChatState()
|
|
||||||
)
|
|
||||||
|
|
||||||
private val _newEvents = MutableSharedFlow<UnsignedEvent>(
|
private val _newEvents = MutableSharedFlow<UnsignedEvent>(
|
||||||
replay = 0,
|
replay = 0,
|
||||||
@@ -134,10 +133,19 @@ class ChatRepository(
|
|||||||
fun refreshChatRooms() {
|
fun refreshChatRooms() {
|
||||||
scope.launch(defaultDispatcher) {
|
scope.launch(defaultDispatcher) {
|
||||||
try {
|
try {
|
||||||
val rooms = nostr.messages.getChatRooms() ?: emptySet()
|
val dbRooms = nostr.messages.getChatRooms() ?: emptySet()
|
||||||
_state.update { currentState ->
|
_state.update { currentState ->
|
||||||
val newMap = currentState.rooms.toMutableMap()
|
val newMap = currentState.rooms.toMutableMap()
|
||||||
rooms.forEach { room -> newMap[room.id] = room }
|
dbRooms.forEach { dbRoom ->
|
||||||
|
val existing = newMap[dbRoom.id]
|
||||||
|
// Only update if the database version is newer or equal
|
||||||
|
if (existing == null || dbRoom.createdAt.asSecs() >= existing.createdAt.asSecs()) {
|
||||||
|
// Preserve Ongoing kind if already marked as such in memory
|
||||||
|
val mergedKind =
|
||||||
|
if (existing?.kind == RoomKind.Ongoing) RoomKind.Ongoing else dbRoom.kind
|
||||||
|
newMap[dbRoom.id] = dbRoom.copy(kind = mergedKind)
|
||||||
|
}
|
||||||
|
}
|
||||||
currentState.copy(rooms = newMap)
|
currentState.copy(rooms = newMap)
|
||||||
}
|
}
|
||||||
} catch (e: CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
@@ -223,16 +231,24 @@ class ChatRepository(
|
|||||||
val rooms = currentState.rooms.toMutableMap()
|
val rooms = currentState.rooms.toMutableMap()
|
||||||
val existingRoom = rooms[roomId]
|
val existingRoom = rooms[roomId]
|
||||||
|
|
||||||
|
val isFromMe = event.author() == currentUser
|
||||||
|
val newKind =
|
||||||
|
if (isFromMe) RoomKind.Ongoing else (existingRoom?.kind ?: RoomKind.Request)
|
||||||
|
|
||||||
if (existingRoom == null) {
|
if (existingRoom == null) {
|
||||||
// New room discovery
|
// New room discovery
|
||||||
val newRoom = Room.new(event, currentUser, roomId)
|
val newRoom = Room.new(event, currentUser, roomId).copy(kind = newKind)
|
||||||
rooms[newRoom.id] = newRoom
|
rooms[newRoom.id] = newRoom
|
||||||
} else if (event.createdAt().asSecs() >= existingRoom.createdAt.asSecs()) {
|
} else if (event.createdAt().asSecs() >= existingRoom.createdAt.asSecs()) {
|
||||||
// Only update preview if message is newer (handles sync/late arrivals)
|
// Only update preview if message is newer (handles sync/late arrivals)
|
||||||
rooms[roomId] = existingRoom.copy(
|
rooms[roomId] = existingRoom.copy(
|
||||||
lastMessage = event.content(),
|
lastMessage = event.content(),
|
||||||
createdAt = event.createdAt()
|
createdAt = event.createdAt(),
|
||||||
|
kind = newKind
|
||||||
)
|
)
|
||||||
|
} else if (isFromMe && existingRoom.kind != RoomKind.Ongoing) {
|
||||||
|
// Even if it's an older message, if it's from me, the room is ongoing
|
||||||
|
rooms[roomId] = existingRoom.copy(kind = RoomKind.Ongoing)
|
||||||
} else {
|
} else {
|
||||||
// Don't update the room list state for older messages
|
// Don't update the room list state for older messages
|
||||||
return@update currentState
|
return@update currentState
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import kotlinx.serialization.json.Json
|
|||||||
import rust.nostr.sdk.AsyncNostrSigner
|
import rust.nostr.sdk.AsyncNostrSigner
|
||||||
import su.reya.coop.blossom.BlossomClient
|
import su.reya.coop.blossom.BlossomClient
|
||||||
|
|
||||||
class MediaRepository {
|
class MediaRepository(
|
||||||
|
private val settingsRepository: SettingsRepository
|
||||||
|
) {
|
||||||
private val httpClient = HttpClient {
|
private val httpClient = HttpClient {
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
json(Json {
|
json(Json {
|
||||||
@@ -25,12 +27,9 @@ class MediaRepository {
|
|||||||
contentType: String? = "image/jpeg"
|
contentType: String? = "image/jpeg"
|
||||||
): String? {
|
): String? {
|
||||||
return try {
|
return try {
|
||||||
val blossom = BlossomClient(url = "https://blossom.band", client = httpClient)
|
val url = settingsRepository.settings.value.blossomServer ?: "https://blossom.band"
|
||||||
val descriptor = blossom.upload(
|
val blossom = BlossomClient(url, httpClient)
|
||||||
file = file,
|
val descriptor = blossom.upload(file = file, contentType = contentType, signer = signer)
|
||||||
contentType = contentType,
|
|
||||||
signer = signer,
|
|
||||||
)
|
|
||||||
descriptor?.url
|
descriptor?.url
|
||||||
} catch (e: CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
throw e
|
throw e
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package su.reya.coop.repository
|
||||||
|
|
||||||
|
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 kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import su.reya.coop.AppStorage
|
||||||
|
import su.reya.coop.Settings
|
||||||
|
|
||||||
|
class SettingsRepository(
|
||||||
|
private val storage: AppStorage,
|
||||||
|
private val scope: CoroutineScope
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private const val KEY_SETTINGS = "app_settings"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val json = Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
encodeDefaults = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _settings = MutableStateFlow(Settings())
|
||||||
|
val settings: StateFlow<Settings> = _settings.asStateFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
scope.launch {
|
||||||
|
_settings.value = load()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(transform: (Settings) -> Settings) {
|
||||||
|
scope.launch {
|
||||||
|
val newSettings = transform(_settings.value)
|
||||||
|
_settings.value = newSettings
|
||||||
|
save(newSettings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun save(settings: Settings) {
|
||||||
|
val jsonString = json.encodeToString(settings)
|
||||||
|
storage.set(KEY_SETTINGS, jsonString)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun load(): Settings {
|
||||||
|
val jsonString = storage.get(KEY_SETTINGS)
|
||||||
|
return if (jsonString != null) {
|
||||||
|
try {
|
||||||
|
json.decodeFromString<Settings>(jsonString)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
Settings()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Settings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,6 @@ package su.reya.coop.viewmodel
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import rust.nostr.sdk.PublicKey
|
import rust.nostr.sdk.PublicKey
|
||||||
import rust.nostr.sdk.RelayMetadata
|
|
||||||
import rust.nostr.sdk.RelayUrl
|
|
||||||
import rust.nostr.sdk.Timestamp
|
import rust.nostr.sdk.Timestamp
|
||||||
import su.reya.coop.Profile
|
import su.reya.coop.Profile
|
||||||
import su.reya.coop.repository.AccountRepository
|
import su.reya.coop.repository.AccountRepository
|
||||||
@@ -16,10 +14,6 @@ class AccountViewModel(
|
|||||||
val state: StateFlow<AccountState> = repository.state
|
val state: StateFlow<AccountState> = repository.state
|
||||||
val isUpdatingProfile: StateFlow<Boolean> = repository.isUpdatingProfile
|
val isUpdatingProfile: StateFlow<Boolean> = repository.isUpdatingProfile
|
||||||
val currentUserProfile: StateFlow<Profile?> = repository.currentUserProfile
|
val currentUserProfile: StateFlow<Profile?> = repository.currentUserProfile
|
||||||
val contactList: StateFlow<Set<PublicKey>> = repository.contactList
|
|
||||||
val isRelayListEmpty: StateFlow<Boolean> = repository.isRelayListEmpty
|
|
||||||
val userRelayList: StateFlow<Map<RelayUrl, RelayMetadata?>> = repository.userRelayList
|
|
||||||
val userMsgRelayList: StateFlow<List<RelayUrl>> = repository.userMsgRelayList
|
|
||||||
|
|
||||||
fun logout(onLogout: () -> Unit = {}) = repository.logout(onLogout)
|
fun logout(onLogout: () -> Unit = {}) = repository.logout(onLogout)
|
||||||
fun dismissNotificationBanner() = repository.dismissNotificationBanner()
|
fun dismissNotificationBanner() = repository.dismissNotificationBanner()
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package su.reya.coop.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import su.reya.coop.Settings
|
||||||
|
import su.reya.coop.repository.SettingsRepository
|
||||||
|
|
||||||
|
class SettingsViewModel(
|
||||||
|
private val repository: SettingsRepository
|
||||||
|
) : ViewModel() {
|
||||||
|
val settings: StateFlow<Settings> = repository.settings
|
||||||
|
|
||||||
|
fun update(transform: (Settings) -> Settings) {
|
||||||
|
repository.update(transform)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user