Delete and move files
This commit is contained in:
parent
4063ee811b
commit
452867246a
File diff suppressed because it is too large
Load Diff
@ -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;
|
@ -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);
|
@ -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);
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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,
|
||||
}));
|
||||
};
|
@ -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));
|
@ -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));
|
@ -1,9 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const OffsetBottom = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export default {
|
||||
OffsetBottom,
|
||||
};
|
@ -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);
|
@ -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);
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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));
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -1,9 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const FullscreenWrapper = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export default {
|
||||
FullscreenWrapper,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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;
|
@ -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;
|
@ -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);
|
@ -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;
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
Loading…
Reference in New Issue
Block a user