feat: add notification for list content updates and manage preferences in SettingsScreen

This commit is contained in:
2025-11-11 23:34:44 +01:00
parent 8241e62132
commit d6a3abab1a
6 changed files with 71 additions and 4 deletions
@@ -42,6 +42,9 @@ class PreferencesManager(
private val _notifyListRemoved = MutableStateFlow(isNotifyListRemovedEnabled()) private val _notifyListRemoved = MutableStateFlow(isNotifyListRemovedEnabled())
val notifyListRemoved: StateFlow<Boolean> = _notifyListRemoved.asStateFlow() val notifyListRemoved: StateFlow<Boolean> = _notifyListRemoved.asStateFlow()
private val _notifyListContentUpdated = MutableStateFlow(isNotifyListContentUpdatedEnabled())
val notifyListContentUpdated: StateFlow<Boolean> = _notifyListContentUpdated.asStateFlow()
fun getServerUrl(): String = prefs.getString(KEY_SERVER_URL, DEFAULT_SERVER_URL) ?: DEFAULT_SERVER_URL fun getServerUrl(): String = prefs.getString(KEY_SERVER_URL, DEFAULT_SERVER_URL) ?: DEFAULT_SERVER_URL
fun setServerUrl(url: String) { fun setServerUrl(url: String) {
@@ -169,6 +172,13 @@ class PreferencesManager(
_notifyListRemoved.value = enabled _notifyListRemoved.value = enabled
} }
private fun isNotifyListContentUpdatedEnabled(): Boolean = prefs.getBoolean(KEY_NOTIFY_LIST_CONTENT_UPDATED, true)
fun setNotifyListContentUpdatedEnabled(enabled: Boolean) {
prefs.edit().putBoolean(KEY_NOTIFY_LIST_CONTENT_UPDATED, enabled).apply()
_notifyListContentUpdated.value = enabled
}
// Persist per-list column widths (fieldId -> widthDp) // Persist per-list column widths (fieldId -> widthDp)
// Stored as a JSON object string in SharedPreferences under key: COLUMN_WIDTHS_PREFIX + listId // Stored as a JSON object string in SharedPreferences under key: COLUMN_WIDTHS_PREFIX + listId
fun getColumnWidths(listId: String): Map<String, Float> { fun getColumnWidths(listId: String): Map<String, Float> {
@@ -261,6 +271,7 @@ class PreferencesManager(
private const val KEY_NOTIFY_LIST_ADDED = "notify_list_added" 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_EDITED = "notify_list_edited"
private const val KEY_NOTIFY_LIST_REMOVED = "notify_list_removed" private const val KEY_NOTIFY_LIST_REMOVED = "notify_list_removed"
private const val KEY_NOTIFY_LIST_CONTENT_UPDATED = "notify_list_content_updated"
private const val KEY_LAST_LIST_NOTIFY_CHECK_TS = "last_list_notify_check_ts" 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 COLUMN_WIDTHS_PREFIX = "column_widths_" // + listId
private const val COLUMN_ALIGN_PREFIX = "column_align_" // + listId private const val COLUMN_ALIGN_PREFIX = "column_align_" // + listId
@@ -57,6 +57,12 @@ object NotificationHelper {
name: String, name: String,
) = showEvent(context, listId, "remove", "List removed", "\"$name\" was deleted") ) = showEvent(context, listId, "remove", "List removed", "\"$name\" was deleted")
fun showListContentUpdated(
context: Context,
listId: String,
name: String,
) = showEvent(context, listId, "content", "List updated", "\"$name\" content changed")
private fun showEvent( private fun showEvent(
context: Context, context: Context,
listId: String, listId: String,
@@ -102,6 +108,7 @@ object NotificationHelper {
"add" -> "Added • $text" "add" -> "Added • $text"
"edit" -> "Edited • $text" "edit" -> "Edited • $text"
"remove" -> "Removed • $text" "remove" -> "Removed • $text"
"content" -> "Updated • $text"
else -> text else -> text
} }
synchronized(recentEventLines) { synchronized(recentEventLines) {
@@ -563,8 +563,8 @@ fun ListDetailScreen(
} }
} }
// Loading overlay during initial content load // Loading overlay only while nothing is available to render yet
if (isLoading) { if (isLoading && stableFields.isEmpty() && stableItems.isEmpty()) {
Box( Box(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
@@ -3,6 +3,8 @@ package com.collabtable.app.ui.screens
import android.content.Context import android.content.Context
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.room.withTransaction import androidx.room.withTransaction
import com.collabtable.app.data.database.CollabTableDatabase import com.collabtable.app.data.database.CollabTableDatabase
import com.collabtable.app.data.model.CollabList import com.collabtable.app.data.model.CollabList
@@ -120,6 +122,19 @@ class ListDetailViewModel(
syncRepository.performSync() syncRepository.performSync()
} }
private fun isInForeground(): Boolean {
val state = ProcessLifecycleOwner.get().lifecycle.currentState
return state.isAtLeast(Lifecycle.State.STARTED)
}
private fun maybeNotifyListContentUpdated() {
val prefs = com.collabtable.app.data.preferences.PreferencesManager.getInstance(context)
if (prefs.notifyListContentUpdated.value && !isInForeground()) {
val name = _list.value?.name ?: "Table"
com.collabtable.app.notifications.NotificationHelper.showListContentUpdated(context, listId, name)
}
}
fun renameList(newName: String) { fun renameList(newName: String) {
viewModelScope.launch { viewModelScope.launch {
val currentList = _list.value val currentList = _list.value
@@ -184,6 +199,7 @@ class ListDetailViewModel(
} }
performSync() performSync()
maybeNotifyListContentUpdated()
} }
} }
@@ -197,6 +213,7 @@ class ListDetailViewModel(
} }
} }
performSync() performSync()
maybeNotifyListContentUpdated()
} }
} }
@@ -224,6 +241,7 @@ class ListDetailViewModel(
} }
} }
performSync() performSync()
maybeNotifyListContentUpdated()
} }
} }
} }
@@ -260,6 +278,7 @@ class ListDetailViewModel(
} }
} }
performSync() performSync()
maybeNotifyListContentUpdated()
} }
} }
@@ -295,6 +314,7 @@ class ListDetailViewModel(
} }
} }
performSync() performSync()
maybeNotifyListContentUpdated()
} }
} }
@@ -332,6 +352,7 @@ class ListDetailViewModel(
} }
} }
performSync() performSync()
maybeNotifyListContentUpdated()
} }
} }
@@ -84,6 +84,7 @@ fun SettingsScreen(
val notifyListAdded by preferencesManager.notifyListAdded.collectAsState() val notifyListAdded by preferencesManager.notifyListAdded.collectAsState()
val notifyListEdited by preferencesManager.notifyListEdited.collectAsState() val notifyListEdited by preferencesManager.notifyListEdited.collectAsState()
val notifyListRemoved by preferencesManager.notifyListRemoved.collectAsState() val notifyListRemoved by preferencesManager.notifyListRemoved.collectAsState()
val notifyListContentUpdated by preferencesManager.notifyListContentUpdated.collectAsState()
val displayUrl = remember(serverUrl) { formatServerUrlForDisplay(serverUrl) } val displayUrl = remember(serverUrl) { formatServerUrlForDisplay(serverUrl) }
var showLeaveDialog by remember { mutableStateOf(false) } var showLeaveDialog by remember { mutableStateOf(false) }
var isLeaving by remember { mutableStateOf(false) } var isLeaving by remember { mutableStateOf(false) }
@@ -379,6 +380,32 @@ fun SettingsScreen(
}, },
) )
} }
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Column(modifier = Modifier.weight(1f)) {
Text("List content updated")
Text(
text = "Notify when fields or items change (add/edit/delete)",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Switch(
checked = notifyListContentUpdated,
onCheckedChange = { checked ->
if (checked) {
ensureNotificationPermission {
preferencesManager.setNotifyListContentUpdatedEnabled(true)
}
} else {
preferencesManager.setNotifyListContentUpdatedEnabled(false)
}
},
)
}
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
@@ -50,8 +50,9 @@ class ListChangeNotificationWorker(
} }
} }
else -> { else -> {
if (prefs.notifyListEdited.value) { // Background diff cannot distinguish rename vs content. Treat as content update here.
NotificationHelper.showListEdited(applicationContext, list.id, list.name) if (prefs.notifyListContentUpdated.value) {
NotificationHelper.showListContentUpdated(applicationContext, list.id, list.name)
} }
} }
} }