Merge pull request #20134 from JoVictorNunes/video-streams-typings-fix

fix: tweak video-streams typings
This commit is contained in:
Ramón Souza 2024-05-03 17:19:52 -03:00 committed by GitHub
commit f742a6d25d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 1551 additions and 1709 deletions

View File

@ -297,6 +297,7 @@ export interface Kurento {
cameraQualityThresholds: CameraQualityThresholds
pagination: Pagination
paginationThresholds: PaginationThresholds
videoMediaServer?: string
}
export interface CameraWsOptions {

View File

@ -15,7 +15,7 @@ import NotificationsBarContainer from '../notifications-bar/container';
import AudioContainer from '../audio/container';
import BannerBarContainer from '/imports/ui/components/banner-bar/container';
import RaiseHandNotifier from '/imports/ui/components/raisehand-notifier/container';
import ManyWebcamsNotifier from '/imports/ui/components/video-provider/many-users-notify/container';
import ManyWebcamsNotifier from '/imports/ui/components/video-provider/video-provider-graphql/many-users-notify/container';
import AudioCaptionsSpeechContainer from '/imports/ui/components/audio/audio-graphql/audio-captions/speech/component';
import UploaderContainer from '/imports/ui/components/presentation/presentation-uploader/container';
import ScreenReaderAlertContainer from '../screenreader-alert/container';

View File

@ -37,7 +37,7 @@ const STATUS = {
/**
* HOC for injecting a file reader utility.
* @param {React.Component} Component
* @param {(props: any) => JSX.Element} Component
* @param {string[]} mimeTypesAllowed String array containing MIME types allowed.
* @param {number} maxFileSize Max file size allowed in Mbytes.
* @returns A new component which accepts the same props as the wrapped component plus

View File

@ -40,12 +40,16 @@ class ConnectionStatusButton extends PureComponent {
setModalIsOpen = (isOpen) => this.setState({ isModalOpen: isOpen });
renderModal(isModalOpen) {
const { isGridLayout, paginationEnabled, viewParticipantsWebcams } = this.props;
return (
isModalOpen ?
<ConnectionStatusModalComponent
{...{
isModalOpen,
setModalIsOpen: this.setModalIsOpen,
isGridLayout,
paginationEnabled,
viewParticipantsWebcams,
}}
/> : null
)

View File

@ -2,6 +2,7 @@ import React from 'react';
import { Meteor } from 'meteor/meteor';
import { withTracker } from 'meteor/react-meteor-data';
import { useSubscription } from '@apollo/client';
import Settings from '/imports/ui/services/settings';
import ConnectionStatusButtonComponent from './component';
import { USER_CURRENT_STATUS_SUBSCRIPTION } from '../queries';
import Auth from '/imports/ui/services/auth';
@ -19,8 +20,14 @@ const connectionStatusButtonContainer = (props) => {
export default withTracker(() => {
const { connected } = Meteor.status();
const isGridLayout = Session.get('isGridEnabled');
const { paginationEnabled } = Settings.application;
const { viewParticipantsWebcams } = Settings.dataSaving;
return {
connected,
isGridLayout,
paginationEnabled,
viewParticipantsWebcams,
};
})(connectionStatusButtonContainer);

View File

@ -216,10 +216,10 @@ class ConnectionStatusComponent extends PureComponent {
* @return {Promise} A Promise that resolves when process started.
*/
async startMonitoringNetwork() {
const { streams } = this.props;
let previousData = await Service.getNetworkData(streams);
const { getVideoStreamsStats } = this.props;
let previousData = await Service.getNetworkData(getVideoStreamsStats);
this.rateInterval = Meteor.setInterval(async () => {
const data = await Service.getNetworkData(streams);
const data = await Service.getNetworkData(getVideoStreamsStats);
const {
outbound: audioCurrentUploadRate,

View File

@ -4,19 +4,24 @@ import { CONNECTION_STATUS_REPORT_SUBSCRIPTION } from '../queries';
import Service from '../service';
import Component from './component';
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
import { useStreams } from '../../video-provider/video-provider-graphql/hooks';
import { useGetStats } from '../../video-provider/video-provider-graphql/hooks';
const ConnectionStatusContainer = (props) => {
const { data } = useSubscription(CONNECTION_STATUS_REPORT_SUBSCRIPTION);
const connectionData = data ? Service.sortConnectionData(data.user_connectionStatusReport) : [];
const { data: currentUser } = useCurrentUser((u) => ({ isModerator: u.isModerator }));
const amIModerator = !!currentUser?.isModerator;
const { streams } = useStreams();
const { isGridLayout, paginationsEnabled, viewParticipantsWebcams } = props;
const getVideoStreamsStats = useGetStats(
isGridLayout,
paginationsEnabled,
viewParticipantsWebcams,
);
return (
<Component
connectionData={connectionData}
amIModerator={amIModerator}
streams={streams}
getVideoStreamsStats={getVideoStreamsStats}
{...props}
/>
);

View File

@ -204,8 +204,8 @@ const getAudioData = async () => {
* @returns An Object containing video data for all video peers and screenshare
* peer
*/
const getVideoData = async (streams) => {
const camerasData = await VideoService.getStats(streams) || {};
const getVideoData = async (getVideoStreamsStats) => {
const camerasData = await getVideoStreamsStats() || {};
const screenshareData = await ScreenshareService.getStats() || {};
@ -220,10 +220,10 @@ const getVideoData = async (streams) => {
* For audio, this will get information about the mic/listen-only stream.
* @returns An Object containing all this data.
*/
const getNetworkData = async (streams) => {
const getNetworkData = async (getVideoStreamsStats) => {
const audio = await getAudioData();
const video = await getVideoData(streams);
const video = await getVideoData(getVideoStreamsStats);
const user = {
time: new Date(),

View File

@ -57,6 +57,15 @@ interface CameraDock {
isResizable?: boolean;
resizableEdge?: ResizableEdge;
tabOrder?: number;
presenterMaxWidth: number;
maxWidth: number;
maxHeight: number;
minHeight: number;
minWidth: number;
left: number;
top: number;
right: number;
zIndex: number;
}
export interface ExternalVideo {

View File

@ -8,10 +8,10 @@ import ScreenShareService from '/imports/ui/components/screenshare/service';
import logger from '/imports/startup/client/logger';
import { SCREENSHARING_ERRORS } from '/imports/api/screenshare/client/bridge/errors';
import { EXTERNAL_VIDEO_STOP } from '../external-video-player/mutations';
import { CAMERA_BROADCAST_STOP } from '../video-provider/mutations';
import {
useSharedDevices, useHasVideoStream, useHasCapReached, useIsUserLocked, useStreams,
useExitVideo,
useStopVideo,
} from '/imports/ui/components/video-provider/video-provider-graphql/hooks';
const VideoPreviewContainer = (props) => {
@ -22,21 +22,16 @@ const VideoPreviewContainer = (props) => {
...rest
} = props;
const [stopExternalVideoShare] = useMutation(EXTERNAL_VIDEO_STOP);
const [cameraBroadcastStop] = useMutation(CAMERA_BROADCAST_STOP);
const sendUserUnshareWebcam = (cameraId) => {
cameraBroadcastStop({ variables: { cameraId } });
};
const { streams } = useStreams();
const exitVideo = useExitVideo()
const exitVideo = useExitVideo();
const stopVideo = useStopVideo();
const startSharingCameraAsContent = buildStartSharingCameraAsContent(stopExternalVideoShare);
const stopSharing = buildStopSharing(sendUserUnshareWebcam, streams, exitVideo);
const stopSharing = buildStopSharing(streams, exitVideo, stopVideo);
const sharedDevices = useSharedDevices();
const hasVideoStream = useHasVideoStream();
const camCapReached = useHasCapReached();
const isCamLocked = useIsUserLocked();
return (
<VideoPreview
@ -81,14 +76,14 @@ export default withTracker(({ setIsOpen, callbackToClose }) => ({
);
ScreenShareService.setCameraAsContentDeviceId(deviceId);
},
buildStopSharing: (sendUserUnshareWebcam, streams, exitVideo) => (deviceId) => {
buildStopSharing: (streams, exitVideo, stopVideo) => (deviceId) => {
callbackToClose();
setIsOpen(false);
if (deviceId) {
const streamId = VideoService.getMyStreamId(deviceId, streams);
if (streamId) VideoService.stopVideo(streamId, sendUserUnshareWebcam, streams);
if (streamId) stopVideo(streamId);
} else {
exitVideo(sendUserUnshareWebcam, streams);
exitVideo();
}
},
stopSharingCameraAsContent: () => {

View File

@ -1,7 +1,6 @@
// @ts-nocheck
/* eslint-disable */
import { useEffect, useRef } from 'react';
import { useSubscription } from '@apollo/client';
import logger from '/imports/startup/client/logger';
import {
VIDEO_STREAMS_SUBSCRIPTION,
VideoStreamsResponse,
@ -17,15 +16,18 @@ const VideoStreamAdapter: React.FC<AdapterProps> = ({
const { data, loading, error } = useSubscription<VideoStreamsResponse>(VIDEO_STREAMS_SUBSCRIPTION);
useEffect(() => {
if (loading || error) return;
if (loading) return;
if (error) {
logger.error(`Video streams subscription failed. ${error.name}: ${error.message}`, error);
return;
}
if (!data) {
setStreams([]);
return;
}
const streams = data.user_camera.map(({ streamId, user, voice }) => ({
stream: streamId,
deviceId: streamId.split('_')[2],
@ -35,18 +37,19 @@ const VideoStreamAdapter: React.FC<AdapterProps> = ({
pin: user.pinned,
floor: voice?.floor || false,
lastFloorTime: voice?.lastFloorTime || '0',
isUserModerator: user.isModerator,
isModerator: user.isModerator,
type: 'stream' as const,
}));
setStreams(streams);
}, [data]);
useEffect(()=>{
useEffect(() => {
if (!ready.current && !loading) {
ready.current = true;
onReady('VideoStreamAdapter');
}
}, [loading])
}, [loading]);
return children;
};

View File

@ -1,9 +1,8 @@
// @ts-nocheck
/* eslint-disable */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ReconnectingWebSocket from 'reconnecting-websocket';
import { defineMessages, injectIntl } from 'react-intl';
import { IntlShape, defineMessages, injectIntl } from 'react-intl';
import { debounce } from '/imports/utils/debounce';
import VideoService from './service';
import VideoListContainer from './video-list/container';
@ -23,6 +22,8 @@ import {
import { notify } from '/imports/ui/services/notification';
import { shouldForceRelay } from '/imports/ui/services/bbb-webrtc-sfu/utils';
import WebRtcPeer from '/imports/ui/services/webrtc-base/peer';
import { StreamItem, StreamUser, VideoItem } from './types';
import { Output } from '../../layout/layoutTypes';
// Default values and default empty object to be backwards compat with 2.2.
// FIXME Remove hardcoded defaults 2.3.
@ -116,25 +117,46 @@ const intlSFUErrors = defineMessages({
},
});
const propTypes = {
streams: PropTypes.arrayOf(Array).isRequired,
intl: PropTypes.objectOf(Object).isRequired,
isUserLocked: PropTypes.bool.isRequired,
swapLayout: PropTypes.bool.isRequired,
currentVideoPageIndex: PropTypes.number.isRequired,
totalNumberOfStreams: PropTypes.number.isRequired,
isMeteorConnected: PropTypes.bool.isRequired,
playStart: PropTypes.func.isRequired,
sendUserUnshareWebcam: PropTypes.func.isRequired,
};
interface VideoProviderGraphqlState {
socketOpen: boolean;
}
class VideoProviderGraphql extends Component {
interface VideoProviderGraphqlProps {
cameraDock: Output['cameraDock'];
focusedId: string;
handleVideoFocus: (id: string) => void;
isGridEnabled: boolean;
isMeteorConnected: boolean;
swapLayout: boolean;
currentUserId: string;
paginationEnabled: boolean;
viewParticipantsWebcams: boolean;
totalNumberOfStreams: number;
isUserLocked: boolean;
currentVideoPageIndex: number;
streams: VideoItem[];
users: StreamUser[];
info: {
userId: string | null | undefined;
userName: string | null | undefined;
meetingId: string | null | undefined;
sessionToken: string | null;
voiceBridge: string | null;
};
playStart: (cameraId: string) => void;
exitVideo: () => void;
lockUser: () => void;
stopVideo: (cameraId?: string) => void;
intl: IntlShape;
}
class VideoProviderGraphql extends Component<VideoProviderGraphqlProps, VideoProviderGraphqlState> {
onBeforeUnload() {
const { sendUserUnshareWebcam } = this.props;
VideoService.onBeforeUnload(sendUserUnshareWebcam);
const { exitVideo } = this.props;
exitVideo();
}
static shouldAttachVideoStream(peer, videoElement) {
static shouldAttachVideoStream(peer: WebRtcPeer, videoElement: HTMLVideoElement) {
// Conditions to safely attach a stream to a video element in all browsers:
// 1 - Peer exists, video element exists
// 2 - Target stream differs from videoElement's (diff)
@ -156,15 +178,47 @@ class VideoProviderGraphql extends Component {
&& diff;
}
constructor(props) {
private info: {
userId: string | null | undefined;
userName: string | null | undefined;
meetingId: string | null | undefined;
sessionToken: string | null;
voiceBridge: string | null;
};
private mounted: boolean = false;
private webRtcPeers: Record<string, WebRtcPeer>;
private debouncedConnectStreams: Function;
private ws: ReconnectingWebSocket | null = null;
private wsQueues: Record<string, {
id: string;
cameraId?: string;
type?: string;
role?: string;
}[] | null>;
private outboundIceQueues: Record<string, RTCIceCandidate[]>;
private restartTimeout: Record<string, NodeJS.Timeout>;
private restartTimer: Record<string, number>;
private videoTags: Record<string, HTMLVideoElement>;
constructor(props: VideoProviderGraphqlProps) {
super(props);
const { info } = this.props;
// socketOpen state is there to force update when the signaling socket opens or closes
this.state = {
socketOpen: false,
};
this._isMounted = false;
this.info = VideoService.getInfo();
this.mounted = false;
this.info = info;
// Signaling message queue arrays indexed by stream (== cameraId)
this.wsQueues = {};
this.restartTimeout = {};
@ -190,19 +244,19 @@ class VideoProviderGraphql extends Component {
}
componentDidMount() {
this._isMounted = true;
this.mounted = true;
VideoService.updatePeerDictionaryReference(this.webRtcPeers);
this.ws = this.openWs();
window.addEventListener('beforeunload', this.onBeforeUnload);
}
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps: VideoProviderGraphqlProps) {
const {
isUserLocked,
streams,
currentVideoPageIndex,
isMeteorConnected,
sendUserUnshareWebcam,
lockUser,
} = this.props;
const { socketOpen } = this.state;
@ -212,7 +266,7 @@ class VideoProviderGraphql extends Component {
if (isMeteorConnected && socketOpen) this.updateStreams(streams, shouldDebounce);
if (!prevProps.isUserLocked && isUserLocked) {
VideoService.lockUser(sendUserUnshareWebcam, streams);
lockUser();
}
// Signaling socket expired its retries and meteor is connected - create
@ -225,8 +279,8 @@ class VideoProviderGraphql extends Component {
}
componentWillUnmount() {
const { sendUserUnshareWebcam, streams, exitVideo } = this.props;
this._isMounted = false;
const { exitVideo } = this.props;
this.mounted = false;
VideoService.updatePeerDictionaryReference({});
if (this.ws) {
@ -250,7 +304,7 @@ class VideoProviderGraphql extends Component {
debug: WS_DEBUG,
maxRetries: WS_MAX_RETRIES,
maxEnqueuedMessages: 0,
}
},
);
ws.onopen = this.onWsOpen;
ws.onclose = this.onWsClose;
@ -267,12 +321,14 @@ class VideoProviderGraphql extends Component {
}
}
_updateLastMsgTime() {
this.ws.isAlive = true;
this.ws.lastMsgTime = Date.now();
private updateLastMsgTime() {
if (this.ws) {
this.ws.isAlive = true;
this.ws.lastMsgTime = Date.now();
}
}
_getTimeSinceLastMsg() {
private getTimeSinceLastMsg() {
return Date.now() - this.ws.lastMsgTime;
}
@ -290,7 +346,7 @@ class VideoProviderGraphql extends Component {
return;
}
if (this._getTimeSinceLastMsg() < (
if (this.getTimeSinceLastMsg() < (
WS_HEARTBEAT_OPTS.interval - WS_HEARTBEAT_OPTS.delay
)) {
return;
@ -310,8 +366,8 @@ class VideoProviderGraphql extends Component {
}
}
onWsMessage(message) {
this._updateLastMsgTime();
onWsMessage(message: { data: string }) {
this.updateLastMsgTime();
const parsedMessage = JSON.parse(message.data);
if (parsedMessage.id === 'pong') return;
@ -344,7 +400,7 @@ class VideoProviderGraphql extends Component {
}
onWsClose() {
const { sendUserUnshareWebcam, streams, exitVideo } = this.props;
const { exitVideo } = this.props;
logger.info({
logCode: 'video_provider_onwsclose',
}, 'Multiple video provider websocket connection closed.');
@ -372,15 +428,15 @@ class VideoProviderGraphql extends Component {
logCode: 'video_provider_onwsopen',
}, 'Multiple video provider websocket connection opened.');
this._updateLastMsgTime();
this.updateLastMsgTime();
this.setupWSHeartbeat();
this.setState({ socketOpen: true });
// Resend queued messages that happened when socket was not connected
Object.entries(this.wsQueues).forEach(([stream, queue]) => {
if (this.webRtcPeers[stream]) {
if (this.webRtcPeers[stream] && queue !== null) {
// Peer - send enqueued
while (queue.length > 0) {
this.sendMessage(queue.pop());
this.sendMessage(queue.pop()!);
}
} else {
// No peer - delete queue
@ -389,18 +445,18 @@ class VideoProviderGraphql extends Component {
});
}
findAllPrivilegedStreams () {
findAllPrivilegedStreams() {
const { streams } = this.props;
// Privileged streams are: floor holders, pinned users
return streams.filter(stream => stream.floor || stream.pin);
return streams.filter((stream) => stream.type === 'stream' && (stream.floor || stream.pin));
}
updateQualityThresholds(numberOfPublishers) {
updateQualityThresholds(numberOfPublishers: number) {
const { threshold, profile } = VideoService.getThreshold(numberOfPublishers);
if (profile) {
const privilegedStreams = this.findAllPrivilegedStreams();
Object.values(this.webRtcPeers)
.filter(peer => peer.isPublisher)
.filter((peer) => peer.isPublisher)
.forEach((peer) => {
// Conditions which make camera revert their original profile
// 1) Threshold 0 means original profile/inactive constraint
@ -413,33 +469,33 @@ class VideoProviderGraphql extends Component {
}
}
getStreamsToConnectAndDisconnect(streams) {
const streamsCameraIds = streams.filter(s => !s?.isGridItem).map(s => s.stream);
getStreamsToConnectAndDisconnect(streams: VideoItem[]) {
const streamsCameraIds = streams.filter((s) => s?.type !== 'grid').map((s) => (s as StreamItem).stream);
const streamsConnected = Object.keys(this.webRtcPeers);
const streamsToConnect = streamsCameraIds.filter(stream => {
const streamsToConnect = streamsCameraIds.filter((stream) => {
return !streamsConnected.includes(stream);
});
const streamsToDisconnect = streamsConnected.filter(stream => {
const streamsToDisconnect = streamsConnected.filter((stream) => {
return !streamsCameraIds.includes(stream);
});
return [streamsToConnect, streamsToDisconnect];
}
connectStreams(streamsToConnect) {
connectStreams(streamsToConnect: string[]) {
streamsToConnect.forEach((stream) => {
const isLocal = VideoService.isLocalStream(stream);
this.createWebRTCPeer(stream, isLocal);
});
}
disconnectStreams(streamsToDisconnect) {
streamsToDisconnect.forEach(stream => this.stopWebRTCPeer(stream, false));
disconnectStreams(streamsToDisconnect: string[]) {
streamsToDisconnect.forEach((stream) => this.stopWebRTCPeer(stream, false));
}
updateStreams(streams, shouldDebounce = false) {
updateStreams(streams: VideoItem[], shouldDebounce = false) {
const [streamsToConnect, streamsToDisconnect] = this.getStreamsToConnectAndDisconnect(streams);
if (shouldDebounce) {
@ -451,7 +507,8 @@ class VideoProviderGraphql extends Component {
this.disconnectStreams(streamsToDisconnect);
if (CAMERA_QUALITY_THRESHOLDS_ENABLED) {
this.updateQualityThresholds(this.props.totalNumberOfStreams);
const { totalNumberOfStreams } = this.props;
this.updateQualityThresholds(totalNumberOfStreams);
}
}
@ -460,14 +517,14 @@ class VideoProviderGraphql extends Component {
this.sendMessage(message);
}
sendMessage(message) {
sendMessage(message: { id: string, cameraId?: string; type?: string; role?: string }) {
const { ws } = this;
if (this.connectedToMediaServer()) {
const jsonMessage = JSON.stringify(message);
try {
ws.send(jsonMessage);
} catch (error) {
ws?.send(jsonMessage);
} catch (error: Error) {
logger.error({
logCode: 'video_provider_ws_send_error',
extraInfo: {
@ -481,7 +538,7 @@ class VideoProviderGraphql extends Component {
const { cameraId } = message;
if (cameraId) {
if (this.wsQueues[cameraId] == null) this.wsQueues[cameraId] = [];
this.wsQueues[cameraId].push(message);
this.wsQueues[cameraId]?.push(message);
}
}
}
@ -490,7 +547,7 @@ class VideoProviderGraphql extends Component {
return this.ws && this.ws.readyState === ReconnectingWebSocket.OPEN;
}
processOutboundIceQueue(peer, role, stream) {
processOutboundIceQueue(peer: WebRtcPeer, role: string, stream: string) {
const queue = this.outboundIceQueues[stream];
while (queue && queue.length) {
const candidate = queue.shift();
@ -498,7 +555,7 @@ class VideoProviderGraphql extends Component {
}
}
sendLocalAnswer (peer, stream, answer) {
sendLocalAnswer (peer: WebRtcPeer, stream: string, answer) {
const message = {
id: 'subscriberAnswer',
type: 'video',
@ -510,7 +567,7 @@ class VideoProviderGraphql extends Component {
this.sendMessage(message);
}
startResponse(message) {
startResponse(message: { cameraId: string; stream: string; role: string }) {
const { cameraId: stream, role } = message;
const peer = this.webRtcPeers[stream];
@ -530,7 +587,7 @@ class VideoProviderGraphql extends Component {
peer.didSDPAnswered = true;
this.processOutboundIceQueue(peer, role, stream);
VideoService.processInboundIceQueue(peer, stream);
}).catch((error) => {
}).catch((error: Error & { code: string }) => {
logger.error({
logCode: 'video_provider_peerconnection_process_error',
extraInfo: {
@ -549,7 +606,7 @@ class VideoProviderGraphql extends Component {
}
}
handleIceCandidate(message) {
handleIceCandidate(message: { cameraId: string; candidate: Record<string, unknown> }) {
const { cameraId: stream, candidate } = message;
const peer = this.webRtcPeers[stream];
@ -575,7 +632,7 @@ class VideoProviderGraphql extends Component {
}
}
clearRestartTimers(stream) {
clearRestartTimers(stream: string) {
if (this.restartTimeout[stream]) {
clearTimeout(this.restartTimeout[stream]);
delete this.restartTimeout[stream];
@ -586,9 +643,9 @@ class VideoProviderGraphql extends Component {
}
}
stopWebRTCPeer(stream, restarting = false) {
stopWebRTCPeer(stream: string, restarting = false) {
const isLocal = VideoService.isLocalStream(stream);
const { sendUserUnshareWebcam, streams } = this.props;
const { stopVideo } = this.props;
// in this case, 'closed' state is not caused by an error;
// we stop listening to prevent this from being treated as an error
@ -599,7 +656,7 @@ class VideoProviderGraphql extends Component {
}
if (isLocal) {
VideoService.stopVideo(stream, sendUserUnshareWebcam, streams);
stopVideo(stream);
}
const role = VideoService.getRole(isLocal);
@ -625,7 +682,7 @@ class VideoProviderGraphql extends Component {
return this.destroyWebRTCPeer(stream);
}
destroyWebRTCPeer(stream) {
destroyWebRTCPeer(stream: string) {
let stopped = false;
const peer = this.webRtcPeers[stream];
const isLocal = VideoService.isLocalStream(stream);
@ -658,7 +715,7 @@ class VideoProviderGraphql extends Component {
return stopped;
}
_createPublisher(stream, peerOptions) {
private createPublisher(stream: string, peerOptions: Record<string, unknown>) {
return new Promise((resolve, reject) => {
try {
const { id: profileId } = VideoService.getCameraProfile();
@ -699,7 +756,7 @@ class VideoProviderGraphql extends Component {
this.replacePCVideoTracks(stream, newStream);
}
});
peer.inactivationHandler = () => this._handleLocalStreamInactive(stream);
peer.inactivationHandler = () => this.handleLocalStreamInactive(stream);
bbbVideoStream.once('inactive', peer.inactivationHandler);
resolve(offer);
}).catch(reject);
@ -709,7 +766,7 @@ class VideoProviderGraphql extends Component {
});
}
_createSubscriber(stream, peerOptions) {
private createSubscriber(stream: string, peerOptions: Record<string, unknown>) {
return new Promise((resolve, reject) => {
try {
const peer = new WebRtcPeer('recvonly', peerOptions);
@ -720,19 +777,19 @@ class VideoProviderGraphql extends Component {
peer.inboundIceQueue = [];
peer.isPublisher = false;
peer.start();
resolve();
resolve(null);
} catch (error) {
reject(error);
}
});
}
async createWebRTCPeer(stream, isLocal) {
async createWebRTCPeer(stream: string, isLocal: boolean) {
let iceServers = [];
const role = VideoService.getRole(isLocal);
const peerBuilderFunc = isLocal
? this._createPublisher.bind(this)
: this._createSubscriber.bind(this);
? this.createPublisher.bind(this)
: this.createSubscriber.bind(this);
// Check if the peer is already being processed
if (this.webRtcPeers[stream]) {
@ -747,7 +804,7 @@ class VideoProviderGraphql extends Component {
audio: false,
video: constraints,
},
onicecandidate: this._getOnIceCandidateCallback(stream, isLocal),
onicecandidate: this.getOnIceCandidateCallback(stream, isLocal),
configuration: {
},
trace: TRACE_LOGS,
@ -778,7 +835,7 @@ class VideoProviderGraphql extends Component {
}
peerBuilderFunc(stream, peerOptions).then((offer) => {
if (!this._isMounted) {
if (!this.mounted) {
return this.stopWebRTCPeer(stream, false);
}
const peer = this.webRtcPeers[stream];
@ -786,7 +843,7 @@ class VideoProviderGraphql extends Component {
if (peer && peer.peerConnection) {
const conn = peer.peerConnection;
conn.onconnectionstatechange = () => {
this._handleIceConnectionStateChange(stream, isLocal);
this.handleIceConnectionStateChange(stream, isLocal);
};
}
@ -812,14 +869,14 @@ class VideoProviderGraphql extends Component {
this.setReconnectionTimeout(stream, isLocal, false);
this.sendMessage(message);
return;
}).catch(error => {
return this._onWebRTCError(error, stream, isLocal);
return null;
}).catch((error) => {
return this.onWebRTCError(error, stream, isLocal);
});
}
}
_getWebRTCStartTimeout(stream, isLocal) {
private getWebRTCStartTimeout(stream: string, isLocal: boolean) {
const { intl } = this.props;
return () => {
@ -868,12 +925,12 @@ class VideoProviderGraphql extends Component {
};
}
_onWebRTCError(error, stream, isLocal) {
private onWebRTCError(error: Error, stream: string, isLocal: boolean) {
const { intl, streams } = this.props;
const { name: errorName, message: errorMessage } = error;
const errorLocale = intlClientErrors[errorName]
|| intlClientErrors[errorMessage]
|| intlSFUErrors[error];
const errorLocale = intlClientErrors[errorName as keyof typeof intlClientErrors]
|| intlClientErrors[errorMessage as keyof typeof intlClientErrors]
|| intlSFUErrors[error as unknown as keyof typeof intlSFUErrors];
logger.error({
logCode: 'video_provider_webrtc_peer_error',
@ -894,7 +951,7 @@ class VideoProviderGraphql extends Component {
// If it's a viewer, set the reconnection timeout. There's a good chance
// no local candidate was generated and it wasn't set.
const peer = this.webRtcPeers[stream];
const stillExists = streams.some(({ stream: streamId }) => streamId === stream);
const stillExists = streams.some((item) => item.type === 'stream' && item.stream === stream);
if (stillExists) {
const isEstablishedConnection = peer && peer.started;
@ -907,12 +964,12 @@ class VideoProviderGraphql extends Component {
}
}
reconnect(stream, isLocal) {
reconnect(stream: string, isLocal: boolean) {
this.stopWebRTCPeer(stream, true);
this.createWebRTCPeer(stream, isLocal);
}
setReconnectionTimeout(stream, isLocal, isEstablishedConnection) {
setReconnectionTimeout(stream: string, isLocal: boolean, isEstablishedConnection: boolean) {
const peer = this.webRtcPeers[stream];
const shouldSetReconnectionTimeout = !this.restartTimeout[stream] && !isEstablishedConnection;
@ -932,15 +989,16 @@ class VideoProviderGraphql extends Component {
this.restartTimer[stream] = newReconnectTimer;
this.restartTimeout[stream] = setTimeout(
this._getWebRTCStartTimeout(stream, isLocal),
this.getWebRTCStartTimeout(stream, isLocal),
this.restartTimer[stream]
);
}
return null;
}
_getOnIceCandidateCallback(stream, isLocal) {
private getOnIceCandidateCallback(stream: string, isLocal: boolean) {
if (SIGNAL_CANDIDATES) {
return (candidate) => {
return (candidate: RTCIceCandidate) => {
const peer = this.webRtcPeers[stream];
const role = VideoService.getRole(isLocal);
@ -956,7 +1014,7 @@ class VideoProviderGraphql extends Component {
return null;
}
sendIceCandidateToSFU(peer, role, candidate, stream) {
sendIceCandidateToSFU(peer: WebRtcPeer, role: string, candidate: RTCIceCandidate | undefined, stream: string) {
const message = {
type: 'video',
role,
@ -967,7 +1025,7 @@ class VideoProviderGraphql extends Component {
this.sendMessage(message);
}
_handleLocalStreamInactive(stream) {
private handleLocalStreamInactive(stream: string) {
const peer = this.webRtcPeers[stream];
const isLocal = VideoService.isLocalStream(stream);
const role = VideoService.getRole(isLocal);
@ -985,17 +1043,16 @@ class VideoProviderGraphql extends Component {
}, 'Local camera stream stopped unexpectedly');
const error = new Error('inactiveError');
this._onWebRTCError(error, stream, isLocal);
this.onWebRTCError(error, stream, isLocal);
}
_handleIceConnectionStateChange(stream, isLocal) {
const { intl } = this.props;
private handleIceConnectionStateChange(stream: string, isLocal: boolean) {
const peer = this.webRtcPeers[stream];
const role = VideoService.getRole(isLocal);
if (peer && peer.peerConnection) {
const pc = peer.peerConnection;
const connectionState = pc.connectionState;
const { connectionState } = pc;
notifyStreamStateChange(stream, connectionState);
if (connectionState === 'failed' || connectionState === 'closed') {
@ -1012,7 +1069,7 @@ class VideoProviderGraphql extends Component {
},
}, `Camera ICE connection state changed: ${connectionState}. Role: ${role}.`);
this._onWebRTCError(error, stream, isLocal);
this.onWebRTCError(error, stream, isLocal);
}
} else {
logger.error({
@ -1022,7 +1079,7 @@ class VideoProviderGraphql extends Component {
}
}
attach (peer, videoElement) {
static attach(peer: WebRtcPeer, videoElement: HTMLVideoElement) {
if (peer && videoElement) {
const stream = peer.isPublisher ? peer.getLocalStream() : peer.getRemoteStream();
videoElement.pause();
@ -1031,11 +1088,11 @@ class VideoProviderGraphql extends Component {
}
}
getVideoElement(streamId) {
getVideoElement(streamId: string) {
return this.videoTags[streamId];
}
attachVideoStream(stream) {
attachVideoStream(stream: string) {
const videoElement = this.getVideoElement(stream);
const isLocal = VideoService.isLocalStream(stream);
const peer = this.webRtcPeers[stream];
@ -1048,7 +1105,7 @@ class VideoProviderGraphql extends Component {
// This is necessary to ensure that the video element is properly
// hidden/shown when the stream is attached.
notifyStreamStateChange(stream, pc.connectionState);
this.attach(peer, videoElement);
VideoProviderGraphql.attach(peer, videoElement);
if (isLocal) {
if (peer.bbbVideoStream == null) {
@ -1062,14 +1119,14 @@ class VideoProviderGraphql extends Component {
);
const { type, name } = getSessionVirtualBackgroundInfo(deviceId);
this.restoreVirtualBackground(peer.bbbVideoStream, type, name).catch((error) => {
VideoProviderGraphql.restoreVirtualBackground(peer.bbbVideoStream, type, name).catch((error) => {
this.handleVirtualBgError(error, type, name);
});
}
}
}
startVirtualBackgroundByDrop(stream, type, name, data) {
startVirtualBackgroundByDrop(stream: string, type: string, name: string, data: string) {
return new Promise((resolve, reject) => {
const peer = this.webRtcPeers[stream];
const { bbbVideoStream } = peer;
@ -1081,13 +1138,13 @@ class VideoProviderGraphql extends Component {
.catch(reject);
}
}).catch((error) => {
this.handleVirtualBgErrorByDropping(error, type, name);
VideoProviderGraphql.handleVirtualBgErrorByDropping(error, type, name);
});
}
handleVirtualBgErrorByDropping(error, type, name) {
static handleVirtualBgErrorByDropping(error: Error, type: string, name: string) {
logger.error({
logCode: `video_provider_virtualbg_error`,
logCode: 'video_provider_virtualbg_error',
extraInfo: {
errorName: error.name,
errorMessage: error.message,
@ -1097,23 +1154,23 @@ class VideoProviderGraphql extends Component {
}, `Failed to start virtual background by dropping image: ${error.message}`);
}
restoreVirtualBackground(stream, type, name) {
static restoreVirtualBackground(stream: BBBVideoStream, type: string, name: string) {
return new Promise((resolve, reject) => {
if (type !== EFFECT_TYPES.NONE_TYPE) {
stream.startVirtualBackground(type, name).then(() => {
resolve();
}).catch((error) => {
resolve(null);
}).catch((error: Error) => {
reject(error);
});
}
resolve();
resolve(null);
});
}
handleVirtualBgError(error, type, name) {
handleVirtualBgError(error: Error, type?: string, name?: string) {
const { intl } = this.props;
logger.error({
logCode: `video_provider_virtualbg_error`,
logCode: 'video_provider_virtualbg_error',
extraInfo: {
errorName: error.name,
errorMessage: error.message,
@ -1125,7 +1182,7 @@ class VideoProviderGraphql extends Component {
notify(intl.formatMessage(intlClientErrors.virtualBgGenericError), 'error', 'video');
}
createVideoTag(stream, video) {
createVideoTag(stream: string, video: HTMLVideoElement) {
const peer = this.webRtcPeers[stream];
this.videoTags[stream] = video;
@ -1134,7 +1191,7 @@ class VideoProviderGraphql extends Component {
}
}
destroyVideoTag(stream) {
destroyVideoTag(stream: string) {
const videoElement = this.videoTags[stream];
if (videoElement == null) return;
@ -1147,7 +1204,7 @@ class VideoProviderGraphql extends Component {
delete this.videoTags[stream];
}
handlePlayStop(message) {
handlePlayStop(message: { cameraId: string; role: string }) {
const { intl } = this.props;
const { cameraId: stream, role } = message;
@ -1163,7 +1220,7 @@ class VideoProviderGraphql extends Component {
this.stopWebRTCPeer(stream, false);
}
handlePlayStart(message) {
handlePlayStart(message: { cameraId: string; role: string }) {
const { cameraId: stream, role } = message;
const peer = this.webRtcPeers[stream];
const { playStart } = this.props;
@ -1192,8 +1249,8 @@ class VideoProviderGraphql extends Component {
}
}
handleSFUError(message) {
const { intl, streams, sendUserUnshareWebcam } = this.props;
handleSFUError(message: { code: string; reason: string; streamId: string }) {
const { intl, streams, stopVideo } = this.props;
const { code, reason, streamId } = message;
const isLocal = VideoService.isLocalStream(streamId);
const role = VideoService.getRole(isLocal);
@ -1211,11 +1268,14 @@ class VideoProviderGraphql extends Component {
if (isLocal) {
// The publisher instance received an error from the server. There's no reconnect,
// stop it.
VideoService.notify(intl.formatMessage(intlSFUErrors[code] || intlSFUErrors[2200]));
VideoService.stopVideo(streamId, sendUserUnshareWebcam, streams);
VideoService.notify(
intl.formatMessage(intlSFUErrors[code as unknown as keyof typeof intlSFUErrors]
|| intlSFUErrors[2200]),
);
stopVideo(streamId);
} else {
const peer = this.webRtcPeers[streamId];
const stillExists = streams.some(({ stream }) => streamId === stream);
const stillExists = streams.some((item) => item.type === 'stream' && streamId === item.stream);
if (stillExists) {
const isEstablishedConnection = peer && peer.started;
@ -1226,7 +1286,7 @@ class VideoProviderGraphql extends Component {
}
}
replacePCVideoTracks(streamId, mediaStream) {
replacePCVideoTracks(streamId: string, mediaStream: MediaStream) {
const peer = this.webRtcPeers[streamId];
const videoElement = this.getVideoElement(streamId);
@ -1252,7 +1312,7 @@ class VideoProviderGraphql extends Component {
}
});
Promise.all(trackReplacers).then(() => {
this.attach(peer, videoElement);
VideoProviderGraphql.attach(peer, videoElement);
});
}
}
@ -1262,7 +1322,7 @@ class VideoProviderGraphql extends Component {
swapLayout,
currentVideoPageIndex,
streams,
cameraDockBounds,
cameraDock,
focusedId,
handleVideoFocus,
isGridEnabled,
@ -1275,7 +1335,7 @@ class VideoProviderGraphql extends Component {
streams,
swapLayout,
currentVideoPageIndex,
cameraDockBounds,
cameraDock,
focusedId,
handleVideoFocus,
isGridEnabled,
@ -1289,6 +1349,4 @@ class VideoProviderGraphql extends Component {
}
}
VideoProviderGraphql.propTypes = propTypes;
export default injectIntl(VideoProviderGraphql);

View File

@ -1,7 +1,4 @@
// @ts-nocheck
/* eslint-disable */
import React from 'react';
import { Session } from 'meteor/session';
import { Meteor } from 'meteor/meteor';
import { withTracker } from 'meteor/react-meteor-data';
import { useMutation } from '@apollo/client';
@ -12,39 +9,48 @@ import Settings from '/imports/ui/services/settings';
import {
useCurrentVideoPageIndex,
useExitVideo,
useInfo,
useIsUserLocked,
useLockUser,
useStopVideo,
useVideoStreams,
} from './hooks';
import { CAMERA_BROADCAST_START, CAMERA_BROADCAST_STOP } from './mutations';
import { CAMERA_BROADCAST_START } from './mutations';
import VideoProvider from './component';
import VideoService from './service';
import { Output } from '/imports/ui/components/layout/layoutTypes';
import { VideoItem } from './types';
interface VideoProviderContainerGraphqlProps {
isGridLayout: boolean;
currUserId: string;
currentUserId: string;
focusedId: string;
swapLayout: boolean;
isGridEnabled: boolean;
paginationEnabled: boolean;
isMeteorConnected: boolean;
viewParticipantsWebcams: boolean;
children: React.ReactNode;
cameraDock: Output['cameraDock'];
handleVideoFocus:(id: string) => void;
}
const VideoProviderContainerGraphql: React.FC<VideoProviderContainerGraphqlProps> = ({ children, ...props }) => {
const VideoProviderContainerGraphql: React.FC<VideoProviderContainerGraphqlProps> = (props) => {
const {
isGridLayout,
currUserId,
currentUserId,
paginationEnabled,
viewParticipantsWebcams,
cameraDock,
focusedId,
handleVideoFocus,
isGridEnabled,
isMeteorConnected,
swapLayout,
} = props;
const [cameraBroadcastStart] = useMutation(CAMERA_BROADCAST_START);
const [cameraBroadcastStop] = useMutation(CAMERA_BROADCAST_STOP);
const sendUserShareWebcam = (cameraId: string) => {
return cameraBroadcastStart({ variables: { cameraId } });
};
const sendUserUnshareWebcam = (cameraId: string) => {
cameraBroadcastStop({ variables: { cameraId } });
};
const playStart = (cameraId: string) => {
if (VideoService.isLocalStream(cameraId)) {
sendUserShareWebcam(cameraId).then(() => {
@ -68,11 +74,11 @@ const VideoProviderContainerGraphql: React.FC<VideoProviderContainerGraphqlProps
gridUsers,
totalNumberOfStreams,
users,
} = useVideoStreams(isGridLayout, paginationEnabled, viewParticipantsWebcams);
} = useVideoStreams(isGridEnabled, paginationEnabled, viewParticipantsWebcams);
let usersVideo = streams;
let usersVideo: VideoItem[] = streams;
if (gridUsers.length > 0 && isGridLayout) {
if (gridUsers.length > 0 && isGridEnabled) {
usersVideo = usersVideo.concat(gridUsers);
}
@ -80,45 +86,71 @@ const VideoProviderContainerGraphql: React.FC<VideoProviderContainerGraphqlProps
currentMeeting?.usersPolicies?.webcamsOnlyForModerator
&& currentUser?.locked
) {
usersVideo = usersVideo.filter((uv) => uv.isUserModerator || uv.userId === currUserId);
usersVideo = usersVideo.filter(
(uv) => (uv.type !== 'connecting' && uv.isModerator) || uv.userId === currentUserId,
);
}
const isUserLocked = useIsUserLocked();
const currentVideoPageIndex = useCurrentVideoPageIndex();
const exitVideo = useExitVideo();
const lockUser = useLockUser();
const stopVideo = useStopVideo();
const info = useInfo();
if (!usersVideo.length && !isGridEnabled) return null;
return (
!usersVideo.length && !isGridLayout
? null
: (
<VideoProvider
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
playStart={playStart}
sendUserUnshareWebcam={sendUserUnshareWebcam}
streams={usersVideo}
totalNumberOfStreams={totalNumberOfStreams}
isUserLocked={isUserLocked}
currentVideoPageIndex={currentVideoPageIndex}
exitVideo={exitVideo}
users={users}
>
{children}
</VideoProvider>
)
<VideoProvider
cameraDock={cameraDock}
focusedId={focusedId}
handleVideoFocus={handleVideoFocus}
isGridEnabled={isGridEnabled}
isMeteorConnected={isMeteorConnected}
swapLayout={swapLayout}
currentUserId={currentUserId}
paginationEnabled={paginationEnabled}
viewParticipantsWebcams={viewParticipantsWebcams}
totalNumberOfStreams={totalNumberOfStreams}
isUserLocked={isUserLocked}
currentVideoPageIndex={currentVideoPageIndex}
streams={usersVideo}
users={users}
info={info}
playStart={playStart}
exitVideo={exitVideo}
lockUser={lockUser}
stopVideo={stopVideo}
/>
);
};
export default withTracker(() => {
const isGridLayout = Session.get('isGridEnabled');
const currUserId = Auth.userID;
const isMeteorConnected = Meteor.status().connected;
type TrackerData = {
currentUserId: string;
isMeteorConnected: boolean;
paginationEnabled: boolean;
viewParticipantsWebcams: boolean;
};
type TrackerProps = {
swapLayout: boolean;
cameraDock: Output['cameraDock'];
focusedId: string;
handleVideoFocus:(id: string) => void;
isGridEnabled: boolean;
};
export default withTracker<TrackerData, TrackerProps>(() => {
const currentUserId = Auth.userID ?? '';
const isMeteorConnected = Meteor.status().connected;
// @ts-expect-error -> Untyped object.
const { paginationEnabled } = Settings.application;
// @ts-expect-error -> Untyped object.
const { viewParticipantsWebcams } = Settings.dataSaving;
return {
currUserId,
isGridLayout,
currentUserId,
isMeteorConnected,
paginationEnabled: Settings.application.paginationEnabled,
viewParticipantsWebcams: Settings.dataSaving.viewParticipantsWebcams,
paginationEnabled,
viewParticipantsWebcams,
};
})(VideoProviderContainerGraphql);

View File

@ -1,12 +1,12 @@
// @ts-nocheck
/* eslint-disable */
import { useCallback, useEffect, useMemo } from 'react';
import { useSubscription, useReactiveVar, useLazyQuery, useMutation } from '@apollo/client';
import { useCallback, useEffect, useState } from 'react';
import {
useSubscription,
useReactiveVar,
useLazyQuery,
useMutation,
} from '@apollo/client';
import { Meteor } from 'meteor/meteor';
import Settings from '/imports/ui/services/settings';
import Auth from '/imports/ui/services/auth';
import Users from '/imports/api/users';
import getFromUserSettings from '/imports/ui/services/users-settings';
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
import useMeeting from '/imports/ui/core/hooks/useMeeting';
import { partition } from '/imports/utils/array-utils';
@ -14,7 +14,7 @@ import { USER_AGGREGATE_COUNT_SUBSCRIPTION } from '/imports/ui/core/graphql/quer
import {
getSortingMethod,
sortVideoStreams,
} from '/imports/ui/components/video-provider/stream-sorting';
} from '/imports/ui/components/video-provider/video-provider-graphql/stream-sorting';
import {
useVideoState,
setVideoState,
@ -28,15 +28,17 @@ import {
VIDEO_STREAMS_USERS_SUBSCRIPTION,
VIEWERS_IN_WEBCAM_COUNT_SUBSCRIPTION,
VideoStreamsUsersResponse,
OwnVideoStreamsResponse,
} from '../queries';
import videoService from '../service';
import { CAMERA_BROADCAST_STOP } from '../mutations';
import { GridItem, StreamItem } from '../types';
import { DesktopPageSizes, MobilePageSizes } from '/imports/ui/Types/meetingClientSettings';
import logger from '/imports/startup/client/logger';
const ROLE_MODERATOR = window.meetingClientSettings.public.user.role_moderator;
const ROLE_VIEWER = window.meetingClientSettings.public.user.role_viewer;
const MIRROR_WEBCAM = window.meetingClientSettings.public.app.mirrorOwnWebcam;
const {
paginationToggleEnabled: PAGINATION_TOGGLE_ENABLED,
desktopPageSizes: DESKTOP_PAGE_SIZES,
mobilePageSizes: MOBILE_PAGE_SIZES,
desktopGridSizes: DESKTOP_GRID_SIZES,
@ -52,27 +54,10 @@ const {
defaultSorting: DEFAULT_SORTING,
} = window.meetingClientSettings.public.kurento.cameraSortingModes;
export const useFetchedVideoStreams = () => {
const { streams: s } = useStreams();
let streams = [...s];
const connectingStream = useConnectingStream(streams);
const isPaginationEnabled = useIsPaginationEnabled();
const isPaginationDisabled = !isPaginationEnabled;
const { viewParticipantsWebcams } = Settings.dataSaving;
if (!viewParticipantsWebcams) streams = videoService.filterLocalOnly(streams);
if (connectingStream) {
streams.push(connectingStream);
}
const pages = useVideoPage(streams);
if (!isPaginationDisabled) {
return pages;
}
return streams;
};
const FILTER_VIDEO_STATS = [
'outbound-rtp',
'inbound-rtp',
];
export const useStatus = () => {
const videoState = useVideoState()[0];
@ -97,25 +82,13 @@ export const useDisableReason = () => {
return disableReason;
};
export const useRole = (isLocal: boolean) => {
return isLocal ? 'share' : 'viewer';
};
export const useMyStreamId = (deviceId: string) => {
const { streams } = useStreams();
const videoStream = streams.find(
(vs) => vs.userId === Auth.userID && vs.deviceId === deviceId,
);
return videoStream ? videoStream.stream : null;
};
export const useIsUserLocked = () => {
const disableCam = useDisableCam();
const { data: currentUser } = useCurrentUser((u) => ({
locked: u.locked,
isModerator: u.isModerator,
}));
return currentUser?.locked && !currentUser.isModerator && disableCam;
return !!currentUser?.locked && !currentUser.isModerator && disableCam;
};
export const useVideoStreamsCount = () => {
@ -134,6 +107,7 @@ export const useLocalVideoStreamsCount = () => {
export const useInfo = () => {
const { data } = useMeeting((m) => ({
voiceSettings: {
// @ts-expect-error -> There seems to be a design issue on the projection portion.
voiceConf: m.voiceSettings?.voiceConf,
},
}));
@ -147,20 +121,11 @@ export const useInfo = () => {
};
};
export const useMirrorOwnWebcam = (userId = null) => {
// only true if setting defined and video ids match
const isOwnWebcam = userId ? Auth.userID === userId : true;
const isEnabledMirroring = getFromUserSettings(
'bbb_mirror_own_webcam',
MIRROR_WEBCAM,
);
return isOwnWebcam && isEnabledMirroring;
};
export const useHasCapReached = () => {
const { data: meeting } = useMeeting((m) => ({
meetingCameraCap: m.meetingCameraCap,
usersPolicies: {
// @ts-expect-error -> There seems to be a design issue on the projection portion.
userCameraCap: m.usersPolicies?.userCameraCap,
},
}));
@ -175,82 +140,23 @@ export const useHasCapReached = () => {
const { meetingCameraCap } = meeting;
const { userCameraCap } = meeting.usersPolicies;
// @ts-expect-error -> There seems to be a design issue on the projection portion.
const meetingCap = meetingCameraCap !== 0 && videoStreamsCount >= meetingCameraCap;
const userCap = userCameraCap !== 0 && localVideoStreamsCount >= userCameraCap;
return meetingCap || userCap;
};
export const useWebcamsOnlyForModerator = () => {
const { data: meeting } = useMeeting((m) => ({
usersPolicies: {
webcamsOnlyForModerator: m.usersPolicies?.webcamsOnlyForModerator,
},
}));
const user = Users.findOne(
{ userId: Auth.userID },
{ fields: { locked: 1, role: 1 } },
);
if (meeting?.usersPolicies && user?.role !== ROLE_MODERATOR && user?.locked) {
return meeting.usersPolicies.webcamsOnlyForModerator;
}
return false;
};
export const useDisableCam = () => {
const { data: meeting } = useMeeting((m) => ({
lockSettings: {
// @ts-expect-error -> There seems to be a design issue on the projection portion.
disableCam: m.lockSettings?.disableCam,
},
}));
return meeting?.lockSettings ? meeting?.lockSettings.disableCam : false;
};
export const useVideoPinByUser = (userId: string) => {
const user = Users.findOne({ userId }, { fields: { pin: 1 } });
return user?.pin || false;
};
export const useSetNumberOfPages = (
numberOfPublishers: number,
numberOfSubscribers: number,
pageSize: number,
) => {
let { currentVideoPageIndex, numberOfPages } = useVideoState()[0];
useEffect(() => {
// Page size 0 means no pagination, return itself
if (pageSize === 0) return;
// Page size refers only to the number of subscribers. Publishers are always
// shown, hence not accounted for
const nOfPages = Math.ceil(numberOfSubscribers / pageSize);
if (nOfPages !== numberOfPages) {
numberOfPages = nOfPages;
// Check if we have to page back on the current video page index due to a
// page ceasing to exist
if (nOfPages === 0) {
currentVideoPageIndex = 0;
} else if (currentVideoPageIndex + 1 > numberOfPages) {
videoService.getPreviousVideoPage();
}
videoService.numberOfPages = nOfPages;
videoService.currentVideoPageIndex = currentVideoPageIndex;
setVideoState((curr) => ({
...curr,
numberOfPages,
currentVideoPageIndex,
}));
}
}, [numberOfPublishers, numberOfSubscribers, pageSize, currentVideoPageIndex, numberOfPages]);
return null;
};
export const usePageSizeDictionary = () => {
const { data: countData } = useSubscription(
USER_AGGREGATE_COUNT_SUBSCRIPTION,
@ -265,7 +171,10 @@ export const usePageSizeDictionary = () => {
// matching threshold entry, return the val.
let targetThreshold;
const processThreshold = (
threshold = {
threshold: {
desktopPageSizes?: DesktopPageSizes,
mobilePageSizes?: MobilePageSizes,
} = {
desktopPageSizes: DESKTOP_PAGE_SIZES,
mobilePageSizes: MOBILE_PAGE_SIZES,
},
@ -309,25 +218,28 @@ export const useMyPageSize = () => {
let size;
switch (myRole) {
case ROLE_MODERATOR:
size = pageSizes.moderator;
size = pageSizes?.moderator;
break;
case ROLE_VIEWER:
default:
size = pageSizes.viewer;
size = pageSizes?.viewer;
}
return size;
return size ?? 0;
};
export const useShouldRenderPaginationToggle = () => PAGINATION_TOGGLE_ENABLED && useMyPageSize() > 0;
export const useIsPaginationEnabled = (paginationEnabled) => paginationEnabled && useMyPageSize() > 0;
export const useIsPaginationEnabled = (paginationEnabled: boolean) => paginationEnabled && useMyPageSize() > 0;
export const useStreams = () => {
const videoStreams = useReactiveVar(streams);
return { streams: videoStreams };
};
type StreamUser = VideoStreamsUsersResponse['user'][number] & {
pin: boolean;
sortName: string;
};
export const useStreamUsers = (isGridEnabled: boolean) => {
const { streams } = useStreams();
const subscription = isGridEnabled
@ -335,24 +247,34 @@ export const useStreamUsers = (isGridEnabled: boolean) => {
: VIDEO_STREAMS_USERS_FILTERED_SUBSCRIPTION;
const variables = isGridEnabled
? {}
: { userIds: streams.map((s) => s.userId) }
: { userIds: streams.map((s) => s.userId) };
const { data, loading, error } = useSubscription<VideoStreamsUsersResponse>(
subscription,
{ variables },
);
const users = useMemo(
() => (data
? data.user.map((user) => ({
const [users, setUsers] = useState<StreamUser[]>([]);
useEffect(() => {
if (loading) return;
if (error) {
logger.error(`Error on load stream users. name=${error.name}`, error);
}
if (data) {
const newData = data.user.map((user) => ({
...user,
pin: user.pinned,
sortName: user.nameSortable,
}))
: []),
[data],
);
}));
setUsers(newData);
} else {
setUsers([]);
}
}, [data]);
return {
users: users || [],
users,
loading,
error,
};
@ -367,11 +289,6 @@ export const useSharedDevices = () => {
return devices;
};
export const useUserIdsFromVideoStreams = () => {
const { streams } = useStreams();
return streams.map((s) => s.userId);
};
export const useNumberOfPages = () => {
const state = useVideoState()[0];
return state.numberOfPages;
@ -401,40 +318,6 @@ export const useGridSize = () => {
return size;
};
export const useVideoPage = (streams) => {
const numberOfPages = useNumberOfPages();
const currentVideoPageIndex = useCurrentVideoPageIndex();
const pageSize = useMyPageSize();
// Publishers are taken into account for the page size calculations. They
// also appear on every page. Same for pinned user.
const [filtered, others] = partition(
streams,
(vs) => Auth.userID === vs.userId || vs.pin,
);
// Separate pin from local cameras
const [pin, mine] = partition(filtered, (vs) => vs.pin);
// Recalculate total number of pages
useSetNumberOfPages(filtered.length, others.length, pageSize);
const chunkIndex = currentVideoPageIndex * pageSize;
// This is an extra check because pagination is globally in effect (hard
// limited page sizes, toggles on), but we might still only have one page.
// Use the default sorting method if that's the case.
const sortingMethod = numberOfPages > 1 ? PAGINATION_SORTING : DEFAULT_SORTING;
const paginatedStreams = sortVideoStreams(others, sortingMethod).slice(
chunkIndex,
chunkIndex + pageSize,
) || [];
if (getSortingMethod(sortingMethod).localFirst) {
return [...pin, ...mine, ...paginatedStreams];
}
return [...pin, ...paginatedStreams, ...mine];
};
export const useVideoStreams = (
isGridEnabled: boolean,
paginationEnabled: boolean,
@ -443,13 +326,13 @@ export const useVideoStreams = (
const [state] = useVideoState();
const { currentVideoPageIndex, numberOfPages } = state;
const { users } = useStreamUsers(isGridEnabled);
const { streams: videoStreams} = useStreams();
const { streams: videoStreams } = useStreams();
const connectingStream = useConnectingStream(videoStreams);
const gridSize = useGridSize();
const myPageSize = useMyPageSize();
const isPaginationEnabled = useIsPaginationEnabled(paginationEnabled);
let streams = [...videoStreams];
let gridUsers = [];
let streams: StreamItem[] = [...videoStreams];
let gridUsers: GridItem[] = [];
if (connectingStream) streams.push(connectingStream);
@ -458,8 +341,14 @@ export const useVideoStreams = (
}
if (isPaginationEnabled) {
const [filtered, others] = partition(streams, (vs) => Auth.userID === vs.userId || vs.pin);
const [pin, mine] = partition(filtered, (vs) => vs.pin);
const [filtered, others] = partition(
streams,
(vs: StreamItem) => Auth.userID === vs.userId || (vs.type === 'stream' && vs.pin),
);
const [pin, mine] = partition(
filtered,
(vs: StreamItem) => vs.type === 'stream' && vs.pin,
);
if (myPageSize !== 0) {
const total = others.length ?? 0;
@ -505,7 +394,7 @@ export const useVideoStreams = (
(user) => !user.loggedOut && !user.left && !streamUsers.includes(user.userId),
)
.map((user) => ({
isGridItem: true,
type: 'grid' as const,
...user,
}))
.slice(0, gridSize - streams.length);
@ -519,108 +408,142 @@ export const useVideoStreams = (
};
};
export const useGridUsers = (users = []) => {
const pageSize = useMyPageSize();
const { streams } = useStreams();
const paginatedStreams = useVideoPage(streams, pageSize);
const isPaginationEnabled = useIsPaginationEnabled();
const isPaginationDisabled = !isPaginationEnabled || pageSize === 0;
const isGridEnabled = videoService.isGridEnabled();
let gridUsers = [];
if (isPaginationDisabled) {
if (isGridEnabled) {
const streamUsers = streams.map((stream) => stream.userId);
gridUsers = users
.filter(
(user) => !user.loggedOut && !user.left && !streamUsers.includes(user.userId),
)
.map((user) => ({
isGridItem: true,
...user,
}));
}
return gridUsers;
}
if (isGridEnabled) {
const streamUsers = paginatedStreams.map((stream) => stream.userId);
gridUsers = users
.filter(
(user) => !user.loggedOut && !user.left && !streamUsers.includes(user.userId),
)
.map((user) => ({
isGridItem: true,
...user,
}));
}
return gridUsers;
};
export const useHasVideoStream = () => {
const { streams } = useStreams();
return streams.some((s) => s.userId === Auth.userID);
};
export const useHasStream = (streams, stream) => {
return streams.find((s) => s.stream === stream);
};
const useOwnVideoStreamsQuery = () => useLazyQuery<OwnVideoStreamsResponse>(
OWN_VIDEO_STREAMS_QUERY,
{ variables: { userId: Auth.userID } },
);
export const useFilterModeratorOnly = (streams) => {
const amIViewer = useMyRole() === ROLE_VIEWER;
if (amIViewer) {
const moderators = Users.find(
{
role: ROLE_MODERATOR,
},
{ fields: { userId: 1 } },
)
.fetch()
.map((user) => user.userId);
return streams.reduce((result, stream) => {
const { userId } = stream;
const isModerator = moderators.includes(userId);
const isMe = Auth.userID === userId;
if (isModerator || isMe) result.push(stream);
return result;
}, []);
}
return streams;
};
export const useExitVideo = () => {
export const useExitVideo = (forceExit = false) => {
const [cameraBroadcastStop] = useMutation(CAMERA_BROADCAST_STOP);
const [getOwnVideoStreams] = useLazyQuery(OWN_VIDEO_STREAMS_QUERY, { variables: { userId: Auth.userID } });
const [getOwnVideoStreams] = useOwnVideoStreamsQuery();
const exitVideo = useCallback(() => {
const exitVideo = useCallback(async () => {
const { isConnected } = getVideoState();
if (isConnected) {
if (isConnected || forceExit) {
const sendUserUnshareWebcam = (cameraId: string) => {
cameraBroadcastStop({ variables: { cameraId } });
};
getOwnVideoStreams().then(({ data }) => {
if (!data) return;
const streams = data.user_camera || [];
streams.forEach((s) => sendUserUnshareWebcam(s.streamId));
videoService.exitedVideo();
return getOwnVideoStreams().then(({ data }) => {
if (data) {
const streams = data.user_camera || [];
streams.forEach((s: { streamId: string }) => sendUserUnshareWebcam(s.streamId));
videoService.exitedVideo();
}
return null;
});
}
return Promise.resolve(null);
}, [cameraBroadcastStop]);
return exitVideo;
};
export const useViewersInWebcamCount = () => {
export const useViewersInWebcamCount = (): number => {
const { data } = useSubscription(VIEWERS_IN_WEBCAM_COUNT_SUBSCRIPTION);
return data?.user_camera_aggregate?.aggregate?.count || 0;
};
export const useLockUser = () => {
const exitVideo = useExitVideo();
return useCallback(() => {
const { isConnected } = getVideoState();
if (isConnected) {
exitVideo();
}
}, [exitVideo]);
};
export const useStopVideo = () => {
const [cameraBroadcastStop] = useMutation(CAMERA_BROADCAST_STOP);
const [getOwnVideoStreams] = useOwnVideoStreamsQuery();
const exit = useExitVideo(true);
return useCallback(async (cameraId?: string) => {
const { data } = await getOwnVideoStreams();
const streams = data?.user_camera ?? [];
const hasTargetStream = streams.some((s) => s.streamId === cameraId);
const hasOtherStream = streams.some((s) => s.streamId !== cameraId);
if (hasTargetStream) {
cameraBroadcastStop({ variables: { cameraId } });
}
if (!hasOtherStream) {
videoService.exitedVideo();
} else {
exit().then(() => {
videoService.exitedVideo();
});
}
}, [cameraBroadcastStop]);
};
export const useActivePeers = (
isGridEnabled: boolean,
paginationEnabled: boolean,
viewParticipantsWebcams: boolean,
) => {
const videoData = useVideoStreams(
isGridEnabled,
paginationEnabled,
viewParticipantsWebcams,
);
if (!videoData) return null;
const { streams: activeVideoStreams } = videoData;
if (!activeVideoStreams) return null;
const activePeers: Record<string, RTCPeerConnection> = {};
activeVideoStreams.forEach((stream) => {
if (videoService.webRtcPeersRef()[stream.stream]) {
activePeers[stream.stream] = videoService.webRtcPeersRef()[stream.stream].peerConnection;
}
});
return activePeers;
};
export const useGetStats = (
isGridEnabled: boolean,
paginationEnabled: boolean,
viewParticipantsWebcams: boolean,
) => {
const peers = useActivePeers(
isGridEnabled,
paginationEnabled,
viewParticipantsWebcams,
);
return useCallback(async () => {
if (!peers) return null;
const stats: Record<string, unknown> = {};
await Promise.all(
Object.keys(peers).map(async (peerId) => {
const peerStats = await peers[peerId].getStats();
const videoStats: Record<string, unknown> = {};
peerStats.forEach((stat) => {
if (FILTER_VIDEO_STATS.includes(stat.type)) {
videoStats[stat.type] = stat;
}
});
stats[peerId] = videoStats;
}),
);
return stats;
}, [peers]);
};

View File

@ -1,10 +1,9 @@
// @ts-nocheck
/* eslint-disable */
import React, { Component } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import { IntlShape, defineMessages, injectIntl } from 'react-intl';
import { notify } from '/imports/ui/services/notification';
import { toast } from 'react-toastify';
import Styled from './styles';
import { LockSettings } from '/imports/ui/Types/meeting';
const intlMessages = defineMessages({
suggestLockTitle: {
@ -27,8 +26,21 @@ const intlMessages = defineMessages({
const REPEAT_INTERVAL = 120000;
class LockViewersNotifyComponent extends Component {
constructor(props) {
interface LockViewersNotifyComponentProps {
toggleWebcamsOnlyForModerator: () => void;
currentUserIsModerator: boolean;
viewersInWebcam: number;
limitOfViewersInWebcam: number;
limitOfViewersInWebcamIsEnable: boolean;
lockSettings: LockSettings;
webcamOnlyForModerator: boolean;
intl: IntlShape;
}
class LockViewersNotifyComponent extends Component<LockViewersNotifyComponentProps, object> {
private interval: NodeJS.Timeout | null;
constructor(props: LockViewersNotifyComponentProps) {
super(props);
this.interval = null;
this.intervalCallback = this.intervalCallback.bind(this);
@ -55,7 +67,7 @@ class LockViewersNotifyComponent extends Component {
this.intervalCallback();
}
if (webcamForViewersIsLocked || (!viwerersInWebcamGreaterThatLimit && this.interval)) {
clearInterval(this.interval);
if (this.interval) clearInterval(this.interval);
this.interval = null;
}
}

View File

@ -1,5 +1,3 @@
// @ts-nocheck
/* eslint-disable */
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import Meetings from '/imports/api/meetings';
@ -9,8 +7,16 @@ import ManyUsersComponent from './component';
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
import { SET_WEBCAM_ONLY_FOR_MODERATOR } from '/imports/ui/components/lock-viewers/mutations';
import { useViewersInWebcamCount } from '../hooks';
import { LockSettings } from '/imports/ui/Types/meeting';
const ManyUsersContainer = (props) => {
interface ManyUsersContainerProps {
lockSettings: LockSettings,
webcamOnlyForModerator: boolean,
limitOfViewersInWebcam: number,
limitOfViewersInWebcamIsEnable: boolean,
}
const ManyUsersContainer: React.FC<ManyUsersContainerProps> = (props) => {
const { data: currentUserData } = useCurrentUser((user) => ({
isModerator: user.isModerator,
}));
@ -27,15 +33,24 @@ const ManyUsersContainer = (props) => {
const viewersInWebcam = useViewersInWebcamCount();
const currentUserIsModerator = currentUserData?.isModerator;
const currentUserIsModerator = !!currentUserData?.isModerator;
const {
limitOfViewersInWebcam,
limitOfViewersInWebcamIsEnable,
lockSettings,
webcamOnlyForModerator,
} = props;
return (
<ManyUsersComponent
{...{
toggleWebcamsOnlyForModerator,
currentUserIsModerator,
viewersInWebcam,
...props,
}}
toggleWebcamsOnlyForModerator={toggleWebcamsOnlyForModerator}
currentUserIsModerator={currentUserIsModerator}
viewersInWebcam={viewersInWebcam}
limitOfViewersInWebcam={limitOfViewersInWebcam}
limitOfViewersInWebcamIsEnable={limitOfViewersInWebcamIsEnable}
lockSettings={lockSettings}
webcamOnlyForModerator={webcamOnlyForModerator}
/>
);
};

View File

@ -1,8 +1,6 @@
// @ts-nocheck
/* eslint-disable */
import styled from 'styled-components';
import { colorPrimary } from '/imports/ui/stylesheets/styled-components/palette';
import Styled from '/imports/ui/components/breakout-room/styles';
import Styled from '/imports/ui/components/breakout-room/breakout-room/styles';
import Button from '/imports/ui/components/common/button/component';
const Info = styled.p`
@ -17,6 +15,7 @@ const ButtonWrapper = styled(Styled.BreakoutActions)`
}
`;
// @ts-expect-error -> Untyped component.
const ManyUsersButton = styled(Button)`
flex: 0 1 48%;
color: ${colorPrimary};

View File

@ -33,13 +33,20 @@ export interface VideoStreamsUsersResponse {
presenter: boolean;
clientType: string;
raiseHand: boolean;
isModerator: boolean
isModerator: boolean;
left: boolean;
reaction: {
reactionEmoji: string;
};
}[];
}
export interface OwnVideoStreamsResponse {
user_camera: {
streamId: string;
}[];
}
export const VIDEO_STREAMS_SUBSCRIPTION = gql`
subscription VideoStreams {
user_camera {
@ -142,6 +149,7 @@ export const VIDEO_STREAMS_USERS_FILTERED_SUBSCRIPTION = gql`
`;
export default {
OWN_VIDEO_STREAMS_QUERY,
VIDEO_STREAMS_SUBSCRIPTION,
VIEWERS_IN_WEBCAM_COUNT_SUBSCRIPTION,
VIDEO_STREAMS_USERS_SUBSCRIPTION,

View File

@ -12,20 +12,21 @@ const [useVideoState, setVideoState, videoState] = createUseLocalState({
const getVideoState = () => videoState();
type ConnectingStream = {
export type ConnectingStream = {
stream: string;
name: string;
userId: string;
type: 'connecting';
} | null;
const connectingStream = makeVar<ConnectingStream>(null);
const useConnectingStream = (streams: { stream: string }[]) => {
const useConnectingStream = (streams?: Stream[]) => {
const connecting = useReactiveVar(connectingStream);
if (!connecting) return null;
const hasStream = streams.find((s) => s.stream === connecting.stream);
const hasStream = streams && streams.find((s) => s.stream === connecting.stream);
if (hasStream) {
return null;
@ -47,7 +48,8 @@ export type Stream = {
pin: boolean;
floor: boolean;
lastFloorTime: string;
isUserModerator: boolean;
isModerator: boolean;
type: 'stream';
}
const streams = makeVar<Stream[]>([]);
@ -56,6 +58,8 @@ const setStreams = (vs: Stream[]) => {
streams(vs);
};
const getStreams = () => streams();
export {
useVideoState,
setVideoState,
@ -63,6 +67,7 @@ export {
useConnectingStream,
setConnectingStream,
setStreams,
getStreams,
streams,
};
@ -73,5 +78,6 @@ export default {
useConnectingStream,
setConnectingStream,
setStreams,
getStreams,
streams,
};

View File

@ -1,12 +1,17 @@
// @ts-nocheck
/* eslint-disable */
import { StreamItem } from './types';
import UserListService from '/imports/ui/components/user-list/service';
import Auth from '/imports/ui/services/auth';
const DEFAULT_SORTING_MODE = 'LOCAL_ALPHABETICAL';
// pin first
export const sortPin = (s1, s2) => {
export const sortPin = (s1: StreamItem, s2: StreamItem) => {
if (s1.type === 'connecting') {
return 1;
}
if (s2.type === 'connecting') {
return -1;
}
if (s1.pin) {
return -1;
} if (s2.pin) {
@ -15,19 +20,27 @@ export const sortPin = (s1, s2) => {
return 0;
};
export const mandatorySorting = (s1, s2) => sortPin(s1, s2);
export const mandatorySorting = (s1: StreamItem, s2: StreamItem) => sortPin(s1, s2);
// lastFloorTime, descending
export const sortVoiceActivity = (s1, s2) => {
export const sortVoiceActivity = (s1: StreamItem, s2: StreamItem) => {
if (s1.type === 'connecting') {
return 1;
}
if (s2.type === 'connecting') {
return -1;
}
if (s2.lastFloorTime < s1.lastFloorTime) {
return -1;
} else if (s2.lastFloorTime > s1.lastFloorTime) {
}
if (s2.lastFloorTime > s1.lastFloorTime) {
return 1;
} else return 0;
}
return 0;
};
// pin -> lastFloorTime (descending) -> alphabetical -> local
export const sortVoiceActivityLocal = (s1, s2) => {
export const sortVoiceActivityLocal = (s1: StreamItem, s2: StreamItem) => {
if (s1.userId === Auth.userID) {
return 1;
} if (s2.userId === Auth.userID) {
@ -40,26 +53,28 @@ export const sortVoiceActivityLocal = (s1, s2) => {
};
// pin -> local -> lastFloorTime (descending) -> alphabetical
export const sortLocalVoiceActivity = (s1, s2) => mandatorySorting(s1, s2)
export const sortLocalVoiceActivity = (s1: StreamItem, s2: StreamItem) => mandatorySorting(s1, s2)
|| UserListService.sortUsersByCurrent(s1, s2)
|| sortVoiceActivity(s1, s2)
|| UserListService.sortUsersByName(s1, s2);
// pin -> local -> alphabetic
export const sortLocalAlphabetical = (s1, s2) => mandatorySorting(s1, s2)
export const sortLocalAlphabetical = (s1: StreamItem, s2: StreamItem) => mandatorySorting(s1, s2)
|| UserListService.sortUsersByCurrent(s1, s2)
|| UserListService.sortUsersByName(s1, s2);
export const sortPresenter = (s1, s2) => {
export const sortPresenter = (s1: StreamItem, s2: StreamItem) => {
if (UserListService.isUserPresenter(s1.userId)) {
return -1;
} else if (UserListService.isUserPresenter(s2.userId)) {
}
if (UserListService.isUserPresenter(s2.userId)) {
return 1;
} else return 0;
}
return 0;
};
// pin -> local -> presenter -> alphabetical
export const sortLocalPresenterAlphabetical = (s1, s2) => mandatorySorting(s1, s2)
export const sortLocalPresenterAlphabetical = (s1: StreamItem, s2: StreamItem) => mandatorySorting(s1, s2)
|| UserListService.sortUsersByCurrent(s1, s2)
|| sortPresenter(s1, s2)
|| UserListService.sortUsersByName(s1, s2);
@ -95,12 +110,13 @@ const SORTING_METHODS = Object.freeze({
LOCAL_ALPHABETICAL: {
sortingMethod: sortLocalAlphabetical,
neededDataTypes: MANDATORY_DATA_TYPES,
filter: false,
localFirst: true,
},
VOICE_ACTIVITY_LOCAL: {
sortingMethod: sortVoiceActivityLocal,
neededDataTypes: {
lastFloorTime: 1, floor: 1, ...MANDATORY_DATA_TYPES,
lastFloorTime: 1, ...MANDATORY_DATA_TYPES,
},
filter: true,
localFirst: false,
@ -108,7 +124,7 @@ const SORTING_METHODS = Object.freeze({
LOCAL_VOICE_ACTIVITY: {
sortingMethod: sortLocalVoiceActivity,
neededDataTypes: {
lastFloorTime: 1, floor: 1, ...MANDATORY_DATA_TYPES,
lastFloorTime: 1, ...MANDATORY_DATA_TYPES,
},
filter: true,
localFirst: true,
@ -116,27 +132,17 @@ const SORTING_METHODS = Object.freeze({
LOCAL_PRESENTER_ALPHABETICAL: {
sortingMethod: sortLocalPresenterAlphabetical,
neededDataTypes: MANDATORY_DATA_TYPES,
filter: false,
localFirst: true,
}
},
});
export const getSortingMethod = (identifier) => {
return SORTING_METHODS[identifier] || SORTING_METHODS[DEFAULT_SORTING_MODE];
export const getSortingMethod = (identifier: string) => {
return SORTING_METHODS[identifier as keyof typeof SORTING_METHODS] || SORTING_METHODS[DEFAULT_SORTING_MODE];
};
export const sortVideoStreams = (streams, mode) => {
const { sortingMethod, filter } = getSortingMethod(mode);
export const sortVideoStreams = (streams: StreamItem[], mode: string) => {
const { sortingMethod } = getSortingMethod(mode);
const sorted = streams.sort(sortingMethod);
if (!filter) return sorted;
return sorted.map(videoStream => ({
stream: videoStream.stream,
isGridItem: videoStream?.isGridItem,
userId: videoStream.userId,
name: videoStream.name,
sortName: videoStream.sortName,
floor: videoStream.floor,
pin: videoStream.pin,
}));
return sorted;
};

View File

@ -0,0 +1,7 @@
import { VideoStreamsUsersResponse } from './queries';
import { ConnectingStream, Stream } from './state';
export type StreamUser = VideoStreamsUsersResponse['user'][number];
export type StreamItem = Stream | NonNullable<ConnectingStream>;
export type GridItem = StreamUser & { type: 'grid' };
export type VideoItem = StreamItem | GridItem;

View File

@ -1,11 +1,7 @@
// @ts-nocheck
/* eslint-disable */
import React, { memo, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { FetchResult } from '@apollo/client';
import ButtonEmoji from '/imports/ui/components/common/button/button-emoji/ButtonEmoji';
import VideoService from '../service';
import { defineMessages, injectIntl } from 'react-intl';
import Styled from './styles';
import { IntlShape, defineMessages, injectIntl } from 'react-intl';
import deviceInfo from '/imports/utils/deviceInfo';
import { debounce } from '/imports/utils/debounce';
import BBBMenu from '/imports/ui/components/common/menu/component';
@ -14,6 +10,9 @@ import Button from '/imports/ui/components/common/button/component';
import VideoPreviewContainer from '/imports/ui/components/video-preview/container';
import { CameraSettingsDropdownItemType } from 'bigbluebutton-html-plugin-sdk/dist/cjs/extensible-areas/camera-settings-dropdown-item/enums';
import Settings from '/imports/ui/services/settings';
import { CameraSettingsDropdownInterface } from 'bigbluebutton-html-plugin-sdk';
import VideoService from '../service';
import Styled from './styles';
const ENABLE_WEBCAM_SELECTOR_BUTTON = window.meetingClientSettings.public.app.enableWebcamSelectorButton;
const ENABLE_CAMERA_BRIGHTNESS = window.meetingClientSettings.public.app.enableCameraBrightness;
@ -59,29 +58,32 @@ const intlMessages = defineMessages({
const JOIN_VIDEO_DELAY_MILLISECONDS = 500;
const propTypes = {
intl: PropTypes.object.isRequired,
hasVideoStream: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired,
cameraSettingsDropdownItems: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
type: PropTypes.string,
})).isRequired,
sendUserUnshareWebcam: PropTypes.func.isRequired,
setLocalSettings: PropTypes.func.isRequired,
};
interface JoinVideoButtonProps {
cameraSettingsDropdownItems: CameraSettingsDropdownInterface[];
hasVideoStream: boolean;
updateSettings: (
obj: object,
msgDescriptor: object | null,
mutation: (settings: Record<string, unknown>) => void,
) => void;
disableReason: string | undefined;
status: string;
setLocalSettings: (settings: Record<string, unknown>) => Promise<FetchResult<object>>;
exitVideo: () => void;
stopVideo: (cameraId?: string | undefined) => void;
intl: IntlShape;
}
const JoinVideoButton = ({
const JoinVideoButton: React.FC<JoinVideoButtonProps> = ({
intl,
hasVideoStream,
status,
disableReason,
updateSettings,
cameraSettingsDropdownItems,
sendUserUnshareWebcam,
setLocalSettings,
streams,
exitVideo: exit,
stopVideo,
}) => {
const { isMobile } = deviceInfo;
const isMobileSharingCamera = hasVideoStream && isMobile;
@ -95,18 +97,20 @@ const JoinVideoButton = ({
const exitVideo = () => isDesktopSharingCamera && (!VideoService.isMultipleCamerasEnabled()
|| shouldEnableWebcamSelectorButton);
const [propsToPassModal, setPropsToPassModal] = useState({});
const [propsToPassModal, setPropsToPassModal] = useState<{ isVisualEffects?: boolean }>({});
const [forceOpen, setForceOpen] = useState(false);
const [isVideoPreviewModalOpen, setVideoPreviewModalIsOpen] = useState(false);
const [wasSelfViewDisabled, setWasSelfViewDisabled] = useState(false);
useEffect(() => {
// @ts-expect-error -> Untyped object.
const isSelfViewDisabled = Settings.application.selfViewDisable;
if (isVideoPreviewModalOpen && isSelfViewDisabled) {
setWasSelfViewDisabled(true);
const obj = {
application:
// @ts-expect-error -> Untyped object.
{ ...Settings.application, selfViewDisable: false },
};
updateSettings(obj, null, setLocalSettings);
@ -116,7 +120,7 @@ const JoinVideoButton = ({
const handleOnClick = debounce(() => {
switch (status) {
case 'videoConnecting':
VideoService.stopVideo(undefined, sendUserUnshareWebcam, streams);
stopVideo();
break;
case 'connected':
default:
@ -129,7 +133,7 @@ const JoinVideoButton = ({
}
}, JOIN_VIDEO_DELAY_MILLISECONDS);
const handleOpenAdvancedOptions = (callback) => {
const handleOpenAdvancedOptions = (callback?: () => void) => {
if (callback) callback();
setForceOpen(isDesktopSharingCamera);
setVideoPreviewModalIsOpen(true);
@ -144,8 +148,8 @@ const JoinVideoButton = ({
};
const label = disableReason
? intl.formatMessage(intlMessages[disableReason])
: intl.formatMessage(intlMessages[getMessageFromStatus()]);
? intl.formatMessage(intlMessages[disableReason as keyof typeof intlMessages])
: intl.formatMessage(intlMessages[getMessageFromStatus() as keyof typeof intlMessages]);
const isSharing = hasVideoStream || status === 'videoConnecting';
@ -168,8 +172,7 @@ const JoinVideoButton = ({
{
key: 'virtualBgSelection',
label: intl.formatMessage(intlMessages.visualEffects),
onClick: () => handleOpenAdvancedOptions((
) => setPropsToPassModal({ isVisualEffects: true })),
onClick: () => handleOpenAdvancedOptions(() => setPropsToPassModal({ isVisualEffects: true })),
},
);
}
@ -182,8 +185,11 @@ const JoinVideoButton = ({
case CameraSettingsDropdownItemType.OPTION:
actions.push({
key: plugin.id,
// @ts-expect-error -> Plugin-related.
label: plugin.label,
// @ts-expect-error -> Plugin-related.
onClick: plugin.onClick,
// @ts-expect-error -> Plugin-related.
icon: plugin.icon,
});
break;
@ -249,8 +255,11 @@ const JoinVideoButton = ({
if (wasSelfViewDisabled) {
setTimeout(() => {
const obj = {
application:
{ ...Settings.application, selfViewDisable: true },
application: {
// @ts-expect-error -> Untyped object.
...Settings.application,
selfViewDisable: true,
},
};
updateSettings(obj, null, setLocalSettings);
setWasSelfViewDisabled(false);
@ -264,13 +273,11 @@ const JoinVideoButton = ({
setIsOpen: setVideoPreviewModalIsOpen,
isOpen: isVideoPreviewModalOpen,
}}
{...propsToPassModal}
isVisualEffects={propsToPassModal.isVisualEffects}
/>
) : null}
</>
);
};
JoinVideoButton.propTypes = propTypes;
export default injectIntl(memo(JoinVideoButton));

View File

@ -1,41 +1,20 @@
// @ts-nocheck
/* eslint-disable */
import React from 'react';
import { useContext } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import { injectIntl } from 'react-intl';
import { useMutation } from '@apollo/client';
import JoinVideoButton from './component';
import VideoService from '../service';
import {
updateSettings,
} from '/imports/ui/components/settings/service';
import React, { useContext } from 'react';
import { CameraSettingsDropdownInterface } from 'bigbluebutton-html-plugin-sdk';
import { updateSettings } from '/imports/ui/components/settings/service';
import { PluginsContext } from '/imports/ui/components/components-data/plugin-context/context';
import { CAMERA_BROADCAST_STOP } from '../mutations';
import useUserChangedLocalSettings from '/imports/ui/services/settings/hooks/useUserChangedLocalSettings';
import {
useDisableReason, useExitVideo, useHasVideoStream, useStatus, useStreams,
useDisableReason, useExitVideo, useHasVideoStream, useStatus, useStopVideo,
} from '/imports/ui/components/video-provider/video-provider-graphql/hooks';
import JoinVideoButton from './component';
const JoinVideoOptionsContainer = (props) => {
const {
updateSettings,
intl,
...restProps
} = props;
const { streams } = useStreams();
const [cameraBroadcastStop] = useMutation(CAMERA_BROADCAST_STOP);
const JoinVideoOptionsContainer: React.FC = () => {
const setLocalSettings = useUserChangedLocalSettings();
const sendUserUnshareWebcam = (cameraId) => {
cameraBroadcastStop({ variables: { cameraId } });
};
const {
pluginsExtensibleAreasAggregatedState,
} = useContext(PluginsContext);
let cameraSettingsDropdownItems = [];
let cameraSettingsDropdownItems: CameraSettingsDropdownInterface[] = [];
if (pluginsExtensibleAreasAggregatedState.cameraSettingsDropdownItems) {
cameraSettingsDropdownItems = [
...pluginsExtensibleAreasAggregatedState.cameraSettingsDropdownItems,
@ -45,25 +24,21 @@ const JoinVideoOptionsContainer = (props) => {
const hasVideoStream = useHasVideoStream();
const disableReason = useDisableReason();
const status = useStatus();
const exitVideo = useExitVideo()
const exitVideo = useExitVideo();
const stopVideo = useStopVideo();
return (
<JoinVideoButton {...{
cameraSettingsDropdownItems,
hasVideoStream,
updateSettings,
disableReason,
status,
sendUserUnshareWebcam,
setLocalSettings,
streams,
exitVideo,
...restProps,
}}
<JoinVideoButton
cameraSettingsDropdownItems={cameraSettingsDropdownItems}
hasVideoStream={hasVideoStream}
updateSettings={updateSettings}
disableReason={disableReason}
status={status}
setLocalSettings={setLocalSettings}
exitVideo={exitVideo}
stopVideo={stopVideo}
/>
);
};
export default injectIntl(withTracker(() => ({
updateSettings,
}))(JoinVideoOptionsContainer));
export default JoinVideoOptionsContainer;

View File

@ -1,8 +1,5 @@
// @ts-nocheck
/* eslint-disable */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import { IntlShape, defineMessages, injectIntl } from 'react-intl';
import { throttle } from '/imports/utils/throttle';
import { range } from '/imports/utils/array-utils';
import Styled from './styles';
@ -12,16 +9,8 @@ import logger from '/imports/startup/client/logger';
import playAndRetry from '/imports/utils/mediaElementPlayRetry';
import VideoService from '../service';
import { ACTIONS } from '/imports/ui/components/layout/enums';
const propTypes = {
streams: PropTypes.arrayOf(PropTypes.object).isRequired,
onVideoItemMount: PropTypes.func.isRequired,
onVideoItemUnmount: PropTypes.func.isRequired,
intl: PropTypes.objectOf(Object).isRequired,
swapLayout: PropTypes.bool.isRequired,
numberOfPages: PropTypes.number.isRequired,
currentVideoPageIndex: PropTypes.number.isRequired,
};
import { Output } from '../../../layout/layoutTypes';
import { StreamUser, VideoItem } from '../types';
const intlMessages = defineMessages({
autoplayBlockedDesc: {
@ -38,7 +27,20 @@ const intlMessages = defineMessages({
},
});
const findOptimalGrid = (canvasWidth, canvasHeight, gutter, aspectRatio, numItems, columns = 1) => {
declare global {
interface WindowEventMap {
'videoPlayFailed': CustomEvent<{ mediaElement: HTMLVideoElement }>;
}
}
const findOptimalGrid = (
canvasWidth: number,
canvasHeight: number,
gutter: number,
aspectRatio: number,
numItems: number,
columns = 1,
) => {
const rows = Math.ceil(numItems / columns);
const gutterTotalWidth = (columns - 1) * gutter;
const gutterTotalHeight = (rows - 1) * gutter;
@ -62,8 +64,48 @@ const findOptimalGrid = (canvasWidth, canvasHeight, gutter, aspectRatio, numItem
const ASPECT_RATIO = 4 / 3;
// const ACTION_NAME_BACKGROUND = 'blurBackground';
class VideoList extends Component {
constructor(props) {
interface VideoListProps {
layoutType: string;
layoutContextDispatch: (...args: unknown[]) => void;
numberOfPages: number;
swapLayout: boolean;
currentVideoPageIndex: number;
cameraDock: Output['cameraDock'];
focusedId: string;
handleVideoFocus: (id: string) => void;
isGridEnabled: boolean;
users: StreamUser[];
streams: VideoItem[];
intl: IntlShape;
onVideoItemMount: (stream: string, video: HTMLVideoElement) => void;
onVideoItemUnmount: (stream: string) => void;
onVirtualBgDrop: (stream: string, type: string, name: string, data: string) => Promise<unknown>;
}
interface VideoListState {
optimalGrid: {
cols: number,
rows: number,
filledArea: number,
width: number;
height: number;
columns: number;
},
autoplayBlocked: boolean,
}
class VideoList extends Component<VideoListProps, VideoListState> {
private ticking: boolean;
private grid: HTMLDivElement | null;
private canvas: HTMLDivElement | null;
private failedMediaElements: unknown[];
private autoplayWasHandled: boolean;
constructor(props: VideoListProps) {
super(props);
this.state = {
@ -71,6 +113,9 @@ class VideoList extends Component {
cols: 1,
rows: 1,
filledArea: 0,
columns: 0,
height: 0,
width: 0,
},
autoplayBlocked: false,
};
@ -96,8 +141,10 @@ class VideoList extends Component {
window.addEventListener('videoPlayFailed', this.handlePlayElementFailed);
}
componentDidUpdate(prevProps) {
const { layoutType, cameraDock, streams, focusedId } = this.props;
componentDidUpdate(prevProps: VideoListProps) {
const {
layoutType, cameraDock, streams, focusedId,
} = this.props;
const { width: cameraDockWidth, height: cameraDockHeight } = cameraDock;
const {
layoutType: prevLayoutType,
@ -148,7 +195,7 @@ class VideoList extends Component {
if (autoplayBlocked) { this.setState({ autoplayBlocked: false }); }
}
handlePlayElementFailed(e) {
handlePlayElementFailed(e: CustomEvent<{ mediaElement: HTMLVideoElement }>) {
const { mediaElement } = e.detail;
const { autoplayBlocked } = this.state;
@ -190,7 +237,7 @@ class VideoList extends Component {
const gridGutter = parseInt(window.getComputedStyle(this.grid)
.getPropertyValue('grid-row-gap'), 10);
const hasFocusedItem = streams.filter(s => s.stream === focusedId).length && numItems > 2;
const hasFocusedItem = streams.filter((s) => s.type !== 'grid' && s.stream === focusedId).length && numItems > 2;
// Has a focused item so we need +3 cells
if (hasFocusedItem) {
@ -303,23 +350,24 @@ class VideoList extends Component {
} = this.props;
const numOfStreams = streams.length;
let videoItems = streams;
const videoItems = streams;
return videoItems.map((item) => {
const { userId, name } = item;
const isStream = !!item.stream;
const isStream = item.type !== 'grid';
const stream = isStream ? item.stream : null;
const key = isStream ? stream : userId;
const isFocused = isStream && focusedId === stream && numOfStreams > 2;
const user = users.find((u) => u.userId === userId) || {};
return (
<Styled.VideoListItem
key={key}
focused={isFocused}
$focused={isFocused}
data-test="webcamVideoItem"
>
<VideoListItemContainer
users={users}
user={user}
numOfStreams={numOfStreams}
cameraId={stream}
userId={userId}
@ -329,12 +377,16 @@ class VideoList extends Component {
onHandleVideoFocus={isStream ? handleVideoFocus : null}
onVideoItemMount={(videoRef) => {
this.handleCanvasResize();
if (isStream) onVideoItemMount(stream, videoRef);
if (isStream) onVideoItemMount(item.stream, videoRef);
}}
stream={streams.find((s) => s.userId === userId) || {}}
stream={streams.find((s) => s.userId === userId)}
onVideoItemUnmount={onVideoItemUnmount}
swapLayout={swapLayout}
onVirtualBgDrop={(type, name, data) => { return isStream ? onVirtualBgDrop(stream, type, name, data) : null; }}
onVirtualBgDrop={
(type, name, data) => {
return isStream ? onVirtualBgDrop(item.stream, type, name, data) : Promise.resolve(null);
}
}
/>
</Styled.VideoListItem>
);
@ -353,7 +405,7 @@ class VideoList extends Component {
return (
<Styled.VideoCanvas
position={position}
$position={position}
ref={(ref) => {
this.canvas = ref;
}}
@ -397,6 +449,4 @@ class VideoList extends Component {
}
}
VideoList.propTypes = propTypes;
export default injectIntl(VideoList);

View File

@ -1,43 +1,64 @@
// @ts-nocheck
/* eslint-disable */
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import VideoList from '/imports/ui/components/video-provider/video-provider-graphql/video-list/component';
import VideoService from '/imports/ui/components/video-provider/video-provider-graphql/service';
import { layoutSelect, layoutSelectOutput, layoutDispatch } from '/imports/ui/components/layout/context';
import Users from '/imports/api/users';
import { layoutSelect, layoutDispatch } from '/imports/ui/components/layout/context';
import { useNumberOfPages } from '../hooks';
import { StreamUser, VideoItem } from '../types';
import { Layout, Output } from '../../../layout/layoutTypes';
const VideoListContainer = ({ children, ...props }) => {
const layoutType = layoutSelect((i) => i.layoutType);
const cameraDock = layoutSelectOutput((i) => i.cameraDock);
interface VideoListContainerProps {
streams: VideoItem[];
swapLayout: boolean;
currentVideoPageIndex: number;
cameraDock: Output['cameraDock'];
focusedId: string;
handleVideoFocus: (id: string) => void;
isGridEnabled: boolean;
users: StreamUser[];
onVideoItemMount: (stream: string, video: HTMLVideoElement) => void;
onVideoItemUnmount: (stream: string) => void;
onVirtualBgDrop: (stream: string, type: string, name: string, data: string) => Promise<unknown>;
}
const VideoListContainer: React.FC<VideoListContainerProps> = (props) => {
const layoutType = layoutSelect((i: Layout) => i.layoutType);
const layoutContextDispatch = layoutDispatch();
const { streams } = props;
const {
streams,
cameraDock,
currentVideoPageIndex,
focusedId,
handleVideoFocus,
isGridEnabled,
swapLayout,
users,
onVideoItemMount,
onVideoItemUnmount,
onVirtualBgDrop,
} = props;
const numberOfPages = useNumberOfPages();
return (
!streams.length
? null
: (
<VideoList {...{
layoutType,
cameraDock,
layoutContextDispatch,
numberOfPages,
...props,
}}
>
{children}
</VideoList>
<VideoList
layoutType={layoutType}
layoutContextDispatch={layoutContextDispatch}
numberOfPages={numberOfPages}
swapLayout={swapLayout}
currentVideoPageIndex={currentVideoPageIndex}
cameraDock={cameraDock}
focusedId={focusedId}
handleVideoFocus={handleVideoFocus}
isGridEnabled={isGridEnabled}
users={users}
streams={streams}
onVideoItemMount={onVideoItemMount}
onVideoItemUnmount={onVideoItemUnmount}
onVirtualBgDrop={onVirtualBgDrop}
/>
)
);
};
export default withTracker((props) => {
const { streams } = props;
return {
...props,
streams,
};
})(VideoListContainer);
export default VideoListContainer;

View File

@ -1,12 +1,10 @@
// @ts-nocheck
/* eslint-disable */
import styled from 'styled-components';
import { colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
import { mdPaddingX } from '/imports/ui/stylesheets/styled-components/general';
import { mediumUp } from '/imports/ui/stylesheets/styled-components/breakpoints';
import { actionsBarHeight, navbarHeight } from '/imports/ui/stylesheets/styled-components/general';
import { actionsBarHeight, navbarHeight, mdPaddingX } from '/imports/ui/stylesheets/styled-components/general';
import Button from '/imports/ui/components/common/button/component';
// @ts-expect-error -> Untyped component.
const NextPageButton = styled(Button)`
color: ${colorWhite};
width: ${mdPaddingX};
@ -33,6 +31,7 @@ const NextPageButton = styled(Button)`
`}
`;
// @ts-expect-error -> Untyped component.
const PreviousPageButton = styled(Button)`
color: ${colorWhite};
width: ${mdPaddingX};
@ -59,19 +58,23 @@ const PreviousPageButton = styled(Button)`
`}
`;
const VideoListItem = styled.div`
const VideoListItem = styled.div<{
$focused: boolean;
}>`
display: flex;
overflow: hidden;
width: 100%;
max-height: 100%;
${({ focused }) => focused && `
${({ $focused }) => $focused && `
grid-column: 1 / span 2;
grid-row: 1 / span 2;
`}
`;
const VideoCanvas = styled.div`
const VideoCanvas = styled.div<{
$position: string;
}>`
position: absolute;
width: 100%;
min-height: calc((100vh - calc(${navbarHeight} + ${actionsBarHeight})) * 0.2);
@ -84,7 +87,7 @@ const VideoCanvas = styled.div`
align-items: center;
justify-content: center;
${({ position }) => (position === 'contentRight' || position === 'contentLeft') && `
${({ $position }) => ($position === 'contentRight' || $position === 'contentLeft') && `
flex-wrap: wrap;
align-content: center;
order: 0;

View File

@ -1,8 +1,6 @@
// @ts-nocheck
/* eslint-disable */
import React, { useEffect, useRef, useState } from 'react';
import { injectIntl, defineMessages, useIntl } from 'react-intl';
import PropTypes from 'prop-types';
import { defineMessages, useIntl } from 'react-intl';
import { Session } from 'meteor/session';
import UserActions from '/imports/ui/components/video-provider/video-provider-graphql/video-list/video-list-item/user-actions/component';
import UserStatus from '/imports/ui/components/video-provider/video-provider-graphql/video-list/video-list-item/user-status/component';
import PinArea from '/imports/ui/components/video-provider/video-provider-graphql/video-list/video-list-item/pin-area/component';
@ -16,8 +14,9 @@ import {
import Settings from '/imports/ui/services/settings';
import VideoService from '/imports/ui/components/video-provider/video-provider-graphql/service';
import Styled from './styles';
import { withDragAndDrop } from './drag-and-drop/component';
import withDragAndDrop from './drag-and-drop/component';
import Auth from '/imports/ui/services/auth';
import { StreamUser, VideoItem } from '../../types';
const intlMessages = defineMessages({
disableDesc: {
@ -27,7 +26,39 @@ const intlMessages = defineMessages({
const VIDEO_CONTAINER_WIDTH_BOUND = 125;
const VideoListItem = (props) => {
interface VideoListItemProps {
isFullscreenContext: boolean;
layoutContextDispatch: (...args: unknown[]) => void;
isRTL: boolean;
amIModerator: boolean;
cameraId: string;
disabledCams: string[];
focused: boolean;
isStream: boolean;
name: string;
numOfStreams: number;
onHandleVideoFocus: ((id: string) => void) | null;
onVideoItemMount: (ref: HTMLVideoElement) => void;
onVideoItemUnmount: (stream: string) => void;
settingsSelfViewDisable: boolean;
stream: VideoItem | undefined;
user: Partial<StreamUser>;
makeDragOperations: (userId?: string) => {
onDragOver: (e: DragEvent) => void,
onDrop: (e: DragEvent) => void,
onDragLeave: (e: DragEvent) => void,
};
dragging: boolean;
draggingOver: boolean;
voiceUser: {
muted: boolean;
listenOnly: boolean;
talking: boolean;
joined: boolean;
};
}
const VideoListItem: React.FC<VideoListItemProps> = (props) => {
const {
name, voiceUser, isFullscreenContext, layoutContextDispatch, user, onHandleVideoFocus,
cameraId, numOfStreams, focused, onVideoItemMount, onVideoItemUnmount,
@ -39,7 +70,7 @@ const VideoListItem = (props) => {
const [videoDataLoaded, setVideoDataLoaded] = useState(false);
const [isStreamHealthy, setIsStreamHealthy] = useState(false);
const [isMirrored, setIsMirrored] = useState(VideoService.mirrorOwnWebcam(user?.userId));
const [isMirrored, setIsMirrored] = useState<boolean>(VideoService.mirrorOwnWebcam(user?.userId));
const [isVideoSqueezed, setIsVideoSqueezed] = useState(false);
const [isSelfViewDisabled, setIsSelfViewDisabled] = useState(false);
@ -50,14 +81,15 @@ const VideoListItem = (props) => {
return setIsVideoSqueezed(false);
});
const videoTag = useRef();
const videoContainer = useRef();
const videoTag = useRef<HTMLVideoElement | null>(null);
const videoContainer = useRef<HTMLDivElement | null>(null);
const videoIsReady = isStreamHealthy && videoDataLoaded && !isSelfViewDisabled;
// @ts-expect-error -> Untyped object.
const { animations, webcamBorderHighlightColor } = Settings.application;
const talking = voiceUser?.talking;
const onStreamStateChange = (e) => {
const onStreamStateChange = (e: CustomEvent) => {
const { streamState } = e.detail;
const newHealthState = !isStreamStateUnhealthy(streamState);
e.stopPropagation();
@ -78,8 +110,8 @@ const VideoListItem = (props) => {
// component did mount
useEffect(() => {
subscribeToStreamStateChange(cameraId, onStreamStateChange);
onVideoItemMount(videoTag.current);
resizeObserver.observe(videoContainer.current);
onVideoItemMount(videoTag.current!);
if (videoContainer.current) resizeObserver.observe(videoContainer.current);
videoTag?.current?.addEventListener('loadeddata', onLoadedData);
return () => {
@ -90,7 +122,7 @@ const VideoListItem = (props) => {
// component will mount
useEffect(() => {
const playElement = (elem) => {
const playElement = (elem: HTMLVideoElement) => {
if (elem.paused) {
elem.play().catch((error) => {
// NotAllowedError equals autoplay issues, fire autoplay handling event
@ -102,10 +134,10 @@ const VideoListItem = (props) => {
}
};
if (!isSelfViewDisabled && videoDataLoaded) {
playElement(videoTag.current);
playElement(videoTag.current!);
}
if ((isSelfViewDisabled && user.userId === Auth.userID) || disabledCams?.includes(cameraId)) {
videoTag.current.pause();
videoTag.current?.pause?.();
}
}, [isSelfViewDisabled, videoDataLoaded]);
@ -122,7 +154,8 @@ const VideoListItem = (props) => {
const renderSqueezedButton = () => (
<UserActions
name={name}
user={{ ...user, ...stream }}
user={user}
stream={stream}
videoContainer={videoContainer}
isVideoSqueezed={isVideoSqueezed}
cameraId={cameraId}
@ -153,7 +186,8 @@ const VideoListItem = (props) => {
<Styled.BottomBar>
<UserActions
name={name}
user={{ ...user, ...stream }}
user={user}
stream={stream}
cameraId={cameraId}
numOfStreams={numOfStreams}
onHandleVideoFocus={onHandleVideoFocus}
@ -180,7 +214,7 @@ const VideoListItem = (props) => {
animations={animations}
>
<UserAvatarVideo
user={{ ...user, ...stream }}
user={user}
unhealthyStream={videoDataLoaded && !isStreamHealthy}
squeezed
/>
@ -192,7 +226,8 @@ const VideoListItem = (props) => {
<>
<Styled.TopBar>
<PinArea
user={{ ...user, ...stream }}
user={user}
stream={stream}
amIModerator={amIModerator}
/>
<ViewActions
@ -207,7 +242,8 @@ const VideoListItem = (props) => {
<Styled.BottomBar>
<UserActions
name={name}
user={{ ...user, ...stream }}
user={user}
stream={stream}
cameraId={cameraId}
numOfStreams={numOfStreams}
onHandleVideoFocus={onHandleVideoFocus}
@ -228,7 +264,14 @@ const VideoListItem = (props) => {
</>
);
const {
onDragLeave,
onDragOver,
onDrop,
} = makeDragOperations(user?.userId);
return (
// @ts-expect-error -> Until everything in Typescript.
<Styled.Content
ref={videoContainer}
talking={talking}
@ -238,7 +281,9 @@ const VideoListItem = (props) => {
animations={animations}
isStream={isStream}
{...{
...makeDragOperations(user?.userId),
onDragLeave,
onDragOver,
onDrop,
dragging,
draggingOver,
}}
@ -253,7 +298,7 @@ const VideoListItem = (props) => {
unhealthyStream={videoDataLoaded && !isStreamHealthy}
data-test={isMirrored ? 'mirroredVideoContainer' : 'videoContainer'}
ref={videoTag}
muted="muted"
muted
autoPlay
playsInline
/>
@ -280,37 +325,5 @@ const VideoListItem = (props) => {
);
};
export default withDragAndDrop(injectIntl(VideoListItem));
VideoListItem.defaultProps = {
numOfStreams: 0,
onVideoItemMount: () => { },
onVideoItemUnmount: () => { },
onVirtualBgDrop: () => { },
};
VideoListItem.propTypes = {
cameraId: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
numOfStreams: PropTypes.number,
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
onHandleVideoFocus: PropTypes.func.isRequired,
onVideoItemMount: PropTypes.func,
onVideoItemUnmount: PropTypes.func,
onVirtualBgDrop: PropTypes.func,
isFullscreenContext: PropTypes.bool.isRequired,
layoutContextDispatch: PropTypes.func.isRequired,
user: PropTypes.shape({
pin: PropTypes.bool.isRequired,
userId: PropTypes.string.isRequired,
}).isRequired,
voiceUser: PropTypes.shape({
muted: PropTypes.bool.isRequired,
listenOnly: PropTypes.bool.isRequired,
talking: PropTypes.bool.isRequired,
joined: PropTypes.bool.isRequired,
}).isRequired,
focused: PropTypes.bool.isRequired,
};
// @ts-expect-error -> Until everything in Typescript.
export default withDragAndDrop(VideoListItem);

View File

@ -1,23 +1,69 @@
// @ts-nocheck
/* eslint-disable */
import React from 'react';
import PropTypes from 'prop-types';
import { withTracker } from 'meteor/react-meteor-data';
import { Session } from 'meteor/session';
import VoiceUsers from '/imports/api/voice-users/';
import Users from '/imports/api/users/';
import VideoListItem from './component';
import { layoutSelect, layoutDispatch } from '/imports/ui/components/layout/context';
import Settings from '/imports/ui/services/settings';
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
import { layoutSelect, layoutDispatch } from '/imports/ui/components/layout/context';
import VideoListItem from './component';
import { StreamUser, VideoItem } from '../../types';
import { Layout } from '/imports/ui/components/layout/layoutTypes';
const VideoListItemContainer = (props) => {
const { cameraId, user } = props;
type TrackerData = {
disabledCams: string[];
settingsSelfViewDisable: boolean;
user: Partial<StreamUser>;
stream: VideoItem | undefined;
voiceUser: {
muted: boolean;
listenOnly: boolean;
talking: boolean;
joined: boolean;
};
}
const fullscreen = layoutSelect((i) => i.fullscreen);
type TrackerProps = {
user: Partial<StreamUser>;
numOfStreams: number;
cameraId: string | null;
userId: string;
name: string;
focused: boolean;
isStream: boolean;
onHandleVideoFocus: ((id: string) => void) | null;
stream: VideoItem | undefined;
onVideoItemUnmount: (stream: string) => void;
swapLayout: boolean;
onVirtualBgDrop: (type: string, name: string, data: string) => void;
onVideoItemMount: (ref: HTMLVideoElement) => void;
}
type VideoListItemContainerProps = TrackerData & Omit<TrackerProps, 'userId'>;
const VideoListItemContainer: React.FC<VideoListItemContainerProps> = (props) => {
const {
cameraId,
disabledCams,
focused,
isStream,
name,
numOfStreams,
onHandleVideoFocus,
onVideoItemMount,
onVideoItemUnmount,
onVirtualBgDrop,
settingsSelfViewDisable,
stream,
user,
voiceUser,
} = props;
const fullscreen = layoutSelect((i: Layout) => i.fullscreen);
const { element } = fullscreen;
const isFullscreenContext = (element === cameraId);
const layoutContextDispatch = layoutDispatch();
const isRTL = layoutSelect((i) => i.isRTL);
const isRTL = layoutSelect((i: Layout) => i.isRTL);
const { data: currentUserData } = useCurrentUser((user) => ({
isModerator: user.isModerator,
@ -29,25 +75,39 @@ const VideoListItemContainer = (props) => {
return (
<VideoListItem
{...props}
{...{
isFullscreenContext,
layoutContextDispatch,
isRTL,
amIModerator,
}}
cameraId={cameraId}
disabledCams={disabledCams}
focused={focused}
isStream={isStream}
name={name}
numOfStreams={numOfStreams}
onHandleVideoFocus={onHandleVideoFocus}
onVideoItemMount={onVideoItemMount}
onVideoItemUnmount={onVideoItemUnmount}
onVirtualBgDrop={onVirtualBgDrop}
settingsSelfViewDisable={settingsSelfViewDisable}
stream={stream}
user={user}
voiceUser={voiceUser}
/>
);
};
export default withTracker((props) => {
export default withTracker<TrackerData, TrackerProps>((props) => {
const {
userId,
users,
user,
stream,
} = props;
return {
// @ts-expect-error -> Untyped object.
settingsSelfViewDisable: Settings.application.selfViewDisable,
voiceUser: VoiceUsers.findOne({ userId },
{
@ -55,9 +115,9 @@ export default withTracker((props) => {
muted: 1, listenOnly: 1, talking: 1, joined: 1,
},
}),
user: (users?.find((u) => u.userId === userId) || {}),
disabledCams: Session.get('disabledCams') || [],
user,
stream,
disabledCams: Session.get('disabledCams') || [],
};
})(VideoListItemContainer);

View File

@ -1,7 +1,7 @@
// @ts-nocheck
/* eslint-disable */
import React, { useContext, useEffect, useState, useCallback } from 'react';
import { injectIntl, defineMessages } from 'react-intl';
import React, {
useContext, useEffect, useState, useCallback,
} from 'react';
import { injectIntl, defineMessages, IntlShape } from 'react-intl';
import Auth from '/imports/ui/services/auth';
import ConfirmationModal from '/imports/ui/components/common/modal/confirmation/component';
import { CustomVirtualBackgroundsContext } from '/imports/ui/components/video-preview/virtual-background/context';
@ -9,6 +9,7 @@ import { EFFECT_TYPES } from '/imports/ui/services/virtual-background/service';
import VirtualBgService from '/imports/ui/components/video-preview/virtual-background/service';
import logger from '/imports/startup/client/logger';
import withFileReader from '/imports/ui/components/common/file-reader/component';
import { Session } from 'meteor/session';
const { MIME_TYPES_ALLOWED, MAX_FILE_SIZE } = VirtualBgService;
@ -23,33 +24,51 @@ const intlMessages = defineMessages({
},
});
const ENABLE_WEBCAM_BACKGROUND_UPLOAD = window.meetingClientSettings.public.virtualBackgrounds.enableVirtualBackgroundUpload;
const PUBLIC_CONFIG = window.meetingClientSettings.public;
const ENABLE_WEBCAM_BACKGROUND_UPLOAD = PUBLIC_CONFIG.virtualBackgrounds.enableVirtualBackgroundUpload;
const DragAndDrop = (props) => {
const { children, intl, readFile, onVirtualBgDrop: onAction, isStream } = props;
interface DragAndDropProps {
readFile: (
file: File,
onSuccess: (data: {
filename: string,
data: string,
uniqueId: string,
}) => void,
onError: (error: Error) => void
) => void;
children: React.ReactElement;
intl: IntlShape;
isStream: boolean;
onVirtualBgDrop: (type: string, name: string, data: string) => Promise<void>;
}
const DragAndDrop: React.FC<DragAndDropProps> = (props) => {
const {
children, intl, readFile, onVirtualBgDrop: onAction, isStream,
} = props;
const [dragging, setDragging] = useState(false);
const [draggingOver, setDraggingOver] = useState(false);
const [isConfirmModalOpen, setConfirmModalIsOpen] = useState(false);
const [file, setFile] = useState(false);
const [file, setFile] = useState<File | null>(null);
const { dispatch: dispatchCustomBackground } = useContext(CustomVirtualBackgroundsContext);
let callback;
const resetEvent = (e) => {
const resetEvent = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
}
};
useEffect(() => {
const onDragOver = (e) => {
const onDragOver = (e: DragEvent) => {
resetEvent(e);
setDragging(true);
};
const onDragLeave = (e) => {
const onDragLeave = (e: DragEvent) => {
resetEvent(e);
setDragging(false);
};
const onDrop = (e) => {
const onDrop = (e: DragEvent) => {
resetEvent(e);
setDragging(false);
};
@ -65,8 +84,12 @@ const DragAndDrop = (props) => {
};
}, []);
const handleStartAndSaveVirtualBackground = (file) => {
const onSuccess = (background) => {
const handleStartAndSaveVirtualBackground = (file: File) => {
const onSuccess = (background: {
filename: string,
data: string,
uniqueId: string,
}) => {
const { filename, data } = background;
if (onAction) {
onAction(EFFECT_TYPES.IMAGE_TYPE, filename, data).then(() => {
@ -79,17 +102,19 @@ const DragAndDrop = (props) => {
},
});
});
} else dispatchCustomBackground({
type: 'new',
background: {
...background,
custom: true,
lastActivityDate: Date.now(),
},
});
} else {
dispatchCustomBackground({
type: 'new',
background: {
...background,
custom: true,
lastActivityDate: Date.now(),
},
});
}
};
const onError = (error) => {
const onError = (error: Error) => {
logger.warn({
logCode: 'read_file_error',
extraInfo: {
@ -102,39 +127,43 @@ const DragAndDrop = (props) => {
readFile(file, onSuccess, onError);
};
callback = (checked) => {
const callback = (checked: boolean) => {
if (!file) return;
handleStartAndSaveVirtualBackground(file);
Session.set('skipBackgroundDropConfirmation', checked);
};
const makeDragOperations = useCallback((userId) => {
const makeDragOperations = useCallback((userId: string) => {
if (!userId || Auth.userID !== userId || !ENABLE_WEBCAM_BACKGROUND_UPLOAD || !isStream) return {};
const startAndSaveVirtualBackground = (file) => handleStartAndSaveVirtualBackground(file);
const startAndSaveVirtualBackground = (file: File) => handleStartAndSaveVirtualBackground(file);
const onDragOverHandler = (e) => {
const onDragOverHandler = (e: DragEvent) => {
resetEvent(e);
setDraggingOver(true);
setDragging(false);
};
const onDropHandler = (e) => {
const onDropHandler = (e: DragEvent) => {
resetEvent(e);
setDraggingOver(false);
setDragging(false);
const { files } = e.dataTransfer;
const file = files[0];
if (e.dataTransfer) {
const { files } = e.dataTransfer;
const file = files[0];
if (Session.get('skipBackgroundDropConfirmation')) {
return startAndSaveVirtualBackground(file);
if (Session.get('skipBackgroundDropConfirmation')) {
return startAndSaveVirtualBackground(file);
}
setFile(file);
setConfirmModalIsOpen(true);
}
setFile(file);
setConfirmModalIsOpen(true);
return null;
};
const onDragLeaveHandler = (e) => {
const onDragLeaveHandler = (e: DragEvent) => {
resetEvent(e);
setDragging(false);
setDraggingOver(false);
@ -147,29 +176,47 @@ const DragAndDrop = (props) => {
};
}, [Auth.userID]);
return <>
{React.cloneElement(children, { ...props, dragging, draggingOver, makeDragOperations })}
{isConfirmModalOpen ? <ConfirmationModal
intl={intl}
onConfirm={callback}
title={intl.formatMessage(intlMessages.confirmationTitle)}
description={intl.formatMessage(intlMessages.confirmationDescription, { 0: file.name })}
checkboxMessageId="app.confirmation.skipConfirm"
{...{
onRequestClose: () => setConfirmModalIsOpen(false),
priority: "low",
setIsOpen: setConfirmModalIsOpen,
isOpen: isConfirmModalOpen
}}
/> : null}
</>;
return (
<>
{React.cloneElement(
children,
{
...props, dragging, draggingOver, makeDragOperations,
},
)}
{isConfirmModalOpen ? (
<ConfirmationModal
intl={intl}
onConfirm={callback}
title={intl.formatMessage(intlMessages.confirmationTitle)}
description={intl.formatMessage(intlMessages.confirmationDescription, { 0: file?.name })}
checkboxMessageId="app.confirmation.skipConfirm"
{...{
onRequestClose: () => setConfirmModalIsOpen(false),
priority: 'low',
setIsOpen: setConfirmModalIsOpen,
isOpen: isConfirmModalOpen,
}}
/>
) : null}
</>
);
};
const Wrapper = (Component) => (props) => (
<DragAndDrop {...props} >
const Wrapper = (Component: (props: object) => JSX.Element) => (props: DragAndDropProps) => (
<DragAndDrop
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
>
<Component />
</DragAndDrop>
);
export const withDragAndDrop = (Component) =>
injectIntl(withFileReader(Wrapper(Component), MIME_TYPES_ALLOWED, MAX_FILE_SIZE));
const withDragAndDrop = (Component: (props: object) => JSX.Element) => injectIntl(
withFileReader(
Wrapper(Component),
MIME_TYPES_ALLOWED, MAX_FILE_SIZE,
),
);
export default withDragAndDrop;

View File

@ -1,12 +1,10 @@
// @ts-nocheck
/* eslint-disable */
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import PropTypes from 'prop-types';
import VideoService from '/imports/ui/components/video-provider/video-provider-graphql/service';
import { useMutation } from '@apollo/client';
import Styled from './styles';
import { SET_CAMERA_PINNED } from '/imports/ui/core/graphql/mutations/userMutations';
import { StreamUser, VideoItem } from '../../../types';
const intlMessages = defineMessages({
unpinLabel: {
@ -17,11 +15,17 @@ const intlMessages = defineMessages({
},
});
const PinArea = (props) => {
interface PinAreaProps {
user: Partial<StreamUser>;
stream: VideoItem | undefined;
amIModerator: boolean;
}
const PinArea: React.FC<PinAreaProps> = (props) => {
const intl = useIntl();
const { user, amIModerator } = props;
const pinned = user?.pin;
const { stream, user, amIModerator } = props;
const pinned = stream?.type === 'stream' && stream?.pin;
const userId = user?.userId;
const shouldRenderPinButton = pinned && userId;
const videoPinActionAvailable = VideoService.isVideoPinEnabledForCurrentUser(amIModerator);
@ -56,10 +60,3 @@ const PinArea = (props) => {
};
export default PinArea;
PinArea.propTypes = {
user: PropTypes.shape({
pin: PropTypes.bool.isRequired,
userId: PropTypes.string.isRequired,
}).isRequired,
};

View File

@ -29,7 +29,15 @@ const fade = keyframes`
}
`;
const Content = styled.div`
const Content = styled.div<{
isStream: boolean;
talking: boolean;
customHighlight: boolean;
animations: boolean;
dragging: boolean;
draggingOver: boolean;
fullscreen: boolean;
}>`
position: relative;
display: flex;
min-width: 100%;
@ -88,7 +96,9 @@ const Content = styled.div`
`}
`;
const WebcamConnecting = styled.div`
const WebcamConnecting = styled.div<{
animations: boolean;
}>`
display: flex;
justify-content: center;
align-items: center;
@ -120,7 +130,9 @@ const LoadingText = styled(TextElipsis)`
font-size: 100%;
`;
const VideoContainer = styled.div`
const VideoContainer = styled.div<{
$selfViewDisabled: boolean;
}>`
display: flex;
justify-content: center;
width: 100%;
@ -129,7 +141,10 @@ const VideoContainer = styled.div`
${({ $selfViewDisabled }) => $selfViewDisabled && 'display: none'}
`;
const Video = styled.video`
const Video = styled.video<{
mirrored: boolean;
unhealthyStream: boolean;
}>`
position: relative;
height: 100%;
width: calc(100% - 1px);
@ -147,7 +162,7 @@ const Video = styled.video`
`;
const VideoDisabled = styled.div`
color: white;
color: white;
width: 100%;
height: 20%;
background-color: rgba(0, 0, 0, 0.7);
@ -164,7 +179,7 @@ color: white;
padding: 20px;
backdrop-filter: blur(10px);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}`;
`;
const TopBar = styled.div`
position: absolute;

View File

@ -1,19 +1,19 @@
// @ts-nocheck
/* eslint-disable */
import React, { useContext } from 'react';
import React, { MutableRefObject, useContext } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useMutation } from '@apollo/client';
import { Session } from 'meteor/session';
import { UserCameraDropdownInterface } from 'bigbluebutton-html-plugin-sdk';
import browserInfo from '/imports/utils/browserInfo';
import VideoService from '/imports/ui/components/video-provider/video-provider-graphql/service';
import FullscreenService from '/imports/ui/components/common/fullscreen-button/service';
import BBBMenu from '/imports/ui/components/common/menu/component';
import PropTypes from 'prop-types';
import { UserCameraDropdownItemType } from 'bigbluebutton-html-plugin-sdk/dist/cjs/extensible-areas/user-camera-dropdown-item/enums';
import { useMutation } from '@apollo/client';
import Styled from './styles';
import Auth from '/imports/ui/services/auth';
import { PluginsContext } from '/imports/ui/components/components-data/plugin-context/context';
import { notify } from '/imports/ui/services/notification';
import { SET_CAMERA_PINNED } from '/imports/ui/core/graphql/mutations/userMutations';
import { StreamUser, VideoItem } from '../../../types';
const intlMessages = defineMessages({
focusLabel: {
@ -74,16 +74,35 @@ const intlMessages = defineMessages({
},
});
const UserActions = (props) => {
interface UserActionProps {
name: string;
user: Partial<StreamUser>;
stream: VideoItem | undefined;
cameraId: string;
numOfStreams: number;
onHandleVideoFocus: ((id: string) => void) | null;
focused: boolean;
onHandleMirror: () => void;
isMirrored: boolean;
isRTL: boolean;
isStream: boolean;
onHandleDisableCam: () => void;
isSelfViewDisabled: boolean;
amIModerator: boolean;
isVideoSqueezed?: boolean,
videoContainer?: MutableRefObject<HTMLDivElement | null>,
}
const UserActions: React.FC<UserActionProps> = (props) => {
const {
name, cameraId, numOfStreams, onHandleVideoFocus, user, focused, onHandleMirror,
isVideoSqueezed, videoContainer, isRTL, isStream, isSelfViewDisabled, isMirrored,
name, cameraId, numOfStreams, onHandleVideoFocus, user, stream, focused, onHandleMirror,
isVideoSqueezed = false, videoContainer, isRTL, isStream, isSelfViewDisabled, isMirrored,
amIModerator,
} = props;
const { pluginsExtensibleAreasAggregatedState } = useContext(PluginsContext);
let userCameraDropdownItems = [];
let userCameraDropdownItems: UserCameraDropdownInterface[] = [];
if (pluginsExtensibleAreasAggregatedState.userCameraDropdownItems) {
userCameraDropdownItems = [
...pluginsExtensibleAreasAggregatedState.userCameraDropdownItems,
@ -97,7 +116,7 @@ const UserActions = (props) => {
const [setCameraPinned] = useMutation(SET_CAMERA_PINNED);
const getAvailableActions = () => {
const pinned = user?.pin;
const pinned = stream?.type === 'stream' && stream?.pin;
const userId = user?.userId;
const isPinnedIntlKey = !pinned ? 'pin' : 'unpin';
const isFocusedIntlKey = !focused ? 'focus' : 'unfocus';
@ -113,7 +132,7 @@ const UserActions = (props) => {
Session.set('disabledCams', [...disabledCams, cameraId]);
notify(intl.formatMessage(intlMessages.disableWarning), 'info', 'warning');
} else {
Session.set('disabledCams', disabledCams.filter((cId) => cId !== cameraId));
Session.set('disabledCams', disabledCams.filter((cId: string) => cId !== cameraId));
}
};
@ -132,7 +151,8 @@ const UserActions = (props) => {
key: `${cameraId}-fullscreen`,
label: intl.formatMessage(intlMessages.fullscreenLabel),
description: intl.formatMessage(intlMessages.fullscreenLabel),
onClick: () => FullscreenService.toggleFullScreen(videoContainer.current),
// @ts-expect-error -> Until everything in Typescript.
onClick: () => FullscreenService.toggleFullScreen(videoContainer?.current),
},
);
}
@ -142,7 +162,7 @@ const UserActions = (props) => {
key: `${cameraId}-disable`,
label: intl.formatMessage(intlMessages[`${enableSelfCamIntlKey}Label`]),
description: intl.formatMessage(intlMessages[`${enableSelfCamIntlKey}Label`]),
onClick: () => toggleDisableCam(cameraId),
onClick: () => toggleDisableCam(),
dataTest: 'selfViewDisableBtn',
});
}
@ -152,7 +172,7 @@ const UserActions = (props) => {
key: `${cameraId}-mirror`,
label: intl.formatMessage(intlMessages[`${isMirroredIntlKey}Label`]),
description: intl.formatMessage(intlMessages[`${isMirroredIntlKey}Desc`]),
onClick: () => onHandleMirror(cameraId),
onClick: () => onHandleMirror(),
dataTest: 'mirrorWebcamBtn',
});
}
@ -162,7 +182,7 @@ const UserActions = (props) => {
key: `${cameraId}-focus`,
label: intl.formatMessage(intlMessages[`${isFocusedIntlKey}Label`]),
description: intl.formatMessage(intlMessages[`${isFocusedIntlKey}Desc`]),
onClick: () => onHandleVideoFocus(cameraId),
onClick: () => onHandleVideoFocus?.(cameraId),
dataTest: 'FocusWebcamBtn',
});
}
@ -189,8 +209,11 @@ const UserActions = (props) => {
case UserCameraDropdownItemType.OPTION:
menuItems.push({
key: pluginItem.id,
// @ts-expect-error -> Plugin-related.
label: pluginItem.label,
// @ts-expect-error -> Plugin-related.
onClick: pluginItem.onClick,
// @ts-expect-error -> Plugin-related.
icon: pluginItem.icon,
});
break;
@ -238,7 +261,7 @@ const UserActions = (props) => {
<Styled.DropdownTrigger
tabIndex={0}
data-test="dropdownWebcamButton"
isRTL={isRTL}
$isRTL={isRTL}
>
{name}
</Styled.DropdownTrigger>
@ -257,8 +280,8 @@ const UserActions = (props) => {
/>
)
: (
<Styled.Dropdown isFirefox={isFirefox}>
<Styled.UserName noMenu={numOfStreams < 3}>
<Styled.Dropdown $isFirefox={isFirefox}>
<Styled.UserName $noMenu={numOfStreams < 3}>
{name}
</Styled.UserName>
</Styled.Dropdown>
@ -274,29 +297,3 @@ const UserActions = (props) => {
};
export default UserActions;
UserActions.defaultProps = {
focused: false,
isVideoSqueezed: false,
videoContainer: () => { },
onHandleVideoFocus: () => {},
};
UserActions.propTypes = {
name: PropTypes.string.isRequired,
cameraId: PropTypes.string.isRequired,
numOfStreams: PropTypes.number.isRequired,
onHandleVideoFocus: PropTypes.func,
user: PropTypes.shape({
pin: PropTypes.bool.isRequired,
userId: PropTypes.string.isRequired,
}).isRequired,
focused: PropTypes.bool,
isVideoSqueezed: PropTypes.bool,
videoContainer: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]),
onHandleMirror: PropTypes.func.isRequired,
onHandleDisableCam: PropTypes.func.isRequired,
};

View File

@ -7,7 +7,9 @@ import { landscape, mediumUp } from '/imports/ui/stylesheets/styled-components/b
import { fontSizeSmaller } from '/imports/ui/stylesheets/styled-components/typography';
import Button from '/imports/ui/components/common/button/component';
const DropdownTrigger = styled(DivElipsis)`
const DropdownTrigger = styled(DivElipsis)<{
$isRTL: boolean;
}>`
user-select: none;
position: relative;
// Keep the background with 0.5 opacity, but leave the text with 1
@ -26,7 +28,7 @@ const DropdownTrigger = styled(DivElipsis)`
content: "\\203a";
position: absolute;
transform: rotate(90deg);
${({ isRTL }) => isRTL && `
${({ $isRTL }) => $isRTL && `
transform: rotate(-90deg);
`}
top: 45%;
@ -36,19 +38,23 @@ const DropdownTrigger = styled(DivElipsis)`
}
`;
const UserName = styled(TextElipsis)`
const UserName = styled(TextElipsis)<{
$noMenu: boolean;
}>`
position: relative;
// Keep the background with 0.5 opacity, but leave the text with 1
color: ${colorOffWhite};
padding: 0 1rem 0 .5rem !important;
font-size: 80%;
${({ noMenu }) => noMenu && `
${({ $noMenu }) => $noMenu && `
padding: 0 .5rem 0 .5rem !important;
`}
`;
const Dropdown = styled.div`
const Dropdown = styled.div<{
$isFirefox: boolean;
}>`
display: flex;
outline: none !important;
background-color: rgba(0, 0, 0, 0.5);
@ -68,7 +74,7 @@ const Dropdown = styled.div`
}
}
${({ isFirefox }) => isFirefox && `
${({ $isFirefox }) => $isFirefox && `
max-width: 100%;
`}
`;

View File

@ -1,15 +1,28 @@
// @ts-nocheck
/* eslint-disable */
import React from 'react';
import PropTypes from 'prop-types';
import Styled from './styles';
import Icon from '/imports/ui/components/common/icon/component';
import UserListService from '/imports/ui/components/user-list/service';
import { StreamUser } from '../../../types';
const UserAvatarVideo = (props) => {
const { user, unhealthyStream, squeezed, voiceUser } = props;
interface UserAvatarVideoProps {
user: Partial<StreamUser>;
// eslint-disable-next-line react/require-default-props
voiceUser?: {
muted?: boolean;
listenOnly?: boolean;
talking?: boolean;
joined?: boolean;
};
squeezed: boolean;
unhealthyStream: boolean;
}
const UserAvatarVideo: React.FC<UserAvatarVideoProps> = (props) => {
const {
name, color, avatar, role, emoji,
user, unhealthyStream, squeezed, voiceUser = {},
} = props;
const {
name = '', color, avatar, role, emoji,
} = user;
let {
presenter, clientType,
@ -21,6 +34,7 @@ const UserAvatarVideo = (props) => {
const handleUserIcon = () => {
if (emoji !== 'none') {
// @ts-expect-error -> Untyped component.
return <Icon iconName={UserListService.normalizeEmojiName(emoji)} />;
}
return name.toLowerCase().slice(0, 2);
@ -29,7 +43,7 @@ const UserAvatarVideo = (props) => {
// hide icons when squeezed
if (squeezed) {
presenter = false;
clientType = false;
clientType = '';
}
return (
@ -42,6 +56,7 @@ const UserAvatarVideo = (props) => {
avatar={avatar}
unhealthyStream={unhealthyStream}
talking={talking}
whiteboardAccess={undefined}
>
{handleUserIcon()}
</Styled.UserAvatarStyled>
@ -49,17 +64,3 @@ const UserAvatarVideo = (props) => {
};
export default UserAvatarVideo;
UserAvatarVideo.propTypes = {
user: PropTypes.shape({
name: PropTypes.string.isRequired,
color: PropTypes.string.isRequired,
avatar: PropTypes.string.isRequired,
role: PropTypes.string.isRequired,
emoji: PropTypes.string.isRequired,
presenter: PropTypes.bool.isRequired,
clientType: PropTypes.string.isRequired,
}).isRequired,
unhealthyStream: PropTypes.bool.isRequired,
squeezed: PropTypes.bool.isRequired,
};

View File

@ -1,5 +1,3 @@
// @ts-nocheck
/* eslint-disable */
import UserAvatar from '/imports/ui/components/user-avatar/component';
import {
userIndicatorsOffset,
@ -10,7 +8,11 @@ import {
} from '/imports/ui/stylesheets/styled-components/palette';
import styled from 'styled-components';
const UserAvatarStyled = styled(UserAvatar)`
const UserAvatarStyled = styled(UserAvatar)<{
unhealthyStream: boolean;
dialIn: boolean;
presenter: boolean;
}>`
height: 60%;
width: 45%;
max-width: 66px;

View File

@ -1,10 +1,18 @@
// @ts-nocheck
/* eslint-disable */
import React from 'react';
import PropTypes from 'prop-types';
import Styled from './styles';
import { StreamUser } from '../../../types';
const UserStatus = (props) => {
interface UserStatusProps {
user: Partial<StreamUser>;
voiceUser: {
muted: boolean;
listenOnly: boolean;
talking: boolean;
joined: boolean;
};
}
const UserStatus: React.FC<UserStatusProps> = (props) => {
const { voiceUser, user } = props;
const listenOnly = voiceUser?.listenOnly;
@ -26,11 +34,3 @@ const UserStatus = (props) => {
};
export default UserStatus;
UserStatus.propTypes = {
voiceUser: PropTypes.shape({
listenOnly: PropTypes.bool.isRequired,
muted: PropTypes.bool.isRequired,
joined: PropTypes.bool.isRequired,
}).isRequired,
};

View File

@ -1,11 +1,18 @@
// @ts-nocheck
/* eslint-disable */import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import React, { MutableRefObject, useEffect } from 'react';
import { ACTIONS } from '/imports/ui/components/layout/enums';
import FullscreenButtonContainer from '/imports/ui/components/common/fullscreen-button/container';
import Styled from './styles';
const ViewActions = (props) => {
interface ViewActionsProps {
name: string;
cameraId: string;
videoContainer: MutableRefObject<HTMLDivElement | null>;
isFullscreenContext: boolean;
layoutContextDispatch: (...args: unknown[]) => void;
isStream: boolean;
}
const ViewActions: React.FC<ViewActionsProps> = (props) => {
const {
name, cameraId, videoContainer, isFullscreenContext, layoutContextDispatch, isStream,
} = props;
@ -13,7 +20,6 @@ const ViewActions = (props) => {
const ALLOW_FULLSCREEN = window.meetingClientSettings.public.app.allowFullscreen;
useEffect(() => () => {
// exit fullscreen when component is unmounted
if (isFullscreenContext) {
layoutContextDispatch({
type: ACTIONS.SET_FULLSCREEN_ELEMENT,
@ -43,14 +49,3 @@ const ViewActions = (props) => {
};
export default ViewActions;
ViewActions.propTypes = {
name: PropTypes.string.isRequired,
cameraId: PropTypes.string.isRequired,
videoContainer: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]).isRequired,
isFullscreenContext: PropTypes.bool.isRequired,
layoutContextDispatch: PropTypes.func.isRequired,
};

View File

@ -1,8 +1,6 @@
/* eslint-disable */
// @ts-nocheck
import React, { useState, useEffect } from 'react';
import Resizable from 're-resizable';
import Draggable from 'react-draggable';
import Draggable, { DraggableEvent } from 'react-draggable';
import { Session } from 'meteor/session';
import { withTracker } from 'meteor/react-meteor-data';
import { useSubscription } from '@apollo/client';
@ -15,17 +13,29 @@ import {
} from '/imports/ui/components/layout/context';
import Settings from '/imports/ui/services/settings';
import { LAYOUT_TYPE, ACTIONS, CAMERADOCK_POSITION } from '/imports/ui/components/layout/enums';
import { sortVideoStreams } from '/imports/ui/components/video-provider/stream-sorting';
import { CURRENT_PRESENTATION_PAGE_SUBSCRIPTION } from '/imports/ui/components/whiteboard/queries';
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
import DropAreaContainer from './drop-areas/container';
import VideoProviderContainer from '/imports/ui/components/video-provider/video-provider-graphql/container';
import Storage from '/imports/ui/services/storage/session';
import Styled from '../styles';
import Styled from './styles';
import { Input, Layout, Output } from '/imports/ui/components/layout/layoutTypes';
import { VideoItem } from '/imports/ui/components/video-provider/video-provider-graphql/types';
const { defaultSorting: DEFAULT_SORTING } = window.meetingClientSettings.public.kurento.cameraSortingModes;
interface WebcamComponentGraphqlProps {
cameraDock: Output['cameraDock'];
swapLayout: boolean;
focusedId: string;
layoutContextDispatch: (...args: unknown[]) => void;
fullscreen: Layout['fullscreen'];
isPresenter: boolean;
displayPresentation: boolean;
cameraOptimalGridSize: Input['cameraDock']['cameraOptimalGridSize'];
isRTL: boolean;
isGridEnabled: boolean;
}
const WebcamComponentGraphql = ({
const WebcamComponentGraphql: React.FC<WebcamComponentGraphqlProps> = ({
cameraDock,
swapLayout,
focusedId,
@ -45,7 +55,7 @@ const WebcamComponentGraphql = ({
const [draggedAtLeastOneTime, setDraggedAtLeastOneTime] = useState(false);
const lastSize = Storage.getItem('webcamSize') || { width: 0, height: 0 };
const { width: lastWidth, height: lastHeight } = lastSize;
const { height: lastHeight } = lastSize as { width: number, height: number };
const isCameraTopOrBottom = cameraDock.position === CAMERADOCK_POSITION.CONTENT_TOP
|| cameraDock.position === CAMERADOCK_POSITION.CONTENT_BOTTOM;
@ -72,7 +82,9 @@ const WebcamComponentGraphql = ({
}, [fullscreen]);
useEffect(() => {
const newCameraMaxWidth = (isPresenter && cameraDock.presenterMaxWidth) ? cameraDock.presenterMaxWidth : cameraDock.maxWidth;
const newCameraMaxWidth = (isPresenter && cameraDock.presenterMaxWidth)
? cameraDock.presenterMaxWidth
: cameraDock.maxWidth;
setCameraMaxWidth(newCameraMaxWidth);
if (isCameraLeftOrRight && cameraDock.width > newCameraMaxWidth) {
@ -91,18 +103,18 @@ const WebcamComponentGraphql = ({
}
const cams = document.getElementById('cameraDock');
cams?.setAttribute("data-position", cameraDock.position);
cams?.setAttribute('data-position', cameraDock.position);
}, [cameraDock.position, cameraDock.maxWidth, isPresenter, displayPresentation]);
const handleVideoFocus = (id) => {
const handleVideoFocus = (id: string) => {
layoutContextDispatch({
type: ACTIONS.SET_FOCUSED_CAMERA_ID,
value: focusedId !== id ? id : false,
});
}
};
const onResizeHandle = (deltaWidth, deltaHeight) => {
if (cameraDock.resizableEdge.top || cameraDock.resizableEdge.bottom) {
const onResizeHandle = (deltaWidth: number, deltaHeight: number) => {
if (cameraDock.resizableEdge?.top || cameraDock.resizableEdge?.bottom) {
layoutContextDispatch(
{
type: ACTIONS.SET_CAMERA_DOCK_SIZE,
@ -115,7 +127,7 @@ const WebcamComponentGraphql = ({
},
);
}
if (cameraDock.resizableEdge.left || cameraDock.resizableEdge.right) {
if (cameraDock.resizableEdge?.left || cameraDock.resizableEdge?.right) {
layoutContextDispatch(
{
type: ACTIONS.SET_CAMERA_DOCK_SIZE,
@ -139,18 +151,19 @@ const WebcamComponentGraphql = ({
});
};
const handleWebcamDragStop = (e) => {
const handleWebcamDragStop = (e: DraggableEvent) => {
setIsDragging(false);
setDraggedAtLeastOneTime(false);
document.body.style.overflow = 'auto';
const dropAreaId = (e.target as HTMLDivElement).id;
if (Object.values(CAMERADOCK_POSITION).includes(e.target.id) && draggedAtLeastOneTime) {
if (Object.values(CAMERADOCK_POSITION).includes(dropAreaId) && draggedAtLeastOneTime) {
const layout = document.getElementById('layout');
layout?.setAttribute("data-cam-position", e?.target?.id);
layout?.setAttribute('data-cam-position', dropAreaId);
layoutContextDispatch({
type: ACTIONS.SET_CAMERA_DOCK_POSITION,
value: e.target.id,
value: dropAreaId,
});
}
@ -160,32 +173,33 @@ const WebcamComponentGraphql = ({
});
};
let draggableOffset = {
const draggableOffset = {
left: isDragging && (isCameraTopOrBottom || isCameraSidebar)
? ((cameraDock.width - cameraSize.width) / 2)
? ((cameraDock.width - (cameraSize?.width ?? 0)) / 2)
: 0,
top: isDragging && isCameraLeftOrRight
? ((cameraDock.height - cameraSize.height) / 2)
? ((cameraDock.height - (cameraSize?.height ?? 0)) / 2)
: 0,
};
if (isRTL) {
draggableOffset.left = draggableOffset.left * -1;
draggableOffset.left *= -1;
}
const isIphone = !!(navigator.userAgent.match(/iPhone/i));
const mobileWidth = `${isDragging ? cameraSize.width : cameraDock.width}pt`;
const mobileHeight = `${isDragging ? cameraSize.height : cameraDock.height}pt`;
const isDesktopWidth = isDragging ? cameraSize.width : cameraDock.width;
const isDesktopHeight = isDragging ? cameraSize.height : cameraDock.height;
const isIphone = !!(navigator.userAgent.match(/iPhone/i));
const mobileWidth = `${isDragging ? cameraSize?.width : cameraDock.width}pt`;
const mobileHeight = `${isDragging ? cameraSize?.height : cameraDock.height}pt`;
const isDesktopWidth = isDragging ? cameraSize?.width : cameraDock.width;
const isDesktopHeight = isDragging ? cameraSize?.height : cameraDock.height;
const camOpacity = isDragging ? 0.5 : undefined;
return (
<>
{isDragging ? <DropAreaContainer /> : null}
<Styled.ResizableWrapper
horizontal={cameraDock.position === CAMERADOCK_POSITION.CONTENT_TOP
$horizontal={cameraDock.position === CAMERADOCK_POSITION.CONTENT_TOP
|| cameraDock.position === CAMERADOCK_POSITION.CONTENT_BOTTOM}
vertical={cameraDock.position === CAMERADOCK_POSITION.CONTENT_LEFT
$vertical={cameraDock.position === CAMERADOCK_POSITION.CONTENT_LEFT
|| cameraDock.position === CAMERADOCK_POSITION.CONTENT_RIGHT}
>
<Draggable
@ -210,13 +224,13 @@ const WebcamComponentGraphql = ({
}
>
<Resizable
minWidth={isDragging ? cameraSize.width : cameraDock.minWidth}
minHeight={isDragging ? cameraSize.height : cameraDock.minHeight}
maxWidth={isDragging ? cameraSize.width : cameraMaxWidth}
maxHeight={isDragging ? cameraSize.height : cameraDock.maxHeight}
minWidth={isDragging ? cameraSize?.width : cameraDock.minWidth}
minHeight={isDragging ? cameraSize?.height : cameraDock.minHeight}
maxWidth={isDragging ? cameraSize?.width : cameraMaxWidth}
maxHeight={isDragging ? cameraSize?.height : cameraDock.maxHeight}
size={{
width: isDragging ? cameraSize.width : cameraDock.width,
height: isDragging ? cameraSize.height : cameraDock.height,
width: isDragging ? cameraSize?.width : cameraDock.width,
height: isDragging ? cameraSize?.height : cameraDock.height,
}}
onResizeStart={() => {
setIsResizing(true);
@ -227,7 +241,7 @@ const WebcamComponentGraphql = ({
value: true,
});
}}
onResize={(e, direction, ref, d) => {
onResize={(_, __, ___, d) => {
onResizeHandle(d.width, d.height);
}}
onResizeStop={() => {
@ -239,11 +253,11 @@ const WebcamComponentGraphql = ({
});
}}
enable={{
top: !isFullscreen && !isDragging && !swapLayout && cameraDock.resizableEdge.top,
top: !isFullscreen && !isDragging && !swapLayout && cameraDock?.resizableEdge?.top,
bottom: !isFullscreen && !isDragging && !swapLayout
&& cameraDock.resizableEdge.bottom,
left: !isFullscreen && !isDragging && !swapLayout && cameraDock.resizableEdge.left,
right: !isFullscreen && !isDragging && !swapLayout && cameraDock.resizableEdge.right,
&& cameraDock?.resizableEdge?.bottom,
left: !isFullscreen && !isDragging && !swapLayout && cameraDock?.resizableEdge?.left,
right: !isFullscreen && !isDragging && !swapLayout && cameraDock?.resizableEdge?.right,
topLeft: false,
topRight: false,
bottomLeft: false,
@ -251,12 +265,12 @@ const WebcamComponentGraphql = ({
}}
style={{
position: 'absolute',
zIndex: isCameraSidebar && !isDragging ? 0 : cameraDock.zIndex,
zIndex: isCameraSidebar && !isDragging ? 0 : cameraDock?.zIndex,
}}
>
<Styled.Draggable
isDraggable={cameraDock.isDraggable && !isFullscreen && !isDragging}
isDragging={isDragging}
$isDraggable={!!cameraDock.isDraggable && !isFullscreen && !isDragging}
$isDragging={isDragging}
id="cameraDock"
role="region"
draggable={cameraDock.isDraggable && !isFullscreen ? 'true' : undefined}
@ -264,7 +278,7 @@ const WebcamComponentGraphql = ({
width: isIphone ? mobileWidth : isDesktopWidth,
height: isIphone ? mobileHeight : isDesktopHeight,
opacity: camOpacity,
background: null,
background: 'none',
}}
>
<VideoProviderContainer
@ -299,11 +313,11 @@ const WebcamContainerGraphql: React.FC<WebcamContainerGraphqlProps> = ({
paginationEnabled,
viewParticipantsWebcams,
}) => {
const fullscreen = layoutSelect((i) => i.fullscreen);
const isRTL = layoutSelect((i) => i.isRTL);
const cameraDockInput = layoutSelectInput((i) => i.cameraDock);
const presentation = layoutSelectOutput((i) => i.presentation);
const cameraDock = layoutSelectOutput((i) => i.cameraDock);
const fullscreen = layoutSelect((i: Layout) => i.fullscreen);
const isRTL = layoutSelect((i: Layout) => i.isRTL);
const cameraDockInput = layoutSelectInput((i: Input) => i.cameraDock);
const presentation = layoutSelectOutput((i: Output) => i.presentation);
const cameraDock = layoutSelectOutput((i: Output) => i.cameraDock);
const layoutContextDispatch = layoutDispatch();
const { data: presentationPageData } = useSubscription(CURRENT_PRESENTATION_PAGE_SUBSCRIPTION);
const presentationPage = presentationPageData?.pres_page_curr[0] || {};
@ -330,9 +344,12 @@ const WebcamContainerGraphql: React.FC<WebcamContainerGraphqlProps> = ({
const { streams: videoUsers, gridUsers } = useVideoStreams(isGridEnabled, paginationEnabled, viewParticipantsWebcams);
let usersVideo;
let usersVideo: VideoItem[];
if (gridUsers.length > 0) {
usersVideo = videoUsers.concat(gridUsers);
usersVideo = [
...videoUsers,
...gridUsers,
];
} else {
usersVideo = videoUsers;
}
@ -348,7 +365,7 @@ const WebcamContainerGraphql: React.FC<WebcamContainerGraphqlProps> = ({
cameraOptimalGridSize,
layoutContextDispatch,
fullscreen,
isPresenter: currentUserData?.presenter,
isPresenter: currentUserData?.presenter ?? false,
displayPresentation,
isRTL,
isGridEnabled,
@ -360,14 +377,26 @@ const WebcamContainerGraphql: React.FC<WebcamContainerGraphqlProps> = ({
: null;
};
export default withTracker<{
audioModalIsOpen: boolean,
}, WebcamContainerGraphqlProps>(() => {
const data = {
audioModalIsOpen: Session.get('audioModalIsOpen'),
paginationEnabled: Settings.application.paginationEnabled,
viewParticipantsWebcams: Settings.dataSaving.viewParticipantsWebcams,
};
type TrackerData = {
audioModalIsOpen: boolean;
paginationEnabled: boolean;
viewParticipantsWebcams: boolean;
};
return data;
type TrackerProps = {
isLayoutSwapped: boolean;
layoutType: string;
};
export default withTracker<TrackerData, TrackerProps>(() => {
const audioModalIsOpen = Session.get('audioModalIsOpen');
// @ts-expect-error -> Untyped object.
const { paginationEnabled } = Settings.application;
// @ts-expect-error -> Untyped object.
const { viewParticipantsWebcams } = Settings.dataSaving;
return {
audioModalIsOpen,
paginationEnabled,
viewParticipantsWebcams,
};
})(WebcamContainerGraphql);

View File

@ -1,7 +1,5 @@
// @ts-nocheck
/* eslint-disable */
import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import { defineMessages, useIntl } from 'react-intl';
import Styled from './styles';
const intlMessages = defineMessages({
@ -11,31 +9,42 @@ const intlMessages = defineMessages({
},
});
const DropArea = ({
id, dataTest, style, intl,
}) => (
<>
<Styled.DropZoneArea
id={id}
data-test={dataTest}
style={
{
...style,
zIndex: style.zIndex + 1,
}
}
/>
<Styled.DropZoneBg
style={
{
...style,
zIndex: style.zIndex,
}
}
>
{intl.formatMessage(intlMessages.dropZoneLabel)}
</Styled.DropZoneBg>
</>
);
interface DropAreaProps {
id: string;
dataTest: string;
style: Record<string, unknown>;
}
export default injectIntl(DropArea);
const DropArea: React.FC<DropAreaProps> = ({
id,
dataTest,
style,
}) => {
const intl = useIntl();
return (
<>
<Styled.DropZoneArea
id={id}
data-test={dataTest}
style={
{
...style,
zIndex: (style.zIndex as number) + 1,
}
}
/>
<Styled.DropZoneBg
style={
{
...style,
zIndex: (style.zIndex as number),
}
}
>
{intl.formatMessage(intlMessages.dropZoneLabel)}
</Styled.DropZoneBg>
</>
);
};
export default DropArea;

View File

@ -1,15 +1,19 @@
// @ts-nocheck
/* eslint-disable */
import React from 'react';
import { layoutSelectOutput } from '/imports/ui/components/layout/context';
import { Output } from '/imports/ui/components/layout/layoutTypes';
import DropArea from './component';
const DropAreaContainer = () => {
const dropZoneAreas = layoutSelectOutput((i) => i.dropZoneAreas);
const dropZoneAreas = layoutSelectOutput((i: Output) => i.dropZoneAreas);
return (
Object.keys(dropZoneAreas).map((objectKey) => (
<DropArea dataTest={`dropArea-${objectKey}`} key={objectKey} id={objectKey} style={dropZoneAreas[objectKey]} />
<DropArea
dataTest={`dropArea-${objectKey}`}
key={objectKey}
id={objectKey}
style={dropZoneAreas[objectKey]}
/>
))
);
};

View File

@ -0,0 +1,44 @@
import styled, { css } from 'styled-components';
const Draggable = styled.div<{
$isDraggable: boolean;
$isDragging: boolean;
}>`
${({ $isDraggable }) => $isDraggable && css`
& > video {
cursor: grabbing;
}
`}
${({ $isDragging }) => $isDragging && css`
background-color: rgba(200, 200, 200, 0.5);
`}
`;
const ResizableWrapper = styled.div<{
$horizontal: boolean;
$vertical: boolean;
}>`
${({ $horizontal }) => $horizontal && css`
& > div span div {
&:hover {
background-color: rgba(255, 255, 255, .3);
}
width: 100% !important;
}
`}
${({ $vertical }) => $vertical && css`
& > div span div {
&:hover {
background-color: rgba(255, 255, 255, .3);
}
height: 100% !important;
}
`}
`;
export default {
Draggable,
ResizableWrapper,
};

View File

@ -3,8 +3,7 @@ import { ReactiveVar, makeVar, useReactiveVar } from '@apollo/client';
function createUseLocalState<T>(initialValue: T):
[
() => [T, (value: T) => void], // hook that returns [state, setter]
// eslint-disable-next-line @typescript-eslint/ban-types
(value: T | Function) => void, // setter
(value: T | ((curr: T) => T)) => void, // setter
ReactiveVar<T> // state
] {
const localState = makeVar(initialValue);
@ -15,7 +14,7 @@ function createUseLocalState<T>(initialValue: T):
}
// eslint-disable-next-line @typescript-eslint/ban-types
function changeLocalState(value: T | Function) {
function changeLocalState(value: T | ((curr: T) => T)) {
if (value instanceof Function) {
return localState(value(localState()));
}

View File

@ -119,7 +119,7 @@ class Page {
}
await this.waitForSelector(e.webcamContainer, VIDEO_LOADING_WAIT_TIME);
await this.waitForSelector(e.leaveVideo, VIDEO_LOADING_WAIT_TIME);
await this.wasRemoved(e.webcamConnecting);
await this.wasRemoved(e.webcamConnecting, VIDEO_LOADING_WAIT_TIME);
}
getLocator(selector) {