mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-02-12 17:00:19 +00:00
Compare commits
37 Commits
debug-test
...
release-77
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3250686ce9 | ||
|
|
8149dfecd0 | ||
|
|
412f462448 | ||
|
|
71b4943480 | ||
|
|
9b3931b575 | ||
|
|
9d7a4c7dea | ||
|
|
5fc0ecf55e | ||
|
|
15f935e83c | ||
|
|
9de8f612a1 | ||
|
|
a01b9655b5 | ||
|
|
6cc2a30ffa | ||
|
|
6f3706f734 | ||
|
|
3e03257b3c | ||
|
|
d8df1a2393 | ||
|
|
2296a50854 | ||
|
|
9d90045918 | ||
|
|
51cbf7272a | ||
|
|
22809437e0 | ||
|
|
153dd4679c | ||
|
|
f909c101da | ||
|
|
4a2c27bf64 | ||
|
|
fba2abb772 | ||
|
|
86d68976bc | ||
|
|
738581b2c8 | ||
|
|
bb1933f892 | ||
|
|
e2d305aea9 | ||
|
|
622043ff7e | ||
|
|
d50163deee | ||
|
|
48555180d8 | ||
|
|
892e40bdca | ||
|
|
c8e300a12f | ||
|
|
ffc02b2b3e | ||
|
|
28d6046cf4 | ||
|
|
3d69907db2 | ||
|
|
a559de5ed1 | ||
|
|
e7712fa35a | ||
|
|
643a6dc130 |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -73,6 +73,10 @@ jobs:
|
||||
node-version: 16
|
||||
cache: 'npm'
|
||||
- run: npm install
|
||||
- name: setup-cocoapods
|
||||
uses: maxim-lobanov/setup-cocoapods@v1
|
||||
with:
|
||||
podfile-path: ios/Podfile.lock
|
||||
- name: Install Pods
|
||||
run: |
|
||||
pod --version
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
dataChannelClosed,
|
||||
dataChannelOpened,
|
||||
e2eRttChanged,
|
||||
endpointMessageReceived,
|
||||
kickedOut,
|
||||
lockStateChanged,
|
||||
nonParticipantMessageReceived,
|
||||
@@ -162,10 +163,8 @@ import { isScreenAudioShared } from './react/features/screen-share/functions';
|
||||
import { toggleScreenshotCaptureSummary } from './react/features/screenshot-capture/actions';
|
||||
import { AudioMixerEffect } from './react/features/stream-effects/audio-mixer/AudioMixerEffect';
|
||||
import { createRnnoiseProcessor } from './react/features/stream-effects/rnnoise';
|
||||
import { endpointMessageReceived } from './react/features/subtitles/actions.any';
|
||||
import { handleToggleVideoMuted } from './react/features/toolbox/actions.any';
|
||||
import { muteLocal } from './react/features/video-menu/actions.any';
|
||||
import { iAmVisitor } from './react/features/visitors/functions';
|
||||
import UIEvents from './service/UI/UIEvents';
|
||||
|
||||
const logger = Logger.getLogger(__filename);
|
||||
@@ -697,10 +696,6 @@ export default {
|
||||
const handleInitialTracks = (options, tracks) => {
|
||||
let localTracks = tracks;
|
||||
|
||||
// No local tracks are added when user joins as a visitor.
|
||||
if (iAmVisitor(state)) {
|
||||
return [];
|
||||
}
|
||||
if (options.startWithAudioMuted || room?.isStartAudioMuted()) {
|
||||
// Always add the track on Safari because of a known issue where audio playout doesn't happen
|
||||
// if the user joins audio and video muted, i.e., if there is no local media capture.
|
||||
@@ -1824,28 +1819,24 @@ export default {
|
||||
|
||||
room.on(
|
||||
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
|
||||
(...args) => {
|
||||
APP.store.dispatch(endpointMessageReceived(...args));
|
||||
if (args && args.length >= 2) {
|
||||
const [ sender, eventData ] = args;
|
||||
|
||||
if (eventData.name === ENDPOINT_TEXT_MESSAGE_NAME) {
|
||||
APP.API.notifyEndpointTextMessageReceived({
|
||||
senderInfo: {
|
||||
jid: sender._jid,
|
||||
id: sender._id
|
||||
},
|
||||
eventData
|
||||
});
|
||||
}
|
||||
(participant, data) => {
|
||||
APP.store.dispatch(endpointMessageReceived(participant, data));
|
||||
if (data?.name === ENDPOINT_TEXT_MESSAGE_NAME) {
|
||||
APP.API.notifyEndpointTextMessageReceived({
|
||||
senderInfo: {
|
||||
jid: participant.getJid(),
|
||||
id: participant.getId()
|
||||
},
|
||||
eventData: data
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
room.on(
|
||||
JitsiConferenceEvents.NON_PARTICIPANT_MESSAGE_RECEIVED,
|
||||
(...args) => {
|
||||
APP.store.dispatch(nonParticipantMessageReceived(...args));
|
||||
APP.API.notifyNonParticipantMessageReceived(...args);
|
||||
(id, data) => {
|
||||
APP.store.dispatch(nonParticipantMessageReceived(id, data));
|
||||
APP.API.notifyNonParticipantMessageReceived(id, data);
|
||||
});
|
||||
|
||||
room.on(
|
||||
|
||||
@@ -326,9 +326,14 @@ var config = {
|
||||
|
||||
// configuration for all things recording related. Existing settings will be migrated here in the future.
|
||||
// recordings: {
|
||||
// // IF true (default) recording audio and video is selected by default in the recording dialog.
|
||||
// // recordAudioAndVideo: true,
|
||||
// // If true, shows a notification at the start of the meeting with a call to action button
|
||||
// // to start recording (for users who can do so).
|
||||
// // suggestRecording: true,
|
||||
// // If true, shows a warning label in the prejoin screen to point out the possibility that
|
||||
// // the call you're joining might be recorded.
|
||||
// // showPrejoinWarning: true,
|
||||
// },
|
||||
|
||||
// recordingService: {
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 48px;
|
||||
max-width: calc(100% - 24px);
|
||||
}
|
||||
|
||||
@keyframes hideSubject {
|
||||
|
||||
@@ -36,6 +36,7 @@ $flagsImagePath: "../images/";
|
||||
@import 'modals/invite/info';
|
||||
@import 'modals/screen-share/share-audio';
|
||||
@import 'modals/screen-share/share-screen-warning';
|
||||
@import 'modals/whiteboard';
|
||||
@import 'videolayout_default';
|
||||
@import 'subject';
|
||||
@import 'popup_menu';
|
||||
|
||||
7
css/modals/_whiteboard.scss
Normal file
7
css/modals/_whiteboard.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.whiteboard {
|
||||
|
||||
.excalidraw-wrapper {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
}
|
||||
1
globals.d.ts
vendored
1
globals.d.ts
vendored
@@ -21,6 +21,7 @@ declare global {
|
||||
JitsiMeetElectron?: any;
|
||||
PressureObserver?: any;
|
||||
PressureRecord?: any;
|
||||
ReactNativeWebView?: any;
|
||||
// selenium tests handler
|
||||
_sharedVideoPlayer: any;
|
||||
alwaysOnTop: { api: any };
|
||||
|
||||
3
globals.native.d.ts
vendored
3
globals.native.d.ts
vendored
@@ -19,6 +19,9 @@ interface IWindow {
|
||||
location: ILocation;
|
||||
PressureObserver?: any;
|
||||
PressureRecord?: any;
|
||||
ReactNativeWebView?: any;
|
||||
TextDecoder?: any;
|
||||
TextEncoder?: any;
|
||||
self: any;
|
||||
top: any;
|
||||
|
||||
|
||||
@@ -560,6 +560,7 @@
|
||||
"noNumbers": "No dial-in numbers.",
|
||||
"noPassword": "None",
|
||||
"noRoom": "No room was specified to dial-in into.",
|
||||
"noWhiteboard": "Could not load the whiteboard.",
|
||||
"numbers": "Dial-in Numbers",
|
||||
"password": "$t(lockRoomPasswordUppercase): ",
|
||||
"reachedLimit": "You have reached the limit of your plan.",
|
||||
@@ -567,7 +568,8 @@
|
||||
"sipAudioOnly": "SIP audio only address",
|
||||
"title": "Share",
|
||||
"tooltip": "Share link and dial-in info for this meeting",
|
||||
"upgradeOptions": "Please check the upgrade options on"
|
||||
"upgradeOptions": "Please check the upgrade options on",
|
||||
"whiteboardError": "Error loading the whiteboard. Please try again later."
|
||||
},
|
||||
"inlineDialogFailure": {
|
||||
"msg": "We stumbled a bit.",
|
||||
@@ -937,6 +939,7 @@
|
||||
"or": "or",
|
||||
"premeeting": "Pre meeting",
|
||||
"proceedAnyway": "Proceed anyway",
|
||||
"recordingWarning": "Other participants may be recording this call",
|
||||
"screenSharingError": "Screen sharing error:",
|
||||
"showScreen": "Enable pre meeting screen",
|
||||
"startWithPhone": "Start with phone audio",
|
||||
@@ -1530,6 +1533,7 @@
|
||||
"whiteboard": {
|
||||
"accessibilityLabel": {
|
||||
"heading": "Whiteboard"
|
||||
}
|
||||
},
|
||||
"screenTitle": "Whiteboard"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ import { isAudioMuteButtonDisabled } from '../../react/features/toolbox/function
|
||||
import { setTileView, toggleTileView } from '../../react/features/video-layout/actions.any';
|
||||
import { muteAllParticipants } from '../../react/features/video-menu/actions';
|
||||
import { setVideoQuality } from '../../react/features/video-quality/actions';
|
||||
import { toggleWhiteboard } from '../../react/features/whiteboard/actions.any';
|
||||
import { toggleWhiteboard } from '../../react/features/whiteboard/actions.web';
|
||||
import { getJitsiMeetTransport } from '../transport';
|
||||
|
||||
import {
|
||||
|
||||
21
package-lock.json
generated
21
package-lock.json
generated
@@ -60,7 +60,7 @@
|
||||
"js-md5": "0.6.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1767.0.0+178f9bbf/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet#release-7790",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
@@ -110,6 +110,7 @@
|
||||
"redux-thunk": "2.4.1",
|
||||
"seamless-scroll-polyfill": "2.1.8",
|
||||
"semver": "7.5.4",
|
||||
"text-encoding": "0.7.0",
|
||||
"tss-react": "4.4.4",
|
||||
"util": "0.12.1",
|
||||
"uuid": "8.3.2",
|
||||
@@ -12904,8 +12905,7 @@
|
||||
},
|
||||
"node_modules/lib-jitsi-meet": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1767.0.0+178f9bbf/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-fhuRtG6n1EqfcAMwXF2BMfa5pBO4lHgWRoWj4tviY8//eTrld98HbWxMkSXKUtL0B9slXlPQYn+WyjYUJ63wuQ==",
|
||||
"resolved": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#e5571260d780d196eec489d61e6b2d241db3d60c",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -18625,6 +18625,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/text-encoding": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz",
|
||||
"integrity": "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==",
|
||||
"deprecated": "no longer maintained"
|
||||
},
|
||||
"node_modules/text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
@@ -29472,8 +29478,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1767.0.0+178f9bbf/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-fhuRtG6n1EqfcAMwXF2BMfa5pBO4lHgWRoWj4tviY8//eTrld98HbWxMkSXKUtL0B9slXlPQYn+WyjYUJ63wuQ==",
|
||||
"version": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#e5571260d780d196eec489d61e6b2d241db3d60c",
|
||||
"from": "lib-jitsi-meet@https://github.com/jitsi/lib-jitsi-meet#release-7790",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "2.2.1",
|
||||
"@jitsi/logger": "2.0.2",
|
||||
@@ -33629,6 +33635,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"text-encoding": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz",
|
||||
"integrity": "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA=="
|
||||
},
|
||||
"text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
"js-md5": "0.6.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1767.0.0+178f9bbf/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet#release-7790",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
@@ -116,6 +116,7 @@
|
||||
"redux-thunk": "2.4.1",
|
||||
"seamless-scroll-polyfill": "2.1.8",
|
||||
"semver": "7.5.4",
|
||||
"text-encoding": "0.7.0",
|
||||
"tss-react": "4.4.4",
|
||||
"util": "0.12.1",
|
||||
"uuid": "8.3.2",
|
||||
|
||||
@@ -51,5 +51,6 @@ import '../video-layout/middleware';
|
||||
import '../video-quality/middleware';
|
||||
import '../videosipgw/middleware';
|
||||
import '../visitors/middleware';
|
||||
import '../whiteboard/middleware.any';
|
||||
|
||||
import './middleware';
|
||||
|
||||
@@ -13,5 +13,6 @@ import '../mobile/react-native-sdk/middleware';
|
||||
import '../mobile/watchos/middleware';
|
||||
import '../share-room/middleware';
|
||||
import '../shared-video/middleware';
|
||||
import '../whiteboard/middleware.native';
|
||||
|
||||
import './middlewares.any';
|
||||
|
||||
@@ -22,6 +22,6 @@ import '../talk-while-muted/middleware';
|
||||
import '../toolbox/middleware';
|
||||
import '../face-landmarks/middleware';
|
||||
import '../gifs/middleware';
|
||||
import '../whiteboard/middleware';
|
||||
import '../whiteboard/middleware.web';
|
||||
|
||||
import './middlewares.any';
|
||||
|
||||
@@ -55,3 +55,4 @@ import '../video-layout/reducer';
|
||||
import '../video-quality/reducer';
|
||||
import '../videosipgw/reducer';
|
||||
import '../visitors/reducer';
|
||||
import '../whiteboard/reducer';
|
||||
|
||||
@@ -16,7 +16,6 @@ import '../noise-suppression/reducer';
|
||||
import '../screenshot-capture/reducer';
|
||||
import '../talk-while-muted/reducer';
|
||||
import '../virtual-background/reducer';
|
||||
import '../whiteboard/reducer';
|
||||
import '../web-hid/reducer';
|
||||
|
||||
import './reducers.any';
|
||||
|
||||
@@ -178,6 +178,18 @@ export const DATA_CHANNEL_OPENED = 'DATA_CHANNEL_OPENED';
|
||||
*/
|
||||
export const DATA_CHANNEL_CLOSED = 'DATA_CHANNEL_CLOSED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which indicates that an endpoint message
|
||||
* sent by another participant to the data channel is received.
|
||||
*
|
||||
* {
|
||||
* type: ENDPOINT_MESSAGE_RECEIVED,
|
||||
* participant: Object,
|
||||
* data: Object
|
||||
* }
|
||||
*/
|
||||
export const ENDPOINT_MESSAGE_RECEIVED = 'ENDPOINT_MESSAGE_RECEIVED';
|
||||
|
||||
/**
|
||||
* The type of action which signals that the user has been kicked out from
|
||||
* the conference.
|
||||
@@ -333,3 +345,13 @@ export const SET_START_MUTED_POLICY = 'SET_START_MUTED_POLICY';
|
||||
* }
|
||||
*/
|
||||
export const SET_ASSUMED_BANDWIDTH_BPS = 'SET_ASSUMED_BANDWIDTH_BPS';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which updated the conference metadata.
|
||||
*
|
||||
* {
|
||||
* type: UPDATE_CONFERENCE_METADATA,
|
||||
* metadata: Object
|
||||
* }
|
||||
*/
|
||||
export const UPDATE_CONFERENCE_METADATA = 'UPDATE_CONFERENCE_METADATA';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createStartMutedConfigurationEvent } from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { endpointMessageReceived } from '../../subtitles/actions.any';
|
||||
import { setIAmVisitor } from '../../visitors/actions';
|
||||
import { iAmVisitor } from '../../visitors/functions';
|
||||
import { overwriteConfig } from '../config/actions';
|
||||
@@ -9,8 +8,15 @@ import { getReplaceParticipant } from '../config/functions';
|
||||
import { connect, disconnect, hangup } from '../connection/actions';
|
||||
import { JITSI_CONNECTION_CONFERENCE_KEY } from '../connection/constants';
|
||||
import { JitsiConferenceEvents, JitsiE2ePingEvents } from '../lib-jitsi-meet';
|
||||
import { setAudioMuted, setAudioUnmutePermissions, setVideoMuted, setVideoUnmutePermissions } from '../media/actions';
|
||||
import {
|
||||
gumPending,
|
||||
setAudioMuted,
|
||||
setAudioUnmutePermissions,
|
||||
setVideoMuted,
|
||||
setVideoUnmutePermissions
|
||||
} from '../media/actions';
|
||||
import { MEDIA_TYPE } from '../media/constants';
|
||||
import { IGUMPendingState } from '../media/types';
|
||||
import {
|
||||
dominantSpeakerChanged,
|
||||
participantKicked,
|
||||
@@ -48,6 +54,7 @@ import {
|
||||
DATA_CHANNEL_CLOSED,
|
||||
DATA_CHANNEL_OPENED,
|
||||
E2E_RTT_CHANGED,
|
||||
ENDPOINT_MESSAGE_RECEIVED,
|
||||
KICKED_OUT,
|
||||
LOCK_STATE_CHANGED,
|
||||
NON_PARTICIPANT_MESSAGE_RECEIVED,
|
||||
@@ -61,7 +68,8 @@ import {
|
||||
SET_PENDING_SUBJECT_CHANGE,
|
||||
SET_ROOM,
|
||||
SET_START_MUTED_POLICY,
|
||||
SET_START_REACTIONS_MUTED
|
||||
SET_START_REACTIONS_MUTED,
|
||||
UPDATE_CONFERENCE_METADATA
|
||||
} from './actionTypes';
|
||||
import {
|
||||
AVATAR_URL_COMMAND,
|
||||
@@ -79,7 +87,7 @@ import {
|
||||
sendLocalParticipant
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
import { IJitsiConference } from './reducer';
|
||||
import { IConferenceMetadata, IJitsiConference } from './reducer';
|
||||
|
||||
/**
|
||||
* Adds conference (event) listeners.
|
||||
@@ -275,6 +283,21 @@ function _addConferenceListeners(conference: IJitsiConference, dispatch: IStore[
|
||||
})));
|
||||
}
|
||||
|
||||
/**
|
||||
* Action for updating the conference metadata.
|
||||
*
|
||||
* @param {IConferenceMetadata} metadata - The metadata object.
|
||||
* @returns {{
|
||||
* type: UPDATE_CONFERENCE_METADATA,
|
||||
* metadata: IConferenceMetadata
|
||||
* }}
|
||||
*/
|
||||
export function updateConferenceMetadata(metadata: IConferenceMetadata | null) {
|
||||
return {
|
||||
type: UPDATE_CONFERENCE_METADATA,
|
||||
metadata
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an action for when the end-to-end RTT against a specific remote participant has changed.
|
||||
@@ -630,6 +653,25 @@ export function dataChannelClosed(code: number, reason: string) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that a participant sent an endpoint message on the data channel.
|
||||
*
|
||||
* @param {Object} participant - The participant details sending the message.
|
||||
* @param {Object} data - The data carried by the endpoint message.
|
||||
* @returns {{
|
||||
* type: ENDPOINT_MESSAGE_RECEIVED,
|
||||
* participant: Object,
|
||||
* data: Object
|
||||
* }}
|
||||
*/
|
||||
export function endpointMessageReceived(participant: Object, data: Object) {
|
||||
return {
|
||||
type: ENDPOINT_MESSAGE_RECEIVED,
|
||||
participant,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to end a conference for all participants.
|
||||
*
|
||||
@@ -1017,6 +1059,11 @@ export function redirect(vnode: string, focusJid: string, username: string) {
|
||||
.then(() => dispatch(conferenceWillInit()))
|
||||
.then(() => dispatch(connect()))
|
||||
.then(() => {
|
||||
|
||||
// Clear the gum pending state in case we have set it to pending since we are starting the
|
||||
// conference without tracks.
|
||||
dispatch(gumPending([ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ], IGUMPendingState.NONE));
|
||||
|
||||
// FIXME: Workaround for the web version. To be removed once we get rid of conference.js
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.conference.startConference([]);
|
||||
|
||||
@@ -25,7 +25,7 @@ import { overwriteConfig } from '../config/actions';
|
||||
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../connection/actionTypes';
|
||||
import { connect, connectionDisconnected, disconnect } from '../connection/actions';
|
||||
import { validateJwt } from '../jwt/functions';
|
||||
import { JitsiConferenceErrors, JitsiConnectionErrors } from '../lib-jitsi-meet';
|
||||
import { JitsiConferenceErrors, JitsiConferenceEvents, JitsiConnectionErrors } from '../lib-jitsi-meet';
|
||||
import { PARTICIPANT_UPDATED, PIN_PARTICIPANT } from '../participants/actionTypes';
|
||||
import { PARTICIPANT_ROLE } from '../participants/constants';
|
||||
import {
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
getPinnedParticipant
|
||||
} from '../participants/functions';
|
||||
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
|
||||
import StateListenerRegistry from '../redux/StateListenerRegistry';
|
||||
import { TRACK_ADDED, TRACK_REMOVED } from '../tracks/actionTypes';
|
||||
import { getLocalTracks } from '../tracks/functions.any';
|
||||
|
||||
@@ -54,7 +55,8 @@ import {
|
||||
conferenceWillLeave,
|
||||
createConference,
|
||||
setLocalSubject,
|
||||
setSubject
|
||||
setSubject,
|
||||
updateConferenceMetadata
|
||||
} from './actions';
|
||||
import { CONFERENCE_LEAVE_REASONS } from './constants';
|
||||
import {
|
||||
@@ -65,6 +67,7 @@ import {
|
||||
restoreConferenceOptions
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
import { IConferenceMetadata } from './reducer';
|
||||
|
||||
/**
|
||||
* Handler for before unload event.
|
||||
@@ -124,6 +127,24 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Set up state change listener to perform maintenance tasks when the conference
|
||||
* is left or failed.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
state => getCurrentConference(state),
|
||||
(conference, { dispatch }, previousConference): void => {
|
||||
if (conference && !previousConference) {
|
||||
conference.on(JitsiConferenceEvents.METADATA_UPDATED, (metadata: IConferenceMetadata) => {
|
||||
dispatch(updateConferenceMetadata(metadata));
|
||||
});
|
||||
}
|
||||
|
||||
if (conference !== previousConference) {
|
||||
dispatch(updateConferenceMetadata(null));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Makes sure to leave a failed conference in order to release any allocated
|
||||
* resources like peer connections, emit participant left events, etc.
|
||||
|
||||
@@ -27,7 +27,8 @@ import {
|
||||
SET_PENDING_SUBJECT_CHANGE,
|
||||
SET_ROOM,
|
||||
SET_START_MUTED_POLICY,
|
||||
SET_START_REACTIONS_MUTED
|
||||
SET_START_REACTIONS_MUTED,
|
||||
UPDATE_CONFERENCE_METADATA
|
||||
} from './actionTypes';
|
||||
import { isRoomValid } from './functions';
|
||||
|
||||
@@ -39,10 +40,23 @@ const DEFAULT_STATE = {
|
||||
leaving: undefined,
|
||||
locked: undefined,
|
||||
membersOnly: undefined,
|
||||
metadata: undefined,
|
||||
password: undefined,
|
||||
passwordRequired: undefined
|
||||
};
|
||||
|
||||
export interface IConferenceMetadata {
|
||||
recording?: {
|
||||
isTranscribingEnabled: boolean;
|
||||
};
|
||||
whiteboard?: {
|
||||
collabDetails: {
|
||||
roomId: string;
|
||||
roomKey: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface IJitsiConference {
|
||||
addCommandListener: Function;
|
||||
addLobbyMessageListener: Function;
|
||||
@@ -141,6 +155,7 @@ export interface IConferenceState {
|
||||
localSubject?: string;
|
||||
locked?: string;
|
||||
membersOnly?: IJitsiConference;
|
||||
metadata?: IConferenceMetadata;
|
||||
obfuscatedRoom?: string;
|
||||
obfuscatedRoomSource?: string;
|
||||
p2p?: Object;
|
||||
@@ -247,6 +262,12 @@ ReducerRegistry.register<IConferenceState>('features/base/conference',
|
||||
startAudioMutedPolicy: action.startAudioMutedPolicy,
|
||||
startVideoMutedPolicy: action.startVideoMutedPolicy
|
||||
};
|
||||
|
||||
case UPDATE_CONFERENCE_METADATA:
|
||||
return {
|
||||
...state,
|
||||
metadata: action.metadata
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
|
||||
@@ -531,6 +531,8 @@ export interface IConfig {
|
||||
};
|
||||
recordingSharingUrl?: string;
|
||||
recordings?: {
|
||||
recordAudioAndVideo?: boolean;
|
||||
showPrejoinWarning?: boolean;
|
||||
suggestRecording?: boolean;
|
||||
};
|
||||
remoteVideoMenu?: {
|
||||
|
||||
@@ -201,6 +201,7 @@ export default [
|
||||
'remoteVideoMenu',
|
||||
'roomPasswordNumberOfDigits',
|
||||
'readOnlyName',
|
||||
'recordings',
|
||||
'replaceParticipant',
|
||||
'resolution',
|
||||
'salesforceUrl',
|
||||
|
||||
@@ -13,6 +13,7 @@ import { withPixelLineHeight } from '../../../styles/functions.web';
|
||||
|
||||
import ConnectionStatus from './ConnectionStatus';
|
||||
import Preview from './Preview';
|
||||
import RecordingWarning from './RecordingWarning';
|
||||
import UnsafeRoomWarning from './UnsafeRoomWarning';
|
||||
|
||||
interface IProps {
|
||||
@@ -57,6 +58,11 @@ interface IProps {
|
||||
*/
|
||||
showDeviceStatus: boolean;
|
||||
|
||||
/**
|
||||
* Indicates whether to display the recording warning.
|
||||
*/
|
||||
showRecordingWarning?: boolean;
|
||||
|
||||
/**
|
||||
* If should show unsafe room warning when joining.
|
||||
*/
|
||||
@@ -167,6 +173,7 @@ const PreMeetingScreen = ({
|
||||
children,
|
||||
className,
|
||||
showDeviceStatus,
|
||||
showRecordingWarning,
|
||||
showUnsafeRoomWarning,
|
||||
skipPrejoinButton,
|
||||
title,
|
||||
@@ -200,6 +207,7 @@ const PreMeetingScreen = ({
|
||||
{skipPrejoinButton}
|
||||
{showUnsafeRoomWarning && <UnsafeRoomWarning />}
|
||||
{showDeviceStatus && <DeviceStatus />}
|
||||
{showRecordingWarning && <RecordingWarning />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { withPixelLineHeight } from '../../../styles/functions.web';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
warning: {
|
||||
bottom: 0,
|
||||
color: theme.palette.text03,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
...withPixelLineHeight(theme.typography.bodyShortRegular),
|
||||
marginBottom: theme.spacing(3),
|
||||
marginTop: theme.spacing(2),
|
||||
paddingLeft: theme.spacing(3),
|
||||
paddingRight: theme.spacing(3),
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
|
||||
'@media (max-width: 720px)': {
|
||||
position: 'relative'
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const RecordingWarning = () => {
|
||||
const { t } = useTranslation();
|
||||
const { classes } = useStyles();
|
||||
|
||||
return (
|
||||
<div className = { classes.warning }>
|
||||
{t('prejoin.recordingWarning')}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecordingWarning;
|
||||
@@ -145,6 +145,24 @@ const useStyles = makeStyles()(theme => {
|
||||
pointerEvents: 'none'
|
||||
},
|
||||
|
||||
contextMenuItemIconDisabled: {
|
||||
'& svg': {
|
||||
fill: `${theme.palette.text03} !important`
|
||||
}
|
||||
},
|
||||
|
||||
contextMenuItemLabelDisabled: {
|
||||
color: theme.palette.text03,
|
||||
|
||||
'&:hover': {
|
||||
background: 'none'
|
||||
},
|
||||
|
||||
'& svg': {
|
||||
fill: theme.palette.text03
|
||||
}
|
||||
},
|
||||
|
||||
contextMenuItemDrawer: {
|
||||
padding: '13px 16px'
|
||||
},
|
||||
@@ -233,13 +251,15 @@ const ContextMenuItem = ({
|
||||
tabIndex = { onClick ? tabIndex : undefined }>
|
||||
{customIcon ? customIcon
|
||||
: icon && <Icon
|
||||
className = { styles.contextMenuItemIcon }
|
||||
className = { cx(styles.contextMenuItemIcon,
|
||||
disabled && styles.contextMenuItemIconDisabled) }
|
||||
size = { 20 }
|
||||
src = { icon } />}
|
||||
{text && (
|
||||
<TextWithOverflow
|
||||
className = { cx(styles.text,
|
||||
_overflowDrawer && styles.drawerText,
|
||||
disabled && styles.contextMenuItemLabelDisabled,
|
||||
textClassName) }
|
||||
overflowType = { overflowType } >
|
||||
{text}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import base64js from 'base64-js';
|
||||
|
||||
import { timeoutPromise } from './timeoutPromise';
|
||||
|
||||
/**
|
||||
@@ -43,3 +45,37 @@ export function doGetJSON(url: string, retry?: boolean, options?: Object) {
|
||||
|
||||
return fetchPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes strings to Base64URL.
|
||||
*
|
||||
* @param {any} data - The byte array to encode.
|
||||
* @returns {string}
|
||||
*/
|
||||
export const encodeToBase64URL = (data: string): string => base64js
|
||||
.fromByteArray(new window.TextEncoder().encode(data))
|
||||
.replace(/=/g, '')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_');
|
||||
|
||||
/**
|
||||
* Decodes strings from Base64URL.
|
||||
*
|
||||
* @param {string} data - The byte array to decode.
|
||||
* @returns {string}
|
||||
*/
|
||||
export const decodeFromBase64URL = (data: string): string => {
|
||||
let s = data;
|
||||
|
||||
// Convert from Base64URL to Base64.
|
||||
if (s.length % 4 === 2) {
|
||||
s += '==';
|
||||
} else if (s.length % 4 === 3) {
|
||||
s += '=';
|
||||
}
|
||||
|
||||
s = s.replace(/-/g, '+').replace(/_/g, '/');
|
||||
|
||||
// Convert Base64 to a byte array.
|
||||
return new window.TextDecoder().decode(base64js.toByteArray(s));
|
||||
};
|
||||
|
||||
@@ -161,8 +161,7 @@ export const getCurrentRoomId = (stateful: IStateful) => {
|
||||
export const isInBreakoutRoom = (stateful: IStateful) => {
|
||||
const conference = getCurrentConference(stateful);
|
||||
|
||||
return conference?.getBreakoutRooms()
|
||||
?.isBreakoutRoom();
|
||||
return conference?.getBreakoutRooms()?.isBreakoutRoom();
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@ import { AnyAction } from 'redux';
|
||||
|
||||
import { IReduxState, IStore } from '../app/types';
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
|
||||
import { CONFERENCE_JOINED } from '../base/conference/actionTypes';
|
||||
import { CONFERENCE_JOINED, ENDPOINT_MESSAGE_RECEIVED } from '../base/conference/actionTypes';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { IJitsiConference } from '../base/conference/reducer';
|
||||
import { openDialog } from '../base/dialog/actions';
|
||||
@@ -92,14 +92,6 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
_addChatMsgListener(action.conference, store);
|
||||
break;
|
||||
|
||||
case OPEN_CHAT:
|
||||
unreadCount = 0;
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyChatUpdated(unreadCount, true);
|
||||
}
|
||||
break;
|
||||
|
||||
case CLOSE_CHAT: {
|
||||
const isPollTabOpen = getState()['features/chat'].isPollsTabFocused;
|
||||
|
||||
@@ -115,6 +107,38 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
break;
|
||||
}
|
||||
|
||||
case ENDPOINT_MESSAGE_RECEIVED: {
|
||||
const state = store.getState();
|
||||
|
||||
if (!isReactionsEnabled(state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { participant, data } = action;
|
||||
|
||||
if (data?.name === ENDPOINT_REACTION_NAME) {
|
||||
store.dispatch(pushReactions(data.reactions));
|
||||
|
||||
_handleReceivedMessage(store, {
|
||||
id: participant.getId(),
|
||||
message: getReactionMessageFromBuffer(data.reactions),
|
||||
privateMessage: false,
|
||||
lobbyChat: false,
|
||||
timestamp: data.timestamp
|
||||
}, false, true);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case OPEN_CHAT:
|
||||
unreadCount = 0;
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyChatUpdated(unreadCount, true);
|
||||
}
|
||||
break;
|
||||
|
||||
case SET_IS_POLL_TAB_FOCUSED: {
|
||||
dispatch(resetNbUnreadPollsMessages());
|
||||
break;
|
||||
@@ -252,33 +276,6 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
|
||||
}
|
||||
);
|
||||
|
||||
// TODO: clean this up, there should be a central place that dispatches this. -saghul
|
||||
conference.on(
|
||||
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
|
||||
(...args: any) => {
|
||||
const state = store.getState();
|
||||
|
||||
if (!isReactionsEnabled(state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (args && args.length >= 2) {
|
||||
const [ { _id }, eventData ] = args;
|
||||
|
||||
if (eventData.name === ENDPOINT_REACTION_NAME) {
|
||||
store.dispatch(pushReactions(eventData.reactions));
|
||||
|
||||
_handleReceivedMessage(store, {
|
||||
id: _id,
|
||||
message: getReactionMessageFromBuffer(eventData.reactions),
|
||||
privateMessage: false,
|
||||
lobbyChat: false,
|
||||
timestamp: eventData.timestamp
|
||||
}, false, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
conference.on(
|
||||
JitsiConferenceEvents.CONFERENCE_ERROR, (errorType: string, error: Error) => {
|
||||
errorType === JitsiConferenceErrors.CHAT_ERROR && _handleChatError(store, error);
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
|
||||
import i18next from 'i18next';
|
||||
|
||||
import { CONFERENCE_JOINED, KICKED_OUT } from '../base/conference/actionTypes';
|
||||
import { IJitsiConference } from '../base/conference/reducer';
|
||||
import { ENDPOINT_MESSAGE_RECEIVED, KICKED_OUT } from '../base/conference/actionTypes';
|
||||
import { hangup } from '../base/connection/actions.web';
|
||||
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||
import { getParticipantDisplayName } from '../base/participants/functions';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import { openAllowToggleCameraDialog, setCameraFacingMode } from '../base/tracks/actions.web';
|
||||
@@ -14,8 +11,15 @@ import './middleware.any';
|
||||
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case CONFERENCE_JOINED: {
|
||||
_addSetCameraFacingModeListener(action.conference);
|
||||
case ENDPOINT_MESSAGE_RECEIVED: {
|
||||
const { participant, data } = action;
|
||||
|
||||
if (data?.name === CAMERA_FACING_MODE_MESSAGE) {
|
||||
APP.store.dispatch(openAllowToggleCameraDialog(
|
||||
/* onAllow */ () => APP.store.dispatch(setCameraFacingMode(data.facingMode)),
|
||||
/* initiatorId */ participant.getId()
|
||||
));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -34,28 +38,3 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Registers listener for {@link JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED} that
|
||||
* will perform various chat related activities.
|
||||
*
|
||||
* @param {IJitsiConference} conference - The conference.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _addSetCameraFacingModeListener(conference: IJitsiConference) {
|
||||
conference.on(
|
||||
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
|
||||
(...args: any) => {
|
||||
if (args && args.length >= 2) {
|
||||
const [ sender, eventData ] = args;
|
||||
|
||||
if (eventData.name === CAMERA_FACING_MODE_MESSAGE) {
|
||||
APP.store.dispatch(openAllowToggleCameraDialog(
|
||||
/* onAllow */ () => APP.store.dispatch(setCameraFacingMode(eventData.facingMode)),
|
||||
/* initiatorId */ sender._id
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ class SharedDocument extends PureComponent<IProps> {
|
||||
style = { styles.sharedDocContainer }>
|
||||
<WebView
|
||||
hideKeyboardAccessoryView = { true }
|
||||
incognito = { true }
|
||||
renderLoading = { this._renderLoading }
|
||||
source = {{ uri: _documentUrl ?? '' }}
|
||||
startInLoadingState = { true }
|
||||
|
||||
@@ -3,12 +3,11 @@ import { AnyAction } from 'redux';
|
||||
import { IStore } from '../app/types';
|
||||
import {
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_WILL_LEAVE
|
||||
CONFERENCE_WILL_LEAVE,
|
||||
ENDPOINT_MESSAGE_RECEIVED
|
||||
} from '../base/conference/actionTypes';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||
import { getLocalParticipant, getParticipantCount } from '../base/participants/functions';
|
||||
import { IParticipant } from '../base/participants/types';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import { TRACK_ADDED, TRACK_REMOVED, TRACK_UPDATED } from '../base/tracks/actionTypes';
|
||||
|
||||
@@ -28,27 +27,22 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA
|
||||
FaceLandmarksDetector.init(store);
|
||||
}
|
||||
|
||||
// allow using remote face centering data when local face centering is not enabled
|
||||
action.conference.on(
|
||||
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
|
||||
(participant: IParticipant | undefined, eventData: any) => {
|
||||
if (!participant || !eventData || !participant.getId) {
|
||||
return;
|
||||
}
|
||||
return next(action);
|
||||
} else if (action.type === ENDPOINT_MESSAGE_RECEIVED) {
|
||||
// Allow using remote face centering data when local face centering is not enabled.
|
||||
const { participant, data } = action;
|
||||
|
||||
if (eventData.type === FACE_BOX_EVENT_TYPE) {
|
||||
dispatch({
|
||||
type: UPDATE_FACE_COORDINATES,
|
||||
faceBox: eventData.faceBox,
|
||||
id: participant.getId()
|
||||
});
|
||||
}
|
||||
if (data?.type === FACE_BOX_EVENT_TYPE) {
|
||||
dispatch({
|
||||
type: UPDATE_FACE_COORDINATES,
|
||||
faceBox: data.faceBox,
|
||||
id: participant.getId()
|
||||
});
|
||||
}
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
||||
|
||||
if (!isEnabled) {
|
||||
return next(action);
|
||||
}
|
||||
@@ -57,7 +51,7 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA
|
||||
case CONFERENCE_WILL_LEAVE : {
|
||||
FaceLandmarksDetector.stopDetection(store);
|
||||
|
||||
return next(action);
|
||||
break;
|
||||
}
|
||||
case TRACK_ADDED: {
|
||||
const { jitsiTrack: { isLocal, videoType }, muted } = action.track;
|
||||
@@ -67,18 +61,18 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA
|
||||
FaceLandmarksDetector.startDetection(store, action.track);
|
||||
}
|
||||
|
||||
return next(action);
|
||||
break;
|
||||
}
|
||||
case TRACK_UPDATED: {
|
||||
const { jitsiTrack: { isLocal, videoType } } = action.track;
|
||||
|
||||
if (videoType !== 'camera' || !isLocal()) {
|
||||
return next(action);
|
||||
break;
|
||||
}
|
||||
|
||||
const { muted } = action.track;
|
||||
|
||||
if (muted !== undefined) {
|
||||
if (typeof muted !== 'undefined') {
|
||||
// addresses video mute state changes
|
||||
if (muted) {
|
||||
FaceLandmarksDetector.stopDetection(store);
|
||||
@@ -87,7 +81,7 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
break;
|
||||
}
|
||||
case TRACK_REMOVED: {
|
||||
const { jitsiTrack: { isLocal, videoType } } = action.track;
|
||||
@@ -96,7 +90,7 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA
|
||||
FaceLandmarksDetector.stopDetection(store);
|
||||
}
|
||||
|
||||
return next(action);
|
||||
break;
|
||||
}
|
||||
case ADD_FACE_LANDMARKS: {
|
||||
const state = getState();
|
||||
@@ -110,7 +104,7 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA
|
||||
// Disabling for now as there is no value of having the data in speakerstats at the server
|
||||
// sendFaceExpressionToServer(conference, faceLandmarks);
|
||||
|
||||
return next(action);
|
||||
break;
|
||||
}
|
||||
case NEW_FACE_COORDINATES: {
|
||||
const state = getState();
|
||||
@@ -127,6 +121,8 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA
|
||||
faceBox,
|
||||
id: localParticipant?.id
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@ class DialInSummary extends PureComponent<IProps> {
|
||||
<JitsiScreen
|
||||
style = { styles.backDrop }>
|
||||
<WebView
|
||||
incognito = { true }
|
||||
onError = { this._onError }
|
||||
onShouldStartLoadWithRequest = { this._onNavigate }
|
||||
renderLoading = { this._renderLoading }
|
||||
|
||||
@@ -17,7 +17,7 @@ import SharedVideo from '../../shared-video/components/web/SharedVideo';
|
||||
import Captions from '../../subtitles/components/web/Captions';
|
||||
import { setTileView } from '../../video-layout/actions.web';
|
||||
import Whiteboard from '../../whiteboard/components/web/Whiteboard';
|
||||
import { isWhiteboardReady } from '../../whiteboard/functions';
|
||||
import { isWhiteboardEnabled } from '../../whiteboard/functions';
|
||||
import { setSeeWhatIsBeingShared } from '../actions.web';
|
||||
import { getLargeVideoParticipant } from '../functions';
|
||||
|
||||
@@ -112,7 +112,7 @@ interface IProps {
|
||||
/**
|
||||
* Whether or not the whiteboard is ready to be used.
|
||||
*/
|
||||
_whiteboardReady: boolean;
|
||||
_whiteboardEnabled: boolean;
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
@@ -193,7 +193,7 @@ class LargeVideo extends Component<IProps> {
|
||||
_isChatOpen,
|
||||
_noAutoPlayVideo,
|
||||
_showDominantSpeakerBadge,
|
||||
_whiteboardReady
|
||||
_whiteboardEnabled
|
||||
} = this.props;
|
||||
const style = this._getCustomStyles();
|
||||
const className = `videocontainer${_isChatOpen ? ' shift-right' : ''}`;
|
||||
@@ -205,7 +205,7 @@ class LargeVideo extends Component<IProps> {
|
||||
ref = { this._containerRef }
|
||||
style = { style }>
|
||||
<SharedVideo />
|
||||
{_whiteboardReady && <Whiteboard />}
|
||||
{_whiteboardEnabled && <Whiteboard />}
|
||||
<div id = 'etherpad' />
|
||||
|
||||
<Watermarks />
|
||||
@@ -378,7 +378,7 @@ function _mapStateToProps(state: IReduxState) {
|
||||
_verticalFilmstripWidth: verticalFilmstripWidth.current,
|
||||
_verticalViewMaxWidth: getVerticalViewMaxWidth(state),
|
||||
_visibleFilmstrip: visible,
|
||||
_whiteboardReady: isWhiteboardReady(state)
|
||||
_whiteboardEnabled: isWhiteboardEnabled(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ import { IStore } from '../app/types';
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
|
||||
import {
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_JOINED
|
||||
CONFERENCE_JOINED,
|
||||
ENDPOINT_MESSAGE_RECEIVED
|
||||
} from '../base/conference/actionTypes';
|
||||
import { conferenceWillJoin } from '../base/conference/actions';
|
||||
import {
|
||||
@@ -79,6 +80,13 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
return _conferenceFailed(store, next, action);
|
||||
case CONFERENCE_JOINED:
|
||||
return _conferenceJoined(store, next, action);
|
||||
case ENDPOINT_MESSAGE_RECEIVED: {
|
||||
const { participant, data } = action;
|
||||
|
||||
_maybeSendLobbyNotification(participant, data, store);
|
||||
|
||||
break;
|
||||
}
|
||||
case KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED: {
|
||||
// We need the full update result to be in the store already
|
||||
const result = next(action);
|
||||
@@ -166,13 +174,6 @@ StateListenerRegistry.register(
|
||||
dispatch(updateLobbyParticipantOnLeave(id));
|
||||
});
|
||||
});
|
||||
|
||||
conference.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, (origin: any, sender: any) =>
|
||||
_maybeSendLobbyNotification(origin, sender, {
|
||||
dispatch,
|
||||
getState
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_LEFT,
|
||||
CONFERENCE_WILL_JOIN,
|
||||
ENDPOINT_MESSAGE_RECEIVED,
|
||||
SET_ROOM
|
||||
} from '../../base/conference/actionTypes';
|
||||
import { JITSI_CONFERENCE_URL_KEY } from '../../base/conference/constants';
|
||||
@@ -184,6 +185,22 @@ externalAPIEnabled && MiddlewareRegistry.register(store => next => action => {
|
||||
break;
|
||||
}
|
||||
|
||||
case ENDPOINT_MESSAGE_RECEIVED: {
|
||||
const { participant, data } = action;
|
||||
|
||||
if (data?.name === ENDPOINT_TEXT_MESSAGE_NAME) {
|
||||
sendEvent(
|
||||
store,
|
||||
ENDPOINT_TEXT_MESSAGE_RECEIVED,
|
||||
/* data */ {
|
||||
message: data.text,
|
||||
senderId: participant.getId()
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case ENTER_PICTURE_IN_PICTURE:
|
||||
sendEvent(store, type, /* data */ {});
|
||||
break;
|
||||
@@ -419,24 +436,6 @@ function _unregisterForNativeEvents() {
|
||||
function _registerForEndpointTextMessages(store: IStore) {
|
||||
const conference = getCurrentConference(store.getState());
|
||||
|
||||
conference?.on(
|
||||
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
|
||||
(...args: any[]) => {
|
||||
if (args && args.length >= 2) {
|
||||
const [ sender, eventData ] = args;
|
||||
|
||||
if (eventData.name === ENDPOINT_TEXT_MESSAGE_NAME) {
|
||||
sendEvent(
|
||||
store,
|
||||
ENDPOINT_TEXT_MESSAGE_RECEIVED,
|
||||
/* data */ {
|
||||
message: eventData.text,
|
||||
senderId: sender._id
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
conference?.on(
|
||||
JitsiConferenceEvents.MESSAGE_RECEIVED,
|
||||
(id: string, message: string, timestamp: number) => {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { isUnsafeRoomWarningEnabled } from '../../../prejoin/functions';
|
||||
// @ts-ignore
|
||||
import WelcomePage from '../../../welcome/components/WelcomePage';
|
||||
import { isWelcomePageEnabled } from '../../../welcome/functions';
|
||||
import Whiteboard from '../../../whiteboard/components/native/Whiteboard';
|
||||
import { _ROOT_NAVIGATION_READY } from '../actionTypes';
|
||||
import { rootNavigationRef } from '../rootNavigationContainerRef';
|
||||
import { screen } from '../routes';
|
||||
@@ -23,7 +24,8 @@ import {
|
||||
navigationContainerTheme,
|
||||
preJoinScreenOptions,
|
||||
unsafeMeetingScreenOptions,
|
||||
welcomeScreenOptions
|
||||
welcomeScreenOptions,
|
||||
whiteboardScreenOptions
|
||||
} from '../screenOptions';
|
||||
|
||||
import ConnectingPage from './ConnectingPage';
|
||||
@@ -94,6 +96,10 @@ const RootNavigationContainer = ({ dispatch, isUnsafeRoomWarningAvailable, isWel
|
||||
component = { ConnectingPage }
|
||||
name = { screen.connecting }
|
||||
options = { connectingScreenOptions } />
|
||||
<RootStack.Screen // @ts-ignore
|
||||
component = { Whiteboard }
|
||||
name = { screen.conference.whiteboard }
|
||||
options = { whiteboardScreenOptions } />
|
||||
<RootStack.Screen
|
||||
component = { Prejoin }
|
||||
name = { screen.preJoin }
|
||||
|
||||
@@ -22,7 +22,8 @@ export const screen = {
|
||||
security: 'Security Options',
|
||||
sharedDocument: 'Shared document',
|
||||
speakerStats: 'Speaker Stats',
|
||||
subtitles: 'Subtitles'
|
||||
subtitles: 'Subtitles',
|
||||
whiteboard: 'Whiteboard'
|
||||
},
|
||||
connecting: 'Connecting',
|
||||
dialInSummary: 'Dial-In Info',
|
||||
|
||||
@@ -188,6 +188,19 @@ export const connectingScreenOptions = {
|
||||
headerShown: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Screen options for the whiteboard screen.
|
||||
*/
|
||||
export const whiteboardScreenOptions = {
|
||||
gestureEnabled: true,
|
||||
headerStyle: {
|
||||
backgroundColor: BaseTheme.palette.ui01
|
||||
},
|
||||
headerTitleStyle: {
|
||||
color: BaseTheme.palette.text01
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Screen options for pre-join screen.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DOMParser } from '@xmldom/xmldom';
|
||||
import { Platform } from 'react-native';
|
||||
import BackgroundTimer from 'react-native-background-timer';
|
||||
import { TextDecoder, TextEncoder } from 'text-encoding';
|
||||
|
||||
import 'promise.allsettled/auto'; // Promise.allSettled.
|
||||
import 'react-native-url-polyfill/auto'; // Complete URL polyfill.
|
||||
@@ -313,4 +314,7 @@ function _visitNode(node, callback) {
|
||||
global.sessionStorage = new Storage();
|
||||
}
|
||||
|
||||
global.TextDecoder = TextDecoder;
|
||||
global.TextEncoder = TextEncoder;
|
||||
|
||||
})(global || window || this); // eslint-disable-line no-invalid-this
|
||||
|
||||
@@ -305,7 +305,7 @@ export function isBreakoutRoomRenameAllowed(state: IReduxState) {
|
||||
const isLocalModerator = isLocalParticipantModerator(state);
|
||||
const conference = getCurrentConference(state);
|
||||
const isRenameBreakoutRoomsSupported
|
||||
= conference?.getBreakoutRooms().isFeatureSupported(BREAKOUT_ROOMS_RENAME_FEATURE);
|
||||
= conference?.getBreakoutRooms()?.isFeatureSupported(BREAKOUT_ROOMS_RENAME_FEATURE) ?? false;
|
||||
|
||||
return isLocalModerator && isRenameBreakoutRoomsSupported;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { IStore } from '../app/types';
|
||||
import { CONFERENCE_JOIN_IN_PROGRESS } from '../base/conference/actionTypes';
|
||||
import { ENDPOINT_MESSAGE_RECEIVED, NON_PARTICIPANT_MESSAGE_RECEIVED } from '../base/conference/actionTypes';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
import { playSound } from '../base/sounds/actions';
|
||||
@@ -59,36 +58,32 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
const result = next(action);
|
||||
|
||||
switch (action.type) {
|
||||
case CONFERENCE_JOIN_IN_PROGRESS: {
|
||||
const { conference } = action;
|
||||
case ENDPOINT_MESSAGE_RECEIVED: {
|
||||
const { participant, data } = action;
|
||||
const isNewPoll = data.type === COMMAND_NEW_POLL;
|
||||
|
||||
conference.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
|
||||
(user: any, data: any) => {
|
||||
const isNewPoll = data.type === COMMAND_NEW_POLL;
|
||||
|
||||
_handleReceivePollsMessage({
|
||||
...data,
|
||||
senderId: isNewPoll ? user._id : undefined,
|
||||
voterId: isNewPoll ? undefined : user._id
|
||||
}, dispatch, getState);
|
||||
});
|
||||
conference.on(JitsiConferenceEvents.NON_PARTICIPANT_MESSAGE_RECEIVED,
|
||||
(id: any, data: any) => {
|
||||
const isNewPoll = data.type === COMMAND_NEW_POLL;
|
||||
|
||||
_handleReceivePollsMessage({
|
||||
...data,
|
||||
senderId: isNewPoll ? id : undefined,
|
||||
voterId: isNewPoll ? undefined : id
|
||||
}, dispatch, getState);
|
||||
});
|
||||
_handleReceivePollsMessage({
|
||||
...data,
|
||||
senderId: isNewPoll ? participant.getId() : undefined,
|
||||
voterId: isNewPoll ? undefined : participant.getId()
|
||||
}, dispatch, getState);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Middleware triggered when a poll is received
|
||||
case RECEIVE_POLL: {
|
||||
case NON_PARTICIPANT_MESSAGE_RECEIVED: {
|
||||
const { id, json: data } = action;
|
||||
const isNewPoll = data.type === COMMAND_NEW_POLL;
|
||||
|
||||
_handleReceivePollsMessage({
|
||||
...data,
|
||||
senderId: isNewPoll ? id : undefined,
|
||||
voterId: isNewPoll ? undefined : id
|
||||
}, dispatch, getState);
|
||||
break;
|
||||
}
|
||||
|
||||
case RECEIVE_POLL: {
|
||||
const state = getState();
|
||||
|
||||
if (arePollsDisabled(state)) {
|
||||
|
||||
@@ -168,12 +168,14 @@ const Prejoin: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
|
||||
{
|
||||
isFocused
|
||||
&& <View style = { largeVideoContainerStyles as StyleProp<ViewStyle> }>
|
||||
<View style = { styles.displayRoomNameBackdrop as StyleProp<TextStyle> }>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { styles.preJoinRoomName as StyleProp<TextStyle> }>
|
||||
{ roomName }
|
||||
</Text>
|
||||
<View style = { styles.conferenceInfo as StyleProp<ViewStyle> }>
|
||||
<View style = { styles.displayRoomNameBackdrop as StyleProp<TextStyle> }>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { styles.preJoinRoomName as StyleProp<TextStyle> }>
|
||||
{ roomName }
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<LargeVideo />
|
||||
</View>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
StyleProp,
|
||||
Text,
|
||||
TextStyle,
|
||||
View,
|
||||
ViewStyle
|
||||
} from 'react-native';
|
||||
|
||||
import { preJoinStyles as styles } from './styles';
|
||||
|
||||
|
||||
const RecordingWarning = (): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<View style = { styles.recordingWarning as StyleProp<ViewStyle> }>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { styles.recordingWarningText as StyleProp<TextStyle> }>
|
||||
{ t('prejoin.recordingWarning') }
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecordingWarning;
|
||||
@@ -106,18 +106,33 @@ export const preJoinStyles = {
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
displayRoomNameBackdrop: {
|
||||
conferenceInfo: {
|
||||
alignSelf: 'center',
|
||||
backgroundColor: BaseTheme.palette.uiBackground,
|
||||
borderRadius: BaseTheme.shape.borderRadius,
|
||||
marginTop: BaseTheme.spacing[3],
|
||||
opacity: 0.7,
|
||||
paddingHorizontal: BaseTheme.spacing[3],
|
||||
paddingVertical: BaseTheme.spacing[1],
|
||||
position: 'absolute',
|
||||
maxWidth: 243,
|
||||
maxWidth: 273,
|
||||
zIndex: 1
|
||||
},
|
||||
displayRoomNameBackdrop: {
|
||||
backgroundColor: BaseTheme.palette.uiBackground,
|
||||
borderRadius: BaseTheme.shape.borderRadius,
|
||||
opacity: 0.7,
|
||||
paddingHorizontal: BaseTheme.spacing[3],
|
||||
paddingVertical: BaseTheme.spacing[1]
|
||||
},
|
||||
recordingWarning: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
lineHeight: 22,
|
||||
marginBottom: BaseTheme.spacing[2],
|
||||
marginTop: BaseTheme.spacing[1],
|
||||
width: 'auto'
|
||||
},
|
||||
recordingWarningText: {
|
||||
color: BaseTheme.palette.text03
|
||||
},
|
||||
unsafeRoomWarningContainer: {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
|
||||
@@ -111,6 +111,11 @@ interface IProps {
|
||||
*/
|
||||
showErrorOnJoin: boolean;
|
||||
|
||||
/**
|
||||
* If the recording warning is visible or not.
|
||||
*/
|
||||
showRecordingWarning: boolean;
|
||||
|
||||
/**
|
||||
* If should show unsafe room warning when joining.
|
||||
*/
|
||||
@@ -219,6 +224,7 @@ const Prejoin = ({
|
||||
showCameraPreview,
|
||||
showDialog,
|
||||
showErrorOnJoin,
|
||||
showRecordingWarning,
|
||||
showUnsafeRoomWarning,
|
||||
unsafeRoomConsent,
|
||||
updateSettings: dispatchUpdateSettings,
|
||||
@@ -390,6 +396,7 @@ const Prejoin = ({
|
||||
return (
|
||||
<PreMeetingScreen
|
||||
showDeviceStatus = { deviceStatusVisible }
|
||||
showRecordingWarning = { showRecordingWarning }
|
||||
showUnsafeRoomWarning = { showUnsafeRoomWarning }
|
||||
title = { t('prejoin.joinMeeting') }
|
||||
videoMuted = { !showCameraPreview }
|
||||
@@ -483,6 +490,7 @@ function mapStateToProps(state: IReduxState) {
|
||||
const { joiningInProgress } = state['features/prejoin'];
|
||||
const { room } = state['features/base/conference'];
|
||||
const { unsafeRoomConsent } = state['features/base/premeeting'];
|
||||
const { showPrejoinWarning: showRecordingWarning } = state['features/base/config'].recordings ?? {};
|
||||
|
||||
return {
|
||||
deviceStatusVisible: isDeviceStatusVisible(state),
|
||||
@@ -496,6 +504,7 @@ function mapStateToProps(state: IReduxState) {
|
||||
showCameraPreview: !isVideoMutedByUser(state),
|
||||
showDialog: isJoinByPhoneDialogVisible(state),
|
||||
showErrorOnJoin,
|
||||
showRecordingWarning: Boolean(showRecordingWarning),
|
||||
showUnsafeRoomWarning: isInsecureRoomName(room) && isUnsafeRoomWarningEnabled(state),
|
||||
unsafeRoomConsent,
|
||||
videoTrack: getLocalJitsiVideoTrack(state)
|
||||
|
||||
@@ -83,3 +83,12 @@ export const START_LOCAL_RECORDING = 'START_LOCAL_RECORDING';
|
||||
* }
|
||||
*/
|
||||
export const STOP_LOCAL_RECORDING = 'STOP_LOCAL_RECORDING';
|
||||
|
||||
/**
|
||||
* Indicates that the start recording notification has been shown.
|
||||
*
|
||||
* {
|
||||
* type: SET_START_RECORDING_NOTIFICATION_SHOWN
|
||||
* }
|
||||
*/
|
||||
export const SET_START_RECORDING_NOTIFICATION_SHOWN = 'SET_START_RECORDING_NOTIFICATION_SHOWN';
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../notifications/constants';
|
||||
import { INotificationProps } from '../notifications/types';
|
||||
import { setRequestingSubtitles } from '../subtitles/actions.any';
|
||||
import { isRecorderTranscriptionsRunning } from '../transcribing/functions';
|
||||
|
||||
import {
|
||||
CLEAR_RECORDING_SESSIONS,
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
SET_MEETING_HIGHLIGHT_BUTTON_STATE,
|
||||
SET_PENDING_RECORDING_NOTIFICATION_UID,
|
||||
SET_SELECTED_RECORDING_SERVICE,
|
||||
SET_START_RECORDING_NOTIFICATION_SHOWN,
|
||||
SET_STREAM_KEY,
|
||||
START_LOCAL_RECORDING,
|
||||
STOP_LOCAL_RECORDING
|
||||
@@ -57,6 +59,20 @@ export function clearRecordingSessions() {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Marks the start recording notification as shown.
|
||||
*
|
||||
* @returns {{
|
||||
* type: SET_START_RECORDING_NOTIFICATION_SHOWN
|
||||
* }}
|
||||
*/
|
||||
export function setStartRecordingNotificationShown() {
|
||||
return {
|
||||
type: SET_START_RECORDING_NOTIFICATION_SHOWN
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the meeting highlight button disable state.
|
||||
*
|
||||
@@ -389,19 +405,22 @@ export function stopLocalVideoRecording() {
|
||||
*/
|
||||
export function showStartRecordingNotificationWithCallback(openRecordingDialog: Function) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
let state = getState();
|
||||
const { recordings } = state['features/base/config'];
|
||||
const { suggestRecording } = recordings || {};
|
||||
const recordButtonProps = getRecordButtonProps(state);
|
||||
const isAlreadyRecording = isRecordingRunning(state);
|
||||
const isAlreadyRecording = isRecordingRunning(state) || isRecorderTranscriptionsRunning(state);
|
||||
const wasNotificationShown = state['features/recording'].wasStartRecordingSuggested;
|
||||
|
||||
if (!suggestRecording
|
||||
|| isAlreadyRecording
|
||||
|| !recordButtonProps.visible
|
||||
|| recordButtonProps.disabled) {
|
||||
|| recordButtonProps.disabled
|
||||
|| wasNotificationShown) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(setStartRecordingNotificationShown());
|
||||
dispatch(showNotification({
|
||||
titleKey: 'notify.suggestRecordingTitle',
|
||||
descriptionKey: 'notify.suggestRecordingDescription',
|
||||
@@ -409,9 +428,9 @@ export function showStartRecordingNotificationWithCallback(openRecordingDialog:
|
||||
customActionType: [ BUTTON_TYPES.PRIMARY ],
|
||||
customActionNameKey: [ 'notify.suggestRecordingAction' ],
|
||||
customActionHandler: [ () => {
|
||||
state = getState();
|
||||
const isModerator = isLocalParticipantModerator(state);
|
||||
const { recordingService } = state['features/base/config'];
|
||||
|
||||
const canBypassDialog = isModerator
|
||||
&& recordingService?.enabled
|
||||
&& isJwtFeatureEnabled(state, 'recording', true);
|
||||
@@ -432,7 +451,7 @@ export function showStartRecordingNotificationWithCallback(openRecordingDialog:
|
||||
});
|
||||
|
||||
if (autoTranscribeOnRecord) {
|
||||
dispatch(setRequestingSubtitles(true, false));
|
||||
dispatch(setRequestingSubtitles(true, false, null));
|
||||
}
|
||||
} else {
|
||||
openRecordingDialog();
|
||||
|
||||
@@ -3,7 +3,13 @@ import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { JitsiRecordingConstants } from '../../base/lib-jitsi-meet';
|
||||
import { getSessionStatusToShow } from '../functions';
|
||||
import { isRecorderTranscriptionsRunning } from '../../transcribing/functions';
|
||||
import {
|
||||
getActiveSession,
|
||||
getSessionStatusToShow,
|
||||
isRecordingRunning,
|
||||
isRemoteParticipantRecordingLocally
|
||||
} from '../functions';
|
||||
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
@@ -13,10 +19,16 @@ interface IProps extends WithTranslation {
|
||||
*/
|
||||
_iAmRecorder: boolean;
|
||||
|
||||
|
||||
/**
|
||||
* Whether this meeting is being transcribed.
|
||||
*/
|
||||
_isTranscribing: boolean;
|
||||
*/
|
||||
_isTranscribing: boolean;
|
||||
|
||||
/**
|
||||
* Whether the recording/livestreaming/transcriber is currently running.
|
||||
*/
|
||||
_isVisible: boolean;
|
||||
|
||||
/**
|
||||
* The status of the higher priority session.
|
||||
@@ -34,91 +46,19 @@ interface IProps extends WithTranslation {
|
||||
mode: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* State of the component.
|
||||
*/
|
||||
interface IState {
|
||||
|
||||
/**
|
||||
* True if the label status is stale, so it needs to be removed.
|
||||
*/
|
||||
staleLabel: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The timeout after a label is considered stale. See {@code _updateStaleStatus}
|
||||
* for more details.
|
||||
*/
|
||||
const STALE_TIMEOUT = 10 * 1000;
|
||||
|
||||
/**
|
||||
* Abstract class for the {@code RecordingLabel} component.
|
||||
*/
|
||||
export default class AbstractRecordingLabel extends Component<IProps, IState> {
|
||||
_mounted: boolean;
|
||||
|
||||
/**
|
||||
* Implements {@code Component#getDerivedStateFromProps}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
static getDerivedStateFromProps(props: IProps, prevState: IState) {
|
||||
return {
|
||||
staleLabel: props._status !== JitsiRecordingConstants.status.OFF
|
||||
&& prevState.staleLabel ? false : prevState.staleLabel
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new {@code AbstractRecordingLabel} component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
staleLabel: true
|
||||
};
|
||||
|
||||
this._updateStaleStatus(undefined, props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React {@code Component}'s componentDidMount.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._mounted = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React {@code Component}'s componentWillUnmount.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this._mounted = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Component#componentDidUpdate}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidUpdate(prevProps: IProps) {
|
||||
this._updateStaleStatus(prevProps, this.props);
|
||||
}
|
||||
|
||||
export default class AbstractRecordingLabel extends Component<IProps> {
|
||||
/**
|
||||
* Implements React {@code Component}'s render.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
return (this.props._status || this.props._isTranscribing) && !this.state.staleLabel && !this.props._iAmRecorder
|
||||
? this._renderLabel() : null;
|
||||
const { _iAmRecorder, _isVisible } = this.props;
|
||||
|
||||
return _isVisible && !_iAmRecorder ? this._renderLabel() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,33 +70,6 @@ export default class AbstractRecordingLabel extends Component<IProps, IState> {
|
||||
_renderLabel(): React.ReactNode | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the stale status of the label on a prop change. A label is stale
|
||||
* if it's in a {@code _status} that doesn't need to be rendered anymore.
|
||||
*
|
||||
* @param {IProps} oldProps - The previous props of the component.
|
||||
* @param {IProps} newProps - The new props of the component.
|
||||
* @returns {void}
|
||||
*/
|
||||
_updateStaleStatus(oldProps: IProps | undefined, newProps: IProps) {
|
||||
if (newProps._status === JitsiRecordingConstants.status.OFF) {
|
||||
if (oldProps?._status !== JitsiRecordingConstants.status.OFF) {
|
||||
setTimeout(() => {
|
||||
if (!this._mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only if it's still OFF.
|
||||
if (this.props._status === JitsiRecordingConstants.status.OFF) {
|
||||
this.setState({
|
||||
staleLabel: true
|
||||
});
|
||||
}
|
||||
}, STALE_TIMEOUT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,10 +85,18 @@ export default class AbstractRecordingLabel extends Component<IProps, IState> {
|
||||
*/
|
||||
export function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
const { mode } = ownProps;
|
||||
const isLiveStreamingLabel = mode === JitsiRecordingConstants.mode.STREAM;
|
||||
const _isTranscribing = isRecorderTranscriptionsRunning(state);
|
||||
const isLivestreamingRunning = Boolean(getActiveSession(state, JitsiRecordingConstants.mode.STREAM));
|
||||
const _isVisible = isLiveStreamingLabel
|
||||
? isLivestreamingRunning // this is the livestreaming label
|
||||
: isRecordingRunning(state) || isRemoteParticipantRecordingLocally(state)
|
||||
|| _isTranscribing; // this is the recording label
|
||||
|
||||
return {
|
||||
_isVisible,
|
||||
_iAmRecorder: Boolean(state['features/base/config'].iAmRecorder),
|
||||
_isTranscribing: state['features/transcribing'].isTranscribing,
|
||||
_isTranscribing,
|
||||
_status: getSessionStatusToShow(state, mode)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ import { isLocalParticipantModerator } from '../../../base/participants/function
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
|
||||
import { isInBreakoutRoom } from '../../../breakout-rooms/functions';
|
||||
import { maybeShowPremiumFeatureDialog } from '../../../jaas/actions';
|
||||
import { getActiveSession } from '../../functions';
|
||||
import { isRecorderTranscriptionsRunning } from '../../../transcribing/functions';
|
||||
import { getActiveSession, isCloudRecordingRunning } from '../../functions';
|
||||
|
||||
import { getLiveStreaming } from './functions';
|
||||
|
||||
@@ -140,7 +141,7 @@ export function _mapStateToProps(state: IReduxState, ownProps: IProps) {
|
||||
}
|
||||
|
||||
// disable the button if the recording is running.
|
||||
if (visible && getActiveSession(state, JitsiRecordingConstants.mode.FILE)) {
|
||||
if (visible && (isCloudRecordingRunning(state) || isRecorderTranscriptionsRunning(state))) {
|
||||
_disabled = true;
|
||||
_tooltip = 'dialog.liveStreamingDisabledBecauseOfActiveRecordingTooltip';
|
||||
}
|
||||
|
||||
@@ -4,14 +4,17 @@ import { WithTranslation } from 'react-i18next';
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { openDialog } from '../../../base/dialog/actions';
|
||||
import { MEET_FEATURES } from '../../../base/jwt/constants';
|
||||
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
|
||||
import { maybeShowPremiumFeatureDialog } from '../../../jaas/actions';
|
||||
import { hideNotification, showNotification } from '../../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../../../notifications/constants';
|
||||
import { iAmVisitor } from '../../../visitors/functions';
|
||||
import { highlightMeetingMoment } from '../../actions.any';
|
||||
import { PROMPT_RECORDING_NOTIFICATION_ID } from '../../constants';
|
||||
import { getActiveSession, getRecordButtonProps, isHighlightMeetingMomentDisabled } from '../../functions';
|
||||
import {
|
||||
getRecordButtonProps,
|
||||
isCloudRecordingRunning,
|
||||
isHighlightMeetingMomentDisabled
|
||||
} from '../../functions';
|
||||
|
||||
import { StartRecordingDialog } from './index';
|
||||
|
||||
@@ -103,7 +106,7 @@ export default class AbstractHighlightButton<P extends IProps, S={}> extends Com
|
||||
* }}
|
||||
*/
|
||||
export function _abstractMapStateToProps(state: IReduxState) {
|
||||
const isRecordingRunning = getActiveSession(state, JitsiRecordingConstants.mode.FILE);
|
||||
const isRecordingRunning = isCloudRecordingRunning(state);
|
||||
const isButtonDisabled = isHighlightMeetingMomentDisabled(state);
|
||||
const { webhookProxyUrl } = state['features/base/config'];
|
||||
const _iAmVisitor = iAmVisitor(state);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { MEET_FEATURES } from '../../../base/jwt/constants';
|
||||
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
|
||||
import { maybeShowPremiumFeatureDialog } from '../../../jaas/actions';
|
||||
import { getRecordButtonProps, isRecordingRunning } from '../../functions';
|
||||
import { canStopRecording, getRecordButtonProps } from '../../functions';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
@@ -131,7 +131,7 @@ export function _mapStateToProps(state: IReduxState) {
|
||||
|
||||
return {
|
||||
_disabled,
|
||||
_isRecordingRunning: isRecordingRunning(state),
|
||||
_isRecordingRunning: canStopRecording(state),
|
||||
_tooltip,
|
||||
visible
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ import { showErrorNotification } from '../../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../../../notifications/constants';
|
||||
import { setRequestingSubtitles } from '../../../subtitles/actions.any';
|
||||
import { setSelectedRecordingService, startLocalVideoRecording } from '../../actions';
|
||||
import { RECORDING_TYPES } from '../../constants';
|
||||
import { RECORDING_METADATA_ID, RECORDING_TYPES } from '../../constants';
|
||||
import { isRecordingSharingEnabled, shouldAutoTranscribeOnRecord, supportsLocalRecording } from '../../functions';
|
||||
|
||||
export interface IProps extends WithTranslation {
|
||||
@@ -32,6 +32,11 @@ export interface IProps extends WithTranslation {
|
||||
*/
|
||||
_conference?: IJitsiConference;
|
||||
|
||||
/**
|
||||
* Whether subtitles should be displayed or not.
|
||||
*/
|
||||
_displaySubtitles?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to show file recordings service, even if integrations
|
||||
* are enabled.
|
||||
@@ -59,6 +64,11 @@ export interface IProps extends WithTranslation {
|
||||
*/
|
||||
_rToken: string;
|
||||
|
||||
/**
|
||||
* Whether the record audio / video option is enabled by default.
|
||||
*/
|
||||
_recordAudioAndVideo: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the local participant is screensharing.
|
||||
*/
|
||||
@@ -69,6 +79,11 @@ export interface IProps extends WithTranslation {
|
||||
*/
|
||||
_screenshotCaptureEnabled: boolean;
|
||||
|
||||
/**
|
||||
* The selected language for subtitles.
|
||||
*/
|
||||
_subtitlesLanguage: string | null;
|
||||
|
||||
/**
|
||||
* The dropbox access token.
|
||||
*/
|
||||
@@ -177,8 +192,8 @@ class AbstractStartRecordingDialog extends Component<IProps, IState> {
|
||||
isValidating: false,
|
||||
userName: undefined,
|
||||
sharingEnabled: true,
|
||||
shouldRecordAudioAndVideo: true,
|
||||
shouldRecordTranscription: true,
|
||||
shouldRecordAudioAndVideo: this.props._recordAudioAndVideo,
|
||||
shouldRecordTranscription: this.props._autoTranscribeOnRecord,
|
||||
spaceLeft: undefined,
|
||||
selectedRecordingService,
|
||||
localRecordingOnlySelf: false
|
||||
@@ -335,10 +350,11 @@ class AbstractStartRecordingDialog extends Component<IProps, IState> {
|
||||
_onSubmit() {
|
||||
const {
|
||||
_appKey,
|
||||
_autoTranscribeOnRecord,
|
||||
_conference,
|
||||
_displaySubtitles,
|
||||
_isDropboxEnabled,
|
||||
_rToken,
|
||||
_subtitlesLanguage,
|
||||
_token,
|
||||
dispatch
|
||||
} = this.props;
|
||||
@@ -398,10 +414,15 @@ class AbstractStartRecordingDialog extends Component<IProps, IState> {
|
||||
});
|
||||
}
|
||||
|
||||
if (_autoTranscribeOnRecord || this.state.shouldRecordTranscription) {
|
||||
dispatch(setRequestingSubtitles(true, false));
|
||||
if (this.state.selectedRecordingService === RECORDING_TYPES.JITSI_REC_SERVICE
|
||||
&& this.state.shouldRecordTranscription) {
|
||||
dispatch(setRequestingSubtitles(true, _displaySubtitles, _subtitlesLanguage));
|
||||
}
|
||||
|
||||
_conference?.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
|
||||
isTranscribingEnabled: this.state.shouldRecordTranscription
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -430,34 +451,32 @@ class AbstractStartRecordingDialog extends Component<IProps, IState> {
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {any} _ownProps - Component's own props.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _appKey: string,
|
||||
* _autoTranscribeOnRecord: boolean,
|
||||
* _conference: JitsiConference,
|
||||
* _fileRecordingsServiceEnabled: boolean,
|
||||
* _fileRecordingsServiceSharingEnabled: boolean,
|
||||
* _isDropboxEnabled: boolean,
|
||||
* _rToken:string,
|
||||
* _tokenExpireDate: number,
|
||||
* _token: string
|
||||
* }}
|
||||
* @returns {IProps}
|
||||
*/
|
||||
export function mapStateToProps(state: IReduxState, _ownProps: any) {
|
||||
const {
|
||||
recordingService,
|
||||
dropbox = { appKey: undefined },
|
||||
localRecording
|
||||
localRecording,
|
||||
recordings = { recordAudioAndVideo: true }
|
||||
} = state['features/base/config'];
|
||||
const {
|
||||
_displaySubtitles,
|
||||
_language: _subtitlesLanguage
|
||||
} = state['features/subtitles'];
|
||||
|
||||
return {
|
||||
_appKey: dropbox.appKey ?? '',
|
||||
_autoTranscribeOnRecord: shouldAutoTranscribeOnRecord(state),
|
||||
_conference: state['features/base/conference'].conference,
|
||||
_displaySubtitles,
|
||||
_fileRecordingsServiceEnabled: recordingService?.enabled ?? false,
|
||||
_fileRecordingsServiceSharingEnabled: isRecordingSharingEnabled(state),
|
||||
_isDropboxEnabled: isDropboxEnabled(state),
|
||||
_localRecordingEnabled: !localRecording?.disable,
|
||||
_rToken: state['features/dropbox'].rToken ?? '',
|
||||
_recordAudioAndVideo: recordings?.recordAudioAndVideo ?? true,
|
||||
_subtitlesLanguage,
|
||||
_tokenExpireDate: state['features/dropbox'].expireDate,
|
||||
_token: state['features/dropbox'].token ?? ''
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ import { _abstractMapStateToProps } from '../../../base/dialog/functions';
|
||||
import { isLocalParticipantModerator } from '../../../base/participants/functions';
|
||||
import { authorizeDropbox, updateDropboxToken } from '../../../dropbox/actions';
|
||||
import { isVpaasMeeting } from '../../../jaas/functions';
|
||||
import { canStartTranscribing } from '../../../subtitles/functions';
|
||||
import { canAddTranscriber } from '../../../transcribing/functions';
|
||||
import { RECORDING_TYPES } from '../../constants';
|
||||
import { supportsLocalRecording } from '../../functions';
|
||||
|
||||
@@ -418,7 +418,7 @@ export function mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
..._abstractMapStateToProps(state),
|
||||
isVpaas: isVpaasMeeting(state),
|
||||
_canStartTranscribing: canStartTranscribing(state),
|
||||
_canStartTranscribing: canAddTranscriber(state),
|
||||
_hideStorageWarning: Boolean(recordingService?.hideStorageWarning),
|
||||
_isModerator: isLocalParticipantModerator(state),
|
||||
_localRecordingAvailable,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
|
||||
import { setVideoMuted } from '../../../base/media/actions';
|
||||
import { setRequestingSubtitles } from '../../../subtitles/actions.any';
|
||||
import { stopLocalVideoRecording } from '../../actions';
|
||||
import { RECORDING_METADATA_ID } from '../../constants';
|
||||
import { getActiveSession } from '../../functions';
|
||||
import { ISessionData } from '../../reducer';
|
||||
|
||||
@@ -25,6 +26,11 @@ export interface IProps extends WithTranslation {
|
||||
*/
|
||||
_conference?: IJitsiConference;
|
||||
|
||||
/**
|
||||
* Whether subtitles should be displayed or not.
|
||||
*/
|
||||
_displaySubtitles?: boolean;
|
||||
|
||||
/**
|
||||
* The redux representation of the recording session to be stopped.
|
||||
*/
|
||||
@@ -35,6 +41,11 @@ export interface IProps extends WithTranslation {
|
||||
*/
|
||||
_localRecording: boolean;
|
||||
|
||||
/**
|
||||
* The selected language for subtitles.
|
||||
*/
|
||||
_subtitlesLanguage: string | null;
|
||||
|
||||
/**
|
||||
* The redux dispatch function.
|
||||
*/
|
||||
@@ -76,22 +87,32 @@ export default class AbstractStopRecordingDialog<P extends IProps>
|
||||
_onSubmit() {
|
||||
sendAnalytics(createRecordingDialogEvent('stop', 'confirm.button'));
|
||||
|
||||
if (this.props._localRecording) {
|
||||
this.props.dispatch(stopLocalVideoRecording());
|
||||
if (this.props.localRecordingVideoStop) {
|
||||
this.props.dispatch(setVideoMuted(true));
|
||||
}
|
||||
} else {
|
||||
const { _fileRecordingSession } = this.props;
|
||||
const {
|
||||
_conference,
|
||||
_displaySubtitles,
|
||||
_fileRecordingSession,
|
||||
_localRecording,
|
||||
_subtitlesLanguage,
|
||||
dispatch,
|
||||
localRecordingVideoStop
|
||||
} = this.props;
|
||||
|
||||
if (_fileRecordingSession) {
|
||||
this.props._conference?.stopRecording(_fileRecordingSession.id);
|
||||
this._toggleScreenshotCapture();
|
||||
if (_localRecording) {
|
||||
dispatch(stopLocalVideoRecording());
|
||||
if (localRecordingVideoStop) {
|
||||
dispatch(setVideoMuted(true));
|
||||
}
|
||||
} else if (_fileRecordingSession) {
|
||||
_conference?.stopRecording(_fileRecordingSession.id);
|
||||
this._toggleScreenshotCapture();
|
||||
}
|
||||
|
||||
// TODO: this should be an action in transcribing. -saghul
|
||||
this.props.dispatch(setRequestingSubtitles(false, false, null));
|
||||
this.props.dispatch(setRequestingSubtitles(Boolean(_displaySubtitles), _displaySubtitles, _subtitlesLanguage));
|
||||
|
||||
this.props._conference?.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
|
||||
isTranscribingEnabled: false
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -112,16 +133,20 @@ export default class AbstractStopRecordingDialog<P extends IProps>
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _conference: JitsiConference,
|
||||
* _fileRecordingSession: Object
|
||||
* }}
|
||||
* @returns {IProps}
|
||||
*/
|
||||
export function _mapStateToProps(state: IReduxState) {
|
||||
const {
|
||||
_displaySubtitles,
|
||||
_language: _subtitlesLanguage
|
||||
} = state['features/subtitles'];
|
||||
|
||||
return {
|
||||
_conference: state['features/base/conference'].conference,
|
||||
_displaySubtitles,
|
||||
_fileRecordingSession:
|
||||
getActiveSession(state, JitsiRecordingConstants.mode.FILE),
|
||||
_localRecording: LocalRecordingManager.isRecordingLocally()
|
||||
_localRecording: LocalRecordingManager.isRecordingLocally(),
|
||||
_subtitlesLanguage
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,9 +36,6 @@ interface ILocalRecordingManager {
|
||||
|
||||
const getMimeType = (): string => {
|
||||
const possibleTypes = [
|
||||
'video/mp4;codecs=h264',
|
||||
'video/webm;codecs=h264',
|
||||
'video/webm;codecs=vp9',
|
||||
'video/webm;codecs=vp8'
|
||||
];
|
||||
|
||||
@@ -164,7 +161,11 @@ const LocalRecordingManager: ILocalRecordingManager = {
|
||||
this.audioContext = undefined;
|
||||
this.audioDestination = undefined;
|
||||
this.totalSize = MAX_SIZE;
|
||||
setTimeout(() => this.saveRecording(this.recordingData, this.getFilename()), 1000);
|
||||
setTimeout(() => {
|
||||
if (this.recordingData.length > 0) {
|
||||
this.saveRecording(this.recordingData, this.getFilename());
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -51,7 +51,9 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent {
|
||||
* @returns {React$Component}
|
||||
*/
|
||||
_renderAdvancedOptions() {
|
||||
if (!this._canStartTranscribing()) {
|
||||
const { selectedRecordingService } = this.props;
|
||||
|
||||
if (selectedRecordingService !== RECORDING_TYPES.JITSI_REC_SERVICE || !this._canStartTranscribing()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,9 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent {
|
||||
* @returns {React$Component}
|
||||
*/
|
||||
_renderAdvancedOptions() {
|
||||
if (!this._canStartTranscribing()) {
|
||||
const { selectedRecordingService } = this.props;
|
||||
|
||||
if (selectedRecordingService !== RECORDING_TYPES.JITSI_REC_SERVICE || !this._canStartTranscribing()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { IReduxState } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import ExpandedLabel, { IProps as AbstractProps } from '../../../base/label/components/native/ExpandedLabel';
|
||||
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
|
||||
import { isRecorderTranscriptionsRunning } from '../../../transcribing/functions';
|
||||
import { getSessionStatusToShow } from '../../functions';
|
||||
|
||||
interface IProps extends AbstractProps {
|
||||
@@ -90,7 +91,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
const { mode } = ownProps;
|
||||
|
||||
return {
|
||||
_isTranscribing: state['features/transcribing'].isTranscribing,
|
||||
_isTranscribing: isRecorderTranscriptionsRunning(state),
|
||||
_status: getSessionStatusToShow(state, mode)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -40,11 +40,11 @@ class RecordingLabel extends AbstractRecordingLabel {
|
||||
_renderLabel() {
|
||||
const { _isTranscribing, _status, classes, mode, t } = this.props;
|
||||
const isRecording = mode === JitsiRecordingConstants.mode.FILE;
|
||||
const icon = isRecording || _isTranscribing ? IconRecord : IconSites;
|
||||
const icon = isRecording ? IconRecord : IconSites;
|
||||
let content;
|
||||
|
||||
if (_status === JitsiRecordingConstants.status.ON) {
|
||||
content = t(isRecording || _isTranscribing ? 'videoStatus.recording' : 'videoStatus.streaming');
|
||||
content = t(isRecording ? 'videoStatus.recording' : 'videoStatus.streaming');
|
||||
|
||||
if (_isTranscribing) {
|
||||
content += ` \u00B7 ${t('transcribing.labelToolTip')}`;
|
||||
|
||||
@@ -60,3 +60,5 @@ export const RECORDING_STATUS_PRIORITIES = [
|
||||
];
|
||||
|
||||
export const START_RECORDING_NOTIFICATION_ID = 'START_RECORDING_NOTIFICATION_ID';
|
||||
|
||||
export const RECORDING_METADATA_ID = 'recording';
|
||||
|
||||
@@ -14,6 +14,7 @@ import { registerSound, unregisterSound } from '../base/sounds/actions';
|
||||
import { isInBreakoutRoom } from '../breakout-rooms/functions';
|
||||
import { isEnabled as isDropboxEnabled } from '../dropbox/functions';
|
||||
import { extractFqnFromPath } from '../dynamic-branding/functions.any';
|
||||
import { canAddTranscriber, isRecorderTranscriptionsRunning } from '../transcribing/functions';
|
||||
|
||||
import LocalRecordingManager from './components/Recording/LocalRecordingManager';
|
||||
import {
|
||||
@@ -139,9 +140,8 @@ export function getSessionStatusToShow(state: IReduxState, mode: string): string
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((!Array.isArray(recordingSessions) || recordingSessions.length === 0)
|
||||
&& mode === JitsiRecordingConstants.mode.FILE
|
||||
&& (LocalRecordingManager.isRecordingLocally() || isRemoteParticipantRecordingLocally(state))) {
|
||||
if (!status && mode === JitsiRecordingConstants.mode.FILE
|
||||
&& (LocalRecordingManager.isRecordingLocally() || isRemoteParticipantRecordingLocally(state))) {
|
||||
status = JitsiRecordingConstants.status.ON;
|
||||
}
|
||||
|
||||
@@ -158,6 +158,16 @@ export function supportsLocalRecording() {
|
||||
&& navigator.product !== 'ReactNative';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there is a cloud recording running.
|
||||
*
|
||||
* @param {IReduxState} state - The redux state to search in.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isCloudRecordingRunning(state: IReduxState) {
|
||||
return Boolean(getActiveSession(state, JitsiRecordingConstants.mode.FILE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there is a recording session running.
|
||||
*
|
||||
@@ -165,15 +175,30 @@ export function supportsLocalRecording() {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isRecordingRunning(state: IReduxState) {
|
||||
const { isTranscribing } = state['features/transcribing'];
|
||||
|
||||
return (
|
||||
Boolean(getActiveSession(state, JitsiRecordingConstants.mode.FILE))
|
||||
isCloudRecordingRunning(state)
|
||||
|| LocalRecordingManager.isRecordingLocally()
|
||||
|| isTranscribing
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the participant can stop recording.
|
||||
*
|
||||
* @param {Object} state - The redux state to search in.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function canStopRecording(state: IReduxState) {
|
||||
if (LocalRecordingManager.isRecordingLocally()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isCloudRecordingRunning(state) || isRecorderTranscriptionsRunning(state)) {
|
||||
return isLocalParticipantModerator(state) && isJwtFeatureEnabled(state, 'recording', true);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the transcription should start automatically when recording starts.
|
||||
*
|
||||
@@ -183,7 +208,7 @@ export function isRecordingRunning(state: IReduxState) {
|
||||
export function shouldAutoTranscribeOnRecord(state: IReduxState) {
|
||||
const { transcription } = state['features/base/config'];
|
||||
|
||||
return transcription?.autoTranscribeOnRecord ?? true;
|
||||
return (transcription?.autoTranscribeOnRecord ?? true) && canAddTranscriber(state);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -323,7 +348,7 @@ export async function sendMeetingHighlight(state: IReduxState) {
|
||||
* @param {Object} state - Redux state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isRemoteParticipantRecordingLocally(state: IReduxState) {
|
||||
export function isRemoteParticipantRecordingLocally(state: IReduxState) {
|
||||
const participants = getRemoteParticipants(state);
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
|
||||
@@ -9,8 +9,10 @@ import JitsiMeetJS, {
|
||||
JitsiRecordingConstants
|
||||
} from '../base/lib-jitsi-meet';
|
||||
import { MEDIA_TYPE } from '../base/media/constants';
|
||||
import { PARTICIPANT_UPDATED } from '../base/participants/actionTypes';
|
||||
import { updateLocalRecordingStatus } from '../base/participants/actions';
|
||||
import { getParticipantDisplayName } from '../base/participants/functions';
|
||||
import { PARTICIPANT_ROLE } from '../base/participants/constants';
|
||||
import { getLocalParticipant, getParticipantDisplayName } from '../base/participants/functions';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
import {
|
||||
@@ -20,6 +22,7 @@ import {
|
||||
import { TRACK_ADDED } from '../base/tracks/actionTypes';
|
||||
import { hideNotification, showErrorNotification, showNotification } from '../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
|
||||
import { isRecorderTranscriptionsRunning } from '../transcribing/functions';
|
||||
|
||||
import { RECORDING_SESSION_UPDATED, START_LOCAL_RECORDING, STOP_LOCAL_RECORDING } from './actionTypes';
|
||||
import {
|
||||
@@ -29,6 +32,7 @@ import {
|
||||
showRecordingError,
|
||||
showRecordingLimitNotification,
|
||||
showRecordingWarning,
|
||||
showStartRecordingNotification,
|
||||
showStartedRecordingNotification,
|
||||
showStoppedRecordingNotification,
|
||||
updateRecordingSessionData
|
||||
@@ -175,6 +179,8 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
|
||||
}
|
||||
|
||||
case RECORDING_SESSION_UPDATED: {
|
||||
const state = getState();
|
||||
|
||||
// When in recorder mode no notifications are shown
|
||||
// or extra sounds are also not desired
|
||||
// but we want to indicate those in case of sip gateway
|
||||
@@ -182,22 +188,21 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
|
||||
iAmRecorder,
|
||||
iAmSipGateway,
|
||||
recordingLimit
|
||||
} = getState()['features/base/config'];
|
||||
} = state['features/base/config'];
|
||||
|
||||
if (iAmRecorder && !iAmSipGateway) {
|
||||
break;
|
||||
}
|
||||
|
||||
const updatedSessionData
|
||||
= getSessionById(getState(), action.sessionData.id);
|
||||
= getSessionById(state, action.sessionData.id);
|
||||
const { initiator, mode = '', terminator } = updatedSessionData ?? {};
|
||||
const { PENDING, OFF, ON } = JitsiRecordingConstants.status;
|
||||
|
||||
if (updatedSessionData?.status === PENDING
|
||||
&& (!oldSessionData || oldSessionData.status !== PENDING)) {
|
||||
if (updatedSessionData?.status === PENDING && oldSessionData?.status !== PENDING) {
|
||||
dispatch(showPendingRecordingNotification(mode));
|
||||
dispatch(hideNotification(START_RECORDING_NOTIFICATION_ID));
|
||||
} else if (updatedSessionData?.status !== PENDING) {
|
||||
} else {
|
||||
dispatch(hidePendingRecordingNotification(mode));
|
||||
|
||||
if (updatedSessionData?.status === ON) {
|
||||
@@ -215,12 +220,13 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
|
||||
dispatch(showStartedRecordingNotification(mode, initiator, action.sessionData.id));
|
||||
}
|
||||
}
|
||||
if (!oldSessionData || oldSessionData.status !== ON) {
|
||||
|
||||
if (oldSessionData?.status !== ON) {
|
||||
sendAnalytics(createRecordingEvent('start', mode));
|
||||
|
||||
let soundID;
|
||||
|
||||
if (mode === JitsiRecordingConstants.mode.FILE) {
|
||||
if (mode === JitsiRecordingConstants.mode.FILE && !isRecorderTranscriptionsRunning(state)) {
|
||||
soundID = RECORDING_ON_SOUND_ID;
|
||||
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
|
||||
soundID = LIVE_STREAMING_ON_SOUND_ID;
|
||||
@@ -234,12 +240,11 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
|
||||
APP.API.notifyRecordingStatusChanged(true, mode);
|
||||
}
|
||||
}
|
||||
} else if (updatedSessionData?.status === OFF
|
||||
&& (!oldSessionData || oldSessionData.status !== OFF)) {
|
||||
} else if (updatedSessionData?.status === OFF && oldSessionData?.status !== OFF) {
|
||||
if (terminator) {
|
||||
dispatch(
|
||||
showStoppedRecordingNotification(
|
||||
mode, getParticipantDisplayName(getState, getResourceId(terminator))));
|
||||
mode, getParticipantDisplayName(state, getResourceId(terminator))));
|
||||
}
|
||||
|
||||
let duration = 0, soundOff, soundOn;
|
||||
@@ -250,7 +255,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
|
||||
}
|
||||
sendAnalytics(createRecordingEvent('stop', mode, duration));
|
||||
|
||||
if (mode === JitsiRecordingConstants.mode.FILE) {
|
||||
if (mode === JitsiRecordingConstants.mode.FILE && !isRecorderTranscriptionsRunning(state)) {
|
||||
soundOff = RECORDING_OFF_SOUND_ID;
|
||||
soundOn = RECORDING_ON_SOUND_ID;
|
||||
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
|
||||
@@ -281,6 +286,21 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PARTICIPANT_UPDATED: {
|
||||
const { id, role } = action.participant;
|
||||
const state = getState();
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
|
||||
if (localParticipant?.id !== id) {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
if (role === PARTICIPANT_ROLE.MODERATOR) {
|
||||
dispatch(showStartRecordingNotification());
|
||||
}
|
||||
|
||||
return next(action);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
SET_MEETING_HIGHLIGHT_BUTTON_STATE,
|
||||
SET_PENDING_RECORDING_NOTIFICATION_UID,
|
||||
SET_SELECTED_RECORDING_SERVICE,
|
||||
SET_START_RECORDING_NOTIFICATION_SHOWN,
|
||||
SET_STREAM_KEY
|
||||
} from './actionTypes';
|
||||
|
||||
@@ -35,6 +36,7 @@ export interface IRecordingState {
|
||||
selectedRecordingService: string;
|
||||
sessionDatas: Array<ISessionData>;
|
||||
streamKey?: string;
|
||||
wasStartRecordingSuggested?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,6 +96,12 @@ ReducerRegistry.register<IRecordingState>(STORE_NAME,
|
||||
disableHighlightMeetingMoment: action.disabled
|
||||
};
|
||||
|
||||
case SET_START_RECORDING_NOTIFICATION_SHOWN:
|
||||
return {
|
||||
...state,
|
||||
wasStartRecordingSuggested: true
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { IReduxState } from '../app/types';
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
|
||||
import { toState } from '../base/redux/functions';
|
||||
import { getActiveSession } from '../recording/functions';
|
||||
import { isCloudRecordingRunning } from '../recording/functions';
|
||||
import { isScreenVideoShared } from '../screen-share/functions';
|
||||
|
||||
import ScreenshotCaptureSummary from './ScreenshotCaptureSummary';
|
||||
@@ -48,7 +47,7 @@ export function isScreenshotCaptureEnabled(state: IReduxState, checkSharing?: bo
|
||||
}
|
||||
|
||||
// Feature enabled only when recording is also on.
|
||||
return Boolean(getActiveSession(state, JitsiRecordingConstants.mode.FILE));
|
||||
return isCloudRecordingRunning(state);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -1,16 +1,3 @@
|
||||
|
||||
/**
|
||||
* The type of (redux) action which indicates that an endpoint message
|
||||
* sent by another participant to the data channel is received.
|
||||
*
|
||||
* {
|
||||
* type: ENDPOINT_MESSAGE_RECEIVED,
|
||||
* participant: Object,
|
||||
* json: Object
|
||||
* }
|
||||
*/
|
||||
export const ENDPOINT_MESSAGE_RECEIVED = 'ENDPOINT_MESSAGE_RECEIVED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which indicates that an existing transcript
|
||||
* has to be removed from the state.
|
||||
|
||||
@@ -1,32 +1,12 @@
|
||||
import { DEFAULT_LANGUAGE } from '../base/i18n/i18next';
|
||||
|
||||
import {
|
||||
ENDPOINT_MESSAGE_RECEIVED,
|
||||
REMOVE_TRANSCRIPT_MESSAGE,
|
||||
SET_REQUESTING_SUBTITLES,
|
||||
TOGGLE_REQUESTING_SUBTITLES,
|
||||
UPDATE_TRANSCRIPT_MESSAGE
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Signals that a participant sent an endpoint message on the data channel.
|
||||
*
|
||||
* @param {Object} participant - The participant details sending the message.
|
||||
* @param {Object} json - The json carried by the endpoint message.
|
||||
* @returns {{
|
||||
* type: ENDPOINT_MESSAGE_RECEIVED,
|
||||
* participant: Object,
|
||||
* json: Object
|
||||
* }}
|
||||
*/
|
||||
export function endpointMessageReceived(participant: Object, json: Object) {
|
||||
return {
|
||||
type: ENDPOINT_MESSAGE_RECEIVED,
|
||||
participant,
|
||||
json
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that a transcript has to be removed from the state.
|
||||
*
|
||||
|
||||
@@ -4,7 +4,7 @@ import { IReduxState } from '../../app/types';
|
||||
import { MEET_FEATURES } from '../../base/jwt/constants';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../base/toolbox/components/AbstractButton';
|
||||
import { maybeShowPremiumFeatureDialog } from '../../jaas/actions';
|
||||
import { canStartTranscribing } from '../functions';
|
||||
import { canStartSubtitles } from '../functions';
|
||||
|
||||
export interface IAbstractProps extends AbstractButtonProps {
|
||||
|
||||
@@ -106,7 +106,7 @@ export function _abstractMapStateToProps(state: IReduxState, ownProps: IAbstract
|
||||
|
||||
// if the participant is moderator, it can enable transcriptions and if
|
||||
// transcriptions are already started for the meeting, guests can just show them
|
||||
const { visible = canStartTranscribing(state) } = ownProps;
|
||||
const { visible = canStartSubtitles(state) } = ownProps;
|
||||
|
||||
return {
|
||||
_requestingSubtitles,
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { IReduxState } from '../app/types';
|
||||
import { isLocalParticipantModerator } from '../base/participants/functions';
|
||||
import { canAddTranscriber, isTranscribing } from '../transcribing/functions';
|
||||
|
||||
/**
|
||||
* Checks whether the participant can start the transcription.
|
||||
* Checks whether the participant can start the subtitles.
|
||||
*
|
||||
* @param {IReduxState} state - The redux state.
|
||||
* @returns {boolean} - True if the participant can start the transcription.
|
||||
* @returns {boolean} - True if the participant can start the subtitles.
|
||||
*/
|
||||
export function canStartTranscribing(state: IReduxState) {
|
||||
const { transcription } = state['features/base/config'];
|
||||
const { isTranscribing } = state['features/transcribing'];
|
||||
|
||||
return Boolean(transcription?.enabled && (isLocalParticipantModerator(state) || isTranscribing));
|
||||
export function canStartSubtitles(state: IReduxState) {
|
||||
return canAddTranscriber(state) || isTranscribing(state);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { IStore } from '../app/types';
|
||||
import { ENDPOINT_MESSAGE_RECEIVED } from '../base/conference/actionTypes';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
|
||||
import {
|
||||
ENDPOINT_MESSAGE_RECEIVED,
|
||||
SET_REQUESTING_SUBTITLES,
|
||||
TOGGLE_REQUESTING_SUBTITLES
|
||||
} from './actionTypes';
|
||||
@@ -90,7 +90,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _endpointMessageReceived({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
|
||||
const { json } = action;
|
||||
const { data: json } = action;
|
||||
|
||||
if (![ JSON_TYPE_TRANSCRIPTION_RESULT, JSON_TYPE_TRANSLATION_RESULT ].includes(json?.type)) {
|
||||
return next(action);
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
* Default State for 'features/transcription' feature.
|
||||
*/
|
||||
const defaultState = {
|
||||
_displaySubtitles: true,
|
||||
_displaySubtitles: false,
|
||||
_transcriptMessages: new Map(),
|
||||
_requestingSubtitles: false,
|
||||
_language: null
|
||||
|
||||
@@ -23,6 +23,7 @@ import { isSpeakerStatsDisabled } from '../../../speaker-stats/functions';
|
||||
import ClosedCaptionButton from '../../../subtitles/components/native/ClosedCaptionButton';
|
||||
import TileViewButton from '../../../video-layout/components/TileViewButton';
|
||||
import styles from '../../../video-menu/components/native/styles';
|
||||
import WhiteboardButton from '../../../whiteboard/components/native/WhiteboardButton';
|
||||
import { getMovableButtons } from '../../functions.native';
|
||||
|
||||
import AudioOnlyButton from './AudioOnlyButton';
|
||||
@@ -156,6 +157,7 @@ class OverflowMenu extends PureComponent<IProps, IState> {
|
||||
<RecordButton { ...buttonProps } />
|
||||
<LiveStreamButton { ...buttonProps } />
|
||||
<LinkToSalesforceButton { ...buttonProps } />
|
||||
<WhiteboardButton { ...buttonProps } />
|
||||
{/* @ts-ignore */}
|
||||
<Divider style = { styles.divider as ViewStyle } />
|
||||
<SharedVideoButton { ...buttonProps } />
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import i18next from 'i18next';
|
||||
|
||||
import { IReduxState } from '../app/types';
|
||||
import { IConfig } from '../base/config/configType';
|
||||
import { isJwtFeatureEnabled } from '../base/jwt/functions';
|
||||
import { isLocalParticipantModerator } from '../base/participants/functions';
|
||||
|
||||
import JITSI_TO_BCP47_MAP from './jitsi-bcp47-map.json';
|
||||
import logger from './logger';
|
||||
@@ -42,3 +45,47 @@ export function determineTranscriptionLanguage(config: IConfig) {
|
||||
|
||||
return safeBCP47Locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether there is transcribing.
|
||||
*
|
||||
* @param {IReduxState} state - The redux state to search in.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isTranscribing(state: IReduxState) {
|
||||
return state['features/transcribing'].isTranscribing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there is a recorder transcription session running.
|
||||
* NOTE: If only the subtitles are running this function will return false.
|
||||
*
|
||||
* @param {Object} state - The redux state to search in.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isRecorderTranscriptionsRunning(state: IReduxState) {
|
||||
const { metadata } = state['features/base/conference'];
|
||||
|
||||
return isTranscribing(state) && Boolean(metadata?.recording?.isTranscribingEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the participant can start the transcription.
|
||||
*
|
||||
* @param {IReduxState} state - The redux state.
|
||||
* @returns {boolean} - True if the participant can start the transcription.
|
||||
*/
|
||||
export function canAddTranscriber(state: IReduxState) {
|
||||
const { transcription } = state['features/base/config'];
|
||||
const isJwtTranscribingEnabled = isJwtFeatureEnabled(state, 'transcription', isLocalParticipantModerator(state));
|
||||
|
||||
if (!transcription?.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isJwtTranscribingEnabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,30 +1,16 @@
|
||||
import { batch } from 'react-redux';
|
||||
|
||||
import { IStore } from '../app/types';
|
||||
import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
|
||||
import {
|
||||
HIDDEN_PARTICIPANT_JOINED,
|
||||
HIDDEN_PARTICIPANT_LEFT,
|
||||
PARTICIPANT_UPDATED
|
||||
} from '../base/participants/actionTypes';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import { playSound } from '../base/sounds/actions';
|
||||
import { showNotification } from '../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
|
||||
import {
|
||||
RECORDING_OFF_SOUND_ID,
|
||||
RECORDING_ON_SOUND_ID
|
||||
} from '../recording/constants';
|
||||
|
||||
import {
|
||||
_TRANSCRIBER_JOINED,
|
||||
_TRANSCRIBER_LEFT
|
||||
} from './actionTypes';
|
||||
import {
|
||||
potentialTranscriberJoined,
|
||||
transcriberJoined,
|
||||
transcriberLeft
|
||||
} from './actions';
|
||||
import './subscriber';
|
||||
|
||||
const TRANSCRIBER_DISPLAY_NAME = 'Transcriber';
|
||||
|
||||
@@ -42,16 +28,6 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
} = getState()['features/transcribing'];
|
||||
|
||||
switch (action.type) {
|
||||
case _TRANSCRIBER_JOINED:
|
||||
notifyTranscribingStatusChanged(true);
|
||||
maybeEmitRecordingNotification(dispatch, getState, true);
|
||||
|
||||
break;
|
||||
case _TRANSCRIBER_LEFT:
|
||||
notifyTranscribingStatusChanged(false);
|
||||
maybeEmitRecordingNotification(dispatch, getState, false);
|
||||
|
||||
break;
|
||||
case HIDDEN_PARTICIPANT_JOINED:
|
||||
if (action.displayName === TRANSCRIBER_DISPLAY_NAME) {
|
||||
dispatch(transcriberJoined(action.id));
|
||||
@@ -79,44 +55,3 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Emit a recording started / stopped notification if the transcription started / stopped. Only
|
||||
* if there is no recording in progress.
|
||||
*
|
||||
* @param {Dispatch} dispatch - The Redux dispatch function.
|
||||
* @param {Function} getState - The Redux state.
|
||||
* @param {boolean} on - Whether the transcription is on or not.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function maybeEmitRecordingNotification(dispatch: IStore['dispatch'], getState: IStore['getState'], on: boolean) {
|
||||
const state = getState();
|
||||
const { sessionDatas } = state['features/recording'];
|
||||
const { status: statusConstants } = JitsiRecordingConstants;
|
||||
|
||||
if (sessionDatas.some(sd => sd.mode !== statusConstants.OFF)) {
|
||||
// If a recording is still ongoing, don't send any notification.
|
||||
return;
|
||||
}
|
||||
|
||||
batch(() => {
|
||||
dispatch(showNotification({
|
||||
descriptionKey: on ? 'recording.on' : 'recording.off',
|
||||
titleKey: 'dialog.recording'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
|
||||
dispatch(playSound(on ? RECORDING_ON_SOUND_ID : RECORDING_OFF_SOUND_ID));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application (if API is enabled) that transcribing has started or stopped.
|
||||
*
|
||||
* @param {boolean} on - True if transcribing is on, false otherwise.
|
||||
* @returns {void}
|
||||
*/
|
||||
function notifyTranscribingStatusChanged(on: boolean) {
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyTranscribingStatusChanged(on);
|
||||
}
|
||||
}
|
||||
|
||||
68
react/features/transcribing/subscriber.ts
Normal file
68
react/features/transcribing/subscriber.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { batch } from 'react-redux';
|
||||
|
||||
import { IStore } from '../app/types';
|
||||
import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
|
||||
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
import { playSound } from '../base/sounds/actions';
|
||||
import { showNotification } from '../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
|
||||
import { RECORDING_OFF_SOUND_ID, RECORDING_ON_SOUND_ID } from '../recording/constants';
|
||||
|
||||
import { isRecorderTranscriptionsRunning } from './functions';
|
||||
|
||||
/**
|
||||
* Listens for large video participant ID changes.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ isRecorderTranscriptionsRunning,
|
||||
/* listener */ (isRecorderTranscriptionsRunningValue, { getState, dispatch }) => {
|
||||
if (isRecorderTranscriptionsRunningValue) {
|
||||
notifyTranscribingStatusChanged(true);
|
||||
maybeEmitRecordingNotification(dispatch, getState, true);
|
||||
} else {
|
||||
notifyTranscribingStatusChanged(false);
|
||||
maybeEmitRecordingNotification(dispatch, getState, false);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Emit a recording started / stopped notification if the transcription started / stopped. Only
|
||||
* if there is no recording in progress.
|
||||
*
|
||||
* @param {Dispatch} dispatch - The Redux dispatch function.
|
||||
* @param {Function} getState - The Redux state.
|
||||
* @param {boolean} on - Whether the transcription is on or not.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function maybeEmitRecordingNotification(dispatch: IStore['dispatch'], getState: IStore['getState'], on: boolean) {
|
||||
const state = getState();
|
||||
const { sessionDatas } = state['features/recording'];
|
||||
const { mode: modeConstants, status: statusConstants } = JitsiRecordingConstants;
|
||||
|
||||
if (sessionDatas.some(sd => sd.mode === modeConstants.FILE && sd.status === statusConstants.ON)) {
|
||||
// If a recording is still ongoing, don't send any notification.
|
||||
return;
|
||||
}
|
||||
|
||||
batch(() => {
|
||||
dispatch(showNotification({
|
||||
descriptionKey: on ? 'recording.on' : 'recording.off',
|
||||
titleKey: 'dialog.recording'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
|
||||
dispatch(playSound(on ? RECORDING_ON_SOUND_ID : RECORDING_OFF_SOUND_ID));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application (if API is enabled) that transcribing has started or stopped.
|
||||
*
|
||||
* @param {boolean} on - True if transcribing is on, false otherwise.
|
||||
* @returns {void}
|
||||
*/
|
||||
function notifyTranscribingStatusChanged(on: boolean) {
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyTranscribingStatusChanged(on);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,11 @@ import i18n from 'i18next';
|
||||
import { batch } from 'react-redux';
|
||||
|
||||
import { IStore } from '../app/types';
|
||||
import { CONFERENCE_JOINED, CONFERENCE_JOIN_IN_PROGRESS } from '../base/conference/actionTypes';
|
||||
import {
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_JOIN_IN_PROGRESS,
|
||||
ENDPOINT_MESSAGE_RECEIVED
|
||||
} from '../base/conference/actionTypes';
|
||||
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||
import { raiseHand } from '../base/participants/actions';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
@@ -70,16 +74,17 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
dispatch(raiseHand(false));
|
||||
});
|
||||
|
||||
conference.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
|
||||
(user: any, data: any) => {
|
||||
if (data?.action === 'promotion-response' && data.approved) {
|
||||
const request = getPromotionRequests(getState())
|
||||
.find(r => r.from === data.id);
|
||||
break;
|
||||
}
|
||||
case ENDPOINT_MESSAGE_RECEIVED: {
|
||||
const { data } = action;
|
||||
|
||||
request && dispatch(clearPromotionRequest(request));
|
||||
}
|
||||
});
|
||||
if (data?.action === 'promotion-response' && data.approved) {
|
||||
const request = getPromotionRequests(getState())
|
||||
.find(r => r.from === data.id);
|
||||
|
||||
request && dispatch(clearPromotionRequest(request));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,68 @@
|
||||
import { IStore } from '../app/types';
|
||||
import { showWarningNotification } from '../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
|
||||
|
||||
import { setWhiteboardOpen } from './actions';
|
||||
import { isWhiteboardAllowed, isWhiteboardOpen, isWhiteboardVisible } from './functions';
|
||||
import { WhiteboardStatus } from './types';
|
||||
|
||||
import {
|
||||
RESET_WHITEBOARD,
|
||||
SETUP_WHITEBOARD,
|
||||
SET_WHITEBOARD_OPEN
|
||||
} from './actionTypes';
|
||||
import { IWhiteboardAction } from './reducer';
|
||||
|
||||
/**
|
||||
* API to toggle the whiteboard.
|
||||
* Configures the whiteboard collaboration details.
|
||||
*
|
||||
* @param {Object} payload - The whiteboard settings.
|
||||
* @returns {{
|
||||
* type: SETUP_WHITEBOARD,
|
||||
* collabDetails: { roomId: string, roomKey: string }
|
||||
* }}
|
||||
*/
|
||||
export const setupWhiteboard = ({ collabDetails }: {
|
||||
collabDetails: { roomId: string; roomKey: string; };
|
||||
}): IWhiteboardAction => {
|
||||
return {
|
||||
type: SETUP_WHITEBOARD,
|
||||
collabDetails
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Cleans up the whiteboard collaboration settings.
|
||||
* To be used only on native for cleanup in between conferences.
|
||||
*
|
||||
* @returns {{
|
||||
* type: RESET_WHITEBOARD
|
||||
* }}
|
||||
*/
|
||||
export const resetWhiteboard = (): IWhiteboardAction => {
|
||||
return { type: RESET_WHITEBOARD };
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the whiteboard visibility status.
|
||||
*
|
||||
* @param {boolean} isOpen - The whiteboard visibility flag.
|
||||
* @returns {{
|
||||
* type: SET_WHITEBOARD_OPEN,
|
||||
* isOpen
|
||||
* }}
|
||||
*/
|
||||
export const setWhiteboardOpen = (isOpen: boolean): IWhiteboardAction => {
|
||||
return {
|
||||
type: SET_WHITEBOARD_OPEN,
|
||||
isOpen
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows a warning notification about the whiteboard user limit.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function toggleWhiteboard() {
|
||||
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
const isAllowed = isWhiteboardAllowed(state);
|
||||
const isOpen = isWhiteboardOpen(state);
|
||||
|
||||
if (isAllowed) {
|
||||
if (isOpen && !isWhiteboardVisible(state)) {
|
||||
dispatch(setWhiteboardOpen(true));
|
||||
} else if (isOpen && isWhiteboardVisible(state)) {
|
||||
dispatch(setWhiteboardOpen(false));
|
||||
} else if (!isOpen) {
|
||||
dispatch(setWhiteboardOpen(true));
|
||||
}
|
||||
} else if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyWhiteboardStatusChanged(WhiteboardStatus.FORBIDDEN);
|
||||
}
|
||||
};
|
||||
}
|
||||
export const notifyWhiteboardLimit = () => (dispatch: IStore['dispatch']) => {
|
||||
dispatch(showWarningNotification({
|
||||
titleKey: 'notify.whiteboardLimitTitle',
|
||||
descriptionKey: 'notify.whiteboardLimitDescription'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
|
||||
};
|
||||
|
||||
23
react/features/whiteboard/actions.native.ts
Normal file
23
react/features/whiteboard/actions.native.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createRestrictWhiteboardEvent } from '../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../analytics/functions';
|
||||
import { IStore } from '../app/types';
|
||||
import { navigateRoot } from '../mobile/navigation/rootNavigationContainerRef';
|
||||
import { screen } from '../mobile/navigation/routes';
|
||||
|
||||
import { resetWhiteboard } from './actions.any';
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
/**
|
||||
* Restricts the whiteboard usage.
|
||||
*
|
||||
* @param {boolean} shouldCloseWhiteboard - Whether to dismiss the whiteboard.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export const restrictWhiteboard = (shouldCloseWhiteboard = true) => (dispatch: IStore['dispatch']) => {
|
||||
if (shouldCloseWhiteboard) {
|
||||
navigateRoot(screen.conference.root);
|
||||
}
|
||||
dispatch(resetWhiteboard());
|
||||
sendAnalytics(createRestrictWhiteboardEvent());
|
||||
};
|
||||
@@ -1,82 +0,0 @@
|
||||
import { createRestrictWhiteboardEvent } from '../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../analytics/functions';
|
||||
import { IStore } from '../app/types';
|
||||
import { showWarningNotification } from '../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
|
||||
|
||||
import {
|
||||
RESET_WHITEBOARD,
|
||||
SETUP_WHITEBOARD,
|
||||
SET_WHITEBOARD_OPEN
|
||||
} from './actionTypes';
|
||||
import { IWhiteboardAction } from './reducer';
|
||||
|
||||
/**
|
||||
* Configures the whiteboard collaboration details.
|
||||
*
|
||||
* @param {Object} payload - The whiteboard settings.
|
||||
* @returns {{
|
||||
* type: SETUP_WHITEBOARD,
|
||||
* collabDetails: { roomId: string, roomKey: string }
|
||||
* }}
|
||||
*/
|
||||
export const setupWhiteboard = ({ collabDetails }: {
|
||||
collabDetails: { roomId: string; roomKey: string; };
|
||||
}): IWhiteboardAction => {
|
||||
return {
|
||||
type: SETUP_WHITEBOARD,
|
||||
collabDetails
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Cleans up the whiteboard collaboration settings.
|
||||
* To be used only on native for cleanup in between conferences.
|
||||
*
|
||||
* @returns {{
|
||||
* type: RESET_WHITEBOARD
|
||||
* }}
|
||||
*/
|
||||
export const resetWhiteboard = (): IWhiteboardAction => {
|
||||
return { type: RESET_WHITEBOARD };
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the whiteboard visibility status.
|
||||
*
|
||||
* @param {boolean} isOpen - The whiteboard visibility flag.
|
||||
* @returns {{
|
||||
* type: SET_WHITEBOARD_OPEN,
|
||||
* isOpen
|
||||
* }}
|
||||
*/
|
||||
export const setWhiteboardOpen = (isOpen: boolean): IWhiteboardAction => {
|
||||
return {
|
||||
type: SET_WHITEBOARD_OPEN,
|
||||
isOpen
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows a warning notification about the whiteboard user limit.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export const notifyWhiteboardLimit = () => (dispatch: IStore['dispatch']) => {
|
||||
dispatch(showWarningNotification({
|
||||
titleKey: 'notify.whiteboardLimitTitle',
|
||||
descriptionKey: 'notify.whiteboardLimitDescription'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
|
||||
};
|
||||
|
||||
/**
|
||||
* Restricts the whiteboard usage.
|
||||
*
|
||||
* @param {boolean} shouldCloseWhiteboard - Whether to dismiss the whiteboard participant.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export const restrictWhiteboard = (shouldCloseWhiteboard = true) => (dispatch: IStore['dispatch']) => {
|
||||
shouldCloseWhiteboard && dispatch(setWhiteboardOpen(false));
|
||||
dispatch(resetWhiteboard());
|
||||
sendAnalytics(createRestrictWhiteboardEvent());
|
||||
};
|
||||
48
react/features/whiteboard/actions.web.ts
Normal file
48
react/features/whiteboard/actions.web.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { createRestrictWhiteboardEvent } from '../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../analytics/functions';
|
||||
import { IStore } from '../app/types';
|
||||
|
||||
import { resetWhiteboard, setWhiteboardOpen } from './actions.any';
|
||||
import { isWhiteboardAllowed, isWhiteboardOpen, isWhiteboardVisible } from './functions';
|
||||
import { WhiteboardStatus } from './types';
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
/**
|
||||
* API to toggle the whiteboard.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function toggleWhiteboard() {
|
||||
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
const isAllowed = isWhiteboardAllowed(state);
|
||||
const isOpen = isWhiteboardOpen(state);
|
||||
|
||||
if (isAllowed) {
|
||||
if (isOpen && !isWhiteboardVisible(state)) {
|
||||
dispatch(setWhiteboardOpen(true));
|
||||
} else if (isOpen && isWhiteboardVisible(state)) {
|
||||
dispatch(setWhiteboardOpen(false));
|
||||
} else if (!isOpen) {
|
||||
dispatch(setWhiteboardOpen(true));
|
||||
}
|
||||
} else if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyWhiteboardStatusChanged(WhiteboardStatus.FORBIDDEN);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Restricts the whiteboard usage.
|
||||
*
|
||||
* @param {boolean} shouldCloseWhiteboard - Whether to dismiss the whiteboard.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export const restrictWhiteboard = (shouldCloseWhiteboard = true) => (dispatch: IStore['dispatch']) => {
|
||||
if (shouldCloseWhiteboard) {
|
||||
dispatch(setWhiteboardOpen(false));
|
||||
}
|
||||
dispatch(resetWhiteboard());
|
||||
sendAnalytics(createRestrictWhiteboardEvent());
|
||||
};
|
||||
220
react/features/whiteboard/components/native/Whiteboard.tsx
Normal file
220
react/features/whiteboard/components/native/Whiteboard.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
import { Route } from '@react-navigation/native';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { View, ViewStyle } from 'react-native';
|
||||
import { WebView } from 'react-native-webview';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { getCurrentConference } from '../../../base/conference/functions';
|
||||
import { IJitsiConference } from '../../../base/conference/reducer';
|
||||
import { openDialog } from '../../../base/dialog/actions';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
|
||||
import LoadingIndicator from '../../../base/react/components/native/LoadingIndicator';
|
||||
import { safeDecodeURIComponent } from '../../../base/util/uri';
|
||||
import { setupWhiteboard } from '../../actions.any';
|
||||
import { WHITEBOARD_ID } from '../../constants';
|
||||
import { getCollabServerUrl, getWhiteboardInfoForURIString } from '../../functions';
|
||||
|
||||
import WhiteboardErrorDialog from './WhiteboardErrorDialog';
|
||||
import styles, { INDICATOR_COLOR } from './styles';
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The whiteboard collab server url.
|
||||
*/
|
||||
collabServerUrl?: string;
|
||||
|
||||
/**
|
||||
* The current Jitsi conference.
|
||||
*/
|
||||
conference?: IJitsiConference;
|
||||
|
||||
/**
|
||||
* Redux store dispatch method.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Window location href.
|
||||
*/
|
||||
locationHref: string;
|
||||
|
||||
/**
|
||||
* Default prop for navigating between screen components(React Navigation).
|
||||
*/
|
||||
navigation: any;
|
||||
|
||||
/**
|
||||
* Default prop for navigating between screen components(React Navigation).
|
||||
*/
|
||||
route: Route<'', {
|
||||
collabDetails: { roomId: string; roomKey: string; };
|
||||
collabServerUrl: string;
|
||||
localParticipantName: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React native component that displays the whiteboard page for a specific room.
|
||||
*/
|
||||
class Whiteboard extends PureComponent<IProps> {
|
||||
|
||||
/**
|
||||
* Initializes a new instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._onError = this._onError.bind(this);
|
||||
this._onNavigate = this._onNavigate.bind(this);
|
||||
this._onMessage = this._onMessage.bind(this);
|
||||
this._renderLoading = this._renderLoading.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidMount()}. Invoked
|
||||
* immediately after mounting occurs.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidMount() {
|
||||
const { navigation, t } = this.props;
|
||||
|
||||
navigation.setOptions({
|
||||
headerTitle: t('whiteboard.screenTitle')
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { locationHref, route } = this.props;
|
||||
const collabServerUrl = safeDecodeURIComponent(route.params?.collabServerUrl);
|
||||
const localParticipantName = safeDecodeURIComponent(route.params?.localParticipantName);
|
||||
const collabDetails = route.params?.collabDetails;
|
||||
const uri = getWhiteboardInfoForURIString(
|
||||
locationHref,
|
||||
collabServerUrl,
|
||||
collabDetails,
|
||||
localParticipantName
|
||||
) ?? '';
|
||||
|
||||
return (
|
||||
<JitsiScreen
|
||||
safeAreaInsets = { [ 'bottom', 'left', 'right' ] }
|
||||
style = { styles.backDrop }>
|
||||
<WebView
|
||||
incognito = { true }
|
||||
javaScriptEnabled = { true }
|
||||
nestedScrollEnabled = { true }
|
||||
onError = { this._onError }
|
||||
onMessage = { this._onMessage }
|
||||
onShouldStartLoadWithRequest = { this._onNavigate }
|
||||
renderLoading = { this._renderLoading }
|
||||
scrollEnabled = { true }
|
||||
setSupportMultipleWindows = { false }
|
||||
source = {{ uri }}
|
||||
startInLoadingState = { true }
|
||||
style = { styles.webView } />
|
||||
</JitsiScreen>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to handle the error if the page fails to load.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onError() {
|
||||
this.props.dispatch(openDialog(WhiteboardErrorDialog));
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to intercept navigation inside the webview and make the native app handle the whiteboard requests.
|
||||
*
|
||||
* NOTE: We don't navigate to anywhere else from that view.
|
||||
*
|
||||
* @param {any} request - The request object.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onNavigate(request: { url: string; }) {
|
||||
const { url } = request;
|
||||
const { locationHref, route } = this.props;
|
||||
const collabServerUrl = route.params?.collabServerUrl;
|
||||
const collabDetails = route.params?.collabDetails;
|
||||
const localParticipantName = route.params?.localParticipantName;
|
||||
|
||||
return url === getWhiteboardInfoForURIString(
|
||||
locationHref,
|
||||
collabServerUrl,
|
||||
collabDetails,
|
||||
localParticipantName
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to handle the message events.
|
||||
*
|
||||
* @param {any} event - The event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onMessage(event: any) {
|
||||
const { collabServerUrl, conference } = this.props;
|
||||
const collabDetails = JSON.parse(event.nativeEvent.data);
|
||||
|
||||
if (collabDetails?.roomId && collabDetails?.roomKey) {
|
||||
this.props.dispatch(setupWhiteboard({ collabDetails }));
|
||||
|
||||
// Broadcast the collab details.
|
||||
conference?.getMetadataHandler().setMetadata(WHITEBOARD_ID, {
|
||||
collabServerUrl,
|
||||
collabDetails
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the loading indicator.
|
||||
*
|
||||
* @returns {React$Component<any>}
|
||||
*/
|
||||
_renderLoading() {
|
||||
return (
|
||||
<View style = { styles.indicatorWrapper as ViewStyle }>
|
||||
<LoadingIndicator
|
||||
color = { INDICATOR_COLOR }
|
||||
size = 'large' />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated
|
||||
* {@code WaitForOwnerDialog}'s props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @private
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState) {
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
const { href = '' } = locationURL ?? {};
|
||||
|
||||
return {
|
||||
conference: getCurrentConference(state),
|
||||
collabServerUrl: getCollabServerUrl(state),
|
||||
locationHref: href
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(Whiteboard));
|
||||
@@ -0,0 +1,43 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconWhiteboard } from '../../../base/icons/svg';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
|
||||
import { setWhiteboardOpen } from '../../actions.any';
|
||||
import { isWhiteboardButtonVisible } from '../../functions';
|
||||
|
||||
/**
|
||||
* Component that renders a toolbar button for the whiteboard.
|
||||
*/
|
||||
class WhiteboardButton extends AbstractButton<AbstractButtonProps> {
|
||||
accessibilityLabel = 'toolbar.accessibilityLabel.showWhiteboard';
|
||||
icon = IconWhiteboard;
|
||||
label = 'toolbar.showWhiteboard';
|
||||
tooltip = 'toolbar.showWhiteboard';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and opens the whiteboard view.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
this.props.dispatch(setWhiteboardOpen(true));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
visible: isWhiteboardButtonVisible(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(WhiteboardButton));
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
|
||||
import AlertDialog from '../../../base/dialog/components/native/AlertDialog';
|
||||
|
||||
/**
|
||||
* Dialog to inform the user that we couldn't load the whiteboard.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const WhiteboardErrorDialog = () => (
|
||||
<AlertDialog
|
||||
contentKey = 'info.whiteboardError' />
|
||||
);
|
||||
|
||||
export default WhiteboardErrorDialog;
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Text, TextStyle } from 'react-native';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import ConfirmDialog from '../../../base/dialog/components/native/ConfirmDialog';
|
||||
import Link from '../../../base/react/components/native/Link';
|
||||
import { getWhiteboardConfig } from '../../functions';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
* Component that renders the whiteboard user limit dialog.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const WhiteboardLimitDialog = () => {
|
||||
const { t } = useTranslation();
|
||||
const { limitUrl } = useSelector(getWhiteboardConfig);
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
cancelLabel = { 'dialog.Ok' }
|
||||
descriptionKey = { 'dialog.whiteboardLimitContent' }
|
||||
isConfirmHidden = { true }
|
||||
title = { 'dialog.whiteboardLimitTitle' }>
|
||||
{limitUrl && (
|
||||
<Text style = { styles.limitUrlText as TextStyle }>
|
||||
{` ${t('dialog.whiteboardLimitReference')}
|
||||
`}
|
||||
<Link
|
||||
style = { styles.limitUrl }
|
||||
url = { limitUrl }>
|
||||
{t('dialog.whiteboardLimitReferenceUrl')}
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
)}
|
||||
</ConfirmDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default WhiteboardLimitDialog;
|
||||
37
react/features/whiteboard/components/native/styles.ts
Normal file
37
react/features/whiteboard/components/native/styles.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
||||
|
||||
export const INDICATOR_COLOR = BaseTheme.palette.ui07;
|
||||
|
||||
const WV_BACKGROUND = BaseTheme.palette.ui03;
|
||||
|
||||
export default {
|
||||
|
||||
backDrop: {
|
||||
backgroundColor: WV_BACKGROUND,
|
||||
flex: 1
|
||||
},
|
||||
|
||||
indicatorWrapper: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: BaseTheme.palette.ui10,
|
||||
height: '100%',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
webView: {
|
||||
backgroundColor: WV_BACKGROUND,
|
||||
flex: 1
|
||||
},
|
||||
|
||||
limitUrlText: {
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
marginBottom: BaseTheme.spacing[2],
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
limitUrl: {
|
||||
color: BaseTheme.palette.link01,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link NoWhiteboardError}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Additional CSS classnames to append to the root of the component.
|
||||
*/
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const NoWhiteboardError = ({ className }: IProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className = { className } >
|
||||
{t('info.noWhiteboard')}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoWhiteboardError;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ExcalidrawApp } from '@jitsi/excalidraw';
|
||||
import clsx from 'clsx';
|
||||
import i18next from 'i18next';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
@@ -143,6 +144,7 @@ const Whiteboard = (props: WithTranslation): JSX.Element => {
|
||||
collabServerUrl = { collabServerUrl }
|
||||
excalidraw = {{
|
||||
isCollaborating: true,
|
||||
langCode: i18next.language,
|
||||
|
||||
// @ts-ignore
|
||||
ref: excalidrawRef,
|
||||
|
||||
91
react/features/whiteboard/components/web/WhiteboardApp.tsx
Normal file
91
react/features/whiteboard/components/web/WhiteboardApp.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import { generateCollaborationLinkData } from '@jitsi/excalidraw';
|
||||
import React, { ComponentType } from 'react';
|
||||
|
||||
import BaseApp from '../../../base/app/components/BaseApp';
|
||||
import GlobalStyles from '../../../base/ui/components/GlobalStyles.web';
|
||||
import JitsiThemeProvider from '../../../base/ui/components/JitsiThemeProvider.web';
|
||||
import { decodeFromBase64URL } from '../../../base/util/httpUtils';
|
||||
import { parseURLParams } from '../../../base/util/parseURLParams';
|
||||
import { safeDecodeURIComponent } from '../../../base/util/uri';
|
||||
import logger from '../../logger';
|
||||
|
||||
import NoWhiteboardError from './NoWhiteboardError';
|
||||
import WhiteboardWrapper from './WhiteboardWrapper';
|
||||
|
||||
/**
|
||||
* Wrapper application for the whiteboard.
|
||||
*
|
||||
* @augments BaseApp
|
||||
*/
|
||||
export default class WhiteboardApp extends BaseApp<any> {
|
||||
/**
|
||||
* Navigates to {@link Whiteboard} upon mount.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
async componentDidMount() {
|
||||
await super.componentDidMount();
|
||||
|
||||
const { state } = parseURLParams(window.location.href, true);
|
||||
const decodedState = JSON.parse(decodeFromBase64URL(state));
|
||||
const { collabServerUrl, localParticipantName } = decodedState;
|
||||
let { roomId, roomKey } = decodedState;
|
||||
|
||||
if (!roomId && !roomKey) {
|
||||
try {
|
||||
const collabDetails = await generateCollaborationLinkData();
|
||||
|
||||
roomId = collabDetails.roomId;
|
||||
roomKey = collabDetails.roomKey;
|
||||
|
||||
if (window.ReactNativeWebView) {
|
||||
setTimeout(() => {
|
||||
window.ReactNativeWebView.postMessage(JSON.stringify(collabDetails));
|
||||
}, 0);
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error('Couldn\'t generate collaboration link data.', e);
|
||||
}
|
||||
}
|
||||
|
||||
super._navigate({
|
||||
component: () => (
|
||||
<>{
|
||||
roomId && roomKey && collabServerUrl
|
||||
? <WhiteboardWrapper
|
||||
className = 'whiteboard'
|
||||
collabDetails = {{
|
||||
roomId,
|
||||
roomKey
|
||||
}}
|
||||
collabServerUrl = { safeDecodeURIComponent(collabServerUrl) }
|
||||
localParticipantName = { localParticipantName } />
|
||||
: <NoWhiteboardError className = 'whiteboard' />
|
||||
}</>
|
||||
) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the parent method to inject {@link AtlasKitThemeProvider} as
|
||||
* the top most component.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_createMainElement(component: ComponentType<any>, props: Object) {
|
||||
return (
|
||||
<JitsiThemeProvider>
|
||||
<GlobalStyles />
|
||||
{super._createMainElement(component, props)}
|
||||
</JitsiThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the platform specific dialog container.
|
||||
*
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_renderDialogContainer() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { translate } from '../../../base/i18n/functions';
|
||||
import { IconWhiteboard, IconWhiteboardHide } from '../../../base/icons/svg';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
|
||||
import { setOverflowMenuVisible } from '../../../toolbox/actions.web';
|
||||
import { setWhiteboardOpen } from '../../actions';
|
||||
import { setWhiteboardOpen } from '../../actions.any';
|
||||
import { isWhiteboardButtonVisible, isWhiteboardVisible } from '../../functions';
|
||||
|
||||
interface IProps extends AbstractButtonProps {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { ExcalidrawApp } from '@jitsi/excalidraw';
|
||||
import i18next from 'i18next';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
|
||||
import { WHITEBOARD_UI_OPTIONS } from '../../constants';
|
||||
|
||||
/**
|
||||
* Whiteboard wrapper for mobile.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const WhiteboardWrapper = ({
|
||||
className,
|
||||
collabDetails,
|
||||
collabServerUrl,
|
||||
localParticipantName
|
||||
}: {
|
||||
className?: string;
|
||||
collabDetails: {
|
||||
roomId: string;
|
||||
roomKey: string;
|
||||
};
|
||||
collabServerUrl: string;
|
||||
localParticipantName: string;
|
||||
}) => {
|
||||
const excalidrawRef = useRef<any>(null);
|
||||
const excalidrawAPIRef = useRef<any>(null);
|
||||
const collabAPIRef = useRef<any>(null);
|
||||
|
||||
const getExcalidrawAPI = useCallback(excalidrawAPI => {
|
||||
if (excalidrawAPIRef.current) {
|
||||
return;
|
||||
}
|
||||
excalidrawAPIRef.current = excalidrawAPI;
|
||||
}, []);
|
||||
|
||||
const getCollabAPI = useCallback(collabAPI => {
|
||||
if (collabAPIRef.current) {
|
||||
return;
|
||||
}
|
||||
collabAPIRef.current = collabAPI;
|
||||
collabAPIRef.current.setUsername(localParticipantName);
|
||||
}, [ localParticipantName ]);
|
||||
|
||||
return (
|
||||
<div className = { className }>
|
||||
<div className = 'excalidraw-wrapper'>
|
||||
<ExcalidrawApp
|
||||
collabDetails = { collabDetails }
|
||||
collabServerUrl = { collabServerUrl }
|
||||
detectScroll = { true }
|
||||
excalidraw = {{
|
||||
isCollaborating: true,
|
||||
langCode: i18next.language,
|
||||
|
||||
// @ts-ignore
|
||||
ref: excalidrawRef,
|
||||
theme: 'light',
|
||||
UIOptions: WHITEBOARD_UI_OPTIONS
|
||||
}}
|
||||
getCollabAPI = { getCollabAPI }
|
||||
getExcalidrawAPI = { getExcalidrawAPI } />
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WhiteboardWrapper;
|
||||
@@ -62,3 +62,10 @@ export const MIN_USER_LIMIT = 10;
|
||||
* Whiteboard soft limit diff.
|
||||
*/
|
||||
export const USER_LIMIT_THRESHOLD = 5;
|
||||
|
||||
/**
|
||||
* The pathName for the whiteboard page.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const WHITEBOARD_PATH_NAME = 'static/whiteboard.html';
|
||||
|
||||
@@ -5,10 +5,11 @@ import { IReduxState } from '../app/types';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { IWhiteboardConfig } from '../base/config/configType';
|
||||
import { getRemoteParticipants, isLocalParticipantModerator } from '../base/participants/functions';
|
||||
import { appendURLParam } from '../base/util/uri';
|
||||
import { encodeToBase64URL } from '../base/util/httpUtils';
|
||||
import { appendURLHashParam, appendURLParam } from '../base/util/uri';
|
||||
import { getCurrentRoomId, isInBreakoutRoom } from '../breakout-rooms/functions';
|
||||
|
||||
import { MIN_USER_LIMIT, USER_LIMIT_THRESHOLD, WHITEBOARD_ID } from './constants';
|
||||
import { MIN_USER_LIMIT, USER_LIMIT_THRESHOLD, WHITEBOARD_ID, WHITEBOARD_PATH_NAME } from './constants';
|
||||
import { IWhiteboardState } from './reducer';
|
||||
|
||||
const getWhiteboardState = (state: IReduxState): IWhiteboardState => state['features/whiteboard'];
|
||||
@@ -35,27 +36,27 @@ export const getCollabDetails = (state: IReduxState): {
|
||||
} | undefined => getWhiteboardState(state).collabDetails;
|
||||
|
||||
/**
|
||||
* Indicates whether the whiteboard is enabled in the config.
|
||||
* Indicates whether the whiteboard collaboration details are available.
|
||||
*
|
||||
* @param {IReduxState} state - The state from the Redux store.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const hasCollabDetails = (state: IReduxState): boolean => Boolean(
|
||||
getCollabDetails(state)?.roomId && getCollabDetails(state)?.roomKey
|
||||
);
|
||||
|
||||
/**
|
||||
* Indicates whether the whiteboard is enabled.
|
||||
*
|
||||
* @param {IReduxState} state - The state from the Redux store.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isWhiteboardEnabled = (state: IReduxState): boolean =>
|
||||
getWhiteboardConfig(state).enabled
|
||||
(getWhiteboardConfig(state).enabled || hasCollabDetails(state))
|
||||
&& getWhiteboardConfig(state).collabServerBaseUrl
|
||||
&& getCurrentConference(state)?.getMetadataHandler()
|
||||
?.isSupported();
|
||||
|
||||
/**
|
||||
* Indicates whether the whiteboard has the collaboration
|
||||
* details and is ready to be used.
|
||||
*
|
||||
* @param {IReduxState} state - The state from the Redux store.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isWhiteboardReady = (state: IReduxState): boolean =>
|
||||
isWhiteboardEnabled(state) && Boolean(getCollabDetails(state));
|
||||
|
||||
/**
|
||||
* Indicates whether the whiteboard is open.
|
||||
*
|
||||
@@ -156,3 +157,50 @@ export const shouldNotifyUserLimit = (state: IReduxState): boolean => {
|
||||
|
||||
return participantCount + USER_LIMIT_THRESHOLD > userLimit;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates the URL for the static whiteboard page.
|
||||
*
|
||||
* @param {string} locationUrl - The window location href.
|
||||
* @param {string} collabServerUrl - The whiteboard collaboration server url.
|
||||
* @param {Object} collabDetails - The whiteboard collaboration details.
|
||||
* @param {string} localParticipantName - The local participant name.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getWhiteboardInfoForURIString(
|
||||
locationUrl: any,
|
||||
collabServerUrl: string,
|
||||
collabDetails: { roomId: string; roomKey: string; },
|
||||
localParticipantName: string
|
||||
): string | undefined {
|
||||
if (!collabServerUrl || !locationUrl) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let state = {};
|
||||
let url = `${locationUrl.substring(0, locationUrl.lastIndexOf('/'))}/${WHITEBOARD_PATH_NAME}`;
|
||||
|
||||
if (collabDetails?.roomId) {
|
||||
state = {
|
||||
...state,
|
||||
roomId: collabDetails.roomId
|
||||
};
|
||||
}
|
||||
|
||||
if (collabDetails?.roomKey) {
|
||||
state = {
|
||||
...state,
|
||||
roomKey: collabDetails.roomKey
|
||||
};
|
||||
}
|
||||
|
||||
state = {
|
||||
...state,
|
||||
collabServerUrl,
|
||||
localParticipantName
|
||||
};
|
||||
|
||||
url = appendURLHashParam(url, 'state', encodeToBase64URL(JSON.stringify(state)));
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
3
react/features/whiteboard/logger.ts
Normal file
3
react/features/whiteboard/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/whiteboard');
|
||||
92
react/features/whiteboard/middleware.any.ts
Normal file
92
react/features/whiteboard/middleware.any.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { createOpenWhiteboardEvent } from '../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../analytics/functions';
|
||||
import { IStore } from '../app/types';
|
||||
import { UPDATE_CONFERENCE_METADATA } from '../base/conference/actionTypes';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
|
||||
import { SET_WHITEBOARD_OPEN } from './actionTypes';
|
||||
import {
|
||||
notifyWhiteboardLimit,
|
||||
resetWhiteboard,
|
||||
restrictWhiteboard,
|
||||
setWhiteboardOpen,
|
||||
setupWhiteboard
|
||||
} from './actions';
|
||||
import { WHITEBOARD_ID } from './constants';
|
||||
import {
|
||||
isWhiteboardOpen,
|
||||
shouldEnforceUserLimit,
|
||||
shouldNotifyUserLimit
|
||||
} from './functions';
|
||||
|
||||
MiddlewareRegistry.register((store: IStore) => next => action => {
|
||||
const state = store.getState();
|
||||
|
||||
switch (action.type) {
|
||||
case SET_WHITEBOARD_OPEN: {
|
||||
const enforceUserLimit = shouldEnforceUserLimit(state);
|
||||
const notifyUserLimit = shouldNotifyUserLimit(state);
|
||||
|
||||
if (action.isOpen && !enforceUserLimit && !notifyUserLimit) {
|
||||
sendAnalytics(createOpenWhiteboardEvent());
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case UPDATE_CONFERENCE_METADATA: {
|
||||
const { metadata } = action;
|
||||
|
||||
if (metadata?.[WHITEBOARD_ID]) {
|
||||
store.dispatch(setupWhiteboard({
|
||||
collabDetails: metadata[WHITEBOARD_ID].collabDetails
|
||||
}));
|
||||
store.dispatch(setWhiteboardOpen(true));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Set up state change listener to perform maintenance tasks when the conference
|
||||
* is left or failed, e.g. Disable the whiteboard if it's left open.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
state => getCurrentConference(state),
|
||||
(conference, { dispatch }, previousConference): void => {
|
||||
if (conference !== previousConference) {
|
||||
dispatch(resetWhiteboard());
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Set up state change listener to limit whiteboard access.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
state => shouldEnforceUserLimit(state),
|
||||
(enforceUserLimit, { dispatch, getState }): void => {
|
||||
if (isWhiteboardOpen(getState()) && enforceUserLimit) {
|
||||
dispatch(restrictWhiteboard());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Set up state change listener to notify about whiteboard usage.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
state => shouldNotifyUserLimit(state),
|
||||
(notifyUserLimit, { dispatch, getState }, prevNotifyUserLimit): void => {
|
||||
if (isWhiteboardOpen(getState()) && notifyUserLimit && !prevNotifyUserLimit) {
|
||||
dispatch(notifyWhiteboardLimit());
|
||||
}
|
||||
}
|
||||
);
|
||||
80
react/features/whiteboard/middleware.native.ts
Normal file
80
react/features/whiteboard/middleware.native.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { IStore } from '../app/types';
|
||||
import { hideDialog, openDialog } from '../base/dialog/actions';
|
||||
import { isDialogOpen } from '../base/dialog/functions';
|
||||
import { getLocalParticipant } from '../base/participants/functions';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import { navigateRoot } from '../mobile/navigation/rootNavigationContainerRef';
|
||||
import { screen } from '../mobile/navigation/routes';
|
||||
|
||||
import { SET_WHITEBOARD_OPEN } from './actionTypes';
|
||||
import {
|
||||
notifyWhiteboardLimit,
|
||||
restrictWhiteboard
|
||||
} from './actions';
|
||||
import WhiteboardLimitDialog from './components/native/WhiteboardLimitDialog';
|
||||
import {
|
||||
getCollabDetails,
|
||||
getCollabServerUrl,
|
||||
shouldEnforceUserLimit,
|
||||
shouldNotifyUserLimit
|
||||
} from './functions';
|
||||
import './middleware.any';
|
||||
|
||||
/**
|
||||
* Middleware which intercepts whiteboard actions to handle changes to the related state.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register((store: IStore) => (next: Function) => async (action: AnyAction) => {
|
||||
const { dispatch, getState } = store;
|
||||
const state = getState();
|
||||
|
||||
switch (action.type) {
|
||||
case SET_WHITEBOARD_OPEN: {
|
||||
const enforceUserLimit = shouldEnforceUserLimit(state);
|
||||
const notifyUserLimit = shouldNotifyUserLimit(state);
|
||||
|
||||
if (enforceUserLimit) {
|
||||
dispatch(restrictWhiteboard(false));
|
||||
dispatch(openDialog(WhiteboardLimitDialog));
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
||||
if (action.isOpen) {
|
||||
if (enforceUserLimit) {
|
||||
dispatch(restrictWhiteboard());
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
||||
if (notifyUserLimit) {
|
||||
dispatch(notifyWhiteboardLimit());
|
||||
}
|
||||
|
||||
if (isDialogOpen(state, WhiteboardLimitDialog)) {
|
||||
dispatch(hideDialog(WhiteboardLimitDialog));
|
||||
}
|
||||
|
||||
const collabDetails = getCollabDetails(state);
|
||||
const collabServerUrl = getCollabServerUrl(state);
|
||||
const localParticipantName = getLocalParticipant(state)?.name;
|
||||
|
||||
navigateRoot(screen.conference.whiteboard, {
|
||||
collabDetails,
|
||||
collabServerUrl,
|
||||
localParticipantName
|
||||
});
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
@@ -1,17 +1,13 @@
|
||||
import { generateCollaborationLinkData } from '@jitsi/excalidraw';
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { createOpenWhiteboardEvent } from '../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../analytics/functions';
|
||||
import { IStore } from '../app/types';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { hideDialog, openDialog } from '../base/dialog/actions';
|
||||
import { isDialogOpen } from '../base/dialog/functions';
|
||||
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||
import { participantJoined, participantLeft, pinParticipant } from '../base/participants/actions';
|
||||
import { FakeParticipant } from '../base/participants/types';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
import { getCurrentRoomId } from '../breakout-rooms/functions';
|
||||
import { addStageParticipant } from '../filmstrip/actions.web';
|
||||
import { isStageFilmstripAvailable } from '../filmstrip/functions.web';
|
||||
@@ -19,9 +15,7 @@ import { isStageFilmstripAvailable } from '../filmstrip/functions.web';
|
||||
import { RESET_WHITEBOARD, SET_WHITEBOARD_OPEN } from './actionTypes';
|
||||
import {
|
||||
notifyWhiteboardLimit,
|
||||
resetWhiteboard,
|
||||
restrictWhiteboard,
|
||||
setWhiteboardOpen,
|
||||
setupWhiteboard
|
||||
} from './actions';
|
||||
import WhiteboardLimitDialog from './components/web/WhiteboardLimitDialog';
|
||||
@@ -29,13 +23,14 @@ import { WHITEBOARD_ID, WHITEBOARD_PARTICIPANT_NAME } from './constants';
|
||||
import {
|
||||
getCollabDetails,
|
||||
getCollabServerUrl,
|
||||
isWhiteboardOpen,
|
||||
isWhiteboardPresent,
|
||||
shouldEnforceUserLimit,
|
||||
shouldNotifyUserLimit
|
||||
} from './functions';
|
||||
import { WhiteboardStatus } from './types';
|
||||
|
||||
import './middleware.any';
|
||||
|
||||
const focusWhiteboard = (store: IStore) => {
|
||||
const { dispatch, getState } = store;
|
||||
const state = getState();
|
||||
@@ -79,7 +74,7 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => async (action
|
||||
dispatch(restrictWhiteboard(false));
|
||||
dispatch(openDialog(WhiteboardLimitDialog));
|
||||
|
||||
return;
|
||||
return next(action);
|
||||
}
|
||||
|
||||
if (!existingCollabDetails) {
|
||||
@@ -99,14 +94,14 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => async (action
|
||||
});
|
||||
raiseWhiteboardNotification(WhiteboardStatus.INSTANTIATED);
|
||||
|
||||
return;
|
||||
return next(action);
|
||||
}
|
||||
|
||||
if (action.isOpen) {
|
||||
if (enforceUserLimit) {
|
||||
dispatch(restrictWhiteboard());
|
||||
|
||||
return;
|
||||
return next(action);
|
||||
}
|
||||
|
||||
if (notifyUserLimit) {
|
||||
@@ -118,10 +113,9 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => async (action
|
||||
}
|
||||
|
||||
focusWhiteboard(store);
|
||||
sendAnalytics(createOpenWhiteboardEvent());
|
||||
raiseWhiteboardNotification(WhiteboardStatus.SHOWN);
|
||||
|
||||
return;
|
||||
return next(action);
|
||||
}
|
||||
|
||||
dispatch(participantLeft(WHITEBOARD_ID, conference, { fakeParticipant: FakeParticipant.Whiteboard }));
|
||||
@@ -152,49 +146,3 @@ function raiseWhiteboardNotification(status: WhiteboardStatus) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up state change listener to perform maintenance tasks when the conference
|
||||
* is left or failed, e.g. Disable the whiteboard if it's left open.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
state => getCurrentConference(state),
|
||||
(conference, { dispatch }, previousConference): void => {
|
||||
if (conference !== previousConference) {
|
||||
dispatch(resetWhiteboard());
|
||||
}
|
||||
if (conference && !previousConference) {
|
||||
conference.on(JitsiConferenceEvents.METADATA_UPDATED, (metadata: any) => {
|
||||
if (metadata[WHITEBOARD_ID]) {
|
||||
dispatch(setupWhiteboard({
|
||||
collabDetails: metadata[WHITEBOARD_ID].collabDetails
|
||||
}));
|
||||
dispatch(setWhiteboardOpen(true));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Set up state change listener to limit whiteboard access.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
state => shouldEnforceUserLimit(state),
|
||||
(enforceUserLimit, { dispatch, getState }): void => {
|
||||
if (isWhiteboardOpen(getState()) && enforceUserLimit) {
|
||||
dispatch(restrictWhiteboard());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Set up state change listener to notify about whiteboard usage.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
state => shouldNotifyUserLimit(state),
|
||||
(notifyUserLimit, { dispatch, getState }, prevNotifyUserLimit): void => {
|
||||
if (isWhiteboardOpen(getState()) && notifyUserLimit && !prevNotifyUserLimit) {
|
||||
dispatch(notifyWhiteboardLimit());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import Platform from './features/base/react/Platform.web';
|
||||
import { getJitsiMeetGlobalNS } from './features/base/util/helpers';
|
||||
import DialInSummaryApp from './features/invite/components/dial-in-summary/web/DialInSummaryApp';
|
||||
import PrejoinApp from './features/prejoin/components/web/PrejoinApp';
|
||||
import WhiteboardApp from './features/whiteboard/components/web/WhiteboardApp';
|
||||
|
||||
const logger = getLogger('index.web');
|
||||
|
||||
@@ -60,7 +61,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
globalNS.entryPoints = {
|
||||
APP: App,
|
||||
PREJOIN: PrejoinApp,
|
||||
DIALIN: DialInSummaryApp
|
||||
DIALIN: DialInSummaryApp,
|
||||
WHITEBOARD: WhiteboardApp
|
||||
};
|
||||
|
||||
globalNS.renderEntryPoint = ({
|
||||
|
||||
@@ -61,6 +61,11 @@ function broadcastMetadata(room)
|
||||
for _, occupant in room:each_occupant() do
|
||||
if jid_node(occupant.jid) ~= 'focus' then
|
||||
send_json_msg(occupant.jid, json_msg)
|
||||
else
|
||||
-- Jicofo reads room_metadata only from the MUC config form. Notify it that the form changed.
|
||||
module:send(st.message({ type='groupchat', from=room.jid, to = occupant.jid })
|
||||
:tag('x', { xmlns='http://jabber.org/protocol/muc#user' })
|
||||
:tag('status', { code='104' }));
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
33
static/whiteboard.html
Normal file
33
static/whiteboard.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<html xmlns="http://www.w3.org/1999/html">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="content-type" content="text/html;charset=utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!--#include virtual="/base.html" -->
|
||||
<link rel="stylesheet" href="css/all.css">
|
||||
<script>
|
||||
window.EXCALIDRAW_ASSET_PATH = 'libs/';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
if (!JitsiMeetJS.app) {
|
||||
return;
|
||||
}
|
||||
|
||||
JitsiMeetJS.app.renderEntryPoint({
|
||||
Component: JitsiMeetJS.app.entryPoints.WHITEBOARD
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<!--#include virtual="/title.html" -->
|
||||
<script><!--#include virtual="/config.js" --></script>
|
||||
<script><!--#include virtual="/interface_config.js" --></script>
|
||||
<script src="libs/lib-jitsi-meet.min.js?v=139"></script>
|
||||
<script src="libs/app.bundle.min.js?v=139"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="react" role="main"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user