diff --git a/crates/sprout-relay/src/handlers/event.rs b/crates/sprout-relay/src/handlers/event.rs index 18382e53f..0c0b9b064 100644 --- a/crates/sprout-relay/src/handlers/event.rs +++ b/crates/sprout-relay/src/handlers/event.rs @@ -360,9 +360,35 @@ async fn handle_ephemeral_event( return; } + // Mark as local before Redis publish to prevent double-delivery when + // the event comes back through the Redis subscriber loop. + state.mark_local_event(&event.id); + if let Err(e) = state.pubsub.publish_event(ch_id, &event).await { + state.local_event_ids.invalidate(&event.id.to_bytes()); warn!(conn_id = %conn_id, event_id = %event_id_hex, "Ephemeral publish failed: {e}"); } + + // Direct fan-out to local WS subscribers. + // Pass the channel_id so fan_out() uses the channel-kind index. + let stored_event = StoredEvent::new(event.clone(), Some(ch_id)); + let matches = state.sub_registry.fan_out(&stored_event); + let event_json = serde_json::to_string(&event) + .expect("nostr::Event serialization is infallible for well-formed events"); + let mut drop_count = 0u32; + for (target_conn_id, sub_id) in &matches { + let msg = format!(r#"["EVENT","{}",{}]"#, sub_id, event_json); + if !state.conn_manager.send_to(*target_conn_id, msg) { + drop_count += 1; + } + } + if drop_count > 0 { + tracing::warn!( + event_id = %event_id_hex, + drop_count, + "fan-out: {drop_count} connection(s) cancelled due to full/closed buffers" + ); + } } conn.send(RelayMessage::ok(event_id_hex, true, "")); diff --git a/desktop/src/features/messages/useChannelTyping.ts b/desktop/src/features/messages/useChannelTyping.ts index b9a77b4f2..331f27503 100644 --- a/desktop/src/features/messages/useChannelTyping.ts +++ b/desktop/src/features/messages/useChannelTyping.ts @@ -11,9 +11,9 @@ import { type TypingState = Record; -const TYPING_INDICATOR_TTL_MS = 5_500; +const TYPING_INDICATOR_TTL_MS = 8_000; const TYPING_PRUNE_INTERVAL_MS = 1_000; -const TYPING_POST_MESSAGE_SUPPRESS_MS = 4_000; +const TYPING_POST_MESSAGE_SUPPRESS_MS = 2_000; function pruneTypingState(state: TypingState, now = Date.now()) { let changed = false;