Files
upstream-mirror 1d2bfcac0c
Some checks failed
Prosody Plugin Tests / test (push) Failing after 10m11s
Close stale issues and PRs / stale (push) Failing after 1s
snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
2026-09-15 15:56:32 +05:00

166 lines
5.9 KiB
TypeScript

import ReducerRegistry from '../base/redux/ReducerRegistry';
import {
SET_TIME_TIMER_ACKNOWLEDGED,
SET_TIME_TIMER_CALENDAR_DURATION,
SET_TIME_TIMER_EXPIRED,
SET_TIME_TIMER_WARNING_TRIGGERED,
START_TIME_TIMER,
STOP_TIME_TIMER,
TICK_TIME_TIMER
} from './actionTypes';
import { WARNING_THRESHOLD_SECONDS } from './constants';
export interface ITimeTimerState {
acknowledged: boolean;
calendarDurationSeconds?: number;
/**
* Scheduled start of the calendar event (ms since epoch) the participant
* is joining, when supplied by a native calendar source. Lets the
* middleware compute elapsed-since-start at join time.
*/
calendarStartTimeUnix?: number;
/**
* Room URL the calendar duration was recorded for. The middleware only
* uses the duration when the joined conference matches this URL, so a
* duration left over from a meeting the user opened but never joined does
* not leak into a different meeting.
*/
calendarUrl?: string;
durationSeconds: number;
expired: boolean;
overSeconds: number;
remainingSeconds: number;
running: boolean;
/**
* Wall-clock time (ms since epoch) of the meeting's scheduled end. This is
* the single source of truth while the timer runs: `remainingSeconds` and
* `overSeconds` are recomputed from it against `Date.now()` on every tick,
* so a throttled background tab can never accumulate drift.
*/
scheduledEndUnix?: number;
warningTriggered: boolean;
}
const DEFAULT_STATE: ITimeTimerState = {
acknowledged: false,
durationSeconds: 0,
expired: false,
overSeconds: 0,
remainingSeconds: 0,
running: false,
warningTriggered: false
};
/**
* Derives the running counters (remaining / over / expired) from the
* scheduled-end timestamp and a wall-clock reading. Keeping this in one place
* means START and TICK stay consistent.
*
* @param {number} durationSeconds - The meeting's scheduled duration.
* @param {number} scheduledEndUnix - Scheduled end, ms since epoch.
* @param {number} nowUnix - Current wall-clock time, ms since epoch.
* @returns {{ expired: boolean, overSeconds: number, remainingSeconds: number }}
*/
function _deriveCounters(durationSeconds: number, scheduledEndUnix: number, nowUnix: number) {
const remainingSeconds = Math.max(0, Math.round((scheduledEndUnix - nowUnix) / 1000));
const overSeconds = Math.max(0, Math.round((nowUnix - scheduledEndUnix) / 1000));
return {
expired: remainingSeconds <= 0,
overSeconds,
remainingSeconds: Math.min(remainingSeconds, durationSeconds)
};
}
ReducerRegistry.register<ITimeTimerState>('features/time-timer', (state = DEFAULT_STATE, action) => {
switch (action.type) {
case SET_TIME_TIMER_CALENDAR_DURATION:
return {
...state,
calendarDurationSeconds: action.durationSeconds,
calendarStartTimeUnix: action.startTimeUnix,
calendarUrl: action.url
};
case START_TIME_TIMER: {
const { durationSeconds, nowUnix } = action;
// `elapsedSeconds` is seconds since the SCHEDULED start. It may be
// negative (joined before the scheduled start) or exceed the duration
// (joined after the scheduled end). We deliberately do NOT clamp it:
// from it and the current wall-clock time we pin the scheduled end as
// an absolute timestamp, and everything else is derived from that
// timestamp here and on every tick. A negative value pushes the end
// out to the true scheduled end, so an early joiner sees 00:00 / full
// duration until the start arrives, then the counter climbs — landing
// on the limit exactly at the scheduled end. A value past the duration
// lands a late joiner directly in the correct overrun / lap state.
const { elapsedSeconds } = action;
const scheduledEndUnix = nowUnix + ((durationSeconds - elapsedSeconds) * 1000);
const counters = _deriveCounters(durationSeconds, scheduledEndUnix, nowUnix);
return {
...state,
acknowledged: false,
durationSeconds,
...counters,
running: true,
scheduledEndUnix,
// Suppress the one-time attention-grab expand if we joined already
// inside the warning window or past the end — the disk just shows
// the correct colour from the start. `warningTriggered` only gates
// the animation, not the colour.
warningTriggered: counters.remainingSeconds <= WARNING_THRESHOLD_SECONDS
};
}
case STOP_TIME_TIMER:
// Full reset of the derived/running state so a stopped or cleared
// timer leaves nothing behind — no frozen pill, no lingering red
// border on the next meeting. Calendar inputs are managed separately
// by SET_TIME_TIMER_CALENDAR_DURATION and intentionally not touched
// here.
return {
...state,
acknowledged: false,
durationSeconds: 0,
expired: false,
overSeconds: 0,
remainingSeconds: 0,
running: false,
scheduledEndUnix: undefined,
warningTriggered: false
};
case SET_TIME_TIMER_EXPIRED:
return {
...state,
expired: true
};
case SET_TIME_TIMER_ACKNOWLEDGED:
return {
...state,
acknowledged: true
};
case SET_TIME_TIMER_WARNING_TRIGGERED:
return {
...state,
warningTriggered: true
};
case TICK_TIME_TIMER: {
if (!state.running || typeof state.scheduledEndUnix !== 'number') {
return state;
}
return {
...state,
..._deriveCounters(state.durationSeconds, state.scheduledEndUnix, action.nowUnix)
};
}
}
return state;
});