Feed pagination, duplicate identity detection, pkarr leak fix, Android SAF

Feed pagination:
- Cursor-based pagination: get_feed_page/get_all_posts_page (20 posts/page)
- Batched engagement queries (3 bulk SQL queries instead of 4 per post)
- IntersectionObserver for infinite scroll (sentinel at midpoint)
- Viewport-based media loading (blobs only load when post enters view)
- Pre-fetch next page immediately after current page renders

Duplicate identity detection:
- Anchor detects when a NodeId is already mesh-connected during initial
  exchange and sets duplicate_active flag in response
- Client skips sync tasks when duplicate detected
- Frontend shows red warning banner

Privacy:
- Fixed pkarr leak: clear_address_lookup() removes default dns.iroh.link
  publishing. Only mDNS (local network) discovery enabled.

Android:
- SAF integration via tauri-plugin-android-fs: exports open native "Save As"
  dialog so users can save to Downloads/Drive/etc.
- Download/export paths use app data dir on Android (writable)
- File picker gated behind desktop cfg (blocking_pick not on Android)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-04-18 15:35:23 -04:00
parent 5e7eed9638
commit 288b53ffb1
12 changed files with 910 additions and 120 deletions

View file

@ -1,6 +1,6 @@
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering as AtomicOrdering};
use std::sync::Arc;
use tracing::{debug, info, warn};
@ -30,6 +30,8 @@ pub struct Node {
pub blob_store: Arc<BlobStore>,
secret_seed: [u8; 32],
bootstrap_anchors: tokio::sync::Mutex<Vec<(NodeId, iroh::EndpointAddr)>>,
/// True if an anchor reported another instance of this identity is already active
pub duplicate_detected: Arc<AtomicBool>,
#[allow(dead_code)]
profile: DeviceProfile,
pub activity_log: Arc<std::sync::Mutex<ActivityLog>>,
@ -136,6 +138,7 @@ impl Node {
blob_store,
secret_seed,
bootstrap_anchors: tokio::sync::Mutex::new(Vec::new()),
duplicate_detected: Arc::new(AtomicBool::new(false)),
profile,
activity_log: activity_log_ref,
last_rebalance_ms,
@ -927,6 +930,34 @@ impl Node {
Ok(self.decrypt_posts(raw, &group_seeds))
}
pub async fn get_feed_page(
&self,
before_ms: Option<u64>,
limit: usize,
) -> anyhow::Result<Vec<(PostId, Post, PostVisibility, Option<String>)>> {
let (raw, group_seeds) = {
let storage = self.storage.get().await;
let posts = storage.get_feed_page(before_ms, limit)?;
let seeds = storage.get_all_group_seeds_map().unwrap_or_default();
(posts, seeds)
};
Ok(self.decrypt_posts(raw, &group_seeds))
}
pub async fn get_all_posts_page(
&self,
before_ms: Option<u64>,
limit: usize,
) -> anyhow::Result<Vec<(PostId, Post, PostVisibility, Option<String>)>> {
let (raw, group_seeds) = {
let storage = self.storage.get().await;
let posts = storage.list_posts_page(before_ms, limit)?;
let seeds = storage.get_all_group_seeds_map().unwrap_or_default();
(posts, seeds)
};
Ok(self.decrypt_posts(raw, &group_seeds))
}
fn decrypt_posts(
&self,
posts: Vec<(PostId, Post, PostVisibility)>,