feat: in-room text chat over the gossip plane

Add room text chat riding the existing iroh-gossip topic (same layer as the
presence roster). New GossipMessage::Chat { name, text, ts }; the gossip loop
forwards it as RoomEvent::ChatMessage, core relays it to the UI as
UiEvent::ChatMessage, and RoomState::send_chat broadcasts an authored line
(display name from self-state, ms timestamp). CoreCommand::SendChat sends; our
own author is suppressed by the existing self-echo guard, so the UI echoes our
sent line locally instead.

UI: a full-width chat dock along the bottom of the room (the chosen layout) —
bottom-anchored scrollback with per-sender name colouring (green = you), an
input with Enter-to-send + a Send button, history capped at 300 lines. The room
window default grows to 900x760 so the dock doesn't squeeze the controls column.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 21:55:49 -04:00
co-authored by Claude Opus 4.8
parent c3cf00f46f
commit a6aca73c67
5 changed files with 179 additions and 6 deletions
+8
View File
@@ -42,6 +42,10 @@ pub enum RoomEvent {
/// from `PeerLeft` precisely so a momentary `NeighborDown` can't tear down the
/// reconnect path the way it used to.
PeerConnectionLost(EndpointId),
/// A peer sent a room text-chat message. Carries the sender's id, their
/// display name (embedded so it shows even without a presence entry), the
/// text, and a sender-stamped millisecond timestamp.
ChatMessage { from: EndpointId, name: String, text: String, ts: u64 },
}
/// Transport-level link state for a peer, surfaced so the UI can show when a
@@ -127,6 +131,10 @@ pub trait RoomState: Send + Sync {
/// Updates our local state (e.g. when user mutes/unmutes) and broadcasts it.
async fn update_self_state(&self, self_state: PeerState) -> Result<(), NetError>;
/// Broadcasts a room text-chat message authored by us (our display name is
/// taken from the current self-state).
async fn send_chat(&self, text: String) -> Result<(), NetError>;
/// Leaves the room and announces departure.
async fn leave(&self) -> Result<(), NetError>;