feat: multi-account switcher #14

Merged
reya merged 6 commits from feat/multi-accounts-switcher into master 2026-03-02 08:08:05 +00:00
11 changed files with 103 additions and 122 deletions
Showing only changes of commit 0c7104de85 - Show all commits

3
assets/icons/group.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="8.75" r="3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><circle cx="4" cy="9.80005" r="2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><circle cx="20" cy="9.80005" r="2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M7.25 16.625V16.5C7.25 13.8766 9.37665 11.75 12 11.75C14.6234 11.75 16.75 13.8766 16.75 16.5V16.625C16.75 17.5225 16.0225 18.25 15.125 18.25H8.875C7.97754 18.25 7.25 17.5225 7.25 16.625Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M4.25 17.2602H2.75C1.64543 17.2602 0.706551 16.3538 0.919944 15.2701C1.25877 13.5493 2.15049 12.3257 4 11.8301" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M19.75 17.2601H21.25C22.3546 17.2601 23.2935 16.3538 23.08 15.27C22.7412 13.5493 21.8495 12.3257 20 11.8301" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -691,8 +691,7 @@ impl ChatRegistry {
}
/// Trigger a refresh of the opened chat rooms by their IDs
pub fn refresh_rooms(&mut self, ids: Option<Vec<u64>>, cx: &mut Context<Self>) {
if let Some(ids) = ids {
pub fn refresh_rooms(&mut self, ids: &[u64], cx: &mut Context<Self>) {
for room in self.rooms.iter() {
if ids.contains(&room.read(cx).id) {
room.update(cx, |this, cx| {
@@ -702,7 +701,6 @@ impl ChatRegistry {
}
}
}
}
/// Unwraps a gift-wrapped event and processes its contents.
async fn extract_rumor(

View File

@@ -103,7 +103,7 @@ impl ImportKey {
// Update the signer
nostr.update(cx, |this, cx| {
this.set_signer(keys, cx);
this.add_key_signer(&keys, cx);
});
} else {
self.set_error("Invalid key", cx);

View File

@@ -16,7 +16,7 @@ use nostr_sdk::prelude::*;
use person::PersonRegistry;
use smallvec::{smallvec, SmallVec};
use state::{NostrRegistry, FIND_DELAY};
use theme::{ActiveTheme, TABBAR_HEIGHT};
use theme::{ActiveTheme, SIDEBAR_WIDTH, TABBAR_HEIGHT};
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::indicator::Indicator;
@@ -585,10 +585,11 @@ impl Render for Sidebar {
)
.when(!show_find_panel && !loading && total_rooms == 0, |this| {
this.child(
div().px_2().child(
div().px_2().w(SIDEBAR_WIDTH).child(
v_flex()
.p_3()
.h_24()
.w_full()
.border_2()
.border_dashed()
.border_color(cx.theme().border_variant)

View File

@@ -11,7 +11,7 @@ use gpui::{
use person::PersonRegistry;
use serde::Deserialize;
use smallvec::{smallvec, SmallVec};
use state::{NostrRegistry, RelayState};
use state::{NostrRegistry, RelayState, SignerEvent};
use theme::{ActiveTheme, Theme, ThemeRegistry, SIDEBAR_WIDTH};
use title_bar::TitleBar;
use ui::avatar::Avatar;
@@ -42,6 +42,7 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> {
#[action(namespace = workspace, no_json)]
enum Command {
ToggleTheme,
ToggleAccount,
RefreshEncryption,
RefreshRelayList,
@@ -85,19 +86,19 @@ impl Workspace {
);
subscriptions.push(
// Observe the nostr entity
cx.observe_in(&nostr, window, move |this, nostr, window, cx| {
if nostr.read(cx).connected {
this.set_layout(window, cx);
// Observe the npubs entity
cx.observe_in(&npubs, window, move |this, npubs, window, cx| {
if !npubs.read(cx).is_empty() {
this.account_selector(window, cx);
}
}),
);
subscriptions.push(
// Observe the npubs entity
cx.observe_in(&npubs, window, move |this, npubs, window, cx| {
if !npubs.read(cx).is_empty() {
this.account_selector(window, cx);
// Subscribe to the signer events
cx.subscribe_in(&nostr, window, move |this, _state, event, window, cx| {
if let SignerEvent::Set = event {
this.set_center_layout(window, cx);
}
}),
);
@@ -141,11 +142,16 @@ impl Workspace {
let ids = this.panel_ids(cx);
chat.update(cx, |this, cx| {
this.refresh_rooms(ids, cx);
this.refresh_rooms(&ids, cx);
});
}),
);
// Set the layout at the end of cycle
cx.defer_in(window, |this, window, cx| {
this.set_layout(window, cx);
});
Self {
titlebar,
dock,
@@ -170,49 +176,40 @@ impl Workspace {
}
/// Get all panel ids
fn panel_ids(&self, cx: &App) -> Option<Vec<u64>> {
let ids: Vec<u64> = self
.dock
fn panel_ids(&self, cx: &App) -> Vec<u64> {
self.dock
.read(cx)
.items
.panel_ids(cx)
.into_iter()
.filter_map(|panel| panel.parse::<u64>().ok())
.collect();
Some(ids)
.collect()
}
/// Set the dock layout
fn set_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let weak_dock = self.dock.downgrade();
// Sidebar
let left = DockItem::panel(Arc::new(sidebar::init(window, cx)));
// Main workspace
let center = DockItem::split_with_sizes(
Axis::Vertical,
vec![DockItem::tabs(
vec![Arc::new(greeter::init(window, cx))],
None,
&weak_dock,
window,
cx,
)],
vec![None],
&weak_dock,
window,
cx,
);
// Update the dock layout
// Update the dock layout with sidebar on the left
self.dock.update(cx, |this, cx| {
this.set_left_dock(left, Some(SIDEBAR_WIDTH), true, window, cx);
});
}
/// Set the center dock layout
fn set_center_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let dock = self.dock.downgrade();
let greeeter = Arc::new(greeter::init(window, cx));
let tabs = DockItem::tabs(vec![greeeter], None, &dock, window, cx);
let center = DockItem::split(Axis::Vertical, vec![tabs], &dock, window, cx);
// Update the layout with center dock
self.dock.update(cx, |this, cx| {
this.set_center(center, window, cx);
});
}
/// Handle command events
fn on_command(&mut self, command: &Command, window: &mut Window, cx: &mut Context<Self>) {
match command {
Command::ShowSettings => {
@@ -305,6 +302,9 @@ impl Workspace {
Command::ToggleTheme => {
self.theme_selector(window, cx);
}
Command::ToggleAccount => {
self.account_selector(window, cx);
}
}
}
@@ -508,6 +508,11 @@ impl Workspace {
Box::new(Command::ToggleTheme),
)
.separator()
.menu_with_icon(
"Accounts",
IconName::Group,
Box::new(Command::ToggleAccount),
)
.menu_with_icon(
"Settings",
IconName::Settings,
@@ -516,19 +521,6 @@ impl Workspace {
}),
)
})
.when(nostr.read(cx).creating, |this| {
this.child(div().text_xs().text_color(cx.theme().text_muted).child(
SharedString::from("Coop is creating a new identity for you..."),
))
})
.when(!nostr.read(cx).connected, |this| {
this.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Connecting...")),
)
})
}
fn titlebar_right(&mut self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {

View File

@@ -40,10 +40,9 @@ pub const WOT_RELAYS: [&str; 1] = ["wss://relay.vertexlab.io"];
pub const SEARCH_RELAYS: [&str; 2] = ["wss://antiprimal.net", "wss://search.nos.today"];
/// Default bootstrap relays
pub const BOOTSTRAP_RELAYS: [&str; 4] = [
"wss://nos.lol",
"wss://relay.damus.io",
pub const BOOTSTRAP_RELAYS: [&str; 3] = [
"wss://relay.primal.net",
"wss://indexer.coracle.social",
"wss://user.kindpag.es",
];

View File

@@ -42,16 +42,6 @@ struct GlobalNostrRegistry(Entity<NostrRegistry>);
impl Global for GlobalNostrRegistry {}
/// Signer event.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum SignerEvent {
/// A new signer has been set
Set,
/// An error occurred
Error(String),
}
/// Nostr Registry
#[derive(Debug)]
pub struct NostrRegistry {
@@ -75,12 +65,6 @@ pub struct NostrRegistry {
/// Relay list state
pub relay_list_state: RelayState,
/// Whether Coop is connected to all bootstrap relays
pub connected: bool,
/// Whether Coop is creating a new signer
pub creating: bool,
/// Tasks for asynchronous operations
tasks: Vec<Task<Result<(), Error>>>,
}
@@ -140,8 +124,6 @@ impl NostrRegistry {
app_keys,
gossip,
relay_list_state: RelayState::Idle,
connected: false,
creating: false,
tasks: vec![],
}
}
@@ -161,12 +143,6 @@ impl NostrRegistry {
self.npubs.clone()
}
/// Set the connected status of the client
fn set_connected(&mut self, cx: &mut Context<Self>) {
self.connected = true;
cx.notify();
}
/// Connect to the bootstrapping relays
fn connect(&mut self, cx: &mut Context<Self>) {
let client = self.client();
@@ -185,13 +161,12 @@ impl NostrRegistry {
}
// Connect to all added relays
client.connect().and_wait(Duration::from_secs(5)).await;
client.connect().and_wait(Duration::from_secs(2)).await;
})
.await;
// Update the state
this.update(cx, |this, cx| {
this.set_connected(cx);
this.get_npubs(cx);
})?;
@@ -317,12 +292,6 @@ impl NostrRegistry {
}));
}
/// Set whether Coop is creating a new signer
fn set_creating(&mut self, creating: bool, cx: &mut Context<Self>) {
self.creating = creating;
cx.notify();
}
/// Create a new identity
fn create_identity(&mut self, cx: &mut Context<Self>) {
let client = self.client();
@@ -335,9 +304,6 @@ impl NostrRegistry {
// Create a write credential task
let write_credential = cx.write_credentials(&username, &username, &secret);
// Set the creating signer status
self.set_creating(true, cx);
// Run async tasks in background
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let signer = async_keys.into_nostr_signer();
@@ -394,8 +360,8 @@ impl NostrRegistry {
// Wait for the task to complete
task.await?;
// Set signer
this.update(cx, |this, cx| {
this.set_creating(false, cx);
this.set_signer(keys, cx);
})?;
@@ -416,8 +382,8 @@ impl NostrRegistry {
cx.spawn(async move |_cx| {
let (_, secret) = read_credential
.await
.map_err(|_| anyhow!("Failed to get signer"))?
.ok_or_else(|| anyhow!("Failed to get signer"))?;
.map_err(|_| anyhow!("Failed to get signer. Please re-import the secret key"))?
.ok_or_else(|| anyhow!("Failed to get signer. Please re-import the secret key"))?;
// Try to parse as a direct secret key first
if let Ok(secret_key) = SecretKey::from_slice(&secret) {
@@ -507,7 +473,7 @@ impl NostrRegistry {
let secret = keys.secret_key().to_secret_bytes();
// Write the credential to the keyring
let write_credential = cx.write_credentials(&username, &username, &secret);
let write_credential = cx.write_credentials(&username, "keys", &secret);
self.tasks.push(cx.spawn(async move |this, cx| {
match write_credential.await {
@@ -546,7 +512,7 @@ impl NostrRegistry {
Ok((public_key, uri)) => {
let username = public_key.to_bech32().unwrap();
let write_credential = this.read_with(cx, |_this, cx| {
cx.write_credentials(&username, &username, uri.to_string().as_bytes())
cx.write_credentials(&username, "nostrconnect", uri.to_string().as_bytes())
})?;
match write_credential.await {
@@ -1020,6 +986,16 @@ fn default_messaging_relays() -> Vec<RelayUrl> {
]
}
/// Signer event.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum SignerEvent {
/// A new signer has been set
Set,
/// An error occurred
Error(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum RelayState {
#[default]

View File

@@ -2,10 +2,11 @@ use std::sync::Arc;
use gpui::prelude::FluentBuilder;
use gpui::{
actions, div, px, AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Edges, Entity,
EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement, ParentElement as _,
Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window,
actions, div, px, AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Decorations,
Edges, Entity, EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement,
ParentElement as _, Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window,
};
use theme::CLIENT_SIDE_DECORATION_ROUNDING;
use crate::dock_area::dock::{Dock, DockPlacement};
use crate::dock_area::panel::{Panel, PanelEvent, PanelStyle, PanelView};
@@ -202,19 +203,16 @@ impl DockItem {
/// Returns all panel ids
pub fn panel_ids(&self, cx: &App) -> Vec<SharedString> {
match self {
Self::Tabs { view, .. } => view.read(cx).panel_ids(cx),
Self::Split { items, .. } => {
let mut total = vec![];
for item in items.iter() {
if let DockItem::Tabs { view, .. } = item {
total.extend(view.read(cx).panel_ids(cx));
}
}
total
}
Self::Panel { .. } => vec![],
Self::Tabs { view, .. } => view.read(cx).panel_ids(cx),
Self::Split { items, .. } => items
.iter()
.filter_map(|item| match item {
DockItem::Tabs { view, .. } => Some(view.read(cx).panel_ids(cx)),
_ => None,
})
.flatten()
.collect(),
}
}
@@ -745,6 +743,7 @@ impl EventEmitter<DockEvent> for DockArea {}
impl Render for DockArea {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity().clone();
let decorations = window.window_decorations();
div()
.id("dock-area")
@@ -754,7 +753,17 @@ impl Render for DockArea {
.on_prepaint(move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds))
.map(|this| {
if let Some(zoom_view) = self.zoom_view.clone() {
this.child(zoom_view)
this.map(|this| match decorations {
Decorations::Server => this,
Decorations::Client { tiling } => this
.when(!(tiling.top || tiling.right), |div| {
div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.top || tiling.left), |div| {
div.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING)
}),
})
.child(zoom_view)
} else {
// render dock
this.child(

View File

@@ -1080,11 +1080,13 @@ impl TabPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.panels.len() > 1 {
if let Some(panel) = self.active_panel(cx) {
self.remove_panel(&panel, window, cx);
}
}
}
}
impl Focusable for TabPanel {
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {

View File

@@ -60,6 +60,7 @@ pub enum IconName {
Sun,
Ship,
Shield,
Group,
UserKey,
Upload,
Usb,
@@ -133,6 +134,7 @@ impl IconNamed for IconName {
Self::UserKey => "icons/user-key.svg",
Self::Upload => "icons/upload.svg",
Self::Usb => "icons/usb.svg",
Self::Group => "icons/group.svg",
Self::PanelLeft => "icons/panel-left.svg",
Self::PanelLeftOpen => "icons/panel-left-open.svg",
Self::PanelRight => "icons/panel-right.svg",

View File

@@ -249,7 +249,6 @@ impl Render for Root {
div()
.id("window")
.size_full()
.bg(gpui::transparent_black())
.map(|div| match decorations {
Decorations::Server => div,
Decorations::Client { tiling } => div