wip
This commit is contained in:
@@ -10,7 +10,7 @@ use common::{EventUtils, BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT};
|
|||||||
use flume::Sender;
|
use flume::Sender;
|
||||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||||
use fuzzy_matcher::FuzzyMatcher;
|
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 nostr_sdk::prelude::*;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
@@ -30,6 +30,28 @@ struct GlobalChatRegistry(Entity<ChatRegistry>);
|
|||||||
|
|
||||||
impl Global for GlobalChatRegistry {}
|
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
|
/// Chat Registry
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ChatRegistry {
|
pub struct ChatRegistry {
|
||||||
@@ -40,26 +62,7 @@ pub struct ChatRegistry {
|
|||||||
loading: bool,
|
loading: bool,
|
||||||
|
|
||||||
/// Tasks for asynchronous operations
|
/// Tasks for asynchronous operations
|
||||||
_tasks: SmallVec<[Task<()>; 4]>,
|
_tasks: SmallVec<[Task<()>; 3]>,
|
||||||
}
|
|
||||||
|
|
||||||
/// 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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<ChatEvent> for ChatRegistry {}
|
impl EventEmitter<ChatEvent> for ChatRegistry {}
|
||||||
@@ -84,7 +87,7 @@ impl ChatRegistry {
|
|||||||
let status = Arc::new(AtomicBool::new(true));
|
let status = Arc::new(AtomicBool::new(true));
|
||||||
|
|
||||||
// Channel for communication between nostr and gpui
|
// 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![];
|
let mut tasks = smallvec![];
|
||||||
|
|
||||||
@@ -102,7 +105,7 @@ impl ChatRegistry {
|
|||||||
tasks.push(
|
tasks.push(
|
||||||
// Handle unwrapping progress
|
// Handle unwrapping progress
|
||||||
cx.background_spawn(
|
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| {
|
cx.spawn(async move |this, cx| {
|
||||||
while let Ok(message) = rx.recv_async().await {
|
while let Ok(message) = rx.recv_async().await {
|
||||||
match message {
|
match message {
|
||||||
Signal::Message(message) => {
|
NostrEvent::Message(message) => {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.new_message(message, cx);
|
this.new_message(message, cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
Signal::Eose => {
|
NostrEvent::Eose => {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.get_rooms(cx);
|
this.get_rooms(cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
Signal::Loading(status) => {
|
NostrEvent::Unwrapping(status) => {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.set_loading(status, cx);
|
this.set_loading(status, cx);
|
||||||
this.get_rooms(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 initialized_at = Timestamp::now();
|
||||||
let subscription_id = SubscriptionId::new(GIFTWRAP_SUBSCRIPTION);
|
let subscription_id = SubscriptionId::new(GIFTWRAP_SUBSCRIPTION);
|
||||||
|
|
||||||
@@ -193,7 +200,7 @@ impl ChatRegistry {
|
|||||||
|
|
||||||
if !sent_by_coop {
|
if !sent_by_coop {
|
||||||
let new_message = NewMessage::new(event.id, rumor);
|
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 {
|
if let Err(e) = tx.send_async(signal).await {
|
||||||
log::error!("Failed to send signal: {}", e);
|
log::error!("Failed to send signal: {}", e);
|
||||||
@@ -212,7 +219,7 @@ impl ChatRegistry {
|
|||||||
}
|
}
|
||||||
RelayMessage::EndOfStoredEvents(id) => {
|
RelayMessage::EndOfStoredEvents(id) => {
|
||||||
if id.as_ref() == &subscription_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);
|
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 loop_duration = Duration::from_secs(20);
|
||||||
let mut is_start_processing = false;
|
let mut is_start_processing = false;
|
||||||
let mut total_loops = 0;
|
let mut total_loops = 0;
|
||||||
@@ -238,7 +245,7 @@ impl ChatRegistry {
|
|||||||
_ = status.compare_exchange(true, false, Ordering::Release, Ordering::Relaxed);
|
_ = status.compare_exchange(true, false, Ordering::Release, Ordering::Relaxed);
|
||||||
|
|
||||||
// Send loading signal
|
// 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);
|
log::error!("Failed to send signal: {}", e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -246,7 +253,7 @@ impl ChatRegistry {
|
|||||||
// Wait until after 2 loops to prevent exiting early while events are still being processed
|
// Wait until after 2 loops to prevent exiting early while events are still being processed
|
||||||
if is_start_processing && total_loops >= 2 {
|
if is_start_processing && total_loops >= 2 {
|
||||||
// Send loading signal
|
// 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);
|
log::error!("Failed to send signal: {}", e);
|
||||||
}
|
}
|
||||||
// Reset the counter
|
// Reset the counter
|
||||||
@@ -270,12 +277,12 @@ impl ChatRegistry {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a room by its ID.
|
/// Get a weak reference to a room by its ID.
|
||||||
pub fn room(&self, id: &u64, cx: &App) -> Option<Entity<Room>> {
|
pub fn room(&self, id: &u64, cx: &App) -> Option<WeakEntity<Room>> {
|
||||||
self.rooms
|
self.rooms
|
||||||
.iter()
|
.iter()
|
||||||
.find(|model| model.read(cx).id == *id)
|
.find(|this| &this.read(cx).id == id)
|
||||||
.cloned()
|
.map(|this| this.downgrade())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all ongoing rooms.
|
/// Get all ongoing rooms.
|
||||||
@@ -297,11 +304,30 @@ impl ChatRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Add a new room to the start of list.
|
/// Add a new room to the start of list.
|
||||||
pub fn add_room(&mut self, room: Entity<Room>, cx: &mut Context<Self>) {
|
pub fn add_room<I>(&mut self, room: I, cx: &mut Context<Self>)
|
||||||
self.rooms.insert(0, room);
|
where
|
||||||
|
I: Into<Room>,
|
||||||
|
{
|
||||||
|
self.rooms.insert(0, cx.new(|_| room.into()));
|
||||||
cx.notify();
|
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.
|
/// Close a room.
|
||||||
pub fn close_room(&mut self, id: u64, cx: &mut Context<Self>) {
|
pub fn close_room(&mut self, id: u64, cx: &mut Context<Self>) {
|
||||||
if self.rooms.iter().any(|r| r.read(cx).id == id) {
|
if self.rooms.iter().any(|r| r.read(cx).id == id) {
|
||||||
@@ -345,17 +371,6 @@ impl ChatRegistry {
|
|||||||
cx.notify();
|
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.
|
/// Extend the registry with new rooms.
|
||||||
fn extend_rooms(&mut self, rooms: HashSet<Room>, cx: &mut Context<Self>) {
|
fn extend_rooms(&mut self, rooms: HashSet<Room>, cx: &mut Context<Self>) {
|
||||||
let mut room_map: HashMap<u64, usize> = 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.
|
/// If the room doesn't exist, it will be created.
|
||||||
/// Updates room ordering based on the most recent messages.
|
/// Updates room ordering based on the most recent messages.
|
||||||
pub fn new_message(&mut self, message: NewMessage, cx: &mut Context<Self>) {
|
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);
|
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) {
|
match self.rooms.iter().find(|room| room.read(cx).id == id) {
|
||||||
let is_new_event = message.rumor.created_at > room.read(cx).created_at;
|
Some(room) => {
|
||||||
let created_at = message.rumor.created_at;
|
let new_message = message.rumor.created_at > room.read(cx).created_at;
|
||||||
|
let created_at = message.rumor.created_at;
|
||||||
|
|
||||||
// Update room
|
// Update room
|
||||||
room.update(cx, |this, cx| {
|
room.update(cx, |this, cx| {
|
||||||
if is_new_event {
|
// Update the last timestamp if the new message is newer
|
||||||
this.set_created_at(created_at, cx);
|
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 {
|
None => {
|
||||||
// Push the new room to the front of the list
|
// Push the new room to the front of the list
|
||||||
self.add_room(cx.new(|_| Room::from(&message.rumor)), cx);
|
self.add_room(&message.rumor, cx);
|
||||||
|
|
||||||
// Notify the UI about the new room
|
// Notify the UI about the new room
|
||||||
cx.emit(ChatEvent::NewRequest(RoomKind::default()));
|
cx.emit(ChatEvent::Ping);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ use common::{nip96_upload, RenderedProfile, RenderedTimestamp};
|
|||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, img, list, px, red, relative, rems, svg, white, AnyElement, App, AppContext,
|
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,
|
InteractiveElement, IntoElement, ListAlignment, ListOffset, ListState, MouseButton, ObjectFit,
|
||||||
ParentElement, PathPromptOptions, Render, RetainAllImageCache, SharedString,
|
ParentElement, PathPromptOptions, Render, RetainAllImageCache, SharedString,
|
||||||
StatefulInteractiveElement, Styled, StyledImage, Subscription, Task, Window,
|
StatefulInteractiveElement, Styled, StyledImage, Subscription, Task, WeakEntity, Window,
|
||||||
};
|
};
|
||||||
use gpui_tokio::Tokio;
|
use gpui_tokio::Tokio;
|
||||||
use indexset::{BTreeMap, BTreeSet};
|
use indexset::{BTreeMap, BTreeSet};
|
||||||
@@ -27,7 +27,6 @@ use ui::button::{Button, ButtonVariants};
|
|||||||
use ui::context_menu::ContextMenuExt;
|
use ui::context_menu::ContextMenuExt;
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||||
use ui::input::{InputEvent, InputState, TextInput};
|
use ui::input::{InputEvent, InputState, TextInput};
|
||||||
use ui::modal::ModalButtonProps;
|
|
||||||
use ui::notification::Notification;
|
use ui::notification::Notification;
|
||||||
use ui::popup_menu::PopupMenuExt;
|
use ui::popup_menu::PopupMenuExt;
|
||||||
use ui::{
|
use ui::{
|
||||||
@@ -43,39 +42,52 @@ mod emoji;
|
|||||||
mod subject;
|
mod subject;
|
||||||
mod text;
|
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))
|
cx.new(|cx| ChatPanel::new(room, window, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Chat Panel
|
||||||
pub struct ChatPanel {
|
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,
|
id: SharedString,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
image_cache: Entity<RetainAllImageCache>,
|
image_cache: Entity<RetainAllImageCache>,
|
||||||
|
|
||||||
_subscriptions: SmallVec<[Subscription; 3]>,
|
/// Chat Room
|
||||||
_tasks: SmallVec<[Task<()>; 2]>,
|
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 {
|
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| {
|
let input = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.placeholder("Message...")
|
.placeholder("Message...")
|
||||||
@@ -87,43 +99,63 @@ impl ChatPanel {
|
|||||||
let attachments = cx.new(|_| vec![]);
|
let attachments = cx.new(|_| vec![]);
|
||||||
let replies_to = cx.new(|_| HashSet::new());
|
let replies_to = cx.new(|_| HashSet::new());
|
||||||
|
|
||||||
let id = room.read(cx).id.to_string().into();
|
|
||||||
let messages = BTreeSet::from([Message::system()]);
|
let messages = BTreeSet::from([Message::system()]);
|
||||||
let list_state = ListState::new(messages.len(), ListAlignment::Bottom, px(1024.));
|
let list_state = ListState::new(messages.len(), ListAlignment::Bottom, px(1024.));
|
||||||
|
|
||||||
let connect = room.read(cx).connect(cx);
|
let id: SharedString = room
|
||||||
let get_messages = room.read(cx).get_messages(cx);
|
.read_with(cx, |this, _cx| this.id.to_string().into())
|
||||||
|
.unwrap_or("Unknown".into());
|
||||||
|
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
let mut tasks = smallvec![];
|
let mut tasks = smallvec![];
|
||||||
|
|
||||||
tasks.push(
|
if let Ok(connect) = room.read_with(cx, |this, cx| this.connect(cx)) {
|
||||||
// Get messaging relays and encryption keys announcement for each member
|
tasks.push(
|
||||||
cx.background_spawn(async move {
|
// Get messaging relays and encryption keys announcement for each member
|
||||||
if let Err(e) = connect.await {
|
cx.background_spawn(async move {
|
||||||
log::error!("Failed to initialize room: {}", e);
|
if let Err(e) = connect.await {
|
||||||
}
|
log::error!("Failed to initialize room: {}", e);
|
||||||
}),
|
}
|
||||||
);
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
tasks.push(
|
if let Ok(get_messages) = room.read_with(cx, |this, cx| this.get_messages(cx)) {
|
||||||
// Load all messages belonging to this room
|
tasks.push(
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
// Load all messages belonging to this room
|
||||||
let result = get_messages.await;
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let result = get_messages.await;
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
match result {
|
match result {
|
||||||
Ok(events) => {
|
Ok(events) => {
|
||||||
this.insert_messages(&events, cx);
|
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) => {
|
RoomEvent::Reload => {
|
||||||
window.push_notification(e.to_string(), cx);
|
this.load_messages(window, cx);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
}),
|
||||||
.ok();
|
);
|
||||||
}),
|
}
|
||||||
);
|
|
||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Subscribe to input events
|
// 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 {
|
Self {
|
||||||
id,
|
id,
|
||||||
messages,
|
messages,
|
||||||
@@ -178,17 +184,14 @@ impl ChatPanel {
|
|||||||
image_cache: RetainAllImageCache::new(cx),
|
image_cache: RetainAllImageCache::new(cx),
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
_tasks: tasks,
|
tasks,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load all messages belonging to this room
|
/// Load all messages belonging to this room
|
||||||
fn load_messages(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn load_messages(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let get_messages = self.room.read(cx).get_messages(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| {
|
||||||
self._tasks.push(
|
|
||||||
// Run the task in the background
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
let result = get_messages.await;
|
let result = get_messages.await;
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
@@ -202,12 +205,13 @@ impl ChatPanel {
|
|||||||
};
|
};
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}),
|
}));
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get user input content and merged all attachments
|
/// Get user input content and merged all attachments
|
||||||
fn input_content(&self, cx: &Context<Self>) -> String {
|
fn input_content(&self, cx: &Context<Self>) -> String {
|
||||||
|
// Get input's value
|
||||||
let mut content = self.input.read(cx).value().trim().to_string();
|
let mut content = self.input.read(cx).value().trim().to_string();
|
||||||
|
|
||||||
// Get all attaches and merge its with message
|
// Get all attaches and merge its with message
|
||||||
@@ -241,19 +245,14 @@ impl ChatPanel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporary disable the message input
|
// Get the current room entity
|
||||||
self.input.update(cx, |this, cx| {
|
let Some(room) = self.room.upgrade().map(|this| this.read(cx)) else {
|
||||||
this.set_loading(false, cx);
|
return;
|
||||||
this.set_disabled(false, cx);
|
};
|
||||||
this.set_value("", window, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get replies_to if it's present
|
// Get replies_to if it's present
|
||||||
let replies: Vec<EventId> = self.replies_to.read(cx).iter().copied().collect();
|
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
|
// Create a temporary message for optimistic update
|
||||||
let rumor = room.create_message(&content, replies.as_ref(), cx);
|
let rumor = room.create_message(&content, replies.as_ref(), cx);
|
||||||
let rumor_id = rumor.id.unwrap();
|
let rumor_id = rumor.id.unwrap();
|
||||||
@@ -272,12 +271,14 @@ impl ChatPanel {
|
|||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
this.remove_all_replies(cx);
|
this.remove_all_replies(cx);
|
||||||
this.remove_all_attachments(cx);
|
this.remove_all_attachments(cx);
|
||||||
|
|
||||||
// Reset the input to its default state
|
// Reset the input to its default state
|
||||||
this.input.update(cx, |this, cx| {
|
this.input.update(cx, |this, cx| {
|
||||||
this.set_loading(false, cx);
|
this.set_loading(false, cx);
|
||||||
this.set_disabled(false, cx);
|
this.set_disabled(false, cx);
|
||||||
this.set_value("", window, cx);
|
this.set_value("", window, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update the message list
|
// Update the message list
|
||||||
this.insert_message(&rumor, true, cx);
|
this.insert_message(&rumor, true, cx);
|
||||||
})
|
})
|
||||||
@@ -285,16 +286,15 @@ impl ChatPanel {
|
|||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
self._tasks.push(
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
// Continue sending the message in the background
|
let result = send_message.await;
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
let result = send_message.await;
|
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
match result {
|
match result {
|
||||||
Ok(reports) => {
|
Ok(reports) => {
|
||||||
// Update room's status
|
// Update room's status
|
||||||
this.room.update(cx, |this, cx| {
|
this.room
|
||||||
|
.update(cx, |this, cx| {
|
||||||
if this.kind != RoomKind::Ongoing {
|
if this.kind != RoomKind::Ongoing {
|
||||||
// Update the room kind to ongoing,
|
// Update the room kind to ongoing,
|
||||||
// but keep the room kind if send failed
|
// but keep the room kind if send failed
|
||||||
@@ -303,50 +303,21 @@ impl ChatPanel {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
.ok();
|
||||||
|
|
||||||
// Insert the sent reports
|
// Insert the sent reports
|
||||||
this.reports_by_id.insert(rumor_id, reports);
|
this.reports_by_id.insert(rumor_id, reports);
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
window.push_notification(e.to_string(), cx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
Err(e) => {
|
||||||
.ok();
|
window.push_notification(e.to_string(), cx);
|
||||||
}),
|
}
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// 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();
|
|
||||||
})
|
})
|
||||||
.detach();
|
.ok();
|
||||||
}
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert a message into the chat panel
|
/// 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
|
/// Check if a message failed to send by its ID
|
||||||
fn is_sent_failed(&self, id: &EventId) -> bool {
|
fn is_sent_failed(&self, id: &EventId) -> bool {
|
||||||
self.reports_by_id
|
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) {
|
fn scroll_to(&self, id: EventId) {
|
||||||
if let Some(ix) = self.messages.iter().position(|m| {
|
if let Some(ix) = self.messages.iter().position(|m| {
|
||||||
if let Message::User(msg) = 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 {
|
fn render_announcement(&self, ix: usize, cx: &Context<Self>) -> AnyElement {
|
||||||
v_flex()
|
v_flex()
|
||||||
.id(ix)
|
.id(ix)
|
||||||
@@ -1158,61 +1122,6 @@ impl ChatPanel {
|
|||||||
|
|
||||||
items
|
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 {
|
impl Panel for ChatPanel {
|
||||||
@@ -1221,24 +1130,19 @@ impl Panel for ChatPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn title(&self, cx: &App) -> AnyElement {
|
fn title(&self, cx: &App) -> AnyElement {
|
||||||
self.room.read_with(cx, |this, cx| {
|
self.room
|
||||||
let proxy = AppSettings::get_proxy_user_avatars(cx);
|
.read_with(cx, |this, cx| {
|
||||||
let label = this.display_name(cx);
|
let proxy = AppSettings::get_proxy_user_avatars(cx);
|
||||||
let url = this.display_image(proxy, cx);
|
let label = this.display_name(cx);
|
||||||
|
let url = this.display_image(proxy, cx);
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1p5()
|
.gap_1p5()
|
||||||
.child(Avatar::new(url).size(rems(1.25)))
|
.child(Avatar::new(url).size(rems(1.25)))
|
||||||
.child(label)
|
.child(label)
|
||||||
.into_any()
|
.into_any_element()
|
||||||
})
|
})
|
||||||
}
|
.unwrap_or(div().child("Unknown").into_any_element())
|
||||||
|
|
||||||
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]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -136,15 +136,20 @@ impl ChatSpace {
|
|||||||
ChatEvent::OpenRoom(id) => {
|
ChatEvent::OpenRoom(id) => {
|
||||||
if let Some(room) = chat.read(cx).room(id, cx) {
|
if let Some(room) = chat.read(cx).room(id, cx) {
|
||||||
this.dock.update(cx, |this, cx| {
|
this.dock.update(cx, |this, cx| {
|
||||||
let panel = chat_ui::init(room, window, cx);
|
this.add_panel(
|
||||||
this.add_panel(Arc::new(panel), DockPlacement::Center, window, cx);
|
Arc::new(chat_ui::init(room, window, cx)),
|
||||||
|
DockPlacement::Center,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChatEvent::CloseRoom(..) => {
|
ChatEvent::CloseRoom(..) => {
|
||||||
this.dock.update(cx, |this, cx| {
|
this.dock.update(cx, |this, cx| {
|
||||||
|
// Force focus to the tab panel
|
||||||
this.focus_tab_panel(window, cx);
|
this.focus_tab_panel(window, cx);
|
||||||
|
// Dispatch the close panel action
|
||||||
cx.defer_in(window, |_, window, cx| {
|
cx.defer_in(window, |_, window, cx| {
|
||||||
window.dispatch_action(Box::new(ClosePanel), cx);
|
window.dispatch_action(Box::new(ClosePanel), cx);
|
||||||
window.close_all_modals(cx);
|
window.close_all_modals(cx);
|
||||||
|
|||||||
@@ -210,12 +210,10 @@ impl Login {
|
|||||||
|
|
||||||
fn connect(&mut self, signer: NostrConnect, cx: &mut Context<Self>) {
|
fn connect(&mut self, signer: NostrConnect, cx: &mut Context<Self>) {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
nostr.update(cx, |this, cx| {
|
||||||
client.set_signer(signer).await;
|
this.set_signer(signer, cx);
|
||||||
})
|
});
|
||||||
.detach();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn login_with_password(&mut self, content: &str, pwd: &str, cx: &mut Context<Self>) {
|
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>) {
|
pub fn login_with_keys(&mut self, keys: Keys, cx: &mut Context<Self>) {
|
||||||
let keystore = KeyStore::global(cx).read(cx).backend();
|
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 username = keys.public_key().to_hex();
|
||||||
let secret = keys.secret_key().to_secret_hex().into_bytes();
|
let secret = keys.secret_key().to_secret_hex().into_bytes();
|
||||||
|
|
||||||
@@ -281,11 +275,14 @@ impl Login {
|
|||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the signer
|
this.update(cx, |_this, cx| {
|
||||||
cx.background_spawn(async move {
|
let nostr = NostrRegistry::global(cx);
|
||||||
client.set_signer(keys).await;
|
|
||||||
|
nostr.update(cx, |this, cx| {
|
||||||
|
this.set_signer(keys, cx);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.detach();
|
.ok();
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ use chat::{ChatEvent, ChatRegistry, Room, RoomKind};
|
|||||||
use common::{DebouncedDelay, RenderedTimestamp, TextUtils, BOOTSTRAP_RELAYS, SEARCH_RELAYS};
|
use common::{DebouncedDelay, RenderedTimestamp, TextUtils, BOOTSTRAP_RELAYS, SEARCH_RELAYS};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
deferred, div, relative, uniform_list, AnyElement, App, AppContext, Context, Entity,
|
deferred, div, relative, uniform_list, App, AppContext, Context, Entity, EventEmitter,
|
||||||
EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render,
|
FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render,
|
||||||
RetainAllImageCache, SharedString, Styled, Subscription, Task, Window,
|
RetainAllImageCache, SharedString, Styled, Subscription, Task, Window,
|
||||||
};
|
};
|
||||||
use gpui_tokio::Tokio;
|
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))
|
cx.new(|cx| Sidebar::new(window, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sidebar.
|
||||||
pub struct Sidebar {
|
pub struct Sidebar {
|
||||||
name: SharedString,
|
name: SharedString,
|
||||||
|
|
||||||
@@ -49,73 +50,75 @@ pub struct Sidebar {
|
|||||||
/// Async search operation
|
/// Async search operation
|
||||||
search_task: Option<Task<()>>,
|
search_task: Option<Task<()>>,
|
||||||
|
|
||||||
|
/// Search input state
|
||||||
find_input: Entity<InputState>,
|
find_input: Entity<InputState>,
|
||||||
|
|
||||||
|
/// Debounced delay for search input
|
||||||
find_debouncer: DebouncedDelay<Self>,
|
find_debouncer: DebouncedDelay<Self>,
|
||||||
|
|
||||||
|
/// Whether searching is in progress
|
||||||
finding: bool,
|
finding: bool,
|
||||||
|
|
||||||
indicator: Entity<Option<RoomKind>>,
|
/// New request flag
|
||||||
|
new_request: bool,
|
||||||
|
|
||||||
|
/// Current chat room filter
|
||||||
active_filter: Entity<RoomKind>,
|
active_filter: Entity<RoomKind>,
|
||||||
|
|
||||||
/// Event subscriptions
|
/// Event subscriptions
|
||||||
_subscriptions: SmallVec<[Subscription; 3]>,
|
_subscriptions: SmallVec<[Subscription; 2]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sidebar {
|
impl Sidebar {
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let active_filter = cx.new(|_| RoomKind::Ongoing);
|
let active_filter = cx.new(|_| RoomKind::Ongoing);
|
||||||
let indicator = cx.new(|_| None);
|
|
||||||
let search_results = cx.new(|_| None);
|
let search_results = cx.new(|_| None);
|
||||||
|
|
||||||
let find_input =
|
// Define the find input state
|
||||||
cx.new(|cx| InputState::new(window, cx).placeholder("Find or start a conversation"));
|
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 chat = ChatRegistry::global(cx);
|
||||||
|
|
||||||
let mut subscriptions = smallvec![];
|
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(
|
subscriptions.push(
|
||||||
// Subscribe for registry new events
|
// Subscribe for registry new events
|
||||||
cx.subscribe_in(&chat, window, move |this, _, event, _window, cx| {
|
cx.subscribe_in(&chat, window, move |this, _s, event, _window, cx| {
|
||||||
if let ChatEvent::NewRequest(kind) = event {
|
if event == &ChatEvent::Ping {
|
||||||
this.indicator.update(cx, |this, cx| {
|
this.new_request = true;
|
||||||
*this = Some(kind.to_owned());
|
cx.notify();
|
||||||
cx.notify();
|
};
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Subscribe for find input events
|
// Subscribe for find input events
|
||||||
cx.subscribe_in(&find_input, window, |this, state, event, window, cx| {
|
cx.subscribe_in(&find_input, window, |this, state, event, window, cx| {
|
||||||
|
let delay = Duration::from_millis(FIND_DELAY);
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
InputEvent::PressEnter { .. } => {
|
InputEvent::PressEnter { .. } => {
|
||||||
this.search(window, cx);
|
this.search(window, cx);
|
||||||
}
|
}
|
||||||
InputEvent::Change => {
|
InputEvent::Change => {
|
||||||
// Clear the result when input is empty
|
|
||||||
if state.read(cx).value().is_empty() {
|
if state.read(cx).value().is_empty() {
|
||||||
|
// Clear the result when input is empty
|
||||||
this.clear(window, cx);
|
this.clear(window, cx);
|
||||||
} else {
|
} else {
|
||||||
// Run debounced search
|
// Run debounced search
|
||||||
this.find_debouncer.fire_new(
|
this.find_debouncer
|
||||||
Duration::from_millis(FIND_DELAY),
|
.fire_new(delay, window, cx, |this, window, cx| {
|
||||||
window,
|
this.debounced_search(window, cx)
|
||||||
cx,
|
});
|
||||||
|this, window, cx| this.debounced_search(window, cx),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -125,7 +128,7 @@ impl Sidebar {
|
|||||||
image_cache: RetainAllImageCache::new(cx),
|
image_cache: RetainAllImageCache::new(cx),
|
||||||
find_debouncer: DebouncedDelay::new(),
|
find_debouncer: DebouncedDelay::new(),
|
||||||
finding: false,
|
finding: false,
|
||||||
indicator,
|
new_request: false,
|
||||||
active_filter,
|
active_filter,
|
||||||
find_input,
|
find_input,
|
||||||
search_results,
|
search_results,
|
||||||
@@ -384,13 +387,13 @@ impl Sidebar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn set_finding(&mut self, status: bool, _window: &mut Window, cx: &mut Context<Self>) {
|
fn set_finding(&mut self, status: bool, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.finding = status;
|
|
||||||
// Disable the input to prevent duplicate requests
|
// Disable the input to prevent duplicate requests
|
||||||
self.find_input.update(cx, |this, cx| {
|
self.find_input.update(cx, |this, cx| {
|
||||||
this.set_disabled(status, cx);
|
this.set_disabled(status, cx);
|
||||||
this.set_loading(status, cx);
|
this.set_loading(status, cx);
|
||||||
});
|
});
|
||||||
|
// Set the finding status
|
||||||
|
self.finding = status;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,47 +415,46 @@ impl Sidebar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn set_filter(&mut self, kind: RoomKind, cx: &mut Context<Self>) {
|
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| {
|
self.active_filter.update(cx, |this, cx| {
|
||||||
*this = kind;
|
*this = kind;
|
||||||
cx.notify();
|
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 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 {
|
match chat.read(cx).room(&id, cx) {
|
||||||
window.push_notification("Failed to open room. Please try again later.", cx);
|
Some(room) => {
|
||||||
return;
|
chat.update(cx, |this, cx| {
|
||||||
};
|
this.emit_room(room, cx);
|
||||||
|
});
|
||||||
// Clear all search results
|
}
|
||||||
self.clear(window, cx);
|
None => {
|
||||||
|
if let Some(room) = self
|
||||||
room
|
.search_results
|
||||||
};
|
.read(cx)
|
||||||
|
.as_ref()
|
||||||
chat.update(cx, |this, cx| {
|
.and_then(|results| results.iter().find(|this| this.read(cx).id == id))
|
||||||
this.push_room(room, cx);
|
.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>) {
|
fn on_reload(&mut self, _ev: &Reload, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
ChatRegistry::global(cx).update(cx, |this, cx| {
|
ChatRegistry::global(cx).update(cx, |this, cx| {
|
||||||
this.get_rooms(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>) {
|
fn on_manage(&mut self, _ev: &RelayStatus, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
@@ -553,7 +555,7 @@ impl Sidebar {
|
|||||||
|
|
||||||
let handler = cx.listener({
|
let handler = cx.listener({
|
||||||
move |this, _, window, cx| {
|
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 {
|
fn panel_id(&self) -> SharedString {
|
||||||
self.name.clone()
|
self.name.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn title(&self, _cx: &App) -> AnyElement {
|
|
||||||
self.name.clone().into_any_element()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<PanelEvent> for Sidebar {}
|
impl EventEmitter<PanelEvent> for Sidebar {}
|
||||||
@@ -672,13 +670,6 @@ impl Render for Sidebar {
|
|||||||
Button::new("all")
|
Button::new("all")
|
||||||
.label("All")
|
.label("All")
|
||||||
.tooltip("All ongoing conversations")
|
.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()
|
.small()
|
||||||
.cta()
|
.cta()
|
||||||
.bold()
|
.bold()
|
||||||
@@ -693,12 +684,10 @@ impl Render for Sidebar {
|
|||||||
Button::new("requests")
|
Button::new("requests")
|
||||||
.label("Requests")
|
.label("Requests")
|
||||||
.tooltip("Incoming new conversations")
|
.tooltip("Incoming new conversations")
|
||||||
.when_some(self.indicator.read(cx).as_ref(), |this, kind| {
|
.when(self.new_request, |this| {
|
||||||
this.when(kind != &RoomKind::Ongoing, |this| {
|
this.child(
|
||||||
this.child(
|
div().size_1().rounded_full().bg(cx.theme().cursor),
|
||||||
div().size_1().rounded_full().bg(cx.theme().cursor),
|
)
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.small()
|
.small()
|
||||||
.cta()
|
.cta()
|
||||||
|
|||||||
@@ -324,7 +324,8 @@ impl Compose {
|
|||||||
};
|
};
|
||||||
|
|
||||||
chat.update(cx, |this, cx| {
|
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);
|
window.close_modal(cx);
|
||||||
|
|||||||
Reference in New Issue
Block a user