add recents tab
This commit is contained in:
@@ -46,8 +46,11 @@ setting_accessors! {
|
|||||||
pub nip4e: bool,
|
pub nip4e: bool,
|
||||||
pub trusted_relays: Vec<String>,
|
pub trusted_relays: Vec<String>,
|
||||||
pub file_server: Url,
|
pub file_server: Url,
|
||||||
|
pub recent_communities: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RECENT_COMMUNITIES_CAP: usize = 10;
|
||||||
|
|
||||||
/// Signer kind
|
/// Signer kind
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub enum SignerKind {
|
pub enum SignerKind {
|
||||||
@@ -130,6 +133,10 @@ pub struct Settings {
|
|||||||
|
|
||||||
/// Server for blossom media attachments
|
/// Server for blossom media attachments
|
||||||
pub file_server: Url,
|
pub file_server: Url,
|
||||||
|
|
||||||
|
/// Recently opened community ids, newest first
|
||||||
|
#[serde(default)]
|
||||||
|
pub recent_communities: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Settings {
|
impl Default for Settings {
|
||||||
@@ -142,6 +149,7 @@ impl Default for Settings {
|
|||||||
nip4e: false,
|
nip4e: false,
|
||||||
trusted_relays: vec![],
|
trusted_relays: vec![],
|
||||||
file_server: Url::parse(DEFAULT_FILE_SERVER).unwrap(),
|
file_server: Url::parse(DEFAULT_FILE_SERVER).unwrap(),
|
||||||
|
recent_communities: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -324,4 +332,14 @@ impl AppSettings {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Move a community to the front of the recently opened list
|
||||||
|
pub fn record_recent_community(&mut self, id: String, cx: &mut Context<Self>) {
|
||||||
|
self.inner.update(cx, |this, cx| {
|
||||||
|
this.recent_communities.retain(|existing| existing != &id);
|
||||||
|
this.recent_communities.insert(0, id);
|
||||||
|
this.recent_communities.truncate(RECENT_COMMUNITIES_CAP);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
pub mod import;
|
pub mod import;
|
||||||
pub mod restore;
|
pub mod restore;
|
||||||
pub mod screening;
|
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
|
|||||||
@@ -1,553 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use anyhow::Error;
|
|
||||||
use common::TimestampExt;
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
App, AppContext, Context, Div, Entity, InteractiveElement, IntoElement, ParentElement, Render,
|
|
||||||
SharedString, Styled, Subscription, Task, Window, div, px, relative, uniform_list,
|
|
||||||
};
|
|
||||||
use instant::Duration;
|
|
||||||
use nostr_sdk::prelude::*;
|
|
||||||
use person::{Person, PersonRegistry, shorten_pubkey};
|
|
||||||
use smallvec::{SmallVec, smallvec};
|
|
||||||
use state::{BOOTSTRAP_RELAYS, NostrAddress, NostrRegistry, TIMEOUT};
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::avatar::Avatar;
|
|
||||||
use ui::button::{Button, ButtonVariants};
|
|
||||||
use ui::indicator::Indicator;
|
|
||||||
use ui::{Disableable, Icon, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
|
||||||
|
|
||||||
pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<Screening> {
|
|
||||||
cx.new(|cx| Screening::new(public_key, window, cx))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Screening
|
|
||||||
pub struct Screening {
|
|
||||||
/// Public Key of the person being screened.
|
|
||||||
public_key: PublicKey,
|
|
||||||
|
|
||||||
/// Whether the person's address is verified.
|
|
||||||
verified: bool,
|
|
||||||
|
|
||||||
/// Whether the person is followed by current user.
|
|
||||||
followed: bool,
|
|
||||||
|
|
||||||
/// Last time the person was active.
|
|
||||||
last_active: Option<Timestamp>,
|
|
||||||
|
|
||||||
/// All mutual contacts of the person being screened.
|
|
||||||
mutual_contacts: Vec<PublicKey>,
|
|
||||||
|
|
||||||
/// Async tasks
|
|
||||||
tasks: SmallVec<[Task<()>; 3]>,
|
|
||||||
|
|
||||||
/// Subscriptions
|
|
||||||
_subscriptions: SmallVec<[Subscription; 1]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Screening {
|
|
||||||
pub fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
|
||||||
let mut subscriptions = smallvec![];
|
|
||||||
|
|
||||||
subscriptions.push(cx.on_release_in(window, move |this, window, cx| {
|
|
||||||
this.tasks.clear();
|
|
||||||
window.close_all_modals(cx);
|
|
||||||
}));
|
|
||||||
|
|
||||||
cx.defer_in(window, |this, _window, cx| {
|
|
||||||
this.check_contact(cx);
|
|
||||||
this.check_wot(cx);
|
|
||||||
this.check_last_activity(cx);
|
|
||||||
this.verify_identifier(cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
|
||||||
public_key,
|
|
||||||
verified: false,
|
|
||||||
followed: false,
|
|
||||||
last_active: None,
|
|
||||||
mutual_contacts: vec![],
|
|
||||||
tasks: smallvec![],
|
|
||||||
_subscriptions: subscriptions,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_contact(&mut self, cx: &mut Context<Self>) {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let client = nostr.read(cx).client();
|
|
||||||
let public_key = self.public_key;
|
|
||||||
|
|
||||||
let Some(current_user) = nostr.read(cx).current_user() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let task: Task<Result<bool, Error>> = cx.background_spawn(async move {
|
|
||||||
// Check if user is in contact list
|
|
||||||
let filter = Filter::new()
|
|
||||||
.author(current_user)
|
|
||||||
.kind(Kind::ContactList)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
let followed = client
|
|
||||||
.database()
|
|
||||||
.query(filter)
|
|
||||||
.await
|
|
||||||
.unwrap_or_default()
|
|
||||||
.into_iter()
|
|
||||||
.next()
|
|
||||||
.map(|event| event.tags.public_keys().any(|k| k == public_key))
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
Ok(followed)
|
|
||||||
});
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
|
||||||
let result = task.await.unwrap_or(false);
|
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.followed = result;
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_wot(&mut self, cx: &mut Context<Self>) {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let client = nostr.read(cx).client();
|
|
||||||
let public_key = self.public_key;
|
|
||||||
|
|
||||||
let Some(current_user) = nostr.read(cx).current_user() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let task: Task<Result<Vec<PublicKey>, Error>> = cx.background_spawn(async move {
|
|
||||||
// Check mutual contacts
|
|
||||||
let filter = Filter::new().kind(Kind::ContactList).pubkey(public_key);
|
|
||||||
let mut mutual_contacts = vec![];
|
|
||||||
|
|
||||||
if let Ok(events) = client.database().query(filter).await {
|
|
||||||
for event in events.into_iter().filter(|ev| ev.pubkey != current_user) {
|
|
||||||
mutual_contacts.push(event.pubkey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(mutual_contacts)
|
|
||||||
});
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
|
||||||
match task.await {
|
|
||||||
Ok(contacts) => {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.mutual_contacts = contacts;
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to fetch mutual contacts: {}", e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_last_activity(&mut self, cx: &mut Context<Self>) {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let client = nostr.read(cx).client();
|
|
||||||
let public_key = self.public_key;
|
|
||||||
|
|
||||||
let task: Task<Option<Timestamp>> = cx.background_spawn(async move {
|
|
||||||
let filter = Filter::new().author(public_key).limit(1);
|
|
||||||
let mut activity: Option<Timestamp> = None;
|
|
||||||
|
|
||||||
// Construct target for subscription
|
|
||||||
let target: HashMap<&str, Vec<Filter>> = BOOTSTRAP_RELAYS
|
|
||||||
.into_iter()
|
|
||||||
.map(|relay| (relay, vec![filter.clone()]))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if let Ok(mut stream) = client
|
|
||||||
.stream_events(target)
|
|
||||||
.timeout(Duration::from_secs(TIMEOUT))
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
while let Some((_url, event)) = stream.next().await {
|
|
||||||
if let Ok(event) = event {
|
|
||||||
activity = Some(event.created_at);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
activity
|
|
||||||
});
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
|
||||||
let result = task.await;
|
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.last_active = result;
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verify_identifier(&mut self, cx: &mut Context<Self>) {
|
|
||||||
let http_client = cx.http_client();
|
|
||||||
let public_key = self.public_key;
|
|
||||||
|
|
||||||
// Skip if the user doesn't have a NIP-05 identifier
|
|
||||||
let Some(address) = self.address(cx) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let task: Task<Result<bool, Error>> =
|
|
||||||
cx.background_spawn(async move { address.verify(&http_client, &public_key).await });
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
|
||||||
let result = task.await.unwrap_or(false);
|
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.verified = result;
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn profile(&self, cx: &Context<Self>) -> Person {
|
|
||||||
let persons = PersonRegistry::global(cx);
|
|
||||||
persons.read(cx).get(&self.public_key, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn address(&self, cx: &Context<Self>) -> Option<Nip05Address> {
|
|
||||||
self.profile(cx)
|
|
||||||
.metadata()
|
|
||||||
.nip05
|
|
||||||
.and_then(|addr| Nip05Address::parse(&addr).ok())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_njump(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let Ok(bech32) = self.profile(cx).public_key().to_bech32();
|
|
||||||
cx.open_url(&format!("https://njump.me/{bech32}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn report(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let client = nostr.read(cx).client();
|
|
||||||
let signer = nostr.read(cx).signer();
|
|
||||||
let public_key = self.public_key;
|
|
||||||
|
|
||||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
|
||||||
let tag = Tag::from(Nip56Tag::PublicKey {
|
|
||||||
public_key,
|
|
||||||
report: Report::Impersonation,
|
|
||||||
});
|
|
||||||
|
|
||||||
let event = EventBuilder::new(Kind::Reporting, "")
|
|
||||||
.tag(tag)
|
|
||||||
.finalize_async(&signer)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Send the report to the public relays
|
|
||||||
client.send_event(&event).to(BOOTSTRAP_RELAYS).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn_in(window, async move |_, cx| {
|
|
||||||
if task.await.is_ok() {
|
|
||||||
cx.update(|window, cx| {
|
|
||||||
window.close_modal(cx);
|
|
||||||
window.push_notification("Report submitted successfully", cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mutual_contacts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let contacts = self.mutual_contacts.clone();
|
|
||||||
|
|
||||||
window.open_modal(cx, move |this, _window, _cx| {
|
|
||||||
let contacts = contacts.clone();
|
|
||||||
let total = contacts.len();
|
|
||||||
|
|
||||||
this.title("Mutual contacts").child(
|
|
||||||
v_flex().gap_1().pb_2().child(
|
|
||||||
uniform_list("contacts", total, move |range, _window, cx| {
|
|
||||||
let persons = PersonRegistry::global(cx);
|
|
||||||
let mut items = Vec::with_capacity(total);
|
|
||||||
|
|
||||||
for ix in range {
|
|
||||||
let Some(contact) = contacts.get(ix) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let profile = persons.read(cx).get(contact, cx);
|
|
||||||
|
|
||||||
items.push(
|
|
||||||
h_flex()
|
|
||||||
.h_11()
|
|
||||||
.w_full()
|
|
||||||
.px_2()
|
|
||||||
.gap_1p5()
|
|
||||||
.rounded(cx.theme().radius)
|
|
||||||
.text_sm()
|
|
||||||
.hover(|this| this.bg(cx.theme().elevated_surface_background))
|
|
||||||
.child(
|
|
||||||
Avatar::new(profile.avatar())
|
|
||||||
.seed(profile.avatar_seed())
|
|
||||||
.small(),
|
|
||||||
)
|
|
||||||
.child(profile.name()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
items
|
|
||||||
})
|
|
||||||
.h(px(300.)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for Screening {
|
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
const CONTACT: &str = "This person is one of your contacts.";
|
|
||||||
const NOT_CONTACT: &str = "This person is not one of your contacts.";
|
|
||||||
const NO_ACTIVITY: &str = "This person hasn't had any activity.";
|
|
||||||
const RELAY_INFO: &str = "Only checked on public relays; may be inaccurate.";
|
|
||||||
const NO_MUTUAL: &str = "You don't have any mutual contacts.";
|
|
||||||
const NIP05_MATCH: &str = "The address matches the user's public key.";
|
|
||||||
const NIP05_NOT_MATCH: &str = "The address does not match the user's public key.";
|
|
||||||
const NO_NIP05: &str = "This person has not set up their friendly address";
|
|
||||||
|
|
||||||
let profile = self.profile(cx);
|
|
||||||
let shorten_pubkey = shorten_pubkey(self.public_key, 8);
|
|
||||||
|
|
||||||
let last_active = self.last_active.map(|_| true);
|
|
||||||
let mutuals = self.mutual_contacts.len();
|
|
||||||
let mutuals_str = format!("You have {} mutual contacts with this person.", mutuals);
|
|
||||||
|
|
||||||
v_flex()
|
|
||||||
.gap_4()
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_3()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.text_center()
|
|
||||||
.child(
|
|
||||||
Avatar::new(profile.avatar())
|
|
||||||
.seed(profile.avatar_seed())
|
|
||||||
.large(),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.font_semibold()
|
|
||||||
.line_height(relative(1.25))
|
|
||||||
.child(profile.name()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_3()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.p_1()
|
|
||||||
.flex_1()
|
|
||||||
.h_7()
|
|
||||||
.justify_center()
|
|
||||||
.rounded_full()
|
|
||||||
.bg(cx.theme().elevated_surface_background)
|
|
||||||
.text_sm()
|
|
||||||
.truncate()
|
|
||||||
.text_ellipsis()
|
|
||||||
.text_center()
|
|
||||||
.line_height(relative(1.))
|
|
||||||
.child(shorten_pubkey),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_1()
|
|
||||||
.child(
|
|
||||||
Button::new("njump")
|
|
||||||
.icon(IconName::Link)
|
|
||||||
.label("njump.me")
|
|
||||||
.secondary()
|
|
||||||
.small()
|
|
||||||
.rounded()
|
|
||||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
|
||||||
this.open_njump(window, cx);
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("report")
|
|
||||||
.tooltip("Report as a scam or impostor")
|
|
||||||
.icon(IconName::Warning)
|
|
||||||
.small()
|
|
||||||
.warning()
|
|
||||||
.rounded()
|
|
||||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
|
||||||
this.report(window, cx);
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_3()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.items_start()
|
|
||||||
.gap_2()
|
|
||||||
.text_sm()
|
|
||||||
.child(status_badge(Some(self.followed), cx))
|
|
||||||
.child(
|
|
||||||
v_flex().text_sm().child("Contact").child(
|
|
||||||
div()
|
|
||||||
.line_clamp(1)
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child({
|
|
||||||
if self.followed {
|
|
||||||
SharedString::from(CONTACT)
|
|
||||||
} else {
|
|
||||||
SharedString::from(NOT_CONTACT)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.items_start()
|
|
||||||
.gap_2()
|
|
||||||
.text_sm()
|
|
||||||
.child(status_badge(last_active, cx))
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.text_sm()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_0p5()
|
|
||||||
.child("Activity on Public Relays")
|
|
||||||
.child(
|
|
||||||
Button::new("active")
|
|
||||||
.icon(IconName::Info)
|
|
||||||
.xsmall()
|
|
||||||
.ghost()
|
|
||||||
.rounded()
|
|
||||||
.tooltip(RELAY_INFO),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.w_full()
|
|
||||||
.line_clamp(1)
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.map(|this| {
|
|
||||||
if let Some(t) = self.last_active {
|
|
||||||
this.child(SharedString::from(format!(
|
|
||||||
"Last active: {}.",
|
|
||||||
t.to_human_time()
|
|
||||||
)))
|
|
||||||
} else {
|
|
||||||
this.child(SharedString::from(NO_ACTIVITY))
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.items_start()
|
|
||||||
.gap_2()
|
|
||||||
.child(status_badge(Some(self.verified), cx))
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.text_sm()
|
|
||||||
.child({
|
|
||||||
if let Some(addr) = self.address(cx) {
|
|
||||||
SharedString::from(format!("{} validation", addr))
|
|
||||||
} else {
|
|
||||||
SharedString::from(
|
|
||||||
"Friendly Address (NIP-05) validation",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.line_clamp(1)
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child({
|
|
||||||
if self.address(cx).is_some() {
|
|
||||||
if self.verified {
|
|
||||||
SharedString::from(NIP05_MATCH)
|
|
||||||
} else {
|
|
||||||
SharedString::from(NIP05_NOT_MATCH)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
SharedString::from(NO_NIP05)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.items_start()
|
|
||||||
.gap_2()
|
|
||||||
.child(status_badge(Some(mutuals > 0), cx))
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.text_sm()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.line_clamp(1)
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child({
|
|
||||||
if mutuals > 0 {
|
|
||||||
SharedString::from(mutuals_str)
|
|
||||||
} else {
|
|
||||||
SharedString::from(NO_MUTUAL)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("mutuals")
|
|
||||||
.icon(IconName::Info)
|
|
||||||
.xsmall()
|
|
||||||
.ghost()
|
|
||||||
.rounded()
|
|
||||||
.disabled(mutuals == 0)
|
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
|
||||||
this.mutual_contacts(window, cx);
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn status_badge(status: Option<bool>, cx: &App) -> Div {
|
|
||||||
h_flex()
|
|
||||||
.size_6()
|
|
||||||
.justify_center()
|
|
||||||
.flex_shrink_0()
|
|
||||||
.map(|this| {
|
|
||||||
if let Some(status) = status {
|
|
||||||
this.child(Icon::new(IconName::CheckCircle).small().text_color({
|
|
||||||
if status {
|
|
||||||
cx.theme().icon_accent
|
|
||||||
} else {
|
|
||||||
cx.theme().icon_muted
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
this.child(Indicator::new().small())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -4,14 +4,15 @@ use std::rc::Rc;
|
|||||||
use auto_update::AutoUpdater;
|
use auto_update::AutoUpdater;
|
||||||
use chat::{ChatEvent, ChatRegistry, RoomKind};
|
use chat::{ChatEvent, ChatRegistry, RoomKind};
|
||||||
use common::TimestampExt;
|
use common::TimestampExt;
|
||||||
use community::{CommunityEvent, CommunityRegistry};
|
use community::{Community, CommunityEvent, CommunityRegistry};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, Context, ElementId, EventEmitter, FocusHandle, Focusable, InteractiveElement,
|
AnyElement, App, Context, ElementId, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
||||||
UniformListScrollHandle, Window, div, px, retain_all, uniform_list,
|
UniformListScrollHandle, Window, div, px, retain_all, uniform_list,
|
||||||
};
|
};
|
||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
|
use settings::AppSettings;
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use state::NostrRegistry;
|
use state::NostrRegistry;
|
||||||
use theme::{ActiveTheme, TABBAR_HEIGHT};
|
use theme::{ActiveTheme, TABBAR_HEIGHT};
|
||||||
@@ -91,6 +92,17 @@ impl Sidebar {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_community(&mut self, community: Entity<Community>, cx: &mut Context<Self>) {
|
||||||
|
let id = community.read(cx).id().to_hex();
|
||||||
|
let settings = AppSettings::global(cx);
|
||||||
|
|
||||||
|
settings.update(cx, |settings, cx| {
|
||||||
|
settings.record_recent_community(id, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
fn render_user(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render_user(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let current_user = nostr.read(cx).current_user();
|
let current_user = nostr.read(cx).current_user();
|
||||||
@@ -195,11 +207,78 @@ impl Sidebar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn recent_communities(cx: &App) -> Vec<Entity<Community>> {
|
||||||
|
const LIMIT: usize = 3;
|
||||||
|
|
||||||
|
let registry = CommunityRegistry::global(cx);
|
||||||
|
let communities = registry.read(cx).communities();
|
||||||
|
let recent = AppSettings::get_recent_communities(cx);
|
||||||
|
|
||||||
|
let mut rows: Vec<Entity<Community>> = recent
|
||||||
|
.iter()
|
||||||
|
.filter_map(|id| {
|
||||||
|
communities
|
||||||
|
.iter()
|
||||||
|
.find(|community| community.read(cx).id().to_hex() == *id)
|
||||||
|
})
|
||||||
|
.take(LIMIT)
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if rows.is_empty() {
|
||||||
|
rows = communities.iter().take(LIMIT).cloned().collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
rows
|
||||||
|
}
|
||||||
|
|
||||||
fn rows_for(tab: SidebarTab, cx: &App) -> Vec<SidebarRow> {
|
fn rows_for(tab: SidebarTab, cx: &App) -> Vec<SidebarRow> {
|
||||||
match tab {
|
match tab {
|
||||||
SidebarTab::Recents => vec![SidebarRow::Hint {
|
SidebarTab::Recents => {
|
||||||
|
let chat = ChatRegistry::global(cx);
|
||||||
|
let rooms = chat.read(cx).rooms(&RoomKind::Ongoing, cx);
|
||||||
|
let registry = CommunityRegistry::global(cx);
|
||||||
|
let community_count = registry.read(cx).communities().len();
|
||||||
|
let communities = recent_communities(cx);
|
||||||
|
|
||||||
|
if communities.is_empty() && rooms.is_empty() {
|
||||||
|
return vec![SidebarRow::Hint {
|
||||||
text: "Nothing recent yet".into(),
|
text: "Nothing recent yet".into(),
|
||||||
}],
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut rows = vec![SidebarRow::Section {
|
||||||
|
label: "Communities".into(),
|
||||||
|
count: community_count,
|
||||||
|
}];
|
||||||
|
|
||||||
|
rows.extend(
|
||||||
|
communities
|
||||||
|
.into_iter()
|
||||||
|
.map(|community| SidebarRow::Community { community }),
|
||||||
|
);
|
||||||
|
rows.push(SidebarRow::Action {
|
||||||
|
label: "Show all communities".into(),
|
||||||
|
tab: SidebarTab::Communities,
|
||||||
|
});
|
||||||
|
|
||||||
|
rows.push(SidebarRow::Section {
|
||||||
|
label: "Chats".into(),
|
||||||
|
count: rooms.len(),
|
||||||
|
});
|
||||||
|
rows.extend(
|
||||||
|
rooms
|
||||||
|
.into_iter()
|
||||||
|
.take(5)
|
||||||
|
.map(|room| SidebarRow::Room { room }),
|
||||||
|
);
|
||||||
|
rows.push(SidebarRow::Action {
|
||||||
|
label: "Show all chats".into(),
|
||||||
|
tab: SidebarTab::Chats,
|
||||||
|
});
|
||||||
|
|
||||||
|
rows
|
||||||
|
}
|
||||||
SidebarTab::Chats => {
|
SidebarTab::Chats => {
|
||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
let chat = chat.read(cx);
|
let chat = chat.read(cx);
|
||||||
@@ -288,15 +367,34 @@ fn render_rows(range: Range<usize>, rows: &[SidebarRow], cx: &Context<Sidebar>)
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
SidebarRow::Community { community } => {
|
SidebarRow::Community { community } => {
|
||||||
let community = community.read(cx);
|
let name = community.read(cx).name();
|
||||||
|
let seed = community.read(cx).id().to_hex();
|
||||||
|
let picture = community.read(cx).icon();
|
||||||
|
let community = community.clone();
|
||||||
|
|
||||||
TreeRow::new(
|
TreeRow::new(
|
||||||
ElementId::NamedInteger("tree-row".into(), index as u64),
|
ElementId::NamedInteger("tree-row".into(), index as u64),
|
||||||
TreeRowKind::Community,
|
TreeRowKind::Community,
|
||||||
community.name(),
|
name,
|
||||||
)
|
)
|
||||||
.avatar(community.id().to_hex())
|
.avatar(seed)
|
||||||
.picture(community.icon())
|
.picture(picture)
|
||||||
|
.on_click(cx.listener(move |this, _event, _window, cx| {
|
||||||
|
this.open_community(community.clone(), cx);
|
||||||
|
}))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
SidebarRow::Action { label, tab } => {
|
||||||
|
let tab = *tab;
|
||||||
|
|
||||||
|
TreeRow::new(
|
||||||
|
ElementId::NamedInteger("tree-row".into(), index as u64),
|
||||||
|
TreeRowKind::Action,
|
||||||
|
label.clone(),
|
||||||
|
)
|
||||||
|
.on_click(cx.listener(move |this, _event, _window, cx| {
|
||||||
|
this.select_tab(tab, cx);
|
||||||
|
}))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
SidebarRow::Hint { text } => TreeRow::new(
|
SidebarRow::Hint { text } => TreeRow::new(
|
||||||
|
|||||||
@@ -12,11 +12,26 @@ use theme::ActiveTheme;
|
|||||||
use ui::avatar::{Avatar, PixelAvatar};
|
use ui::avatar::{Avatar, PixelAvatar};
|
||||||
use ui::{Icon, IconName, Selectable, Sizable, StyledExt, h_flex};
|
use ui::{Icon, IconName, Selectable, Sizable, StyledExt, h_flex};
|
||||||
|
|
||||||
|
use super::tab::SidebarTab;
|
||||||
|
|
||||||
pub enum SidebarRow {
|
pub enum SidebarRow {
|
||||||
Section { label: SharedString, count: usize },
|
Section {
|
||||||
Room { room: Entity<Room> },
|
label: SharedString,
|
||||||
Community { community: Entity<Community> },
|
count: usize,
|
||||||
Hint { text: SharedString },
|
},
|
||||||
|
Room {
|
||||||
|
room: Entity<Room>,
|
||||||
|
},
|
||||||
|
Community {
|
||||||
|
community: Entity<Community>,
|
||||||
|
},
|
||||||
|
Action {
|
||||||
|
label: SharedString,
|
||||||
|
tab: SidebarTab,
|
||||||
|
},
|
||||||
|
Hint {
|
||||||
|
text: SharedString,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
@@ -24,6 +39,7 @@ pub enum TreeRowKind {
|
|||||||
Section,
|
Section,
|
||||||
Room,
|
Room,
|
||||||
Community,
|
Community,
|
||||||
|
Action,
|
||||||
Hint,
|
Hint,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +125,7 @@ impl RenderOnce for TreeRow {
|
|||||||
let is_section = self.kind == TreeRowKind::Section;
|
let is_section = self.kind == TreeRowKind::Section;
|
||||||
let is_room = self.kind == TreeRowKind::Room;
|
let is_room = self.kind == TreeRowKind::Room;
|
||||||
let is_community = self.kind == TreeRowKind::Community;
|
let is_community = self.kind == TreeRowKind::Community;
|
||||||
|
let is_action = self.kind == TreeRowKind::Action;
|
||||||
let is_hint = self.kind == TreeRowKind::Hint;
|
let is_hint = self.kind == TreeRowKind::Hint;
|
||||||
let is_selected = self.selected;
|
let is_selected = self.selected;
|
||||||
|
|
||||||
@@ -146,6 +163,11 @@ impl RenderOnce for TreeRow {
|
|||||||
.font_semibold()
|
.font_semibold()
|
||||||
})
|
})
|
||||||
.when(is_room || is_community, |this| this.text_sm())
|
.when(is_room || is_community, |this| this.text_sm())
|
||||||
|
.when(is_action, |this| {
|
||||||
|
this.text_sm()
|
||||||
|
.font_medium()
|
||||||
|
.text_color(cx.theme().text_accent)
|
||||||
|
})
|
||||||
.when(is_hint, |this| {
|
.when(is_hint, |this| {
|
||||||
this.text_xs()
|
this.text_xs()
|
||||||
.font_normal()
|
.font_normal()
|
||||||
|
|||||||
Reference in New Issue
Block a user