Move NotificationScreen to different flavors to integrate Distributor dialog selection. Make some adjustments for handling changes to push distributors, and other fixes.

This commit is contained in:
KotlinGeekDev
2023-10-18 17:07:26 +01:00
parent 3f5000f278
commit 904b070306
6 changed files with 437 additions and 18 deletions
@@ -1,5 +1,9 @@
package com.vitorpamplona.amethyst.service.notifications 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 com.vitorpamplona.amethyst.Amethyst
import org.unifiedpush.android.connector.UnifiedPush import org.unifiedpush.android.connector.UnifiedPush
@@ -9,9 +13,9 @@ interface PushDistributorActions {
fun saveDistributor(distributor: String) fun saveDistributor(distributor: String)
fun removeSavedDistributor() fun removeSavedDistributor()
} }
class PushDistributorHandler() : PushDistributorActions { object PushDistributorHandler : PushDistributorActions {
private val appContext = Amethyst.instance.applicationContext private val appContext = Amethyst.instance.applicationContext
private val unifiedPush: UnifiedPush = UnifiedPush() private val unifiedPush: UnifiedPush = UnifiedPush
private var endpointInternal = "" private var endpointInternal = ""
val endpoint = endpointInternal val endpoint = endpointInternal
@@ -19,6 +23,11 @@ class PushDistributorHandler() : PushDistributorActions {
fun getSavedEndpoint() = endpoint fun getSavedEndpoint() = endpoint
fun setEndpoint(newEndpoint: String) { fun setEndpoint(newEndpoint: String) {
endpointInternal = newEndpoint endpointInternal = newEndpoint
Log.d("PushHandler", "New endpoint saved : $endpointInternal")
}
fun removeEndpoint() {
endpointInternal = ""
} }
override fun getSavedDistributor(): String { override fun getSavedDistributor(): String {
@@ -31,13 +40,37 @@ class PushDistributorHandler() : PushDistributorActions {
return unifiedPush.getDistributors(appContext) return unifiedPush.getDistributors(appContext)
} }
fun formattedDistributorNames(): List<String> {
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) { override fun saveDistributor(distributor: String) {
unifiedPush.saveDistributor(appContext, distributor) unifiedPush.saveDistributor(appContext, distributor)
unifiedPush.registerApp(appContext)
} }
override fun removeSavedDistributor() { override fun removeSavedDistributor() {
unifiedPush.safeRemoveDistributor(appContext) unifiedPush.safeRemoveDistributor(appContext)
} }
fun forceRemoveDistributor(context: Context) {
unifiedPush.forceRemoveDistributor(context)
}
} }
fun UnifiedPush() = UnifiedPush
@@ -20,18 +20,23 @@ import kotlinx.coroutines.launch
import org.unifiedpush.android.connector.MessagingReceiver import org.unifiedpush.android.connector.MessagingReceiver
class PushMessageReceiver : MessagingReceiver() { class PushMessageReceiver : MessagingReceiver() {
private val TAG = "Amethyst-OSSPushReceiver"
private val appContext = Amethyst.instance.applicationContext private val appContext = Amethyst.instance.applicationContext
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val eventCache = LruCache<String, String>(100) private val eventCache = LruCache<String, String>(100)
private val pushHandler = PushDistributorHandler() private val pushHandler = PushDistributorHandler
override fun onMessage(context: Context, message: ByteArray, instance: String) { override fun onMessage(context: Context, message: ByteArray, instance: String) {
val messageStr = String(message) 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 { scope.launch {
try {
parseMessage(messageStr)?.let { parseMessage(messageStr)?.let {
receiveIfNew(it) 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) { 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) pushHandler.setEndpoint(endpoint)
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
RegisterAccounts(LocalPreferences.allSavedAccounts()).go(endpoint) RegisterAccounts(LocalPreferences.allSavedAccounts()).go(endpoint)
@@ -62,17 +67,22 @@ class PushMessageReceiver : MessagingReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
val intentData = intent.dataString val intentData = intent.dataString
val intentAction = intent.action.toString() 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) super.onReceive(context, intent)
} }
override fun onRegistrationFailed(context: Context, instance: String) { override fun onRegistrationFailed(context: Context, instance: String) {
Log.d(TAG, "Registration failed for Instance: $instance")
scope.cancel() scope.cancel()
super.onRegistrationFailed(context, instance) pushHandler.forceRemoveDistributor(context)
} }
override fun onUnregistered(context: Context, instance: String) { 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 { fun notificationManager(): NotificationManager {
@@ -2,17 +2,18 @@ package com.vitorpamplona.amethyst.service.notifications
import android.util.Log import android.util.Log
import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.AccountInfo
import kotlinx.coroutines.Dispatchers
object PushNotificationUtils { object PushNotificationUtils {
var hasInit: Boolean = false var hasInit: Boolean = false
private val pushHandler = PushDistributorHandler() private val pushHandler = PushDistributorHandler
suspend fun init(accounts: List<AccountInfo>) = with(Dispatchers.IO) { suspend fun init(accounts: List<AccountInfo>) {
if (hasInit || pushHandler.savedDistributorExists()) { if (hasInit || pushHandler.savedDistributorExists()) {
return@with return
} }
try { try {
if (pushHandler.savedDistributorExists()) {
RegisterAccounts(accounts).go(pushHandler.getSavedEndpoint()) RegisterAccounts(accounts).go(pushHandler.getSavedEndpoint())
}
} catch (e: Exception) { } catch (e: Exception) {
Log.d("Amethyst-OSSPushUtils", "Failed to get endpoint.") Log.d("Amethyst-OSSPushUtils", "Failed to get endpoint.")
} }
@@ -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<String>,
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<String>) : AxisValueFormatter<AxisPosition.Horizontal.Bottom> {
override fun formatValue(
value: Float,
chartValues: ChartValues
): String {
return axisLabels[value.roundToInt()]
}
}
@Stable
class CountAxisValueFormatter() : AxisValueFormatter<AxisPosition.Vertical.Start> {
override fun formatValue(
value: Float,
chartValues: ChartValues
): String {
return showCount(value.roundToInt())
}
}
@Stable
class AmountAxisValueFormatter() : AxisValueFormatter<AxisPosition.Vertical.End> {
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)
}
}
@@ -62,7 +62,12 @@ class RegisterAccounts(
it.isSuccessful it.isSuccessful
} }
} catch (e: java.lang.Exception) { } 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)
} }
} }