776 lines
32 KiB
Rust
776 lines
32 KiB
Rust
use std::rc::Rc;
|
|
use std::sync::Arc;
|
|
|
|
use ::settings::AppSettings;
|
|
use anyhow::Error;
|
|
use auto_update::AutoUpdater;
|
|
use chat::{ChatEvent, ChatRegistry};
|
|
use common::download_dir;
|
|
use community::{CommunityEvent, CommunityRegistry};
|
|
use community_ui::CommunityPanel;
|
|
use device::{DeviceEvent, DeviceRegistry};
|
|
use gpui::prelude::FluentBuilder;
|
|
use gpui::{
|
|
Action, AnyElement, App, AppContext, Context, Entity, InteractiveElement, IntoElement,
|
|
ParentElement, Render, SharedString, Styled, Subscription, Task, WeakEntity, Window, div, px,
|
|
};
|
|
use nostr_sdk::prelude::*;
|
|
use person::{PersonRegistry, shorten_pubkey};
|
|
use serde::Deserialize;
|
|
use smallvec::{SmallVec, smallvec};
|
|
use state::{NostrRegistry, StateEvent};
|
|
use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry};
|
|
use ui::button::{Button, ButtonVariants};
|
|
use ui::dock::{self, ClosePanel, DockArea, DockLayout, DockPlacement, Panel, PanelHandle};
|
|
use ui::menu::{DropdownMenu, PopupMenuItem};
|
|
use ui::notification::{Notification, NotificationKind};
|
|
use ui::{Icon, IconName, Root, Sizable, WindowExtension, h_flex, v_flex};
|
|
|
|
use crate::dialogs::restore::RestoreEncryption;
|
|
use crate::dialogs::{new_chat, new_community, settings};
|
|
use crate::panels::{
|
|
backup, browse, contact_list, greeter, inbox, messaging_relays, profile, relay_list, requests,
|
|
search,
|
|
};
|
|
use crate::sidebar::Sidebar;
|
|
|
|
mod dialogs;
|
|
mod panels;
|
|
mod sidebar;
|
|
|
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> {
|
|
cx.new(|cx| Workspace::new(window, cx))
|
|
}
|
|
|
|
struct DeviceNotifcation;
|
|
struct MsgRelayNotification;
|
|
|
|
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
|
#[action(namespace = workspace, no_json)]
|
|
enum Command {
|
|
ToggleTheme,
|
|
Update,
|
|
RefreshMessagingRelays,
|
|
BackupEncryption,
|
|
ImportEncryption,
|
|
RefreshEncryption,
|
|
ResetEncryption,
|
|
ShowRelayList,
|
|
ShowMessaging,
|
|
ShowProfile,
|
|
ShowSettings,
|
|
ShowBackup,
|
|
ShowContactList,
|
|
ShowInbox,
|
|
ShowRequests,
|
|
ShowBrowse,
|
|
ShowSearch,
|
|
NewChat,
|
|
NewCommunity,
|
|
}
|
|
|
|
pub struct Workspace {
|
|
dock: Entity<DockArea>,
|
|
title_bar_chrome: Rc<dock::TitleBarChrome>,
|
|
/// The community panel currently docked, if any
|
|
community_panel: Option<WeakEntity<CommunityPanel>>,
|
|
/// Async tasks
|
|
tasks: Vec<Task<Result<(), Error>>>,
|
|
/// Event subscriptions
|
|
_subscriptions: SmallVec<[Subscription; 7]>,
|
|
}
|
|
|
|
impl Workspace {
|
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
|
let chat = ChatRegistry::global(cx);
|
|
let communities = CommunityRegistry::global(cx);
|
|
let device = DeviceRegistry::global(cx);
|
|
let nostr = NostrRegistry::global(cx);
|
|
|
|
let sidebar = cx.new(|cx| Sidebar::new(window, cx));
|
|
let (dock, title_bar_chrome) = dock::dock_area("coop", window, cx);
|
|
|
|
let mut subscriptions = smallvec![];
|
|
|
|
subscriptions.push(
|
|
// Observe system appearance and update theme
|
|
cx.observe_window_appearance(window, |_this, window, cx| {
|
|
Theme::sync_system_appearance(Some(window), cx);
|
|
}),
|
|
);
|
|
|
|
subscriptions.push(
|
|
// Subscribe to the nostr events
|
|
cx.subscribe_in(&nostr, window, move |_this, _state, event, window, cx| {
|
|
if let StateEvent::SignerChanged = event {
|
|
window.close_all_modals(cx);
|
|
}
|
|
}),
|
|
);
|
|
|
|
subscriptions.push(
|
|
// Observe all events emitted by the device registry
|
|
cx.subscribe_in(&device, window, |_this, _device, event, window, cx| {
|
|
match event {
|
|
DeviceEvent::Requesting => {
|
|
const MSG: &str =
|
|
"Please open other client and approve the request for encryption key.";
|
|
|
|
let note = Notification::new()
|
|
.id::<DeviceNotifcation>()
|
|
.autohide(false)
|
|
.title("Wait for approval")
|
|
.message(MSG)
|
|
.with_kind(NotificationKind::Info);
|
|
|
|
window.push_notification(note, cx);
|
|
}
|
|
DeviceEvent::NotSet => {
|
|
const MSG: &str =
|
|
"User're not setup encryption key yet. Do you want to create one?";
|
|
|
|
let note = Notification::new()
|
|
.id::<DeviceNotifcation>()
|
|
.message(MSG)
|
|
.with_kind(NotificationKind::Info)
|
|
.action(|_this, _window, _cx| {
|
|
Button::new("retry").label("Retry").on_click(
|
|
move |_this, window, cx| {
|
|
let device = DeviceRegistry::global(cx);
|
|
device.update(cx, |this, cx| {
|
|
this.set_announcement(Keys::generate(), cx);
|
|
});
|
|
window.clear_notification::<DeviceNotifcation>(cx);
|
|
},
|
|
)
|
|
});
|
|
|
|
window.push_notification(note, cx);
|
|
}
|
|
DeviceEvent::Set => {
|
|
let note = Notification::new()
|
|
.id::<DeviceNotifcation>()
|
|
.message("Encryption Key has been set")
|
|
.with_kind(NotificationKind::Success);
|
|
|
|
window.push_notification(note, cx);
|
|
}
|
|
DeviceEvent::Error(error) => {
|
|
window.push_notification(Notification::error(error).autohide(false), cx);
|
|
}
|
|
};
|
|
}),
|
|
);
|
|
|
|
subscriptions.push(
|
|
// Observe all events emitted by the chat registry
|
|
cx.subscribe_in(&chat, window, move |this, chat, ev, window, cx| {
|
|
match ev {
|
|
ChatEvent::InboxRelayNotFound => {
|
|
const MSG: &str = "Messaging Relays not found. Cannot receive messages.";
|
|
|
|
window.push_notification(
|
|
Notification::warning(MSG)
|
|
.id::<MsgRelayNotification>()
|
|
.autohide(false)
|
|
.action(|_this, _window, _cx| {
|
|
Button::new("retry").label("Retry").on_click(
|
|
move |_this, window, cx| {
|
|
let chat = ChatRegistry::global(cx);
|
|
chat.update(cx, |this, cx| {
|
|
this.get_metadata(cx);
|
|
});
|
|
window.clear_notification::<MsgRelayNotification>(cx);
|
|
},
|
|
)
|
|
}),
|
|
cx,
|
|
);
|
|
}
|
|
ChatEvent::OpenRoom(id) => {
|
|
if let Some(room) = chat.read(cx).room(id, cx) {
|
|
this.add_panel_to_dock(
|
|
chat_ui::init(room, window, cx),
|
|
DockPlacement::Center,
|
|
window,
|
|
cx,
|
|
);
|
|
}
|
|
}
|
|
ChatEvent::CloseRoom(..) => {
|
|
this.dock.update(cx, |area, cx| {
|
|
// Force focus to the tab panel
|
|
ui::dock::focus_tab_panel(area, window, cx);
|
|
|
|
// Dispatch the close panel action
|
|
cx.defer_in(window, |_, window, cx| {
|
|
window.dispatch_action(Box::new(ClosePanel), cx);
|
|
window.close_all_modals(cx);
|
|
});
|
|
});
|
|
}
|
|
ChatEvent::Error(error) => {
|
|
window.push_notification(Notification::error(error).autohide(false), cx);
|
|
}
|
|
_ => {}
|
|
};
|
|
}),
|
|
);
|
|
|
|
subscriptions.push(
|
|
// Observe all events emitted by the community registry
|
|
cx.subscribe_in(
|
|
&communities,
|
|
window,
|
|
move |this, communities, event, window, cx| match event {
|
|
CommunityEvent::Open(id) => {
|
|
if let Some(community) = communities.read(cx).community(id) {
|
|
let panel = community_ui::init(community, window, cx);
|
|
|
|
this.community_panel = Some(panel.downgrade());
|
|
this.add_panel_to_dock(panel, DockPlacement::Center, window, cx);
|
|
}
|
|
}
|
|
CommunityEvent::Close(_) => {
|
|
let Some(panel) = this
|
|
.community_panel
|
|
.take()
|
|
.and_then(|panel| panel.upgrade())
|
|
else {
|
|
return;
|
|
};
|
|
|
|
this.dock.update(cx, |area, cx| {
|
|
ui::dock::add_panel(
|
|
area,
|
|
PanelHandle::new(panel),
|
|
DockPlacement::Center,
|
|
window,
|
|
cx,
|
|
);
|
|
ui::dock::focus_tab_panel(area, window, cx);
|
|
|
|
cx.defer_in(window, |_, window, cx| {
|
|
window.dispatch_action(Box::new(ClosePanel), cx);
|
|
});
|
|
});
|
|
}
|
|
_ => {}
|
|
},
|
|
),
|
|
);
|
|
|
|
cx.defer_in(window, move |this, window, cx| {
|
|
let sidebar = PanelHandle::new(sidebar);
|
|
|
|
this.dock.update(cx, |area, cx| {
|
|
let left = DockLayout::tabs().panel_view(Arc::new(sidebar), cx);
|
|
area.set_dock(DockPlacement::Left, left, window, cx);
|
|
area.set_dock_size(DockPlacement::Left, SIDEBAR_WIDTH, window, cx);
|
|
});
|
|
|
|
let greeter = PanelHandle::new(greeter::init(window, cx));
|
|
let center = DockLayout::v_split()
|
|
.child(DockLayout::tabs().panel_view(Arc::new(greeter), cx), None);
|
|
|
|
this.dock.update(cx, |area, cx| {
|
|
area.set_center(center, window, cx);
|
|
});
|
|
});
|
|
|
|
Self {
|
|
dock,
|
|
title_bar_chrome,
|
|
community_panel: None,
|
|
tasks: vec![],
|
|
_subscriptions: subscriptions,
|
|
}
|
|
}
|
|
|
|
/// Add a panel to the dock, from anywhere that has the window but not the workspace.
|
|
pub fn add_panel<P: Panel>(
|
|
panel: Entity<P>,
|
|
placement: DockPlacement,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) {
|
|
if let Some(root) = window.root::<Root>().flatten()
|
|
&& let Ok(workspace) = root.read(cx).view().clone().downcast::<Self>()
|
|
{
|
|
workspace.update(cx, |this, cx| {
|
|
this.add_panel_to_dock(panel, placement, window, cx)
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Add a panel to the dock, or focus it if it is already docked.
|
|
fn add_panel_to_dock<P: Panel>(
|
|
&mut self,
|
|
panel: Entity<P>,
|
|
placement: DockPlacement,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.dock.update(cx, |area, cx| {
|
|
ui::dock::add_panel(area, PanelHandle::new(panel), placement, window, cx)
|
|
});
|
|
}
|
|
|
|
/// Handle command events
|
|
fn on_command(&mut self, command: &Command, window: &mut Window, cx: &mut Context<Self>) {
|
|
match command {
|
|
Command::ShowSettings => {
|
|
let view = settings::init(window, cx);
|
|
|
|
window.open_modal(cx, move |this, _window, _cx| {
|
|
this.width(px(520.))
|
|
.show_close(true)
|
|
.pb_2()
|
|
.title("Preferences")
|
|
.child(view.clone())
|
|
});
|
|
}
|
|
Command::ShowProfile => {
|
|
let nostr = NostrRegistry::global(cx);
|
|
|
|
if let Some(public_key) = nostr.read(cx).current_user() {
|
|
self.add_panel_to_dock(
|
|
profile::init(public_key, window, cx),
|
|
DockPlacement::Left,
|
|
window,
|
|
cx,
|
|
);
|
|
}
|
|
}
|
|
Command::ShowContactList => {
|
|
self.add_panel_to_dock(
|
|
contact_list::init(window, cx),
|
|
DockPlacement::Left,
|
|
window,
|
|
cx,
|
|
);
|
|
}
|
|
Command::ShowInbox => {
|
|
self.add_panel_to_dock(inbox::init(window, cx), DockPlacement::Center, window, cx);
|
|
}
|
|
Command::ShowRequests => {
|
|
self.add_panel_to_dock(
|
|
requests::init(window, cx),
|
|
DockPlacement::Center,
|
|
window,
|
|
cx,
|
|
);
|
|
}
|
|
Command::ShowBrowse => {
|
|
self.add_panel_to_dock(browse::init(window, cx), DockPlacement::Center, window, cx);
|
|
}
|
|
Command::ShowSearch => {
|
|
self.add_panel_to_dock(search::init(window, cx), DockPlacement::Center, window, cx);
|
|
}
|
|
Command::NewChat => {
|
|
new_chat::open(window, cx);
|
|
}
|
|
Command::NewCommunity => {
|
|
new_community::open(window, cx);
|
|
}
|
|
Command::ShowBackup => {
|
|
self.add_panel_to_dock(backup::init(window, cx), DockPlacement::Left, window, cx);
|
|
}
|
|
Command::ShowMessaging => {
|
|
self.add_panel_to_dock(
|
|
messaging_relays::init(window, cx),
|
|
DockPlacement::Left,
|
|
window,
|
|
cx,
|
|
);
|
|
}
|
|
Command::RefreshMessagingRelays => {
|
|
let chat = ChatRegistry::global(cx);
|
|
// Trigger a refresh of the chat registry
|
|
chat.update(cx, |this, cx| {
|
|
this.reload(cx);
|
|
});
|
|
}
|
|
Command::ShowRelayList => {
|
|
self.add_panel_to_dock(
|
|
relay_list::init(window, cx),
|
|
DockPlacement::Right,
|
|
window,
|
|
cx,
|
|
);
|
|
}
|
|
Command::RefreshEncryption => {
|
|
let device = DeviceRegistry::global(cx);
|
|
device.update(cx, |this, cx| {
|
|
this.get_announcement(cx);
|
|
});
|
|
}
|
|
Command::ResetEncryption => {
|
|
self.confirm_reset_encryption(window, cx);
|
|
}
|
|
Command::ToggleTheme => {
|
|
self.theme_selector(window, cx);
|
|
}
|
|
Command::BackupEncryption => {
|
|
let device = DeviceRegistry::global(cx).downgrade();
|
|
let save_dialog = cx.prompt_for_new_path(download_dir(), Some("encryption.txt"));
|
|
|
|
self.tasks.push(cx.spawn_in(window, async move |_this, cx| {
|
|
// Get the output path from the save dialog
|
|
let output_path = match save_dialog.await {
|
|
Ok(Ok(Some(path))) => path,
|
|
Ok(Ok(None)) | Err(_) => return Ok(()),
|
|
Ok(Err(error)) => {
|
|
cx.update(|window, cx| {
|
|
let message = format!("Failed to pick save location: {error:#}");
|
|
let note = Notification::error(message).autohide(false);
|
|
window.push_notification(note, cx);
|
|
})?;
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
// Get the backup task
|
|
let backup =
|
|
device.read_with(cx, |this, cx| this.backup(output_path.clone(), cx))?;
|
|
|
|
// Run the backup task
|
|
backup.await?;
|
|
|
|
// Open the backup file with the system's default application
|
|
cx.update(|_window, cx| {
|
|
cx.open_with_system(output_path.as_path());
|
|
})?;
|
|
|
|
Ok(())
|
|
}));
|
|
}
|
|
Command::ImportEncryption => {
|
|
self.import_encryption(window, cx);
|
|
}
|
|
Command::Update => {
|
|
if let Some(auto_updater) = AutoUpdater::try_global(cx) {
|
|
auto_updater.update(cx, |this, cx| this.check(cx));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn confirm_reset_encryption(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
const ENC_MSG: &str = "Encryption Key is a special key that used to encrypt and decrypt your messages. \
|
|
Your identity is completely decoupled from all encryption processes to protect your privacy.";
|
|
|
|
const ENC_WARN: &str = "By resetting your encryption key, you will lose access to \
|
|
all your encrypted messages before. This action cannot be undone.";
|
|
|
|
let device = DeviceRegistry::global(cx);
|
|
let ent = device.downgrade();
|
|
|
|
window.open_modal(cx, move |this, _window, cx| {
|
|
let ent = ent.clone();
|
|
|
|
this.confirm()
|
|
.show_close(true)
|
|
.title("Reset Encryption Key")
|
|
.child(
|
|
v_flex()
|
|
.gap_1()
|
|
.text_sm()
|
|
.child(SharedString::from(ENC_MSG))
|
|
.child(
|
|
div()
|
|
.italic()
|
|
.text_color(cx.theme().text_danger)
|
|
.child(SharedString::from(ENC_WARN)),
|
|
),
|
|
)
|
|
.on_ok(move |_ev, _window, cx| {
|
|
ent.update(cx, |this, cx| {
|
|
this.set_announcement(Keys::generate(), cx);
|
|
})
|
|
.ok();
|
|
// true to close modal
|
|
true
|
|
})
|
|
});
|
|
}
|
|
|
|
fn import_encryption(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
let restore = cx.new(|cx| RestoreEncryption::new(window, cx));
|
|
window.open_modal(cx, move |this, _window, _cx| {
|
|
this.width(px(420.))
|
|
.title("Restore Encryption")
|
|
.child(restore.clone())
|
|
});
|
|
}
|
|
|
|
fn theme_selector(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
window.open_modal(cx, move |this, _window, cx| {
|
|
let registry = ThemeRegistry::global(cx);
|
|
let themes = registry.read(cx).themes();
|
|
|
|
this.width(px(520.))
|
|
.show_close(true)
|
|
.title("Select theme")
|
|
.child(v_flex().gap_2().w_full().children({
|
|
let mut items = vec![];
|
|
|
|
for (ix, (path, theme)) in themes.iter().enumerate() {
|
|
items.push(
|
|
h_flex()
|
|
.id(ix)
|
|
.group("")
|
|
.px_2()
|
|
.h_8()
|
|
.w_full()
|
|
.justify_between()
|
|
.rounded(cx.theme().radius)
|
|
.bg(cx.theme().ghost_element_background)
|
|
.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
|
.child(
|
|
h_flex()
|
|
.gap_1p5()
|
|
.flex_1()
|
|
.text_sm()
|
|
.child(theme.name.clone())
|
|
.child(
|
|
div()
|
|
.text_xs()
|
|
.italic()
|
|
.text_color(cx.theme().text_muted)
|
|
.child(theme.author.clone()),
|
|
),
|
|
)
|
|
.child(
|
|
h_flex()
|
|
.gap_1()
|
|
.invisible()
|
|
.group_hover("", |this| this.visible())
|
|
.child(
|
|
Button::new(format!("url-{ix}"))
|
|
.icon(IconName::Link)
|
|
.ghost()
|
|
.small()
|
|
.on_click({
|
|
let theme = theme.clone();
|
|
move |_ev, _window, cx| {
|
|
cx.open_url(&theme.url);
|
|
}
|
|
}),
|
|
)
|
|
.child(
|
|
Button::new(format!("set-{ix}"))
|
|
.icon(IconName::Check)
|
|
.primary()
|
|
.small()
|
|
.on_click({
|
|
let path = path.clone();
|
|
move |_ev, window, cx| {
|
|
let settings = AppSettings::global(cx);
|
|
let path = path.clone();
|
|
|
|
settings.update(cx, |this, cx| {
|
|
this.set_theme(path, window, cx);
|
|
})
|
|
}
|
|
}),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
items
|
|
}))
|
|
});
|
|
}
|
|
|
|
fn titlebar_right(_window: &mut Window, cx: &mut App) -> AnyElement {
|
|
let auto_updater = AutoUpdater::try_global(cx);
|
|
let chat = ChatRegistry::global(cx);
|
|
let nip4e_enabled = AppSettings::get_nip4e(cx);
|
|
let nostr = NostrRegistry::global(cx);
|
|
|
|
let Some(public_key) = nostr.read(cx).current_user() else {
|
|
return div().into_any_element();
|
|
};
|
|
|
|
let persons = PersonRegistry::global(cx);
|
|
let profile = persons.read(cx).get(&public_key, cx);
|
|
let announcement = profile.announcement();
|
|
|
|
let updater_status = auto_updater.as_ref().and_then(|updater| {
|
|
let updater = updater.read(cx);
|
|
(!updater.idle()).then(|| updater.status())
|
|
});
|
|
|
|
let staged_update = auto_updater
|
|
.as_ref()
|
|
.is_some_and(|updater| updater.read(cx).staged());
|
|
|
|
h_flex()
|
|
.when(!cx.theme().platform.is_mac(), |this| this.pr_2())
|
|
.gap_2()
|
|
.when_some(updater_status, |this, status| {
|
|
this.child(div().text_xs().italic().child(status))
|
|
})
|
|
.when(staged_update, |this| {
|
|
this.child(
|
|
Button::new("restart-to-update")
|
|
.label("Restart to Update")
|
|
.tooltip("Quit and relaunch into the installed update")
|
|
.small()
|
|
.ghost()
|
|
.on_click(|_event, _window, cx| {
|
|
if let Some(auto_updater) = AutoUpdater::try_global(cx) {
|
|
auto_updater.update(cx, |this, cx| this.restart(cx));
|
|
}
|
|
}),
|
|
)
|
|
})
|
|
.when(nip4e_enabled, |this| {
|
|
this.child(
|
|
Button::new("key")
|
|
.icon(IconName::UserKey)
|
|
.tooltip("Decoupled encryption key")
|
|
.small()
|
|
.ghost()
|
|
.dropdown_menu(move |this, _window, _cx| {
|
|
this.min_w(px(260.))
|
|
.label("Encryption Key")
|
|
.when_some(announcement.as_ref(), |this, announcement| {
|
|
let name = announcement.client_name();
|
|
let pkey = shorten_pubkey(announcement.public_key(), 8);
|
|
|
|
this.item(PopupMenuItem::element(move |_window, cx| {
|
|
h_flex()
|
|
.gap_1()
|
|
.text_sm()
|
|
.child(
|
|
Icon::new(IconName::Device)
|
|
.small()
|
|
.text_color(cx.theme().icon_muted),
|
|
)
|
|
.child(name.clone())
|
|
}))
|
|
.item(
|
|
PopupMenuItem::element(move |_window, cx| {
|
|
h_flex()
|
|
.gap_1()
|
|
.text_sm()
|
|
.child(
|
|
Icon::new(IconName::UserKey)
|
|
.small()
|
|
.text_color(cx.theme().icon_muted),
|
|
)
|
|
.child(SharedString::from(pkey.clone()))
|
|
}),
|
|
)
|
|
})
|
|
.separator()
|
|
.menu_with_icon(
|
|
"Backup",
|
|
IconName::Shield,
|
|
Box::new(Command::BackupEncryption),
|
|
)
|
|
.menu_with_icon(
|
|
"Restore from secret key",
|
|
IconName::Usb,
|
|
Box::new(Command::ImportEncryption),
|
|
)
|
|
.separator()
|
|
.menu_with_icon(
|
|
"Reload",
|
|
IconName::Refresh,
|
|
Box::new(Command::RefreshEncryption),
|
|
)
|
|
.menu_with_icon(
|
|
"Reset",
|
|
IconName::Warning,
|
|
Box::new(Command::ResetEncryption),
|
|
)
|
|
}),
|
|
)
|
|
})
|
|
.child(
|
|
Button::new("inbox")
|
|
.icon(IconName::Inbox)
|
|
.small()
|
|
.ghost()
|
|
.dropdown_menu(move |this, _window, cx| {
|
|
let urls: Vec<(SharedString, SharedString)> = profile
|
|
.messaging_relays()
|
|
.iter()
|
|
.map(|url| {
|
|
(
|
|
SharedString::from(url.to_string()),
|
|
chat.read(cx).count_messages(url).to_string().into(),
|
|
)
|
|
})
|
|
.collect();
|
|
|
|
// Header
|
|
let menu = this.min_w(px(260.)).label("Messaging Relays");
|
|
|
|
// Content
|
|
let menu = urls.into_iter().fold(menu, |this, (url, count)| {
|
|
this.item(PopupMenuItem::element(move |_window, cx| {
|
|
h_flex()
|
|
.px_1()
|
|
.w_full()
|
|
.text_sm()
|
|
.justify_between()
|
|
.child(url.clone())
|
|
.child(
|
|
div()
|
|
.text_xs()
|
|
.text_color(cx.theme().text_muted)
|
|
.child(count.clone()),
|
|
)
|
|
}))
|
|
});
|
|
|
|
// Footer
|
|
menu.separator()
|
|
.menu_with_icon(
|
|
"Manage gossip relays",
|
|
IconName::Relay,
|
|
Box::new(Command::ShowRelayList),
|
|
)
|
|
.menu_with_icon(
|
|
"Manage messaging relays",
|
|
IconName::Relay,
|
|
Box::new(Command::ShowMessaging),
|
|
)
|
|
.separator()
|
|
.menu_with_icon(
|
|
"Reload",
|
|
IconName::Refresh,
|
|
Box::new(Command::RefreshMessagingRelays),
|
|
)
|
|
}),
|
|
)
|
|
.into_any_element()
|
|
}
|
|
}
|
|
|
|
impl Render for Workspace {
|
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let modal_layer = Root::render_modal_layer(window, cx);
|
|
let notification_layer = Root::render_notification_layer(window, cx);
|
|
|
|
// Render the title bar chrome
|
|
self.title_bar_chrome.set_trailing(Self::titlebar_right);
|
|
|
|
div()
|
|
.id("workspace")
|
|
.on_action(cx.listener(Self::on_command))
|
|
.relative()
|
|
.size_full()
|
|
.child(self.dock.clone())
|
|
// Notifications
|
|
.children(notification_layer)
|
|
// Modals
|
|
.children(modal_layer)
|
|
}
|
|
}
|