Skip to content
Merged
15 changes: 15 additions & 0 deletions crates/client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,21 @@ impl ClientHandle {
!self.shared.borrow().state.servers.is_empty()
}

/// Remove a server from the local state and persist the change.
///
/// If the removed server was active, switches to the first remaining
/// server (or clears the active server if none remain).
pub fn leave_server(&self, server_id: &str) {
let mut shared = self.shared.borrow_mut();
shared.state.servers.remove(server_id);
if shared.state.active_server.as_deref() == Some(server_id) {
shared.state.active_server = shared.state.servers.keys().next().cloned();
}
// Persist updated server list.
let ids: Vec<String> = shared.state.servers.keys().cloned().collect();
storage::save_server_list(&ids);
}

/// Create a brand-new server with the local user as owner.
///
/// Automatically creates a "general" text channel, initializes the
Expand Down
86 changes: 72 additions & 14 deletions crates/web/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ use send_wrapper::SendWrapper;
use willow_client::{ClientConfig, ClientEvent, ClientHandle, DisplayMessage, VoiceSignalPayload};

use crate::components::{
AddServerPanel, ChannelHeader, ChatInput, FileShareButton, MemberList, MessageList,
PinnedPanel, ServerList, ServerSettingsPanel, SettingsPanel, Sidebar, WelcomeScreen,
AddServerPanel, ChannelHeader, ChatInput, CommandPalette, FileShareButton, MemberList,
MessageList, PinnedPanel, ServerList, SettingsPanel, Sidebar, WelcomeScreen,
};
use crate::event_processing::{extract_roles, process_event_batch, refresh_all_signals};
use crate::handlers;
use crate::state::{self, ChannelViewState};
use crate::icons;
use crate::state::{self, ChannelViewState, SettingsTab};
use crate::voice::VoiceManager;

// Notification sounds disabled for now.
Expand Down Expand Up @@ -99,6 +100,30 @@ pub fn App() -> impl IntoView {
);
}

// Register Ctrl+K / Cmd+K for command palette.
{
use wasm_bindgen::JsCast;
let write_for_palette = write;
let closure = wasm_bindgen::closure::Closure::<dyn Fn(web_sys::KeyboardEvent)>::new(
move |ev: web_sys::KeyboardEvent| {
if (ev.ctrl_key() || ev.meta_key()) && ev.key() == "k" {
ev.prevent_default();
write_for_palette
.ui
.set_show_palette
.update(|v| *v = !*v);
}
},
);
if let Some(window) = web_sys::window() {
let _ = window.add_event_listener_with_callback(
"keydown",
closure.as_ref().unchecked_ref(),
);
}
closure.forget();
}

// Populate initial state from the client.
refresh_all_signals(&handle, &write);

