Extract profile metadata into new metadata crate
Rust / build (macos-latest, stable) (push) Canceled after 0s
Rust / build (ubuntu-latest, stable) (push) Canceled after 0s
Rust / build (windows-latest, stable) (push) Canceled after 0s
Rust / build (macos-latest, stable) (pull_request) Canceled after 0s
Rust / build (ubuntu-latest, stable) (pull_request) Canceled after 0s
Rust / build (windows-latest, stable) (pull_request) Canceled after 0s
Rust / build (macos-latest, stable) (push) Canceled after 0s
Rust / build (ubuntu-latest, stable) (push) Canceled after 0s
Rust / build (windows-latest, stable) (push) Canceled after 0s
Rust / build (macos-latest, stable) (pull_request) Canceled after 0s
Rust / build (ubuntu-latest, stable) (pull_request) Canceled after 0s
Rust / build (windows-latest, stable) (pull_request) Canceled after 0s
This commit is contained in:
Generated
+16
@@ -5457,6 +5457,20 @@ dependencies = [
|
|||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "metadata"
|
||||||
|
version = "0.2.0-alpha"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"flume 0.11.1",
|
||||||
|
"futures",
|
||||||
|
"gpui-pre",
|
||||||
|
"log",
|
||||||
|
"nostr-sdk",
|
||||||
|
"signed_state",
|
||||||
|
"utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "metal"
|
name = "metal"
|
||||||
version = "0.33.0"
|
version = "0.33.0"
|
||||||
@@ -8116,6 +8130,7 @@ dependencies = [
|
|||||||
"gpui-pre",
|
"gpui-pre",
|
||||||
"gpui-pre-platform",
|
"gpui-pre-platform",
|
||||||
"gpui-pre-reqwest-client",
|
"gpui-pre-reqwest-client",
|
||||||
|
"metadata",
|
||||||
"paths",
|
"paths",
|
||||||
"settings",
|
"settings",
|
||||||
"signed_state",
|
"signed_state",
|
||||||
@@ -11031,6 +11046,7 @@ dependencies = [
|
|||||||
"gpui-component",
|
"gpui-component",
|
||||||
"gpui-pre",
|
"gpui-pre",
|
||||||
"log",
|
"log",
|
||||||
|
"metadata",
|
||||||
"nostr",
|
"nostr",
|
||||||
"paths",
|
"paths",
|
||||||
"settings",
|
"settings",
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "metadata"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
signed_state = { path = "../signed_state" }
|
||||||
|
utils = { path = "../utils" }
|
||||||
|
|
||||||
|
nostr-sdk.workspace = true
|
||||||
|
flume.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
gpui.workspace = true
|
||||||
|
log.workspace = true
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
mod profile;
|
||||||
|
|
||||||
|
use gpui::{App, AppContext};
|
||||||
|
pub use profile::{Profile, ProfileDirectory, ProfileEvent, ProfileStore};
|
||||||
|
|
||||||
|
/// Initialize the global profile store.
|
||||||
|
pub fn init(cx: &mut App) {
|
||||||
|
ProfileStore::set_global(cx.new(ProfileStore::new), cx);
|
||||||
|
}
|
||||||
@@ -7,11 +7,9 @@ use gpui::{
|
|||||||
App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Subscription, Task,
|
App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Subscription, Task,
|
||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
use signed_state::{Backend, BackendEvent, sync_bootstrap_only};
|
||||||
use utils::shorten_pubkey;
|
use utils::shorten_pubkey;
|
||||||
|
|
||||||
use crate::backend::{Backend, BackendEvent};
|
|
||||||
use crate::bootstrap::sync_bootstrap_only;
|
|
||||||
|
|
||||||
const BATCH_TIMEOUT: Duration = Duration::from_millis(500);
|
const BATCH_TIMEOUT: Duration = Duration::from_millis(500);
|
||||||
const REQUEST_CHUNK: usize = 100;
|
const REQUEST_CHUNK: usize = 100;
|
||||||
|
|
||||||
@@ -104,7 +102,7 @@ impl Global for GlobalProfileStore {}
|
|||||||
impl EventEmitter<ProfileEvent> for ProfileStore {}
|
impl EventEmitter<ProfileEvent> for ProfileStore {}
|
||||||
|
|
||||||
impl ProfileStore {
|
impl ProfileStore {
|
||||||
pub fn global(cx: &App) -> Entity<Self> {
|
pub(crate) fn global(cx: &App) -> Entity<Self> {
|
||||||
cx.global::<GlobalProfileStore>().0.clone()
|
cx.global::<GlobalProfileStore>().0.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +166,9 @@ impl ProfileStore {
|
|||||||
.kind(Kind::Metadata)
|
.kind(Kind::Metadata)
|
||||||
.authors(chunk.iter().copied());
|
.authors(chunk.iter().copied());
|
||||||
|
|
||||||
sync_bootstrap_only(&client, filter, opts).await.ok();
|
if let Err(e) = sync_bootstrap_only(&client, filter, opts).await {
|
||||||
|
log::warn!("profile fetch failed: {e}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit even when the sync failed, the database may hold newer data.
|
// Emit even when the sync failed, the database may hold newer data.
|
||||||
@@ -320,7 +320,6 @@ impl ProfileDirectory {
|
|||||||
Ok::<_, anyhow::Error>(Profile::latest_profiles(events))
|
Ok::<_, anyhow::Error>(Profile::latest_profiles(events))
|
||||||
});
|
});
|
||||||
|
|
||||||
self.tasks.retain(|task| !task.is_ready());
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
let loaded = work.await;
|
let loaded = work.await;
|
||||||
|
|
||||||
@@ -365,7 +364,10 @@ impl ProfileDirectory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with_pending<R>(&self, then: impl FnOnce(&mut HashSet<PublicKey>) -> R) -> R {
|
fn with_pending<R, T>(&self, then: T) -> R
|
||||||
|
where
|
||||||
|
T: FnOnce(&mut HashSet<PublicKey>) -> R,
|
||||||
|
{
|
||||||
let mut pending = self.pending.borrow_mut();
|
let mut pending = self.pending.borrow_mut();
|
||||||
then(&mut pending)
|
then(&mut pending)
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@ pub(crate) async fn subscribe_bootstrap_only(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn sync_bootstrap_only(
|
pub async fn sync_bootstrap_only(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
filter: Filter,
|
filter: Filter,
|
||||||
opts: SyncOptions,
|
opts: SyncOptions,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ mod git_store;
|
|||||||
mod inbox;
|
mod inbox;
|
||||||
mod local_repos;
|
mod local_repos;
|
||||||
mod patch_series;
|
mod patch_series;
|
||||||
mod profile;
|
|
||||||
mod push;
|
mod push;
|
||||||
mod refresh;
|
mod refresh;
|
||||||
mod repo;
|
mod repo;
|
||||||
@@ -16,14 +15,13 @@ mod sync_status;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
pub use backend::{Backend, BackendEvent};
|
pub use backend::{Backend, BackendEvent};
|
||||||
pub use bootstrap::user_grasp_list_servers;
|
pub use bootstrap::{sync_bootstrap_only, user_grasp_list_servers};
|
||||||
pub use checkouts::{CheckoutStatus, CheckoutsStore};
|
pub use checkouts::{CheckoutStatus, CheckoutsStore};
|
||||||
pub use git_store::Mirrors;
|
pub use git_store::Mirrors;
|
||||||
use gpui::{App, AppContext};
|
use gpui::{App, AppContext};
|
||||||
pub use inbox::Inbox;
|
pub use inbox::Inbox;
|
||||||
pub use local_repos::{LocalReposStore, ResolvedLocalRepo};
|
pub use local_repos::{LocalReposStore, ResolvedLocalRepo};
|
||||||
pub use nostr_sdk::prelude::Timestamp;
|
pub use nostr_sdk::prelude::Timestamp;
|
||||||
pub use profile::{Profile, ProfileDirectory, ProfileEvent, ProfileStore};
|
|
||||||
pub use push::{GraspServer, PushOutcome};
|
pub use push::{GraspServer, PushOutcome};
|
||||||
pub use refresh::{RefreshGate, RefreshRequest};
|
pub use refresh::{RefreshGate, RefreshRequest};
|
||||||
pub use repo::RepoStore;
|
pub use repo::RepoStore;
|
||||||
@@ -51,7 +49,6 @@ pub fn init(
|
|||||||
Mirrors::install(repos_root);
|
Mirrors::install(repos_root);
|
||||||
|
|
||||||
Backend::set_global(cx.new(|cx| Backend::new(client, signer, cx)), cx);
|
Backend::set_global(cx.new(|cx| Backend::new(client, signer, cx)), cx);
|
||||||
ProfileStore::set_global(cx.new(ProfileStore::new), cx);
|
|
||||||
RepoListStore::set_global(cx.new(RepoListStore::new), cx);
|
RepoListStore::set_global(cx.new(RepoListStore::new), cx);
|
||||||
LocalReposStore::set_global(cx.new(|cx| LocalReposStore::new(scan_paths, cx)), cx);
|
LocalReposStore::set_global(cx.new(|cx| LocalReposStore::new(scan_paths, cx)), cx);
|
||||||
CheckoutsStore::set_global(cx.new(CheckoutsStore::new), cx);
|
CheckoutsStore::set_global(cx.new(CheckoutsStore::new), cx);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ publish.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
assets = { path = "../assets" }
|
assets = { path = "../assets" }
|
||||||
dock = { workspace = true }
|
dock = { workspace = true }
|
||||||
|
metadata = { path = "../metadata" }
|
||||||
paths = { path = "../paths" }
|
paths = { path = "../paths" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
signed_core = { path = "../signed_core" }
|
signed_core = { path = "../signed_core" }
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ use gpui_component::tree::{TreeEntry, TreeItem, TreeState, tree};
|
|||||||
use gpui_component::{
|
use gpui_component::{
|
||||||
ActiveTheme, Sizable, StyledExt, VirtualListScrollHandle, h_flex, v_flex, v_virtual_list,
|
ActiveTheme, Sizable, StyledExt, VirtualListScrollHandle, h_flex, v_flex, v_virtual_list,
|
||||||
};
|
};
|
||||||
|
use metadata::ProfileDirectory;
|
||||||
use signed_git::{
|
use signed_git::{
|
||||||
CommitDiff, DiffHunk, DiffLine, DiffLineKind, DiffStatus, FileCommit, FileDiff, Repo,
|
CommitDiff, DiffHunk, DiffLine, DiffLineKind, DiffStatus, FileCommit, FileDiff, Repo,
|
||||||
};
|
};
|
||||||
use signed_state::{ProfileDirectory, RepoStore};
|
use signed_state::RepoStore;
|
||||||
use signed_ui::{placeholder, tree_row};
|
use signed_ui::{placeholder, tree_row};
|
||||||
use utils::relative_time_secs;
|
use utils::relative_time_secs;
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ use gpui_component::button::{Button, ButtonVariants};
|
|||||||
use gpui_component::input::{Textarea, TextareaState};
|
use gpui_component::input::{Textarea, TextareaState};
|
||||||
use gpui_component::tag::Tag;
|
use gpui_component::tag::Tag;
|
||||||
use gpui_component::{ActiveTheme, Icon, Sizable, StyledExt, h_flex, v_flex};
|
use gpui_component::{ActiveTheme, Icon, Sizable, StyledExt, h_flex, v_flex};
|
||||||
|
use metadata::ProfileDirectory;
|
||||||
use nostr::prelude::{Event, EventId, PublicKey};
|
use nostr::prelude::{Event, EventId, PublicKey};
|
||||||
use signed_state::{ProfileDirectory, RepoStore};
|
use signed_state::RepoStore;
|
||||||
use signed_ui::Avatar;
|
use signed_ui::Avatar;
|
||||||
use utils::relative_time;
|
use utils::relative_time;
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,10 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use gpui_component::button::{Button, ButtonVariants};
|
use gpui_component::button::{Button, ButtonVariants};
|
||||||
use gpui_component::{ActiveTheme, Icon, IconName, IconNamed, Sizable, StyledExt, h_flex, v_flex};
|
use gpui_component::{ActiveTheme, Icon, IconName, IconNamed, Sizable, StyledExt, h_flex, v_flex};
|
||||||
|
use metadata::ProfileDirectory;
|
||||||
use nostr::prelude::{Event, EventId, Kind, PublicKey, Timestamp};
|
use nostr::prelude::{Event, EventId, Kind, PublicKey, Timestamp};
|
||||||
use signed_core::{InboxItem, InboxReadState, RepoAddr};
|
use signed_core::{InboxItem, InboxReadState, RepoAddr};
|
||||||
use signed_state::{
|
use signed_state::{Backend, BackendEvent, Inbox, RefreshGate, RefreshRequest, RepoListStore};
|
||||||
Backend, BackendEvent, Inbox, ProfileDirectory, RefreshGate, RefreshRequest, RepoListStore,
|
|
||||||
};
|
|
||||||
use signed_ui::{Avatar, CountBadge};
|
use signed_ui::{Avatar, CountBadge};
|
||||||
use utils::relative_time;
|
use utils::relative_time;
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ use gpui::{
|
|||||||
use gpui_component::input::TextareaState;
|
use gpui_component::input::TextareaState;
|
||||||
use gpui_component::scroll::ScrollableElement;
|
use gpui_component::scroll::ScrollableElement;
|
||||||
use gpui_component::{ActiveTheme, StyledExt, h_flex, v_flex};
|
use gpui_component::{ActiveTheme, StyledExt, h_flex, v_flex};
|
||||||
|
use metadata::ProfileDirectory;
|
||||||
use nostr::prelude::{EventId, PublicKey};
|
use nostr::prelude::{EventId, PublicKey};
|
||||||
use signed_core::GitEvent;
|
use signed_core::GitEvent;
|
||||||
use signed_state::{ProfileDirectory, RepoStore};
|
use signed_state::RepoStore;
|
||||||
use signed_ui::{Avatar, placeholder, status_badge};
|
use signed_ui::{Avatar, placeholder, status_badge};
|
||||||
use utils::relative_time;
|
use utils::relative_time;
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ use gpui_component::scroll::Scrollbar;
|
|||||||
use gpui_component::{
|
use gpui_component::{
|
||||||
ActiveTheme, Icon, VirtualListScrollHandle, WindowExt, h_flex, v_flex, v_virtual_list,
|
ActiveTheme, Icon, VirtualListScrollHandle, WindowExt, h_flex, v_flex, v_virtual_list,
|
||||||
};
|
};
|
||||||
|
use metadata::ProfileDirectory;
|
||||||
use nostr::prelude::{EventId, PublicKey};
|
use nostr::prelude::{EventId, PublicKey};
|
||||||
use signed_core::{GitEvent, RepoStatus};
|
use signed_core::{GitEvent, RepoStatus};
|
||||||
use signed_state::{ProfileDirectory, RepoStore};
|
use signed_state::RepoStore;
|
||||||
use signed_ui::{Avatar, SegmentButton, placeholder, status_badge};
|
use signed_ui::{Avatar, SegmentButton, placeholder, status_badge};
|
||||||
use utils::relative_time;
|
use utils::relative_time;
|
||||||
|
|
||||||
|
|||||||
@@ -18,12 +18,13 @@ use gpui::prelude::*;
|
|||||||
use gpui::{AnyElement, App, Entity, div};
|
use gpui::{AnyElement, App, Entity, div};
|
||||||
use gpui_component::{Sizable as _, h_flex};
|
use gpui_component::{Sizable as _, h_flex};
|
||||||
pub use inbox::InboxView;
|
pub use inbox::InboxView;
|
||||||
|
use metadata::ProfileDirectory;
|
||||||
pub(crate) use placeholder_panel::PlaceholderPanel;
|
pub(crate) use placeholder_panel::PlaceholderPanel;
|
||||||
pub use repo::RepoDetailView;
|
pub use repo::RepoDetailView;
|
||||||
pub(crate) use repo::{RepoItem, open_repo_item, open_repo_panel};
|
pub(crate) use repo::{RepoItem, open_repo_item, open_repo_panel};
|
||||||
pub use repo_list::RepoListView;
|
pub use repo_list::RepoListView;
|
||||||
pub use sidebar::SidebarPanel;
|
pub use sidebar::SidebarPanel;
|
||||||
use signed_state::{ProfileDirectory, RepoStore};
|
use signed_state::RepoStore;
|
||||||
use signed_ui::Avatar;
|
use signed_ui::Avatar;
|
||||||
|
|
||||||
pub(crate) fn tab_title(avatar: AnyElement, label: impl IntoElement) -> impl IntoElement {
|
pub(crate) fn tab_title(avatar: AnyElement, label: impl IntoElement) -> impl IntoElement {
|
||||||
|
|||||||
@@ -20,10 +20,11 @@ use gpui_component::{
|
|||||||
ActiveTheme, Sizable, StyledExt, VirtualListScrollHandle, WindowExt, h_flex, v_flex,
|
ActiveTheme, Sizable, StyledExt, VirtualListScrollHandle, WindowExt, h_flex, v_flex,
|
||||||
v_virtual_list,
|
v_virtual_list,
|
||||||
};
|
};
|
||||||
|
use metadata::ProfileDirectory;
|
||||||
use nostr::prelude::{Event, EventId, Kind, PublicKey, Url};
|
use nostr::prelude::{Event, EventId, Kind, PublicKey, Url};
|
||||||
use signed_core::{GitEvent, PullRequest, RepoAddr};
|
use signed_core::{GitEvent, PullRequest, RepoAddr};
|
||||||
use signed_git::{FileCommit, PatchParser};
|
use signed_git::{FileCommit, PatchParser};
|
||||||
use signed_state::{Backend, Mirrors, ProfileDirectory, RepoStore};
|
use signed_state::{Backend, Mirrors, RepoStore};
|
||||||
use signed_ui::{Avatar, CountBadge, placeholder, status_badge};
|
use signed_ui::{Avatar, CountBadge, placeholder, status_badge};
|
||||||
use utils::{relative_time, relative_time_secs};
|
use utils::{relative_time, relative_time_secs};
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ use gpui_component::scroll::Scrollbar;
|
|||||||
use gpui_component::{
|
use gpui_component::{
|
||||||
ActiveTheme, Icon, IconName, VirtualListScrollHandle, h_flex, v_flex, v_virtual_list,
|
ActiveTheme, Icon, IconName, VirtualListScrollHandle, h_flex, v_flex, v_virtual_list,
|
||||||
};
|
};
|
||||||
|
use metadata::ProfileDirectory;
|
||||||
use nostr::prelude::{EventId, Kind, PublicKey};
|
use nostr::prelude::{EventId, Kind, PublicKey};
|
||||||
use signed_core::{GitEvent, RepoStatus};
|
use signed_core::{GitEvent, RepoStatus};
|
||||||
use signed_state::{ProfileDirectory, RepoStore};
|
use signed_state::RepoStore;
|
||||||
use signed_ui::{Avatar, DropdownButton, SegmentButton, placeholder, status_badge};
|
use signed_ui::{Avatar, DropdownButton, SegmentButton, placeholder, status_badge};
|
||||||
use utils::relative_time;
|
use utils::relative_time;
|
||||||
|
|
||||||
|
|||||||
@@ -20,10 +20,11 @@ use gpui_component::{
|
|||||||
ActiveTheme, Disableable, Icon, IconName, Sizable, VirtualListScrollHandle, h_flex, v_flex,
|
ActiveTheme, Disableable, Icon, IconName, Sizable, VirtualListScrollHandle, h_flex, v_flex,
|
||||||
v_virtual_list,
|
v_virtual_list,
|
||||||
};
|
};
|
||||||
|
use metadata::ProfileDirectory;
|
||||||
use nostr::prelude::*;
|
use nostr::prelude::*;
|
||||||
use signed_core::{Announcement, RepoAddr};
|
use signed_core::{Announcement, RepoAddr};
|
||||||
use signed_git::{GitCache, Repo};
|
use signed_git::{GitCache, Repo};
|
||||||
use signed_state::{Backend, CheckoutsStore, Mirrors, ProfileDirectory, RepoListStore, RepoStore};
|
use signed_state::{Backend, CheckoutsStore, Mirrors, RepoListStore, RepoStore};
|
||||||
use signed_ui::{CountBadge, placeholder, ref_selector_trigger};
|
use signed_ui::{CountBadge, placeholder, ref_selector_trigger};
|
||||||
use utils::middle_truncate;
|
use utils::middle_truncate;
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ use gpui::prelude::*;
|
|||||||
use gpui::{AnyElement, App, Entity, SharedString, Window, div, px};
|
use gpui::{AnyElement, App, Entity, SharedString, Window, div, px};
|
||||||
use gpui_component::clipboard::Clipboard;
|
use gpui_component::clipboard::Clipboard;
|
||||||
use gpui_component::{ActiveTheme, StyledExt, WindowExt, h_flex, v_flex};
|
use gpui_component::{ActiveTheme, StyledExt, WindowExt, h_flex, v_flex};
|
||||||
|
use metadata::ProfileDirectory;
|
||||||
use nostr::prelude::PublicKey;
|
use nostr::prelude::PublicKey;
|
||||||
use signed_core::Announcement;
|
use signed_core::Announcement;
|
||||||
use signed_state::ProfileDirectory;
|
|
||||||
use signed_ui::Avatar;
|
use signed_ui::Avatar;
|
||||||
use utils::middle_truncate;
|
use utils::middle_truncate;
|
||||||
|
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ use gpui_component::spinner::Spinner;
|
|||||||
use gpui_component::{
|
use gpui_component::{
|
||||||
ActiveTheme, Colorize, Icon, IconName, Sizable, StyledExt, ThemeStyled, h_flex, v_flex,
|
ActiveTheme, Colorize, Icon, IconName, Sizable, StyledExt, ThemeStyled, h_flex, v_flex,
|
||||||
};
|
};
|
||||||
|
use metadata::ProfileDirectory;
|
||||||
use nostr::nips::nip19::Nip19Coordinate;
|
use nostr::nips::nip19::Nip19Coordinate;
|
||||||
use nostr::prelude::{PublicKey, RelayUrl, ToBech32, Url};
|
use nostr::prelude::{PublicKey, RelayUrl, ToBech32, Url};
|
||||||
use signed_core::{Announcement, RepoAddr, RepoStatus};
|
use signed_core::{Announcement, RepoAddr, RepoStatus};
|
||||||
use signed_git::{FileCommit, GitCache, RefSync, Repo};
|
use signed_git::{FileCommit, GitCache, RefSync, Repo};
|
||||||
use signed_state::{
|
use signed_state::{
|
||||||
Backend, CheckoutStatus, CheckoutSyncStatus, CheckoutsStore, LocalReposStore, Mirrors,
|
Backend, CheckoutStatus, CheckoutSyncStatus, CheckoutsStore, LocalReposStore, Mirrors,
|
||||||
Nip34Binding, Nip34Kind, ProfileDirectory, PushStepStatus, RepoListStore, RepoStore,
|
Nip34Binding, Nip34Kind, PushStepStatus, RepoListStore, RepoStore, SyncStatusStore,
|
||||||
SyncStatusStore,
|
|
||||||
};
|
};
|
||||||
use signed_ui::{
|
use signed_ui::{
|
||||||
Avatar, CountBadge, DropdownButton, copy_row, menu_copy_row, ref_selector_trigger,
|
Avatar, CountBadge, DropdownButton, copy_row, menu_copy_row, ref_selector_trigger,
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ use gpui_component::{
|
|||||||
ActiveTheme, Icon, IconName, Sizable, StyledExt, VirtualListScrollHandle, h_flex, v_flex,
|
ActiveTheme, Icon, IconName, Sizable, StyledExt, VirtualListScrollHandle, h_flex, v_flex,
|
||||||
v_virtual_list,
|
v_virtual_list,
|
||||||
};
|
};
|
||||||
|
use metadata::ProfileDirectory;
|
||||||
use nostr::prelude::PublicKey;
|
use nostr::prelude::PublicKey;
|
||||||
use signed_core::Announcement;
|
use signed_core::Announcement;
|
||||||
use signed_state::{ProfileDirectory, RepoListStore, Timestamp};
|
use signed_state::{RepoListStore, Timestamp};
|
||||||
use signed_ui::{Avatar, SegmentButton};
|
use signed_ui::{Avatar, SegmentButton};
|
||||||
use utils::relative_time;
|
use utils::relative_time;
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ use gpui_component::input::{Input, InputState, Textarea, TextareaState};
|
|||||||
use gpui_component::scroll::ScrollableElement;
|
use gpui_component::scroll::ScrollableElement;
|
||||||
use gpui_component::spinner::Spinner;
|
use gpui_component::spinner::Spinner;
|
||||||
use gpui_component::{ActiveTheme, Icon, IconName, Sizable, h_flex, v_flex};
|
use gpui_component::{ActiveTheme, Icon, IconName, Sizable, h_flex, v_flex};
|
||||||
use signed_state::{ProfileDirectory, RepoStore};
|
use metadata::ProfileDirectory;
|
||||||
|
use signed_state::RepoStore;
|
||||||
|
|
||||||
use crate::views::{repo_tab_avatar, tab_title};
|
use crate::views::{repo_tab_avatar, tab_title};
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,12 @@ use gpui_component::dialog::DialogButtonProps;
|
|||||||
use gpui_component::input::InputState;
|
use gpui_component::input::InputState;
|
||||||
use gpui_component::menu::{DropdownMenu, PopupMenuItem};
|
use gpui_component::menu::{DropdownMenu, PopupMenuItem};
|
||||||
use gpui_component::{ActiveTheme, Icon, IconName, Sizable, StyledExt, WindowExt, h_flex, v_flex};
|
use gpui_component::{ActiveTheme, Icon, IconName, Sizable, StyledExt, WindowExt, h_flex, v_flex};
|
||||||
|
use metadata::{Profile, ProfileDirectory};
|
||||||
use nostr::prelude::{PublicKey, RelayUrl};
|
use nostr::prelude::{PublicKey, RelayUrl};
|
||||||
use signed_core::{Announcement, RepoAddr};
|
use signed_core::{Announcement, RepoAddr};
|
||||||
use signed_state::{
|
use signed_state::{
|
||||||
Backend, BackendEvent, CheckoutsStore, LocalReposStore, Nip34Binding, Nip34Kind, Profile,
|
Backend, BackendEvent, CheckoutsStore, LocalReposStore, Nip34Binding, Nip34Kind, RepoListStore,
|
||||||
ProfileDirectory, RepoListStore, ResolvedLocalRepo, SyncStatusStore,
|
ResolvedLocalRepo, SyncStatusStore,
|
||||||
};
|
};
|
||||||
use signed_ui::{Avatar, NavItem, title_bar_drag_handlers};
|
use signed_ui::{Avatar, NavItem, title_bar_drag_handlers};
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ icons = [
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
assets = { path = "../crates/assets" }
|
assets = { path = "../crates/assets" }
|
||||||
|
metadata = { path = "../crates/metadata" }
|
||||||
paths = { path = "../crates/paths" }
|
paths = { path = "../crates/paths" }
|
||||||
settings = { path = "../crates/settings" }
|
settings = { path = "../crates/settings" }
|
||||||
signed_state = { path = "../crates/signed_state" }
|
signed_state = { path = "../crates/signed_state" }
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ fn main() {
|
|||||||
settings.local_repos.scan_paths,
|
settings.local_repos.scan_paths,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
metadata::init(cx);
|
||||||
|
|
||||||
cx.set_app_identity("info.reya.signed", "Signed");
|
cx.set_app_identity("info.reya.signed", "Signed");
|
||||||
|
|
||||||
|
|||||||
@@ -1,230 +0,0 @@
|
|||||||
# Per-view profile directories
|
|
||||||
|
|
||||||
Status: proposal
|
|
||||||
Reference: coop `a365a8f` "bind user metadata to per-view directories"
|
|
||||||
|
|
||||||
## Background
|
|
||||||
|
|
||||||
`ProfileStore` (`crates/signed_state/src/profile.rs`) is a single global entity:
|
|
||||||
|
|
||||||
- Warm-loads the 500 most recent metadata events at startup (`WARM_LIMIT`).
|
|
||||||
- `get(pk)` returns a cached clone or a placeholder and queues a network fetch
|
|
||||||
(flume channel, 500 ms batching, chunks of 100, `sync_bootstrap_only`).
|
|
||||||
- Subscribes to `BackendEvent::ProfileUpdates` to re-query the local database.
|
|
||||||
- Every view reads through `ProfileStore::global(cx)`.
|
|
||||||
|
|
||||||
Problems:
|
|
||||||
|
|
||||||
- Invalidation is global: every `cx.notify()` on the store invalidates every
|
|
||||||
panel that read it, so any profile update repaints all profile-consuming
|
|
||||||
surfaces (including `panel.cached` ones), not just the panel whose data
|
|
||||||
changed.
|
|
||||||
- Startup work and memory scale with the local database, not with what is on
|
|
||||||
screen, and the global map never shrinks.
|
|
||||||
- Every screen shares one data set, so nothing is cancellable or reclaimable.
|
|
||||||
|
|
||||||
## Goals
|
|
||||||
|
|
||||||
- Profile data is owned per panel/screen. A directory dies with its panel,
|
|
||||||
cancelling outstanding work and dropping its cache.
|
|
||||||
- An update re-renders only the panel whose data changed: reads during render
|
|
||||||
are tracked by the window, and `panel.cached` repaints a panel as soon as
|
|
||||||
its directory notifies.
|
|
||||||
- Local database first: query the local cache, fetch from relays only for keys
|
|
||||||
the directory cannot resolve.
|
|
||||||
- One shared network pipeline (batching, dedupe) so N panels still produce one
|
|
||||||
relay sync per key.
|
|
||||||
|
|
||||||
Non-goals: changing `Profile` accessors, changing `Backend`, persistence.
|
|
||||||
|
|
||||||
## Design
|
|
||||||
|
|
||||||
Two pieces replace the fat global:
|
|
||||||
|
|
||||||
1. `ProfileStore` (global, thin): network fetch service + event broadcaster.
|
|
||||||
Holds no profiles and does no warm load.
|
|
||||||
- `fetch(keys)`: dedupe via an in-flight set, batch via the existing flume
|
|
||||||
channel + 500 ms timeout, chunks of 100 through `sync_bootstrap_only`.
|
|
||||||
- Emits `ProfileEvent::Updated(Vec<PublicKey>)` (a) after each synced batch
|
|
||||||
and (b) when `BackendEvent::ProfileUpdates` arrives from the pump.
|
|
||||||
2. `ProfileDirectory` (per panel/screen entity): the metadata one surface
|
|
||||||
renders.
|
|
||||||
- `track(keys, cx)`: queries the local database for missing keys, inserts
|
|
||||||
what it finds, queues the rest for a network fetch.
|
|
||||||
- `profile(key, cx)`: cached value or placeholder; a miss marks the key
|
|
||||||
pending and queues a fetch (same call shape as today's `get`).
|
|
||||||
- Subscribes to `ProfileEvent::Updated`, refreshes only keys it tracks or
|
|
||||||
has pending, notifies itself on change.
|
|
||||||
- Stores its own tasks and subscription; both drop with the panel.
|
|
||||||
|
|
||||||
```
|
|
||||||
panel view ──track(keys)──▶ ProfileDirectory ──DB query──▶ local database
|
|
||||||
▲ │ keys not found
|
|
||||||
│ cx.notify() ▼
|
|
||||||
└────refresh(authors)── ProfileEvent::Updated ──── ProfileStore (global)
|
|
||||||
▲ │ batch +
|
|
||||||
│ │ sync_bootstrap_only
|
|
||||||
BackendEvent::ProfileUpdates ▼
|
|
||||||
relay pump ─────────────▶ relays
|
|
||||||
```
|
|
||||||
|
|
||||||
Panels own `Entity<ProfileDirectory>` and read it directly in render; no
|
|
||||||
`cx.observe` is needed. gpui-pre records which entities a window reads while
|
|
||||||
drawing (`App::record_entities_accessed`) and, on `cx.notify`, invalidates
|
|
||||||
exactly the views that read them (`App::notify` → `Window::invalidate_view`).
|
|
||||||
This is the mechanism coop relies on — its `Directory` only calls
|
|
||||||
`cx.notify()` — and it composes with `panel.cached(...)`, which repaints a
|
|
||||||
panel when the panel view itself is invalidated. Scoping data per directory is
|
|
||||||
what scopes invalidation: notifying the old global store would invalidate
|
|
||||||
every panel that ever read it, while notifying one directory invalidates only
|
|
||||||
the panels reading it.
|
|
||||||
|
|
||||||
## API sketch
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// signed_state/src/profile.rs
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum ProfileEvent {
|
|
||||||
/// Metadata for these authors may have changed in the local database.
|
|
||||||
Updated(Vec<PublicKey>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<ProfileEvent> for ProfileStore {}
|
|
||||||
|
|
||||||
/// Thin global: batches metadata requests from directories and syncs them
|
|
||||||
/// into the local database. Holds no profile data.
|
|
||||||
pub struct ProfileStore {
|
|
||||||
in_flight: RefCell<HashSet<PublicKey>>,
|
|
||||||
sender: Sender<PublicKey>,
|
|
||||||
_subscription: Subscription,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProfileStore {
|
|
||||||
pub fn global(cx: &App) -> Entity<Self>;
|
|
||||||
pub(crate) fn set_global(entity: Entity<Self>, cx: &mut App);
|
|
||||||
/// Queue a batched metadata request; no-op for keys already in flight.
|
|
||||||
pub fn fetch(&self, keys: impl IntoIterator<Item = PublicKey>);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Metadata cache for one panel or screen.
|
|
||||||
pub struct ProfileDirectory {
|
|
||||||
profiles: HashMap<PublicKey, Profile>,
|
|
||||||
pending: RefCell<HashSet<PublicKey>>,
|
|
||||||
tasks: SmallVec<[Task<()>; 2]>,
|
|
||||||
_subscriptions: SmallVec<[Subscription; 1]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProfileDirectory {
|
|
||||||
/// Create a directory, following the profile store's events.
|
|
||||||
pub fn new(cx: &mut App) -> Entity<Self>;
|
|
||||||
/// Resolve keys from the local database, queue the rest for fetch.
|
|
||||||
pub fn track(&mut self, keys: impl IntoIterator<Item = PublicKey>, cx: &mut Context<Self>);
|
|
||||||
/// The profile for a key, or a placeholder that queues a fetch.
|
|
||||||
pub fn profile(&self, public_key: &PublicKey, cx: &App) -> Profile;
|
|
||||||
/// Re-query tracked/pending keys named by a `ProfileEvent`.
|
|
||||||
fn refresh(&mut self, authors: &[PublicKey], cx: &mut Context<Self>);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Panel wiring:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub struct IssuesView {
|
|
||||||
// ...
|
|
||||||
directory: Entity<ProfileDirectory>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// constructor — no observe wiring; render-time reads of the directory are
|
|
||||||
// tracked automatically and `cx.notify()` on it invalidates the panel.
|
|
||||||
let directory = ProfileDirectory::new(cx);
|
|
||||||
```
|
|
||||||
|
|
||||||
Behavior details carried over from coop's `Directory`:
|
|
||||||
|
|
||||||
- The `pending` set guards the window between `track`/`profile` and the
|
|
||||||
database task finishing, so events arriving mid-query are still accepted.
|
|
||||||
- Inserts never overwrite data that already arrived from an event
|
|
||||||
(insert-if-absent on bulk load).
|
|
||||||
- `refresh` removes a pending key only when the database produced a profile,
|
|
||||||
so keys that exist on no relay stay pending and never loop into refetches;
|
|
||||||
`in_flight` in the store is released on every `Updated` emission.
|
|
||||||
|
|
||||||
## Where directories live
|
|
||||||
|
|
||||||
| Surface | File | Keys tracked | When |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| `SidebarPanel` | `views/sidebar/mod.rs` | current user, local repo owners | on `SignerChanged`, on local repos update |
|
|
||||||
| `RepoListView` | `views/repo_list.rs` | announcement owners | when cards rebuild |
|
|
||||||
| `RepoDetailView` | `views/repo/mod.rs`, `repo/about.rs` | owner, maintainers, binding owners | on announcement/sync; about dialog captures the handle |
|
|
||||||
| `IssuesView` | `views/issues/mod.rs` | issue authors, repo owner (tab title) | in `rebuild` |
|
|
||||||
| `IssueDetailView` | `views/issues/detail.rs` | author, discussion participants | on store events; passed to `discussion::` helpers |
|
|
||||||
| `PullRequestsView` | `views/pull_requests/mod.rs` | PR authors, repo owner | in `rebuild` |
|
|
||||||
| `PullRequestDetailView` | `views/pull_requests/detail.rs` | author, participants | on store events |
|
|
||||||
| `NewPullRequestView`, `SendPatchView`, `CommitDiffView` | respective files | repo owner (tab title only) | on construction |
|
|
||||||
| `InboxView` | `views/inbox.rs` | activity authors | in rows rebuild |
|
|
||||||
|
|
||||||
Helpers that stop reading the global and take `&Entity<ProfileDirectory>`
|
|
||||||
instead — every caller has `&mut self` or captures the handle:
|
|
||||||
|
|
||||||
- `repo_tab_avatar(store, cx)` in `views/mod.rs` (all `Panel::title` impls)
|
|
||||||
- `discussion::sidebar_section`, `discussion::comments_section`
|
|
||||||
- `about::maintainers` via `open_about_dialog` (dialog closure captures the
|
|
||||||
entity handle, same as coop's device-registry notification)
|
|
||||||
- `bound_repo_label` in `views/repo/mod.rs`
|
|
||||||
- `sub_activity` in `views/inbox.rs`, `local_avatar` in `views/sidebar/mod.rs`
|
|
||||||
|
|
||||||
## Migration steps
|
|
||||||
|
|
||||||
1. `signed_state`: introduce `ProfileEvent` + `EventEmitter`, extract the
|
|
||||||
"latest metadata per author" reduction from `apply_authors` into a pure
|
|
||||||
`fn latest_profiles(events: Vec<Event>) -> Vec<Profile>`, and strip
|
|
||||||
`ProfileStore` down to the fetch service (drop `profiles`, `seen`, `load`,
|
|
||||||
`apply_seen`, `apply_authors`, `get`, `WARM_LIMIT`). The batch loop emits
|
|
||||||
`ProfileEvent::Updated` instead of querying; the `BackendEvent::ProfileUpdates`
|
|
||||||
subscription re-emits and releases in-flight keys.
|
|
||||||
2. `signed_state`: add `ProfileDirectory` in the same `profile.rs` and export
|
|
||||||
it from `lib.rs`. `init` keeps creating the global store.
|
|
||||||
3. `workspace`: add a `directory` field to each surface in the table, replace
|
|
||||||
`ProfileStore::global(cx).read(cx).get(...)` with
|
|
||||||
`self.directory.read(cx).profile(...)`, thread the handle through the free
|
|
||||||
helpers, and call `track` from each surface's data-load/rebuild path.
|
|
||||||
4. Delete `ProfileStore::get` last; the compiler walks the remaining call
|
|
||||||
sites. The workspace crate will not compile between steps 1 and 3, so this
|
|
||||||
lands as one change.
|
|
||||||
|
|
||||||
## Edge cases
|
|
||||||
|
|
||||||
- **Relay event during a database query**: covered by the pending set.
|
|
||||||
- **Keys with no profile on relays**: stay pending/in-flight, no retry storm
|
|
||||||
(strictly no worse than today's permanent `seen` dedupe).
|
|
||||||
- **Panel closed**: directory drops → subscription, tasks, and cache drop; no
|
|
||||||
cross-panel notifies.
|
|
||||||
- **Same person on many panels**: each directory keeps its own `Profile`
|
|
||||||
clone. Metadata JSON is small and queries are local index lookups; if this
|
|
||||||
ever shows up, a directory can attach to `RepoStore` later.
|
|
||||||
- **Own profile edits**: publishing flows through `Backend` → pump →
|
|
||||||
`ProfileUpdates` → store event → directories refresh, so no extra channel is
|
|
||||||
needed.
|
|
||||||
- **Dock tab titles**: `Panel::title` receives `&mut self`, so panels pass
|
|
||||||
their own directory to `repo_tab_avatar`; no global needed for chrome.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
- Unit: `latest_profiles` keeps the newest event per author (pure, no GPUI).
|
|
||||||
- Unit: `ProfileDirectory` pending-set dedupe and insert-if-absent merge via
|
|
||||||
`TestAppContext` (the dock crate already uses GPUI test contexts), seeding
|
|
||||||
the directory through a `#[cfg(test)]` helper rather than the network.
|
|
||||||
- Manual: cold start with an empty database, open explore / repo / issues /
|
|
||||||
inbox, confirm placeholders resolve without interacting with each panel, and
|
|
||||||
that `panel.cached` tabs still repaint on profile arrival.
|
|
||||||
|
|
||||||
## Alternatives considered
|
|
||||||
|
|
||||||
- **Attach the directory to `RepoStore`** instead of panels: repo surfaces
|
|
||||||
sharing one store would share one directory, but `RepoStore` instances are
|
|
||||||
created per open and sidebar/inbox/explore still need their own; revisit
|
|
||||||
only if duplicated queries ever matter.
|
|
||||||
- **Keep the global store as-is**: read tracking already propagates updates,
|
|
||||||
but the warm load, unbounded cache, and all-panels invalidation remain; does
|
|
||||||
not meet the goals.
|
|
||||||
Reference in New Issue
Block a user