feat: improve compose modal

This commit is contained in:
2025-02-09 14:29:34 +07:00
parent 0daebe5762
commit 600af900e9
14 changed files with 661 additions and 524 deletions

View File

@@ -326,20 +326,26 @@ fn main() {
while let Some(signal) = signal_rx.recv().await { while let Some(signal) = signal_rx.recv().await {
match signal { match signal {
Signal::Eose => { Signal::Eose => {
cx.update_window(*window.deref(), |_this, _window, cx| { if let Err(e) =
cx.update_global::<ChatRegistry, _>(|this, cx| { cx.update_window(*window.deref(), |_this, window, cx| {
this.load(cx); cx.update_global::<ChatRegistry, _>(|this, cx| {
}); this.load(window, cx);
}) });
.unwrap(); })
{
error!("Error: {}", e)
}
} }
Signal::Event(event) => { Signal::Event(event) => {
cx.update_window(*window.deref(), |_this, _window, cx| { if let Err(e) =
cx.update_global::<ChatRegistry, _>(|this, cx| { cx.update_window(*window.deref(), |_this, window, cx| {
this.new_room_message(event, cx); cx.update_global::<ChatRegistry, _>(|this, cx| {
}); this.new_room_message(event, window, cx);
}) });
.unwrap(); })
{
error!("Error: {}", e)
}
} }
} }
} }

View File

@@ -191,7 +191,8 @@ impl AppView {
PanelKind::Room(id) => { PanelKind::Room(id) => {
if let Some(weak_room) = cx.global::<ChatRegistry>().get_room(id, cx) { if let Some(weak_room) = cx.global::<ChatRegistry>().get_room(id, cx) {
if let Some(room) = weak_room.upgrade() { if let Some(room) = weak_room.upgrade() {
let panel = Arc::new(chat::init(room, window, cx)); let panel = Arc::new(chat::init(&room, window, cx));
self.dock.update(cx, |dock_area, cx| { self.dock.update(cx, |dock_area, cx| {
dock_area.add_panel(panel, action.position, window, cx); dock_area.add_panel(panel, action.position, window, cx);
}); });

View File

@@ -10,7 +10,7 @@ use ui::{
#[derive(Clone, Debug, IntoElement)] #[derive(Clone, Debug, IntoElement)]
pub struct Message { pub struct Message {
member: NostrProfile, profile: NostrProfile,
content: SharedString, content: SharedString,
ago: SharedString, ago: SharedString,
} }
@@ -18,7 +18,7 @@ pub struct Message {
impl PartialEq for Message { impl PartialEq for Message {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
let content = self.content == other.content; let content = self.content == other.content;
let member = self.member == other.member; let member = self.profile == other.profile;
let ago = self.ago == other.ago; let ago = self.ago == other.ago;
content && member && ago content && member && ago
@@ -26,9 +26,9 @@ impl PartialEq for Message {
} }
impl Message { impl Message {
pub fn new(member: NostrProfile, content: SharedString, ago: SharedString) -> Self { pub fn new(profile: NostrProfile, content: SharedString, ago: SharedString) -> Self {
Self { Self {
member, profile,
content, content,
ago, ago,
} }
@@ -58,7 +58,7 @@ impl RenderOnce for Message {
}), }),
) )
.child( .child(
img(self.member.avatar()) img(self.profile.avatar())
.size_8() .size_8()
.rounded_full() .rounded_full()
.flex_shrink_0(), .flex_shrink_0(),
@@ -75,7 +75,7 @@ impl RenderOnce for Message {
.items_baseline() .items_baseline()
.gap_2() .gap_2()
.text_xs() .text_xs()
.child(div().font_semibold().child(self.member.name())) .child(div().font_semibold().child(self.profile.name()))
.child( .child(
div() div()
.child(self.ago) .child(self.ago)

View File

@@ -2,13 +2,14 @@ use async_utility::task::spawn;
use chat_state::room::Room; use chat_state::room::Room;
use common::{ use common::{
constants::IMAGE_SERVICE, constants::IMAGE_SERVICE,
profile::NostrProfile,
utils::{compare, message_time, nip96_upload}, utils::{compare, message_time, nip96_upload},
}; };
use gpui::{ use gpui::{
div, img, list, prelude::FluentBuilder, px, white, AnyElement, App, AppContext, Context, div, img, list, prelude::FluentBuilder, px, white, AnyElement, App, AppContext, Context,
Entity, EventEmitter, Flatten, FocusHandle, Focusable, InteractiveElement, IntoElement, Entity, EventEmitter, Flatten, FocusHandle, Focusable, InteractiveElement, IntoElement,
ListAlignment, ListState, ObjectFit, ParentElement, PathPromptOptions, Pixels, Render, ListAlignment, ListState, ObjectFit, ParentElement, PathPromptOptions, Pixels, Render,
SharedString, StatefulInteractiveElement, Styled, StyledImage, WeakEntity, Window, SharedString, StatefulInteractiveElement, Styled, StyledImage, Window,
}; };
use itertools::Itertools; use itertools::Itertools;
use message::Message; use message::Message;
@@ -27,7 +28,7 @@ use ui::{
mod message; mod message;
pub fn init(room: Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<Chat> { pub fn init(room: &Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<Chat> {
Chat::new(room, window, cx) Chat::new(room, window, cx)
} }
@@ -45,7 +46,8 @@ pub struct Chat {
// Chat Room // Chat Room
id: SharedString, id: SharedString,
name: SharedString, name: SharedString,
room: Entity<Room>, owner: NostrProfile,
members: Vec<NostrProfile>,
state: Entity<State>, state: Entity<State>,
list: ListState, list: ListState,
// New Message // New Message
@@ -56,12 +58,15 @@ pub struct Chat {
} }
impl Chat { impl Chat {
pub fn new(model: Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<Self> { pub fn new(model: &Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<Self> {
let room = model.read(cx); let room = model.read(cx);
let id = room.id.to_string().into(); let id = room.id.to_string().into();
let name = room.title.clone().unwrap_or("Untitled".into()); let name = room.title.clone().unwrap_or("Untitled".into());
let owner = room.owner.clone();
let members = room.members.clone();
cx.new(|cx| { cx.new(|cx| {
// Load all messages
cx.observe_new::<Self>(|this, window, cx| { cx.observe_new::<Self>(|this, window, cx| {
if let Some(window) = window { if let Some(window) = window {
this.load_messages(window, cx); this.load_messages(window, cx);
@@ -69,7 +74,13 @@ impl Chat {
}) })
.detach(); .detach();
// Form // Observe and load new messages
cx.observe_in(model, window, |this: &mut Chat, model, _, cx| {
this.load_new_messages(&model, cx);
})
.detach();
// New message form
let input = cx.new(|cx| { let input = cx.new(|cx| {
TextInput::new(window, cx) TextInput::new(window, cx)
.appearance(false) .appearance(false)
@@ -77,60 +88,56 @@ impl Chat {
.placeholder("Message...") .placeholder("Message...")
}); });
// List
let state = cx.new(|_| State {
count: 0,
items: vec![],
});
// Send message when user presses enter // Send message when user presses enter
cx.subscribe_in( cx.subscribe_in(
&input, &input,
window, window,
move |this: &mut Chat, view, input_event, window, cx| { move |this: &mut Chat, _, input_event, window, cx| {
if let InputEvent::PressEnter = input_event { if let InputEvent::PressEnter = input_event {
this.send_message(view.downgrade(), window, cx); this.send_message(window, cx);
} }
}, },
) )
.detach(); .detach();
// List state model
let state = cx.new(|_| State {
count: 0,
items: vec![],
});
// Update list on every state changes // Update list on every state changes
cx.observe(&state, |this, model, cx| { cx.observe(&state, |this, model, cx| {
let items = model.read(cx).items.clone();
this.list = ListState::new( this.list = ListState::new(
items.len(), model.read(cx).items.len(),
ListAlignment::Bottom, ListAlignment::Bottom,
Pixels(256.), Pixels(1024.),
move |idx, _window, _cx| { move |idx, _window, cx| {
let item = items.get(idx).unwrap().clone(); if let Some(message) = model.read(cx).items.get(idx) {
div().child(item).into_any_element() div().child(message.clone()).into_any_element()
} else {
div().into_any_element()
}
}, },
); );
cx.notify(); cx.notify();
}) })
.detach(); .detach();
cx.observe_in(&model, window, |this, model, window, cx| {
this.load_new_messages(model.downgrade(), window, cx);
})
.detach();
let attaches = cx.new(|_| None); let attaches = cx.new(|_| None);
Self { Self {
closable: true, closable: true,
zoomable: true, zoomable: true,
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
room: model, list: ListState::new(0, ListAlignment::Bottom, Pixels(1024.), move |_, _, _| {
list: ListState::new(0, ListAlignment::Bottom, Pixels(256.), move |_, _, _| {
div().into_any_element() div().into_any_element()
}), }),
is_uploading: false, is_uploading: false,
id, id,
name, name,
owner,
members,
input, input,
state, state,
attaches, attaches,
@@ -138,150 +145,139 @@ impl Chat {
}) })
} }
fn load_messages(&self, _window: &mut Window, cx: &mut Context<Self>) { fn load_messages(&self, window: &mut Window, cx: &mut Context<Self>) {
let room = self.room.read(cx); let window_handle = window.window_handle();
let members = room.members.clone(); // Get current user
let owner = room.owner.clone(); let author = self.owner.public_key();
// Get all public keys // Get other users in room
let all_keys = room.get_pubkeys(); let pubkeys = self
.members
.iter()
.map(|m| m.public_key())
.collect::<Vec<_>>();
// Get all public keys for comparisation
let mut all_keys = pubkeys.clone();
all_keys.push(author);
// Async cx.spawn(|this, mut cx| async move {
let async_state = self.state.clone(); let (tx, rx) = oneshot::channel::<Events>();
let mut async_cx = cx.to_async();
cx.foreground_executor() cx.background_spawn({
.spawn(async move { let client = get_client();
let events: anyhow::Result<Events, anyhow::Error> = async_cx
.background_executor()
.spawn({
let client = get_client();
let pubkeys = members.iter().map(|m| m.public_key()).collect::<Vec<_>>();
async move { let recv = Filter::new()
let signer = client.signer().await?; .kind(Kind::PrivateDirectMessage)
let author = signer.get_public_key().await?; .author(author)
.pubkeys(pubkeys.iter().copied());
let recv = Filter::new() let send = Filter::new()
.kind(Kind::PrivateDirectMessage) .kind(Kind::PrivateDirectMessage)
.author(author) .authors(pubkeys)
.pubkeys(pubkeys.clone()); .pubkey(author);
let send = Filter::new() // Get all DM events in database
.kind(Kind::PrivateDirectMessage) async move {
.authors(pubkeys) let recv_events = client.database().query(recv).await.unwrap();
.pubkey(author); let send_events = client.database().query(send).await.unwrap();
let events = recv_events.merge(send_events);
// Get all DM events in database _ = tx.send(events);
let recv_events = client.database().query(recv).await?;
let send_events = client.database().query(send).await?;
let events = recv_events.merge(send_events);
Ok(events)
}
})
.await;
if let Ok(events) = events {
let items: Vec<Message> = events
.into_iter()
.sorted_by_key(|ev| ev.created_at)
.filter_map(|ev| {
let mut pubkeys: Vec<_> = ev.tags.public_keys().copied().collect();
pubkeys.push(ev.pubkey);
if compare(&pubkeys, &all_keys) {
let member = if let Some(member) =
members.iter().find(|&m| m.public_key() == ev.pubkey)
{
member.to_owned()
} else {
owner.clone()
};
Some(Message::new(
member,
ev.content.into(),
message_time(ev.created_at).into(),
))
} else {
None
}
})
.collect();
let total = items.len();
_ = async_cx.update_entity(&async_state, |a, b| {
a.items = items;
a.count = total;
b.notify();
});
} }
}) })
.detach(); .detach();
if let Ok(events) = rx.await {
_ = cx.update_window(window_handle, |_, _, cx| {
_ = this.update(cx, |this, cx| {
let items: Vec<Message> = events
.into_iter()
.sorted_by_key(|ev| ev.created_at)
.filter_map(|ev| {
let mut pubkeys: Vec<_> = ev.tags.public_keys().copied().collect();
pubkeys.push(ev.pubkey);
if compare(&pubkeys, &all_keys) {
let member = if let Some(member) =
this.members.iter().find(|&m| m.public_key() == ev.pubkey)
{
member.to_owned()
} else {
this.owner.clone()
};
Some(Message::new(
member,
ev.content.into(),
message_time(ev.created_at).into(),
))
} else {
None
}
})
.collect();
cx.update_entity(&this.state, |this, cx| {
this.count = items.len();
this.items = items;
cx.notify();
});
});
});
}
})
.detach();
} }
fn load_new_messages( fn load_new_messages(&self, model: &Entity<Room>, cx: &mut Context<Self>) {
&self, let room = model.read(cx);
model: WeakEntity<Room>, let items: Vec<Message> = room
_window: &mut Window, .new_messages
cx: &mut Context<Self>, .iter()
) { .filter_map(|event| {
if let Some(model) = model.upgrade() { room.member(&event.pubkey).map(|member| {
let room = model.read(cx); Message::new(
let items: Vec<Message> = room member,
.new_messages event.content.clone().into(),
.iter() message_time(event.created_at).into(),
.filter_map(|event| { )
room.member(&event.pubkey).map(|member| { })
Message::new( })
member, .collect();
event.content.clone().into(),
message_time(event.created_at).into(), cx.update_entity(&self.state, |this, cx| {
) let messages: Vec<Message> = items
}) .into_iter()
.filter_map(|new| {
if !this.items.iter().any(|old| old == &new) {
Some(new)
} else {
None
}
}) })
.collect(); .collect();
cx.update_entity(&self.state, |model, cx| { this.items.extend(messages);
let messages: Vec<Message> = items this.count = this.items.len();
.into_iter() cx.notify();
.filter_map(|new| { });
if !model.items.iter().any(|old| old == &new) {
Some(new)
} else {
None
}
})
.collect();
model.items.extend(messages);
model.count = model.items.len();
cx.notify();
});
}
} }
fn send_message( fn send_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
&mut self,
view: WeakEntity<TextInput>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let window_handle = window.window_handle(); let window_handle = window.window_handle();
let room = self.room.read(cx);
let owner = room.owner.clone(); // Get current user
let mut members = room.members.to_vec(); let author = self.owner.public_key();
members.push(owner.clone());
// Get other users in room
let mut pubkeys = self
.members
.iter()
.map(|m| m.public_key())
.collect::<Vec<_>>();
pubkeys.push(author);
// Get message // Get message
let mut content = self.input.read(cx).text().to_string(); let mut content = self.input.read(cx).text().to_string();
if content.is_empty() {
window.push_notification("Message cannot be empty", cx);
return;
}
// Get all attaches and merge with message // Get all attaches and merge with message
if let Some(attaches) = self.attaches.read(cx).as_ref() { if let Some(attaches) = self.attaches.read(cx).as_ref() {
let merged = attaches let merged = attaches
@@ -293,75 +289,77 @@ impl Chat {
content = format!("{}\n{}", content, merged) content = format!("{}\n{}", content, merged)
} }
// Update input state if content.is_empty() {
if let Some(input) = view.upgrade() { window.push_notification("Cannot send an empty message", cx);
cx.update_entity(&input, |input, cx| { return;
input.set_loading(true, window, cx);
input.set_disabled(true, window, cx);
});
} }
cx.spawn(|this, mut cx| async move { // Disable input when sending message
// Send message to all members self.input.update(cx, |this, cx| {
cx.background_executor() this.set_loading(true, window, cx);
.spawn({ this.set_disabled(true, window, cx);
let client = get_client(); });
let content = content.clone().to_string();
let tags: Vec<Tag> = members
.iter()
.filter_map(|m| {
if m.public_key() != owner.public_key() {
Some(Tag::public_key(m.public_key()))
} else {
None
}
})
.collect();
async move { cx.spawn(|this, mut cx| async move {
// Send message to all members cx.background_spawn({
for member in members.iter() { let client = get_client();
_ = client let content = content.clone();
.send_private_msg(member.public_key(), &content, tags.clone()) let tags: Vec<Tag> = pubkeys
.await .iter()
.filter_map(|pubkey| {
if pubkey != &author {
Some(Tag::public_key(*pubkey))
} else {
None
}
})
.collect();
async move {
// Send message to all members
for pubkey in pubkeys.iter() {
if let Err(_e) = client
.send_private_msg(*pubkey, &content, tags.clone())
.await
{
// TODO: handle error
} }
} }
}) }
.detach(); })
.detach();
if let Some(view) = this.upgrade() { _ = cx.update_window(window_handle, |_, window, cx| {
_ = cx.update_entity(&view, |this, cx| { _ = this.update(cx, |this, cx| {
cx.update_entity(&this.state, |model, cx| { let message = Message::new(
let message = Message::new( this.owner.clone(),
owner, content.to_string().into(),
content.to_string().into(), message_time(Timestamp::now()).into(),
message_time(Timestamp::now()).into(), );
);
model.items.extend(vec![message]); // Update message list
model.count = model.items.len(); cx.update_entity(&this.state, |this, cx| {
this.items.extend(vec![message]);
this.count = this.items.len();
cx.notify(); cx.notify();
}); });
cx.notify();
});
}
if let Some(input) = view.upgrade() { // Reset message input
cx.update_window(window_handle, |_, window, cx| { cx.update_entity(&this.input, |this, cx| {
cx.update_entity(&input, |input, cx| { this.set_loading(false, window, cx);
input.set_loading(false, window, cx); this.set_disabled(false, window, cx);
input.set_disabled(false, window, cx); this.set_text("", window, cx);
input.set_text("", window, cx); cx.notify();
}); });
}) });
.unwrap() });
}
}) })
.detach(); .detach();
} }
fn upload(&mut self, _window: &mut Window, cx: &mut Context<Self>) { fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let attaches = self.attaches.clone(); let window_handle = window.window_handle();
let paths = cx.prompt_for_paths(PathPromptOptions { let paths = cx.prompt_for_paths(PathPromptOptions {
files: true, files: true,
directories: false, directories: false,
@@ -372,7 +370,7 @@ impl Chat {
self.set_loading(true, cx); self.set_loading(true, cx);
// TODO: support multiple upload // TODO: support multiple upload
cx.spawn(move |this, mut async_cx| async move { cx.spawn(move |this, mut cx| async move {
match Flatten::flatten(paths.await.map_err(|e| e.into())) { match Flatten::flatten(paths.await.map_err(|e| e.into())) {
Ok(Some(mut paths)) => { Ok(Some(mut paths)) => {
let path = paths.pop().unwrap(); let path = paths.pop().unwrap();
@@ -382,33 +380,39 @@ impl Chat {
spawn(async move { spawn(async move {
let client = get_client(); let client = get_client();
if let Ok(url) = nip96_upload(client, file_data).await { if let Ok(url) = nip96_upload(client, file_data).await {
_ = tx.send(url); _ = tx.send(url);
} }
}); });
if let Ok(url) = rx.await { if let Ok(url) = rx.await {
// Stop loading spinner _ = cx.update_window(window_handle, |_, _, cx| {
if let Some(view) = this.upgrade() { _ = this.update(cx, |this, cx| {
_ = async_cx.update_entity(&view, |this, cx| { // Stop loading spinner
this.set_loading(false, cx); this.set_loading(false, cx);
});
}
// Update attaches model this.attaches.update(cx, |this, cx| {
_ = async_cx.update_entity(&attaches, |model, cx| { if let Some(model) = this.as_mut() {
if let Some(model) = model.as_mut() { model.push(url);
model.push(url); } else {
} else { *this = Some(vec![url]);
*model = Some(vec![url]); }
} cx.notify();
cx.notify(); });
});
}); });
} }
} }
} }
Ok(None) => {} Ok(None) => {
// Stop loading spinner
if let Some(view) = this.upgrade() {
cx.update_entity(&view, |this, cx| {
this.set_loading(false, cx);
})
.unwrap();
}
}
Err(_) => {} Err(_) => {}
} }
}) })
@@ -436,15 +440,8 @@ impl Panel for Chat {
self.id.clone() self.id.clone()
} }
fn panel_facepile(&self, cx: &App) -> Option<Vec<String>> { fn panel_facepile(&self, _cx: &App) -> Option<Vec<String>> {
Some( Some(self.members.iter().map(|member| member.avatar()).collect())
self.room
.read(cx)
.members
.iter()
.map(|member| member.avatar())
.collect(),
)
} }
fn title(&self, _cx: &App) -> AnyElement { fn title(&self, _cx: &App) -> AnyElement {
@@ -561,11 +558,7 @@ impl Render for Chat {
.rounded(ButtonRounded::Medium) .rounded(ButtonRounded::Medium)
.label("SEND") .label("SEND")
.on_click(cx.listener(|this, _, window, cx| { .on_click(cx.listener(|this, _, window, cx| {
this.send_message( this.send_message(window, cx)
this.input.downgrade(),
window,
cx,
)
})), })),
), ),
), ),

View File

@@ -1,14 +1,14 @@
use std::str::FromStr;
use async_utility::task::spawn; use async_utility::task::spawn;
use common::{constants::IMAGE_SERVICE, profile::NostrProfile, utils::nip96_upload}; use common::{constants::IMAGE_SERVICE, profile::NostrProfile, utils::nip96_upload};
use gpui::{ use gpui::{
div, img, AnyElement, App, AppContext, Context, Entity, EventEmitter, Flatten, FocusHandle, div, img, prelude::FluentBuilder, AnyElement, App, AppContext, Context, Entity, EventEmitter,
Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Styled, Window, Flatten, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render,
SharedString, Styled, Window,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use smol::fs; use smol::fs;
use state::get_client; use state::get_client;
use std::str::FromStr;
use tokio::sync::oneshot; use tokio::sync::oneshot;
use ui::{ use ui::{
button::{Button, ButtonVariants}, button::{Button, ButtonVariants},
@@ -112,34 +112,39 @@ impl Profile {
spawn(async move { spawn(async move {
let client = get_client(); let client = get_client();
if let Ok(url) = nip96_upload(client, file_data).await { if let Ok(url) = nip96_upload(client, file_data).await {
_ = tx.send(url); _ = tx.send(url);
} }
}); });
if let Ok(url) = rx.await { if let Ok(url) = rx.await {
// Stop loading spinner cx.update_window(window_handle, |_, window, cx| {
if let Some(view) = this.upgrade() { // Stop loading spinner
cx.update_entity(&view, |this, cx| { this.update(cx, |this, cx| {
this.set_loading(false, cx); this.set_loading(false, cx);
}) })
.unwrap(); .unwrap();
}
// Update avatar input // Set avatar input
if let Some(input) = avatar_input.upgrade() { avatar_input
cx.update_window(window_handle, |_, window, cx| { .update(cx, |this, cx| {
cx.update_entity(&input, |this, cx| {
this.set_text(url.to_string(), window, cx); this.set_text(url.to_string(), window, cx);
}); })
}) .unwrap();
.unwrap(); })
} .unwrap();
} }
} }
} }
Ok(None) => {} Ok(None) => {
// Stop loading spinner
if let Some(view) = this.upgrade() {
cx.update_entity(&view, |this, cx| {
this.set_loading(false, cx);
})
.unwrap();
}
}
Err(_) => {} Err(_) => {}
} }
}) })
@@ -189,18 +194,14 @@ impl Profile {
.detach(); .detach();
if rx.await.is_ok() { if rx.await.is_ok() {
if let Some(profile) = this.upgrade() { cx.update_window(window_handle, |_, window, cx| {
cx.update_window(window_handle, |_, window, cx| { this.update(cx, |this, cx| {
cx.update_entity(&profile, |this, cx| { this.set_submitting(false, cx);
this.set_submitting(false, cx); window.push_notification("Your profile has been updated successfully", cx);
window.push_notification(
"Your profile has been updated successfully",
cx,
);
})
}) })
.unwrap(); .unwrap()
} })
.unwrap();
} }
}) })
.detach(); .detach();
@@ -263,16 +264,29 @@ impl Render for Profile {
.gap_2() .gap_2()
.w_full() .w_full()
.h_24() .h_24()
.child( .map(|this| {
img(format!( let picture = self.avatar_input.read(cx).text();
"{}/?url={}&w=100&h=100&fit=cover&mask=circle&n=-1",
IMAGE_SERVICE, if picture.is_empty() {
self.avatar_input.read(cx).text() this.child(
)) img("brand/avatar.png")
.size_10() .size_10()
.rounded_full() .rounded_full()
.flex_shrink_0(), .flex_shrink_0(),
) )
} else {
this.child(
img(format!(
"{}/?url={}&w=100&h=100&fit=cover&mask=circle&n=-1",
IMAGE_SERVICE,
self.avatar_input.read(cx).text()
))
.size_10()
.rounded_full()
.flex_shrink_0(),
)
}
})
.child( .child(
div() div()
.flex() .flex()

View File

@@ -1,25 +1,25 @@
use app_state::registry::AppRegistry; use app_state::registry::AppRegistry;
use chat_state::room::Room; use chat_state::registry::ChatRegistry;
use common::{ use common::{
constants::FAKE_SIG,
profile::NostrProfile, profile::NostrProfile,
utils::{random_name, room_hash}, utils::{random_name, signer_public_key},
}; };
use gpui::{ use gpui::{
div, img, impl_internal_actions, prelude::FluentBuilder, px, relative, uniform_list, App, div, img, impl_internal_actions, prelude::FluentBuilder, px, relative, uniform_list, App,
AppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement, ParentElement, AppContext, BorrowAppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement,
Render, SharedString, StatefulInteractiveElement, Styled, TextAlign, Window, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, TextAlign, Window,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use serde::Deserialize; use serde::Deserialize;
use state::get_client; use state::get_client;
use std::{collections::HashSet, time::Duration}; use std::{collections::HashSet, str::FromStr, time::Duration};
use tokio::sync::oneshot; use tokio::sync::oneshot;
use ui::{ use ui::{
button::{Button, ButtonRounded}, button::{Button, ButtonRounded},
indicator::Indicator,
input::{InputEvent, TextInput}, input::{InputEvent, TextInput},
theme::{scale::ColorScaleStep, ActiveTheme}, theme::{scale::ColorScaleStep, ActiveTheme},
Icon, IconName, Sizable, Size, StyledExt, ContextModal, Icon, IconName, Sizable, Size, StyledExt,
}; };
#[derive(Clone, PartialEq, Eq, Deserialize)] #[derive(Clone, PartialEq, Eq, Deserialize)]
@@ -31,15 +31,16 @@ pub struct Compose {
title_input: Entity<TextInput>, title_input: Entity<TextInput>,
message_input: Entity<TextInput>, message_input: Entity<TextInput>,
user_input: Entity<TextInput>, user_input: Entity<TextInput>,
contacts: Entity<Option<Vec<NostrProfile>>>, contacts: Entity<Vec<NostrProfile>>,
selected: Entity<HashSet<PublicKey>>, selected: Entity<HashSet<PublicKey>>,
focus_handle: FocusHandle, focus_handle: FocusHandle,
is_loading: bool, is_loading: bool,
is_submitting: bool,
} }
impl Compose { impl Compose {
pub fn new(window: &mut Window, cx: &mut Context<'_, Self>) -> Self { pub fn new(window: &mut Window, cx: &mut Context<'_, Self>) -> Self {
let contacts = cx.new(|_| None); let contacts = cx.new(|_| Vec::with_capacity(200));
let selected = cx.new(|_| HashSet::new()); let selected = cx.new(|_| HashSet::new());
let user_input = cx.new(|cx| { let user_input = cx.new(|cx| {
@@ -64,9 +65,10 @@ impl Compose {
TextInput::new(window, cx) TextInput::new(window, cx)
.appearance(false) .appearance(false)
.text_size(Size::XSmall) .text_size(Size::XSmall)
.placeholder("Hello... (Optional)") .placeholder("Hello...")
}); });
// Handle Enter event for message input
cx.subscribe_in( cx.subscribe_in(
&user_input, &user_input,
window, window,
@@ -79,23 +81,22 @@ impl Compose {
.detach(); .detach();
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
let client = get_client();
let (tx, rx) = oneshot::channel::<Vec<NostrProfile>>(); let (tx, rx) = oneshot::channel::<Vec<NostrProfile>>();
cx.background_executor() cx.background_executor()
.spawn(async move { .spawn(async move {
let signer = client.signer().await.unwrap(); let client = get_client();
let public_key = signer.get_public_key().await.unwrap(); if let Ok(public_key) = signer_public_key(client).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();
if let Ok(profiles) = client.database().contacts(public_key).await { _ = tx.send(members);
let members: Vec<NostrProfile> = profiles }
.into_iter()
.map(|profile| {
NostrProfile::new(profile.public_key(), profile.metadata())
})
.collect();
_ = tx.send(members);
} }
}) })
.detach(); .detach();
@@ -104,7 +105,7 @@ impl Compose {
if let Some(view) = this.upgrade() { if let Some(view) = this.upgrade() {
_ = cx.update_entity(&view, |this, cx| { _ = cx.update_entity(&view, |this, cx| {
this.contacts.update(cx, |this, cx| { this.contacts.update(cx, |this, cx| {
*this = Some(contacts); this.extend(contacts);
cx.notify(); cx.notify();
}); });
cx.notify(); cx.notify();
@@ -121,43 +122,116 @@ impl Compose {
contacts, contacts,
selected, selected,
is_loading: false, is_loading: false,
is_submitting: false,
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
} }
} }
pub fn room(&self, _window: &Window, cx: &App) -> Option<Room> { pub fn compose(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if let Some(current_user) = cx.global::<AppRegistry>().user() { let selected = self.selected.read(cx).to_owned();
// Convert selected pubkeys into nostr tags let message = self.message_input.read(cx).text();
let tags: Vec<Tag> = self
.selected
.read(cx)
.iter()
.map(|pk| Tag::public_key(*pk))
.collect();
let tags = Tags::new(tags);
// Convert selected pubkeys into members if selected.is_empty() {
let members: Vec<NostrProfile> = self window.push_notification("You need to add at least 1 receiver", cx);
.selected return;
.read(cx)
.clone()
.into_iter()
.map(|pk| NostrProfile::new(pk, Metadata::new()))
.collect();
// Get room's id
let id = room_hash(&tags);
// Get room's owner (current user)
let owner = NostrProfile::new(current_user.public_key(), Metadata::new());
// Get room's title
let title = self.title_input.read(cx).text().to_string().into();
Some(Room::new(id, owner, members, Some(title), Timestamp::now()))
} else {
None
} }
if message.is_empty() {
window.push_notification("Message is required", cx);
return;
}
let current_user = if let Some(profile) = cx.global::<AppRegistry>().user() {
profile
} else {
return;
};
// Show loading spinner
self.set_submitting(true, cx);
// Get nostr client
let client = get_client();
// Get message from user's input
let content = message.to_string();
// Get room title from user's input
let title = Tag::title(self.title_input.read(cx).text().to_string());
// Get all pubkeys
let current_user = current_user.public_key();
let mut pubkeys: Vec<PublicKey> = selected.iter().copied().collect();
pubkeys.push(current_user);
// Convert selected pubkeys into Nostr tags
let mut tag_list: Vec<Tag> = selected.iter().map(|pk| Tag::public_key(*pk)).collect();
tag_list.push(title);
let tags = Tags::new(tag_list);
let window_handle = window.window_handle();
cx.spawn(|this, mut cx| async move {
let (tx, rx) = oneshot::channel::<Event>();
cx.background_spawn(async move {
let mut event: Option<Event> = None;
for pubkey in pubkeys.iter() {
if let Ok(output) = client
.send_private_msg(*pubkey, &content, tags.clone())
.await
{
if pubkey == &current_user && event.is_none() {
if let Ok(Some(ev)) = client.database().event_by_id(&output.val).await {
if let Ok(UnwrappedGift { mut rumor, .. }) =
client.unwrap_gift_wrap(&ev).await
{
// Compute event id if not exist
rumor.ensure_id();
if let Some(id) = rumor.id {
let ev = Event::new(
id,
rumor.pubkey,
rumor.created_at,
rumor.kind,
rumor.tags,
rumor.content,
Signature::from_str(FAKE_SIG).unwrap(),
);
event = Some(ev);
}
}
}
}
}
}
if let Some(event) = event {
_ = tx.send(event);
}
})
.detach();
if let Ok(event) = rx.await {
_ = cx.update_window(window_handle, |_, window, cx| {
cx.update_global::<ChatRegistry, _>(|this, cx| {
this.new_room_message(event, window, cx);
});
// Stop loading spinner
_ = this.update(cx, |this, cx| {
this.set_submitting(false, cx);
});
// Close modal
window.close_modal(cx);
});
}
})
.detach();
} }
pub fn label(&self, _window: &Window, cx: &App) -> SharedString { pub fn label(&self, _window: &Window, cx: &App) -> SharedString {
@@ -168,35 +242,47 @@ impl Compose {
} }
} }
pub fn is_submitting(&self) -> bool {
self.is_submitting
}
fn add(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn add(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let window_handle = window.window_handle(); let window_handle = window.window_handle();
let content = self.user_input.read(cx).text().to_string(); let content = self.user_input.read(cx).text().to_string();
let input = self.user_input.downgrade();
// Show loading spinner // Show loading spinner
self.set_loading(true, cx); self.set_loading(true, cx);
if let Ok(public_key) = PublicKey::parse(&content) { if let Ok(public_key) = PublicKey::parse(&content) {
cx.spawn(|this, mut async_cx| async move { if self
let query: anyhow::Result<Metadata, anyhow::Error> = async_cx .contacts
.background_executor() .read(cx)
.spawn(async move { .iter()
let client = get_client(); .any(|c| c.public_key() == public_key)
let metadata = client {
.fetch_metadata(public_key, Duration::from_secs(3)) self.set_loading(false, cx);
.await?; return;
};
Ok(metadata) cx.spawn(|this, mut cx| async move {
}) let (tx, rx) = oneshot::channel::<Metadata>();
.await;
if let Ok(metadata) = query { cx.background_spawn(async move {
if let Some(view) = this.upgrade() { let client = get_client();
_ = async_cx.update_entity(&view, |this, cx| { let metadata = (client
.fetch_metadata(public_key, Duration::from_secs(3))
.await)
.unwrap_or_default();
_ = tx.send(metadata);
})
.detach();
if let Ok(metadata) = rx.await {
_ = cx.update_window(window_handle, |_, window, cx| {
_ = this.update(cx, |this, cx| {
this.contacts.update(cx, |this, cx| { this.contacts.update(cx, |this, cx| {
if let Some(members) = this { this.insert(0, NostrProfile::new(public_key, metadata));
members.insert(0, NostrProfile::new(public_key, metadata));
}
cx.notify(); cx.notify();
}); });
@@ -205,22 +291,22 @@ impl Compose {
cx.notify(); cx.notify();
}); });
// Stop loading indicator
this.set_loading(false, cx); this.set_loading(false, cx);
});
}
if let Some(input) = input.upgrade() { // Clear input
_ = async_cx.update_window(window_handle, |_, window, cx| { this.user_input.update(cx, |this, cx| {
cx.update_entity(&input, |this, cx| {
this.set_text("", window, cx); this.set_text("", window, cx);
}) cx.notify();
});
}); });
} });
} }
}) })
.detach(); .detach();
} else { } else {
// Handle error self.set_loading(false, cx);
window.push_notification("Public Key is not valid", cx);
} }
} }
@@ -229,6 +315,11 @@ impl Compose {
cx.notify(); cx.notify();
} }
fn set_submitting(&mut self, status: bool, cx: &mut Context<Self>) {
self.is_submitting = status;
cx.notify();
}
fn on_action_select( fn on_action_select(
&mut self, &mut self,
action: &SelectContact, action: &SelectContact,
@@ -318,114 +409,103 @@ impl Render for Compose {
.child(self.user_input.clone()), .child(self.user_input.clone()),
) )
.map(|this| { .map(|this| {
if let Some(contacts) = self.contacts.read(cx).clone() { let contacts = self.contacts.read(cx).clone();
let view = cx.entity(); let view = cx.entity();
let total = contacts.len();
if total != 0 { if contacts.is_empty() {
this.child( this.child(
div() div()
.w_full() .w_full()
.h_24() .h_24()
.flex() .flex()
.flex_col() .flex_col()
.items_center() .items_center()
.justify_center() .justify_center()
.text_align(TextAlign::Center) .text_align(TextAlign::Center)
.child( .child(
div() div()
.text_xs() .text_xs()
.font_semibold() .font_semibold()
.line_height(relative(1.2)) .line_height(relative(1.2))
.child("No contacts"), .child("No contacts"),
)
.child(
div()
.text_xs()
.text_color(
cx.theme()
.base
.step(cx, ColorScaleStep::ELEVEN),
)
.child("Your recently contacts will appear here."),
),
)
} else {
this.child(
uniform_list(
view,
"contacts",
total,
move |this, range, _window, cx| {
let selected = this.selected.read(cx);
let mut items = Vec::new();
for ix in range {
let item = contacts.get(ix).unwrap().clone();
let is_select =
selected.contains(&item.public_key());
items.push(
div()
.id(ix)
.w_full()
.h_9()
.px_2()
.flex()
.items_center()
.justify_between()
.child(
div()
.flex()
.items_center()
.gap_2()
.text_xs()
.child(div().flex_shrink_0().child(
img(item.avatar()).size_6(),
))
.child(item.name()),
)
.when(is_select, |this| {
this.child(
Icon::new(IconName::CircleCheck)
.size_3()
.text_color(
cx.theme().base.step(
cx,
ColorScaleStep::TWELVE,
),
),
)
})
.hover(|this| {
this.bg(cx
.theme()
.base
.step(cx, ColorScaleStep::THREE))
})
.on_click(move |_, window, cx| {
window.dispatch_action(
Box::new(SelectContact(
item.public_key(),
)),
cx,
);
}),
);
}
items
},
) )
.min_h(px(300.)), .child(
) div()
} .text_xs()
.text_color(
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
)
.child("Your recently contacts will appear here."),
),
)
} else { } else {
this.flex() this.child(
.items_center() uniform_list(
.justify_center() view,
.h_16() "contacts",
.child(Indicator::new().small()) contacts.len(),
move |this, range, _window, cx| {
let selected = this.selected.read(cx);
let mut items = Vec::new();
for ix in range {
let item = contacts.get(ix).unwrap().clone();
let is_select = selected.contains(&item.public_key());
items.push(
div()
.id(ix)
.w_full()
.h_9()
.px_2()
.flex()
.items_center()
.justify_between()
.child(
div()
.flex()
.items_center()
.gap_2()
.text_xs()
.child(
div().flex_shrink_0().child(
img(item.avatar()).size_6(),
),
)
.child(item.name()),
)
.when(is_select, |this| {
this.child(
Icon::new(IconName::CircleCheck)
.size_3()
.text_color(cx.theme().base.step(
cx,
ColorScaleStep::TWELVE,
)),
)
})
.hover(|this| {
this.bg(cx
.theme()
.base
.step(cx, ColorScaleStep::THREE))
})
.on_click(move |_, window, cx| {
window.dispatch_action(
Box::new(SelectContact(
item.public_key(),
)),
cx,
);
}),
);
}
items
},
)
.min_h(px(250.)),
)
} }
}), }),
) )

View File

@@ -89,7 +89,7 @@ impl Inbox {
.text_xs() .text_xs()
.rounded(px(cx.theme().radius)) .rounded(px(cx.theme().radius))
.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::FOUR))) .hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::FOUR)))
.child(div().font_medium().map(|this| { .child(div().flex_1().truncate().font_medium().map(|this| {
if room.is_group { if room.is_group {
this.flex() this.flex()
.items_center() .items_center()
@@ -113,6 +113,7 @@ impl Inbox {
})) }))
.child( .child(
div() div()
.flex_shrink_0()
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)) .text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
.child(ago), .child(ago),
) )

View File

@@ -1,9 +1,8 @@
use crate::views::sidebar::inbox::Inbox; use crate::views::sidebar::inbox::Inbox;
use chat_state::registry::ChatRegistry;
use compose::Compose; use compose::Compose;
use gpui::{ use gpui::{
div, px, AnyElement, App, AppContext, BorrowAppContext, Context, Entity, EventEmitter, div, px, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
StatefulInteractiveElement, Styled, Window, StatefulInteractiveElement, Styled, Window,
}; };
use ui::{ use ui::{
@@ -11,7 +10,7 @@ use ui::{
dock_area::panel::{Panel, PanelEvent}, dock_area::panel::{Panel, PanelEvent},
popup_menu::PopupMenu, popup_menu::PopupMenu,
theme::{scale::ColorScaleStep, ActiveTheme}, theme::{scale::ColorScaleStep, ActiveTheme},
v_flex, ContextModal, Icon, IconName, Sizable, StyledExt, v_flex, ContextModal, Disableable, Icon, IconName, Sizable, StyledExt,
}; };
mod compose; mod compose;
@@ -53,6 +52,7 @@ impl Sidebar {
window.open_modal(cx, move |modal, window, cx| { window.open_modal(cx, move |modal, window, cx| {
let label = compose.read(cx).label(window, cx); let label = compose.read(cx).label(window, cx);
let is_submitting = compose.read(cx).is_submitting();
modal modal
.title("Direct Messages") .title("Direct Messages")
@@ -70,14 +70,10 @@ impl Sidebar {
.bold() .bold()
.rounded(ButtonRounded::Large) .rounded(ButtonRounded::Large)
.w_full() .w_full()
.loading(is_submitting)
.disabled(is_submitting)
.on_click(window.listener_for(&compose, |this, _, window, cx| { .on_click(window.listener_for(&compose, |this, _, window, cx| {
if let Some(room) = this.room(window, cx) { this.compose(window, cx)
cx.update_global::<ChatRegistry, _>(|this, cx| {
this.new_room(room, cx);
});
window.close_modal(cx);
}
})), })),
), ),
) )

View File

@@ -1,6 +1,6 @@
use anyhow::Error; use anyhow::Error;
use common::utils::{compare, room_hash}; use common::utils::{compare, room_hash};
use gpui::{App, AppContext, Entity, Global, WeakEntity}; use gpui::{App, AppContext, Entity, Global, WeakEntity, Window};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use state::get_client; use state::get_client;
@@ -28,8 +28,11 @@ impl ChatRegistry {
let mut profiles = Vec::new(); let mut profiles = Vec::new();
for public_key in pubkeys.into_iter() { for public_key in pubkeys.into_iter() {
let query = client.database().metadata(public_key).await?; let metadata = client
let metadata = query.unwrap_or_default(); .database()
.metadata(public_key)
.await?
.unwrap_or_default();
profiles.push((public_key, metadata)); profiles.push((public_key, metadata));
} }
@@ -56,14 +59,16 @@ impl ChatRegistry {
cx.set_global(Self { inbox }); cx.set_global(Self { inbox });
} }
pub fn load(&mut self, cx: &mut App) { pub fn load(&mut self, window: &mut Window, cx: &mut App) {
let window_handle = window.window_handle();
self.inbox.update(cx, |this, cx| { self.inbox.update(cx, |this, cx| {
let task = this.load(cx.to_async()); let task = this.load(cx.to_async());
cx.spawn(|this, mut async_cx| async move { cx.spawn(|this, mut cx| async move {
if let Some(inbox) = this.upgrade() { if let Ok(events) = task.await {
if let Ok(events) = task.await { _ = cx.update_window(window_handle, |_, _, cx| {
_ = async_cx.update_entity(&inbox, |this, cx| { _ = this.update(cx, |this, cx| {
let current_rooms = this.get_room_ids(cx); let current_rooms = this.get_room_ids(cx);
let items: Vec<Entity<Room>> = events let items: Vec<Entity<Room>> = events
.into_iter() .into_iter()
@@ -83,7 +88,7 @@ impl ChatRegistry {
cx.notify(); cx.notify();
}); });
} });
} }
}) })
.detach(); .detach();
@@ -114,29 +119,42 @@ impl ChatRegistry {
}) })
} }
pub fn new_room_message(&mut self, event: Event, cx: &mut App) { pub fn new_room_message(&mut self, event: Event, window: &mut Window, cx: &mut App) {
let window_handle = window.window_handle();
// Get all pubkeys from event's tags for comparision
let mut pubkeys: Vec<_> = event.tags.public_keys().copied().collect(); let mut pubkeys: Vec<_> = event.tags.public_keys().copied().collect();
pubkeys.push(event.pubkey); pubkeys.push(event.pubkey);
self.inbox.update(cx, |this, cx| { if let Some(room) = self
if let Some(room) = this.rooms.iter().find(|room| { .inbox
let all_keys = room.read(cx).get_pubkeys(); .read(cx)
compare(&all_keys, &pubkeys) .rooms
}) { .iter()
room.update(cx, |this, cx| { .find(|room| compare(&room.read(cx).get_pubkeys(), &pubkeys))
this.new_messages.push(event); {
cx.notify(); let weak_room = room.downgrade();
})
} else {
let room = cx.new(|_| Room::parse(&event));
self.inbox.update(cx, |this, cx| { cx.spawn(|mut cx| async move {
this.rooms.insert(0, room); if let Err(e) = cx.update_window(window_handle, |_, _, cx| {
cx.notify(); _ = weak_room.update(cx, |this, cx| {
}) this.last_seen = event.created_at;
} this.new_messages.push(event);
cx.notify(); cx.notify();
}) });
}) {
println!("Error: {}", e)
}
})
.detach();
} else {
let room = cx.new(|_| Room::parse(&event));
self.inbox.update(cx, |this, cx| {
this.rooms.insert(0, room);
cx.notify();
});
}
} }
} }

View File

@@ -4,6 +4,7 @@ use common::{
}; };
use gpui::SharedString; use gpui::SharedString;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use std::collections::HashSet;
#[derive(Debug)] #[derive(Debug)]
pub struct Room { pub struct Room {
@@ -59,14 +60,19 @@ impl Room {
let id = room_hash(&event.tags); let id = room_hash(&event.tags);
let last_seen = event.created_at; let last_seen = event.created_at;
// Always equal to current user
let owner = NostrProfile::new(event.pubkey, Metadata::default()); let owner = NostrProfile::new(event.pubkey, Metadata::default());
// Get all pubkeys that invole in this group
let members: Vec<NostrProfile> = event let members: Vec<NostrProfile> = event
.tags .tags
.public_keys() .public_keys()
.copied() .collect::<HashSet<_>>()
.map(|public_key| NostrProfile::new(public_key, Metadata::default())) .into_iter()
.map(|public_key| NostrProfile::new(*public_key, Metadata::default()))
.collect(); .collect();
// Get title from event's tags
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_owned().into()) tag.content().map(|s| s.to_owned().into())
} else { } else {

View File

@@ -12,4 +12,4 @@ pub const ALL_MESSAGES_SUB_ID: &str = "listen_all_giftwraps";
pub const IMAGE_SERVICE: &str = "https://wsrv.nl"; pub const IMAGE_SERVICE: &str = "https://wsrv.nl";
/// NIP96 Media Server /// NIP96 Media Server
pub const NIP96_SERVER: &str = "https://nostrcheck.me"; pub const NIP96_SERVER: &str = "https://nostrmedia.com";

View File

@@ -7,6 +7,18 @@ pub struct NostrProfile {
metadata: Metadata, metadata: Metadata,
} }
impl AsRef<PublicKey> for NostrProfile {
fn as_ref(&self) -> &PublicKey {
&self.public_key
}
}
impl AsRef<Metadata> for NostrProfile {
fn as_ref(&self) -> &Metadata {
&self.metadata
}
}
impl PartialEq for NostrProfile { impl PartialEq for NostrProfile {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.public_key() == other.public_key() self.public_key() == other.public_key()

View File

@@ -1,5 +1,6 @@
use crate::constants::NIP96_SERVER; use crate::constants::NIP96_SERVER;
use chrono::{Datelike, Local, TimeZone}; use chrono::{Datelike, Local, TimeZone};
use itertools::Itertools;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use rnglib::{Language, RNG}; use rnglib::{Language, RNG};
use std::{ use std::{
@@ -7,6 +8,13 @@ use std::{
hash::{DefaultHasher, Hash, Hasher}, hash::{DefaultHasher, Hash, Hasher},
}; };
pub async fn signer_public_key(client: &Client) -> anyhow::Result<PublicKey, anyhow::Error> {
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
Ok(public_key)
}
pub async fn nip96_upload(client: &Client, file: Vec<u8>) -> anyhow::Result<Url, anyhow::Error> { pub async fn nip96_upload(client: &Client, file: Vec<u8>) -> anyhow::Result<Url, anyhow::Error> {
let signer = client.signer().await?; let signer = client.signer().await?;
let server_url = Url::parse(NIP96_SERVER)?; let server_url = Url::parse(NIP96_SERVER)?;
@@ -18,8 +26,9 @@ pub async fn nip96_upload(client: &Client, file: Vec<u8>) -> anyhow::Result<Url,
} }
pub fn room_hash(tags: &Tags) -> u64 { pub fn room_hash(tags: &Tags) -> u64 {
let pubkeys: Vec<PublicKey> = tags.public_keys().copied().collect(); let pubkeys: Vec<&PublicKey> = tags.public_keys().unique_by(|&pubkey| pubkey).collect();
let mut hasher = DefaultHasher::new(); let mut hasher = DefaultHasher::new();
// Generate unique hash // Generate unique hash
pubkeys.hash(&mut hasher); pubkeys.hash(&mut hasher);

View File

@@ -140,6 +140,7 @@ impl TextElement {
// cursor blink // cursor blink
let cursor_height = let cursor_height =
window.text_style().font_size.to_pixels(window.rem_size()) + px(2.); window.text_style().font_size.to_pixels(window.rem_size()) + px(2.);
cursor = Some(fill( cursor = Some(fill(
Bounds::new( Bounds::new(
point( point(
@@ -148,7 +149,7 @@ impl TextElement {
), ),
size(px(1.), cursor_height), size(px(1.), cursor_height),
), ),
cx.theme().accent.step(cx, ColorScaleStep::NINE), cx.theme().accent.step(cx, ColorScaleStep::TEN),
)) ))
}; };
} }