use crate::{ constants::IMAGE_SERVICE, get_client, states::chat::room::Room, utils::{ago, compare, nip96_upload}, }; use async_utility::task::spawn; use gpui::{ div, img, list, px, white, AnyElement, AppContext, Context, EventEmitter, Flatten, FocusHandle, FocusableView, InteractiveElement, IntoElement, ListAlignment, ListState, Model, ObjectFit, ParentElement, PathPromptOptions, Pixels, Render, SharedString, StatefulInteractiveElement, Styled, StyledImage, View, ViewContext, VisualContext, WeakModel, WeakView, WindowContext, }; use itertools::Itertools; use message::Message; use nostr_sdk::prelude::*; use smol::fs; use tokio::sync::oneshot; use ui::{ button::{Button, ButtonVariants}, dock_area::{ panel::{Panel, PanelEvent}, state::PanelState, }, input::{InputEvent, TextInput}, popup_menu::PopupMenu, prelude::FluentBuilder, theme::{scale::ColorScaleStep, ActiveTheme}, v_flex, Icon, IconName, }; mod message; #[derive(Clone)] pub struct State { count: usize, items: Vec, } pub struct ChatPanel { // Panel closeable: bool, zoomable: bool, focus_handle: FocusHandle, // Chat Room id: SharedString, name: SharedString, room: Model, state: Model, list: ListState, // New Message input: View, // Media attaches: Model>>, is_uploading: bool, } impl ChatPanel { pub fn new(model: Model, cx: &mut WindowContext) -> View { let room = model.read(cx); let id = room.id.to_string().into(); let name = room.title.clone().unwrap_or("Untitled".into()); cx.observe_new_views::(|this, cx| { this.load_messages(cx); }) .detach(); cx.new_view(|cx| { // Form let input = cx.new_view(|cx| { TextInput::new(cx) .appearance(false) .text_size(ui::Size::Small) .placeholder("Message...") .cleanable() }); // List let state = cx.new_model(|_| State { count: 0, items: vec![], }); // Send message when user presses enter cx.subscribe( &input, move |this: &mut ChatPanel, view, input_event, cx| { if let InputEvent::PressEnter = input_event { this.send_message(view.downgrade(), cx); } }, ) .detach(); // Update list on every state changes cx.observe(&state, |this, model, cx| { let items = model.read(cx).items.clone(); this.list = ListState::new( items.len(), ListAlignment::Bottom, Pixels(256.), move |idx, _cx| { let item = items.get(idx).unwrap().clone(); div().child(item).into_any_element() }, ); cx.notify(); }) .detach(); cx.observe(&model, |this, model, cx| { this.load_new_messages(model.downgrade(), cx); }) .detach(); let attaches = cx.new_model(|_| None); Self { closeable: true, zoomable: true, focus_handle: cx.focus_handle(), room: model, list: ListState::new(0, ListAlignment::Bottom, Pixels(256.), move |_, _| { div().into_any_element() }), is_uploading: false, id, name, input, state, attaches, } }) } fn load_messages(&self, cx: &mut ViewContext) { let room = self.room.read(cx); let members = room.members.clone(); let owner = room.owner.clone(); // Get all public keys let all_keys = room.get_all_keys(); // Async let async_state = self.state.clone(); let mut async_cx = cx.to_async(); cx.foreground_executor() .spawn(async move { let events: anyhow::Result = async_cx .background_executor() .spawn({ let client = get_client(); let pubkeys = members.iter().map(|m| m.public_key()).collect::>(); async move { let signer = client.signer().await?; let author = signer.get_public_key().await?; let recv = Filter::new() .kind(Kind::PrivateDirectMessage) .author(author) .pubkeys(pubkeys.clone()); let send = Filter::new() .kind(Kind::PrivateDirectMessage) .authors(pubkeys) .pubkey(author); // Get all DM events in database let query = client.database().query(vec![recv, send]).await?; Ok(query) } }) .await; if let Ok(events) = events { let items: Vec = events .into_iter() .sorted_by_key(|ev| ev.created_at) .filter_map(|ev| { let mut pubkeys: Vec<_> = ev.tags.public_keys().copied().collect(); pubkeys.push(ev.pubkey); if compare(&pubkeys, &all_keys) { let member = if let Some(member) = members.iter().find(|&m| m.public_key() == ev.pubkey) { member.to_owned() } else { owner.clone() }; Some(Message::new( member, ev.content.into(), ago(ev.created_at).into(), )) } else { None } }) .collect(); let total = items.len(); _ = async_cx.update_model(&async_state, |a, b| { a.items = items; a.count = total; b.notify(); }); } }) .detach(); } fn load_new_messages(&self, model: WeakModel, cx: &mut ViewContext) { if let Some(model) = model.upgrade() { let room = model.read(cx); let items: Vec = room .new_messages .iter() .filter_map(|event| { room.member(&event.pubkey).map(|member| { Message::new( member, event.content.clone().into(), ago(event.created_at).into(), ) }) }) .collect(); cx.update_model(&self.state, |model, cx| { model.items.extend(items); model.count = model.items.len(); cx.notify(); }); } } fn send_message(&mut self, view: WeakView, cx: &mut ViewContext) { let room = self.room.read(cx); let owner = room.owner.clone(); let members = room.members.to_vec(); // Get message let mut content = self.input.read(cx).text().to_string(); // Get all attaches and merge with message if let Some(attaches) = self.attaches.read(cx).as_ref() { let merged = attaches .iter() .map(|url| url.to_string()) .collect::>() .join("\n"); content = format!("{}\n{}", content, merged) } // Async let async_state = self.state.clone(); let mut async_cx = cx.to_async(); cx.foreground_executor() .spawn(async move { // Send message to all members async_cx .background_executor() .spawn({ let client = get_client(); let content = content.clone().to_string(); let tags: Vec = members .iter() .filter_map(|m| { if m.public_key() != owner.public_key() { Some(Tag::public_key(m.public_key())) } else { None } }) .collect(); async move { // Send message to all members for member in members.iter() { _ = client .send_private_msg(member.public_key(), &content, tags.clone()) .await } } }) .detach(); _ = async_cx.update_model(&async_state, |model, cx| { let message = Message::new( owner, content.to_string().into(), ago(Timestamp::now()).into(), ); model.items.extend(vec![message]); model.count = model.items.len(); cx.notify(); }); if let Some(input) = view.upgrade() { _ = async_cx.update_view(&input, |input, cx| { input.set_text("", cx); }); } }) .detach(); } fn upload(&mut self, cx: &mut ViewContext) { let attaches = self.attaches.clone(); let paths = cx.prompt_for_paths(PathPromptOptions { files: true, directories: false, multiple: false, }); // Show loading spinner self.is_uploading = true; cx.notify(); // TODO: support multiple upload cx.spawn(move |this, mut async_cx| async move { match Flatten::flatten(paths.await.map_err(|e| e.into())) { Ok(Some(mut paths)) => { let path = paths.pop().unwrap(); if let Ok(file_data) = fs::read(path).await { let (tx, rx) = oneshot::channel::(); spawn(async move { if let Ok(url) = nip96_upload(file_data).await { _ = tx.send(url); } }); if let Ok(url) = rx.await { // Stop loading spinner if let Some(view) = this.upgrade() { _ = async_cx.update_view(&view, |this, cx| { this.is_uploading = false; cx.notify(); }); } // Update attaches model _ = async_cx.update_model(&attaches, |model, cx| { if let Some(model) = model.as_mut() { model.push(url); } else { *model = Some(vec![url]); } cx.notify(); }); } } } Ok(None) => {} Err(_) => {} } }) .detach(); } fn remove(&mut self, url: &Url, cx: &mut ViewContext) { self.attaches.update(cx, |model, cx| { if let Some(urls) = model.as_mut() { let ix = urls.iter().position(|x| x == url).unwrap(); urls.remove(ix); cx.notify(); } }); } } impl Panel for ChatPanel { fn panel_id(&self) -> SharedString { self.id.clone() } fn panel_metadata(&self) -> Option { None } fn title(&self, _cx: &WindowContext) -> AnyElement { self.name.clone().into_any_element() } fn closeable(&self, _cx: &WindowContext) -> bool { self.closeable } fn zoomable(&self, _cx: &WindowContext) -> bool { self.zoomable } fn popup_menu(&self, menu: PopupMenu, _cx: &WindowContext) -> PopupMenu { menu.track_focus(&self.focus_handle) } fn toolbar_buttons(&self, _cx: &WindowContext) -> Vec