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) => {
|
||||
_ = async_cx.update_global::<MetadataRegistry, _>(|state, _cx| {
|
||||
state.seen(public_key);
|
||||
})
|
||||
}
|
||||
Signal::RecvEvent(event) => {
|
||||
_ = async_cx.update_global::<ChatRegistry, _>(|state, _| {
|
||||
state.push(event);
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
use gpui::*;
|
||||
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 new_messages: Vec<Event>,
|
||||
pub new_messages: Vec<Message>,
|
||||
pub reload: bool,
|
||||
pub is_initialized: bool,
|
||||
}
|
||||
@@ -22,8 +37,8 @@ impl ChatRegistry {
|
||||
self.reload = true;
|
||||
}
|
||||
|
||||
pub fn push(&mut self, event: Event) {
|
||||
self.new_messages.push(event);
|
||||
pub fn push(&mut self, event: Event, metadata: Option<Metadata>) {
|
||||
self.new_messages.push(Message { event, metadata });
|
||||
}
|
||||
|
||||
fn new() -> Self {
|
||||
|
||||
@@ -5,7 +5,6 @@ use coop_ui::{
|
||||
IconName, Root, Sizable, TitleBar,
|
||||
};
|
||||
use gpui::*;
|
||||
use nostr_sdk::prelude::*;
|
||||
use prelude::FluentBuilder;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
@@ -14,12 +13,12 @@ use super::{
|
||||
dock::{chat::ChatPanel, left_dock::LeftDock, welcome::WelcomePanel},
|
||||
onboarding::Onboarding,
|
||||
};
|
||||
use crate::states::account::AccountRegistry;
|
||||
use crate::states::{account::AccountRegistry, chat::Room};
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Deserialize)]
|
||||
pub struct AddPanel {
|
||||
pub title: Option<String>,
|
||||
pub from: PublicKey,
|
||||
pub room: Arc<Room>,
|
||||
pub position: DockPlacement,
|
||||
}
|
||||
|
||||
impl_actions!(dock, [AddPanel]);
|
||||
@@ -112,10 +111,10 @@ impl AppView {
|
||||
}
|
||||
|
||||
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| {
|
||||
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,
|
||||
dock::{Panel, PanelEvent, PanelState, TitleStyle},
|
||||
popup_menu::PopupMenu,
|
||||
v_flex,
|
||||
};
|
||||
use form::Form;
|
||||
use gpui::*;
|
||||
use list::MessageList;
|
||||
use nostr_sdk::*;
|
||||
use room::ChatRoom;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub mod form;
|
||||
pub mod list;
|
||||
use crate::states::chat::Room;
|
||||
|
||||
mod room;
|
||||
|
||||
pub struct ChatPanel {
|
||||
// Panel
|
||||
@@ -18,22 +17,20 @@ pub struct ChatPanel {
|
||||
closeable: bool,
|
||||
zoomable: bool,
|
||||
focus_handle: FocusHandle,
|
||||
// Chat Room
|
||||
list: View<MessageList>,
|
||||
form: View<Form>,
|
||||
// Room
|
||||
room: View<ChatRoom>,
|
||||
}
|
||||
|
||||
impl ChatPanel {
|
||||
pub fn new(from: PublicKey, cx: &mut WindowContext) -> View<Self> {
|
||||
let form = cx.new_view(|cx| Form::new(from, cx));
|
||||
let list = cx.new_view(|cx| {
|
||||
let list = MessageList::new(from, cx);
|
||||
// Load messages from database
|
||||
list.init(cx);
|
||||
// Subscribe for new message
|
||||
list.subscribe(cx);
|
||||
pub fn new(room: &Arc<Room>, cx: &mut WindowContext) -> View<Self> {
|
||||
let room = cx.new_view(|cx| {
|
||||
let view = ChatRoom::new(room, cx);
|
||||
// Load messages
|
||||
view.load(cx);
|
||||
// Subscribe for new messages
|
||||
view.subscribe(cx);
|
||||
|
||||
list
|
||||
view
|
||||
});
|
||||
|
||||
cx.new_view(|cx| Self {
|
||||
@@ -41,8 +38,7 @@ impl ChatPanel {
|
||||
closeable: true,
|
||||
zoomable: true,
|
||||
focus_handle: cx.focus_handle(),
|
||||
list,
|
||||
form,
|
||||
room,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -91,9 +87,6 @@ impl FocusableView for ChatPanel {
|
||||
|
||||
impl Render for ChatPanel {
|
||||
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.child(div().flex_1().min_h_0().child(self.list.clone()))
|
||||
.child(self.form.clone())
|
||||
div().size_full().child(self.room.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 gpui::*;
|
||||
use nostr_sdk::prelude::*;
|
||||
@@ -5,45 +7,36 @@ use prelude::FluentBuilder;
|
||||
|
||||
use crate::{
|
||||
get_client,
|
||||
states::{metadata::MetadataRegistry, signal::SignalRegistry},
|
||||
states::{chat::Room, metadata::MetadataRegistry, signal::SignalRegistry},
|
||||
utils::{ago, show_npub},
|
||||
views::app::AddPanel,
|
||||
};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct ChatItem {
|
||||
struct Item {
|
||||
id: ElementId,
|
||||
public_key: PublicKey,
|
||||
room: Arc<Room>,
|
||||
metadata: Option<Metadata>,
|
||||
last_seen: Timestamp,
|
||||
title: Option<String>,
|
||||
// Interactive
|
||||
base: Div,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
impl ChatItem {
|
||||
pub fn new(
|
||||
public_key: PublicKey,
|
||||
metadata: Option<Metadata>,
|
||||
last_seen: Timestamp,
|
||||
title: Option<String>,
|
||||
) -> Self {
|
||||
let id = SharedString::from(public_key.to_hex()).into();
|
||||
impl Item {
|
||||
pub fn new(room: Arc<Room>, metadata: Option<Metadata>) -> Self {
|
||||
let id = SharedString::from(room.owner.to_hex()).into();
|
||||
|
||||
Self {
|
||||
id,
|
||||
public_key,
|
||||
room,
|
||||
metadata,
|
||||
last_seen,
|
||||
title,
|
||||
base: div(),
|
||||
selected: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for ChatItem {
|
||||
impl Selectable for Item {
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
@@ -54,16 +47,16 @@ impl Selectable for ChatItem {
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveElement for ChatItem {
|
||||
impl InteractiveElement for Item {
|
||||
fn interactivity(&mut self) -> &mut gpui::Interactivity {
|
||||
self.base.interactivity()
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ChatItem {
|
||||
impl RenderOnce for Item {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let ago = ago(self.last_seen.as_u64());
|
||||
let fallback_name = show_npub(self.public_key, 16);
|
||||
let ago = ago(self.room.last_seen.as_u64());
|
||||
let fallback_name = show_npub(self.room.owner, 16);
|
||||
|
||||
let mut content = div()
|
||||
.font_medium()
|
||||
@@ -129,34 +122,41 @@ impl RenderOnce for ChatItem {
|
||||
)
|
||||
.on_click(move |_, cx| {
|
||||
cx.dispatch_action(Box::new(AddPanel {
|
||||
title: self.title.clone(),
|
||||
from: self.public_key,
|
||||
room: self.room.clone(),
|
||||
position: coop_ui::dock::DockPlacement::Center,
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Chat {
|
||||
title: Option<String>,
|
||||
pub struct InboxItem {
|
||||
room: Arc<Room>,
|
||||
metadata: Model<Option<Metadata>>,
|
||||
last_seen: Timestamp,
|
||||
pub(crate) public_key: PublicKey,
|
||||
pub(crate) sender: PublicKey,
|
||||
}
|
||||
|
||||
impl Chat {
|
||||
impl InboxItem {
|
||||
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;
|
||||
|
||||
// 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) {
|
||||
tag.content().map(|s| s.to_string())
|
||||
} else {
|
||||
// TODO: create random name?
|
||||
None
|
||||
};
|
||||
|
||||
let metadata = cx.new_model(|_| None);
|
||||
|
||||
// Request metadata
|
||||
_ = cx.global::<SignalRegistry>().tx.send(public_key);
|
||||
_ = cx.global::<SignalRegistry>().tx.send(sender);
|
||||
|
||||
// Reload when received metadata
|
||||
cx.observe_global::<MetadataRegistry>(|chat, cx| {
|
||||
@@ -164,16 +164,22 @@ impl Chat {
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
public_key,
|
||||
last_seen,
|
||||
metadata,
|
||||
let room = Arc::new(Room {
|
||||
title,
|
||||
members,
|
||||
last_seen,
|
||||
owner: sender,
|
||||
});
|
||||
|
||||
Self {
|
||||
room,
|
||||
sender,
|
||||
metadata,
|
||||
}
|
||||
}
|
||||
|
||||
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 mut async_cx = cx.to_async();
|
||||
|
||||
@@ -194,17 +200,17 @@ impl Chat {
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Chat {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render_item(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let metadata = self.metadata.read(cx).clone();
|
||||
let room = self.room.clone();
|
||||
|
||||
div().child(ChatItem::new(
|
||||
self.public_key,
|
||||
metadata,
|
||||
self.last_seen,
|
||||
self.title.clone(),
|
||||
))
|
||||
Item::new(room, metadata)
|
||||
}
|
||||
}
|
||||
|
||||
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::{
|
||||
skeleton::Skeleton, theme::ActiveTheme, v_flex, Collapsible, Icon, IconName, StyledExt,
|
||||
};
|
||||
use gpui::*;
|
||||
use item::InboxItem;
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use prelude::FluentBuilder;
|
||||
@@ -10,12 +10,12 @@ use std::cmp::Reverse;
|
||||
|
||||
use crate::{get_client, states::chat::ChatRegistry};
|
||||
|
||||
pub mod chat;
|
||||
pub mod item;
|
||||
|
||||
pub struct Inbox {
|
||||
label: SharedString,
|
||||
events: Model<Option<Vec<Event>>>,
|
||||
chats: Model<Vec<View<Chat>>>,
|
||||
chats: Model<Vec<View<InboxItem>>>,
|
||||
is_loading: bool,
|
||||
is_fetching: bool,
|
||||
is_collapsed: bool,
|
||||
@@ -37,8 +37,8 @@ impl Inbox {
|
||||
for message in new_messages.into_iter() {
|
||||
cx.update_model(&inbox.events, |model, b| {
|
||||
if let Some(events) = model {
|
||||
if !events.iter().any(|ev| ev.pubkey == message.pubkey) {
|
||||
events.push(message);
|
||||
if !events.iter().any(|ev| ev.pubkey == message.event.pubkey) {
|
||||
events.push(message.event);
|
||||
b.notify();
|
||||
}
|
||||
}
|
||||
@@ -56,15 +56,14 @@ impl Inbox {
|
||||
|
||||
if let Some(events) = events {
|
||||
let views = inbox.chats.read(cx);
|
||||
let public_keys: Vec<PublicKey> =
|
||||
views.iter().map(|v| v.read(cx).public_key).collect();
|
||||
let public_keys: Vec<PublicKey> = views.iter().map(|v| v.read(cx).sender).collect();
|
||||
|
||||
for event in events
|
||||
.into_iter()
|
||||
.sorted_by_key(|ev| Reverse(ev.created_at))
|
||||
{
|
||||
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| {
|
||||
a.push(view);
|
||||
@@ -79,7 +78,7 @@ impl Inbox {
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.observe_new_views::<Chat>(|chat, cx| {
|
||||
cx.observe_new_views::<InboxItem>(|chat, cx| {
|
||||
chat.load_metadata(cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
Reference in New Issue
Block a user