Skip to content

Commit

Permalink
Countdown Timer Feature (#185)
Browse files Browse the repository at this point in the history
* init

* Adjust countdownTimerChip styles

* Clean component

* Refactor component

* Make bigger refactor

* Comment code

* Clean code

* Fix start when Enter key pressed

* Clean code

* Clean code

* Clean code

* Clean code

* Fix delimiters

* Clean Code

* Standarize state

* Standarize names

* Show participant list (for testing)

* Fix counting stopped at 00:00:01

* Fix opened TextField Date Picker at start (mobile)

* Add visual indicator

* Fix indicator for new joining user

* Fix color format

* Standarize names

* Open/close timer panel when clicking on  Chip

* Fromat code

* Simplify code

* Format code

* Clean unused

* Standarize names

* Clean unused

* Fix name

* Fix name

* Fix name

* tmp

* Manage mobile when format time is 00:00

* Add Permission

* Fix style

* Refactor code

* Add sound notification when time's up

* Add text notification when time's up

* Merge countdownTimerSlice with roomSlice

* Revert showing participant list (for testing)

* Remove unused Label

* Update translations

* Restore original .eslintrc

* Temporary disable Eslint type error reporting

* Remove unused

* Add missing comments

* Standarize namespace

---------

Co-authored-by: Rémai Gábor <[email protected]>
  • Loading branch information
roman-drozd-it and N7Remus authored Nov 18, 2024
1 parent 7058d6b commit 15972d8
Show file tree
Hide file tree
Showing 20 changed files with 614 additions and 10 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@reduxjs/toolkit": "^1.9.7",
"awaitqueue": "^3.0.2",
"bowser": "^2.11.0",
"moment": "^2.29.4",
"debug": "^4.3.7",
"dompurify": "^3.1.6",
"file-saver": "^2.0.5",
Expand Down
3 changes: 3 additions & 0 deletions public/config/config.example.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ var config = {
'raisedHand': {
'play': '/sounds/notify-hand.mp3'
},
'finishedCountdownTimer': {
'play': '/sounds/notify-countdowntimer.mp3'
},
'default': {
'debounce': 5000,
'play': '/sounds/notify.mp3'
Expand Down
Binary file added public/sounds/notify-countdowntimer.mp3
Binary file not shown.
152 changes: 152 additions & 0 deletions src/components/countdowntimer/CountdownTimer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import React, { useRef } from 'react';
import { IconButton, Grid, Switch, TextField, styled } from '@mui/material';
import { HighlightOff as HighlightOffIcon, Pause as PauseIcon, PlayArrow as PlayArrowIcon } from '@mui/icons-material';
import moment from 'moment';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import { setCountdownTimerInitialTime, startCountdownTimer, stopCountdownTimer, disableCountdownTimer, enableCountdownTimer } from '../../store/actions/countdownTimerActions';
import {
countdownTimerStartLabel, countdownTimerStopLabel,
countdownTimerEnableLabel, countdownTimerDisableLabel, countdownTimerSetLabel }
from '../translated/translatedComponents';
import { isMobileSelector } from '../../store/selectors';

const CountdownTimerDiv = styled('div')(({ theme }) => ({
display: 'flex',
marginRight: theme.spacing(1),
marginTop: theme.spacing(1),
flexDirection: 'column',
gap: theme.spacing(1),
}));

const CountdownTimer = () : JSX.Element => {
const isMobile = useAppSelector(isMobileSelector);
const dispatch = useAppDispatch();
const isEnabled = useAppSelector((state) => state.room.countdownTimer.isEnabled);
const isStarted = useAppSelector((state) => state.room.countdownTimer.isStarted);
const remainingTime = useAppSelector((state) => state.room.countdownTimer.remainingTime);

const inputRef = useRef<HTMLDivElement>(null);

const handleFocus = () => {

if (inputRef.current) {
inputRef.current.focus();
}

const timeout = setTimeout(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, 50);

return () => {
clearTimeout(timeout);
};
};

return (
<CountdownTimerDiv>
<Grid
sx={{ flexGrow: '1', justifyContent: 'space-between' }}
container wrap='nowrap'
alignItems='center' >

{/* set */}
<Grid item xs={8}>
<TextField fullWidth
aria-label={countdownTimerSetLabel()}
inputRef={inputRef}
autoFocus={!isMobile}
sx={{ flexGrow: '1' }}
variant='outlined'
label={(isMobile) ? 'timer (HH:mm)' : 'timer (HH:mm:ss)'}
disabled={!isEnabled || (isStarted && remainingTime !== '00:00:00')}
type='time'
value={remainingTime}
size='small'
InputLabelProps={{ shrink: true }}
inputProps={{ step: '1' }}
onChange={(e) => {
const time = (isMobile && moment(e.target.value, 'HH:mm', true).isValid())
? moment(`${e.target.value}:00`, 'HH:mm:ss').format('HH:mm:ss')
: moment(`${e.target.value}`, 'HH:mm:ss').format('HH:mm:ss');

dispatch(setCountdownTimerInitialTime(time));
}}
onKeyDown={(e) => {
if (remainingTime !== '00:00:00') {
if (e.key === 'Enter') {
dispatch(startCountdownTimer());
e.preventDefault();
}
}
}}
/>
</Grid>

{/* reset */}
<Grid item xs={1}>
<IconButton
aria-label={countdownTimerStartLabel()}
sx={{ flexGrow: '1' }}
color='error'
size='small'
disabled={ !isEnabled || (isStarted || remainingTime === '00:00:00') }
onClick={() => {
dispatch(setCountdownTimerInitialTime('00:00:00'));
}}
>
<HighlightOffIcon />
</IconButton>
</Grid>

{/* start/stop */}
<Grid item xs={1}>
<IconButton
aria-label={ !isStarted ?
countdownTimerStartLabel() :
countdownTimerStopLabel()
}
sx={{ flexGrow: '1' }}
color='error'
size='small'
disabled={!isEnabled || remainingTime === '00:00:00'}
onClick={() => {
if (!isStarted) {
dispatch(startCountdownTimer());
} else {
dispatch(stopCountdownTimer());
handleFocus();
}
}}
>
{!isStarted ? <PlayArrowIcon /> : <PauseIcon /> }
</IconButton>
</Grid>

{/* enable/disable */}
<Grid item xs={1}>
<Switch
aria-label={ !isStarted ?
countdownTimerDisableLabel() :
countdownTimerEnableLabel()
}
sx={{ flexGrow: '1' }}
checked={isEnabled}
disabled={isStarted}
onChange={() => {
dispatch(isEnabled ?
disableCountdownTimer() :
enableCountdownTimer()
);
}}
color='error'
size='small'
/>
</Grid>
</Grid>
</CountdownTimerDiv>
);
};

export default CountdownTimer;
53 changes: 53 additions & 0 deletions src/components/countdowntimer/CountdownTimerChip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';
import { Chip } from '@mui/material';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import { uiActions } from '../../store/slices/uiSlice';
import AvTimerIcon from '@mui/icons-material/AvTimer';
import moment from 'moment';

const CountdownTimerChip = (): JSX.Element => {
const dispatch = useAppDispatch();
const isEnabled = useAppSelector((state) => state.room.countdownTimer.isEnabled);
const remainingTime = useAppSelector((state) => state.room.countdownTimer.remainingTime);
const initialTime = useAppSelector((state) => state.room.countdownTimer.initialTime);

const participantListOpen = useAppSelector((state) => state.ui.participantListOpen);

const openUsersTab = () => dispatch(uiActions.setUi({ participantListOpen: !participantListOpen }));

const secondsSet = moment.duration(initialTime).asSeconds();
const secondsLeft = moment.duration(remainingTime).asSeconds();
const percentage = parseFloat(((secondsLeft / secondsSet) * 100).toFixed(2));

let indicatorColor: string;
const backgroundColor: string = 'rgba(128, 128, 128, 0.5)'; // Declare the 'backgroundColor' variable here

switch (true) {
case percentage <= 100 && percentage >= 50: indicatorColor = '#2E7A27'; break;
case percentage < 50 && percentage >= 20: indicatorColor = '#FFA500'; break;
case percentage < 20: indicatorColor = '#FF0000'; break;
default: indicatorColor = backgroundColor;
}

return (
<>
{isEnabled && (
<Chip
sx={{
color: 'white',
backgroundColor: backgroundColor,
background: `linear-gradient(to right, ${indicatorColor} ${percentage}%, ${backgroundColor} ${percentage}%)`,
animation: `${percentage}% blink-animation 1s infinite`,
width: '86px',
}}
label={remainingTime}
size="small"
icon={<AvTimerIcon style={{ color: 'white' }} />}
onClick={() => openUsersTab()}
/>
)}
</>
);
};

export default CountdownTimerChip;
13 changes: 11 additions & 2 deletions src/components/participantlist/ParticipantList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import { breakoutRoomsSelector, inParentRoomSelector, parentParticipantListSelec
import { permissions } from '../../utils/roles';
import {
breakoutRoomsLabel,
participantsLabel
participantsLabel,
countdownTimerTitleLabel
} from '../translated/translatedComponents';
import ListMe from './ListMe';
import ListModerator from './ListModerator';
import ListPeer from './ListPeer';
import BreakoutModerator from '../breakoutrooms/BreakoutModerator';
import ListBreakoutRoom from '../breakoutrooms/ListBreakoutRoom';
import CountdownTimer from '../countdowntimer/CountdownTimer';

const ParticipantListDiv = styled(Box)(({ theme }) => ({
width: '100%',
Expand All @@ -37,7 +39,14 @@ const ParticipantList = (): JSX.Element => {

return (
<ParticipantListDiv>
{ isModerator && <ListModerator /> }
{ isModerator && <>
<ListModerator />
<ListHeader>
{countdownTimerTitleLabel()}
</ListHeader>
<CountdownTimer />
</>
}
{ (breakoutsEnabled && (rooms.length > 0 || canCreateRooms)) &&
<>
<ListHeader>
Expand Down
6 changes: 5 additions & 1 deletion src/components/topbar/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import LeaveButton from '../textbuttons/LeaveButton';
import { formatDuration } from '../../utils/formatDuration';
import LogoutButton from '../controlbuttons/LogoutButton';
import RecordIcon from '../recordicon/RecordIcon';
import CountdownTimerChip from '../countdowntimer/CountdownTimerChip';

interface TopBarProps {
fullscreenEnabled: boolean;
Expand Down Expand Up @@ -120,9 +121,12 @@ const TopBar = ({ fullscreenEnabled, fullscreen, onFullscreen }: TopBarProps): R
{ canPromote && lobbyPeersLength > 0 && <LobbyButton type='iconbutton' /> }
{ loginEnabled && (loggedIn ? <LogoutButton type='iconbutton' /> : <LoginButton type='iconbutton' />) }
</TopBarDiv>
<TopBarDiv marginRight={2}>
<TopBarDiv marginRight={1}>
<StyledChip size='small' label={ formatDuration(meetingDuration) } />
</TopBarDiv>
<TopBarDiv marginRight={2}>
<CountdownTimerChip />
</TopBarDiv>
<LeaveButton />
</Toolbar>
</StyledAppBar>
Expand Down
34 changes: 33 additions & 1 deletion src/components/translated/translatedComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,39 @@ export const roomServerConnectionError = (message: string): string => intl.forma
defaultMessage: `Room-server: ${message}`
});

export const countdownTimerTitleLabel = (): string => intl.formatMessage({
id: 'label.countdownTimer.title',
defaultMessage: 'Countdown timer'
});

export const countdownTimerStartLabel = (): string => intl.formatMessage({
id: 'label.countdownTimer.start',
defaultMessage: 'Start'
});

export const countdownTimerStopLabel = (): string => intl.formatMessage({
id: 'label.countdownTimer.stop',
defaultMessage: 'Stop'
});

export const countdownTimerEnableLabel = (): string => intl.formatMessage({
id: 'label.countdownTimer.enable',
defaultMessage: 'Enable'
});

export const countdownTimerDisableLabel = (): string => intl.formatMessage({
id: 'label.countdownTimer.disable',
defaultMessage: 'Disable'
});

export const countdownTimerSetLabel = (): string => intl.formatMessage({
id: 'label.countdownTimer.set',
defaultMessage: 'Set'
});
export const countdownTimerFinishedLabel = (): string => intl.formatMessage({
id: 'label.countdownTimer.finished',
defaultMessage: 'Time is up!'
});
export const tenantSettingsLabel = (): string => intl.formatMessage({
id: 'label.managementTenantSettings',
defaultMessage: 'Tenant settings'
Expand Down Expand Up @@ -810,7 +843,6 @@ export const imprintLabel = (): string => intl.formatMessage({
id: 'label.imprint',
defaultMessage: 'Imprint'
});

export const privacyLabel = (): string => intl.formatMessage({
id: 'label.privacy',
defaultMessage: 'Privacy'
Expand Down
Loading

0 comments on commit 15972d8

Please sign in to comment.