Group names survive a reinstall, and reach our own other devices

Two gaps the group-name change left behind.

The encrypted history export carried the messages but not the names. That
was harmless while the relay answered with them — a reinstalled device got
them back for free. It holds none now, so this device's copy is the only
copy, and restoring a backup would have produced a chat list of groups all
called "Group": exactly the loss the export exists to prevent. Names ride
the payload, and only fill gaps on import — a backup is older than the
device by definition, so a rename made since must not be undone.

And fan-out excludes our own account, so a second phone we own was told by
nobody: the relay has no name to serve it and the members only send to each
other. The announcement now addresses our own account too. The fan-out loop
already skipped our own device, so what goes out reaches our OTHER devices
and nothing else.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-08-30 21:23:52 -04:00
co-authored by Claude Opus 4.8
parent d128e8d56e
commit 41533baae6
3 changed files with 69 additions and 2 deletions
@@ -1144,6 +1144,13 @@ class Vault(private val storage: Storage, private val key: SecretKey) {
.put("callLog", callLog())
.also { p -> readJson(STORIES)?.let { p.put("stories", it) } }
.also { p -> readJson(ROSTER)?.let { p.put("roster", it) } }
// 🔴 **The relay does not hold group names any more**, so this
// device's copy is the only copy. Before that change a restored
// device got them back from the server for free; an export that
// carried the messages but not the names would restore a chat list
// of groups all called "Group" — the exact loss this file exists to
// prevent.
.also { p -> readJson(GROUP_NAMES)?.let { p.put("groupNames", it) } }
}
data class ImportCounts(val messages: Int, val stories: Int)
@@ -1238,6 +1245,22 @@ class Vault(private val storage: Storage, private val key: SecretKey) {
saveRoster(current, offServer || imported.optBoolean("offServer", false))
}
// 🔴 **Group names ride the backup, because nothing else can supply
// them now.** The relay stopped holding them, so a restored device that
// did not read them here would show every group as "Group" until some
// other member happened to send a message.
//
// ⚠️ Only for groups this device has no name for. A backup is older
// than the device's own state by definition, and a rename made since
// must not be undone by restoring.
payload.optJSONObject("groupNames")?.let { imported ->
for (gid in imported.keys()) {
if (groupName(gid) != null) continue
val name = imported.optString(gid)
if (name.isNotEmpty()) saveGroupName(gid, name)
}
}
return ImportCounts(newMessages, newStories)
}