wip: refactor
This commit is contained in:
@@ -246,16 +246,29 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Signal::RecvEvent(event) => {
|
||||||
|
let metadata = async_cx
|
||||||
|
.background_executor()
|
||||||
|
.spawn(async move {
|
||||||
|
if let Ok(metadata) =
|
||||||
|
client.database().metadata(event.pubkey).await
|
||||||
|
{
|
||||||
|
metadata
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
_ = async_cx.update_global::<ChatRegistry, _>(|state, _| {
|
||||||
|
state.push(event, metadata);
|
||||||
|
});
|
||||||
|
}
|
||||||
Signal::RecvMetadata(public_key) => {
|
Signal::RecvMetadata(public_key) => {
|
||||||
_ = async_cx.update_global::<MetadataRegistry, _>(|state, _cx| {
|
_ = async_cx.update_global::<MetadataRegistry, _>(|state, _cx| {
|
||||||
state.seen(public_key);
|
state.seen(public_key);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Signal::RecvEvent(event) => {
|
|
||||||
_ = async_cx.update_global::<ChatRegistry, _>(|state, _| {
|
|
||||||
state.push(event);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,23 @@
|
|||||||
use gpui::*;
|
use gpui::*;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Deserialize)]
|
||||||
|
pub struct Room {
|
||||||
|
pub owner: PublicKey,
|
||||||
|
pub members: Vec<PublicKey>,
|
||||||
|
pub last_seen: Timestamp,
|
||||||
|
pub title: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Message {
|
||||||
|
pub event: Event,
|
||||||
|
pub metadata: Option<Metadata>,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ChatRegistry {
|
pub struct ChatRegistry {
|
||||||
pub new_messages: Vec<Event>,
|
pub new_messages: Vec<Message>,
|
||||||
pub reload: bool,
|
pub reload: bool,
|
||||||
pub is_initialized: bool,
|
pub is_initialized: bool,
|
||||||
}
|
}
|
||||||
@@ -22,8 +37,8 @@ impl ChatRegistry {
|
|||||||
self.reload = true;
|
self.reload = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push(&mut self, event: Event) {
|
pub fn push(&mut self, event: Event, metadata: Option<Metadata>) {
|
||||||
self.new_messages.push(event);
|
self.new_messages.push(Message { event, metadata });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ use coop_ui::{
|
|||||||
IconName, Root, Sizable, TitleBar,
|
IconName, Root, Sizable, TitleBar,
|
||||||
};
|
};
|
||||||
use gpui::*;
|
use gpui::*;
|
||||||
use nostr_sdk::prelude::*;
|
|
||||||
use prelude::FluentBuilder;
|
use prelude::FluentBuilder;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -14,12 +13,12 @@ use super::{
|
|||||||
dock::{chat::ChatPanel, left_dock::LeftDock, welcome::WelcomePanel},
|
dock::{chat::ChatPanel, left_dock::LeftDock, welcome::WelcomePanel},
|
||||||
onboarding::Onboarding,
|
onboarding::Onboarding,
|
||||||
};
|
};
|
||||||
use crate::states::account::AccountRegistry;
|
use crate::states::{account::AccountRegistry, chat::Room};
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Deserialize)]
|
#[derive(Clone, PartialEq, Eq, Deserialize)]
|
||||||
pub struct AddPanel {
|
pub struct AddPanel {
|
||||||
pub title: Option<String>,
|
pub room: Arc<Room>,
|
||||||
pub from: PublicKey,
|
pub position: DockPlacement,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_actions!(dock, [AddPanel]);
|
impl_actions!(dock, [AddPanel]);
|
||||||
@@ -112,10 +111,10 @@ impl AppView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_action_add_panel(&mut self, action: &AddPanel, cx: &mut ViewContext<Self>) {
|
fn on_action_add_panel(&mut self, action: &AddPanel, cx: &mut ViewContext<Self>) {
|
||||||
let chat_panel = Arc::new(ChatPanel::new(action.from, cx));
|
let chat_panel = Arc::new(ChatPanel::new(&action.room, cx));
|
||||||
|
|
||||||
self.dock.update(cx, |dock_area, cx| {
|
self.dock.update(cx, |dock_area, cx| {
|
||||||
dock_area.add_panel(chat_panel, DockPlacement::Center, cx);
|
dock_area.add_panel(chat_panel, action.position, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
use coop_ui::{
|
|
||||||
button::{Button, ButtonVariants},
|
|
||||||
input::{InputEvent, TextInput},
|
|
||||||
theme::ActiveTheme,
|
|
||||||
Icon, IconName,
|
|
||||||
};
|
|
||||||
use gpui::*;
|
|
||||||
use nostr_sdk::prelude::*;
|
|
||||||
|
|
||||||
use crate::get_client;
|
|
||||||
|
|
||||||
pub struct Form {
|
|
||||||
to: PublicKey,
|
|
||||||
input: View<TextInput>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Form {
|
|
||||||
pub fn new(to: PublicKey, cx: &mut ViewContext<'_, Self>) -> Self {
|
|
||||||
let input = cx.new_view(|cx| {
|
|
||||||
TextInput::new(cx)
|
|
||||||
.appearance(false)
|
|
||||||
.text_size(coop_ui::Size::Small)
|
|
||||||
.placeholder("Message...")
|
|
||||||
.cleanable()
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.subscribe(&input, move |form, _, input_event, cx| {
|
|
||||||
if let InputEvent::PressEnter = input_event {
|
|
||||||
form.send_message(cx);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
Self { to, input }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_message(&mut self, cx: &mut ViewContext<Self>) {
|
|
||||||
let send_to = self.to;
|
|
||||||
let content = self.input.read(cx).text().to_string();
|
|
||||||
let content_clone = content.clone();
|
|
||||||
|
|
||||||
let async_input = self.input.clone();
|
|
||||||
let mut async_cx = cx.to_async();
|
|
||||||
|
|
||||||
cx.foreground_executor()
|
|
||||||
.spawn(async move {
|
|
||||||
let client = get_client();
|
|
||||||
|
|
||||||
async_cx
|
|
||||||
.background_executor()
|
|
||||||
.spawn(async move {
|
|
||||||
let signer = client.signer().await.unwrap();
|
|
||||||
let public_key = signer.get_public_key().await.unwrap();
|
|
||||||
|
|
||||||
// Send message to all members
|
|
||||||
if client
|
|
||||||
.send_private_msg(send_to, content, vec![])
|
|
||||||
.await
|
|
||||||
.is_ok()
|
|
||||||
{
|
|
||||||
// Send a copy to yourself
|
|
||||||
_ = client
|
|
||||||
.send_private_msg(
|
|
||||||
public_key,
|
|
||||||
content_clone,
|
|
||||||
vec![Tag::public_key(send_to)],
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
_ = async_cx.update_view(&async_input, |input, cx| {
|
|
||||||
input.set_text("", cx);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for Form {
|
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
|
||||||
div()
|
|
||||||
.flex_shrink_0()
|
|
||||||
.w_full()
|
|
||||||
.h_12()
|
|
||||||
.border_t_1()
|
|
||||||
.border_color(cx.theme().border.opacity(0.7))
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.gap_2()
|
|
||||||
.px_2()
|
|
||||||
.child(
|
|
||||||
Button::new("upload")
|
|
||||||
.icon(Icon::new(IconName::Upload))
|
|
||||||
.ghost(),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex_1()
|
|
||||||
.flex()
|
|
||||||
.bg(cx.theme().muted)
|
|
||||||
.rounded(px(cx.theme().radius))
|
|
||||||
.px_2()
|
|
||||||
.child(self.input.clone()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
use gpui::*;
|
|
||||||
use nostr_sdk::prelude::*;
|
|
||||||
use prelude::FluentBuilder;
|
|
||||||
|
|
||||||
use crate::{get_client, states::chat::ChatRegistry};
|
|
||||||
|
|
||||||
pub struct MessageList {
|
|
||||||
member: PublicKey,
|
|
||||||
messages: Model<Option<Events>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MessageList {
|
|
||||||
pub fn new(from: PublicKey, cx: &mut ViewContext<'_, Self>) -> Self {
|
|
||||||
let messages = cx.new_model(|_| None);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
member: from,
|
|
||||||
messages,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init(&self, cx: &mut ViewContext<Self>) {
|
|
||||||
let messages = self.messages.clone();
|
|
||||||
let member = self.member;
|
|
||||||
|
|
||||||
let mut async_cx = cx.to_async();
|
|
||||||
|
|
||||||
cx.foreground_executor()
|
|
||||||
.spawn(async move {
|
|
||||||
let client = get_client();
|
|
||||||
let signer = client.signer().await.unwrap();
|
|
||||||
let public_key = signer.get_public_key().await.unwrap();
|
|
||||||
|
|
||||||
let recv = Filter::new()
|
|
||||||
.kind(Kind::PrivateDirectMessage)
|
|
||||||
.author(member)
|
|
||||||
.pubkey(public_key);
|
|
||||||
|
|
||||||
let send = Filter::new()
|
|
||||||
.kind(Kind::PrivateDirectMessage)
|
|
||||||
.author(public_key)
|
|
||||||
.pubkey(member);
|
|
||||||
|
|
||||||
let events = async_cx
|
|
||||||
.background_executor()
|
|
||||||
.spawn(async move { client.database().query(vec![recv, send]).await })
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Ok(events) = events {
|
|
||||||
_ = async_cx.update_model(&messages, |a, b| {
|
|
||||||
*a = Some(events);
|
|
||||||
b.notify();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn subscribe(&self, cx: &mut ViewContext<Self>) {
|
|
||||||
let messages = self.messages.clone();
|
|
||||||
|
|
||||||
cx.observe_global::<ChatRegistry>(move |_, cx| {
|
|
||||||
let state = cx.global::<ChatRegistry>();
|
|
||||||
let events = state.new_messages.clone();
|
|
||||||
|
|
||||||
cx.update_model(&messages, |a, b| {
|
|
||||||
if let Some(m) = a {
|
|
||||||
m.extend(events);
|
|
||||||
b.notify();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for MessageList {
|
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
|
||||||
div()
|
|
||||||
.h_full()
|
|
||||||
.flex()
|
|
||||||
.flex_col_reverse()
|
|
||||||
.justify_end()
|
|
||||||
.when_some(self.messages.read(cx).as_ref(), |this, messages| {
|
|
||||||
this.children(messages.clone().into_iter().map(|m| {
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.child(m.pubkey.to_hex())
|
|
||||||
.child(m.content)
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,15 +2,14 @@ use coop_ui::{
|
|||||||
button::Button,
|
button::Button,
|
||||||
dock::{Panel, PanelEvent, PanelState, TitleStyle},
|
dock::{Panel, PanelEvent, PanelState, TitleStyle},
|
||||||
popup_menu::PopupMenu,
|
popup_menu::PopupMenu,
|
||||||
v_flex,
|
|
||||||
};
|
};
|
||||||
use form::Form;
|
|
||||||
use gpui::*;
|
use gpui::*;
|
||||||
use list::MessageList;
|
use room::ChatRoom;
|
||||||
use nostr_sdk::*;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub mod form;
|
use crate::states::chat::Room;
|
||||||
pub mod list;
|
|
||||||
|
mod room;
|
||||||
|
|
||||||
pub struct ChatPanel {
|
pub struct ChatPanel {
|
||||||
// Panel
|
// Panel
|
||||||
@@ -18,22 +17,20 @@ pub struct ChatPanel {
|
|||||||
closeable: bool,
|
closeable: bool,
|
||||||
zoomable: bool,
|
zoomable: bool,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
// Chat Room
|
// Room
|
||||||
list: View<MessageList>,
|
room: View<ChatRoom>,
|
||||||
form: View<Form>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChatPanel {
|
impl ChatPanel {
|
||||||
pub fn new(from: PublicKey, cx: &mut WindowContext) -> View<Self> {
|
pub fn new(room: &Arc<Room>, cx: &mut WindowContext) -> View<Self> {
|
||||||
let form = cx.new_view(|cx| Form::new(from, cx));
|
let room = cx.new_view(|cx| {
|
||||||
let list = cx.new_view(|cx| {
|
let view = ChatRoom::new(room, cx);
|
||||||
let list = MessageList::new(from, cx);
|
// Load messages
|
||||||
// Load messages from database
|
view.load(cx);
|
||||||
list.init(cx);
|
// Subscribe for new messages
|
||||||
// Subscribe for new message
|
view.subscribe(cx);
|
||||||
list.subscribe(cx);
|
|
||||||
|
|
||||||
list
|
view
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.new_view(|cx| Self {
|
cx.new_view(|cx| Self {
|
||||||
@@ -41,8 +38,7 @@ impl ChatPanel {
|
|||||||
closeable: true,
|
closeable: true,
|
||||||
zoomable: true,
|
zoomable: true,
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
list,
|
room,
|
||||||
form,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,9 +87,6 @@ impl FocusableView for ChatPanel {
|
|||||||
|
|
||||||
impl Render for ChatPanel {
|
impl Render for ChatPanel {
|
||||||
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
|
||||||
v_flex()
|
div().size_full().child(self.room.clone())
|
||||||
.size_full()
|
|
||||||
.child(div().flex_1().min_h_0().child(self.list.clone()))
|
|
||||||
.child(self.form.clone())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
309
crates/app/src/views/dock/chat/room.rs
Normal file
309
crates/app/src/views/dock/chat/room.rs
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
use coop_ui::{
|
||||||
|
button::{Button, ButtonVariants},
|
||||||
|
input::{InputEvent, TextInput},
|
||||||
|
theme::ActiveTheme,
|
||||||
|
v_flex, Icon, IconName,
|
||||||
|
};
|
||||||
|
use gpui::*;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
get_client,
|
||||||
|
states::chat::{ChatRegistry, Room},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, IntoElement)]
|
||||||
|
pub struct MessageItem {
|
||||||
|
author: PublicKey,
|
||||||
|
metadata: Option<Metadata>,
|
||||||
|
content: SharedString,
|
||||||
|
created_at: Timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageItem {
|
||||||
|
pub fn new(
|
||||||
|
author: PublicKey,
|
||||||
|
metadata: Option<Metadata>,
|
||||||
|
content: String,
|
||||||
|
created_at: Timestamp,
|
||||||
|
) -> Self {
|
||||||
|
MessageItem {
|
||||||
|
author,
|
||||||
|
metadata,
|
||||||
|
created_at,
|
||||||
|
content: content.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for MessageItem {
|
||||||
|
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||||
|
div().flex().flex_col().text_sm().child(self.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Messages {
|
||||||
|
count: usize,
|
||||||
|
items: Vec<MessageItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ChatRoom {
|
||||||
|
owner: PublicKey,
|
||||||
|
members: Arc<[PublicKey]>,
|
||||||
|
// Form
|
||||||
|
input: View<TextInput>,
|
||||||
|
// Messages
|
||||||
|
list: ListState,
|
||||||
|
messages: Model<Messages>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChatRoom {
|
||||||
|
pub fn new(room: &Arc<Room>, cx: &mut ViewContext<'_, Self>) -> Self {
|
||||||
|
let members: Arc<[PublicKey]> = room.members.clone().into();
|
||||||
|
let owner = room.owner;
|
||||||
|
|
||||||
|
// Form
|
||||||
|
let input = cx.new_view(|cx| {
|
||||||
|
TextInput::new(cx)
|
||||||
|
.appearance(false)
|
||||||
|
.text_size(coop_ui::Size::Small)
|
||||||
|
.placeholder("Message...")
|
||||||
|
.cleanable()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send message when user presses enter on form.
|
||||||
|
cx.subscribe(&input, move |this, _, input_event, cx| {
|
||||||
|
if let InputEvent::PressEnter = input_event {
|
||||||
|
this.send_message(cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
let messages = cx.new_model(|_| Messages {
|
||||||
|
count: 0,
|
||||||
|
items: vec![],
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.observe(&messages, |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();
|
||||||
|
|
||||||
|
let list = ListState::new(0, ListAlignment::Bottom, Pixels(256.), move |_, _| {
|
||||||
|
div().into_any_element()
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
owner,
|
||||||
|
members,
|
||||||
|
input,
|
||||||
|
list,
|
||||||
|
messages,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(&self, cx: &mut ViewContext<Self>) {
|
||||||
|
let async_messages = self.messages.clone();
|
||||||
|
let mut async_cx = cx.to_async();
|
||||||
|
|
||||||
|
cx.foreground_executor()
|
||||||
|
.spawn({
|
||||||
|
let client = get_client();
|
||||||
|
let members = self.members.to_vec();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
let signer = client.signer().await.unwrap();
|
||||||
|
let public_key = signer.get_public_key().await.unwrap();
|
||||||
|
|
||||||
|
let recv = Filter::new()
|
||||||
|
.kind(Kind::PrivateDirectMessage)
|
||||||
|
.authors(members.clone())
|
||||||
|
.pubkey(public_key);
|
||||||
|
|
||||||
|
let send = Filter::new()
|
||||||
|
.kind(Kind::PrivateDirectMessage)
|
||||||
|
.author(public_key)
|
||||||
|
.pubkeys(members);
|
||||||
|
|
||||||
|
let events = async_cx
|
||||||
|
.background_executor()
|
||||||
|
.spawn(async move { client.database().query(vec![recv, send]).await })
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Ok(events) = events {
|
||||||
|
let public_keys: Vec<PublicKey> = events
|
||||||
|
.iter()
|
||||||
|
.unique_by(|ev| ev.pubkey)
|
||||||
|
.map(|ev| ev.pubkey)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut profiles = async_cx
|
||||||
|
.background_executor()
|
||||||
|
.spawn(async move {
|
||||||
|
let mut data: HashMap<PublicKey, Option<Metadata>> = HashMap::new();
|
||||||
|
|
||||||
|
for public_key in public_keys.into_iter() {
|
||||||
|
if let Ok(metadata) =
|
||||||
|
client.database().metadata(public_key).await
|
||||||
|
{
|
||||||
|
data.insert(public_key, metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let items: Vec<MessageItem> = events
|
||||||
|
.into_iter()
|
||||||
|
.sorted_by_key(|ev| ev.created_at)
|
||||||
|
.map(|ev| {
|
||||||
|
// Get user's metadata
|
||||||
|
let metadata = profiles.get_mut(&ev.pubkey).and_then(Option::take);
|
||||||
|
// Return message item
|
||||||
|
MessageItem::new(ev.pubkey, metadata, ev.content, ev.created_at)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let total = items.len();
|
||||||
|
|
||||||
|
_ = async_cx.update_model(&async_messages, |a, b| {
|
||||||
|
a.items = items;
|
||||||
|
a.count = total;
|
||||||
|
b.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subscribe(&self, cx: &mut ViewContext<Self>) {
|
||||||
|
let messages = self.messages.clone();
|
||||||
|
|
||||||
|
cx.observe_global::<ChatRegistry>(move |_, cx| {
|
||||||
|
let state = cx.global::<ChatRegistry>();
|
||||||
|
let events = state.new_messages.clone();
|
||||||
|
// let mut metadata = state.metadata.clone();
|
||||||
|
|
||||||
|
// TODO: filter messages
|
||||||
|
let items: Vec<MessageItem> = events
|
||||||
|
.into_iter()
|
||||||
|
.map(|m| {
|
||||||
|
MessageItem::new(
|
||||||
|
m.event.pubkey,
|
||||||
|
m.metadata,
|
||||||
|
m.event.content,
|
||||||
|
m.event.created_at,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
cx.update_model(&messages, |a, b| {
|
||||||
|
a.items.extend(items);
|
||||||
|
a.count = a.items.len();
|
||||||
|
b.notify();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: support chat room
|
||||||
|
pub fn send_message(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
let owner = self.owner;
|
||||||
|
let content = self.input.read(cx).text().to_string();
|
||||||
|
let content_clone = content.clone();
|
||||||
|
|
||||||
|
let async_input = self.input.clone();
|
||||||
|
let mut async_cx = cx.to_async();
|
||||||
|
|
||||||
|
cx.foreground_executor()
|
||||||
|
.spawn({
|
||||||
|
let client = get_client();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
let send: anyhow::Result<(), anyhow::Error> = async_cx
|
||||||
|
.background_executor()
|
||||||
|
.spawn(async move {
|
||||||
|
let signer = client.signer().await?;
|
||||||
|
let public_key = signer.get_public_key().await?;
|
||||||
|
|
||||||
|
// Send message to [owner]
|
||||||
|
if client
|
||||||
|
.send_private_msg(owner, content, vec![])
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
// Send a copy to [yourself]
|
||||||
|
_ = client
|
||||||
|
.send_private_msg(
|
||||||
|
public_key,
|
||||||
|
content_clone,
|
||||||
|
vec![Tag::public_key(owner)],
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if send.is_ok() {
|
||||||
|
_ = async_cx.update_view(&async_input, |input, cx| {
|
||||||
|
input.set_text("", cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ChatRoom {
|
||||||
|
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
|
||||||
|
v_flex()
|
||||||
|
.size_full()
|
||||||
|
.child(list(self.list.clone()).size_full().flex_1())
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex_shrink_0()
|
||||||
|
.w_full()
|
||||||
|
.h_12()
|
||||||
|
.border_t_1()
|
||||||
|
.border_color(cx.theme().border.opacity(0.7))
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_2()
|
||||||
|
.px_2()
|
||||||
|
.child(
|
||||||
|
Button::new("upload")
|
||||||
|
.icon(Icon::new(IconName::Upload))
|
||||||
|
.ghost(),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex_1()
|
||||||
|
.flex()
|
||||||
|
.bg(cx.theme().muted)
|
||||||
|
.rounded(px(cx.theme().radius))
|
||||||
|
.px_2()
|
||||||
|
.child(self.input.clone()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use coop_ui::{theme::ActiveTheme, Selectable, StyledExt};
|
use coop_ui::{theme::ActiveTheme, Selectable, StyledExt};
|
||||||
use gpui::*;
|
use gpui::*;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
@@ -5,45 +7,36 @@ use prelude::FluentBuilder;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
get_client,
|
get_client,
|
||||||
states::{metadata::MetadataRegistry, signal::SignalRegistry},
|
states::{chat::Room, metadata::MetadataRegistry, signal::SignalRegistry},
|
||||||
utils::{ago, show_npub},
|
utils::{ago, show_npub},
|
||||||
views::app::AddPanel,
|
views::app::AddPanel,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
struct ChatItem {
|
struct Item {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
public_key: PublicKey,
|
room: Arc<Room>,
|
||||||
metadata: Option<Metadata>,
|
metadata: Option<Metadata>,
|
||||||
last_seen: Timestamp,
|
|
||||||
title: Option<String>,
|
|
||||||
// Interactive
|
// Interactive
|
||||||
base: Div,
|
base: Div,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChatItem {
|
impl Item {
|
||||||
pub fn new(
|
pub fn new(room: Arc<Room>, metadata: Option<Metadata>) -> Self {
|
||||||
public_key: PublicKey,
|
let id = SharedString::from(room.owner.to_hex()).into();
|
||||||
metadata: Option<Metadata>,
|
|
||||||
last_seen: Timestamp,
|
|
||||||
title: Option<String>,
|
|
||||||
) -> Self {
|
|
||||||
let id = SharedString::from(public_key.to_hex()).into();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
public_key,
|
room,
|
||||||
metadata,
|
metadata,
|
||||||
last_seen,
|
|
||||||
title,
|
|
||||||
base: div(),
|
base: div(),
|
||||||
selected: false,
|
selected: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Selectable for ChatItem {
|
impl Selectable for Item {
|
||||||
fn selected(mut self, selected: bool) -> Self {
|
fn selected(mut self, selected: bool) -> Self {
|
||||||
self.selected = selected;
|
self.selected = selected;
|
||||||
self
|
self
|
||||||
@@ -54,16 +47,16 @@ impl Selectable for ChatItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InteractiveElement for ChatItem {
|
impl InteractiveElement for Item {
|
||||||
fn interactivity(&mut self) -> &mut gpui::Interactivity {
|
fn interactivity(&mut self) -> &mut gpui::Interactivity {
|
||||||
self.base.interactivity()
|
self.base.interactivity()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for ChatItem {
|
impl RenderOnce for Item {
|
||||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||||
let ago = ago(self.last_seen.as_u64());
|
let ago = ago(self.room.last_seen.as_u64());
|
||||||
let fallback_name = show_npub(self.public_key, 16);
|
let fallback_name = show_npub(self.room.owner, 16);
|
||||||
|
|
||||||
let mut content = div()
|
let mut content = div()
|
||||||
.font_medium()
|
.font_medium()
|
||||||
@@ -129,34 +122,41 @@ impl RenderOnce for ChatItem {
|
|||||||
)
|
)
|
||||||
.on_click(move |_, cx| {
|
.on_click(move |_, cx| {
|
||||||
cx.dispatch_action(Box::new(AddPanel {
|
cx.dispatch_action(Box::new(AddPanel {
|
||||||
title: self.title.clone(),
|
room: self.room.clone(),
|
||||||
from: self.public_key,
|
position: coop_ui::dock::DockPlacement::Center,
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Chat {
|
pub struct InboxItem {
|
||||||
title: Option<String>,
|
room: Arc<Room>,
|
||||||
metadata: Model<Option<Metadata>>,
|
metadata: Model<Option<Metadata>>,
|
||||||
last_seen: Timestamp,
|
pub(crate) sender: PublicKey,
|
||||||
pub(crate) public_key: PublicKey,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Chat {
|
impl InboxItem {
|
||||||
pub fn new(event: Event, cx: &mut ViewContext<'_, Self>) -> Self {
|
pub fn new(event: Event, cx: &mut ViewContext<'_, Self>) -> Self {
|
||||||
let public_key = event.pubkey;
|
let sender = event.pubkey;
|
||||||
let last_seen = event.created_at;
|
let last_seen = event.created_at;
|
||||||
|
|
||||||
|
// Get all members from event's tag
|
||||||
|
let mut members: Vec<PublicKey> = event.tags.public_keys().copied().collect();
|
||||||
|
// Add sender to members
|
||||||
|
members.insert(0, sender);
|
||||||
|
|
||||||
|
// Get title from event's tag
|
||||||
let title = if let Some(tag) = event.tags.find(TagKind::Title) {
|
let title = if let Some(tag) = event.tags.find(TagKind::Title) {
|
||||||
tag.content().map(|s| s.to_string())
|
tag.content().map(|s| s.to_string())
|
||||||
} else {
|
} else {
|
||||||
|
// TODO: create random name?
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let metadata = cx.new_model(|_| None);
|
let metadata = cx.new_model(|_| None);
|
||||||
|
|
||||||
// Request metadata
|
// Request metadata
|
||||||
_ = cx.global::<SignalRegistry>().tx.send(public_key);
|
_ = cx.global::<SignalRegistry>().tx.send(sender);
|
||||||
|
|
||||||
// Reload when received metadata
|
// Reload when received metadata
|
||||||
cx.observe_global::<MetadataRegistry>(|chat, cx| {
|
cx.observe_global::<MetadataRegistry>(|chat, cx| {
|
||||||
@@ -164,16 +164,22 @@ impl Chat {
|
|||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
Self {
|
let room = Arc::new(Room {
|
||||||
public_key,
|
|
||||||
last_seen,
|
|
||||||
metadata,
|
|
||||||
title,
|
title,
|
||||||
|
members,
|
||||||
|
last_seen,
|
||||||
|
owner: sender,
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
room,
|
||||||
|
sender,
|
||||||
|
metadata,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_metadata(&mut self, cx: &mut ViewContext<Self>) {
|
pub fn load_metadata(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
let public_key = self.public_key;
|
let public_key = self.sender;
|
||||||
let async_metadata = self.metadata.clone();
|
let async_metadata = self.metadata.clone();
|
||||||
let mut async_cx = cx.to_async();
|
let mut async_cx = cx.to_async();
|
||||||
|
|
||||||
@@ -194,17 +200,17 @@ impl Chat {
|
|||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for Chat {
|
fn render_item(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
|
||||||
let metadata = self.metadata.read(cx).clone();
|
let metadata = self.metadata.read(cx).clone();
|
||||||
|
let room = self.room.clone();
|
||||||
|
|
||||||
div().child(ChatItem::new(
|
Item::new(room, metadata)
|
||||||
self.public_key,
|
}
|
||||||
metadata,
|
}
|
||||||
self.last_seen,
|
|
||||||
self.title.clone(),
|
impl Render for InboxItem {
|
||||||
))
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
div().child(self.render_item(cx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
use chat::Chat;
|
|
||||||
use coop_ui::{
|
use coop_ui::{
|
||||||
skeleton::Skeleton, theme::ActiveTheme, v_flex, Collapsible, Icon, IconName, StyledExt,
|
skeleton::Skeleton, theme::ActiveTheme, v_flex, Collapsible, Icon, IconName, StyledExt,
|
||||||
};
|
};
|
||||||
use gpui::*;
|
use gpui::*;
|
||||||
|
use item::InboxItem;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use prelude::FluentBuilder;
|
use prelude::FluentBuilder;
|
||||||
@@ -10,12 +10,12 @@ use std::cmp::Reverse;
|
|||||||
|
|
||||||
use crate::{get_client, states::chat::ChatRegistry};
|
use crate::{get_client, states::chat::ChatRegistry};
|
||||||
|
|
||||||
pub mod chat;
|
pub mod item;
|
||||||
|
|
||||||
pub struct Inbox {
|
pub struct Inbox {
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
events: Model<Option<Vec<Event>>>,
|
events: Model<Option<Vec<Event>>>,
|
||||||
chats: Model<Vec<View<Chat>>>,
|
chats: Model<Vec<View<InboxItem>>>,
|
||||||
is_loading: bool,
|
is_loading: bool,
|
||||||
is_fetching: bool,
|
is_fetching: bool,
|
||||||
is_collapsed: bool,
|
is_collapsed: bool,
|
||||||
@@ -37,8 +37,8 @@ impl Inbox {
|
|||||||
for message in new_messages.into_iter() {
|
for message in new_messages.into_iter() {
|
||||||
cx.update_model(&inbox.events, |model, b| {
|
cx.update_model(&inbox.events, |model, b| {
|
||||||
if let Some(events) = model {
|
if let Some(events) = model {
|
||||||
if !events.iter().any(|ev| ev.pubkey == message.pubkey) {
|
if !events.iter().any(|ev| ev.pubkey == message.event.pubkey) {
|
||||||
events.push(message);
|
events.push(message.event);
|
||||||
b.notify();
|
b.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,15 +56,14 @@ impl Inbox {
|
|||||||
|
|
||||||
if let Some(events) = events {
|
if let Some(events) = events {
|
||||||
let views = inbox.chats.read(cx);
|
let views = inbox.chats.read(cx);
|
||||||
let public_keys: Vec<PublicKey> =
|
let public_keys: Vec<PublicKey> = views.iter().map(|v| v.read(cx).sender).collect();
|
||||||
views.iter().map(|v| v.read(cx).public_key).collect();
|
|
||||||
|
|
||||||
for event in events
|
for event in events
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.sorted_by_key(|ev| Reverse(ev.created_at))
|
.sorted_by_key(|ev| Reverse(ev.created_at))
|
||||||
{
|
{
|
||||||
if !public_keys.contains(&event.pubkey) {
|
if !public_keys.contains(&event.pubkey) {
|
||||||
let view = cx.new_view(|cx| Chat::new(event, cx));
|
let view = cx.new_view(|cx| InboxItem::new(event, cx));
|
||||||
|
|
||||||
cx.update_model(&inbox.chats, |a, b| {
|
cx.update_model(&inbox.chats, |a, b| {
|
||||||
a.push(view);
|
a.push(view);
|
||||||
@@ -79,7 +78,7 @@ impl Inbox {
|
|||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
cx.observe_new_views::<Chat>(|chat, cx| {
|
cx.observe_new_views::<InboxItem>(|chat, cx| {
|
||||||
chat.load_metadata(cx);
|
chat.load_metadata(cx);
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|||||||
Reference in New Issue
Block a user