Background persistence (#505)

* persistence step 1

* fix build

* messages in the background work, notifications not yet

* app state store

* DM icon shows up

* notification launches when app is closed!

* keep ui updated

* lifecycle fixes

* extensive logging, maybe revert later

* send nickname in announcement

* quit in notification

* setting in about sheet

* fix quit bitchat

* lifecycle fixes

* power mode based on background state

* stats for both direciotns

* fix graph persistence

* better counting

* count per device

* only compute when debug sheet is open? untested

* fix read receipts

* fix read receipts fully

* fix unread badge if messages have been read in focus

* foreground promotion fix

* fix app kill in notification

* adjust to new tor

* nice

* about sheet design
This commit is contained in:
callebtc
2025-12-13 16:43:39 +07:00
committed by GitHub
parent e96330e50b
commit 3f8c236a72
24 changed files with 2079 additions and 575 deletions
@@ -0,0 +1,109 @@
package com.bitchat.android.services
import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.DeliveryStatus
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* Process-wide in-memory state store that survives Activity recreation.
* The foreground Mesh service updates this store; UI subscribes/hydrates from it.
*/
object AppStateStore {
// Global de-dup set by message id to avoid duplicate keys in Compose lists
private val seenMessageIds = mutableSetOf<String>()
// Connected peer IDs (mesh ephemeral IDs)
private val _peers = MutableStateFlow<List<String>>(emptyList())
val peers: StateFlow<List<String>> = _peers.asStateFlow()
// Public mesh timeline messages (non-channel)
private val _publicMessages = MutableStateFlow<List<BitchatMessage>>(emptyList())
val publicMessages: StateFlow<List<BitchatMessage>> = _publicMessages.asStateFlow()
// Private messages by peerID
private val _privateMessages = MutableStateFlow<Map<String, List<BitchatMessage>>>(emptyMap())
val privateMessages: StateFlow<Map<String, List<BitchatMessage>>> = _privateMessages.asStateFlow()
// Channel messages by channel name
private val _channelMessages = MutableStateFlow<Map<String, List<BitchatMessage>>>(emptyMap())
val channelMessages: StateFlow<Map<String, List<BitchatMessage>>> = _channelMessages.asStateFlow()
fun setPeers(ids: List<String>) {
_peers.value = ids
}
fun addPublicMessage(msg: BitchatMessage) {
synchronized(this) {
if (seenMessageIds.contains(msg.id)) return
seenMessageIds.add(msg.id)
_publicMessages.value = _publicMessages.value + msg
}
}
fun addPrivateMessage(peerID: String, msg: BitchatMessage) {
synchronized(this) {
if (seenMessageIds.contains(msg.id)) return
seenMessageIds.add(msg.id)
val map = _privateMessages.value.toMutableMap()
val list = (map[peerID] ?: emptyList()) + msg
map[peerID] = list
_privateMessages.value = map
}
}
private fun statusPriority(status: DeliveryStatus?): Int = when (status) {
null -> 0
is DeliveryStatus.Sending -> 1
is DeliveryStatus.Sent -> 2
is DeliveryStatus.PartiallyDelivered -> 3
is DeliveryStatus.Delivered -> 4
is DeliveryStatus.Read -> 5
is DeliveryStatus.Failed -> 0
}
fun updatePrivateMessageStatus(messageID: String, status: DeliveryStatus) {
synchronized(this) {
val map = _privateMessages.value.toMutableMap()
var changed = false
map.keys.toList().forEach { peer ->
val list = map[peer]?.toMutableList() ?: mutableListOf()
val idx = list.indexOfFirst { it.id == messageID }
if (idx >= 0) {
val current = list[idx].deliveryStatus
// Do not downgrade (e.g., Read -> Delivered)
if (statusPriority(status) >= statusPriority(current)) {
list[idx] = list[idx].copy(deliveryStatus = status)
map[peer] = list
changed = true
}
}
}
if (changed) {
_privateMessages.value = map
}
}
}
fun addChannelMessage(channel: String, msg: BitchatMessage) {
synchronized(this) {
if (seenMessageIds.contains(msg.id)) return
seenMessageIds.add(msg.id)
val map = _channelMessages.value.toMutableMap()
val list = (map[channel] ?: emptyList()) + msg
map[channel] = list
_channelMessages.value = map
}
}
// Clear all in-memory state (used for full app shutdown)
fun clear() {
synchronized(this) {
seenMessageIds.clear()
_peers.value = emptyList()
_publicMessages.value = emptyList()
_privateMessages.value = emptyMap()
_channelMessages.value = emptyMap()
}
}
}
@@ -0,0 +1,21 @@
package com.bitchat.android.services
import android.content.Context
import com.bitchat.android.ui.DataManager
/**
* Provides current user's nickname for announcements and leave messages.
* If no nickname saved, falls back to the provided peerID.
*/
object NicknameProvider {
fun getNickname(context: Context, myPeerID: String): String {
return try {
val dm = DataManager(context.applicationContext)
val nick = dm.loadNickname()
if (nick.isNullOrBlank()) myPeerID else nick
} catch (_: Exception) {
myPeerID
}
}
}