feat: add user menu and sign-out support
This commit is contained in:
@@ -7,6 +7,8 @@
|
||||
### Added
|
||||
|
||||
- Add an identity import dialog, importing an existing identity by pasting its `nsec` secret key (encrypted with a new passphrase), NIP-49 `ncryptsec` (unlocked with its passphrase), or `bunker://` NIP-46 URI
|
||||
- Add a dropdown menu on the signed-in user button in the sidebar
|
||||
- Add sign-out feature, confirmed by an alert dialog before deleting the stored session credential
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@ use std::time::{Duration, Instant};
|
||||
|
||||
use anyhow::{Error, anyhow, bail};
|
||||
use bitcoin_hashes::sha1::Hash as Sha1Hash;
|
||||
use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, Global, Task, WeakEntity};
|
||||
use gpui::{
|
||||
App, AppContext, AsyncApp, Context, Entity, EventEmitter, Global, Task, TaskExt, WeakEntity,
|
||||
};
|
||||
use nostr::event::IntoEventBuilder;
|
||||
use nostr::nips::nip19::Nip19Coordinate;
|
||||
use nostr_connect::prelude::*;
|
||||
@@ -944,6 +946,20 @@ impl Backend {
|
||||
self.passphrase_required
|
||||
}
|
||||
|
||||
pub fn sign_out(&mut self, cx: &mut Context<Self>) {
|
||||
self.current_user = None;
|
||||
self.passphrase_required = false;
|
||||
|
||||
self.inbox.update(cx, |inbox, cx| {
|
||||
inbox.reset(cx);
|
||||
});
|
||||
|
||||
cx.delete_credentials(USER_KEYRING).detach_and_log_err(cx);
|
||||
cx.emit(BackendEvent::SignerChanged);
|
||||
cx.emit(BackendEvent::SignerRequired);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn sync_inbox(&mut self, cx: &mut Context<Self>) {
|
||||
let repo_store = RepoListStore::global(cx);
|
||||
let client = self.client.clone();
|
||||
|
||||
@@ -3,6 +3,7 @@ mod dialog_state;
|
||||
pub(crate) mod discussion;
|
||||
mod inbox;
|
||||
mod issues;
|
||||
mod placeholder_panel;
|
||||
mod pull_requests;
|
||||
mod repo;
|
||||
mod repo_list;
|
||||
@@ -15,6 +16,7 @@ use gpui::prelude::*;
|
||||
use gpui::{AnyElement, App, div};
|
||||
use gpui_component::{Sizable as _, h_flex};
|
||||
pub use inbox::InboxView;
|
||||
pub(crate) use placeholder_panel::PlaceholderPanel;
|
||||
pub use repo::RepoDetailView;
|
||||
pub(crate) use repo::{RepoItem, open_repo_item, open_repo_panel};
|
||||
pub use repo_list::RepoListView;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
use dock::{BasePanel, Panel, PanelEvent};
|
||||
use gpui::{
|
||||
App, Context, EventEmitter, FocusHandle, Focusable, IntoElement, Render, SharedString, Window,
|
||||
};
|
||||
use signed_ui::placeholder;
|
||||
|
||||
/// A center panel reserving the place of a view that is not built yet.
|
||||
pub struct PlaceholderPanel {
|
||||
name: &'static str,
|
||||
title: SharedString,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl PlaceholderPanel {
|
||||
pub(crate) fn new(name: &'static str, title: &'static str, cx: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
name,
|
||||
title: SharedString::from(title),
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BasePanel for PlaceholderPanel {
|
||||
fn panel_name(&self) -> &'static str {
|
||||
self.name
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for PlaceholderPanel {
|
||||
fn title(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
self.title.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for PlaceholderPanel {}
|
||||
|
||||
impl Focusable for PlaceholderPanel {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for PlaceholderPanel {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
placeholder("Nothing here yet.", cx)
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,11 @@ use gpui::{
|
||||
SharedString, Subscription, WeakEntity, Window, div, img, px, relative, uniform_list, white,
|
||||
};
|
||||
use gpui_base::Button as BaseButton;
|
||||
use gpui_component::button::{Button, ButtonVariants};
|
||||
use gpui_component::button::{Button, ButtonVariant, ButtonVariants};
|
||||
use gpui_component::dialog::DialogButtonProps;
|
||||
use gpui_component::input::InputState;
|
||||
use gpui_component::{ActiveTheme, Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
|
||||
use gpui_component::menu::{DropdownMenu, PopupMenuItem};
|
||||
use gpui_component::{ActiveTheme, Icon, IconName, Sizable, StyledExt, WindowExt, h_flex, v_flex};
|
||||
use nostr::prelude::RelayUrl;
|
||||
use signed_core::{Announcement, RepoAddr};
|
||||
use signed_state::{
|
||||
@@ -25,7 +27,7 @@ use signed_state::{
|
||||
};
|
||||
use signed_ui::{Avatar, NavItem, PixelAvatar, title_bar_drag_handlers};
|
||||
|
||||
use super::{InboxView, RepoDetailView, RepoListView, open_repo_panel};
|
||||
use super::{InboxView, PlaceholderPanel, RepoDetailView, RepoListView, open_repo_panel};
|
||||
|
||||
mod create_repo_dialog;
|
||||
pub(crate) mod grasp_servers;
|
||||
@@ -54,6 +56,8 @@ pub struct SidebarPanel {
|
||||
dock_area: WeakEntity<DockArea>,
|
||||
inbox: Option<WeakEntity<InboxView>>,
|
||||
explore: Option<WeakEntity<RepoListView>>,
|
||||
profile: Option<WeakEntity<PlaceholderPanel>>,
|
||||
relays: Option<WeakEntity<PlaceholderPanel>>,
|
||||
banner: SharedString,
|
||||
/// User's announced repositories.
|
||||
announcements: Arc<Vec<Announcement>>,
|
||||
@@ -113,6 +117,8 @@ impl SidebarPanel {
|
||||
dock_area,
|
||||
inbox: None,
|
||||
explore: None,
|
||||
profile: None,
|
||||
relays: None,
|
||||
banner: pick_banner(),
|
||||
announcements: Arc::new(Vec::new()),
|
||||
local_repos: Arc::new(Vec::new()),
|
||||
@@ -221,6 +227,51 @@ impl SidebarPanel {
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn open_profile(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
Self::open_placeholder_panel(
|
||||
&self.dock_area,
|
||||
&mut self.profile,
|
||||
"profile",
|
||||
"Profile",
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn open_relays(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
Self::open_placeholder_panel(
|
||||
&self.dock_area,
|
||||
&mut self.relays,
|
||||
"relays",
|
||||
"Relays",
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
/// Opens one of the not-yet-built panels, reusing the already-open one.
|
||||
fn open_placeholder_panel(
|
||||
dock_area: &WeakEntity<DockArea>,
|
||||
slot: &mut Option<WeakEntity<PlaceholderPanel>>,
|
||||
name: &'static str,
|
||||
title: &'static str,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if slot.as_ref().and_then(WeakEntity::upgrade).is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let panel = cx.new(|cx| PlaceholderPanel::new(name, title, cx));
|
||||
*slot = Some(panel.downgrade());
|
||||
|
||||
dock_area
|
||||
.update(cx, |dock_area, cx| {
|
||||
add_center_panel(dock_area, panel_handle(panel), window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn open_onboarding(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Enter desired name"));
|
||||
let pass_input = cx.new(|cx| {
|
||||
@@ -524,6 +575,7 @@ impl SidebarPanel {
|
||||
) -> impl IntoElement {
|
||||
let name = profile.name();
|
||||
let picture = profile.picture();
|
||||
let panel = cx.entity().downgrade();
|
||||
|
||||
title_bar_drag_handlers(
|
||||
h_flex()
|
||||
@@ -532,12 +584,44 @@ impl SidebarPanel {
|
||||
.when(cfg!(target_os = "macos"), |this| this.pl(px(80.)))
|
||||
.child(
|
||||
div().child(
|
||||
Button::new("user").text().dropdown_caret(true).child(
|
||||
Button::new("user")
|
||||
.text()
|
||||
.dropdown_caret(true)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Avatar::new(name.clone()).picture(picture))
|
||||
.child(div().text_xs().font_semibold().child(name)),
|
||||
)
|
||||
.dropdown_menu(move |menu, _, _| {
|
||||
menu.item(PopupMenuItem::new("View Profile").on_click({
|
||||
let panel = panel.clone();
|
||||
move |_, window, cx| {
|
||||
let _ = panel.update(cx, |this, cx| {
|
||||
this.open_profile(window, cx);
|
||||
});
|
||||
}
|
||||
}))
|
||||
.item(PopupMenuItem::new("View Relays").on_click({
|
||||
let panel = panel.clone();
|
||||
move |_, window, cx| {
|
||||
let _ = panel.update(cx, |this, cx| {
|
||||
this.open_relays(window, cx);
|
||||
});
|
||||
}
|
||||
}))
|
||||
.separator()
|
||||
.item(
|
||||
PopupMenuItem::element(|_, cx| {
|
||||
div().text_color(cx.theme().danger).child("Exit")
|
||||
})
|
||||
.on_click(
|
||||
|_, window, cx| {
|
||||
open_exit_dialog(window, cx);
|
||||
},
|
||||
),
|
||||
)
|
||||
}),
|
||||
),
|
||||
),
|
||||
window,
|
||||
@@ -781,3 +865,26 @@ impl Render for SidebarPanel {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn open_exit_dialog(window: &mut Window, cx: &mut App) {
|
||||
window.open_alert_dialog(cx, |alert, _, _| {
|
||||
alert
|
||||
.title("Exit Signed?")
|
||||
.description(
|
||||
"You will be signed out on this device and will need to sign in again on the next session.",
|
||||
)
|
||||
.button_props(
|
||||
DialogButtonProps::default()
|
||||
.ok_text("Exit")
|
||||
.ok_variant(ButtonVariant::Danger)
|
||||
.show_cancel(true),
|
||||
)
|
||||
.on_ok(|_, _, cx| {
|
||||
let backend = Backend::global(cx);
|
||||
backend.update(cx, |backend, cx| {
|
||||
backend.sign_out(cx);
|
||||
});
|
||||
true
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
# TODO
|
||||
|
||||
- Profile panel: the sidebar user menu's "View profile" opens an empty placeholder panel; build the actual profile view.
|
||||
- Relays panel: the sidebar user menu's "View relays" opens an empty placeholder panel; build the actual relays view.
|
||||
|
||||
Reference in New Issue
Block a user