refactor chats (#15)

* refactor

* update

* update

* update

* remove nostrprofile struct

* update

* refactor contacts

* prevent double login
This commit is contained in:
reya
2025-04-10 08:10:53 +07:00
committed by GitHub
parent f7610cc9c9
commit 3246abace1
27 changed files with 1166 additions and 909 deletions

View File

@@ -1,4 +1,5 @@
use account::Account;
use common::profile::SharedProfile;
use global::get_client;
use gpui::{
actions, div, img, impl_internal_actions, prelude::FluentBuilder, px, App, AppContext, Axis,
@@ -172,7 +173,7 @@ impl ChatSpace {
.icon(Icon::new(IconName::ChevronDownSmall))
.when_some(
Account::global(cx).read(cx).profile.as_ref(),
|this, profile| this.child(img(profile.avatar.clone()).size_5()),
|this, profile| this.child(img(profile.shared_avatar()).size_5()),
)
.popup_menu(move |this, _, _cx| {
this.menu(

View File

@@ -18,7 +18,8 @@ use gpui::{point, SharedString, TitlebarOptions};
use gpui::{WindowBackgroundAppearance, WindowDecorations};
use nostr_sdk::{
pool::prelude::ReqExitPolicy, Event, EventBuilder, EventId, Filter, JsonUtil, Keys, Kind,
PublicKey, RelayMessage, RelayPoolNotification, SubscribeAutoCloseOptions, SubscriptionId, Tag,
Metadata, PublicKey, RelayMessage, RelayPoolNotification, SubscribeAutoCloseOptions,
SubscriptionId, Tag,
};
use smol::Timer;
use std::{collections::HashSet, mem, sync::Arc, time::Duration};
@@ -34,6 +35,8 @@ actions!(coop, [Quit]);
enum Signal {
/// Receive event
Event(Event),
/// Receive metadata
Metadata(Box<(PublicKey, Option<Metadata>)>),
/// Receive EOSE
Eose,
}
@@ -149,6 +152,14 @@ fn main() {
event_tx.send(Signal::Event(event)).await.ok();
}
}
Kind::Metadata => {
let metadata = Metadata::from_json(&event.content).ok();
event_tx
.send(Signal::Metadata(Box::new((event.pubkey, metadata))))
.await
.ok();
}
Kind::ContactList => {
if let Ok(signer) = client.signer().await {
if let Ok(public_key) = signer.get_public_key().await {
@@ -241,14 +252,23 @@ fn main() {
while let Ok(signal) = event_rx.recv().await {
cx.update(|window, cx| {
match signal {
Signal::Eose => {
chats.update(cx, |this, cx| this.load_rooms(window, cx));
}
Signal::Event(event) => {
chats.update(cx, |this, cx| {
this.push_message(event, window, cx)
});
}
Signal::Metadata(data) => {
chats.update(cx, |this, cx| {
this.add_profile(data.0, data.1, cx)
});
}
Signal::Eose => {
chats.update(cx, |this, cx| {
// This function maybe called multiple times
// TODO: only handle the last EOSE signal
this.load_rooms(window, cx)
});
}
};
})
.ok();

View File

@@ -1,7 +1,7 @@
use anyhow::{anyhow, Error};
use async_utility::task::spawn;
use chats::{message::RoomMessage, room::Room, ChatRegistry};
use common::utils::nip96_upload;
use common::{nip96_upload, profile::SharedProfile};
use global::{constants::IMAGE_SERVICE, get_client};
use gpui::{
div, img, list, prelude::FluentBuilder, px, relative, svg, white, AnyElement, App, AppContext,
@@ -27,7 +27,7 @@ use ui::{
const ALERT: &str = "has not set up Messaging (DM) Relays, so they will NOT receive your messages.";
pub fn init(id: &u64, window: &mut Window, cx: &mut App) -> Result<Arc<Entity<Chat>>, Error> {
if let Some(room) = ChatRegistry::global(cx).read(cx).get(id, cx) {
if let Some(room) = ChatRegistry::global(cx).read(cx).room(id, cx) {
Ok(Arc::new(Chat::new(id, room, window, cx)))
} else {
Err(anyhow!("Chat Room not found."))
@@ -137,14 +137,14 @@ impl Chat {
this.update(cx, |this, cx| {
result.into_iter().for_each(|item| {
if !item.1 {
if let Some(profile) =
this.room.read_with(cx, |this, _| this.member(&item.0))
{
this.push_system_message(
format!("{} {}", profile.name, ALERT),
cx,
);
}
let profile = this
.room
.read_with(cx, |this, _| this.profile_by_pubkey(&item.0, cx));
this.push_system_message(
format!("{} {}", profile.shared_name(), ALERT),
cx,
);
}
});
})
@@ -367,7 +367,7 @@ impl Chat {
this.bg(cx.theme().accent.step(cx, ColorScaleStep::NINE))
}),
)
.child(img(item.author.avatar.clone()).size_8().flex_shrink_0())
.child(img(item.author.shared_avatar()).size_8().flex_shrink_0())
.child(
div()
.flex()
@@ -381,17 +381,11 @@ impl Chat {
.gap_2()
.text_xs()
.child(
div().font_semibold().child(item.author.name.clone()),
div().font_semibold().child(item.author.shared_name()),
)
.child(
div()
.child(item.created_at.human_readable())
.text_color(
cx.theme()
.base
.step(cx, ColorScaleStep::ELEVEN),
),
),
.child(div().child(item.ago()).text_color(
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
)),
)
.child(div().text_sm().child(text.element(
"body".into(),
@@ -445,11 +439,7 @@ impl Panel for Chat {
fn title(&self, cx: &App) -> AnyElement {
self.room.read_with(cx, |this, _| {
let facepill: Vec<SharedString> = this
.members
.iter()
.map(|member| member.avatar.clone())
.collect();
let facepill: Vec<SharedString> = this.avatars(cx);
div()
.flex()
@@ -461,13 +451,19 @@ impl Panel for Chat {
.flex_row_reverse()
.items_center()
.justify_start()
.children(facepill.into_iter().enumerate().rev().map(|(ix, face)| {
div()
.when(ix > 0, |div| div.ml_neg_1())
.child(img(face).size_4())
})),
.children(
facepill
.into_iter()
.enumerate()
.rev()
.map(|(ix, facepill)| {
div()
.when(ix > 0, |div| div.ml_neg_1())
.child(img(facepill).size_4())
}),
),
)
.when_some(this.subject(), |this, name| this.child(name))
.child(this.display_name(cx))
.into_any()
})
}

View File

@@ -1,10 +1,14 @@
use common::profile::NostrProfile;
use std::collections::BTreeSet;
use anyhow::Error;
use common::profile::SharedProfile;
use global::get_client;
use gpui::{
div, img, prelude::FluentBuilder, px, uniform_list, AnyElement, App, AppContext, Context,
Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement,
Render, SharedString, Styled, Window,
Render, SharedString, Styled, Task, Window,
};
use itertools::Itertools;
use nostr_sdk::prelude::*;
use ui::{
button::Button,
@@ -20,7 +24,7 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Contacts> {
}
pub struct Contacts {
contacts: Entity<Option<Vec<NostrProfile>>>,
contacts: Option<Vec<Profile>>,
// Panel
name: SharedString,
closable: bool,
@@ -29,48 +33,42 @@ pub struct Contacts {
}
impl Contacts {
pub fn new(_window: &mut Window, cx: &mut App) -> Entity<Self> {
let contacts = cx.new(|_| None);
let async_contact = contacts.clone();
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
cx.new(|cx| Self::view(window, cx))
}
cx.spawn(async move |cx| {
fn view(_window: &mut Window, cx: &mut Context<Self>) -> Self {
cx.spawn(async move |this, cx| {
let client = get_client();
let (tx, rx) = oneshot::channel::<Vec<NostrProfile>>();
cx.background_executor()
.spawn(async move {
let signer = client.signer().await.unwrap();
let public_key = signer.get_public_key().await.unwrap();
let task: Task<Result<BTreeSet<Profile>, Error>> = cx.background_spawn(async move {
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let profiles = client.database().contacts(public_key).await?;
if let Ok(profiles) = client.database().contacts(public_key).await {
let members: Vec<NostrProfile> = profiles
.into_iter()
.map(|profile| {
NostrProfile::new(profile.public_key(), profile.metadata())
})
.collect();
Ok(profiles)
});
_ = tx.send(members);
}
if let Ok(contacts) = task.await {
cx.update(|cx| {
this.update(cx, |this, cx| {
this.contacts = Some(contacts.into_iter().collect_vec());
cx.notify();
})
.ok();
})
.detach();
if let Ok(contacts) = rx.await {
_ = cx.update_entity(&async_contact, |this, cx| {
*this = Some(contacts);
cx.notify();
});
.ok();
}
})
.detach();
cx.new(|cx| Self {
contacts,
Self {
contacts: None,
name: "Contacts".into(),
closable: true,
zoomable: true,
focus_handle: cx.focus_handle(),
})
}
}
}
@@ -111,7 +109,7 @@ impl Focusable for Contacts {
impl Render for Contacts {
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
div().size_full().pt_2().px_2().map(|this| {
if let Some(contacts) = self.contacts.read(cx).clone() {
if let Some(contacts) = self.contacts.clone() {
this.child(
uniform_list(
cx.entity().clone(),
@@ -141,9 +139,9 @@ impl Render for Contacts {
.child(
div()
.flex_shrink_0()
.child(img(item.avatar).size_6()),
.child(img(item.shared_avatar()).size_6()),
)
.child(item.name),
.child(item.shared_name()),
)
.hover(|this| {
this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE))

View File

@@ -1,7 +1,7 @@
use std::{sync::Arc, time::Duration};
use account::Account;
use common::utils::create_qr;
use common::create_qr;
use global::get_client_keys;
use gpui::{
div, img, prelude::FluentBuilder, relative, AnyElement, App, AppContext, Context, Entity,

View File

@@ -1,6 +1,6 @@
use account::Account;
use async_utility::task::spawn;
use common::utils::nip96_upload;
use common::nip96_upload;
use global::{constants::IMAGE_SERVICE, get_client};
use gpui::{
div, img, prelude::FluentBuilder, px, relative, AnyElement, App, AppContext, Context, Entity,

View File

@@ -1,5 +1,5 @@
use async_utility::task::spawn;
use common::utils::nip96_upload;
use common::nip96_upload;
use global::{constants::IMAGE_SERVICE, get_client};
use gpui::{
div, img, prelude::FluentBuilder, AnyElement, App, AppContext, Context, Entity, EventEmitter,

View File

@@ -1,8 +1,9 @@
use anyhow::Error;
use chats::{
room::{Room, RoomKind},
ChatRegistry,
};
use common::{profile::NostrProfile, utils::random_name};
use common::{profile::SharedProfile, random_name};
use global::get_client;
use gpui::{
div, img, impl_internal_actions, prelude::FluentBuilder, px, relative, uniform_list, App,
@@ -14,7 +15,11 @@ use nostr_sdk::prelude::*;
use serde::Deserialize;
use smallvec::{smallvec, SmallVec};
use smol::Timer;
use std::{collections::HashSet, rc::Rc, time::Duration};
use std::{
collections::{BTreeSet, HashSet},
rc::Rc,
time::Duration,
};
use ui::{
button::{Button, ButtonRounded},
input::{InputEvent, TextInput},
@@ -33,7 +38,7 @@ impl_internal_actions!(contacts, [SelectContact]);
pub struct Compose {
title_input: Entity<TextInput>,
user_input: Entity<TextInput>,
contacts: Entity<Vec<NostrProfile>>,
contacts: Entity<Vec<Profile>>,
selected: Entity<HashSet<PublicKey>>,
focus_handle: FocusHandle,
is_loading: bool,
@@ -80,26 +85,17 @@ impl Compose {
},
));
let client = get_client();
let (tx, rx) = oneshot::channel::<Vec<NostrProfile>>();
cx.background_spawn(async move {
let signer = client.signer().await.unwrap();
let public_key = signer.get_public_key().await.unwrap();
if let Ok(profiles) = client.database().contacts(public_key).await {
let members: Vec<NostrProfile> = profiles
.into_iter()
.map(|profile| NostrProfile::new(profile.public_key(), profile.metadata()))
.collect();
_ = tx.send(members);
}
})
.detach();
cx.spawn(async move |this, cx| {
if let Ok(contacts) = rx.await {
let task: Task<Result<BTreeSet<Profile>, Error>> = cx.background_spawn(async move {
let client = get_client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let profiles = client.database().contacts(public_key).await?;
Ok(profiles)
});
if let Ok(contacts) = task.await {
cx.update(|cx| {
this.update(cx, |this, cx| {
this.contacts.update(cx, |this, cx| {
@@ -107,6 +103,7 @@ impl Compose {
cx.notify();
});
})
.ok()
})
.ok();
}
@@ -174,7 +171,7 @@ impl Compose {
.ok();
let chats = ChatRegistry::global(cx);
let room = Room::new(&event, RoomKind::Ongoing);
let room = Room::new(&event).kind(RoomKind::Ongoing);
chats.update(cx, |chats, cx| {
match chats.push(room, cx) {
@@ -215,7 +212,7 @@ impl Compose {
// Show loading spinner
self.set_loading(true, cx);
let task: Task<Result<NostrProfile, anyhow::Error>> = if content.contains("@") {
let task: Task<Result<Profile, anyhow::Error>> = if content.contains("@") {
cx.background_spawn(async move {
let profile = nip05::profile(&content, None).await?;
let public_key = profile.public_key;
@@ -225,7 +222,7 @@ impl Compose {
.await?
.unwrap_or_default();
Ok(NostrProfile::new(public_key, metadata))
Ok(Profile::new(public_key, metadata))
})
} else {
let Ok(public_key) = PublicKey::parse(&content) else {
@@ -240,7 +237,7 @@ impl Compose {
.await?
.unwrap_or_default();
Ok(NostrProfile::new(public_key, metadata))
Ok(Profile::new(public_key, metadata))
})
};
@@ -249,7 +246,7 @@ impl Compose {
Ok(profile) => {
cx.update(|window, cx| {
this.update(cx, |this, cx| {
let public_key = profile.public_key;
let public_key = profile.public_key();
this.contacts.update(cx, |this, cx| {
this.insert(0, profile);
@@ -441,7 +438,7 @@ impl Render for Compose {
for ix in range {
let item = contacts.get(ix).unwrap().clone();
let is_select = selected.contains(&item.public_key);
let is_select = selected.contains(&item.public_key());
items.push(
div()
@@ -458,12 +455,10 @@ impl Render for Compose {
.items_center()
.gap_2()
.text_xs()
.child(
div().flex_shrink_0().child(
img(item.avatar).size_6(),
),
)
.child(item.name),
.child(div().flex_shrink_0().child(
img(item.shared_avatar()).size_6(),
))
.child(item.shared_name()),
)
.when(is_select, |this| {
this.child(
@@ -484,7 +479,7 @@ impl Render for Compose {
.on_click(move |_, window, cx| {
window.dispatch_action(
Box::new(SelectContact(
item.public_key,
item.public_key(),
)),
cx,
);

View File

@@ -138,31 +138,18 @@ impl Sidebar {
for room in rooms {
let room = room.read(cx);
let room_id = room.id;
let ago = room.last_seen().ago();
let Some(member) = room.first_member() else {
continue;
};
let id = room.id;
let ago = room.ago();
let label = room.display_name(cx);
let img = room.display_image(cx).map(img);
let label = if room.is_group() {
room.subject().unwrap_or("Unnamed".into())
} else {
member.name.clone()
};
let img = if !room.is_group() {
Some(img(member.avatar.clone()))
} else {
None
};
let item = FolderItem::new(room_id as usize)
let item = FolderItem::new(id as usize)
.label(label)
.description(ago)
.img(img)
.on_click({
cx.listener(move |this, _, window, cx| {
this.open_room(room_id, window, cx);
this.open_room(id, window, cx);
})
});