Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions Makefile.toml
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,11 @@ target/native/x86_64-unknown-linux-gnu/${BUILD_PROFILE}/web-container-tool sign
description = "Build the Dioxus UI with example data"
env = { UI_FEATURES = "example-data" }
# build-chat-delegate is required because UI includes delegate WASM via include_bytes!
dependencies = ["build-chat-delegate"]
# build-tailwind is required because ui/assets/styles.css is gitignored (it is a
# build product). Without it `dx build` fails on a clean tree with
# "Asset at /assets/styles.css doesn't exist" — CI only gets away with omitting
# it here because build.yml runs `npm run build:css` as an explicit step.
dependencies = ["build-chat-delegate", "build-tailwind"]
command = "dx"
args = ["build", "--${BUILD_PROFILE}", "--features", "${UI_FEATURES}"]
cwd = "./ui"
Expand All @@ -259,7 +263,8 @@ cwd = "./ui"
description = "Build the Dioxus UI without Freenet sync"
env = { UI_FEATURES = "no-sync" }
# build-chat-delegate is required because UI includes delegate WASM via include_bytes!
dependencies = ["build-chat-delegate"]
# build-tailwind is required — see the note on build-ui-example.
dependencies = ["build-chat-delegate", "build-tailwind"]
command = "dx"
args = ["build", "--${BUILD_PROFILE}", "--features", "${UI_FEATURES}"]
cwd = "./ui"
Expand All @@ -268,7 +273,10 @@ cwd = "./ui"
description = "Build the Dioxus UI with example data and no Freenet sync"
env = { UI_FEATURES = "example-data,no-sync" }
# build-chat-delegate is required because UI includes delegate WASM via include_bytes!
dependencies = ["build-chat-delegate"]
# build-tailwind is required — see the note on build-ui-example. This is the
# task AGENTS.md tells you to run before the Playwright suite, so it has to
# work from a clean checkout.
dependencies = ["build-chat-delegate", "build-tailwind"]
command = "dx"
args = ["build", "--${BUILD_PROFILE}", "--features", "${UI_FEATURES}"]
cwd = "./ui"
Expand Down Expand Up @@ -787,7 +795,9 @@ dependencies = ["build-ui"]
description = "Development build"
env = { UI_FEATURES = "" }
# build-chat-delegate is required because UI includes delegate WASM via include_bytes!
dependencies = ["build-chat-delegate"]
# build-tailwind is required — see the note on build-ui-example. (dev-example
# already had it; this task was the odd one out.)
dependencies = ["build-chat-delegate", "build-tailwind"]
command = "dx"
args = ["serve"]
cwd = "./ui"
Expand Down
2 changes: 1 addition & 1 deletion ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ rand.workspace = true
getrandom = { version = "0.2.15", features = ["js", "wasm-bindgen", "js-sys"], default-features = false }

# UI Framework
dioxus = { version = "0.7.3", features = ["web"] }
dioxus = { version = "0.7.9", features = ["web"] }
# Only `fa_solid_icons` are referenced in the UI (verified by grep). Dropping
# the brands/regular feature sets trims their icon modules from the build.
dioxus-free-icons = { version = "0.10.0", features = ["font-awesome-solid"] }
Expand Down
100 changes: 88 additions & 12 deletions ui/src/components/room_list/edit_room_modal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use crate::components::app::{CURRENT_ROOM, EDIT_ROOM_MODAL, ROOMS};
use crate::util::ecies::{seal_for_room, unseal_bytes_with_secrets};
use dioxus::logger::tracing::{error, info, warn};
use dioxus::prelude::*;
use dioxus_free_icons::icons::fa_solid_icons::FaCopy;
use dioxus_free_icons::Icon;
use freenet_scaffold::ComposableState;
use river_core::room_state::configuration::{AuthorizedConfigurationV1, Configuration};
use river_core::room_state::privacy::{PrivacyMode, RoomDisplayMetadata};
Expand Down Expand Up @@ -150,12 +152,24 @@ pub fn EditRoomModal() -> Element {
title: "Ed25519 public key (Curve25519 elliptic curve)",
"Room Public Key"
}
input {
r#type: "text",
readonly: true,
title: "Ed25519 public key (Curve25519 elliptic curve)",
class: "w-full px-3 py-2 bg-surface border border-border rounded-lg text-text-muted text-sm font-mono cursor-text select-all",
value: "{bs58::encode(room_data.owner_vk.as_bytes()).into_string()}"
div {
class: "flex items-center gap-2",
input {
r#type: "text",
readonly: true,
"data-testid": "room-public-key-input",
title: "Ed25519 public key (Curve25519 elliptic curve)",
// `select-text`, NOT `select-all` — see the note on
// `CopyButton` below. `user-select: all` makes this
// field completely unselectable in Firefox.
class: "flex-1 min-w-0 px-3 py-2 bg-surface border border-border rounded-lg text-text-muted text-sm font-mono cursor-text select-text",
value: "{bs58::encode(room_data.owner_vk.as_bytes()).into_string()}"
}
CopyButton {
value: bs58::encode(room_data.owner_vk.as_bytes()).into_string(),
testid: "room-public-key-copy-button",
label: "Copy room public key",
}
}
}
// Contract ID
Expand All @@ -165,11 +179,21 @@ pub fn EditRoomModal() -> Element {
class: "block text-sm font-medium text-text-muted mb-1",
"Contract ID"
}
input {
r#type: "text",
readonly: true,
class: "w-full px-3 py-2 bg-surface border border-border rounded-lg text-text-muted text-sm font-mono cursor-text select-all",
value: "{room_data.contract_key.id()}"
div {
class: "flex items-center gap-2",
input {
r#type: "text",
readonly: true,
"data-testid": "contract-id-input",
// `select-text`, NOT `select-all` — see `CopyButton`.
class: "flex-1 min-w-0 px-3 py-2 bg-surface border border-border rounded-lg text-text-muted text-sm font-mono cursor-text select-text",
value: "{room_data.contract_key.id()}"
}
CopyButton {
value: room_data.contract_key.id().to_string(),
testid: "contract-id-copy-button",
label: "Copy contract ID",
}
}
}

