add recents tab
This commit is contained in:
@@ -46,8 +46,11 @@ setting_accessors! {
|
||||
pub nip4e: bool,
|
||||
pub trusted_relays: Vec<String>,
|
||||
pub file_server: Url,
|
||||
pub recent_communities: Vec<String>,
|
||||
}
|
||||
|
||||
const RECENT_COMMUNITIES_CAP: usize = 10;
|
||||
|
||||
/// Signer kind
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum SignerKind {
|
||||
@@ -130,6 +133,10 @@ pub struct Settings {
|
||||
|
||||
/// Server for blossom media attachments
|
||||
pub file_server: Url,
|
||||
|
||||
/// Recently opened community ids, newest first
|
||||
#[serde(default)]
|
||||
pub recent_communities: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
@@ -142,6 +149,7 @@ impl Default for Settings {
|
||||
nip4e: false,
|
||||
trusted_relays: vec![],
|
||||
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 restore;
|
||||
pub mod screening;
|
||||
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 chat::{ChatEvent, ChatRegistry, RoomKind};
|
||||
use common::TimestampExt;
|
||||
use community::{CommunityEvent, CommunityRegistry};
|
||||
use community::{Community, CommunityEvent, CommunityRegistry};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AnyElement, App, Context, ElementId, EventEmitter, FocusHandle, Focusable, InteractiveElement,
|
||||
IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
||||
AnyElement, App, Context, ElementId, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
||||
UniformListScrollHandle, Window, div, px, retain_all, uniform_list,
|
||||
};
|
||||
use person::PersonRegistry;
|
||||
use settings::AppSettings;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::NostrRegistry;
|
||||
use theme::{ActiveTheme, TABBAR_HEIGHT};
|
||||
@@ -91,6 +92,17 @@ impl Sidebar {
|
||||
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 {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
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> {
|
||||
match tab {
|
||||
SidebarTab::Recents => vec![SidebarRow::Hint {
|
||||
text: "Nothing recent yet".into(),
|
||||
}],
|
||||
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(),
|
||||
}];
|
||||
}
|
||||
|
||||
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 => {
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let chat = chat.read(cx);
|
||||
@@ -288,15 +367,34 @@ fn render_rows(range: Range<usize>, rows: &[SidebarRow], cx: &Context<Sidebar>)
|
||||
.into_any_element()
|
||||
}
|
||||
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(
|
||||
ElementId::NamedInteger("tree-row".into(), index as u64),
|
||||
TreeRowKind::Community,
|
||||
community.name(),
|
||||
name,
|
||||
)
|
||||
.avatar(community.id().to_hex())
|
||||
.picture(community.icon())
|
||||
.avatar(seed)
|
||||
.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()
|
||||
}
|
||||
SidebarRow::Hint { text } => TreeRow::new(
|
||||
|
||||
@@ -12,11 +12,26 @@ use theme::ActiveTheme;
|
||||
use ui::avatar::{Avatar, PixelAvatar};
|
||||
use ui::{Icon, IconName, Selectable, Sizable, StyledExt, h_flex};
|
||||
|
||||
use super::tab::SidebarTab;
|
||||
|
||||
pub enum SidebarRow {
|
||||
Section { label: SharedString, count: usize },
|
||||
Room { room: Entity<Room> },
|
||||
Community { community: Entity<Community> },
|
||||
Hint { text: SharedString },
|
||||
Section {
|
||||
label: SharedString,
|
||||
count: usize,
|
||||
},
|
||||
Room {
|
||||
room: Entity<Room>,
|
||||
},
|
||||
Community {
|
||||
community: Entity<Community>,
|
||||
},
|
||||
Action {
|
||||
label: SharedString,
|
||||
tab: SidebarTab,
|
||||
},
|
||||
Hint {
|
||||
text: SharedString,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -24,6 +39,7 @@ pub enum TreeRowKind {
|
||||
Section,
|
||||
Room,
|
||||
Community,
|
||||
Action,
|
||||
Hint,
|
||||
}
|
||||
|
||||
@@ -109,6 +125,7 @@ impl RenderOnce for TreeRow {
|
||||
let is_section = self.kind == TreeRowKind::Section;
|
||||
let is_room = self.kind == TreeRowKind::Room;
|
||||
let is_community = self.kind == TreeRowKind::Community;
|
||||
let is_action = self.kind == TreeRowKind::Action;
|
||||
let is_hint = self.kind == TreeRowKind::Hint;
|
||||
let is_selected = self.selected;
|
||||
|
||||
@@ -146,6 +163,11 @@ impl RenderOnce for TreeRow {
|
||||
.font_semibold()
|
||||
})
|
||||
.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| {
|
||||
this.text_xs()
|
||||
.font_normal()
|
||||
|
||||
Reference in New Issue
Block a user