diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt b/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt index 6b36226e..2af13b5e 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt @@ -118,6 +118,12 @@ object LocalPreferences { }.apply() } + suspend fun isNotificationTypeConfigured(): Boolean { + val context = NostrSigner.getInstance() + val prefs = encryptedPreferences(context) + return prefs.contains(PrefKeys.NOTIFICATION_TYPE) + } + suspend fun loadSettingsFromEncryptedStorage(): AmberSettings { val context = NostrSigner.getInstance() @@ -130,7 +136,7 @@ object LocalPreferences { } ?: listOf(RelaySetupInfo("wss://relay.nsec.app", read = true, write = true, feedTypes = COMMON_FEED_TYPES)), lastBiometricsTime = getLong(PrefKeys.LAST_BIOMETRICS_TIME, 0), useAuth = getBoolean(PrefKeys.USE_AUTH, false), - notificationType = parseNotificationType(getInt(PrefKeys.NOTIFICATION_TYPE, 0)), + notificationType = parseNotificationType(getInt(PrefKeys.NOTIFICATION_TYPE, 1)), biometricsTimeType = parseBiometricsTimeType(getInt(PrefKeys.BIOMETRICS_TYPE, 0)), ) } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/NostrSigner.kt b/app/src/main/java/com/greenart7c3/nostrsigner/NostrSigner.kt index 79a1b96d..d5128517 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/NostrSigner.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/NostrSigner.kt @@ -19,10 +19,15 @@ import com.vitorpamplona.ammolite.relays.RelayPool import com.vitorpamplona.ammolite.relays.RelaySetupInfo import com.vitorpamplona.ammolite.service.HttpClientManager import java.util.concurrent.ConcurrentHashMap +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking class NostrSigner : Application() { + val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private var databases = ConcurrentHashMap() lateinit var settings: AmberSettings @@ -163,6 +168,11 @@ class NostrSigner : Application() { } } + override fun onTerminate() { + super.onTerminate() + applicationIOScope.cancel() + } + companion object { @Volatile private var instance: NostrSigner? = null diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberSettings.kt b/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberSettings.kt index 6f815ef9..158e56a8 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberSettings.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberSettings.kt @@ -11,6 +11,6 @@ data class AmberSettings( val defaultRelays: List = listOf(RelaySetupInfo("wss://relay.nsec.app", read = true, write = true, feedTypes = COMMON_FEED_TYPES)), val lastBiometricsTime: Long = 0, val useAuth: Boolean = false, - val notificationType: NotificationType = NotificationType.PUSH, + val notificationType: NotificationType = NotificationType.DIRECT, val biometricsTimeType: BiometricsTimeType = BiometricsTimeType.EVERY_TIME, ) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountScreen.kt index 88dc7180..97b96043 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountScreen.kt @@ -9,6 +9,8 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons @@ -18,6 +20,7 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -26,7 +29,6 @@ import androidx.compose.runtime.collectAsState 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 @@ -74,71 +76,94 @@ fun AccountScreen( MainLoginPage(accountStateViewModel) } is AccountState.LoggedIn -> { - var intentData by remember { - mutableStateOf(null) + var isNotificationsConfigured by remember { + mutableStateOf(false) } - LaunchedEffect(intent, intents) { - IntentUtils.getIntentData(context, intent, packageName, intent.getStringExtra("route"), state.account) { - intentData = it + LaunchedEffect(Unit) { + launch(Dispatchers.IO) { + isNotificationsConfigured = LocalPreferences.isNotificationTypeConfigured() } - val data = - intents.firstOrNull { - it.currentAccount.isNotBlank() - } + } - data?.bunkerRequest?.let { - if (it.currentAccount.isNotBlank()) { - if (LocalPreferences.currentAccount(context) != it.currentAccount) { - accountStateViewModel.switchUser(it.currentAccount, null) + if (isNotificationsConfigured) { + var intentData by remember { + mutableStateOf(null) + } + LaunchedEffect(intent, intents) { + IntentUtils.getIntentData(context, intent, packageName, intent.getStringExtra("route"), state.account) { + intentData = it + } + val data = + intents.firstOrNull { + it.currentAccount.isNotBlank() + } + + data?.bunkerRequest?.let { + if (it.currentAccount.isNotBlank()) { + if (LocalPreferences.currentAccount(context) != it.currentAccount) { + accountStateViewModel.switchUser(it.currentAccount, null) + } } } } - } - if (intentData != null) { - if (intents.none { item -> item.id == intentData!!.id }) { - val oldIntents = intents.toMutableList() - oldIntents.add(intentData!!) - flow.value = oldIntents - } - } - - val newIntents = - intents.ifEmpty { - if (intentData == null) { - listOf() - } else { - listOf(intentData) - } - }.mapNotNull { it } - val database = NostrSigner.getInstance().getDatabase(state.account.keyPair.pubKey.toNpub()) - val localRoute = mutableStateOf(newIntents.firstNotNullOfOrNull { it.route } ?: state.route) - val scope = rememberCoroutineScope() - - SideEffect { - scope.launch(Dispatchers.IO) { - PushNotificationUtils.accountState = accountStateViewModel - try { - NostrSigner.getInstance().applicationContext.startForegroundService( - Intent(NostrSigner.getInstance().applicationContext, ConnectivityService::class.java), - ) - } catch (e: Exception) { - Log.d("NostrSigner", "Failed to start ConnectivityService", e) - } - - @Suppress("KotlinConstantConditions") - if (NostrSigner.getInstance().settings.notificationType == NotificationType.DIRECT && BuildConfig.FLAVOR != "offline") { - NostrSigner.getInstance().checkForNewRelays() - NotificationDataSource.start() - delay(5000) + if (intentData != null) { + if (intents.none { item -> item.id == intentData!!.id }) { + val oldIntents = intents.toMutableList() + oldIntents.add(intentData!!) + flow.value = oldIntents } } + + val newIntents = + intents.ifEmpty { + if (intentData == null) { + listOf() + } else { + listOf(intentData) + } + }.mapNotNull { it } + val database = NostrSigner.getInstance().getDatabase(state.account.keyPair.pubKey.toNpub()) + val localRoute = mutableStateOf(newIntents.firstNotNullOfOrNull { it.route } ?: state.route) + + SideEffect { + NostrSigner.getInstance().applicationIOScope.launch(Dispatchers.IO) { + PushNotificationUtils.accountState = accountStateViewModel + try { + NostrSigner.getInstance().applicationContext.startForegroundService( + Intent(NostrSigner.getInstance().applicationContext, ConnectivityService::class.java), + ) + } catch (e: Exception) { + Log.d("NostrSigner", "Failed to start ConnectivityService", e) + } + + @Suppress("KotlinConstantConditions") + if (NostrSigner.getInstance().settings.notificationType == NotificationType.DIRECT && BuildConfig.FLAVOR != "offline") { + NostrSigner.getInstance().checkForNewRelays() + NotificationDataSource.start() + delay(5000) + } + } + } + + AmberListenerSingleton.accountStateViewModel = accountStateViewModel + + DisplayErrorMessages(accountStateViewModel) + MainScreen(state.account, accountStateViewModel, newIntents, packageName, appName, localRoute, database) + } else { + AmberListenerSingleton.accountStateViewModel = accountStateViewModel + DisplayErrorMessages(accountStateViewModel) + Scaffold { innerPadding -> + NotificationTypeScreen( + Modifier + .fillMaxSize() + .padding(innerPadding), + onDone = { + isNotificationsConfigured = true + }, + ) + } } - - AmberListenerSingleton.accountStateViewModel = accountStateViewModel - - DisplayErrorMessages(accountStateViewModel) - MainScreen(state.account, accountStateViewModel, newIntents, packageName, appName, localRoute, database) } } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/EditPermission.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/EditPermission.kt index b99fe768..6397d5e0 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/EditPermission.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/EditPermission.kt @@ -109,7 +109,7 @@ fun EditPermission( } LaunchedEffect(Unit) { - scope.launch(Dispatchers.IO) { + launch(Dispatchers.IO) { permissions.addAll(database.applicationDao().getAllByKey(selectedPackage).sortedBy { "${it.type}-${it.kind}" }) applicationData = database.applicationDao().getByKey(selectedPackage)!!.application checked = applicationData.useSecret diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt index 297f4066..5bac29bc 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt @@ -524,7 +524,9 @@ private suspend fun askNotificationPermission( onShouldShowRequestPermissionRationale: () -> Unit, ) { if (ContextCompat.checkSelfPermission(context, "android.permission.POST_NOTIFICATIONS") == PackageManager.PERMISSION_GRANTED) { - initNotifications(context) + initNotifications( + context = context, + ) return } @@ -572,7 +574,9 @@ fun MainScreen( ) { isGranted: Boolean -> if (isGranted) { scope.launch(Dispatchers.IO) { - initNotifications(context) + initNotifications( + context = context, + ) } } else { if (LocalPreferences.shouldShowRationale(context) == null) { @@ -587,11 +591,12 @@ fun MainScreen( LaunchedEffect(Unit) { launch(Dispatchers.IO) { askNotificationPermission( - context, - requestPermissionLauncher, - ) { - showDialog = true - } + context = context, + requestPermissionLauncher = requestPermissionLauncher, + onShouldShowRequestPermissionRationale = { + showDialog = true + }, + ) } } } @@ -821,7 +826,9 @@ fun MainScreen( Modifier .fillMaxSize() .padding(padding), - navController = navController, + onDone = { + navController.navigateUp() + }, ) } }, diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/NotificationTypeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/NotificationTypeScreen.kt index 98b5099c..0eee0801 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/NotificationTypeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/NotificationTypeScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Surface import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember @@ -20,11 +21,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import com.greenart7c3.nostrsigner.BuildConfig import com.greenart7c3.nostrsigner.LocalPreferences import com.greenart7c3.nostrsigner.NostrSigner import com.greenart7c3.nostrsigner.R +import com.greenart7c3.nostrsigner.relays.AmberListenerSingleton import com.greenart7c3.nostrsigner.service.ConnectivityService import com.greenart7c3.nostrsigner.service.NotificationDataSource import com.greenart7c3.nostrsigner.service.PushNotificationUtils @@ -38,17 +39,35 @@ import kotlinx.coroutines.launch @Composable fun NotificationTypeScreen( modifier: Modifier = Modifier, - navController: NavController, + onDone: () -> Unit, ) { val context = LocalContext.current + + LaunchedEffect(Unit) { + launch(Dispatchers.IO) { + if (!LocalPreferences.isNotificationTypeConfigured()) { + AmberListenerSingleton.accountStateViewModel?.toast( + context.getString(R.string.notification_settings), + context.getString(R.string.please_configure_your_notification_settings_before_continuing), + ) + } + } + } + val scope = rememberCoroutineScope() var notificationItemsIndex by remember { mutableIntStateOf(NostrSigner.getInstance().settings.notificationType.screenCode) } val notificationItems = persistentListOf( - TitleExplainer(stringResource(NotificationType.PUSH.resourceId)), - TitleExplainer(stringResource(NotificationType.DIRECT.resourceId)), + TitleExplainer( + stringResource(NotificationType.PUSH.resourceId), + stringResource(R.string.push_notifications_explainer), + ), + TitleExplainer( + stringResource(NotificationType.DIRECT.resourceId), + stringResource(R.string.direct_notifications_explainer), + ), ) Surface( modifier.fillMaxSize(), @@ -112,7 +131,7 @@ fun NotificationTypeScreen( ), ) scope.launch(Dispatchers.Main) { - navController.navigateUp() + onDone() } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0a016f3b..ee48ad4a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -364,4 +364,8 @@ Blobs stored on mediaservers Modular Article Header Modular Article Content + Using push notifications is significantly slower and less private + Keep the relays connections open all the time. Using this option is faster and will result in a better experience + Notification Settings + Please configure your notification settings before continuing.