184 lines
7.3 KiB
TypeScript
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;
|
|
}
|