Expand Down Expand Up @@ -223,7 +248,6 @@ pub fn App() -> impl IntoView {
let servers = app_state.server.servers;
let show_sidebar = app_state.ui.show_sidebar;
let show_add_server = app_state.ui.show_add_server;
let show_server_settings = app_state.ui.show_server_settings;
let show_settings = app_state.ui.show_settings;
let show_pinned = app_state.ui.show_pinned;
let show_members = app_state.ui.show_members;
Expand All @@ -239,6 +263,7 @@ pub fn App() -> impl IntoView {
let replying_to = app_state.chat.replying_to;
let editing = app_state.chat.editing;
let channel_views = app_state.chat.channel_views;
let show_palette = app_state.ui.show_palette;

// Pre-clone handle for use inside the view closure.
let handle_for_voice_join = handle.clone();
Expand All @@ -259,6 +284,8 @@ pub fn App() -> impl IntoView {
} else {
let ch_click = on_channel_click.clone();
let srv_click = on_server_click.clone();
let ch_click_for_palette = on_channel_click.clone();
let srv_click_for_palette = on_server_click.clone();
let send = on_send.clone();
let edit_send = on_edit_send.clone();
let del_msg = on_delete_msg.clone();
Expand All @@ -280,9 +307,14 @@ pub fn App() -> impl IntoView {
on_add_server_click=move |_| {
write.ui.set_show_add_server.update(|v| *v = !*v);
write.ui.set_show_settings.set(false);
write.ui.set_show_server_settings.set(false);
write.ui.set_show_sidebar.set(false);
}
on_open_settings=Callback::new(move |_| {
write.ui.set_settings_tab.set(SettingsTab::Server);
write.ui.set_show_settings.set(true);
write.ui.set_show_add_server.set(false);
write.ui.set_show_sidebar.set(false);
})
/>
// Overlay to close sidebar on mobile tap
<div
Expand All @@ -299,13 +331,15 @@ pub fn App() -> impl IntoView {
server_name=app_state.server.active_server_name
on_channel_click=ch_click
on_settings_click=move |_| {
write.ui.set_show_settings.update(|v| *v = !*v);
write.ui.set_show_server_settings.set(false);
write.ui.set_settings_tab.set(SettingsTab::Profile);
write.ui.set_show_settings.set(true);
write.ui.set_show_add_server.set(false);
write.ui.set_show_sidebar.set(false);
}
on_server_settings_click=move |_| {
write.ui.set_show_server_settings.update(|v| *v = !*v);
write.ui.set_show_settings.set(false);
write.ui.set_settings_tab.set(SettingsTab::Server);
write.ui.set_show_settings.set(true);
write.ui.set_show_add_server.set(false);
write.ui.set_show_sidebar.set(false);
}
on_voice_join={
Expand Down Expand Up @@ -376,7 +410,7 @@ pub fn App() -> impl IntoView {
<div class="settings-panel">
<div class="server-settings-header">
<button class="btn btn-sm" on:click=move |_| write.ui.set_show_add_server.set(false)>
"\u{2190} Back"
{icons::icon_arrow_left()} " Back"
</button>
<h2>"Add a Server"</h2>
</div>
Expand All @@ -388,12 +422,10 @@ pub fn App() -> impl IntoView {
/>
</div>
}.into_any()
} else if show_server_settings.get() {
view! { <ServerSettingsPanel peer_id=peer_id roles=Signal::from(roles) on_back=move |_| write.ui.set_show_server_settings.set(false) /> }.into_any()
} else if show_settings.get() {
view! { <SettingsPanel peer_id=peer_id on_server_settings=move |_| {
let tab = app_state.ui.settings_tab.get_untracked();
view! { <SettingsPanel peer_id=peer_id roles=Signal::from(roles) default_tab=tab on_close=move |_| {
write.ui.set_show_settings.set(false);
write.ui.set_show_server_settings.set(true);
} /> }.into_any()
} else {
let send2 = send.clone();
Expand All @@ -420,6 +452,7 @@ pub fn App() -> impl IntoView {
on_menu_click=move |_| write.ui.set_show_sidebar.update(|v| *v = !*v)
on_members_click=move |_| write.ui.set_show_members.update(|v| *v = !*v)
on_pinned_click=Callback::new(move |_| write.ui.set_show_pinned.update(|v| *v = !*v))
on_search_click=Callback::new(move |_| write.ui.set_show_palette.set(true))
/>
{move || {
if show_pinned.get() {
Expand Down Expand Up @@ -507,6 +540,31 @@ pub fn App() -> impl IntoView {
peer_id=peer_id
/>
</div>
{move || {
if show_palette.get() {
let ch_click_palette = ch_click_for_palette.clone();
let srv_click_palette = srv_click_for_palette.clone();
Some(view! {
<CommandPalette
on_close=Callback::new(move |_| write.ui.set_show_palette.set(false))
on_switch_channel=Callback::new(move |name: String| {
ch_click_palette(name);
write.ui.set_show_palette.set(false);
})
on_switch_server=Callback::new(move |id: String| {
srv_click_palette(id);
write.ui.set_show_palette.set(false);
})
on_open_members=Callback::new(move |_| {
write.ui.set_show_members.set(true);
write.ui.set_show_palette.set(false);
})
/>
})
} else {
None
}
}}
</div>
}.into_any()
}
Expand Down
5 changes: 3 additions & 2 deletions crates/web/src/components/add_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use leptos::prelude::*;
use send_wrapper::SendWrapper;

use crate::app::WebClientHandle;
use crate::icons;

/// Panel for creating a new server or joining an existing one via invite code.
/// Shown when the user clicks the "+" button in the server rail.
Expand Down Expand Up @@ -129,7 +130,7 @@ pub fn AddServerPanel(on_done: impl Fn(()) + Send + Clone + 'static) -> impl Int
/>
<div class="join-profile-buttons">
<button class="btn btn-sm" on:click=move |_| set_join_step.set(false)>
"\u{2190} Back"
{icons::icon_arrow_left()} " Back"
</button>
<button class="btn btn-primary welcome-btn" on:click=confirm>
"Join Server"
Expand All @@ -148,7 +149,7 @@ pub fn AddServerPanel(on_done: impl Fn(()) + Send + Clone + 'static) -> impl Int
on:input=move |ev| set_join_code.set(event_target_value(&ev))
></textarea>
<button class="btn btn-primary welcome-btn" on:click=on_join_next>
"Next \u{2192}"
"Next " {icons::icon_arrow_right()}
</button>
</div>
}.into_any()
Expand Down
26 changes: 13 additions & 13 deletions crates/web/src/components/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use leptos::prelude::*;
use willow_client::DisplayMessage;

use super::MessageView;
use crate::icons;

/// Header bar showing the current channel name and connected peer count.
#[component]
Expand All @@ -11,30 +12,29 @@ pub fn ChannelHeader(
on_menu_click: impl Fn(()) + Send + Clone + 'static,
on_members_click: impl Fn(()) + Send + Clone + 'static,
#[prop(optional, into)] on_pinned_click: Option<Callback<()>>,
/// Called when the user clicks the search icon (opens command palette).
#[prop(optional, into)]
on_search_click: Option<Callback<()>>,
) -> impl IntoView {
view! {
<div class="channel-header">
<button class="mobile-nav-toggle" on:click=move |_| on_menu_click(())>
"="
{icons::icon_menu()}
</button>
<span>"# " {move || channel.get()}</span>
<span>{icons::icon_hash()} " " {move || channel.get()}</span>
<span class="channel-header-right">
{on_search_click.map(|cb| view! {
<button class="search-toggle" title="Search (Ctrl+K)" on:click=move |_| cb.run(())>
{icons::icon_search()}
</button>
})}
{on_pinned_click.map(|cb| view! {
<button class="pinned-toggle" title="Pinned Messages" on:click=move |_| cb.run(())>
"\u{1F4CC}"
{icons::icon_pin()}
</button>
})}
<span class="peer-count">
{move || {
let n = peer_count.get();
if n == 1 { "1 peer".to_string() } else { format!("{n} peers") }
}}
</span>
<button class="mobile-members-toggle" on:click=move |_| on_members_click(())>
{move || {
let n = peer_count.get();
format!("\u{1f465} {n}")
}}
{icons::icon_users()} " " {move || peer_count.get().to_string()}
</button>
</span>
</div>
Expand Down
Loading