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