feat: implement notification system for list events, including added, edited, and removed notifications with user preferences and background checks
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<!-- Android 13+ runtime notification permission -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application
|
||||
android:name=".CollabTableApplication"
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
package com.collabtable.app
|
||||
|
||||
import android.app.Application
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import com.collabtable.app.data.api.ApiClient
|
||||
import com.collabtable.app.data.repository.SyncRepository
|
||||
import com.collabtable.app.notifications.NotificationHelper
|
||||
import com.collabtable.app.work.ListChangeNotificationWorker
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -23,5 +35,40 @@ class CollabTableApplication : Application() {
|
||||
// Silent; UI layers will surface subsequent sync issues.
|
||||
}
|
||||
}
|
||||
|
||||
// Create notification channel(s) for list events
|
||||
createNotificationChannels()
|
||||
|
||||
// Schedule periodic background check for list changes to surface notifications
|
||||
scheduleListChangeWorker()
|
||||
|
||||
// Auto-clear notifications when app comes to foreground
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
NotificationHelper.clearListEventNotifications(this@CollabTableApplication)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun createNotificationChannels() {
|
||||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val listEventsChannel = NotificationChannel(
|
||||
NotificationHelper.CHANNEL_LIST_EVENTS,
|
||||
"List Events",
|
||||
NotificationManager.IMPORTANCE_DEFAULT,
|
||||
).apply {
|
||||
description = "Notifications for list add, edit, and remove events"
|
||||
}
|
||||
manager.createNotificationChannel(listEventsChannel)
|
||||
}
|
||||
|
||||
private fun scheduleListChangeWorker() {
|
||||
val work = PeriodicWorkRequestBuilder<ListChangeNotificationWorker>(15, TimeUnit.MINUTES)
|
||||
.build()
|
||||
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
|
||||
"list_change_notifier",
|
||||
ExistingPeriodicWorkPolicy.UPDATE,
|
||||
work,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+51
@@ -30,6 +30,16 @@ class PreferencesManager(context: Context) {
|
||||
private val _syncPollIntervalMs = MutableStateFlow(getSyncPollIntervalMs())
|
||||
val syncPollIntervalMs: StateFlow<Long> = _syncPollIntervalMs.asStateFlow()
|
||||
|
||||
// Notification preferences (per list event type)
|
||||
private val _notifyListAdded = MutableStateFlow(isNotifyListAddedEnabled())
|
||||
val notifyListAdded: StateFlow<Boolean> = _notifyListAdded.asStateFlow()
|
||||
|
||||
private val _notifyListEdited = MutableStateFlow(isNotifyListEditedEnabled())
|
||||
val notifyListEdited: StateFlow<Boolean> = _notifyListEdited.asStateFlow()
|
||||
|
||||
private val _notifyListRemoved = MutableStateFlow(isNotifyListRemovedEnabled())
|
||||
val notifyListRemoved: StateFlow<Boolean> = _notifyListRemoved.asStateFlow()
|
||||
|
||||
fun getServerUrl(): String {
|
||||
return prefs.getString(KEY_SERVER_URL, DEFAULT_SERVER_URL) ?: DEFAULT_SERVER_URL
|
||||
}
|
||||
@@ -141,6 +151,43 @@ class PreferencesManager(context: Context) {
|
||||
_syncPollIntervalMs.value = clamped
|
||||
}
|
||||
|
||||
// Last time we checked lists for background notifications
|
||||
fun getLastListNotifyCheckTimestamp(): Long {
|
||||
return prefs.getLong(KEY_LAST_LIST_NOTIFY_CHECK_TS, 0L)
|
||||
}
|
||||
|
||||
fun setLastListNotifyCheckTimestamp(ts: Long) {
|
||||
prefs.edit().putLong(KEY_LAST_LIST_NOTIFY_CHECK_TS, ts).apply()
|
||||
}
|
||||
|
||||
// Notification settings accessors
|
||||
private fun isNotifyListAddedEnabled(): Boolean {
|
||||
return prefs.getBoolean(KEY_NOTIFY_LIST_ADDED, true)
|
||||
}
|
||||
|
||||
fun setNotifyListAddedEnabled(enabled: Boolean) {
|
||||
prefs.edit().putBoolean(KEY_NOTIFY_LIST_ADDED, enabled).apply()
|
||||
_notifyListAdded.value = enabled
|
||||
}
|
||||
|
||||
private fun isNotifyListEditedEnabled(): Boolean {
|
||||
return prefs.getBoolean(KEY_NOTIFY_LIST_EDITED, true)
|
||||
}
|
||||
|
||||
fun setNotifyListEditedEnabled(enabled: Boolean) {
|
||||
prefs.edit().putBoolean(KEY_NOTIFY_LIST_EDITED, enabled).apply()
|
||||
_notifyListEdited.value = enabled
|
||||
}
|
||||
|
||||
private fun isNotifyListRemovedEnabled(): Boolean {
|
||||
return prefs.getBoolean(KEY_NOTIFY_LIST_REMOVED, true)
|
||||
}
|
||||
|
||||
fun setNotifyListRemovedEnabled(enabled: Boolean) {
|
||||
prefs.edit().putBoolean(KEY_NOTIFY_LIST_REMOVED, enabled).apply()
|
||||
_notifyListRemoved.value = enabled
|
||||
}
|
||||
|
||||
// Persist per-list column widths (fieldId -> widthDp)
|
||||
// Stored as a JSON object string in SharedPreferences under key: COLUMN_WIDTHS_PREFIX + listId
|
||||
fun getColumnWidths(listId: String): Map<String, Float> {
|
||||
@@ -183,6 +230,10 @@ class PreferencesManager(context: Context) {
|
||||
private const val KEY_AMOLED_DARK = "amoled_dark"
|
||||
private const val KEY_SORT_ORDER = "sort_order"
|
||||
private const val KEY_SYNC_POLL_INTERVAL_MS = "sync_poll_interval_ms"
|
||||
private const val KEY_NOTIFY_LIST_ADDED = "notify_list_added"
|
||||
private const val KEY_NOTIFY_LIST_EDITED = "notify_list_edited"
|
||||
private const val KEY_NOTIFY_LIST_REMOVED = "notify_list_removed"
|
||||
private const val KEY_LAST_LIST_NOTIFY_CHECK_TS = "last_list_notify_check_ts"
|
||||
private const val COLUMN_WIDTHS_PREFIX = "column_widths_" // + listId
|
||||
private const val DEFAULT_SERVER_URL = "http://10.0.2.2:3000/api/"
|
||||
private const val DEFAULT_SYNC_POLL_INTERVAL_MS = 250L
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package com.collabtable.app.notifications
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.collabtable.app.R
|
||||
|
||||
object NotificationHelper {
|
||||
const val CHANNEL_LIST_EVENTS = "list_events"
|
||||
private const val GROUP_LIST_EVENTS = "group_list_events"
|
||||
private const val SUMMARY_ID = 1000
|
||||
|
||||
// Keep a small rolling buffer of recent event lines for the summary
|
||||
private val recentEventLines = ArrayDeque<String>()
|
||||
@Volatile private var totalEventCount: Int = 0
|
||||
private val postedIds = mutableSetOf<Int>()
|
||||
|
||||
private fun builder(context: Context, title: String, text: String): NotificationCompat.Builder {
|
||||
return NotificationCompat.Builder(context, CHANNEL_LIST_EVENTS)
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.setContentTitle(title)
|
||||
.setContentText(text)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setAutoCancel(true)
|
||||
}
|
||||
|
||||
private fun notify(context: Context, id: Int, builder: NotificationCompat.Builder) {
|
||||
with(NotificationManagerCompat.from(context)) {
|
||||
notify(id, builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
fun showListAdded(context: Context, listId: String, name: String) = showEvent(context, listId, "add", "List added", "\"$name\" was created")
|
||||
fun showListEdited(context: Context, listId: String, name: String) = showEvent(context, listId, "edit", "List edited", "\"$name\" was renamed/updated")
|
||||
fun showListRemoved(context: Context, listId: String, name: String) = showEvent(context, listId, "remove", "List removed", "\"$name\" was deleted")
|
||||
|
||||
private fun showEvent(context: Context, listId: String, type: String, title: String, text: String) {
|
||||
val b = builder(context, title, text).setGroup(GROUP_LIST_EVENTS)
|
||||
val id = (type + listId).hashCode()
|
||||
synchronized(postedIds) { postedIds.add(id) }
|
||||
notify(context, id, b)
|
||||
recordEventLine(type, text)
|
||||
postSummary(context)
|
||||
}
|
||||
|
||||
private fun postSummary(context: Context) {
|
||||
val inbox = NotificationCompat.InboxStyle()
|
||||
synchronized(recentEventLines) {
|
||||
recentEventLines.take(5).forEach { line -> inbox.addLine(line) }
|
||||
inbox.setSummaryText("${totalEventCount} total event" + if (totalEventCount == 1) "" else "s")
|
||||
}
|
||||
val summary = NotificationCompat.Builder(context, CHANNEL_LIST_EVENTS)
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.setContentTitle("List activity")
|
||||
.setContentText("Recent list changes")
|
||||
.setStyle(inbox)
|
||||
.setGroup(GROUP_LIST_EVENTS)
|
||||
.setGroupSummary(true)
|
||||
.setAutoCancel(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setNumber(totalEventCount)
|
||||
notify(context, SUMMARY_ID, summary)
|
||||
}
|
||||
|
||||
private fun recordEventLine(type: String, text: String) {
|
||||
val line = when (type) {
|
||||
"add" -> "Added • $text"
|
||||
"edit" -> "Edited • $text"
|
||||
"remove" -> "Removed • $text"
|
||||
else -> text
|
||||
}
|
||||
synchronized(recentEventLines) {
|
||||
recentEventLines.addFirst(line)
|
||||
// Keep only the last 8 lines to bound memory and UI clutter
|
||||
while (recentEventLines.size > 8) recentEventLines.removeLast()
|
||||
}
|
||||
totalEventCount += 1
|
||||
}
|
||||
|
||||
fun clearListEventNotifications(context: Context) {
|
||||
val mgr = NotificationManagerCompat.from(context)
|
||||
synchronized(postedIds) {
|
||||
postedIds.forEach { id -> mgr.cancel(id) }
|
||||
postedIds.clear()
|
||||
}
|
||||
mgr.cancel(SUMMARY_ID)
|
||||
synchronized(recentEventLines) {
|
||||
recentEventLines.clear()
|
||||
totalEventCount = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,9 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.collabtable.app.data.database.CollabTableDatabase
|
||||
import com.collabtable.app.data.model.CollabList
|
||||
import com.collabtable.app.data.repository.SyncRepository
|
||||
import com.collabtable.app.notifications.NotificationHelper
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import com.collabtable.app.utils.Logger
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -128,6 +131,7 @@ class ListsViewModel(
|
||||
database.listDao().insertList(newList)
|
||||
// Sync immediately after creating
|
||||
performSync()
|
||||
maybeNotifyListAdded(newList)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +151,7 @@ class ListsViewModel(
|
||||
)
|
||||
// Sync immediately after renaming
|
||||
performSync()
|
||||
maybeNotifyListEdited(list.copy(name = newName.trim()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,6 +169,7 @@ class ListsViewModel(
|
||||
database.itemDao().softDeleteItemsByList(listId, ts)
|
||||
// Sync immediately after deleting
|
||||
performSync()
|
||||
maybeNotifyListRemoved(list)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,4 +182,30 @@ class ListsViewModel(
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun isInForeground(): Boolean {
|
||||
val state = ProcessLifecycleOwner.get().lifecycle.currentState
|
||||
return state.isAtLeast(Lifecycle.State.STARTED)
|
||||
}
|
||||
|
||||
private fun maybeNotifyListAdded(list: CollabList) {
|
||||
val prefs = com.collabtable.app.data.preferences.PreferencesManager.getInstance(context)
|
||||
if (prefs.notifyListAdded.value && !isInForeground()) {
|
||||
NotificationHelper.showListAdded(context, list.id, list.name)
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeNotifyListEdited(list: CollabList) {
|
||||
val prefs = com.collabtable.app.data.preferences.PreferencesManager.getInstance(context)
|
||||
if (prefs.notifyListEdited.value && !isInForeground()) {
|
||||
NotificationHelper.showListEdited(context, list.id, list.name)
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeNotifyListRemoved(list: CollabList) {
|
||||
val prefs = com.collabtable.app.data.preferences.PreferencesManager.getInstance(context)
|
||||
if (prefs.notifyListRemoved.value && !isInForeground()) {
|
||||
NotificationHelper.showListRemoved(context, list.id, list.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
-1
@@ -41,7 +41,10 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import com.collabtable.app.data.preferences.PreferencesManager
|
||||
import android.os.Build
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -57,9 +60,26 @@ fun ServerSetupScreen(onSetupComplete: () -> Unit) {
|
||||
val validationResult by viewModel.validationResult.collectAsState()
|
||||
val validationError by viewModel.validationError.collectAsState()
|
||||
|
||||
val permissionLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||
// If granted, enable all; else disable all
|
||||
preferencesManager.setNotifyListAddedEnabled(granted)
|
||||
preferencesManager.setNotifyListEditedEnabled(granted)
|
||||
preferencesManager.setNotifyListRemovedEnabled(granted)
|
||||
onSetupComplete()
|
||||
}
|
||||
|
||||
LaunchedEffect(validationResult) {
|
||||
if (validationResult == true) {
|
||||
onSetupComplete()
|
||||
// On Android 13+ prompt for notifications; otherwise default to enabled
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
permissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
|
||||
} else {
|
||||
preferencesManager.setNotifyListAddedEnabled(true)
|
||||
preferencesManager.setNotifyListEditedEnabled(true)
|
||||
preferencesManager.setNotifyListRemovedEnabled(true)
|
||||
onSetupComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+153
@@ -29,6 +29,8 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -36,6 +38,8 @@ import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -47,6 +51,10 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.collabtable.app.R
|
||||
import com.collabtable.app.data.api.ApiClient
|
||||
import com.collabtable.app.data.database.CollabTableDatabase
|
||||
@@ -70,11 +78,15 @@ fun SettingsScreen(
|
||||
val preferencesManager = remember { PreferencesManager.getInstance(context) }
|
||||
val syncRepository = remember { SyncRepository(context) }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val serverUrl = preferencesManager.getServerUrl()
|
||||
val themeMode by preferencesManager.themeMode.collectAsState()
|
||||
val dynamicColor by preferencesManager.dynamicColor.collectAsState()
|
||||
val amoledDark by preferencesManager.amoledDark.collectAsState()
|
||||
val notifyListAdded by preferencesManager.notifyListAdded.collectAsState()
|
||||
val notifyListEdited by preferencesManager.notifyListEdited.collectAsState()
|
||||
val notifyListRemoved by preferencesManager.notifyListRemoved.collectAsState()
|
||||
val displayUrl = remember(serverUrl) { formatServerUrlForDisplay(serverUrl) }
|
||||
var showLeaveDialog by remember { mutableStateOf(false) }
|
||||
var isLeaving by remember { mutableStateOf(false) }
|
||||
@@ -83,6 +95,38 @@ fun SettingsScreen(
|
||||
var syncIntervalError by remember { mutableStateOf<String?>(null) }
|
||||
// Authentication UI removed
|
||||
|
||||
// Permission launcher for Android 13+ notification permission
|
||||
var pendingPermissionAction by remember { mutableStateOf<(() -> Unit)?>(null) }
|
||||
val permissionLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||
if (granted) {
|
||||
pendingPermissionAction?.invoke()
|
||||
} else {
|
||||
coroutineScope.launch {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = "Notification permission denied. Enable it in system settings to turn this on.",
|
||||
)
|
||||
}
|
||||
}
|
||||
pendingPermissionAction = null
|
||||
}
|
||||
|
||||
fun ensureNotificationPermission(onGranted: () -> Unit) {
|
||||
if (Build.VERSION.SDK_INT < 33) {
|
||||
onGranted()
|
||||
return
|
||||
}
|
||||
val granted =
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
if (granted) {
|
||||
onGranted()
|
||||
} else {
|
||||
pendingPermissionAction = onGranted
|
||||
permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
}
|
||||
|
||||
// Removed test connectivity logic; the connection indicator is shown via ConnectionStatusAction
|
||||
|
||||
Scaffold(
|
||||
@@ -104,6 +148,7 @@ fun SettingsScreen(
|
||||
),
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier =
|
||||
@@ -372,6 +417,114 @@ fun SettingsScreen(
|
||||
|
||||
}
|
||||
|
||||
// Notifications section (placed after main content but before dialogs)
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(padding)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Notifications",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text("List added")
|
||||
Text(
|
||||
text = "Notify when a new list/table is created",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = notifyListAdded,
|
||||
onCheckedChange = { checked ->
|
||||
if (checked) {
|
||||
ensureNotificationPermission {
|
||||
preferencesManager.setNotifyListAddedEnabled(true)
|
||||
}
|
||||
} else {
|
||||
preferencesManager.setNotifyListAddedEnabled(false)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text("List edited")
|
||||
Text(
|
||||
text = "Notify when a list/table name changes",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = notifyListEdited,
|
||||
onCheckedChange = { checked ->
|
||||
if (checked) {
|
||||
ensureNotificationPermission {
|
||||
preferencesManager.setNotifyListEditedEnabled(true)
|
||||
}
|
||||
} else {
|
||||
preferencesManager.setNotifyListEditedEnabled(false)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text("List removed")
|
||||
Text(
|
||||
text = "Notify when a list/table is deleted",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = notifyListRemoved,
|
||||
onCheckedChange = { checked ->
|
||||
if (checked) {
|
||||
ensureNotificationPermission {
|
||||
preferencesManager.setNotifyListRemovedEnabled(true)
|
||||
}
|
||||
} else {
|
||||
preferencesManager.setNotifyListRemovedEnabled(false)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = "Notifications fire only when the app is in background or not visible.",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showLeaveDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showLeaveDialog = false },
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package com.collabtable.app.work
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.collabtable.app.data.database.CollabTableDatabase
|
||||
import com.collabtable.app.data.model.CollabList
|
||||
import com.collabtable.app.data.preferences.PreferencesManager
|
||||
import com.collabtable.app.notifications.NotificationHelper
|
||||
|
||||
class ListChangeNotificationWorker(
|
||||
appContext: Context,
|
||||
workerParams: WorkerParameters,
|
||||
) : CoroutineWorker(appContext, workerParams) {
|
||||
override suspend fun doWork(): Result {
|
||||
val prefs = PreferencesManager.getInstance(applicationContext)
|
||||
val db = CollabTableDatabase.getDatabase(applicationContext)
|
||||
|
||||
val since = prefs.getLastListNotifyCheckTimestamp()
|
||||
val now = System.currentTimeMillis()
|
||||
return try {
|
||||
val changed = db.listDao().getListsUpdatedSince(since)
|
||||
if (changed.isNotEmpty()) {
|
||||
changed.forEach { list ->
|
||||
emitNotificationsFor(list, prefs)
|
||||
}
|
||||
}
|
||||
// Update checkpoint regardless to avoid duplicate spam
|
||||
prefs.setLastListNotifyCheckTimestamp(now)
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
Result.retry()
|
||||
}
|
||||
}
|
||||
|
||||
private fun emitNotificationsFor(list: CollabList, prefs: PreferencesManager) {
|
||||
// Determine event type
|
||||
when {
|
||||
list.isDeleted -> {
|
||||
if (prefs.notifyListRemoved.value) {
|
||||
NotificationHelper.showListRemoved(applicationContext, list.id, list.name)
|
||||
}
|
||||
}
|
||||
list.createdAt == list.updatedAt -> {
|
||||
if (prefs.notifyListAdded.value) {
|
||||
NotificationHelper.showListAdded(applicationContext, list.id, list.name)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
if (prefs.notifyListEdited.value) {
|
||||
NotificationHelper.showListEdited(applicationContext, list.id, list.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user