4 Commits
Author SHA1 Message Date
reya 690dc37fc5 chore: fix web build 2026-11-01 16:40:49 +07:00
reya 2f2661ecd2 chore: improve native theme usage 2026-11-01 16:15:28 +07:00
reya d045a7ef3a chore: update the unlock dialog 2026-10-30 20:17:37 +07:00
reya 23d7017f25 feat: add text to speech (#69)
Reviewed-on: #69
2026-10-30 07:16:46 +00:00
7 changed files with 142 additions and 82 deletions
+3 -2
View File
@@ -39,8 +39,9 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install Rust nightly - name: Install Rust nightly
uses: dtolnay/rust-toolchain@nightly uses: dtolnay/rust-toolchain@master
with: with:
toolchain: nightly-2026-10-31
targets: wasm32-unknown-unknown targets: wasm32-unknown-unknown
components: llvm-tools-preview components: llvm-tools-preview
@@ -51,4 +52,4 @@ jobs:
env: env:
CC_wasm32_unknown_unknown: clang CC_wasm32_unknown_unknown: clang
AR_wasm32_unknown_unknown: llvm-ar AR_wasm32_unknown_unknown: llvm-ar
run: cargo +nightly check -p coop_web --target wasm32-unknown-unknown run: cargo +nightly-2026-10-31 check -p coop_web --target wasm32-unknown-unknown
+28 -9
View File
@@ -330,22 +330,40 @@ impl AppSettings {
}); });
} }
/// Apply the persisted theme tuning on top of the active theme
pub fn apply_theme_style(cx: &mut App) { pub fn apply_theme_style(cx: &mut App) {
Self::apply_theme_tuning(Self::get_theme(cx), cx);
}
fn apply_theme_tuning(tuning: ThemeSettings, cx: &mut App) {
use gpui_kit::component::Theme; use gpui_kit::component::Theme;
let theme = Self::get_theme(cx); {
let styled = Theme::global_mut(cx); let styled = Theme::global_mut(cx);
styled.font_size = px(theme.font_size); styled.font_size = px(tuning.font_size);
styled.focus_ring = theme.focus_ring; styled.focus_ring = tuning.focus_ring;
if let Some(radius) = theme.radius { if let Some(radius) = tuning.radius {
styled.radius = px(radius); styled.radius = px(radius);
} }
if let Some(radius_lg) = theme.radius_lg { if let Some(radius_lg) = tuning.radius_lg {
styled.radius_lg = px(radius_lg); styled.radius_lg = px(radius_lg);
} }
Theme::sync_base(cx); }
cx.refresh_windows();
Theme::update(cx, |styled| {
let background = styled.background;
let foreground = styled.foreground;
let (hover_alpha, active_alpha) = if styled.is_dark() {
(0.08, 0.12)
} else {
(0.04, 0.06)
};
if styled.list_hover == background {
styled.list_hover = foreground.alpha(hover_alpha);
}
if styled.tab_active == background {
styled.tab_active = foreground.alpha(active_alpha);
}
});
} }
/// Load settings /// Load settings
@@ -416,7 +434,8 @@ impl AppSettings {
AppearanceMode::Light => Theme::change(gpui_kit::component::ThemeMode::Light, None, cx), AppearanceMode::Light => Theme::change(gpui_kit::component::ThemeMode::Light, None, cx),
AppearanceMode::Dark => Theme::change(gpui_kit::component::ThemeMode::Dark, None, cx), AppearanceMode::Dark => Theme::change(gpui_kit::component::ThemeMode::Dark, None, cx),
} }
cx.refresh_windows(); let tuning = self.inner.read(cx).theme;
Self::apply_theme_tuning(tuning, cx);
} }
/// Check if decoupling encryption key is enabled /// Check if decoupling encryption key is enabled
+2
View File
@@ -30,6 +30,7 @@ pub enum IconName {
Search, Search,
Settings, Settings,
Settings2, Settings2,
Unlock,
Upload, Upload,
Warning, Warning,
} }
@@ -63,6 +64,7 @@ impl IconNamed for IconName {
Self::Search => "icons/search.svg", Self::Search => "icons/search.svg",
Self::Settings => "icons/settings.svg", Self::Settings => "icons/settings.svg",
Self::Settings2 => "icons/settings2.svg", Self::Settings2 => "icons/settings2.svg",
Self::Unlock => "icons/unlock.svg",
Self::Upload => "icons/upload.svg", Self::Upload => "icons/upload.svg",
Self::Warning => "icons/warning.svg", Self::Warning => "icons/warning.svg",
} }
+68 -24
View File
@@ -1,15 +1,18 @@
use anyhow::Error; use anyhow::Error;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled, App, AppContext, Context, Div, Entity, IntoElement, ParentElement, Render, SharedString,
Subscription, Task, Window, div, px, relative, Styled, Subscription, Task, Window, div, px, relative,
}; };
use gpui_kit::component::button::{Button, ButtonVariants}; use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dialog::{DialogDescription, DialogFooter, DialogHeader, DialogTitle};
use gpui_kit::component::form::{field, v_form};
use gpui_kit::component::input::{Input, InputEvent, InputState}; use gpui_kit::component::input::{Input, InputEvent, InputState};
use gpui_kit::component::{ActiveTheme, Disableable, WindowExt, v_flex}; use gpui_kit::component::{ActiveTheme, Disableable, WindowExt, v_flex};
use instant::Duration; use instant::Duration;
use nostr_connect::prelude::*; use nostr_connect::prelude::*;
use state::{NostrRegistry, StateEvent}; use state::{NostrRegistry, StateEvent};
use ui::IconName;
pub fn open(window: &mut Window, cx: &mut App) { pub fn open(window: &mut Window, cx: &mut App) {
let import = cx.new(|cx| ImportIdentity::new(window, cx)); let import = cx.new(|cx| ImportIdentity::new(window, cx));
@@ -32,7 +35,6 @@ pub fn open_unlock(window: &mut Window, cx: &mut App) {
this.close_button(false) this.close_button(false)
.overlay_closable(false) .overlay_closable(false)
.keyboard(false) .keyboard(false)
.title("Unlock your identity")
.child(import.clone()) .child(import.clone())
}); });
} }
@@ -74,7 +76,15 @@ impl ImportIdentity {
fn build(mode: Mode, window: &mut Window, cx: &mut Context<Self>) -> Self { fn build(mode: Mode, window: &mut Window, cx: &mut Context<Self>) -> Self {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let key_input = cx.new(|cx| InputState::new(window, cx).placeholder("secret or bunker://")); let key_input = cx.new(|cx| InputState::new(window, cx).placeholder("secret or bunker://"));
let pass_input = cx.new(|cx| InputState::new(window, cx).masked(true)); let pass_input = cx.new(|cx| {
let input = InputState::new(window, cx).masked(true);
if mode == Mode::Unlock {
input.placeholder("Passphrase to unlock your identity")
} else {
input
}
});
let repass_input = cx.new(|cx| InputState::new(window, cx).masked(true)); let repass_input = cx.new(|cx| InputState::new(window, cx).masked(true));
let error = cx.new(|_| None); let error = cx.new(|_| None);
@@ -235,15 +245,59 @@ impl ImportIdentity {
Ok(()) Ok(())
})); }));
} }
fn render_unlock(&self, cx: &mut Context<Self>) -> Div {
v_flex()
.gap_4()
.child(
DialogHeader::new()
.child(DialogTitle::new().child("Unlock your identity"))
.child(
DialogDescription::new()
.child("Enter the passphrase used to encrypt this identity."),
),
)
.child(
v_form().child(
field()
.label("Passphrase")
.required(true)
.child(Input::new(&self.pass_input)),
),
)
.when_some(self.error.read(cx).as_ref(), |this, error| {
this.child(
div()
.text_sm()
.text_color(cx.theme().danger)
.child(error.clone()),
)
})
.child(
DialogFooter::new().child(
Button::new("unlock")
.primary()
.icon(IconName::Unlock)
.label("Unlock")
.loading(self.loading)
.disabled(self.loading)
.on_click(cx.listener(move |this, _ev, _window, cx| {
this.login(cx);
})),
),
)
}
} }
impl Render for ImportIdentity { impl Render for ImportIdentity {
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
if self.mode == Mode::Unlock {
return self.render_unlock(cx);
}
const BUNKER_WARN: &str = "Nostr Connect will usually take more time to get all your messages. Please keep your session open until you see all your messages."; const BUNKER_WARN: &str = "Nostr Connect will usually take more time to get all your messages. Please keep your session open until you see all your messages.";
const KEY_WARN: &str = "Coop encrypts your key with your passphrase and stores it on this device. You only need the passphrase to sign in next time."; const KEY_WARN: &str = "Coop encrypts your key with your passphrase and stores it on this device. You only need the passphrase to sign in next time.";
let is_unlock = self.mode == Mode::Unlock;
let value = self.key_input.read(cx).value().trim().to_string(); let value = self.key_input.read(cx).value().trim().to_string();
let require_password = value.starts_with("ncryptsec1"); let require_password = value.starts_with("ncryptsec1");
let require_encrypt = value.starts_with("nsec1"); let require_encrypt = value.starts_with("nsec1");
@@ -257,25 +311,19 @@ impl Render for ImportIdentity {
.child( .child(
v_flex() v_flex()
.gap_2() .gap_2()
.when(!is_unlock, |this| { .child(
this.child(
v_flex() v_flex()
.gap_1() .gap_1()
.text_color(cx.theme().muted_foreground) .text_color(cx.theme().muted_foreground)
.child("Use secret key or bunker connection") .child("Use secret key or bunker connection")
.child(Input::new(&self.key_input)), .child(Input::new(&self.key_input)),
) )
}) .when(require_password, |this| {
.when(is_unlock || require_password, |this| {
this.child( this.child(
v_flex() v_flex()
.gap_1() .gap_1()
.text_color(cx.theme().muted_foreground) .text_color(cx.theme().muted_foreground)
.child(if is_unlock { .child("Decrypt Password:")
"Passphrase to unlock your identity:"
} else {
"Decrypt Password:"
})
.child(Input::new(&self.pass_input)), .child(Input::new(&self.pass_input)),
) )
}) })
@@ -323,7 +371,7 @@ impl Render for ImportIdentity {
.child({ .child({
let content = v_flex().gap_1().child( let content = v_flex().gap_1().child(
Button::new("login") Button::new("login")
.label(if is_unlock { "Unlock" } else { "Continue" }) .label("Continue")
.primary() .primary()
.loading(self.loading) .loading(self.loading)
.disabled(self.loading) .disabled(self.loading)
@@ -333,8 +381,7 @@ impl Render for ImportIdentity {
); );
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
let content = content.when(!is_unlock, |this| { let content = content.child(
this.child(
Button::new("proxy") Button::new("proxy")
.label("Connect via Web Extension") .label("Connect via Web Extension")
.ghost() .ghost()
@@ -342,12 +389,10 @@ impl Render for ImportIdentity {
.on_click(cx.listener(move |this, _ev, _window, cx| { .on_click(cx.listener(move |this, _ev, _window, cx| {
this.proxy(cx); this.proxy(cx);
})), })),
) );
});
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
let content = content.when(!is_unlock, |this| { let content = content.child(
this.child(
Button::new("extension") Button::new("extension")
.label("Continue with Browser Extension") .label("Continue with Browser Extension")
.ghost() .ghost()
@@ -355,8 +400,7 @@ impl Render for ImportIdentity {
.on_click(cx.listener(move |this, _ev, _window, cx| { .on_click(cx.listener(move |this, _ev, _window, cx| {
this.extension(cx); this.extension(cx);
})), })),
) );
});
content content
}) })
+3 -2
View File
@@ -91,11 +91,12 @@ impl Workspace {
let workspace = cx.entity().downgrade(); let workspace = cx.entity().downgrade();
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
if AppSettings::get_appearance_mode(cx) == AppearanceMode::System {
subscriptions.push(cx.observe_window_appearance(window, |_this, window, cx| { subscriptions.push(cx.observe_window_appearance(window, |_this, window, cx| {
if AppSettings::get_appearance_mode(cx) == AppearanceMode::System {
Theme::sync_system_appearance(Some(window), cx); Theme::sync_system_appearance(Some(window), cx);
})); AppSettings::apply_theme_style(cx);
} }
}));
subscriptions.push( subscriptions.push(
// Subscribe to the nostr events // Subscribe to the nostr events
+4 -11
View File
@@ -72,14 +72,13 @@ fn main() {
let appearance = settings::persisted_appearance_mode(); let appearance = settings::persisted_appearance_mode();
let font_size = settings::persisted_theme().font_size; let font_size = settings::persisted_theme().font_size;
match apply_native_theme(appearance, font_size, cx) {
Ok(()) => start_theme_watcher(appearance, cx), if let Err(error) = apply_native_theme(appearance, font_size, cx) {
Err(error) => {
log::warn!("native theme unavailable ({error})"); log::warn!("native theme unavailable ({error})");
apply_appearance(appearance, cx); apply_appearance(appearance, cx);
AppSettings::apply_theme_style(cx); AppSettings::apply_theme_style(cx);
} }
} start_theme_watcher(cx);
// Initialize the nostr client // Initialize the nostr client
state::init(cx, cli_key); state::init(cx, cli_key);
@@ -213,14 +212,12 @@ fn apply_appearance(appearance: AppearanceMode, cx: &mut App) {
/// Holds the watcher subscription so it lives as long as the app. /// Holds the watcher subscription so it lives as long as the app.
struct ThemeWatcher { struct ThemeWatcher {
_subscription: ThemeSubscription, _subscription: ThemeSubscription,
/// The appearance mode the observer in `workspace` was registered with.
appearance: std::cell::Cell<AppearanceMode>,
} }
impl Global for ThemeWatcher {} impl Global for ThemeWatcher {}
/// Start a background task to watch for OS theme changes and re-apply the native theme. /// Start a background task to watch for OS theme changes and re-apply the native theme.
fn start_theme_watcher(initial_appearance: AppearanceMode, cx: &mut App) { fn start_theme_watcher(cx: &mut App) {
let (sender, receiver) = flume::unbounded(); let (sender, receiver) = flume::unbounded();
match native_theme::watch::on_theme_change(move |event| { match native_theme::watch::on_theme_change(move |event| {
if let Err(error) = sender.send(event) { if let Err(error) = sender.send(event) {
@@ -230,7 +227,6 @@ fn start_theme_watcher(initial_appearance: AppearanceMode, cx: &mut App) {
Ok(subscription) => { Ok(subscription) => {
cx.set_global(ThemeWatcher { cx.set_global(ThemeWatcher {
_subscription: subscription, _subscription: subscription,
appearance: std::cell::Cell::new(initial_appearance),
}); });
cx.spawn(async move |cx| { cx.spawn(async move |cx| {
while receiver.recv_async().await.is_ok() { while receiver.recv_async().await.is_ok() {
@@ -238,9 +234,6 @@ fn start_theme_watcher(initial_appearance: AppearanceMode, cx: &mut App) {
cx.update(|cx| { cx.update(|cx| {
let appearance = AppSettings::get_appearance_mode(cx); let appearance = AppSettings::get_appearance_mode(cx);
let font_size = AppSettings::get_theme(cx).font_size; let font_size = AppSettings::get_theme(cx).font_size;
if let Some(watcher) = cx.try_global::<ThemeWatcher>() {
watcher.appearance.set(appearance);
}
if let Err(error) = apply_native_theme(appearance, font_size, cx) { if let Err(error) = apply_native_theme(appearance, font_size, cx) {
log::warn!("failed to apply updated OS theme ({error})"); log::warn!("failed to apply updated OS theme ({error})");
} }
+5 -5
View File
@@ -21,12 +21,12 @@ if [[ "$1" == "--release" ]]; then
echo -e "${YELLOW}Building in release mode${NC}" echo -e "${YELLOW}Building in release mode${NC}"
fi fi
# wasm_thread requires a nightly compiler for its atomics features NIGHTLY="nightly-2026-10-31"
CARGO="cargo +nightly" CARGO="cargo +$NIGHTLY"
# Step 0: Toolchain prerequisites # Step 0: Toolchain prerequisites
echo -e "${GREEN}Step 0: Checking toolchain...${NC}" echo -e "${GREEN}Step 0: Checking toolchain...${NC}"
rustup target add wasm32-unknown-unknown --toolchain nightly rustup target add wasm32-unknown-unknown --toolchain "$NIGHTLY"
if [[ "$(uname)" == "Darwin" ]]; then if [[ "$(uname)" == "Darwin" ]]; then
# secp256k1-sys compiles C for wasm; Apple clang has the wasm32 target and # secp256k1-sys compiles C for wasm; Apple clang has the wasm32 target and
@@ -35,7 +35,7 @@ if [[ "$(uname)" == "Darwin" ]]; then
echo -e "${RED}Apple clang not found. Install the Xcode command line tools: xcode-select --install${NC}" echo -e "${RED}Apple clang not found. Install the Xcode command line tools: xcode-select --install${NC}"
exit 1 exit 1
fi fi
rustup component add llvm-tools-preview --toolchain nightly rustup component add llvm-tools-preview --toolchain "$NIGHTLY"
RUSTUP_HOME=$(rustup show home) RUSTUP_HOME=$(rustup show home)
if [[ "$(uname -m)" == "arm64" ]]; then if [[ "$(uname -m)" == "arm64" ]]; then
@@ -43,7 +43,7 @@ if [[ "$(uname)" == "Darwin" ]]; then
else else
HOST_ARCH="x86_64-apple-darwin" HOST_ARCH="x86_64-apple-darwin"
fi fi
LLVM_BIN="$RUSTUP_HOME/toolchains/nightly-$HOST_ARCH/lib/rustlib/$HOST_ARCH/bin" LLVM_BIN="$RUSTUP_HOME/toolchains/$NIGHTLY-$HOST_ARCH/lib/rustlib/$HOST_ARCH/bin"
export CC_wasm32_unknown_unknown=clang export CC_wasm32_unknown_unknown=clang
export AR_wasm32_unknown_unknown="$LLVM_BIN/llvm-ar" export AR_wasm32_unknown_unknown="$LLVM_BIN/llvm-ar"