feat: screening (#96)
* . * . * refactor * . * screening * add report user function * add danger and warning styles * update deps * update * fix line height * .
This commit is contained in:
@@ -25,6 +25,7 @@ use ui::modal::ModalButtonProps;
|
||||
use ui::{ContextModal, IconName, Root, Sizable, StyledExt, TitleBar};
|
||||
|
||||
use crate::views::chat::{self, Chat};
|
||||
use crate::views::screening::Screening;
|
||||
use crate::views::user_profile::UserProfile;
|
||||
use crate::views::{
|
||||
login, new_account, onboarding, preferences, sidebar, startup, user_profile, welcome,
|
||||
@@ -73,7 +74,7 @@ pub struct ChatSpace {
|
||||
dock: Entity<DockArea>,
|
||||
toolbar: bool,
|
||||
#[allow(unused)]
|
||||
subscriptions: SmallVec<[Subscription; 5]>,
|
||||
subscriptions: SmallVec<[Subscription; 6]>,
|
||||
}
|
||||
|
||||
impl ChatSpace {
|
||||
@@ -172,13 +173,20 @@ impl ChatSpace {
|
||||
}
|
||||
}));
|
||||
|
||||
// Automatically run on_load function from UserProfile
|
||||
// Automatically run on_load function when UserProfile is created
|
||||
subscriptions.push(cx.observe_new::<UserProfile>(|this, window, cx| {
|
||||
if let Some(window) = window {
|
||||
this.on_load(window, cx);
|
||||
}
|
||||
}));
|
||||
|
||||
// Automatically run on_load function when Screening is created
|
||||
subscriptions.push(cx.observe_new::<Screening>(|this, window, cx| {
|
||||
if let Some(window) = window {
|
||||
this.on_load(window, cx);
|
||||
}
|
||||
}));
|
||||
|
||||
// Subscribe to open chat room requests
|
||||
subscriptions.push(cx.subscribe_in(
|
||||
®istry,
|
||||
|
||||
@@ -405,10 +405,11 @@ async fn handle_nostr_notifications(
|
||||
.kind(Kind::InboxRelays)
|
||||
.limit(1);
|
||||
|
||||
if client.subscribe(filter, Some(opts)).await.is_ok() {
|
||||
if let Ok(output) = client.subscribe(filter, Some(opts)).await {
|
||||
log::info!(
|
||||
"Subscribed to get DM relays: {}",
|
||||
event.pubkey.to_bech32().unwrap()
|
||||
"Subscribed to get DM relays: {} - Relays: {:?}",
|
||||
event.pubkey.to_bech32().unwrap(),
|
||||
output.success
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ pub mod new_account;
|
||||
pub mod onboarding;
|
||||
pub mod preferences;
|
||||
pub mod relays;
|
||||
pub mod screening;
|
||||
pub mod sidebar;
|
||||
pub mod startup;
|
||||
pub mod user_profile;
|
||||
|
||||
@@ -51,7 +51,7 @@ impl Preferences {
|
||||
|
||||
fn open_edit_profile(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let edit_profile = edit_profile::init(window, cx);
|
||||
let title = SharedString::new(t!("preferences.modal_profile_title"));
|
||||
let title = SharedString::new(t!("profile.title"));
|
||||
|
||||
window.open_modal(cx, move |modal, _window, _cx| {
|
||||
modal
|
||||
|
||||
288
crates/coop/src/views/screening.rs
Normal file
288
crates/coop/src/views/screening.rs
Normal file
@@ -0,0 +1,288 @@
|
||||
use common::display::{shorten_pubkey, DisplayProfile};
|
||||
use common::nip05::nip05_verify;
|
||||
use global::nostr_client;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, relative, rems, App, AppContext, Context, Entity, IntoElement, ParentElement, Render,
|
||||
SharedString, Styled, Task, Window,
|
||||
};
|
||||
use gpui_tokio::Tokio;
|
||||
use i18n::t;
|
||||
use identity::Identity;
|
||||
use nostr_sdk::prelude::*;
|
||||
use registry::Registry;
|
||||
use settings::AppSettings;
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonRounded, ButtonVariants};
|
||||
use ui::{h_flex, v_flex, ContextModal, Icon, IconName, Sizable, StyledExt};
|
||||
|
||||
pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<Screening> {
|
||||
Screening::new(public_key, window, cx)
|
||||
}
|
||||
|
||||
pub struct Screening {
|
||||
public_key: PublicKey,
|
||||
followed: bool,
|
||||
connections: usize,
|
||||
verified: bool,
|
||||
}
|
||||
|
||||
impl Screening {
|
||||
pub fn new(public_key: PublicKey, _window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
cx.new(|_| Self {
|
||||
public_key,
|
||||
followed: false,
|
||||
connections: 0,
|
||||
verified: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// Skip if user isn't logged in
|
||||
let Some(identity) = Identity::read_global(cx).public_key() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let public_key = self.public_key;
|
||||
|
||||
let check_trust_score: Task<(bool, usize)> = cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
|
||||
let follow = Filter::new()
|
||||
.kind(Kind::ContactList)
|
||||
.author(identity)
|
||||
.pubkey(public_key)
|
||||
.limit(1);
|
||||
|
||||
let connection = Filter::new()
|
||||
.kind(Kind::ContactList)
|
||||
.pubkey(public_key)
|
||||
.limit(1);
|
||||
|
||||
let is_follow = client.database().count(follow).await.unwrap_or(0) >= 1;
|
||||
let connects = client.database().count(connection).await.unwrap_or(0);
|
||||
|
||||
(is_follow, connects)
|
||||
});
|
||||
|
||||
let verify_nip05 = if let Some(address) = self.address(cx) {
|
||||
Some(Tokio::spawn(cx, async move {
|
||||
nip05_verify(public_key, &address).await.unwrap_or(false)
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let (followed, connections) = check_trust_score.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.followed = followed;
|
||||
this.connections = connections;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
|
||||
// Update the NIP05 verification status if user has NIP05 address
|
||||
if let Some(task) = verify_nip05 {
|
||||
if let Ok(verified) = task.await {
|
||||
this.update(cx, |this, cx| {
|
||||
this.verified = verified;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn profile(&self, cx: &Context<Self>) -> Profile {
|
||||
let registry = Registry::read_global(cx);
|
||||
registry.get_person(&self.public_key, cx)
|
||||
}
|
||||
|
||||
fn address(&self, cx: &Context<Self>) -> Option<String> {
|
||||
self.profile(cx).metadata().nip05
|
||||
}
|
||||
|
||||
fn open_njump(&mut self, _window: &mut Window, cx: &mut App) {
|
||||
let Ok(bech32) = self.public_key.to_bech32();
|
||||
cx.open_url(&format!("https://njump.me/{bech32}"));
|
||||
}
|
||||
|
||||
fn report(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let public_key = self.public_key;
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let builder = EventBuilder::report(
|
||||
vec![Tag::public_key_report(public_key, Report::Impersonation)],
|
||||
"scam/impersonation",
|
||||
);
|
||||
let _ = client.send_event_builder(builder).await?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
if task.await.is_ok() {
|
||||
cx.update(|window, cx| {
|
||||
window.close_modal(cx);
|
||||
window.push_notification(t!("screening.report_msg"), cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Screening {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let proxy = AppSettings::get_global(cx).settings.proxy_user_avatars;
|
||||
let profile = self.profile(cx);
|
||||
let shorten_pubkey = shorten_pubkey(profile.public_key(), 8);
|
||||
|
||||
v_flex()
|
||||
.w_full()
|
||||
.px_4()
|
||||
.pt_8()
|
||||
.pb_4()
|
||||
.gap_4()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_3()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_center()
|
||||
.child(Avatar::new(profile.avatar_url(proxy)).size(rems(4.)))
|
||||
.child(
|
||||
div()
|
||||
.font_semibold()
|
||||
.line_height(relative(1.25))
|
||||
.child(profile.display_name()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.p_1()
|
||||
.flex_1()
|
||||
.h_7()
|
||||
.flex()
|
||||
.items_center()
|
||||
.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(
|
||||
Button::new("njump")
|
||||
.label(t!("profile.njump"))
|
||||
.secondary()
|
||||
.small()
|
||||
.rounded(ButtonRounded::Full)
|
||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
||||
this.open_njump(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("report")
|
||||
.tooltip(t!("screening.report"))
|
||||
.icon(IconName::Info)
|
||||
.danger_alt()
|
||||
.small()
|
||||
.rounded(ButtonRounded::Full)
|
||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
||||
this.report(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.when_some(self.address(cx), |this, address| {
|
||||
this.child(h_flex().gap_2().map(|this| {
|
||||
if self.verified {
|
||||
this.text_sm()
|
||||
.child(
|
||||
Icon::new(IconName::CheckCircleFill)
|
||||
.small()
|
||||
.flex_shrink_0()
|
||||
.text_color(cx.theme().icon_accent),
|
||||
)
|
||||
.child(div().flex_1().child(SharedString::new(t!(
|
||||
"screening.verified",
|
||||
address = address
|
||||
))))
|
||||
} else {
|
||||
this.text_sm()
|
||||
.child(
|
||||
Icon::new(IconName::CheckCircleFill)
|
||||
.small()
|
||||
.text_color(cx.theme().icon_muted),
|
||||
)
|
||||
.child(div().flex_1().child(SharedString::new(t!(
|
||||
"screening.not_verified",
|
||||
address = address
|
||||
))))
|
||||
}
|
||||
}))
|
||||
})
|
||||
.child(h_flex().gap_2().map(|this| {
|
||||
if !self.followed {
|
||||
this.text_sm()
|
||||
.child(
|
||||
Icon::new(IconName::CheckCircleFill)
|
||||
.small()
|
||||
.text_color(cx.theme().icon_muted),
|
||||
)
|
||||
.child(SharedString::new(t!("screening.not_contact")))
|
||||
} else {
|
||||
this.text_sm()
|
||||
.child(
|
||||
Icon::new(IconName::CheckCircleFill)
|
||||
.small()
|
||||
.text_color(cx.theme().icon_accent),
|
||||
)
|
||||
.child(SharedString::new(t!("screening.contact")))
|
||||
}
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.text_sm()
|
||||
.child(
|
||||
Icon::new(IconName::CheckCircleFill)
|
||||
.small()
|
||||
.flex_shrink_0()
|
||||
.text_color({
|
||||
if self.connections > 0 {
|
||||
cx.theme().icon_accent
|
||||
} else {
|
||||
cx.theme().icon_muted
|
||||
}
|
||||
}),
|
||||
)
|
||||
.map(|this| {
|
||||
if self.connections > 0 {
|
||||
this.child(SharedString::new(t!(
|
||||
"screening.total_connections",
|
||||
u = self.connections
|
||||
)))
|
||||
} else {
|
||||
this.child(SharedString::new(t!("screening.no_connections")))
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, rems, App, ClickEvent, Div, InteractiveElement, IntoElement, ParentElement as _,
|
||||
RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
|
||||
};
|
||||
use settings::AppSettings;
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::StyledExt;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct DisplayRoom {
|
||||
ix: usize,
|
||||
base: Div,
|
||||
img: Option<SharedString>,
|
||||
label: Option<SharedString>,
|
||||
description: Option<SharedString>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
handler: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>,
|
||||
}
|
||||
|
||||
impl DisplayRoom {
|
||||
pub fn new(ix: usize) -> Self {
|
||||
Self {
|
||||
ix,
|
||||
base: div().h_9().w_full().px_1p5(),
|
||||
img: None,
|
||||
label: None,
|
||||
description: None,
|
||||
handler: Rc::new(|_, _, _| {}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
|
||||
self.label = Some(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn description(mut self, description: impl Into<SharedString>) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn img(mut self, img: impl Into<SharedString>) -> Self {
|
||||
self.img = Some(img.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.handler = Rc::new(handler);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for DisplayRoom {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let handler = self.handler.clone();
|
||||
let hide_avatar = AppSettings::get_global(cx).settings.hide_user_avatars;
|
||||
|
||||
self.base
|
||||
.id(self.ix)
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.text_sm()
|
||||
.rounded(cx.theme().radius)
|
||||
.when(!hide_avatar, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.size_6()
|
||||
.rounded_full()
|
||||
.overflow_hidden()
|
||||
.map(|this| {
|
||||
if let Some(path) = self.img {
|
||||
this.child(Avatar::new(path).size(rems(1.5)))
|
||||
} else {
|
||||
this.child(
|
||||
img("brand/avatar.png")
|
||||
.rounded_full()
|
||||
.size_6()
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.when_some(self.label, |this, label| {
|
||||
this.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.line_clamp(1)
|
||||
.text_ellipsis()
|
||||
.font_medium()
|
||||
.child(label),
|
||||
)
|
||||
})
|
||||
.when_some(self.description, |this, description| {
|
||||
this.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.child(description),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.hover(|this| this.bg(cx.theme().elevated_surface_background))
|
||||
.on_click(move |ev, window, cx| handler(ev, window, cx))
|
||||
}
|
||||
}
|
||||
174
crates/coop/src/views/sidebar/list_item.rs
Normal file
174
crates/coop/src/views/sidebar/list_item.rs
Normal file
@@ -0,0 +1,174 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, rems, App, ClickEvent, Div, InteractiveElement, IntoElement,
|
||||
ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
|
||||
};
|
||||
use i18n::t;
|
||||
use nostr_sdk::prelude::*;
|
||||
use registry::room::RoomKind;
|
||||
use settings::AppSettings;
|
||||
use theme::ActiveTheme;
|
||||
use ui::actions::OpenProfile;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::context_menu::ContextMenuExt;
|
||||
use ui::modal::ModalButtonProps;
|
||||
use ui::{h_flex, ContextModal, StyledExt};
|
||||
|
||||
use crate::views::screening;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct RoomListItem {
|
||||
ix: usize,
|
||||
base: Div,
|
||||
public_key: PublicKey,
|
||||
name: Option<SharedString>,
|
||||
avatar: Option<SharedString>,
|
||||
created_at: Option<SharedString>,
|
||||
kind: Option<RoomKind>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
handler: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>,
|
||||
}
|
||||
|
||||
impl RoomListItem {
|
||||
pub fn new(ix: usize, public_key: PublicKey) -> Self {
|
||||
Self {
|
||||
ix,
|
||||
public_key,
|
||||
base: h_flex().h_9().w_full().px_1p5(),
|
||||
name: None,
|
||||
avatar: None,
|
||||
created_at: None,
|
||||
kind: None,
|
||||
handler: Rc::new(|_, _, _| {}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(mut self, name: impl Into<SharedString>) -> Self {
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn created_at(mut self, created_at: impl Into<SharedString>) -> Self {
|
||||
self.created_at = Some(created_at.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn avatar(mut self, avatar: impl Into<SharedString>) -> Self {
|
||||
self.avatar = Some(avatar.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn kind(mut self, kind: RoomKind) -> Self {
|
||||
self.kind = Some(kind);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.handler = Rc::new(handler);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for RoomListItem {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let public_key = self.public_key;
|
||||
let kind = self.kind;
|
||||
let handler = self.handler.clone();
|
||||
let hide_avatar = AppSettings::get_global(cx).settings.hide_user_avatars;
|
||||
let screening = AppSettings::get_global(cx).settings.screening;
|
||||
|
||||
self.base
|
||||
.id(self.ix)
|
||||
.gap_2()
|
||||
.text_sm()
|
||||
.rounded(cx.theme().radius)
|
||||
.when(!hide_avatar, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.size_6()
|
||||
.rounded_full()
|
||||
.overflow_hidden()
|
||||
.map(|this| {
|
||||
if let Some(img) = self.avatar {
|
||||
this.child(Avatar::new(img).size(rems(1.5)))
|
||||
} else {
|
||||
this.child(
|
||||
img("brand/avatar.png")
|
||||
.rounded_full()
|
||||
.size_6()
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.when_some(self.name, |this, name| {
|
||||
this.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.line_clamp(1)
|
||||
.text_ellipsis()
|
||||
.truncate()
|
||||
.font_medium()
|
||||
.child(name),
|
||||
)
|
||||
})
|
||||
.when_some(self.created_at, |this, ago| {
|
||||
this.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.child(ago),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.context_menu(move |this, _window, _cx| {
|
||||
// TODO: add share chat room
|
||||
this.menu(t!("profile.view"), Box::new(OpenProfile(public_key)))
|
||||
})
|
||||
.hover(|this| this.bg(cx.theme().elevated_surface_background))
|
||||
.on_click(move |event, window, cx| {
|
||||
let handler = handler.clone();
|
||||
|
||||
if let Some(kind) = kind {
|
||||
if kind != RoomKind::Ongoing && screening {
|
||||
let screening = screening::init(public_key, window, cx);
|
||||
|
||||
window.open_modal(cx, move |this, _window, _cx| {
|
||||
let handler_clone = handler.clone();
|
||||
|
||||
this.confirm()
|
||||
.child(screening.clone())
|
||||
.button_props(
|
||||
ModalButtonProps::default()
|
||||
.cancel_text(t!("screening.ignore"))
|
||||
.ok_text(t!("screening.response")),
|
||||
)
|
||||
.on_ok(move |event, window, cx| {
|
||||
handler_clone(event, window, cx);
|
||||
// true to close the modal
|
||||
true
|
||||
})
|
||||
});
|
||||
} else {
|
||||
handler(event, window, cx)
|
||||
}
|
||||
} else {
|
||||
handler(event, window, cx)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@ use std::time::Duration;
|
||||
use anyhow::{anyhow, Error};
|
||||
use common::debounced_delay::DebouncedDelay;
|
||||
use common::display::DisplayProfile;
|
||||
use common::nip05::nip05_verify;
|
||||
use element::DisplayRoom;
|
||||
use global::constants::{BOOTSTRAP_RELAYS, DEFAULT_MODAL_WIDTH, SEARCH_RELAYS};
|
||||
use global::nostr_client;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
@@ -20,6 +18,7 @@ use gpui_tokio::Tokio;
|
||||
use i18n::t;
|
||||
use identity::Identity;
|
||||
use itertools::Itertools;
|
||||
use list_item::RoomListItem;
|
||||
use nostr_sdk::prelude::*;
|
||||
use registry::room::{Room, RoomKind};
|
||||
use registry::{Registry, RoomEmitter};
|
||||
@@ -37,7 +36,7 @@ use ui::{ContextModal, IconName, Selectable, Sizable, StyledExt};
|
||||
|
||||
use crate::views::compose;
|
||||
|
||||
mod element;
|
||||
mod list_item;
|
||||
|
||||
const FIND_DELAY: u64 = 600;
|
||||
const FIND_LIMIT: usize = 10;
|
||||
@@ -58,7 +57,6 @@ pub struct Sidebar {
|
||||
// Rooms
|
||||
indicator: Entity<Option<RoomKind>>,
|
||||
active_filter: Entity<RoomKind>,
|
||||
trusted_only: bool,
|
||||
// GPUI
|
||||
focus_handle: FocusHandle,
|
||||
image_cache: Entity<RetainAllImageCache>,
|
||||
@@ -129,7 +127,6 @@ impl Sidebar {
|
||||
image_cache: RetainAllImageCache::new(cx),
|
||||
find_debouncer: DebouncedDelay::new(),
|
||||
finding: false,
|
||||
trusted_only: false,
|
||||
cancel_handle,
|
||||
indicator,
|
||||
active_filter,
|
||||
@@ -153,10 +150,16 @@ impl Sidebar {
|
||||
}
|
||||
|
||||
async fn create_temp_room(identity: PublicKey, public_key: PublicKey) -> Result<Room, Error> {
|
||||
let client = nostr_client();
|
||||
let keys = Keys::generate();
|
||||
let builder = EventBuilder::private_msg_rumor(public_key, "");
|
||||
let event = builder.build(identity).sign(&keys).await?;
|
||||
let room = Room::new(&event).kind(RoomKind::Ongoing);
|
||||
|
||||
// Request to get user's metadata
|
||||
Self::request_metadata(client, public_key).await?;
|
||||
|
||||
// Create a temporary room
|
||||
let room = Room::new(&event).rearrange_by(identity);
|
||||
|
||||
Ok(room)
|
||||
}
|
||||
@@ -165,7 +168,6 @@ impl Sidebar {
|
||||
let client = nostr_client();
|
||||
let timeout = Duration::from_secs(2);
|
||||
let mut rooms: BTreeSet<Room> = BTreeSet::new();
|
||||
let mut processed: BTreeSet<PublicKey> = BTreeSet::new();
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::Metadata)
|
||||
@@ -177,24 +179,13 @@ impl Sidebar {
|
||||
.await
|
||||
{
|
||||
// Process to verify the search results
|
||||
for event in events.into_iter() {
|
||||
if processed.contains(&event.pubkey) {
|
||||
for event in events.into_iter().unique_by(|event| event.pubkey) {
|
||||
// Skip if author is match current user
|
||||
if event.pubkey == identity {
|
||||
continue;
|
||||
}
|
||||
processed.insert(event.pubkey);
|
||||
|
||||
let metadata = Metadata::from_json(event.content).unwrap_or_default();
|
||||
|
||||
// Skip if NIP-05 is not found
|
||||
let Some(target) = metadata.nip05.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Skip if NIP-05 is not valid or failed to verify
|
||||
if !nip05_verify(event.pubkey, target).await.unwrap_or(false) {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Return a temporary room
|
||||
if let Ok(room) = Self::create_temp_room(identity, event.pubkey).await {
|
||||
rooms.insert(room);
|
||||
}
|
||||
@@ -299,14 +290,8 @@ impl Sidebar {
|
||||
let address = query.to_owned();
|
||||
|
||||
let task = Tokio::spawn(cx, async move {
|
||||
let client = nostr_client();
|
||||
|
||||
if let Ok(profile) = common::nip05::nip05_profile(&address).await {
|
||||
let public_key = profile.public_key;
|
||||
// Request for user metadata
|
||||
Self::request_metadata(client, public_key).await.ok();
|
||||
// Return a temporary room
|
||||
Self::create_temp_room(identity, public_key).await
|
||||
Self::create_temp_room(identity, profile.public_key).await
|
||||
} else {
|
||||
Err(anyhow!(t!("sidebar.addr_error")))
|
||||
}
|
||||
@@ -362,11 +347,6 @@ impl Sidebar {
|
||||
};
|
||||
|
||||
let task: Task<Result<Room, Error>> = cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
|
||||
// Request metadata for this user
|
||||
Self::request_metadata(client, public_key).await?;
|
||||
|
||||
// Create a gift wrap event to represent as room
|
||||
Self::create_temp_room(identity, public_key).await
|
||||
});
|
||||
@@ -544,11 +524,6 @@ impl Sidebar {
|
||||
});
|
||||
}
|
||||
|
||||
fn set_trusted_only(&mut self, cx: &mut Context<Self>) {
|
||||
self.trusted_only = !self.trusted_only;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn open_room(&mut self, id: u64, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let room = if let Some(room) = Registry::read_global(cx).room(&id, cx) {
|
||||
room
|
||||
@@ -695,20 +670,19 @@ impl Sidebar {
|
||||
for ix in range {
|
||||
if let Some(room) = rooms.get(ix) {
|
||||
let this = room.read(cx);
|
||||
let id = this.id;
|
||||
let ago = this.ago();
|
||||
let label = this.display_name(cx);
|
||||
let img = this.display_image(proxy, cx);
|
||||
|
||||
let handler = cx.listener(move |this, _, window, cx| {
|
||||
this.open_room(id, window, cx);
|
||||
let handler = cx.listener({
|
||||
let id = this.id;
|
||||
move |this, _, window, cx| {
|
||||
this.open_room(id, window, cx);
|
||||
}
|
||||
});
|
||||
|
||||
items.push(
|
||||
DisplayRoom::new(ix)
|
||||
.img(img)
|
||||
.label(label)
|
||||
.description(ago)
|
||||
RoomListItem::new(ix, this.members[0])
|
||||
.avatar(this.display_image(proxy, cx))
|
||||
.name(this.display_name(cx))
|
||||
.created_at(this.ago())
|
||||
.kind(this.kind)
|
||||
.on_click(handler),
|
||||
)
|
||||
}
|
||||
@@ -761,7 +735,7 @@ impl Render for Sidebar {
|
||||
if self.active_filter.read(cx) == &RoomKind::Ongoing {
|
||||
registry.ongoing_rooms(cx)
|
||||
} else {
|
||||
registry.request_rooms(self.trusted_only, cx)
|
||||
registry.request_rooms(cx)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -872,28 +846,10 @@ impl Render for Sidebar {
|
||||
.rounded(ButtonRounded::Full)
|
||||
.selected(!self.filter(&RoomKind::Ongoing, cx))
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.set_filter(RoomKind::Unknown, cx);
|
||||
this.set_filter(RoomKind::default(), cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
.when(!self.filter(&RoomKind::Ongoing, cx), |this| {
|
||||
this.child(
|
||||
Button::new("trusted")
|
||||
.tooltip(t!("sidebar.trusted_contacts_tooltip"))
|
||||
.map(|this| {
|
||||
if self.trusted_only {
|
||||
this.icon(IconName::FilterFill)
|
||||
} else {
|
||||
this.icon(IconName::Filter)
|
||||
}
|
||||
})
|
||||
.small()
|
||||
.transparent()
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.set_trusted_only(cx);
|
||||
})),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.when(registry.loading, |this| {
|
||||
this.child(
|
||||
@@ -918,7 +874,7 @@ impl Render for Sidebar {
|
||||
)
|
||||
.when(registry.loading, |this| {
|
||||
this.child(
|
||||
div().absolute().bottom_4().px_4().child(
|
||||
div().absolute().bottom_4().px_4().w_full().child(
|
||||
div()
|
||||
.p_1()
|
||||
.w_full()
|
||||
|
||||
@@ -188,10 +188,13 @@ impl Render for UserProfile {
|
||||
.when(!self.followed, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.w_32()
|
||||
.p_1()
|
||||
.rounded_full()
|
||||
.bg(cx.theme().surface_background)
|
||||
.bg(cx.theme().elevated_surface_background)
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.child(SharedString::new(t!("profile.unknown"))),
|
||||
)
|
||||
}),
|
||||
@@ -211,7 +214,7 @@ impl Render for UserProfile {
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.p_1p5()
|
||||
.p_2()
|
||||
.h_9()
|
||||
.rounded_md()
|
||||
.bg(cx.theme().elevated_surface_background)
|
||||
@@ -246,15 +249,18 @@ impl Render for UserProfile {
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::new(t!("profile.label_bio"))),
|
||||
)
|
||||
.when_some(profile.metadata().about, |this, bio| {
|
||||
this.child(
|
||||
div()
|
||||
.p_1p5()
|
||||
.rounded_md()
|
||||
.bg(cx.theme().elevated_surface_background)
|
||||
.child(bio),
|
||||
)
|
||||
}),
|
||||
.child(
|
||||
div()
|
||||
.p_2()
|
||||
.rounded_md()
|
||||
.bg(cx.theme().elevated_surface_background)
|
||||
.child(
|
||||
profile
|
||||
.metadata()
|
||||
.about
|
||||
.unwrap_or(t!("profile.no_bio").to_string()),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("open-njump")
|
||||
|
||||
Reference in New Issue
Block a user