From bbeb5927483f74fbb8fad7b9731ae20888dc71de Mon Sep 17 00:00:00 2001 From: zskhan Date: Tue, 18 Nov 2025 14:31:59 +0100 Subject: [PATCH 1/3] feat: add clock context to show the countdown timer in the upcoming meeting and check/update meeting status on every second. --- src/i18n/en-US.json | 4 + .../components/Meeting/ClockContext.tsx | 42 ++++++ .../Meeting/MeetingList/MeetingList.tsx | 96 ++++--------- .../MeetingListItem/MeetingListItem.styles.ts | 6 + .../MeetingListItem/MeetingListItem.tsx | 66 +++++++-- .../MeetingStatus/MeetingStatus.styles.ts | 59 ++++++++ .../MeetingStatus/MeetingStatus.tsx | 82 +++++++++++ .../MeetingListItemGroup.tsx | 11 +- .../TodayAndOngoingSection.tsx | 60 ++++++++ .../Meeting/{Meeting.tsx => Meetings.tsx} | 2 +- .../components/Meeting/mocks/MeetingMocks.ts | 130 ++++++++++++++++++ ...ingDatesHandler.ts => MeetingDatesUtil.ts} | 23 +++- .../Meeting/utils/MeetingStatusUtil.ts | 93 +++++++++++++ src/script/page/MainContent/MainContent.tsx | 4 +- src/types/i18n.d.ts | 8 +- 15 files changed, 598 insertions(+), 88 deletions(-) create mode 100644 src/script/components/Meeting/ClockContext.tsx create mode 100644 src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingStatus/MeetingStatus.styles.ts create mode 100644 src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingStatus/MeetingStatus.tsx create mode 100644 src/script/components/Meeting/MeetingList/TodayAndOngoingSection/TodayAndOngoingSection.tsx rename src/script/components/Meeting/{Meeting.tsx => Meetings.tsx} (96%) create mode 100644 src/script/components/Meeting/mocks/MeetingMocks.ts rename src/script/components/Meeting/utils/{MeetingDatesHandler.ts => MeetingDatesUtil.ts} (56%) create mode 100644 src/script/components/Meeting/utils/MeetingStatusUtil.ts diff --git a/src/i18n/en-US.json b/src/i18n/en-US.json index 22edbc11a3c..7900a429142 100644 --- a/src/i18n/en-US.json +++ b/src/i18n/en-US.json @@ -1198,7 +1198,11 @@ "meetings.tabs.next": "Next", "meetings.tabs.past": "Past", "meetings.list.today": "Today", + "meetings.list.onGoing": "Ongoing", + "meetings.list.onGoing.header": "Now", "meetings.list.tomorrow": "Tomorrow", + "meetings.meetingStatus.participating": "Attending", + "meetings.meetingStatus.startingIn": "Starting in {countdown}", "meetings.startMeetingHelp": "Start a meeting with team members, guests, or external parties. Your communication is always end-to-end encrypted, offering the highest level of security.", "mlsConversationRecovered": "You haven't used this device for a while, or an issue has occurred. Some older messages may not appear here.", "mlsSignature": "MLS with {signature} Signature", diff --git a/src/script/components/Meeting/ClockContext.tsx b/src/script/components/Meeting/ClockContext.tsx new file mode 100644 index 00000000000..2baafa95eb4 --- /dev/null +++ b/src/script/components/Meeting/ClockContext.tsx @@ -0,0 +1,42 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {createContext, PropsWithChildren, useEffect, useState} from 'react'; + +import {TIME_IN_MILLIS} from 'Util/TimeUtil'; + +/** + * A lightweight context that updates once per second(cna be changed by providing tickMS from outside) with the current time (in ms). + */ +export const ClockContext = createContext(Date.now()); + +export interface ClockProviderProps extends PropsWithChildren { + tickMs?: number; +} + +export const ClockProvider = ({children, tickMs = TIME_IN_MILLIS.SECOND}: ClockProviderProps) => { + const [now, setNow] = useState(() => Date.now()); + + useEffect(() => { + const id = window.setInterval(() => setNow(Date.now()), tickMs); + return () => window.clearInterval(id); + }, [tickMs]); + + return {children}; +}; diff --git a/src/script/components/Meeting/MeetingList/MeetingList.tsx b/src/script/components/Meeting/MeetingList/MeetingList.tsx index ac095395d2a..683d2918f19 100644 --- a/src/script/components/Meeting/MeetingList/MeetingList.tsx +++ b/src/script/components/Meeting/MeetingList/MeetingList.tsx @@ -17,10 +17,11 @@ * */ -import {ReactNode, useState} from 'react'; +import {ReactNode, useCallback, useState} from 'react'; import {Button, ButtonVariant} from '@wireapp/react-ui-kit'; +import {ClockProvider} from 'Components/Meeting/ClockContext'; import { emptyListContainerStyles, emptyTabsListContainerStyles, @@ -29,7 +30,9 @@ import {EmptyMeetingList} from 'Components/Meeting/EmptyMeetingList/EmptyMeeting import {meetingListContainerStyles, showAllButtonStyles} from 'Components/Meeting/MeetingList/MeetingList.styles'; import {MeetingListItemGroup} from 'Components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItemGroup'; import {MeetingTab, MeetingTabs} from 'Components/Meeting/MeetingList/MeetingTabs/MeetingTabs'; -import {getTodayTomorrowLabels, groupByStartHour} from 'Components/Meeting/utils/MeetingDatesHandler'; +import {TodayAndOngoingSection} from 'Components/Meeting/MeetingList/TodayAndOngoingSection/TodayAndOngoingSection'; +import {MEETINGS_PAST, MEETINGS_TODAY, MEETINGS_TOMORROW} from 'Components/Meeting/mocks/MeetingMocks'; +import {getTodayTomorrowLabels, groupByStartHour} from 'Components/Meeting/utils/MeetingDatesUtil'; import {t} from 'Util/LocalizerUtil'; export interface Meeting { @@ -38,6 +41,8 @@ export interface Meeting { schedule: string; conversation_id: string; title: string; + // Ask iOS and Android about how to identify this status + attending?: boolean; } export enum MeetingTabsTitle { @@ -45,76 +50,28 @@ export enum MeetingTabsTitle { PAST = 'past', } -export const MeetingList = () => { - // Temporary mocked data to visualize the UI until the backend is wired - const meetingsToday: Meeting[] = [ - { - start_date: '2025-06-03T07:30:00', - end_date: '2025-06-03T07:40:00', - schedule: 'Single', - conversation_id: '1', - title: 'Meeting 1', - }, - { - start_date: '2025-06-03T07:45:00', - end_date: '2025-06-03T10:15:00', - schedule: 'Single', - conversation_id: '2', - title: 'Meeting 2', - }, - { - start_date: '2025-06-03T08:00:00', - end_date: '2025-06-03T10:15:00', - schedule: 'Daily', - conversation_id: '3', - title: 'Meeting 3', - }, - ]; - - const meetingsTomorrow: Meeting[] = [ - { - start_date: '2025-06-04T07:00:00', - end_date: '2025-06-04T15:15:00', - schedule: 'Single', - conversation_id: '4', - title: 'Meeting 4', - }, - { - start_date: '2025-06-04T08:00:00', - end_date: '2025-06-04T08:15:00', - schedule: 'Monthly', - conversation_id: '5', - title: 'Meeting 5', - }, - { - start_date: '2025-06-04T17:00:00', - end_date: '2025-06-04T18:15:00', - schedule: 'Monthly', - conversation_id: '6', - title: 'Meeting 6', - }, - { - start_date: '2025-06-04T09:00:00', - end_date: '2025-06-04T10:15:00', - schedule: 'Monthly', - conversation_id: '7', - title: 'Meeting 7', - }, - ]; +export interface TodayAndOngoingSectionProps { + meetingsToday: Meeting[]; + headerForOnGoing: string; + headerForToday: string; +} +export const MeetingList = () => { const [activeTab, setActiveTab] = useState(MeetingTabsTitle.NEXT); const {today, tomorrow} = getTodayTomorrowLabels(); + const headerForOnGoing = `${t('meetings.list.onGoing.header')}`; const headerForToday = `${t('meetings.list.today')} (${today})`; const headerForTomorrow = `${t('meetings.list.tomorrow')} (${tomorrow})`; - const groupedMeetingsToday = groupByStartHour(meetingsToday); - const groupedMeetingsTomorrow = groupByStartHour(meetingsTomorrow); + const groupedMeetingsTomorrow = groupByStartHour(MEETINGS_TOMORROW); - const hasMeetingsToday = meetingsToday.length > 0; - const hasMeetingsTomorrow = meetingsTomorrow.length > 0; + const hasMeetingsToday = MEETINGS_TODAY.length > 0; + const hasMeetingsTomorrow = MEETINGS_TOMORROW.length > 0; const isNextTab = activeTab === MeetingTabsTitle.NEXT; + const handleTabChange = useCallback((tab: MeetingTab) => setActiveTab(tab), []); + let content: ReactNode; if (!hasMeetingsToday && !hasMeetingsTomorrow) { @@ -126,10 +83,14 @@ export const MeetingList = () => { } if (isNextTab) { - // Next tab content = hasMeetingsToday ? ( <> - + +
@@ -141,9 +102,8 @@ export const MeetingList = () => {
); } else { - // Past tab content = hasMeetingsTomorrow ? ( - + ) : (
{ return (
- - {content} + + {content}
); }; diff --git a/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingListItem.styles.ts b/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingListItem.styles.ts index 9860ca2956f..5a7ca6072e9 100644 --- a/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingListItem.styles.ts +++ b/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingListItem.styles.ts @@ -20,6 +20,7 @@ import {CSSObject} from '@emotion/react'; export const itemStyles: CSSObject = { + fontSize: '14px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', @@ -42,6 +43,11 @@ export const itemStyles: CSSObject = { }, }; +export const onGoingMeetingStyles: CSSObject = { + background: 'var(--accent-color-highlight)', + border: '1px solid var(--accent-color)', +}; + export const leftStyles: CSSObject = { display: 'flex', alignItems: 'center', diff --git a/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingListItem.tsx b/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingListItem.tsx index d8d80ed9e79..1510b7e5770 100644 --- a/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingListItem.tsx +++ b/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingListItem.tsx @@ -17,8 +17,11 @@ * */ +import {memo, useContext, useMemo} from 'react'; + import {CalendarIcon, CallIcon} from '@wireapp/react-ui-kit'; +import {ClockContext} from 'Components/Meeting/ClockContext'; import {Meeting} from 'Components/Meeting/MeetingList/MeetingList'; import {MeetingAction} from 'Components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingAction/MeetingAction'; import { @@ -27,23 +30,63 @@ import { itemStyles, leftStyles, metaStyles, + onGoingMeetingStyles, rightStyles, titleStyles, } from 'Components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingListItem.styles'; +import {MeetingStatus} from 'Components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingStatus/MeetingStatus'; +import {getMeetingStatusAt, MeetingStatuses} from 'Components/Meeting/utils/MeetingStatusUtil'; import {formatLocale} from 'Util/TimeUtil'; -export const MeetingListItem = ({title, start_date, end_date, schedule}: Meeting) => { - const start = new Date(start_date); - const end = new Date(end_date); +const MeetingListItemComponent = ({title, start_date, end_date, schedule, attending}: Meeting) => { + const nowMs = useContext(ClockContext); + + const {time, showCalendarIcon} = useMemo(() => { + const start = new Date(start_date); + const end = new Date(end_date); + const startMs = start.getTime(); + const endMs = end.getTime(); + const isPast = nowMs > endMs; + const isOngoing = nowMs >= startMs && nowMs < endMs; - const sameMeridiem = formatLocale(start, 'a') === formatLocale(end, 'a'); + if (isPast) { + const dayOfWeek = formatLocale(start, 'EEEE'); + const month = formatLocale(start, 'MMMM'); + const day = formatLocale(start, 'd'); + const time = formatLocale(start, 'h:mm a'); + return { + time: `${dayOfWeek}, ${month} ${day} • Started ${time}`, + showCalendarIcon: false, + }; + } - const time = sameMeridiem - ? `${formatLocale(start, 'h:mm')} – ${formatLocale(end, 'h:mm a')}` - : `${formatLocale(start, 'h:mm a')} – ${formatLocale(end, 'h:mm a')}`; + if (isOngoing) { + const time = formatLocale(start, 'h:mm a'); + return { + time: `Started at ${time}`, + showCalendarIcon: false, + }; + } + + const sameMeridiem = formatLocale(start, 'a') === formatLocale(end, 'a'); + const timeRange = sameMeridiem + ? `${formatLocale(start, 'h:mm')} – ${formatLocale(end, 'h:mm a')}` + : `${formatLocale(start, 'h:mm a')} – ${formatLocale(end, 'h:mm a')}`; + return { + time: timeRange, + showCalendarIcon: true, + }; + }, [start_date, end_date, nowMs]); + + const meetingStatus = useMemo( + () => getMeetingStatusAt(nowMs, start_date, end_date, attending), + [nowMs, start_date, end_date, attending], + ); + + const isOngoing = meetingStatus === MeetingStatuses.ON_GOING || meetingStatus === MeetingStatuses.PARTICIPATING; return ( -
+
@@ -51,7 +94,8 @@ export const MeetingListItem = ({title, start_date, end_date, schedule}: Meeting
{title}
- {time} + {showCalendarIcon && } + {time} {schedule && (
{schedule} @@ -61,8 +105,12 @@ export const MeetingListItem = ({title, start_date, end_date, schedule}: Meeting
+
); }; + +export const MeetingListItem = memo(MeetingListItemComponent); +MeetingListItem.displayName = 'MeetingListItem'; diff --git a/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingStatus/MeetingStatus.styles.ts b/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingStatus/MeetingStatus.styles.ts new file mode 100644 index 00000000000..fe8554071df --- /dev/null +++ b/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingStatus/MeetingStatus.styles.ts @@ -0,0 +1,59 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {CSSObject} from '@emotion/react/dist/emotion-react.cjs'; + +export const participatingStatusStyles = { + color: 'var(--accent-color)', + fontSize: '14px', + fontWeight: 'var(--font-weight-semibold)', + display: 'flex', + alignItems: 'center', +}; + +export const startingSoonStatusStyles: CSSObject = { + color: 'var(--accent-color)', + textTransform: 'uppercase', + fontWeight: 'var(--font-weight-semibold)', +}; + +export const participatingStatusIconStyles = { + marginRight: '8px', + fill: 'var(--accent-color)', +}; + +export const joinButtonContainerStyles = { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +}; + +export const joinButtonStyles = { + height: '32px', + borderRadius: '8px', + fontSize: '14px', + fontWeight: 'var(--font-weight-semibold)', + minWidth: '83px', + color: 'var(--white)', +}; + +export const joinButtonIconStyles: CSSObject = { + marginRight: '8px', + fill: 'var(--white)', +}; diff --git a/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingStatus/MeetingStatus.tsx b/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingStatus/MeetingStatus.tsx new file mode 100644 index 00000000000..a728ed5da61 --- /dev/null +++ b/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingStatus/MeetingStatus.tsx @@ -0,0 +1,82 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {memo, useContext, useMemo} from 'react'; + +import {Button, ButtonVariant, CallIcon} from '@wireapp/react-ui-kit'; + +import {ClockContext} from 'Components/Meeting/ClockContext'; +import { + joinButtonContainerStyles, + joinButtonIconStyles, + joinButtonStyles, + participatingStatusIconStyles, + participatingStatusStyles, + startingSoonStatusStyles, +} from 'Components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingStatus/MeetingStatus.styles'; +import {getCountdownSeconds, getMeetingStatusAt, MeetingStatuses} from 'Components/Meeting/utils/MeetingStatusUtil'; +import {t} from 'Util/LocalizerUtil'; +import {formatSeconds} from 'Util/TimeUtil'; + +export interface MeetingStatusProps { + start_date: string; + end_date: string; + attending?: boolean; +} + +const MeetingStatusComponent = ({start_date, end_date, attending}: MeetingStatusProps) => { + const nowMs = useContext(ClockContext); + + const meetingStatus = useMemo( + () => getMeetingStatusAt(nowMs, start_date, end_date, attending), + [nowMs, start_date, end_date, attending], + ); + + if (meetingStatus === MeetingStatuses.PARTICIPATING) { + return ( +
+ {t('meetings.meetingStatus.participating')} +
+ ); + } + + if (meetingStatus === MeetingStatuses.ON_GOING) { + return ( +
+ +
+ ); + } + + if (meetingStatus === MeetingStatuses.STARTING_SOON) { + // eslint-disable-next-line react-hooks/rules-of-hooks + const countdown = useMemo(() => { + const seconds = getCountdownSeconds(nowMs, start_date); + return formatSeconds(seconds); + }, [nowMs, start_date]); + return
{t('meetings.meetingStatus.startingIn', {countdown})}
; + } + + return null; +}; + +export const MeetingStatus = memo(MeetingStatusComponent); +MeetingStatus.displayName = 'MeetingStatus'; diff --git a/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItemGroup.tsx b/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItemGroup.tsx index dea978edc1f..45e7a7b39da 100644 --- a/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItemGroup.tsx +++ b/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItemGroup.tsx @@ -17,6 +17,8 @@ * */ +import {memo} from 'react'; + import {set} from 'date-fns'; import {Meeting, MeetingTabsTitle} from 'Components/Meeting/MeetingList/MeetingList'; @@ -40,7 +42,7 @@ export enum MeetingGroupBy { HOUR = 'hour', } -export const MeetingListItemGroup = ({ +const MeetingListItemGroupComponent = ({ header, groupedMeetings, view = MeetingTabsTitle.NEXT, @@ -53,8 +55,8 @@ export const MeetingListItemGroup = ({ minute: '2-digit', }); - // Sort by hour key (string -> number), then drop empty buckets - const groups = Object.entries(groupedMeetings).sort(([a], [b]) => +a - +b); + // Sort by hour key + const groups = Object.entries(groupedMeetings).sort(([meetingA], [meetingB]) => +meetingA - +meetingB); const nonEmptyGroups = groups.filter(([, items]) => items?.length); const isEmpty = nonEmptyGroups.length === 0; @@ -95,3 +97,6 @@ export const MeetingListItemGroup = ({ ); }; + +export const MeetingListItemGroup = memo(MeetingListItemGroupComponent); +MeetingListItemGroup.displayName = 'MeetingListItemGroup'; diff --git a/src/script/components/Meeting/MeetingList/TodayAndOngoingSection/TodayAndOngoingSection.tsx b/src/script/components/Meeting/MeetingList/TodayAndOngoingSection/TodayAndOngoingSection.tsx new file mode 100644 index 00000000000..32288f6d9d2 --- /dev/null +++ b/src/script/components/Meeting/MeetingList/TodayAndOngoingSection/TodayAndOngoingSection.tsx @@ -0,0 +1,60 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {memo, useContext, useMemo} from 'react'; + +import {ClockContext} from 'Components/Meeting/ClockContext'; +import {MeetingTabsTitle, TodayAndOngoingSectionProps} from 'Components/Meeting/MeetingList/MeetingList'; +import {MeetingListItemGroup} from 'Components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItemGroup'; +import {groupByStartHour} from 'Components/Meeting/utils/MeetingDatesUtil'; +import {getOnGoingMeetingsAt} from 'Components/Meeting/utils/MeetingStatusUtil'; + +const TodayAndOngoingSectionComponent = ({ + meetingsToday, + headerForOnGoing, + headerForToday, +}: TodayAndOngoingSectionProps) => { + const nowMs = useContext(ClockContext); + + const onGoingMeetings = useMemo(() => getOnGoingMeetingsAt(meetingsToday, nowMs), [meetingsToday, nowMs]); + const ongoingIds = useMemo(() => new Set(onGoingMeetings.map(meeting => meeting.conversation_id)), [onGoingMeetings]); + + // Exclude ongoing items from today's grouped list + const todayNotOngoing = useMemo( + () => meetingsToday.filter(meeting => !ongoingIds.has(meeting.conversation_id)), + [meetingsToday, ongoingIds], + ); + const groupedMeetingsToday = useMemo(() => groupByStartHour(todayNotOngoing), [todayNotOngoing]); + + return ( + <> + {onGoingMeetings.length > 0 && ( + + )} + + + ); +}; + +export const TodayAndOngoingSection = memo(TodayAndOngoingSectionComponent); +TodayAndOngoingSection.displayName = 'TodayAndOngoingSection'; diff --git a/src/script/components/Meeting/Meeting.tsx b/src/script/components/Meeting/Meetings.tsx similarity index 96% rename from src/script/components/Meeting/Meeting.tsx rename to src/script/components/Meeting/Meetings.tsx index fff40470e1f..7f978e5f801 100644 --- a/src/script/components/Meeting/Meeting.tsx +++ b/src/script/components/Meeting/Meetings.tsx @@ -21,7 +21,7 @@ import {contentStyles} from 'Components/Meeting/Meeting.styles'; import {MeetingHeader} from 'Components/Meeting/MeetingHeader/MeetingHeader'; import {MeetingList} from 'Components/Meeting/MeetingList/MeetingList'; -export const Meeting = () => ( +export const Meetings = () => ( <>
diff --git a/src/script/components/Meeting/mocks/MeetingMocks.ts b/src/script/components/Meeting/mocks/MeetingMocks.ts new file mode 100644 index 00000000000..bf039af118f --- /dev/null +++ b/src/script/components/Meeting/mocks/MeetingMocks.ts @@ -0,0 +1,130 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +// This whole file is just for mocking purposes, so we can use the MeetingList component +// Once the backend is ready, we can remove this file + +import {Meeting} from 'Components/Meeting/MeetingList/MeetingList'; + +const now = new Date(); +const toISO = (d: Date) => d.toISOString(); + +const addMinutes = (d: Date, m: number) => { + const copy = new Date(d); + copy.setMinutes(copy.getMinutes() + m); + return copy; +}; + +const addSeconds = (d: Date, s: number) => { + const copy = new Date(d); + copy.setSeconds(copy.getSeconds() + s); + return copy; +}; + +const addHours = (d: Date, s: number) => { + const copy = new Date(d); + copy.setHours(copy.getHours() + s); + return copy; +}; + +const addDay = (d: Date, s: number) => { + const copy = new Date(d); + copy.setDate(copy.getDate() + s); + return copy; +}; + +export const MEETINGS_TODAY: Meeting[] = [ + { + start_date: toISO(addSeconds(now, 5)), + end_date: toISO(addMinutes(now, 33)), + schedule: 'Single', + conversation_id: '1', + title: 'Daily stand‑up', + }, + { + start_date: toISO(addHours(now, -2)), + end_date: toISO(addMinutes(now, 28)), + schedule: 'Single', + conversation_id: '2', + title: 'Sprint planning', + attending: true, + }, + { + start_date: toISO(addHours(now, 2)), + end_date: toISO(addHours(now, 3)), + schedule: 'Single', + conversation_id: '3', + title: 'Client sync', + }, + { + start_date: toISO(addHours(now, -3)), + end_date: toISO(addHours(now, -2)), + schedule: 'Single', + conversation_id: '4', + title: 'Retrospective passed Today', + }, +]; + +export const MEETINGS_TOMORROW: Meeting[] = [ + { + start_date: toISO(addDay(addMinutes(now, -120), 1)), + end_date: toISO(addDay(addMinutes(now, -30), 1)), + schedule: 'Single', + conversation_id: '4', + title: 'Meetings 4', + }, + { + start_date: toISO(addDay(now, 1)), + end_date: toISO(addDay(addMinutes(now, 20), 1)), + schedule: 'Monthly', + conversation_id: '5', + title: 'Meetings 5', + }, + { + start_date: toISO(addDay(addMinutes(now, 180), 1)), + end_date: toISO(addDay(addMinutes(now, 240), 1)), + schedule: 'Monthly', + conversation_id: '6', + title: 'Meetings 6', + }, + { + start_date: toISO(addDay(addMinutes(now, 300), 1)), + end_date: toISO(addDay(addMinutes(now, 460), 1)), + schedule: 'Monthly', + conversation_id: '7', + title: 'Meetings 7', + }, +]; + +export const MEETINGS_PAST: Meeting[] = [ + { + start_date: toISO(addDay(addMinutes(now, -120), -1)), + end_date: toISO(addDay(addMinutes(now, -20), -1)), + schedule: 'Single', + conversation_id: '4', + title: 'Retrospective passed Yesterday', + }, + { + start_date: toISO(addDay(addMinutes(now, -60), -1)), + end_date: toISO(addDay(now, -1)), + schedule: 'Monthly', + conversation_id: '5', + title: 'All‑hands', + }, +]; diff --git a/src/script/components/Meeting/utils/MeetingDatesHandler.ts b/src/script/components/Meeting/utils/MeetingDatesUtil.ts similarity index 56% rename from src/script/components/Meeting/utils/MeetingDatesHandler.ts rename to src/script/components/Meeting/utils/MeetingDatesUtil.ts index a8d9dd0e0ac..03d102197cb 100644 --- a/src/script/components/Meeting/utils/MeetingDatesHandler.ts +++ b/src/script/components/Meeting/utils/MeetingDatesUtil.ts @@ -20,9 +20,20 @@ import {Meeting} from 'Components/Meeting/MeetingList/MeetingList'; import {FnDate, formatLocale} from 'Util/TimeUtil'; -const formatWeekdayMonthDay = (date: FnDate | string | number) => formatLocale(date, 'EEEE, MMMM d'); +/** + * Formats a given date into a string with the pattern "Weekday, Month Day". + * + * @param {FnDate | string | number} date - The date to format. Can be a `FnDate`, string, or timestamp. + * @returns {string} - The formatted date string. + */ +const formatWeekdayMonthDay = (date: FnDate | string | number): string => formatLocale(date, 'EEEE, MMMM d'); -export const getTodayTomorrowLabels = () => { +/** + * Generates labels for "today" and "tomorrow" with their respective formatted dates. + * + * @returns {{today: string, tomorrow: string}} - An object containing the formatted labels for today and tomorrow. + */ +export const getTodayTomorrowLabels = (): {today: string; tomorrow: string} => { const today = new Date(); const tomorrow = new Date(); tomorrow.setDate(today.getDate() + 1); @@ -33,7 +44,13 @@ export const getTodayTomorrowLabels = () => { }; }; -export const groupByStartHour = (meetings: Meeting[]) => { +/** + * Groups a list of meetings by their start hour. + * + * @param {Meeting[]} meetings - The list of meetings to group. + * @returns {Record} - An object where the keys are the start hours (0-23) and the values are arrays of meetings. + */ +export const groupByStartHour = (meetings: Meeting[]): Record => { const groupedMeetings: Record = {}; for (const meeting of meetings) { const hour = new Date(meeting.start_date).getHours(); diff --git a/src/script/components/Meeting/utils/MeetingStatusUtil.ts b/src/script/components/Meeting/utils/MeetingStatusUtil.ts new file mode 100644 index 00000000000..d436cdad480 --- /dev/null +++ b/src/script/components/Meeting/utils/MeetingStatusUtil.ts @@ -0,0 +1,93 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {Meeting} from 'Components/Meeting/MeetingList/MeetingList'; +import {TIME_IN_MILLIS} from 'Util/TimeUtil'; + +export enum MeetingStatuses { + ON_GOING = 'on_going', + STARTING_SOON = 'starting_soon', + PARTICIPATING = 'participating', + UPCOMING = 'upcoming', + PAST = 'past', +} + +/** + * Threshold in milliseconds to determine if a meeting is starting soon. + */ +export const STARTING_SOON_THRESHOLD_MS = 5 * TIME_IN_MILLIS.MINUTE; + +/** + * Filters the list of meetings to return only those that are ongoing at the specified time. + * + * @param {Meeting[]} meetings - The list of meetings to filter. + * @param {number} nowMs - The current time in milliseconds. + * @returns {Meeting[]} - The list of ongoing meetings. + */ +export const getOnGoingMeetingsAt = (meetings: Meeting[], nowMs: number): Meeting[] => + meetings.filter(meeting => { + const startMs = new Date(meeting.start_date).getTime(); + const endMs = new Date(meeting.end_date).getTime(); + return nowMs >= startMs && nowMs < endMs; + }); + +/** + * Determines the status of a meeting at a specific time. + * + * @param {number} nowMs - The current time in milliseconds. + * @param {string} start_date - The start date of the meeting in ISO format. + * @param {string} end_date - The end date of the meeting in ISO format. + * @param {boolean} [attending=false] - Whether the user is attending the meeting. + * @returns {MeetingStatuses} - The status of the meeting. + */ +export const getMeetingStatusAt = ( + nowMs: number, + start_date: string, + end_date: string, + attending: boolean = false, +): MeetingStatuses => { + const startMs = new Date(start_date).getTime(); + const endMs = new Date(end_date).getTime(); + + if (nowMs > endMs) { + return MeetingStatuses.PAST; + } + + if (nowMs >= startMs) { + return attending ? MeetingStatuses.PARTICIPATING : MeetingStatuses.ON_GOING; + } + + if (startMs <= nowMs + STARTING_SOON_THRESHOLD_MS) { + return MeetingStatuses.STARTING_SOON; + } + + return MeetingStatuses.UPCOMING; +}; + +/** + * Calculates the countdown in seconds until a meeting starts. + * + * @param {number} nowMs - The current time in milliseconds. + * @param {string} start_date - The start date of the meeting in ISO format. + * @returns {number} - The countdown in seconds, or 0 if the meeting has already started. + */ +export const getCountdownSeconds = (nowMs: number, start_date: string): number => { + const startMs = new Date(start_date).getTime(); + return Math.max(0, Math.ceil((startMs - nowMs) / TIME_IN_MILLIS.SECOND)); +}; diff --git a/src/script/page/MainContent/MainContent.tsx b/src/script/page/MainContent/MainContent.tsx index 34888e309e7..3e71aee6fbd 100644 --- a/src/script/page/MainContent/MainContent.tsx +++ b/src/script/page/MainContent/MainContent.tsx @@ -29,7 +29,7 @@ import {Conversation} from 'Components/Conversation'; import {HistoryExport} from 'Components/HistoryExport'; import {HistoryImport} from 'Components/HistoryImport'; import * as Icon from 'Components/Icon'; -import {Meeting} from 'Components/Meeting/Meeting'; +import {Meetings} from 'Components/Meeting/Meetings'; import {useLegalHoldModalState} from 'Components/Modals/LegalHoldModal/LegalHoldModal.state'; import {ClientState} from 'Repositories/client/ClientState'; import {ConversationState} from 'Repositories/conversation/ConversationState'; @@ -280,7 +280,7 @@ const MainContent = ({ /> )} - {contentState === ContentState.MEETINGS && } + {contentState === ContentState.MEETINGS && } diff --git a/src/types/i18n.d.ts b/src/types/i18n.d.ts index f0f347e6516..7b24302cc72 100644 --- a/src/types/i18n.d.ts +++ b/src/types/i18n.d.ts @@ -584,6 +584,7 @@ declare module 'I18n/en-US.json' { 'conversationAudioAssetUploadFailed': `Upload failed: {name}`; 'conversationAudioAssetUploading': `Uploading: {name}`; 'conversationButtonSeparator': `or`; + 'conversationCellsConversationEnabled': `File collaboration (Cells beta) is on`; 'conversationClassified': `Security level: VS-NfD`; 'conversationCommonFeature1': `Up to [bold]{capacity}[/bold] people`; 'conversationCommonFeature2': `Video conferencing`; @@ -602,7 +603,6 @@ declare module 'I18n/en-US.json' { 'conversationContextMenuLike': `Like`; 'conversationContextMenuReply': `Reply`; 'conversationContextMenuUnlike': `Unlike`; - 'conversationCellsConversationEnabled': `File collaboration (Cells beta) is on`; 'conversationCreateReceiptsEnabled': `Read receipts are on`; 'conversationCreateTeam': `with [showmore]all team members[/showmore]`; 'conversationCreateTeamGuest': `with [showmore]all team members and one guest[/showmore]`; @@ -625,8 +625,8 @@ declare module 'I18n/en-US.json' { 'conversationDetailsActionArchive': `Archive`; 'conversationDetailsActionBlock': `Block`; 'conversationDetailsActionCancelRequest': `Cancel request`; - 'conversationDetailsActionCellsTitle': `File collaboration (Cells beta version) is on`; 'conversationDetailsActionCellsOption': `Permanently on for this conversation.`; + 'conversationDetailsActionCellsTitle': `File collaboration (Cells beta version) is on`; 'conversationDetailsActionClear': `Clear content`; 'conversationDetailsActionConversationParticipants': `Show all ({number})`; 'conversationDetailsActionCreateGroup': `Create group`; @@ -1202,7 +1202,11 @@ declare module 'I18n/en-US.json' { 'meetings.tabs.next': `Next`; 'meetings.tabs.past': `Past`; 'meetings.list.today': `Today`; + 'meetings.list.onGoing': `Ongoing`; + 'meetings.list.onGoing.header': `Now`; 'meetings.list.tomorrow': `Tomorrow`; + 'meetings.meetingStatus.participating': `Attending`; + 'meetings.meetingStatus.startingIn': `Starting in {countdown}`; 'meetings.startMeetingHelp': `Start a meeting with team members, guests, or external parties. Your communication is always end-to-end encrypted, offering the highest level of security.`; 'mlsConversationRecovered': `You haven\'t used this device for a while, or an issue has occurred. Some older messages may not appear here.`; 'mlsSignature': `MLS with {signature} Signature`; From c3184e8f3b3d84d637a7243267715e9f7b50fdc6 Mon Sep 17 00:00:00 2001 From: zskhan Date: Wed, 19 Nov 2025 00:27:07 +0100 Subject: [PATCH 2/3] feat: use translations instead of harcoded strings --- src/i18n/en-US.json | 9 +++++++++ .../Meeting/EmptyMeetingList/EmptyMeetingList.tsx | 4 ++-- .../MeetNowMultiActionButton.tsx | 11 ++++++----- .../MeetingListItem/MeetingAction/MeetingAction.tsx | 13 +++++++------ .../MeetingListItem/MeetingListItem.tsx | 5 +++-- src/types/i18n.d.ts | 9 +++++++++ 6 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/i18n/en-US.json b/src/i18n/en-US.json index 7900a429142..f6f82cb5fbf 100644 --- a/src/i18n/en-US.json +++ b/src/i18n/en-US.json @@ -1203,7 +1203,16 @@ "meetings.list.tomorrow": "Tomorrow", "meetings.meetingStatus.participating": "Attending", "meetings.meetingStatus.startingIn": "Starting in {countdown}", + "meetings.meetingStatus.startedAt": "Started at {time}", "meetings.startMeetingHelp": "Start a meeting with team members, guests, or external parties. Your communication is always end-to-end encrypted, offering the highest level of security.", + "meetings.action.meetNow": "Meet Now", + "meetings.action.scheduleMeeting": "Schedule Meeting", + "meetings.action.startMeeting": "Start meeting", + "meetings.action.createConversation": "Create conversation", + "meetings.action.copyLink": "Copy link", + "meetings.action.editMeeting": "Edit meeting", + "meetings.action.deleteMeetingForMe": "Delete meeting for me", + "meetings.action.deleteMeetingForAll": "Delete meeting for everyone", "mlsConversationRecovered": "You haven't used this device for a while, or an issue has occurred. Some older messages may not appear here.", "mlsSignature": "MLS with {signature} Signature", "mlsThumbprint": "MLS Thumbprint", diff --git a/src/script/components/Meeting/EmptyMeetingList/EmptyMeetingList.tsx b/src/script/components/Meeting/EmptyMeetingList/EmptyMeetingList.tsx index 84d8a5c357e..6574b9de801 100644 --- a/src/script/components/Meeting/EmptyMeetingList/EmptyMeetingList.tsx +++ b/src/script/components/Meeting/EmptyMeetingList/EmptyMeetingList.tsx @@ -41,10 +41,10 @@ export const EmptyMeetingList = ({text, helperText, showCallingButton = true}: E {showCallingButton && (
)} diff --git a/src/script/components/Meeting/MeetNowMultiActionButton/MeetNowMultiActionButton.tsx b/src/script/components/Meeting/MeetNowMultiActionButton/MeetNowMultiActionButton.tsx index 9c648d98cbe..038c34379a2 100644 --- a/src/script/components/Meeting/MeetNowMultiActionButton/MeetNowMultiActionButton.tsx +++ b/src/script/components/Meeting/MeetNowMultiActionButton/MeetNowMultiActionButton.tsx @@ -25,6 +25,7 @@ import { callingButtonGroupStyles, dropdownIconStyles, } from 'Components/Meeting/MeetNowMultiActionButton/MeetNowMultiActionButton.styles'; +import {t} from 'Util/LocalizerUtil'; import {showContextMenu} from '../../../ui/ContextMenu'; @@ -40,16 +41,16 @@ export const MeetNowMultiActionButton = () => { offset: 0, entries: [ { - title: 'Meet Now', - label: 'Meet Now', + title: t('meetings.action.meetNow'), + label: t('meetings.action.meetNow'), click: () => { handleMeetingButton(); resetIconInversion(); }, }, { - title: 'Schedule Meeting', - label: 'Schedule Meeting', + title: t('meetings.action.scheduleMeeting'), + label: t('meetings.action.scheduleMeeting'), click: () => { // add scheduling functionality here resetIconInversion(); @@ -75,7 +76,7 @@ export const MeetNowMultiActionButton = () => { icon={} onClick={handleMeetingButton} > - Meet now + {t('meetings.action.meetNow')} { entries: [ { icon: () => , - label: 'Start meeting', + label: t('meetings.action.startMeeting'), }, { icon: () => , - label: 'Create conversation', + label: t('meetings.action.createConversation'), }, { icon: () => , - label: 'Copy link', + label: t('meetings.action.copyLink'), }, { icon: () => , - label: 'Edit meeting', + label: t('meetings.action.editMeeting'), }, { css: contextMenuDangerItemStyles, icon: () => , - label: 'Remove meeting for me', + label: t('meetings.action.deleteMeetingForMe'), }, { css: contextMenuDangerItemStyles, icon: () => , - label: 'Delete meeting for everyone', + label: t('meetings.action.deleteMeetingForAll'), }, ], identifier: 'message-options-menu', diff --git a/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingListItem.tsx b/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingListItem.tsx index 1510b7e5770..7c72f2be314 100644 --- a/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingListItem.tsx +++ b/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingListItem.tsx @@ -36,6 +36,7 @@ import { } from 'Components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingListItem.styles'; import {MeetingStatus} from 'Components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingStatus/MeetingStatus'; import {getMeetingStatusAt, MeetingStatuses} from 'Components/Meeting/utils/MeetingStatusUtil'; +import {t} from 'Util/LocalizerUtil'; import {formatLocale} from 'Util/TimeUtil'; const MeetingListItemComponent = ({title, start_date, end_date, schedule, attending}: Meeting) => { @@ -55,7 +56,7 @@ const MeetingListItemComponent = ({title, start_date, end_date, schedule, attend const day = formatLocale(start, 'd'); const time = formatLocale(start, 'h:mm a'); return { - time: `${dayOfWeek}, ${month} ${day} • Started ${time}`, + time: `${dayOfWeek}, ${month} ${day} • ${t('meetings.meetingStatus.startedAt', {time})}`, showCalendarIcon: false, }; } @@ -63,7 +64,7 @@ const MeetingListItemComponent = ({title, start_date, end_date, schedule, attend if (isOngoing) { const time = formatLocale(start, 'h:mm a'); return { - time: `Started at ${time}`, + time: t('meetings.meetingStatus.startedAt', {time}), showCalendarIcon: false, }; } diff --git a/src/types/i18n.d.ts b/src/types/i18n.d.ts index 7b24302cc72..050963b3e3f 100644 --- a/src/types/i18n.d.ts +++ b/src/types/i18n.d.ts @@ -1207,7 +1207,16 @@ declare module 'I18n/en-US.json' { 'meetings.list.tomorrow': `Tomorrow`; 'meetings.meetingStatus.participating': `Attending`; 'meetings.meetingStatus.startingIn': `Starting in {countdown}`; + 'meetings.meetingStatus.startedAt': `Started at {time}`; 'meetings.startMeetingHelp': `Start a meeting with team members, guests, or external parties. Your communication is always end-to-end encrypted, offering the highest level of security.`; + 'meetings.action.meetNow': `Meet Now`; + 'meetings.action.scheduleMeeting': `Schedule Meeting`; + 'meetings.action.startMeeting': `Start meeting`; + 'meetings.action.createConversation': `Create conversation`; + 'meetings.action.copyLink': `Copy link`; + 'meetings.action.editMeeting': `Edit meeting`; + 'meetings.action.deleteMeetingForMe': `Delete meeting for me`; + 'meetings.action.deleteMeetingForAll': `Delete meeting for everyone`; 'mlsConversationRecovered': `You haven\'t used this device for a while, or an issue has occurred. Some older messages may not appear here.`; 'mlsSignature': `MLS with {signature} Signature`; 'mlsThumbprint': `MLS Thumbprint`; From 16487d14b643e000e537535675ef719cc3b413e4 Mon Sep 17 00:00:00 2001 From: zskhan Date: Wed, 19 Nov 2025 11:58:53 +0100 Subject: [PATCH 3/3] feat: review compliance. --- .../Meeting/MeetingList/MeetingList.tsx | 52 +++++++++++-------- .../MeetingListItem/MeetingListItem.styles.ts | 4 +- .../MeetingListItem/MeetingListItem.tsx | 12 +++-- .../MeetingStatus/MeetingStatus.styles.ts | 4 +- .../MeetingStatus/MeetingStatus.tsx | 48 ++++++++--------- .../MeetingListItemGroup.styles.ts | 2 +- .../MeetingListItemGroup.tsx | 8 +-- .../MeetingList/MeetingTabs/MeetingTabs.tsx | 20 +++---- .../TodayAndOngoingSection.tsx | 27 ++++++---- .../components/Meeting/mocks/MeetingMocks.ts | 8 +-- .../Meeting/utils/MeetingDatesUtil.ts | 10 ++-- .../Meeting/utils/MeetingStatusUtil.ts | 22 ++++---- 12 files changed, 116 insertions(+), 101 deletions(-) diff --git a/src/script/components/Meeting/MeetingList/MeetingList.tsx b/src/script/components/Meeting/MeetingList/MeetingList.tsx index 683d2918f19..69f827f523e 100644 --- a/src/script/components/Meeting/MeetingList/MeetingList.tsx +++ b/src/script/components/Meeting/MeetingList/MeetingList.tsx @@ -35,7 +35,7 @@ import {MEETINGS_PAST, MEETINGS_TODAY, MEETINGS_TOMORROW} from 'Components/Meeti import {getTodayTomorrowLabels, groupByStartHour} from 'Components/Meeting/utils/MeetingDatesUtil'; import {t} from 'Util/LocalizerUtil'; -export interface Meeting { +export interface MeetingEntity { start_date: string; end_date: string; schedule: string; @@ -45,19 +45,19 @@ export interface Meeting { attending?: boolean; } -export enum MeetingTabsTitle { - NEXT = 'next', +export enum MEETING_TABS_TITLE { + UPCOMING = 'upcoming', PAST = 'past', } export interface TodayAndOngoingSectionProps { - meetingsToday: Meeting[]; + meetingsToday: MeetingEntity[]; headerForOnGoing: string; headerForToday: string; } export const MeetingList = () => { - const [activeTab, setActiveTab] = useState(MeetingTabsTitle.NEXT); + const [activeTab, setActiveTab] = useState(MEETING_TABS_TITLE.UPCOMING); const {today, tomorrow} = getTodayTomorrowLabels(); const headerForOnGoing = `${t('meetings.list.onGoing.header')}`; @@ -68,7 +68,7 @@ export const MeetingList = () => { const hasMeetingsToday = MEETINGS_TODAY.length > 0; const hasMeetingsTomorrow = MEETINGS_TOMORROW.length > 0; - const isNextTab = activeTab === MeetingTabsTitle.NEXT; + const isUpcomingTab = activeTab === MEETING_TABS_TITLE.UPCOMING; const handleTabChange = useCallback((tab: MeetingTab) => setActiveTab(tab), []); @@ -82,8 +82,16 @@ export const MeetingList = () => { ); } - if (isNextTab) { - content = hasMeetingsToday ? ( + if (isUpcomingTab) { + if (!hasMeetingsToday) { + return ( +
+ +
+ ); + } + + content = ( <> {
- ) : ( -
- -
); } else { - content = hasMeetingsTomorrow ? ( - - ) : ( -
- -
- ); + if (!hasMeetingsTomorrow) { + return ( +
+ +
+ ); + } + + content = ; } return ( diff --git a/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingListItem.styles.ts b/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingListItem.styles.ts index 5a7ca6072e9..a0e439c535f 100644 --- a/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingListItem.styles.ts +++ b/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingListItem.styles.ts @@ -20,7 +20,7 @@ import {CSSObject} from '@emotion/react'; export const itemStyles: CSSObject = { - fontSize: '14px', + fontSize: 'var(--font-size-medium)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', @@ -60,7 +60,7 @@ export const titleStyles: CSSObject = { export const metaStyles: CSSObject = { color: 'var(--secondary-text-color)', - fontSize: 12, + fontSize: 'var(--font-size-small)', marginTop: 4, display: 'flex', alignItems: 'center', diff --git a/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingListItem.tsx b/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingListItem.tsx index 7c72f2be314..1fd8b9715eb 100644 --- a/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingListItem.tsx +++ b/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingListItem.tsx @@ -22,7 +22,7 @@ import {memo, useContext, useMemo} from 'react'; import {CalendarIcon, CallIcon} from '@wireapp/react-ui-kit'; import {ClockContext} from 'Components/Meeting/ClockContext'; -import {Meeting} from 'Components/Meeting/MeetingList/MeetingList'; +import {MeetingEntity} from 'Components/Meeting/MeetingList/MeetingList'; import {MeetingAction} from 'Components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingAction/MeetingAction'; import { badgeWrapperStyles, @@ -35,11 +35,11 @@ import { titleStyles, } from 'Components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingListItem.styles'; import {MeetingStatus} from 'Components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingStatus/MeetingStatus'; -import {getMeetingStatusAt, MeetingStatuses} from 'Components/Meeting/utils/MeetingStatusUtil'; +import {getMeetingStatusAt, MEETING_STATUS} from 'Components/Meeting/utils/MeetingStatusUtil'; import {t} from 'Util/LocalizerUtil'; import {formatLocale} from 'Util/TimeUtil'; -const MeetingListItemComponent = ({title, start_date, end_date, schedule, attending}: Meeting) => { +const MeetingListItemComponent = ({title, start_date, end_date, schedule, attending}: MeetingEntity) => { const nowMs = useContext(ClockContext); const {time, showCalendarIcon} = useMemo(() => { @@ -84,10 +84,12 @@ const MeetingListItemComponent = ({title, start_date, end_date, schedule, attend [nowMs, start_date, end_date, attending], ); - const isOngoing = meetingStatus === MeetingStatuses.ON_GOING || meetingStatus === MeetingStatuses.PARTICIPATING; + const isOngoing = meetingStatus === MEETING_STATUS.ON_GOING || meetingStatus === MEETING_STATUS.PARTICIPATING; + + const meetingItemStyles = isOngoing ? [itemStyles, onGoingMeetingStyles] : [itemStyles]; return ( -
+
diff --git a/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingStatus/MeetingStatus.styles.ts b/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingStatus/MeetingStatus.styles.ts index fe8554071df..5961c6c7fda 100644 --- a/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingStatus/MeetingStatus.styles.ts +++ b/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingStatus/MeetingStatus.styles.ts @@ -21,7 +21,7 @@ import {CSSObject} from '@emotion/react/dist/emotion-react.cjs'; export const participatingStatusStyles = { color: 'var(--accent-color)', - fontSize: '14px', + fontSize: 'var(--font-size-medium)', fontWeight: 'var(--font-weight-semibold)', display: 'flex', alignItems: 'center', @@ -47,7 +47,7 @@ export const joinButtonContainerStyles = { export const joinButtonStyles = { height: '32px', borderRadius: '8px', - fontSize: '14px', + fontSize: 'var(--font-size-medium)', fontWeight: 'var(--font-weight-semibold)', minWidth: '83px', color: 'var(--white)', diff --git a/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingStatus/MeetingStatus.tsx b/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingStatus/MeetingStatus.tsx index a728ed5da61..0f6591a1b8c 100644 --- a/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingStatus/MeetingStatus.tsx +++ b/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingStatus/MeetingStatus.tsx @@ -30,7 +30,7 @@ import { participatingStatusStyles, startingSoonStatusStyles, } from 'Components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingStatus/MeetingStatus.styles'; -import {getCountdownSeconds, getMeetingStatusAt, MeetingStatuses} from 'Components/Meeting/utils/MeetingStatusUtil'; +import {getCountdownSeconds, getMeetingStatusAt, MEETING_STATUS} from 'Components/Meeting/utils/MeetingStatusUtil'; import {t} from 'Util/LocalizerUtil'; import {formatSeconds} from 'Util/TimeUtil'; @@ -48,34 +48,32 @@ const MeetingStatusComponent = ({start_date, end_date, attending}: MeetingStatus [nowMs, start_date, end_date, attending], ); - if (meetingStatus === MeetingStatuses.PARTICIPATING) { - return ( -
- {t('meetings.meetingStatus.participating')} -
- ); - } + switch (meetingStatus) { + case MEETING_STATUS.PARTICIPATING: + return ( +
+ {t('meetings.meetingStatus.participating')} +
+ ); - if (meetingStatus === MeetingStatuses.ON_GOING) { - return ( -
- -
- ); - } + case MEETING_STATUS.ON_GOING: + return ( +
+ +
+ ); - if (meetingStatus === MeetingStatuses.STARTING_SOON) { - // eslint-disable-next-line react-hooks/rules-of-hooks - const countdown = useMemo(() => { + case MEETING_STATUS.STARTING_SOON: { const seconds = getCountdownSeconds(nowMs, start_date); - return formatSeconds(seconds); - }, [nowMs, start_date]); - return
{t('meetings.meetingStatus.startingIn', {countdown})}
; - } + const countdown = formatSeconds(seconds); + return
{t('meetings.meetingStatus.startingIn', {countdown})}
; + } - return null; + default: + return null; + } }; export const MeetingStatus = memo(MeetingStatusComponent); diff --git a/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItemGroup.styles.ts b/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItemGroup.styles.ts index 756ad365aa7..9ccd228604f 100644 --- a/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItemGroup.styles.ts +++ b/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItemGroup.styles.ts @@ -31,7 +31,7 @@ export const sectionHeaderStyles: CSSObject = { export const hourLabelStyles: CSSObject = { color: 'var(--secondary-text-color)', - fontSize: '12px', + fontSize: 'var(--font-size-small)', marginTop: 12, marginBottom: 8, }; diff --git a/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItemGroup.tsx b/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItemGroup.tsx index 45e7a7b39da..f063a346f1c 100644 --- a/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItemGroup.tsx +++ b/src/script/components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItemGroup.tsx @@ -21,7 +21,7 @@ import {memo} from 'react'; import {set} from 'date-fns'; -import {Meeting, MeetingTabsTitle} from 'Components/Meeting/MeetingList/MeetingList'; +import {MEETING_TABS_TITLE, MeetingEntity} from 'Components/Meeting/MeetingList/MeetingList'; import {MeetingListItem} from 'Components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingListItem'; import { hourLabelStyles, @@ -33,7 +33,7 @@ import {t} from 'Util/LocalizerUtil'; interface MeetingListItemGroupProps { header?: string; - groupedMeetings: Record; + groupedMeetings: Record; view?: MeetingTab; } @@ -45,9 +45,9 @@ export enum MeetingGroupBy { const MeetingListItemGroupComponent = ({ header, groupedMeetings, - view = MeetingTabsTitle.NEXT, + view = MEETING_TABS_TITLE.UPCOMING, }: MeetingListItemGroupProps) => { - const groupBy = view === MeetingTabsTitle.PAST ? MeetingGroupBy.NONE : MeetingGroupBy.HOUR; + const groupBy = view === MEETING_TABS_TITLE.PAST ? MeetingGroupBy.NONE : MeetingGroupBy.HOUR; const formatHourLabel = (date: string) => set(new Date(date), {minutes: 0, seconds: 0, milliseconds: 0}).toLocaleTimeString([], { diff --git a/src/script/components/Meeting/MeetingList/MeetingTabs/MeetingTabs.tsx b/src/script/components/Meeting/MeetingList/MeetingTabs/MeetingTabs.tsx index 230363ae48f..fbf41189e34 100644 --- a/src/script/components/Meeting/MeetingList/MeetingTabs/MeetingTabs.tsx +++ b/src/script/components/Meeting/MeetingList/MeetingTabs/MeetingTabs.tsx @@ -17,11 +17,11 @@ * */ -import {MeetingTabsTitle} from 'Components/Meeting/MeetingList/MeetingList'; +import {MEETING_TABS_TITLE} from 'Components/Meeting/MeetingList/MeetingList'; import {tabStyles, tabsWrapperStyles} from 'Components/Meeting/MeetingList/MeetingTabs/MeetingTabs.styles'; import {t} from 'Util/LocalizerUtil'; -export type MeetingTab = MeetingTabsTitle.NEXT | MeetingTabsTitle.PAST; +export type MeetingTab = MEETING_TABS_TITLE.UPCOMING | MEETING_TABS_TITLE.PAST; interface MeetingTabsProps { active: MeetingTab; @@ -32,21 +32,21 @@ export const MeetingTabs = ({active, onChange}: MeetingTabsProps) => (
onChange(MeetingTabsTitle.NEXT)} - onKeyDown={event => (event.key === 'Enter' || event.key === ' ') && onChange(MeetingTabsTitle.NEXT)} + css={tabStyles(active === MEETING_TABS_TITLE.UPCOMING)} + onClick={() => onChange(MEETING_TABS_TITLE.UPCOMING)} + onKeyDown={event => (event.key === 'Enter' || event.key === ' ') && onChange(MEETING_TABS_TITLE.UPCOMING)} > {t('meetings.tabs.next')}
onChange(MeetingTabsTitle.PAST)} - onKeyDown={event => (event.key === 'Enter' || event.key === ' ') && onChange(MeetingTabsTitle.PAST)} + css={tabStyles(active === MEETING_TABS_TITLE.PAST)} + onClick={() => onChange(MEETING_TABS_TITLE.PAST)} + onKeyDown={event => (event.key === 'Enter' || event.key === ' ') && onChange(MEETING_TABS_TITLE.PAST)} > {t('meetings.tabs.past')}
diff --git a/src/script/components/Meeting/MeetingList/TodayAndOngoingSection/TodayAndOngoingSection.tsx b/src/script/components/Meeting/MeetingList/TodayAndOngoingSection/TodayAndOngoingSection.tsx index 32288f6d9d2..bf9f1de1c0c 100644 --- a/src/script/components/Meeting/MeetingList/TodayAndOngoingSection/TodayAndOngoingSection.tsx +++ b/src/script/components/Meeting/MeetingList/TodayAndOngoingSection/TodayAndOngoingSection.tsx @@ -20,7 +20,7 @@ import {memo, useContext, useMemo} from 'react'; import {ClockContext} from 'Components/Meeting/ClockContext'; -import {MeetingTabsTitle, TodayAndOngoingSectionProps} from 'Components/Meeting/MeetingList/MeetingList'; +import {MEETING_TABS_TITLE, TodayAndOngoingSectionProps} from 'Components/Meeting/MeetingList/MeetingList'; import {MeetingListItemGroup} from 'Components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItemGroup'; import {groupByStartHour} from 'Components/Meeting/utils/MeetingDatesUtil'; import {getOnGoingMeetingsAt} from 'Components/Meeting/utils/MeetingStatusUtil'; @@ -40,18 +40,27 @@ const TodayAndOngoingSectionComponent = ({ () => meetingsToday.filter(meeting => !ongoingIds.has(meeting.conversation_id)), [meetingsToday, ongoingIds], ); + const groupedMeetingsToday = useMemo(() => groupByStartHour(todayNotOngoing), [todayNotOngoing]); + if (meetingsToday.length === 0) { + return ; + } + + const meetingForToday = ; + + if (onGoingMeetings.length === 0) { + return meetingForToday; + } + return ( <> - {onGoingMeetings.length > 0 && ( - - )} - + + {meetingForToday} ); }; diff --git a/src/script/components/Meeting/mocks/MeetingMocks.ts b/src/script/components/Meeting/mocks/MeetingMocks.ts index bf039af118f..fcdee2f7594 100644 --- a/src/script/components/Meeting/mocks/MeetingMocks.ts +++ b/src/script/components/Meeting/mocks/MeetingMocks.ts @@ -20,7 +20,7 @@ // This whole file is just for mocking purposes, so we can use the MeetingList component // Once the backend is ready, we can remove this file -import {Meeting} from 'Components/Meeting/MeetingList/MeetingList'; +import {MeetingEntity} from 'Components/Meeting/MeetingList/MeetingList'; const now = new Date(); const toISO = (d: Date) => d.toISOString(); @@ -49,7 +49,7 @@ const addDay = (d: Date, s: number) => { return copy; }; -export const MEETINGS_TODAY: Meeting[] = [ +export const MEETINGS_TODAY: MeetingEntity[] = [ { start_date: toISO(addSeconds(now, 5)), end_date: toISO(addMinutes(now, 33)), @@ -81,7 +81,7 @@ export const MEETINGS_TODAY: Meeting[] = [ }, ]; -export const MEETINGS_TOMORROW: Meeting[] = [ +export const MEETINGS_TOMORROW: MeetingEntity[] = [ { start_date: toISO(addDay(addMinutes(now, -120), 1)), end_date: toISO(addDay(addMinutes(now, -30), 1)), @@ -112,7 +112,7 @@ export const MEETINGS_TOMORROW: Meeting[] = [ }, ]; -export const MEETINGS_PAST: Meeting[] = [ +export const MEETINGS_PAST: MeetingEntity[] = [ { start_date: toISO(addDay(addMinutes(now, -120), -1)), end_date: toISO(addDay(addMinutes(now, -20), -1)), diff --git a/src/script/components/Meeting/utils/MeetingDatesUtil.ts b/src/script/components/Meeting/utils/MeetingDatesUtil.ts index 03d102197cb..2b583a41000 100644 --- a/src/script/components/Meeting/utils/MeetingDatesUtil.ts +++ b/src/script/components/Meeting/utils/MeetingDatesUtil.ts @@ -17,7 +17,7 @@ * */ -import {Meeting} from 'Components/Meeting/MeetingList/MeetingList'; +import {MeetingEntity} from 'Components/Meeting/MeetingList/MeetingList'; import {FnDate, formatLocale} from 'Util/TimeUtil'; /** @@ -47,11 +47,11 @@ export const getTodayTomorrowLabels = (): {today: string; tomorrow: string} => { /** * Groups a list of meetings by their start hour. * - * @param {Meeting[]} meetings - The list of meetings to group. - * @returns {Record} - An object where the keys are the start hours (0-23) and the values are arrays of meetings. + * @param {MeetingEntity[]} meetings - The list of meetings to group. + * @returns {Record} - An object where the keys are the start hours (0-23) and the values are arrays of meetings. */ -export const groupByStartHour = (meetings: Meeting[]): Record => { - const groupedMeetings: Record = {}; +export const groupByStartHour = (meetings: MeetingEntity[]): Record => { + const groupedMeetings: Record = {}; for (const meeting of meetings) { const hour = new Date(meeting.start_date).getHours(); (groupedMeetings[hour] ??= []).push(meeting); diff --git a/src/script/components/Meeting/utils/MeetingStatusUtil.ts b/src/script/components/Meeting/utils/MeetingStatusUtil.ts index d436cdad480..9de5207a6ca 100644 --- a/src/script/components/Meeting/utils/MeetingStatusUtil.ts +++ b/src/script/components/Meeting/utils/MeetingStatusUtil.ts @@ -17,10 +17,10 @@ * */ -import {Meeting} from 'Components/Meeting/MeetingList/MeetingList'; +import {MeetingEntity} from 'Components/Meeting/MeetingList/MeetingList'; import {TIME_IN_MILLIS} from 'Util/TimeUtil'; -export enum MeetingStatuses { +export enum MEETING_STATUS { ON_GOING = 'on_going', STARTING_SOON = 'starting_soon', PARTICIPATING = 'participating', @@ -36,11 +36,11 @@ export const STARTING_SOON_THRESHOLD_MS = 5 * TIME_IN_MILLIS.MINUTE; /** * Filters the list of meetings to return only those that are ongoing at the specified time. * - * @param {Meeting[]} meetings - The list of meetings to filter. + * @param {MeetingEntity[]} meetings - The list of meetings to filter. * @param {number} nowMs - The current time in milliseconds. - * @returns {Meeting[]} - The list of ongoing meetings. + * @returns {MeetingEntity[]} - The list of ongoing meetings. */ -export const getOnGoingMeetingsAt = (meetings: Meeting[], nowMs: number): Meeting[] => +export const getOnGoingMeetingsAt = (meetings: MeetingEntity[], nowMs: number): MeetingEntity[] => meetings.filter(meeting => { const startMs = new Date(meeting.start_date).getTime(); const endMs = new Date(meeting.end_date).getTime(); @@ -54,30 +54,30 @@ export const getOnGoingMeetingsAt = (meetings: Meeting[], nowMs: number): Meetin * @param {string} start_date - The start date of the meeting in ISO format. * @param {string} end_date - The end date of the meeting in ISO format. * @param {boolean} [attending=false] - Whether the user is attending the meeting. - * @returns {MeetingStatuses} - The status of the meeting. + * @returns {MEETING_STATUS} - The status of the meeting. */ export const getMeetingStatusAt = ( nowMs: number, start_date: string, end_date: string, attending: boolean = false, -): MeetingStatuses => { +): MEETING_STATUS => { const startMs = new Date(start_date).getTime(); const endMs = new Date(end_date).getTime(); if (nowMs > endMs) { - return MeetingStatuses.PAST; + return MEETING_STATUS.PAST; } if (nowMs >= startMs) { - return attending ? MeetingStatuses.PARTICIPATING : MeetingStatuses.ON_GOING; + return attending ? MEETING_STATUS.PARTICIPATING : MEETING_STATUS.ON_GOING; } if (startMs <= nowMs + STARTING_SOON_THRESHOLD_MS) { - return MeetingStatuses.STARTING_SOON; + return MEETING_STATUS.STARTING_SOON; } - return MeetingStatuses.UPCOMING; + return MEETING_STATUS.UPCOMING; }; /**