AppImage video fix, proper import posts, first-run chooser, file pickers

- bundleMediaFramework: true — bundles full GStreamer plugin set in AppImage.
  Fixes WebKit hang on video/audio (missing appsink/autoaudiosink plugins).
  MSDK/VA plugins removed post-build to avoid Ubuntu DMA assertion crash.
- Import creates complete posts: BlobHeader, VisibilityIntent, pinned blobs,
  self-follow. Imported posts now indistinguishable from locally created ones.
- First-run chooser: Start Fresh or Import Identity on fresh install only.
  Profile setup shown for existing identities without display name.
- File pickers: native Browse buttons on export (folder) and import (ZIP)
  via tauri-plugin-dialog.
- Export path: relative paths resolved against home dir.
- Lightbox close: only on overlay/image click, not inner form content.
- Growth loop skips self as N2 candidate.
- Node shutdown on identity switch (prevents zombie background tasks).
- media-src CSP includes asset protocol.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-04-16 13:49:52 -04:00
parent ec731fdb4b
commit cba30a1bb3
7 changed files with 186 additions and 11 deletions

View file

@ -150,26 +150,57 @@ pub async fn import_public_posts(
let mut imported = 0usize; let mut imported = 0usize;
let mut blobs_imported = 0usize; let mut blobs_imported = 0usize;
info!(post_count = parsed.posts.len(), skipped = parsed.skipped, "Import phase 2: storing to DB");
// Ensure we follow ourselves so imported posts appear in feed
{
let s = storage.get().await;
let _ = s.add_follow(our_node_id);
}
let now = now_ms();
for (new_post, _vis, blob_data) in &parsed.posts { for (new_post, _vis, blob_data) in &parsed.posts {
let new_id = compute_post_id(new_post); let new_id = compute_post_id(new_post);
let s = storage.get().await; let s = storage.get().await;
if s.get_post(&new_id).ok().flatten().is_some() { if s.get_post(&new_id).ok().flatten().is_some() {
continue; // duplicate
}
s.store_post_with_visibility(&new_id, new_post, &PostVisibility::Public)?;
drop(s); drop(s);
debug!(post = hex::encode(new_id), "Import: skipping duplicate post");
continue;
}
// Store post with intent (matches create_post_with_visibility behavior)
s.store_post_with_intent(&new_id, new_post, &PostVisibility::Public, &crate::types::VisibilityIntent::Public)?;
// Store blobs + record them, matching normal post creation
for (att, data) in blob_data { for (att, data) in blob_data {
if !blob_store.has(&att.cid) { if !blob_store.has(&att.cid) {
blob_store.store(&att.cid, data)?; blob_store.store(&att.cid, data)?;
let s = storage.get().await; }
let _ = s.record_blob(&att.cid, &new_id, our_node_id, data.len() as u64, &att.mime_type, att.size_bytes); s.record_blob(&att.cid, &new_id, our_node_id, data.len() as u64, &att.mime_type, att.size_bytes)?;
let _ = s.pin_blob(&att.cid);
blobs_imported += 1; blobs_imported += 1;
} }
}
// Create BlobHeader (matches what engagement/sync expects)
let header = crate::types::BlobHeader {
post_id: new_id,
author: *our_node_id,
reactions: vec![],
comments: vec![],
policy: crate::types::CommentPolicy::default(),
updated_at: now,
thread_splits: vec![],
receipt_slots: vec![],
comment_slots: vec![],
prior_author: None,
};
let header_json = serde_json::to_string(&header).unwrap_or_default();
let _ = s.store_blob_header(&new_id, our_node_id, &header_json, now);
drop(s);
imported += 1; imported += 1;
debug!(imported, post = hex::encode(new_id), "Import: stored post");
} }
info!(imported, skipped = parsed.skipped, blobs = blobs_imported, "Public post import complete"); info!(imported, skipped = parsed.skipped, blobs = blobs_imported, "Public post import complete");

View file

@ -1561,6 +1561,10 @@ impl Network {
}; };
let (candidate_id, score) = match candidate { let (candidate_id, score) = match candidate {
Some((nid, score)) if nid == self.our_node_id => {
debug!("Growth loop: skipping self as candidate");
continue;
}
Some(c) => c, Some(c) => c,
None => { None => {
debug!("Growth loop: no N2 candidates available"); debug!("Growth loop: no N2 candidates available");

File diff suppressed because one or more lines are too long

View file

@ -2144,6 +2144,72 @@
"const": "core:window:deny-unminimize", "const": "core:window:deny-unminimize",
"markdownDescription": "Denies the unminimize command without any pre-configured scope." "markdownDescription": "Denies the unminimize command without any pre-configured scope."
}, },
{
"description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`",
"type": "string",
"const": "dialog:default",
"markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`"
},
{
"description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)",
"type": "string",
"const": "dialog:allow-ask",
"markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
},
{
"description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)",
"type": "string",
"const": "dialog:allow-confirm",
"markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
},
{
"description": "Enables the message command without any pre-configured scope.",
"type": "string",
"const": "dialog:allow-message",
"markdownDescription": "Enables the message command without any pre-configured scope."
},
{
"description": "Enables the open command without any pre-configured scope.",
"type": "string",
"const": "dialog:allow-open",
"markdownDescription": "Enables the open command without any pre-configured scope."
},
{
"description": "Enables the save command without any pre-configured scope.",
"type": "string",
"const": "dialog:allow-save",
"markdownDescription": "Enables the save command without any pre-configured scope."
},
{
"description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)",
"type": "string",
"const": "dialog:deny-ask",
"markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
},
{
"description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)",
"type": "string",
"const": "dialog:deny-confirm",
"markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
},
{
"description": "Denies the message command without any pre-configured scope.",
"type": "string",
"const": "dialog:deny-message",
"markdownDescription": "Denies the message command without any pre-configured scope."
},
{
"description": "Denies the open command without any pre-configured scope.",
"type": "string",
"const": "dialog:deny-open",
"markdownDescription": "Denies the open command without any pre-configured scope."
},
{
"description": "Denies the save command without any pre-configured scope.",
"type": "string",
"const": "dialog:deny-save",
"markdownDescription": "Denies the save command without any pre-configured scope."
},
{ {
"description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`", "description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`",
"type": "string", "type": "string",

View file

@ -2144,6 +2144,72 @@
"const": "core:window:deny-unminimize", "const": "core:window:deny-unminimize",
"markdownDescription": "Denies the unminimize command without any pre-configured scope." "markdownDescription": "Denies the unminimize command without any pre-configured scope."
}, },
{
"description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`",
"type": "string",
"const": "dialog:default",
"markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`"
},
{
"description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)",
"type": "string",
"const": "dialog:allow-ask",
"markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
},
{
"description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)",
"type": "string",
"const": "dialog:allow-confirm",
"markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
},
{
"description": "Enables the message command without any pre-configured scope.",
"type": "string",
"const": "dialog:allow-message",
"markdownDescription": "Enables the message command without any pre-configured scope."
},
{
"description": "Enables the open command without any pre-configured scope.",
"type": "string",
"const": "dialog:allow-open",
"markdownDescription": "Enables the open command without any pre-configured scope."
},
{
"description": "Enables the save command without any pre-configured scope.",
"type": "string",
"const": "dialog:allow-save",
"markdownDescription": "Enables the save command without any pre-configured scope."
},
{
"description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)",
"type": "string",
"const": "dialog:deny-ask",
"markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
},
{
"description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)",
"type": "string",
"const": "dialog:deny-confirm",
"markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
},
{
"description": "Denies the message command without any pre-configured scope.",
"type": "string",
"const": "dialog:deny-message",
"markdownDescription": "Denies the message command without any pre-configured scope."
},
{
"description": "Denies the open command without any pre-configured scope.",
"type": "string",
"const": "dialog:deny-open",
"markdownDescription": "Denies the open command without any pre-configured scope."
},
{
"description": "Denies the save command without any pre-configured scope.",
"type": "string",
"const": "dialog:deny-save",
"markdownDescription": "Denies the save command without any pre-configured scope."
},
{ {
"description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`", "description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`",
"type": "string", "type": "string",

View file

@ -18,7 +18,7 @@
} }
], ],
"security": { "security": {
"csp": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' blob: http://asset.localhost; media-src 'self' blob:", "csp": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' blob: http://asset.localhost; media-src 'self' blob: http://asset.localhost",
"assetProtocol": { "assetProtocol": {
"enable": true, "enable": true,
"scope": ["$APPDATA/**", "$HOME/.local/share/itsgoin/**"] "scope": ["$APPDATA/**", "$HOME/.local/share/itsgoin/**"]
@ -33,6 +33,11 @@
"icons/128x128@2x.png", "icons/128x128@2x.png",
"icons/icon.icns", "icons/icon.icns",
"icons/icon.ico" "icons/icon.ico"
] ],
"linux": {
"appimage": {
"bundleMediaFramework": true
}
}
} }
} }

