feat: rewrite the nip-4e implementation (#1)
Some checks are pending
Rust / build (macos-latest, stable) (push) Waiting to run
Rust / build (ubuntu-latest, stable) (push) Waiting to run
Rust / build (windows-latest, stable) (push) Waiting to run
Some checks are pending
Rust / build (macos-latest, stable) (push) Waiting to run
Rust / build (ubuntu-latest, stable) (push) Waiting to run
Rust / build (windows-latest, stable) (push) Waiting to run
Make NIP-4e a core feature, not an optional feature. Note: - The UI is broken and needs to be updated in a separate PR. Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -9,8 +9,6 @@ state = { path = "../state" }
|
||||
ui = { path = "../ui" }
|
||||
theme = { path = "../theme" }
|
||||
common = { path = "../common" }
|
||||
account = { path = "../account" }
|
||||
encryption = { path = "../encryption" }
|
||||
person = { path = "../person" }
|
||||
chat = { path = "../chat" }
|
||||
settings = { path = "../settings" }
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use encryption::SignerKind;
|
||||
use gpui::Action;
|
||||
use nostr_sdk::prelude::*;
|
||||
use serde::Deserialize;
|
||||
@@ -7,10 +6,6 @@ use serde::Deserialize;
|
||||
#[action(namespace = chat, no_json)]
|
||||
pub struct SeenOn(pub EventId);
|
||||
|
||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[action(namespace = chat, no_json)]
|
||||
pub struct SetSigner(pub SignerKind);
|
||||
|
||||
/// Define a open public key action
|
||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize, Debug)]
|
||||
#[action(namespace = pubkey, no_json)]
|
||||
|
||||
@@ -2,22 +2,21 @@ use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
|
||||
pub use actions::*;
|
||||
use chat::{Message, RenderedMessage, Room, RoomKind, RoomSignal, SendOptions, SendReport};
|
||||
use common::{nip96_upload, RenderedProfile, RenderedTimestamp};
|
||||
use encryption::SignerKind;
|
||||
use chat::{Message, RenderedMessage, Room, RoomEvent, RoomKind, SendReport};
|
||||
use common::{nip96_upload, RenderedTimestamp};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, list, px, red, relative, rems, svg, white, AnyElement, App, AppContext,
|
||||
ClipboardItem, Context, Element, Entity, EventEmitter, Flatten, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, ListAlignment, ListOffset, ListState, MouseButton, ObjectFit,
|
||||
ParentElement, PathPromptOptions, Render, RetainAllImageCache, SharedString,
|
||||
StatefulInteractiveElement, Styled, StyledImage, Subscription, Task, Window,
|
||||
ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
|
||||
IntoElement, ListAlignment, ListOffset, ListState, MouseButton, ObjectFit, ParentElement,
|
||||
PathPromptOptions, Render, RetainAllImageCache, SharedString, StatefulInteractiveElement,
|
||||
Styled, StyledImage, Subscription, Task, WeakEntity, Window,
|
||||
};
|
||||
use gpui_tokio::Tokio;
|
||||
use indexset::{BTreeMap, BTreeSet};
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::PersonRegistry;
|
||||
use person::{Person, PersonRegistry};
|
||||
use settings::AppSettings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use smol::fs;
|
||||
@@ -28,7 +27,6 @@ use ui::button::{Button, ButtonVariants};
|
||||
use ui::context_menu::ContextMenuExt;
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::modal::ModalButtonProps;
|
||||
use ui::notification::Notification;
|
||||
use ui::popup_menu::PopupMenuExt;
|
||||
use ui::{
|
||||
@@ -41,43 +39,54 @@ use crate::text::RenderedText;
|
||||
|
||||
mod actions;
|
||||
mod emoji;
|
||||
mod subject;
|
||||
mod text;
|
||||
|
||||
pub fn init(room: Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<ChatPanel> {
|
||||
pub fn init(room: WeakEntity<Room>, window: &mut Window, cx: &mut App) -> Entity<ChatPanel> {
|
||||
cx.new(|cx| ChatPanel::new(room, window, cx))
|
||||
}
|
||||
|
||||
/// Chat Panel
|
||||
pub struct ChatPanel {
|
||||
// Chat Room
|
||||
room: Entity<Room>,
|
||||
|
||||
// Messages
|
||||
list_state: ListState,
|
||||
messages: BTreeSet<Message>,
|
||||
rendered_texts_by_id: BTreeMap<EventId, RenderedText>,
|
||||
reports_by_id: BTreeMap<EventId, Vec<SendReport>>,
|
||||
|
||||
// New Message
|
||||
input: Entity<InputState>,
|
||||
options: Entity<SendOptions>,
|
||||
replies_to: Entity<HashSet<EventId>>,
|
||||
|
||||
// Media Attachment
|
||||
attachments: Entity<Vec<Url>>,
|
||||
uploading: bool,
|
||||
|
||||
// Panel
|
||||
id: SharedString,
|
||||
focus_handle: FocusHandle,
|
||||
image_cache: Entity<RetainAllImageCache>,
|
||||
|
||||
_subscriptions: SmallVec<[Subscription; 3]>,
|
||||
_tasks: SmallVec<[Task<()>; 2]>,
|
||||
/// Chat Room
|
||||
room: WeakEntity<Room>,
|
||||
|
||||
/// Message list state
|
||||
list_state: ListState,
|
||||
|
||||
/// All messages
|
||||
messages: BTreeSet<Message>,
|
||||
|
||||
/// Mapping message ids to their rendered texts
|
||||
rendered_texts_by_id: BTreeMap<EventId, RenderedText>,
|
||||
|
||||
/// Mapping message ids to their reports
|
||||
reports_by_id: BTreeMap<EventId, Vec<SendReport>>,
|
||||
|
||||
/// Input state
|
||||
input: Entity<InputState>,
|
||||
|
||||
/// Replies to
|
||||
replies_to: Entity<HashSet<EventId>>,
|
||||
|
||||
/// Media Attachment
|
||||
attachments: Entity<Vec<Url>>,
|
||||
|
||||
/// Upload state
|
||||
uploading: bool,
|
||||
|
||||
/// Async operations
|
||||
tasks: SmallVec<[Task<()>; 2]>,
|
||||
|
||||
/// Event subscriptions
|
||||
_subscriptions: SmallVec<[Subscription; 2]>,
|
||||
}
|
||||
|
||||
impl ChatPanel {
|
||||
pub fn new(room: Entity<Room>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
pub fn new(room: WeakEntity<Room>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder("Message...")
|
||||
@@ -88,45 +97,64 @@ impl ChatPanel {
|
||||
|
||||
let attachments = cx.new(|_| vec![]);
|
||||
let replies_to = cx.new(|_| HashSet::new());
|
||||
let options = cx.new(|_| SendOptions::default());
|
||||
|
||||
let id = room.read(cx).id.to_string().into();
|
||||
let messages = BTreeSet::from([Message::system()]);
|
||||
let list_state = ListState::new(messages.len(), ListAlignment::Bottom, px(1024.));
|
||||
|
||||
let connect = room.read(cx).connect(cx);
|
||||
let get_messages = room.read(cx).get_messages(cx);
|
||||
let id: SharedString = room
|
||||
.read_with(cx, |this, _cx| this.id.to_string().into())
|
||||
.unwrap_or("Unknown".into());
|
||||
|
||||
let mut subscriptions = smallvec![];
|
||||
let mut tasks = smallvec![];
|
||||
|
||||
tasks.push(
|
||||
// Get messaging relays and encryption keys announcement for each member
|
||||
cx.background_spawn(async move {
|
||||
if let Err(e) = connect.await {
|
||||
log::error!("Failed to initialize room: {}", e);
|
||||
}
|
||||
}),
|
||||
);
|
||||
if let Ok(connect) = room.read_with(cx, |this, cx| this.connect(cx)) {
|
||||
tasks.push(
|
||||
// Get messaging relays and encryption keys announcement for each member
|
||||
cx.background_spawn(async move {
|
||||
if let Err(e) = connect.await {
|
||||
log::error!("Failed to initialize room: {}", e);
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
tasks.push(
|
||||
// Load all messages belonging to this room
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let result = get_messages.await;
|
||||
if let Ok(get_messages) = room.read_with(cx, |this, cx| this.get_messages(cx)) {
|
||||
tasks.push(
|
||||
// Load all messages belonging to this room
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let result = get_messages.await;
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
match result {
|
||||
Ok(events) => {
|
||||
this.insert_messages(events, cx);
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
match result {
|
||||
Ok(events) => {
|
||||
this.insert_messages(&events, cx);
|
||||
}
|
||||
Err(e) => {
|
||||
window.push_notification(e.to_string(), cx);
|
||||
}
|
||||
};
|
||||
})
|
||||
.ok();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(room) = room.upgrade() {
|
||||
subscriptions.push(
|
||||
// Subscribe to room events
|
||||
cx.subscribe_in(&room, window, move |this, _room, event, window, cx| {
|
||||
match event {
|
||||
RoomEvent::Incoming(message) => {
|
||||
this.insert_message(message, false, cx);
|
||||
}
|
||||
Err(e) => {
|
||||
window.push_notification(e.to_string(), cx);
|
||||
RoomEvent::Reload => {
|
||||
this.load_messages(window, cx);
|
||||
}
|
||||
};
|
||||
})
|
||||
.ok();
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
subscriptions.push(
|
||||
// Subscribe to input events
|
||||
@@ -141,47 +169,6 @@ impl ChatPanel {
|
||||
),
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
// Subscribe to room events
|
||||
cx.subscribe_in(&room, window, move |this, _, signal, window, cx| {
|
||||
match signal {
|
||||
RoomSignal::NewMessage((gift_wrap_id, event)) => {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let tracker = nostr.read(cx).tracker();
|
||||
let gift_wrap_id = gift_wrap_id.to_owned();
|
||||
let message = Message::user(event.clone());
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let tracker = tracker.read().await;
|
||||
|
||||
this.update_in(cx, |this, _window, cx| {
|
||||
if !tracker.sent_ids().contains(&gift_wrap_id) {
|
||||
this.insert_message(message, false, cx);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
RoomSignal::Refresh => {
|
||||
this.load_messages(window, cx);
|
||||
}
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
// Observe when user close chat panel
|
||||
cx.on_release_in(window, move |this, window, cx| {
|
||||
this.messages.clear();
|
||||
this.rendered_texts_by_id.clear();
|
||||
this.reports_by_id.clear();
|
||||
this.image_cache.update(cx, |this, cx| {
|
||||
this.clear(window, cx);
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
Self {
|
||||
id,
|
||||
messages,
|
||||
@@ -190,30 +177,26 @@ impl ChatPanel {
|
||||
input,
|
||||
replies_to,
|
||||
attachments,
|
||||
options,
|
||||
rendered_texts_by_id: BTreeMap::new(),
|
||||
reports_by_id: BTreeMap::new(),
|
||||
uploading: false,
|
||||
image_cache: RetainAllImageCache::new(cx),
|
||||
focus_handle: cx.focus_handle(),
|
||||
_subscriptions: subscriptions,
|
||||
_tasks: tasks,
|
||||
tasks,
|
||||
}
|
||||
}
|
||||
|
||||
/// Load all messages belonging to this room
|
||||
fn load_messages(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let get_messages = self.room.read(cx).get_messages(cx);
|
||||
|
||||
self._tasks.push(
|
||||
// Run the task in the background
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if let Ok(get_messages) = self.room.read_with(cx, |this, cx| this.get_messages(cx)) {
|
||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||
let result = get_messages.await;
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
match result {
|
||||
Ok(events) => {
|
||||
this.insert_messages(events, cx);
|
||||
this.insert_messages(&events, cx);
|
||||
}
|
||||
Err(e) => {
|
||||
window.push_notification(Notification::error(e.to_string()), cx);
|
||||
@@ -221,12 +204,13 @@ impl ChatPanel {
|
||||
};
|
||||
})
|
||||
.ok();
|
||||
}),
|
||||
);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/// Get user input content and merged all attachments
|
||||
fn input_content(&self, cx: &Context<Self>) -> String {
|
||||
// Get input's value
|
||||
let mut content = self.input.read(cx).value().trim().to_string();
|
||||
|
||||
// Get all attaches and merge its with message
|
||||
@@ -260,26 +244,20 @@ impl ChatPanel {
|
||||
return;
|
||||
}
|
||||
|
||||
// Temporary disable the message input
|
||||
self.input.update(cx, |this, cx| {
|
||||
this.set_loading(false, cx);
|
||||
this.set_disabled(false, cx);
|
||||
this.set_value("", window, cx);
|
||||
});
|
||||
// Get the current room entity
|
||||
let Some(room) = self.room.upgrade().map(|this| this.read(cx)) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Get replies_to if it's present
|
||||
let replies: Vec<EventId> = self.replies_to.read(cx).iter().copied().collect();
|
||||
|
||||
// Get the current room entity
|
||||
let room = self.room.read(cx);
|
||||
let opts = self.options.read(cx);
|
||||
|
||||
// Create a temporary message for optimistic update
|
||||
let rumor = room.create_message(&content, replies.as_ref(), cx);
|
||||
let rumor_id = rumor.id.unwrap();
|
||||
|
||||
// Create a task for sending the message in the background
|
||||
let send_message = room.send_message(&rumor, opts, cx);
|
||||
let send_message = room.send_message(&rumor, cx);
|
||||
|
||||
// Optimistically update message list
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
@@ -290,29 +268,32 @@ impl ChatPanel {
|
||||
|
||||
// Update the message list and reset the states
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.insert_message(Message::user(rumor), true, cx);
|
||||
this.remove_all_replies(cx);
|
||||
this.remove_all_attachments(cx);
|
||||
|
||||
// Reset the input to its default state
|
||||
this.input.update(cx, |this, cx| {
|
||||
this.set_loading(false, cx);
|
||||
this.set_disabled(false, cx);
|
||||
this.set_value("", window, cx);
|
||||
});
|
||||
|
||||
// Update the message list
|
||||
this.insert_message(&rumor, true, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
|
||||
self._tasks.push(
|
||||
// Continue sending the message in the background
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let result = send_message.await;
|
||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||
let result = send_message.await;
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
match result {
|
||||
Ok(reports) => {
|
||||
// Update room's status
|
||||
this.room.update(cx, |this, cx| {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
match result {
|
||||
Ok(reports) => {
|
||||
// Update room's status
|
||||
this.room
|
||||
.update(cx, |this, cx| {
|
||||
if this.kind != RoomKind::Ongoing {
|
||||
// Update the room kind to ongoing,
|
||||
// but keep the room kind if send failed
|
||||
@@ -321,50 +302,21 @@ impl ChatPanel {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
|
||||
// Insert the sent reports
|
||||
this.reports_by_id.insert(rumor_id, reports);
|
||||
// Insert the sent reports
|
||||
this.reports_by_id.insert(rumor_id, reports);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
Err(e) => {
|
||||
window.push_notification(e.to_string(), cx);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/// Resend a failed message
|
||||
#[allow(dead_code)]
|
||||
fn resend_message(&mut self, id: &EventId, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(reports) = self.reports_by_id.get(id).cloned() {
|
||||
let id_clone = id.to_owned();
|
||||
let resend = self.room.read(cx).resend_message(reports, cx);
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let result = resend.await;
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
match result {
|
||||
Ok(reports) => {
|
||||
this.reports_by_id.entry(id_clone).and_modify(|this| {
|
||||
*this = reports;
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
Err(e) => {
|
||||
window.push_notification(Notification::error(e.to_string()), cx);
|
||||
}
|
||||
};
|
||||
})
|
||||
.ok();
|
||||
Err(e) => {
|
||||
window.push_notification(e.to_string(), cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
.ok();
|
||||
}));
|
||||
}
|
||||
|
||||
/// Insert a message into the chat panel
|
||||
@@ -390,21 +342,13 @@ impl ChatPanel {
|
||||
}
|
||||
|
||||
/// Convert and insert a vector of nostr events into the chat panel
|
||||
fn insert_messages(&mut self, events: Vec<UnsignedEvent>, cx: &mut Context<Self>) {
|
||||
for event in events {
|
||||
let m = Message::user(event);
|
||||
fn insert_messages(&mut self, events: &[UnsignedEvent], cx: &mut Context<Self>) {
|
||||
for event in events.iter() {
|
||||
// Bulk inserting messages, so no need to scroll to the latest message
|
||||
self.insert_message(m, false, cx);
|
||||
self.insert_message(event, false, cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a warning message into the chat panel
|
||||
#[allow(dead_code)]
|
||||
fn insert_warning(&mut self, content: impl Into<String>, cx: &mut Context<Self>) {
|
||||
let m = Message::warning(content.into());
|
||||
self.insert_message(m, true, cx);
|
||||
}
|
||||
|
||||
/// Check if a message failed to send by its ID
|
||||
fn is_sent_failed(&self, id: &EventId) -> bool {
|
||||
self.reports_by_id
|
||||
@@ -436,15 +380,6 @@ impl ChatPanel {
|
||||
})
|
||||
}
|
||||
|
||||
fn profile(&self, public_key: &PublicKey, cx: &Context<Self>) -> Profile {
|
||||
let persons = PersonRegistry::global(cx);
|
||||
persons.read(cx).get_person(public_key, cx)
|
||||
}
|
||||
|
||||
fn signer_kind(&self, cx: &App) -> SignerKind {
|
||||
self.options.read(cx).signer_kind
|
||||
}
|
||||
|
||||
fn scroll_to(&self, id: EventId) {
|
||||
if let Some(ix) = self.messages.iter().position(|m| {
|
||||
if let Message::User(msg) = m {
|
||||
@@ -511,25 +446,19 @@ impl ChatPanel {
|
||||
Some(url)
|
||||
});
|
||||
|
||||
if let Ok(task) = upload {
|
||||
if let Ok(task) = upload.await {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_uploading(true, cx);
|
||||
})
|
||||
.ok();
|
||||
|
||||
let result = Flatten::flatten(task.await.map_err(|e| e.into()));
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
match result {
|
||||
Ok(Some(url)) => {
|
||||
this.update_in(cx, |this, _window, cx| {
|
||||
match task {
|
||||
Some(url) => {
|
||||
this.add_attachment(url, cx);
|
||||
this.set_uploading(false, cx);
|
||||
}
|
||||
Ok(None) => {
|
||||
this.set_uploading(false, cx);
|
||||
}
|
||||
Err(e) => {
|
||||
window.push_notification(Notification::error(e.to_string()), cx);
|
||||
None => {
|
||||
this.set_uploading(false, cx);
|
||||
}
|
||||
};
|
||||
@@ -570,6 +499,11 @@ impl ChatPanel {
|
||||
});
|
||||
}
|
||||
|
||||
fn profile(&self, public_key: &PublicKey, cx: &Context<Self>) -> Person {
|
||||
let persons = PersonRegistry::global(cx);
|
||||
persons.read(cx).get(public_key, cx)
|
||||
}
|
||||
|
||||
fn render_announcement(&self, ix: usize, cx: &Context<Self>) -> AnyElement {
|
||||
v_flex()
|
||||
.id(ix)
|
||||
@@ -660,7 +594,6 @@ impl ChatPanel {
|
||||
text: AnyElement,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
let proxy = AppSettings::get_proxy_user_avatars(cx);
|
||||
let hide_avatar = AppSettings::get_hide_user_avatars(cx);
|
||||
|
||||
let id = message.id;
|
||||
@@ -691,7 +624,7 @@ impl ChatPanel {
|
||||
this.child(
|
||||
div()
|
||||
.id(SharedString::from(format!("{ix}-avatar")))
|
||||
.child(Avatar::new(author.avatar(proxy)).size(rems(2.)))
|
||||
.child(Avatar::new(author.avatar()).size(rems(2.)))
|
||||
.context_menu(move |this, _window, _cx| {
|
||||
let view = Box::new(OpenPublicKey(public_key));
|
||||
let copy = Box::new(CopyPublicKey(public_key));
|
||||
@@ -716,7 +649,7 @@ impl ChatPanel {
|
||||
div()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text)
|
||||
.child(author.display_name()),
|
||||
.child(author.name()),
|
||||
)
|
||||
.child(message.created_at.to_human_time())
|
||||
.when_some(is_sent_success, |this, status| {
|
||||
@@ -773,7 +706,7 @@ impl ChatPanel {
|
||||
.child(
|
||||
div()
|
||||
.text_color(cx.theme().text_accent)
|
||||
.child(author.display_name()),
|
||||
.child(author.name()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
@@ -854,9 +787,9 @@ impl ChatPanel {
|
||||
|
||||
fn render_report(report: &SendReport, cx: &App) -> impl IntoElement {
|
||||
let persons = PersonRegistry::global(cx);
|
||||
let profile = persons.read(cx).get_person(&report.receiver, cx);
|
||||
let name = profile.display_name();
|
||||
let avatar = profile.avatar(true);
|
||||
let profile = persons.read(cx).get(&report.receiver, cx);
|
||||
let name = profile.name();
|
||||
let avatar = profile.avatar();
|
||||
|
||||
v_flex()
|
||||
.gap_2()
|
||||
@@ -1116,7 +1049,7 @@ impl ChatPanel {
|
||||
fn render_reply(&self, id: &EventId, cx: &Context<Self>) -> impl IntoElement {
|
||||
if let Some(text) = self.message(id) {
|
||||
let persons = PersonRegistry::global(cx);
|
||||
let profile = persons.read(cx).get_person(&text.author, cx);
|
||||
let profile = persons.read(cx).get(&text.author, cx);
|
||||
|
||||
div()
|
||||
.w_full()
|
||||
@@ -1139,7 +1072,7 @@ impl ChatPanel {
|
||||
.child(
|
||||
div()
|
||||
.text_color(cx.theme().text_accent)
|
||||
.child(profile.display_name()),
|
||||
.child(profile.name()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -1181,126 +1114,6 @@ impl ChatPanel {
|
||||
|
||||
items
|
||||
}
|
||||
|
||||
fn subject_button(&self, cx: &App) -> Button {
|
||||
let room = self.room.downgrade();
|
||||
let subject = self
|
||||
.room
|
||||
.read(cx)
|
||||
.subject
|
||||
.as_ref()
|
||||
.map(|subject| subject.to_string());
|
||||
|
||||
Button::new("subject")
|
||||
.icon(IconName::Edit)
|
||||
.tooltip("Change the subject of the conversation")
|
||||
.on_click(move |_, window, cx| {
|
||||
let view = subject::init(subject.clone(), window, cx);
|
||||
let room = room.clone();
|
||||
let weak_view = view.downgrade();
|
||||
|
||||
window.open_modal(cx, move |this, _window, _cx| {
|
||||
let room = room.clone();
|
||||
let weak_view = weak_view.clone();
|
||||
|
||||
this.confirm()
|
||||
.title("Change the subject of the conversation")
|
||||
.child(view.clone())
|
||||
.button_props(ModalButtonProps::default().ok_text("Change"))
|
||||
.on_ok(move |_, _window, cx| {
|
||||
if let Ok(subject) =
|
||||
weak_view.read_with(cx, |this, cx| this.new_subject(cx))
|
||||
{
|
||||
room.update(cx, |this, cx| {
|
||||
this.set_subject(subject, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
// true to close the modal
|
||||
true
|
||||
})
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
fn reload_button(&self, _cx: &App) -> Button {
|
||||
let room = self.room.downgrade();
|
||||
|
||||
Button::new("reload")
|
||||
.icon(IconName::Refresh)
|
||||
.tooltip("Reload")
|
||||
.on_click(move |_ev, window, cx| {
|
||||
_ = room.update(cx, |this, cx| {
|
||||
this.emit_refresh(cx);
|
||||
window.push_notification("Reloaded", cx);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
fn on_open_seen_on(&mut self, ev: &SeenOn, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let id = ev.0;
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
let tracker = nostr.read(cx).tracker();
|
||||
|
||||
let task: Task<Result<Vec<RelayUrl>, Error>> = cx.background_spawn(async move {
|
||||
let tracker = tracker.read().await;
|
||||
let mut relays: Vec<RelayUrl> = vec![];
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::ApplicationSpecificData)
|
||||
.event(id)
|
||||
.limit(1);
|
||||
|
||||
if let Some(event) = client.database().query(filter).await?.first_owned() {
|
||||
if let Some(Ok(id)) = event.tags.identifier().map(EventId::parse) {
|
||||
if let Some(urls) = tracker.seen_on_relays.get(&id).cloned() {
|
||||
relays.extend(urls);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(relays)
|
||||
});
|
||||
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
if let Ok(urls) = task.await {
|
||||
cx.update(|window, cx| {
|
||||
window.open_modal(cx, move |this, _window, cx| {
|
||||
this.show_close(true)
|
||||
.title(SharedString::from("Seen on"))
|
||||
.child(v_flex().pb_4().gap_2().children({
|
||||
let mut items = Vec::with_capacity(urls.len());
|
||||
|
||||
for url in urls.clone().into_iter() {
|
||||
items.push(
|
||||
h_flex()
|
||||
.h_8()
|
||||
.px_2()
|
||||
.bg(cx.theme().elevated_surface_background)
|
||||
.rounded(cx.theme().radius)
|
||||
.font_semibold()
|
||||
.text_xs()
|
||||
.child(SharedString::from(url.to_string())),
|
||||
)
|
||||
}
|
||||
|
||||
items
|
||||
}))
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn on_set_encryption(&mut self, ev: &SetSigner, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.options.update(cx, move |this, cx| {
|
||||
this.signer_kind = ev.0;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for ChatPanel {
|
||||
@@ -1309,24 +1122,18 @@ impl Panel for ChatPanel {
|
||||
}
|
||||
|
||||
fn title(&self, cx: &App) -> AnyElement {
|
||||
self.room.read_with(cx, |this, cx| {
|
||||
let proxy = AppSettings::get_proxy_user_avatars(cx);
|
||||
let label = this.display_name(cx);
|
||||
let url = this.display_image(proxy, cx);
|
||||
self.room
|
||||
.read_with(cx, |this, cx| {
|
||||
let label = this.display_name(cx);
|
||||
let url = this.display_image(cx);
|
||||
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Avatar::new(url).size(rems(1.25)))
|
||||
.child(label)
|
||||
.into_any()
|
||||
})
|
||||
}
|
||||
|
||||
fn toolbar_buttons(&self, _window: &Window, cx: &App) -> Vec<Button> {
|
||||
let subject_button = self.subject_button(cx);
|
||||
let reload_button = self.reload_button(cx);
|
||||
|
||||
vec![subject_button, reload_button]
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Avatar::new(url).size(rems(1.25)))
|
||||
.child(label)
|
||||
.into_any_element()
|
||||
})
|
||||
.unwrap_or(div().child("Unknown").into_any_element())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1340,11 +1147,7 @@ impl Focusable for ChatPanel {
|
||||
|
||||
impl Render for ChatPanel {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let kind = self.signer_kind(cx);
|
||||
|
||||
v_flex()
|
||||
.on_action(cx.listener(Self::on_open_seen_on))
|
||||
.on_action(cx.listener(Self::on_set_encryption))
|
||||
.image_cache(self.image_cache.clone())
|
||||
.size_full()
|
||||
.child(
|
||||
@@ -1399,31 +1202,7 @@ impl Render for ChatPanel {
|
||||
.large(),
|
||||
),
|
||||
)
|
||||
.child(TextInput::new(&self.input))
|
||||
.child(
|
||||
Button::new("encryptions")
|
||||
.icon(IconName::Encryption)
|
||||
.ghost()
|
||||
.large()
|
||||
.popup_menu(move |this, _window, _cx| {
|
||||
this.label("Encrypt by:")
|
||||
.menu_with_check(
|
||||
"Encryption Key",
|
||||
matches!(kind, SignerKind::Encryption),
|
||||
Box::new(SetSigner(SignerKind::Encryption)),
|
||||
)
|
||||
.menu_with_check(
|
||||
"User's Identity",
|
||||
matches!(kind, SignerKind::User),
|
||||
Box::new(SetSigner(SignerKind::User)),
|
||||
)
|
||||
.menu_with_check(
|
||||
"Auto",
|
||||
matches!(kind, SignerKind::Auto),
|
||||
Box::new(SetSigner(SignerKind::Auto)),
|
||||
)
|
||||
}),
|
||||
),
|
||||
.child(TextInput::new(&self.input)),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
use common::RenderedProfile;
|
||||
use gpui::{
|
||||
AnyElement, App, ElementId, HighlightStyle, InteractiveText, IntoElement, SharedString,
|
||||
StyledText, UnderlineStyle, Window,
|
||||
@@ -254,8 +253,8 @@ fn render_pubkey(
|
||||
cx: &App,
|
||||
) {
|
||||
let persons = PersonRegistry::global(cx);
|
||||
let profile = persons.read(cx).get_person(&public_key, cx);
|
||||
let display_name = format!("@{}", profile.display_name());
|
||||
let profile = persons.read(cx).get(&public_key, cx);
|
||||
let display_name = format!("@{}", profile.name());
|
||||
|
||||
text.replace_range(range.clone(), &display_name);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user