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

184 lines
7.3 KiB
TypeScript

import { IReduxState } from '../app/types';
import BaseTheme from '../base/ui/components/BaseTheme';
import { WARNING_THRESHOLD_SECONDS } from './constants';
import { ITimeTimerState } from './reducer';
import { ICalendarTimerEvent, ITimerVisualState } from './types';
// Minimum visible red disk fraction at expiry, so the wedge isn't invisible.
const EXPIRED_WEDGE_FLOOR = 0.02;
/**
* Derives the disk's visual state (colour, fill fraction, elapsed seconds)
* from the timer reducer state.
*
* @param {ITimeTimerState} state - The current time-timer reducer state.
* @returns {ITimerVisualState}
*/
export function getTimerVisualState(state: ITimeTimerState): ITimerVisualState {
const { durationSeconds, remainingSeconds, overSeconds, expired } = state;
// Time elapsed since the calendar event's scheduled start. Before the
// scheduled end this is `duration - remaining` (which already accounts
// for late joiners via `defaultRemaining`). After the scheduled end the
// timer keeps ticking via `overSeconds`, so total elapsed =
// duration + overrun.
const elapsedSeconds = Math.max(
0,
(durationSeconds - remainingSeconds) + overSeconds
);
// Warning state: within the last `WARNING_THRESHOLD_SECONDS` of the
// scheduled duration but not yet expired. Used both for the amber
// colouring and (in middleware) to trigger the one-time bar-expand.
const warning = !expired && remainingSeconds > 0 && remainingSeconds <= WARNING_THRESHOLD_SECONDS;
let fillColor = BaseTheme.palette.timeTimerDisk;
let fraction = durationSeconds > 0
? (durationSeconds - remainingSeconds) / durationSeconds
: 0;
if (warning) {
fillColor = BaseTheme.palette.timeTimerWarning;
}
let overrunArcEndDeg: number | undefined;
if (expired) {
fillColor = BaseTheme.palette.timeTimerExpiredDisk;
if (durationSeconds > 0) {
// Treat overrun as a series of laps, each one
// `durationSeconds` long — the disk fills exactly once over
// the first lap (matching the original scheduled duration),
// then stays solid red while a glowing arc sweeps subsequent
// laps to convey ongoing motion.
const lapIndex = Math.floor(overSeconds / durationSeconds);
const lapFraction = (overSeconds % durationSeconds) / durationSeconds;
if (lapIndex === 0) {
// Lap 1: standard growing wedge, with a small visible
// floor right at expiry so the red slice is never invisible.
fraction = Math.max(EXPIRED_WEDGE_FLOOR, lapFraction);
} else {
// Lap 2+: disk fully filled. The leading edge of the
// current lap is rendered separately as a glowing
// comet-tail arc (see Disk component).
fraction = 1;
overrunArcEndDeg = lapFraction * 360;
}
} else {
fraction = EXPIRED_WEDGE_FLOOR;
}
}
return { elapsedSeconds, fillColor, fraction, overrunArcEndDeg, warning };
}
/**
* Computes the {@code setCalendarTimerDuration} arguments for a calendar
* event the user is about to join, or {@code undefined} when the event has
* no parseable start/end (so the caller should clear any stale duration
* instead). Shared by web's and native's calendar list `_onPress`, so both
* platforms record the same duration/start-time the same way.
*
* @param {ICalendarTimerEvent} [event] - The calendar event being joined.
* @returns {{ durationSeconds: number, startTimeUnix: number }|undefined}
*/
export function computeCalendarTimerDuration(event?: ICalendarTimerEvent) {
const startUnix = typeof event?.startDate === 'number'
? event.startDate
: Date.parse(event?.startDate ?? '');
const endUnix = typeof event?.endDate === 'number'
? event.endDate
: Date.parse(event?.endDate ?? '');
if (!isNaN(startUnix) && !isNaN(endUnix) && endUnix > startUnix) {
return {
durationSeconds: Math.round((endUnix - startUnix) / 1000),
startTimeUnix: startUnix
};
}
return undefined;
}
/**
* Whether the time-timer is enabled. It is enabled by default — only an
* explicit {@code timeTimer.enabled === false} turns it off. The timer
* renders nothing until a duration is known, so being on by default is
* harmless and lets calendar / iframe-API deployments see it with no extra
* config. This is the single source of truth for the enabled rule; use it
* everywhere (middleware, components) rather than reading the config inline.
*
* @param {IReduxState} state - The redux state.
* @returns {boolean}
*/
export function isTimeTimerEnabled(state: IReduxState): boolean {
return state['features/base/config']?.timeTimer?.enabled !== false;
}
/**
* How long, in seconds of meeting time, the visible countdown is held back
* for. Off by default (0) — the timer shows as soon as a duration is known.
*
* The server (`mod_time_restricted`) hands every client the meeting's limit
* the moment they join, so a deployment that does not want the countdown on
* screen for the whole meeting sets `timeTimer.suppressForSeconds` and gets
* it revealed part-way through instead. The threshold is measured against
* time elapsed since the meeting's scheduled start — not since this
* participant joined — so the timer appears at the same moment for everyone
* and a late joiner past the threshold sees it immediately.
*
* @param {IReduxState} state - The redux state.
* @returns {number}
*/
export function getTimeTimerSuppressSeconds(state: IReduxState): number {
const suppressForSeconds = state['features/base/config']?.timeTimer?.suppressForSeconds;
return typeof suppressForSeconds === 'number' && suppressForSeconds > 0 ? suppressForSeconds : 0;
}
/**
* Whether the time-timer should currently be on screen: the feature is
* enabled, a timer is running for this meeting, and we are past any
* configured suppression window.
*
* This is the single source of truth for the pill's visibility — the info bar
* uses it too, because {@code TimeTimerPill} supersedes the subject and
* conference-timer labels and hiding all three would leave the bar empty.
*
* Note this gates the countdown display only. A meeting that runs over still
* raises the timer-ended notification and the overrun treatment on schedule,
* whatever the suppression window is.
*
* @param {IReduxState} state - The redux state.
* @returns {boolean}
*/
export function isTimeTimerVisible(state: IReduxState): boolean {
const timerState = state['features/time-timer'];
if (!isTimeTimerEnabled(state) || !timerState.running) {
return false;
}
const suppressForSeconds = getTimeTimerSuppressSeconds(state);
return suppressForSeconds <= 0
|| getTimerVisualState(timerState).elapsedSeconds >= suppressForSeconds;
}
/**
* Whether the meeting has run past its scheduled end and the user has not yet
* dismissed the timer-ended notification. Drives the red border around the
* conference grid.
*
* @param {IReduxState} state - The redux state.
* @returns {boolean}
*/
export function isTimeTimerExpiredUnacknowledged(state: IReduxState): boolean {
const { acknowledged, expired } = state['features/time-timer'];
return expired && !acknowledged;
}