Fix: imported DMs silently hidden from Messages tab

Two bugs ganging up:

1) Import ignored the intent field. `ExportedPost.intent` was always
   serialized on export but the import path hardcoded every encrypted
   post to `VisibilityIntent::Friends` (import.rs:308-311), discarding
   whatever `ep.intent` said. DMs got misfiled as Friends.

2) The Messages tab filter only surfaces posts whose `intentKind` is
   `direct` (or `unknown` with the right visibility shape). Posts with
   `intentKind = friends` skip the filter — DMs became invisible after
   an "everything" import, even though the rows were in the DB and the
   per-persona decrypt loop would have resolved them to plaintext.

Fixes:

- `parse_exported_intent(raw, vis)` in import.rs: parses the Debug-format
  intent string the export writes, handling Public / Friends / Circle /
  Direct / Control / Profile / Announcement / GroupKeyDistribute. For
  `Direct`, recovers the recipient list from `PostVisibility::Encrypted`
  since the Debug format for `Vec<NodeId>` isn't cleanly parseable.
- Heuristic fallback when the export carries no intent (pre-v0.6.1
  source DBs, where the intent column wasn't populated): Encrypted
  posts with <=3 recipients are classified as `Direct`, larger
  recipient lists stay `Friends`. DMs typically wrap to 1-2 people;
  Friends wraps to every public follow.
- `StagedImport.posts` tuple grows an `intent` slot; the store step
  uses the parsed/inferred intent instead of the hardcoded default.

One-time startup migration:
- Sweeps existing posts where `visibility_intent = "Friends"` and
  visibility is Encrypted with <=3 recipients; rewrites to Direct.
  Guarded by `mig_import_dm_fixup_v1` settings key so it runs once
  per DB. Handles already-imported corrupt state so users don't need
  to re-import.

Tests: 124 / 124 core tests pass.
This commit is contained in:
Scott Reimers 2026-04-23 08:11:11 -04:00
parent fb0e293e2d
commit d990da5bda
2 changed files with 143 additions and 12 deletions

View file

@ -674,6 +674,65 @@ impl Storage {
"CREATE INDEX IF NOT EXISTS idx_group_keys_root ON group_keys(canonical_root_post_id);"
)?;
// v0.6.2 import bugfix: the import pipeline up through f b0e293
// filed EVERY encrypted post as VisibilityIntent::Friends, including
// DMs. The Messages tab in the UI only shows posts whose intent is
// `Direct` (or `unknown` with the right visibility shape), so DMs
// imported from an "everything" bundle silently disappeared.
//
// This one-time migration reclassifies short-recipient encrypted
// posts filed as Friends back to Direct, using the same small-list
// heuristic as the import code (recipients <= 3 → Direct). A guard
// flag in the settings kv ensures this sweep runs once per DB.
let imported_dm_fixup_done = self.get_setting("mig_import_dm_fixup_v1")?.is_some();
if !imported_dm_fixup_done {
let mut fixed = 0i64;
let mut stmt = self.conn.prepare(
"SELECT id, visibility FROM posts
WHERE visibility_intent = '\"Friends\"'
AND visibility LIKE '%Encrypted%'"
)?;
let rows: Vec<(Vec<u8>, String)> = stmt
.query_map([], |row| Ok((row.get::<_, Vec<u8>>(0)?, row.get::<_, String>(1)?)))?
.filter_map(|r| r.ok())
.collect();
drop(stmt);
for (id_bytes, vis_json) in rows {
// Parse out the recipients array from the Encrypted
// visibility. Short recipient list (≤ 3) = likely DM.
let vis: Result<PostVisibility, _> = serde_json::from_str(&vis_json);
let should_fix = match vis {
Ok(PostVisibility::Encrypted { recipients }) => recipients.len() <= 3,
_ => false,
};
if !should_fix { continue; }
// Build a Direct-intent value with the recipient list recovered
// from the visibility — semantically equivalent to what the
// user's original send-side intent would have been.
let vis_parsed: PostVisibility = match serde_json::from_str(&vis_json) {
Ok(v) => v,
Err(_) => continue,
};
let recipients: Vec<NodeId> = match vis_parsed {
PostVisibility::Encrypted { recipients } => {
recipients.iter().map(|wk| wk.recipient).collect()
}
_ => continue,
};
let intent = crate::types::VisibilityIntent::Direct(recipients);
let intent_json = serde_json::to_string(&intent).unwrap_or_default();
let n = self.conn.execute(
"UPDATE posts SET visibility_intent = ?1 WHERE id = ?2",
params![intent_json, id_bytes],
)?;
fixed += n as i64;
}
self.set_setting("mig_import_dm_fixup_v1", &fixed.to_string())?;
if fixed > 0 {
tracing::info!(count = fixed, "Migrated imported DMs from Friends-intent to Direct-intent");
}
}
// Add device_role column to peers if missing (Active CDN replication)
let has_device_role = self.conn.prepare(
"SELECT COUNT(*) FROM pragma_table_info('peers') WHERE name='device_role'"