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:
+41
-2
@@ -20,6 +20,9 @@ pub struct GossipPayload {
|
||||
pub enum GossipMessage {
|
||||
Announce(PeerState),
|
||||
Leave,
|
||||
/// A room text-chat message: the author's display name, the text, and a
|
||||
/// sender-stamped millisecond timestamp.
|
||||
Chat { name: String, text: String, ts: u64 },
|
||||
}
|
||||
|
||||
pub struct IrohGossipState {
|
||||
@@ -168,6 +171,15 @@ impl RoomState for IrohGossipState {
|
||||
let _ = event_tx.send(RoomEvent::PeerLeft(payload.author)).await;
|
||||
}
|
||||
}
|
||||
GossipMessage::Chat { name, text, ts } => {
|
||||
crate::log_msg(&format!("Gossip chat from author={:?}", payload.author));
|
||||
let _ = event_tx.send(RoomEvent::ChatMessage {
|
||||
from: payload.author,
|
||||
name,
|
||||
text,
|
||||
ts,
|
||||
}).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -240,6 +252,33 @@ impl RoomState for IrohGossipState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_chat(&self, text: String) -> Result<(), NetError> {
|
||||
let (name, author) = {
|
||||
let guard = self.self_state.lock().unwrap();
|
||||
match guard.as_ref() {
|
||||
Some(s) => (s.name.clone(), s.addr.id),
|
||||
None => return Err(NetError::Other("Not in a room".to_string())),
|
||||
}
|
||||
};
|
||||
let ts = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
|
||||
let sender_opt = self.active_sender.lock().unwrap().clone();
|
||||
if let Some(sender) = sender_opt {
|
||||
let payload = GossipPayload {
|
||||
author,
|
||||
msg: GossipMessage::Chat { name, text, ts },
|
||||
};
|
||||
if let Ok(bytes) = serde_json::to_vec(&payload) {
|
||||
sender.broadcast(bytes.into()).await
|
||||
.map_err(|e| NetError::Gossip(e.to_string()))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn leave(&self) -> Result<(), NetError> {
|
||||
crate::log_msg("RoomState::leave called");
|
||||
{
|
||||
@@ -328,8 +367,8 @@ mod tests {
|
||||
GossipMessage::Announce(state) => {
|
||||
assert_eq!(state, peer_state);
|
||||
}
|
||||
GossipMessage::Leave => {
|
||||
panic!("Expected GossipMessage::Announce, got Leave");
|
||||
GossipMessage::Leave | GossipMessage::Chat { .. } => {
|
||||
panic!("Expected GossipMessage::Announce");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user