Phase 5 (0.6.4-beta) frontend: Personas UI + compose picker + feed pills
Settings > Personas: - List all held posting identities with display_name + truncated nodeId - Default badge; Set-default / Delete buttons per non-default persona - "New Persona" modal prompts for a display name and creates via IPC Compose box: - A #persona-select dropdown appears when 2+ personas exist - doPost attaches postingIdHex to create_post / create_post_with_files when a non-default persona is selected Tauri: - create_post and create_post_with_files take an optional posting_id_hex; when present they route through create_post_as, otherwise through the default create_post_with_visibility - PostDto gains asPersona: name of the authoring posting identity if the author matches any of our held personas - is_me now recognises ALL our posting identities, not just the network key (both post_to_dto and post_to_dto_batch) Feed: - Per-post "(you) as <PersonaName>" label on own posts authored by a non-default persona - Persona filter pill row above the feed (hidden for single-persona users); pills toggle between All and each persona; matches when post.author or post.recipients contains the selected posting id - Applied after loadFeed initial render and after appendFeedPage so filter survives infinite-scroll App.js: - personasCache + loadPersonas() loaded on startup so compose picker is populated before the Feed tab mounts - loadPersonas() also called when Settings tab opens Backend was unchanged; only the UI and IPC surface expanded. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7bdb2eb736
commit
eea868b4cc
3 changed files with 245 additions and 15 deletions
|
|
@ -58,6 +58,10 @@ struct PostDto {
|
|||
reaction_counts: Vec<ReactionCountDto>,
|
||||
/// Number of comments on this post
|
||||
comment_count: u64,
|
||||
/// If the post is authored by one of our held posting identities, the
|
||||
/// persona's display_name. None for posts authored by peers (or if the
|
||||
/// local persona has no display name).
|
||||
as_persona: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
|
@ -208,7 +212,18 @@ async fn post_to_dto(
|
|||
decrypted: Option<&str>,
|
||||
node: &Node,
|
||||
) -> PostDto {
|
||||
let is_me = &post.author == &node.node_id;
|
||||
// "is_me" now means: authored by ANY posting identity we hold, not just the
|
||||
// network key. Covers the multi-persona case from 0.6.4+.
|
||||
let (is_me, as_persona) = {
|
||||
let s = node.storage.get().await;
|
||||
match s.get_posting_identity(&post.author) {
|
||||
Ok(Some(pi)) => {
|
||||
let name = if pi.display_name.is_empty() { None } else { Some(pi.display_name) };
|
||||
(true, name)
|
||||
}
|
||||
_ => (false, None),
|
||||
}
|
||||
};
|
||||
let author_name = match node.resolve_display_name(&post.author).await {
|
||||
Ok((name, _, _)) if !name.is_empty() => Some(name),
|
||||
_ => None,
|
||||
|
|
@ -280,6 +295,7 @@ async fn post_to_dto(
|
|||
recipients,
|
||||
reaction_counts,
|
||||
comment_count,
|
||||
as_persona,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -409,6 +425,7 @@ async fn create_post(
|
|||
visibility: Option<String>,
|
||||
circle_name: Option<String>,
|
||||
recipient_hex: Option<String>,
|
||||
posting_id_hex: Option<String>,
|
||||
) -> Result<PostDto, String> {
|
||||
let node = get_node(&state).await;
|
||||
let intent = match visibility.as_deref() {
|
||||
|
|
@ -424,10 +441,13 @@ async fn create_post(
|
|||
}
|
||||
_ => VisibilityIntent::Public,
|
||||
};
|
||||
let (id, post, vis) = node
|
||||
.create_post_with_visibility(content, intent, vec![])
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let (id, post, vis) = match posting_id_hex {
|
||||
Some(pid_hex) => {
|
||||
let pid = itsgoin_core::parse_node_id_hex(&pid_hex).map_err(|e| e.to_string())?;
|
||||
node.create_post_as(&pid, content, intent, vec![]).await
|
||||
}
|
||||
None => node.create_post_with_visibility(content, intent, vec![]).await,
|
||||
}.map_err(|e| e.to_string())?;
|
||||
let decrypted = decrypt_just_created(&node, &post, &vis).await;
|
||||
Ok(post_to_dto(&id, &post, &vis, decrypted.as_deref(), &node).await)
|
||||
}
|
||||
|
|
@ -440,6 +460,7 @@ async fn create_post_with_files(
|
|||
circle_name: Option<String>,
|
||||
recipient_hex: Option<String>,
|
||||
files: Vec<(String, String)>,
|
||||
posting_id_hex: Option<String>,
|
||||
) -> Result<PostDto, String> {
|
||||
let node = get_node(&state).await;
|
||||
let intent = match visibility.as_deref() {
|
||||
|
|
@ -467,10 +488,13 @@ async fn create_post_with_files(
|
|||
})
|
||||
.collect::<Result<Vec<_>, String>>()?;
|
||||
|
||||
let (id, post, vis) = node
|
||||
.create_post_with_visibility(content, intent, attachment_data)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let (id, post, vis) = match posting_id_hex {
|
||||
Some(pid_hex) => {
|
||||
let pid = itsgoin_core::parse_node_id_hex(&pid_hex).map_err(|e| e.to_string())?;
|
||||
node.create_post_as(&pid, content, intent, attachment_data).await
|
||||
}
|
||||
None => node.create_post_with_visibility(content, intent, attachment_data).await,
|
||||
}.map_err(|e| e.to_string())?;
|
||||
let decrypted = decrypt_just_created(&node, &post, &vis).await;
|
||||
Ok(post_to_dto(&id, &post, &vis, decrypted.as_deref(), &node).await)
|
||||
}
|
||||
|
|
@ -819,20 +843,32 @@ async fn post_to_dto_batch(
|
|||
let post_ids: Vec<itsgoin_core::types::PostId> = posts.iter().map(|(id, _, _, _)| *id).collect();
|
||||
|
||||
// Batch queries — 3 queries total instead of 4 × N
|
||||
let (reaction_map, comment_map, intent_map) = {
|
||||
let (reaction_map, comment_map, intent_map, posting_identities) = {
|
||||
let storage = node.storage.get().await;
|
||||
let reactions = storage.get_reaction_counts_batch(&post_ids, &node.node_id).unwrap_or_default();
|
||||
let comments = storage.get_comment_counts_batch(&post_ids).unwrap_or_default();
|
||||
let intents = storage.get_post_intents_batch(&post_ids).unwrap_or_default();
|
||||
(reactions, comments, intents)
|
||||
let identities = storage.list_posting_identities().unwrap_or_default();
|
||||
(reactions, comments, intents, identities)
|
||||
};
|
||||
// Map posting-id -> display-name so we can tag author=persona posts.
|
||||
let persona_names: HashMap<itsgoin_core::types::NodeId, Option<String>> = posting_identities
|
||||
.into_iter()
|
||||
.map(|pi| {
|
||||
let name = if pi.display_name.is_empty() { None } else { Some(pi.display_name) };
|
||||
(pi.node_id, name)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Batch resolve display names
|
||||
let mut name_cache: HashMap<itsgoin_core::types::NodeId, Option<String>> = HashMap::new();
|
||||
|
||||
let mut dtos = Vec::with_capacity(posts.len());
|
||||
for (id, post, vis, decrypted) in posts {
|
||||
let is_me = post.author == node.node_id;
|
||||
let (is_me, as_persona) = match persona_names.get(&post.author) {
|
||||
Some(name) => (true, name.clone()),
|
||||
None => (false, None),
|
||||
};
|
||||
|
||||
let author_name = if let Some(cached) = name_cache.get(&post.author) {
|
||||
cached.clone()
|
||||
|
|
@ -897,6 +933,7 @@ async fn post_to_dto_batch(
|
|||
recipients,
|
||||
reaction_counts,
|
||||
comment_count,
|
||||
as_persona: as_persona.clone(),
|
||||
});
|
||||
}
|
||||
dtos
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue