Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -1198,8 +1198,21 @@
"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.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",
Expand Down
42 changes: 42 additions & 0 deletions src/script/components/Meeting/ClockContext.tsx
Original file line number Diff line number Diff line change
@@ -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<number>(Date.now());

export interface ClockProviderProps extends PropsWithChildren {
tickMs?: number;
}

export const ClockProvider = ({children, tickMs = TIME_IN_MILLIS.SECOND}: ClockProviderProps) => {
const [now, setNow] = useState<number>(() => Date.now());

useEffect(() => {
const id = window.setInterval(() => setNow(Date.now()), tickMs);
return () => window.clearInterval(id);
}, [tickMs]);

return <ClockContext.Provider value={now}>{children}</ClockContext.Provider>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ export const EmptyMeetingList = ({text, helperText, showCallingButton = true}: E
{showCallingButton && (
<div css={emptyListActionButtonContainerStyles}>
<Button variant={ButtonVariant.TERTIARY}>
<CallIcon css={emptyListActionButtonsStyles} /> Meet Now
<CallIcon css={emptyListActionButtonsStyles} /> {t('meetings.action.meetNow')}
</Button>
<Button variant={ButtonVariant.TERTIARY}>
<CalendarIcon css={emptyListActionButtonsStyles} /> Schedule Meeting
<CalendarIcon css={emptyListActionButtonsStyles} /> {t('meetings.action.scheduleMeeting')}
</Button>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
callingButtonGroupStyles,
dropdownIconStyles,
} from 'Components/Meeting/MeetNowMultiActionButton/MeetNowMultiActionButton.styles';
import {t} from 'Util/LocalizerUtil';

import {showContextMenu} from '../../../ui/ContextMenu';

Expand All @@ -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();
Expand All @@ -75,7 +76,7 @@ export const MeetNowMultiActionButton = () => {
icon={<CallIcon />}
onClick={handleMeetingButton}
>
Meet now
{t('meetings.action.meetNow')}
</ButtonGroup.Button>
<ButtonGroup.Button
css={callingButtonGroupStyles}
Expand Down
144 changes: 55 additions & 89 deletions src/script/components/Meeting/MeetingList/MeetingList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -29,91 +30,47 @@ 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 {
export interface MeetingEntity {
start_date: string;
end_date: string;
schedule: string;
conversation_id: string;
title: string;
// Ask iOS and Android about how to identify this status
attending?: boolean;
}

export enum MeetingTabsTitle {
NEXT = 'next',
export enum MEETING_TABS_TITLE {
UPCOMING = 'upcoming',
PAST = 'past',
}

export interface TodayAndOngoingSectionProps {
meetingsToday: MeetingEntity[];
headerForOnGoing: string;
headerForToday: string;
}

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',
},
];

const [activeTab, setActiveTab] = useState<MeetingTab>(MeetingTabsTitle.NEXT);
const [activeTab, setActiveTab] = useState<MeetingTab>(MEETING_TABS_TITLE.UPCOMING);

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 isNextTab = activeTab === MeetingTabsTitle.NEXT;
const hasMeetingsToday = MEETINGS_TODAY.length > 0;
const hasMeetingsTomorrow = MEETINGS_TOMORROW.length > 0;
const isUpcomingTab = activeTab === MEETING_TABS_TITLE.UPCOMING;

const handleTabChange = useCallback((tab: MeetingTab) => setActiveTab(tab), []);

let content: ReactNode;

Expand All @@ -125,40 +82,49 @@ export const MeetingList = () => {
);
}

if (isNextTab) {
// Next tab
content = hasMeetingsToday ? (
if (isUpcomingTab) {
if (!hasMeetingsToday) {
return (
<div css={emptyTabsListContainerStyles}>
<EmptyMeetingList text={t('meetings.noUpcomingMeetingsText')} />
</div>
);
}

content = (
<>
<MeetingListItemGroup header={headerForToday} groupedMeetings={groupedMeetingsToday} />
<TodayAndOngoingSection
meetingsToday={MEETINGS_TODAY}
headerForOnGoing={headerForOnGoing}
headerForToday={headerForToday}
/>

<MeetingListItemGroup header={headerForTomorrow} groupedMeetings={groupedMeetingsTomorrow} />
<div css={showAllButtonStyles}>
<Button variant={ButtonVariant.TERTIARY}>{t('meetings.showAllLabel')}</Button>
</div>
</>
) : (
<div css={emptyTabsListContainerStyles}>
<EmptyMeetingList text={t('meetings.noUpcomingMeetingsText')} />
</div>
);
} else {
// Past tab
content = hasMeetingsTomorrow ? (
<MeetingListItemGroup view={MeetingTabsTitle.PAST} groupedMeetings={{0: meetingsTomorrow}} />
) : (
<div css={emptyTabsListContainerStyles}>
<EmptyMeetingList
showCallingButton={false}
text={t('meetings.noPastMeetingsText')}
helperText={t('meetings.noPastMeetingsHelperText')}
/>
</div>
);
if (!hasMeetingsTomorrow) {
return (
<div css={emptyTabsListContainerStyles}>
<EmptyMeetingList
showCallingButton={false}
text={t('meetings.noPastMeetingsText')}
helperText={t('meetings.noPastMeetingsHelperText')}
/>
</div>
);
}

content = <MeetingListItemGroup view={MEETING_TABS_TITLE.PAST} groupedMeetings={{0: MEETINGS_PAST}} />;
}

return (
<div css={meetingListContainerStyles}>
<MeetingTabs active={activeTab} onChange={setActiveTab} />
{content}
<MeetingTabs active={activeTab} onChange={handleTabChange} />
<ClockProvider>{content}</ClockProvider>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
iconContainerStyle,
iconStyles,
} from 'Components/Meeting/MeetingList/MeetingListItemGroup/MeetingListItem/MeetingAction/MeetingAction.styles';
import {t} from 'Util/LocalizerUtil';

import {showContextMenu} from '../../../../../../ui/ContextMenu';

Expand All @@ -46,29 +47,29 @@ export const MeetingAction = () => {
entries: [
{
icon: () => <CallIcon />,
label: 'Start meeting',
label: t('meetings.action.startMeeting'),
},
{
icon: () => <CirclePlusIcon />,
label: 'Create conversation',
label: t('meetings.action.createConversation'),
},
{
icon: () => <ShareLinkIcon />,
label: 'Copy link',
label: t('meetings.action.copyLink'),
},
{
icon: () => <EditIcon />,
label: 'Edit meeting',
label: t('meetings.action.editMeeting'),
},
{
css: contextMenuDangerItemStyles,
icon: () => <CloseIcon css={contextMenuDangerItemIconStyles} />,
label: 'Remove meeting for me',
label: t('meetings.action.deleteMeetingForMe'),
},
{
css: contextMenuDangerItemStyles,
icon: () => <TrashIcon css={contextMenuDangerItemIconStyles} />,
label: 'Delete meeting for everyone',
label: t('meetings.action.deleteMeetingForAll'),
},
],
identifier: 'message-options-menu',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import {CSSObject} from '@emotion/react';

export const itemStyles: CSSObject = {
fontSize: 'var(--font-size-medium)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
Expand All @@ -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',
Expand All @@ -54,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',
Expand Down
Loading