calls badge clears on view; outbox tests track the directory-first send
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
7727cfa432
commit
31548674de
@@ -533,8 +533,10 @@ private fun BottomNav(
|
||||
KitIcon.Chats, KitIcon.ChatsActive, "Messages", phase.unread.size, "nav-messages")
|
||||
NavButton(model, phase, MessengerViewModel.Phase.Screen.CALLS,
|
||||
KitIcon.Call, KitIcon.Call, "Calls",
|
||||
// Missed calls are worth a badge; answered ones are not news.
|
||||
phase.calls.count { !it.connected && it.incoming }, "nav-calls")
|
||||
// Missed calls SINCE the tab was last opened — it used to count
|
||||
// every missed call in the log for ever, a badge nothing could
|
||||
// clear. Opening the tab is what "seeing" them means.
|
||||
model.missedCallsUnseen(phase), "nav-calls")
|
||||
NavButton(model, phase, MessengerViewModel.Phase.Screen.STORIES,
|
||||
KitIcon.Stories, KitIcon.Stories, "Stories", 0, "nav-stories")
|
||||
// Alerts left the bar 2026-08-28 — five tabs plus it was crowded, and
|
||||
|
||||
@@ -709,7 +709,11 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) {
|
||||
|
||||
fun closeChat() = update { it.copy(openChat = null) }
|
||||
|
||||
fun showScreen(screen: Phase.Screen) = update { it.copy(screen = screen, error = null) }
|
||||
fun showScreen(screen: Phase.Screen) {
|
||||
// Opening the Calls tab is what clears its missed badge.
|
||||
if (screen == Phase.Screen.CALLS) markCallsSeen()
|
||||
update { it.copy(screen = screen, error = null) }
|
||||
}
|
||||
|
||||
// -- Contacts: the rest of the CRUD --------------------------------------
|
||||
|
||||
@@ -1109,6 +1113,19 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) {
|
||||
/** Remembered across the privacy prompt, so "Understood, call" keeps it. */
|
||||
private var pendingCallVideo = false
|
||||
|
||||
/** Missed incoming calls newer than the last time the Calls tab was open. */
|
||||
fun missedCallsUnseen(phase: Phase.SignedIn): Int {
|
||||
val seenAt = callPrefs().getLong(CALLS_SEEN_AT, 0L)
|
||||
return phase.calls.count { !it.connected && it.incoming && it.at > seenAt }
|
||||
}
|
||||
|
||||
/** The Calls tab was opened: everything currently missed counts as seen. */
|
||||
fun markCallsSeen() {
|
||||
callPrefs().edit().putLong(CALLS_SEEN_AT, System.currentTimeMillis()).apply()
|
||||
// Nudge the state so the badge re-reads the pref.
|
||||
update { it.copy() }
|
||||
}
|
||||
|
||||
fun requestCall(peerId: Long, video: Boolean = false) {
|
||||
pendingCallVideo = video
|
||||
if (callPrefs().getBoolean(CALL_PRIVACY_ACKNOWLEDGED, false)) placeCall(peerId, video)
|
||||
@@ -1656,6 +1673,7 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) {
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val CALLS_SEEN_AT = "calls_seen_at"
|
||||
const val POLL_INTERVAL_MS = 5_000L
|
||||
const val CALL_SETUP_POLL_MS = 1_000L
|
||||
}
|
||||
|
||||
@@ -162,6 +162,10 @@ class OutboxTest {
|
||||
// than widening its visibility for a test.
|
||||
.put("pub", android.util.Base64.encodeToString(
|
||||
opk.publicKey, android.util.Base64.NO_WRAP)))
|
||||
// The send asks the directory first (free), THEN the key route (which
|
||||
// pops a prekey) — two requests now, same payload shape.
|
||||
server.enqueue(json(org.json.JSONObject()
|
||||
.put("devices", org.json.JSONArray().put(bundle)).toString()))
|
||||
server.enqueue(json(org.json.JSONObject()
|
||||
.put("devices", org.json.JSONArray().put(bundle)).toString()))
|
||||
server.enqueue(json("""{"ok":true}""")) // the envelope post
|
||||
@@ -207,6 +211,9 @@ class OutboxTest {
|
||||
.put("id", opk.id)
|
||||
.put("pub", android.util.Base64.encodeToString(
|
||||
opk.publicKey, android.util.Base64.NO_WRAP)))
|
||||
// Directory first, then keys — see the note in the first test.
|
||||
server.enqueue(json(org.json.JSONObject()
|
||||
.put("devices", org.json.JSONArray().put(bundle)).toString()))
|
||||
server.enqueue(json(org.json.JSONObject()
|
||||
.put("devices", org.json.JSONArray().put(bundle)).toString()))
|
||||
server.enqueue(json("""{"ok":true}"""))
|
||||
@@ -248,11 +255,13 @@ class OutboxTest {
|
||||
// Member 101 registers; 102 is still away.
|
||||
val m1 = DeviceIdentity.create()
|
||||
val opk1 = m1.generateOneTimePreKeys(1).first()
|
||||
server.enqueue(json(org.json.JSONObject().put("devices",
|
||||
val m1Wire = org.json.JSONObject().put("devices",
|
||||
org.json.JSONArray().put(m1.registrationWire().put("opk",
|
||||
org.json.JSONObject().put("id", opk1.id).put("pub",
|
||||
android.util.Base64.encodeToString(opk1.publicKey,
|
||||
android.util.Base64.NO_WRAP))))).toString()))
|
||||
android.util.Base64.NO_WRAP))))).toString()
|
||||
server.enqueue(json(m1Wire)) // the directory
|
||||
server.enqueue(json(m1Wire)) // the key route
|
||||
server.enqueue(json("""{"ok":true}"""))
|
||||
server.enqueue(json("""{"devices":[]}""")) // 102 still gone
|
||||
session.flushOutbox()
|
||||
@@ -265,11 +274,13 @@ class OutboxTest {
|
||||
// Member 102 comes back.
|
||||
val m2 = DeviceIdentity.create()
|
||||
val opk2 = m2.generateOneTimePreKeys(1).first()
|
||||
server.enqueue(json(org.json.JSONObject().put("devices",
|
||||
val m2Wire = org.json.JSONObject().put("devices",
|
||||
org.json.JSONArray().put(m2.registrationWire().put("opk",
|
||||
org.json.JSONObject().put("id", opk2.id).put("pub",
|
||||
android.util.Base64.encodeToString(opk2.publicKey,
|
||||
android.util.Base64.NO_WRAP))))).toString()))
|
||||
android.util.Base64.NO_WRAP))))).toString()
|
||||
server.enqueue(json(m2Wire)) // the directory
|
||||
server.enqueue(json(m2Wire)) // the key route
|
||||
server.enqueue(json("""{"ok":true}"""))
|
||||
val touched = session.flushOutbox()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user