View file

@ -3514,7 +3514,6 @@ $('#import-btn').addEventListener('click', () => {
} }
status.textContent = result; status.textContent = result;
toast('Import complete!'); toast('Import complete!');
loadFeed(true);
} catch (e) { } catch (e) {
status.textContent = 'Error: ' + e; status.textContent = 'Error: ' + e;
toast('Import failed: ' + e); toast('Import failed: ' + e);
@ -3674,7 +3673,7 @@ async function init() {
} catch (_) {} } catch (_) {}
} }
if (isFirstRun) { if (isFirstRun) {
// First-run chooser: start fresh or import // First-run: start fresh or import
const chooser = document.createElement('div'); const chooser = document.createElement('div');
chooser.className = 'image-lightbox'; chooser.className = 'image-lightbox';
chooser.style.cursor = 'default'; chooser.style.cursor = 'default';
@ -3698,6 +3697,10 @@ async function init() {
// Open the import wizard // Open the import wizard
document.getElementById('import-btn')?.click(); document.getElementById('import-btn')?.click();
}); });
} else if (info && !info.hasProfile) {
// Not first run, but no profile set — show name setup
setupOverlay.classList.remove('hidden');
setupName.focus();
} }
// Pre-load feed + messages from local DB (instant — no network needed) // Pre-load feed + messages from local DB (instant — no network needed)
await loadFeed(true).catch(() => {}); await loadFeed(true).catch(() => {});