Delete and move files

This commit is contained in:
João Victor 2024-06-17 08:35:11 -03:00
parent 4063ee811b
commit 452867246a
72 changed files with 107 additions and 6025 deletions

View File

@ -1,130 +0,0 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import { useMutation } from '@apollo/client';
import VideoProvider from './component';
import VideoService from './service';
import { sortVideoStreams } from '/imports/ui/components/video-provider/stream-sorting';
import { CAMERA_BROADCAST_START, CAMERA_BROADCAST_STOP } from './mutations';
import { getVideoData, getVideoDataGrid } from './queries';
import useMeeting from '/imports/ui/core/hooks/useMeeting';
import Auth from '/imports/ui/services/auth';
import useCurrentUser from '../../core/hooks/useCurrentUser';
import VideoProviderContainerGraphql from './video-provider-graphql/container';
import useDeduplicatedSubscription from '../../core/hooks/useDeduplicatedSubscription';
const VideoProviderContainer = ({ children, ...props }) => {
const { streams, isGridEnabled } = props;
const [cameraBroadcastStart] = useMutation(CAMERA_BROADCAST_START);
const [cameraBroadcastStop] = useMutation(CAMERA_BROADCAST_STOP);
const sendUserShareWebcam = (cameraId) => {
cameraBroadcastStart({ variables: { cameraId } });
};
const sendUserUnshareWebcam = (cameraId) => {
cameraBroadcastStop({ variables: { cameraId } });
};
const playStart = (cameraId) => {
if (VideoService.isLocalStream(cameraId)) {
sendUserShareWebcam(cameraId);
VideoService.joinedVideo();
}
};
return (
!streams.length && !isGridEnabled
? null
: (
<VideoProvider
{...props}
playStart={playStart}
sendUserUnshareWebcam={sendUserUnshareWebcam}
>
{children}
</VideoProvider>
)
);
};
withTracker(({ swapLayout, ...rest }) => {
const isGridLayout = Session.get('isGridEnabled');
const graphqlQuery = isGridLayout ? getVideoDataGrid : getVideoData;
const currUserId = Auth.userID;
const { data: currentMeeting } = useMeeting((m) => ({
usersPolicies: m.usersPolicies,
}));
const { data: currentUser } = useCurrentUser((user) => ({
locked: user.locked,
}));
const fetchedStreams = VideoService.fetchVideoStreams();
const variables = isGridLayout
? {}
: {
userIds: fetchedStreams.map((stream) => stream.userId) || [],
};
const {
data: videoUserSubscription,
} = useDeduplicatedSubscription(graphqlQuery, { variables });
const users = videoUserSubscription?.user || [];
let streams = [];
let gridUsers = [];
let totalNumberOfStreams = 0;
if (isGridLayout) {
streams = fetchedStreams;
gridUsers = VideoService.getGridUsers(videoUserSubscription?.user, fetchedStreams);
totalNumberOfStreams = fetchedStreams.length;
} else {
const {
streams: s,
totalNumberOfStreams: ts,
} = VideoService.getVideoStreams();
streams = s;
totalNumberOfStreams = ts;
}
let usersVideo = streams;
const {
defaultSorting: DEFAULT_SORTING,
} = window.meetingClientSettings.public.kurento.cameraSortingModes;
if (gridUsers.length > 0) {
const items = usersVideo.concat(gridUsers);
usersVideo = sortVideoStreams(items, DEFAULT_SORTING);
}
if (currentMeeting?.usersPolicies?.webcamsOnlyForModerator
&& currentUser?.locked) {
if (users.length > 0) {
usersVideo = usersVideo.filter((uv) => {
if (uv.userId === currUserId) {
return true;
}
const user = users.find((u) => u.userId === uv.userId);
return user?.isModerator;
});
}
}
return {
swapLayout,
streams: usersVideo,
totalNumberOfStreams,
isUserLocked: VideoService.isUserLocked(),
currentVideoPageIndex: VideoService.getCurrentVideoPageIndex(),
isMeteorConnected: Meteor.status().connected,
users,
...rest,
};
})(VideoProviderContainer);
export default VideoProviderContainerGraphql;

View File

@ -1,101 +0,0 @@
import React, { Component } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import { notify } from '/imports/ui/services/notification';
import { toast } from 'react-toastify';
import Styled from './styles';
const intlMessages = defineMessages({
suggestLockTitle: {
id: 'app.video.suggestWebcamLock',
description: 'Label for notification title',
},
suggestLockReason: {
id: 'app.video.suggestWebcamLockReason',
description: 'Reason for activate the webcams\'s lock',
},
enable: {
id: 'app.video.enable',
description: 'Enable button label',
},
cancel: {
id: 'app.video.cancel',
description: 'Cancel button label',
},
});
const REPEAT_INTERVAL = 120000;
class LockViewersNotifyComponent extends Component {
constructor(props) {
super(props);
this.interval = null;
this.intervalCallback = this.intervalCallback.bind(this);
}
componentDidUpdate() {
const {
viewersInWebcam,
lockSettings,
limitOfViewersInWebcam,
webcamOnlyForModerator,
currentUserIsModerator,
limitOfViewersInWebcamIsEnable,
} = this.props;
const viwerersInWebcamGreaterThatLimit = (viewersInWebcam >= limitOfViewersInWebcam)
&& limitOfViewersInWebcamIsEnable;
const webcamForViewersIsLocked = lockSettings.disableCam || webcamOnlyForModerator;
if (viwerersInWebcamGreaterThatLimit
&& !webcamForViewersIsLocked
&& currentUserIsModerator
&& !this.interval) {
this.interval = setInterval(this.intervalCallback, REPEAT_INTERVAL);
this.intervalCallback();
}
if (webcamForViewersIsLocked || (!viwerersInWebcamGreaterThatLimit && this.interval)) {
clearInterval(this.interval);
this.interval = null;
}
}
intervalCallback() {
const {
toggleWebcamsOnlyForModerator,
intl,
} = this.props;
const lockToastId = `suggestLock-${new Date().getTime()}`;
notify(
(
<>
<Styled.Info>{intl.formatMessage(intlMessages.suggestLockTitle)}</Styled.Info>
<Styled.ButtonWrapper>
<Styled.ManyUsersButton
label={intl.formatMessage(intlMessages.enable)}
aria-label={intl.formatMessage(intlMessages.enable)}
onClick={toggleWebcamsOnlyForModerator}
/>
|
<Styled.ManyUsersButton
label={intl.formatMessage(intlMessages.cancel)}
aria-label={intl.formatMessage(intlMessages.cancel)}
onClick={() => toast.dismiss(lockToastId)}
/>
</Styled.ButtonWrapper>
<Styled.Info>{intl.formatMessage(intlMessages.suggestLockReason)}</Styled.Info>
</>
),
'info',
'rooms',
{
toastId: lockToastId,
},
);
}
render() {
return null;
}
}
export default injectIntl(LockViewersNotifyComponent);

View File

@ -1,55 +0,0 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import Meetings from '/imports/api/meetings';
import Auth from '/imports/ui/services/auth';
import Users from '/imports/api/users/';
import VideoStreams from '/imports/api/video-streams';
import { useMutation } from '@apollo/client';
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';
const ManyUsersContainer = (props) => {
const { data: currentUserData } = useCurrentUser((user) => ({
isModerator: user.isModerator,
}));
const [setWebcamOnlyForModerator] = useMutation(SET_WEBCAM_ONLY_FOR_MODERATOR);
const toggleWebcamsOnlyForModerator = () => {
setWebcamOnlyForModerator({
variables: {
webcamsOnlyForModerator: true,
},
});
};
const currentUserIsModerator = currentUserData?.isModerator;
return <ManyUsersComponent {...{ toggleWebcamsOnlyForModerator, currentUserIsModerator, ...props }} />;
};
export default withTracker(() => {
const ROLE_VIEWER = window.meetingClientSettings.public.user.role_viewer;
const meeting = Meetings.findOne({
meetingId: Auth.meetingID,
}, { fields: { 'usersPolicies.webcamsOnlyForModerator': 1, lockSettings: 1 } });
const videoStreams = VideoStreams.find({ meetingId: Auth.meetingID },
{ fields: { userId: 1 } }).fetch();
const videoUsersIds = videoStreams.map(u => u.userId);
return {
viewersInWebcam: Users.find({
meetingId: Auth.meetingID,
userId: {
$in: videoUsersIds,
},
role: ROLE_VIEWER,
presenter: false,
}, { fields: {} }).count(),
lockSettings: meeting.lockSettings,
webcamOnlyForModerator: meeting.usersPolicies.webcamsOnlyForModerator,
limitOfViewersInWebcam: window.meetingClientSettings.public.app.viewersInWebcam,
limitOfViewersInWebcamIsEnable: window.meetingClientSettings
.public.app.enableLimitOfViewersInWebcam,
};
})(ManyUsersContainer);

View File

