feat: add user menu and sign-out support
This commit is contained in:
@@ -7,6 +7,8 @@
|
|||||||
### Added
|
### 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 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
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ use std::time::{Duration, Instant};
|
|||||||
|
|
||||||
use anyhow::{Error, anyhow, bail};
|
use anyhow::{Error, anyhow, bail};
|
||||||
use bitcoin_hashes::sha1::Hash as Sha1Hash;
|
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::event::IntoEventBuilder;
|
||||||
use nostr::nips::nip19::Nip19Coordinate;
|
use nostr::nips::nip19::Nip19Coordinate;
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
@@ -944,6 +946,20 @@ impl Backend {
|
|||||||
self.passphrase_required
|
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>) {
|
fn sync_inbox(&mut self, cx: &mut Context<Self>) {
|
||||||
let repo_store = RepoListStore::global(cx);
|
let repo_store = RepoListStore::global(cx);
|
||||||
let client = self.client.clone();
|
let client = self.client.clone();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ mod dialog_state;
|
|||||||
pub(crate) mod discussion;
|
pub(crate) mod discussion;
|
||||||
mod inbox;
|
mod inbox;
|
||||||
mod issues;
|
mod issues;
|
||||||
|
mod placeholder_panel;
|
||||||
mod pull_requests;
|
mod pull_requests;
|
||||||
mod repo;
|
mod repo;
|
||||||
mod repo_list;
|
mod repo_list;
|
||||||
@@ -15,6 +16,7 @@ use gpui::prelude::*;
|
|||||||
use gpui::{AnyElement, App, div};
|
use gpui::{AnyElement, App, div};
|
||||||
use gpui_component::{Sizable as _, h_flex};
|
use gpui_component::{Sizable as _, h_flex};
|
||||||
pub use inbox::InboxView;
|
pub use inbox::InboxView;
|
||||||
|
pub(crate) use placeholder_panel::PlaceholderPanel;
|
||||||
pub use repo::RepoDetailView;
|
pub use repo::RepoDetailView;
|
||||||
pub(crate) use repo::{RepoItem, open_repo_item, open_repo_panel};
|
pub(crate) use repo::{RepoItem, open_repo_item, open_repo_panel};
|
||||||
pub use repo_list::RepoListView;
|
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,
|
SharedString, Subscription, WeakEntity, Window, div, img, px, relative, uniform_list, white,
|
||||||
};
|
};
|
||||||
use gpui_base::Button as BaseButton;
|
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::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 nostr::prelude::RelayUrl;
|
||||||
use signed_core::{Announcement, RepoAddr};
|
use signed_core::{Announcement, RepoAddr};
|
||||||
use signed_state::{
|
use signed_state::{
|
||||||
@@ -25,7 +27,7 @@ use signed_state::{
|
|||||||
};
|
};
|
||||||
use signed_ui::{Avatar, NavItem, PixelAvatar, title_bar_drag_handlers};
|
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;
|
mod create_repo_dialog;
|
||||||
pub(crate) mod grasp_servers;
|
pub(crate) mod grasp_servers;
|
||||||
@@ -54,6 +56,8 @@ pub struct SidebarPanel {
|
|||||||
dock_area: WeakEntity<DockArea>,
|
dock_area: WeakEntity<DockArea>,
|
||||||
inbox: Option<WeakEntity<InboxView>>,
|
inbox: Option<WeakEntity<InboxView>>,
|
||||||
explore: Option<WeakEntity<RepoListView>>,
|
explore: Option<WeakEntity<RepoListView>>,
|
||||||
|
profile: Option<WeakEntity<PlaceholderPanel>>,
|
||||||
|
relays: Option<WeakEntity<PlaceholderPanel>>,
|
||||||
banner: SharedString,
|
banner: SharedString,
|
||||||
/// User's announced repositories.
|
/// User's announced repositories.
|
||||||
announcements: Arc<Vec<Announcement>>,
|
announcements: Arc<Vec<Announcement>>,
|
||||||
@@ -113,6 +117,8 @@ impl SidebarPanel {
|
|||||||
dock_area,
|
dock_area,
|
||||||
inbox: None,
|
inbox: None,
|
||||||
explore: None,
|
explore: None,
|
||||||
|
profile: None,
|
||||||
|
relays: None,
|
||||||
banner: pick_banner(),
|
banner: pick_banner(),
|
||||||
announcements: Arc::new(Vec::new()),
|
announcements: Arc::new(Vec::new()),
|
||||||
local_repos: Arc::new(Vec::new()),
|
local_repos: Arc::new(Vec::new()),
|
||||||
@@ -221,6 +227,51 @@ impl SidebarPanel {
|
|||||||
.ok();
|
.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>) {
|
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 name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Enter desired name"));
|
||||||
let pass_input = cx.new(|cx| {
|
let pass_input = cx.new(|cx| {
|
||||||
@@ -524,6 +575,7 @@ impl SidebarPanel {
|
|||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
let name = profile.name();
|
let name = profile.name();
|
||||||
let picture = profile.picture();
|
let picture = profile.picture();
|
||||||
|
let panel = cx.entity().downgrade();
|
||||||
|
|
||||||
title_bar_drag_handlers(
|
title_bar_drag_handlers(
|
||||||
h_flex()
|
h_flex()
|
||||||
@@ -532,12 +584,44 @@ impl SidebarPanel {
|
|||||||
.when(cfg!(target_os = "macos"), |this| this.pl(px(80.)))
|
.when(cfg!(target_os = "macos"), |this| this.pl(px(80.)))
|
||||||
.child(
|
.child(
|
||||||
div().child(
|
div().child(
|
||||||
Button::new("user").text().dropdown_caret(true).child(
|
Button::new("user")
|
||||||
|
.text()
|
||||||
|
.dropdown_caret(true)
|
||||||
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(Avatar::new(name.clone()).picture(picture))
|
.child(Avatar::new(name.clone()).picture(picture))
|
||||||
.child(div().text_xs().font_semibold().child(name)),
|
.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,
|
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
|
# 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