chore: improve performance #41
@@ -51,6 +51,7 @@ import su.reya.coop.screens.UpdateProfileScreen
|
|||||||
import su.reya.coop.viewmodel.AuthViewModel
|
import su.reya.coop.viewmodel.AuthViewModel
|
||||||
import su.reya.coop.viewmodel.ChatViewModel
|
import su.reya.coop.viewmodel.ChatViewModel
|
||||||
import su.reya.coop.viewmodel.NostrViewModel
|
import su.reya.coop.viewmodel.NostrViewModel
|
||||||
|
import su.reya.coop.viewmodel.RelayViewModel
|
||||||
|
|
||||||
val LocalNostrViewModel = staticCompositionLocalOf<NostrViewModel> {
|
val LocalNostrViewModel = staticCompositionLocalOf<NostrViewModel> {
|
||||||
error("No NostrViewModel provided")
|
error("No NostrViewModel provided")
|
||||||
@@ -64,6 +65,10 @@ val LocalAuthViewModel = staticCompositionLocalOf<AuthViewModel> {
|
|||||||
error("No AuthViewModel provided")
|
error("No AuthViewModel provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val LocalRelayViewModel = staticCompositionLocalOf<RelayViewModel> {
|
||||||
|
error("No RelayViewModel provided")
|
||||||
|
}
|
||||||
|
|
||||||
val LocalSnackbarHostState = staticCompositionLocalOf<SnackbarHostState> {
|
val LocalSnackbarHostState = staticCompositionLocalOf<SnackbarHostState> {
|
||||||
error("No SnackbarHostState provided")
|
error("No SnackbarHostState provided")
|
||||||
}
|
}
|
||||||
@@ -80,6 +85,7 @@ val LocalScanResult = staticCompositionLocalOf<QrScanResult> {
|
|||||||
@Composable
|
@Composable
|
||||||
fun App(
|
fun App(
|
||||||
nostrViewModel: NostrViewModel,
|
nostrViewModel: NostrViewModel,
|
||||||
|
relayViewModel: RelayViewModel,
|
||||||
chatViewModel: ChatViewModel,
|
chatViewModel: ChatViewModel,
|
||||||
authViewModel: AuthViewModel,
|
authViewModel: AuthViewModel,
|
||||||
) {
|
) {
|
||||||
@@ -133,6 +139,11 @@ fun App(
|
|||||||
snackbarHostState.showSnackbar(message)
|
snackbarHostState.showSnackbar(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
launch {
|
||||||
|
relayViewModel.errorEvents.collect { message ->
|
||||||
|
snackbarHostState.showSnackbar(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(activity) {
|
LaunchedEffect(activity) {
|
||||||
@@ -174,6 +185,7 @@ fun App(
|
|||||||
) {
|
) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalNostrViewModel provides nostrViewModel,
|
LocalNostrViewModel provides nostrViewModel,
|
||||||
|
LocalRelayViewModel provides relayViewModel,
|
||||||
LocalChatViewModel provides chatViewModel,
|
LocalChatViewModel provides chatViewModel,
|
||||||
LocalAuthViewModel provides authViewModel,
|
LocalAuthViewModel provides authViewModel,
|
||||||
LocalSnackbarHostState provides snackbarHostState,
|
LocalSnackbarHostState provides snackbarHostState,
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ import android.content.Intent
|
|||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class ExternalSignerLauncher {
|
class ExternalSignerLauncher(
|
||||||
|
private val mainDispatcher: CoroutineDispatcher = Dispatchers.Main,
|
||||||
|
) {
|
||||||
private var launcher: ActivityResultLauncher<Intent>? = null
|
private var launcher: ActivityResultLauncher<Intent>? = null
|
||||||
private var pendingResult: CompletableDeferred<ActivityResult>? = null
|
private var pendingResult: CompletableDeferred<ActivityResult>? = null
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
@@ -19,7 +22,7 @@ class ExternalSignerLauncher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun launch(intent: Intent): ActivityResult = mutex.withLock {
|
suspend fun launch(intent: Intent): ActivityResult = mutex.withLock {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(mainDispatcher) {
|
||||||
val deferred = CompletableDeferred<ActivityResult>()
|
val deferred = CompletableDeferred<ActivityResult>()
|
||||||
pendingResult = deferred
|
pendingResult = deferred
|
||||||
launcher?.launch(intent) ?: throw IllegalStateException("Signer not registered")
|
launcher?.launch(intent) ?: throw IllegalStateException("Signer not registered")
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import su.reya.coop.repository.MediaRepository
|
|||||||
import su.reya.coop.viewmodel.AuthViewModel
|
import su.reya.coop.viewmodel.AuthViewModel
|
||||||
import su.reya.coop.viewmodel.ChatViewModel
|
import su.reya.coop.viewmodel.ChatViewModel
|
||||||
import su.reya.coop.viewmodel.NostrViewModel
|
import su.reya.coop.viewmodel.NostrViewModel
|
||||||
|
import su.reya.coop.viewmodel.RelayViewModel
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@@ -29,6 +30,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
private val storage = AppStore(this@MainActivity)
|
private val storage = AppStore(this@MainActivity)
|
||||||
private val mediaRepository = MediaRepository()
|
private val mediaRepository = MediaRepository()
|
||||||
private val nostrViewModel = NostrViewModel(NostrManager.instance, mediaRepository)
|
private val nostrViewModel = NostrViewModel(NostrManager.instance, mediaRepository)
|
||||||
|
private val relayViewModel = RelayViewModel(NostrManager.instance)
|
||||||
private val chatViewModel = ChatViewModel(NostrManager.instance, mediaRepository)
|
private val chatViewModel = ChatViewModel(NostrManager.instance, mediaRepository)
|
||||||
private val androidSigner =
|
private val androidSigner =
|
||||||
AndroidExternalSigner(this@MainActivity, externalSignerLauncher)
|
AndroidExternalSigner(this@MainActivity, externalSignerLauncher)
|
||||||
@@ -38,6 +40,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
return when {
|
return when {
|
||||||
modelClass.isAssignableFrom(NostrViewModel::class.java) -> nostrViewModel
|
modelClass.isAssignableFrom(NostrViewModel::class.java) -> nostrViewModel
|
||||||
|
modelClass.isAssignableFrom(RelayViewModel::class.java) -> relayViewModel
|
||||||
modelClass.isAssignableFrom(ChatViewModel::class.java) -> chatViewModel
|
modelClass.isAssignableFrom(ChatViewModel::class.java) -> chatViewModel
|
||||||
modelClass.isAssignableFrom(AuthViewModel::class.java) -> authViewModel
|
modelClass.isAssignableFrom(AuthViewModel::class.java) -> authViewModel
|
||||||
else -> throw IllegalArgumentException("Unknown ViewModel class")
|
else -> throw IllegalArgumentException("Unknown ViewModel class")
|
||||||
@@ -47,6 +50,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val nostrViewModel: NostrViewModel by viewModels { factory }
|
private val nostrViewModel: NostrViewModel by viewModels { factory }
|
||||||
|
private val relayViewModel: RelayViewModel by viewModels { factory }
|
||||||
private val chatViewModel: ChatViewModel by viewModels { factory }
|
private val chatViewModel: ChatViewModel by viewModels { factory }
|
||||||
private val authViewModel: AuthViewModel by viewModels { factory }
|
private val authViewModel: AuthViewModel by viewModels { factory }
|
||||||
|
|
||||||
@@ -93,6 +97,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
setContent {
|
setContent {
|
||||||
App(
|
App(
|
||||||
nostrViewModel = nostrViewModel,
|
nostrViewModel = nostrViewModel,
|
||||||
|
relayViewModel = relayViewModel,
|
||||||
chatViewModel = chatViewModel,
|
chatViewModel = chatViewModel,
|
||||||
authViewModel = authViewModel,
|
authViewModel = authViewModel,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import androidx.core.app.NotificationCompat
|
|||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -27,6 +28,8 @@ private const val GROUP_KEY_MESSAGES = "su.reya.coop.MESSAGES"
|
|||||||
|
|
||||||
class NostrForegroundService : Service() {
|
class NostrForegroundService : Service() {
|
||||||
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
|
var ioDispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||||
private val nostr by lazy { NostrManager.instance }
|
private val nostr by lazy { NostrManager.instance }
|
||||||
private var notificationJob: Job? = null
|
private var notificationJob: Job? = null
|
||||||
|
|
||||||
|
|||||||
@@ -265,10 +265,8 @@ fun AddContactDialog(onDismissRequest: () -> Unit) {
|
|||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
scope.launch {
|
nostrViewModel.addContact(contact)
|
||||||
val success = nostrViewModel.addContact(contact)
|
onDismissRequest()
|
||||||
if (success) onDismissRequest()
|
|
||||||
}
|
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(Res.drawable.ic_check),
|
painter = painterResource(Res.drawable.ic_check),
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ import su.reya.coop.LocalAuthViewModel
|
|||||||
import su.reya.coop.LocalChatViewModel
|
import su.reya.coop.LocalChatViewModel
|
||||||
import su.reya.coop.LocalNavigator
|
import su.reya.coop.LocalNavigator
|
||||||
import su.reya.coop.LocalNostrViewModel
|
import su.reya.coop.LocalNostrViewModel
|
||||||
|
import su.reya.coop.LocalRelayViewModel
|
||||||
import su.reya.coop.LocalScanResult
|
import su.reya.coop.LocalScanResult
|
||||||
import su.reya.coop.LocalSnackbarHostState
|
import su.reya.coop.LocalSnackbarHostState
|
||||||
import su.reya.coop.Room
|
import su.reya.coop.Room
|
||||||
@@ -114,6 +115,7 @@ fun HomeScreen() {
|
|||||||
val nostrViewModel = LocalNostrViewModel.current
|
val nostrViewModel = LocalNostrViewModel.current
|
||||||
val chatViewModel = LocalChatViewModel.current
|
val chatViewModel = LocalChatViewModel.current
|
||||||
val authViewModel = LocalAuthViewModel.current
|
val authViewModel = LocalAuthViewModel.current
|
||||||
|
val relayViewModel = LocalRelayViewModel.current
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val sheetState = rememberModalBottomSheetState(true)
|
val sheetState = rememberModalBottomSheetState(true)
|
||||||
@@ -123,7 +125,7 @@ fun HomeScreen() {
|
|||||||
val userProfile by nostrViewModel.currentUserProfile.collectAsStateWithLifecycle()
|
val userProfile by nostrViewModel.currentUserProfile.collectAsStateWithLifecycle()
|
||||||
val chatRooms by chatViewModel.chatRooms.collectAsStateWithLifecycle()
|
val chatRooms by chatViewModel.chatRooms.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val isRelayListEmpty by nostrViewModel.isRelayListEmpty.collectAsStateWithLifecycle()
|
val isRelayListEmpty by relayViewModel.isRelayListEmpty.collectAsStateWithLifecycle()
|
||||||
val isSyncing by chatViewModel.isSyncing.collectAsStateWithLifecycle()
|
val isSyncing by chatViewModel.isSyncing.collectAsStateWithLifecycle()
|
||||||
val isPartialProcessedGiftWrap by chatViewModel.isPartialProcessedGiftWrap.collectAsStateWithLifecycle()
|
val isPartialProcessedGiftWrap by chatViewModel.isPartialProcessedGiftWrap.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
@@ -319,11 +321,9 @@ fun HomeScreen() {
|
|||||||
isRefreshing = isRefreshing,
|
isRefreshing = isRefreshing,
|
||||||
state = pullToRefreshState,
|
state = pullToRefreshState,
|
||||||
onRefresh = {
|
onRefresh = {
|
||||||
scope.launch {
|
isRefreshing = true
|
||||||
isRefreshing = true
|
chatViewModel.refreshChatRooms()
|
||||||
chatViewModel.refreshChatRooms()
|
isRefreshing = false
|
||||||
isRefreshing = false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
indicator = {
|
indicator = {
|
||||||
PullToRefreshDefaults.LoadingIndicator(
|
PullToRefreshDefaults.LoadingIndicator(
|
||||||
@@ -469,7 +469,7 @@ fun HomeScreen() {
|
|||||||
// Show the relay setup dialog if the msg relay list is empty
|
// Show the relay setup dialog if the msg relay list is empty
|
||||||
if (isRelayListEmpty) {
|
if (isRelayListEmpty) {
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = { nostrViewModel.dismissRelayWarning() },
|
onDismissRequest = { relayViewModel.dismissRelayWarning() },
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
) {
|
) {
|
||||||
@@ -573,15 +573,9 @@ fun HomeScreen() {
|
|||||||
TextButton(
|
TextButton(
|
||||||
enabled = !isBusy,
|
enabled = !isBusy,
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
isBusy = true
|
||||||
isBusy = true
|
relayViewModel.refetchMsgRelays()
|
||||||
try {
|
isBusy = false
|
||||||
nostrViewModel.refetchMsgRelays()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
snackbarHostState.showSnackbar("Failed to refresh metadata: ${e.message}")
|
|
||||||
}
|
|
||||||
isBusy = false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
@@ -595,10 +589,8 @@ fun HomeScreen() {
|
|||||||
Button(
|
Button(
|
||||||
enabled = !isBusy,
|
enabled = !isBusy,
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
relayViewModel.useDefaultMsgRelayList()
|
||||||
nostrViewModel.useDefaultMsgRelayList()
|
scope.launch { sheetState.hide() }
|
||||||
sheetState.hide()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
|
|||||||
@@ -96,14 +96,16 @@ fun NewChatScreen() {
|
|||||||
selectedReceivers.add(pubkey)
|
selectedReceivers.add(pubkey)
|
||||||
}
|
}
|
||||||
} else if (query.contains("@")) {
|
} else if (query.contains("@")) {
|
||||||
val pubkey = nostrViewModel.searchByAddress(query)
|
nostrViewModel.searchByAddress(query) { pubkey ->
|
||||||
if (pubkey != null) {
|
if (pubkey != null) {
|
||||||
selectedReceivers.add(pubkey)
|
selectedReceivers.add(pubkey)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val results = nostrViewModel.searchByNostr(query)
|
nostrViewModel.searchByNostr(query) { results ->
|
||||||
searchResults.clear()
|
searchResults.clear()
|
||||||
searchResults.addAll(results)
|
searchResults.addAll(results)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
query = ""
|
query = ""
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import androidx.compose.ui.text.input.ImeAction
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import coop.composeapp.generated.resources.Res
|
import coop.composeapp.generated.resources.Res
|
||||||
import coop.composeapp.generated.resources.ic_arrow_back
|
import coop.composeapp.generated.resources.ic_arrow_back
|
||||||
import coop.composeapp.generated.resources.ic_check
|
import coop.composeapp.generated.resources.ic_check
|
||||||
@@ -67,14 +68,16 @@ import rust.nostr.sdk.RelayMetadata
|
|||||||
import rust.nostr.sdk.RelayUrl
|
import rust.nostr.sdk.RelayUrl
|
||||||
import su.reya.coop.LocalNavigator
|
import su.reya.coop.LocalNavigator
|
||||||
import su.reya.coop.LocalNostrViewModel
|
import su.reya.coop.LocalNostrViewModel
|
||||||
|
import su.reya.coop.LocalRelayViewModel
|
||||||
import su.reya.coop.LocalSnackbarHostState
|
import su.reya.coop.LocalSnackbarHostState
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RelayScreen() {
|
fun RelayScreen() {
|
||||||
val navigator = LocalNavigator.current
|
val navigator = LocalNavigator.current
|
||||||
val snackbarHostState = LocalSnackbarHostState.current
|
|
||||||
val nostrViewModel = LocalNostrViewModel.current
|
val nostrViewModel = LocalNostrViewModel.current
|
||||||
|
val relayViewModel = LocalRelayViewModel.current
|
||||||
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val msgRelayList = remember { mutableStateListOf<RelayUrl>() }
|
val msgRelayList = remember { mutableStateListOf<RelayUrl>() }
|
||||||
@@ -96,8 +99,25 @@ fun RelayScreen() {
|
|||||||
var relayToDelete by remember { mutableStateOf<String?>(null) }
|
var relayToDelete by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
relayList.putAll(nostrViewModel.currentUserRelayList())
|
relayViewModel.loadCurrentUserRelayList()
|
||||||
msgRelayList.addAll(nostrViewModel.currentUserMsgRelayList())
|
relayViewModel.loadCurrentUserMsgRelayList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val loadedRelayList by relayViewModel.currentUserRelayList.collectAsStateWithLifecycle()
|
||||||
|
val loadedMsgRelayList by relayViewModel.currentUserMsgRelayList.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
LaunchedEffect(loadedRelayList) {
|
||||||
|
if (loadedRelayList.isNotEmpty()) {
|
||||||
|
relayList.clear()
|
||||||
|
relayList.putAll(loadedRelayList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(loadedMsgRelayList) {
|
||||||
|
if (loadedMsgRelayList.isNotEmpty()) {
|
||||||
|
msgRelayList.clear()
|
||||||
|
msgRelayList.addAll(loadedMsgRelayList)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -314,20 +334,16 @@ fun RelayScreen() {
|
|||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
if (msgRelayList.size == 1) {
|
||||||
if (msgRelayList.size == 1) {
|
scope.launch {
|
||||||
snackbarHostState.showSnackbar("You must have at least one relay")
|
snackbarHostState.showSnackbar("You must have at least one relay")
|
||||||
relayToDelete = null
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
nostrViewModel.removeMsgRelay(relayToDelete!!)
|
|
||||||
msgRelayList.removeIf { it.toString() == relayToDelete }
|
|
||||||
relayToDelete = null
|
|
||||||
} catch (e: Exception) {
|
|
||||||
snackbarHostState.showSnackbar("Failed to remove relay: ${e.message}")
|
|
||||||
}
|
}
|
||||||
|
relayToDelete = null
|
||||||
|
return@TextButton
|
||||||
}
|
}
|
||||||
|
relayViewModel.removeMsgRelay(relayToDelete!!)
|
||||||
|
msgRelayList.removeIf { it.toString() == relayToDelete }
|
||||||
|
relayToDelete = null
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text("Confirm")
|
Text("Confirm")
|
||||||
@@ -349,7 +365,7 @@ fun AddRelayDialog(
|
|||||||
onMsgRelayAdded: (newRelay: String) -> Unit,
|
onMsgRelayAdded: (newRelay: String) -> Unit,
|
||||||
onRelayAdded: (newRelay: String, metadata: RelayMetadata?) -> Unit,
|
onRelayAdded: (newRelay: String, metadata: RelayMetadata?) -> Unit,
|
||||||
) {
|
) {
|
||||||
val nostrViewModel = LocalNostrViewModel.current
|
val relayViewModel = LocalRelayViewModel.current
|
||||||
val snackbarHostState = LocalSnackbarHostState.current
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -397,26 +413,24 @@ fun AddRelayDialog(
|
|||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
scope.launch {
|
if (!isError) {
|
||||||
if (!isError) {
|
when (selected) {
|
||||||
when (selected) {
|
"Messaging" -> {
|
||||||
"Messaging" -> {
|
relayViewModel.addMsgRelay(relayAddress)
|
||||||
nostrViewModel.addMsgRelay(relayAddress)
|
onMsgRelayAdded(relayAddress)
|
||||||
onMsgRelayAdded(relayAddress)
|
}
|
||||||
}
|
|
||||||
|
"Inbox" -> {
|
||||||
"Inbox" -> {
|
relayViewModel.addInboxRelay(relayAddress)
|
||||||
nostrViewModel.addInboxRelay(relayAddress)
|
onRelayAdded(relayAddress, RelayMetadata.WRITE)
|
||||||
onRelayAdded(relayAddress, RelayMetadata.WRITE)
|
}
|
||||||
}
|
|
||||||
|
"Outbox" -> {
|
||||||
"Outbox" -> {
|
relayViewModel.addOutboxRelay(relayAddress)
|
||||||
nostrViewModel.addOutboxRelay(relayAddress)
|
onRelayAdded(relayAddress, RelayMetadata.READ)
|
||||||
onRelayAdded(relayAddress, RelayMetadata.READ)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
onDismissRequest()
|
|
||||||
}
|
}
|
||||||
|
onDismissRequest()
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|||||||
@@ -101,11 +101,9 @@ fun RequestListScreen() {
|
|||||||
isRefreshing = isRefreshing,
|
isRefreshing = isRefreshing,
|
||||||
state = pullToRefreshState,
|
state = pullToRefreshState,
|
||||||
onRefresh = {
|
onRefresh = {
|
||||||
scope.launch {
|
isRefreshing = true
|
||||||
isRefreshing = true
|
chatViewModel.refreshChatRooms()
|
||||||
chatViewModel.refreshChatRooms()
|
isRefreshing = false
|
||||||
isRefreshing = false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
indicator = {
|
indicator = {
|
||||||
PullToRefreshDefaults.LoadingIndicator(
|
PullToRefreshDefaults.LoadingIndicator(
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -29,7 +28,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import coop.composeapp.generated.resources.Res
|
import coop.composeapp.generated.resources.Res
|
||||||
import coop.composeapp.generated.resources.ic_cancel
|
import coop.composeapp.generated.resources.ic_cancel
|
||||||
import coop.composeapp.generated.resources.ic_check_circle
|
import coop.composeapp.generated.resources.ic_check_circle
|
||||||
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
|
||||||
import rust.nostr.sdk.Timestamp
|
import rust.nostr.sdk.Timestamp
|
||||||
@@ -46,7 +44,6 @@ fun ScreenerCard(room: Room) {
|
|||||||
val pubkey = room.members.firstOrNull() ?: return
|
val pubkey = room.members.firstOrNull() ?: return
|
||||||
|
|
||||||
val nostrViewModel = LocalNostrViewModel.current
|
val nostrViewModel = LocalNostrViewModel.current
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
var isContact by remember { mutableStateOf(false) }
|
var isContact by remember { mutableStateOf(false) }
|
||||||
var mutualContacts by remember { mutableStateOf<Set<PublicKey>>(emptySet()) }
|
var mutualContacts by remember { mutableStateOf<Set<PublicKey>>(emptySet()) }
|
||||||
@@ -56,14 +53,12 @@ fun ScreenerCard(room: Room) {
|
|||||||
val profile by profileFlow.collectAsStateWithLifecycle()
|
val profile by profileFlow.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
LaunchedEffect(pubkey) {
|
LaunchedEffect(pubkey) {
|
||||||
scope.launch {
|
// Check contact
|
||||||
// Check contact
|
nostrViewModel.verifyContact(pubkey) { isContact = it }
|
||||||
nostrViewModel.verifyContact(pubkey).let { isContact = it }
|
// Get mutual contacts
|
||||||
// Get mutual contacts
|
nostrViewModel.mutualContacts(pubkey) { mutualContacts = it }
|
||||||
nostrViewModel.mutualContacts(pubkey).let { mutualContacts = it }
|
// Get the last activity
|
||||||
// Get the last activity
|
nostrViewModel.verifyActivity(pubkey) { lastActivity = it }
|
||||||
nostrViewModel.verifyActivity(pubkey)?.let { lastActivity = it }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import coop.composeapp.generated.resources.Res
|
import coop.composeapp.generated.resources.Res
|
||||||
import coop.composeapp.generated.resources.ic_arrow_back
|
import coop.composeapp.generated.resources.ic_arrow_back
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -77,7 +78,11 @@ import su.reya.coop.shared.Avatar
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatScreen(id: Long, screening: Boolean = false) {
|
fun ChatScreen(
|
||||||
|
id: Long,
|
||||||
|
screening: Boolean = false,
|
||||||
|
coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||||
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val snackbarHostState = LocalSnackbarHostState.current
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
val navigator = LocalNavigator.current
|
val navigator = LocalNavigator.current
|
||||||
@@ -121,7 +126,7 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
|
|||||||
val sendFile = { uri: Uri ->
|
val sendFile = { uri: Uri ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
// Read file on IO dispatcher
|
// Read file on IO dispatcher
|
||||||
val file = withContext(Dispatchers.IO) {
|
val file = withContext(coroutineDispatcher) {
|
||||||
context.contentResolver.openInputStream(uri)?.use { it.readBytes() }
|
context.contentResolver.openInputStream(uri)?.use { it.readBytes() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,12 +154,13 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
|
|||||||
|
|
||||||
LaunchedEffect(id) {
|
LaunchedEffect(id) {
|
||||||
// Get messages
|
// Get messages
|
||||||
val initialMessages = chatViewModel.getChatRoomMessages(id)
|
chatViewModel.loadChatRoomMessages(id) { initialMessages ->
|
||||||
messages.clear()
|
messages.clear()
|
||||||
messages.addAll(initialMessages)
|
messages.addAll(initialMessages)
|
||||||
|
|
||||||
// Stop loading spinner
|
// Stop loading spinner
|
||||||
loading = false
|
loading = false
|
||||||
|
}
|
||||||
|
|
||||||
// Get msg relays for each member
|
// Get msg relays for each member
|
||||||
chatViewModel.chatRoomConnect(id)
|
chatViewModel.chatRoomConnect(id)
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import coil3.compose.AsyncImage
|
|||||||
import coop.composeapp.generated.resources.Res
|
import coop.composeapp.generated.resources.Res
|
||||||
import coop.composeapp.generated.resources.ic_arrow_back
|
import coop.composeapp.generated.resources.ic_arrow_back
|
||||||
import coop.composeapp.generated.resources.ic_plus
|
import coop.composeapp.generated.resources.ic_plus
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -69,7 +70,8 @@ fun ProfileEditor(
|
|||||||
initialBio: String = "",
|
initialBio: String = "",
|
||||||
initialPicture: Any? = null, // Accepts Uri (picked) or String (current URL)
|
initialPicture: Any? = null, // Accepts Uri (picked) or String (current URL)
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onConfirm: (name: String, bio: String, pictureBytes: ByteArray?, contentType: String?) -> Unit
|
onConfirm: (name: String, bio: String, pictureBytes: ByteArray?, contentType: String?) -> Unit,
|
||||||
|
ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val snackbarHostState = LocalSnackbarHostState.current
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
@@ -269,7 +271,7 @@ fun ProfileEditor(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
isBusy = true
|
isBusy = true
|
||||||
try {
|
try {
|
||||||
val bytes = withContext(Dispatchers.IO) {
|
val bytes = withContext(ioDispatcher) {
|
||||||
(picture as? Uri)?.let {
|
(picture as? Uri)?.let {
|
||||||
context.contentResolver.openInputStream(it)?.readBytes()
|
context.contentResolver.openInputStream(it)?.readBytes()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package su.reya.coop.nostr
|
package su.reya.coop.nostr
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
@@ -119,7 +120,9 @@ class MessageManager(private val nostr: Nostr) {
|
|||||||
setCachedRumor(event.id(), unsignedEvent)
|
setCachedRumor(event.id(), unsignedEvent)
|
||||||
|
|
||||||
return unsignedEvent
|
return unsignedEvent
|
||||||
} catch (e: Throwable) {
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
println("Failed to unwrap gift ${event.id().toHex()}: ${e.message}")
|
println("Failed to unwrap gift ${event.id().toHex()}: ${e.message}")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -131,7 +134,9 @@ class MessageManager(private val nostr: Nostr) {
|
|||||||
val event = client?.database()?.query(filter)?.first()
|
val event = client?.database()?.query(filter)?.first()
|
||||||
|
|
||||||
return event?.content()?.let { UnsignedEvent.fromJson(it).ensureId() }
|
return event?.content()?.let { UnsignedEvent.fromJson(it).ensureId() }
|
||||||
} catch (e: Throwable) {
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to get cached rumor: ${e.message}", e)
|
throw IllegalStateException("Failed to get cached rumor: ${e.message}", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,7 +160,9 @@ class MessageManager(private val nostr: Nostr) {
|
|||||||
.finalizeAsync(Keys.generate())
|
.finalizeAsync(Keys.generate())
|
||||||
|
|
||||||
client?.database()?.saveEvent(event)
|
client?.database()?.saveEvent(event)
|
||||||
} catch (e: Throwable) {
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
println("Failed to set cached rumor: ${e.message}")
|
println("Failed to set cached rumor: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,6 +204,8 @@ class MessageManager(private val nostr: Nostr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return roomsMap.values.sortedByDescending { it.createdAt.asSecs() }.toSet()
|
return roomsMap.values.sortedByDescending { it.createdAt.asSecs() }.toSet()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("Failed to get chat rooms: ${e.message}")
|
println("Failed to get chat rooms: ${e.message}")
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package su.reya.coop.nostr
|
package su.reya.coop.nostr
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
@@ -50,7 +51,9 @@ object NostrManager {
|
|||||||
val ALL_RELAYS = BOOTSTRAP_RELAYS + INDEXER_RELAY
|
val ALL_RELAYS = BOOTSTRAP_RELAYS + INDEXER_RELAY
|
||||||
}
|
}
|
||||||
|
|
||||||
class Nostr {
|
class Nostr(
|
||||||
|
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||||
|
) {
|
||||||
var client: Client? = null
|
var client: Client? = null
|
||||||
private set
|
private set
|
||||||
var signer: UniversalSigner = UniversalSigner(Keys.generate())
|
var signer: UniversalSigner = UniversalSigner(Keys.generate())
|
||||||
@@ -164,7 +167,7 @@ class Nostr {
|
|||||||
var processedCount = 0
|
var processedCount = 0
|
||||||
var eoseReceived = false
|
var eoseReceived = false
|
||||||
|
|
||||||
launch(Dispatchers.Default) {
|
launch(defaultDispatcher) {
|
||||||
for (event in giftWrapQueue) {
|
for (event in giftWrapQueue) {
|
||||||
val rumor = messages.extractRumor(event)
|
val rumor = messages.extractRumor(event)
|
||||||
processedCount++
|
processedCount++
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package su.reya.coop.repository
|
|||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.serialization.json.Json
|
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
|
||||||
@@ -31,6 +32,8 @@ class MediaRepository {
|
|||||||
signer = signer,
|
signer = signer,
|
||||||
)
|
)
|
||||||
descriptor?.url
|
descriptor?.url
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("Upload failed: ${e.message}")
|
println("Upload failed: ${e.message}")
|
||||||
null
|
null
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package su.reya.coop.viewmodel
|
package su.reya.coop.viewmodel
|
||||||
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import rust.nostr.sdk.AsyncNostrSigner
|
import rust.nostr.sdk.AsyncNostrSigner
|
||||||
import rust.nostr.sdk.EncryptedSecretKey
|
import rust.nostr.sdk.EncryptedSecretKey
|
||||||
@@ -32,6 +35,7 @@ class AuthViewModel(
|
|||||||
private val storage: AppStorage,
|
private val storage: AppStorage,
|
||||||
private val mediaRepository: MediaRepository,
|
private val mediaRepository: MediaRepository,
|
||||||
private val externalSignerHandler: ExternalSignerHandler? = null,
|
private val externalSignerHandler: ExternalSignerHandler? = null,
|
||||||
|
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
companion object {
|
companion object {
|
||||||
private const val KEY_USER_SIGNER = "user_signer"
|
private const val KEY_USER_SIGNER = "user_signer"
|
||||||
@@ -112,21 +116,21 @@ class AuthViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getOrInitAppKeys(): Keys {
|
private suspend fun getOrInitAppKeys(): Keys = withContext(defaultDispatcher) {
|
||||||
val secret = storage.getSecret(KEY_APP_KEYS)
|
val secret = storage.getSecret(KEY_APP_KEYS)
|
||||||
// If app keys are already stored, use them
|
// If app keys are already stored, use them
|
||||||
if (secret != null) return Keys.parse(secret)
|
if (secret != null) return@withContext Keys.parse(secret)
|
||||||
// Generate new app keys and save to the secret storage
|
// Generate new app keys and save to the secret storage
|
||||||
val keys = Keys.generate()
|
val keys = Keys.generate()
|
||||||
storage.setSecret(KEY_APP_KEYS, keys.secretKey().toBech32())
|
storage.setSecret(KEY_APP_KEYS, keys.secretKey().toBech32())
|
||||||
return keys
|
keys
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createSigner(
|
private suspend fun createSigner(
|
||||||
secret: String,
|
secret: String,
|
||||||
password: String? = null
|
password: String? = null
|
||||||
): Pair<AsyncNostrSigner, String?> {
|
): Pair<AsyncNostrSigner, String?> = withContext(defaultDispatcher) {
|
||||||
return when {
|
when {
|
||||||
secret.startsWith("nsec1") -> Keys.parse(secret) to null
|
secret.startsWith("nsec1") -> Keys.parse(secret) to null
|
||||||
|
|
||||||
secret.startsWith("ncryptsec1") -> {
|
secret.startsWith("ncryptsec1") -> {
|
||||||
|
|||||||
@@ -134,31 +134,33 @@ class ChatViewModel(
|
|||||||
return _state.value.rooms[id]
|
return _state.value.rooms[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun refreshChatRooms() {
|
fun refreshChatRooms() {
|
||||||
try {
|
viewModelScope.launch {
|
||||||
val rooms = nostr.messages.getChatRooms() ?: emptySet()
|
try {
|
||||||
_state.update { currentState ->
|
val rooms = nostr.messages.getChatRooms() ?: emptySet()
|
||||||
val newMap = currentState.rooms.toMutableMap()
|
_state.update { currentState ->
|
||||||
rooms.forEach { room -> newMap[room.id] = room }
|
val newMap = currentState.rooms.toMutableMap()
|
||||||
currentState.copy(rooms = newMap)
|
rooms.forEach { room -> newMap[room.id] = room }
|
||||||
|
currentState.copy(rooms = newMap)
|
||||||
|
}
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Error: ${e.message}")
|
||||||
}
|
}
|
||||||
} catch (e: CancellationException) {
|
|
||||||
throw e
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showError("Error: ${e.message}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getChatRoomMessages(roomId: Long): List<UnsignedEvent> {
|
fun loadChatRoomMessages(roomId: Long, onResult: (List<UnsignedEvent>) -> Unit) {
|
||||||
try {
|
viewModelScope.launch {
|
||||||
return nostr.messages.getChatRoomMessages(roomId)
|
try {
|
||||||
} catch (e: CancellationException) {
|
onResult(nostr.messages.getChatRoomMessages(roomId))
|
||||||
throw e
|
} catch (e: CancellationException) {
|
||||||
} catch (e: Exception) {
|
throw e
|
||||||
showError("Error: ${e.message}")
|
} catch (e: Exception) {
|
||||||
|
showError("Error: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return emptyList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun chatRoomConnect(roomId: Long) {
|
fun chatRoomConnect(roomId: Long) {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -13,7 +12,6 @@ import kotlinx.coroutines.flow.filterNotNull
|
|||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
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
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -21,19 +19,15 @@ import kotlinx.coroutines.sync.Mutex
|
|||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
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.nostr.Nostr
|
import su.reya.coop.nostr.Nostr
|
||||||
import su.reya.coop.repository.MediaRepository
|
import su.reya.coop.repository.MediaRepository
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
|
|
||||||
data class NostrAppState(
|
data class NostrAppState(
|
||||||
val isBusy: Boolean = false,
|
val isBusy: Boolean = false,
|
||||||
val isRelayListEmpty: Boolean = false,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class NostrViewModel(
|
class NostrViewModel(
|
||||||
@@ -51,9 +45,6 @@ class NostrViewModel(
|
|||||||
private val metadataRequestChannel = Channel<PublicKey>(Channel.UNLIMITED)
|
private val metadataRequestChannel = Channel<PublicKey>(Channel.UNLIMITED)
|
||||||
private val seenPublicKeys = mutableSetOf<PublicKey>()
|
private val seenPublicKeys = mutableSetOf<PublicKey>()
|
||||||
|
|
||||||
val isRelayListEmpty = appState.map { it.isRelayListEmpty }
|
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val currentUserProfile = nostr.signer.publicKeyFlow
|
val currentUserProfile = nostr.signer.publicKeyFlow
|
||||||
.flatMapLatest { pubkey ->
|
.flatMapLatest { pubkey ->
|
||||||
@@ -69,8 +60,8 @@ class NostrViewModel(
|
|||||||
// Automatically reconnect bootstrap relays
|
// Automatically reconnect bootstrap relays
|
||||||
reconnect()
|
reconnect()
|
||||||
|
|
||||||
// Observe the signer state and verify the relay list
|
// Fetch metadata for the current user
|
||||||
observeSignerAndCheckRelays()
|
fetchUserMetadata()
|
||||||
|
|
||||||
// Get all local stored metadata
|
// Get all local stored metadata
|
||||||
getCacheMetadata()
|
getCacheMetadata()
|
||||||
@@ -153,7 +144,7 @@ class NostrViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeSignerAndCheckRelays() {
|
private fun fetchUserMetadata() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
// Wait until the client is ready
|
// Wait until the client is ready
|
||||||
nostr.waitUntilInitialized()
|
nostr.waitUntilInitialized()
|
||||||
@@ -163,13 +154,6 @@ class NostrViewModel(
|
|||||||
|
|
||||||
// Get all metadata for the current user
|
// Get all metadata for the current user
|
||||||
nostr.profiles.getUserMetadata()
|
nostr.profiles.getUserMetadata()
|
||||||
|
|
||||||
// Small delay to ensure all relays are connected
|
|
||||||
delay(2.seconds)
|
|
||||||
|
|
||||||
// Check if the relay list is empty
|
|
||||||
val relays = nostr.relays.getMsgRelays(currentUser)
|
|
||||||
if (relays.isEmpty()) _appState.update { it.copy(isRelayListEmpty = true) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,15 +178,6 @@ class NostrViewModel(
|
|||||||
|
|
||||||
fun resetInternalState() {
|
fun resetInternalState() {
|
||||||
_contactList.value = emptySet()
|
_contactList.value = emptySet()
|
||||||
_appState.update {
|
|
||||||
it.copy(
|
|
||||||
isRelayListEmpty = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dismissRelayWarning() {
|
|
||||||
_appState.update { it.copy(isRelayListEmpty = false) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateProfile(
|
fun updateProfile(
|
||||||
@@ -238,131 +213,36 @@ class NostrViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun refetchMsgRelays() {
|
private fun newContact(publicKey: PublicKey) {
|
||||||
val currentUser = nostr.signer.getPublicKeyAsync() ?: return
|
|
||||||
val relays = nostr.relays.fetchMsgRelays(currentUser)
|
|
||||||
|
|
||||||
if (relays.isNotEmpty()) dismissRelayWarning()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun useDefaultMsgRelayList() {
|
|
||||||
try {
|
|
||||||
val defaultRelays = nostr.relays.getDefaultMsgRelayList()
|
|
||||||
nostr.relays.setMsgRelays(defaultRelays)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showError("Error: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun currentUserRelayList(): Map<RelayUrl, RelayMetadata?> {
|
|
||||||
try {
|
|
||||||
val currentUser = nostr.signer.getPublicKeyAsync() ?: throw Exception("User not found")
|
|
||||||
return nostr.relays.getRelayList(currentUser)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showError("Error: ${e.message}")
|
|
||||||
return emptyMap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun addInboxRelay(relay: String) {
|
|
||||||
try {
|
|
||||||
val relayUrl = RelayUrl.parse(relay)
|
|
||||||
val relays = currentUserRelayList().toMutableMap()
|
|
||||||
relays[relayUrl] = RelayMetadata.WRITE
|
|
||||||
|
|
||||||
nostr.relays.setRelaylist(relays)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showError("Error: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun addOutboxRelay(relay: String) {
|
|
||||||
try {
|
|
||||||
val relayUrl = RelayUrl.parse(relay)
|
|
||||||
val relays = currentUserRelayList().toMutableMap()
|
|
||||||
relays[relayUrl] = RelayMetadata.READ
|
|
||||||
|
|
||||||
nostr.relays.setRelaylist(relays)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showError("Error: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun removeRelay(relay: String) {
|
|
||||||
try {
|
|
||||||
val relayUrl = RelayUrl.parse(relay)
|
|
||||||
val relays = currentUserRelayList().toMutableMap()
|
|
||||||
relays.remove(relayUrl)
|
|
||||||
|
|
||||||
nostr.relays.setRelaylist(relays)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showError("Error: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun currentUserMsgRelayList(): List<RelayUrl> {
|
|
||||||
try {
|
|
||||||
val currentUser = nostr.signer.getPublicKeyAsync() ?: throw Exception("User not found")
|
|
||||||
return nostr.relays.getMsgRelays(currentUser)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showError("Error: ${e.message}")
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun addMsgRelay(relay: String) {
|
|
||||||
try {
|
|
||||||
val relayUrl = RelayUrl.parse(relay)
|
|
||||||
val relays = currentUserMsgRelayList().toMutableSet()
|
|
||||||
relays.add(relayUrl)
|
|
||||||
|
|
||||||
nostr.relays.setMsgRelays(relays.toList())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showError("Error: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun removeMsgRelay(relay: String) {
|
|
||||||
try {
|
|
||||||
val relayUrl = RelayUrl.parse(relay)
|
|
||||||
val relays = currentUserMsgRelayList().toMutableSet()
|
|
||||||
relays.remove(relayUrl)
|
|
||||||
|
|
||||||
nostr.relays.setMsgRelays(relays.toList())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showError("Error: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun newContact(publicKey: PublicKey) {
|
|
||||||
if (publicKey in contactList.value) return
|
if (publicKey in contactList.value) return
|
||||||
|
|
||||||
try {
|
viewModelScope.launch {
|
||||||
val updated = contactList.value + publicKey
|
try {
|
||||||
// Publish new event
|
val updated = contactList.value + publicKey
|
||||||
nostr.profiles.setContactList(updated.toList())
|
// Publish new event
|
||||||
// Optimistic local update
|
nostr.profiles.setContactList(updated.toList())
|
||||||
_contactList.update { it + publicKey }
|
// Optimistic local update
|
||||||
} catch (e: Exception) {
|
_contactList.update { it + publicKey }
|
||||||
showError("Error: ${e.message}")
|
} catch (e: Exception) {
|
||||||
|
showError("Error: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun addContact(address: String): Boolean {
|
fun addContact(address: String) {
|
||||||
val pubkey = try {
|
viewModelScope.launch {
|
||||||
if (address.contains("@")) {
|
val pubkey = try {
|
||||||
nostr.profiles.searchByAddress(address)
|
if (address.contains("@")) {
|
||||||
} else {
|
nostr.profiles.searchByAddress(address)
|
||||||
PublicKey.parse(address)
|
} else {
|
||||||
|
PublicKey.parse(address)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Invalid contact address: ${e.message}")
|
||||||
|
return@launch
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
showError("Invalid contact address: ${e.message}")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return run {
|
|
||||||
newContact(pubkey)
|
newContact(pubkey)
|
||||||
true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,48 +262,58 @@ class NostrViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun searchByAddress(query: String): PublicKey? {
|
fun searchByAddress(query: String, onResult: (PublicKey?) -> Unit) {
|
||||||
try {
|
viewModelScope.launch {
|
||||||
return nostr.profiles.searchByAddress(query)
|
try {
|
||||||
} catch (e: Exception) {
|
onResult(nostr.profiles.searchByAddress(query))
|
||||||
showError("Error: ${e.message}")
|
} catch (e: Exception) {
|
||||||
}
|
showError("Error: ${e.message}")
|
||||||
return null
|
onResult(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun searchByNostr(query: String): List<PublicKey> {
|
|
||||||
try {
|
|
||||||
return nostr.profiles.searchByNostr(query)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showError("Error: ${e.message}")
|
|
||||||
}
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun verifyActivity(pubkey: PublicKey): Timestamp? {
|
|
||||||
return try {
|
|
||||||
nostr.profiles.verifyActivity(pubkey)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showError("Error: ${e.message}")
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun verifyContact(pubkey: PublicKey): Boolean {
|
fun searchByNostr(query: String, onResult: (List<PublicKey>) -> Unit) {
|
||||||
return try {
|
viewModelScope.launch {
|
||||||
nostr.profiles.verifyContact(pubkey)
|
try {
|
||||||
} catch (e: Exception) {
|
onResult(nostr.profiles.searchByNostr(query))
|
||||||
showError("Error: ${e.message}")
|
} catch (e: Exception) {
|
||||||
false
|
showError("Error: ${e.message}")
|
||||||
|
onResult(emptyList())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun mutualContacts(pubkey: PublicKey): Set<PublicKey> {
|
fun verifyActivity(pubkey: PublicKey, onResult: (Timestamp?) -> Unit) {
|
||||||
return try {
|
viewModelScope.launch {
|
||||||
nostr.profiles.mutualContacts(pubkey)
|
try {
|
||||||
} catch (e: Exception) {
|
onResult(nostr.profiles.verifyActivity(pubkey))
|
||||||
showError("Error: ${e.message}")
|
} catch (e: Exception) {
|
||||||
setOf()
|
showError("Error: ${e.message}")
|
||||||
|
onResult(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun verifyContact(pubkey: PublicKey, onResult: (Boolean) -> Unit) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
onResult(nostr.profiles.verifyContact(pubkey))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Error: ${e.message}")
|
||||||
|
onResult(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mutualContacts(pubkey: PublicKey, onResult: (Set<PublicKey>) -> Unit) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
onResult(nostr.profiles.mutualContacts(pubkey))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Error: ${e.message}")
|
||||||
|
onResult(emptySet())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
package su.reya.coop.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import rust.nostr.sdk.RelayMetadata
|
||||||
|
import rust.nostr.sdk.RelayUrl
|
||||||
|
import su.reya.coop.nostr.Nostr
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
class RelayViewModel(
|
||||||
|
private val nostr: Nostr,
|
||||||
|
) : BaseViewModel() {
|
||||||
|
private val _isRelayListEmpty = MutableStateFlow(false)
|
||||||
|
val isRelayListEmpty: StateFlow<Boolean> = _isRelayListEmpty.asStateFlow()
|
||||||
|
|
||||||
|
private val _currentUserRelayList = MutableStateFlow<Map<RelayUrl, RelayMetadata?>>(emptyMap())
|
||||||
|
val currentUserRelayList = _currentUserRelayList.asStateFlow()
|
||||||
|
|
||||||
|
private val _currentUserMsgRelayList = MutableStateFlow<List<RelayUrl>>(emptyList())
|
||||||
|
val currentUserMsgRelayList = _currentUserMsgRelayList.asStateFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
checkRelayList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkRelayList() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
// Wait until the client is ready
|
||||||
|
nostr.waitUntilInitialized()
|
||||||
|
|
||||||
|
// Wait until a signer is explicitly set (which updates publicKeyFlow)
|
||||||
|
val currentUser = nostr.signer.publicKeyFlow.filterNotNull().first()
|
||||||
|
|
||||||
|
// Small delay to ensure all relays are connected
|
||||||
|
delay(2.seconds)
|
||||||
|
|
||||||
|
// Check if the relay list is empty
|
||||||
|
val relays = nostr.relays.getMsgRelays(currentUser)
|
||||||
|
if (relays.isEmpty()) _isRelayListEmpty.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissRelayWarning() {
|
||||||
|
_isRelayListEmpty.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetInternalState() {
|
||||||
|
_isRelayListEmpty.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refetchMsgRelays() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val currentUser = nostr.signer.getPublicKeyAsync() ?: return@launch
|
||||||
|
val relays = nostr.relays.fetchMsgRelays(currentUser)
|
||||||
|
|
||||||
|
if (relays.isNotEmpty()) dismissRelayWarning()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun useDefaultMsgRelayList() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val defaultRelays = nostr.relays.getDefaultMsgRelayList()
|
||||||
|
nostr.relays.setMsgRelays(defaultRelays)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadCurrentUserRelayList() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val currentUser = nostr.signer.getPublicKeyAsync() ?: throw Exception("User not found")
|
||||||
|
_currentUserRelayList.value = nostr.relays.getRelayList(currentUser)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun currentUserRelayListInternal(): Map<RelayUrl, RelayMetadata?> {
|
||||||
|
try {
|
||||||
|
val currentUser = nostr.signer.getPublicKeyAsync() ?: throw Exception("User not found")
|
||||||
|
return nostr.relays.getRelayList(currentUser)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Error: ${e.message}")
|
||||||
|
return emptyMap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addInboxRelay(relay: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val relayUrl = RelayUrl.parse(relay)
|
||||||
|
val relays = currentUserRelayListInternal().toMutableMap()
|
||||||
|
relays[relayUrl] = RelayMetadata.WRITE
|
||||||
|
|
||||||
|
nostr.relays.setRelaylist(relays)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addOutboxRelay(relay: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val relayUrl = RelayUrl.parse(relay)
|
||||||
|
val relays = currentUserRelayListInternal().toMutableMap()
|
||||||
|
relays[relayUrl] = RelayMetadata.READ
|
||||||
|
|
||||||
|
nostr.relays.setRelaylist(relays)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeRelay(relay: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val relayUrl = RelayUrl.parse(relay)
|
||||||
|
val relays = currentUserRelayListInternal().toMutableMap()
|
||||||
|
relays.remove(relayUrl)
|
||||||
|
|
||||||
|
nostr.relays.setRelaylist(relays)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadCurrentUserMsgRelayList() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val currentUser = nostr.signer.getPublicKeyAsync() ?: throw Exception("User not found")
|
||||||
|
_currentUserMsgRelayList.value = nostr.relays.getMsgRelays(currentUser)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun currentUserMsgRelayListInternal(): List<RelayUrl> {
|
||||||
|
try {
|
||||||
|
val currentUser = nostr.signer.getPublicKeyAsync() ?: throw Exception("User not found")
|
||||||
|
return nostr.relays.getMsgRelays(currentUser)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Error: ${e.message}")
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addMsgRelay(relay: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val relayUrl = RelayUrl.parse(relay)
|
||||||
|
val relays = currentUserMsgRelayListInternal().toMutableSet()
|
||||||
|
relays.add(relayUrl)
|
||||||
|
|
||||||
|
nostr.relays.setMsgRelays(relays.toList())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeMsgRelay(relay: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val relayUrl = RelayUrl.parse(relay)
|
||||||
|
val relays = currentUserMsgRelayListInternal().toMutableSet()
|
||||||
|
relays.remove(relayUrl)
|
||||||
|
|
||||||
|
nostr.relays.setMsgRelays(relays.toList())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user