@ -1,44 +0,0 @@
import styled from 'styled-components';
import { colorPrimary } from '/imports/ui/stylesheets/styled-components/palette';
import Button from '/imports/ui/components/common/button/component';
import { headingsFontWeight } from '/imports/ui/stylesheets/styled-components/typography';
const Info = styled.p`
margin: 0;
`;
const ButtonWrapper = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
font-weight: ${headingsFontWeight};
color: ${colorPrimary};
& > button {
padding: 0 0 0 .5rem;
}
background-color: inherit;
&:focus,&:hover {
background-color: inherit;
}
`;
const ManyUsersButton = styled(Button)`
flex: 0 1 48%;
color: ${colorPrimary};
margin: 0;
font-weight: inherit;
background-color: inherit;
&:focus,&:hover {
background-color: inherit;
}
`;
export default {
Info,
ButtonWrapper,
ManyUsersButton,
};

View File

@ -1,55 +1,119 @@
import { gql } from '@apollo/client';
import { User } from '../../Types/user';
import type { User } from './types';
export interface getVideoDataResponse {
user: Array<Pick<User, 'loggedOut'| 'away'| 'disconnected'| 'emoji'| 'name'>>
interface Voice {
floor: boolean;
lastFloorTime: string;
}
export type queryUser = Pick<User, 'loggedOut'| 'away'| 'disconnected'| 'emoji'| 'name'>
export const getVideoData = gql`
subscription getvideoData($userIds: [String]!) {
user(where: {userId: {_in: $userIds}}) {
loggedOut
away
disconnected
emoji
name
nameSortable
role
avatar
color
presenter
clientType
userId
raiseHand
isModerator
reactionEmoji
export interface VideoStreamsResponse {
user_camera: {
streamId: string;
user: User;
voice?: Voice;
}[];
}
export interface GridUsersResponse {
user: User[];
}
export interface OwnVideoStreamsResponse {
user_camera: {
streamId: string;
}[];
}
export const VIDEO_STREAMS_SUBSCRIPTION = gql`
subscription VideoStreams {
user_camera {
streamId
user {
name
userId
nameSortable
pinned
away
disconnected
emoji
role
avatar
color
presenter
clientType
raiseHand
isModerator
reactionEmoji
}
voice {
floor
lastFloorTime
}
}
}
}
`;
export const getVideoDataGrid = gql`
subscription getVideoDataGrid {
user {
loggedOut
away
disconnected
emoji
name
nameSortable
role
avatar
color
presenter
clientType
userId
raiseHand
reactionEmoji
export const OWN_VIDEO_STREAMS_QUERY = gql`
query OwnVideoStreams($userId: String!, $streamIdPrefix: String!) {
user_camera(
where: {
userId: { _eq: $userId },
streamId: { _like: $streamIdPrefix }
},
) {
streamId
}
}
`;
export const VIEWERS_IN_WEBCAM_COUNT_SUBSCRIPTION = gql`
subscription ViewerVideoStreams {
user_camera_aggregate(where: {
user: { role: { _eq: "VIEWER" }, presenter: { _eq: false } }
}) {
aggregate {
count
}
}
}
`;
export const GRID_USERS_SUBSCRIPTION = gql`
subscription GridUsers($exceptUserIds: [String]!, $limit: Int!) {
user(
where: {
userId: {
_nin: $exceptUserIds,
},
},
limit: $limit,
order_by: {
nameSortable: asc,
userId: asc,
},
) {
name
userId
nameSortable
pinned
away
disconnected
emoji
role
avatar
color
presenter
clientType
raiseHand
isModerator
reactionEmoji
}
}
}
`;
export default {
getVideoData,
getVideoDataGrid,
OWN_VIDEO_STREAMS_QUERY,
VIDEO_STREAMS_SUBSCRIPTION,
VIEWERS_IN_WEBCAM_COUNT_SUBSCRIPTION,
GRID_USERS_SUBSCRIPTION,
};

View File

@ -1,140 +0,0 @@
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) => {
if (s1.pin) {
return -1;
} if (s2.pin) {
return 1;
}
return 0;
};
export const mandatorySorting = (s1, s2) => sortPin(s1, s2);
// lastFloorTime, descending
export const sortVoiceActivity = (s1, s2) => {
if (s2.lastFloorTime < s1.lastFloorTime) {
return -1;
} else if (s2.lastFloorTime > s1.lastFloorTime) {
return 1;
} else return 0;
};
// pin -> lastFloorTime (descending) -> alphabetical -> local
export const sortVoiceActivityLocal = (s1, s2) => {
if (s1.userId === Auth.userID) {
return 1;
} if (s2.userId === Auth.userID) {
return -1;
}
return mandatorySorting(s1, s2)
|| sortVoiceActivity(s1, s2)
|| UserListService.sortUsersByName(s1, s2);
};
// pin -> local -> lastFloorTime (descending) -> alphabetical
export const sortLocalVoiceActivity = (s1, s2) => 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)
|| UserListService.sortUsersByCurrent(s1, s2)
|| UserListService.sortUsersByName(s1, s2);
export const sortPresenter = (s1, s2) => {
if (UserListService.isUserPresenter(s1.userId)) {
return -1;
} else if (UserListService.isUserPresenter(s2.userId)) {
return 1;
} else return 0;
};
// pin -> local -> presenter -> alphabetical
export const sortLocalPresenterAlphabetical = (s1, s2) => mandatorySorting(s1, s2)
|| UserListService.sortUsersByCurrent(s1, s2)
|| sortPresenter(s1, s2)
|| UserListService.sortUsersByName(s1, s2);
// SORTING_METHODS: registrar of configurable video stream sorting modes
// Keys are the method name (String) which are to be configured in settings.yml
// ${streamSortingMethod} flag.
//
// Values are a objects which describe the sorting mode:
// - sortingMethod (function): a sorting function defined in this module
// - neededData (Object): data members that will be fetched from the server's
// video-streams collection
// - filter (Boolean): whether the sorted stream list has to be post processed
// to remove uneeded attributes. The needed attributes are: userId, streams
// and name. Anything other than that is superfluous.
// - localFirst (Boolean): true pushes local streams to the beginning of the list,
// false to the end
// The reason why this flags exists is due to pagination: local streams are
// stripped out of the streams list prior to sorting+partiotioning. They're
// added (pushed) afterwards. To avoid re-sorting the page, this flag indicates
// where it should go.
//
// To add a new sorting flavor:
// 1 - implement a sorting function, add it here (like eg sortPresenterAlphabetical)
// 1.1.: the sorting function has the same behaviour as a regular .sort callback
// 2 - add an entry to SORTING_METHODS, the key being the name to be used
// in settings.yml and the value object like the aforementioned
const MANDATORY_DATA_TYPES = {
userId: 1, stream: 1, name: 1, sortName: 1, deviceId: 1, floor: 1, pin: 1,
};
const SORTING_METHODS = Object.freeze({
// Default
LOCAL_ALPHABETICAL: {
sortingMethod: sortLocalAlphabetical,
neededDataTypes: MANDATORY_DATA_TYPES,
localFirst: true,
},
VOICE_ACTIVITY_LOCAL: {
sortingMethod: sortVoiceActivityLocal,
neededDataTypes: {
lastFloorTime: 1, floor: 1, ...MANDATORY_DATA_TYPES,
},
filter: true,
localFirst: false,
},
LOCAL_VOICE_ACTIVITY: {
sortingMethod: sortLocalVoiceActivity,
neededDataTypes: {
lastFloorTime: 1, floor: 1, ...MANDATORY_DATA_TYPES,
},
filter: true,
localFirst: true,
},
LOCAL_PRESENTER_ALPHABETICAL: {
sortingMethod: sortLocalPresenterAlphabetical,
neededDataTypes: MANDATORY_DATA_TYPES,
localFirst: true,
}
});
export const getSortingMethod = (identifier) => {
return SORTING_METHODS[identifier] || SORTING_METHODS[DEFAULT_SORTING_MODE];
};
export const sortVideoStreams = (streams, mode) => {
const { sortingMethod, filter } = 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,
}));
};

View File

@ -1,274 +0,0 @@
import React, { memo, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
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 deviceInfo from '/imports/utils/deviceInfo';
import { debounce } from '/imports/utils/debounce';
import BBBMenu from '/imports/ui/components/common/menu/component';
import { isVirtualBackgroundsEnabled } from '/imports/ui/services/features';
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 { getSettingsSingletonInstance } from '/imports/ui/services/settings';
const intlMessages = defineMessages({
videoSettings: {
id: 'app.video.videoSettings',
description: 'Open video settings',
},
visualEffects: {
id: 'app.video.visualEffects',
description: 'Visual effects label',
},
joinVideo: {
id: 'app.video.joinVideo',
description: 'Join video button label',
},
leaveVideo: {
id: 'app.video.leaveVideo',
description: 'Leave video button label',
},
advancedVideo: {
id: 'app.video.advancedVideo',
description: 'Open advanced video label',
},
videoLocked: {
id: 'app.video.videoLocked',
description: 'video disabled label',
},
videoConnecting: {
id: 'app.video.connecting',
description: 'video connecting label',
},
camCapReached: {
id: 'app.video.meetingCamCapReached',
description: 'meeting camera cap label',
},
meteorDisconnected: {
id: 'app.video.clientDisconnected',
description: 'Meteor disconnected label',
},
});
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,
};
const JoinVideoButton = ({
intl,
hasVideoStream,
status,
disableReason,
updateSettings,
cameraSettingsDropdownItems,
sendUserUnshareWebcam,
setLocalSettings,
away,
}) => {
const ENABLE_WEBCAM_SELECTOR_BUTTON = window.meetingClientSettings.public.app.enableWebcamSelectorButton;
const ENABLE_CAMERA_BRIGHTNESS = window.meetingClientSettings.public.app.enableCameraBrightness;
const { isMobile } = deviceInfo;
const isMobileSharingCamera = hasVideoStream && isMobile;
const isDesktopSharingCamera = hasVideoStream && !isMobile;
const shouldEnableWebcamSelectorButton = ENABLE_WEBCAM_SELECTOR_BUTTON
&& isDesktopSharingCamera;
const shouldEnableWebcamVisualEffectsButton = (isVirtualBackgroundsEnabled()
|| ENABLE_CAMERA_BRIGHTNESS)
&& hasVideoStream
&& !isMobile;
const exitVideo = () => isDesktopSharingCamera && (!VideoService.isMultipleCamerasEnabled()
|| shouldEnableWebcamSelectorButton);
const [propsToPassModal, setPropsToPassModal] = useState({});
const [forceOpen, setForceOpen] = useState(false);
const [isVideoPreviewModalOpen, setVideoPreviewModalIsOpen] = useState(false);
const [wasSelfViewDisabled, setWasSelfViewDisabled] = useState(false);
const Settings = getSettingsSingletonInstance();
useEffect(() => {
const isSelfViewDisabled = Settings.application.selfViewDisable;
if (isVideoPreviewModalOpen && isSelfViewDisabled) {
setWasSelfViewDisabled(true);
const obj = {
application:
{ ...Settings.application, selfViewDisable: false },
};
updateSettings(obj, null, setLocalSettings);
}
}, [isVideoPreviewModalOpen]);
const handleOnClick = debounce(() => {
switch (status) {
case 'videoConnecting':
VideoService.stopVideo(undefined, sendUserUnshareWebcam);
break;
case 'connected':
default:
if (exitVideo()) {
VideoService.exitVideo(sendUserUnshareWebcam);
} else {
setForceOpen(isMobileSharingCamera);
setVideoPreviewModalIsOpen(true);
}
}
}, JOIN_VIDEO_DELAY_MILLISECONDS);
const handleOpenAdvancedOptions = (callback) => {
if (callback) callback();
setForceOpen(isDesktopSharingCamera);
setVideoPreviewModalIsOpen(true);
};
const getMessageFromStatus = () => {
let statusMessage = status;
if (status !== 'videoConnecting') {
statusMessage = exitVideo() ? 'leaveVideo' : 'joinVideo';
}
return statusMessage;
};
const label = disableReason
? intl.formatMessage(intlMessages[disableReason])
: intl.formatMessage(intlMessages[getMessageFromStatus()]);
const isSharing = hasVideoStream || status === 'videoConnecting';
const renderUserActions = () => {
const actions = [];
if (shouldEnableWebcamSelectorButton) {
actions.push(
{
key: 'advancedVideo',
label: intl.formatMessage(intlMessages.advancedVideo),
onClick: () => handleOpenAdvancedOptions(),
dataTest: 'advancedVideoSettingsButton',
},
);
}
if (shouldEnableWebcamVisualEffectsButton) {
actions.push(
{
key: 'virtualBgSelection',
label: intl.formatMessage(intlMessages.visualEffects),
onClick: () => handleOpenAdvancedOptions((
) => setPropsToPassModal({ isVisualEffects: true })),
},
);
}
if (actions.length === 0 || away) return null;
const customStyles = { top: '-3.6rem' };
cameraSettingsDropdownItems.forEach((plugin) => {
switch (plugin.type) {
case CameraSettingsDropdownItemType.OPTION:
actions.push({
key: plugin.id,
label: plugin.label,
onClick: plugin.onClick,
icon: plugin.icon,
});
break;
case CameraSettingsDropdownItemType.SEPARATOR:
actions.push({
key: plugin.id,
isSeparator: true,
});
break;
default:
break;
}
});
return (
<BBBMenu
customStyles={!isMobile ? customStyles : null}
trigger={(
<ButtonEmoji
emoji="device_list_selector"
data-test="videoDropdownMenu"
hideLabel
label={intl.formatMessage(intlMessages.videoSettings)}
rotate
tabIndex={0}
/>
)}
actions={actions}
opts={{
id: 'video-dropdown-menu',
keepMounted: true,
transitionDuration: 0,
elevation: 3,
getcontentanchorel: null,
fullwidth: 'true',
anchorOrigin: { vertical: 'top', horizontal: 'center' },
transformOrigin: { vertical: 'top', horizontal: 'center' },
}}
/>
);
};
return (
<>
<Styled.OffsetBottom>
<Button
label={label}
data-test={hasVideoStream ? 'leaveVideo' : 'joinVideo'}
onClick={handleOnClick}
hideLabel
color={isSharing ? 'primary' : 'default'}
icon={isSharing ? 'video' : 'video_off'}
ghost={!isSharing}
size="lg"
circle
disabled={!!disableReason}
/>
{renderUserActions()}
</Styled.OffsetBottom>
{isVideoPreviewModalOpen ? (
<VideoPreviewContainer
{...{
callbackToClose: () => {
if (wasSelfViewDisabled) {
setTimeout(() => {
const obj = {
application:
{ ...Settings.application, selfViewDisable: true },
};
updateSettings(obj, null, setLocalSettings);
setWasSelfViewDisabled(false);
}, 100);
}
setPropsToPassModal({});
setForceOpen(false);
},
forceOpen,
priority: 'low',
setIsOpen: setVideoPreviewModalIsOpen,
isOpen: isVideoPreviewModalOpen,
}}
{...propsToPassModal}
/>
) : null}
</>
);
};
JoinVideoButton.propTypes = propTypes;
export default injectIntl(memo(JoinVideoButton));

View File

@ -1,70 +0,0 @@
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 { 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 useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
const JoinVideoOptionsContainer = (props) => {
const {
updateSettings,
hasVideoStream,
disableReason,
status,
intl,
...restProps
} = props;
const [cameraBroadcastStop] = useMutation(CAMERA_BROADCAST_STOP);
const setLocalSettings = useUserChangedLocalSettings();
const sendUserUnshareWebcam = (cameraId) => {
cameraBroadcastStop({ variables: { cameraId } });
};
const {
pluginsExtensibleAreasAggregatedState,
} = useContext(PluginsContext);
let cameraSettingsDropdownItems = [];
if (pluginsExtensibleAreasAggregatedState.cameraSettingsDropdownItems) {
cameraSettingsDropdownItems = [
...pluginsExtensibleAreasAggregatedState.cameraSettingsDropdownItems,
];
}
const { data: currentUserData } = useCurrentUser((user) => ({
away: user.away,
}));
const away = currentUserData?.away;
return (
<JoinVideoButton {...{
cameraSettingsDropdownItems,
hasVideoStream,
updateSettings,
disableReason,
status,
sendUserUnshareWebcam,
setLocalSettings,
away,
...restProps,
}}
/>
);
};
export default injectIntl(withTracker(() => ({
hasVideoStream: VideoService.hasVideoStream(),
updateSettings,
disableReason: VideoService.disableReason(),
status: VideoService.getStatus(),
}))(JoinVideoOptionsContainer));

View File

@ -1,9 +0,0 @@
import styled from 'styled-components';
const OffsetBottom = styled.div`
position: relative;
`;
export default {
OffsetBottom,
};

View File

@ -1,400 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import { throttle } from '/imports/utils/throttle';
import { range } from '/imports/utils/array-utils';
import Styled from './styles';
import VideoListItemContainer from './video-list-item/container';
import AutoplayOverlay from '../../media/autoplay-overlay/component';
import logger from '/imports/startup/client/logger';
import playAndRetry from '/imports/utils/mediaElementPlayRetry';
import VideoService from '/imports/ui/components/video-provider/service';
import { ACTIONS } from '../../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,
};
const intlMessages = defineMessages({
autoplayBlockedDesc: {
id: 'app.videoDock.autoplayBlockedDesc',
},
autoplayAllowLabel: {
id: 'app.videoDock.autoplayAllowLabel',
},
nextPageLabel: {
id: 'app.video.pagination.nextPage',
},
prevPageLabel: {
id: 'app.video.pagination.prevPage',
},
});
const findOptimalGrid = (canvasWidth, canvasHeight, gutter, aspectRatio, numItems, columns = 1) => {
const rows = Math.ceil(numItems / columns);
const gutterTotalWidth = (columns - 1) * gutter;
const gutterTotalHeight = (rows - 1) * gutter;
const usableWidth = canvasWidth - gutterTotalWidth;
const usableHeight = canvasHeight - gutterTotalHeight;
let cellWidth = Math.floor(usableWidth / columns);
let cellHeight = Math.ceil(cellWidth / aspectRatio);
if ((cellHeight * rows) > usableHeight) {
cellHeight = Math.floor(usableHeight / rows);
cellWidth = Math.ceil(cellHeight * aspectRatio);
}
return {
columns,
rows,
width: (cellWidth * columns) + gutterTotalWidth,
height: (cellHeight * rows) + gutterTotalHeight,
filledArea: (cellWidth * cellHeight) * numItems,
};
};
const ASPECT_RATIO = 4 / 3;
// const ACTION_NAME_BACKGROUND = 'blurBackground';
class VideoList extends Component {
constructor(props) {
super(props);
this.state = {
optimalGrid: {
cols: 1,
rows: 1,
filledArea: 0,
},
autoplayBlocked: false,
};
this.ticking = false;
this.grid = null;
this.canvas = null;
this.failedMediaElements = [];
this.handleCanvasResize = throttle(this.handleCanvasResize.bind(this), 66,
{
leading: true,
trailing: true,
});
this.setOptimalGrid = this.setOptimalGrid.bind(this);
this.handleAllowAutoplay = this.handleAllowAutoplay.bind(this);
this.handlePlayElementFailed = this.handlePlayElementFailed.bind(this);
this.autoplayWasHandled = false;
}
componentDidMount() {
this.handleCanvasResize();
window.addEventListener('resize', this.handleCanvasResize, false);
window.addEventListener('videoPlayFailed', this.handlePlayElementFailed);
}
componentDidUpdate(prevProps) {
const { layoutType, cameraDock, streams, focusedId } = this.props;
const { width: cameraDockWidth, height: cameraDockHeight } = cameraDock;
const {
layoutType: prevLayoutType,
cameraDock: prevCameraDock,
streams: prevStreams,
focusedId: prevFocusedId,
} = prevProps;
const { width: prevCameraDockWidth, height: prevCameraDockHeight } = prevCameraDock;
if (layoutType !== prevLayoutType
|| focusedId !== prevFocusedId
|| cameraDockWidth !== prevCameraDockWidth
|| cameraDockHeight !== prevCameraDockHeight
|| streams.length !== prevStreams.length) {
this.handleCanvasResize();
}
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleCanvasResize, false);
window.removeEventListener('videoPlayFailed', this.handlePlayElementFailed);
}
handleAllowAutoplay() {
const { autoplayBlocked } = this.state;
logger.info({
logCode: 'video_provider_autoplay_allowed',
}, 'Video media autoplay allowed by the user');
this.autoplayWasHandled = true;
window.removeEventListener('videoPlayFailed', this.handlePlayElementFailed);
while (this.failedMediaElements.length) {
const mediaElement = this.failedMediaElements.shift();
if (mediaElement) {
const played = playAndRetry(mediaElement);
if (!played) {
logger.error({
logCode: 'video_provider_autoplay_handling_failed',
}, 'Video autoplay handling failed to play media');
} else {
logger.info({
logCode: 'video_provider_media_play_success',
}, 'Video media played successfully');
}
}
}
if (autoplayBlocked) { this.setState({ autoplayBlocked: false }); }
}
handlePlayElementFailed(e) {
const { mediaElement } = e.detail;
const { autoplayBlocked } = this.state;
e.stopPropagation();
this.failedMediaElements.push(mediaElement);
if (!autoplayBlocked && !this.autoplayWasHandled) {
logger.info({
logCode: 'video_provider_autoplay_prompt',
}, 'Prompting user for action to play video media');
this.setState({ autoplayBlocked: true });
}
}
handleCanvasResize() {
if (!this.ticking) {
window.requestAnimationFrame(() => {
this.ticking = false;
this.setOptimalGrid();
});
}
this.ticking = true;
}
setOptimalGrid() {
const {
streams,
cameraDock,
layoutContextDispatch,
} = this.props;
let numItems = streams.length;
if (numItems < 1 || !this.canvas || !this.grid) {
return;
}
const { focusedId } = this.props;
const canvasWidth = cameraDock?.width;
const canvasHeight = cameraDock?.height;
const gridGutter = parseInt(window.getComputedStyle(this.grid)
.getPropertyValue('grid-row-gap'), 10);
const hasFocusedItem = streams.filter(s => s.stream === focusedId).length && numItems > 2;
// Has a focused item so we need +3 cells
if (hasFocusedItem) {
numItems += 3;
}
const optimalGrid = range(1, numItems + 1)
.reduce((currentGrid, col) => {
const testGrid = findOptimalGrid(
canvasWidth, canvasHeight, gridGutter,
ASPECT_RATIO, numItems, col,
);
// We need a minimum of 2 rows and columns for the focused
const focusedConstraint = hasFocusedItem ? testGrid.rows > 1 && testGrid.columns > 1 : true;
const betterThanCurrent = testGrid.filledArea > currentGrid.filledArea;
return focusedConstraint && betterThanCurrent ? testGrid : currentGrid;
}, { filledArea: 0 });
layoutContextDispatch({
type: ACTIONS.SET_CAMERA_DOCK_OPTIMAL_GRID_SIZE,
value: {
width: optimalGrid.width,
height: optimalGrid.height,
},
});
this.setState({
optimalGrid,
});
}
displayPageButtons() {
const { numberOfPages, cameraDock } = this.props;
const { width: cameraDockWidth } = cameraDock;
if (!VideoService.isPaginationEnabled() || numberOfPages <= 1 || cameraDockWidth === 0) {
return false;
}
return true;
}
renderNextPageButton() {
const {
intl,
numberOfPages,
currentVideoPageIndex,
cameraDock,
} = this.props;
const { position } = cameraDock;
if (!this.displayPageButtons()) return null;
const currentPage = currentVideoPageIndex + 1;
const nextPageLabel = intl.formatMessage(intlMessages.nextPageLabel);
const nextPageDetailedLabel = `${nextPageLabel} (${currentPage}/${numberOfPages})`;
return (
<Styled.NextPageButton
role="button"
aria-label={nextPageLabel}
color="primary"
icon="right_arrow"
size="md"
onClick={VideoService.getNextVideoPage}
label={nextPageDetailedLabel}
hideLabel
position={position}
/>
);
}
renderPreviousPageButton() {
const {
intl,
currentVideoPageIndex,
numberOfPages,
cameraDock,
} = this.props;
const { position } = cameraDock;
if (!this.displayPageButtons()) return null;
const currentPage = currentVideoPageIndex + 1;
const prevPageLabel = intl.formatMessage(intlMessages.prevPageLabel);
const prevPageDetailedLabel = `${prevPageLabel} (${currentPage}/${numberOfPages})`;
return (
<Styled.PreviousPageButton
role="button"
aria-label={prevPageLabel}
color="primary"
icon="left_arrow"
size="md"
onClick={VideoService.getPreviousVideoPage}
label={prevPageDetailedLabel}
hideLabel
position={position}
/>
);
}
renderVideoList() {
const {
streams,
onVirtualBgDrop,
onVideoItemMount,
onVideoItemUnmount,
swapLayout,
handleVideoFocus,
focusedId,
users,
} = this.props;
const numOfStreams = streams.length;
let videoItems = streams;
return videoItems.map((item) => {
const { userId, name } = item;
const isStream = !!item.stream;
const stream = isStream ? item.stream : null;
const key = isStream ? stream : userId;
const isFocused = isStream && focusedId === stream && numOfStreams > 2;
return (
<Styled.VideoListItem
key={key}
focused={isFocused}
data-test="webcamVideoItem"
>
<VideoListItemContainer
users={users}
numOfStreams={numOfStreams}
cameraId={stream}
userId={userId}
name={name}
focused={isFocused}
isStream={isStream}
onHandleVideoFocus={isStream ? handleVideoFocus : null}
onVideoItemMount={(videoRef) => {
this.handleCanvasResize();
if (isStream) onVideoItemMount(stream, videoRef);
}}
stream={streams.find((s) => s.userId === userId) || {}}
onVideoItemUnmount={onVideoItemUnmount}
swapLayout={swapLayout}
onVirtualBgDrop={(type, name, data) => { return isStream ? onVirtualBgDrop(stream, type, name, data) : null; }}
/>
</Styled.VideoListItem>
);
});
}
render() {
const {
streams,
intl,
cameraDock,
isGridEnabled,
} = this.props;
const { optimalGrid, autoplayBlocked } = this.state;
const { position } = cameraDock;
return (
<Styled.VideoCanvas
position={position}
ref={(ref) => {
this.canvas = ref;
}}
style={{
minHeight: 'inherit',
}}
>
{this.renderPreviousPageButton()}
{!streams.length && !isGridEnabled ? null : (
<Styled.VideoList
ref={(ref) => {
this.grid = ref;
}}
style={{
width: `${optimalGrid.width}px`,
height: `${optimalGrid.height}px`,
gridTemplateColumns: `repeat(${optimalGrid.columns}, 1fr)`,
gridTemplateRows: `repeat(${optimalGrid.rows}, 1fr)`,
}}
>
{this.renderVideoList()}
</Styled.VideoList>
)}
{!autoplayBlocked ? null : (
<AutoplayOverlay
autoplayBlockedDesc={intl.formatMessage(intlMessages.autoplayBlockedDesc)}
autoplayAllowLabel={intl.formatMessage(intlMessages.autoplayAllowLabel)}
handleAllowAutoplay={this.handleAllowAutoplay}
/>
)}
{
(position === 'contentRight' || position === 'contentLeft')
&& <Styled.Break />
}
{this.renderNextPageButton()}
</Styled.VideoCanvas>
);
}
}
VideoList.propTypes = propTypes;
export default injectIntl(VideoList);

View File

@ -1,39 +0,0 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import VideoList from '/imports/ui/components/video-provider/video-list/component';
import VideoService from '/imports/ui/components/video-provider/service';
import { layoutSelect, layoutSelectOutput, layoutDispatch } from '../../layout/context';
import Users from '/imports/api/users';
const VideoListContainer = ({ children, ...props }) => {
const layoutType = layoutSelect((i) => i.layoutType);
const cameraDock = layoutSelectOutput((i) => i.cameraDock);
const layoutContextDispatch = layoutDispatch();
const { streams } = props;
return (
!streams.length
? null
: (
<VideoList {...{
layoutType,
cameraDock,
layoutContextDispatch,
...props,
}}
>
{children}
</VideoList>
)
);
};
export default withTracker((props) => {
const { streams } = props;
return {
...props,
numberOfPages: VideoService.getNumberOfPages(),
streams,
};
})(VideoListContainer);

View File

@ -1,118 +0,0 @@
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 Button from '/imports/ui/components/common/button/component';
const NextPageButton = styled(Button)`
color: ${colorWhite};
width: ${mdPaddingX};
& > i {
[dir="rtl"] & {
-webkit-transform: scale(-1, 1);
-moz-transform: scale(-1, 1);
-ms-transform: scale(-1, 1);
-o-transform: scale(-1, 1);
transform: scale(-1, 1);
}
}
margin-left: 1px;
@media ${mediumUp} {
margin-left: 2px;
}
${({ position }) => (position === 'contentRight' || position === 'contentLeft') && `
order: 3;
margin-right: 2px;
`}
`;
const PreviousPageButton = styled(Button)`
color: ${colorWhite};
width: ${mdPaddingX};
i {
[dir="rtl"] & {
-webkit-transform: scale(-1, 1);
-moz-transform: scale(-1, 1);
-ms-transform: scale(-1, 1);
-o-transform: scale(-1, 1);
transform: scale(-1, 1);
}
}
margin-right: 1px;
@media ${mediumUp} {
margin-right: 2px;
}
${({ position }) => (position === 'contentRight' || position === 'contentLeft') && `
order: 2;
margin-left: 2px;
`}
`;
const VideoListItem = styled.div`
display: flex;
overflow: hidden;
width: 100%;
max-height: 100%;
${({ focused }) => focused && `
grid-column: 1 / span 2;
grid-row: 1 / span 2;
`}
`;
const VideoCanvas = styled.div`
position: absolute;
width: 100%;
min-height: calc((100vh - calc(${navbarHeight} + ${actionsBarHeight})) * 0.2);
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
${({ position }) => (position === 'contentRight' || position === 'contentLeft') && `
flex-wrap: wrap;
align-content: center;
order: 0;
`}
`;
const VideoList = styled.div`
display: grid;
grid-auto-flow: dense;
grid-gap: 1px;
justify-content: center;
@media ${mediumUp} {
grid-gap: 2px;
}
`;
const Break = styled.div`
order: 1;
flex-basis: 100%;
height: 5px;
`;
export default {
NextPageButton,
PreviousPageButton,
VideoListItem,
VideoCanvas,
VideoList,
Break,
};

View File

@ -1,323 +0,0 @@
/* eslint-disable no-nested-ternary */
import React, { useEffect, useRef, useState } from 'react';
import { injectIntl, defineMessages, useIntl } from 'react-intl';
import PropTypes from 'prop-types';
import UserActions from '/imports/ui/components/video-provider/video-list/video-list-item/user-actions/component';
import UserStatus from '/imports/ui/components/video-provider/video-list/video-list-item/user-status/component';
import PinArea from '/imports/ui/components/video-provider/video-list/video-list-item/pin-area/component';
import UserAvatarVideo from '/imports/ui/components/video-provider/video-list/video-list-item/user-avatar/component';
import ViewActions from '/imports/ui/components/video-provider/video-list/video-list-item/view-actions/component';
import {
isStreamStateUnhealthy,
subscribeToStreamStateChange,
unsubscribeFromStreamStateChange,
} from '/imports/ui/services/bbb-webrtc-sfu/stream-state-service';
import { getSettingsSingletonInstance } from '/imports/ui/services/settings';
import VideoService from '/imports/ui/components/video-provider/service';
import Styled from './styles';
import { withDragAndDrop } from './drag-and-drop/component';
import Auth from '/imports/ui/services/auth';
import Session from '/imports/ui/services/storage/in-memory';
const intlMessages = defineMessages({
disableDesc: {
id: 'app.videoDock.webcamDisableDesc',
},
});
const VIDEO_CONTAINER_WIDTH_BOUND = 125;
const VideoListItem = ({
name,
voiceUser,
isFullscreenContext,
layoutContextDispatch,
user,
onHandleVideoFocus,
cameraId,
numOfStreams = 0,
focused,
onVideoItemMount = () => {},
onVideoItemUnmount = () => {},
makeDragOperations,
dragging,
draggingOver,
isRTL,
isStream,
settingsSelfViewDisable,
disabledCams,
amIModerator,
stream,
}) => {
const intl = useIntl();
const [videoDataLoaded, setVideoDataLoaded] = useState(false);
const [isStreamHealthy, setIsStreamHealthy] = useState(false);
const [isMirrored, setIsMirrored] = useState(VideoService.mirrorOwnWebcam(user?.userId));
const [isVideoSqueezed, setIsVideoSqueezed] = useState(false);
const [isSelfViewDisabled, setIsSelfViewDisabled] = useState(false);
const resizeObserver = new ResizeObserver((entry) => {
if (entry && entry[0]?.contentRect?.width < VIDEO_CONTAINER_WIDTH_BOUND) {
return setIsVideoSqueezed(true);
}
return setIsVideoSqueezed(false);
});
const videoTag = useRef();
const videoContainer = useRef();
const videoIsReady = isStreamHealthy && videoDataLoaded && !isSelfViewDisabled;
const Settings = getSettingsSingletonInstance();
const { animations, webcamBorderHighlightColor } = Settings.application;
const talking = voiceUser?.talking;
const onStreamStateChange = (e) => {
const { streamState } = e.detail;
const newHealthState = !isStreamStateUnhealthy(streamState);
e.stopPropagation();
setIsStreamHealthy(newHealthState);
};
const onLoadedData = () => {
setVideoDataLoaded(true);
window.dispatchEvent(new Event('resize'));
/* used when re-sharing cameras after leaving a breakout room.
it is needed in cases where the user has more than one active camera
so we only share the second camera after the first
has finished loading (can't share more than one at the same time) */
Session.setItem('canConnect', true);
};
// component did mount
useEffect(() => {
subscribeToStreamStateChange(cameraId, onStreamStateChange);
onVideoItemMount(videoTag.current);
resizeObserver.observe(videoContainer.current);
videoTag?.current?.addEventListener('loadeddata', onLoadedData);
return () => {
videoTag?.current?.removeEventListener('loadeddata', onLoadedData);
resizeObserver.disconnect();
};
}, []);
// component will mount
useEffect(() => {
const playElement = (elem) => {
if (elem.paused) {
elem.play().catch((error) => {
// NotAllowedError equals autoplay issues, fire autoplay handling event
if (error.name === 'NotAllowedError') {
const tagFailedEvent = new CustomEvent('videoPlayFailed', { detail: { mediaElement: elem } });
window.dispatchEvent(tagFailedEvent);
}
});
}
};
if (!isSelfViewDisabled && videoDataLoaded) {
playElement(videoTag.current);
}
if ((isSelfViewDisabled && user.userId === Auth.userID) || disabledCams?.includes(cameraId)) {
videoTag.current.pause();
}
}, [isSelfViewDisabled, videoDataLoaded]);
// component will unmount
useEffect(() => () => {
unsubscribeFromStreamStateChange(cameraId, onStreamStateChange);
onVideoItemUnmount(cameraId);
}, []);
useEffect(() => {
setIsSelfViewDisabled(settingsSelfViewDisable);
}, [settingsSelfViewDisable]);
const renderSqueezedButton = () => (
<UserActions
name={name}
user={{ ...user, ...stream }}
videoContainer={videoContainer}
isVideoSqueezed={isVideoSqueezed}
cameraId={cameraId}
numOfStreams={numOfStreams}
onHandleVideoFocus={onHandleVideoFocus}
focused={focused}
onHandleMirror={() => setIsMirrored((value) => !value)}
isMirrored={isMirrored}
isRTL={isRTL}
isStream={isStream}
onHandleDisableCam={() => setIsSelfViewDisabled((value) => !value)}
isSelfViewDisabled={isSelfViewDisabled}
amIModerator={amIModerator}
/>
);
const renderWebcamConnecting = () => (
<Styled.WebcamConnecting
data-test="webcamConnecting"
animations={animations}
>
<UserAvatarVideo
user={{ ...user, ...stream }}
voiceUser={voiceUser}
unhealthyStream={videoDataLoaded && !isStreamHealthy}
squeezed={false}
/>
<Styled.BottomBar>
<UserActions
name={name}
user={{ ...user, ...stream }}
cameraId={cameraId}
numOfStreams={numOfStreams}
onHandleVideoFocus={onHandleVideoFocus}
focused={focused}
onHandleMirror={() => setIsMirrored((value) => !value)}
isMirrored={isMirrored}
isRTL={isRTL}
isStream={isStream}
onHandleDisableCam={() => setIsSelfViewDisabled((value) => !value)}
isSelfViewDisabled={isSelfViewDisabled}
amIModerator={amIModerator}
/>
<UserStatus
voiceUser={voiceUser}
user={{ ...user, ...stream }}
/>
</Styled.BottomBar>
</Styled.WebcamConnecting>
);
const renderWebcamConnectingSqueezed = () => (
<Styled.WebcamConnecting
data-test="webcamConnectingSqueezed"
animations={animations}
>
<UserAvatarVideo
user={{ ...user, ...stream }}
unhealthyStream={videoDataLoaded && !isStreamHealthy}
squeezed
/>
{renderSqueezedButton()}
</Styled.WebcamConnecting>
);
const renderDefaultButtons = () => (
<>
<Styled.TopBar>
<PinArea
user={{ ...user, ...stream }}
amIModerator={amIModerator}
/>
<ViewActions
videoContainer={videoContainer}
name={name}
cameraId={cameraId}
isFullscreenContext={isFullscreenContext}
layoutContextDispatch={layoutContextDispatch}
isStream={isStream}
/>
</Styled.TopBar>
<Styled.BottomBar>
<UserActions
name={name}
user={{ ...user, ...stream }}
cameraId={cameraId}
numOfStreams={numOfStreams}
onHandleVideoFocus={onHandleVideoFocus}
focused={focused}
onHandleMirror={() => setIsMirrored((value) => !value)}
isMirrored={isMirrored}
isRTL={isRTL}
isStream={isStream}
onHandleDisableCam={() => setIsSelfViewDisabled((value) => !value)}
isSelfViewDisabled={isSelfViewDisabled}
amIModerator={amIModerator}
/>
<UserStatus
voiceUser={voiceUser}
user={{ ...user, ...stream }}
/>
</Styled.BottomBar>
</>
);
return (
<Styled.Content
ref={videoContainer}
talking={talking}
customHighlight={webcamBorderHighlightColor}
fullscreen={isFullscreenContext}
data-test={talking ? 'webcamItemTalkingUser' : 'webcamItem'}
animations={animations}
isStream={isStream}
{...{
...makeDragOperations(user?.userId),
dragging,
draggingOver,
}}
>
<Styled.VideoContainer
$selfViewDisabled={(isSelfViewDisabled && user.userId === Auth.userID)
|| disabledCams.includes(cameraId)}
>
<Styled.Video
mirrored={isMirrored}
unhealthyStream={videoDataLoaded && !isStreamHealthy}
data-test={isMirrored ? 'mirroredVideoContainer' : 'videoContainer'}
ref={videoTag}
muted="muted"
autoPlay
playsInline
/>
</Styled.VideoContainer>
{isStream && ((isSelfViewDisabled && user.userId === Auth.userID)
|| disabledCams.includes(cameraId)) && (
<Styled.VideoDisabled>
{intl.formatMessage(intlMessages.disableDesc)}
</Styled.VideoDisabled>
)}
{/* eslint-disable-next-line no-nested-ternary */}
{(videoIsReady || (isSelfViewDisabled || disabledCams.includes(cameraId))) && (
isVideoSqueezed ? renderSqueezedButton() : renderDefaultButtons()
)}
{!videoIsReady && (!isSelfViewDisabled || !isStream) && (
isVideoSqueezed ? renderWebcamConnectingSqueezed() : renderWebcamConnecting()
)}
{((isSelfViewDisabled && user.userId === Auth.userID) || disabledCams.includes(cameraId))
&& renderWebcamConnecting()}
</Styled.Content>
);
};
export default withDragAndDrop(injectIntl(VideoListItem));
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,
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,
};

View File

@ -1,66 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { withTracker } from 'meteor/react-meteor-data';
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 { getSettingsSingletonInstance } from '/imports/ui/services/settings';
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
const VideoListItemContainer = (props) => {
const { cameraId, user } = props;
const fullscreen = layoutSelect((i) => i.fullscreen);
const { element } = fullscreen;
const isFullscreenContext = (element === cameraId);
const layoutContextDispatch = layoutDispatch();
const isRTL = layoutSelect((i) => i.isRTL);
const { data: currentUserData } = useCurrentUser((user) => ({
isModerator: user.isModerator,
}));
const amIModerator = currentUserData?.isModerator;
if (!user) return null;
return (
<VideoListItem
{...props}
{...{
isFullscreenContext,
layoutContextDispatch,
isRTL,
amIModerator,
}}
/>
);
};
export default withTracker((props) => {
const {
userId,
users,
stream,
} = props;
const Settings = getSettingsSingletonInstance();
return {
settingsSelfViewDisable: Settings.application.selfViewDisable,
voiceUser: VoiceUsers.findOne({ userId },
{
fields: {
muted: 1, listenOnly: 1, talking: 1, joined: 1,
},
}),
user: (users?.find((u) => u.userId === userId) || {}),
disabledCams: Session.get('disabledCams') || [],
stream,
};
})(VideoListItemContainer);
VideoListItemContainer.propTypes = {
cameraId: PropTypes.string.isRequired,
};

View File

@ -1,174 +0,0 @@
import React, { useContext, useEffect, useState, useCallback } from 'react';
import { injectIntl, defineMessages } 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';
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 '/imports/ui/services/storage/in-memory';
const { MIME_TYPES_ALLOWED, MAX_FILE_SIZE } = VirtualBgService;
const intlMessages = defineMessages({
confirmationTitle: {
id: 'app.confirmation.virtualBackground.title',
description: 'Confirmation modal title',
},
confirmationDescription: {
id: 'app.confirmation.virtualBackground.description',
description: 'Confirmation modal description',
},
});
const DragAndDrop = (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 { dispatch: dispatchCustomBackground } = useContext(CustomVirtualBackgroundsContext);
const ENABLE_WEBCAM_BACKGROUND_UPLOAD = window.meetingClientSettings.public.virtualBackgrounds.enableVirtualBackgroundUpload;
let callback;
const resetEvent = (e) => {
e.preventDefault();
e.stopPropagation();
}
useEffect(() => {
const onDragOver = (e) => {
resetEvent(e);
setDragging(true);
};
const onDragLeave = (e) => {
resetEvent(e);
setDragging(false);
};
const onDrop = (e) => {
resetEvent(e);
setDragging(false);
};
window.addEventListener('dragover', onDragOver);
window.addEventListener('dragleave', onDragLeave);
window.addEventListener('drop', onDrop);
return () => {
window.removeEventListener('dragover', onDragOver);
window.removeEventListener('dragleave', onDragLeave);
window.removeEventListener('drop', onDrop);
};
}, []);
const handleStartAndSaveVirtualBackground = (file) => {
const onSuccess = (background) => {
const { filename, data } = background;
if (onAction) {
onAction(EFFECT_TYPES.IMAGE_TYPE, filename, data).then(() => {
dispatchCustomBackground({
type: 'new',
background: {
...background,
custom: true,
lastActivityDate: Date.now(),
},
});
});
} else dispatchCustomBackground({
type: 'new',
background: {
...background,
custom: true,
lastActivityDate: Date.now(),
},
});
};
const onError = (error) => {
logger.warn({
logCode: 'read_file_error',
extraInfo: {
errorName: error.name,
errorMessage: error.message,
},
}, error.message);
};
readFile(file, onSuccess, onError);
};
callback = (checked) => {
handleStartAndSaveVirtualBackground(file);
Session.setItem('skipBackgroundDropConfirmation', checked);
};
const makeDragOperations = useCallback((userId) => {
if (!userId || Auth.userID !== userId || !ENABLE_WEBCAM_BACKGROUND_UPLOAD || !isStream) return {};
const startAndSaveVirtualBackground = (file) => handleStartAndSaveVirtualBackground(file);
const onDragOverHandler = (e) => {
resetEvent(e);
setDraggingOver(true);
setDragging(false);
};
const onDropHandler = (e) => {
resetEvent(e);
setDraggingOver(false);
setDragging(false);
const { files } = e.dataTransfer;
const file = files[0];
if (Session.get('skipBackgroundDropConfirmation')) {
return startAndSaveVirtualBackground(file);
}
setFile(file);
setConfirmModalIsOpen(true);
};
const onDragLeaveHandler = (e) => {
resetEvent(e);
setDragging(false);
setDraggingOver(false);
};
return {
onDragOver: onDragOverHandler,
onDrop: onDropHandler,
onDragLeave: onDragLeaveHandler,
};
}, [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}
</>;
};
const Wrapper = (Component) => (props) => (
<DragAndDrop {...props} >
<Component />
</DragAndDrop>
);
export const withDragAndDrop = (Component) =>
injectIntl(withFileReader(Wrapper(Component), MIME_TYPES_ALLOWED, MAX_FILE_SIZE));

View File

@ -1,63 +0,0 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import PropTypes from 'prop-types';
import VideoService from '/imports/ui/components/video-provider/service';
import { useMutation } from '@apollo/client';
import Styled from './styles';
import { SET_CAMERA_PINNED } from '/imports/ui/core/graphql/mutations/userMutations';
const intlMessages = defineMessages({
unpinLabel: {
id: 'app.videoDock.webcamUnpinLabel',
},
unpinLabelDisabled: {
id: 'app.videoDock.webcamUnpinLabelDisabled',
},
});
const PinArea = (props) => {
const intl = useIntl();
const { user, amIModerator } = props;
const pinned = user?.pin;
const userId = user?.userId;
const shouldRenderPinButton = pinned && userId;
const videoPinActionAvailable = VideoService.isVideoPinEnabledForCurrentUser(amIModerator);
const [setCameraPinned] = useMutation(SET_CAMERA_PINNED);
if (!shouldRenderPinButton) return <Styled.PinButtonWrapper />;
return (
<Styled.PinButtonWrapper>
<Styled.PinButton
color="default"
icon={!pinned ? 'pin-video_on' : 'pin-video_off'}
size="sm"
onClick={() => {
setCameraPinned({
variables: {
userId,
pinned: false,
},
});
}}
label={videoPinActionAvailable
? intl.formatMessage(intlMessages.unpinLabel)
: intl.formatMessage(intlMessages.unpinLabelDisabled)}
hideLabel
disabled={!videoPinActionAvailable}
data-test="pinVideoButton"
/>
</Styled.PinButtonWrapper>
);
};
export default PinArea;
PinArea.propTypes = {
user: PropTypes.shape({
pin: PropTypes.bool.isRequired,
userId: PropTypes.string.isRequired,
}).isRequired,
};

View File

@ -1,43 +0,0 @@
import styled from 'styled-components';
import Button from '/imports/ui/components/common/button/component';
import { colorTransparent, colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
const PinButton = styled(Button)`
padding: 5px;
&,
&:active,
&:hover,
&:focus {
background-color: ${colorTransparent} !important;
border: none !important;
& > i {
border: none !important;
color: ${colorWhite};
font-size: 1rem;
background-color: ${colorTransparent} !important;
}
}
`;
const PinButtonWrapper = styled.div`
background-color: rgba(0,0,0,.3);
cursor: pointer;
border: 0;
margin: 2px;
height: fit-content;
[dir="rtl"] & {
right: auto;
left :0;
}
[class*="presentationZoomControls"] & {
position: relative !important;
}
`;
export default {
PinButtonWrapper,
PinButton,
};

View File

@ -1,196 +0,0 @@
import styled, { keyframes, css } from 'styled-components';
import {
colorPrimary,
colorBlack,
colorWhite,
webcamBackgroundColor,
colorDanger,
webcamPlaceholderBorder,
} from '/imports/ui/stylesheets/styled-components/palette';
import { TextElipsis } from '/imports/ui/stylesheets/styled-components/placeholders';
const rotate360 = keyframes`
from {
transform: rotate(360deg);
}
to {
transform: rotate(0deg);
}
`;
const fade = keyframes`
from {
opacity: 0.7;
}
to {
opacity: 0;
}
`;
const Content = styled.div`
position: relative;
display: flex;
min-width: 100%;
border-radius: 10px;
&::after {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
pointer-events: none;
border: 2px solid ${colorBlack};
border-radius: 10px;
${({ isStream }) => !isStream && `
border: 2px solid ${webcamPlaceholderBorder};
`}
${({ talking }) => talking && `
border: 2px solid ${colorPrimary};
`}
${({ talking, customHighlight }) => talking && customHighlight && customHighlight.length > 0 && `
border: 2px solid rgb(${customHighlight[0]}, ${customHighlight[1]}, ${customHighlight[2]});
`}
${({ animations }) => animations && `
transition: opacity .1s;
`}
}
${({ dragging, animations }) => dragging && animations && css`
&::after {
animation: ${fade} .5s linear infinite;
animation-direction: alternate;
}
`}
${({ dragging, draggingOver }) => (dragging || draggingOver) && `
&::after {
opacity: 0.7;
border-style: dashed;
border-color: ${colorDanger};
transition: opacity 0s;
}
`}
${({ fullscreen }) => fullscreen && `
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 99;
`}
`;
const WebcamConnecting = styled.div`
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
min-width: 100%;
border-radius: 10px;
background-color: ${webcamBackgroundColor};
z-index: 0;
&::after {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
opacity: 0;
pointer-events: none;
${({ animations }) => animations && `
transition: opacity .1s;
`}
}
`;
const LoadingText = styled(TextElipsis)`
color: ${colorWhite};
font-size: 100%;
`;
const VideoContainer = styled.div`
display: flex;
justify-content: center;
width: 100%;
height: 100%;
${({ $selfViewDisabled }) => $selfViewDisabled && 'display: none'}
`;
const Video = styled.video`
position: relative;
height: 100%;
width: calc(100% - 1px);
object-fit: contain;
background-color: ${colorBlack};
border-radius: 10px;
${({ mirrored }) => mirrored && `
transform: scale(-1, 1);
`}
${({ unhealthyStream }) => unhealthyStream && `
filter: grayscale(50%) opacity(50%);
`}
`;
const VideoDisabled = styled.div`
color: white;
width: 100%;
height: 20%;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
position: absolute;
border-radius: 10px;
z-index: 2;
top: 40%;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
padding: 20px;
backdrop-filter: blur(10px);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}`;
const TopBar = styled.div`
position: absolute;
display: flex;
width: 100%;
z-index: 1;
top: 0;
padding: 5px;
justify-content: space-between;
`;
const BottomBar = styled.div`
position: absolute;
display: flex;
width: 100%;
z-index: 1;
bottom: 0;
padding: 1px 7px;
justify-content: space-between;
`;
export default {
Content,
WebcamConnecting,
LoadingText,
VideoContainer,
Video,
TopBar,
BottomBar,
VideoDisabled,
};

View File

@ -1,303 +0,0 @@
import React, { useContext } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import browserInfo from '/imports/utils/browserInfo';
import VideoService from '/imports/ui/components/video-provider/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 Session from '/imports/ui/services/storage/in-memory';
const intlMessages = defineMessages({
focusLabel: {
id: 'app.videoDock.webcamFocusLabel',
},
focusDesc: {
id: 'app.videoDock.webcamFocusDesc',
},
unfocusLabel: {
id: 'app.videoDock.webcamUnfocusLabel',
},
unfocusDesc: {
id: 'app.videoDock.webcamUnfocusDesc',
},
pinLabel: {
id: 'app.videoDock.webcamPinLabel',
},
unpinLabel: {
id: 'app.videoDock.webcamUnpinLabel',
},
disableLabel: {
id: 'app.videoDock.webcamDisableLabel',
},
enableLabel: {
id: 'app.videoDock.webcamEnableLabel',
},
pinDesc: {
id: 'app.videoDock.webcamPinDesc',
},
unpinDesc: {
id: 'app.videoDock.webcamUnpinDesc',
},
enableMirrorLabel: {
id: 'app.videoDock.webcamEnableMirrorLabel',
},
enableMirrorDesc: {
id: 'app.videoDock.webcamEnableMirrorDesc',
},
disableMirrorLabel: {
id: 'app.videoDock.webcamDisableMirrorLabel',
},
disableMirrorDesc: {
id: 'app.videoDock.webcamDisableMirrorDesc',
},
fullscreenLabel: {
id: 'app.videoDock.webcamFullscreenLabel',
description: 'Make fullscreen option label',
},
squeezedLabel: {
id: 'app.videoDock.webcamSqueezedButtonLabel',
description: 'User selected webcam squeezed options',
},
disableDesc: {
id: 'app.videoDock.webcamDisableDesc',
},
disableWarning: {
id: 'app.videoDock.webcamDisableWarning',
},
});
const UserActions = ({
name,
cameraId,
numOfStreams,
onHandleVideoFocus = () => {},
user,
focused = false,
onHandleMirror,
isVideoSqueezed = false,
videoContainer = () => {},
isRTL,
isStream,
isSelfViewDisabled,
isMirrored,
amIModerator,
}) => {
const { pluginsExtensibleAreasAggregatedState } = useContext(PluginsContext);
let userCameraDropdownItems = [];
if (pluginsExtensibleAreasAggregatedState.userCameraDropdownItems) {
userCameraDropdownItems = [
...pluginsExtensibleAreasAggregatedState.userCameraDropdownItems,
];
}
const intl = useIntl();
const enableVideoMenu = window.meetingClientSettings.public.kurento.enableVideoMenu || false;
const { isFirefox } = browserInfo;
const [setCameraPinned] = useMutation(SET_CAMERA_PINNED);
const getAvailableActions = () => {
const pinned = user?.pin;
const userId = user?.userId;
const isPinnedIntlKey = !pinned ? 'pin' : 'unpin';
const isFocusedIntlKey = !focused ? 'focus' : 'unfocus';
const isMirroredIntlKey = !isMirrored ? 'enableMirror' : 'disableMirror';
const disabledCams = Session.get('disabledCams') || [];
const isCameraDisabled = disabledCams && disabledCams?.includes(cameraId);
const enableSelfCamIntlKey = !isCameraDisabled ? 'disable' : 'enable';
const menuItems = [];
const toggleDisableCam = () => {
if (!isCameraDisabled) {
Session.setItem('disabledCams', [...disabledCams, cameraId]);
notify(intl.formatMessage(intlMessages.disableWarning), 'info', 'warning');
} else {
Session.setItem('disabledCams', disabledCams.filter((cId) => cId !== cameraId));
}
};
if (isVideoSqueezed) {
menuItems.push({
key: `${cameraId}-name`,
label: name,
description: name,
onClick: () => { },
disabled: true,
});
if (isStream) {
menuItems.push(
{
key: `${cameraId}-fullscreen`,
label: intl.formatMessage(intlMessages.fullscreenLabel),
description: intl.formatMessage(intlMessages.fullscreenLabel),
onClick: () => FullscreenService.toggleFullScreen(videoContainer.current),
},
);
}
}
if (userId === Auth.userID && isStream && !isSelfViewDisabled) {
menuItems.push({
key: `${cameraId}-disable`,
label: intl.formatMessage(intlMessages[`${enableSelfCamIntlKey}Label`]),
description: intl.formatMessage(intlMessages[`${enableSelfCamIntlKey}Label`]),
onClick: () => toggleDisableCam(cameraId),
dataTest: 'selfViewDisableBtn',
});
}
if (isStream) {
menuItems.push({
key: `${cameraId}-mirror`,
label: intl.formatMessage(intlMessages[`${isMirroredIntlKey}Label`]),
description: intl.formatMessage(intlMessages[`${isMirroredIntlKey}Desc`]),
onClick: () => onHandleMirror(cameraId),
dataTest: 'mirrorWebcamBtn',
});
}
if (numOfStreams > 2 && isStream) {
menuItems.push({
key: `${cameraId}-focus`,
label: intl.formatMessage(intlMessages[`${isFocusedIntlKey}Label`]),
description: intl.formatMessage(intlMessages[`${isFocusedIntlKey}Desc`]),
onClick: () => onHandleVideoFocus(cameraId),
dataTest: 'FocusWebcamBtn',
});
}
if (VideoService.isVideoPinEnabledForCurrentUser(amIModerator) && isStream) {
menuItems.push({
key: `${cameraId}-pin`,
label: intl.formatMessage(intlMessages[`${isPinnedIntlKey}Label`]),
description: intl.formatMessage(intlMessages[`${isPinnedIntlKey}Desc`]),
onClick: () => {
setCameraPinned({
variables: {
userId,
pinned: !pinned,
},
});
},
dataTest: 'pinWebcamBtn',
});
}
userCameraDropdownItems.forEach((pluginItem) => {
switch (pluginItem.type) {
case UserCameraDropdownItemType.OPTION:
menuItems.push({
key: pluginItem.id,
label: pluginItem.label,
onClick: pluginItem.onClick,
icon: pluginItem.icon,
});
break;
case UserCameraDropdownItemType.SEPARATOR:
menuItems.push({
key: pluginItem.id,
isSeparator: true,
});
break;
default:
break;
}
});
return menuItems;
};
const renderSqueezedButton = () => (
<Styled.MenuWrapperSqueezed>
<BBBMenu
trigger={(
<Styled.OptionsButton
label={intl.formatMessage(intlMessages.squeezedLabel)}
aria-label={`${name} ${intl.formatMessage(intlMessages.squeezedLabel)}`}
data-test="webcamOptionsMenuSqueezed"
icon="device_list_selector"
ghost
color="primary"
hideLabel
size="sm"
onClick={() => null}
/>
)}
actions={getAvailableActions()}
/>
</Styled.MenuWrapperSqueezed>
);
const renderDefaultButton = () => (
<Styled.MenuWrapper>
{enableVideoMenu && getAvailableActions().length >= 1
? (
<BBBMenu
trigger={(
<Styled.DropdownTrigger
tabIndex={0}
data-test="dropdownWebcamButton"
isRTL={isRTL}
>
{name}
</Styled.DropdownTrigger>
)}
actions={getAvailableActions()}
opts={{
id: `webcam-${user?.userId}-dropdown-menu`,
keepMounted: true,
transitionDuration: 0,
elevation: 3,
getcontentanchorel: null,
fullwidth: 'true',
anchorOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' },
transformOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' },
}}
/>
)
: (
<Styled.Dropdown isFirefox={isFirefox}>
<Styled.UserName noMenu={numOfStreams < 3}>
{name}
</Styled.UserName>
</Styled.Dropdown>
)}
</Styled.MenuWrapper>
);
return (
isVideoSqueezed
? renderSqueezedButton()
: renderDefaultButton()
);
};
export default UserActions;
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

@ -1,124 +0,0 @@
import styled from 'styled-components';
import { colorOffWhite } from '/imports/ui/stylesheets/styled-components/palette';
import { TextElipsis, DivElipsis } from '/imports/ui/stylesheets/styled-components/placeholders';
import { landscape, mediumUp } from '/imports/ui/stylesheets/styled-components/breakpoints';
import { fontSizeSmaller } from '/imports/ui/stylesheets/styled-components/typography';
import Button from '/imports/ui/components/common/button/component';
const DropdownTrigger = styled(DivElipsis)`
user-select: none;
position: relative;
// Keep the background with 0.5 opacity, but leave the text with 1
background-color: rgba(0, 0, 0, 0.5);
border-radius: 10px;
color: ${colorOffWhite};
padding: 0 1rem 0 .5rem !important;
font-size: 80%;
cursor: pointer;
white-space: nowrap;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
&::after {
content: "\\203a";
position: absolute;
transform: rotate(90deg);
${({ isRTL }) => isRTL && `
transform: rotate(-90deg);
`}
top: 45%;
width: 0;
line-height: 0;
right: .45rem;
}
`;
const UserName = styled(TextElipsis)`
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 && `
padding: 0 .5rem 0 .5rem !important;
`}
`;
const Dropdown = styled.div`
display: flex;
outline: none !important;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 10px;
display: inline-block;
@media ${mediumUp} {
>[aria-expanded] {
padding: .25rem;
}
}
@media ${landscape} {
button {
width: calc(100vw - 4rem);
margin-left: 1rem;
}
}
${({ isFirefox }) => isFirefox && `
max-width: 100%;
`}
`;
const MenuWrapper = styled.div`
max-width: 75%;
`;
const MenuWrapperSqueezed = styled.div`
position: absolute;
right: 0;
top: 0;
`;
const OptionsButton = styled(Button)`
position: absolute;
right: 7px;
top: 7px;
z-index: 2;
background-color: rgba(0,0,0,0.4);
color: ${colorOffWhite};
overflow: hidden;
border: none !important;
padding: 3px;
i {
width: auto;
font-size: ${fontSizeSmaller} !important;
background-color: transparent !important;
}
&,
&:active,
&:focus,
&:hover {
background-color: rgba(0,0,0,0.5) !important;
border: none !important;
color: white !important;
opacity: 100% !important;
}
&:hover {
transform: scale(1.3);
transition-duration: 150ms;
}
`;
export default {
DropdownTrigger,
UserName,
Dropdown,
MenuWrapper,
MenuWrapperSqueezed,
OptionsButton,
};

View File

@ -1,63 +0,0 @@
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';
const UserAvatarVideo = (props) => {
const { user, unhealthyStream, squeezed, voiceUser } = props;
const {
name, color, avatar, role, emoji,
} = user;
let {
presenter, clientType,
} = user;
const talking = voiceUser?.talking || false;
const ROLE_MODERATOR = window.meetingClientSettings.public.user.role_moderator;
const handleUserIcon = () => {
if (emoji !== 'none') {
return <Icon iconName={UserListService.normalizeEmojiName(emoji)} />;
}
return name.toLowerCase().slice(0, 2);
};
// hide icons when squeezed
if (squeezed) {
presenter = false;
clientType = false;
}
return (
<Styled.UserAvatarStyled
moderator={role === ROLE_MODERATOR}
presenter={presenter}
dialIn={clientType === 'dial-in-user'}
color={color}
emoji={emoji !== 'none'}
avatar={avatar}
unhealthyStream={unhealthyStream}
talking={talking}
>
{handleUserIcon()}
</Styled.UserAvatarStyled>
);
};
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,52 +0,0 @@
import UserAvatar from '/imports/ui/components/user-avatar/component';
import {
userIndicatorsOffset,
mdPaddingY,
} from '/imports/ui/stylesheets/styled-components/general';
import {
colorPrimary,
} from '/imports/ui/stylesheets/styled-components/palette';
import styled from 'styled-components';
const UserAvatarStyled = styled(UserAvatar)`
height: 60%;
width: 45%;
max-width: 66px;
max-height: 66px;
scale: 1.5;
${({ unhealthyStream }) => unhealthyStream && `
filter: grayscale(50%) opacity(50%);
`}
${({ dialIn }) => dialIn && `
&:before {
content: "\\00a0\\e91a\\00a0";
padding: ${mdPaddingY};
opacity: 1;
top: ${userIndicatorsOffset};
right: ${userIndicatorsOffset};
bottom: auto;
left: auto;
border-radius: 50%;
background-color: ${colorPrimary};
padding: 0.7rem !important;
[dir="rtl"] & {
left: auto;
right: ${userIndicatorsOffset};
letter-spacing: -.33rem;
}
}
`}
${({ presenter }) => presenter && `
&:before {
padding: 0.7rem !important;
}
`};
`;
export default {
UserAvatarStyled,
};

View File

@ -1,34 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import Styled from './styles';
const UserStatus = (props) => {
const { voiceUser, user } = props;
const listenOnly = voiceUser?.listenOnly;
const muted = voiceUser?.muted;
const voiceUserJoined = voiceUser?.joined;
const emoji = user?.reactionEmoji;
const raiseHand = user?.raiseHand;
const away = user?.away;
return (
<div>
{away && !raiseHand && '⏰'}
{raiseHand && '✋'}
{(emoji && emoji !== 'none' && !raiseHand && !away) && emoji}
{(muted && !listenOnly) && <Styled.Muted iconName="unmute_filled" />}
{listenOnly && <Styled.Voice iconName="listen" /> }
{(voiceUserJoined && !muted) && <Styled.Voice iconName="unmute" />}
</div>
);
};
export default UserStatus;
UserStatus.propTypes = {
voiceUser: PropTypes.shape({
listenOnly: PropTypes.bool.isRequired,
muted: PropTypes.bool.isRequired,
joined: PropTypes.bool.isRequired,
}).isRequired,
};

View File

@ -1,36 +0,0 @@
import styled from 'styled-components';
import Icon from '/imports/ui/components/common/icon/component';
import { colorDanger, colorSuccess, colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
const Voice = styled(Icon)`
height: 1.1rem;
width: 1.1rem;
margin-left: 0.5rem;
color: ${colorWhite};
border-radius: 50%;
&::before {
font-size: 80%;
}
background-color: ${colorSuccess};
`;
const Muted = styled(Icon)`
height: 1.1rem;
width: 1.1rem;
color: ${colorWhite};
border-radius: 50%;
margin-left: 0.5rem;
&::before {
font-size: 80%;
}
background-color: ${colorDanger};
`;
export default {
Voice,
Muted,
};

View File

@ -1,55 +0,0 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
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) => {
const {
name, cameraId, videoContainer, isFullscreenContext, layoutContextDispatch, isStream,
} = props;
const ALLOW_FULLSCREEN = window.meetingClientSettings.public.app.allowFullscreen;
useEffect(() => () => {
// exit fullscreen when component is unmounted
if (isFullscreenContext) {
layoutContextDispatch({
type: ACTIONS.SET_FULLSCREEN_ELEMENT,
value: {
element: '',
group: '',
},
});
}
}, []);
if (!ALLOW_FULLSCREEN || !isStream) return null;
return (
<Styled.FullscreenWrapper>
<FullscreenButtonContainer
data-test="webcamsFullscreenButton"
fullscreenRef={videoContainer.current}
elementName={name}
elementId={cameraId}
elementGroup="webcams"
isFullscreen={isFullscreenContext}
dark
/>
</Styled.FullscreenWrapper>
);
};
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,9 +0,0 @@
import styled from 'styled-components';
const FullscreenWrapper = styled.div`
position: relative;
`;
export default {
FullscreenWrapper,
};

View File

@ -1,22 +0,0 @@
import { gql } from '@apollo/client';
export const CAMERA_BROADCAST_START = gql`
mutation CameraBroadcastStart($cameraId: String!) {
cameraBroadcastStart(
stream: $cameraId
)
}
`;
export const CAMERA_BROADCAST_STOP = gql`
mutation CameraBroadcastStop($cameraId: String!) {
cameraBroadcastStop(
stream: $cameraId
)
}
`;
export default {
CAMERA_BROADCAST_START,
CAMERA_BROADCAST_STOP,
};

View File

@ -1,119 +0,0 @@
import { gql } from '@apollo/client';
import type { User } from './types';
interface Voice {
floor: boolean;
lastFloorTime: string;
}
export interface VideoStreamsResponse {
user_camera: {
streamId: string;
user: User;
voice?: Voice;
}[];
}
export interface GridUsersResponse {
user: User[];
}
export interface OwnVideoStreamsResponse {
user_camera: {
streamId: string;
}[];
}
export const VIDEO_STREAMS_SUBSCRIPTION = gql`
subscription VideoStreams {
user_camera {
streamId
user {
name
userId
nameSortable
pinned
away
disconnected
emoji
role
avatar
color
presenter
clientType
raiseHand
isModerator
reactionEmoji
}
voice {
floor
lastFloorTime
}
}
}
`;
export const OWN_VIDEO_STREAMS_QUERY = gql`
query OwnVideoStreams($userId: String!, $streamIdPrefix: String!) {
user_camera(
where: {
userId: { _eq: $userId },
streamId: { _like: $streamIdPrefix }
},
) {
streamId
}
}
`;
export const VIEWERS_IN_WEBCAM_COUNT_SUBSCRIPTION = gql`
subscription ViewerVideoStreams {
user_camera_aggregate(where: {
user: { role: { _eq: "VIEWER" }, presenter: { _eq: false } }
}) {
aggregate {
count
}
}
}
`;
export const GRID_USERS_SUBSCRIPTION = gql`
subscription GridUsers($exceptUserIds: [String]!, $limit: Int!) {
user(
where: {
userId: {
_nin: $exceptUserIds,
},
},
limit: $limit,
order_by: {
nameSortable: asc,
userId: asc,
},
) {
name
userId
nameSortable
pinned
away
disconnected
emoji
role
avatar
color
presenter
clientType
raiseHand
isModerator
reactionEmoji
}
}
`;
export default {
OWN_VIDEO_STREAMS_QUERY,
VIDEO_STREAMS_SUBSCRIPTION,
VIEWERS_IN_WEBCAM_COUNT_SUBSCRIPTION,
GRID_USERS_SUBSCRIPTION,
};

View File

@ -1,270 +0,0 @@
import React, { useState, useEffect } from 'react';
import Resizable from 're-resizable';
import Draggable from 'react-draggable';
import Styled from './styles';
import { ACTIONS, CAMERADOCK_POSITION } from '../layout/enums';
import DropAreaContainer from './drop-areas/container';
import VideoProviderContainer from '/imports/ui/components/video-provider/container';
import Storage from '/imports/ui/services/storage/session';
import { colorContentBackground } from '/imports/ui/stylesheets/styled-components/palette';
const WebcamComponent = ({
cameraDock,
swapLayout,
focusedId,
layoutContextDispatch,
fullscreen,
isPresenter,
displayPresentation,
cameraOptimalGridSize: cameraSize,
isRTL,
isGridEnabled,
}) => {
const [isResizing, setIsResizing] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const [isFullscreen, setIsFullScreen] = useState(false);
const [resizeStart, setResizeStart] = useState({ width: 0, height: 0 });
const [cameraMaxWidth, setCameraMaxWidth] = useState(0);
const [draggedAtLeastOneTime, setDraggedAtLeastOneTime] = useState(false);
const lastSize = Storage.getItem('webcamSize') || { width: 0, height: 0 };
const { width: lastWidth, height: lastHeight } = lastSize;
const isCameraTopOrBottom = cameraDock.position === CAMERADOCK_POSITION.CONTENT_TOP
|| cameraDock.position === CAMERADOCK_POSITION.CONTENT_BOTTOM;
const isCameraLeftOrRight = cameraDock.position === CAMERADOCK_POSITION.CONTENT_LEFT
|| cameraDock.position === CAMERADOCK_POSITION.CONTENT_RIGHT;
const isCameraSidebar = cameraDock.position === CAMERADOCK_POSITION.SIDEBAR_CONTENT_BOTTOM;
useEffect(() => {
const handleVisibility = () => {
if (document.hidden) {
document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
}
};
document.addEventListener('visibilitychange', handleVisibility);
return () => {
document.removeEventListener('visibilitychange', handleVisibility);
};
}, []);
useEffect(() => {
setIsFullScreen(fullscreen.group === 'webcams');
}, [fullscreen]);
useEffect(() => {
const newCameraMaxWidth = (isPresenter && cameraDock.presenterMaxWidth) ? cameraDock.presenterMaxWidth : cameraDock.maxWidth;
setCameraMaxWidth(newCameraMaxWidth);
if (isCameraLeftOrRight && cameraDock.width > newCameraMaxWidth) {
layoutContextDispatch(
{
type: ACTIONS.SET_CAMERA_DOCK_SIZE,
value: {
width: newCameraMaxWidth,
height: cameraDock.height,
browserWidth: window.innerWidth,
browserHeight: window.innerHeight,
},
},
);
Storage.setItem('webcamSize', { width: newCameraMaxWidth, height: lastHeight });
}
const cams = document.getElementById('cameraDock');
cams?.setAttribute("data-position", cameraDock.position);
}, [cameraDock.position, cameraDock.maxWidth, isPresenter, displayPresentation]);
const handleVideoFocus = (id) => {
layoutContextDispatch({
type: ACTIONS.SET_FOCUSED_CAMERA_ID,
value: focusedId !== id ? id : false,
});
}
const onResizeHandle = (deltaWidth, deltaHeight) => {
if (cameraDock.resizableEdge.top || cameraDock.resizableEdge.bottom) {
layoutContextDispatch(
{
type: ACTIONS.SET_CAMERA_DOCK_SIZE,
value: {
width: cameraDock.width,
height: resizeStart.height + deltaHeight,
browserWidth: window.innerWidth,
browserHeight: window.innerHeight,
},
},
);
}
if (cameraDock.resizableEdge.left || cameraDock.resizableEdge.right) {
layoutContextDispatch(
{
type: ACTIONS.SET_CAMERA_DOCK_SIZE,
value: {
width: resizeStart.width + deltaWidth,
height: cameraDock.height,
browserWidth: window.innerWidth,
browserHeight: window.innerHeight,
},
},
);
}
};
const handleWebcamDragStart = () => {
setIsDragging(true);
document.body.style.overflow = 'hidden';
layoutContextDispatch({
type: ACTIONS.SET_CAMERA_DOCK_IS_DRAGGING,
value: true,
});
};
const handleWebcamDragStop = (e) => {
setIsDragging(false);
setDraggedAtLeastOneTime(false);
document.body.style.overflow = 'auto';
if (Object.values(CAMERADOCK_POSITION).includes(e.target.id) && draggedAtLeastOneTime) {
const layout = document.getElementById('layout');
layout?.setAttribute("data-cam-position", e?.target?.id);
layoutContextDispatch({
type: ACTIONS.SET_CAMERA_DOCK_POSITION,
value: e.target.id,
});
}
layoutContextDispatch({
type: ACTIONS.SET_CAMERA_DOCK_IS_DRAGGING,
value: false,
});
};
let draggableOffset = {
left: isDragging && (isCameraTopOrBottom || isCameraSidebar)
? ((cameraDock.width - cameraSize.width) / 2)
: 0,
top: isDragging && isCameraLeftOrRight
? ((cameraDock.height - cameraSize.height) / 2)
: 0,
};
if (isRTL) {
draggableOffset.left = 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 camOpacity = isDragging ? 0.5 : undefined;
return (
<>
{isDragging ? <DropAreaContainer /> : null}
<Styled.ResizableWrapper
horizontal={cameraDock.position === CAMERADOCK_POSITION.CONTENT_TOP
|| cameraDock.position === CAMERADOCK_POSITION.CONTENT_BOTTOM}
vertical={cameraDock.position === CAMERADOCK_POSITION.CONTENT_LEFT
|| cameraDock.position === CAMERADOCK_POSITION.CONTENT_RIGHT}
>
<Draggable
handle="video"
bounds="html"
onStart={handleWebcamDragStart}
onDrag={() => {
if (!draggedAtLeastOneTime) {
setDraggedAtLeastOneTime(true);
}
}}
onStop={handleWebcamDragStop}
onMouseDown={
cameraDock.isDraggable ? (e) => e.preventDefault() : undefined
}
disabled={!cameraDock.isDraggable || isResizing || isFullscreen}
position={
{
x: cameraDock.left - cameraDock.right + draggableOffset.left,
y: cameraDock.top + draggableOffset.top,
}
}
>
<Resizable
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,
}}
onResizeStart={() => {
setIsResizing(true);
setResizeStart({ width: cameraDock.width, height: cameraDock.height });
onResizeHandle(cameraDock.width, cameraDock.height);
layoutContextDispatch({
type: ACTIONS.SET_CAMERA_DOCK_IS_RESIZING,
value: true,
});
}}
onResize={(e, direction, ref, d) => {
onResizeHandle(d.width, d.height);
}}
onResizeStop={() => {
setResizeStart({ width: 0, height: 0 });
setTimeout(() => setIsResizing(false), 500);
layoutContextDispatch({
type: ACTIONS.SET_CAMERA_DOCK_IS_RESIZING,
value: false,
});
}}
enable={{
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,
topLeft: false,
topRight: false,
bottomLeft: false,
bottomRight: false,
}}
style={{
position: 'absolute',
zIndex: isCameraSidebar && !isDragging ? 0 : cameraDock.zIndex,
}}
>
<Styled.Draggable
isDraggable={cameraDock.isDraggable && !isFullscreen && !isDragging}
isDragging={isDragging}
id="cameraDock"
role="region"
draggable={cameraDock.isDraggable && !isFullscreen ? 'true' : undefined}
style={{
width: isIphone ? mobileWidth : isDesktopWidth,
height: isIphone ? mobileHeight : isDesktopHeight,
opacity: camOpacity,
background: null,
}}
>
<VideoProviderContainer
{...{
swapLayout,
cameraDock,
focusedId,
handleVideoFocus,
isGridEnabled,
}}
/>
</Styled.Draggable>
</Resizable>
</Draggable>
</Styled.ResizableWrapper>
</>
);
};
export default WebcamComponent;

View File

@ -1,100 +0,0 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import VideoService from '/imports/ui/components/video-provider/service';
import {
layoutSelect,
layoutSelectInput,
layoutSelectOutput,
layoutDispatch,
} from '../layout/context';
import WebcamComponent from '/imports/ui/components/webcam/component';
import { LAYOUT_TYPE } from '../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 WebcamContainerGraphql from './webcam-graphql/component';
import useDeduplicatedSubscription from '../../core/hooks/useDeduplicatedSubscription';
const WebcamContainer = ({
audioModalIsOpen,
usersVideo,
layoutType,
isLayoutSwapped,
}) => {
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 layoutContextDispatch = layoutDispatch();
const { data: presentationPageData } = useDeduplicatedSubscription(
CURRENT_PRESENTATION_PAGE_SUBSCRIPTION,
);
const presentationPage = presentationPageData?.pres_page_curr[0] || {};
const hasPresentation = !!presentationPage?.presentationId;
const swapLayout = !hasPresentation || isLayoutSwapped;
let floatingOverlay = false;
let hideOverlay = false;
if (swapLayout) {
floatingOverlay = true;
hideOverlay = true;
}
const { cameraOptimalGridSize } = cameraDockInput;
const { display: displayPresentation } = presentation;
const { data: currentUserData } = useCurrentUser((user) => ({
presenter: user.presenter,
}));
const isGridEnabled = layoutType === LAYOUT_TYPE.VIDEO_FOCUS;
return !audioModalIsOpen
&& (usersVideo.length > 0 || isGridEnabled)
? (
<WebcamComponent
{...{
swapLayout,
usersVideo,
focusedId: cameraDock.focusedId,
cameraDock,
cameraOptimalGridSize,
layoutContextDispatch,
fullscreen,
isPresenter: currentUserData?.presenter,
displayPresentation,
isRTL,
isGridEnabled,
floatingOverlay,
hideOverlay,
}}
/>
)
: null;
};
withTracker(() => {
const data = {
audioModalIsOpen: Session.get('audioModalIsOpen'),
isMeteorConnected: Meteor.status().connected,
};
const { streams: usersVideo, gridUsers } = VideoService.getVideoStreams();
const { defaultSorting: DEFAULT_SORTING } = window.meetingClientSettings.public.kurento.cameraSortingModes;
if (gridUsers.length > 0) {
const items = usersVideo.concat(gridUsers);
data.usersVideo = sortVideoStreams(items, DEFAULT_SORTING);
} else {
data.usersVideo = usersVideo;
}
return data;
})(WebcamContainer);
export default WebcamContainerGraphql;

View File

@ -1,39 +0,0 @@
import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import Styled from './styles';
const intlMessages = defineMessages({
dropZoneLabel: {
id: 'app.video.dropZoneLabel',
description: 'message showing where the user can drop cameraDock',
},
});
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>
</>
);
export default injectIntl(DropArea);

View File

@ -1,15 +0,0 @@
import React from 'react';
import { layoutSelectOutput } from '../../layout/context';
import DropArea from './component';
const DropAreaContainer = () => {
const dropZoneAreas = layoutSelectOutput((i) => i.dropZoneAreas);
return (
Object.keys(dropZoneAreas).map((objectKey) => (
<DropArea dataTest={`dropArea-${objectKey}`} key={objectKey} id={objectKey} style={dropZoneAreas[objectKey]} />
))
);
};
export default DropAreaContainer;

View File

@ -1,40 +0,0 @@
import styled from 'styled-components';
import { colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
const DropZoneArea = styled.div`
position: absolute;
background: transparent;
-webkit-box-shadow: inset 0px 0px 0px 1px rgba(0, 0, 0, .2);
-moz-box-shadow: inset 0px 0px 0px 1px rgba(0, 0, 0, .2);
box-shadow: inset 0px 0px 0px 1px rgba(0, 0, 0, .2);
font-weight: bold;
font-family: sans-serif;
display: flex;
align-items: center;
justify-content: center;
cursor: grabbing;
&:hover {
background-color: rgba(0, 0, 0, .1);
}
`;
const DropZoneBg = styled.div`
position: absolute;
background-color: rgba(0, 0, 0, .5);
-webkit-box-shadow: inset 0px 0px 0px 1px #666;
-moz-box-shadow: inset 0px 0px 0px 1px #666;
box-shadow: inset 0px 0px 0px 1px #666;
color: ${colorWhite};
font-weight: bold;
font-family: sans-serif;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
`;
export default {
DropZoneArea,
DropZoneBg,
};

View File

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