Presentation: floating emoji reactions broadcast to all participants #98
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_whiteboard#98
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Improvement
During a presentation, viewers have no way to express engagement -- no thumbs-up / heart / clap / laugh / wow. Miro, Figma, Zoom, Teams all offer floating emoji reactions during shared sessions. Each reaction floats up and fades out; everyone in the session sees it.
This pairs naturally with the "share presentation link" feature (issue spawned alongside this one). With both, a presenter can run a slide deck with attendees who see slides + their own and others' reactions in real time.
Expected behavior
var(--wb-...)styling as the existing prev / next / exit buttons.presentation.reactionevent over the existing per-board WebSocket channel:{type: 'presentation.reaction', emoji: '\u{1F44D}', sender: <localUserId>, t: <Date.now()>}.presentation.reactionevent spawns the same floating emoji animation in EVERY viewer's tab (including the sender's, but the sender already saw it locally on click; the existing sync.js localUserId echo-skip handles that).body.wb-presenting). If a user exits, incoming events are ignored locally.Affected files (expected)
crates/hero_whiteboard_ui/templates/web/board.html-- add the reaction button cluster to the existing presentation control bar; CSS for the floating-emoji animation; a small JS module that handles click-to-broadcast and event-to-render.crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.js-- handlemsg.type === 'presentation.reaction'inhandleWsMessageand dispatch to a host hook (e.g.window.onPresentationReaction(msg)).crates/hero_whiteboard_ui/static/web/js/whiteboard/frames.js-- exposesendReaction(emoji)that wraps the WebSocket send viaWhiteboardSync._wsSend({type:'presentation.reaction', emoji, ...})so the reaction module doesn't have to reach into sync internals directly.No server / SDK / openrpc / DB changes -- the WebSocket relay is a pass-through.
Acceptance criteria
cargo check / clippy / fmt --check / testclean.Notes
@keyframesfor the float animation (translateY + opacity). Self-removing nodes viaanimationendkeep the DOM clean.position: fixedoverlay withpointer-events: noneso they don't intercept clicks on the control bar or the slide.senderfield usesWhiteboardSync.getLocalUserId()for the existing echo-skip; an event withsender === localUserIdis dropped on receive (so only remote viewers' reactions render via the WS path; the local user's reaction fires from the click handler directly).Spec — Issue #98: Floating emoji reactions during presentation
Objective
Let any participant in a live presentation (presenter or audience) send a floating emoji reaction that animates upward locally and broadcasts to everyone else in the same board over the existing per-board WebSocket relay. Five fixed presets: thumbs-up, heart, clap, party, wow. Reactions are ephemeral, throttled, and only visible while in presentation mode.
Requirements
var(--wb-...)styling as the other.pres-btnbuttons.{type: 'presentation.reaction', emoji, t}over the existing WebSocket fan-out (sender stamping is the existingwsSendbehaviour: it setssender = localUserId).msg.senderso a misbehaving client can't flood.objects.data, not in the DB, gone on reload.pointer-events: none).animationendso the overlay stays clean.Files to Modify
crates/hero_whiteboard_ui/static/web/js/whiteboard/reactions.js—WhiteboardReactionsIIFE withinit,fire(emoji),onIncoming(msg),spawn(emoji). Internal per-local + per-sender throttle.crates/hero_whiteboard_ui/templates/web/board.html:#presentation-controls.reactions.js.bindPresentationIIFE, callWhiteboardReactions.init()(or do it on DOMContentLoaded — whichever is simpler).crates/hero_whiteboard_ui/templates/web/board_view.html: same three additions (CSS, buttons, script tag + init).crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.js:handleWsMessage, after the existingpresentation.slidebranch, add apresentation.reactionbranch that dispatches toWhiteboardReactions.onIncoming(msg). The existingif (msg.sender === localUserId) return;echo-skip at the top ofhandleWsMessagealready prevents the sender from rendering twice.Files to Leave Alone
frames.js— the issue suggests an optionalsendReaction(emoji)wrapper, butreactions.jscan callWhiteboardSync._wsSenddirectly (same pattern themes/connectors use). Avoiding the indirection keeps the diff smaller.Step-by-Step Plan
Step 1 — Create
reactions.jsFile:
crates/hero_whiteboard_ui/static/web/js/whiteboard/reactions.jsSkeleton:
Step 2 — Wire
reactions.jsand add CSS inboard.htmlFile:
crates/hero_whiteboard_ui/templates/web/board.htmlAdd CSS to the existing
<style>block (the one that already styles#pres-spotlight/#presentation-controls):Markup change in
#presentation-controls— prepend reaction buttons + a divider before the existingpres-prev:Add the script tag near the other
whiteboard/*.jsincludes in the{% block scripts %}section (alphabetical-ish order, nearframes.js):In
bindPresentation()callWhiteboardReactions.init()once after the existingsetOnChangeblock (or unconditionally on DOMContentLoaded — pick the spot that keeps the diff minimal).Step 3 — Mirror Step 2 changes in
board_view.htmlFile:
crates/hero_whiteboard_ui/templates/web/board_view.htmlSame CSS additions, same 5 buttons + divider prepended to the control bar, same
<script src=".../reactions.js">include, sameWhiteboardReactions.init()call inside the readonlybindPresentation. The audience gets identical reaction UX as the presenter.Step 4 — Receiver in
sync.jsFile:
crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.jsIn
handleWsMessage, after the existingpresentation.slidebranch:The localUserId echo-skip at the top of
handleWsMessagealready drops our own broadcast, so we only ever render remote reactions through this branch (the local user already saw their own emoji from the click handler).Acceptance Criteria
presentation.slidesync.cargo fmt --all -- --check/cargo check --workspace/cargo clippy --workspace -- -D warningsclean.Notes
_lastLocal; remote throttle uses_lastBySender[sender]. Together they cap incoming spam without coordination.#wb-reactions-overlaydiv that exists for the whole page lifetime; individual emoji nodes self-remove onanimationend.pointer-events: noneon the overlay AND each reaction prevents them from intercepting clicks on the control bar or the canvas backdrop.9999(spotlight) <10000(reactions overlay) <10001(control bar) <10002(toast in present mode). Reactions float behind the control bar, which is what we want.frames.jschanges —reactions.jscallsWhiteboardSync._wsSenddirectly, mirroring the pattern themes/connectors already use.reactions.jsis a separate file (not inline) so the same module is available to bothboard.htmlandboard_view.htmlwithout duplication.Test Results
The Askama board.html and board_view.html templates re-compiled cleanly via cargo check.
Implementation Summary
Four files touched: one new module + three wires. Total +70/-0 in the tracked diff plus one new ~70-line file (
reactions.js). No server / SDK / openrpc / DB changes — the WebSocket relay is a pass-through.crates/hero_whiteboard_ui/static/web/js/whiteboard/reactions.js(new)WhiteboardReactionsIIFE with the public surface:init()— appends#wb-reactions-overlaytodocument.body(idempotent) and binds click handlers to every.pres-reaction[data-emoji]button inside#presentation-controls(idempotent via_bound).fire(emoji)— local-throttle (~6/sec viaTHROTTLE_MS = 167), spawns local emoji, and broadcasts{type: 'presentation.reaction', emoji, t}via the existingWhiteboardSync._wsSend. The send path stampssenderautomatically.onIncoming(msg)— gates onWhiteboardFrames.isPresentationMode(); per-sender throttle keyed bymsg.sender || 'anon'; spawns the same animation. The localUserId echo-skip insync.js::handleWsMessageensures the originator doesn't render their own reaction twice.spawn(emoji)— creates a<span class="wb-reaction">at a randomleftwithin ±120 px of the viewport center, sets a--wb-react-driftrandom horizontal offset, and self-removes onanimationend.crates/hero_whiteboard_ui/templates/web/board.html#wb-reactions-overlay(z-index 10000,pointer-events: none,inset: 0,overflow: hidden),.wb-reaction(positioned, font-size 32 px, animation),@keyframes wb-react-float(translate up + drift + opacity),.pres-reaction { font-size: 18px; line-height: 1; }.#presentation-controls, then a.pres-divider, then the existing prev/counter/next/share/exit cluster.<script src="{{ base_path }}/js/whiteboard/reactions.js">added next toframes.js.bindPresentation()now callsWhiteboardReactions.init()after wiring the existing buttons.crates/hero_whiteboard_ui/templates/web/board_view.htmlSame three wires (CSS, buttons + divider, script tag, init call). Audience gets identical reaction UX.
crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.jsAfter the existing
presentation.slidebranch inhandleWsMessage:The
if (msg.sender === localUserId) return;echo-skip earlier in the function already drops the originator's own broadcast.Verification
cargo fmt --all -- --check: cleancargo check --workspace: clean (Askama re-compiled both templates)node --check reactions.jsandnode --check sync.js: cleanManual smoke test
/s/<token>?present=1).isPresentationMode()).Notes / scope
pointer-events: noneon both the overlay and each.wb-reactionensures reactions never intercept clicks on the control bar or the canvas backdrop.THROTTLE_MS = 167(~6/sec) applies to both local sends and incoming events keyed bymsg.sender. A misbehaving client can't flood other tabs.animationendso the overlay never accumulates DOM cruft.frames.jschange —reactions.jscallsWhiteboardSync._wsSenddirectly, mirroring the same pattern used bythemes.jsandconnectors.js.