Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d04a453
feat(webapp): add Meet Now modal and instant meeting flow
thisisamir98 Jul 17, 2026
92df5de
Merge branch 'main' of github.com:wireapp/wire-webapp into meet-now
thisisamir98 Jul 20, 2026
57fee67
fix(webapp): inject ConversationState into useMeetNowSubmit
thisisamir98 Jul 20, 2026
09af48e
test(webapp): cover Meet Now submit join outcomes in hook tests
thisisamir98 Jul 20, 2026
55bd7db
refactor(webapp): rename Meet Now modal files to camelCase
thisisamir98 Jul 20, 2026
a492d24
fix(webapp): return explicit Meet Now submit outcomes
thisisamir98 Jul 20, 2026
906e9e2
fix(webapp): prevent Meet Now modal close during submit
thisisamir98 Jul 20, 2026
b6dafb4
fix(webapp): use semantic form for Meet Now modal
thisisamir98 Jul 20, 2026
be166f7
refactor(webapp): pass meeting commands into create services
thisisamir98 Jul 20, 2026
1e171cc
refactor(webapp): extract shared meeting modules from modal features
thisisamir98 Jul 20, 2026
6e99522
refactor(webapp): extract meet now submit orchestration
thisisamir98 Jul 20, 2026
0f0a002
test(api-client): add canonical conversationSchema contract tests
thisisamir98 Jul 20, 2026
9772294
refactor(webapp): inline meeting store service wiring
thisisamir98 Jul 20, 2026
7441393
test(webapp): assert Result and Maybe variants in store tests
thisisamir98 Jul 20, 2026
0c8467b
fix(webapp): unwrap Maybe dates in meeting store tests
thisisamir98 Jul 20, 2026
5ae0ef7
refactor(webapp): make meeting form errors explicit
thisisamir98 Jul 20, 2026
0a21375
refactor(webapp): remove unused Meet Now data-uie-name attrs
thisisamir98 Jul 20, 2026
2dffe38
refactor(webapp): rename MeetNowSubmitSuccess to CreateMeetingSuccess
thisisamir98 Jul 20, 2026
551eea8
refactor(webapp): import shared modal styles directly in Meet Now
thisisamir98 Jul 20, 2026
e0a590c
chore(webapp): apply ESLint auto-fixes to Meeting modules
thisisamir98 Jul 20, 2026
4199723
fix(webapp): close Meet Now modal after partial create failures
thisisamir98 Jul 21, 2026
7ebb00b
test(webapp): use ScheduleMeetingCommand in meeting store test
thisisamir98 Jul 21, 2026
2474ee7
test(webapp): narrow Maybe values in meeting store edit tests
thisisamir98 Jul 21, 2026
8ca1c3e
refactor(webapp): use UpdateMeetingCommand in shared meeting service
thisisamir98 Jul 21, 2026
2fd351e
fix(webapp): use explicit empty title checks in meeting validation
thisisamir98 Jul 21, 2026
14786db
fix import order error in meeting validation
thisisamir98 Jul 21, 2026
20dbeb0
test(webapp): cover Meet Now modal submit generation guard
thisisamir98 Jul 21, 2026
bf00bef
Merge branch 'main' of github.com:wireapp/wire-webapp into meet-now
thisisamir98 Jul 21, 2026
6ab11b4
fix(webapp): close schedule modal on persisted submit failures
thisisamir98 Jul 21, 2026
4683222
fix(webapp): use context-aware meeting submit error copy
thisisamir98 Jul 21, 2026
d55811d
test(webapp): use production invoker in Meet Now modal tests
thisisamir98 Jul 21, 2026
9afd4cd
fix(webapp): represent missing schedule times as explicit form errors
thisisamir98 Jul 21, 2026
59c6e9f
Merge branch 'main' of github.com:wireapp/wire-webapp into meet-now
thisisamir98 Jul 21, 2026
d710d52
fix(webapp): fix Meet Now lint and regenerate i18n artifacts
thisisamir98 Jul 21, 2026
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
8 changes: 8 additions & 0 deletions apps/webapp/src/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -1268,9 +1268,17 @@
"meetings.action.editMeeting": "Edit meeting",
"meetings.action.meetNow": "Meet Now",
"meetings.action.scheduleMeeting": "Schedule Meeting",
"meetings.error.addParticipantsFailed": "The meeting was saved, but some participants could not be added. Please try again.",
"meetings.error.conversationSetupFailed": "The meeting was saved, but something went wrong while setting up the conversation. Please try again.",
"meetings.error.setupFailedTitle": "Could not finish meeting setup",
"meetings.list.loadError": "Could not load meetings. Please try again.",
"meetings.list.today": "Today",
"meetings.list.tomorrow": "Tomorrow",
"meetings.meetNowModal.closeAriaLabel": "Close",
"meetings.meetNowModal.error.createFailed": "Something went wrong while starting the meeting. Please try again.",
"meetings.meetNowModal.error.createFailedTitle": "Could not start meeting",
"meetings.meetNowModal.startMeeting": "Start Meeting",
"meetings.meetNowModal.title": "Meet now",
"meetings.meetingStatus.participating": "Attending",
"meetings.meetingStatus.startedAt": "Started at {time}",
"meetings.navigation.label": "Meetings",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const EmptyMeetingList = () => {
<p css={emptyListHeadingStyles}>{translate('meetings.noMeetingsText')}</p>
<p css={emptyListBodyStyles}>{translate('meetings.startMeetingHelp')}</p>
<div css={emptyListActionButtonContainerStyles}>
<Button variant={ButtonVariant.TERTIARY} onClick={handleMeetNow} data-uie-name="meet-now">
<Button variant={ButtonVariant.TERTIARY} onClick={handleMeetNow}>
<CallIcon css={emptyListActionButtonsStyles} /> {translate('meetings.action.meetNow')}
</Button>
<Button variant={ButtonVariant.TERTIARY} onClick={handleScheduleMeeting} data-uie-name="schedule-meeting">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ const createMeetingStoreForTest = () =>
hasLoadError: false,
loadMeetings: jest.fn(),
scheduleMeeting: jest.fn(),
meetNowMeeting: jest.fn(),
updateMeeting: jest.fn(),
loadMeetingForEdit: jest.fn(),
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ import {

export interface MeetingParticipantsPickerProps {
id: string;
dataUieName: string;
dataUieName?: string;
users: User[];
selectedUsers: User[];
onSelectedUsersChange: (users: User[]) => void;
Expand Down Expand Up @@ -166,7 +166,7 @@ export const MeetingParticipantsPicker = ({
<div
ref={triggerRef}
css={controlStyles({isDisabled: disabled, isOpen, markInvalid})}
data-uie-name={`${dataUieName}-control`}
data-uie-name={dataUieName ? `${dataUieName}-control` : undefined}
data-disabled={disabled || undefined}
aria-expanded={isOpen}
aria-haspopup="listbox"
Expand All @@ -175,7 +175,7 @@ export const MeetingParticipantsPicker = ({
<div css={valueContainerStyles}>
<SearchIcon aria-hidden="true" css={searchIconStyles} />
{hasSelection && (
<span css={selectedSummaryStyles} data-uie-name={`${dataUieName}-summary`}>
<span css={selectedSummaryStyles} data-uie-name={dataUieName ? `${dataUieName}-summary` : undefined}>
{selectedSummary}
</span>
)}
Expand All @@ -191,7 +191,7 @@ export const MeetingParticipantsPicker = ({
disabled={disabled}
placeholder={showPlaceholder ? placeholder : ''}
aria-label={placeholder}
data-uie-name={`${dataUieName}-input`}
data-uie-name={dataUieName ? `${dataUieName}-input` : undefined}
onChange={event => {
onFilterChange(event.target.value);
if (!isOpen) {
Expand All @@ -214,7 +214,7 @@ export const MeetingParticipantsPicker = ({
css={chevronButtonStyles}
isDisabled={disabled}
aria-label={label ?? placeholder}
data-uie-name={`${dataUieName}-toggle`}
data-uie-name={dataUieName ? `${dataUieName}-toggle` : undefined}
onPress={() => handleOpenChange(!isOpen)}
>
<ChevronDownIcon aria-hidden="true" width={16} height={16} css={chevronIconStyles(isOpen)} />
Expand All @@ -237,7 +237,7 @@ export const MeetingParticipantsPicker = ({
<div
id={listboxId}
css={listContainerStyles}
data-uie-name={`dropdown-${dataUieName}`}
data-uie-name={dataUieName ? `dropdown-${dataUieName}` : undefined}
role="listbox"
aria-multiselectable="true"
>
Expand All @@ -257,7 +257,7 @@ export const MeetingParticipantsPicker = ({
allowRemoteSearch
filterRemoteTeamUsers
showAllProvidedUsers
dataUieName={`${dataUieName}-list`}
dataUieName={dataUieName ? `${dataUieName}-list` : undefined}
/>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type {ScheduleFormErrorCode} from './ScheduleFormErrors';

export const meetingSubmitErrors = {
createFailed: 'createFailed',
conversationSetupFailed: 'conversationSetupFailed',
updateFailed: 'updateFailed',
editMeetingIdMissing: 'editMeetingIdMissing',
addParticipantsFailed: 'addParticipantsFailed',
Expand Down
24 changes: 15 additions & 9 deletions apps/webapp/src/script/components/Meeting/Meetings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ import {MeetingHeader} from 'Components/Meeting/MeetingHeader/MeetingHeader';
import {MeetingList} from 'Components/Meeting/MeetingList/MeetingList';
import {createMeetingStore} from 'Components/Meeting/meetingStore/createMeetingStore';
import {MeetingStoreProvider, useMeetingStore} from 'Components/Meeting/meetingStore/MeetingStoreProvider';
import {MeetNowModal} from 'Components/Meeting/meetNowModal/meetNowModal';
import {ScheduleMeetingModal} from 'Components/Meeting/ScheduleMeetingModal';
import {meetNowMeeting, scheduleMeeting, updateMeeting} from 'Components/Meeting/shared/service/meetingService';
import {useApplicationContext} from 'src/script/page/rootProvider';

const MeetingsContent = () => {
Expand Down Expand Up @@ -54,6 +56,7 @@ const MeetingsContent = () => {
</div>
<MeetingCallingView />
<ScheduleMeetingModal />
<MeetNowModal />
</div>
);
};
Expand All @@ -62,15 +65,18 @@ export const Meetings = () => {
const {mainViewModel, wallClock} = useApplicationContext();
const {meetings: meetingsRepository, conversation: conversationRepository} = mainViewModel.content.repositories;

const store = useMemo(
() =>
createMeetingStore({
meetingsRepository,
conversationRepository,
wallClock,
}),
[meetingsRepository, conversationRepository, wallClock],
);
const store = useMemo(() => {
const meetingServiceDeps = {meetingsRepository, conversationRepository, wallClock};

return createMeetingStore({
...meetingServiceDeps,
serviceTasks: {
scheduleMeeting: command => scheduleMeeting(command, meetingServiceDeps),
meetNowMeeting: command => meetNowMeeting(command, meetingServiceDeps),
updateMeeting: command => updateMeeting(command, meetingServiceDeps),
},
});
}, [meetingsRepository, conversationRepository, wallClock]);

return (
<MeetingStoreProvider store={store}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,7 @@ import {
} from '@wireapp/react-ui-kit';

import {MeetingParticipantsPicker} from 'Components/Meeting/MeetingParticipantsPicker';
import type {User} from 'Repositories/entity/User';
import {currentLanguage} from 'src/script/auth/localeConfig';
import {useApplicationContext} from 'src/script/page/rootProvider';

import {useMeetingParticipants} from 'Components/Meeting/shared/participants/useMeetingParticipants';
import {
scheduleMeetingFormColumnCss,
scheduleMeetingFormDividerCss,
Expand All @@ -48,7 +45,11 @@ import {
scheduleMeetingSelectMenuPortalStyles,
scheduleMeetingTitleClearButtonStyles,
scheduleMeetingTitleInputWrapperStyles,
} from './ScheduleMeetingForm.styles';
} from 'Components/Meeting/shared/styles/meetingForm.styles';
import type {User} from 'Repositories/entity/User';
import {currentLanguage} from 'src/script/auth/localeConfig';
import {useApplicationContext} from 'src/script/page/rootProvider';

import {
SCHEDULE_MEETING_RECURRENCE_OPTIONS,
SCHEDULE_MEETING_RECURRENCE_TRANSLATION_KEYS,
Expand All @@ -59,7 +60,6 @@ import type {
ScheduleMeetingMode,
ScheduleMeetingRecurrenceOption,
} from './scheduleMeetingTypes';
import {useScheduleMeetingParticipants} from './useScheduleMeetingParticipants';

const toDateTimePickerValue = (value: Maybe<Date>): Date | null => value.unwrapOr(null);

Expand Down Expand Up @@ -95,7 +95,7 @@ export const ScheduleMeetingForm = ({
selfUser,
}: ScheduleMeetingFormProps) => {
const {mainViewModel, translate, wallClock} = useApplicationContext();
const {users} = useScheduleMeetingParticipants();
const {users} = useMeetingParticipants();
const portalContainer = getOverlayPortalContainer();

const contentViewModel = mainViewModel.content;
Expand Down Expand Up @@ -170,8 +170,15 @@ export const ScheduleMeetingForm = ({
return startDate.compare(todayValue) > 0 ? startDate : todayValue;
}, [formState.start, todayValue]);

const startErrorText = firstNonEmptyError(errors.startInPast);
const endErrorText = firstNonEmptyError(errors.endInPast, errors.endBeforeStart);
const startErrorText = firstNonEmptyError(
errors.startInPast,
formState.start.isNothing ? errors.missingTimes : undefined,
);
const endErrorText = firstNonEmptyError(
errors.endInPast,
errors.endBeforeStart,
formState.end.isNothing ? errors.missingTimes : undefined,
);

return (
<div css={scheduleMeetingFormLayoutCss} data-uie-name="schedule-meeting-form" data-uie-mode={mode}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,6 @@ import {container} from 'tsyringe';

import {Button, ButtonVariant, CalendarIcon, CloseIcon} from '@wireapp/react-ui-kit';

import {ModalComponent} from 'Components/Modals/ModalComponent';
import {UserState} from 'Repositories/user/userState';
import {useApplicationContext} from 'src/script/page/rootProvider';
import {handleEscDown} from 'Util/keyboardUtil';

import {ScheduleMeetingForm} from './ScheduleMeetingForm';
import {
bodyStyles,
closeButtonStyles,
Expand All @@ -39,7 +33,14 @@ import {
submitButtonIconStyles,
submitButtonStyles,
wrapperStyles,
} from './ScheduleMeetingModal.styles';
} from 'Components/Meeting/shared/styles/meetingModalShell.styles';
import {ModalComponent} from 'Components/Modals/ModalComponent';
import {UserState} from 'Repositories/user/userState';
import {useApplicationContext} from 'src/script/page/rootProvider';
import {handleEscDown} from 'Util/keyboardUtil';

import {ScheduleMeetingForm} from './ScheduleMeetingForm';
import {wasScheduleMeetingPersisted} from './scheduleMeetingTypes';
import {hasScheduleMeetingFormErrors, useScheduleMeetingModal} from './useScheduleMeetingModal';
import {useScheduleMeetingSubmit} from './useScheduleMeetingSubmit';

Expand All @@ -66,10 +67,11 @@ export const ScheduleMeetingModal = () => {

const displayErrors = useMemo(
() => ({
title: errors.title ? translate(errors.title) : undefined,
startInPast: errors.startInPast ? translate(errors.startInPast) : undefined,
endInPast: errors.endInPast ? translate(errors.endInPast) : undefined,
endBeforeStart: errors.endBeforeStart ? translate(errors.endBeforeStart) : undefined,
title: errors.title !== undefined ? translate(errors.title) : undefined,
missingTimes: errors.missingTimes !== undefined ? translate(errors.missingTimes) : undefined,
startInPast: errors.startInPast !== undefined ? translate(errors.startInPast) : undefined,
endInPast: errors.endInPast !== undefined ? translate(errors.endInPast) : undefined,
endBeforeStart: errors.endBeforeStart !== undefined ? translate(errors.endBeforeStart) : undefined,
}),
[errors, translate],
);
Expand All @@ -86,8 +88,8 @@ export const ScheduleMeetingModal = () => {
}

fireAndForgetInvoker.fireAndForget(async (): Promise<void> => {
const didSchedule = await submit(formState);
if (didSchedule) {
const submitResult = await submit(formState);
if (wasScheduleMeetingPersisted(submitResult)) {
handleClose();
}
});
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,43 @@ export interface ScheduleMeetingFormState {

export type ScheduleMeetingFormErrorKey =
| 'meetings.scheduleModal.error.titleRequired'
| 'meetings.scheduleModal.error.missingTimes'
| 'meetings.scheduleModal.error.endBeforeStart'
| 'meetings.schedule.errors.startInPast'
| 'meetings.schedule.errors.endInPast';

export interface ScheduleMeetingFormErrors {
title?: ScheduleMeetingFormErrorKey;
startInPast?: ScheduleMeetingFormErrorKey;
endInPast?: ScheduleMeetingFormErrorKey;
endBeforeStart?: ScheduleMeetingFormErrorKey;
title: ScheduleMeetingFormErrorKey | undefined;
missingTimes: ScheduleMeetingFormErrorKey | undefined;
startInPast: ScheduleMeetingFormErrorKey | undefined;
endInPast: ScheduleMeetingFormErrorKey | undefined;
endBeforeStart: ScheduleMeetingFormErrorKey | undefined;
}

export const emptyScheduleMeetingFormErrors = (): ScheduleMeetingFormErrors => ({
title: undefined,
missingTimes: undefined,
startInPast: undefined,
endInPast: undefined,
endBeforeStart: undefined,
});

export interface ScheduleMeetingFormDisplayErrors {
title?: string;
startInPast?: string;
endInPast?: string;
endBeforeStart?: string;
title: string | undefined;
missingTimes: string | undefined;
startInPast: string | undefined;
endInPast: string | undefined;
endBeforeStart: string | undefined;
}

export const scheduleMeetingSubmitResults = {
submitFailed: 'submitFailed',
setupFailed: 'setupFailed',
succeeded: 'succeeded',
} as const;

export type ScheduleMeetingSubmitResult =
(typeof scheduleMeetingSubmitResults)[keyof typeof scheduleMeetingSubmitResults];

export const wasScheduleMeetingPersisted = (result: ScheduleMeetingSubmitResult): boolean =>
result !== scheduleMeetingSubmitResults.submitFailed;
Loading
Loading