Compare commits
3 Commits
v0.2.6
...
6bb4ef2805
| Author | SHA1 | Date | |
|---|---|---|---|
| 6bb4ef2805 | |||
| e4e73da229 | |||
| 255c840847 |
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/
|
||||||
@@ -39,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
|
||||||
@@ -56,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")
|
||||||
}
|
}
|
||||||
@@ -79,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 {
|
||||||
@@ -92,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")
|
||||||
@@ -102,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
|
||||||
@@ -113,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()
|
||||||
@@ -190,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) }
|
||||||
|
|
||||||
|
|||||||
@@ -126,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) }
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
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}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ 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() {
|
||||||
|
|||||||
@@ -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