Skip to content
Draft
22 changes: 22 additions & 0 deletions .factory/oracles/80.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"issue": 80,
"files": [
"worker/durable-object-fake.js",
"worker/polls.test.js",
"worker/polls-zoom.test.js",
"worker/club-poll-template.test.js",
"worker/club-meetings.test.js",
"worker/club-share.test.js",
"worker/routing.test.js",
"worker/csp.test.js",
"worker/gating.test.js",
"packages/shared/__tests__/polls.test.js",
"packages/shared/__tests__/profileMerge.test.js",
"packages/shared/__tests__/reportImage.test.js",
"packages/shared/__tests__/clubArchive.test.js",
"apps/zoom-app/src/utils/zoomSdk.test.js",
"apps/zoom-app/src/components/PollTab.test.jsx",
"apps/zoom-app/src/components/TimerStage.test.jsx",
"e2e/zoom.meeting-polls.spec.js"
]
}
422 changes: 422 additions & 0 deletions .factory/plans/80-meeting-polls.md

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions apps/zoom-app/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@ import TimerSounds from './components/TimerSounds';
import ContactCapture from './components/ContactCapture';
import ZoomConnectionNotice from './components/ZoomConnectionNotice';
import ClubStatusBanner from './components/ClubStatusBanner';
import { PollVoteBanner } from './components/PollVote';
import { useFlag } from './hooks/useFlag';
import './App.css';

const LiveTab = lazy(() => import('./components/LiveTab'));
const AgendaTab = lazy(() => import('./components/AgendaTab'));
const ReportTab = lazy(() => import('./components/ReportTab'));
const PollTab = lazy(() => import('./components/PollTab'));

function App() {
const [activeTab, setActiveTab] = useState('live');
const { enabled: pollsOn } = useFlag('polls');

return (
<ToastProvider>
Expand All @@ -27,6 +31,8 @@ function App() {
is rarely whoever pays. */}
<ClubStatusBanner />
<NavTabs activeTab={activeTab} onTabChange={setActiveTab} />
{/* A poll someone opened in this meeting: one tap to the ballot. */}
{pollsOn && activeTab !== 'poll' && <PollVoteBanner onOpen={() => setActiveTab('poll')} />}

<Suspense fallback={
<div style={{display:'flex',alignItems:'center',justifyContent:'center',height:'100%',flex:1}}>
Expand All @@ -38,6 +44,7 @@ function App() {
{activeTab === 'live' && <LiveTab />}
{activeTab === 'agenda' && <AgendaTab onSwitchToLive={() => setActiveTab('live')} />}
{activeTab === 'report' && <ReportTab />}
{activeTab === 'poll' && pollsOn && <PollTab />}
</div>
</Suspense>

Expand Down
7 changes: 5 additions & 2 deletions apps/zoom-app/src/components/LiveTab.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect, useRef, useMemo, memo, lazy, Suspense } from 'react';
import { useState, useEffect, useRef, useMemo, memo, lazy, Suspense, useSyncExternalStore } from 'react';
import { useTimer, useTimerTick } from '../context/TimerContext';
import { useToast } from '../context/ToastContext';
import { Play, Square, RotateCcw, Eraser, Video, Image, Volume2, Lock } from 'lucide-react';
Expand All @@ -14,7 +14,7 @@ import { DEFAULT_ROLE_RULES, DEFAULT_CUSTOM_RULES, loadTimeInputMode, saveTimeIn
import { getVideoState, setVideoState, applyOverlay, removeOverlay, clearVideoPipelines, isOverlayActive, getBackgroundUrl, getSdkStatus, setLogCallback, getOverlayMode, setOverlayMode, getOverlayTimePosition, setOverlayTimePosition, getOverlayTimeScale, setOverlayTimeScale, isOverlayTimeVisible, setOverlayTimeVisible, setOverlayTimeLabel, setPopoutChangeCallback, setShareChangeCallback, setAppShare, setAppPopout, isAppShareActive, isAppPoppedOut, isVideoOverlayMode, getClubBadgePlacement, setClubBadgePlacement, resetClubBadgePlacement, isClubBadgeMoved, getZoomParticipants, OVERLAY_MODE_CARD, OVERLAY_MODE_STAGE, notifyCardImagesChanged, preloadBackgroundImages } from '../utils/zoomSdk';
import { useClub } from '../hooks/useClub';
import { SOUNDS_LOCK_MESSAGES, useSoundsAccess } from '../hooks/useSounds';
import { formatTime, saveOverlayMode, saveStageClockHidden, loadStageClockHidden, saveRevealFaceWhenIdle, loadRevealFaceWhenIdle } from '@toastmaster-timer/shared';
import { formatTime, saveOverlayMode, saveStageClockHidden, loadStageClockHidden, saveRevealFaceWhenIdle, loadRevealFaceWhenIdle, getStageWinners, subscribeStageWinners } from '@toastmaster-timer/shared';
import { AlertTriangle, ChevronDown, ChevronUp } from 'lucide-react';
import { trackEvent } from '../utils/posthog';

Expand All @@ -38,6 +38,8 @@ const PREVIEW_COLORS = [

export default memo(function LiveTab() {
const { isRunning, elapsedTime, currentStatus } = useTimerTick();
// Winners the POLL tab's "Show on Stage" put up, for this device's stage only.
const pollWinners = useSyncExternalStore(subscribeStageWinners, getStageWinners, getStageWinners);
const {
currentSpeaker,
startTimer,
Expand Down Expand Up @@ -1006,6 +1008,7 @@ export default memo(function LiveTab() {
onRenameSpeaker={handleRenameSpeaker}
clubKit={clubKit}
badgePlacement={badgePlacement}
pollWinners={pollWinners}
/>
)}

Expand Down
4 changes: 4 additions & 0 deletions apps/zoom-app/src/components/NavTabs.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { memo } from 'react';
import { trackEvent } from '../utils/posthog';
import { useFlag } from '../hooks/useFlag';

export default memo(function NavTabs({ activeTab, onTabChange }) {
// Agenda, Live, Report: the meeting's own order. The agenda is prepared
Expand All @@ -10,6 +11,9 @@ export default memo(function NavTabs({ activeTab, onTabChange }) {
{ id: 'live', label: 'LIVE' },
{ id: 'report', label: 'REPORT' },
];
// The poll comes last, as it does in the meeting: the vote is taken at the end.
const { enabled: pollsOn } = useFlag('polls');
if (pollsOn) tabs.push({ id: 'poll', label: 'POLL' });

const handleTabChange = (tabId) => {
(window.requestIdleCallback || setTimeout)(() => trackEvent('tab_viewed', {
Expand Down
Loading