wip: refactor

This commit is contained in:
2024-12-23 10:22:57 +07:00
parent 52db4153f1
commit 9a9f3327ea
9 changed files with 425 additions and 293 deletions

View File

@@ -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);
});
}
_ => {} _ => {}
} }
} }

View File

@@ -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 {

View File

@@ -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);
}); });
} }
} }

View File

@@ -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()),
)
}
}

View File

@@ -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)
}))
})
}
}

View File

@@ -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())
} }
} }

View 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()),
),
)
}
}

View File

@@ -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))
} }
} }

View File

@@ -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();