feat(whiteboard) add initial implementation (#12185)
This commit is contained in:
parent
d43eea91cf
commit
93406bb12c
16
Makefile
16
Makefile
|
@ -5,6 +5,8 @@ LIBJITSIMEET_DIR = node_modules/lib-jitsi-meet
|
|||
OLM_DIR = node_modules/@matrix-org/olm
|
||||
TF_WASM_DIR = node_modules/@tensorflow/tfjs-backend-wasm/dist/
|
||||
RNNOISE_WASM_DIR = node_modules/@jitsi/rnnoise-wasm/dist
|
||||
EXCALIDRAW_DIR = node_modules/@jitsi/excalidraw/dist/excalidraw-assets
|
||||
EXCALIDRAW_DIR_DEV = node_modules/@jitsi/excalidraw/dist/excalidraw-assets-dev
|
||||
TFLITE_WASM = react/features/stream-effects/virtual-background/vendor/tflite
|
||||
MEET_MODELS_DIR = react/features/stream-effects/virtual-background/vendor/models
|
||||
FACE_MODELS_DIR = node_modules/@vladmandic/human-models/models
|
||||
|
@ -27,7 +29,7 @@ clean:
|
|||
rm -fr $(BUILD_DIR)
|
||||
|
||||
.NOTPARALLEL:
|
||||
deploy: deploy-init deploy-appbundle deploy-rnnoise-binary deploy-tflite deploy-meet-models deploy-lib-jitsi-meet deploy-olm deploy-tf-wasm deploy-css deploy-local deploy-face-landmarks
|
||||
deploy: deploy-init deploy-appbundle deploy-rnnoise-binary deploy-excalidraw deploy-tflite deploy-meet-models deploy-lib-jitsi-meet deploy-olm deploy-tf-wasm deploy-css deploy-local deploy-face-landmarks
|
||||
|
||||
deploy-init:
|
||||
rm -fr $(DEPLOY_DIR)
|
||||
|
@ -87,6 +89,16 @@ deploy-tflite:
|
|||
$(TFLITE_WASM)/*.wasm \
|
||||
$(DEPLOY_DIR)
|
||||
|
||||
deploy-excalidraw:
|
||||
cp -R \
|
||||
$(EXCALIDRAW_DIR) \
|
||||
$(DEPLOY_DIR)/
|
||||
|
||||
deploy-excalidraw-dev:
|
||||
cp -R \
|
||||
$(EXCALIDRAW_DIR_DEV) \
|
||||
$(DEPLOY_DIR)/
|
||||
|
||||
deploy-meet-models:
|
||||
cp \
|
||||
$(MEET_MODELS_DIR)/*.tflite \
|
||||
|
@ -109,7 +121,7 @@ deploy-local:
|
|||
([ ! -x deploy-local.sh ] || ./deploy-local.sh)
|
||||
|
||||
.NOTPARALLEL:
|
||||
dev: deploy-init deploy-css deploy-rnnoise-binary deploy-tflite deploy-meet-models deploy-lib-jitsi-meet deploy-olm deploy-tf-wasm deploy-face-landmarks
|
||||
dev: deploy-init deploy-css deploy-rnnoise-binary deploy-tflite deploy-meet-models deploy-lib-jitsi-meet deploy-olm deploy-tf-wasm deploy-excalidraw-dev deploy-face-landmarks
|
||||
$(WEBPACK_DEV_SERVER)
|
||||
|
||||
source-package:
|
||||
|
|
|
@ -1502,6 +1502,15 @@ var config = {
|
|||
|
||||
// Application logo url
|
||||
// defaultLogoUrl: 'images/watermark.svg',
|
||||
|
||||
// Settings for the Excalidraw whiteboard integration.
|
||||
// whiteboard: {
|
||||
// // Whether the feature is enabled or not.
|
||||
// enabled: true,
|
||||
// // The server used to support whiteboard collaboration.
|
||||
// // https://github.com/jitsi/excalidraw-backend
|
||||
// collabServerBaseUrl: 'https://excalidraw-backend.example.com',
|
||||
// },
|
||||
};
|
||||
|
||||
// Set the default values for JaaS customers
|
||||
|
|
|
@ -45,3 +45,7 @@
|
|||
margin: -16px -24px;
|
||||
z-index: $popoverZ;
|
||||
}
|
||||
|
||||
.excalidraw .popover {
|
||||
margin: 0;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
* tiled thumbnail experience.
|
||||
*/
|
||||
.tile-view,
|
||||
.whiteboard-container,
|
||||
.stage-filmstrip {
|
||||
/**
|
||||
* Let the avatar grow with the tile.
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
<link rel="manifest" id="manifest-placeholder">
|
||||
|
||||
<script>
|
||||
window.EXCALIDRAW_ASSET_PATH = 'libs/';
|
||||
// Dynamically generate the manifest location URL. It must be served from the document origin, and we may have
|
||||
// the base pointing to the CDN. This way we can generate a full URL which will bypass the base.
|
||||
document.querySelector('#manifest-placeholder').setAttribute('href', window.location.origin + '/manifest.json');
|
||||
|
|
|
@ -1115,7 +1115,8 @@
|
|||
"toggleFilmstrip": "Toggle filmstrip",
|
||||
"undock": "Undock into separate window",
|
||||
"videoblur": "Toggle video blur",
|
||||
"videomute": "Start / Stop camera"
|
||||
"videomute": "Start / Stop camera",
|
||||
"whiteboard": "Show / Hide whiteboard"
|
||||
},
|
||||
"addPeople": "Add people to your call",
|
||||
"audioOnlyOff": "Disable low bandwidth mode",
|
||||
|
@ -1146,6 +1147,7 @@
|
|||
"giphy": "Toggle GIPHY menu",
|
||||
"hangup": "Leave the meeting",
|
||||
"help": "Help",
|
||||
"hideWhiteboard": "Hide whiteboard",
|
||||
"invite": "Invite people",
|
||||
"joinBreakoutRoom": "Join breakout room",
|
||||
"laugh": "Laugh",
|
||||
|
@ -1191,6 +1193,7 @@
|
|||
"shareaudio": "Share audio",
|
||||
"sharedvideo": "Share video",
|
||||
"shortcuts": "View shortcuts",
|
||||
"showWhiteboard": "Show whiteboard",
|
||||
"silence": "Silence",
|
||||
"speakerStats": "Speaker stats",
|
||||
"startScreenSharing": "Start screen sharing",
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
"@giphy/react-components": "5.6.0",
|
||||
"@giphy/react-native-sdk": "1.7.0",
|
||||
"@hapi/bourne": "2.0.0",
|
||||
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.11/jitsi-excalidraw-0.0.11.tgz",
|
||||
"@jitsi/js-utils": "2.0.4",
|
||||
"@jitsi/logger": "2.0.0",
|
||||
"@jitsi/rnnoise-wasm": "0.1.0",
|
||||
|
@ -3760,6 +3761,16 @@
|
|||
"eslint": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@jitsi/excalidraw": {
|
||||
"version": "0.0.11",
|
||||
"resolved": "https://github.com/jitsi/excalidraw/releases/download/v0.0.11/jitsi-excalidraw-0.0.11.tgz",
|
||||
"integrity": "sha512-R0om5mYmjjozmJ6i5PXPSQy8/kiMTCqk/QVxOVryEUlHps4UeLNx+Gb/tjzNBN/C6db6ea+Vxt/l27nh9frphg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@jitsi/js-utils": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@jitsi/js-utils/-/js-utils-2.0.4.tgz",
|
||||
|
@ -23076,6 +23087,10 @@
|
|||
"integrity": "sha512-7ea1H2mpU5Z0azjcAsu0dx2T0fGzpcAfMmk2pEJ1xTQMp5YCwLYRu7ify9nupEbZUwnvJqyXoSeJQwwBQoX6Wg==",
|
||||
"dev": true
|
||||
},
|
||||
"@jitsi/excalidraw": {
|
||||
"version": "https://github.com/jitsi/excalidraw/releases/download/v0.0.11/jitsi-excalidraw-0.0.11.tgz",
|
||||
"integrity": "sha512-R0om5mYmjjozmJ6i5PXPSQy8/kiMTCqk/QVxOVryEUlHps4UeLNx+Gb/tjzNBN/C6db6ea+Vxt/l27nh9frphg=="
|
||||
},
|
||||
"@jitsi/js-utils": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@jitsi/js-utils/-/js-utils-2.0.4.tgz",
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
"@giphy/react-components": "5.6.0",
|
||||
"@giphy/react-native-sdk": "1.7.0",
|
||||
"@hapi/bourne": "2.0.0",
|
||||
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.11/jitsi-excalidraw-0.0.11.tgz",
|
||||
"@jitsi/js-utils": "2.0.4",
|
||||
"@jitsi/logger": "2.0.0",
|
||||
"@jitsi/rnnoise-wasm": "0.1.0",
|
||||
|
|
|
@ -22,5 +22,6 @@ import '../talk-while-muted/middleware';
|
|||
import '../virtual-background/middleware';
|
||||
import '../face-landmarks/middleware';
|
||||
import '../gifs/middleware';
|
||||
import '../whiteboard/middleware';
|
||||
|
||||
import './middlewares.any';
|
||||
|
|
|
@ -15,4 +15,6 @@ import '../noise-suppression/reducer';
|
|||
import '../screenshot-capture/reducer';
|
||||
import '../talk-while-muted/reducer';
|
||||
import '../virtual-background/reducer';
|
||||
import '../whiteboard/reducer';
|
||||
|
||||
import './reducers.any';
|
||||
|
|
|
@ -75,6 +75,7 @@ import { IVideoLayoutState } from '../video-layout/reducer';
|
|||
import { IVideoQualityPersistedState, IVideoQualityState } from '../video-quality/reducer';
|
||||
import { IVideoSipGW } from '../videosipgw/reducer';
|
||||
import { IVirtualBackground } from '../virtual-background/reducer';
|
||||
import { IWhiteboardState } from '../whiteboard/reducer';
|
||||
|
||||
export interface IStore {
|
||||
dispatch: ThunkDispatch<IState, void, AnyAction>;
|
||||
|
@ -159,4 +160,5 @@ export interface IState {
|
|||
'features/video-quality-persistent-storage': IVideoQualityPersistedState;
|
||||
'features/videosipgw': IVideoSipGW;
|
||||
'features/virtual-background': IVirtualBackground;
|
||||
'features/whiteboard': IWhiteboardState;
|
||||
}
|
||||
|
|
|
@ -492,4 +492,8 @@ export interface IConfig {
|
|||
webrtcIceUdpDisable?: boolean;
|
||||
websocket?: string;
|
||||
websocketKeepAliveUrl?: string;
|
||||
whiteboard?: {
|
||||
collabServerBaseUrl?: string;
|
||||
enabled?: boolean;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -230,5 +230,6 @@ export default [
|
|||
'useTurnUdp',
|
||||
'videoQuality',
|
||||
'webrtcIceTcpDisable',
|
||||
'webrtcIceUdpDisable'
|
||||
'webrtcIceUdpDisable',
|
||||
'whiteboard.enabled'
|
||||
].concat(extraConfigWhitelist);
|
||||
|
|
|
@ -47,7 +47,8 @@ export const TOOLBAR_BUTTONS = [
|
|||
'stats',
|
||||
'tileview',
|
||||
'toggle-camera',
|
||||
'videoquality'
|
||||
'videoquality',
|
||||
'whiteboard'
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
@ -151,3 +151,5 @@ export { default as IconWifi2Bars } from './wifi-2.svg';
|
|||
export { default as IconWifi3Bars } from './wifi-3.svg';
|
||||
export { default as IconYahoo } from './yahoo.svg';
|
||||
export { default as IconSip } from './sip.svg';
|
||||
export { default as IconShowWhiteboard } from './whiteboard-show.svg';
|
||||
export { default as IconHideWhiteboard } from './whiteboard-hide.svg';
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 5V14H9.0011H14.9994H20V5H4ZM16.7664 16H21C21.5523 16 22 15.5523 22 15V4C22 3.44772 21.5523 3 21 3H3C2.44772 3 2 3.44772 2 4V15C2 15.5523 2.44772 16 3 16H7.23405L5.14266 19.4857C4.85851 19.9592 5.01208 20.5735 5.48566 20.8576C5.95924 21.1418 6.5735 20.9882 6.85764 20.5146L9.56643 16H14.4341L17.1428 20.5146C17.427 20.9882 18.0413 21.1418 18.5148 20.8576C18.9884 20.5735 19.142 19.9592 18.8578 19.4857L16.7664 16Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.14686 3.47869C2.43485 3.00744 3.05034 2.85887 3.52159 3.14686L21.5216 14.1469C21.9928 14.4348 22.1414 15.0503 21.8534 15.5216C21.5654 15.9928 20.9499 16.1414 20.4787 15.8534L2.47869 4.85343C2.00744 4.56544 1.85887 3.94995 2.14686 3.47869Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 891 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 5V14H9.0011H14.9994H20V5H4ZM16.7664 16H21C21.5523 16 22 15.5523 22 15V4C22 3.44772 21.5523 3 21 3H3C2.44772 3 2 3.44772 2 4V15C2 15.5523 2.44772 16 3 16H7.23405L5.14266 19.4857C4.85851 19.9592 5.01208 20.5735 5.48566 20.8576C5.95924 21.1418 6.5735 20.9882 6.85764 20.5146L9.56643 16H14.4341L17.1428 20.5146C17.427 20.9882 18.0413 21.1418 18.5148 20.8576C18.9884 20.5735 19.142 19.9592 18.8578 19.4857L16.7664 16Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 584 B |
|
@ -407,6 +407,7 @@ export function participantLeft(id: string, conference: any, participantLeftProp
|
|||
id,
|
||||
isReplaced: participantLeftProps.isReplaced,
|
||||
isVirtualScreenshareParticipant: participantLeftProps.isVirtualScreenshareParticipant,
|
||||
isWhiteboard: participantLeftProps.isWhiteboard,
|
||||
isFakeParticipant: participantLeftProps.isFakeParticipant
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { IconPhone } from '../icons/svg';
|
||||
import { IconPhone, IconShowWhiteboard } from '../icons/svg';
|
||||
|
||||
/**
|
||||
* The relative path to the default/stock avatar (image) file used on both
|
||||
|
@ -75,3 +75,8 @@ export const PARTICIPANT_ROLE = {
|
|||
* @type {string}
|
||||
*/
|
||||
export const LOWER_HAND_AUDIO_LEVEL = 0.2;
|
||||
|
||||
/**
|
||||
* Icon URL for the whiteboard participant.
|
||||
*/
|
||||
export const WHITEBOARD_PARTICIPANT_ICON = IconShowWhiteboard;
|
||||
|
|
|
@ -16,7 +16,12 @@ import { toState } from '../redux/functions';
|
|||
import { getScreenShareTrack, getVideoTrackByParticipant } from '../tracks/functions';
|
||||
import { createDeferred } from '../util/helpers';
|
||||
|
||||
import { JIGASI_PARTICIPANT_ICON, MAX_DISPLAY_NAME_LENGTH, PARTICIPANT_ROLE } from './constants';
|
||||
import {
|
||||
JIGASI_PARTICIPANT_ICON,
|
||||
MAX_DISPLAY_NAME_LENGTH,
|
||||
PARTICIPANT_ROLE,
|
||||
WHITEBOARD_PARTICIPANT_ICON
|
||||
} from './constants';
|
||||
// @ts-ignore
|
||||
import { preloadImage } from './preloadImage';
|
||||
import { Participant } from './types';
|
||||
|
@ -31,6 +36,9 @@ const AVATAR_CHECKER_FUNCTIONS = [
|
|||
(participant: Participant) => {
|
||||
return participant?.isJigasi ? JIGASI_PARTICIPANT_ICON : null;
|
||||
},
|
||||
(participant: Participant) => {
|
||||
return participant?.isWhiteboard ? WHITEBOARD_PARTICIPANT_ICON : null;
|
||||
},
|
||||
(participant: Participant) => {
|
||||
return participant?.avatarURL ? participant.avatarURL : null;
|
||||
},
|
||||
|
|
|
@ -267,19 +267,19 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
}
|
||||
|
||||
case PARTICIPANT_JOINED: {
|
||||
const { isVirtualScreenshareParticipant } = action.participant;
|
||||
const { isVirtualScreenshareParticipant, isWhiteboard } = action.participant;
|
||||
|
||||
// Do not play sounds when a virtual participant tile is created for screenshare.
|
||||
!isVirtualScreenshareParticipant && _maybePlaySounds(store, action);
|
||||
(!isVirtualScreenshareParticipant && !isWhiteboard) && _maybePlaySounds(store, action);
|
||||
|
||||
return _participantJoinedOrUpdated(store, next, action);
|
||||
}
|
||||
|
||||
case PARTICIPANT_LEFT: {
|
||||
const { isVirtualScreenshareParticipant } = action.participant;
|
||||
const { isVirtualScreenshareParticipant, isWhiteboard } = action.participant;
|
||||
|
||||
// Do not play sounds when a tile for screenshare is removed.
|
||||
!isVirtualScreenshareParticipant && _maybePlaySounds(store, action);
|
||||
(!isVirtualScreenshareParticipant && !isWhiteboard) && _maybePlaySounds(store, action);
|
||||
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -514,6 +514,7 @@ function _participantJoined({ participant }: { participant: Participant; }) {
|
|||
isLocalScreenShare,
|
||||
isReplacing,
|
||||
isJigasi,
|
||||
isWhiteboard,
|
||||
loadableAvatarUrl,
|
||||
local,
|
||||
name,
|
||||
|
@ -548,6 +549,7 @@ function _participantJoined({ participant }: { participant: Participant; }) {
|
|||
isLocalScreenShare,
|
||||
isReplacing,
|
||||
isJigasi,
|
||||
isWhiteboard,
|
||||
loadableAvatarUrl,
|
||||
local: local || false,
|
||||
name,
|
||||
|
|
|
@ -19,6 +19,7 @@ export interface Participant {
|
|||
isReplaced?: boolean;
|
||||
isReplacing?: number;
|
||||
isVirtualScreenshareParticipant?: boolean;
|
||||
isWhiteboard?: boolean;
|
||||
jwtId?: string;
|
||||
loadableAvatarUrl?: string;
|
||||
loadableAvatarUrlUseCORS?: boolean;
|
||||
|
|
|
@ -60,7 +60,12 @@ const StageParticipantNameLabel = () => {
|
|||
const toolboxVisible: boolean = useSelector(isToolboxVisible);
|
||||
const showDisplayName = useSelector(isDisplayNameVisible);
|
||||
|
||||
if (showDisplayName && nameToDisplay && selectedId !== localId && !isTileView) {
|
||||
if (showDisplayName
|
||||
&& nameToDisplay
|
||||
&& selectedId !== localId
|
||||
&& !isTileView
|
||||
&& !largeVideoParticipant?.isWhiteboard
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
className = { cx(
|
||||
|
|
|
@ -163,10 +163,10 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
|
||||
case PARTICIPANT_LEFT: {
|
||||
const { participant } = action;
|
||||
const { isFakeParticipant, isVirtualScreenshareParticipant } = participant;
|
||||
const { isFakeParticipant, isVirtualScreenshareParticipant, isWhiteboard } = participant;
|
||||
|
||||
// Skip sending participant left event for fake or virtual screenshare participants.
|
||||
if (isFakeParticipant || isVirtualScreenshareParticipant) {
|
||||
if (isFakeParticipant || isVirtualScreenshareParticipant || isWhiteboard) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
@ -1050,6 +1050,7 @@ class Thumbnail extends Component<Props, State> {
|
|||
_thumbnailType === THUMBNAIL_TYPE.TILE && 'tile-view-mode'
|
||||
) }>
|
||||
<ThumbnailTopIndicators
|
||||
disableConnectionIndicator = { _participant?.isWhiteboard }
|
||||
hidePopover = { this._hidePopover }
|
||||
indicatorsClassName = { classes.indicatorsBackground }
|
||||
isHovered = { isHovered }
|
||||
|
@ -1069,6 +1070,7 @@ class Thumbnail extends Component<Props, State> {
|
|||
isVirtualScreenshareParticipant = { false }
|
||||
local = { local }
|
||||
participantId = { id }
|
||||
showStatusIndicators = { !_participant?.isWhiteboard }
|
||||
thumbnailType = { _thumbnailType } />
|
||||
</div>
|
||||
{!_gifSrc && this._renderAvatar(styles.avatar) }
|
||||
|
@ -1113,13 +1115,13 @@ class Thumbnail extends Component<Props, State> {
|
|||
return null;
|
||||
}
|
||||
|
||||
const { isFakeParticipant, isLocalScreenShare, local } = _participant;
|
||||
const { isFakeParticipant, isLocalScreenShare, isWhiteboard, local } = _participant;
|
||||
|
||||
if (local) {
|
||||
return this._renderParticipant(true);
|
||||
}
|
||||
|
||||
if (isFakeParticipant) {
|
||||
if (isFakeParticipant && !isWhiteboard) {
|
||||
return this._renderFakeParticipant();
|
||||
}
|
||||
|
||||
|
|
|
@ -26,6 +26,11 @@ declare let interfaceConfig: any;
|
|||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Whether to hide the connection indicator.
|
||||
*/
|
||||
disableConnectionIndicator?: boolean;
|
||||
|
||||
/**
|
||||
* Hide popover callback.
|
||||
*/
|
||||
|
@ -85,6 +90,7 @@ const useStyles = makeStyles()(() => {
|
|||
});
|
||||
|
||||
const ThumbnailTopIndicators = ({
|
||||
disableConnectionIndicator,
|
||||
hidePopover,
|
||||
indicatorsClassName,
|
||||
isVirtualScreenshareParticipant,
|
||||
|
@ -102,7 +108,7 @@ const ThumbnailTopIndicators = ({
|
|||
const _indicatorIconSize = NORMAL;
|
||||
const _connectionIndicatorAutoHideEnabled = Boolean(
|
||||
useSelector((state: IState) => state['features/base/config'].connectionIndicators?.autoHide) ?? true);
|
||||
const _connectionIndicatorDisabled = _isMobile
|
||||
const _connectionIndicatorDisabled = _isMobile || disableConnectionIndicator
|
||||
|| Boolean(useSelector((state: IState) => state['features/base/config'].connectionIndicators?.disabled));
|
||||
const _isMultiStreamEnabled = useSelector(getMultipleVideoSupportFeatureFlag);
|
||||
const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled;
|
||||
|
|
|
@ -17,6 +17,8 @@ import { getLargeVideoParticipant } from '../../large-video/functions';
|
|||
import { SharedVideo } from '../../shared-video/components/web';
|
||||
import { Captions } from '../../subtitles/';
|
||||
import { setTileView } from '../../video-layout/actions';
|
||||
import Whiteboard from '../../whiteboard/components/web/Whiteboard';
|
||||
import { isWhiteboardEnabled } from '../../whiteboard/functions';
|
||||
import { setSeeWhatIsBeingShared } from '../actions.web';
|
||||
|
||||
import ScreenSharePlaceholder from './ScreenSharePlaceholder.web';
|
||||
|
@ -87,7 +89,7 @@ type Props = {
|
|||
/**
|
||||
* The large video participant id.
|
||||
*/
|
||||
_largeVideoParticipantId: string,
|
||||
_largeVideoParticipantId: string,
|
||||
|
||||
/**
|
||||
* Whether or not the local screen share is on large-video.
|
||||
|
@ -97,7 +99,12 @@ type Props = {
|
|||
/**
|
||||
* Whether or not the screen sharing is visible.
|
||||
*/
|
||||
_seeWhatIsBeingShared: boolean,
|
||||
_seeWhatIsBeingShared: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the whiteboard is enabled.
|
||||
*/
|
||||
_whiteboardEnabled: boolean;
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
|
@ -166,7 +173,8 @@ class LargeVideo extends Component<Props> {
|
|||
_displayScreenSharingPlaceholder,
|
||||
_isChatOpen,
|
||||
_noAutoPlayVideo,
|
||||
_showDominantSpeakerBadge
|
||||
_showDominantSpeakerBadge,
|
||||
_whiteboardEnabled
|
||||
} = this.props;
|
||||
const style = this._getCustomStyles();
|
||||
const className = `videocontainer${_isChatOpen ? ' shift-right' : ''}`;
|
||||
|
@ -178,6 +186,7 @@ class LargeVideo extends Component<Props> {
|
|||
ref = { this._containerRef }
|
||||
style = { style }>
|
||||
<SharedVideo />
|
||||
{_whiteboardEnabled && <Whiteboard />}
|
||||
<div id = 'etherpad' />
|
||||
|
||||
<Watermarks />
|
||||
|
@ -361,7 +370,8 @@ function _mapStateToProps(state) {
|
|||
_showDominantSpeakerBadge: !hideDominantSpeakerBadge,
|
||||
_verticalFilmstripWidth: verticalFilmstripWidth.current,
|
||||
_verticalViewMaxWidth: getVerticalViewMaxWidth(state),
|
||||
_visibleFilmstrip: visible
|
||||
_visibleFilmstrip: visible,
|
||||
_whiteboardEnabled: isWhiteboardEnabled(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -129,10 +129,11 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
const { participant: p } = action;
|
||||
const { conference } = state['features/base/conference'];
|
||||
|
||||
// Do not display notifications for the virtual screenshare tiles.
|
||||
// Do not display notifications for the virtual screenshare and whiteboard tiles.
|
||||
if (conference
|
||||
&& !p.local
|
||||
&& !p.isVirtualScreenshareParticipant
|
||||
&& !p.isWhiteboard
|
||||
&& !joinLeaveNotificationsDisabled()
|
||||
&& !p.isReplacing) {
|
||||
dispatch(showParticipantJoinedNotification(
|
||||
|
@ -153,6 +154,7 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
if (participant
|
||||
&& !participant.local
|
||||
&& !participant.isVirtualScreenshareParticipant
|
||||
&& !participant.isWhiteboard
|
||||
&& !action.participant.isReplaced) {
|
||||
dispatch(showParticipantLeftNotification(
|
||||
getParticipantDisplayName(state, participant.id)
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
getParticipantByIdOrUndefined
|
||||
} from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import FakeParticipantContextMenu from '../../../video-menu/components/web/FakeParticipantContextMenu';
|
||||
import ParticipantContextMenu from '../../../video-menu/components/web/ParticipantContextMenu';
|
||||
|
||||
type Props = {
|
||||
|
@ -90,18 +91,26 @@ class MeetingParticipantContextMenu extends Component<Props> {
|
|||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ParticipantContextMenu
|
||||
closeDrawer = { closeDrawer }
|
||||
drawerParticipant = { drawerParticipant }
|
||||
localVideoOwner = { _localVideoOwner }
|
||||
offsetTarget = { offsetTarget }
|
||||
onEnter = { onEnter }
|
||||
onLeave = { onLeave }
|
||||
onSelect = { onSelect }
|
||||
participant = { _participant }
|
||||
thumbnailMenu = { false } />
|
||||
);
|
||||
const props = {
|
||||
closeDrawer,
|
||||
drawerParticipant,
|
||||
offsetTarget,
|
||||
onEnter,
|
||||
onLeave,
|
||||
onSelect,
|
||||
participant: _participant,
|
||||
thumbnailMenu: false
|
||||
};
|
||||
|
||||
if (_participant?.isFakeParticipant) {
|
||||
return (
|
||||
<FakeParticipantContextMenu
|
||||
{ ...props }
|
||||
localVideoOwner = { _localVideoOwner } />
|
||||
);
|
||||
}
|
||||
|
||||
return <ParticipantContextMenu { ...props } />;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -251,7 +251,12 @@ function MeetingParticipantItem({
|
|||
return (
|
||||
<ParticipantItem
|
||||
actionsTrigger = { ACTION_TRIGGER.HOVER }
|
||||
audioMediaState = { audioMediaState }
|
||||
{
|
||||
...(_participant?.isFakeParticipant ? {} : {
|
||||
audioMediaState,
|
||||
videoMediaState: _videoMediaState
|
||||
})
|
||||
}
|
||||
disableModeratorIndicator = { _disableModeratorIndicator }
|
||||
displayName = { _displayName }
|
||||
isHighlighted = { isHighlighted }
|
||||
|
@ -262,7 +267,6 @@ function MeetingParticipantItem({
|
|||
overflowDrawer = { overflowDrawer }
|
||||
participantID = { _participantID }
|
||||
raisedHand = { _raisedHand }
|
||||
videoMediaState = { _videoMediaState }
|
||||
youText = { youText }>
|
||||
|
||||
{!overflowDrawer && !_participant?.isFakeParticipant
|
||||
|
@ -282,7 +286,7 @@ function MeetingParticipantItem({
|
|||
</>
|
||||
}
|
||||
|
||||
{!overflowDrawer && _localVideoOwner && _participant?.isFakeParticipant && (
|
||||
{!overflowDrawer && (_localVideoOwner || _participant?.isWhiteboard) && _participant?.isFakeParticipant && (
|
||||
<ParticipantActionEllipsis
|
||||
accessibilityLabel = { participantActionEllipsisLabel }
|
||||
onClick = { onContextMenu } />
|
||||
|
|
|
@ -102,6 +102,8 @@ import { VideoQualityButton, VideoQualityDialog } from '../../../video-quality/c
|
|||
// @ts-ignore
|
||||
import { VideoBackgroundButton, toggleBackgroundEffect } from '../../../virtual-background';
|
||||
import { VIRTUAL_BACKGROUND_TYPE } from '../../../virtual-background/constants';
|
||||
import WhiteboardButton from '../../../whiteboard/components/web/WhiteboardButton';
|
||||
import { isWhiteboardEnabled } from '../../../whiteboard/functions';
|
||||
import {
|
||||
setFullScreen,
|
||||
setHangupMenuVisible,
|
||||
|
@ -319,6 +321,11 @@ interface Props extends WithTranslation {
|
|||
*/
|
||||
_visible: boolean;
|
||||
|
||||
/**
|
||||
* Whether the whiteboard is visible.
|
||||
*/
|
||||
_whiteboardEnabled: boolean;
|
||||
|
||||
/**
|
||||
* An object containing the CSS classes.
|
||||
*/
|
||||
|
@ -714,7 +721,8 @@ class Toolbox extends Component<Props> {
|
|||
_isMobile,
|
||||
_hasSalesforce,
|
||||
_multiStreamModeEnabled,
|
||||
_screenSharing
|
||||
_screenSharing,
|
||||
_whiteboardEnabled
|
||||
} = this.props;
|
||||
|
||||
const microphone = {
|
||||
|
@ -845,6 +853,12 @@ class Toolbox extends Component<Props> {
|
|||
};
|
||||
|
||||
|
||||
const whiteboard = _whiteboardEnabled && {
|
||||
key: 'whiteboard',
|
||||
Content: WhiteboardButton,
|
||||
group: 3
|
||||
};
|
||||
|
||||
const etherpad = {
|
||||
key: 'etherpad',
|
||||
Content: SharedDocumentButton,
|
||||
|
@ -932,6 +946,7 @@ class Toolbox extends Component<Props> {
|
|||
shareVideo,
|
||||
shareAudio,
|
||||
noiseSuppression,
|
||||
whiteboard,
|
||||
etherpad,
|
||||
virtualBackground,
|
||||
dockIframe,
|
||||
|
@ -1533,7 +1548,8 @@ function _mapStateToProps(state: IState, ownProps: Partial<Props>) {
|
|||
_tileViewEnabled: shouldDisplayTileView(state),
|
||||
_toolbarButtons: toolbarButtons,
|
||||
_virtualSource: state['features/virtual-background'].virtualSource,
|
||||
_visible: isToolboxVisible(state)
|
||||
_visible: isToolboxVisible(state),
|
||||
_whiteboardEnabled: isWhiteboardEnabled(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import { IState } from '../app/types';
|
|||
import { getToolbarButtons } from '../base/config/functions.web';
|
||||
import { hasAvailableDevices } from '../base/devices/functions';
|
||||
import { isScreenMediaShared } from '../screen-share/functions';
|
||||
import { isWhiteboardVisible } from '../whiteboard/functions';
|
||||
|
||||
import { TOOLBAR_TIMEOUT } from './constants';
|
||||
|
||||
|
@ -48,9 +49,17 @@ export function isToolboxVisible(state: IState) {
|
|||
visible
|
||||
} = state['features/toolbox'];
|
||||
const { audioSettingsVisible, videoSettingsVisible } = state['features/settings'];
|
||||
const whiteboardVisible = isWhiteboardVisible(state);
|
||||
|
||||
return Boolean(!iAmRecorder && !iAmSipGateway
|
||||
&& (timeoutID || visible || alwaysVisible || audioSettingsVisible || videoSettingsVisible));
|
||||
&& (
|
||||
timeoutID
|
||||
|| visible
|
||||
|| alwaysVisible
|
||||
|| audioSettingsVisible
|
||||
|| videoSettingsVisible
|
||||
|| whiteboardVisible
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
/* eslint-disable lines-around-comment */
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
// @ts-ignore
|
||||
import TogglePinToStageButton from '../../../../features/video-menu/components/web/TogglePinToStageButton';
|
||||
// @ts-ignore
|
||||
import { Avatar } from '../../../base/avatar';
|
||||
import ContextMenu from '../../../base/components/context-menu/ContextMenu';
|
||||
import ContextMenuItemGroup from '../../../base/components/context-menu/ContextMenuItemGroup';
|
||||
import { IconShareVideo } from '../../../base/icons/svg';
|
||||
import { Participant } from '../../../base/participants/types';
|
||||
// @ts-ignore
|
||||
import { stopSharedVideo } from '../../../shared-video/actions.any';
|
||||
// @ts-ignore
|
||||
import { showOverflowDrawer } from '../../../toolbox/functions.web';
|
||||
// @ts-ignore
|
||||
import { setWhiteboardOpen } from '../../../whiteboard/actions';
|
||||
import { WHITEBOARD_ID } from '../../../whiteboard/constants';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Class name for the context menu.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* Closes a drawer if open.
|
||||
*/
|
||||
closeDrawer?: () => void;
|
||||
|
||||
/**
|
||||
* The participant for which the drawer is open.
|
||||
* It contains the displayName & participantID.
|
||||
*/
|
||||
drawerParticipant?: {
|
||||
displayName: string;
|
||||
participantID: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Shared video local participant owner.
|
||||
*/
|
||||
localVideoOwner?: boolean;
|
||||
|
||||
/**
|
||||
* Target elements against which positioning calculations are made.
|
||||
*/
|
||||
offsetTarget?: HTMLElement;
|
||||
|
||||
/**
|
||||
* Callback for the mouse entering the component.
|
||||
*/
|
||||
onEnter?: (e?: React.MouseEvent) => void;
|
||||
|
||||
/**
|
||||
* Callback for the mouse leaving the component.
|
||||
*/
|
||||
onLeave?: (e?: React.MouseEvent) => void;
|
||||
|
||||
/**
|
||||
* Callback for making a selection in the menu.
|
||||
*/
|
||||
onSelect: (value?: boolean | React.MouseEvent) => void;
|
||||
|
||||
/**
|
||||
* Participant reference.
|
||||
*/
|
||||
participant: Participant;
|
||||
|
||||
/**
|
||||
* Whether or not the menu is displayed in the thumbnail remote video menu.
|
||||
*/
|
||||
thumbnailMenu?: boolean;
|
||||
};
|
||||
|
||||
const FakeParticipantContextMenu = ({
|
||||
className,
|
||||
closeDrawer,
|
||||
drawerParticipant,
|
||||
localVideoOwner,
|
||||
offsetTarget,
|
||||
onEnter,
|
||||
onLeave,
|
||||
onSelect,
|
||||
participant,
|
||||
thumbnailMenu
|
||||
}: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const _overflowDrawer: boolean = useSelector(showOverflowDrawer);
|
||||
|
||||
const clickHandler = useCallback(() => onSelect(true), [ onSelect ]);
|
||||
|
||||
const _onStopSharedVideo = useCallback(() => {
|
||||
clickHandler();
|
||||
dispatch(stopSharedVideo());
|
||||
}, [ stopSharedVideo ]);
|
||||
|
||||
const _onHideWhiteboard = useCallback(() => {
|
||||
clickHandler();
|
||||
dispatch(setWhiteboardOpen(false));
|
||||
}, [ setWhiteboardOpen ]);
|
||||
|
||||
const _getActions = useCallback(() => {
|
||||
if (participant.isWhiteboard) {
|
||||
return [ {
|
||||
accessibilityLabel: t('toolbar.hideWhiteboard'),
|
||||
icon: IconShareVideo,
|
||||
onClick: _onHideWhiteboard,
|
||||
text: t('toolbar.hideWhiteboard')
|
||||
} ];
|
||||
}
|
||||
|
||||
if (localVideoOwner) {
|
||||
return [ {
|
||||
accessibilityLabel: t('toolbar.stopSharedVideo'),
|
||||
icon: IconShareVideo,
|
||||
onClick: _onStopSharedVideo,
|
||||
text: t('toolbar.stopSharedVideo')
|
||||
} ];
|
||||
}
|
||||
}, [ localVideoOwner, participant.isWhiteboard ]);
|
||||
|
||||
return (
|
||||
<ContextMenu
|
||||
className = { className }
|
||||
entity = { participant }
|
||||
hidden = { thumbnailMenu ? false : undefined }
|
||||
inDrawer = { thumbnailMenu && _overflowDrawer }
|
||||
isDrawerOpen = { Boolean(drawerParticipant) }
|
||||
offsetTarget = { offsetTarget }
|
||||
onClick = { onSelect }
|
||||
onDrawerClose = { thumbnailMenu ? onSelect : closeDrawer }
|
||||
onMouseEnter = { onEnter }
|
||||
onMouseLeave = { onLeave }>
|
||||
{!thumbnailMenu && _overflowDrawer && drawerParticipant && <ContextMenuItemGroup
|
||||
actions = { [ {
|
||||
accessibilityLabel: drawerParticipant.displayName,
|
||||
customIcon: <Avatar
|
||||
participantId = { drawerParticipant.participantID }
|
||||
size = { 20 } />,
|
||||
text: drawerParticipant.displayName
|
||||
} ] } />}
|
||||
|
||||
<ContextMenuItemGroup
|
||||
actions = { _getActions() }>
|
||||
{participant.isWhiteboard && <TogglePinToStageButton
|
||||
key = 'pinToStage'
|
||||
participantID = { WHITEBOARD_ID } />}
|
||||
</ContextMenuItemGroup>
|
||||
|
||||
</ContextMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default FakeParticipantContextMenu;
|
|
@ -14,7 +14,6 @@ import { Avatar } from '../../../base/avatar';
|
|||
import ContextMenu from '../../../base/components/context-menu/ContextMenu';
|
||||
import ContextMenuItemGroup from '../../../base/components/context-menu/ContextMenuItemGroup';
|
||||
import { isIosMobileBrowser, isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { IconShareVideo } from '../../../base/icons/svg';
|
||||
import { MEDIA_TYPE } from '../../../base/media/constants';
|
||||
import { PARTICIPANT_ROLE } from '../../../base/participants/constants';
|
||||
import { getLocalParticipant } from '../../../base/participants/functions';
|
||||
|
@ -31,8 +30,6 @@ import { isForceMuted } from '../../../participants-pane/functions';
|
|||
// @ts-ignore
|
||||
import { requestRemoteControl, stopController } from '../../../remote-control';
|
||||
// @ts-ignore
|
||||
import { stopSharedVideo } from '../../../shared-video/actions.any';
|
||||
// @ts-ignore
|
||||
import { showOverflowDrawer } from '../../../toolbox/functions.web';
|
||||
|
||||
// @ts-ignore
|
||||
|
@ -77,11 +74,6 @@ type Props = {
|
|||
participantID: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Shared video local participant owner.
|
||||
*/
|
||||
localVideoOwner?: boolean;
|
||||
|
||||
/**
|
||||
* Target elements against which positioning calculations are made.
|
||||
*/
|
||||
|
@ -136,7 +128,6 @@ const ParticipantContextMenu = ({
|
|||
className,
|
||||
closeDrawer,
|
||||
drawerParticipant,
|
||||
localVideoOwner,
|
||||
offsetTarget,
|
||||
onEnter,
|
||||
onLeave,
|
||||
|
@ -176,11 +167,6 @@ const ParticipantContextMenu = ({
|
|||
|
||||
const clickHandler = useCallback(() => onSelect(true), [ onSelect ]);
|
||||
|
||||
const _onStopSharedVideo = useCallback(() => {
|
||||
clickHandler();
|
||||
dispatch(stopSharedVideo());
|
||||
}, [ stopSharedVideo ]);
|
||||
|
||||
const _getCurrentParticipantId = useCallback(() => {
|
||||
const drawer = _overflowDrawer && !thumbnailMenu;
|
||||
|
||||
|
@ -197,13 +183,6 @@ const ParticipantContextMenu = ({
|
|||
&& typeof _volume === 'number'
|
||||
&& !isNaN(_volume);
|
||||
|
||||
const fakeParticipantActions = [ {
|
||||
accessibilityLabel: t('toolbar.stopSharedVideo'),
|
||||
icon: IconShareVideo,
|
||||
onClick: _onStopSharedVideo,
|
||||
text: t('toolbar.stopSharedVideo')
|
||||
} ];
|
||||
|
||||
if (_isModerator) {
|
||||
if ((thumbnailMenu || _overflowDrawer) && isModerationSupported && _isAudioMuted) {
|
||||
buttons.push(<AskToUnmuteButton
|
||||
|
@ -328,36 +307,29 @@ const ParticipantContextMenu = ({
|
|||
size = { 20 } />,
|
||||
text: drawerParticipant.displayName
|
||||
} ] } />}
|
||||
{participant?.isFakeParticipant ? localVideoOwner && (
|
||||
<ContextMenuItemGroup
|
||||
actions = { fakeParticipantActions } />
|
||||
) : (
|
||||
<>
|
||||
{buttons.length > 0 && (
|
||||
<ContextMenuItemGroup>
|
||||
{buttons}
|
||||
</ContextMenuItemGroup>
|
||||
)}
|
||||
<ContextMenuItemGroup>
|
||||
{buttons2}
|
||||
</ContextMenuItemGroup>
|
||||
{showVolumeSlider && (
|
||||
<ContextMenuItemGroup>
|
||||
<VolumeSlider
|
||||
initialValue = { _volume }
|
||||
key = 'volume-slider'
|
||||
onChange = { _onVolumeChange } />
|
||||
</ContextMenuItemGroup>
|
||||
)}
|
||||
{breakoutRoomsButtons.length > 0 && (
|
||||
<ContextMenuItemGroup>
|
||||
<div className = { styles.text }>
|
||||
{t('breakoutRooms.actions.sendToBreakoutRoom')}
|
||||
</div>
|
||||
{breakoutRoomsButtons}
|
||||
</ContextMenuItemGroup>
|
||||
)}
|
||||
</>
|
||||
{buttons.length > 0 && (
|
||||
<ContextMenuItemGroup>
|
||||
{buttons}
|
||||
</ContextMenuItemGroup>
|
||||
)}
|
||||
<ContextMenuItemGroup>
|
||||
{buttons2}
|
||||
</ContextMenuItemGroup>
|
||||
{showVolumeSlider && (
|
||||
<ContextMenuItemGroup>
|
||||
<VolumeSlider
|
||||
initialValue = { _volume }
|
||||
key = 'volume-slider'
|
||||
onChange = { _onVolumeChange } />
|
||||
</ContextMenuItemGroup>
|
||||
)}
|
||||
{breakoutRoomsButtons.length > 0 && (
|
||||
<ContextMenuItemGroup>
|
||||
<div className = { styles.text }>
|
||||
{t('breakoutRooms.actions.sendToBreakoutRoom')}
|
||||
</div>
|
||||
{breakoutRoomsButtons}
|
||||
</ContextMenuItemGroup>
|
||||
)}
|
||||
</ContextMenu>
|
||||
);
|
||||
|
|
|
@ -9,7 +9,7 @@ import { isMobileBrowser } from '../../../base/environment/utils';
|
|||
// @ts-ignore
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { IconHorizontalPoints } from '../../../base/icons/svg';
|
||||
import { getParticipantById } from '../../../base/participants/functions';
|
||||
import { getLocalParticipant, getParticipantById } from '../../../base/participants/functions';
|
||||
import { Participant } from '../../../base/participants/types';
|
||||
// @ts-ignore
|
||||
import { Popover } from '../../../base/popover';
|
||||
|
@ -24,7 +24,7 @@ import { THUMBNAIL_TYPE } from '../../../filmstrip/constants';
|
|||
// @ts-ignore
|
||||
import { renderConnectionStatus } from '../../actions.web';
|
||||
|
||||
// @ts-ignore
|
||||
import FakeParticipantContextMenu from './FakeParticipantContextMenu';
|
||||
import ParticipantContextMenu from './ParticipantContextMenu';
|
||||
// @ts-ignore
|
||||
import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton';
|
||||
|
@ -40,6 +40,11 @@ interface Props extends WithTranslation {
|
|||
*/
|
||||
_disabled: Boolean;
|
||||
|
||||
/**
|
||||
* Shared video local participant owner.
|
||||
*/
|
||||
_localVideoOwner?: boolean;
|
||||
|
||||
/**
|
||||
* The position relative to the trigger the remote menu should display
|
||||
* from. Valid values are those supported by AtlasKit
|
||||
|
@ -237,15 +242,27 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderRemoteVideoMenu() {
|
||||
const { _participant, _remoteControlState, classes } = this.props;
|
||||
const { _localVideoOwner, _participant, _remoteControlState, classes } = this.props;
|
||||
|
||||
const props = {
|
||||
className: classes.contextMenu,
|
||||
onSelect: this._onPopoverClose,
|
||||
participant: _participant,
|
||||
thumbnailMenu: true
|
||||
};
|
||||
|
||||
if (_participant?.isFakeParticipant) {
|
||||
return (
|
||||
<FakeParticipantContextMenu
|
||||
{ ...props }
|
||||
localVideoOwner = { _localVideoOwner } />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ParticipantContextMenu
|
||||
className = { classes.contextMenu }
|
||||
onSelect = { this._onPopoverClose }
|
||||
participant = { _participant }
|
||||
remoteControlState = { _remoteControlState }
|
||||
thumbnailMenu = { true } />
|
||||
{ ...props }
|
||||
remoteControlState = { _remoteControlState } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -261,6 +278,7 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
|||
function _mapStateToProps(state: IState, ownProps: Partial<Props>) {
|
||||
const { participantID, thumbnailType } = ownProps;
|
||||
let _remoteControlState = null;
|
||||
const localParticipantId = getLocalParticipant(state)?.id;
|
||||
const participant = getParticipantById(state, participantID ?? '');
|
||||
const _participantDisplayName = participant?.name;
|
||||
const _isRemoteControlSessionActive = participant?.remoteControlSessionStatus ?? false;
|
||||
|
@ -271,6 +289,7 @@ function _mapStateToProps(state: IState, ownProps: Partial<Props>) {
|
|||
const { overflowDrawer } = state['features/toolbox'];
|
||||
const { showConnectionInfo } = state['features/base/connection'];
|
||||
const { remoteVideoMenu } = state['features/base/config'];
|
||||
const { ownerId } = state['features/shared-video'];
|
||||
|
||||
if (_supportsRemoteControl
|
||||
&& ((!active && !_isRemoteControlSessionActive) || activeParticipant === participantID)) {
|
||||
|
@ -301,6 +320,7 @@ function _mapStateToProps(state: IState, ownProps: Partial<Props>) {
|
|||
|
||||
return {
|
||||
_disabled: remoteVideoMenu?.disabled,
|
||||
_localVideoOwner: Boolean(ownerId === localParticipantId),
|
||||
_menuPosition,
|
||||
_overflowDrawer: overflowDrawer,
|
||||
_participant: participant ?? { id: '' },
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* Close the whiteboard collaboration session.
|
||||
* {{
|
||||
* type: RESET_WHITEBOARD
|
||||
* }}
|
||||
*/
|
||||
export const RESET_WHITEBOARD: string = 'RESET_WHITEBOARD';
|
||||
|
||||
/**
|
||||
* Configure the whiteboard collaboration details.
|
||||
* {{
|
||||
* type: SETUP_WHITEBOARD,
|
||||
* collabDetails
|
||||
* }}
|
||||
*/
|
||||
export const SETUP_WHITEBOARD: string = 'SETUP_WHITEBOARD';
|
||||
|
||||
/**
|
||||
* Sets the whiteboard visibility state.
|
||||
* {{
|
||||
* type: SET_WHITEBOARD_OPEN,
|
||||
* isOpen
|
||||
* }}
|
||||
*/
|
||||
export const SET_WHITEBOARD_OPEN: string = 'SET_WHITEBOARD_OPEN';
|
|
@ -0,0 +1,52 @@
|
|||
import {
|
||||
RESET_WHITEBOARD,
|
||||
SETUP_WHITEBOARD,
|
||||
SET_WHITEBOARD_OPEN
|
||||
} from './actionTypes';
|
||||
import { WhiteboardAction } 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; };
|
||||
}): WhiteboardAction => {
|
||||
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 = (): WhiteboardAction => {
|
||||
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): WhiteboardAction => {
|
||||
return {
|
||||
type: SET_WHITEBOARD_OPEN,
|
||||
isOpen
|
||||
};
|
||||
};
|
|
@ -0,0 +1,142 @@
|
|||
/* eslint-disable import/order */
|
||||
/* eslint-disable lines-around-comment */
|
||||
|
||||
import { ExcalidrawApp } from '@jitsi/excalidraw';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import clsx from 'clsx';
|
||||
|
||||
// @ts-ignore
|
||||
import Filmstrip from '../../../../../modules/UI/videolayout/Filmstrip';
|
||||
// @ts-ignore
|
||||
import { getVerticalViewMaxWidth } from '../../../filmstrip/functions.web';
|
||||
// @ts-ignore
|
||||
import { getToolboxHeight } from '../../../toolbox/functions.web';
|
||||
import {
|
||||
getCollabDetails,
|
||||
getCollabServerUrl,
|
||||
isWhiteboardOpen,
|
||||
isWhiteboardVisible
|
||||
} from '../../functions';
|
||||
// @ts-ignore
|
||||
import { shouldDisplayTileView } from '../../../video-layout/functions.any';
|
||||
import { getLocalParticipant } from '../../../base/participants/functions';
|
||||
import { WHITEBOARD_UI_OPTIONS } from '../../constants';
|
||||
import { IState } from '../../../app/types';
|
||||
|
||||
/**
|
||||
* Space taken by meeting elements like the subject and the watermark.
|
||||
*/
|
||||
const HEIGHT_OFFSET = 80;
|
||||
|
||||
declare const interfaceConfig: any;
|
||||
|
||||
interface IDimensions {
|
||||
|
||||
/* The height of the component. */
|
||||
height: string;
|
||||
|
||||
/* The width of the component. */
|
||||
width: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Whiteboard component.
|
||||
*
|
||||
* @returns {JSX.Element} - The React component.
|
||||
*/
|
||||
const Whiteboard: () => JSX.Element = () => {
|
||||
const excalidrawRef = useRef<any>(null);
|
||||
const collabAPIRef = useRef<any>(null);
|
||||
|
||||
const isOpen = useSelector(isWhiteboardOpen);
|
||||
const isVisible = useSelector(isWhiteboardVisible);
|
||||
const isInTileView = useSelector(shouldDisplayTileView);
|
||||
const { clientHeight, clientWidth } = useSelector((state: IState) => state['features/base/responsive-ui']);
|
||||
const { visible: filmstripVisible, isResizing } = useSelector((state: IState) => state['features/filmstrip']);
|
||||
const filmstripWidth: number = useSelector(getVerticalViewMaxWidth);
|
||||
const collabDetails = useSelector(getCollabDetails);
|
||||
const collabServerUrl = useSelector(getCollabServerUrl);
|
||||
const { defaultRemoteDisplayName } = useSelector((state: IState) => state['features/base/config']);
|
||||
const localParticipantName = useSelector(getLocalParticipant)?.name || defaultRemoteDisplayName || 'Fellow Jitster';
|
||||
|
||||
useEffect(() => {
|
||||
if (!collabAPIRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
collabAPIRef.current.setUsername(localParticipantName);
|
||||
}, [ localParticipantName ]);
|
||||
|
||||
/**
|
||||
* Computes the width and the height of the component.
|
||||
*
|
||||
* @returns {IDimensions} - The dimensions of the component.
|
||||
*/
|
||||
const getDimensions = (): IDimensions => {
|
||||
let width: number;
|
||||
let height: number;
|
||||
|
||||
if (interfaceConfig.VERTICAL_FILMSTRIP) {
|
||||
if (filmstripVisible) {
|
||||
width = clientWidth - filmstripWidth;
|
||||
} else {
|
||||
width = clientWidth;
|
||||
}
|
||||
height = clientHeight - getToolboxHeight();
|
||||
} else {
|
||||
if (filmstripVisible) {
|
||||
height = clientHeight - Filmstrip.getFilmstripHeight();
|
||||
} else {
|
||||
height = clientHeight;
|
||||
}
|
||||
width = clientWidth;
|
||||
}
|
||||
|
||||
return {
|
||||
width: `${width}px`,
|
||||
height: `${height - HEIGHT_OFFSET}px`
|
||||
};
|
||||
};
|
||||
|
||||
const getCollabAPI = useCallback(collabAPI => {
|
||||
if (collabAPIRef.current) {
|
||||
return;
|
||||
}
|
||||
collabAPIRef.current = collabAPI;
|
||||
collabAPIRef.current.setUsername(localParticipantName);
|
||||
}, [ localParticipantName ]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { clsx(
|
||||
isResizing && 'disable-pointer',
|
||||
'whiteboard-container'
|
||||
) }
|
||||
style = {{
|
||||
...getDimensions(),
|
||||
marginTop: `${HEIGHT_OFFSET}px`,
|
||||
display: `${isInTileView || !isVisible ? 'none' : 'block'}`
|
||||
}}>
|
||||
{
|
||||
isOpen && (
|
||||
<div className = 'excalidraw-wrapper'>
|
||||
<ExcalidrawApp
|
||||
collabDetails = { collabDetails }
|
||||
collabServerUrl = { collabServerUrl }
|
||||
excalidraw = {{
|
||||
isCollaborating: true,
|
||||
// @ts-ignore
|
||||
ref: excalidrawRef,
|
||||
theme: 'light',
|
||||
UIOptions: WHITEBOARD_UI_OPTIONS
|
||||
}}
|
||||
getCollabAPI = { getCollabAPI } />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Whiteboard;
|
|
@ -0,0 +1,88 @@
|
|||
/* eslint-disable import/order */
|
||||
import type { Dispatch } from 'redux';
|
||||
import { IState } from '../../../app/types';
|
||||
|
||||
// @ts-ignore
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { IconHideWhiteboard, IconShowWhiteboard } from '../../../base/icons/svg';
|
||||
|
||||
// @ts-ignore
|
||||
import { connect } from '../../../base/redux';
|
||||
|
||||
// @ts-ignore
|
||||
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
|
||||
|
||||
// @ts-ignore
|
||||
import { setOverflowMenuVisible } from '../../../toolbox/actions.web';
|
||||
import { setWhiteboardOpen } from '../../actions';
|
||||
import { isWhiteboardVisible } from '../../functions';
|
||||
|
||||
|
||||
type Props = AbstractButtonProps & {
|
||||
|
||||
/**
|
||||
* Whether or not the button is toggled.
|
||||
*/
|
||||
_toggled: boolean;
|
||||
|
||||
/**
|
||||
* The redux {@code dispatch} function.
|
||||
*/
|
||||
dispatch: Dispatch<any>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Component that renders a toolbar button for the whiteboard.
|
||||
*/
|
||||
class WhiteboardButton extends AbstractButton<Props, any, any> {
|
||||
accessibilityLabel = 'toolbar.accessibilityLabel.whiteboard';
|
||||
icon = IconShowWhiteboard;
|
||||
label = 'toolbar.showWhiteboard';
|
||||
toggledIcon = IconHideWhiteboard;
|
||||
toggledLabel = 'toolbar.hideWhiteboard';
|
||||
toggledTooltip = 'toolbar.hideWhiteboard';
|
||||
tooltip = 'toolbar.showWhiteboard';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and opens / closes the whiteboard view.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
|
||||
// @ts-ignore
|
||||
const { dispatch, _toggled } = this.props;
|
||||
|
||||
dispatch(setWhiteboardOpen(!_toggled));
|
||||
dispatch(setOverflowMenuVisible(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this button is in toggled state or not.
|
||||
*
|
||||
* @override
|
||||
* @protected
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_isToggled() {
|
||||
// @ts-ignore
|
||||
return this.props._toggled;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state: IState) {
|
||||
return {
|
||||
_toggled: isWhiteboardVisible(state)
|
||||
};
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
export default translate(connect(_mapStateToProps)(WhiteboardButton));
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* Fixed name of the whiteboard fake participant.
|
||||
*/
|
||||
export const WHITEBOARD_PARTICIPANT_NAME = 'Whiteboard';
|
||||
|
||||
/**
|
||||
* Whiteboard ID.
|
||||
*/
|
||||
export const WHITEBOARD_ID = 'whiteboard';
|
||||
|
||||
/**
|
||||
* Whiteboard UI Options.
|
||||
*/
|
||||
export const WHITEBOARD_UI_OPTIONS = {
|
||||
canvasActions: {
|
||||
allowedShapes: [
|
||||
'arrow', 'diamond', 'ellipse', 'freedraw', 'line', 'rectangle', 'selection', 'text'
|
||||
],
|
||||
allowedShortcuts: [
|
||||
'cut', 'deleteSelectedElements', 'redo', 'selectAll', 'undo'
|
||||
],
|
||||
disableAlignItems: true,
|
||||
disableFileDrop: true,
|
||||
disableGrouping: true,
|
||||
disableHints: true,
|
||||
disableLink: true,
|
||||
disableShortcuts: true,
|
||||
disableVerticalAlignOptions: true,
|
||||
fontSizeOptions: [ 's', 'm', 'l' ],
|
||||
hideArrowHeadsOptions: true,
|
||||
hideColorInput: true,
|
||||
hideClearCanvas: true,
|
||||
hideFontFamily: true,
|
||||
hideHelpDialog: true,
|
||||
hideIOActions: true,
|
||||
hideLayers: true,
|
||||
hideLibraries: true,
|
||||
hideLockButton: true,
|
||||
hideOpacityInput: true,
|
||||
hideSharpness: true,
|
||||
hideStrokeStyle: true,
|
||||
hideTextAlign: true,
|
||||
hideThemeControls: true,
|
||||
hideUserList: true,
|
||||
saveAsImageOptions: {
|
||||
defaultBackgroundValue: true,
|
||||
disableScale: true,
|
||||
disableSelection: true,
|
||||
disableClipboard: true,
|
||||
disableSceneEmbed: true,
|
||||
hideTheme: true
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,95 @@
|
|||
/* eslint-disable import/order */
|
||||
import md5 from 'js-md5';
|
||||
|
||||
// @ts-ignore
|
||||
import { getPinnedParticipant } from '../../features/base/participants';
|
||||
import { IState } from '../app/types';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { getRemoteParticipants, isLocalParticipantModerator } from '../base/participants/functions';
|
||||
import { appendURLParam } from '../base/util/uri';
|
||||
|
||||
// @ts-ignore
|
||||
import { getCurrentRoomId, isInBreakoutRoom } from '../breakout-rooms/functions';
|
||||
|
||||
import { WHITEBOARD_ID } from './constants';
|
||||
import { IWhiteboardState } from './reducer';
|
||||
|
||||
const getWhiteboardState = (state: IState): IWhiteboardState => state['features/whiteboard'];
|
||||
|
||||
/**
|
||||
* Indicates whether the whiteboard is enabled in the config.
|
||||
*
|
||||
* @param {Object} state - The state from the Redux store.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isWhiteboardEnabled = (state: IState): boolean =>
|
||||
state['features/base/config'].whiteboard?.enabled
|
||||
&& state['features/base/config'].whiteboard?.collabServerBaseUrl
|
||||
&& getCurrentConference(state)?.getMetadataHandler()
|
||||
?.isSupported();
|
||||
|
||||
/**
|
||||
* Indicates whether the whiteboard is open.
|
||||
*
|
||||
* @param {Object} state - The state from the Redux store.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isWhiteboardOpen = (state: IState): boolean => getWhiteboardState(state).isOpen;
|
||||
|
||||
/**
|
||||
* Indicates whether the whiteboard button is visible.
|
||||
*
|
||||
* @param {Object} state - The state from the Redux store.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isWhiteboardButtonVisible = (state: IState): boolean =>
|
||||
isWhiteboardEnabled(state) && (isLocalParticipantModerator(state) || isWhiteboardOpen(state));
|
||||
|
||||
/**
|
||||
* Indicates whether the whiteboard is present as a meeting participant.
|
||||
*
|
||||
* @param {Object} state - The state from the Redux store.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isWhiteboardPresent = (state: IState): boolean => getRemoteParticipants(state).has(WHITEBOARD_ID);
|
||||
|
||||
/**
|
||||
* Returns the whiteboard collaboration link.
|
||||
*
|
||||
* @param {Object} state - The state from the Redux store.
|
||||
* @returns {{ roomId: string, roomKey: string}|null}
|
||||
*/
|
||||
export const getCollabDetails = (state: IState): {
|
||||
roomId: string; roomKey: string;
|
||||
} | undefined => getWhiteboardState(state).collabDetails;
|
||||
|
||||
/**
|
||||
* Returns the whiteboard collaboration server url.
|
||||
*
|
||||
* @param {Object} state - The state from the Redux store.
|
||||
* @returns {string}
|
||||
*/
|
||||
export const getCollabServerUrl = (state: IState): string | undefined => {
|
||||
const collabServerBaseUrl = state['features/base/config'].whiteboard?.collabServerBaseUrl;
|
||||
|
||||
if (!collabServerBaseUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
const inBreakoutRoom = isInBreakoutRoom(state);
|
||||
const roomId = getCurrentRoomId(state);
|
||||
const room = md5.hex(`${locationURL?.href}${inBreakoutRoom ? `|${roomId}` : ''}`);
|
||||
|
||||
return appendURLParam(collabServerBaseUrl, 'room', room);
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether the whiteboard is visible on stage.
|
||||
*
|
||||
* @param {Object} state - The state from the Redux store.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isWhiteboardVisible = (state: IState): boolean =>
|
||||
getPinnedParticipant(state)?.id === WHITEBOARD_ID
|
||||
|| state['features/large-video'].participantId === WHITEBOARD_ID;
|
|
@ -0,0 +1,127 @@
|
|||
/* eslint-disable import/order */
|
||||
import { generateCollaborationLinkData } from '@jitsi/excalidraw';
|
||||
import { IStore } from '../app/types';
|
||||
|
||||
import { participantJoined, participantLeft, pinParticipant } from '../base/participants/actions';
|
||||
|
||||
// @ts-ignore
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
|
||||
// @ts-ignore
|
||||
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
||||
import { RESET_WHITEBOARD, SET_WHITEBOARD_OPEN } from './actionTypes';
|
||||
import { getCollabDetails, getCollabServerUrl, isWhiteboardPresent } from './functions';
|
||||
import { WHITEBOARD_ID, WHITEBOARD_PARTICIPANT_NAME } from './constants';
|
||||
import { resetWhiteboard, setWhiteboardOpen, setupWhiteboard } from './actions';
|
||||
import { getCurrentRoomId } from '../breakout-rooms/functions';
|
||||
|
||||
// @ts-ignore
|
||||
import { addStageParticipant } from '../filmstrip/actions.web';
|
||||
|
||||
// @ts-ignore
|
||||
import { isStageFilmstripAvailable } from '../filmstrip/functions';
|
||||
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||
|
||||
const focusWhiteboard = (store: IStore) => {
|
||||
const { dispatch, getState } = store;
|
||||
const state = getState();
|
||||
const conference = getCurrentConference(state);
|
||||
const stageFilmstrip = isStageFilmstripAvailable(state);
|
||||
const isPresent = isWhiteboardPresent(state);
|
||||
|
||||
if (!isPresent) {
|
||||
dispatch(participantJoined({
|
||||
conference,
|
||||
id: WHITEBOARD_ID,
|
||||
isFakeParticipant: true,
|
||||
isWhiteboard: true,
|
||||
name: WHITEBOARD_PARTICIPANT_NAME
|
||||
}));
|
||||
}
|
||||
if (stageFilmstrip) {
|
||||
dispatch(addStageParticipant(WHITEBOARD_ID, true));
|
||||
} else {
|
||||
dispatch(pinParticipant(WHITEBOARD_ID));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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: any) => {
|
||||
const { dispatch, getState } = store;
|
||||
const state = getState();
|
||||
const conference = getCurrentConference(state);
|
||||
|
||||
switch (action.type) {
|
||||
case SET_WHITEBOARD_OPEN: {
|
||||
const existingCollabDetails = getCollabDetails(state);
|
||||
|
||||
if (!existingCollabDetails) {
|
||||
const collabDetails = await generateCollaborationLinkData();
|
||||
const collabServerUrl = getCollabServerUrl(state);
|
||||
|
||||
focusWhiteboard(store);
|
||||
dispatch(setupWhiteboard({
|
||||
collabDetails: {
|
||||
roomId: getCurrentRoomId(state),
|
||||
roomKey: collabDetails.roomKey
|
||||
}
|
||||
}));
|
||||
conference.getMetadataHandler().setMetadata(WHITEBOARD_ID, {
|
||||
collabServerUrl,
|
||||
roomKey: collabDetails.roomKey
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.isOpen) {
|
||||
focusWhiteboard(store);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(participantLeft(WHITEBOARD_ID, conference, { isWhiteboard: true }));
|
||||
break;
|
||||
}
|
||||
case RESET_WHITEBOARD: {
|
||||
dispatch(participantLeft(WHITEBOARD_ID, conference, { isWhiteboard: 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(
|
||||
|
||||
// @ts-ignore
|
||||
state => getCurrentConference(state),
|
||||
|
||||
// @ts-ignore
|
||||
(conference, { dispatch, getState }, previousConference): void => {
|
||||
if (conference !== previousConference) {
|
||||
dispatch(resetWhiteboard());
|
||||
}
|
||||
if (conference && !previousConference) {
|
||||
conference.on(JitsiConferenceEvents.METADATA_UPDATED, (metadata: any) => {
|
||||
if (metadata[WHITEBOARD_ID]) {
|
||||
dispatch(setupWhiteboard({
|
||||
collabDetails: {
|
||||
roomId: getCurrentRoomId(getState()),
|
||||
roomKey: metadata[WHITEBOARD_ID].roomKey
|
||||
}
|
||||
}));
|
||||
dispatch(setWhiteboardOpen(true));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
|
@ -0,0 +1,53 @@
|
|||
import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
|
||||
import { RESET_WHITEBOARD, SETUP_WHITEBOARD } from './actionTypes';
|
||||
|
||||
export interface IWhiteboardState {
|
||||
|
||||
/**
|
||||
* The whiteboard collaboration details.
|
||||
*/
|
||||
collabDetails?: { roomId: string; roomKey: string; };
|
||||
|
||||
/**
|
||||
* The indicator which determines whether the whiteboard is open.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_STATE: IWhiteboardState = {
|
||||
isOpen: false
|
||||
};
|
||||
|
||||
export interface WhiteboardAction extends Partial<IWhiteboardState> {
|
||||
|
||||
/**
|
||||
* The whiteboard collaboration details.
|
||||
*/
|
||||
collabDetails?: { roomId: string; roomKey: string; };
|
||||
|
||||
/**
|
||||
* The action type.
|
||||
*/
|
||||
type: string;
|
||||
}
|
||||
|
||||
ReducerRegistry.register(
|
||||
'features/whiteboard',
|
||||
(state: IWhiteboardState = DEFAULT_STATE, action: WhiteboardAction) => {
|
||||
switch (action.type) {
|
||||
case SETUP_WHITEBOARD: {
|
||||
return {
|
||||
...state,
|
||||
isOpen: true,
|
||||
collabDetails: action.collabDetails
|
||||
};
|
||||
}
|
||||
case RESET_WHITEBOARD:
|
||||
return DEFAULT_STATE;
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
|
@ -306,7 +306,9 @@ module.exports = (_env, argv) => {
|
|||
process: 'process/browser'
|
||||
})
|
||||
],
|
||||
performance: getPerformanceHints(perfHintOptions, 4 * 1024 * 1024)
|
||||
|
||||
performance: getPerformanceHints(perfHintOptions, 5 * 1024 * 1024)
|
||||
|
||||
}),
|
||||
Object.assign({}, config, {
|
||||
entry: {
|
||||
|
|
Loading…
Reference in New Issue