fix(chat): resolve merged-away user ids so DMs to merged contacts don't vanish

Root cause of 'messages to some contacts disappear (gone after reopen)': the #2
account merge deletes the merged-away user row. Any lingering reference to that old
id — a cached contact, an in-flight DM — then saved against a dead recipient / 404'd
on thread fetch, so messages silently vanished.

- New user_aliases table records old_id -> survivor on every merge (mergeInto).
- users.resolve(id) follows the redirect.
- DM send (recipient), thread fetch (with), and read now resolve() the peer id, so a
  stale id transparently routes to the surviving account.

Fixes future merges fully. Contacts merged BEFORE this (no alias recorded) may need a
one-off data check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 13:23:49 +05:30
parent 1128f9811a
commit 12be747609
3 changed files with 30 additions and 8 deletions
+8
View File
@@ -22,6 +22,9 @@ const teams = {
const users = {
anyExists: () => !!db.prepare('SELECT 1 FROM users LIMIT 1').get(),
byId: (id) => db.prepare('SELECT * FROM users WHERE id=?').get(id),
// Follow a merge redirect: a merged-away id resolves to the surviving account, else returns id
// unchanged. Use for any user id that arrived from the client (DM recipient, thread peer).
resolve: (id) => { if (!id) return id; const a = db.prepare('SELECT user_id FROM user_aliases WHERE old_id=?').get(id); return a ? a.user_id : id; },
byEmail: (email) => db.prepare('SELECT * FROM users WHERE email=? COLLATE NOCASE').get(email),
emailExists: (email) => !!db.prepare('SELECT 1 FROM users WHERE email=? COLLATE NOCASE').get(email),
// Match on the stable BizGaze person-id so email- and mobile-logins resolve to one account (#2).
@@ -82,6 +85,11 @@ const users = {
// Drop the merged-away account's auth so a stale token can't resurrect the empty row.
run('DELETE FROM sessions_auth WHERE user_id=?', fromId);
run('DELETE FROM refresh_tokens WHERE user_id=?', fromId);
// Record old_id -> survivor so lingering references (cached contacts, in-flight DMs) resolve
// instead of hitting the deleted row (which made messages to merged contacts vanish).
const _iu = db.prepare('SELECT team_id FROM users WHERE id=?').get(intoId);
run('INSERT OR REPLACE INTO user_aliases (old_id,user_id,team_id,created_at) VALUES (?,?,?,?)', fromId, intoId, (_iu && _iu.team_id) || null, now());
run('UPDATE user_aliases SET user_id=? WHERE user_id=?', intoId, fromId); // re-chain earlier aliases to the new survivor
run('DELETE FROM users WHERE id=?', fromId);
db.exec('COMMIT');
} catch (e) {