feat: implement basic notification #6

Merged
reya merged 6 commits from feat/notifications into master 2026-05-29 06:56:48 +00:00
4 changed files with 40 additions and 32 deletions
Showing only changes of commit 08058c0297 - Show all commits

View File

@@ -39,14 +39,12 @@ 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.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute import androidx.navigation.toRoute
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import su.reya.coop.coop.storage.SecretStore
import su.reya.coop.screens.ChatScreen import su.reya.coop.screens.ChatScreen
import su.reya.coop.screens.HomeScreen import su.reya.coop.screens.HomeScreen
import su.reya.coop.screens.ImportScreen import su.reya.coop.screens.ImportScreen
@@ -72,7 +70,10 @@ val LocalNavController = staticCompositionLocalOf<NavController> {
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Composable @Composable
fun App(openRoomId: Long? = null) { fun App(
viewModel: NostrViewModel,
openRoomId: Long? = null
) {
val context = LocalContext.current val context = LocalContext.current
val navController = rememberNavController() val navController = rememberNavController()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -81,10 +82,6 @@ fun App(openRoomId: Long? = null) {
// Snackbar // Snackbar
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
// Initialize Nostr View Model and Secret Store
val secretStore = remember { SecretStore(context) }
val viewModel: NostrViewModel = viewModel { NostrViewModel(NostrManager.instance, secretStore) }
// Enabled the dynamic color scheme // Enabled the dynamic color scheme
val colorScheme = when { val colorScheme = when {
android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S -> { android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S -> {
@@ -101,12 +98,6 @@ fun App(openRoomId: Long? = null) {
} }
} }
LaunchedEffect(openRoomId) {
if (openRoomId != null) {
navController.navigate(Screen.Chat(openRoomId))
}
}
MaterialExpressiveTheme( MaterialExpressiveTheme(
colorScheme = colorScheme, colorScheme = colorScheme,
typography = Typography(), typography = Typography(),
@@ -123,15 +114,20 @@ fun App(openRoomId: Long? = null) {
LaunchedEffect(emptySecret) { LaunchedEffect(emptySecret) {
// Navigate to the home screen if the secret is already set // Navigate to the home screen if the secret is already set
if (emptySecret == false) { if (emptySecret == false && openRoomId == null) {
navController.navigate(Screen.Home) { navController.navigate(Screen.Home) {
popUpTo(Screen.Onboarding) { inclusive = true } popUpTo(Screen.Onboarding) { inclusive = true }
} }
} }
} }
// Show loading screen while initializing LaunchedEffect(openRoomId) {
if (emptySecret == null) return@CompositionLocalProvider if (openRoomId != null) {
navController.navigate(Screen.Chat(openRoomId)) {
popUpTo(Screen.Home) { saveState = true }
}
}
}
// 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) {

View File

@@ -7,26 +7,40 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import su.reya.coop.coop.storage.SecretStore
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen() val splashScreen = installSplashScreen()
enableEdgeToEdge() enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val intent = Intent(this, NostrForegroundService::class.java) val serviceIntent = Intent(this, NostrForegroundService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent) startForegroundService(serviceIntent)
} else { } else {
startService(intent) startService(serviceIntent)
} }
val roomId = intent.getLongExtra("room_id", -1L) val roomId = intent.getLongExtra("room_id", -1L)
val secretStore = SecretStore(this)
val viewModel = NostrViewModel(NostrManager.instance, secretStore)
splashScreen.setKeepOnScreenCondition {
viewModel.emptySecret.value == null
}
setContent { setContent {
App(openRoomId = if (roomId != -1L) roomId else null) App(
viewModel = viewModel,
openRoomId = if (roomId != -1L) roomId else null
)
} }
} }
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
}
} }

View File

@@ -6,13 +6,13 @@ import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import rust.nostr.sdk.AckPolicy import rust.nostr.sdk.AckPolicy
import rust.nostr.sdk.Alphabet import rust.nostr.sdk.Alphabet
import rust.nostr.sdk.AsyncNostrSigner import rust.nostr.sdk.AsyncNostrSigner
@@ -170,8 +170,6 @@ class Nostr {
suspend fun setSigner(new: AsyncNostrSigner) { suspend fun setSigner(new: AsyncNostrSigner) {
try { try {
signer.switch(new) signer.switch(new)
// Fetch metadata for current user
getUserMetadata()
} catch (e: Exception) { } catch (e: Exception) {
throw IllegalStateException("Failed to set signer: ${e.message}", e) throw IllegalStateException("Failed to set signer: ${e.message}", e)
} }
@@ -244,10 +242,10 @@ class Nostr {
onContactListUpdate: (List<PublicKey>) -> Unit, onContactListUpdate: (List<PublicKey>) -> Unit,
onNewMessage: (UnsignedEvent) -> Unit, onNewMessage: (UnsignedEvent) -> Unit,
onSubscriptionClose: () -> Unit, onSubscriptionClose: () -> Unit,
) = coroutineScope { ) = supervisorScope {
val now = Timestamp.now() val now = Timestamp.now()
val processedEvent = mutableSetOf<EventId>() val processedEvent = mutableSetOf<EventId>()
val notifications = client?.notifications() ?: return@coroutineScope val notifications = client?.notifications() ?: return@supervisorScope
var eoseTrackerJob: Job? = null var eoseTrackerJob: Job? = null

View File

@@ -82,7 +82,7 @@ class NostrViewModel(
// Observe new events from the Nostr client // Observe new events from the Nostr client
runObserver() runObserver()
// Wait and merge metadata requests into a single batch // Wait and merge metadata requests into a single batch
runMetadataBatching() runMetadataBatching()
} }
@@ -201,9 +201,6 @@ class NostrViewModel(
private fun login() { private fun login() {
viewModelScope.launch { viewModelScope.launch {
// Wait until the client is ready
nostr.waitUntilInitialized()
// Get user's signer secret // Get user's signer secret
val secret = secretStore.get("user_signer") val secret = secretStore.get("user_signer")
@@ -249,6 +246,9 @@ class NostrViewModel(
_isPartialProcessedGiftWrap.value = true _isPartialProcessedGiftWrap.value = true
} }
// Get all metadata for the current user
nostr.getUserMetadata()
// Small delay to ensure all relays are connected // Small delay to ensure all relays are connected
delay(3000) delay(3000)
@@ -261,7 +261,7 @@ class NostrViewModel(
break break
} }
delay(1000) delay(500)
} }
} }
} }