feat: add user menu and sign-out support
Rust / build (macos-latest, stable) (push) Waiting to run
Rust / build (ubuntu-latest, stable) (push) Waiting to run
Rust / build (windows-latest, stable) (push) Waiting to run

This commit is contained in:
2026-10-06 15:50:30 +07:00
parent 62f355d70e
commit 237b6a6996
6 changed files with 188 additions and 10 deletions
+17 -1
View File
@@ -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();
+2
View File
@@ -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)
}
}
+116 -9
View File
@@ -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(
h_flex()
.gap_1()
.child(Avatar::new(name.clone()).picture(picture))
.child(div().text_xs().font_semibold().child(name)),
),
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
})
});
}