Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a3ab489d44 |
@@ -19,12 +19,14 @@ kotlin {
|
|||||||
androidMain.dependencies {
|
androidMain.dependencies {
|
||||||
implementation(libs.compose.uiToolingPreview)
|
implementation(libs.compose.uiToolingPreview)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation(libs.androidx.navigation.compose)
|
|
||||||
implementation(libs.androidx.lifecycle.process)
|
implementation(libs.androidx.lifecycle.process)
|
||||||
|
implementation(libs.androidx.lifecycle.runtimeCompose)
|
||||||
|
implementation(libs.jetbrains.navigation3.ui)
|
||||||
|
implementation(libs.jetbrains.lifecycle.viewmodelNavigation3)
|
||||||
implementation(libs.androidx.core.splashscreen)
|
implementation(libs.androidx.core.splashscreen)
|
||||||
|
implementation("su.reya:nostr-sdk-kmp:0.2.3")
|
||||||
implementation("io.coil-kt.coil3:coil-compose:3.4.0")
|
implementation("io.coil-kt.coil3:coil-compose:3.4.0")
|
||||||
implementation("io.coil-kt.coil3:coil-network-okhttp:3.4.0")
|
implementation("io.coil-kt.coil3:coil-network-okhttp:3.4.0")
|
||||||
implementation("su.reya:nostr-sdk-kmp:0.2.3")
|
|
||||||
implementation("io.github.kalinjul.easyqrscan:scanner:0.7.0")
|
implementation("io.github.kalinjul.easyqrscan:scanner:0.7.0")
|
||||||
implementation("io.github.alexzhirkevich:qrose:1.1.2")
|
implementation("io.github.alexzhirkevich:qrose:1.1.2")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package su.reya.coop
|
package su.reya.coop
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -29,8 +33,10 @@ import androidx.compose.runtime.CompositionLocalProvider
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -39,12 +45,14 @@ import androidx.compose.ui.text.font.FontStyle
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
import androidx.core.util.Consumer
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation3.runtime.NavBackStack
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation3.runtime.NavKey
|
||||||
import androidx.navigation.navDeepLink
|
import androidx.navigation3.runtime.entryProvider
|
||||||
import androidx.navigation.toRoute
|
import androidx.navigation3.runtime.rememberNavBackStack
|
||||||
|
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||||
|
import androidx.navigation3.ui.NavDisplay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import su.reya.coop.screens.ChatScreen
|
import su.reya.coop.screens.ChatScreen
|
||||||
import su.reya.coop.screens.HomeScreen
|
import su.reya.coop.screens.HomeScreen
|
||||||
@@ -65,26 +73,41 @@ val LocalSnackbarHostState = staticCompositionLocalOf<SnackbarHostState> {
|
|||||||
error("No SnackbarHostState provided")
|
error("No SnackbarHostState provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
val LocalNavController = staticCompositionLocalOf<NavController> {
|
val LocalNavigator = staticCompositionLocalOf<Navigator> {
|
||||||
error("No NavController provided")
|
error("No Navigator provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
val LocalScanResult = staticCompositionLocalOf<QrScanResult> {
|
||||||
|
error("No QrScanResult provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun App(viewModel: NostrViewModel) {
|
fun App(viewModel: NostrViewModel) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val navController = rememberNavController()
|
val activity = context as? ComponentActivity
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val darkMode = isSystemInDarkTheme()
|
val sheetState = rememberModalBottomSheetState()
|
||||||
|
val backStack = rememberNavBackStack(Screen.Home)
|
||||||
|
val navigator = remember(backStack) { Navigator(backStack) }
|
||||||
|
val qrScanResult = remember { QrScanResult() }
|
||||||
|
|
||||||
|
val signerRequired by viewModel.signerRequired.collectAsState(initial = null)
|
||||||
|
val isRelayListEmpty by viewModel.isRelayListEmpty.collectAsState()
|
||||||
|
|
||||||
// Snackbar
|
// Snackbar
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
// Check if dark theme enabled
|
||||||
|
val darkMode = isSystemInDarkTheme()
|
||||||
|
|
||||||
// Enabled the dynamic color scheme
|
// Enabled the dynamic color scheme
|
||||||
val colorScheme = when {
|
val colorScheme = when {
|
||||||
// Enable the dynamic color scheme for Android 12+
|
// Enable the dynamic color scheme for Android 12+
|
||||||
android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S -> {
|
android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S -> {
|
||||||
if (darkMode) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
if (isSystemInDarkTheme()) dynamicDarkColorScheme(context) else dynamicLightColorScheme(
|
||||||
|
context
|
||||||
|
)
|
||||||
}
|
}
|
||||||
// When dark mode is enabled, use the dark color scheme
|
// When dark mode is enabled, use the dark color scheme
|
||||||
darkMode -> darkColorScheme()
|
darkMode -> darkColorScheme()
|
||||||
@@ -92,12 +115,48 @@ fun App(viewModel: NostrViewModel) {
|
|||||||
else -> expressiveLightColorScheme()
|
else -> expressiveLightColorScheme()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BackHandler(enabled = backStack.size > 1) {
|
||||||
|
navigator.goBack()
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.errorEvents.collect { message ->
|
viewModel.errorEvents.collect { message ->
|
||||||
snackbarHostState.showSnackbar(message)
|
snackbarHostState.showSnackbar(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(activity) {
|
||||||
|
activity?.let {
|
||||||
|
fun handleIntent(intent: Intent) {
|
||||||
|
val screen = Screen.fromIntent(intent)
|
||||||
|
// Prevent pushing the same screen
|
||||||
|
if (screen != null && backStack.lastOrNull() != screen) {
|
||||||
|
navigator.navigate(screen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the intent that started the Activity
|
||||||
|
handleIntent(it.intent)
|
||||||
|
|
||||||
|
// Handle new intents while the Activity is running
|
||||||
|
val listener = Consumer<Intent> { intent -> handleIntent(intent) }
|
||||||
|
it.addOnNewIntentListener(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(backStack.size) {
|
||||||
|
if (backStack.isEmpty()) {
|
||||||
|
(context as? Activity)?.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(signerRequired) {
|
||||||
|
if (signerRequired == true && backStack.last() != Screen.Onboarding) {
|
||||||
|
backStack.clear()
|
||||||
|
backStack.add(Screen.Onboarding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MaterialExpressiveTheme(
|
MaterialExpressiveTheme(
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
typography = Typography(),
|
typography = Typography(),
|
||||||
@@ -106,109 +165,70 @@ fun App(viewModel: NostrViewModel) {
|
|||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalNostrViewModel provides viewModel,
|
LocalNostrViewModel provides viewModel,
|
||||||
LocalSnackbarHostState provides snackbarHostState,
|
LocalSnackbarHostState provides snackbarHostState,
|
||||||
LocalNavController provides navController,
|
LocalNavigator provides navigator,
|
||||||
|
LocalScanResult provides qrScanResult,
|
||||||
) {
|
) {
|
||||||
val signerRequired by viewModel.signerRequired.collectAsState(initial = null)
|
NavDisplay(
|
||||||
val isRelayListEmpty by viewModel.isRelayListEmpty.collectAsState()
|
backStack = backStack,
|
||||||
val sheetState = rememberModalBottomSheetState()
|
onBack = {
|
||||||
|
if (backStack.size > 1) {
|
||||||
LaunchedEffect(signerRequired) {
|
backStack.removeLastOrNull()
|
||||||
// Navigate to the home screen if the secret is already set
|
} else {
|
||||||
if (signerRequired == false) {
|
(context as? Activity)?.finish()
|
||||||
navController.navigate(Screen.Home) {
|
}
|
||||||
popUpTo(Screen.Onboarding) { inclusive = true }
|
},
|
||||||
|
entryDecorators = listOf(
|
||||||
|
rememberSaveableStateHolderNavEntryDecorator(),
|
||||||
|
rememberViewModelStoreNavEntryDecorator()
|
||||||
|
),
|
||||||
|
entryProvider = entryProvider {
|
||||||
|
entry<Screen.Home> {
|
||||||
|
HomeScreen()
|
||||||
|
}
|
||||||
|
entry<Screen.Onboarding> {
|
||||||
|
OnboardingScreen()
|
||||||
|
}
|
||||||
|
entry<Screen.Import> {
|
||||||
|
ImportScreen(
|
||||||
|
onSave = { secret ->
|
||||||
|
viewModel.importIdentity(secret)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
entry<Screen.NewIdentity> {
|
||||||
|
NewIdentityScreen(
|
||||||
|
onSave = { name, bio, uri ->
|
||||||
|
val contentType =
|
||||||
|
uri?.let { context.contentResolver.getType(it) }
|
||||||
|
val picture = uri?.let {
|
||||||
|
context.contentResolver.openInputStream(it)?.use { input ->
|
||||||
|
input.readBytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModel.createIdentity(name, bio, picture, contentType)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
entry<Screen.Chat> { key ->
|
||||||
|
ChatScreen(id = key.id)
|
||||||
|
}
|
||||||
|
entry<Screen.NewChat> {
|
||||||
|
NewChatScreen()
|
||||||
|
}
|
||||||
|
entry<Screen.Profile> { key ->
|
||||||
|
ProfileScreen(pubkey = key.pubkey)
|
||||||
|
}
|
||||||
|
entry<Screen.Scan> {
|
||||||
|
ScanScreen()
|
||||||
|
}
|
||||||
|
entry<Screen.MyQr> {
|
||||||
|
MyQrScreen()
|
||||||
|
}
|
||||||
|
entry<Screen.Relay> {
|
||||||
|
RelayScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
// Keep the splash screen visible until the secret check is complete
|
|
||||||
if (signerRequired == null) {
|
|
||||||
return@CompositionLocalProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
NavHost(
|
|
||||||
navController = navController,
|
|
||||||
startDestination = if (signerRequired!!) Screen.Onboarding else Screen.Home
|
|
||||||
) {
|
|
||||||
composable<Screen.Onboarding> { backStackEntry ->
|
|
||||||
OnboardingScreen(
|
|
||||||
onOpenImport = { navController.navigate(Screen.Import) },
|
|
||||||
onOpenNew = { navController.navigate(Screen.NewIdentity) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
composable<Screen.Import> { backStackEntry ->
|
|
||||||
val isCreating by viewModel.isCreating.collectAsState()
|
|
||||||
|
|
||||||
ImportScreen(
|
|
||||||
isLoading = isCreating,
|
|
||||||
onBack = { navController.popBackStack() },
|
|
||||||
onSave = { secret ->
|
|
||||||
viewModel.importIdentity(secret)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
composable<Screen.NewIdentity> { backStackEntry ->
|
|
||||||
val isCreating by viewModel.isCreating.collectAsState()
|
|
||||||
|
|
||||||
NewIdentityScreen(
|
|
||||||
isLoading = isCreating,
|
|
||||||
onBack = { navController.popBackStack() },
|
|
||||||
onSave = { name, bio, uri ->
|
|
||||||
val contentType = uri?.let { context.contentResolver.getType(it) }
|
|
||||||
val picture = uri?.let {
|
|
||||||
context.contentResolver.openInputStream(it)?.use { input ->
|
|
||||||
input.readBytes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
viewModel.createIdentity(name, bio, picture, contentType)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
composable<Screen.Home> { backStackEntry ->
|
|
||||||
HomeScreen(
|
|
||||||
onOpenChat = { id -> navController.navigate(Screen.Chat(id)) },
|
|
||||||
onNewChat = { navController.navigate(Screen.NewChat) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
composable<Screen.Chat>(
|
|
||||||
deepLinks = listOf(
|
|
||||||
navDeepLink<Screen.Chat>(basePath = "coop://chat")
|
|
||||||
)
|
|
||||||
) { backStackEntry ->
|
|
||||||
val chat: Screen.Chat = backStackEntry.toRoute()
|
|
||||||
ChatScreen(
|
|
||||||
id = chat.id,
|
|
||||||
onBack = { navController.popBackStack() },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
composable<Screen.Profile> { backStackEntry ->
|
|
||||||
val profile: Screen.Profile = backStackEntry.toRoute()
|
|
||||||
ProfileScreen(
|
|
||||||
pubkey = profile.pubkey,
|
|
||||||
onBack = { navController.popBackStack() },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
composable<Screen.NewChat> { backStackEntry ->
|
|
||||||
NewChatScreen(
|
|
||||||
onBack = { navController.popBackStack() },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
composable<Screen.Scan> { backStackEntry ->
|
|
||||||
ScanScreen(
|
|
||||||
onBack = { navController.popBackStack() },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
composable<Screen.MyQr> { backStackEntry ->
|
|
||||||
MyQrScreen(
|
|
||||||
onBack = { navController.popBackStack() },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
composable<Screen.Relay> { backStackEntry ->
|
|
||||||
RelayScreen(
|
|
||||||
onBack = { navController.popBackStack() },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
||||||
@@ -267,3 +287,23 @@ fun App(viewModel: NostrViewModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Navigator(private val backStack: NavBackStack<NavKey>) {
|
||||||
|
fun navigate(route: NavKey) {
|
||||||
|
backStack.add(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun goBack() {
|
||||||
|
if (backStack.size > 1) {
|
||||||
|
backStack.removeAt(backStack.lastIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class QrScanResult {
|
||||||
|
var content by mutableStateOf<String?>(null)
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
content = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,25 @@
|
|||||||
package su.reya.coop
|
package su.reya.coop
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.navigation3.runtime.NavKey
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
sealed interface Screen {
|
sealed interface Screen : NavKey {
|
||||||
|
companion object {
|
||||||
|
fun fromIntent(intent: Intent): Screen? {
|
||||||
|
val data = intent.data ?: return null
|
||||||
|
if (data.scheme != "coop") return null
|
||||||
|
|
||||||
|
return when (data.host) {
|
||||||
|
// Matches coop://chat/{id}
|
||||||
|
"chat" -> data.pathSegments.firstOrNull()?.toLongOrNull()?.let { Chat(it) }
|
||||||
|
// Matches coop://profile/{pubkey}
|
||||||
|
"profile" -> data.pathSegments.firstOrNull()?.let { Profile(it) }
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object Home : Screen
|
data object Home : Screen
|
||||||
|
|
||||||
|
|||||||
@@ -30,9 +30,10 @@ class NostrForegroundService : Service() {
|
|||||||
return ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
|
return ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
createNotificationChannel()
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
createNotificationChannel()
|
||||||
|
}
|
||||||
|
|
||||||
val notification = createNotification()
|
val notification = createNotification()
|
||||||
startForeground(1, notification)
|
startForeground(1, notification)
|
||||||
@@ -78,7 +79,7 @@ class NostrForegroundService : Service() {
|
|||||||
val manager = getSystemService(NotificationManager::class.java)
|
val manager = getSystemService(NotificationManager::class.java)
|
||||||
|
|
||||||
val serviceChannel = NotificationChannel(
|
val serviceChannel = NotificationChannel(
|
||||||
"nostr_service_silent",
|
"nostr_service",
|
||||||
"Nostr Background Status",
|
"Nostr Background Status",
|
||||||
NotificationManager.IMPORTANCE_MIN
|
NotificationManager.IMPORTANCE_MIN
|
||||||
).apply {
|
).apply {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ import coop.composeapp.generated.resources.ic_send
|
|||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import org.jetbrains.compose.resources.painterResource
|
import org.jetbrains.compose.resources.painterResource
|
||||||
import rust.nostr.sdk.UnsignedEvent
|
import rust.nostr.sdk.UnsignedEvent
|
||||||
import su.reya.coop.LocalNavController
|
import su.reya.coop.LocalNavigator
|
||||||
import su.reya.coop.LocalNostrViewModel
|
import su.reya.coop.LocalNostrViewModel
|
||||||
import su.reya.coop.LocalSnackbarHostState
|
import su.reya.coop.LocalSnackbarHostState
|
||||||
import su.reya.coop.Screen
|
import su.reya.coop.Screen
|
||||||
@@ -66,12 +66,9 @@ import su.reya.coop.shared.pictureFlow
|
|||||||
import su.reya.coop.short
|
import su.reya.coop.short
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatScreen(
|
fun ChatScreen(id: Long) {
|
||||||
id: Long,
|
|
||||||
onBack: () -> Unit,
|
|
||||||
) {
|
|
||||||
val snackbarHostState = LocalSnackbarHostState.current
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
val navController = LocalNavController.current
|
val navigator = LocalNavigator.current
|
||||||
val viewModel = LocalNostrViewModel.current
|
val viewModel = LocalNostrViewModel.current
|
||||||
|
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
@@ -153,7 +150,7 @@ fun ChatScreen(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
room.members.firstOrNull()?.let { pubkey ->
|
room.members.firstOrNull()?.let { pubkey ->
|
||||||
navController.navigate(Screen.Profile(pubkey.toBech32()))
|
navigator.navigate(Screen.Profile(pubkey.toBech32()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
@@ -185,7 +182,7 @@ fun ChatScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = { navigator.goBack() }) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(Res.drawable.ic_arrow_back),
|
painter = painterResource(Res.drawable.ic_arrow_back),
|
||||||
contentDescription = "Back"
|
contentDescription = "Back"
|
||||||
|
|||||||
@@ -70,8 +70,9 @@ import coop.composeapp.generated.resources.ic_scanner
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jetbrains.compose.resources.painterResource
|
import org.jetbrains.compose.resources.painterResource
|
||||||
import rust.nostr.sdk.PublicKey
|
import rust.nostr.sdk.PublicKey
|
||||||
import su.reya.coop.LocalNavController
|
import su.reya.coop.LocalNavigator
|
||||||
import su.reya.coop.LocalNostrViewModel
|
import su.reya.coop.LocalNostrViewModel
|
||||||
|
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
|
||||||
import su.reya.coop.Screen
|
import su.reya.coop.Screen
|
||||||
@@ -83,11 +84,9 @@ import su.reya.coop.short
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeScreen(
|
fun HomeScreen() {
|
||||||
onOpenChat: (Long) -> Unit,
|
val navigator = LocalNavigator.current
|
||||||
onNewChat: () -> Unit,
|
val qrScanResult = LocalScanResult.current
|
||||||
) {
|
|
||||||
val navController = LocalNavController.current
|
|
||||||
val snackbarHostState = LocalSnackbarHostState.current
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
val clipboardManager = LocalClipboard.current
|
val clipboardManager = LocalClipboard.current
|
||||||
val viewModel = LocalNostrViewModel.current
|
val viewModel = LocalNostrViewModel.current
|
||||||
@@ -107,33 +106,24 @@ fun HomeScreen(
|
|||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
var isRefreshing by remember { mutableStateOf(false) }
|
var isRefreshing by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val savedStateHandle = navController.currentBackStackEntry?.savedStateHandle
|
|
||||||
val qrResult by savedStateHandle
|
|
||||||
?.getStateFlow<String?>("qr_result", null)
|
|
||||||
?.collectAsState()
|
|
||||||
?: remember { mutableStateOf(null) }
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (qrResult == null) {
|
viewModel.getChatRooms()
|
||||||
viewModel.getChatRooms()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(qrResult) {
|
LaunchedEffect(qrScanResult.content) {
|
||||||
qrResult?.let { result ->
|
qrScanResult.content?.let { result ->
|
||||||
runCatching { PublicKey.parse(result) }
|
runCatching { PublicKey.parse(result) }
|
||||||
.onSuccess { pubkey ->
|
.onSuccess { pubkey ->
|
||||||
try {
|
try {
|
||||||
val roomId = viewModel.createChatRoom(listOf(pubkey))
|
val roomId = viewModel.createChatRoom(listOf(pubkey))
|
||||||
navController.navigate(Screen.Chat(roomId))
|
navigator.navigate(Screen.Chat(roomId))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.message?.let { snackbarHostState.showSnackbar(it) }
|
e.message?.let { snackbarHostState.showSnackbar(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onFailure { e -> println("Failed to parse QR: ${e.message}") }
|
.onFailure { e -> println("Failed to parse QR: ${e.message}") }
|
||||||
|
|
||||||
// Clear the nav state
|
// Clear the nav state
|
||||||
navController.currentBackStackEntry?.savedStateHandle?.remove<String>("qr_result")
|
qrScanResult.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +143,7 @@ fun HomeScreen(
|
|||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
// QR Scanner
|
// QR Scanner
|
||||||
IconButton(onClick = { navController.navigate(Screen.Scan) }) {
|
IconButton(onClick = { navigator.navigate(Screen.Scan) }) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(Res.drawable.ic_scanner),
|
painter = painterResource(Res.drawable.ic_scanner),
|
||||||
contentDescription = "Scanner"
|
contentDescription = "Scanner"
|
||||||
@@ -184,7 +174,7 @@ fun HomeScreen(
|
|||||||
state = rememberTooltipState(),
|
state = rememberTooltipState(),
|
||||||
) {
|
) {
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
onClick = onNewChat,
|
onClick = { navigator.navigate(Screen.NewChat) },
|
||||||
expanded = expandedFab,
|
expanded = expandedFab,
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -261,7 +251,7 @@ fun HomeScreen(
|
|||||||
items(chatRooms.toList(), key = { it.id }) { room ->
|
items(chatRooms.toList(), key = { it.id }) { room ->
|
||||||
ChatRoom(
|
ChatRoom(
|
||||||
room = room,
|
room = room,
|
||||||
onClick = { onOpenChat(room.id) }
|
onClick = { navigator.navigate(Screen.Chat(room.id)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,7 +329,7 @@ fun HomeScreen(
|
|||||||
}
|
}
|
||||||
FilledIconButton(
|
FilledIconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
dismissAndRun { navController.navigate(Screen.MyQr) }
|
dismissAndRun { navigator.navigate(Screen.MyQr) }
|
||||||
},
|
},
|
||||||
shape = MaterialShapes.Square.toShape()
|
shape = MaterialShapes.Square.toShape()
|
||||||
) {
|
) {
|
||||||
@@ -408,11 +398,11 @@ fun ChatRoom(room: Room, onClick: () -> Unit) {
|
|||||||
fun BottomMenuList(
|
fun BottomMenuList(
|
||||||
onDismiss: (suspend () -> Unit) -> Unit
|
onDismiss: (suspend () -> Unit) -> Unit
|
||||||
) {
|
) {
|
||||||
val navController = LocalNavController.current
|
val navigator = LocalNavigator.current
|
||||||
val viewModel = LocalNostrViewModel.current
|
val viewModel = LocalNostrViewModel.current
|
||||||
|
|
||||||
val defaultMenuList = listOf(
|
val defaultMenuList = listOf(
|
||||||
"Relay Management" to { navController.navigate(Screen.Relay) },
|
"Relay Management" to { navigator.navigate(Screen.Relay) },
|
||||||
"Spams & Blocks" to { },
|
"Spams & Blocks" to { },
|
||||||
"Contacts" to { },
|
"Contacts" to { },
|
||||||
"Settings" to { }
|
"Settings" to { }
|
||||||
|
|||||||
@@ -58,8 +58,9 @@ import org.jetbrains.compose.resources.painterResource
|
|||||||
import rust.nostr.sdk.Keys
|
import rust.nostr.sdk.Keys
|
||||||
import rust.nostr.sdk.NostrConnectUri
|
import rust.nostr.sdk.NostrConnectUri
|
||||||
import rust.nostr.sdk.PublicKey
|
import rust.nostr.sdk.PublicKey
|
||||||
import su.reya.coop.LocalNavController
|
import su.reya.coop.LocalNavigator
|
||||||
import su.reya.coop.LocalNostrViewModel
|
import su.reya.coop.LocalNostrViewModel
|
||||||
|
import su.reya.coop.LocalScanResult
|
||||||
import su.reya.coop.LocalSnackbarHostState
|
import su.reya.coop.LocalSnackbarHostState
|
||||||
import su.reya.coop.Screen
|
import su.reya.coop.Screen
|
||||||
import su.reya.coop.shared.Avatar
|
import su.reya.coop.shared.Avatar
|
||||||
@@ -69,12 +70,11 @@ import su.reya.coop.short
|
|||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ImportScreen(
|
fun ImportScreen(
|
||||||
isLoading: Boolean,
|
|
||||||
onBack: () -> Unit,
|
|
||||||
onSave: (secret: String) -> Unit
|
onSave: (secret: String) -> Unit
|
||||||
) {
|
) {
|
||||||
val snackbarHostState = LocalSnackbarHostState.current
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
val navController = LocalNavController.current
|
val navigator = LocalNavigator.current
|
||||||
|
val qrScanResult = LocalScanResult.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val viewModel = LocalNostrViewModel.current
|
val viewModel = LocalNostrViewModel.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -89,19 +89,14 @@ fun ImportScreen(
|
|||||||
}
|
}
|
||||||
}.collectAsState(null)
|
}.collectAsState(null)
|
||||||
|
|
||||||
|
|
||||||
val profile = metadata?.asRecord()
|
val profile = metadata?.asRecord()
|
||||||
val displayName = profile?.displayName ?: profile?.name ?: pubkey?.short() ?: "Unknown"
|
val displayName = profile?.displayName ?: profile?.name ?: pubkey?.short() ?: "Unknown"
|
||||||
val picture = profile?.picture
|
val picture = profile?.picture
|
||||||
|
|
||||||
val savedStateHandle = navController.currentBackStackEntry?.savedStateHandle
|
val isLoading by viewModel.isCreating.collectAsState()
|
||||||
val qrResult by savedStateHandle
|
|
||||||
?.getStateFlow<String?>("qr_result", null)
|
|
||||||
?.collectAsState()
|
|
||||||
?: remember { mutableStateOf(null) }
|
|
||||||
|
|
||||||
LaunchedEffect(qrResult) {
|
LaunchedEffect(qrScanResult.content) {
|
||||||
qrResult?.let { result ->
|
qrScanResult.content?.let { result ->
|
||||||
runCatching {
|
runCatching {
|
||||||
if (result.startsWith("nsec")) {
|
if (result.startsWith("nsec")) {
|
||||||
Keys.parse(result)
|
Keys.parse(result)
|
||||||
@@ -113,8 +108,9 @@ fun ImportScreen(
|
|||||||
}
|
}
|
||||||
.onSuccess { it -> secret = result }
|
.onSuccess { it -> secret = result }
|
||||||
.onFailure { e -> println("Failed to parse QR: ${e.message}") }
|
.onFailure { e -> println("Failed to parse QR: ${e.message}") }
|
||||||
|
|
||||||
// Clear the nav state
|
// Clear the nav state
|
||||||
navController.currentBackStackEntry?.savedStateHandle?.remove<String>("qr_result")
|
qrScanResult.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +129,7 @@ fun ImportScreen(
|
|||||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
),
|
),
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = { navigator.goBack() }) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(Res.drawable.ic_arrow_back),
|
painter = painterResource(Res.drawable.ic_arrow_back),
|
||||||
contentDescription = "Back"
|
contentDescription = "Back"
|
||||||
@@ -141,7 +137,7 @@ fun ImportScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = { navController.navigate(Screen.Scan) }) {
|
IconButton(onClick = { navigator.navigate(Screen.Scan) }) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(Res.drawable.ic_scanner),
|
painter = painterResource(Res.drawable.ic_scanner),
|
||||||
contentDescription = "Scanner"
|
contentDescription = "Scanner"
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ import coop.composeapp.generated.resources.Res
|
|||||||
import coop.composeapp.generated.resources.ic_arrow_back
|
import coop.composeapp.generated.resources.ic_arrow_back
|
||||||
import io.github.alexzhirkevich.qrose.rememberQrCodePainter
|
import io.github.alexzhirkevich.qrose.rememberQrCodePainter
|
||||||
import org.jetbrains.compose.resources.painterResource
|
import org.jetbrains.compose.resources.painterResource
|
||||||
|
import su.reya.coop.LocalNavigator
|
||||||
import su.reya.coop.LocalNostrViewModel
|
import su.reya.coop.LocalNostrViewModel
|
||||||
import su.reya.coop.LocalSnackbarHostState
|
import su.reya.coop.LocalSnackbarHostState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MyQrScreen(
|
fun MyQrScreen() {
|
||||||
onBack: () -> Unit
|
val navigator = LocalNavigator.current
|
||||||
) {
|
|
||||||
val snackbarHostState = LocalSnackbarHostState.current
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
val viewModel = LocalNostrViewModel.current
|
val viewModel = LocalNostrViewModel.current
|
||||||
val currentUser = viewModel.currentUser() ?: return
|
val currentUser = viewModel.currentUser() ?: return
|
||||||
@@ -41,7 +41,7 @@ fun MyQrScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = { navigator.goBack() }) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(Res.drawable.ic_arrow_back),
|
painter = painterResource(Res.drawable.ic_arrow_back),
|
||||||
contentDescription = "Back"
|
contentDescription = "Back"
|
||||||
|
|||||||
@@ -54,8 +54,9 @@ import coop.composeapp.generated.resources.ic_scanner
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import org.jetbrains.compose.resources.painterResource
|
import org.jetbrains.compose.resources.painterResource
|
||||||
import rust.nostr.sdk.PublicKey
|
import rust.nostr.sdk.PublicKey
|
||||||
import su.reya.coop.LocalNavController
|
import su.reya.coop.LocalNavigator
|
||||||
import su.reya.coop.LocalNostrViewModel
|
import su.reya.coop.LocalNostrViewModel
|
||||||
|
import su.reya.coop.LocalScanResult
|
||||||
import su.reya.coop.LocalSnackbarHostState
|
import su.reya.coop.LocalSnackbarHostState
|
||||||
import su.reya.coop.Screen
|
import su.reya.coop.Screen
|
||||||
import su.reya.coop.shared.Avatar
|
import su.reya.coop.shared.Avatar
|
||||||
@@ -63,11 +64,10 @@ import su.reya.coop.short
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun NewChatScreen(
|
fun NewChatScreen() {
|
||||||
onBack: () -> Unit,
|
|
||||||
) {
|
|
||||||
val snackbarHostState = LocalSnackbarHostState.current
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
val navController = LocalNavController.current
|
val navigator = LocalNavigator.current
|
||||||
|
val qrScanResult = LocalScanResult.current
|
||||||
val viewModel = LocalNostrViewModel.current
|
val viewModel = LocalNostrViewModel.current
|
||||||
|
|
||||||
val contactList by viewModel.contactList.collectAsState(initial = emptySet())
|
val contactList by viewModel.contactList.collectAsState(initial = emptySet())
|
||||||
@@ -76,12 +76,6 @@ fun NewChatScreen(
|
|||||||
val selectedReceivers = remember { mutableStateListOf<PublicKey>() }
|
val selectedReceivers = remember { mutableStateListOf<PublicKey>() }
|
||||||
var query by remember { mutableStateOf("") }
|
var query by remember { mutableStateOf("") }
|
||||||
|
|
||||||
val savedStateHandle = navController.currentBackStackEntry?.savedStateHandle
|
|
||||||
val qrResult by savedStateHandle
|
|
||||||
?.getStateFlow<String?>("qr_result", null)
|
|
||||||
?.collectAsState()
|
|
||||||
?: remember { mutableStateOf(null) }
|
|
||||||
|
|
||||||
LaunchedEffect(query) {
|
LaunchedEffect(query) {
|
||||||
if (query.length >= 3) {
|
if (query.length >= 3) {
|
||||||
delay(500) // 500ms debounce
|
delay(500) // 500ms debounce
|
||||||
@@ -111,13 +105,19 @@ fun NewChatScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(qrResult) {
|
LaunchedEffect(qrScanResult.content) {
|
||||||
qrResult?.let { result ->
|
qrScanResult.content?.let { result ->
|
||||||
|
// Verify the content
|
||||||
runCatching { PublicKey.parse(result) }
|
runCatching { PublicKey.parse(result) }
|
||||||
.onSuccess { pubkey -> selectedReceivers.add(pubkey) }
|
.onSuccess { pubkey ->
|
||||||
.onFailure { e -> println("Failed to parse QR: ${e.message}") }
|
selectedReceivers.add(pubkey)
|
||||||
|
}
|
||||||
|
.onFailure { e ->
|
||||||
|
println("Failed to parse QR: ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
// Clear the nav state
|
// Clear the nav state
|
||||||
navController.currentBackStackEntry?.savedStateHandle?.remove<String>("qr_result")
|
qrScanResult.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ fun NewChatScreen(
|
|||||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
),
|
),
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = { navigator.goBack() }) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(Res.drawable.ic_arrow_back),
|
painter = painterResource(Res.drawable.ic_arrow_back),
|
||||||
contentDescription = "Back"
|
contentDescription = "Back"
|
||||||
@@ -144,7 +144,7 @@ fun NewChatScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = { navController.navigate(Screen.Scan) }) {
|
IconButton(onClick = { navigator.navigate(Screen.Scan) }) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(Res.drawable.ic_scanner),
|
painter = painterResource(Res.drawable.ic_scanner),
|
||||||
contentDescription = "Scanner"
|
contentDescription = "Scanner"
|
||||||
@@ -168,7 +168,7 @@ fun NewChatScreen(
|
|||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
val roomId = viewModel.createChatRoom(selectedReceivers.toList())
|
val roomId = viewModel.createChatRoom(selectedReceivers.toList())
|
||||||
navController.navigate(Screen.Chat(roomId))
|
navigator.navigate(Screen.Chat(roomId))
|
||||||
},
|
},
|
||||||
expanded = false,
|
expanded = false,
|
||||||
icon = {
|
icon = {
|
||||||
@@ -259,7 +259,7 @@ fun NewChatScreen(
|
|||||||
selectedReceivers = selectedReceivers,
|
selectedReceivers = selectedReceivers,
|
||||||
onContactClick = { pubkey ->
|
onContactClick = { pubkey ->
|
||||||
val roomId = viewModel.createChatRoom(listOf(pubkey))
|
val roomId = viewModel.createChatRoom(listOf(pubkey))
|
||||||
navController.navigate(Screen.Chat(roomId))
|
navigator.navigate(Screen.Chat(roomId))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.size(16.dp))
|
Spacer(modifier = Modifier.size(16.dp))
|
||||||
@@ -270,7 +270,7 @@ fun NewChatScreen(
|
|||||||
selectedReceivers = selectedReceivers,
|
selectedReceivers = selectedReceivers,
|
||||||
onContactClick = { pubkey ->
|
onContactClick = { pubkey ->
|
||||||
val roomId = viewModel.createChatRoom(listOf(pubkey))
|
val roomId = viewModel.createChatRoom(listOf(pubkey))
|
||||||
navController.navigate(Screen.Chat(roomId))
|
navigator.navigate(Screen.Chat(roomId))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.size(16.dp))
|
Spacer(modifier = Modifier.size(16.dp))
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import androidx.compose.material3.TopAppBar
|
|||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.toShape
|
import androidx.compose.material3.toShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
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
|
||||||
@@ -54,22 +55,27 @@ 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 org.jetbrains.compose.resources.painterResource
|
import org.jetbrains.compose.resources.painterResource
|
||||||
|
import su.reya.coop.LocalNavigator
|
||||||
|
import su.reya.coop.LocalNostrViewModel
|
||||||
import su.reya.coop.LocalSnackbarHostState
|
import su.reya.coop.LocalSnackbarHostState
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun NewIdentityScreen(
|
fun NewIdentityScreen(
|
||||||
isLoading: Boolean,
|
|
||||||
onBack: () -> Unit,
|
|
||||||
onSave: (name: String, bio: String?, picture: Uri?) -> Unit
|
onSave: (name: String, bio: String?, picture: Uri?) -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val snackbarHostState = LocalSnackbarHostState.current
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
|
val navigator = LocalNavigator.current
|
||||||
|
val viewModel = LocalNostrViewModel.current
|
||||||
|
|
||||||
var name by remember { mutableStateOf("") }
|
var name by remember { mutableStateOf("") }
|
||||||
var bio by remember { mutableStateOf("") }
|
var bio by remember { mutableStateOf("") }
|
||||||
var picture by remember { mutableStateOf<Uri?>(null) }
|
var picture by remember { mutableStateOf<Uri?>(null) }
|
||||||
|
|
||||||
|
val isLoading by viewModel.isCreating.collectAsState()
|
||||||
|
|
||||||
val launcher = rememberLauncherForActivityResult(
|
val launcher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.GetContent()
|
contract = ActivityResultContracts.GetContent()
|
||||||
) { uri: Uri? ->
|
) { uri: Uri? ->
|
||||||
@@ -88,7 +94,7 @@ fun NewIdentityScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = { navigator.goBack() }) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(Res.drawable.ic_arrow_back),
|
painter = painterResource(Res.drawable.ic_arrow_back),
|
||||||
contentDescription = "Back"
|
contentDescription = "Back"
|
||||||
|
|||||||
@@ -37,13 +37,17 @@ import androidx.compose.ui.unit.dp
|
|||||||
import coop.composeapp.generated.resources.Res
|
import coop.composeapp.generated.resources.Res
|
||||||
import coop.composeapp.generated.resources.coop
|
import coop.composeapp.generated.resources.coop
|
||||||
import org.jetbrains.compose.resources.painterResource
|
import org.jetbrains.compose.resources.painterResource
|
||||||
|
import su.reya.coop.LocalNavigator
|
||||||
import su.reya.coop.LocalSnackbarHostState
|
import su.reya.coop.LocalSnackbarHostState
|
||||||
|
import su.reya.coop.Screen
|
||||||
import su.reya.coop.shared.getExpressiveFontFamily
|
import su.reya.coop.shared.getExpressiveFontFamily
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun OnboardingScreen(onOpenImport: () -> Unit, onOpenNew: () -> Unit) {
|
fun OnboardingScreen() {
|
||||||
val snackbarHostState = LocalSnackbarHostState.current
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
|
val navigator = LocalNavigator.current
|
||||||
|
|
||||||
val logoPainter = painterResource(Res.drawable.coop)
|
val logoPainter = painterResource(Res.drawable.coop)
|
||||||
val expressiveFont = getExpressiveFontFamily()
|
val expressiveFont = getExpressiveFontFamily()
|
||||||
val annotatedText = buildAnnotatedString {
|
val annotatedText = buildAnnotatedString {
|
||||||
@@ -127,7 +131,7 @@ fun OnboardingScreen(onOpenImport: () -> Unit, onOpenNew: () -> Unit) {
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.size(24.dp))
|
Spacer(modifier = Modifier.size(24.dp))
|
||||||
Button(
|
Button(
|
||||||
onClick = onOpenNew,
|
onClick = { navigator.navigate(Screen.NewIdentity) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.size(ButtonDefaults.MediumContainerHeight),
|
.size(ButtonDefaults.MediumContainerHeight),
|
||||||
@@ -139,7 +143,7 @@ fun OnboardingScreen(onOpenImport: () -> Unit, onOpenNew: () -> Unit) {
|
|||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.size(8.dp))
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = onOpenImport,
|
onClick = { navigator.navigate(Screen.Import) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(ButtonDefaults.MediumContainerHeight),
|
.height(ButtonDefaults.MediumContainerHeight),
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ import coop.composeapp.generated.resources.ic_share
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jetbrains.compose.resources.painterResource
|
import org.jetbrains.compose.resources.painterResource
|
||||||
import rust.nostr.sdk.PublicKey
|
import rust.nostr.sdk.PublicKey
|
||||||
import su.reya.coop.LocalNavController
|
import su.reya.coop.LocalNavigator
|
||||||
import su.reya.coop.LocalNostrViewModel
|
import su.reya.coop.LocalNostrViewModel
|
||||||
import su.reya.coop.LocalSnackbarHostState
|
import su.reya.coop.LocalSnackbarHostState
|
||||||
import su.reya.coop.Screen
|
import su.reya.coop.Screen
|
||||||
@@ -54,15 +54,12 @@ import su.reya.coop.short
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ProfileScreen(
|
fun ProfileScreen(pubkey: String) {
|
||||||
onBack: () -> Unit,
|
|
||||||
pubkey: String
|
|
||||||
) {
|
|
||||||
val pubkey = runCatching { PublicKey.parse(pubkey) }.getOrNull() ?: return
|
val pubkey = runCatching { PublicKey.parse(pubkey) }.getOrNull() ?: return
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val snackbarHostState = LocalSnackbarHostState.current
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
val navController = LocalNavController.current
|
val navigator = LocalNavigator.current
|
||||||
val viewModel = LocalNostrViewModel.current
|
val viewModel = LocalNostrViewModel.current
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -88,7 +85,7 @@ fun ProfileScreen(
|
|||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { },
|
title = { },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = { navigator.goBack() }) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(Res.drawable.ic_arrow_back),
|
painter = painterResource(Res.drawable.ic_arrow_back),
|
||||||
contentDescription = "Back"
|
contentDescription = "Back"
|
||||||
@@ -162,7 +159,7 @@ fun ProfileScreen(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
val roomId = viewModel.createChatRoom(listOf(pubkey))
|
val roomId = viewModel.createChatRoom(listOf(pubkey))
|
||||||
navController.navigate(Screen.Chat(roomId))
|
navigator.navigate(Screen.Chat(roomId))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.message?.let { snackbarHostState.showSnackbar(it) }
|
e.message?.let { snackbarHostState.showSnackbar(it) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,14 +34,14 @@ import coop.composeapp.generated.resources.ic_arrow_back
|
|||||||
import org.jetbrains.compose.resources.painterResource
|
import org.jetbrains.compose.resources.painterResource
|
||||||
import rust.nostr.sdk.RelayMetadata
|
import rust.nostr.sdk.RelayMetadata
|
||||||
import rust.nostr.sdk.RelayUrl
|
import rust.nostr.sdk.RelayUrl
|
||||||
|
import su.reya.coop.LocalNavigator
|
||||||
import su.reya.coop.LocalNostrViewModel
|
import su.reya.coop.LocalNostrViewModel
|
||||||
import su.reya.coop.LocalSnackbarHostState
|
import su.reya.coop.LocalSnackbarHostState
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RelayScreen(
|
fun RelayScreen() {
|
||||||
onBack: () -> Unit
|
val navigator = LocalNavigator.current
|
||||||
) {
|
|
||||||
val snackbarHostState = LocalSnackbarHostState.current
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
val viewModel = LocalNostrViewModel.current
|
val viewModel = LocalNostrViewModel.current
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ fun RelayScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = { navigator.goBack() }) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(Res.drawable.ic_arrow_back),
|
painter = painterResource(Res.drawable.ic_arrow_back),
|
||||||
contentDescription = "Back"
|
contentDescription = "Back"
|
||||||
|
|||||||
@@ -30,21 +30,16 @@ import org.jetbrains.compose.resources.painterResource
|
|||||||
import org.publicvalue.multiplatform.qrcode.CameraPosition
|
import org.publicvalue.multiplatform.qrcode.CameraPosition
|
||||||
import org.publicvalue.multiplatform.qrcode.CodeType
|
import org.publicvalue.multiplatform.qrcode.CodeType
|
||||||
import org.publicvalue.multiplatform.qrcode.ScannerWithPermissions
|
import org.publicvalue.multiplatform.qrcode.ScannerWithPermissions
|
||||||
import su.reya.coop.LocalNavController
|
import su.reya.coop.LocalNavigator
|
||||||
|
import su.reya.coop.LocalScanResult
|
||||||
import su.reya.coop.LocalSnackbarHostState
|
import su.reya.coop.LocalSnackbarHostState
|
||||||
|
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
@Composable
|
@Composable
|
||||||
fun ScanScreen(
|
fun ScanScreen() {
|
||||||
onBack: () -> Unit
|
val navigator = LocalNavigator.current
|
||||||
) {
|
|
||||||
val navController = LocalNavController.current
|
|
||||||
val snackbarHostState = LocalSnackbarHostState.current
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
|
val qrScanResult = LocalScanResult.current
|
||||||
val onResult: (String) -> Unit = { result ->
|
|
||||||
navController.previousBackStackEntry?.savedStateHandle?.set("qr_result", result)
|
|
||||||
navController.popBackStack()
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
@@ -57,7 +52,7 @@ fun ScanScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = { navigator.goBack() }) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(Res.drawable.ic_arrow_back),
|
painter = painterResource(Res.drawable.ic_arrow_back),
|
||||||
contentDescription = "Back"
|
contentDescription = "Back"
|
||||||
@@ -76,7 +71,8 @@ fun ScanScreen(
|
|||||||
ScannerWithPermissions(
|
ScannerWithPermissions(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
onScanned = {
|
onScanned = {
|
||||||
onResult(it)
|
qrScanResult.content = it
|
||||||
|
navigator.goBack()
|
||||||
true
|
true
|
||||||
},
|
},
|
||||||
types = listOf(CodeType.QR),
|
types = listOf(CodeType.QR),
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ androidx-appcompat = "1.7.1"
|
|||||||
androidx-core = "1.18.0"
|
androidx-core = "1.18.0"
|
||||||
androidx-espresso = "3.7.0"
|
androidx-espresso = "3.7.0"
|
||||||
androidx-lifecycle = "2.10.0"
|
androidx-lifecycle = "2.10.0"
|
||||||
androidx-navigation = "2.9.8"
|
|
||||||
androidx-testExt = "1.3.0"
|
androidx-testExt = "1.3.0"
|
||||||
androidx-splashscreen = "1.2.0"
|
androidx-splashscreen = "1.2.0"
|
||||||
composeMultiplatform = "1.11.0"
|
composeMultiplatform = "1.11.0"
|
||||||
@@ -17,6 +16,7 @@ junit = "4.13.2"
|
|||||||
kotlin = "2.3.21"
|
kotlin = "2.3.21"
|
||||||
kotlinx-serialization = "1.11.0"
|
kotlinx-serialization = "1.11.0"
|
||||||
material3 = "1.11.0-alpha07"
|
material3 = "1.11.0-alpha07"
|
||||||
|
multiplatform-nav3-ui = "1.1.1"
|
||||||
ktor = "3.5.0"
|
ktor = "3.5.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
@@ -31,7 +31,6 @@ androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", vers
|
|||||||
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
|
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
|
||||||
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidx-splashscreen" }
|
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidx-splashscreen" }
|
||||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
|
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
|
||||||
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
|
|
||||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
|
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
|
||||||
compose-uiTooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "composeMultiplatform" }
|
compose-uiTooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "composeMultiplatform" }
|
||||||
androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
|
androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
|
||||||
@@ -49,6 +48,8 @@ ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "kto
|
|||||||
ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" }
|
ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" }
|
||||||
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
|
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
|
||||||
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
|
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
|
||||||
|
jetbrains-navigation3-ui = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "multiplatform-nav3-ui" }
|
||||||
|
jetbrains-lifecycle-viewmodelNavigation3 = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "androidx-lifecycle" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import rust.nostr.sdk.AsyncNostrSigner
|
||||||
import rust.nostr.sdk.EventBuilder
|
import rust.nostr.sdk.EventBuilder
|
||||||
import rust.nostr.sdk.EventId
|
import rust.nostr.sdk.EventId
|
||||||
import rust.nostr.sdk.Keys
|
import rust.nostr.sdk.Keys
|
||||||
@@ -33,7 +34,7 @@ import rust.nostr.sdk.UnsignedEvent
|
|||||||
import su.reya.coop.blossom.BlossomClient
|
import su.reya.coop.blossom.BlossomClient
|
||||||
import su.reya.coop.storage.SecretStorage
|
import su.reya.coop.storage.SecretStorage
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
class NostrViewModel(
|
class NostrViewModel(
|
||||||
private val nostr: Nostr,
|
private val nostr: Nostr,
|
||||||
@@ -201,34 +202,26 @@ class NostrViewModel(
|
|||||||
|
|
||||||
private fun login() {
|
private fun login() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
// Get user's signer secret
|
try {
|
||||||
val secret = secretStore.get("user_signer")
|
val secret = secretStore.get("user_signer")
|
||||||
|
|
||||||
// If no secret is found, show onboarding screen
|
if (secret == null) {
|
||||||
if (secret == null) {
|
_signerRequired.value = true
|
||||||
_signerRequired.value = true
|
return@launch
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the empty secret state
|
|
||||||
_signerRequired.value = false
|
|
||||||
|
|
||||||
// Handle different signer types
|
|
||||||
if (secret.startsWith("nsec1")) {
|
|
||||||
val keys = Keys.parse(secret)
|
|
||||||
nostr.setSigner(keys)
|
|
||||||
} else if (secret.startsWith("bunker://")) {
|
|
||||||
try {
|
|
||||||
val appKeys = getOrInitAppKeys()
|
|
||||||
val bunker = NostrConnectUri.parse(secret)
|
|
||||||
val timeout = Duration.parse("50s") // 50 seconds timeout
|
|
||||||
val remote = NostrConnect(uri = bunker, appKeys, timeout, opts = null)
|
|
||||||
nostr.setSigner(remote)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showError("Error: ${e.message}")
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
throw IllegalArgumentException("Invalid secret format: $secret")
|
runCatching {
|
||||||
|
val signer = createSigner(secret)
|
||||||
|
nostr.setSigner(signer)
|
||||||
|
}.onSuccess {
|
||||||
|
_signerRequired.value = false
|
||||||
|
}.onFailure { e ->
|
||||||
|
showError("Login failed: ${e.message}")
|
||||||
|
_signerRequired.value = true
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Login failed: ${e.message}")
|
||||||
|
_signerRequired.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -317,6 +310,20 @@ class NostrViewModel(
|
|||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun createSigner(secret: String): AsyncNostrSigner {
|
||||||
|
return when {
|
||||||
|
secret.startsWith("nsec1") -> Keys.parse(secret)
|
||||||
|
secret.startsWith("bunker://") -> {
|
||||||
|
val appKeys = getOrInitAppKeys()
|
||||||
|
val bunker = NostrConnectUri.parse(secret)
|
||||||
|
val timeout = 50.seconds // or Duration.parse("50s")
|
||||||
|
NostrConnect(uri = bunker, appKeys, timeout, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> throw IllegalArgumentException("Invalid secret format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun createIdentity(
|
fun createIdentity(
|
||||||
name: String,
|
name: String,
|
||||||
bio: String?,
|
bio: String?,
|
||||||
@@ -371,46 +378,25 @@ class NostrViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun verifyIdentity(secret: String): PublicKey? {
|
suspend fun verifyIdentity(secret: String): PublicKey? {
|
||||||
if (secret.startsWith("nsec1")) {
|
return runCatching {
|
||||||
val keys = Keys.parse(secret)
|
val signer = createSigner(secret)
|
||||||
return keys.publicKey()
|
if (secret.startsWith("bunker://")) {
|
||||||
} else if (secret.startsWith("bunker://")) {
|
showError("Please approve the connection.")
|
||||||
val appKeys = getOrInitAppKeys()
|
}
|
||||||
val bunker = NostrConnectUri.parse(secret)
|
signer.getPublicKeyAsync()
|
||||||
val timeout = Duration.parse("50s") // 50 seconds timeout
|
}.getOrNull()
|
||||||
val remote = NostrConnect(uri = bunker, appKeys, timeout, null)
|
|
||||||
|
|
||||||
// Show toast to ask user to approve the connection
|
|
||||||
showError("Please approve the connection.")
|
|
||||||
|
|
||||||
return remote.getPublicKeyAsync()
|
|
||||||
} else {
|
|
||||||
throw IllegalArgumentException("Invalid secret: $secret")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun importIdentity(secret: String) {
|
fun importIdentity(secret: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (secret.startsWith("nsec1")) {
|
runCatching {
|
||||||
val keys = Keys.parse(secret)
|
val signer = createSigner(secret)
|
||||||
nostr.setSigner(keys)
|
nostr.setSigner(signer)
|
||||||
secretStore.set("user_signer", secret)
|
secretStore.set("user_signer", secret)
|
||||||
// Set an empty secret state
|
}.onSuccess {
|
||||||
_signerRequired.value = false
|
_signerRequired.value = false
|
||||||
} else if (secret.startsWith("bunker://")) {
|
}.onFailure { e ->
|
||||||
try {
|
showError(e.message ?: "Invalid Secret or Bunker URI")
|
||||||
val appKeys = getOrInitAppKeys()
|
|
||||||
val bunker = NostrConnectUri.parse(secret)
|
|
||||||
val timeout = Duration.parse("50s") // 50 seconds timeout
|
|
||||||
val remote = NostrConnect(uri = bunker, appKeys, timeout, null)
|
|
||||||
nostr.setSigner(remote)
|
|
||||||
secretStore.set("user_signer", secret)
|
|
||||||
_signerRequired.value = false
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showError("Error: ${e.message}")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showError("Please enter a valid Secret or Bunker URI.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user