add issue detail

This commit is contained in:
2026-08-21 19:45:25 +07:00
parent 052c30d12b
commit 9bdc3daa7e
8 changed files with 429 additions and 111 deletions
Generated
+37 -37
View File
@@ -1268,7 +1268,7 @@ dependencies = [
[[package]] [[package]]
name = "collections" name = "collections"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1b04e4caf01e376624fb514ef85b0e6d8ee5d930" source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637"
dependencies = [ dependencies = [
"gpui_util", "gpui_util",
"indexmap", "indexmap",
@@ -1734,7 +1734,7 @@ dependencies = [
[[package]] [[package]]
name = "derive_refineable" name = "derive_refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1b04e4caf01e376624fb514ef85b0e6d8ee5d930" source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1887,9 +1887,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]] [[package]]
name = "either" name = "either"
version = "1.17.0" version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
[[package]] [[package]]
name = "embed-resource" name = "embed-resource"
@@ -3494,7 +3494,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui" name = "gpui"
version = "0.2.2" version = "0.2.2"
source = "git+https://github.com/zed-industries/zed#1b04e4caf01e376624fb514ef85b0e6d8ee5d930" source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637"
dependencies = [ dependencies = [
"accesskit", "accesskit",
"anyhow", "anyhow",
@@ -3567,7 +3567,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui-base" name = "gpui-base"
version = "0.5.2" version = "0.5.2"
source = "git+https://github.com/longbridge/gpui-component#270cf3ce8cf6f4cdac28db5c30dd92631849ed6e" source = "git+https://github.com/longbridge/gpui-component#14359a0cdd00657eb68fa946bfd99cd83c26e002"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"anyhow", "anyhow",
@@ -3599,7 +3599,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui-component" name = "gpui-component"
version = "0.5.2" version = "0.5.2"
source = "git+https://github.com/longbridge/gpui-component#270cf3ce8cf6f4cdac28db5c30dd92631849ed6e" source = "git+https://github.com/longbridge/gpui-component#14359a0cdd00657eb68fa946bfd99cd83c26e002"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -3679,7 +3679,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui-component-assets" name = "gpui-component-assets"
version = "0.5.1" version = "0.5.1"
source = "git+https://github.com/longbridge/gpui-component#270cf3ce8cf6f4cdac28db5c30dd92631849ed6e" source = "git+https://github.com/longbridge/gpui-component#14359a0cdd00657eb68fa946bfd99cd83c26e002"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"gpui", "gpui",
@@ -3693,7 +3693,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui-component-macros" name = "gpui-component-macros"
version = "0.5.1" version = "0.5.1"
source = "git+https://github.com/longbridge/gpui-component#270cf3ce8cf6f4cdac28db5c30dd92631849ed6e" source = "git+https://github.com/longbridge/gpui-component#14359a0cdd00657eb68fa946bfd99cd83c26e002"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -3703,7 +3703,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_apple" name = "gpui_apple"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1b04e4caf01e376624fb514ef85b0e6d8ee5d930" source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"block", "block",
@@ -3726,7 +3726,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_linux" name = "gpui_linux"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1b04e4caf01e376624fb514ef85b0e6d8ee5d930" source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637"
dependencies = [ dependencies = [
"accesskit", "accesskit",
"accesskit_unix", "accesskit_unix",
@@ -3772,7 +3772,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_macos" name = "gpui_macos"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1b04e4caf01e376624fb514ef85b0e6d8ee5d930" source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637"
dependencies = [ dependencies = [
"accesskit", "accesskit",
"accesskit_macos", "accesskit_macos",
@@ -3818,7 +3818,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_macros" name = "gpui_macros"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1b04e4caf01e376624fb514ef85b0e6d8ee5d930" source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@@ -3829,7 +3829,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_platform" name = "gpui_platform"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1b04e4caf01e376624fb514ef85b0e6d8ee5d930" source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637"
dependencies = [ dependencies = [
"console_error_panic_hook", "console_error_panic_hook",
"gpui", "gpui",
@@ -3842,7 +3842,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_shared_string" name = "gpui_shared_string"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1b04e4caf01e376624fb514ef85b0e6d8ee5d930" source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637"
dependencies = [ dependencies = [
"schemars", "schemars",
"serde", "serde",
@@ -3852,7 +3852,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_util" name = "gpui_util"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1b04e4caf01e376624fb514ef85b0e6d8ee5d930" source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"log", "log",
@@ -3862,7 +3862,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_web" name = "gpui_web"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1b04e4caf01e376624fb514ef85b0e6d8ee5d930" source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"console_error_panic_hook", "console_error_panic_hook",
@@ -3886,7 +3886,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_wgpu" name = "gpui_wgpu"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1b04e4caf01e376624fb514ef85b0e6d8ee5d930" source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytemuck", "bytemuck",
@@ -3912,7 +3912,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_windows" name = "gpui_windows"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1b04e4caf01e376624fb514ef85b0e6d8ee5d930" source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637"
dependencies = [ dependencies = [
"accesskit", "accesskit",
"accesskit_windows", "accesskit_windows",
@@ -3949,9 +3949,9 @@ dependencies = [
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.4.17" version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f877e75f39e9827ec50a572dd592684ac28c029578726c85f1b2aa6ab807449" checksum = "839c0e8a181239723652be9062bb56ca5bf5f64011f73b623f6f4fc59086a228"
dependencies = [ dependencies = [
"atomic-waker", "atomic-waker",
"bytes", "bytes",
@@ -4239,7 +4239,7 @@ dependencies = [
[[package]] [[package]]
name = "http_client" name = "http_client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1b04e4caf01e376624fb514ef85b0e6d8ee5d930" source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-compression", "async-compression",
@@ -4259,7 +4259,7 @@ dependencies = [
[[package]] [[package]]
name = "http_client_tls" name = "http_client_tls"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1b04e4caf01e376624fb514ef85b0e6d8ee5d930" source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637"
dependencies = [ dependencies = [
"rustls", "rustls",
"rustls-platform-verifier 0.5.3", "rustls-platform-verifier 0.5.3",
@@ -4434,9 +4434,9 @@ checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
[[package]] [[package]]
name = "icu_provider" name = "icu_provider"
version = "2.3.0" version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428" checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"icu_locale_core", "icu_locale_core",
@@ -5235,7 +5235,7 @@ dependencies = [
[[package]] [[package]]
name = "media" name = "media"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1b04e4caf01e376624fb514ef85b0e6d8ee5d930" source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bindgen", "bindgen",
@@ -6282,7 +6282,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]] [[package]]
name = "perf" name = "perf"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1b04e4caf01e376624fb514ef85b0e6d8ee5d930" source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637"
dependencies = [ dependencies = [
"collections", "collections",
"serde", "serde",
@@ -7079,7 +7079,7 @@ dependencies = [
[[package]] [[package]]
name = "refineable" name = "refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1b04e4caf01e376624fb514ef85b0e6d8ee5d930" source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637"
dependencies = [ dependencies = [
"derive_refineable", "derive_refineable",
] ]
@@ -7162,7 +7162,7 @@ dependencies = [
[[package]] [[package]]
name = "reqwest_client" name = "reqwest_client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1b04e4caf01e376624fb514ef85b0e6d8ee5d930" source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -7574,7 +7574,7 @@ dependencies = [
[[package]] [[package]]
name = "scheduler" name = "scheduler"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1b04e4caf01e376624fb514ef85b0e6d8ee5d930" source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637"
dependencies = [ dependencies = [
"async-task", "async-task",
"backtrace", "backtrace",
@@ -8301,7 +8301,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "sum_tree" name = "sum_tree"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1b04e4caf01e376624fb514ef85b0e6d8ee5d930" source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637"
dependencies = [ dependencies = [
"heapless 0.9.3", "heapless 0.9.3",
"log", "log",
@@ -9674,7 +9674,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]] [[package]]
name = "util_macros" name = "util_macros"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1b04e4caf01e376624fb514ef85b0e6d8ee5d930" source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637"
dependencies = [ dependencies = [
"perf", "perf",
"quote", "quote",
@@ -11454,9 +11454,9 @@ dependencies = [
[[package]] [[package]]
name = "zerovec-derive" name = "zerovec-derive"
version = "0.11.5" version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f212a141d820099d57ffafb9569be9617a6f27d3dc881fbee8fb56642f917a9" checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -11472,7 +11472,7 @@ checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12"
[[package]] [[package]]
name = "zlog" name = "zlog"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1b04e4caf01e376624fb514ef85b0e6d8ee5d930" source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -11489,7 +11489,7 @@ checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
[[package]] [[package]]
name = "ztracing" name = "ztracing"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1b04e4caf01e376624fb514ef85b0e6d8ee5d930" source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637"
dependencies = [ dependencies = [
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
@@ -11500,7 +11500,7 @@ dependencies = [
[[package]] [[package]]
name = "ztracing_macro" name = "ztracing_macro"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#1b04e4caf01e376624fb514ef85b0e6d8ee5d930" source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637"
[[package]] [[package]]
name = "zune-core" name = "zune-core"
+4 -2
View File
@@ -3,11 +3,12 @@ use nostr::prelude::*;
use crate::RepoAddr; use crate::RepoAddr;
/// Kinds that make up the activity of a repository. /// Kinds that make up the activity of a repository.
pub const ACTIVITY_KINDS: [Kind; 8] = [ pub const ACTIVITY_KINDS: [Kind; 9] = [
Kind::GitPatch, Kind::GitPatch,
Kind::GitPullRequest, Kind::GitPullRequest,
Kind::GitPullRequestUpdate, Kind::GitPullRequestUpdate,
Kind::GitIssue, Kind::GitIssue,
Kind::Comment,
Kind::GitStatusOpen, Kind::GitStatusOpen,
Kind::GitStatusApplied, Kind::GitStatusApplied,
Kind::GitStatusClosed, Kind::GitStatusClosed,
@@ -30,7 +31,8 @@ pub fn state(addr: &RepoAddr) -> Filter {
.identifier(addr.identifier.clone()) .identifier(addr.identifier.clone())
} }
/// All NIP-34 activity addressed to a repository (`#a` tag). /// All NIP-34 activity addressed to a repository (`#a` tag): issues, PRs,
/// patches, statuses and comments (kind 1111).
/// ///
/// Note: the `a` tag on status events is optional per NIP-34, so statuses /// Note: the `a` tag on status events is optional per NIP-34, so statuses
/// published without it won't be matched here. /// published without it won't be matched here.
+49 -14
View File
@@ -11,8 +11,9 @@ use crate::backend::{Backend, BackendEvent};
/// events (e.g. per-event `NostrUpdate`s) collapse into one query. /// events (e.g. per-event `NostrUpdate`s) collapse into one query.
const REFRESH_DEBOUNCE: Duration = Duration::from_millis(300); const REFRESH_DEBOUNCE: Duration = Duration::from_millis(300);
/// Per-repository store: announcement, state, issues, patches, PRs and /// Per-repository store: announcement, state, issues, patches, PRs,
/// their resolved statuses. Always derived from the local database. /// comments and their resolved statuses. Always derived from the local
/// database.
pub struct RepoStore { pub struct RepoStore {
addr: RepoAddr, addr: RepoAddr,
pub announcement: Option<Announcement>, pub announcement: Option<Announcement>,
@@ -23,6 +24,8 @@ pub struct RepoStore {
pub issues: Vec<Event>, pub issues: Vec<Event>,
pub patches: Vec<Event>, pub patches: Vec<Event>,
pub pull_requests: Vec<Event>, pub pull_requests: Vec<Event>,
/// Comments on issues / PRs, oldest first.
pub comments: Vec<Event>,
statuses: Vec<Event>, statuses: Vec<Event>,
/// Error of the last action initiated from this store, if any. /// Error of the last action initiated from this store, if any.
pub last_error: Option<String>, pub last_error: Option<String>,
@@ -73,6 +76,7 @@ impl RepoStore {
issues: Vec::new(), issues: Vec::new(),
patches: Vec::new(), patches: Vec::new(),
pull_requests: Vec::new(), pull_requests: Vec::new(),
comments: Vec::new(),
statuses: Vec::new(), statuses: Vec::new(),
last_error: None, last_error: None,
refreshing: false, refreshing: false,
@@ -172,8 +176,8 @@ impl RepoStore {
let all_states = states.into_iter().filter(|e| !deletions.is_deleted(e)); let all_states = states.into_iter().filter(|e| !deletions.is_deleted(e));
let state = latest(all_states).map(|state| parse_state(&state)); let state = latest(all_states).map(|state| parse_state(&state));
let (mut issues, mut patches, mut pull_requests, mut statuses) = let (mut issues, mut patches, mut pull_requests, mut statuses, mut comments) =
(Vec::new(), Vec::new(), Vec::new(), Vec::new()); (Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new());
for event in activity { for event in activity {
if deletions.is_deleted(&event) { if deletions.is_deleted(&event) {
@@ -183,6 +187,7 @@ impl RepoStore {
Kind::GitIssue => issues.push(event), Kind::GitIssue => issues.push(event),
Kind::GitPatch => patches.push(event), Kind::GitPatch => patches.push(event),
Kind::GitPullRequest | Kind::GitPullRequestUpdate => pull_requests.push(event), Kind::GitPullRequest | Kind::GitPullRequestUpdate => pull_requests.push(event),
Kind::Comment => comments.push(event),
kind if RepoStatus::from_kind(kind).is_some() => statuses.push(event), kind if RepoStatus::from_kind(kind).is_some() => statuses.push(event),
_ => {} _ => {}
} }
@@ -191,6 +196,7 @@ impl RepoStore {
sort_newest_first(&mut issues); sort_newest_first(&mut issues);
sort_newest_first(&mut patches); sort_newest_first(&mut patches);
sort_newest_first(&mut pull_requests); sort_newest_first(&mut pull_requests);
sort_oldest_first(&mut comments);
Ok::<_, Error>(( Ok::<_, Error>((
announcement, announcement,
@@ -199,21 +205,23 @@ impl RepoStore {
patches, patches,
pull_requests, pull_requests,
statuses, statuses,
comments,
)) ))
}); });
self.tasks.retain(|task| !task.is_ready()); 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 (announcement, state, issues, patches, pull_requests, statuses) = match work.await { let (announcement, state, issues, patches, pull_requests, statuses, comments) =
Ok(data) => data, match work.await {
Err(e) => { Ok(data) => data,
return this.update(cx, |this, cx| { Err(e) => {
this.refreshing = false; return this.update(cx, |this, cx| {
this.last_error = Some(e.to_string()); this.refreshing = false;
cx.notify(); this.last_error = Some(e.to_string());
}); cx.notify();
} });
}; }
};
let again = this.update(cx, |this, cx| { let again = this.update(cx, |this, cx| {
this.announcement = announcement; this.announcement = announcement;
@@ -226,6 +234,7 @@ impl RepoStore {
this.issues = issues; this.issues = issues;
this.patches = patches; this.patches = patches;
this.pull_requests = pull_requests; this.pull_requests = pull_requests;
this.comments = comments;
this.statuses = statuses; this.statuses = statuses;
cx.notify(); cx.notify();
@@ -297,6 +306,28 @@ impl RepoStore {
self.send(builder, cx); self.send(builder, cx);
} }
/// Comments on a root event (issue / PR), oldest first.
pub fn comments_of(&self, root: &EventId) -> impl Iterator<Item = &Event> {
self.comments
.iter()
.filter(move |e| signed_core::references_root(e, root))
}
/// Comment on a root event (issue / PR) per NIP-34 (kind 1111).
pub fn comment(&mut self, root: &Event, content: String, cx: &mut Context<Self>) {
let Ok(root_ref) = Tag::parse(["e", &root.id.to_hex(), "", "root"]) else {
return;
};
let builder = EventBuilder::new(Kind::Comment, content).tags([
root_ref,
Tag::public_key(root.pubkey),
Tag::coordinate(self.addr.clone(), None),
]);
self.send(builder, cx);
}
/// Open a pull request on this repository: a root PR event whose content /// Open a pull request on this repository: a root PR event whose content
/// is the `git format-patch` output of the proposed changes. /// is the `git format-patch` output of the proposed changes.
/// ///
@@ -390,6 +421,10 @@ fn sort_newest_first(events: &mut [Event]) {
events.sort_by_key(|e| std::cmp::Reverse(e.created_at)); events.sort_by_key(|e| std::cmp::Reverse(e.created_at));
} }
fn sort_oldest_first(events: &mut [Event]) {
events.sort_by_key(|e| e.created_at);
}
/// The proposed commit of a `git format-patch` output: the `From <commit>` /// The proposed commit of a `git format-patch` output: the `From <commit>`
/// header on its first line. /// header on its first line.
fn patch_current_commit(patch: &str) -> Option<&str> { fn patch_current_commit(patch: &str) -> Option<&str> {
@@ -262,6 +262,7 @@ impl RepoDetailView {
.selectable(true) .selectable(true)
.scrollable(true) .scrollable(true)
.p_4() .p_4()
.text_xs()
.into_any_element() .into_any_element()
} }
@@ -305,7 +306,7 @@ impl RepoDetailView {
.bordered(false) .bordered(false)
.rounded_none() .rounded_none()
.h_full() .h_full()
.text_sm() .text_xs()
.into_any_element() .into_any_element()
} }
} }
@@ -5,11 +5,14 @@ use std::collections::HashMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use anyhow::Error; use anyhow::Error;
use assets::CustomIconName;
use gpui::prelude::*; use gpui::prelude::*;
use gpui::{AnyElement, App, Task, Window, div, px}; use gpui::{AnyElement, App, Task, Window, div, px};
use gpui_component::list::ListItem; use gpui_component::list::ListItem;
use gpui_component::tooltip::Tooltip;
use gpui_component::tree::{TreeEntry, TreeItem}; use gpui_component::tree::{TreeEntry, TreeItem};
use gpui_component::{ActiveTheme, Icon, IconName, Sizable, h_flex, v_flex}; use gpui_component::{ActiveTheme, Icon, IconName, Sizable, h_flex, v_flex};
use signed_core::RepoStatus;
/// A `Send` file-tree node: the tree is built on a background thread and /// A `Send` file-tree node: the tree is built on a background thread and
/// converted into [`TreeItem`]s (which hold `Rc` state, so they cannot /// converted into [`TreeItem`]s (which hold `Rc` state, so they cannot
@@ -227,6 +230,53 @@ pub(super) fn placeholder(message: &str, cx: &App) -> AnyElement {
.into_any_element() .into_any_element()
} }
/// The status badge shown next to an issue or pull request: icon + colored
/// square, with a tooltip describing the status.
pub(super) fn status_badge(status: RepoStatus, cx: &App) -> AnyElement {
let (icon, label, tooltip, bg, fg) = match status {
RepoStatus::Open => (
CustomIconName::GitIssueOpen,
"open",
"Issue is open",
cx.theme().secondary,
cx.theme().secondary_foreground,
),
RepoStatus::Closed => (
CustomIconName::GitIssueClosed,
"closed",
"Issue is closed",
cx.theme().danger,
cx.theme().danger_foreground,
),
RepoStatus::Draft => (
CustomIconName::GitIssueOngoing,
"draft",
"Issue is draft",
cx.theme().accent,
cx.theme().accent_foreground,
),
RepoStatus::Applied => (
CustomIconName::GitIssueOpen,
"applied",
"Issue is completed",
cx.theme().primary,
cx.theme().primary_foreground,
),
};
v_flex()
.id(label)
.flex_shrink_0()
.size_7()
.items_center()
.justify_center()
.rounded(cx.theme().radius)
.bg(bg)
.child(Icon::new(icon).small().text_color(fg))
.tooltip(move |window, cx| Tooltip::new(tooltip).build(window, cx))
.into_any_element()
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -0,0 +1,238 @@
use dock::{Panel, PanelEvent};
use gpui::prelude::*;
use gpui::{
App, Context, Entity, EventEmitter, FocusHandle, Focusable, Render, SharedString, Window, div,
px,
};
use gpui_component::avatar::Avatar;
use gpui_component::button::{Button, ButtonVariants};
use gpui_component::input::{Textarea, TextareaState};
use gpui_component::{ActiveTheme, Sizable, StyledExt, h_flex, v_flex};
use nostr::prelude::{Event, EventId};
use signed_core::activity_subject;
use signed_state::{ProfileStore, RepoStore};
use utils::relative_time;
use super::helpers::{placeholder, status_badge};
/// Detail panel of a single issue.
pub struct IssueDetailView {
focus_handle: FocusHandle,
/// Repo store holding the issues and their statuses.
store: Entity<RepoStore>,
issue_id: EventId,
/// Input state of the "leave a comment" textarea.
comment_input: Entity<TextareaState>,
}
impl IssueDetailView {
pub fn new(
store: Entity<RepoStore>,
issue_id: EventId,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
// Issue author avatars stay in the shared cache until the panel closes.
crate::image_cache::clear_on_release(&cx.entity(), window, cx);
let comment_input =
cx.new(|cx| TextareaState::new(window, cx).placeholder("Leave a comment…"));
Self {
focus_handle: cx.focus_handle(),
store,
issue_id,
comment_input,
}
}
fn render_comments(&mut self, id: &EventId, cx: &mut Context<Self>) -> impl IntoElement {
let store = self.store.read(cx);
let comments: Vec<&Event> = store.comments_of(id).collect();
v_flex()
.gap_3()
.children(comments.iter().map(|comment| {
let profile = ProfileStore::global(cx).read(cx).get(&comment.pubkey);
let author = profile.name();
let picture = profile.picture();
let age = relative_time(comment.created_at);
v_flex()
.gap_1()
.child(
h_flex()
.gap_2()
.text_sm()
.child(
h_flex()
.gap_1()
.child(
Avatar::new()
.name(author.clone())
.when_some(picture, |this, url| this.src(url))
.xsmall(),
)
.child(author),
)
.child(
div()
.text_color(cx.theme().muted_foreground)
.child(SharedString::from(age)),
),
)
.child(
div()
.text_sm()
.child(SharedString::from(comment.content.clone())),
)
}))
.into_any_element()
}
fn render_form(&mut self, id: &EventId, _cx: &mut Context<Self>) -> impl IntoElement {
let comment_input = self.comment_input.clone();
let store = self.store.clone();
let id = id.to_owned();
v_flex()
.gap_2()
.child(Textarea::new(&self.comment_input).h(px(96.)))
.child(
h_flex().justify_end().child(
Button::new("comment")
.primary()
.label("Comment")
.tooltip("Post comment")
.on_click(move |_event, window, cx| {
let content = comment_input.read(cx).value().trim().to_string();
if content.is_empty() {
return;
}
let Some(root) = store
.read(cx)
.issues
.iter()
.find(|issue| issue.id == id)
.cloned()
else {
return;
};
store.update(cx, |store, cx| {
store.comment(&root, content, cx);
});
comment_input.update(cx, |input, cx| {
input.set_value("", window, cx);
});
}),
),
)
.into_any_element()
}
}
impl Panel for IssueDetailView {
fn panel_name(&self) -> &'static str {
"issue_detail"
}
fn title(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let short_id = self
.store
.read(cx)
.issues
.iter()
.find(|issue| issue.id == self.issue_id)
.map(|issue| {
let hex = issue.id.to_hex();
SharedString::from(&hex[..8])
})
.unwrap_or_else(|| SharedString::from("Issue"));
div().text_sm().child(short_id)
}
}
impl EventEmitter<PanelEvent> for IssueDetailView {}
impl Focusable for IssueDetailView {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for IssueDetailView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
// Extract everything owned first: the store borrow must end before
// the markdown state is (re)built below.
let (title, author, picture, status, age, issue_id, content) = {
let store = self.store.read(cx);
let Some(issue) = store.issues.iter().find(|issue| issue.id == self.issue_id) else {
return placeholder("Issue not found", cx);
};
let profile = ProfileStore::global(cx).read(cx).get(&issue.pubkey);
(
activity_subject(issue),
profile.name(),
profile.picture(),
store.status_of(issue),
relative_time(issue.created_at),
issue.id,
issue.content.clone(),
)
};
v_flex()
.id("issue-detail")
.size_full()
.overflow_y_scroll()
.gap_6()
.px_4()
.child(
h_flex()
.gap_2()
.items_center()
.child(status_badge(status, cx))
.child(div().font_semibold().child(title)),
)
.child(
v_flex()
.px_4()
.gap_8()
.child(
v_flex()
.gap_2()
.child(
h_flex()
.gap_2()
.text_sm()
.child(
h_flex()
.gap_1()
.child(
Avatar::new()
.name(author.clone())
.when_some(picture, |this, url| this.src(url))
.small(),
)
.child(author),
)
.child(
div()
.text_color(cx.theme().muted_foreground)
.child(SharedString::from("opened")),
)
.child(
div()
.text_color(cx.theme().muted_foreground)
.child(SharedString::from(age)),
),
)
.child(div().text_sm().child(SharedString::from(&content))),
)
.child(self.render_comments(&issue_id, cx))
.child(self.render_form(&issue_id, cx)),
)
.into_any_element()
}
}
@@ -3,13 +3,14 @@
//! the header's All/Open/Closed filter. //! the header's All/Open/Closed filter.
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc;
use assets::CustomIconName; use assets::CustomIconName;
use dock::{Panel, PanelEvent}; use dock::{DockArea, DockPlacement, Panel, PanelEvent};
use gpui::prelude::*; use gpui::prelude::*;
use gpui::{ use gpui::{
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Render, AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Render,
SharedString, Size, Window, div, px, relative, size, SharedString, Size, WeakEntity, Window, div, px, relative, size,
}; };
use gpui_base::Button as BaseButton; use gpui_base::Button as BaseButton;
use gpui_component::avatar::Avatar; use gpui_component::avatar::Avatar;
@@ -18,16 +19,16 @@ use gpui_component::dialog::{DialogDescription, DialogFooter, DialogHeader, Dial
use gpui_component::form::{field, v_form}; use gpui_component::form::{field, v_form};
use gpui_component::input::{Input, InputState, Textarea, TextareaState}; use gpui_component::input::{Input, InputState, Textarea, TextareaState};
use gpui_component::scroll::Scrollbar; use gpui_component::scroll::Scrollbar;
use gpui_component::tooltip::Tooltip;
use gpui_component::{ use gpui_component::{
ActiveTheme, Icon, Sizable, VirtualListScrollHandle, WindowExt, h_flex, v_flex, v_virtual_list, ActiveTheme, Icon, Sizable, VirtualListScrollHandle, WindowExt, h_flex, v_flex, v_virtual_list,
}; };
use nostr::prelude::Event; use nostr::prelude::{Event, EventId};
use signed_core::{RepoStatus, activity_subject}; use signed_core::{RepoStatus, activity_subject};
use signed_state::{ProfileStore, RepoStore}; use signed_state::{ProfileStore, RepoStore};
use utils::relative_time; use utils::relative_time;
use super::helpers::placeholder; use super::helpers::{placeholder, status_badge};
use super::issue_detail::IssueDetailView;
/// Height of one issue row in the virtual list: 8px vertical padding /// Height of one issue row in the virtual list: 8px vertical padding
/// (`py_2`) on top and bottom, a 32px title line (`h_8`) and a 24px meta /// (`py_2`) on top and bottom, a 32px title line (`h_8`) and a 24px meta
@@ -63,6 +64,8 @@ impl IssueFilter {
pub struct IssuesView { pub struct IssuesView {
focus_handle: FocusHandle, focus_handle: FocusHandle,
/// Dock area the issue detail panel is opened in.
dock_area: WeakEntity<DockArea>,
/// Repo store holding the issues and their statuses. /// Repo store holding the issues and their statuses.
store: Entity<RepoStore>, store: Entity<RepoStore>,
/// Display name of the repository, for the panel title. /// Display name of the repository, for the panel title.
@@ -83,6 +86,7 @@ pub struct IssuesView {
impl IssuesView { impl IssuesView {
pub fn new( pub fn new(
dock_area: WeakEntity<DockArea>,
store: Entity<RepoStore>, store: Entity<RepoStore>,
repo_name: SharedString, repo_name: SharedString,
window: &mut Window, window: &mut Window,
@@ -94,6 +98,7 @@ impl IssuesView {
Self { Self {
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
dock_area,
store, store,
repo_name, repo_name,
filter: IssueFilter::Open, filter: IssueFilter::Open,
@@ -104,7 +109,28 @@ impl IssuesView {
} }
} }
fn render_row(&self, ix: usize, issue: &Event, cx: &App) -> AnyElement { /// Open the detail panel of `issue_id` at the bottom of the dock area.
fn open_issue_detail(
&mut self,
issue_id: EventId,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(dock_area) = self.dock_area.upgrade() else {
return;
};
let panel = cx.new(|cx| IssueDetailView::new(self.store.clone(), issue_id, window, cx));
dock_area.update(cx, |dock_area, cx| {
dock_area.add_panel(Arc::new(panel), DockPlacement::Bottom, window, cx);
});
}
/// Render one row of the issue list; `ix` is the row index and
/// `issue_ix` the index of the issue in the store's `issues`.
fn render_row(&self, ix: usize, issue_ix: usize, cx: &mut Context<Self>) -> AnyElement {
let issue = &self.store.read(cx).issues[issue_ix];
let title = activity_subject(issue); let title = activity_subject(issue);
let id_hex = issue.id.to_hex(); let id_hex = issue.id.to_hex();
let profile = ProfileStore::global(cx).read(cx).get(&issue.pubkey); let profile = ProfileStore::global(cx).read(cx).get(&issue.pubkey);
@@ -112,6 +138,7 @@ impl IssuesView {
let picture = profile.picture(); let picture = profile.picture();
let age = relative_time(issue.created_at); let age = relative_time(issue.created_at);
let status = self.store.read(cx).status_of(issue); let status = self.store.read(cx).status_of(issue);
let issue_id = issue.id;
h_flex() h_flex()
.id(ix) .id(ix)
@@ -122,7 +149,10 @@ impl IssuesView {
.border_b_1() .border_b_1()
.border_color(cx.theme().border) .border_color(cx.theme().border)
.items_start() .items_start()
.child(Self::render_status(status, cx)) .on_click(cx.listener(move |this, _event, window, cx| {
this.open_issue_detail(issue_id, window, cx);
}))
.child(status_badge(status, cx))
.child( .child(
v_flex() v_flex()
.flex_1() .flex_1()
@@ -148,7 +178,7 @@ impl IssuesView {
Avatar::new() Avatar::new()
.name(author.clone()) .name(author.clone())
.when_some(picture, |this, url| this.src(url)) .when_some(picture, |this, url| this.src(url))
.xsmall(), .small(),
) )
.child(div().child(author)), .child(div().child(author)),
) )
@@ -165,51 +195,6 @@ impl IssuesView {
.into_any_element() .into_any_element()
} }
fn render_status(status: RepoStatus, cx: &App) -> AnyElement {
let (icon, label, tooltip, bg, fg) = match status {
RepoStatus::Open => (
CustomIconName::GitIssueOpen,
"open",
"Issue is open",
cx.theme().secondary,
cx.theme().secondary_foreground,
),
RepoStatus::Closed => (
CustomIconName::GitIssueClosed,
"closed",
"Issue is closed",
cx.theme().danger,
cx.theme().danger_foreground,
),
RepoStatus::Draft => (
CustomIconName::GitIssueOngoing,
"draft",
"Issue is draft",
cx.theme().accent,
cx.theme().accent_foreground,
),
RepoStatus::Applied => (
CustomIconName::GitIssueOpen,
"applied",
"Issue is completed",
cx.theme().primary,
cx.theme().primary_foreground,
),
};
v_flex()
.id(label)
.flex_shrink_0()
.size_7()
.items_center()
.justify_center()
.rounded(cx.theme().radius)
.bg(bg)
.child(Icon::new(icon).small().text_color(fg))
.tooltip(move |window, cx| Tooltip::new(tooltip).build(window, cx))
.into_any_element()
}
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement { fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
let store = self.store.read(cx); let store = self.store.read(cx);
let (total, open, closed) = let (total, open, closed) =
@@ -494,11 +479,10 @@ impl Render for IssuesView {
"issues", "issues",
sizes, sizes,
move |this, range, _window, cx| { move |this, range, _window, cx| {
let issues = &this.store.read(cx).issues;
range range
.map(|ix| { .map(|ix| {
let issue = this.visible_issues[ix]; let issue = this.visible_issues[ix];
this.render_row(issue, &issues[issue], cx) this.render_row(ix, issue, cx)
}) })
.collect() .collect()
}, },
+10 -2
View File
@@ -32,6 +32,7 @@ mod browser;
mod commits; mod commits;
mod diff; mod diff;
mod helpers; mod helpers;
mod issue_detail;
mod issues; mod issues;
mod pull_requests; mod pull_requests;
@@ -661,8 +662,15 @@ impl RepoDetailView {
return; return;
}; };
let panel = let panel = cx.new(|cx| {
cx.new(|cx| IssuesView::new(self.store.clone(), self.display_name(cx), window, cx)); IssuesView::new(
self.dock_area.clone(),
self.store.clone(),
self.display_name(cx),
window,
cx,
)
});
dock_area.update(cx, |dock_area, cx| { dock_area.update(cx, |dock_area, cx| {
dock_area.add_panel(Arc::new(panel), DockPlacement::Center, window, cx); dock_area.add_panel(Arc::new(panel), DockPlacement::Center, window, cx);