This commit is contained in:
2026-09-04 17:23:10 +07:00
parent 1496b7afeb
commit 15c2122f38
13 changed files with 63 additions and 46 deletions
Generated
-1
View File
@@ -7900,7 +7900,6 @@ dependencies = [
name = "signed_core" name = "signed_core"
version = "1.0.0" version = "1.0.0"
dependencies = [ dependencies = [
"gpui",
"nostr", "nostr",
] ]
+4
View File
@@ -2,10 +2,12 @@ use std::path::PathBuf;
use std::sync::OnceLock; use std::sync::OnceLock;
/// The application name. /// The application name.
///
/// It derives the platform-specific data, config and cache directory paths. /// It derives the platform-specific data, config and cache directory paths.
pub const APP_NAME: &str = "Signed"; pub const APP_NAME: &str = "Signed";
/// Lowercased form of [`APP_NAME`]. /// Lowercased form of [`APP_NAME`].
///
/// Used in XDG-style paths on Linux and FreeBSD, and the macOS `~/.config` fallback. /// Used in XDG-style paths on Linux and FreeBSD, and the macOS `~/.config` fallback.
pub const APP_NAME_LOWERCASE: &str = "signed"; pub const APP_NAME_LOWERCASE: &str = "signed";
@@ -27,12 +29,14 @@ pub fn home_dir() -> PathBuf {
} }
/// Returns the current user's Desktop folder. /// Returns the current user's Desktop folder.
///
/// Falls back to the home directory or an empty path when it cannot be determined. /// Falls back to the home directory or an empty path when it cannot be determined.
pub fn desktop_dir() -> PathBuf { pub fn desktop_dir() -> PathBuf {
dirs::desktop_dir().unwrap_or_else(|| dirs::home_dir().unwrap_or_default()) dirs::desktop_dir().unwrap_or_else(|| dirs::home_dir().unwrap_or_default())
} }
/// Returns the current user's Documents folder. /// Returns the current user's Documents folder.
///
/// Falls back to the home directory or an empty path when it cannot be determined. /// Falls back to the home directory or an empty path when it cannot be determined.
pub fn documents_dir() -> PathBuf { pub fn documents_dir() -> PathBuf {
dirs::document_dir().unwrap_or_else(|| dirs::home_dir().unwrap_or_default()) dirs::document_dir().unwrap_or_else(|| dirs::home_dir().unwrap_or_default())
-1
View File
@@ -5,5 +5,4 @@ edition.workspace = true
publish.workspace = true publish.workspace = true
[dependencies] [dependencies]
gpui.workspace = true
nostr.workspace = true nostr.workspace = true
+16 -18
View File
@@ -1,6 +1,5 @@
use std::collections::HashSet; use std::collections::HashSet;
use gpui::SharedString;
use nostr::prelude::*; use nostr::prelude::*;
use crate::RepoAddr; use crate::RepoAddr;
@@ -16,8 +15,8 @@ pub struct Announcement {
pub owner: PublicKey, pub owner: PublicKey,
/// When the announcement was published, used for latest-wins resolution. /// When the announcement was published, used for latest-wins resolution.
pub created_at: Timestamp, pub created_at: Timestamp,
pub name: Option<SharedString>, pub name: Option<String>,
pub description: Option<SharedString>, pub description: Option<String>,
/// Webpage URLs for browsing. /// Webpage URLs for browsing.
pub web: Vec<Url>, pub web: Vec<Url>,
/// URLs for `git clone`. /// URLs for `git clone`.
@@ -62,17 +61,17 @@ impl Upstream {
} }
/// Text for display. /// Text for display.
pub fn display(&self) -> SharedString { pub fn display(&self) -> String {
match &self.addr { match &self.addr {
Some(addr) => SharedString::from(addr.to_string()), Some(addr) => addr.to_string(),
None => SharedString::from(self.raw.clone()), None => self.raw.clone(),
} }
} }
} }
/// Subject of a NIP-34 issue or pull request event. /// Subject of a NIP-34 issue or pull request event.
/// Taken from the `subject` tag, else the first non-empty line of the content. /// Taken from the `subject` tag, else the first non-empty line of the content.
pub fn activity_subject(event: &Event) -> SharedString { pub fn activity_subject(event: &Event) -> String {
let subject = event let subject = event
.tags .tags
.iter() .iter()
@@ -82,16 +81,15 @@ pub fn activity_subject(event: &Event) -> SharedString {
}); });
subject subject
.map(SharedString::from)
.or_else(|| { .or_else(|| {
event event
.content .content
.lines() .lines()
.map(str::trim) .map(str::trim)
.find(|line| !line.is_empty()) .find(|line| !line.is_empty())
.map(SharedString::from) .map(|value| value.to_string())
}) })
.unwrap_or(SharedString::from("Untitled")) .unwrap_or("Untitled".to_string())
} }
/// The patch set of a pull request. /// The patch set of a pull request.
@@ -219,8 +217,8 @@ impl Announcement {
let mut hashtags: Vec<String> = Vec::new(); let mut hashtags: Vec<String> = Vec::new();
hashtags.extend(event.tags.hashtags().map(|t| t.to_string())); hashtags.extend(event.tags.hashtags().map(|t| t.to_string()));
let mut name: Option<SharedString> = None; let mut name: Option<String> = None;
let mut description: Option<SharedString> = None; let mut description: Option<String> = None;
let mut web: Vec<Url> = Vec::new(); let mut web: Vec<Url> = Vec::new();
let mut clone: Vec<Url> = Vec::new(); let mut clone: Vec<Url> = Vec::new();
let mut relays: Vec<RelayUrl> = Vec::new(); let mut relays: Vec<RelayUrl> = Vec::new();
@@ -230,8 +228,8 @@ impl Announcement {
for tag in event.tags.iter() { for tag in event.tags.iter() {
match Nip34Tag::parse(tag.as_slice()) { match Nip34Tag::parse(tag.as_slice()) {
Ok(Nip34Tag::Name(value)) => name = Some(value.into()), Ok(Nip34Tag::Name(value)) => name = Some(value),
Ok(Nip34Tag::Description(value)) => description = Some(value.into()), Ok(Nip34Tag::Description(value)) => description = Some(value),
Ok(Nip34Tag::Web(urls)) => web.extend(urls), Ok(Nip34Tag::Web(urls)) => web.extend(urls),
Ok(Nip34Tag::Clone(urls)) => clone.extend(urls), Ok(Nip34Tag::Clone(urls)) => clone.extend(urls),
Ok(Nip34Tag::Relays(urls)) => relays.extend(urls), Ok(Nip34Tag::Relays(urls)) => relays.extend(urls),
@@ -288,10 +286,10 @@ impl Announcement {
} }
/// The description of the repository, or a default if none is provided. /// The description of the repository, or a default if none is provided.
pub fn description(&self) -> SharedString { pub fn description(&self) -> String {
self.description self.description
.clone() .clone()
.unwrap_or(SharedString::from("No description")) .unwrap_or("No description".to_string())
} }
/// The effective maintainers of this repository, /// The effective maintainers of this repository,
@@ -307,11 +305,11 @@ impl Announcement {
} }
/// The `git clone` URLs for this repository, deduplicated. /// The `git clone` URLs for this repository, deduplicated.
pub fn clone_urls(&self) -> Vec<SharedString> { pub fn clone_urls(&self) -> Vec<String> {
let mut seen = HashSet::new(); let mut seen = HashSet::new();
self.clone self.clone
.iter() .iter()
.map(|url| SharedString::from(format!("git clone {url}"))) .map(|url| format!("git clone {url}"))
.filter(|command| seen.insert(command.clone())) .filter(|command| seen.insert(command.clone()))
.collect() .collect()
} }
+1 -1
View File
@@ -153,7 +153,7 @@ impl RepoStore {
self.announcement self.announcement
.as_ref() .as_ref()
.map_or(SharedString::default(), |a| { .map_or(SharedString::default(), |a| {
a.name.clone().unwrap_or(SharedString::from("Unknown")) SharedString::from(a.name.as_deref().unwrap_or("Unknown"))
}) })
} }
+4 -1
View File
@@ -5,10 +5,13 @@ use gpui_component::menu::PopupMenuItem;
use gpui_component::{ActiveTheme, StyledExt, h_flex}; use gpui_component::{ActiveTheme, StyledExt, h_flex};
/// A muted command row with a copy button. /// A muted command row with a copy button.
pub fn copy_row<E>(copy_id: E, command: &SharedString, cx: &App) -> Div pub fn copy_row<E, T>(copy_id: E, command: T, cx: &App) -> Div
where where
E: Into<ElementId>, E: Into<ElementId>,
T: Into<SharedString>,
{ {
let command = command.into();
h_flex() h_flex()
.h_8() .h_8()
.w_full() .w_full()
@@ -30,8 +30,9 @@ fn announcement_rows(announcement: &Announcement, cx: &App) -> AnyElement {
text( text(
announcement announcement
.name .name
.clone() .as_deref()
.unwrap_or_else(|| SharedString::from("")), .map(SharedString::from)
.unwrap_or_else(|| SharedString::from("-")),
), ),
cx, cx,
)); ));
@@ -41,8 +42,9 @@ fn announcement_rows(announcement: &Announcement, cx: &App) -> AnyElement {
text( text(
announcement announcement
.description .description
.clone() .as_deref()
.unwrap_or_else(|| SharedString::from("")), .map(SharedString::from)
.unwrap_or_else(|| SharedString::from("-")),
), ),
cx, cx,
)); ));
@@ -131,7 +133,12 @@ fn row(label: &'static str, value: AnyElement, cx: &App) -> AnyElement {
} }
/// Plain text value, wrapping within the dialog. /// Plain text value, wrapping within the dialog.
fn text(value: SharedString) -> AnyElement { fn text<T>(value: T) -> AnyElement
where
T: Into<SharedString>,
{
let value = value.into();
div() div()
.text_sm() .text_sm()
.w_full() .w_full()
@@ -143,7 +143,7 @@ impl RepoDetailView {
self.code_element(path.as_ref(), cx) self.code_element(path.as_ref(), cx)
} }
} }
Some(FileContent::Binary) => placeholder("Binary file preview not supported", cx), Some(FileContent::Binary) => placeholder("Binary file - preview not supported", cx),
Some(FileContent::TooLarge) => placeholder("File is too large to preview", cx), Some(FileContent::TooLarge) => placeholder("File is too large to preview", cx),
Some(FileContent::Failed(message)) => placeholder(message, cx), Some(FileContent::Failed(message)) => placeholder(message, cx),
None => preview_spinner(), None => preview_spinner(),
@@ -1342,7 +1342,8 @@ impl RepoDetailView {
.map(|announcement| { .map(|announcement| {
announcement announcement
.name .name
.clone() .as_deref()
.map(SharedString::from)
.unwrap_or_else(|| SharedString::from(announcement.id.clone())) .unwrap_or_else(|| SharedString::from(announcement.id.clone()))
}) })
.unwrap_or_default() .unwrap_or_default()
@@ -2359,13 +2360,14 @@ fn fork_row(announcement: &Announcement, cx: &mut Context<RepoDetailView>) -> Op
.find(|a| a.addr() == *addr) .find(|a| a.addr() == *addr)
.map(|a| { .map(|a| {
a.name a.name
.clone() .as_deref()
.map(SharedString::from)
.unwrap_or_else(|| SharedString::from(a.id.clone())) .unwrap_or_else(|| SharedString::from(a.id.clone()))
}) })
.unwrap_or_else(|| SharedString::from(addr.identifier.clone())); .unwrap_or_else(|| SharedString::from(addr.identifier.clone()));
(SharedString::from(format!("Forked from {name}")), true) (SharedString::from(format!("Forked from {name}")), true)
} }
None => (upstream.display(), false), None => (SharedString::from(upstream.display().as_str()), false),
}; };
let row = h_flex() let row = h_flex()
@@ -140,7 +140,8 @@ fn fork_candidates<'a>(
fn fork_display_name(announcement: &Announcement) -> SharedString { fn fork_display_name(announcement: &Announcement) -> SharedString {
announcement announcement
.name .name
.clone() .as_deref()
.map(SharedString::from)
.unwrap_or_else(|| SharedString::from(announcement.id.clone())) .unwrap_or_else(|| SharedString::from(announcement.id.clone()))
} }
+7 -4
View File
@@ -187,12 +187,14 @@ impl RepoListView {
let name = announcement let name = announcement
.name .name
.clone() .as_deref()
.unwrap_or_else(|| SharedString::from(announcement.id.clone())); .map(SharedString::from)
.unwrap_or(SharedString::from(announcement.id.clone()));
let description = announcement let description = announcement
.description .description
.clone() .as_deref()
.map(SharedString::from)
.unwrap_or(SharedString::from("No description")); .unwrap_or(SharedString::from("No description"));
let activity = last_activity let activity = last_activity
@@ -213,7 +215,8 @@ impl RepoListView {
.find(|a| a.addr() == *addr) .find(|a| a.addr() == *addr)
.map(|a| { .map(|a| {
a.name a.name
.clone() .as_deref()
.map(SharedString::from)
.unwrap_or_else(|| SharedString::from(a.id.clone())) .unwrap_or_else(|| SharedString::from(a.id.clone()))
}) })
.unwrap_or_else(|| SharedString::from(addr.identifier.clone())); .unwrap_or_else(|| SharedString::from(addr.identifier.clone()));
+2 -1
View File
@@ -347,7 +347,8 @@ impl SidebarPanel {
) -> impl IntoElement { ) -> impl IntoElement {
let name = announcement let name = announcement
.name .name
.clone() .as_deref()
.map(SharedString::from)
.unwrap_or_else(|| SharedString::from(announcement.id.clone())); .unwrap_or_else(|| SharedString::from(announcement.id.clone()));
let avatar = PixelAvatar::new(format!("{}:{}", announcement.owner, announcement.id)); let avatar = PixelAvatar::new(format!("{}:{}", announcement.owner, announcement.id));
+9 -9
View File
@@ -55,19 +55,19 @@ Key points of the write side:
branches; all git ops run in that folder. Checkouts of the target repo branches; all git ops run in that folder. Checkouts of the target repo
are remembered (folder pick + app clones) and matched implicitly are remembered (folder pick + app clones) and matched implicitly
(origin URL or EUC against the announcement), so the panel prefills the (origin URL or EUC against the announcement), so the panel prefills the
freshest one no folder dialog for the common case. freshest one - no folder dialog for the common case.
- *Announced fork*: the fork's heads are fetched into the target repo's - *Announced fork*: the fork's heads are fetched into the target repo's
GitCache mirror under `refs/fork/<owner-hex>/<id>/*` (private GitCache mirror under `refs/fork/<owner-hex>/<id>/*` (private
namespace; the browser never sees them). "Merge Into" lists the namespace; the browser never sees them). "Merge Into" lists the
mirror's `refs/remotes/origin/*`, "Pull From" the imported fork mirror's `refs/remotes/origin/*`, "Pull From" the imported fork
branches, and every git op merge-base, range diff/commits, branches, and every git op - merge-base, range diff/commits,
format-patch, tip push runs in the mirror, which holds both format-patch, tip push - runs in the mirror, which holds both
histories. Fork candidates are announcements related to the target by histories. Fork candidates are announcements related to the target by
`u` tag or shared EUC, own forks first, without `clone` URLs excluded. `u` tag or shared EUC, own forks first, without `clone` URLs excluded.
- **GRASP-06 hosting**: the tip is pushed under `refs/nostr/<event-id>` - **GRASP-06 hosting**: the tip is pushed under `refs/nostr/<event-id>`
(nak's convention) to the *author's* grasp servers first (nak's convention) to the *author's* grasp servers first -
`https://<host>/prs/<author-npub>/<repo-id>.git`, resolved from the `https://<host>/prs/<author-npub>/<repo-id>.git`, resolved from the
author's kind-10317 grasp list, falling back to the settings defaults author's kind-10317 grasp list, falling back to the settings defaults -
then to the base repository's announced grasp servers. The `clone` tag then to the base repository's announced grasp servers. The `clone` tag
lists those `/prs/` URLs first, then the announced clone URLs (fixed lists those `/prs/` URLs first, then the announced clone URLs (fixed
before signing; dead URLs are inert, the patches stay the source of before signing; dead URLs are inert, the patches stay the source of
@@ -77,8 +77,8 @@ Key points of the write side:
grows past NIP-34's 60 KB guidance; the PR's `c` tag carries the *last* grows past NIP-34's 60 KB guidance; the PR's `c` tag carries the *last*
commit of the series (the tip), and each part carries its own commit of the series (the tip), and each part carries its own
`commit`/`r` tags. `commit`/`r` tags.
- **Push before publish**: failure is non-fatal the patch events remain - **Push before publish**: failure is non-fatal - the patch events remain
the source of truth and surfaces as a `last_warning` banner. the source of truth - and surfaces as a `last_warning` banner.
- **1619 updates are paste-only today** (no repo path holds the new tip's - **1619 updates are paste-only today** (no repo path holds the new tip's
objects), so updates are not pushed; hosting them is deferred until the objects), so updates are not pushed; hosting them is deferred until the
update dialog gains a local-checkout source. update dialog gains a local-checkout source.
@@ -120,7 +120,7 @@ thread: current branch vs its base (announced HEAD, else `main`, else the
first branch), commits ahead, dirty worktrees excluded. A banner in the first branch), commits ahead, dirty worktrees excluded. A banner in the
repository panel then offers a prefilled New PR panel for the first branch repository panel then offers a prefilled New PR panel for the first branch
that is ahead with **no open PR by you** proposing it (`branch-name` tag, that is ahead with **no open PR by you** proposing it (`branch-name` tag,
falling back to the `c` tip tag) NIP-34-native dedupe, refreshed falling back to the `c` tip tag) - NIP-34-native dedupe, refreshed
periodically and whenever the checkouts/announcements change. The panel periodically and whenever the checkouts/announcements change. The panel
never submits anything on its own; suggestions only navigate and prefill. never submits anything on its own; suggestions only navigate and prefill.
@@ -165,7 +165,7 @@ Reader rules that keep the flow consistent:
- **Status**: only status events by the root author or a repository - **Status**: only status events by the root author or a repository
maintainer count; the newest wins, `Open` is the default. maintainer count; the newest wins, `Open` is the default.
- **Tip**: only kind-1619 updates by the PR author move the tip a - **Tip**: only kind-1619 updates by the PR author move the tip - a
stranger's update is ignored. stranger's update is ignored.
- **Diff**: the patch set is preferred (NIP-34 `e`-linked chain); PRs from - **Diff**: the patch set is preferred (NIP-34 `e`-linked chain); PRs from
other clients without patch events fall back to diffing other clients without patch events fall back to diffing