consolidate shared helpers into utils crate
This commit is contained in:
@@ -7,3 +7,4 @@ publish.workspace = true
|
||||
[dependencies]
|
||||
nostr.workspace = true
|
||||
serde.workspace = true
|
||||
utils = { path = "../utils" }
|
||||
|
||||
@@ -188,8 +188,8 @@ where
|
||||
.map(|root| {
|
||||
let mut events = groups.remove(&root).unwrap_or_default();
|
||||
let mut own_events = own_groups.remove(&root).unwrap_or_default();
|
||||
sort_newest_first(&mut events);
|
||||
sort_newest_first(&mut own_events);
|
||||
utils::sort_newest_first(&mut events);
|
||||
utils::sort_newest_first(&mut own_events);
|
||||
|
||||
let root_event = lookup(root);
|
||||
|
||||
@@ -218,15 +218,6 @@ where
|
||||
items
|
||||
}
|
||||
|
||||
/// Sort thread events newest first, ties broken by id.
|
||||
fn sort_newest_first(events: &mut [Event]) {
|
||||
events.sort_by(|a, b| {
|
||||
b.created_at
|
||||
.cmp(&a.created_at)
|
||||
.then_with(|| b.id.to_hex().cmp(&a.id.to_hex()))
|
||||
});
|
||||
}
|
||||
|
||||
/// Read and archive state of the inbox, a high-water-mark model.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct InboxReadState {
|
||||
|
||||
@@ -1418,7 +1418,7 @@ impl PushOutcome {
|
||||
self.failing()
|
||||
.map(|server| {
|
||||
let reason =
|
||||
flatten_whitespace(server.reason.as_deref().unwrap_or("unknown error"));
|
||||
utils::flatten_whitespace(server.reason.as_deref().unwrap_or("unknown error"));
|
||||
format!("{}: {reason}", server.relay)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
@@ -1441,18 +1441,6 @@ impl PushOutcome {
|
||||
}
|
||||
}
|
||||
|
||||
fn flatten_whitespace(text: &str) -> String {
|
||||
const MAX_CHARS: usize = 200;
|
||||
let flat: String = text.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
if flat.chars().count() <= MAX_CHARS {
|
||||
flat
|
||||
} else {
|
||||
let mut clipped: String = flat.chars().take(MAX_CHARS).collect();
|
||||
clipped.push('…');
|
||||
clipped
|
||||
}
|
||||
}
|
||||
|
||||
/// Reasons a push attempt should be retried with a freshly staged state
|
||||
/// event and a fresh git advertisement.
|
||||
///
|
||||
|
||||
@@ -7,6 +7,7 @@ use gpui::{App, AppContext, Context, Entity, Global, Subscription};
|
||||
use nostr::prelude::*;
|
||||
use settings::{CheckoutRecord, SettingsStore};
|
||||
use signed_core::{Announcement, RepoAddr};
|
||||
use utils::same_repo_url;
|
||||
|
||||
use crate::backend::{Backend, BackendEvent};
|
||||
use crate::git_store::repo_mirror_root;
|
||||
@@ -511,23 +512,6 @@ impl CheckoutsStore {
|
||||
}
|
||||
}
|
||||
|
||||
fn url_identity(url: &str) -> Option<(String, Option<u16>, String)> {
|
||||
let parsed = Url::parse(url).ok()?;
|
||||
let host = parsed.host_str()?.to_ascii_lowercase();
|
||||
let mut path = parsed.path().trim_matches('/').to_owned();
|
||||
if let Some(stripped) = path.strip_suffix(".git") {
|
||||
path = stripped.to_owned();
|
||||
}
|
||||
Some((host, parsed.port(), path))
|
||||
}
|
||||
|
||||
fn same_repo_url(a: &str, b: &str) -> bool {
|
||||
match (url_identity(a), url_identity(b)) {
|
||||
(Some(a), Some(b)) => a == b,
|
||||
_ => a == b,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_associations<'a>(
|
||||
remembered: &[Remembered],
|
||||
scanned: &[(PathBuf, Option<String>, Option<String>)],
|
||||
|
||||
@@ -392,12 +392,12 @@ impl RepoStore {
|
||||
.into_iter()
|
||||
.filter(|e| !deletions.is_deleted(e));
|
||||
|
||||
let announcement = latest(all_announcements)
|
||||
let announcement = utils::latest(all_announcements)
|
||||
.as_ref()
|
||||
.and_then(Announcement::from_event);
|
||||
|
||||
let all_states = states.into_iter().filter(|e| !deletions.is_deleted(e));
|
||||
let state = latest(all_states).map(|state| parse_state(&state));
|
||||
let state = utils::latest(all_states).map(|state| parse_state(&state));
|
||||
|
||||
let (mut issues, mut patches, mut pull_requests, mut statuses, mut comments) =
|
||||
(Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new());
|
||||
@@ -450,10 +450,10 @@ impl RepoStore {
|
||||
}
|
||||
}
|
||||
|
||||
sort_newest_first(&mut issues);
|
||||
sort_newest_first(&mut patches);
|
||||
sort_newest_first(&mut pull_requests);
|
||||
sort_oldest_first(&mut comments);
|
||||
utils::sort_newest_first(&mut issues);
|
||||
utils::sort_newest_first(&mut patches);
|
||||
utils::sort_newest_first(&mut pull_requests);
|
||||
utils::sort_oldest_first(&mut comments);
|
||||
|
||||
let maintainers = announcement
|
||||
.as_ref()
|
||||
@@ -1447,13 +1447,6 @@ impl RepoStore {
|
||||
}
|
||||
}
|
||||
|
||||
fn latest<I>(events: I) -> Option<Event>
|
||||
where
|
||||
I: IntoIterator<Item = Event>,
|
||||
{
|
||||
events.into_iter().max_by_key(|e| e.created_at)
|
||||
}
|
||||
|
||||
fn status_of(status_by_root: &HashMap<EventId, RepoStatus>, root: &Event) -> RepoStatus {
|
||||
status_by_root
|
||||
.get(&root.id)
|
||||
@@ -1492,14 +1485,6 @@ fn resolve_statuses(
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn sort_newest_first(events: &mut [Event]) {
|
||||
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.
|
||||
/// It is the `From <commit>` header on the first line.
|
||||
fn patch_current_commit(patch: &str) -> Option<&str> {
|
||||
|
||||
@@ -11,7 +11,6 @@ mod title_bar;
|
||||
mod tree_row;
|
||||
|
||||
pub mod copy_row;
|
||||
pub mod util;
|
||||
|
||||
pub use avatar::Avatar;
|
||||
pub use copy_row::{copy_row, menu_copy_row};
|
||||
@@ -25,4 +24,3 @@ pub use setting::{SelectOption, setting_block, setting_row};
|
||||
pub use status_badge::status_badge;
|
||||
pub use title_bar::title_bar_drag_handlers;
|
||||
pub use tree_row::tree_row;
|
||||
pub use util::middle_truncate;
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
/// `[head chars]...[tail chars]` middle truncation.
|
||||
///
|
||||
/// Values too short for the ellipsis to save space are left alone.
|
||||
pub fn middle_truncate(value: &str, head: usize, tail: usize) -> String {
|
||||
let len = value.chars().count();
|
||||
if len <= head + tail + 3 {
|
||||
return value.to_string();
|
||||
}
|
||||
let head: String = value.chars().take(head).collect();
|
||||
let tail: String = value.chars().skip(len - tail).collect();
|
||||
format!("{head}...{tail}")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn middle_truncates_long_values_only() {
|
||||
assert_eq!(
|
||||
middle_truncate(
|
||||
"a008def15796fba9a0d6fab04e8fd57089285d9fd505da5a83fe8aad57a3564d",
|
||||
10,
|
||||
10,
|
||||
),
|
||||
"a008def157...ad57a3564d"
|
||||
);
|
||||
assert_eq!(
|
||||
middle_truncate(
|
||||
"30617:a008def15796fba9a0d6fab04e8fd57089285d9fd505da5a83fe8aad57a3564d:ngit",
|
||||
10,
|
||||
10
|
||||
),
|
||||
"30617:a008...3564d:ngit"
|
||||
);
|
||||
assert_eq!(middle_truncate("short", 10, 10), "short");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
mod pubkey;
|
||||
mod repo_url;
|
||||
mod text;
|
||||
mod time;
|
||||
|
||||
pub use pubkey::shorten_pubkey;
|
||||
pub use time::{relative_time, relative_time_secs};
|
||||
pub use repo_url::same_repo_url;
|
||||
pub use text::{flatten_whitespace, middle_truncate};
|
||||
pub use time::{latest, relative_time, relative_time_secs, sort_newest_first, sort_oldest_first};
|
||||
|
||||
@@ -1,49 +1,14 @@
|
||||
use nostr::prelude::*;
|
||||
|
||||
/// Shorten a [`PublicKey`] to `npub1abc...wxyz` form.
|
||||
use crate::text::middle_truncate;
|
||||
|
||||
const HEAD_CHARS: usize = 9;
|
||||
const TAIL_CHARS: usize = 4;
|
||||
|
||||
pub fn shorten_pubkey(public_key: PublicKey) -> String {
|
||||
let encoded = public_key
|
||||
.to_bech32()
|
||||
.unwrap_or_else(|_| public_key.to_hex());
|
||||
|
||||
truncate_middle(&encoded)
|
||||
}
|
||||
|
||||
fn truncate_middle(value: &str) -> String {
|
||||
const HEAD_CHARS: usize = 9;
|
||||
const TAIL_CHARS: usize = 4;
|
||||
|
||||
let length = value.chars().count();
|
||||
if length <= HEAD_CHARS + TAIL_CHARS + 3 {
|
||||
return value.to_owned();
|
||||
}
|
||||
|
||||
let head: String = value.chars().take(HEAD_CHARS).collect();
|
||||
let tail: String = value.chars().skip(length - TAIL_CHARS).collect();
|
||||
|
||||
format!("{head}...{tail}")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const PUBLIC_KEY_HEX: &str = "68d81165918100b7da43fc28f7d1fc12554466e1115886b9e7bb326f65ec4272";
|
||||
|
||||
#[test]
|
||||
fn shortens_a_valid_pubkey() {
|
||||
let public_key = PublicKey::from_hex(PUBLIC_KEY_HEX).expect("valid pubkey");
|
||||
let npub = public_key.to_bech32().expect("valid pubkey encodes");
|
||||
|
||||
assert_eq!(
|
||||
shorten_pubkey(public_key),
|
||||
format!("{}...{}", &npub[..9], &npub[npub.len() - 4..])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaves_short_values_intact() {
|
||||
assert_eq!(truncate_middle("npub1short"), "npub1short");
|
||||
assert_eq!(truncate_middle("thirteenchars"), "thirteenchars");
|
||||
}
|
||||
middle_truncate(&encoded, HEAD_CHARS, TAIL_CHARS)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
use nostr::prelude::*;
|
||||
|
||||
/// Compare git URLs ignoring scheme, trailing `.git` and path slashes, so a
|
||||
/// grasp announce URL matches its https origin. Unparseable values compare literally.
|
||||
pub fn same_repo_url(a: &str, b: &str) -> bool {
|
||||
match (url_identity(a), url_identity(b)) {
|
||||
(Some(a), Some(b)) => a == b,
|
||||
_ => a == b,
|
||||
}
|
||||
}
|
||||
|
||||
fn url_identity(url: &str) -> Option<(String, Option<u16>, String)> {
|
||||
let parsed = Url::parse(url).ok()?;
|
||||
let host = parsed.host_str()?.to_ascii_lowercase();
|
||||
let mut path = parsed.path().trim_matches('/').to_owned();
|
||||
if let Some(stripped) = path.strip_suffix(".git") {
|
||||
path = stripped.to_owned();
|
||||
}
|
||||
Some((host, parsed.port(), path))
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
pub fn middle_truncate(value: &str, head: usize, tail: usize) -> String {
|
||||
let len = value.chars().count();
|
||||
if len <= head + tail + 3 {
|
||||
return value.to_string();
|
||||
}
|
||||
let head: String = value.chars().take(head).collect();
|
||||
let tail: String = value.chars().skip(len - tail).collect();
|
||||
format!("{head}...{tail}")
|
||||
}
|
||||
|
||||
pub fn flatten_whitespace(text: &str) -> String {
|
||||
const MAX_CHARS: usize = 200;
|
||||
let flat = text.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
if flat.chars().count() <= MAX_CHARS {
|
||||
flat
|
||||
} else {
|
||||
let mut clipped: String = flat.chars().take(MAX_CHARS).collect();
|
||||
clipped.push('…');
|
||||
clipped
|
||||
}
|
||||
}
|
||||
+19
-20
@@ -1,6 +1,24 @@
|
||||
use nostr::prelude::*;
|
||||
|
||||
/// Format a timestamp as a short relative time, e.g. `3h ago`.
|
||||
pub fn sort_newest_first(events: &mut [Event]) {
|
||||
events.sort_by(|a, b| {
|
||||
b.created_at
|
||||
.cmp(&a.created_at)
|
||||
.then_with(|| b.id.to_hex().cmp(&a.id.to_hex()))
|
||||
});
|
||||
}
|
||||
|
||||
pub fn sort_oldest_first(events: &mut [Event]) {
|
||||
events.sort_by_key(|e| e.created_at);
|
||||
}
|
||||
|
||||
pub fn latest<I>(events: I) -> Option<Event>
|
||||
where
|
||||
I: IntoIterator<Item = Event>,
|
||||
{
|
||||
events.into_iter().max_by_key(|e| e.created_at)
|
||||
}
|
||||
|
||||
pub fn relative_time(timestamp: Timestamp) -> String {
|
||||
let now = Timestamp::now().as_secs();
|
||||
let secs = now.saturating_sub(timestamp.as_secs());
|
||||
@@ -20,25 +38,6 @@ pub fn relative_time(timestamp: Timestamp) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Format a unix timestamp in seconds as a short relative time, e.g. `3h ago`.
|
||||
pub fn relative_time_secs(secs: i64) -> String {
|
||||
relative_time(Timestamp::from_secs(secs.max(0) as u64))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn formats_relative_time() {
|
||||
let now = Timestamp::now();
|
||||
|
||||
assert_eq!(relative_time(now), "just now");
|
||||
assert_eq!(relative_time(now - 300), "5m ago");
|
||||
assert_eq!(relative_time(now - 7_200), "2h ago");
|
||||
assert_eq!(relative_time(now - 3 * 86_400), "3d ago");
|
||||
assert_eq!(relative_time(now - 60 * 86_400), "2mo ago");
|
||||
assert_eq!(relative_time(now - 800 * 86_400), "2y ago");
|
||||
assert_eq!(relative_time(now + 600), "just now");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ use signed_state::{
|
||||
Backend, CheckoutsStore, RepoListStore, RepoStore, ensure_repo_mirror, repo_mirror_path,
|
||||
};
|
||||
use signed_ui::{CountBadge, placeholder, ref_selector_trigger};
|
||||
use utils::middle_truncate;
|
||||
|
||||
use crate::views::commit_diff::{COMMIT_ROW_HEIGHT, CommitDiffView, DiffPane, commit_row};
|
||||
use crate::views::{repo_tab_avatar, tab_title};
|
||||
@@ -104,17 +105,8 @@ fn shorten_owner(owner: &PublicKey) -> String {
|
||||
hex.chars().take(10).collect()
|
||||
}
|
||||
|
||||
/// Truncate a label for the fixed-width controls of the compare bar.
|
||||
fn truncate_label(label: &str) -> SharedString {
|
||||
const MAX: usize = 18;
|
||||
let mut chars = label.chars();
|
||||
let (prefix, rest) = (chars.by_ref().take(MAX).collect::<String>(), chars.next());
|
||||
let label = if rest.is_some() {
|
||||
format!("{}…", &prefix[..prefix.len().saturating_sub(1)])
|
||||
} else {
|
||||
prefix
|
||||
};
|
||||
SharedString::from(label)
|
||||
SharedString::from(middle_truncate(label, 18, 0))
|
||||
}
|
||||
|
||||
fn checkout_source_item(
|
||||
|
||||
@@ -5,7 +5,8 @@ use gpui_component::{ActiveTheme, StyledExt, WindowExt, h_flex, v_flex};
|
||||
use nostr::prelude::PublicKey;
|
||||
use signed_core::Announcement;
|
||||
use signed_state::ProfileStore;
|
||||
use signed_ui::{Avatar, middle_truncate};
|
||||
use signed_ui::Avatar;
|
||||
use utils::middle_truncate;
|
||||
|
||||
pub(super) fn open_about_dialog(announcement: Announcement, window: &mut Window, cx: &mut App) {
|
||||
window.open_dialog(cx, move |dialog, _window, cx| {
|
||||
|
||||
@@ -31,9 +31,9 @@ use signed_state::{
|
||||
pr_proposes_checkout,
|
||||
};
|
||||
use signed_ui::{
|
||||
Avatar, CountBadge, DropdownButton, PixelAvatar, copy_row, menu_copy_row, middle_truncate,
|
||||
ref_selector_trigger,
|
||||
Avatar, CountBadge, DropdownButton, PixelAvatar, copy_row, menu_copy_row, ref_selector_trigger,
|
||||
};
|
||||
use utils::middle_truncate;
|
||||
|
||||
mod about;
|
||||
mod actions;
|
||||
|
||||
Reference in New Issue
Block a user