wip
This commit is contained in:
@@ -10,7 +10,7 @@ use common::{EventUtils, BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT};
|
||||
use flume::Sender;
|
||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||
use fuzzy_matcher::FuzzyMatcher;
|
||||
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task};
|
||||
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task, WeakEntity};
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
@@ -30,6 +30,28 @@ struct GlobalChatRegistry(Entity<ChatRegistry>);
|
||||
|
||||
impl Global for GlobalChatRegistry {}
|
||||
|
||||
/// Chat event.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum ChatEvent {
|
||||
/// An event to open a room by its ID
|
||||
OpenRoom(u64),
|
||||
/// An event to close a room by its ID
|
||||
CloseRoom(u64),
|
||||
/// An event to notify UI about a new chat request
|
||||
Ping,
|
||||
}
|
||||
|
||||
/// Channel signal.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum NostrEvent {
|
||||
/// Message received from relay pool
|
||||
Message(NewMessage),
|
||||
/// Unwrapping status
|
||||
Unwrapping(bool),
|
||||
/// Eose received from relay pool
|
||||
Eose,
|
||||
}
|
||||
|
||||
/// Chat Registry
|
||||
#[derive(Debug)]
|
||||
pub struct ChatRegistry {
|
||||
@@ -40,26 +62,7 @@ pub struct ChatRegistry {
|
||||
loading: bool,
|
||||
|
||||
/// Tasks for asynchronous operations
|
||||
_tasks: SmallVec<[Task<()>; 4]>,
|
||||
}
|
||||
|
||||
/// Chat event.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum ChatEvent {
|
||||
OpenRoom(u64),
|
||||
CloseRoom(u64),
|
||||
NewRequest(RoomKind),
|
||||
}
|
||||
|
||||
/// Channel signal.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum Signal {
|
||||
/// Message received from relay pool
|
||||
Message(NewMessage),
|
||||
/// Loading status of the registry
|
||||
Loading(bool),
|
||||
/// Eose received from relay pool
|
||||
Eose,
|
||||
_tasks: SmallVec<[Task<()>; 3]>,
|
||||
}
|
||||
|
||||
impl EventEmitter<ChatEvent> for ChatRegistry {}
|
||||
@@ -84,7 +87,7 @@ impl ChatRegistry {
|
||||
let status = Arc::new(AtomicBool::new(true));
|
||||
|
||||
// Channel for communication between nostr and gpui
|
||||
let (tx, rx) = flume::bounded::<Signal>(2048);
|
||||
let (tx, rx) = flume::bounded::<NostrEvent>(2048);
|
||||
|
||||
let mut tasks = smallvec![];
|
||||
|
||||
@@ -102,7 +105,7 @@ impl ChatRegistry {
|
||||
tasks.push(
|
||||
// Handle unwrapping progress
|
||||
cx.background_spawn(
|
||||
async move { Self::handle_unwrapping(&client, &status, &tx).await },
|
||||
async move { Self::unwrapping_status(&client, &status, &tx).await },
|
||||
),
|
||||
);
|
||||
|
||||
@@ -111,19 +114,19 @@ impl ChatRegistry {
|
||||
cx.spawn(async move |this, cx| {
|
||||
while let Ok(message) = rx.recv_async().await {
|
||||
match message {
|
||||
Signal::Message(message) => {
|
||||
NostrEvent::Message(message) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.new_message(message, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Signal::Eose => {
|
||||
NostrEvent::Eose => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.get_rooms(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Signal::Loading(status) => {
|
||||
NostrEvent::Unwrapping(status) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_loading(status, cx);
|
||||
this.get_rooms(cx);
|
||||
@@ -142,7 +145,11 @@ impl ChatRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_notifications(client: &Client, loading: &Arc<AtomicBool>, tx: &Sender<Signal>) {
|
||||
async fn handle_notifications(
|
||||
client: &Client,
|
||||
loading: &Arc<AtomicBool>,
|
||||
tx: &Sender<NostrEvent>,
|
||||
) {
|
||||
let initialized_at = Timestamp::now();
|
||||
let subscription_id = SubscriptionId::new(GIFTWRAP_SUBSCRIPTION);
|
||||
|
||||
@@ -193,7 +200,7 @@ impl ChatRegistry {
|
||||
|
||||
if !sent_by_coop {
|
||||
let new_message = NewMessage::new(event.id, rumor);
|
||||
let signal = Signal::Message(new_message);
|
||||
let signal = NostrEvent::Message(new_message);
|
||||
|
||||
if let Err(e) = tx.send_async(signal).await {
|
||||
log::error!("Failed to send signal: {}", e);
|
||||
@@ -212,7 +219,7 @@ impl ChatRegistry {
|
||||
}
|
||||
RelayMessage::EndOfStoredEvents(id) => {
|
||||
if id.as_ref() == &subscription_id {
|
||||
if let Err(e) = tx.send_async(Signal::Eose).await {
|
||||
if let Err(e) = tx.send_async(NostrEvent::Eose).await {
|
||||
log::error!("Failed to send signal: {}", e);
|
||||
}
|
||||
}
|
||||
@@ -222,7 +229,7 @@ impl ChatRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_unwrapping(client: &Client, status: &Arc<AtomicBool>, tx: &Sender<Signal>) {
|
||||
async fn unwrapping_status(client: &Client, status: &Arc<AtomicBool>, tx: &Sender<NostrEvent>) {
|
||||
let loop_duration = Duration::from_secs(20);
|
||||
let mut is_start_processing = false;
|
||||
let mut total_loops = 0;
|
||||
@@ -238,7 +245,7 @@ impl ChatRegistry {
|
||||
_ = status.compare_exchange(true, false, Ordering::Release, Ordering::Relaxed);
|
||||
|
||||
// Send loading signal
|
||||
if let Err(e) = tx.send_async(Signal::Loading(true)).await {
|
||||
if let Err(e) = tx.send_async(NostrEvent::Unwrapping(true)).await {
|
||||
log::error!("Failed to send signal: {}", e);
|
||||
}
|
||||
} else {
|
||||
@@ -246,7 +253,7 @@ impl ChatRegistry {
|
||||
// Wait until after 2 loops to prevent exiting early while events are still being processed
|
||||
if is_start_processing && total_loops >= 2 {
|
||||
// Send loading signal
|
||||
if let Err(e) = tx.send_async(Signal::Loading(false)).await {
|
||||
if let Err(e) = tx.send_async(NostrEvent::Unwrapping(false)).await {
|
||||
log::error!("Failed to send signal: {}", e);
|
||||
}
|
||||
// Reset the counter
|
||||
@@ -270,12 +277,12 @@ impl ChatRegistry {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Get a room by its ID.
|
||||
pub fn room(&self, id: &u64, cx: &App) -> Option<Entity<Room>> {
|
||||
/// Get a weak reference to a room by its ID.
|
||||
pub fn room(&self, id: &u64, cx: &App) -> Option<WeakEntity<Room>> {
|
||||
self.rooms
|
||||
.iter()
|
||||
.find(|model| model.read(cx).id == *id)
|
||||
.cloned()
|
||||
.find(|this| &this.read(cx).id == id)
|
||||
.map(|this| this.downgrade())
|
||||
}
|
||||
|
||||
/// Get all ongoing rooms.
|
||||
@@ -297,11 +304,30 @@ impl ChatRegistry {
|
||||
}
|
||||
|
||||
/// Add a new room to the start of list.
|
||||
pub fn add_room(&mut self, room: Entity<Room>, cx: &mut Context<Self>) {
|
||||
self.rooms.insert(0, room);
|
||||
pub fn add_room<I>(&mut self, room: I, cx: &mut Context<Self>)
|
||||
where
|
||||
I: Into<Room>,
|
||||
{
|
||||
self.rooms.insert(0, cx.new(|_| room.into()));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Emit an open room event.
|
||||
/// If the room is new, add it to the registry.
|
||||
pub fn emit_room(&mut self, room: WeakEntity<Room>, cx: &mut Context<Self>) {
|
||||
if let Some(room) = room.upgrade() {
|
||||
let id = room.read(cx).id;
|
||||
|
||||
// If the room is new, add it to the registry.
|
||||
if !self.rooms.iter().any(|r| r.read(cx).id == id) {
|
||||
self.rooms.insert(0, room);
|
||||
}
|
||||
|
||||
// Emit the open room event.
|
||||
cx.emit(ChatEvent::OpenRoom(id));
|
||||
}
|
||||
}
|
||||
|
||||
/// Close a room.
|
||||
pub fn close_room(&mut self, id: u64, cx: &mut Context<Self>) {
|
||||
if self.rooms.iter().any(|r| r.read(cx).id == id) {
|
||||
@@ -345,17 +371,6 @@ impl ChatRegistry {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Push a new room to the chat registry
|
||||
pub fn push_room(&mut self, room: Entity<Room>, cx: &mut Context<Self>) {
|
||||
let id = room.read(cx).id;
|
||||
|
||||
if !self.rooms.iter().any(|r| r.read(cx).id == id) {
|
||||
self.add_room(room, cx);
|
||||
}
|
||||
|
||||
cx.emit(ChatEvent::OpenRoom(id));
|
||||
}
|
||||
|
||||
/// Extend the registry with new rooms.
|
||||
fn extend_rooms(&mut self, rooms: HashSet<Room>, cx: &mut Context<Self>) {
|
||||
let mut room_map: HashMap<u64, usize> = self
|
||||
@@ -506,39 +521,45 @@ impl ChatRegistry {
|
||||
/// If the room doesn't exist, it will be created.
|
||||
/// Updates room ordering based on the most recent messages.
|
||||
pub fn new_message(&mut self, message: NewMessage, cx: &mut Context<Self>) {
|
||||
let id = message.rumor.uniq_id();
|
||||
let author = message.rumor.pubkey;
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
// Get the unique id
|
||||
let id = message.rumor.uniq_id();
|
||||
// Get the author
|
||||
let author = message.rumor.pubkey;
|
||||
|
||||
if let Some(room) = self.rooms.iter().find(|room| room.read(cx).id == id) {
|
||||
let is_new_event = message.rumor.created_at > room.read(cx).created_at;
|
||||
let created_at = message.rumor.created_at;
|
||||
match self.rooms.iter().find(|room| room.read(cx).id == id) {
|
||||
Some(room) => {
|
||||
let new_message = message.rumor.created_at > room.read(cx).created_at;
|
||||
let created_at = message.rumor.created_at;
|
||||
|
||||
// Update room
|
||||
room.update(cx, |this, cx| {
|
||||
if is_new_event {
|
||||
this.set_created_at(created_at, cx);
|
||||
// Update room
|
||||
room.update(cx, |this, cx| {
|
||||
// Update the last timestamp if the new message is newer
|
||||
if new_message {
|
||||
this.set_created_at(created_at, cx);
|
||||
}
|
||||
|
||||
// Set this room is ongoing if the new message is from current user
|
||||
if author == nostr.read(cx).identity().read(cx).public_key() {
|
||||
this.set_ongoing(cx);
|
||||
}
|
||||
|
||||
// Emit the new message to the room
|
||||
this.emit_message(message, cx);
|
||||
});
|
||||
|
||||
// Resort all rooms in the registry by their created at (after updated)
|
||||
if new_message {
|
||||
self.sort(cx);
|
||||
}
|
||||
|
||||
// Set this room is ongoing if the new message is from current user
|
||||
if author == nostr.read(cx).identity().read(cx).public_key() {
|
||||
this.set_ongoing(cx);
|
||||
}
|
||||
|
||||
// Emit the new message to the room
|
||||
this.emit_message(message, cx);
|
||||
});
|
||||
|
||||
// Resort all rooms in the registry by their created at (after updated)
|
||||
if is_new_event {
|
||||
self.sort(cx);
|
||||
}
|
||||
} else {
|
||||
// Push the new room to the front of the list
|
||||
self.add_room(cx.new(|_| Room::from(&message.rumor)), cx);
|
||||
None => {
|
||||
// Push the new room to the front of the list
|
||||
self.add_room(&message.rumor, cx);
|
||||
|
||||
// Notify the UI about the new room
|
||||
cx.emit(ChatEvent::NewRequest(RoomKind::default()));
|
||||
// Notify the UI about the new room
|
||||
cx.emit(ChatEvent::Ping);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@ use common::{nip96_upload, RenderedProfile, 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,
|
||||
ClipboardItem, Context, Entity, EventEmitter, Flatten, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, ListAlignment, ListOffset, ListState, MouseButton, ObjectFit,
|
||||
ParentElement, PathPromptOptions, Render, RetainAllImageCache, SharedString,
|
||||
StatefulInteractiveElement, Styled, StyledImage, Subscription, Task, Window,
|
||||
StatefulInteractiveElement, Styled, StyledImage, Subscription, Task, WeakEntity, Window,
|
||||
};
|
||||
use gpui_tokio::Tokio;
|
||||
use indexset::{BTreeMap, BTreeSet};
|
||||
@@ -27,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::{
|
||||
@@ -43,39 +42,52 @@ 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>,
|
||||
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...")
|
||||
@@ -87,43 +99,63 @@ impl ChatPanel {
|
||||
let attachments = cx.new(|_| vec![]);
|
||||
let replies_to = cx.new(|_| HashSet::new());
|
||||
|
||||
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
|
||||
@@ -138,32 +170,6 @@ impl ChatPanel {
|
||||
),
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
// Subscribe to room events
|
||||
cx.subscribe_in(&room, window, move |this, _, signal, window, cx| {
|
||||
match signal {
|
||||
RoomEvent::Incoming(message) => {
|
||||
this.insert_message(message, false, cx);
|
||||
}
|
||||
RoomEvent::Reload => {
|
||||
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,
|
||||
@@ -178,17 +184,14 @@ impl ChatPanel {
|
||||
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| {
|
||||
@@ -202,12 +205,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
|
||||
@@ -241,19 +245,14 @@ 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);
|
||||
|
||||
// Create a temporary message for optimistic update
|
||||
let rumor = room.create_message(&content, replies.as_ref(), cx);
|
||||
let rumor_id = rumor.id.unwrap();
|
||||
@@ -272,12 +271,14 @@ impl ChatPanel {
|
||||
this.update_in(cx, |this, window, 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);
|
||||
})
|
||||
@@ -285,16 +286,15 @@ impl ChatPanel {
|
||||
})
|
||||
.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
|
||||
@@ -303,50 +303,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
|
||||
@@ -379,13 +350,6 @@ impl ChatPanel {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -417,11 +381,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 scroll_to(&self, id: EventId) {
|
||||
if let Some(ix) = self.messages.iter().position(|m| {
|
||||
if let Message::User(msg) = m {
|
||||
@@ -547,6 +506,11 @@ 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 render_announcement(&self, ix: usize, cx: &Context<Self>) -> AnyElement {
|
||||
v_flex()
|
||||
.id(ix)
|
||||
@@ -1158,61 +1122,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);
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for ChatPanel {
|
||||
@@ -1221,24 +1130,19 @@ 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 proxy = AppSettings::get_proxy_user_avatars(cx);
|
||||
let label = this.display_name(cx);
|
||||
let url = this.display_image(proxy, 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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -136,15 +136,20 @@ impl ChatSpace {
|
||||
ChatEvent::OpenRoom(id) => {
|
||||
if let Some(room) = chat.read(cx).room(id, cx) {
|
||||
this.dock.update(cx, |this, cx| {
|
||||
let panel = chat_ui::init(room, window, cx);
|
||||
this.add_panel(Arc::new(panel), DockPlacement::Center, window, cx);
|
||||
this.add_panel(
|
||||
Arc::new(chat_ui::init(room, window, cx)),
|
||||
DockPlacement::Center,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
ChatEvent::CloseRoom(..) => {
|
||||
this.dock.update(cx, |this, cx| {
|
||||
// Force focus to the tab panel
|
||||
this.focus_tab_panel(window, cx);
|
||||
|
||||
// Dispatch the close panel action
|
||||
cx.defer_in(window, |_, window, cx| {
|
||||
window.dispatch_action(Box::new(ClosePanel), cx);
|
||||
window.close_all_modals(cx);
|
||||
|
||||
@@ -210,12 +210,10 @@ impl Login {
|
||||
|
||||
fn connect(&mut self, signer: NostrConnect, cx: &mut Context<Self>) {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
client.set_signer(signer).await;
|
||||
})
|
||||
.detach();
|
||||
nostr.update(cx, |this, cx| {
|
||||
this.set_signer(signer, cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn login_with_password(&mut self, content: &str, pwd: &str, cx: &mut Context<Self>) {
|
||||
@@ -260,10 +258,6 @@ impl Login {
|
||||
|
||||
pub fn login_with_keys(&mut self, keys: Keys, cx: &mut Context<Self>) {
|
||||
let keystore = KeyStore::global(cx).read(cx).backend();
|
||||
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
|
||||
let username = keys.public_key().to_hex();
|
||||
let secret = keys.secret_key().to_secret_hex().into_bytes();
|
||||
|
||||
@@ -281,11 +275,14 @@ impl Login {
|
||||
.ok();
|
||||
}
|
||||
|
||||
// Update the signer
|
||||
cx.background_spawn(async move {
|
||||
client.set_signer(keys).await;
|
||||
this.update(cx, |_this, cx| {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
|
||||
nostr.update(cx, |this, cx| {
|
||||
this.set_signer(keys, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ use chat::{ChatEvent, ChatRegistry, Room, RoomKind};
|
||||
use common::{DebouncedDelay, RenderedTimestamp, TextUtils, BOOTSTRAP_RELAYS, SEARCH_RELAYS};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
deferred, div, relative, uniform_list, AnyElement, App, AppContext, Context, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render,
|
||||
deferred, div, relative, uniform_list, App, AppContext, Context, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render,
|
||||
RetainAllImageCache, SharedString, Styled, Subscription, Task, Window,
|
||||
};
|
||||
use gpui_tokio::Tokio;
|
||||
@@ -34,6 +34,7 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Sidebar> {
|
||||
cx.new(|cx| Sidebar::new(window, cx))
|
||||
}
|
||||
|
||||
/// Sidebar.
|
||||
pub struct Sidebar {
|
||||
name: SharedString,
|
||||
|
||||
@@ -49,73 +50,75 @@ pub struct Sidebar {
|
||||
/// Async search operation
|
||||
search_task: Option<Task<()>>,
|
||||
|
||||
/// Search input state
|
||||
find_input: Entity<InputState>,
|
||||
|
||||
/// Debounced delay for search input
|
||||
find_debouncer: DebouncedDelay<Self>,
|
||||
|
||||
/// Whether searching is in progress
|
||||
finding: bool,
|
||||
|
||||
indicator: Entity<Option<RoomKind>>,
|
||||
/// New request flag
|
||||
new_request: bool,
|
||||
|
||||
/// Current chat room filter
|
||||
active_filter: Entity<RoomKind>,
|
||||
|
||||
/// Event subscriptions
|
||||
_subscriptions: SmallVec<[Subscription; 3]>,
|
||||
_subscriptions: SmallVec<[Subscription; 2]>,
|
||||
}
|
||||
|
||||
impl Sidebar {
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let active_filter = cx.new(|_| RoomKind::Ongoing);
|
||||
let indicator = cx.new(|_| None);
|
||||
let search_results = cx.new(|_| None);
|
||||
|
||||
let find_input =
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder("Find or start a conversation"));
|
||||
// Define the find input state
|
||||
let find_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder("Find or start a conversation")
|
||||
.clean_on_escape()
|
||||
});
|
||||
|
||||
// Get the chat registry
|
||||
let chat = ChatRegistry::global(cx);
|
||||
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
subscriptions.push(
|
||||
// Clear the image cache when sidebar is closed
|
||||
cx.on_release_in(window, move |this, window, cx| {
|
||||
this.image_cache.update(cx, |this, cx| {
|
||||
this.clear(window, cx);
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
// Subscribe for registry new events
|
||||
cx.subscribe_in(&chat, window, move |this, _, event, _window, cx| {
|
||||
if let ChatEvent::NewRequest(kind) = event {
|
||||
this.indicator.update(cx, |this, cx| {
|
||||
*this = Some(kind.to_owned());
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
cx.subscribe_in(&chat, window, move |this, _s, event, _window, cx| {
|
||||
if event == &ChatEvent::Ping {
|
||||
this.new_request = true;
|
||||
cx.notify();
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
// Subscribe for find input events
|
||||
cx.subscribe_in(&find_input, window, |this, state, event, window, cx| {
|
||||
let delay = Duration::from_millis(FIND_DELAY);
|
||||
|
||||
match event {
|
||||
InputEvent::PressEnter { .. } => {
|
||||
this.search(window, cx);
|
||||
}
|
||||
InputEvent::Change => {
|
||||
// Clear the result when input is empty
|
||||
if state.read(cx).value().is_empty() {
|
||||
// Clear the result when input is empty
|
||||
this.clear(window, cx);
|
||||
} else {
|
||||
// Run debounced search
|
||||
this.find_debouncer.fire_new(
|
||||
Duration::from_millis(FIND_DELAY),
|
||||
window,
|
||||
cx,
|
||||
|this, window, cx| this.debounced_search(window, cx),
|
||||
);
|
||||
this.find_debouncer
|
||||
.fire_new(delay, window, cx, |this, window, cx| {
|
||||
this.debounced_search(window, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -125,7 +128,7 @@ impl Sidebar {
|
||||
image_cache: RetainAllImageCache::new(cx),
|
||||
find_debouncer: DebouncedDelay::new(),
|
||||
finding: false,
|
||||
indicator,
|
||||
new_request: false,
|
||||
active_filter,
|
||||
find_input,
|
||||
search_results,
|
||||
@@ -384,13 +387,13 @@ impl Sidebar {
|
||||
}
|
||||
|
||||
fn set_finding(&mut self, status: bool, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.finding = status;
|
||||
// Disable the input to prevent duplicate requests
|
||||
self.find_input.update(cx, |this, cx| {
|
||||
this.set_disabled(status, cx);
|
||||
this.set_loading(status, cx);
|
||||
});
|
||||
|
||||
// Set the finding status
|
||||
self.finding = status;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -412,47 +415,46 @@ impl Sidebar {
|
||||
}
|
||||
|
||||
fn set_filter(&mut self, kind: RoomKind, cx: &mut Context<Self>) {
|
||||
self.indicator.update(cx, |this, cx| {
|
||||
*this = None;
|
||||
cx.notify();
|
||||
});
|
||||
self.active_filter.update(cx, |this, cx| {
|
||||
*this = kind;
|
||||
cx.notify();
|
||||
});
|
||||
self.new_request = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn open_room(&mut self, id: u64, window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn open(&mut self, id: u64, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let room = if let Some(room) = chat.read(cx).room(&id, cx) {
|
||||
room
|
||||
} else {
|
||||
let Some(result) = self.search_results.read(cx).as_ref() else {
|
||||
window.push_notification("Failed to open room. Please try again later.", cx);
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(room) = result.iter().find(|this| this.read(cx).id == id).cloned() else {
|
||||
window.push_notification("Failed to open room. Please try again later.", cx);
|
||||
return;
|
||||
};
|
||||
|
||||
// Clear all search results
|
||||
self.clear(window, cx);
|
||||
|
||||
room
|
||||
};
|
||||
|
||||
chat.update(cx, |this, cx| {
|
||||
this.push_room(room, cx);
|
||||
});
|
||||
match chat.read(cx).room(&id, cx) {
|
||||
Some(room) => {
|
||||
chat.update(cx, |this, cx| {
|
||||
this.emit_room(room, cx);
|
||||
});
|
||||
}
|
||||
None => {
|
||||
if let Some(room) = self
|
||||
.search_results
|
||||
.read(cx)
|
||||
.as_ref()
|
||||
.and_then(|results| results.iter().find(|this| this.read(cx).id == id))
|
||||
.map(|this| this.downgrade())
|
||||
{
|
||||
chat.update(cx, |this, cx| {
|
||||
this.emit_room(room, cx);
|
||||
});
|
||||
// Clear all search results
|
||||
self.clear(window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_reload(&mut self, _ev: &Reload, window: &mut Window, cx: &mut Context<Self>) {
|
||||
ChatRegistry::global(cx).update(cx, |this, cx| {
|
||||
this.get_rooms(cx);
|
||||
});
|
||||
window.push_notification("Refreshed", cx);
|
||||
window.push_notification("Reload", cx);
|
||||
}
|
||||
|
||||
fn on_manage(&mut self, _ev: &RelayStatus, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -553,7 +555,7 @@ impl Sidebar {
|
||||
|
||||
let handler = cx.listener({
|
||||
move |this, _, window, cx| {
|
||||
this.open_room(room_id, window, cx);
|
||||
this.open(room_id, window, cx);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -577,10 +579,6 @@ impl Panel for Sidebar {
|
||||
fn panel_id(&self) -> SharedString {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn title(&self, _cx: &App) -> AnyElement {
|
||||
self.name.clone().into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for Sidebar {}
|
||||
@@ -672,13 +670,6 @@ impl Render for Sidebar {
|
||||
Button::new("all")
|
||||
.label("All")
|
||||
.tooltip("All ongoing conversations")
|
||||
.when_some(self.indicator.read(cx).as_ref(), |this, kind| {
|
||||
this.when(kind == &RoomKind::Ongoing, |this| {
|
||||
this.child(
|
||||
div().size_1().rounded_full().bg(cx.theme().cursor),
|
||||
)
|
||||
})
|
||||
})
|
||||
.small()
|
||||
.cta()
|
||||
.bold()
|
||||
@@ -693,12 +684,10 @@ impl Render for Sidebar {
|
||||
Button::new("requests")
|
||||
.label("Requests")
|
||||
.tooltip("Incoming new conversations")
|
||||
.when_some(self.indicator.read(cx).as_ref(), |this, kind| {
|
||||
this.when(kind != &RoomKind::Ongoing, |this| {
|
||||
this.child(
|
||||
div().size_1().rounded_full().bg(cx.theme().cursor),
|
||||
)
|
||||
})
|
||||
.when(self.new_request, |this| {
|
||||
this.child(
|
||||
div().size_1().rounded_full().bg(cx.theme().cursor),
|
||||
)
|
||||
})
|
||||
.small()
|
||||
.cta()
|
||||
|
||||
@@ -324,7 +324,8 @@ impl Compose {
|
||||
};
|
||||
|
||||
chat.update(cx, |this, cx| {
|
||||
this.push_room(cx.new(|_| Room::new(subject, public_key, receivers)), cx);
|
||||
let room = cx.new(|_| Room::new(subject, public_key, receivers));
|
||||
this.emit_room(room.downgrade(), cx);
|
||||
});
|
||||
|
||||
window.close_modal(cx);
|
||||
|
||||
Reference in New Issue
Block a user