Expand All @@ -193,7 +217,9 @@ pub fn EditRoomModal() -> Element {
input {
r#type: "text",
readonly: true,
class: "flex-1 px-3 py-2 bg-surface border border-border rounded-lg text-text-muted text-sm font-mono cursor-text select-all",
"data-testid": "secret-version-input",
// `select-text`, NOT `select-all` — see `CopyButton`.
class: "flex-1 min-w-0 px-3 py-2 bg-surface border border-border rounded-lg text-text-muted text-sm font-mono cursor-text select-text",
value: "{secret_version}"
}
if is_owner {
Expand Down Expand Up @@ -358,6 +384,56 @@ pub fn EditRoomModal() -> Element {
}
}

/// Copy-to-clipboard button for the read-only key fields above.
///
/// # Why those inputs are `select-text` and must never be `select-all`
///
/// The Room Public Key / Contract ID / Secret Version inputs used Tailwind's
/// `select-all` (`user-select: all`), which made them impossible to select or
/// copy by hand in Firefox: click-drag selected nothing, double-click selected
/// nothing (freenet/river#537). Firefox parses the declaration — the computed
/// value really is `all` — but selecting inside an `<input>` under it yields a
/// zero-length selection, so `Ctrl+C` copies nothing. Chromium and WebKit
/// instead select the whole value, which is why this looked Firefox-specific.
///
/// Measured with a standalone repro driven by Playwright (characters selected
/// by a click-drag across the field, then by a double-click):
///
/// | input rule | Firefox | Chromium | WebKit |
/// |---------------------------|---------|----------|--------|
/// | `user-select: all` | **0** | 44 | 44 |
/// | `user-select: text` | 44 | 44 | 44 |
///
/// So the fields declare `select-text` explicitly rather than relying on the
/// `auto` default. Do NOT "restore" `select-all` — it re-breaks Firefox.
/// Pinned by `ui/tests/room-info-key-selection.spec.ts`, which runs against
/// Firefox in CI.
///
/// This is a child component rather than inline markup because the "Copied!"
/// feedback needs `use_signal`, and the fields render inside an `if let`
/// branch where a hook call would be conditional.
#[component]
fn CopyButton(value: String, testid: String, label: String) -> Element {
let mut copied = use_signal(|| false);
let value_for_clipboard = value.clone();

rsx! {
button {
r#type: "button",
"data-testid": "{testid}",
"aria-label": "{label}",
title: "{label}",
class: "flex-shrink-0 px-3 py-2 bg-surface hover:bg-surface-hover border border-border rounded-lg text-text-muted hover:text-text text-sm transition-colors flex items-center gap-1.5",
onclick: move |_| {
crate::util::copy_to_clipboard(&value_for_clipboard);
copied.set(true);
},
Icon { icon: FaCopy, width: 12, height: 12 }
span { if *copied.read() { "Copied!" } else { "Copy" } }
}
}
}

#[component]
fn RoomDescriptionField(config: Configuration, is_owner: bool) -> Element {
let initial_desc = {
Expand Down
Loading
Loading