This commit is contained in:
2026-01-05 08:43:28 +07:00
parent 067f88dfa6
commit 23f8cc49c6
12 changed files with 142 additions and 264 deletions

View File

@@ -253,6 +253,11 @@ impl ChatRegistry {
}
}
/// Get the loading status of the chat registry
pub fn loading(&self) -> bool {
self.loading
}
/// Set the loading status of the chat registry
pub fn set_loading(&mut self, loading: bool, cx: &mut Context<Self>) {
self.loading = loading;
@@ -511,7 +516,7 @@ impl ChatRegistry {
}
// Set this room is ongoing if the new message is from current user
if author == nostr.read(cx).identity(cx).public_key() {
if author == nostr.read(cx).identity().read(cx).public_key() {
this.set_ongoing(cx);
}

View File

@@ -263,7 +263,7 @@ impl Room {
pub fn display_member(&self, cx: &App) -> Profile {
let persons = PersonRegistry::global(cx);
let nostr = NostrRegistry::global(cx);
let public_key = nostr.read(cx).identity(cx).public_key();
let public_key = nostr.read(cx).identity().read(cx).public_key();
let target_member = self
.members
@@ -369,7 +369,7 @@ impl Room {
let nostr = NostrRegistry::global(cx);
// Get current user
let public_key = nostr.read(cx).identity(cx).public_key();
let public_key = nostr.read(cx).identity().read(cx).public_key();
// Get room's subject
let subject = self.subject.clone();
@@ -438,7 +438,7 @@ impl Room {
let client = nostr.read(cx).client();
// Get current user's public key and relays
let current_user = nostr.read(cx).identity(cx).public_key();
let current_user = nostr.read(cx).identity().read(cx).public_key();
let current_user_relays = nostr.read(cx).messaging_relays(&current_user, cx);
let rumor = rumor.to_owned();

View File

@@ -2,7 +2,7 @@ use std::collections::HashSet;
use std::time::Duration;
pub use actions::*;
use chat::{Message, RenderedMessage, Room, RoomKind, RoomSignal, SendOptions, SendReport};
use chat::{Message, RenderedMessage, Room, RoomKind, RoomSignal, SendReport};
use common::{nip96_upload, RenderedProfile, RenderedTimestamp};
use gpui::prelude::FluentBuilder;
use gpui::{
@@ -59,7 +59,6 @@ pub struct ChatPanel {
// New Message
input: Entity<InputState>,
options: Entity<SendOptions>,
replies_to: Entity<HashSet<EventId>>,
// Media Attachment
@@ -87,7 +86,6 @@ impl ChatPanel {
let attachments = cx.new(|_| vec![]);
let replies_to = cx.new(|_| HashSet::new());
let options = cx.new(|_| SendOptions::default());
let id = room.read(cx).id.to_string().into();
let messages = BTreeSet::from([Message::system()]);
@@ -145,18 +143,11 @@ impl ChatPanel {
cx.subscribe_in(&room, window, move |this, _, signal, window, cx| {
match signal {
RoomSignal::NewMessage((gift_wrap_id, event)) => {
let nostr = NostrRegistry::global(cx);
let tracker = nostr.read(cx).tracker();
let gift_wrap_id = gift_wrap_id.to_owned();
let message = Message::user(event.clone());
cx.spawn_in(window, async move |this, cx| {
let tracker = tracker.read().await;
this.update_in(cx, |this, _window, cx| {
if !tracker.sent_ids().contains(&gift_wrap_id) {
this.insert_message(message, false, cx);
}
})
.ok();
})
@@ -189,7 +180,6 @@ impl ChatPanel {
input,
replies_to,
attachments,
options,
rendered_texts_by_id: BTreeMap::new(),
reports_by_id: BTreeMap::new(),
uploading: false,
@@ -271,14 +261,13 @@ impl ChatPanel {
// Get the current room entity
let room = self.room.read(cx);
let opts = self.options.read(cx);
// Create a temporary message for optimistic update
let rumor = room.create_message(&content, replies.as_ref(), cx);
let rumor_id = rumor.id.unwrap();
// Create a task for sending the message in the background
let send_message = room.send_message(&rumor, opts, cx);
let send_message = room.send_message(&rumor, cx);
// Optimistically update message list
cx.spawn_in(window, async move |this, cx| {
@@ -440,10 +429,6 @@ impl ChatPanel {
persons.read(cx).get_person(public_key, cx)
}
fn signer_kind(&self, cx: &App) -> SignerKind {
self.options.read(cx).signer_kind
}
fn scroll_to(&self, id: EventId) {
if let Some(ix) = self.messages.iter().position(|m| {
if let Message::User(msg) = m {
@@ -1235,71 +1220,6 @@ impl ChatPanel {
});
})
}
fn on_open_seen_on(&mut self, ev: &SeenOn, window: &mut Window, cx: &mut Context<Self>) {
let id = ev.0;
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let tracker = nostr.read(cx).tracker();
let task: Task<Result<Vec<RelayUrl>, Error>> = cx.background_spawn(async move {
let tracker = tracker.read().await;
let mut relays: Vec<RelayUrl> = vec![];
let filter = Filter::new()
.kind(Kind::ApplicationSpecificData)
.event(id)
.limit(1);
if let Some(event) = client.database().query(filter).await?.first_owned() {
if let Some(Ok(id)) = event.tags.identifier().map(EventId::parse) {
if let Some(urls) = tracker.seen_on_relays.get(&id).cloned() {
relays.extend(urls);
}
}
}
Ok(relays)
});
cx.spawn_in(window, async move |_, cx| {
if let Ok(urls) = task.await {
cx.update(|window, cx| {
window.open_modal(cx, move |this, _window, cx| {
this.show_close(true)
.title(SharedString::from("Seen on"))
.child(v_flex().pb_4().gap_2().children({
let mut items = Vec::with_capacity(urls.len());
for url in urls.clone().into_iter() {
items.push(
h_flex()
.h_8()
.px_2()
.bg(cx.theme().elevated_surface_background)
.rounded(cx.theme().radius)
.font_semibold()
.text_xs()
.child(SharedString::from(url.to_string())),
)
}
items
}))
});
})
.ok();
}
})
.detach();
}
fn on_set_encryption(&mut self, ev: &SetSigner, _: &mut Window, cx: &mut Context<Self>) {
self.options.update(cx, move |this, cx| {
this.signer_kind = ev.0;
cx.notify();
});
}
}
impl Panel for ChatPanel {
@@ -1339,11 +1259,7 @@ impl Focusable for ChatPanel {
impl Render for ChatPanel {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let kind = self.signer_kind(cx);
v_flex()
.on_action(cx.listener(Self::on_open_seen_on))
.on_action(cx.listener(Self::on_set_encryption))
.image_cache(self.image_cache.clone())
.size_full()
.child(
@@ -1398,31 +1314,7 @@ impl Render for ChatPanel {
.large(),
),
)
.child(TextInput::new(&self.input))
.child(
Button::new("encryptions")
.icon(IconName::Encryption)
.ghost()
.large()
.popup_menu(move |this, _window, _cx| {
this.label("Encrypt by:")
.menu_with_check(
"Encryption Key",
matches!(kind, SignerKind::Encryption),
Box::new(SetSigner(SignerKind::Encryption)),
)
.menu_with_check(
"User's Identity",
matches!(kind, SignerKind::User),
Box::new(SetSigner(SignerKind::User)),
)
.menu_with_check(
"Auto",
matches!(kind, SignerKind::Auto),
Box::new(SetSigner(SignerKind::Auto)),
)
}),
),
.child(TextInput::new(&self.input)),
),
),
)

View File

@@ -1,12 +1,9 @@
use std::sync::Arc;
use account::Account;
use auto_update::{AutoUpdateStatus, AutoUpdater};
use chat::{ChatEvent, ChatRegistry};
use chat_ui::{CopyPublicKey, OpenPublicKey};
use common::{RenderedProfile, DEFAULT_SIDEBAR_WIDTH};
use encryption::Encryption;
use encryption_ui::EncryptionPanel;
use gpui::prelude::FluentBuilder;
use gpui::{
deferred, div, px, relative, rems, App, AppContext, Axis, ClipboardItem, Context, Entity,
@@ -19,6 +16,7 @@ use person::PersonRegistry;
use relay_auth::RelayAuth;
use settings::AppSettings;
use smallvec::{smallvec, SmallVec};
use state::NostrRegistry;
use theme::{ActiveTheme, Theme, ThemeMode, ThemeRegistry};
use title_bar::TitleBar;
use ui::avatar::Avatar;
@@ -27,7 +25,6 @@ use ui::dock_area::dock::DockPlacement;
use ui::dock_area::panel::PanelView;
use ui::dock_area::{ClosePanel, DockArea, DockItem};
use ui::modal::ModalButtonProps;
use ui::popover::{Popover, PopoverContent};
use ui::popup_menu::PopupMenuExt;
use ui::{h_flex, v_flex, ContextModal, IconName, Root, Sizable, StyledExt};
@@ -61,9 +58,6 @@ pub struct ChatSpace {
/// App's Dock Area
dock: Entity<DockArea>,
/// App's Encryption Panel
encryption_panel: Entity<EncryptionPanel>,
/// Determines if the chat space is ready to use
ready: bool,
@@ -73,13 +67,14 @@ pub struct ChatSpace {
impl ChatSpace {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let nostr = NostrRegistry::global(cx);
let chat = ChatRegistry::global(cx);
let keystore = KeyStore::global(cx);
let account = Account::global(cx);
let title_bar = cx.new(|_| TitleBar::new());
let dock = cx.new(|cx| DockArea::new(window, cx));
let encryption_panel = encryption_ui::init(window, cx);
let identity = nostr.read(cx).identity();
let mut subscriptions = smallvec![];
@@ -92,8 +87,8 @@ impl ChatSpace {
subscriptions.push(
// Observe account entity changes
cx.observe_in(&account, window, move |this, state, window, cx| {
if !this.ready && state.read(cx).has_account() {
cx.observe_in(&identity, window, move |this, state, window, cx| {
if !this.ready && state.read(cx).has_public_key() {
this.set_default_layout(window, cx);
// Load all chat room in the database if available
@@ -175,7 +170,6 @@ impl ChatSpace {
Self {
dock,
title_bar,
encryption_panel,
ready: false,
_subscriptions: subscriptions,
}
@@ -447,11 +441,11 @@ impl ChatSpace {
}
fn titlebar_left(&mut self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
let account = Account::global(cx);
let nostr = NostrRegistry::global(cx);
let chat = ChatRegistry::global(cx);
let status = chat.read(cx).loading;
let status = chat.read(cx).loading();
if !account.read(cx).has_account() {
if !nostr.read(cx).identity().read(cx).has_public_key() {
return div();
}
@@ -479,10 +473,12 @@ impl ChatSpace {
fn titlebar_right(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let proxy = AppSettings::get_proxy_user_avatars(cx);
let auto_update = AutoUpdater::global(cx);
let account = Account::global(cx);
let relay_auth = RelayAuth::global(cx);
let pending_requests = relay_auth.read(cx).pending_requests(cx);
let encryption_panel = self.encryption_panel.downgrade();
let nostr = NostrRegistry::global(cx);
let identity = nostr.read(cx).identity();
h_flex()
.gap_2()
@@ -542,16 +538,10 @@ impl ChatSpace {
}),
)
})
.when(account.read(cx).has_account(), |this| {
let account = Account::global(cx);
let public_key = account.read(cx).public_key();
.when_some(identity.read(cx).option_public_key(), |this, public_key| {
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get_person(&public_key, cx);
let encryption = Encryption::global(cx);
let has_encryption = encryption.read(cx).has_encryption(cx);
let keystore = KeyStore::global(cx);
let is_using_file_keystore = keystore.read(cx).is_using_file_keystore();
@@ -562,37 +552,6 @@ impl ChatSpace {
};
this.child(
h_flex()
.gap_1()
.child(
Popover::new("encryption")
.trigger(
Button::new("encryption-trigger")
.tooltip("Manage Encryption Key")
.icon(IconName::Encryption)
.rounded()
.small()
.cta()
.map(|this| match has_encryption {
true => this.ghost_alt(),
false => this.warning(),
}),
)
.content(move |window, cx| {
let encryption_panel = encryption_panel.clone();
cx.new(|cx| {
PopoverContent::new(window, cx, move |_window, _cx| {
if let Some(view) = encryption_panel.upgrade() {
view.clone().into_any_element()
} else {
div().into_any_element()
}
})
})
}),
)
.child(
Button::new("user")
.small()
.reverse()
@@ -620,24 +579,11 @@ impl ChatSpace {
!is_using_file_keystore,
)
.separator()
.menu_with_icon(
"Dark Mode",
IconName::Sun,
Box::new(DarkMode),
)
.menu_with_icon("Dark Mode", IconName::Sun, Box::new(DarkMode))
.menu_with_icon("Themes", IconName::Moon, Box::new(Themes))
.menu_with_icon(
"Settings",
IconName::Settings,
Box::new(Settings),
)
.menu_with_icon(
"Sign Out",
IconName::Logout,
Box::new(Logout),
)
.menu_with_icon("Settings", IconName::Settings, Box::new(Settings))
.menu_with_icon("Sign Out", IconName::Logout, Box::new(Logout))
}),
),
)
})
}

View File

@@ -96,12 +96,6 @@ fn main() {
// Initialize settings
settings::init(cx);
// Initialize account state
account::init(cx);
// Initialize encryption state
encryption::init(cx);
// Initialize app registry
chat::init(cx);

View File

@@ -1,7 +1,6 @@
use std::ops::Range;
use std::time::Duration;
use account::Account;
use anyhow::{anyhow, Error};
use chat::{ChatEvent, ChatRegistry, Room, RoomKind};
use common::{DebouncedDelay, RenderedTimestamp, TextUtils, BOOTSTRAP_RELAYS, SEARCH_RELAYS};
@@ -199,11 +198,9 @@ impl Sidebar {
}
fn search_by_nip50(&mut self, query: &str, window: &mut Window, cx: &mut Context<Self>) {
let account = Account::global(cx);
let public_key = account.read(cx).public_key();
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let public_key = nostr.read(cx).identity().read(cx).public_key();
let query = query.to_owned();
@@ -597,7 +594,7 @@ impl Focusable for Sidebar {
impl Render for Sidebar {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let chat = ChatRegistry::global(cx);
let loading = chat.read(cx).loading;
let loading = chat.read(cx).loading();
// Get rooms from either search results or the chat registry
let rooms = if let Some(results) = self.search_results.read(cx).as_ref() {

View File

@@ -259,17 +259,11 @@ impl UserProfile {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let gossip = nostr.read(cx).gossip();
let public_key = nostr.read(cx).identity().read(cx).public_key();
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
cx.background_spawn(async move {
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let gossip = gossip.read().await;
let write_relays = gossip.inbox_relays(&public_key);
// Ensure connections to the write relays
gossip.ensure_connections(&client, &write_relays).await;
// Sign the new metadata event
let event = EventBuilder::metadata(&new_metadata).sign(&signer).await?;

View File

@@ -1,7 +1,6 @@
use std::ops::Range;
use std::time::Duration;
use account::Account;
use anyhow::{anyhow, Error};
use chat::{ChatRegistry, Room};
use common::{nip05_profile, RenderedProfile, TextUtils, BOOTSTRAP_RELAYS};
@@ -312,9 +311,8 @@ impl Compose {
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let chat = ChatRegistry::global(cx);
let account = Account::global(cx);
let public_key = account.read(cx).public_key();
let nostr = NostrRegistry::global(cx);
let public_key = nostr.read(cx).identity().read(cx).public_key();
let receivers: Vec<PublicKey> = self.selected(cx);
let subject_input = self.title_input.read(cx).value();

View File

@@ -158,17 +158,13 @@ impl SetupRelay {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let gossip = nostr.read(cx).gossip();
let public_key = nostr.read(cx).identity().read(cx).public_key();
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
let relays = self.relays.clone();
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let gossip = gossip.read().await;
let write_relays = gossip.inbox_relays(&public_key);
// Ensure connections to the write relays
gossip.ensure_connections(&client, &write_relays).await;
let tags: Vec<Tag> = relays
.iter()

View File

@@ -29,11 +29,16 @@ pub fn init(cre: Credential, window: &mut Window, cx: &mut App) -> Entity<Startu
/// Startup
#[derive(Debug)]
pub struct Startup {
credential: Credential,
loading: bool,
name: SharedString,
focus_handle: FocusHandle,
/// Local user credentials
credential: Credential,
/// Whether the loadng is in progress
loading: bool,
/// Image cache
image_cache: Entity<RetainAllImageCache>,
/// Event subscriptions
@@ -164,15 +169,12 @@ impl Startup {
}
fn login_with_keys(&mut self, secret: SecretKey, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let keys = Keys::new(secret);
let nostr = NostrRegistry::global(cx);
// Update the signer
cx.background_spawn(async move {
client.set_signer(keys).await;
nostr.update(cx, |this, cx| {
this.set_signer(keys, cx);
})
.detach();
}
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {

View File

@@ -75,6 +75,11 @@ impl Identity {
self.public_key = None;
}
/// Returns the public key of the identity.
pub fn option_public_key(&self) -> Option<PublicKey> {
self.public_key
}
/// Returns the public key of the identity.
pub fn public_key(&self) -> PublicKey {
// This method is safe to unwrap because the public key is always called when the identity is created.

View File

@@ -286,8 +286,8 @@ impl NostrRegistry {
}
/// Get current identity
pub fn identity(&self, cx: &App) -> Identity {
self.identity.read(cx).clone()
pub fn identity(&self) -> Entity<Identity> {
self.identity.clone()
}
/// Get a relay hint (messaging relay) for a given public key
@@ -299,9 +299,58 @@ impl NostrRegistry {
.cloned()
}
/// Get a list of write relays for a given public key
pub fn write_relays(&self, public_key: &PublicKey, cx: &App) -> Vec<RelayUrl> {
let client = self.client();
let relays = self.gossip.read(cx).write_relays(public_key);
let async_relays = relays.clone();
// Ensure relay connections
cx.background_spawn(async move {
for url in async_relays.iter() {
client.add_relay(url).await.ok();
client.connect_relay(url).await.ok();
}
})
.detach();
relays
}
/// Get a list of read relays for a given public key
pub fn read_relays(&self, public_key: &PublicKey, cx: &App) -> Vec<RelayUrl> {
let client = self.client();
let relays = self.gossip.read(cx).read_relays(public_key);
let async_relays = relays.clone();
// Ensure relay connections
cx.background_spawn(async move {
for url in async_relays.iter() {
client.add_relay(url).await.ok();
client.connect_relay(url).await.ok();
}
})
.detach();
relays
}
/// Get a list of messaging relays for a given public key
pub fn messaging_relays(&self, public_key: &PublicKey, cx: &App) -> Vec<RelayUrl> {
self.gossip.read(cx).messaging_relays(public_key)
let client = self.client();
let relays = self.gossip.read(cx).messaging_relays(public_key);
let async_relays = relays.clone();
// Ensure relay connections
cx.background_spawn(async move {
for url in async_relays.iter() {
client.add_relay(url).await.ok();
client.connect_relay(url).await.ok();
}
})
.detach();
relays
}
/// Set the signer for the nostr client and verify the public key
@@ -370,7 +419,7 @@ impl NostrRegistry {
fn get_relay_list(&mut self, cx: &mut Context<Self>) {
let client = self.client();
let async_identity = self.identity.downgrade();
let public_key = self.identity(cx).public_key();
let public_key = self.identity().read(cx).public_key();
let task: Task<Result<RelayState, Error>> = cx.background_spawn(async move {
let filter = Filter::new()
@@ -415,8 +464,8 @@ impl NostrRegistry {
fn get_messaging_relays(&mut self, cx: &mut Context<Self>) {
let client = self.client();
let async_identity = self.identity.downgrade();
let public_key = self.identity(cx).public_key();
let write_relays = self.gossip.read(cx).write_relays(&public_key);
let public_key = self.identity().read(cx).public_key();
let write_relays = self.write_relays(&public_key, cx);
let task: Task<Result<RelayState, Error>> = cx.background_spawn(async move {
let filter = Filter::new()
@@ -460,8 +509,8 @@ impl NostrRegistry {
/// Continuously get gift wrap events for the current user in their messaging relays
fn get_messages(&mut self, cx: &mut Context<Self>) {
let client = self.client();
let public_key = self.identity(cx).public_key();
let messaging_relays = self.gossip.read(cx).messaging_relays(&public_key);
let public_key = self.identity().read(cx).public_key();
let messaging_relays = self.messaging_relays(&public_key, cx);
cx.background_spawn(async move {
let id = SubscriptionId::new(GIFTWRAP_SUBSCRIPTION);
@@ -480,7 +529,7 @@ impl NostrRegistry {
/// Publish an event to author's write relays
pub fn publish(&self, event: Event, cx: &App) -> Task<Result<Output<EventId>, Error>> {
let client = self.client();
let write_relays = self.gossip.read(cx).write_relays(&event.pubkey);
let write_relays = self.write_relays(&event.pubkey, cx);
cx.background_spawn(async move { Ok(client.send_event_to(&write_relays, &event).await?) })
}
@@ -491,7 +540,7 @@ impl NostrRegistry {
I: Into<Vec<Kind>>,
{
let client = self.client();
let write_relays = self.gossip.read(cx).write_relays(&author);
let write_relays = self.write_relays(&author, cx);
// Construct filters based on event kinds
let filters: Vec<Filter> = kinds