P1: the contact list can live only on your devices
Session.roster() reads the vault once the list is off-server, mirrors the relay's copy into the vault while one exists, and falls back to the sealed copy when the network fails. The roster rides the history backup. Settings gains the one-way door with the costs stated where the button is: presence goes dark, and a new device restores contacts from the encrypted backup rather than the server. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2a5970309e
commit
62356139dc
@@ -62,6 +62,7 @@ class Vault(private val storage: Storage, private val key: SecretKey) {
|
||||
private const val OUTBOX = "outbox"
|
||||
private const val CALL_LOG = "call-log"
|
||||
private const val STORY_AUDIENCE = "story-audience"
|
||||
private const val ROSTER = "roster"
|
||||
/** Kept because the whole log is re-sealed on every write. */
|
||||
private const val CALL_LOG_MAX = 500
|
||||
|
||||
@@ -572,6 +573,27 @@ class Vault(private val storage: Storage, private val key: SecretKey) {
|
||||
|
||||
fun saveStoryAudience(audience: JSONObject) = writeJson(STORY_AUDIENCE, audience)
|
||||
|
||||
// -- Contact list (P1) ---------------------------------------------------
|
||||
|
||||
/**
|
||||
* The contact list, once it lives here rather than on the relay.
|
||||
*
|
||||
* `offServer` is the one-way door: once true, adds and removes are
|
||||
* local-only and the relay holds no copy. Kept in the vault like
|
||||
* everything else — who somebody talks to is exactly the metadata this
|
||||
* app exists to keep private — and exported/merged with the history
|
||||
* backup, so a new device restores its contacts from the user's own
|
||||
* backup rather than from a server-side graph.
|
||||
*/
|
||||
fun loadRoster(): Pair<org.json.JSONArray, Boolean> {
|
||||
val json = readJson(ROSTER) ?: return org.json.JSONArray() to false
|
||||
return (json.optJSONArray("items") ?: org.json.JSONArray()) to
|
||||
json.optBoolean("offServer", false)
|
||||
}
|
||||
|
||||
fun saveRoster(items: org.json.JSONArray, offServer: Boolean) =
|
||||
writeJson(ROSTER, JSONObject().put("items", items).put("offServer", offServer))
|
||||
|
||||
/** Rewrite a thread wholesale, for restating a message's delivery state. */
|
||||
fun replaceHistory(threadId: String, messages: JSONArray) =
|
||||
writeJson(historyName(threadId), JSONObject().put("messages", messages))
|
||||
@@ -594,6 +616,7 @@ class Vault(private val storage: Storage, private val key: SecretKey) {
|
||||
.put("threads", threadsObj)
|
||||
.put("callLog", callLog())
|
||||
.also { p -> readJson(STORIES)?.let { p.put("stories", it) } }
|
||||
.also { p -> readJson(ROSTER)?.let { p.put("roster", it) } }
|
||||
}
|
||||
|
||||
data class ImportCounts(val messages: Int, val stories: Int)
|
||||
@@ -674,6 +697,20 @@ class Vault(private val storage: Storage, private val key: SecretKey) {
|
||||
}
|
||||
}
|
||||
|
||||
// The contact list rides the backup too (P1): union by id, and the
|
||||
// off-server mode is adopted if the backup carries it.
|
||||
payload.optJSONObject("roster")?.let { imported ->
|
||||
val (current, offServer) = loadRoster()
|
||||
val known = HashSet<Long>()
|
||||
for (i in 0 until current.length()) known.add(current.getJSONObject(i).optLong("id"))
|
||||
val items = imported.optJSONArray("items") ?: org.json.JSONArray()
|
||||
for (i in 0 until items.length()) {
|
||||
val e = items.optJSONObject(i) ?: continue
|
||||
if (known.add(e.optLong("id"))) current.put(e)
|
||||
}
|
||||
saveRoster(current, offServer || imported.optBoolean("offServer", false))
|
||||
}
|
||||
|
||||
return ImportCounts(newMessages, newStories)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user