4d6f4b3ded
* Refactor: Make bundle using webpack * Fix: restore after install codes and a few settings * Fix: build script folder permission * Refactor: Remove support to async import on audio bridges * Upgrade npm using nvm * Avoid questions on npm ci execution * Let npm ci install dev dependencies (as we need the build tools here) * Fix: enconding * Fix: old lock files * Remove: bbb-config dependency to bbb-html5 service, bbb-html5 isn't a service anymore * Fix: TS errors * Fix: eslint * Fix: chat styles * npm install with "lockfileVersion": 3 (newer npm) * build: allow nodejs 22 * node 22; drop meteor from CI and bbb-conf * TEMP: use bbb-install without mongo but with node 22 and newer image * build: relax nodejs condition to not trip 22.6 * build: ensure dir /usr/share/bigbluebutton/nginx exists * init sites-available/bbb; drop disable-transparent- * nginx complaining of missing file and ; * TMP: print status of services * WIP: tweak nginx location to debug * Fix: webcam widgets alignments * akka-apps -- update location of settings.yml * build: add locales path for nginx * docs and config changes for removal of meteor * Fix: build encoding and locales enpoint folder path * build: set wss url for media * Add: Enable minimizer and modify to Terser * Fix: TS errors --------- Co-authored-by: Tiago Jacobs <tiago.jacobs@gmail.com> Co-authored-by: Anton Georgiev <anto.georgiev@gmail.com> Co-authored-by: Anton Georgiev <antobinary@users.noreply.github.com>
503 lines
15 KiB
TypeScript
503 lines
15 KiB
TypeScript
import React, {
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
} from 'react';
|
|
import { defineMessages, useIntl } from 'react-intl';
|
|
import { isEmpty } from 'radash';
|
|
import { ApolloLink, useQuery } from '@apollo/client';
|
|
import {
|
|
JoinErrorCodeTable,
|
|
MeetingEndedTable,
|
|
openLearningDashboardUrl,
|
|
setLearningDashboardCookie,
|
|
} from './service';
|
|
import { MeetingEndDataResponse, getMeetingEndData } from './queries';
|
|
import useAuthData from '/imports/ui/core/local-states/useAuthData';
|
|
import Icon from '/imports/ui/components/common/icon/icon-ts/component';
|
|
import Styled from './styles';
|
|
import Rating from './rating/component';
|
|
import { LoadingContext } from '../common/loading-screen/loading-screen-HOC/component';
|
|
import logger from '/imports/startup/client/logger';
|
|
import apolloContextHolder from '/imports/ui/core/graphql/apolloContextHolder/apolloContextHolder';
|
|
import getFromUserSettings from '/imports/ui/services/users-settings';
|
|
|
|
const intlMessage = defineMessages({
|
|
410: {
|
|
id: 'app.meeting.ended',
|
|
description: 'message when meeting is ended',
|
|
},
|
|
403: {
|
|
id: 'app.error.removed',
|
|
description: 'Message to display when user is removed from the conference',
|
|
},
|
|
430: {
|
|
id: 'app.error.meeting.ended',
|
|
description: 'user logged conference',
|
|
},
|
|
'acl-not-allowed': {
|
|
id: 'app.error.removed',
|
|
description: 'Message to display when user is removed from the conference',
|
|
},
|
|
messageEnded: {
|
|
id: 'app.meeting.endedMessage',
|
|
description: 'message saying to go back to home screen',
|
|
},
|
|
messageEndedByUser: {
|
|
id: 'app.meeting.endedByUserMessage',
|
|
description: 'message informing who ended the meeting',
|
|
},
|
|
messageEndedByNoModeratorSingular: {
|
|
id: 'app.meeting.endedByNoModeratorMessageSingular',
|
|
description: 'message informing that the meeting was ended due to no moderator present (singular)',
|
|
},
|
|
messageEndedByNoModeratorPlural: {
|
|
id: 'app.meeting.endedByNoModeratorMessagePlural',
|
|
description: 'message informing that the meeting was ended due to no moderator present (plural)',
|
|
},
|
|
buttonOkay: {
|
|
id: 'app.meeting.endNotification.ok.label',
|
|
description: 'label okay for button',
|
|
},
|
|
title: {
|
|
id: 'app.feedback.title',
|
|
description: 'title for feedback screen',
|
|
},
|
|
subtitle: {
|
|
id: 'app.feedback.subtitle',
|
|
description: 'subtitle for feedback screen',
|
|
},
|
|
textarea: {
|
|
id: 'app.feedback.textarea',
|
|
description: 'placeholder for textarea',
|
|
},
|
|
confirmDesc: {
|
|
id: 'app.leaveConfirmation.confirmDesc',
|
|
description: 'adds context to confim option',
|
|
},
|
|
sendLabel: {
|
|
id: 'app.feedback.sendFeedback',
|
|
description: 'send feedback button label',
|
|
},
|
|
sendDesc: {
|
|
id: 'app.feedback.sendFeedbackDesc',
|
|
description: 'adds context to send feedback option',
|
|
},
|
|
[JoinErrorCodeTable.DUPLICATE_USER]: {
|
|
id: 'app.meeting.logout.duplicateUserEjectReason',
|
|
description: 'message for duplicate users',
|
|
},
|
|
[JoinErrorCodeTable.PERMISSION_FAILED]: {
|
|
id: 'app.meeting.logout.permissionEjectReason',
|
|
description: 'message for whom was kicked by doing something without permission',
|
|
},
|
|
[JoinErrorCodeTable.EJECT_USER]: {
|
|
id: 'app.meeting.logout.ejectedFromMeeting',
|
|
description: 'message when the user is removed by someone',
|
|
},
|
|
[JoinErrorCodeTable.SYSTEM_EJECT_USER]: {
|
|
id: 'app.meeting.logout.ejectedFromMeeting',
|
|
description: 'message when the user is removed by the system',
|
|
},
|
|
[JoinErrorCodeTable.MAX_PARTICIPANTS]: {
|
|
id: 'app.meeting.logout.maxParticipantsReached',
|
|
description: 'message when the user is rejected due to max participants limit',
|
|
},
|
|
[JoinErrorCodeTable.VALIDATE_TOKEN]: {
|
|
id: 'app.meeting.logout.validateTokenFailedEjectReason',
|
|
description: 'invalid auth token',
|
|
},
|
|
[JoinErrorCodeTable.USER_INACTIVITY]: {
|
|
id: 'app.meeting.logout.userInactivityEjectReason',
|
|
description: 'message to whom was kicked by inactivity',
|
|
},
|
|
[JoinErrorCodeTable.USER_LOGGED_OUT]: {
|
|
id: 'app.feedback.title',
|
|
description: 'message to whom was kicked by logging out',
|
|
},
|
|
[JoinErrorCodeTable.BANNED_USER_REJOINING]: {
|
|
id: 'app.error.userBanned',
|
|
description: 'message to whom was banned',
|
|
},
|
|
open_activity_report_btn: {
|
|
id: 'app.learning-dashboard.clickHereToOpen',
|
|
description: 'description of link to open activity report',
|
|
},
|
|
[MeetingEndedTable.ENDED_FROM_API]: {
|
|
id: 'app.meeting.endedFromAPI',
|
|
description: '',
|
|
},
|
|
[MeetingEndedTable.ENDED_WHEN_NOT_JOINED]: {
|
|
id: 'app.meeting.endedWhenNoUserJoined',
|
|
description: '',
|
|
},
|
|
[MeetingEndedTable.ENDED_WHEN_LAST_USER_LEFT]: {
|
|
id: 'app.meeting.endedWhenLastUserLeft',
|
|
description: '',
|
|
},
|
|
[MeetingEndedTable.ENDED_AFTER_USER_LOGGED_OUT]: {
|
|
id: 'app.meeting.endedWhenLastUserLeft',
|
|
description: '',
|
|
},
|
|
[MeetingEndedTable.ENDED_AFTER_EXCEEDING_DURATION]: {
|
|
id: 'app.meeting.endedAfterExceedingDuration',
|
|
description: '',
|
|
},
|
|
[MeetingEndedTable.BREAKOUT_ENDED_EXCEEDING_DURATION]: {
|
|
id: 'app.meeting.breakoutEndedAfterExceedingDuration',
|
|
description: '',
|
|
},
|
|
[MeetingEndedTable.BREAKOUT_ENDED_BY_MOD]: {
|
|
id: 'app.meeting.breakoutEndedByModerator',
|
|
description: '',
|
|
},
|
|
[MeetingEndedTable.ENDED_DUE_TO_NO_AUTHED_USER]: {
|
|
id: 'app.meeting.endedDueNoAuthed',
|
|
description: '',
|
|
},
|
|
[MeetingEndedTable.ENDED_DUE_TO_NO_MODERATOR]: {
|
|
id: 'app.meeting.endedDueNoModerators',
|
|
description: '',
|
|
},
|
|
[MeetingEndedTable.ENDED_DUE_TO_SERVICE_INTERRUPTION]: {
|
|
id: 'app.meeting.endedDueServiceInterruption',
|
|
description: '',
|
|
},
|
|
});
|
|
|
|
interface MeetingEndedContainerProps {
|
|
endedBy: string;
|
|
meetingEndedCode: string;
|
|
joinErrorCode: string;
|
|
}
|
|
|
|
interface MeetingEndedProps extends MeetingEndedContainerProps {
|
|
allowDefaultLogoutUrl: boolean;
|
|
askForFeedbackOnLogout: boolean
|
|
learningDashboardAccessToken: string;
|
|
isModerator: boolean;
|
|
learningDashboardBase: string;
|
|
isBreakout: boolean;
|
|
}
|
|
|
|
const MeetingEnded: React.FC<MeetingEndedProps> = ({
|
|
endedBy,
|
|
joinErrorCode,
|
|
meetingEndedCode,
|
|
allowDefaultLogoutUrl,
|
|
askForFeedbackOnLogout,
|
|
learningDashboardAccessToken,
|
|
isModerator,
|
|
learningDashboardBase,
|
|
isBreakout,
|
|
}) => {
|
|
const loadingContextInfo = useContext(LoadingContext);
|
|
const intl = useIntl();
|
|
const [{
|
|
authToken,
|
|
meetingId,
|
|
logoutUrl,
|
|
userName,
|
|
userId,
|
|
}] = useAuthData();
|
|
const [selectedStars, setSelectedStars] = useState(0);
|
|
const [dispatched, setDispatched] = useState(false);
|
|
|
|
const generateEndMessage = useCallback((joinErrorCode: string, meetingEndedCode: string, endedBy: string) => {
|
|
if (!isEmpty(endedBy)) {
|
|
return intl.formatMessage(intlMessage.messageEndedByUser, { 0: endedBy });
|
|
}
|
|
// OR opetaror always returns the first truthy value
|
|
|
|
const code = meetingEndedCode || joinErrorCode || '410';
|
|
return intl.formatMessage(intlMessage[code]);
|
|
}, []);
|
|
|
|
const sendFeedback = useCallback(() => {
|
|
const textarea = document.getElementById('feedbackComment') as HTMLTextAreaElement;
|
|
const comment = (textarea?.value || '').trim();
|
|
|
|
const message = {
|
|
rating: selectedStars,
|
|
userId,
|
|
userName,
|
|
authToken,
|
|
meetingId,
|
|
comment,
|
|
isModerator,
|
|
};
|
|
|
|
const pathMatch = window.location.pathname.match('^(.*)/html5client/join$');
|
|
if (pathMatch == null) {
|
|
throw new Error('Failed to match BBB client URI');
|
|
}
|
|
const serverPathPrefix = pathMatch[1];
|
|
|
|
const sessionToken = sessionStorage.getItem('sessionToken');
|
|
|
|
const url = `https://${window.location.hostname}${serverPathPrefix}/bigbluebutton/api/feedback?sessionToken=${sessionToken}`;
|
|
const options = {
|
|
method: 'POST',
|
|
body: JSON.stringify(message),
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
};
|
|
setDispatched(true);
|
|
|
|
fetch(url, options).then(() => {
|
|
if (!isModerator) {
|
|
const REDIRECT_WAIT_TIME = 5000;
|
|
setTimeout(() => {
|
|
window.location.href = logoutUrl;
|
|
}, REDIRECT_WAIT_TIME);
|
|
}
|
|
}).catch((e) => {
|
|
logger.warn({
|
|
logCode: 'user_feedback_not_sent_error',
|
|
extraInfo: {
|
|
errorName: e.name,
|
|
errorMessage: e.message,
|
|
},
|
|
}, `Unable to send feedback: ${e.message}`);
|
|
});
|
|
}, [selectedStars]);
|
|
|
|
const confirmRedirect = (isBreakout: boolean, allowRedirect: boolean) => {
|
|
if (isBreakout) window.close();
|
|
if (allowRedirect) {
|
|
window.location.href = logoutUrl;
|
|
}
|
|
};
|
|
|
|
const logoutButton = useMemo(() => {
|
|
const { locale } = intl;
|
|
return (
|
|
(
|
|
<Styled.Wrapper>
|
|
{
|
|
learningDashboardAccessToken && isModerator
|
|
// Always set cookie in case Dashboard is already opened
|
|
&& setLearningDashboardCookie(learningDashboardAccessToken, meetingId) === true
|
|
? (
|
|
<Styled.Text>
|
|
<Styled.MeetingEndedButton
|
|
color="default"
|
|
onClick={() => openLearningDashboardUrl(learningDashboardAccessToken,
|
|
meetingId,
|
|
authToken,
|
|
learningDashboardBase,
|
|
locale)}
|
|
aria-details={intl.formatMessage(intlMessage.open_activity_report_btn)}
|
|
>
|
|
<Icon
|
|
iconName="multi_whiteboard"
|
|
/>
|
|
</Styled.MeetingEndedButton>
|
|
</Styled.Text>
|
|
) : null
|
|
}
|
|
<Styled.Text>
|
|
{intl.formatMessage(intlMessage.messageEnded)}
|
|
</Styled.Text>
|
|
|
|
<Styled.MeetingEndedButton
|
|
color="primary"
|
|
onClick={() => confirmRedirect(isBreakout, allowDefaultLogoutUrl)}
|
|
/* @eslint-disable-next-line */
|
|
aria-details={intl.formatMessage(intlMessage.confirmDesc)}
|
|
>
|
|
{intl.formatMessage(intlMessage.buttonOkay)}
|
|
</Styled.MeetingEndedButton>
|
|
</Styled.Wrapper>
|
|
)
|
|
);
|
|
}, [learningDashboardAccessToken, isModerator, meetingId, authToken, learningDashboardBase]);
|
|
|
|
const feedbackScreen = useMemo(() => {
|
|
const shouldShowFeedback = askForFeedbackOnLogout && !dispatched;
|
|
const noRating = selectedStars === 0;
|
|
return (
|
|
<>
|
|
<Styled.Text>
|
|
{shouldShowFeedback
|
|
? intl.formatMessage(intlMessage.subtitle)
|
|
: intl.formatMessage(intlMessage.messageEnded)}
|
|
</Styled.Text>
|
|
|
|
{shouldShowFeedback ? (
|
|
<div data-test="rating">
|
|
<Rating
|
|
total="5"
|
|
onRate={setSelectedStars}
|
|
/>
|
|
{!noRating ? (
|
|
<Styled.TextArea
|
|
rows={5}
|
|
id="feedbackComment"
|
|
placeholder={intl.formatMessage(intlMessage.textarea)}
|
|
aria-describedby="textareaDesc"
|
|
/>
|
|
) : null}
|
|
</div>
|
|
) : null}
|
|
<Styled.Wrapper>
|
|
{noRating ? (
|
|
<Styled.MeetingEndedButton
|
|
color="primary"
|
|
onClick={() => setDispatched(true)}
|
|
aria-details={intl.formatMessage(intlMessage.confirmDesc)}
|
|
>
|
|
{intl.formatMessage(intlMessage.buttonOkay)}
|
|
</Styled.MeetingEndedButton>
|
|
) : null}
|
|
{!noRating ? (
|
|
<Styled.MeetingEndedButton
|
|
onClick={sendFeedback}
|
|
aria-details={intl.formatMessage(intlMessage.sendDesc)}
|
|
>
|
|
{intl.formatMessage(intlMessage.sendLabel)}
|
|
</Styled.MeetingEndedButton>
|
|
) : null}
|
|
</Styled.Wrapper>
|
|
</>
|
|
);
|
|
}, [askForFeedbackOnLogout, dispatched, selectedStars]);
|
|
|
|
useEffect(() => {
|
|
// Sets Loading to falsed and removes loading splash screen
|
|
loadingContextInfo.setLoading(false, '');
|
|
// Stops all media tracks
|
|
window.dispatchEvent(new Event('StopAudioTracks'));
|
|
// get the media tag from the session storage
|
|
// @ts-ignore
|
|
const data = window.meetingClientSettings.public.media;
|
|
// get media element and stops it and removes the audio source
|
|
const mediaElement = document.querySelector<HTMLMediaElement>(data.mediaTag);
|
|
if (mediaElement) {
|
|
mediaElement.pause();
|
|
mediaElement.srcObject = null;
|
|
}
|
|
// stops apollo client and removes it connection
|
|
const apolloClient = apolloContextHolder.getClient();
|
|
// stops client queries
|
|
if (apolloClient) {
|
|
apolloClient.stop();
|
|
}
|
|
|
|
apolloContextHolder.setShouldRetry(false);
|
|
|
|
const ws = apolloContextHolder.getLink();
|
|
// stops client connection after 5 seconds, if made immediately some data is lost
|
|
if (ws) {
|
|
setTimeout(() => {
|
|
// overwrites the link with an empty link
|
|
// if not new connection is made
|
|
apolloClient.setLink(ApolloLink.empty());
|
|
// closes the connection
|
|
ws.terminate();
|
|
}, 5000);
|
|
}
|
|
}, []);
|
|
|
|
return (
|
|
<Styled.Parent>
|
|
<Styled.Modal data-test="meetingEndedModal">
|
|
<Styled.Content>
|
|
<Styled.Title>
|
|
{generateEndMessage(joinErrorCode, meetingEndedCode, endedBy)}
|
|
</Styled.Title>
|
|
{allowDefaultLogoutUrl && !askForFeedbackOnLogout ? logoutButton : null}
|
|
{askForFeedbackOnLogout ? feedbackScreen : null}
|
|
</Styled.Content>
|
|
</Styled.Modal>
|
|
</Styled.Parent>
|
|
);
|
|
};
|
|
|
|
const MeetingEndedContainer: React.FC<MeetingEndedContainerProps> = ({
|
|
endedBy,
|
|
meetingEndedCode,
|
|
joinErrorCode,
|
|
}) => {
|
|
const {
|
|
loading: meetingEndLoading,
|
|
error: meetingEndError,
|
|
data: meetingEndData,
|
|
} = useQuery<MeetingEndDataResponse>(getMeetingEndData);
|
|
|
|
if (meetingEndLoading || !meetingEndData) {
|
|
return (
|
|
<MeetingEnded
|
|
endedBy=""
|
|
joinErrorCode=""
|
|
meetingEndedCode=""
|
|
allowDefaultLogoutUrl={false}
|
|
askForFeedbackOnLogout={false}
|
|
learningDashboardAccessToken=""
|
|
isModerator={false}
|
|
learningDashboardBase=""
|
|
isBreakout={false}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (meetingEndError) {
|
|
logger.error('Error on fetching meeting end data: ', meetingEndError);
|
|
return (
|
|
<MeetingEnded
|
|
endedBy=""
|
|
joinErrorCode=""
|
|
meetingEndedCode=""
|
|
allowDefaultLogoutUrl={false}
|
|
askForFeedbackOnLogout={false}
|
|
learningDashboardAccessToken=""
|
|
isModerator={false}
|
|
learningDashboardBase=""
|
|
isBreakout={false}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const {
|
|
user_current,
|
|
} = meetingEndData;
|
|
const {
|
|
isModerator,
|
|
meeting,
|
|
} = user_current[0];
|
|
|
|
const {
|
|
learningDashboard,
|
|
isBreakout,
|
|
clientSettings,
|
|
} = meeting;
|
|
|
|
const {
|
|
askForFeedbackOnLogout,
|
|
allowDefaultLogoutUrl,
|
|
learningDashboardBase,
|
|
} = clientSettings;
|
|
|
|
const shouldAskForFeedback = askForFeedbackOnLogout
|
|
|| getFromUserSettings('bbb_ask_for_feedback_on_logout');
|
|
|
|
return (
|
|
<MeetingEnded
|
|
endedBy={endedBy}
|
|
joinErrorCode={joinErrorCode}
|
|
meetingEndedCode={meetingEndedCode}
|
|
allowDefaultLogoutUrl={allowDefaultLogoutUrl}
|
|
askForFeedbackOnLogout={shouldAskForFeedback}
|
|
learningDashboardAccessToken={learningDashboard?.learningDashboardAccessToken}
|
|
isModerator={isModerator}
|
|
learningDashboardBase={learningDashboardBase}
|
|
isBreakout={isBreakout}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export default MeetingEndedContainer;
|