@@ -45,3 +45,39 @@ impl EventUtils for Event {
|
|||||||
a == b
|
a == b
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl EventUtils for UnsignedEvent {
|
||||||
|
fn uniq_id(&self) -> u64 {
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
let mut pubkeys: Vec<PublicKey> = vec![];
|
||||||
|
|
||||||
|
// Add all public keys from event
|
||||||
|
pubkeys.push(self.pubkey);
|
||||||
|
pubkeys.extend(self.tags.public_keys().collect::<Vec<_>>());
|
||||||
|
|
||||||
|
// Generate unique hash
|
||||||
|
pubkeys
|
||||||
|
.into_iter()
|
||||||
|
.unique()
|
||||||
|
.sorted()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.hash(&mut hasher);
|
||||||
|
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn all_pubkeys(&self) -> Vec<PublicKey> {
|
||||||
|
let mut public_keys: Vec<PublicKey> = self.tags.public_keys().copied().collect();
|
||||||
|
public_keys.push(self.pubkey);
|
||||||
|
|
||||||
|
public_keys
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compare_pubkeys(&self, other: &[PublicKey]) -> bool {
|
||||||
|
let pubkeys = self.all_pubkeys();
|
||||||
|
let a: HashSet<_> = pubkeys.iter().collect();
|
||||||
|
let b: HashSet<_> = other.iter().collect();
|
||||||
|
|
||||||
|
a == b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,17 +5,17 @@ use anyhow::{anyhow, Error};
|
|||||||
use common::display::{ReadableProfile, TextUtils};
|
use common::display::{ReadableProfile, TextUtils};
|
||||||
use common::nip05::nip05_profile;
|
use common::nip05::nip05_profile;
|
||||||
use global::constants::BOOTSTRAP_RELAYS;
|
use global::constants::BOOTSTRAP_RELAYS;
|
||||||
use global::nostr_client;
|
use global::{css, nostr_client};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, relative, rems, uniform_list, AppContext, Context, Entity, InteractiveElement,
|
div, px, relative, rems, uniform_list, App, AppContext, Context, Entity, InteractiveElement,
|
||||||
IntoElement, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled,
|
IntoElement, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled,
|
||||||
Subscription, Task, Window,
|
Subscription, Task, Window,
|
||||||
};
|
};
|
||||||
use i18n::t;
|
use gpui_tokio::Tokio;
|
||||||
use itertools::Itertools;
|
use i18n::{shared_t, t};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use registry::room::{Room, RoomKind};
|
use registry::room::Room;
|
||||||
use registry::Registry;
|
use registry::Registry;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
@@ -24,6 +24,7 @@ use theme::ActiveTheme;
|
|||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::input::{InputEvent, InputState, TextInput};
|
use ui::input::{InputEvent, InputState, TextInput};
|
||||||
|
use ui::modal::ModalButtonProps;
|
||||||
use ui::notification::Notification;
|
use ui::notification::Notification;
|
||||||
use ui::{h_flex, v_flex, ContextModal, Disableable, Icon, IconName, Sizable, StyledExt};
|
use ui::{h_flex, v_flex, ContextModal, Disableable, Icon, IconName, Sizable, StyledExt};
|
||||||
|
|
||||||
@@ -37,19 +38,43 @@ pub fn compose_button() -> impl IntoElement {
|
|||||||
.rounded()
|
.rounded()
|
||||||
.on_click(move |_, window, cx| {
|
.on_click(move |_, window, cx| {
|
||||||
let compose = cx.new(|cx| Compose::new(window, cx));
|
let compose = cx.new(|cx| Compose::new(window, cx));
|
||||||
let title = SharedString::new(t!("sidebar.direct_messages"));
|
let weak_view = compose.downgrade();
|
||||||
|
|
||||||
window.open_modal(cx, move |modal, _window, _cx| {
|
window.open_modal(cx, move |modal, _window, cx| {
|
||||||
modal.title(title.clone()).child(compose.clone())
|
let weak_view = weak_view.clone();
|
||||||
|
let label = if compose.read(cx).selected(cx).len() > 1 {
|
||||||
|
shared_t!("compose.create_group_dm_button")
|
||||||
|
} else {
|
||||||
|
shared_t!("compose.create_dm_button")
|
||||||
|
};
|
||||||
|
|
||||||
|
modal
|
||||||
|
.alert()
|
||||||
|
.overlay_closable(true)
|
||||||
|
.keyboard(true)
|
||||||
|
.show_close(true)
|
||||||
|
.button_props(ModalButtonProps::default().ok_text(label))
|
||||||
|
.title(shared_t!("sidebar.direct_messages"))
|
||||||
|
.child(compose.clone())
|
||||||
|
.on_ok(move |_, window, cx| {
|
||||||
|
weak_view
|
||||||
|
.update(cx, |this, cx| {
|
||||||
|
this.submit(window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
// false to prevent the modal from closing
|
||||||
|
false
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
struct Contact {
|
struct Contact {
|
||||||
public_key: PublicKey,
|
public_key: PublicKey,
|
||||||
select: bool,
|
selected: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsRef<PublicKey> for Contact {
|
impl AsRef<PublicKey> for Contact {
|
||||||
@@ -62,12 +87,12 @@ impl Contact {
|
|||||||
pub fn new(public_key: PublicKey) -> Self {
|
pub fn new(public_key: PublicKey) -> Self {
|
||||||
Self {
|
Self {
|
||||||
public_key,
|
public_key,
|
||||||
select: false,
|
selected: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select(mut self) -> Self {
|
pub fn selected(mut self) -> Self {
|
||||||
self.select = true;
|
self.selected = true;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,188 +100,198 @@ impl Contact {
|
|||||||
pub struct Compose {
|
pub struct Compose {
|
||||||
/// Input for the room's subject
|
/// Input for the room's subject
|
||||||
title_input: Entity<InputState>,
|
title_input: Entity<InputState>,
|
||||||
|
|
||||||
/// Input for the room's members
|
/// Input for the room's members
|
||||||
user_input: Entity<InputState>,
|
user_input: Entity<InputState>,
|
||||||
/// The current user's contacts
|
|
||||||
contacts: Vec<Entity<Contact>>,
|
/// User's contacts
|
||||||
/// Input error message
|
contacts: Entity<Vec<Contact>>,
|
||||||
|
|
||||||
|
/// Error message
|
||||||
error_message: Entity<Option<SharedString>>,
|
error_message: Entity<Option<SharedString>>,
|
||||||
adding: bool,
|
|
||||||
submitting: bool,
|
_subscriptions: SmallVec<[Subscription; 1]>,
|
||||||
#[allow(dead_code)]
|
_tasks: SmallVec<[Task<()>; 1]>,
|
||||||
subscriptions: SmallVec<[Subscription; 1]>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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(|_| vec![]);
|
||||||
|
let error_message = cx.new(|_| None);
|
||||||
|
|
||||||
let user_input =
|
let user_input =
|
||||||
cx.new(|cx| InputState::new(window, cx).placeholder(t!("compose.placeholder_npub")));
|
cx.new(|cx| InputState::new(window, cx).placeholder("npub or nprofile..."));
|
||||||
|
|
||||||
let title_input =
|
let title_input =
|
||||||
cx.new(|cx| InputState::new(window, cx).placeholder(t!("compose.placeholder_title")));
|
cx.new(|cx| InputState::new(window, cx).placeholder("Family...(Optional)"));
|
||||||
|
|
||||||
let error_message = cx.new(|_| None);
|
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
|
let mut tasks = smallvec![];
|
||||||
// Handle Enter event for user input
|
|
||||||
subscriptions.push(cx.subscribe_in(
|
|
||||||
&user_input,
|
|
||||||
window,
|
|
||||||
move |this, _input, event, window, cx| {
|
|
||||||
if let InputEvent::PressEnter { .. } = event {
|
|
||||||
this.add_and_select_contact(window, cx)
|
|
||||||
};
|
|
||||||
},
|
|
||||||
));
|
|
||||||
|
|
||||||
let get_contacts: Task<Result<Vec<Contact>, Error>> = cx.background_spawn(async move {
|
let get_contacts: Task<Result<Vec<Contact>, Error>> = cx.background_spawn(async move {
|
||||||
let client = nostr_client();
|
let client = nostr_client();
|
||||||
let signer = client.signer().await?;
|
let signer = client.signer().await?;
|
||||||
let public_key = signer.get_public_key().await?;
|
let public_key = signer.get_public_key().await?;
|
||||||
let profiles = client.database().contacts(public_key).await?;
|
let profiles = client.database().contacts(public_key).await?;
|
||||||
let contacts = profiles
|
let contacts: Vec<Contact> = profiles
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|profile| Contact::new(profile.public_key()))
|
.map(|profile| Contact::new(profile.public_key()))
|
||||||
.collect_vec();
|
.collect();
|
||||||
|
|
||||||
Ok(contacts)
|
Ok(contacts)
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
tasks.push(
|
||||||
match get_contacts.await {
|
// Load all contacts
|
||||||
Ok(contacts) => {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
this.update(cx, |this, cx| {
|
match get_contacts.await {
|
||||||
this.extend_contacts(contacts, cx);
|
Ok(contacts) => {
|
||||||
})
|
this.update(cx, |this, cx| {
|
||||||
.ok();
|
this.extend_contacts(contacts, cx);
|
||||||
}
|
})
|
||||||
Err(e) => {
|
.ok();
|
||||||
cx.update(|window, cx| {
|
}
|
||||||
window.push_notification(Notification::error(e.to_string()), cx);
|
Err(e) => {
|
||||||
})
|
cx.update(|window, cx| {
|
||||||
.ok();
|
window.push_notification(Notification::error(e.to_string()), cx);
|
||||||
}
|
})
|
||||||
};
|
.ok();
|
||||||
})
|
}
|
||||||
.detach();
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
subscriptions.push(
|
||||||
|
// Handle Enter event for user input
|
||||||
|
cx.subscribe_in(
|
||||||
|
&user_input,
|
||||||
|
window,
|
||||||
|
move |this, _input, event, window, cx| {
|
||||||
|
if let InputEvent::PressEnter { .. } = event {
|
||||||
|
this.add_and_select_contact(window, cx)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
adding: false,
|
|
||||||
submitting: false,
|
|
||||||
contacts: vec![],
|
|
||||||
title_input,
|
title_input,
|
||||||
user_input,
|
user_input,
|
||||||
error_message,
|
error_message,
|
||||||
subscriptions,
|
contacts,
|
||||||
|
_subscriptions: subscriptions,
|
||||||
|
_tasks: tasks,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn request_metadata(client: &Client, public_key: PublicKey) -> Result<(), Error> {
|
async fn request_metadata(public_key: PublicKey) -> Result<(), Error> {
|
||||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
let client = nostr_client();
|
||||||
|
let css = css();
|
||||||
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList];
|
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList];
|
||||||
let filter = Filter::new().author(public_key).kinds(kinds).limit(10);
|
let filter = Filter::new().author(public_key).kinds(kinds).limit(10);
|
||||||
|
|
||||||
client
|
client
|
||||||
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
|
.subscribe_to(BOOTSTRAP_RELAYS, filter, css.auto_close_opts)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let public_keys: Vec<PublicKey> = self.selected(cx);
|
|
||||||
|
|
||||||
if public_keys.is_empty() {
|
|
||||||
self.set_error(Some(t!("compose.receiver_required").into()), cx);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Show loading spinner
|
|
||||||
self.set_submitting(true, cx);
|
|
||||||
|
|
||||||
// Convert selected pubkeys into Nostr tags
|
|
||||||
let mut tag_list: Vec<Tag> = public_keys.iter().map(|pk| Tag::public_key(*pk)).collect();
|
|
||||||
|
|
||||||
// Add subject if it is present
|
|
||||||
if !self.title_input.read(cx).value().is_empty() {
|
|
||||||
tag_list.push(Tag::custom(
|
|
||||||
TagKind::Subject,
|
|
||||||
vec![self.title_input.read(cx).value().to_string()],
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let event: Task<Result<Room, Error>> = cx.background_spawn(async move {
|
|
||||||
let signer = nostr_client().signer().await?;
|
|
||||||
let public_key = signer.get_public_key().await?;
|
|
||||||
|
|
||||||
let room = EventBuilder::private_msg_rumor(public_keys[0], "")
|
|
||||||
.tags(Tags::from_list(tag_list))
|
|
||||||
.build(public_key)
|
|
||||||
.sign(&Keys::generate())
|
|
||||||
.await
|
|
||||||
.map(|event| Room::new(&event).kind(RoomKind::Ongoing))?;
|
|
||||||
|
|
||||||
Ok(room)
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
match event.await {
|
|
||||||
Ok(room) => {
|
|
||||||
cx.update(|window, cx| {
|
|
||||||
let registry = Registry::global(cx);
|
|
||||||
// Reset local state
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_submitting(false, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
// Create and insert the new room into the registry
|
|
||||||
registry.update(cx, |this, cx| {
|
|
||||||
this.push_room(cx.new(|_| room), cx);
|
|
||||||
});
|
|
||||||
// Close the current modal
|
|
||||||
window.close_modal(cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_error(Some(e.to_string().into()), cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extend_contacts<I>(&mut self, contacts: I, cx: &mut Context<Self>)
|
fn extend_contacts<I>(&mut self, contacts: I, cx: &mut Context<Self>)
|
||||||
where
|
where
|
||||||
I: IntoIterator<Item = Contact>,
|
I: IntoIterator<Item = Contact>,
|
||||||
{
|
{
|
||||||
self.contacts
|
self.contacts.update(cx, |this, cx| {
|
||||||
.extend(contacts.into_iter().map(|contact| cx.new(|_| contact)));
|
this.extend(contacts);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_contact(&mut self, contact: Contact, cx: &mut Context<Self>) {
|
fn push_contact(&mut self, contact: Contact, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
if !self
|
let pk = contact.public_key;
|
||||||
.contacts
|
|
||||||
.iter()
|
if !self.contacts.read(cx).iter().any(|c| c.public_key == pk) {
|
||||||
.any(|e| e.read(cx).public_key == contact.public_key)
|
self._tasks.push(cx.background_spawn(async move {
|
||||||
{
|
Self::request_metadata(pk).await.ok();
|
||||||
self.contacts.insert(0, cx.new(|_| contact));
|
}));
|
||||||
cx.notify();
|
|
||||||
|
cx.defer_in(window, |this, window, cx| {
|
||||||
|
this.contacts.update(cx, |this, cx| {
|
||||||
|
this.insert(0, contact);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
this.user_input.update(cx, |this, cx| {
|
||||||
|
this.set_value("", window, cx);
|
||||||
|
this.set_loading(false, cx);
|
||||||
|
});
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
self.set_error(Some(t!("compose.contact_existed").into()), cx);
|
self.set_error(Some(t!("compose.contact_existed").into()), cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn selected(&self, cx: &Context<Self>) -> Vec<PublicKey> {
|
fn select_contact(&mut self, public_key: PublicKey, cx: &mut Context<Self>) {
|
||||||
|
self.contacts.update(cx, |this, cx| {
|
||||||
|
if let Some(contact) = this.iter_mut().find(|c| c.public_key == public_key) {
|
||||||
|
contact.selected = true;
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_and_select_contact(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let content = self.user_input.read(cx).value().to_string();
|
||||||
|
|
||||||
|
// Show loading indicator in the input
|
||||||
|
self.user_input.update(cx, |this, cx| {
|
||||||
|
this.set_loading(true, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Ok(public_key) = content.to_public_key() {
|
||||||
|
let contact = Contact::new(public_key).selected();
|
||||||
|
self.push_contact(contact, window, cx);
|
||||||
|
} else if content.contains("@") {
|
||||||
|
let task = Tokio::spawn(cx, async move {
|
||||||
|
if let Ok(profile) = nip05_profile(&content).await {
|
||||||
|
let public_key = profile.public_key;
|
||||||
|
let contact = Contact::new(public_key).selected();
|
||||||
|
|
||||||
|
Ok(contact)
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Not found"))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
match task.await {
|
||||||
|
Ok(Ok(contact)) => {
|
||||||
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
this.push_contact(contact, window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_error(Some(e.to_string().into()), cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Tokio error: {e}");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected(&self, cx: &App) -> Vec<PublicKey> {
|
||||||
self.contacts
|
self.contacts
|
||||||
|
.read(cx)
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|contact| {
|
.filter_map(|contact| {
|
||||||
if contact.read(cx).select {
|
if contact.selected {
|
||||||
Some(contact.read(cx).public_key)
|
Some(contact.public_key)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -264,84 +299,49 @@ impl Compose {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_and_select_contact(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let content = self.user_input.read(cx).value().to_string();
|
let registry = Registry::global(cx);
|
||||||
|
let public_keys: Vec<PublicKey> = self.selected(cx);
|
||||||
|
|
||||||
// Prevent multiple requests
|
if !self.user_input.read(cx).value().is_empty() {
|
||||||
self.set_adding(true, cx);
|
self.add_and_select_contact(window, cx);
|
||||||
|
|
||||||
// Show loading indicator in the input
|
|
||||||
self.user_input.update(cx, |this, cx| {
|
|
||||||
this.set_loading(true, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
let task: Task<Result<Contact, Error>> = if content.contains("@") {
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
let (tx, rx) = oneshot::channel::<Option<Nip05Profile>>();
|
|
||||||
|
|
||||||
nostr_sdk::async_utility::task::spawn(async move {
|
|
||||||
let profile = nip05_profile(&content).await.ok();
|
|
||||||
tx.send(profile).ok();
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Ok(Some(profile)) = rx.await {
|
|
||||||
let client = nostr_client();
|
|
||||||
let public_key = profile.public_key;
|
|
||||||
let contact = Contact::new(public_key).select();
|
|
||||||
|
|
||||||
Self::request_metadata(client, public_key).await?;
|
|
||||||
|
|
||||||
Ok(contact)
|
|
||||||
} else {
|
|
||||||
Err(anyhow!(t!("common.not_found")))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else if let Ok(public_key) = content.to_public_key() {
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
let client = nostr_client();
|
|
||||||
let contact = Contact::new(public_key).select();
|
|
||||||
|
|
||||||
Self::request_metadata(client, public_key).await?;
|
|
||||||
|
|
||||||
Ok(contact)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
self.set_error(Some(t!("common.pubkey_invalid").into()), cx);
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
if public_keys.is_empty() {
|
||||||
match task.await {
|
self.set_error(Some(t!("compose.receiver_required").into()), cx);
|
||||||
Ok(contact) => {
|
return;
|
||||||
cx.update(|window, cx| {
|
};
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.push_contact(contact, cx);
|
// Convert selected pubkeys into Nostr tags
|
||||||
this.set_adding(false, cx);
|
let mut tags: Tags = Tags::from_list(
|
||||||
this.user_input.update(cx, |this, cx| {
|
public_keys
|
||||||
this.set_value("", window, cx);
|
.iter()
|
||||||
this.set_loading(false, cx);
|
.map(|pubkey| Tag::public_key(pubkey.to_owned()))
|
||||||
});
|
.collect(),
|
||||||
})
|
);
|
||||||
.ok();
|
|
||||||
})
|
// Add subject if it is present
|
||||||
.ok();
|
if !self.title_input.read(cx).value().is_empty() {
|
||||||
}
|
tags.push(Tag::custom(
|
||||||
Err(e) => {
|
TagKind::Subject,
|
||||||
this.update(cx, |this, cx| {
|
vec![self.title_input.read(cx).value().to_string()],
|
||||||
this.set_error(Some(e.to_string().into()), cx);
|
));
|
||||||
})
|
}
|
||||||
.ok();
|
|
||||||
}
|
// Create a new room
|
||||||
};
|
let room = Room::new(public_keys[0], tags, cx);
|
||||||
})
|
|
||||||
.detach();
|
// Insert the new room into the registry
|
||||||
|
registry.update(cx, |this, cx| {
|
||||||
|
this.push_room(cx.new(|_| room), cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close the current modal
|
||||||
|
window.close_modal(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_error(&mut self, error: impl Into<Option<SharedString>>, cx: &mut Context<Self>) {
|
fn set_error(&mut self, error: impl Into<Option<SharedString>>, cx: &mut Context<Self>) {
|
||||||
if self.adding {
|
|
||||||
self.set_adding(false, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unlock the user input
|
// Unlock the user input
|
||||||
self.user_input.update(cx, |this, cx| {
|
self.user_input.update(cx, |this, cx| {
|
||||||
this.set_loading(false, cx);
|
this.set_loading(false, cx);
|
||||||
@@ -364,48 +364,35 @@ impl Compose {
|
|||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_adding(&mut self, status: bool, cx: &mut Context<Self>) {
|
|
||||||
self.adding = status;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_submitting(&mut self, status: bool, cx: &mut Context<Self>) {
|
|
||||||
self.submitting = status;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn list_items(&self, range: Range<usize>, cx: &Context<Self>) -> Vec<impl IntoElement> {
|
fn list_items(&self, range: Range<usize>, cx: &Context<Self>) -> Vec<impl IntoElement> {
|
||||||
let proxy = AppSettings::get_proxy_user_avatars(cx);
|
let proxy = AppSettings::get_proxy_user_avatars(cx);
|
||||||
let registry = Registry::read_global(cx);
|
let registry = Registry::read_global(cx);
|
||||||
let mut items = Vec::with_capacity(self.contacts.len());
|
let mut items = Vec::with_capacity(self.contacts.read(cx).len());
|
||||||
|
|
||||||
for ix in range {
|
for ix in range {
|
||||||
let Some(entity) = self.contacts.get(ix).cloned() else {
|
let Some(contact) = self.contacts.read(cx).get(ix) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let public_key = entity.read(cx).as_ref();
|
let public_key = contact.public_key;
|
||||||
let profile = registry.get_person(public_key, cx);
|
let profile = registry.get_person(&public_key, cx);
|
||||||
let selected = entity.read(cx).select;
|
|
||||||
|
|
||||||
items.push(
|
items.push(
|
||||||
h_flex()
|
h_flex()
|
||||||
.id(ix)
|
.id(ix)
|
||||||
.px_1()
|
.px_2()
|
||||||
.h_9()
|
.h_11()
|
||||||
.w_full()
|
.w_full()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.rounded(cx.theme().radius)
|
.rounded(cx.theme().radius)
|
||||||
.child(
|
.child(
|
||||||
div()
|
h_flex()
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.gap_1p5()
|
.gap_1p5()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.child(Avatar::new(profile.avatar_url(proxy)).size(rems(1.75)))
|
.child(Avatar::new(profile.avatar_url(proxy)).size(rems(1.75)))
|
||||||
.child(profile.display_name()),
|
.child(profile.display_name()),
|
||||||
)
|
)
|
||||||
.when(selected, |this| {
|
.when(contact.selected, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
Icon::new(IconName::CheckCircleFill)
|
Icon::new(IconName::CheckCircleFill)
|
||||||
.small()
|
.small()
|
||||||
@@ -413,11 +400,8 @@ impl Compose {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.hover(|this| this.bg(cx.theme().elevated_surface_background))
|
.hover(|this| this.bg(cx.theme().elevated_surface_background))
|
||||||
.on_click(cx.listener(move |_this, _event, _window, cx| {
|
.on_click(cx.listener(move |this, _, _window, cx| {
|
||||||
entity.update(cx, |this, cx| {
|
this.select_contact(public_key, cx);
|
||||||
this.select = !this.select;
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -428,24 +412,17 @@ impl Compose {
|
|||||||
|
|
||||||
impl Render for Compose {
|
impl Render for Compose {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let label = if self.submitting {
|
|
||||||
t!("compose.creating_dm_button")
|
|
||||||
} else if self.selected(cx).len() > 1 {
|
|
||||||
t!("compose.create_group_dm_button")
|
|
||||||
} else {
|
|
||||||
t!("compose.create_dm_button")
|
|
||||||
};
|
|
||||||
|
|
||||||
let error = self.error_message.read(cx).as_ref();
|
let error = self.error_message.read(cx).as_ref();
|
||||||
|
let loading = self.user_input.read(cx).loading(cx);
|
||||||
|
let contacts = self.contacts.read(cx);
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.mb_4()
|
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(SharedString::new(t!("compose.description"))),
|
.child(shared_t!("compose.description")),
|
||||||
)
|
)
|
||||||
.when_some(error, |this, msg| {
|
.when_some(error, |this, msg| {
|
||||||
this.child(
|
this.child(
|
||||||
@@ -466,13 +443,13 @@ impl Render for Compose {
|
|||||||
div()
|
div()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.child(SharedString::new(t!("compose.subject_label"))),
|
.child(shared_t!("compose.subject_label")),
|
||||||
)
|
)
|
||||||
.child(TextInput::new(&self.title_input).small().appearance(false)),
|
.child(TextInput::new(&self.title_input).small().appearance(false)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.my_1()
|
.pt_1()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
@@ -481,22 +458,18 @@ impl Render for Compose {
|
|||||||
div()
|
div()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.child(SharedString::new(t!("compose.to_label"))),
|
.child(shared_t!("compose.to_label")),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
TextInput::new(&self.user_input)
|
||||||
.gap_1()
|
.small()
|
||||||
.child(
|
.disabled(loading)
|
||||||
TextInput::new(&self.user_input)
|
.suffix(
|
||||||
.small()
|
|
||||||
.disabled(self.adding),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("add")
|
Button::new("add")
|
||||||
.icon(IconName::PlusCircleFill)
|
.icon(IconName::PlusCircleFill)
|
||||||
.ghost()
|
.transparent()
|
||||||
.loading(self.adding)
|
.small()
|
||||||
.disabled(self.adding)
|
.disabled(loading)
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
this.add_and_select_contact(window, cx);
|
this.add_and_select_contact(window, cx);
|
||||||
})),
|
})),
|
||||||
@@ -504,7 +477,7 @@ impl Render for Compose {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
if self.contacts.is_empty() {
|
if contacts.is_empty() {
|
||||||
this.child(
|
this.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.h_24()
|
.h_24()
|
||||||
@@ -512,48 +485,32 @@ impl Render for Compose {
|
|||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.text_center()
|
.text_center()
|
||||||
|
.text_xs()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_xs()
|
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.line_height(relative(1.2))
|
.line_height(relative(1.2))
|
||||||
.child(SharedString::new(t!(
|
.child(shared_t!("compose.no_contacts_message")),
|
||||||
"compose.no_contacts_message"
|
|
||||||
))),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().text_xs().text_color(cx.theme().text_muted).child(
|
div()
|
||||||
SharedString::new(t!(
|
.text_color(cx.theme().text_muted)
|
||||||
"compose.no_contacts_description"
|
.child(shared_t!("compose.no_contacts_description")),
|
||||||
)),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
this.child(
|
this.child(
|
||||||
uniform_list(
|
uniform_list(
|
||||||
"contacts",
|
"contacts",
|
||||||
self.contacts.len(),
|
contacts.len(),
|
||||||
cx.processor(move |this, range, _window, cx| {
|
cx.processor(move |this, range, _window, cx| {
|
||||||
this.list_items(range, cx)
|
this.list_items(range, cx)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.min_h(px(300.)),
|
.h(px(300.)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
|
||||||
Button::new("create_dm_btn")
|
|
||||||
.label(label)
|
|
||||||
.primary()
|
|
||||||
.small()
|
|
||||||
.w_full()
|
|
||||||
.loading(self.submitting)
|
|
||||||
.disabled(self.submitting || self.adding)
|
|
||||||
.on_click(cx.listener(move |this, _event, window, cx| {
|
|
||||||
this.submit(window, cx);
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ impl Sidebar {
|
|||||||
Self::request_metadata(client, public_key).await?;
|
Self::request_metadata(client, public_key).await?;
|
||||||
|
|
||||||
// Create a temporary room
|
// Create a temporary room
|
||||||
let room = Room::new(&event).rearrange_by(identity);
|
let room = Room::from(&event).current_user(identity);
|
||||||
|
|
||||||
Ok(room)
|
Ok(room)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ impl Registry {
|
|||||||
let is_ongoing = client.database().count(filter).await.unwrap_or(1) >= 1;
|
let is_ongoing = client.database().count(filter).await.unwrap_or(1) >= 1;
|
||||||
|
|
||||||
// Create a new room
|
// Create a new room
|
||||||
let room = Room::new(&event).rearrange_by(public_key);
|
let room = Room::from(&event).current_user(public_key);
|
||||||
|
|
||||||
if is_ongoing || bypassed {
|
if is_ongoing || bypassed {
|
||||||
rooms.insert(room.kind(RoomKind::Ongoing));
|
rooms.insert(room.kind(RoomKind::Ongoing));
|
||||||
@@ -458,9 +458,7 @@ impl Registry {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let room = Room::new(&event)
|
let room = Room::from(&event).current_user(identity);
|
||||||
.kind(RoomKind::default())
|
|
||||||
.rearrange_by(identity);
|
|
||||||
|
|
||||||
// Push the new room to the front of the list
|
// Push the new room to the front of the list
|
||||||
self.add_room(cx.new(|_| room), cx);
|
self.add_room(cx.new(|_| room), cx);
|
||||||
|
|||||||
@@ -124,13 +124,13 @@ impl Eq for Room {}
|
|||||||
|
|
||||||
impl EventEmitter<RoomSignal> for Room {}
|
impl EventEmitter<RoomSignal> for Room {}
|
||||||
|
|
||||||
impl Room {
|
impl From<&Event> for Room {
|
||||||
pub fn new(event: &Event) -> Self {
|
fn from(val: &Event) -> Self {
|
||||||
let id = event.uniq_id();
|
let id = val.uniq_id();
|
||||||
let created_at = event.created_at;
|
let created_at = val.created_at;
|
||||||
|
|
||||||
// Get the members from the event's tags and event's pubkey
|
// Get the members from the event's tags and event's pubkey
|
||||||
let members = event
|
let members = val
|
||||||
.all_pubkeys()
|
.all_pubkeys()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.unique()
|
.unique()
|
||||||
@@ -138,20 +138,20 @@ impl Room {
|
|||||||
.collect_vec();
|
.collect_vec();
|
||||||
|
|
||||||
// Get the subject from the event's tags
|
// Get the subject from the event's tags
|
||||||
let subject = if let Some(tag) = event.tags.find(TagKind::Subject) {
|
let subject = if let Some(tag) = val.tags.find(TagKind::Subject) {
|
||||||
tag.content().map(|s| s.to_owned())
|
tag.content().map(|s| s.to_owned())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the picture from the event's tags
|
// Get the picture from the event's tags
|
||||||
let picture = if let Some(tag) = event.tags.find(TagKind::custom("picture")) {
|
let picture = if let Some(tag) = val.tags.find(TagKind::custom("picture")) {
|
||||||
tag.content().map(|s| s.to_owned())
|
tag.content().map(|s| s.to_owned())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
Self {
|
Room {
|
||||||
id,
|
id,
|
||||||
created_at,
|
created_at,
|
||||||
subject,
|
subject,
|
||||||
@@ -160,47 +160,82 @@ impl Room {
|
|||||||
kind: RoomKind::default(),
|
kind: RoomKind::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets the kind of the room and returns the modified room
|
impl From<&UnsignedEvent> for Room {
|
||||||
///
|
fn from(val: &UnsignedEvent) -> Self {
|
||||||
/// This is a builder-style method that allows chaining room modifications.
|
let id = val.uniq_id();
|
||||||
///
|
let created_at = val.created_at;
|
||||||
/// # Arguments
|
|
||||||
///
|
// Get the members from the event's tags and event's pubkey
|
||||||
/// * `kind` - The RoomKind to set for this room
|
let members = val
|
||||||
///
|
.all_pubkeys()
|
||||||
/// # Returns
|
.into_iter()
|
||||||
///
|
.unique()
|
||||||
/// The modified Room instance with the new kind
|
.sorted()
|
||||||
pub fn kind(mut self, kind: RoomKind) -> Self {
|
.collect_vec();
|
||||||
self.kind = kind;
|
|
||||||
self
|
// Get the subject from the event's tags
|
||||||
|
let subject = if let Some(tag) = val.tags.find(TagKind::Subject) {
|
||||||
|
tag.content().map(|s| s.to_owned())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the picture from the event's tags
|
||||||
|
let picture = if let Some(tag) = val.tags.find(TagKind::custom("picture")) {
|
||||||
|
tag.content().map(|s| s.to_owned())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Room {
|
||||||
|
id,
|
||||||
|
created_at,
|
||||||
|
subject,
|
||||||
|
picture,
|
||||||
|
members,
|
||||||
|
kind: RoomKind::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Room {
|
||||||
|
/// Constructs a new room instance with a given receiver.
|
||||||
|
pub fn new(receiver: PublicKey, tags: Tags, cx: &App) -> Self {
|
||||||
|
let identity = Registry::read_global(cx).identity(cx);
|
||||||
|
|
||||||
|
let mut event = EventBuilder::private_msg_rumor(receiver, "")
|
||||||
|
.tags(tags)
|
||||||
|
.build(identity.public_key());
|
||||||
|
|
||||||
|
// Ensure event ID is generated
|
||||||
|
event.ensure_id();
|
||||||
|
|
||||||
|
Room::from(&event).current_user(identity.public_key())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the rearrange_by field of the room and returns the modified room
|
/// Constructs a new room instance from an nostr event.
|
||||||
///
|
pub fn from(event: impl Into<Room>) -> Self {
|
||||||
/// This is a builder-style method that allows chaining room modifications.
|
event.into()
|
||||||
///
|
}
|
||||||
/// # Arguments
|
|
||||||
///
|
/// Call this function to ensure the current user is always at the bottom of the members list
|
||||||
/// * `rearrange_by` - The PublicKey to set for rearranging the member list
|
pub fn current_user(mut self, public_key: PublicKey) -> Self {
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// The modified Room instance with the new member list after rearrangement
|
|
||||||
pub fn rearrange_by(mut self, rearrange_by: PublicKey) -> Self {
|
|
||||||
let (not_match, matches): (Vec<PublicKey>, Vec<PublicKey>) =
|
let (not_match, matches): (Vec<PublicKey>, Vec<PublicKey>) =
|
||||||
self.members.iter().partition(|&key| key != &rearrange_by);
|
self.members.iter().partition(|&key| key != &public_key);
|
||||||
self.members = not_match;
|
self.members = not_match;
|
||||||
self.members.extend(matches);
|
self.members.extend(matches);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the kind of the room and returns the modified room
|
||||||
|
pub fn kind(mut self, kind: RoomKind) -> Self {
|
||||||
|
self.kind = kind;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the room kind to ongoing
|
/// Set the room kind to ongoing
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `cx` - The context to notify about the update
|
|
||||||
pub fn set_ongoing(&mut self, cx: &mut Context<Self>) {
|
pub fn set_ongoing(&mut self, cx: &mut Context<Self>) {
|
||||||
if self.kind != RoomKind::Ongoing {
|
if self.kind != RoomKind::Ongoing {
|
||||||
self.kind = RoomKind::Ongoing;
|
self.kind = RoomKind::Ongoing;
|
||||||
@@ -209,59 +244,29 @@ impl Room {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if the room is a group chat
|
/// Checks if the room is a group chat
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// true if the room has more than 2 members, false otherwise
|
|
||||||
pub fn is_group(&self) -> bool {
|
pub fn is_group(&self) -> bool {
|
||||||
self.members.len() > 2
|
self.members.len() > 2
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the creation timestamp of the room
|
/// Updates the creation timestamp of the room
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `created_at` - The new Timestamp to set
|
|
||||||
/// * `cx` - The context to notify about the update
|
|
||||||
pub fn created_at(&mut self, created_at: impl Into<Timestamp>, cx: &mut Context<Self>) {
|
pub fn created_at(&mut self, created_at: impl Into<Timestamp>, cx: &mut Context<Self>) {
|
||||||
self.created_at = created_at.into();
|
self.created_at = created_at.into();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the subject of the room
|
/// Updates the subject of the room
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `subject` - The new subject to set
|
|
||||||
/// * `cx` - The context to notify about the update
|
|
||||||
pub fn subject(&mut self, subject: String, cx: &mut Context<Self>) {
|
pub fn subject(&mut self, subject: String, cx: &mut Context<Self>) {
|
||||||
self.subject = Some(subject);
|
self.subject = Some(subject);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the picture of the room
|
/// Updates the picture of the room
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `picture` - The new subject to set
|
|
||||||
/// * `cx` - The context to notify about the update
|
|
||||||
pub fn picture(&mut self, picture: String, cx: &mut Context<Self>) {
|
pub fn picture(&mut self, picture: String, cx: &mut Context<Self>) {
|
||||||
self.picture = Some(picture);
|
self.picture = Some(picture);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the display name for the room
|
/// Gets the display name for the room
|
||||||
///
|
|
||||||
/// If the room has a subject set, that will be used as the display name.
|
|
||||||
/// Otherwise, it will generate a name based on the room members.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `cx` - The application context
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// A string containing the display name
|
|
||||||
pub fn display_name(&self, cx: &App) -> String {
|
pub fn display_name(&self, cx: &App) -> String {
|
||||||
if let Some(subject) = self.subject.clone() {
|
if let Some(subject) = self.subject.clone() {
|
||||||
subject
|
subject
|
||||||
@@ -271,20 +276,6 @@ impl Room {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the display image for the room
|
/// Gets the display image for the room
|
||||||
///
|
|
||||||
/// The image is determined by:
|
|
||||||
/// - The room's picture if set
|
|
||||||
/// - The first member's avatar for 1:1 chats
|
|
||||||
/// - A default group image for group chats
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `proxy` - Whether to use the proxy for the avatar URL
|
|
||||||
/// * `cx` - The application context
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// A string containing the image path or URL
|
|
||||||
pub fn display_image(&self, proxy: bool, cx: &App) -> String {
|
pub fn display_image(&self, proxy: bool, cx: &App) -> String {
|
||||||
if let Some(picture) = self.picture.as_ref() {
|
if let Some(picture) = self.picture.as_ref() {
|
||||||
picture.clone()
|
picture.clone()
|
||||||
@@ -420,6 +411,7 @@ impl Room {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut event = builder.tags(tags).build(receiver);
|
let mut event = builder.tags(tags).build(receiver);
|
||||||
|
|
||||||
// Ensure event ID is set
|
// Ensure event ID is set
|
||||||
event.ensure_id();
|
event.ensure_id();
|
||||||
|
|
||||||
|
|||||||
@@ -781,6 +781,11 @@ impl InputState {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the loading state of the input field.
|
||||||
|
pub fn loading(&self, _cx: &App) -> bool {
|
||||||
|
self.loading
|
||||||
|
}
|
||||||
|
|
||||||
/// Set true to show indicator at the input right.
|
/// Set true to show indicator at the input right.
|
||||||
pub fn set_loading(&mut self, loading: bool, cx: &mut Context<Self>) {
|
pub fn set_loading(&mut self, loading: bool, cx: &mut Context<Self>) {
|
||||||
self.loading = loading;
|
self.loading = loading;
|
||||||
|
|||||||
@@ -315,10 +315,6 @@ preferences:
|
|||||||
en: "Display"
|
en: "Display"
|
||||||
|
|
||||||
compose:
|
compose:
|
||||||
placeholder_npub:
|
|
||||||
en: "npub or nprofile..."
|
|
||||||
placeholder_title:
|
|
||||||
en: "Family...(Optional)"
|
|
||||||
create_dm_button:
|
create_dm_button:
|
||||||
en: "Create DM"
|
en: "Create DM"
|
||||||
creating_dm_button:
|
creating_dm_button:
|
||||||
|
|||||||
Reference in New Issue
Block a user