From 57ff5c63b09aeb2e2c6cebdaa10f3d3145916b26 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Thu, 12 Oct 2023 18:36:59 +0100 Subject: [PATCH 01/13] Add UnifiedPush dependency. --- app/build.gradle | 3 +++ settings.gradle | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 5bc4ff384..43a833020 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -174,6 +174,9 @@ dependencies { playImplementation platform('com.google.firebase:firebase-bom:32.3.1') playImplementation 'com.google.firebase:firebase-messaging-ktx' + //PushNotifications(FDroid) + fdroidImplementation 'com.github.UnifiedPush:android-connector:2.2.0' + // Charts implementation "com.patrykandpatrick.vico:core:${vico_version}" implementation "com.patrykandpatrick.vico:compose:${vico_version}" diff --git a/settings.gradle b/settings.gradle index 017aa10c6..d535b492f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,7 +3,12 @@ pluginManagement { gradlePluginPortal() google() mavenCentral() - maven { url "https://jitpack.io" } + maven { + url "https://jitpack.io" + content { + includeModule 'com.github.UnifiedPush', 'android-connector' + } + } } } dependencyResolutionManagement { From 17adbf3603d4af126709632cbd42bffec09ca489 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Thu, 12 Oct 2023 19:48:54 +0100 Subject: [PATCH 02/13] Introduce PushMessageReceiver and set up manifest. --- app/src/fdroid/AndroidManifest.xml | 23 ++++++++++++++++ .../notifications/PushMessageReceiver.kt | 27 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 app/src/fdroid/AndroidManifest.xml create mode 100644 app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt diff --git a/app/src/fdroid/AndroidManifest.xml b/app/src/fdroid/AndroidManifest.xml new file mode 100644 index 000000000..c4ef213f9 --- /dev/null +++ b/app/src/fdroid/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt new file mode 100644 index 000000000..14d05a96b --- /dev/null +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt @@ -0,0 +1,27 @@ +package com.vitorpamplona.amethyst.service.notifications + +import android.content.Context +import android.content.Intent +import org.unifiedpush.android.connector.MessagingReceiver + +class PushMessageReceiver: MessagingReceiver() { + override fun onMessage(context: Context, message: ByteArray, instance: String) { + super.onMessage(context, message, instance) + } + + override fun onNewEndpoint(context: Context, endpoint: String, instance: String) { + super.onNewEndpoint(context, endpoint, instance) + } + + override fun onReceive(context: Context, intent: Intent) { + super.onReceive(context, intent) + } + + override fun onRegistrationFailed(context: Context, instance: String) { + super.onRegistrationFailed(context, instance) + } + + override fun onUnregistered(context: Context, instance: String) { + super.onUnregistered(context, instance) + } +} \ No newline at end of file From 68f4752c66b24c76d89344f9332e71fc1bae03c9 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Wed, 18 Oct 2023 03:20:22 +0100 Subject: [PATCH 03/13] Introduce PushDistributorHandler and wire up PushMessageReceiver and PushNotificationUtils. --- .../notifications/PushDistributorHandler.kt | 43 +++++++++++++ .../notifications/PushMessageReceiver.kt | 62 +++++++++++++++++-- .../notifications/PushNotificationUtils.kt | 15 ++++- 3 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt new file mode 100644 index 000000000..60c272285 --- /dev/null +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt @@ -0,0 +1,43 @@ +package com.vitorpamplona.amethyst.service.notifications + +import com.vitorpamplona.amethyst.Amethyst +import org.unifiedpush.android.connector.UnifiedPush + +interface PushDistributorActions { + fun getSavedDistributor(): String + fun getInstalledDistributors(): List + fun saveDistributor(distributor: String) + fun removeSavedDistributor() +} +class PushDistributorHandler() : PushDistributorActions { + private val appContext = Amethyst.instance.applicationContext + private val unifiedPush: UnifiedPush = UnifiedPush() + + private var endpointInternal = "" + val endpoint = endpointInternal + + fun getEndpoint() = endpoint + fun setEndpoint(newEndpoint: String) { + endpointInternal = newEndpoint + } + + override fun getSavedDistributor(): String { + return unifiedPush.getDistributor(appContext) + } + + fun savedDistributorExists(): Boolean = getSavedDistributor().isNotEmpty() + + override fun getInstalledDistributors(): List { + return unifiedPush.getDistributors(appContext) + } + + override fun saveDistributor(distributor: String) { + unifiedPush.saveDistributor(appContext, distributor) + } + + override fun removeSavedDistributor() { + unifiedPush.safeRemoveDistributor(appContext) + } +} + +fun UnifiedPush() = UnifiedPush diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt index 14d05a96b..a232c3ea6 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt @@ -1,27 +1,81 @@ package com.vitorpamplona.amethyst.service.notifications +import android.app.NotificationManager import android.content.Context import android.content.Intent +import android.util.Log +import android.util.LruCache +import androidx.core.content.ContextCompat +import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.LocalPreferences +import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateDMChannel +import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateZapChannel +import com.vitorpamplona.quartz.events.Event +import com.vitorpamplona.quartz.events.GiftWrapEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch import org.unifiedpush.android.connector.MessagingReceiver -class PushMessageReceiver: MessagingReceiver() { +class PushMessageReceiver : MessagingReceiver() { + private val appContext = Amethyst.instance.applicationContext + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val eventCache = LruCache(100) + private val pushHandler = PushDistributorHandler() + override fun onMessage(context: Context, message: ByteArray, instance: String) { - super.onMessage(context, message, instance) + val messageStr = String(message) + Log.d("Amethyst-OSSPush", "New message ${message.decodeToString()} for Instance: $instance") + scope.launch { + parseMessage(messageStr)?.let { + receiveIfNew(it) + } + } + } + + private suspend fun parseMessage(message: String): GiftWrapEvent? { + (Event.fromJson(message) as? GiftWrapEvent)?.let { + return it + } + return null + } + + private suspend fun receiveIfNew(event: GiftWrapEvent) { + if (eventCache.get(event.id) == null) { + eventCache.put(event.id, event.id) + EventNotificationConsumer(appContext).consume(event) + } } override fun onNewEndpoint(context: Context, endpoint: String, instance: String) { - super.onNewEndpoint(context, endpoint, instance) + Log.d("Amethyst-OSSPush", "New endpoint provided:- $endpoint for Instance: $instance") + pushHandler.setEndpoint(endpoint) + scope.launch(Dispatchers.IO) { + RegisterAccounts(LocalPreferences.allSavedAccounts()).go(endpoint) + notificationManager().getOrCreateZapChannel(appContext) + notificationManager().getOrCreateDMChannel(appContext) + } } override fun onReceive(context: Context, intent: Intent) { + val intentData = intent.dataString + val intentAction = intent.action.toString() + Log.d("Amethyst-OSSPush", "Intent Data:- $intentData Intent Action: $intentAction") super.onReceive(context, intent) } override fun onRegistrationFailed(context: Context, instance: String) { + scope.cancel() super.onRegistrationFailed(context, instance) } override fun onUnregistered(context: Context, instance: String) { super.onUnregistered(context, instance) } -} \ No newline at end of file + + fun notificationManager(): NotificationManager { + return ContextCompat.getSystemService(appContext, NotificationManager::class.java) as NotificationManager + } +} diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt index 2855d5e5a..733735ee3 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt @@ -1,9 +1,20 @@ package com.vitorpamplona.amethyst.service.notifications +import android.util.Log import com.vitorpamplona.amethyst.AccountInfo +import kotlinx.coroutines.Dispatchers object PushNotificationUtils { - var hasInit: Boolean = true - suspend fun init(accounts: List) { + var hasInit: Boolean = false + private val pushHandler = PushDistributorHandler() + suspend fun init(accounts: List) = with(Dispatchers.IO) { + if (hasInit || pushHandler.savedDistributorExists()) { + return@with + } + try { + RegisterAccounts(accounts).go(pushHandler.getEndpoint()) + } catch (e: Exception) { + Log.d("Amethyst-OSSPushUtils", "Failed to get endpoint.") + } } } From 3f5000f2788171bce627fed0f8d34b9124d179bd Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Wed, 18 Oct 2023 03:30:02 +0100 Subject: [PATCH 04/13] Change getEndpoint function name to avoid clashing method signatures. --- .../amethyst/service/notifications/PushDistributorHandler.kt | 2 +- .../amethyst/service/notifications/PushNotificationUtils.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt index 60c272285..cad25f904 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt @@ -16,7 +16,7 @@ class PushDistributorHandler() : PushDistributorActions { private var endpointInternal = "" val endpoint = endpointInternal - fun getEndpoint() = endpoint + fun getSavedEndpoint() = endpoint fun setEndpoint(newEndpoint: String) { endpointInternal = newEndpoint } diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt index 733735ee3..a767a2ae5 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt @@ -12,7 +12,7 @@ object PushNotificationUtils { return@with } try { - RegisterAccounts(accounts).go(pushHandler.getEndpoint()) + RegisterAccounts(accounts).go(pushHandler.getSavedEndpoint()) } catch (e: Exception) { Log.d("Amethyst-OSSPushUtils", "Failed to get endpoint.") } From 904b0703066b83230d865022f3cdbb3ca0541a62 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Wed, 18 Oct 2023 17:07:26 +0100 Subject: [PATCH 05/13] Move NotificationScreen to different flavors to integrate Distributor dialog selection. Make some adjustments for handling changes to push distributors, and other fixes. --- .../notifications/PushDistributorHandler.kt | 41 +- .../notifications/PushMessageReceiver.kt | 26 +- .../notifications/PushNotificationUtils.kt | 11 +- .../ui/screen/loggedIn/NotificationScreen.kt | 370 ++++++++++++++++++ .../service/notifications/RegisterAccounts.kt | 7 +- .../ui/screen/loggedIn/NotificationScreen.kt | 0 6 files changed, 437 insertions(+), 18 deletions(-) create mode 100644 app/src/fdroid/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt rename app/src/{main => play}/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt (100%) diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt index cad25f904..0219c6c55 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt @@ -1,5 +1,9 @@ package com.vitorpamplona.amethyst.service.notifications +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.util.Log import com.vitorpamplona.amethyst.Amethyst import org.unifiedpush.android.connector.UnifiedPush @@ -9,9 +13,9 @@ interface PushDistributorActions { fun saveDistributor(distributor: String) fun removeSavedDistributor() } -class PushDistributorHandler() : PushDistributorActions { +object PushDistributorHandler : PushDistributorActions { private val appContext = Amethyst.instance.applicationContext - private val unifiedPush: UnifiedPush = UnifiedPush() + private val unifiedPush: UnifiedPush = UnifiedPush private var endpointInternal = "" val endpoint = endpointInternal @@ -19,6 +23,11 @@ class PushDistributorHandler() : PushDistributorActions { fun getSavedEndpoint() = endpoint fun setEndpoint(newEndpoint: String) { endpointInternal = newEndpoint + Log.d("PushHandler", "New endpoint saved : $endpointInternal") + } + + fun removeEndpoint() { + endpointInternal = "" } override fun getSavedDistributor(): String { @@ -31,13 +40,37 @@ class PushDistributorHandler() : PushDistributorActions { return unifiedPush.getDistributors(appContext) } + fun formattedDistributorNames(): List { + val distributorsArray = getInstalledDistributors().toTypedArray() + val distributorsNameArray = distributorsArray.map { + try { + val ai = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + appContext.packageManager.getApplicationInfo( + it, + PackageManager.ApplicationInfoFlags.of( + PackageManager.GET_META_DATA.toLong() + ) + ) + } else { + appContext.packageManager.getApplicationInfo(it, 0) + } + appContext.packageManager.getApplicationLabel(ai) + } catch (e: PackageManager.NameNotFoundException) { + it + } as String + }.toTypedArray() + return distributorsNameArray.toList() + } + override fun saveDistributor(distributor: String) { unifiedPush.saveDistributor(appContext, distributor) + unifiedPush.registerApp(appContext) } override fun removeSavedDistributor() { unifiedPush.safeRemoveDistributor(appContext) } + fun forceRemoveDistributor(context: Context) { + unifiedPush.forceRemoveDistributor(context) + } } - -fun UnifiedPush() = UnifiedPush diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt index a232c3ea6..a14b672e9 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt @@ -20,17 +20,22 @@ import kotlinx.coroutines.launch import org.unifiedpush.android.connector.MessagingReceiver class PushMessageReceiver : MessagingReceiver() { + private val TAG = "Amethyst-OSSPushReceiver" private val appContext = Amethyst.instance.applicationContext private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val eventCache = LruCache(100) - private val pushHandler = PushDistributorHandler() + private val pushHandler = PushDistributorHandler override fun onMessage(context: Context, message: ByteArray, instance: String) { val messageStr = String(message) - Log.d("Amethyst-OSSPush", "New message ${message.decodeToString()} for Instance: $instance") + Log.d(TAG, "New message ${message.decodeToString()} for Instance: $instance") scope.launch { - parseMessage(messageStr)?.let { - receiveIfNew(it) + try { + parseMessage(messageStr)?.let { + receiveIfNew(it) + } + } catch (e: Exception) { + Log.d(TAG, "Message could not be parsed: ${e.message}") } } } @@ -50,7 +55,7 @@ class PushMessageReceiver : MessagingReceiver() { } override fun onNewEndpoint(context: Context, endpoint: String, instance: String) { - Log.d("Amethyst-OSSPush", "New endpoint provided:- $endpoint for Instance: $instance") + Log.d(TAG, "New endpoint provided:- $endpoint for Instance: $instance") pushHandler.setEndpoint(endpoint) scope.launch(Dispatchers.IO) { RegisterAccounts(LocalPreferences.allSavedAccounts()).go(endpoint) @@ -62,17 +67,22 @@ class PushMessageReceiver : MessagingReceiver() { override fun onReceive(context: Context, intent: Intent) { val intentData = intent.dataString val intentAction = intent.action.toString() - Log.d("Amethyst-OSSPush", "Intent Data:- $intentData Intent Action: $intentAction") + Log.d(TAG, "Intent Data:- $intentData Intent Action: $intentAction") super.onReceive(context, intent) } override fun onRegistrationFailed(context: Context, instance: String) { + Log.d(TAG, "Registration failed for Instance: $instance") scope.cancel() - super.onRegistrationFailed(context, instance) + pushHandler.forceRemoveDistributor(context) } override fun onUnregistered(context: Context, instance: String) { - super.onUnregistered(context, instance) + val removedEndpoint = pushHandler.endpoint + Log.d(TAG, "Endpoint: $removedEndpoint removed for Instance: $instance") + Log.d(TAG, "App is unregistered. ") + pushHandler.forceRemoveDistributor(context) + pushHandler.removeEndpoint() } fun notificationManager(): NotificationManager { diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt index a767a2ae5..6e153d28f 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt @@ -2,17 +2,18 @@ package com.vitorpamplona.amethyst.service.notifications import android.util.Log import com.vitorpamplona.amethyst.AccountInfo -import kotlinx.coroutines.Dispatchers object PushNotificationUtils { var hasInit: Boolean = false - private val pushHandler = PushDistributorHandler() - suspend fun init(accounts: List) = with(Dispatchers.IO) { + private val pushHandler = PushDistributorHandler + suspend fun init(accounts: List) { if (hasInit || pushHandler.savedDistributorExists()) { - return@with + return } try { - RegisterAccounts(accounts).go(pushHandler.getSavedEndpoint()) + if (pushHandler.savedDistributorExists()) { + RegisterAccounts(accounts).go(pushHandler.getSavedEndpoint()) + } } catch (e: Exception) { Log.d("Amethyst-OSSPushUtils", "Failed to get endpoint.") } diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt new file mode 100644 index 000000000..15ea069ac --- /dev/null +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt @@ -0,0 +1,370 @@ +package com.vitorpamplona.amethyst.ui.screen.loggedIn + +import android.Manifest +import android.os.Build +import android.util.Log +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Divider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.halilibo.richtext.markdown.Markdown +import com.halilibo.richtext.ui.RichTextStyle +import com.halilibo.richtext.ui.material3.Material3RichText +import com.halilibo.richtext.ui.resolveDefaults +import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis +import com.patrykandpatrick.vico.compose.axis.vertical.endAxis +import com.patrykandpatrick.vico.compose.axis.vertical.startAxis +import com.patrykandpatrick.vico.compose.chart.Chart +import com.patrykandpatrick.vico.compose.chart.line.lineChart +import com.patrykandpatrick.vico.compose.component.shape.shader.fromBrush +import com.patrykandpatrick.vico.compose.style.ProvideChartStyle +import com.patrykandpatrick.vico.core.DefaultAlpha +import com.patrykandpatrick.vico.core.axis.AxisPosition +import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter +import com.patrykandpatrick.vico.core.chart.composed.plus +import com.patrykandpatrick.vico.core.chart.line.LineChart +import com.patrykandpatrick.vico.core.chart.values.ChartValues +import com.patrykandpatrick.vico.core.component.shape.shader.DynamicShaders +import com.vitorpamplona.amethyst.service.NostrAccountDataSource +import com.vitorpamplona.amethyst.service.notifications.PushDistributorHandler +import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.note.OneGiga +import com.vitorpamplona.amethyst.ui.note.OneKilo +import com.vitorpamplona.amethyst.ui.note.OneMega +import com.vitorpamplona.amethyst.ui.note.UserReactionsRow +import com.vitorpamplona.amethyst.ui.note.UserReactionsViewModel +import com.vitorpamplona.amethyst.ui.note.showCount +import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel +import com.vitorpamplona.amethyst.ui.screen.RefresheableCardView +import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys +import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange +import com.vitorpamplona.amethyst.ui.theme.RoyalBlue +import com.vitorpamplona.amethyst.ui.theme.chartStyle +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal +import java.math.RoundingMode +import java.text.DecimalFormat +import kotlin.math.roundToInt + +@Composable +fun NotificationScreen( + notifFeedViewModel: NotificationViewModel, + userReactionsStatsModel: UserReactionsViewModel, + accountViewModel: AccountViewModel, + nav: (String) -> Unit +) { + WatchAccountForNotifications(notifFeedViewModel, accountViewModel) + + CheckifItNeedsToRequestNotificationPermission() + + val lifeCycleOwner = LocalLifecycleOwner.current + DisposableEffect(lifeCycleOwner) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) { + NostrAccountDataSource.invalidateFilters() + } + } + + lifeCycleOwner.lifecycle.addObserver(observer) + onDispose { + lifeCycleOwner.lifecycle.removeObserver(observer) + } + } + val pushHandler = PushDistributorHandler + var distributorPresent by remember { + mutableStateOf(pushHandler.savedDistributorExists()) + } + val list = pushHandler.getInstalledDistributors() + val readableList = pushHandler.formattedDistributorNames() + if (!distributorPresent) { + SelectPushDistributor( + distrbutorList = readableList.toImmutableList(), + onDistributorSelected = { index, name -> + val fullDistributorName = list[index] + pushHandler.saveDistributor(fullDistributorName) + Log.d("Amethyst", "NotificationScreen: Distributor registered.") + }, + onDismiss = { + distributorPresent = true + Log.d("Amethyst", "NotificationScreen: Distributor dialog dismissed.") + } + ) + } else { + val currentDistributor = pushHandler.getSavedDistributor() + pushHandler.saveDistributor(currentDistributor) + } + + Column(Modifier.fillMaxHeight()) { + Column( + modifier = Modifier.padding(vertical = 0.dp) + ) { + SummaryBar( + model = userReactionsStatsModel + ) + + RefresheableCardView( + viewModel = notifFeedViewModel, + accountViewModel = accountViewModel, + nav = nav, + routeForLastRead = Route.Notification.base, + scrollStateKey = ScrollStateKeys.NOTIFICATION_SCREEN + ) + } + } +} + +// TODO: Turn this into an Account flag +var hasAlreadyAskedNotificationPermissions = false + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun CheckifItNeedsToRequestNotificationPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !hasAlreadyAskedNotificationPermissions) { + val notificationPermissionState = rememberPermissionState( + Manifest.permission.POST_NOTIFICATIONS + ) + + if (!notificationPermissionState.status.isGranted) { + hasAlreadyAskedNotificationPermissions = true + + // This will pause the APP, including the connection with relays. + LaunchedEffect(notificationPermissionState) { + notificationPermissionState.launchPermissionRequest() + } + } + } +} + +@Composable +fun WatchAccountForNotifications( + notifFeedViewModel: NotificationViewModel, + accountViewModel: AccountViewModel +) { + val accountState by accountViewModel.accountLiveData.observeAsState() + + LaunchedEffect(accountViewModel, accountState?.account?.defaultNotificationFollowList) { + NostrAccountDataSource.invalidateFilters() + notifFeedViewModel.checkKeysInvalidateDataAndSendToTop() + } +} + +@Composable +fun SelectPushDistributor( + distrbutorList: ImmutableList, + onDistributorSelected: (Int, String) -> Unit, + onDismiss: () -> Unit +) { + Dialog(onDismissRequest = onDismiss) { + Surface( + modifier = Modifier + .border( + width = Dp.Hairline, + color = MaterialTheme.colorScheme.background, + shape = CircleShape + ) + .fillMaxWidth() + ) { + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + ) { + Box(modifier = Modifier.align(CenterHorizontally)) { + Material3RichText( + style = RichTextStyle().resolveDefaults() + ) { + Markdown(content = "### Select a distributor") + } + } + distrbutorList.forEachIndexed { index, distributor -> + TextButton( + onClick = { + onDistributorSelected(index, distributor) + onDismiss() + }, + modifier = Modifier.fillMaxWidth() + ) { + Material3RichText( + style = RichTextStyle().resolveDefaults() + ) { + Markdown(content = distributor) + } + } + } + } + } + } +} + +@Composable +fun SummaryBar(model: UserReactionsViewModel) { + var showChart by remember { + mutableStateOf(false) + } + + UserReactionsRow(model) { + showChart = !showChart + } + + if (showChart) { + val lineChartCount = + lineChart( + lines = listOf(RoyalBlue, Color.Green, Color.Red).map { lineChartColor -> + LineChart.LineSpec( + lineColor = lineChartColor.toArgb(), + lineBackgroundShader = DynamicShaders.fromBrush( + Brush.verticalGradient( + listOf( + lineChartColor.copy(DefaultAlpha.LINE_BACKGROUND_SHADER_START), + lineChartColor.copy(DefaultAlpha.LINE_BACKGROUND_SHADER_END) + ) + ) + ) + ) + }, + targetVerticalAxisPosition = AxisPosition.Vertical.Start + ) + + val lineChartZaps = + lineChart( + lines = listOf(BitcoinOrange).map { lineChartColor -> + LineChart.LineSpec( + lineColor = lineChartColor.toArgb(), + lineBackgroundShader = DynamicShaders.fromBrush( + Brush.verticalGradient( + listOf( + lineChartColor.copy(DefaultAlpha.LINE_BACKGROUND_SHADER_START), + lineChartColor.copy(DefaultAlpha.LINE_BACKGROUND_SHADER_END) + ) + ) + ) + ) + }, + targetVerticalAxisPosition = AxisPosition.Vertical.End + ) + + Row( + modifier = Modifier + .padding(vertical = 10.dp, horizontal = 20.dp) + .clickable(onClick = { showChart = !showChart }) + ) { + ProvideChartStyle( + chartStyle = MaterialTheme.colorScheme.chartStyle + ) { + ObserveAndShowChart(model, lineChartCount, lineChartZaps) + } + } + } + + Divider( + thickness = 0.25.dp + ) +} + +@Composable +private fun ObserveAndShowChart( + model: UserReactionsViewModel, + lineChartCount: LineChart, + lineChartZaps: LineChart +) { + val axisModel by model.axisLabels.collectAsState() + val chartModel by model.chartModel.collectAsState() + chartModel?.let { + Chart( + chart = remember(lineChartCount, lineChartZaps) { + lineChartCount.plus(lineChartZaps) + }, + model = it, + startAxis = startAxis( + valueFormatter = CountAxisValueFormatter() + ), + endAxis = endAxis( + valueFormatter = AmountAxisValueFormatter() + ), + bottomAxis = bottomAxis( + valueFormatter = LabelValueFormatter(axisModel) + ) + ) + } +} + +@Stable +class LabelValueFormatter(val axisLabels: List) : AxisValueFormatter { + override fun formatValue( + value: Float, + chartValues: ChartValues + ): String { + return axisLabels[value.roundToInt()] + } +} + +@Stable +class CountAxisValueFormatter() : AxisValueFormatter { + override fun formatValue( + value: Float, + chartValues: ChartValues + ): String { + return showCount(value.roundToInt()) + } +} + +@Stable +class AmountAxisValueFormatter() : AxisValueFormatter { + override fun formatValue( + value: Float, + chartValues: ChartValues + ): String { + return showAmountAxis(value.toBigDecimal()) + } +} + +var dfG: DecimalFormat = DecimalFormat("#G") +var dfM: DecimalFormat = DecimalFormat("#M") +var dfK: DecimalFormat = DecimalFormat("#k") +var dfN: DecimalFormat = DecimalFormat("#") + +fun showAmountAxis(amount: BigDecimal?): String { + if (amount == null) return "" + if (amount.abs() < BigDecimal(0.01)) return "" + + return when { + amount >= OneGiga -> dfG.format(amount.div(OneGiga).setScale(0, RoundingMode.HALF_UP)) + amount >= OneMega -> dfM.format(amount.div(OneMega).setScale(0, RoundingMode.HALF_UP)) + amount >= OneKilo -> dfK.format(amount.div(OneKilo).setScale(0, RoundingMode.HALF_UP)) + else -> dfN.format(amount) + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt index 3e6ecdcd4..8a90c3abd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt @@ -62,7 +62,12 @@ class RegisterAccounts( it.isSuccessful } } catch (e: java.lang.Exception) { - Log.e("FirebaseMsgService", "Unable to register with push server", e) + val tag = if (BuildConfig.FLAVOR == "play") { + "FirebaseMsgService" + } else { + "UnifiedPushService" + } + Log.e(tag, "Unable to register with push server", e) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt b/app/src/play/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt similarity index 100% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt rename to app/src/play/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt From f68f32f942add5a86ee93ed14dbe70fc7821c02b Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Thu, 12 Oct 2023 18:36:59 +0100 Subject: [PATCH 06/13] Add UnifiedPush dependency. --- app/build.gradle | 3 +++ settings.gradle | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index a8168b9d7..f299f7b3b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -174,6 +174,9 @@ dependencies { playImplementation platform('com.google.firebase:firebase-bom:32.3.1') playImplementation 'com.google.firebase:firebase-messaging-ktx' + //PushNotifications(FDroid) + fdroidImplementation 'com.github.UnifiedPush:android-connector:2.2.0' + // Charts implementation "com.patrykandpatrick.vico:core:${vico_version}" implementation "com.patrykandpatrick.vico:compose:${vico_version}" diff --git a/settings.gradle b/settings.gradle index 017aa10c6..d535b492f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,7 +3,12 @@ pluginManagement { gradlePluginPortal() google() mavenCentral() - maven { url "https://jitpack.io" } + maven { + url "https://jitpack.io" + content { + includeModule 'com.github.UnifiedPush', 'android-connector' + } + } } } dependencyResolutionManagement { From 18931fb7438b18377a2b3daaeee8503644790bc8 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Thu, 12 Oct 2023 19:48:54 +0100 Subject: [PATCH 07/13] Introduce PushMessageReceiver and set up manifest. --- app/src/fdroid/AndroidManifest.xml | 23 ++++++++++++++++ .../notifications/PushMessageReceiver.kt | 27 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 app/src/fdroid/AndroidManifest.xml create mode 100644 app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt diff --git a/app/src/fdroid/AndroidManifest.xml b/app/src/fdroid/AndroidManifest.xml new file mode 100644 index 000000000..c4ef213f9 --- /dev/null +++ b/app/src/fdroid/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt new file mode 100644 index 000000000..14d05a96b --- /dev/null +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt @@ -0,0 +1,27 @@ +package com.vitorpamplona.amethyst.service.notifications + +import android.content.Context +import android.content.Intent +import org.unifiedpush.android.connector.MessagingReceiver + +class PushMessageReceiver: MessagingReceiver() { + override fun onMessage(context: Context, message: ByteArray, instance: String) { + super.onMessage(context, message, instance) + } + + override fun onNewEndpoint(context: Context, endpoint: String, instance: String) { + super.onNewEndpoint(context, endpoint, instance) + } + + override fun onReceive(context: Context, intent: Intent) { + super.onReceive(context, intent) + } + + override fun onRegistrationFailed(context: Context, instance: String) { + super.onRegistrationFailed(context, instance) + } + + override fun onUnregistered(context: Context, instance: String) { + super.onUnregistered(context, instance) + } +} \ No newline at end of file From 26107ff5e95ed44512750cec598c4e35f1797c8b Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Wed, 18 Oct 2023 03:20:22 +0100 Subject: [PATCH 08/13] Introduce PushDistributorHandler and wire up PushMessageReceiver and PushNotificationUtils. --- .../notifications/PushDistributorHandler.kt | 43 +++++++++++++ .../notifications/PushMessageReceiver.kt | 62 +++++++++++++++++-- .../notifications/PushNotificationUtils.kt | 15 ++++- 3 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt new file mode 100644 index 000000000..60c272285 --- /dev/null +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt @@ -0,0 +1,43 @@ +package com.vitorpamplona.amethyst.service.notifications + +import com.vitorpamplona.amethyst.Amethyst +import org.unifiedpush.android.connector.UnifiedPush + +interface PushDistributorActions { + fun getSavedDistributor(): String + fun getInstalledDistributors(): List + fun saveDistributor(distributor: String) + fun removeSavedDistributor() +} +class PushDistributorHandler() : PushDistributorActions { + private val appContext = Amethyst.instance.applicationContext + private val unifiedPush: UnifiedPush = UnifiedPush() + + private var endpointInternal = "" + val endpoint = endpointInternal + + fun getEndpoint() = endpoint + fun setEndpoint(newEndpoint: String) { + endpointInternal = newEndpoint + } + + override fun getSavedDistributor(): String { + return unifiedPush.getDistributor(appContext) + } + + fun savedDistributorExists(): Boolean = getSavedDistributor().isNotEmpty() + + override fun getInstalledDistributors(): List { + return unifiedPush.getDistributors(appContext) + } + + override fun saveDistributor(distributor: String) { + unifiedPush.saveDistributor(appContext, distributor) + } + + override fun removeSavedDistributor() { + unifiedPush.safeRemoveDistributor(appContext) + } +} + +fun UnifiedPush() = UnifiedPush diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt index 14d05a96b..a232c3ea6 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt @@ -1,27 +1,81 @@ package com.vitorpamplona.amethyst.service.notifications +import android.app.NotificationManager import android.content.Context import android.content.Intent +import android.util.Log +import android.util.LruCache +import androidx.core.content.ContextCompat +import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.LocalPreferences +import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateDMChannel +import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateZapChannel +import com.vitorpamplona.quartz.events.Event +import com.vitorpamplona.quartz.events.GiftWrapEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch import org.unifiedpush.android.connector.MessagingReceiver -class PushMessageReceiver: MessagingReceiver() { +class PushMessageReceiver : MessagingReceiver() { + private val appContext = Amethyst.instance.applicationContext + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val eventCache = LruCache(100) + private val pushHandler = PushDistributorHandler() + override fun onMessage(context: Context, message: ByteArray, instance: String) { - super.onMessage(context, message, instance) + val messageStr = String(message) + Log.d("Amethyst-OSSPush", "New message ${message.decodeToString()} for Instance: $instance") + scope.launch { + parseMessage(messageStr)?.let { + receiveIfNew(it) + } + } + } + + private suspend fun parseMessage(message: String): GiftWrapEvent? { + (Event.fromJson(message) as? GiftWrapEvent)?.let { + return it + } + return null + } + + private suspend fun receiveIfNew(event: GiftWrapEvent) { + if (eventCache.get(event.id) == null) { + eventCache.put(event.id, event.id) + EventNotificationConsumer(appContext).consume(event) + } } override fun onNewEndpoint(context: Context, endpoint: String, instance: String) { - super.onNewEndpoint(context, endpoint, instance) + Log.d("Amethyst-OSSPush", "New endpoint provided:- $endpoint for Instance: $instance") + pushHandler.setEndpoint(endpoint) + scope.launch(Dispatchers.IO) { + RegisterAccounts(LocalPreferences.allSavedAccounts()).go(endpoint) + notificationManager().getOrCreateZapChannel(appContext) + notificationManager().getOrCreateDMChannel(appContext) + } } override fun onReceive(context: Context, intent: Intent) { + val intentData = intent.dataString + val intentAction = intent.action.toString() + Log.d("Amethyst-OSSPush", "Intent Data:- $intentData Intent Action: $intentAction") super.onReceive(context, intent) } override fun onRegistrationFailed(context: Context, instance: String) { + scope.cancel() super.onRegistrationFailed(context, instance) } override fun onUnregistered(context: Context, instance: String) { super.onUnregistered(context, instance) } -} \ No newline at end of file + + fun notificationManager(): NotificationManager { + return ContextCompat.getSystemService(appContext, NotificationManager::class.java) as NotificationManager + } +} diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt index 2855d5e5a..733735ee3 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt @@ -1,9 +1,20 @@ package com.vitorpamplona.amethyst.service.notifications +import android.util.Log import com.vitorpamplona.amethyst.AccountInfo +import kotlinx.coroutines.Dispatchers object PushNotificationUtils { - var hasInit: Boolean = true - suspend fun init(accounts: List) { + var hasInit: Boolean = false + private val pushHandler = PushDistributorHandler() + suspend fun init(accounts: List) = with(Dispatchers.IO) { + if (hasInit || pushHandler.savedDistributorExists()) { + return@with + } + try { + RegisterAccounts(accounts).go(pushHandler.getEndpoint()) + } catch (e: Exception) { + Log.d("Amethyst-OSSPushUtils", "Failed to get endpoint.") + } } } From 7b22b96968a416e3fa35b30ce176d2619354fe00 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Wed, 18 Oct 2023 03:30:02 +0100 Subject: [PATCH 09/13] Change getEndpoint function name to avoid clashing method signatures. --- .../amethyst/service/notifications/PushDistributorHandler.kt | 2 +- .../amethyst/service/notifications/PushNotificationUtils.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt index 60c272285..cad25f904 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt @@ -16,7 +16,7 @@ class PushDistributorHandler() : PushDistributorActions { private var endpointInternal = "" val endpoint = endpointInternal - fun getEndpoint() = endpoint + fun getSavedEndpoint() = endpoint fun setEndpoint(newEndpoint: String) { endpointInternal = newEndpoint } diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt index 733735ee3..a767a2ae5 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt @@ -12,7 +12,7 @@ object PushNotificationUtils { return@with } try { - RegisterAccounts(accounts).go(pushHandler.getEndpoint()) + RegisterAccounts(accounts).go(pushHandler.getSavedEndpoint()) } catch (e: Exception) { Log.d("Amethyst-OSSPushUtils", "Failed to get endpoint.") } From 68e91e405ae731cbe3887b47c71c6234c4200c83 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Wed, 18 Oct 2023 17:07:26 +0100 Subject: [PATCH 10/13] Move NotificationScreen to different flavors to integrate Distributor dialog selection. Make some adjustments for handling changes to push distributors, and other fixes. --- .../notifications/PushDistributorHandler.kt | 41 +- .../notifications/PushMessageReceiver.kt | 26 +- .../notifications/PushNotificationUtils.kt | 11 +- .../ui/screen/loggedIn/NotificationScreen.kt | 370 ++++++++++++++++++ .../service/notifications/RegisterAccounts.kt | 7 +- .../ui/screen/loggedIn/NotificationScreen.kt | 0 6 files changed, 437 insertions(+), 18 deletions(-) create mode 100644 app/src/fdroid/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt rename app/src/{main => play}/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt (100%) diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt index cad25f904..0219c6c55 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt @@ -1,5 +1,9 @@ package com.vitorpamplona.amethyst.service.notifications +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.util.Log import com.vitorpamplona.amethyst.Amethyst import org.unifiedpush.android.connector.UnifiedPush @@ -9,9 +13,9 @@ interface PushDistributorActions { fun saveDistributor(distributor: String) fun removeSavedDistributor() } -class PushDistributorHandler() : PushDistributorActions { +object PushDistributorHandler : PushDistributorActions { private val appContext = Amethyst.instance.applicationContext - private val unifiedPush: UnifiedPush = UnifiedPush() + private val unifiedPush: UnifiedPush = UnifiedPush private var endpointInternal = "" val endpoint = endpointInternal @@ -19,6 +23,11 @@ class PushDistributorHandler() : PushDistributorActions { fun getSavedEndpoint() = endpoint fun setEndpoint(newEndpoint: String) { endpointInternal = newEndpoint + Log.d("PushHandler", "New endpoint saved : $endpointInternal") + } + + fun removeEndpoint() { + endpointInternal = "" } override fun getSavedDistributor(): String { @@ -31,13 +40,37 @@ class PushDistributorHandler() : PushDistributorActions { return unifiedPush.getDistributors(appContext) } + fun formattedDistributorNames(): List { + val distributorsArray = getInstalledDistributors().toTypedArray() + val distributorsNameArray = distributorsArray.map { + try { + val ai = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + appContext.packageManager.getApplicationInfo( + it, + PackageManager.ApplicationInfoFlags.of( + PackageManager.GET_META_DATA.toLong() + ) + ) + } else { + appContext.packageManager.getApplicationInfo(it, 0) + } + appContext.packageManager.getApplicationLabel(ai) + } catch (e: PackageManager.NameNotFoundException) { + it + } as String + }.toTypedArray() + return distributorsNameArray.toList() + } + override fun saveDistributor(distributor: String) { unifiedPush.saveDistributor(appContext, distributor) + unifiedPush.registerApp(appContext) } override fun removeSavedDistributor() { unifiedPush.safeRemoveDistributor(appContext) } + fun forceRemoveDistributor(context: Context) { + unifiedPush.forceRemoveDistributor(context) + } } - -fun UnifiedPush() = UnifiedPush diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt index a232c3ea6..a14b672e9 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt @@ -20,17 +20,22 @@ import kotlinx.coroutines.launch import org.unifiedpush.android.connector.MessagingReceiver class PushMessageReceiver : MessagingReceiver() { + private val TAG = "Amethyst-OSSPushReceiver" private val appContext = Amethyst.instance.applicationContext private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val eventCache = LruCache(100) - private val pushHandler = PushDistributorHandler() + private val pushHandler = PushDistributorHandler override fun onMessage(context: Context, message: ByteArray, instance: String) { val messageStr = String(message) - Log.d("Amethyst-OSSPush", "New message ${message.decodeToString()} for Instance: $instance") + Log.d(TAG, "New message ${message.decodeToString()} for Instance: $instance") scope.launch { - parseMessage(messageStr)?.let { - receiveIfNew(it) + try { + parseMessage(messageStr)?.let { + receiveIfNew(it) + } + } catch (e: Exception) { + Log.d(TAG, "Message could not be parsed: ${e.message}") } } } @@ -50,7 +55,7 @@ class PushMessageReceiver : MessagingReceiver() { } override fun onNewEndpoint(context: Context, endpoint: String, instance: String) { - Log.d("Amethyst-OSSPush", "New endpoint provided:- $endpoint for Instance: $instance") + Log.d(TAG, "New endpoint provided:- $endpoint for Instance: $instance") pushHandler.setEndpoint(endpoint) scope.launch(Dispatchers.IO) { RegisterAccounts(LocalPreferences.allSavedAccounts()).go(endpoint) @@ -62,17 +67,22 @@ class PushMessageReceiver : MessagingReceiver() { override fun onReceive(context: Context, intent: Intent) { val intentData = intent.dataString val intentAction = intent.action.toString() - Log.d("Amethyst-OSSPush", "Intent Data:- $intentData Intent Action: $intentAction") + Log.d(TAG, "Intent Data:- $intentData Intent Action: $intentAction") super.onReceive(context, intent) } override fun onRegistrationFailed(context: Context, instance: String) { + Log.d(TAG, "Registration failed for Instance: $instance") scope.cancel() - super.onRegistrationFailed(context, instance) + pushHandler.forceRemoveDistributor(context) } override fun onUnregistered(context: Context, instance: String) { - super.onUnregistered(context, instance) + val removedEndpoint = pushHandler.endpoint + Log.d(TAG, "Endpoint: $removedEndpoint removed for Instance: $instance") + Log.d(TAG, "App is unregistered. ") + pushHandler.forceRemoveDistributor(context) + pushHandler.removeEndpoint() } fun notificationManager(): NotificationManager { diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt index a767a2ae5..6e153d28f 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt @@ -2,17 +2,18 @@ package com.vitorpamplona.amethyst.service.notifications import android.util.Log import com.vitorpamplona.amethyst.AccountInfo -import kotlinx.coroutines.Dispatchers object PushNotificationUtils { var hasInit: Boolean = false - private val pushHandler = PushDistributorHandler() - suspend fun init(accounts: List) = with(Dispatchers.IO) { + private val pushHandler = PushDistributorHandler + suspend fun init(accounts: List) { if (hasInit || pushHandler.savedDistributorExists()) { - return@with + return } try { - RegisterAccounts(accounts).go(pushHandler.getSavedEndpoint()) + if (pushHandler.savedDistributorExists()) { + RegisterAccounts(accounts).go(pushHandler.getSavedEndpoint()) + } } catch (e: Exception) { Log.d("Amethyst-OSSPushUtils", "Failed to get endpoint.") } diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt new file mode 100644 index 000000000..15ea069ac --- /dev/null +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt @@ -0,0 +1,370 @@ +package com.vitorpamplona.amethyst.ui.screen.loggedIn + +import android.Manifest +import android.os.Build +import android.util.Log +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Divider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.halilibo.richtext.markdown.Markdown +import com.halilibo.richtext.ui.RichTextStyle +import com.halilibo.richtext.ui.material3.Material3RichText +import com.halilibo.richtext.ui.resolveDefaults +import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis +import com.patrykandpatrick.vico.compose.axis.vertical.endAxis +import com.patrykandpatrick.vico.compose.axis.vertical.startAxis +import com.patrykandpatrick.vico.compose.chart.Chart +import com.patrykandpatrick.vico.compose.chart.line.lineChart +import com.patrykandpatrick.vico.compose.component.shape.shader.fromBrush +import com.patrykandpatrick.vico.compose.style.ProvideChartStyle +import com.patrykandpatrick.vico.core.DefaultAlpha +import com.patrykandpatrick.vico.core.axis.AxisPosition +import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter +import com.patrykandpatrick.vico.core.chart.composed.plus +import com.patrykandpatrick.vico.core.chart.line.LineChart +import com.patrykandpatrick.vico.core.chart.values.ChartValues +import com.patrykandpatrick.vico.core.component.shape.shader.DynamicShaders +import com.vitorpamplona.amethyst.service.NostrAccountDataSource +import com.vitorpamplona.amethyst.service.notifications.PushDistributorHandler +import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.note.OneGiga +import com.vitorpamplona.amethyst.ui.note.OneKilo +import com.vitorpamplona.amethyst.ui.note.OneMega +import com.vitorpamplona.amethyst.ui.note.UserReactionsRow +import com.vitorpamplona.amethyst.ui.note.UserReactionsViewModel +import com.vitorpamplona.amethyst.ui.note.showCount +import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel +import com.vitorpamplona.amethyst.ui.screen.RefresheableCardView +import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys +import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange +import com.vitorpamplona.amethyst.ui.theme.RoyalBlue +import com.vitorpamplona.amethyst.ui.theme.chartStyle +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal +import java.math.RoundingMode +import java.text.DecimalFormat +import kotlin.math.roundToInt + +@Composable +fun NotificationScreen( + notifFeedViewModel: NotificationViewModel, + userReactionsStatsModel: UserReactionsViewModel, + accountViewModel: AccountViewModel, + nav: (String) -> Unit +) { + WatchAccountForNotifications(notifFeedViewModel, accountViewModel) + + CheckifItNeedsToRequestNotificationPermission() + + val lifeCycleOwner = LocalLifecycleOwner.current + DisposableEffect(lifeCycleOwner) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) { + NostrAccountDataSource.invalidateFilters() + } + } + + lifeCycleOwner.lifecycle.addObserver(observer) + onDispose { + lifeCycleOwner.lifecycle.removeObserver(observer) + } + } + val pushHandler = PushDistributorHandler + var distributorPresent by remember { + mutableStateOf(pushHandler.savedDistributorExists()) + } + val list = pushHandler.getInstalledDistributors() + val readableList = pushHandler.formattedDistributorNames() + if (!distributorPresent) { + SelectPushDistributor( + distrbutorList = readableList.toImmutableList(), + onDistributorSelected = { index, name -> + val fullDistributorName = list[index] + pushHandler.saveDistributor(fullDistributorName) + Log.d("Amethyst", "NotificationScreen: Distributor registered.") + }, + onDismiss = { + distributorPresent = true + Log.d("Amethyst", "NotificationScreen: Distributor dialog dismissed.") + } + ) + } else { + val currentDistributor = pushHandler.getSavedDistributor() + pushHandler.saveDistributor(currentDistributor) + } + + Column(Modifier.fillMaxHeight()) { + Column( + modifier = Modifier.padding(vertical = 0.dp) + ) { + SummaryBar( + model = userReactionsStatsModel + ) + + RefresheableCardView( + viewModel = notifFeedViewModel, + accountViewModel = accountViewModel, + nav = nav, + routeForLastRead = Route.Notification.base, + scrollStateKey = ScrollStateKeys.NOTIFICATION_SCREEN + ) + } + } +} + +// TODO: Turn this into an Account flag +var hasAlreadyAskedNotificationPermissions = false + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun CheckifItNeedsToRequestNotificationPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !hasAlreadyAskedNotificationPermissions) { + val notificationPermissionState = rememberPermissionState( + Manifest.permission.POST_NOTIFICATIONS + ) + + if (!notificationPermissionState.status.isGranted) { + hasAlreadyAskedNotificationPermissions = true + + // This will pause the APP, including the connection with relays. + LaunchedEffect(notificationPermissionState) { + notificationPermissionState.launchPermissionRequest() + } + } + } +} + +@Composable +fun WatchAccountForNotifications( + notifFeedViewModel: NotificationViewModel, + accountViewModel: AccountViewModel +) { + val accountState by accountViewModel.accountLiveData.observeAsState() + + LaunchedEffect(accountViewModel, accountState?.account?.defaultNotificationFollowList) { + NostrAccountDataSource.invalidateFilters() + notifFeedViewModel.checkKeysInvalidateDataAndSendToTop() + } +} + +@Composable +fun SelectPushDistributor( + distrbutorList: ImmutableList, + onDistributorSelected: (Int, String) -> Unit, + onDismiss: () -> Unit +) { + Dialog(onDismissRequest = onDismiss) { + Surface( + modifier = Modifier + .border( + width = Dp.Hairline, + color = MaterialTheme.colorScheme.background, + shape = CircleShape + ) + .fillMaxWidth() + ) { + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + ) { + Box(modifier = Modifier.align(CenterHorizontally)) { + Material3RichText( + style = RichTextStyle().resolveDefaults() + ) { + Markdown(content = "### Select a distributor") + } + } + distrbutorList.forEachIndexed { index, distributor -> + TextButton( + onClick = { + onDistributorSelected(index, distributor) + onDismiss() + }, + modifier = Modifier.fillMaxWidth() + ) { + Material3RichText( + style = RichTextStyle().resolveDefaults() + ) { + Markdown(content = distributor) + } + } + } + } + } + } +} + +@Composable +fun SummaryBar(model: UserReactionsViewModel) { + var showChart by remember { + mutableStateOf(false) + } + + UserReactionsRow(model) { + showChart = !showChart + } + + if (showChart) { + val lineChartCount = + lineChart( + lines = listOf(RoyalBlue, Color.Green, Color.Red).map { lineChartColor -> + LineChart.LineSpec( + lineColor = lineChartColor.toArgb(), + lineBackgroundShader = DynamicShaders.fromBrush( + Brush.verticalGradient( + listOf( + lineChartColor.copy(DefaultAlpha.LINE_BACKGROUND_SHADER_START), + lineChartColor.copy(DefaultAlpha.LINE_BACKGROUND_SHADER_END) + ) + ) + ) + ) + }, + targetVerticalAxisPosition = AxisPosition.Vertical.Start + ) + + val lineChartZaps = + lineChart( + lines = listOf(BitcoinOrange).map { lineChartColor -> + LineChart.LineSpec( + lineColor = lineChartColor.toArgb(), + lineBackgroundShader = DynamicShaders.fromBrush( + Brush.verticalGradient( + listOf( + lineChartColor.copy(DefaultAlpha.LINE_BACKGROUND_SHADER_START), + lineChartColor.copy(DefaultAlpha.LINE_BACKGROUND_SHADER_END) + ) + ) + ) + ) + }, + targetVerticalAxisPosition = AxisPosition.Vertical.End + ) + + Row( + modifier = Modifier + .padding(vertical = 10.dp, horizontal = 20.dp) + .clickable(onClick = { showChart = !showChart }) + ) { + ProvideChartStyle( + chartStyle = MaterialTheme.colorScheme.chartStyle + ) { + ObserveAndShowChart(model, lineChartCount, lineChartZaps) + } + } + } + + Divider( + thickness = 0.25.dp + ) +} + +@Composable +private fun ObserveAndShowChart( + model: UserReactionsViewModel, + lineChartCount: LineChart, + lineChartZaps: LineChart +) { + val axisModel by model.axisLabels.collectAsState() + val chartModel by model.chartModel.collectAsState() + chartModel?.let { + Chart( + chart = remember(lineChartCount, lineChartZaps) { + lineChartCount.plus(lineChartZaps) + }, + model = it, + startAxis = startAxis( + valueFormatter = CountAxisValueFormatter() + ), + endAxis = endAxis( + valueFormatter = AmountAxisValueFormatter() + ), + bottomAxis = bottomAxis( + valueFormatter = LabelValueFormatter(axisModel) + ) + ) + } +} + +@Stable +class LabelValueFormatter(val axisLabels: List) : AxisValueFormatter { + override fun formatValue( + value: Float, + chartValues: ChartValues + ): String { + return axisLabels[value.roundToInt()] + } +} + +@Stable +class CountAxisValueFormatter() : AxisValueFormatter { + override fun formatValue( + value: Float, + chartValues: ChartValues + ): String { + return showCount(value.roundToInt()) + } +} + +@Stable +class AmountAxisValueFormatter() : AxisValueFormatter { + override fun formatValue( + value: Float, + chartValues: ChartValues + ): String { + return showAmountAxis(value.toBigDecimal()) + } +} + +var dfG: DecimalFormat = DecimalFormat("#G") +var dfM: DecimalFormat = DecimalFormat("#M") +var dfK: DecimalFormat = DecimalFormat("#k") +var dfN: DecimalFormat = DecimalFormat("#") + +fun showAmountAxis(amount: BigDecimal?): String { + if (amount == null) return "" + if (amount.abs() < BigDecimal(0.01)) return "" + + return when { + amount >= OneGiga -> dfG.format(amount.div(OneGiga).setScale(0, RoundingMode.HALF_UP)) + amount >= OneMega -> dfM.format(amount.div(OneMega).setScale(0, RoundingMode.HALF_UP)) + amount >= OneKilo -> dfK.format(amount.div(OneKilo).setScale(0, RoundingMode.HALF_UP)) + else -> dfN.format(amount) + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt index 9d023231b..d0394692c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt @@ -62,7 +62,12 @@ class RegisterAccounts( it.isSuccessful } } catch (e: java.lang.Exception) { - Log.e("FirebaseMsgService", "Unable to register with push server", e) + val tag = if (BuildConfig.FLAVOR == "play") { + "FirebaseMsgService" + } else { + "UnifiedPushService" + } + Log.e(tag, "Unable to register with push server", e) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt b/app/src/play/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt similarity index 100% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt rename to app/src/play/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt From 33bf305ab5e81b03ad58932b11ff43b1dfe06929 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Thu, 19 Oct 2023 13:25:09 +0100 Subject: [PATCH 11/13] Add some padding to individual list items. --- .../amethyst/ui/screen/loggedIn/NotificationScreen.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt index 15ea069ac..6a0b76331 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt @@ -72,6 +72,7 @@ import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel import com.vitorpamplona.amethyst.ui.screen.RefresheableCardView import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange +import com.vitorpamplona.amethyst.ui.theme.HalfPadding import com.vitorpamplona.amethyst.ui.theme.RoyalBlue import com.vitorpamplona.amethyst.ui.theme.chartStyle import kotlinx.collections.immutable.ImmutableList @@ -216,7 +217,7 @@ fun SelectPushDistributor( onDistributorSelected(index, distributor) onDismiss() }, - modifier = Modifier.fillMaxWidth() + modifier = HalfPadding.fillMaxWidth() ) { Material3RichText( style = RichTextStyle().resolveDefaults() From e1a3975a07a69e50d48cdd52d200f3ad235861c7 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Fri, 20 Oct 2023 03:40:33 +0100 Subject: [PATCH 12/13] Move NotificationScreen back and rework it in the manner of TranslatableRichTextViewer. --- .../loggedIn/CustomNotificationScreen.kt | 117 ++++++ .../ui/screen/loggedIn/NotificationScreen.kt | 371 ------------------ .../amethyst/ui/navigation/AppNavigation.kt | 4 +- .../ui/screen/loggedIn/NotificationScreen.kt | 3 - .../loggedIn/CustomNotificationScreen.kt | 18 + 5 files changed, 137 insertions(+), 376 deletions(-) create mode 100644 app/src/fdroid/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/CustomNotificationScreen.kt delete mode 100644 app/src/fdroid/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt rename app/src/{play => main}/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt (98%) create mode 100644 app/src/play/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/CustomNotificationScreen.kt diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/CustomNotificationScreen.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/CustomNotificationScreen.kt new file mode 100644 index 000000000..a91cf4984 --- /dev/null +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/CustomNotificationScreen.kt @@ -0,0 +1,117 @@ +package com.vitorpamplona.amethyst.ui.screen.loggedIn + +import android.util.Log +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.window.Dialog +import com.halilibo.richtext.markdown.Markdown +import com.halilibo.richtext.ui.RichTextStyle +import com.halilibo.richtext.ui.material3.Material3RichText +import com.halilibo.richtext.ui.resolveDefaults +import com.vitorpamplona.amethyst.service.notifications.PushDistributorHandler +import com.vitorpamplona.amethyst.ui.note.UserReactionsViewModel +import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel +import com.vitorpamplona.amethyst.ui.theme.HalfPadding +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +@Composable +fun CustomNotificationScreen( + notifFeedViewModel: NotificationViewModel, + userReactionsStatsModel: UserReactionsViewModel, + accountViewModel: AccountViewModel, + nav: (String) -> Unit +) { + val pushHandler = PushDistributorHandler + var distributorPresent by remember { + mutableStateOf(pushHandler.savedDistributorExists()) + } + val list = pushHandler.getInstalledDistributors() + val readableList = pushHandler.formattedDistributorNames() + if (!distributorPresent) { + SelectPushDistributor( + distrbutorList = readableList.toImmutableList(), + onDistributorSelected = { index, name -> + val fullDistributorName = list[index] + pushHandler.saveDistributor(fullDistributorName) + Log.d("Amethyst", "NotificationScreen: Distributor registered.") + }, + onDismiss = { + distributorPresent = true + Log.d("Amethyst", "NotificationScreen: Distributor dialog dismissed.") + } + ) + } else { + val currentDistributor = pushHandler.getSavedDistributor() + pushHandler.saveDistributor(currentDistributor) + } + + NotificationScreen( + notifFeedViewModel = notifFeedViewModel, + userReactionsStatsModel = userReactionsStatsModel, + accountViewModel = accountViewModel, + nav = nav + ) +} + +@Composable +fun SelectPushDistributor( + distrbutorList: ImmutableList, + onDistributorSelected: (Int, String) -> Unit, + onDismiss: () -> Unit +) { + Dialog(onDismissRequest = onDismiss) { + Surface( + modifier = Modifier + .border( + width = Dp.Hairline, + color = MaterialTheme.colorScheme.background, + shape = CircleShape + ) + .fillMaxWidth() + ) { + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + ) { + Box(modifier = Modifier.align(CenterHorizontally)) { + Material3RichText( + style = RichTextStyle().resolveDefaults() + ) { + Markdown(content = "### Select a distributor") + } + } + distrbutorList.forEachIndexed { index, distributor -> + TextButton( + onClick = { + onDistributorSelected(index, distributor) + onDismiss() + }, + modifier = HalfPadding.fillMaxWidth() + ) { + Material3RichText( + style = RichTextStyle().resolveDefaults() + ) { + Markdown(content = distributor) + } + } + } + } + } + } +} diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt deleted file mode 100644 index 6a0b76331..000000000 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt +++ /dev/null @@ -1,371 +0,0 @@ -package com.vitorpamplona.amethyst.ui.screen.loggedIn - -import android.Manifest -import android.os.Build -import android.util.Log -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Divider -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.Stable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment.Companion.CenterHorizontally -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.isGranted -import com.google.accompanist.permissions.rememberPermissionState -import com.halilibo.richtext.markdown.Markdown -import com.halilibo.richtext.ui.RichTextStyle -import com.halilibo.richtext.ui.material3.Material3RichText -import com.halilibo.richtext.ui.resolveDefaults -import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis -import com.patrykandpatrick.vico.compose.axis.vertical.endAxis -import com.patrykandpatrick.vico.compose.axis.vertical.startAxis -import com.patrykandpatrick.vico.compose.chart.Chart -import com.patrykandpatrick.vico.compose.chart.line.lineChart -import com.patrykandpatrick.vico.compose.component.shape.shader.fromBrush -import com.patrykandpatrick.vico.compose.style.ProvideChartStyle -import com.patrykandpatrick.vico.core.DefaultAlpha -import com.patrykandpatrick.vico.core.axis.AxisPosition -import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter -import com.patrykandpatrick.vico.core.chart.composed.plus -import com.patrykandpatrick.vico.core.chart.line.LineChart -import com.patrykandpatrick.vico.core.chart.values.ChartValues -import com.patrykandpatrick.vico.core.component.shape.shader.DynamicShaders -import com.vitorpamplona.amethyst.service.NostrAccountDataSource -import com.vitorpamplona.amethyst.service.notifications.PushDistributorHandler -import com.vitorpamplona.amethyst.ui.navigation.Route -import com.vitorpamplona.amethyst.ui.note.OneGiga -import com.vitorpamplona.amethyst.ui.note.OneKilo -import com.vitorpamplona.amethyst.ui.note.OneMega -import com.vitorpamplona.amethyst.ui.note.UserReactionsRow -import com.vitorpamplona.amethyst.ui.note.UserReactionsViewModel -import com.vitorpamplona.amethyst.ui.note.showCount -import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel -import com.vitorpamplona.amethyst.ui.screen.RefresheableCardView -import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys -import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange -import com.vitorpamplona.amethyst.ui.theme.HalfPadding -import com.vitorpamplona.amethyst.ui.theme.RoyalBlue -import com.vitorpamplona.amethyst.ui.theme.chartStyle -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList -import java.math.BigDecimal -import java.math.RoundingMode -import java.text.DecimalFormat -import kotlin.math.roundToInt - -@Composable -fun NotificationScreen( - notifFeedViewModel: NotificationViewModel, - userReactionsStatsModel: UserReactionsViewModel, - accountViewModel: AccountViewModel, - nav: (String) -> Unit -) { - WatchAccountForNotifications(notifFeedViewModel, accountViewModel) - - CheckifItNeedsToRequestNotificationPermission() - - val lifeCycleOwner = LocalLifecycleOwner.current - DisposableEffect(lifeCycleOwner) { - val observer = LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_RESUME) { - NostrAccountDataSource.invalidateFilters() - } - } - - lifeCycleOwner.lifecycle.addObserver(observer) - onDispose { - lifeCycleOwner.lifecycle.removeObserver(observer) - } - } - val pushHandler = PushDistributorHandler - var distributorPresent by remember { - mutableStateOf(pushHandler.savedDistributorExists()) - } - val list = pushHandler.getInstalledDistributors() - val readableList = pushHandler.formattedDistributorNames() - if (!distributorPresent) { - SelectPushDistributor( - distrbutorList = readableList.toImmutableList(), - onDistributorSelected = { index, name -> - val fullDistributorName = list[index] - pushHandler.saveDistributor(fullDistributorName) - Log.d("Amethyst", "NotificationScreen: Distributor registered.") - }, - onDismiss = { - distributorPresent = true - Log.d("Amethyst", "NotificationScreen: Distributor dialog dismissed.") - } - ) - } else { - val currentDistributor = pushHandler.getSavedDistributor() - pushHandler.saveDistributor(currentDistributor) - } - - Column(Modifier.fillMaxHeight()) { - Column( - modifier = Modifier.padding(vertical = 0.dp) - ) { - SummaryBar( - model = userReactionsStatsModel - ) - - RefresheableCardView( - viewModel = notifFeedViewModel, - accountViewModel = accountViewModel, - nav = nav, - routeForLastRead = Route.Notification.base, - scrollStateKey = ScrollStateKeys.NOTIFICATION_SCREEN - ) - } - } -} - -// TODO: Turn this into an Account flag -var hasAlreadyAskedNotificationPermissions = false - -@OptIn(ExperimentalPermissionsApi::class) -@Composable -fun CheckifItNeedsToRequestNotificationPermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !hasAlreadyAskedNotificationPermissions) { - val notificationPermissionState = rememberPermissionState( - Manifest.permission.POST_NOTIFICATIONS - ) - - if (!notificationPermissionState.status.isGranted) { - hasAlreadyAskedNotificationPermissions = true - - // This will pause the APP, including the connection with relays. - LaunchedEffect(notificationPermissionState) { - notificationPermissionState.launchPermissionRequest() - } - } - } -} - -@Composable -fun WatchAccountForNotifications( - notifFeedViewModel: NotificationViewModel, - accountViewModel: AccountViewModel -) { - val accountState by accountViewModel.accountLiveData.observeAsState() - - LaunchedEffect(accountViewModel, accountState?.account?.defaultNotificationFollowList) { - NostrAccountDataSource.invalidateFilters() - notifFeedViewModel.checkKeysInvalidateDataAndSendToTop() - } -} - -@Composable -fun SelectPushDistributor( - distrbutorList: ImmutableList, - onDistributorSelected: (Int, String) -> Unit, - onDismiss: () -> Unit -) { - Dialog(onDismissRequest = onDismiss) { - Surface( - modifier = Modifier - .border( - width = Dp.Hairline, - color = MaterialTheme.colorScheme.background, - shape = CircleShape - ) - .fillMaxWidth() - ) { - Column( - modifier = Modifier - .background(MaterialTheme.colorScheme.background) - ) { - Box(modifier = Modifier.align(CenterHorizontally)) { - Material3RichText( - style = RichTextStyle().resolveDefaults() - ) { - Markdown(content = "### Select a distributor") - } - } - distrbutorList.forEachIndexed { index, distributor -> - TextButton( - onClick = { - onDistributorSelected(index, distributor) - onDismiss() - }, - modifier = HalfPadding.fillMaxWidth() - ) { - Material3RichText( - style = RichTextStyle().resolveDefaults() - ) { - Markdown(content = distributor) - } - } - } - } - } - } -} - -@Composable -fun SummaryBar(model: UserReactionsViewModel) { - var showChart by remember { - mutableStateOf(false) - } - - UserReactionsRow(model) { - showChart = !showChart - } - - if (showChart) { - val lineChartCount = - lineChart( - lines = listOf(RoyalBlue, Color.Green, Color.Red).map { lineChartColor -> - LineChart.LineSpec( - lineColor = lineChartColor.toArgb(), - lineBackgroundShader = DynamicShaders.fromBrush( - Brush.verticalGradient( - listOf( - lineChartColor.copy(DefaultAlpha.LINE_BACKGROUND_SHADER_START), - lineChartColor.copy(DefaultAlpha.LINE_BACKGROUND_SHADER_END) - ) - ) - ) - ) - }, - targetVerticalAxisPosition = AxisPosition.Vertical.Start - ) - - val lineChartZaps = - lineChart( - lines = listOf(BitcoinOrange).map { lineChartColor -> - LineChart.LineSpec( - lineColor = lineChartColor.toArgb(), - lineBackgroundShader = DynamicShaders.fromBrush( - Brush.verticalGradient( - listOf( - lineChartColor.copy(DefaultAlpha.LINE_BACKGROUND_SHADER_START), - lineChartColor.copy(DefaultAlpha.LINE_BACKGROUND_SHADER_END) - ) - ) - ) - ) - }, - targetVerticalAxisPosition = AxisPosition.Vertical.End - ) - - Row( - modifier = Modifier - .padding(vertical = 10.dp, horizontal = 20.dp) - .clickable(onClick = { showChart = !showChart }) - ) { - ProvideChartStyle( - chartStyle = MaterialTheme.colorScheme.chartStyle - ) { - ObserveAndShowChart(model, lineChartCount, lineChartZaps) - } - } - } - - Divider( - thickness = 0.25.dp - ) -} - -@Composable -private fun ObserveAndShowChart( - model: UserReactionsViewModel, - lineChartCount: LineChart, - lineChartZaps: LineChart -) { - val axisModel by model.axisLabels.collectAsState() - val chartModel by model.chartModel.collectAsState() - chartModel?.let { - Chart( - chart = remember(lineChartCount, lineChartZaps) { - lineChartCount.plus(lineChartZaps) - }, - model = it, - startAxis = startAxis( - valueFormatter = CountAxisValueFormatter() - ), - endAxis = endAxis( - valueFormatter = AmountAxisValueFormatter() - ), - bottomAxis = bottomAxis( - valueFormatter = LabelValueFormatter(axisModel) - ) - ) - } -} - -@Stable -class LabelValueFormatter(val axisLabels: List) : AxisValueFormatter { - override fun formatValue( - value: Float, - chartValues: ChartValues - ): String { - return axisLabels[value.roundToInt()] - } -} - -@Stable -class CountAxisValueFormatter() : AxisValueFormatter { - override fun formatValue( - value: Float, - chartValues: ChartValues - ): String { - return showCount(value.roundToInt()) - } -} - -@Stable -class AmountAxisValueFormatter() : AxisValueFormatter { - override fun formatValue( - value: Float, - chartValues: ChartValues - ): String { - return showAmountAxis(value.toBigDecimal()) - } -} - -var dfG: DecimalFormat = DecimalFormat("#G") -var dfM: DecimalFormat = DecimalFormat("#M") -var dfK: DecimalFormat = DecimalFormat("#k") -var dfN: DecimalFormat = DecimalFormat("#") - -fun showAmountAxis(amount: BigDecimal?): String { - if (amount == null) return "" - if (amount.abs() < BigDecimal(0.01)) return "" - - return when { - amount >= OneGiga -> dfG.format(amount.div(OneGiga).setScale(0, RoundingMode.HALF_UP)) - amount >= OneMega -> dfM.format(amount.div(OneMega).setScale(0, RoundingMode.HALF_UP)) - amount >= OneKilo -> dfK.format(amount.div(OneKilo).setScale(0, RoundingMode.HALF_UP)) - else -> dfN.format(amount) - } -} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 3dd0c08ff..57cba335e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -38,13 +38,13 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomScreenByAuthor import com.vitorpamplona.amethyst.ui.screen.loggedIn.CommunityScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.CustomNotificationScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.DiscoverScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.GeoHashScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.HashtagScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.HiddenUsersScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.HomeScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadRedirectScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.NotificationScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.ProfileScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.SettingsScreen @@ -157,7 +157,7 @@ fun AppNavigation( Route.Notification.let { route -> composable(route.route, route.arguments, content = { - NotificationScreen( + CustomNotificationScreen( notifFeedViewModel = notifFeedViewModel, userReactionsStatsModel = userReactionsStatsModel, accountViewModel = accountViewModel, diff --git a/app/src/play/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt similarity index 98% rename from app/src/play/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt index 6658af233..7f66ef265 100644 --- a/app/src/play/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt @@ -32,12 +32,9 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import com.patrykandpatrick.vico.compose.axis.axisLabelComponent -import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis import com.patrykandpatrick.vico.compose.axis.horizontal.rememberBottomAxis -import com.patrykandpatrick.vico.compose.axis.vertical.endAxis import com.patrykandpatrick.vico.compose.axis.vertical.rememberEndAxis import com.patrykandpatrick.vico.compose.axis.vertical.rememberStartAxis -import com.patrykandpatrick.vico.compose.axis.vertical.startAxis import com.patrykandpatrick.vico.compose.chart.Chart import com.patrykandpatrick.vico.compose.chart.line.lineChart import com.patrykandpatrick.vico.compose.component.shape.shader.fromBrush diff --git a/app/src/play/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/CustomNotificationScreen.kt b/app/src/play/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/CustomNotificationScreen.kt new file mode 100644 index 000000000..fd56ec9ae --- /dev/null +++ b/app/src/play/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/CustomNotificationScreen.kt @@ -0,0 +1,18 @@ +package com.vitorpamplona.amethyst.ui.screen.loggedIn + +import androidx.compose.runtime.Composable +import com.vitorpamplona.amethyst.ui.note.UserReactionsViewModel +import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel + +@Composable +fun CustomNotificationScreen( + notifFeedViewModel: NotificationViewModel, + userReactionsStatsModel: UserReactionsViewModel, + accountViewModel: AccountViewModel, + nav: (String) -> Unit +) = NotificationScreen( + notifFeedViewModel = notifFeedViewModel, + userReactionsStatsModel = userReactionsStatsModel, + accountViewModel = accountViewModel, + nav = nav +) From 6dc82815b6a47e4426e45e972c7d5f3a29e875e4 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Sun, 22 Oct 2023 17:45:44 +0100 Subject: [PATCH 13/13] Cleanup endpoint URL before sending to push server(and storing it locally). --- .../amethyst/service/notifications/PushMessageReceiver.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt index a14b672e9..0263c6fe4 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt @@ -56,9 +56,10 @@ class PushMessageReceiver : MessagingReceiver() { override fun onNewEndpoint(context: Context, endpoint: String, instance: String) { Log.d(TAG, "New endpoint provided:- $endpoint for Instance: $instance") - pushHandler.setEndpoint(endpoint) + val sanitizedEndpoint = endpoint.dropLast(5) + pushHandler.setEndpoint(sanitizedEndpoint) scope.launch(Dispatchers.IO) { - RegisterAccounts(LocalPreferences.allSavedAccounts()).go(endpoint) + RegisterAccounts(LocalPreferences.allSavedAccounts()).go(sanitizedEndpoint) notificationManager().getOrCreateZapChannel(appContext) notificationManager().getOrCreateDMChannel(appContext) }