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.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import kotlinx.coroutines.launch
import su.reya.coop.coop.storage.SecretStore
import su.reya.coop.screens.ChatScreen
import su.reya.coop.screens.HomeScreen
import su.reya.coop.screens.ImportScreen
@@ -72,7 +70,10 @@ val LocalNavController = staticCompositionLocalOf<NavController> {
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Composable
fun App(openRoomId: Long? = null) {
fun App(
viewModel: NostrViewModel,
openRoomId: Long? = null
) {
val context = LocalContext.current
val navController = rememberNavController()
val scope = rememberCoroutineScope()
@@ -81,10 +82,6 @@ fun App(openRoomId: Long? = null) {
// Snackbar
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
val colorScheme = when {
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(
colorScheme = colorScheme,
typography = Typography(),
@@ -123,15 +114,20 @@ fun App(openRoomId: Long? = null) {
LaunchedEffect(emptySecret) {
// Navigate to the home screen if the secret is already set
if (emptySecret == false) {
if (emptySecret == false && openRoomId == null) {
navController.navigate(Screen.Home) {
popUpTo(Screen.Onboarding) { inclusive = true }
}
}
}
// Show loading screen while initializing
if (emptySecret == null) return@CompositionLocalProvider
LaunchedEffect(openRoomId) {
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
if (isRelayListEmpty) {

View File

@@ -7,26 +7,40 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import su.reya.coop.coop.storage.SecretStore
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
val splashScreen = installSplashScreen()
enableEdgeToEdge()
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) {
startForegroundService(intent)
startForegroundService(serviceIntent)
} else {
startService(intent)
startService(serviceIntent)
}
val roomId = intent.getLongExtra("room_id", -1L)
val secretStore = SecretStore(this)
val viewModel = NostrViewModel(NostrManager.instance, secretStore)
splashScreen.setKeepOnScreenCondition {
viewModel.emptySecret.value == null
}
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.statement.HttpResponse
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import rust.nostr.sdk.AckPolicy
import rust.nostr.sdk.Alphabet
import rust.nostr.sdk.AsyncNostrSigner
@@ -170,8 +170,6 @@ class Nostr {
suspend fun setSigner(new: AsyncNostrSigner) {
try {
signer.switch(new)
// Fetch metadata for current user
getUserMetadata()
} catch (e: Exception) {
throw IllegalStateException("Failed to set signer: ${e.message}", e)
}
@@ -244,10 +242,10 @@ class Nostr {
onContactListUpdate: (List<PublicKey>) -> Unit,
onNewMessage: (UnsignedEvent) -> Unit,
onSubscriptionClose: () -> Unit,
) = coroutineScope {
) = supervisorScope {
val now = Timestamp.now()
val processedEvent = mutableSetOf<EventId>()
val notifications = client?.notifications() ?: return@coroutineScope
val notifications = client?.notifications() ?: return@supervisorScope
var eoseTrackerJob: Job? = null

View File

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