feat(w7): cosmetic room labels carried in the ticket (P5)
Rooms can now be named. A "Room name (optional)" field on the home Create card mints a ticket carrying the label; PeerSpeakTicket gains a #[serde(default)] `name` field (backward/forward compatible — serde ignores unknown fields and defaults missing ones, so old/new builds still interoperate, just without labels). restamp preserves the label so member-issued doors keep it; new label_of helper reads it. Every member (creator or joiner) sets current_room.name from the ticket, so presence reports a consistent "in <name>" to friends, and the room-screen header shows the label under the wordmark. Labels are sanitized via sanitize_name on both mint and display (untrusted peer-supplied ticket). CoreCommand::Join gains room_name (used only when creating). +2 ticket tests (label round-trip through restamp/label_of, pre-label backward-compat). clippy --all-targets clean, 257 lib tests green. Pure seam unit-tested + home field screenshot-verified; the in-room header label and friend-side "in HangOut" presence display need a live/2-machine confirm. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+43
-9
@@ -110,6 +110,7 @@ fn clamp_chat_drawer_width(width: f32, window_w: f32) -> f32 {
|
||||
pub enum AppMessage {
|
||||
NicknameChanged(String),
|
||||
TicketInputChanged(String),
|
||||
RoomNameChanged(String),
|
||||
JoinPressed,
|
||||
CreatePressed,
|
||||
LeavePressed,
|
||||
@@ -216,6 +217,9 @@ fn core_subscription() -> impl iced::futures::Stream<Item = UiEvent> {
|
||||
pub struct AppState {
|
||||
name: String,
|
||||
ticket_input: String,
|
||||
/// The optional cosmetic room label typed on the home "Create" card (W7).
|
||||
/// Carried in the minted ticket so joiners inherit it; empty = unnamed room.
|
||||
room_name_input: String,
|
||||
status_message: String,
|
||||
self_id: String,
|
||||
ticket: String,
|
||||
@@ -355,6 +359,7 @@ impl Default for AppState {
|
||||
// Pre-fill the nickname with the last one used (or "Peer" by default).
|
||||
name: config.username.clone(),
|
||||
ticket_input: "".to_string(),
|
||||
room_name_input: "".to_string(),
|
||||
status_message: "Ready to connect".to_string(),
|
||||
self_id: "".to_string(),
|
||||
ticket: "".to_string(),
|
||||
@@ -521,6 +526,9 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
||||
AppMessage::TicketInputChanged(val) => {
|
||||
state.ticket_input = val;
|
||||
}
|
||||
AppMessage::RoomNameChanged(val) => {
|
||||
state.room_name_input = val;
|
||||
}
|
||||
AppMessage::JoinPressed => {
|
||||
let input_device = state.selected_input.as_ref().map(|d| d.name.clone());
|
||||
let output_device = state.selected_output.as_ref().map(|d| d.name.clone());
|
||||
@@ -534,6 +542,7 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
||||
let _ = state.controller.send(CoreCommand::Join {
|
||||
name: state.name.clone(),
|
||||
ticket: state.ticket_input.clone(),
|
||||
room_name: String::new(), // joining: the label comes from the ticket
|
||||
input_device,
|
||||
output_device,
|
||||
echo_cancellation: state.config.echo_cancellation_enabled,
|
||||
@@ -553,6 +562,7 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
||||
let _ = state.controller.send(CoreCommand::Join {
|
||||
name: state.name.clone(),
|
||||
ticket: "create".to_string(),
|
||||
room_name: state.room_name_input.clone(),
|
||||
input_device,
|
||||
output_device,
|
||||
echo_cancellation: state.config.echo_cancellation_enabled,
|
||||
@@ -848,6 +858,7 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
||||
let _ = state.controller.send(CoreCommand::Join {
|
||||
name: state.name.clone(),
|
||||
ticket,
|
||||
room_name: String::new(), // joining: the label comes from the ticket
|
||||
input_device,
|
||||
output_device,
|
||||
echo_cancellation: state.config.echo_cancellation_enabled,
|
||||
@@ -1266,11 +1277,21 @@ fn connect_card(state: &AppState) -> Element<'_, AppMessage> {
|
||||
.padding(10)
|
||||
];
|
||||
|
||||
let create_btn = button(btn_content(IconKind::Create, "Create New Room", color_crust))
|
||||
.on_press(AppMessage::CreatePressed)
|
||||
.style(b_style(color_blue, color_lavender, color_crust, 8.0))
|
||||
.padding(12)
|
||||
.width(iced::Length::Fill);
|
||||
// Optional cosmetic room label (W7) above the Create button: it rides in the
|
||||
// minted ticket so everyone who joins inherits "in <name>". Enter also creates.
|
||||
let create_group = column![
|
||||
text_input("Room name (optional)", &state.room_name_input)
|
||||
.on_input(AppMessage::RoomNameChanged)
|
||||
.on_submit(AppMessage::CreatePressed)
|
||||
.style(t_style)
|
||||
.padding(10),
|
||||
vertical_space(8.0),
|
||||
button(btn_content(IconKind::Create, "Create New Room", color_crust))
|
||||
.on_press(AppMessage::CreatePressed)
|
||||
.style(b_style(color_blue, color_lavender, color_crust, 8.0))
|
||||
.padding(12)
|
||||
.width(iced::Length::Fill),
|
||||
];
|
||||
|
||||
let join_group = column![
|
||||
text("Join Existing Room").size(14).color(color_subtext),
|
||||
@@ -1296,7 +1317,7 @@ fn connect_card(state: &AppState) -> Element<'_, AppMessage> {
|
||||
vertical_space(20.0),
|
||||
nickname_input,
|
||||
vertical_space(16.0),
|
||||
create_btn,
|
||||
create_group,
|
||||
vertical_space(16.0),
|
||||
text("— OR —").size(12).color(color_surface).align_x(iced::alignment::Horizontal::Center),
|
||||
vertical_space(16.0),
|
||||
@@ -2154,10 +2175,23 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
||||
// --- ROOM SCREEN ---
|
||||
let participant_count = state.peers.len() + 1; // peers + you
|
||||
let call_secs = state.call_started.map(|t| t.elapsed().as_secs()).unwrap_or(0);
|
||||
// The room's cosmetic label (W7) rides in our share ticket; show it under
|
||||
// the wordmark when the room was named. Sanitized since a joined ticket is
|
||||
// peer-supplied.
|
||||
let room_label = crate::sanitize::sanitize_name(
|
||||
&crate::network::PeerSpeakTicket::label_of(&state.ticket),
|
||||
);
|
||||
let title: Element<'_, AppMessage> = if room_label.is_empty() {
|
||||
text("PEERSPEAK").size(20).color(color_blue).into()
|
||||
} else {
|
||||
column![
|
||||
text("PEERSPEAK").size(20).color(color_blue),
|
||||
text(room_label).size(13).color(color_subtext),
|
||||
]
|
||||
.into()
|
||||
};
|
||||
let header = row![
|
||||
text("PEERSPEAK")
|
||||
.size(20)
|
||||
.color(color_blue),
|
||||
title,
|
||||
horizontal_space(),
|
||||
row![
|
||||
icon(IconKind::People, 15.0, color_subtext),
|
||||
|
||||
Reference in New Issue
Block a user