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:
co-authored by
Claude Opus 4.8
parent
d128e8d56e
commit
41533baae6
@@ -1312,8 +1312,13 @@ class Conversations(
|
||||
val key = vault.loadDelivery()?.key ?: return
|
||||
val payload = SealedSender.deliveryKeyEnvelope(
|
||||
key, groupId = groupId, groupName = name)
|
||||
for (member in memberIds) {
|
||||
if (member == myUserId) continue
|
||||
// 🔴 **Our own account is included, and that is the point.** Fan-out
|
||||
// excludes us everywhere else, so a second phone we own would be told
|
||||
// by nobody: the relay has no name to serve it and the members only
|
||||
// send to each other. The fan-out loop already skips our own device
|
||||
// ("never talk to ourselves"), so what goes out is addressed to our
|
||||
// OTHER devices and to nothing else.
|
||||
for (member in memberIds + listOfNotNull(myUserId)) {
|
||||
runCatching { sendPlaintext(member, payload) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,6 +232,45 @@ class GroupNameTest {
|
||||
assertEquals(0, server.requestCount)
|
||||
}
|
||||
|
||||
// -- Surviving a reinstall -------------------------------------------------
|
||||
|
||||
/**
|
||||
* 🔴 **A restored backup must bring the group names with it.** The relay
|
||||
* used to answer with the name, so a reinstalled device got it back for
|
||||
* free; it holds none now, which makes the device's copy the only copy. An
|
||||
* export carrying the messages but not the names restores a chat list of
|
||||
* groups all called "Group" — the exact loss the export exists to stop.
|
||||
*/
|
||||
@Test
|
||||
fun `an exported backup carries group names into a fresh device`() {
|
||||
val old = vault()
|
||||
old.saveGroupName("g1", "Book club")
|
||||
val payload = old.exportPayload()
|
||||
|
||||
val fresh = vault()
|
||||
assertNull(fresh.groupName("g1"))
|
||||
fresh.mergeImportedPayload(payload, 0.0)
|
||||
|
||||
assertEquals("Book club", fresh.groupName("g1"))
|
||||
}
|
||||
|
||||
/**
|
||||
* ⚠️ A backup is older than the device by definition. A rename made since
|
||||
* the export must not be undone by restoring it.
|
||||
*/
|
||||
@Test
|
||||
fun `a restored backup does not overwrite a newer name`() {
|
||||
val old = vault()
|
||||
old.saveGroupName("g1", "Book club")
|
||||
val payload = old.exportPayload()
|
||||
|
||||
val current = vault()
|
||||
current.saveGroupName("g1", "Book club, Tuesdays")
|
||||
current.mergeImportedPayload(payload, 0.0)
|
||||
|
||||
assertEquals("Book club, Tuesdays", current.groupName("g1"))
|
||||
}
|
||||
|
||||
// -- Where it is kept ------------------------------------------------------
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user