Merge branch 'v2.6.x-release' of github.com:bigbluebutton/bigbluebutton into merge-26-27

This commit is contained in:
Anton Georgiev 2023-03-10 13:03:56 -05:00
commit 2c5cd8f2a0
388 changed files with 4829 additions and 11580 deletions

View File

@ -5,6 +5,9 @@ on:
- 'develop' - 'develop'
- 'v2.[5-9].x-release' - 'v2.[5-9].x-release'
- 'v[3-9].*.x-release' - 'v[3-9].*.x-release'
paths-ignore:
- 'docs/**'
- '**/*.md'
pull_request: pull_request:
types: [opened, synchronize, reopened] types: [opened, synchronize, reopened]
permissions: permissions:

View File

@ -7,8 +7,8 @@ on:
- 'v*' - 'v*'
- 'develop' - 'develop'
paths: paths:
- 'docs/' - 'docs/**'
- '.github/' - '.github/**'
# Do not build the docs concurrently # Do not build the docs concurrently
concurrency: concurrency:

2
.gitignore vendored
View File

@ -21,3 +21,5 @@ record-and-playback/.loadpath
*~ *~
cache/* cache/*
artifacts/* artifacts/*
bbb-presentation-video.zip
bbb-presentation-video

View File

@ -68,7 +68,12 @@ trait PresentationUploadTokenReqMsgHdlr extends RightsManagementTrait {
log.info("handlePresentationUploadTokenReqMsg" + liveMeeting.props.meetingProp.intId + log.info("handlePresentationUploadTokenReqMsg" + liveMeeting.props.meetingProp.intId +
" userId=" + msg.header.userId + " filename=" + msg.body.filename) " userId=" + msg.header.userId + " filename=" + msg.body.filename)
if (filterPresentationMessage(liveMeeting.users2x, msg.header.userId) && if (liveMeeting.props.meetingProp.disabledFeatures.contains("presentation")) {
broadcastPresentationUploadTokenFailResp(msg)
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "Presentation is disabled for this meeting"
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
} else if (filterPresentationMessage(liveMeeting.users2x, msg.header.userId) &&
permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) { permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
val meetingId = liveMeeting.props.meetingProp.intId val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to request presentation upload token." val reason = "No permission to request presentation upload token."

View File

@ -182,7 +182,6 @@ case class PresentationHasInvalidMimeTypeErrorSysPubMsgBody(
fileExtension: String, fileExtension: String,
) )
object PresentationUploadedFileTimeoutErrorSysPubMsg { val NAME = "PresentationUploadedFileTimeoutErrorSysPubMsg" } object PresentationUploadedFileTimeoutErrorSysPubMsg { val NAME = "PresentationUploadedFileTimeoutErrorSysPubMsg" }
case class PresentationUploadedFileTimeoutErrorSysPubMsg( case class PresentationUploadedFileTimeoutErrorSysPubMsg(
header: BbbClientMsgHeader, header: BbbClientMsgHeader,

View File

@ -105,7 +105,6 @@ libraryDependencies ++= Seq(
"javax.validation" % "validation-api" % "2.0.1.Final", "javax.validation" % "validation-api" % "2.0.1.Final",
"org.springframework.boot" % "spring-boot-starter-validation" % "2.7.1", "org.springframework.boot" % "spring-boot-starter-validation" % "2.7.1",
"org.springframework.data" % "spring-data-commons" % "2.7.6", "org.springframework.data" % "spring-data-commons" % "2.7.6",
"org.glassfish" % "javax.el" % "3.0.1-b12",
"org.apache.httpcomponents" % "httpclient" % "4.5.13", "org.apache.httpcomponents" % "httpclient" % "4.5.13",
"org.postgresql" % "postgresql" % "42.4.3", "org.postgresql" % "postgresql" % "42.4.3",
"org.hibernate" % "hibernate-core" % "5.6.1.Final", "org.hibernate" % "hibernate-core" % "5.6.1.Final",

View File

@ -20,17 +20,8 @@ package org.bigbluebutton.api;
import java.io.File; import java.io.File;
import java.net.URI; import java.net.URI;
import java.util.AbstractMap; import java.util.*;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
@ -263,7 +254,6 @@ public class MeetingService implements MessageListener {
RegisteredUser ru = registeredUser.getValue(); RegisteredUser ru = registeredUser.getValue();
long elapsedTime = now - ru.getGuestWaitedOn(); long elapsedTime = now - ru.getGuestWaitedOn();
log.info("Determining if user [{}] should be purged. Elapsed time waiting [{}] with guest status [{}]", registeredUserID, elapsedTime, ru.getGuestStatus());
if (elapsedTime >= waitingGuestUsersTimeout && ru.getGuestStatus() == GuestPolicy.WAIT) { if (elapsedTime >= waitingGuestUsersTimeout && ru.getGuestStatus() == GuestPolicy.WAIT) {
log.info("Purging user [{}]", registeredUserID); log.info("Purging user [{}]", registeredUserID);
if (meeting.userUnregistered(registeredUserID) != null) { if (meeting.userUnregistered(registeredUserID) != null) {
@ -549,6 +539,11 @@ public class MeetingService implements MessageListener {
return recordingService.isRecordingExist(recordId); return recordingService.isRecordingExist(recordId);
} }
public boolean isMeetingWithDisabledPresentation(String meetingId) {
Meeting m = getMeeting(meetingId);
return m.getDisabledFeatures().contains("presentation");
}
public String getRecordings2x(List<String> idList, List<String> states, Map<String, String> metadataFilters, String offset, String limit) { public String getRecordings2x(List<String> idList, List<String> states, Map<String, String> metadataFilters, String offset, String limit) {
Pageable pageable = null; Pageable pageable = null;
int o = -1; int o = -1;

View File

@ -30,6 +30,7 @@ public class RecordingServiceDbImpl implements RecordingService {
private RecordingMetadataReaderHelper recordingServiceHelper; private RecordingMetadataReaderHelper recordingServiceHelper;
private String recordStatusDir; private String recordStatusDir;
private String captionsDir; private String captionsDir;
private Boolean allowFetchAllRecordings;
private String presentationBaseDir; private String presentationBaseDir;
private String defaultServerUrl; private String defaultServerUrl;
private String defaultTextTrackUrl; private String defaultTextTrackUrl;
@ -74,7 +75,7 @@ public class RecordingServiceDbImpl implements RecordingService {
@Override @Override
public String getRecordings2x(List<String> idList, List<String> states, Map<String, String> metadataFilters, int offset, Pageable pageable) { public String getRecordings2x(List<String> idList, List<String> states, Map<String, String> metadataFilters, int offset, Pageable pageable) {
// If no IDs or limit were provided return no recordings instead of every recording // If no IDs or limit were provided return no recordings instead of every recording
if((idList == null || idList.isEmpty()) && pageable == null) return xmlService.noRecordings(); if((idList == null || idList.isEmpty()) && pageable == null && !allowFetchAllRecordings) return xmlService.noRecordings();
logger.info("Retrieving all recordings"); logger.info("Retrieving all recordings");
Set<Recording> recordings = new HashSet<>(dataStore.findAll(Recording.class)); Set<Recording> recordings = new HashSet<>(dataStore.findAll(Recording.class));
@ -262,6 +263,10 @@ public class RecordingServiceDbImpl implements RecordingService {
captionsDir = dir; captionsDir = dir;
} }
public void setAllowFetchAllRecordings(Boolean allowFetchAllRecordings) {
this.allowFetchAllRecordings = allowFetchAllRecordings;
}
public void setRecordingServiceHelper(RecordingMetadataReaderHelper r) { public void setRecordingServiceHelper(RecordingMetadataReaderHelper r) {
recordingServiceHelper = r; recordingServiceHelper = r;
} }

View File

@ -62,6 +62,7 @@ public class RecordingServiceFileImpl implements RecordingService {
private XmlService xmlService; private XmlService xmlService;
private String recordStatusDir; private String recordStatusDir;
private String captionsDir; private String captionsDir;
private Boolean allowFetchAllRecordings;
private String presentationBaseDir; private String presentationBaseDir;
private String defaultServerUrl; private String defaultServerUrl;
private String defaultTextTrackUrl; private String defaultTextTrackUrl;
@ -203,7 +204,7 @@ public class RecordingServiceFileImpl implements RecordingService {
public String getRecordings2x(List<String> idList, List<String> states, Map<String, String> metadataFilters, int offset, Pageable pageable) { public String getRecordings2x(List<String> idList, List<String> states, Map<String, String> metadataFilters, int offset, Pageable pageable) {
// If no IDs or limit were provided return no recordings instead of every recording // If no IDs or limit were provided return no recordings instead of every recording
if(idList.isEmpty() && pageable == null) return xmlService.noRecordings(); if(idList.isEmpty() && pageable == null && !allowFetchAllRecordings) return xmlService.noRecordings();
List<RecordingMetadata> recsList = getRecordingsMetadata(idList, states); List<RecordingMetadata> recsList = getRecordingsMetadata(idList, states);
ArrayList<RecordingMetadata> recs = filterRecordingsByMetadata(recsList, metadataFilters); ArrayList<RecordingMetadata> recs = filterRecordingsByMetadata(recsList, metadataFilters);
@ -434,6 +435,8 @@ public class RecordingServiceFileImpl implements RecordingService {
captionsDir = dir; captionsDir = dir;
} }
public void setAllowFetchAllRecordings(Boolean allowFetchAllRecordings) { this.allowFetchAllRecordings = allowFetchAllRecordings; }
public void setRecordingServiceHelper(RecordingMetadataReaderHelper r) { public void setRecordingServiceHelper(RecordingMetadataReaderHelper r) {
recordingServiceHelper = r; recordingServiceHelper = r;
} }

View File

@ -64,6 +64,7 @@ public class SlidesGenerationProgressNotifier {
); );
messagingService.sendDocConversionMsg(invalidMimeType); messagingService.sendDocConversionMsg(invalidMimeType);
} }
public void sendUploadFileTimedout(UploadedPresentation pres, int page) { public void sendUploadFileTimedout(UploadedPresentation pres, int page) {
UploadFileTimedoutMessage errorMessage = new UploadFileTimedoutMessage( UploadFileTimedoutMessage errorMessage = new UploadFileTimedoutMessage(
pres.getPodId(), pres.getPodId(),

View File

@ -346,7 +346,7 @@ class App extends React.Component {
</h1> </h1>
<div className="mt-3 col-text-right py-1 text-gray-500 inline-block"> <div className="mt-3 col-text-right py-1 text-gray-500 inline-block">
<p className="font-bold"> <p className="font-bold">
<div className="inline"> <div className="inline" data-test="meetingDateDashboard">
<FormattedDate <FormattedDate
value={activitiesJson.createdOn} value={activitiesJson.createdOn}
year="numeric" year="numeric"
@ -359,7 +359,7 @@ class App extends React.Component {
activitiesJson.endedOn > 0 activitiesJson.endedOn > 0
? ( ? (
<span className="px-2 py-1 font-semibold leading-tight text-red-700 bg-red-100 rounded-full"> <span className="px-2 py-1 font-semibold leading-tight text-red-700 bg-red-100 rounded-full">
<FormattedMessage id="app.learningDashboard.indicators.meetingStatusEnded" defaultMessage="Ended" /> <FormattedMessage id="app.learningDashboard.indicators.meetingStatusEnded" defaultMessage="Ended" data-test="meetingStatusEndedDashboard" />
</span> </span>
) )
: null : null
@ -367,14 +367,14 @@ class App extends React.Component {
{ {
activitiesJson.endedOn === 0 activitiesJson.endedOn === 0
? ( ? (
<span className="px-2 py-1 font-semibold leading-tight text-green-700 bg-green-100 rounded-full"> <span className="px-2 py-1 font-semibold leading-tight text-green-700 bg-green-100 rounded-full" data-test="meetingStatusActiveDashboard">
<FormattedMessage id="app.learningDashboard.indicators.meetingStatusActive" defaultMessage="Active" /> <FormattedMessage id="app.learningDashboard.indicators.meetingStatusActive" defaultMessage="Active" />
</span> </span>
) )
: null : null
} }
</p> </p>
<p> <p data-test="meetingDurationTimeDashboard">
<FormattedMessage id="app.learningDashboard.indicators.duration" defaultMessage="Duration" /> <FormattedMessage id="app.learningDashboard.indicators.duration" defaultMessage="Duration" />
:&nbsp; :&nbsp;
{tsToHHmmss(totalOfActivity())} {tsToHHmmss(totalOfActivity())}
@ -389,7 +389,7 @@ class App extends React.Component {
}} }}
> >
<TabsListUnstyled className="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4"> <TabsListUnstyled className="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
<TabUnstyled className="rounded focus:outline-none focus:ring focus:ring-pink-500 ring-offset-2"> <TabUnstyled className="rounded focus:outline-none focus:ring focus:ring-pink-500 ring-offset-2" data-test="activeUsersPanelDashboard">
<Card> <Card>
<CardContent classes={{ root: '!p-0' }}> <CardContent classes={{ root: '!p-0' }}>
<CardBody <CardBody
@ -420,7 +420,7 @@ class App extends React.Component {
</CardContent> </CardContent>
</Card> </Card>
</TabUnstyled> </TabUnstyled>
<TabUnstyled className="rounded focus:outline-none focus:ring focus:ring-green-500 ring-offset-2"> <TabUnstyled className="rounded focus:outline-none focus:ring focus:ring-green-500 ring-offset-2" data-test="activityScorePanelDashboard">
<Card> <Card>
<CardContent classes={{ root: '!p-0' }}> <CardContent classes={{ root: '!p-0' }}>
<CardBody <CardBody
@ -430,7 +430,7 @@ class App extends React.Component {
maximumFractionDigits: 1, maximumFractionDigits: 1,
})} })}
cardClass={tab === TABS.OVERVIEW_ACTIVITY_SCORE ? 'border-green-500' : 'hover:border-green-500 border-white'} cardClass={tab === TABS.OVERVIEW_ACTIVITY_SCORE ? 'border-green-500' : 'hover:border-green-500 border-white'}
iconClass="bg-green-200 text-green-500" iconClass="bg-green-200 text-green-700"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -456,7 +456,7 @@ class App extends React.Component {
</CardContent> </CardContent>
</Card> </Card>
</TabUnstyled> </TabUnstyled>
<TabUnstyled className="rounded focus:outline-none focus:ring focus:ring-purple-500 ring-offset-2"> <TabUnstyled className="rounded focus:outline-none focus:ring focus:ring-purple-500 ring-offset-2" data-test="timelinePanelDashboard">
<Card> <Card>
<CardContent classes={{ root: '!p-0' }}> <CardContent classes={{ root: '!p-0' }}>
<CardBody <CardBody
@ -470,7 +470,7 @@ class App extends React.Component {
</CardContent> </CardContent>
</Card> </Card>
</TabUnstyled> </TabUnstyled>
<TabUnstyled className="rounded focus:outline-none focus:ring focus:ring-blue-500 ring-offset-2"> <TabUnstyled className="rounded focus:outline-none focus:ring focus:ring-blue-500 ring-offset-2" data-test="pollsPanelDashboard">
<Card> <Card>
<CardContent classes={{ root: '!p-0' }}> <CardContent classes={{ root: '!p-0' }}>
<CardBody <CardBody
@ -557,7 +557,7 @@ class App extends React.Component {
<hr className="my-8" /> <hr className="my-8" />
<div className="flex justify-between pb-8 text-xs text-gray-800 dark:text-gray-400 whitespace-nowrap flex-col sm:flex-row"> <div className="flex justify-between pb-8 text-xs text-gray-800 dark:text-gray-400 whitespace-nowrap flex-col sm:flex-row">
<div className="flex flex-col justify-center mb-4 sm:mb-0"> <div className="flex flex-col justify-center mb-4 sm:mb-0">
<p> <p className="text-gray-700">
{ {
lastUpdated && ( lastUpdated && (
<> <>
@ -583,7 +583,7 @@ class App extends React.Component {
</div> </div>
<button <button
type="button" type="button"
className="border-2 border-gray-200 rounded-md px-4 py-2 bg-white focus:outline-none focus:ring ring-offset-2 focus:ring-gray-500 focus:ring-opacity-50" className="border-2 text-gray-700 border-gray-200 rounded-md px-4 py-2 bg-white focus:outline-none focus:ring ring-offset-2 focus:ring-gray-500 focus:ring-opacity-50"
onClick={this.handleSaveSessionData.bind(this)} onClick={this.handleSaveSessionData.bind(this)}
> >
<FormattedMessage <FormattedMessage

View File

@ -303,6 +303,10 @@ const UserDatailsComponent = (props) => {
return []; return [];
} }
const Duration = new Date(getSumOfTime(Object.values(user.intIds)))
.toISOString()
.substring(11, 19);
return ( return (
<div className="fixed inset-0 flex flex-row z-50"> <div className="fixed inset-0 flex flex-row z-50">
<div <div
@ -348,35 +352,27 @@ const UserDatailsComponent = (props) => {
<div className="bg-gray-500 [--line-height:2px] h-[var(--line-height)] absolute top-[calc(50%-var(--line-height)/2)] left-[10px] right-[10px] rounded-2xl" /> <div className="bg-gray-500 [--line-height:2px] h-[var(--line-height)] absolute top-[calc(50%-var(--line-height)/2)] left-[10px] right-[10px] rounded-2xl" />
<div <div
role="progressbar" role="progressbar"
aria-label={`${`${intl.formatMessage({ id: 'app.learningDashboard.userDetails.onlineIndicator', defaultMessage: '{0} online time' }, { 0: user.name })} ${Duration}`}`}
className="ltr:bg-gradient-to-br rtl:bg-gradient-to-bl from-green-100 to-green-600 absolute h-full rounded-2xl text-right rtl:text-left text-ellipsis overflow-hidden" className="ltr:bg-gradient-to-br rtl:bg-gradient-to-bl from-green-100 to-green-600 absolute h-full rounded-2xl text-right rtl:text-left text-ellipsis overflow-hidden"
style={{ style={{
right: `calc(${document.dir === 'ltr' ? userEndOffsetTime : userStartOffsetTime}% + 10px)`, right: `calc(${document.dir === 'ltr' ? userEndOffsetTime : userStartOffsetTime}% + 10px)`,
left: `calc(${document.dir === 'ltr' ? userStartOffsetTime : userEndOffsetTime}% + 10px)`, left: `calc(${document.dir === 'ltr' ? userStartOffsetTime : userEndOffsetTime}% + 10px)`,
}} }}
> >
<div <div className="mx-3 inline-block text-white">
aria-describedby={`online-indicator-desc-${user.userKey}`} { Duration }
aria-label={intl.formatMessage({ id: 'app.learningDashboard.usersTable.colOnline', defaultMessage: 'Online time' })}
className="mx-3 inline-block text-white"
>
{ new Date(getSumOfTime(Object.values(user.intIds)))
.toISOString()
.substring(11, 19) }
</div> </div>
<p id={`online-indicator-desc-${user.userKey}`} className="absolute w-0 h-0 p-0 border-0 m-0 overflow-hidden">
{`${intl.formatMessage({ id: 'app.learningDashboard.userDetails.onlineIndicator', defaultMessage: '{0} online time' }, { 0: user.name })} ${new Date(getSumOfTime(Object.values(user.intIds))).toISOString().substring(11, 19)}`}
</p>
</div> </div>
</div> </div>
<div className="flex flex-row justify-between font-light text-gray-700"> <div className="flex flex-row justify-between font-light text-gray-700">
<div> <div aria-label={`${intl.formatMessage({ id: 'app.learningDashboard.userDetails.startTime', defaultMessage: 'Joined' })} ${new Date(createdOn).toISOString().substring(11, 19)}`}>
<div><FormattedMessage id="app.learningDashboard.userDetails.startTime" defaultMessage="Start Time" /></div> <div aria-hidden="true"><FormattedMessage id="app.learningDashboard.userDetails.startTime" defaultMessage="Start Time" /></div>
<div> <div aria-hidden="true">
<FormattedTime value={createdOn} /> <FormattedTime value={createdOn} />
</div> </div>
</div> </div>
<div className="ltr:text-right rtl:text-left"> <div className="ltr:text-right rtl:text-left">
<div><FormattedMessage id="app.learningDashboard.userDetails.endTime" defaultMessage="End Time" /></div> <div aria-hidden="true"><FormattedMessage id="app.learningDashboard.userDetails.endTime" defaultMessage="End Time" /></div>
<div> <div>
{ endedOn === 0 ? ( { endedOn === 0 ? (
<span className="px-2 py-1 font-semibold leading-tight text-green-700 bg-green-100 rounded-full"> <span className="px-2 py-1 font-semibold leading-tight text-green-700 bg-green-100 rounded-full">
@ -390,19 +386,17 @@ const UserDatailsComponent = (props) => {
</div> </div>
</div> </div>
<div className="p-6 flex flex-row justify-between text-gray-700"> <div className="p-6 flex flex-row justify-between text-gray-700">
<div> <div aria-label={`Duration ${Duration}`}>
<div className="text-gray-900 font-medium"> <div aria-hidden="true" className="text-gray-900 font-medium">
{ new Date(getSumOfTime(Object.values(user.intIds))) { Duration }
.toISOString()
.substring(11, 19) }
</div> </div>
<div><FormattedMessage id="app.learningDashboard.indicators.duration" defaultMessage="Duration" /></div> <div aria-hidden="true"><FormattedMessage id="app.learningDashboard.indicators.duration" defaultMessage="Duration" /></div>
</div> </div>
<div> <div aria-label={`${intl.formatMessage({ id: 'app.learningDashboard.userDetails.joined', defaultMessage: 'Joined' })} ${new Date(joinTime).toISOString().substring(11, 19)}`}>
<div className="font-medium"> <div aria-hidden="true" className="font-medium">
<FormattedTime value={joinTime} /> <FormattedTime value={joinTime} />
</div> </div>
<div><FormattedMessage id="app.learningDashboard.userDetails.joined" defaultMessage="Joined" /></div> <div aria-hidden="true"><FormattedMessage id="app.learningDashboard.userDetails.joined" defaultMessage="Joined" /></div>
</div> </div>
<div> <div>
<div className="font-medium"> <div className="font-medium">

View File

@ -233,7 +233,7 @@ class UsersTable extends React.Component {
const opacity = user.leftOn > 0 ? 'opacity-75' : ''; const opacity = user.leftOn > 0 ? 'opacity-75' : '';
return ( return (
<tr key={user} className="text-gray-700"> <tr key={user} className="text-gray-700">
<td className={`flex items-center px-4 py-3 col-text-left text-sm ${opacity}`}> <td className={`flex items-center px-4 py-3 col-text-left text-sm ${opacity}`} data-test="userLabelDashboard">
<div className="inline-block relative w-8 h-8 rounded-full"> <div className="inline-block relative w-8 h-8 rounded-full">
<UserAvatar user={user} /> <UserAvatar user={user} />
<div <div
@ -253,7 +253,7 @@ class UsersTable extends React.Component {
</button> </button>
{ Object.values(user.intIds || {}).map((intId, index) => ( { Object.values(user.intIds || {}).map((intId, index) => (
<> <>
<p className="text-xs text-gray-600 dark:text-gray-400"> <p className="text-xs text-gray-700 dark:text-gray-400">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline" className="h-4 w-4 inline"
@ -279,7 +279,7 @@ class UsersTable extends React.Component {
</p> </p>
{ intId.leftOn > 0 { intId.leftOn > 0
? ( ? (
<p className="text-xs text-gray-600 dark:text-gray-400"> <p className="text-xs text-gray-700 dark:text-gray-400">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline" className="h-4 w-4 inline"
@ -315,7 +315,7 @@ class UsersTable extends React.Component {
)) } )) }
</div> </div>
</td> </td>
<td className={`px-4 py-3 text-sm text-center items-center ${opacity}`}> <td className={`px-4 py-3 text-sm text-center items-center ${opacity}`} data-test="userOnlineTimeDashboard">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline" className="h-4 w-4 inline"
@ -360,7 +360,7 @@ class UsersTable extends React.Component {
}()) }())
} }
</td> </td>
<td className={`px-4 py-3 text-sm text-center items-center ${opacity}`}> <td className={`px-4 py-3 text-sm text-center items-center ${opacity}`} data-test="userTotalTalkTimeDashboard">
{ user.talk.totalTime > 0 { user.talk.totalTime > 0
? ( ? (
<span className="text-center"> <span className="text-center">
@ -383,7 +383,7 @@ class UsersTable extends React.Component {
</span> </span>
) : null } ) : null }
</td> </td>
<td className={`px-4 py-3 text-sm text-center ${opacity}`}> <td className={`px-4 py-3 text-sm text-center ${opacity}`} data-test="userWebcamTimeDashboard">
{ getSumOfTime(user.webcams) > 0 { getSumOfTime(user.webcams) > 0
? ( ? (
<span className="text-center"> <span className="text-center">
@ -406,7 +406,7 @@ class UsersTable extends React.Component {
</span> </span>
) : null } ) : null }
</td> </td>
<td className={`px-4 py-3 text-sm text-center ${opacity}`}> <td className={`px-4 py-3 text-sm text-center ${opacity}`} data-test="userTotalMessagesDashboard">
{ user.totalOfMessages > 0 { user.totalOfMessages > 0
? ( ? (
<span> <span>
@ -429,7 +429,7 @@ class UsersTable extends React.Component {
</span> </span>
) : null } ) : null }
</td> </td>
<td className={`px-4 py-3 text-sm col-text-left ${opacity}`}> <td className={`px-4 py-3 text-sm col-text-left ${opacity}`} data-test="userTotalEmojisDashboard">
{ {
Object.keys(usersEmojisSummary[user.userKey] || {}).map((emoji) => ( Object.keys(usersEmojisSummary[user.userKey] || {}).map((emoji) => (
<div className="text-xs whitespace-nowrap"> <div className="text-xs whitespace-nowrap">
@ -445,7 +445,7 @@ class UsersTable extends React.Component {
)) ))
} }
</td> </td>
<td className={`px-4 py-3 text-sm text-center ${opacity}`}> <td className={`px-4 py-3 text-sm text-center ${opacity}`} data-test="userRaiseHandDashboard">
{ user.emojis.filter((emoji) => emoji.name === 'raiseHand').length > 0 { user.emojis.filter((emoji) => emoji.name === 'raiseHand').length > 0
? ( ? (
<span> <span>
@ -470,7 +470,7 @@ class UsersTable extends React.Component {
</td> </td>
{ {
!user.isModerator ? ( !user.isModerator ? (
<td className={`px-4 py-3 text-sm text-center items ${opacity}`}> <td className={`px-4 py-3 text-sm text-center items ${opacity}`} data-test="userActivityScoreDashboard">
<svg viewBox="0 0 82 12" width="82" height="12" className="flex-none m-auto inline"> <svg viewBox="0 0 82 12" width="82" height="12" className="flex-none m-auto inline">
<rect width="12" height="12" fill={usersActivityScore[user.userKey] > 0 ? '#A7F3D0' : '#e4e4e7'} /> <rect width="12" height="12" fill={usersActivityScore[user.userKey] > 0 ? '#A7F3D0' : '#e4e4e7'} />
<rect width="12" height="12" x="14" fill={usersActivityScore[user.userKey] > 2 ? '#6EE7B7' : '#e4e4e7'} /> <rect width="12" height="12" x="14" fill={usersActivityScore[user.userKey] > 2 ? '#6EE7B7' : '#e4e4e7'} />
@ -490,7 +490,7 @@ class UsersTable extends React.Component {
</td> </td>
) )
} }
<td className="px-3.5 2xl:px-4 py-3 text-xs text-center"> <td className="px-3.5 2xl:px-4 py-3 text-xs text-center" data-test="userStatusDashboard">
{ {
Object.values(user.intIds)[Object.values(user.intIds).length - 1].leftOn > 0 Object.values(user.intIds)[Object.values(user.intIds).length - 1].leftOn > 0
? ( ? (

View File

@ -3,6 +3,12 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer base {
.text-gray-700 {
color: #374151 !important;
}
}
@layer utilities { @layer utilities {
.bg-inherit { .bg-inherit {
background-color: inherit; background-color: inherit;

View File

@ -1 +1 @@
git clone --branch v1.4.0 --depth 1 https://github.com/bigbluebutton/bbb-pads bbb-pads git clone --branch v1.4.1 --depth 1 https://github.com/bigbluebutton/bbb-pads bbb-pads

View File

@ -1 +1 @@
git clone --branch v5.0.0-rc.1 --depth 1 https://github.com/bigbluebutton/bbb-playback bbb-playback git clone --branch v5.0.0-rc.2 --depth 1 https://github.com/bigbluebutton/bbb-playback bbb-playback

View File

@ -1 +1,11 @@
git clone --branch 4.0.0-beta.4 --depth 1 https://github.com/bigbluebutton/bbb-presentation-video bbb-presentation-video #!/bin/sh
set -ex
RELEASE=4.0.0-rc.1
cat <<MSG
This tool downloads prebuilt packages built on Github Actions
The corresponding source can be browsed at https://github.com/bigbluebutton/bbb-presentation-video/tree/${RELEASE}
Build logs are at https://github.com/bigbluebutton/bbb-presentation-video/actions/workflows/package.yml?query=branch%3A${RELEASE}
MSG
curl -Lf -o bbb-presentation-video.zip "https://github.com/bigbluebutton/bbb-presentation-video/releases/download/${RELEASE}/ubuntu-20.04.zip"
rm -rf bbb-presentation-video
unzip -o bbb-presentation-video.zip -d bbb-presentation-video

View File

@ -1 +1,2 @@
git clone --branch v2.10.0-alpha.1 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu git clone --branch v2.10.0-alpha.1 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu

View File

@ -1 +1 @@
BIGBLUEBUTTON_RELEASE=2.6.0-rc.4 BIGBLUEBUTTON_RELEASE=2.6.0-rc.7

View File

@ -110,12 +110,25 @@ enableUFWRules() {
ufw allow "Nginx Full" ufw allow "Nginx Full"
ufw allow 16384:32768/udp ufw allow 16384:32768/udp
# Check if coturn is running on this server and, if so, open firewall port # Check if haproxy is running on this server and, if so, open port 3478 on ufw
if systemctl status coturn > /dev/null; then
echo " - Local turnserver detected -- opening port 3478" if systemctl is-enabled haproxy> /dev/null 2>&1; then
if systemctl -q is-active haproxy; then
echo " - Local haproxy detected and running -- opening port 3478"
ufw allow 3478 ufw allow 3478
# echo " - Forcing FireFox to use turn server" # echo " - Forcing FireFox to use turn server"
# yq w -i $HTML5_CONFIG public.kurento.forceRelayOnFirefox true # yq w -i $HTML5_CONFIG public.kurento.forceRelayOnFirefox true
else
if grep -q 3478 /etc/ufw/user.rules; then
echo " - Local haproxy not running -- closing port 3478"
ufw delete allow 3478
fi
fi
else
if grep -q 3478 /etc/ufw/user.rules; then
echo " - Local haproxy not running -- closing port 3478"
ufw delete allow 3478
fi
fi fi
ufw --force enable ufw --force enable
@ -255,9 +268,9 @@ notCalled() {
# apply-config.sh. # apply-config.sh.
# #
# By creating apply-config.sh manually, it will not be overwritten by any package updates. You can call functions in this # By creating apply-config.sh manually, it will not be overwritten by any package updates. You can call functions in this
# library for commong BigBlueButton configuration tasks. # library for common BigBlueButton configuration tasks.
## Start Copying HEre ## Start Copying Here
cat > /etc/bigbluebutton/bbb-conf/apply-config.sh << HERE cat > /etc/bigbluebutton/bbb-conf/apply-config.sh << HERE
#!/bin/bash #!/bin/bash

View File

@ -181,12 +181,14 @@ NCPU=$(nproc --all)
BBB_USER=bigbluebutton BBB_USER=bigbluebutton
TURN=$SERVLET_DIR/WEB-INF/classes/spring/turn-stun-servers.xml if [ $EUID == 0 ]; then
TURN_ETC_CONFIG=/etc/bigbluebutton/turn-stun-servers.xml TURN=$SERVLET_DIR/WEB-INF/classes/spring/turn-stun-servers.xml
if [ -f "$TURN_ETC_CONFIG" ]; then TURN_ETC_CONFIG=/etc/bigbluebutton/turn-stun-servers.xml
if [ -f "$TURN_ETC_CONFIG" ]; then
TURN=$TURN_ETC_CONFIG TURN=$TURN_ETC_CONFIG
fi
STUN="$(xmlstarlet sel -N x="http://www.springframework.org/schema/beans" -t -m '_:beans/_:bean[@class="org.bigbluebutton.web.services.turn.StunTurnService"]/_:property[@name="stunServers"]/_:set/_:ref' -v @bean -nl $TURN)"
fi fi
STUN="$(xmlstarlet sel -N x="http://www.springframework.org/schema/beans" -t -m '_:beans/_:bean[@class="org.bigbluebutton.web.services.turn.StunTurnService"]/_:property[@name="stunServers"]/_:set/_:ref' -v @bean -nl $TURN)"
PROTOCOL=http PROTOCOL=http
if [ -f $SERVLET_DIR/WEB-INF/classes/bigbluebutton.properties ]; then if [ -f $SERVLET_DIR/WEB-INF/classes/bigbluebutton.properties ]; then

View File

@ -98,6 +98,16 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
main { main {
display: initial; display: initial;
} }
.overrideSelect {
background-color: #FFF !important;
color: #000 !important;
}
.select {
background-color: rgba(66, 133, 244, 1) !important;
color: #FFF !important;
}
</style> </style>
<script> <script>
document.addEventListener('gesturestart', function (e) { document.addEventListener('gesturestart', function (e) {

View File

@ -7,7 +7,7 @@ const collectionOptions = Meteor.isClient ? {
const AuthTokenValidation = new Mongo.Collection('auth-token-validation', collectionOptions); const AuthTokenValidation = new Mongo.Collection('auth-token-validation', collectionOptions);
if (Meteor.isServer) { if (Meteor.isServer) {
AuthTokenValidation._ensureIndex({ meetingId: 1, userId: 1 }); AuthTokenValidation._ensureIndex({ connectionId: 1 });
} }
export const ValidationStates = Object.freeze({ export const ValidationStates = Object.freeze({

View File

@ -32,7 +32,7 @@ export default function handlePresentationConversionUpdate({ body }, meetingId)
check(meetingId, String); check(meetingId, String);
check(presentationId, Match.Maybe(String)); check(presentationId, Match.Maybe(String));
check(podId, String); check(podId, Match.Maybe(String));
check(status, String); check(status, String);
check(temporaryPresentationId, Match.Maybe(String)); check(temporaryPresentationId, Match.Maybe(String));

View File

@ -50,6 +50,14 @@ export default function setCurrentPresentation(meetingId, podId, presentationId)
const oldPresentation = Presentations.findOne(oldCurrent.selector); const oldPresentation = Presentations.findOne(oldCurrent.selector);
const newPresentation = Presentations.findOne(newCurrent.selector); const newPresentation = Presentations.findOne(newCurrent.selector);
// We update it before unset current to avoid the case where theres no current presentation.
if (newPresentation) {
try{
Presentations.update(newPresentation._id, newCurrent.modifier);
} catch(e){
newCurrent.callback(e);
}
}
if (oldPresentation) { if (oldPresentation) {
try{ try{
@ -59,12 +67,5 @@ export default function setCurrentPresentation(meetingId, podId, presentationId)
} }
} }
if (newPresentation) {
try{
Presentations.update(newPresentation._id, newCurrent.modifier);
} catch(e){
newCurrent.callback(e);
}
}
} }

View File

@ -13,7 +13,7 @@ const oldParameters = {
displayBrandingArea: 'bbb_display_branding_area', displayBrandingArea: 'bbb_display_branding_area',
enableVideo: 'bbb_enable_video', enableVideo: 'bbb_enable_video',
forceListenOnly: 'bbb_force_listen_only', forceListenOnly: 'bbb_force_listen_only',
hidePresentation: 'bbb_hide_presentation', hidePresentationOnJoin: 'bbb_hide_presentation',
listenOnlyMode: 'bbb_listen_only_mode', listenOnlyMode: 'bbb_listen_only_mode',
multiUserPenOnly: 'bbb_multi_user_pen_only', multiUserPenOnly: 'bbb_multi_user_pen_only',
multiUserTools: 'bbb_multi_user_tools', multiUserTools: 'bbb_multi_user_tools',
@ -56,7 +56,7 @@ const currentParameters = [
'bbb_custom_style', 'bbb_custom_style',
'bbb_custom_style_url', 'bbb_custom_style_url',
// LAYOUT // LAYOUT
'bbb_hide_presentation', 'bbb_hide_presentation_on_join',
'bbb_show_participants_on_login', 'bbb_show_participants_on_login',
'bbb_show_public_chat_on_login', 'bbb_show_public_chat_on_login',
'bbb_hide_actions_bar', 'bbb_hide_actions_bar',

View File

@ -16,7 +16,7 @@ export default function userLeftFlagUpdated(meetingId, userId, left) {
try { try {
const numberAffected = Users.update(selector, modifier); const numberAffected = Users.update(selector, modifier);
if (numberAffected) { if (numberAffected) {
Logger.info(`Updated user ${userId} with left flag as ${left}`); Logger.info(`Updated user ${userId} with left flag as ${left} in meeting ${meetingId}`);
} }
} catch (err) { } catch (err) {
Logger.error(`Changed user role: ${err}`); Logger.error(`Changed user role: ${err}`);

View File

@ -21,9 +21,11 @@ import DebugWindow from '/imports/ui/components/debug-window/component';
import { ACTIONS, PANELS } from '../../ui/components/layout/enums'; import { ACTIONS, PANELS } from '../../ui/components/layout/enums';
import { isChatEnabled } from '/imports/ui/services/features'; import { isChatEnabled } from '/imports/ui/services/features';
import { makeCall } from '/imports/ui/services/api'; import { makeCall } from '/imports/ui/services/api';
import BBBStorage from '/imports/ui/services/storage';
const CHAT_CONFIG = Meteor.settings.public.chat; const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id; const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
const USER_WAS_EJECTED = 'userWasEjected';
const HTML = document.getElementsByTagName('html')[0]; const HTML = document.getElementsByTagName('html')[0];
@ -256,6 +258,7 @@ class Base extends Component {
meetingEndedReason, meetingEndedReason,
meetingIsBreakout, meetingIsBreakout,
subscriptionsReady, subscriptionsReady,
userWasEjected,
} = this.props; } = this.props;
if ((loading || !subscriptionsReady) && !meetingHasEnded && meetingExist) { if ((loading || !subscriptionsReady) && !meetingHasEnded && meetingExist) {
@ -270,7 +273,7 @@ class Base extends Component {
return null; return null;
} }
if (ejected) { if (ejected || userWasEjected) {
return ( return (
<MeetingEnded <MeetingEnded
code="403" code="403"
@ -280,7 +283,7 @@ class Base extends Component {
); );
} }
if (meetingHasEnded && !meetingIsBreakout) { if ((meetingHasEnded && !meetingIsBreakout) || userWasEjected) {
return ( return (
<MeetingEnded <MeetingEnded
code={codeError} code={codeError}
@ -290,7 +293,7 @@ class Base extends Component {
); );
} }
if (codeError && !meetingHasEnded) { if ((codeError && !meetingHasEnded) || userWasEjected) {
// 680 is set for the codeError when the user requests a logout. // 680 is set for the codeError when the user requests a logout.
if (codeError !== '680') { if (codeError !== '680') {
return (<ErrorScreen code={codeError} callback={() => Base.setExitReason('error')} />); return (<ErrorScreen code={codeError} callback={() => Base.setExitReason('error')} />);
@ -386,6 +389,10 @@ export default withTracker(() => {
const { connectionID, connectionAuthTime } = Auth; const { connectionID, connectionAuthTime } = Auth;
const connectionIdUpdateTime = User?.connectionIdUpdateTime; const connectionIdUpdateTime = User?.connectionIdUpdateTime;
if (ejected) {
BBBStorage.setItem(USER_WAS_EJECTED, ejected);
}
if (currentConnectionId && currentConnectionId !== connectionID && connectionIdUpdateTime > connectionAuthTime) { if (currentConnectionId && currentConnectionId !== connectionID && connectionIdUpdateTime > connectionAuthTime) {
Session.set('codeError', '409'); Session.set('codeError', '409');
Session.set('errorMessageDescription', 'joined_another_window_reason') Session.set('errorMessageDescription', 'joined_another_window_reason')
@ -397,6 +404,7 @@ export default withTracker(() => {
const { streams: usersVideo } = VideoService.getVideoStreams(); const { streams: usersVideo } = VideoService.getVideoStreams();
return { return {
userWasEjected: BBBStorage.getItem(USER_WAS_EJECTED),
approved, approved,
ejected, ejected,
ejectedReason, ejectedReason,

View File

@ -11,6 +11,8 @@ import BBBMenu from '/imports/ui/components/common/menu/component';
import Styled from './styles'; import Styled from './styles';
import { colorPrimary } from '/imports/ui/stylesheets/styled-components/palette'; import { colorPrimary } from '/imports/ui/stylesheets/styled-components/palette';
import { PANELS, ACTIONS, LAYOUT_TYPE } from '../../layout/enums'; import { PANELS, ACTIONS, LAYOUT_TYPE } from '../../layout/enums';
import { isPresentationEnabled } from '/imports/ui/services/features';
import {isLayoutsEnabled} from '/imports/ui/services/features';
const propTypes = { const propTypes = {
amIPresenter: PropTypes.bool.isRequired, amIPresenter: PropTypes.bool.isRequired,
@ -154,9 +156,9 @@ class ActionsDropdown extends PureComponent {
const actions = []; const actions = [];
if (amIPresenter) { if (amIPresenter && isPresentationEnabled()) {
actions.push({ actions.push({
icon: "presentation", icon: "upload",
dataTest: "managePresentations", dataTest: "managePresentations",
label: formatMessage(presentationLabel), label: formatMessage(presentationLabel),
key: this.presentationItemId, key: this.presentationItemId,
@ -218,21 +220,25 @@ class ActionsDropdown extends PureComponent {
}) })
} }
if (amIPresenter && showPushLayout) { if (amIPresenter && showPushLayout && isLayoutsEnabled()) {
actions.push({ actions.push({
icon: 'send', icon: 'send',
label: intl.formatMessage(intlMessages.propagateLayoutLabel), label: intl.formatMessage(intlMessages.propagateLayoutLabel),
key: 'propagate layout', key: 'propagate layout',
onClick: amIPresenter ? setMeetingLayout : setPushLayout, onClick: amIPresenter ? setMeetingLayout : setPushLayout,
dataTest: 'propagateLayout',
}); });
} }
if (isLayoutsEnabled()){
actions.push({ actions.push({
icon: 'send', icon: 'send',
label: intl.formatMessage(intlMessages.layoutModal), label: intl.formatMessage(intlMessages.layoutModal),
key: 'layoutModal', key: 'layoutModal',
onClick: () => mountModal(<LayoutModalContainer {...this.props} />), onClick: () => mountModal(<LayoutModalContainer {...this.props} />),
dataTest: 'layoutModal',
}); });
}
return actions; return actions;
} }

View File

@ -9,6 +9,7 @@ import AudioControlsContainer from '../audio/audio-controls/container';
import JoinVideoOptionsContainer from '../video-provider/video-button/container'; import JoinVideoOptionsContainer from '../video-provider/video-button/container';
import PresentationOptionsContainer from './presentation-options/component'; import PresentationOptionsContainer from './presentation-options/component';
import RaiseHandDropdownContainer from './raise-hand/container'; import RaiseHandDropdownContainer from './raise-hand/container';
import { isPresentationEnabled } from '/imports/ui/services/features';
class ActionsBar extends PureComponent { class ActionsBar extends PureComponent {
render() { render() {
@ -21,6 +22,7 @@ class ActionsBar extends PureComponent {
handleTakePresenter, handleTakePresenter,
intl, intl,
isSharingVideo, isSharingVideo,
isSharedNotesPinned,
hasScreenshare, hasScreenshare,
stopExternalVideoShare, stopExternalVideoShare,
isCaptionsAvailable, isCaptionsAvailable,
@ -39,6 +41,8 @@ class ActionsBar extends PureComponent {
setPushLayout, setPushLayout,
} = this.props; } = this.props;
const shouldShowOptionsButton = (isPresentationEnabled() && isThereCurrentPresentation)
|| isSharingVideo || hasScreenshare || isSharedNotesPinned;
return ( return (
<Styled.ActionsBar <Styled.ActionsBar
style={ style={
@ -90,6 +94,7 @@ class ActionsBar extends PureComponent {
/> />
</Styled.Center> </Styled.Center>
<Styled.Right> <Styled.Right>
{ shouldShowOptionsButton ?
<PresentationOptionsContainer <PresentationOptionsContainer
presentationIsOpen={presentationIsOpen} presentationIsOpen={presentationIsOpen}
setPresentationIsOpen={setPresentationIsOpen} setPresentationIsOpen={setPresentationIsOpen}
@ -97,7 +102,10 @@ class ActionsBar extends PureComponent {
hasPresentation={isThereCurrentPresentation} hasPresentation={isThereCurrentPresentation}
hasExternalVideo={isSharingVideo} hasExternalVideo={isSharingVideo}
hasScreenshare={hasScreenshare} hasScreenshare={hasScreenshare}
hasPinnedSharedNotes={isSharedNotesPinned}
/> />
: null
}
{isRaiseHandButtonEnabled {isRaiseHandButtonEnabled
? ( ? (
<RaiseHandDropdownContainer {...{ <RaiseHandDropdownContainer {...{

View File

@ -14,7 +14,7 @@ import ExternalVideoService from '/imports/ui/components/external-video-player/s
import CaptionsService from '/imports/ui/components/captions/service'; import CaptionsService from '/imports/ui/components/captions/service';
import { layoutSelectOutput, layoutDispatch } from '../layout/context'; import { layoutSelectOutput, layoutDispatch } from '../layout/context';
import { isVideoBroadcasting } from '/imports/ui/components/screenshare/service'; import { isVideoBroadcasting } from '/imports/ui/components/screenshare/service';
import { isExternalVideoEnabled, isPollingEnabled } from '/imports/ui/services/features'; import { isExternalVideoEnabled, isPollingEnabled, isPresentationEnabled } from '/imports/ui/services/features';
import MediaService from '../media/service'; import MediaService from '../media/service';
@ -57,10 +57,11 @@ export default withTracker(() => ({
currentSlidHasContent: PresentationService.currentSlidHasContent(), currentSlidHasContent: PresentationService.currentSlidHasContent(),
parseCurrentSlideContent: PresentationService.parseCurrentSlideContent, parseCurrentSlideContent: PresentationService.parseCurrentSlideContent,
isSharingVideo: Service.isSharingVideo(), isSharingVideo: Service.isSharingVideo(),
isSharedNotesPinned: Service.isSharedNotesPinned(),
hasScreenshare: isVideoBroadcasting(), hasScreenshare: isVideoBroadcasting(),
isCaptionsAvailable: CaptionsService.isCaptionsAvailable(), isCaptionsAvailable: CaptionsService.isCaptionsAvailable(),
isMeteorConnected: Meteor.status().connected, isMeteorConnected: Meteor.status().connected,
isPollingEnabled: isPollingEnabled(), isPollingEnabled: isPollingEnabled() && isPresentationEnabled(),
isSelectRandomUserEnabled: SELECT_RANDOM_USER_ENABLED, isSelectRandomUserEnabled: SELECT_RANDOM_USER_ENABLED,
isRaiseHandButtonEnabled: RAISE_HAND_BUTTON_ENABLED, isRaiseHandButtonEnabled: RAISE_HAND_BUTTON_ENABLED,
isThereCurrentPresentation: Presentations.findOne({ meetingId: Auth.meetingID, current: true }, isThereCurrentPresentation: Presentations.findOne({ meetingId: Auth.meetingID, current: true },

View File

@ -37,6 +37,7 @@ const PresentationOptionsContainer = ({
hasPresentation, hasPresentation,
hasExternalVideo, hasExternalVideo,
hasScreenshare, hasScreenshare,
hasPinnedSharedNotes,
}) => { }) => {
let buttonType = 'presentation'; let buttonType = 'presentation';
if (hasExternalVideo) { if (hasExternalVideo) {
@ -46,7 +47,7 @@ const PresentationOptionsContainer = ({
buttonType = 'desktop'; buttonType = 'desktop';
} }
const isThereCurrentPresentation = hasExternalVideo || hasScreenshare || hasPresentation; const isThereCurrentPresentation = hasExternalVideo || hasScreenshare || hasPresentation || hasPinnedSharedNotes;
return ( return (
<Button <Button
icon={`${buttonType}${!presentationIsOpen ? '_off' : ''}`} icon={`${buttonType}${!presentationIsOpen ? '_off' : ''}`}

View File

@ -5,6 +5,7 @@ import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
import BBBMenu from '/imports/ui/components/common/menu/component'; import BBBMenu from '/imports/ui/components/common/menu/component';
import Button from '/imports/ui/components/common/button/component'; import Button from '/imports/ui/components/common/button/component';
import Styled from './styles'; import Styled from './styles';
import { EMOJI_STATUSES } from '/imports/utils/statuses';
const propTypes = { const propTypes = {
intl: PropTypes.shape({ intl: PropTypes.shape({
@ -22,15 +23,27 @@ const intlMessages = defineMessages({
id: 'app.actionsBar.emojiMenu.statusTriggerLabel', id: 'app.actionsBar.emojiMenu.statusTriggerLabel',
description: 'label for option to show emoji menu', description: 'label for option to show emoji menu',
}, },
clearStatusLabel: {
id: 'app.actionsBar.emojiMenu.noneLabel',
description: 'label for status clearing',
},
}); });
class RaiseHandDropdown extends PureComponent { class RaiseHandDropdown extends PureComponent {
constructor(props) {
super(props);
this.state = {
isHandRaised: false,
};
}
getAvailableActions() { getAvailableActions() {
const { const {
userId, userId,
getEmojiList, getEmojiList,
setEmojiStatus, setEmojiStatus,
intl, intl,
currentUser,
} = this.props; } = this.props;
const actions = []; const actions = [];
@ -41,6 +54,11 @@ class RaiseHandDropdown extends PureComponent {
key: s, key: s,
label: intl.formatMessage({ id: `app.actionsBar.emojiMenu.${s}Label` }), label: intl.formatMessage({ id: `app.actionsBar.emojiMenu.${s}Label` }),
onClick: () => { onClick: () => {
if (currentUser.emoji === 'raiseHand') {
this.setState({
isHandRaised: true,
});
}
setEmojiStatus(userId, s); setEmojiStatus(userId, s);
}, },
icon: getEmojiList[s], icon: getEmojiList[s],
@ -57,30 +75,51 @@ class RaiseHandDropdown extends PureComponent {
shortcuts, shortcuts,
} = this.props; } = this.props;
return ( const {
<Button isHandRaised,
icon="hand" } = this.state;
label={intl.formatMessage({
id: `app.actionsBar.emojiMenu.${ const label = currentUser.emoji !== 'raiseHand' && currentUser.emoji !== 'none'
? intlMessages.clearStatusLabel
: {id: `app.actionsBar.emojiMenu.${
currentUser.emoji === 'raiseHand' currentUser.emoji === 'raiseHand'
? 'lowerHandLabel' ? 'lowerHandLabel'
: 'raiseHandLabel' : 'raiseHandLabel'
}`, }`,
})} };
return (
<Button
icon={EMOJI_STATUSES[currentUser.emoji === 'none'
? 'raiseHand' : currentUser.emoji]}
label={intl.formatMessage(
label,
)}
accessKey={shortcuts.raisehand} accessKey={shortcuts.raisehand}
color={currentUser.emoji === 'raiseHand' ? 'primary' : 'default'} color={currentUser.emoji !== 'none' ? 'primary' : 'default'}
data-test={currentUser.emoji === 'raiseHand' ? 'lowerHandLabel' : 'raiseHandLabel'} data-test={currentUser.emoji === 'raiseHand' ? 'lowerHandLabel' : 'raiseHandLabel'}
ghost={currentUser.emoji !== 'raiseHand'} ghost={currentUser.emoji === 'none'}
emoji={currentUser.emoji} emoji={currentUser.emoji}
hideLabel hideLabel
circle circle
size="lg" size="lg"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
if (currentUser.emoji !== 'none'
&& currentUser.emoji !== 'raiseHand') {
setEmojiStatus(
currentUser.userId,
isHandRaised ? 'raiseHand' : 'none',
);
} else {
this.setState({
isHandRaised: false,
});
setEmojiStatus( setEmojiStatus(
currentUser.userId, currentUser.userId,
currentUser.emoji === 'raiseHand' ? 'none' : 'raiseHand', currentUser.emoji === 'raiseHand' ? 'none' : 'raiseHand',
); );
}
}} }}
/> />
); );

View File

@ -4,6 +4,7 @@ import { makeCall } from '/imports/ui/services/api';
import Meetings from '/imports/api/meetings'; import Meetings from '/imports/api/meetings';
import Breakouts from '/imports/api/breakouts'; import Breakouts from '/imports/api/breakouts';
import { getVideoUrl } from '/imports/ui/components/external-video-player/service'; import { getVideoUrl } from '/imports/ui/components/external-video-player/service';
import NotesService from '/imports/ui/components/notes/service';
import BreakoutsHistory from '/imports/api/breakouts-history'; import BreakoutsHistory from '/imports/api/breakouts-history';
const USER_CONFIG = Meteor.settings.public.user; const USER_CONFIG = Meteor.settings.public.user;
@ -73,5 +74,6 @@ export default {
getLastBreakouts, getLastBreakouts,
getUsersNotJoined, getUsersNotJoined,
takePresenterRole, takePresenterRole,
isSharedNotesPinned: () => NotesService.isSharedNotesPinned(),
isSharingVideo: () => getVideoUrl(), isSharingVideo: () => getVideoUrl(),
}; };

View File

@ -488,6 +488,7 @@ class App extends Component {
pushLayoutMeeting, pushLayoutMeeting,
selectedLayout, selectedLayout,
setMeetingLayout, setMeetingLayout,
setPushLayout,
shouldShowScreenshare, shouldShowScreenshare,
shouldShowExternalVideo, shouldShowExternalVideo,
} = this.props; } = this.props;
@ -517,6 +518,7 @@ class App extends Component {
pushLayoutMeeting, pushLayoutMeeting,
selectedLayout, selectedLayout,
setMeetingLayout, setMeetingLayout,
setPushLayout,
shouldShowScreenshare, shouldShowScreenshare,
shouldShowExternalVideo, shouldShowExternalVideo,
}} }}

View File

@ -15,6 +15,7 @@ import UserInfos from '/imports/api/users-infos';
import Settings from '/imports/ui/services/settings'; import Settings from '/imports/ui/services/settings';
import MediaService from '/imports/ui/components/media/service'; import MediaService from '/imports/ui/components/media/service';
import LayoutService from '/imports/ui/components/layout/service'; import LayoutService from '/imports/ui/components/layout/service';
import { isPresentationEnabled } from '/imports/ui/services/features';
import _ from 'lodash'; import _ from 'lodash';
import { import {
layoutSelect, layoutSelect,
@ -59,6 +60,8 @@ const AppContainer = (props) => {
return ref.current; return ref.current;
} }
const layoutType = useRef(null);
const { const {
actionsbar, actionsbar,
selectedLayout, selectedLayout,
@ -92,12 +95,23 @@ const AppContainer = (props) => {
const { sidebarContentPanel, isOpen: sidebarContentIsOpen } = sidebarContent; const { sidebarContentPanel, isOpen: sidebarContentIsOpen } = sidebarContent;
const { sidebarNavPanel, isOpen: sidebarNavigationIsOpen } = sidebarNavigation; const { sidebarNavPanel, isOpen: sidebarNavigationIsOpen } = sidebarNavigation;
const { isOpen: presentationIsOpen } = presentation; const { isOpen } = presentation;
const shouldShowPresentation = propsShouldShowPresentation const presentationIsOpen = isOpen;
&& (presentationIsOpen || presentationRestoreOnUpdate);
const shouldShowPresentation = (propsShouldShowPresentation
&& (presentationIsOpen || presentationRestoreOnUpdate)) && isPresentationEnabled();
const { focusedId } = cameraDock; const { focusedId } = cameraDock;
if(
layoutContextDispatch
&& (typeof meetingLayout != "undefined")
&& (layoutType.current != meetingLayout)
) {
layoutType.current = meetingLayout;
MediaService.setPresentationIsOpen(layoutContextDispatch, true);
}
const horizontalPosition = cameraDock.position === 'contentLeft' || cameraDock.position === 'contentRight'; const horizontalPosition = cameraDock.position === 'contentLeft' || cameraDock.position === 'contentRight';
// this is not exactly right yet // this is not exactly right yet
let presentationVideoRate; let presentationVideoRate;
@ -132,6 +146,9 @@ const AppContainer = (props) => {
}); });
}; };
useEffect(() => {
MediaService.buildLayoutWhenPresentationAreaIsDisabled(layoutContextDispatch)});
return currentUserId return currentUserId
? ( ? (
<App <App
@ -308,7 +325,7 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
'bbb_force_restore_presentation_on_new_events', 'bbb_force_restore_presentation_on_new_events',
Meteor.settings.public.presentation.restoreOnUpdate, Meteor.settings.public.presentation.restoreOnUpdate,
), ),
hidePresentation: getFromUserSettings('bbb_hide_presentation', LAYOUT_CONFIG.hidePresentation), hidePresentationOnJoin: getFromUserSettings('bbb_hide_presentation_on_join', LAYOUT_CONFIG.hidePresentationOnJoin),
hideActionsBar: getFromUserSettings('bbb_hide_actions_bar', false), hideActionsBar: getFromUserSettings('bbb_hide_actions_bar', false),
isModalOpen: !!getModal(), isModalOpen: !!getModal(),
ignorePollNotifications: Session.get('ignorePollNotifications'), ignorePollNotifications: Session.get('ignorePollNotifications'),

View File

@ -69,7 +69,7 @@ const Select = ({
if (voices.length === 0) { if (voices.length === 0) {
return ( return (
<div <div data-test="speechRecognition"
style={{ style={{
fontSize: '.75rem', fontSize: '.75rem',
padding: '1rem 0', padding: '1rem 0',

View File

@ -1,11 +0,0 @@
import React from 'react';
const BreakoutRemainingTime = props => (
<span data-test="timeRemaining">
{props.children}
</span>
);
export default BreakoutRemainingTime;

View File

@ -5,7 +5,7 @@ import { Session } from 'meteor/session';
import logger from '/imports/startup/client/logger'; import logger from '/imports/startup/client/logger';
import Styled from './styles'; import Styled from './styles';
import Service from './service'; import Service from './service';
import BreakoutRoomContainer from './breakout-remaining-time/container'; import MeetingRemainingTime from '../notifications-bar/meeting-remaining-time/container';
import MessageFormContainer from './message-form/container'; import MessageFormContainer from './message-form/container';
import VideoService from '/imports/ui/components/video-provider/service'; import VideoService from '/imports/ui/components/video-provider/service';
import { PANELS, ACTIONS } from '../layout/enums'; import { PANELS, ACTIONS } from '../layout/enums';
@ -495,7 +495,7 @@ class BreakoutRoom extends PureComponent {
ref={(ref) => this.durationContainerRef = ref} ref={(ref) => this.durationContainerRef = ref}
> >
<Styled.Duration> <Styled.Duration>
<BreakoutRoomContainer <MeetingRemainingTime
messageDuration={intlMessages.breakoutDuration} messageDuration={intlMessages.breakoutDuration}
breakoutRoom={breakoutRooms[0]} breakoutRoom={breakoutRooms[0]}
fromBreakoutPanel fromBreakoutPanel

View File

@ -158,6 +158,16 @@ class TimeWindowList extends PureComponent {
) { ) {
this.listRef.forceUpdateGrid(); this.listRef.forceUpdateGrid();
} }
const msgListItem = document.querySelector('span[data-test="msgListItem"]');
if (msgListItem) {
const virtualizedGridInnerScrollContainer = msgListItem.parentElement;
const virtualizedGrid = virtualizedGridInnerScrollContainer.parentElement;
virtualizedGridInnerScrollContainer.setAttribute('role', 'list');
virtualizedGridInnerScrollContainer.setAttribute('tabIndex', '0');
virtualizedGrid.removeAttribute('tabIndex');
virtualizedGrid.removeAttribute('aria-label');
}
} }
handleScrollUpdate(position, target) { handleScrollUpdate(position, target) {
@ -219,6 +229,8 @@ class TimeWindowList extends PureComponent {
<span <span
style={style} style={style}
key={`span-${key}-${index}`} key={`span-${key}-${index}`}
role="listitem"
data-test="msgListItem"
> >
<TimeWindowChatItem <TimeWindowChatItem
key={key} key={key}

View File

@ -1,78 +0,0 @@
import Auth from '/imports/ui/services/auth';
import { throttle } from 'lodash';
import logger from '/imports/startup/client/logger';
const Cursor = new Mongo.Collection(null);
let cursorStreamListener = null;
export const clearCursors = () => {
Cursor.remove({});
};
function updateCursor(userId, payload) {
const selector = {
userId,
whiteboardId: payload.whiteboardId,
};
const modifier = {
$set: {
userId,
...payload,
},
};
return Cursor.upsert(selector, modifier);
}
export function publishCursorUpdate(payload) {
if (cursorStreamListener) {
const throttledEmit = throttle(cursorStreamListener.emit.bind(cursorStreamListener), 30, { trailing: true });
throttledEmit('publish', payload);
}
return updateCursor(Auth.userID, payload);
}
export function initCursorStreamListener() {
logger.info({
logCode: 'init_cursor_stream_listener',
extraInfo: { meetingId: Auth.meetingID, userId: Auth.userID },
}, 'initCursorStreamListener called');
/**
* We create a promise to add the handlers after a ddp subscription stop.
* The problem was caused because we add handlers to stream before the onStop event happens,
* which set the handlers to undefined.
*/
cursorStreamListener = new Meteor.Streamer(`cursor-${Auth.meetingID}`, { retransmit: false });
const startStreamHandlersPromise = new Promise((resolve) => {
const checkStreamHandlersInterval = setInterval(() => {
const streamHandlersSize = Object.values(Meteor.StreamerCentral.instances[`cursor-${Auth.meetingID}`].handlers)
.filter(el => el != undefined)
.length;
if (!streamHandlersSize) {
resolve(clearInterval(checkStreamHandlersInterval));
}
}, 250);
});
startStreamHandlersPromise.then(() => {
logger.debug({ logCode: 'cursor_stream_handler_attach' }, 'Attaching handlers for cursor stream');
cursorStreamListener.on('message', ({ cursors }) => {
Object.keys(cursors).forEach((cursorId) => {
const cursor = cursors[cursorId];
const userId = cursor.userId;
delete cursor.userId;
if (Auth.userID === userId) return;
updateCursor(userId, cursor);
});
});
});
}
export default Cursor;

View File

@ -215,7 +215,7 @@ class VideoPlayer extends Component {
componentWillUnmount() { componentWillUnmount() {
const { const {
layoutContextDispatch, layoutContextDispatch,
hidePresentation, hidePresentationOnJoin,
} = this.props; } = this.props;
window.removeEventListener('beforeunload', this.onBeforeUnload); window.removeEventListener('beforeunload', this.onBeforeUnload);
@ -232,7 +232,7 @@ class VideoPlayer extends Component {
value: false, value: false,
}); });
if (hidePresentation) { if (hidePresentationOnJoin) {
layoutContextDispatch({ layoutContextDispatch({
type: ACTIONS.SET_PRESENTATION_IS_OPEN, type: ACTIONS.SET_PRESENTATION_IS_OPEN,
value: false, value: false,

View File

@ -105,6 +105,7 @@ class ExternalVideoModal extends Component {
<label htmlFor="video-modal-input"> <label htmlFor="video-modal-input">
{intl.formatMessage(intlMessages.input)} {intl.formatMessage(intlMessages.input)}
<input <input
autoFocus
id="video-modal-input" id="video-modal-input"
onChange={this.updateVideoUrlHandler} onChange={this.updateVideoUrlHandler}
name="video-modal-input" name="video-modal-input"

View File

@ -4,6 +4,7 @@ import { layoutDispatch, layoutSelect, layoutSelectInput } from '/imports/ui/com
import DEFAULT_VALUES from '/imports/ui/components/layout/defaultValues'; import DEFAULT_VALUES from '/imports/ui/components/layout/defaultValues';
import { INITIAL_INPUT_STATE } from '/imports/ui/components/layout/initState'; import { INITIAL_INPUT_STATE } from '/imports/ui/components/layout/initState';
import { ACTIONS, PANELS, CAMERADOCK_POSITION } from '/imports/ui/components/layout/enums'; import { ACTIONS, PANELS, CAMERADOCK_POSITION } from '/imports/ui/components/layout/enums';
import { isPresentationEnabled } from '/imports/ui/services/features';
const windowWidth = () => window.document.documentElement.clientWidth; const windowWidth = () => window.document.documentElement.clientWidth;
const windowHeight = () => window.document.documentElement.clientHeight; const windowHeight = () => window.document.documentElement.clientHeight;
@ -34,6 +35,7 @@ const SmartLayout = (props) => {
const navbarInput = layoutSelectInput((i) => i.navBar); const navbarInput = layoutSelectInput((i) => i.navBar);
const externalVideoInput = layoutSelectInput((i) => i.externalVideo); const externalVideoInput = layoutSelectInput((i) => i.externalVideo);
const screenShareInput = layoutSelectInput((i) => i.screenShare); const screenShareInput = layoutSelectInput((i) => i.screenShare);
const sharedNotesInput = layoutSelectInput((i) => i.sharedNotes);
const layoutContextDispatch = layoutDispatch(); const layoutContextDispatch = layoutDispatch();
const prevDeviceType = usePrevious(deviceType); const prevDeviceType = usePrevious(deviceType);
@ -262,11 +264,12 @@ const SmartLayout = (props) => {
const { isOpen, currentSlide } = presentationInput; const { isOpen, currentSlide } = presentationInput;
const { hasExternalVideo } = externalVideoInput; const { hasExternalVideo } = externalVideoInput;
const { hasScreenShare } = screenShareInput; const { hasScreenShare } = screenShareInput;
const { isPinned: isSharedNotesPinned } = sharedNotesInput;
const mediaBounds = {}; const mediaBounds = {};
const { element: fullscreenElement } = fullscreen; const { element: fullscreenElement } = fullscreen;
const { num: currentSlideNumber } = currentSlide; const { num: currentSlideNumber } = currentSlide;
if (!isOpen || (currentSlideNumber === 0 && !hasExternalVideo && !hasScreenShare)) { if (!isOpen || ((isPresentationEnabled() && currentSlideNumber === 0) && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned)) {
mediaBounds.width = 0; mediaBounds.width = 0;
mediaBounds.height = 0; mediaBounds.height = 0;
mediaBounds.top = 0; mediaBounds.top = 0;

View File

@ -14,6 +14,7 @@ const LayoutModalComponent = (props) => {
const { const {
intl, intl,
closeModal, closeModal,
isModerator,
isPresenter, isPresenter,
showToggleLabel, showToggleLabel,
application, application,
@ -101,7 +102,7 @@ const LayoutModalComponent = (props) => {
}; };
const renderPushLayoutsOptions = () => { const renderPushLayoutsOptions = () => {
if (!isPresenter) { if (!isModerator && !isPresenter) {
return null; return null;
} }
@ -142,6 +143,7 @@ const LayoutModalComponent = (props) => {
onClick={() => handleSwitchLayout(layout)} onClick={() => handleSwitchLayout(layout)}
active={(layout === selectedLayout).toString()} active={(layout === selectedLayout).toString()}
aria-describedby="layout-btn-desc" aria-describedby="layout-btn-desc"
data-test={`${layout}Layout`}
/> />
</Styled.ButtonLayoutContainer> </Styled.ButtonLayoutContainer>
))} ))}
@ -186,6 +188,7 @@ const propTypes = {
formatMessage: PropTypes.func.isRequired, formatMessage: PropTypes.func.isRequired,
}).isRequired, }).isRequired,
closeModal: PropTypes.func.isRequired, closeModal: PropTypes.func.isRequired,
isModerator: PropTypes.bool.isRequired,
isPresenter: PropTypes.bool.isRequired, isPresenter: PropTypes.bool.isRequired,
showToggleLabel: PropTypes.bool.isRequired, showToggleLabel: PropTypes.bool.isRequired,
application: PropTypes.shape({ application: PropTypes.shape({

View File

@ -8,7 +8,7 @@ import { LAYOUT_TYPE, ACTIONS } from '../enums';
import { isMobile } from '../utils'; import { isMobile } from '../utils';
import { updateSettings } from '/imports/ui/components/settings/service'; import { updateSettings } from '/imports/ui/components/settings/service';
const HIDE_PRESENTATION = Meteor.settings.public.layout.hidePresentation; const HIDE_PRESENTATION = Meteor.settings.public.layout.hidePresentationOnJoin;
const equalDouble = (n1, n2) => { const equalDouble = (n1, n2) => {
const precision = 0.01; const precision = 0.01;
@ -39,6 +39,7 @@ const propTypes = {
pushLayoutMeeting: PropTypes.bool, pushLayoutMeeting: PropTypes.bool,
selectedLayout: PropTypes.string, selectedLayout: PropTypes.string,
setMeetingLayout: PropTypes.func, setMeetingLayout: PropTypes.func,
setPushLayout: PropTypes.func,
shouldShowScreenshare: PropTypes.bool, shouldShowScreenshare: PropTypes.bool,
shouldShowExternalVideo: PropTypes.bool, shouldShowExternalVideo: PropTypes.bool,
}; };
@ -73,7 +74,7 @@ class PushLayoutEngine extends React.Component {
} }
Settings.save(); Settings.save();
const initialPresentation = !getFromUserSettings('bbb_hide_presentation', HIDE_PRESENTATION || !meetingPresentationIsOpen) || shouldShowScreenshare || shouldShowExternalVideo; const initialPresentation = !getFromUserSettings('bbb_hide_presentation_on_join', HIDE_PRESENTATION || !meetingPresentationIsOpen) || shouldShowScreenshare || shouldShowExternalVideo;
MediaService.setPresentationIsOpen(layoutContextDispatch, initialPresentation); MediaService.setPresentationIsOpen(layoutContextDispatch, initialPresentation);
if (selectedLayout === 'custom') { if (selectedLayout === 'custom') {
@ -136,6 +137,7 @@ class PushLayoutEngine extends React.Component {
pushLayoutMeeting, pushLayoutMeeting,
selectedLayout, selectedLayout,
setMeetingLayout, setMeetingLayout,
setPushLayout,
} = this.props; } = this.props;
const meetingLayoutDidChange = meetingLayout !== prevProps.meetingLayout; const meetingLayoutDidChange = meetingLayout !== prevProps.meetingLayout;
@ -240,9 +242,13 @@ class PushLayoutEngine extends React.Component {
|| focusedCamera !== prevProps.focusedCamera || focusedCamera !== prevProps.focusedCamera
|| !equalDouble(presentationVideoRate, prevProps.presentationVideoRate); || !equalDouble(presentationVideoRate, prevProps.presentationVideoRate);
if ((pushLayout && layoutChanged) // change layout sizes / states if (pushLayout !== prevProps.pushLayout) { // push layout once after presenter toggles / special case where we set pushLayout to false in all viewers
|| (pushLayout !== prevProps.pushLayout) // push layout once after presenter toggles / special case where we set pushLayout to false in all viewers if (isModerator) {
) { setPushLayout(pushLayout);
}
}
if (pushLayout && layoutChanged || pushLayout !== prevProps.pushLayout) { // change layout sizes / states
if (isPresenter) { if (isPresenter) {
setMeetingLayout(); setMeetingLayout();
} }

View File

@ -1,12 +1,15 @@
import Presentations from '/imports/api/presentations'; import Presentations from '/imports/api/presentations';
import { isVideoBroadcasting } from '/imports/ui/components/screenshare/service';
import { getVideoUrl } from '/imports/ui/components/external-video-player/service';
import Settings from '/imports/ui/services/settings'; import Settings from '/imports/ui/services/settings';
import getFromUserSettings from '/imports/ui/services/users-settings'; import getFromUserSettings from '/imports/ui/services/users-settings';
import { isExternalVideoEnabled, isScreenSharingEnabled } from '/imports/ui/services/features'; import { isExternalVideoEnabled, isScreenSharingEnabled } from '/imports/ui/services/features';
import { ACTIONS } from '../layout/enums'; import { ACTIONS } from '../layout/enums';
import UserService from '/imports/ui/components/user-list/service'; import UserService from '/imports/ui/components/user-list/service';
import NotesService from '/imports/ui/components/notes/service'; import NotesService from '/imports/ui/components/notes/service';
import { getVideoUrl } from '/imports/ui/components/external-video-player/service';
import VideoStreams from '/imports/api/video-streams';
import { isPresentationEnabled } from '/imports/ui/services/features';
import { isVideoBroadcasting } from '/imports/ui/components/screenshare/service';
import Auth from '/imports/ui/services/auth/index';
const LAYOUT_CONFIG = Meteor.settings.public.layout; const LAYOUT_CONFIG = Meteor.settings.public.layout;
const KURENTO_CONFIG = Meteor.settings.public.kurento; const KURENTO_CONFIG = Meteor.settings.public.kurento;
@ -50,7 +53,32 @@ const setPresentationIsOpen = (layoutContextDispatch, value) => {
}); });
}; };
const isThereWebcamOn = (meetingID) => {
return VideoStreams.find({
meetingId: meetingID
}).count() > 0;
}
const buildLayoutWhenPresentationAreaIsDisabled = (layoutContextDispatch) => {
const isSharingVideo = getVideoUrl();
const isSharedNotesPinned = NotesService.isSharedNotesPinned();
const hasScreenshare = isVideoBroadcasting();
const isThereWebcam = isThereWebcamOn(Auth.meetingID);
const isGeneralMediaOff = !hasScreenshare && !isSharedNotesPinned && !isSharingVideo
const webcamIsOnlyContent = isThereWebcam && isGeneralMediaOff;
const isThereNoMedia = !isThereWebcam && isGeneralMediaOff;
const isPresentationDisabled = !isPresentationEnabled();
if (isPresentationDisabled && (webcamIsOnlyContent || isThereNoMedia)) {
setPresentationIsOpen(layoutContextDispatch, false);
}
}
export default { export default {
buildLayoutWhenPresentationAreaIsDisabled,
getPresentationInfo, getPresentationInfo,
shouldShowWhiteboard, shouldShowWhiteboard,
shouldShowScreenshare, shouldShowScreenshare,

View File

@ -178,6 +178,8 @@ class NavBar extends Component {
ariaLabel += hasNotification ? (` ${intl.formatMessage(intlMessages.newMessages)}`) : ''; ariaLabel += hasNotification ? (` ${intl.formatMessage(intlMessages.newMessages)}`) : '';
const isExpanded = sidebarNavigation.isOpen; const isExpanded = sidebarNavigation.isOpen;
const { isPhone } = deviceInfo;
const { acs } = this.state; const { acs } = this.state;
@ -214,7 +216,7 @@ class NavBar extends Component {
&& <Styled.ArrowLeft iconName="left_arrow" />} && <Styled.ArrowLeft iconName="left_arrow" />}
<Styled.NavbarToggleButton <Styled.NavbarToggleButton
onClick={this.handleToggleUserList} onClick={this.handleToggleUserList}
color='dark' color={isPhone && isExpanded ? 'primary' : 'dark'}
size='md' size='md'
circle circle
hideLabel hideLabel
@ -259,3 +261,4 @@ class NavBar extends Component {
NavBar.propTypes = propTypes; NavBar.propTypes = propTypes;
NavBar.defaultProps = defaultProps; NavBar.defaultProps = defaultProps;
export default withShortcutHelper(withModalMounter(injectIntl(NavBar)), 'toggleUserList'); export default withShortcutHelper(withModalMounter(injectIntl(NavBar)), 'toggleUserList');

View File

@ -8,7 +8,7 @@ import {
colorBackground, colorBackground,
} from '/imports/ui/stylesheets/styled-components/palette'; } from '/imports/ui/stylesheets/styled-components/palette';
import { fontSizeBase } from '/imports/ui/stylesheets/styled-components/typography'; import { fontSizeBase } from '/imports/ui/stylesheets/styled-components/typography';
import { phoneLandscape } from '/imports/ui/stylesheets/styled-components/breakpoints'; import { phoneLandscape, smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
import Button from '/imports/ui/components/common/button/component'; import Button from '/imports/ui/components/common/button/component';
const Navbar = styled.header` const Navbar = styled.header`
@ -39,6 +39,9 @@ const ArrowLeft = styled(Icon)`
font-size: 40%; font-size: 40%;
color: ${colorWhite}; color: ${colorWhite};
left: .25rem; left: .25rem;
@media ${smallOnly} {
display: none;
}
`; `;
const ArrowRight = styled(Icon)` const ArrowRight = styled(Icon)`
@ -46,6 +49,9 @@ const ArrowRight = styled(Icon)`
font-size: 40%; font-size: 40%;
color: ${colorWhite}; color: ${colorWhite};
right: .0125rem; right: .0125rem;
@media ${smallOnly} {
display: none;
}
`; `;
const Center = styled.div` const Center = styled.div`

View File

@ -9,6 +9,7 @@ import { PANELS, ACTIONS, LAYOUT_TYPE } from '../layout/enums';
import browserInfo from '/imports/utils/browserInfo'; import browserInfo from '/imports/utils/browserInfo';
import Header from '/imports/ui/components/common/control-header/component'; import Header from '/imports/ui/components/common/control-header/component';
import NotesDropdown from '/imports/ui/components/notes/notes-dropdown/container'; import NotesDropdown from '/imports/ui/components/notes/notes-dropdown/container';
import { isPresentationEnabled } from '../../services/features';
const CHAT_CONFIG = Meteor.settings.public.chat; const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id; const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
@ -96,8 +97,8 @@ const Notes = ({
useEffect(() => { useEffect(() => {
if ( if (
isOnMediaArea isOnMediaArea
&& sidebarContent.isOpen && (sidebarContent.isOpen || !isPresentationEnabled())
&& sidebarContent.sidebarContentPanel === PANELS.SHARED_NOTES && (sidebarContent.sidebarContentPanel === PANELS.SHARED_NOTES || !isPresentationEnabled())
) { ) {
if (layoutType === LAYOUT_TYPE.VIDEO_FOCUS) { if (layoutType === LAYOUT_TYPE.VIDEO_FOCUS) {
layoutContextDispatch({ layoutContextDispatch({
@ -125,6 +126,10 @@ const Notes = ({
type: ACTIONS.SET_NOTES_IS_PINNED, type: ACTIONS.SET_NOTES_IS_PINNED,
value: true, value: true,
}); });
layoutContextDispatch({
type: ACTIONS.SET_PRESENTATION_IS_OPEN,
value: true,
});
return () => { return () => {
layoutContextDispatch({ layoutContextDispatch({

View File

@ -6,7 +6,7 @@ import _ from 'lodash';
import Auth from '/imports/ui/services/auth'; import Auth from '/imports/ui/services/auth';
import { MeetingTimeRemaining } from '/imports/api/meetings'; import { MeetingTimeRemaining } from '/imports/api/meetings';
import Meetings from '/imports/api/meetings'; import Meetings from '/imports/api/meetings';
import BreakoutRemainingTime from '/imports/ui/components/breakout-room/breakout-remaining-time/container'; import MeetingRemainingTime from './meeting-remaining-time/container';
import Styled from './styles'; import Styled from './styles';
import { layoutSelectInput, layoutDispatch } from '../layout/context'; import { layoutSelectInput, layoutDispatch } from '../layout/context';
import { ACTIONS } from '../layout/enums'; import { ACTIONS } from '../layout/enums';
@ -176,7 +176,7 @@ export default injectIntl(withTracker(({ intl }) => {
if (currentBreakout) { if (currentBreakout) {
data.message = ( data.message = (
<BreakoutRemainingTime <MeetingRemainingTime
breakoutRoom={currentBreakout} breakoutRoom={currentBreakout}
messageDuration={intlMessages.breakoutTimeRemaining} messageDuration={intlMessages.breakoutTimeRemaining}
timeEndedMessage={intlMessages.breakoutWillClose} timeEndedMessage={intlMessages.breakoutWillClose}
@ -197,7 +197,7 @@ export default injectIntl(withTracker(({ intl }) => {
if (underThirtyMin && !isBreakout) { if (underThirtyMin && !isBreakout) {
data.message = ( data.message = (
<BreakoutRemainingTime <MeetingRemainingTime
breakoutRoom={meetingTimeRemaining} breakoutRoom={meetingTimeRemaining}
messageDuration={intlMessages.meetingTimeRemaining} messageDuration={intlMessages.meetingTimeRemaining}
timeEndedMessage={intlMessages.meetingWillClose} timeEndedMessage={intlMessages.meetingWillClose}

View File

@ -0,0 +1,9 @@
import React from 'react';
const MeetingRemainingTime = (props) => (
<span data-test="timeRemaining">
{ props.children }
</span>
);
export default MeetingRemainingTime;

View File

@ -4,9 +4,10 @@ import { defineMessages, injectIntl } from 'react-intl';
import injectNotify from '/imports/ui/components/common/toast/inject-notify/component'; import injectNotify from '/imports/ui/components/common/toast/inject-notify/component';
import humanizeSeconds from '/imports/utils/humanizeSeconds'; import humanizeSeconds from '/imports/utils/humanizeSeconds';
import _ from 'lodash'; import _ from 'lodash';
import BreakoutRemainingTimeComponent from './component'; import MeetingRemainingTimeComponent from './component';
import BreakoutService from '/imports/ui/components/breakout-room/service'; import BreakoutService from '/imports/ui/components/breakout-room/service';
import { Text, Time } from './styles'; import { Text, Time } from './styles';
import { meetingIsBreakout } from '/imports/ui/components/app/service';
const intlMessages = defineMessages({ const intlMessages = defineMessages({
failedMessage: { failedMessage: {
@ -37,6 +38,10 @@ const intlMessages = defineMessages({
id: 'app.meeting.alertBreakoutEndsUnderMinutes', id: 'app.meeting.alertBreakoutEndsUnderMinutes',
description: 'Alert that tells that the breakout ends under x minutes', description: 'Alert that tells that the breakout ends under x minutes',
}, },
alertMeetingEndsUnderMinutes: {
id: 'app.meeting.alertMeetingEndsUnderMinutes',
description: 'Alert that tells that the meeting ends under x minutes',
},
}); });
let timeRemaining = 0; let timeRemaining = 0;
@ -66,17 +71,17 @@ class breakoutRemainingTimeContainer extends React.Component {
const time = words.pop(); const time = words.pop();
const text = words.join(' '); const text = words.join(' ');
return ( return (
<BreakoutRemainingTimeComponent> <MeetingRemainingTimeComponent>
<Text>{text}</Text> <Text>{text}</Text>
<br /> <br />
<Time data-test="breakoutRemainingTime">{time}</Time> <Time data-test="breakoutRemainingTime">{time}</Time>
</BreakoutRemainingTimeComponent> </MeetingRemainingTimeComponent>
); );
} }
return ( return (
<BreakoutRemainingTimeComponent> <MeetingRemainingTimeComponent>
{message} {message}
</BreakoutRemainingTimeComponent> </MeetingRemainingTimeComponent>
); );
} }
} }
@ -137,8 +142,11 @@ export default injectNotify(injectIntl(withTracker(({
if (alertsInSeconds.includes(time) && time !== lastAlertTime && displayAlerts) { if (alertsInSeconds.includes(time) && time !== lastAlertTime && displayAlerts) {
const timeInMinutes = time / 60; const timeInMinutes = time / 60;
const msg = { id: `${intlMessages.alertBreakoutEndsUnderMinutes.id}${timeInMinutes === 1 ? 'Singular' : 'Plural'}` }; const message = meetingIsBreakout()
const alertMessage = intl.formatMessage(msg, { 0: timeInMinutes }) ? intlMessages.alertBreakoutEndsUnderMinutes
: intlMessages.alertMeetingEndsUnderMinutes;
const msg = { id: `${message.id}${timeInMinutes === 1 ? 'Singular' : 'Plural'}` };
const alertMessage = intl.formatMessage(msg, { 0: timeInMinutes });
lastAlertTime = time; lastAlertTime = time;
notify(alertMessage, 'info', 'rooms'); notify(alertMessage, 'info', 'rooms');

View File

@ -54,6 +54,7 @@ class Polling extends Component {
checkedAnswers: [], checkedAnswers: [],
}; };
this.pollingContainer = null;
this.play = this.play.bind(this); this.play = this.play.bind(this);
this.handleUpdateResponseInput = this.handleUpdateResponseInput.bind(this); this.handleUpdateResponseInput = this.handleUpdateResponseInput.bind(this);
this.renderButtonAnswers = this.renderButtonAnswers.bind(this); this.renderButtonAnswers = this.renderButtonAnswers.bind(this);
@ -65,6 +66,7 @@ class Polling extends Component {
componentDidMount() { componentDidMount() {
this.play(); this.play();
this.pollingContainer && this.pollingContainer?.focus();
} }
play() { play() {
@ -302,7 +304,9 @@ class Polling extends Component {
<Styled.PollingContainer <Styled.PollingContainer
autoWidth={stackOptions} autoWidth={stackOptions}
data-test="pollingContainer" data-test="pollingContainer"
role="alert" role="complementary"
ref={el => this.pollingContainer = el}
tabIndex={-1}
> >
{ {
question.length > 0 && ( question.length > 0 && (

View File

@ -144,7 +144,7 @@ const QText = styled.div`
padding-right: ${smPaddingX}; padding-right: ${smPaddingX};
`; `;
const PollingContainer = styled.div` const PollingContainer = styled.aside`
pointer-events:auto; pointer-events:auto;
min-width: ${pollWidth}; min-width: ${pollWidth};
position: absolute; position: absolute;
@ -161,6 +161,10 @@ const PollingContainer = styled.div`
bottom: ${pollBottomOffset}; bottom: ${pollBottomOffset};
right: ${jumboPaddingX}; right: ${jumboPaddingX};
&:focus {
border: 1px solid ${colorPrimary};
}
[dir="rtl"] & { [dir="rtl"] & {
left: ${jumboPaddingX}; left: ${jumboPaddingX};
right: auto; right: auto;

View File

@ -1,24 +1,14 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import WhiteboardOverlayContainer from '/imports/ui/components/whiteboard/whiteboard-overlay/container'
import WhiteboardContainer from '/imports/ui/components/whiteboard/container'; import WhiteboardContainer from '/imports/ui/components/whiteboard/container';
import WhiteboardToolbarContainer from '/imports/ui/components/whiteboard/whiteboard-toolbar/container';
import { HUNDRED_PERCENT, MAX_PERCENT } from '/imports/utils/slideCalcUtils'; import { HUNDRED_PERCENT, MAX_PERCENT } from '/imports/utils/slideCalcUtils';
import { SPACE } from '/imports/utils/keyCodes'; import { SPACE } from '/imports/utils/keyCodes';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { Session } from 'meteor/session'; import { Session } from 'meteor/session';
import PresentationToolbarContainer from './presentation-toolbar/container'; import PresentationToolbarContainer from './presentation-toolbar/container';
import PresentationPlaceholder from './presentation-placeholder/component';
import PresentationMenu from './presentation-menu/container'; import PresentationMenu from './presentation-menu/container';
import CursorWrapperContainer from './cursor/cursor-wrapper-container/container';
import AnnotationGroupContainer from '../whiteboard/annotation-group/container';
import PresentationOverlayContainer from './presentation-overlay/container';
import Slide from './slide/component';
import Styled from './styles'; import Styled from './styles';
import MediaService from '../media/service';
// import PresentationCloseButton from './presentation-close-button/component';
import DownloadPresentationButton from './download-presentation-button/component';
import FullscreenService from '/imports/ui/components/common/fullscreen-button/service'; import FullscreenService from '/imports/ui/components/common/fullscreen-button/service';
import Icon from '/imports/ui/components/common/icon/component'; import Icon from '/imports/ui/components/common/icon/component';
import PollingContainer from '/imports/ui/components/polling/container'; import PollingContainer from '/imports/ui/components/polling/container';
@ -27,7 +17,7 @@ import DEFAULT_VALUES from '../layout/defaultValues';
import { colorContentBackground } from '/imports/ui/stylesheets/styled-components/palette'; import { colorContentBackground } from '/imports/ui/stylesheets/styled-components/palette';
import browserInfo from '/imports/utils/browserInfo'; import browserInfo from '/imports/utils/browserInfo';
import { addNewAlert } from '../screenreader-alert/service'; import { addNewAlert } from '../screenreader-alert/service';
import { clearCursors } from '/imports/ui/components/cursor/service'; import { clearCursors } from '/imports/ui/components/whiteboard/cursors/service';
const intlMessages = defineMessages({ const intlMessages = defineMessages({
presentationLabel: { presentationLabel: {
@ -60,11 +50,19 @@ const intlMessages = defineMessages({
}, },
}); });
const ALLOW_FULLSCREEN = Meteor.settings.public.app.allowFullscreen;
const OLD_MINIMIZE_BUTTON_ENABLED = Meteor.settings.public.presentation.oldMinimizeButton;
const { isSafari } = browserInfo; const { isSafari } = browserInfo;
const FULLSCREEN_CHANGE_EVENT = isSafari ? 'webkitfullscreenchange' : 'fullscreenchange'; const FULLSCREEN_CHANGE_EVENT = isSafari ? 'webkitfullscreenchange' : 'fullscreenchange';
const getToolbarHeight = () => {
let height = 0;
const toolbarEl = document.getElementById('presentationToolbarWrapper');
if (toolbarEl) {
const { clientHeight } = toolbarEl;
height = clientHeight;
}
return height;
};
class Presentation extends PureComponent { class Presentation extends PureComponent {
constructor() { constructor() {
super(); super();
@ -72,7 +70,6 @@ class Presentation extends PureComponent {
this.state = { this.state = {
presentationWidth: 0, presentationWidth: 0,
presentationHeight: 0, presentationHeight: 0,
showSlide: false,
zoom: 100, zoom: 100,
fitToWidth: false, fitToWidth: false,
isFullscreen: false, isFullscreen: false,
@ -128,24 +125,6 @@ class Presentation extends PureComponent {
return stateChange; return stateChange;
} }
setIsPanning() {
this.setState({
isPanning: !this.state.isPanning,
});
}
handlePanShortcut(e) {
const { userIsPresenter } = this.props;
if (e.keyCode === SPACE && userIsPresenter) {
switch(e.type) {
case 'keyup':
return this.state.isPanning && this.setIsPanning();
case 'keydown':
return !this.state.isPanning && this.setIsPanning();
}
}
}
componentDidMount() { componentDidMount() {
this.getInitialPresentationSizes(); this.getInitialPresentationSizes();
this.refPresentationContainer.addEventListener('keydown', this.handlePanShortcut); this.refPresentationContainer.addEventListener('keydown', this.handlePanShortcut);
@ -180,7 +159,6 @@ class Presentation extends PureComponent {
presentationIsOpen, presentationIsOpen,
currentSlide, currentSlide,
publishedPoll, publishedPoll,
isViewer,
setPresentationIsOpen, setPresentationIsOpen,
restoreOnUpdate, restoreOnUpdate,
layoutContextDispatch, layoutContextDispatch,
@ -189,10 +167,11 @@ class Presentation extends PureComponent {
numCameras, numCameras,
intl, intl,
multiUser, multiUser,
clearFakeAnnotations,
} = this.props; } = this.props;
const { presentationWidth, presentationHeight, zoom, isPanning, fitToWidth } = this.state; const {
presentationWidth, presentationHeight, zoom, isPanning, fitToWidth,
} = this.state;
const { const {
numCameras: prevNumCameras, numCameras: prevNumCameras,
presentationBounds: prevPresentationBounds, presentationBounds: prevPresentationBounds,
@ -200,7 +179,6 @@ class Presentation extends PureComponent {
} = prevProps; } = prevProps;
if (prevMultiUser && !multiUser) { if (prevMultiUser && !multiUser) {
clearFakeAnnotations();
clearCursors(); clearCursors();
} }
@ -265,7 +243,7 @@ class Presentation extends PureComponent {
}); });
} }
if (!presentationIsOpen && restoreOnUpdate && !userIsPresenter && currentSlide) { if (!presentationIsOpen && restoreOnUpdate && currentSlide) {
const slideChanged = currentSlide.id !== prevProps.currentSlide.id; const slideChanged = currentSlide.id !== prevProps.currentSlide.id;
const positionChanged = slidePosition const positionChanged = slidePosition
.viewBoxHeight !== prevProps.slidePosition.viewBoxHeight .viewBoxHeight !== prevProps.slidePosition.viewBoxHeight
@ -276,8 +254,8 @@ class Presentation extends PureComponent {
} }
} }
if ((presentationBounds !== prevPresentationBounds) || if ((presentationBounds !== prevPresentationBounds)
(!presentationWidth && !presentationHeight)) this.onResize(); || (!presentationWidth && !presentationHeight)) this.onResize();
} else if (slidePosition) { } else if (slidePosition) {
const { width: currWidth, height: currHeight } = slidePosition; const { width: currWidth, height: currHeight } = slidePosition;
@ -294,7 +272,8 @@ class Presentation extends PureComponent {
}); });
} }
if ((zoom <= HUNDRED_PERCENT && isPanning && !fitToWidth) || !userIsPresenter && prevProps.userIsPresenter) { if ((zoom <= HUNDRED_PERCENT && isPanning && !fitToWidth)
|| (!userIsPresenter && prevProps.userIsPresenter)) {
this.setIsPanning(); this.setIsPanning();
} }
} }
@ -320,10 +299,19 @@ class Presentation extends PureComponent {
} }
} }
setTldrawAPI(api) { handlePanShortcut(e) {
this.setState({ const { userIsPresenter } = this.props;
tldrawAPI: api, const { isPanning } = this.state;
}); if (e.keyCode === SPACE && userIsPresenter) {
switch (e.type) {
case 'keyup':
return isPanning && this.setIsPanning();
case 'keydown':
return !isPanning && this.setIsPanning();
default:
}
}
return null;
} }
handleResize() { handleResize() {
@ -347,6 +335,22 @@ class Presentation extends PureComponent {
} }
} }
setTldrawAPI(api) {
this.setState({
tldrawAPI: api,
});
}
setTldrawIsMounting(value) {
this.setState({ tldrawIsMounting: value });
}
setIsPanning() {
this.setState((prevState) => ({
isPanning: !prevState.isPanning,
}));
}
setPresentationRef(ref) { setPresentationRef(ref) {
this.refPresentationContainer = ref; this.refPresentationContainer = ref;
} }
@ -357,16 +361,6 @@ class Presentation extends PureComponent {
return this.svggroup; return this.svggroup;
} }
getToolbarHeight() {
let height = 0;
const toolbarEl = document.getElementById('presentationToolbarWrapper');
if (toolbarEl) {
const { clientHeight } = toolbarEl;
height = clientHeight;
}
return height;
}
getPresentationSizesAvailable() { getPresentationSizesAvailable() {
const { const {
presentationBounds, presentationBounds,
@ -380,7 +374,7 @@ class Presentation extends PureComponent {
if (newPresentationAreaSize) { if (newPresentationAreaSize) {
presentationSizes.presentationWidth = newPresentationAreaSize.presentationAreaWidth; presentationSizes.presentationWidth = newPresentationAreaSize.presentationAreaWidth;
presentationSizes.presentationHeight = newPresentationAreaSize presentationSizes.presentationHeight = newPresentationAreaSize
.presentationAreaHeight - (this.getToolbarHeight() || 0); .presentationAreaHeight - (getToolbarHeight() || 0);
return presentationSizes; return presentationSizes;
} }
@ -396,11 +390,9 @@ class Presentation extends PureComponent {
const presentationSizes = this.getPresentationSizesAvailable(); const presentationSizes = this.getPresentationSizesAvailable();
if (Object.keys(presentationSizes).length > 0) { if (Object.keys(presentationSizes).length > 0) {
// setting the state of the available space for the svg // setting the state of the available space for the svg
// and set the showSlide to true to start rendering the slide
this.setState({ this.setState({
presentationHeight: presentationSizes.presentationHeight, presentationHeight: presentationSizes.presentationHeight,
presentationWidth: presentationSizes.presentationWidth, presentationWidth: presentationSizes.presentationWidth,
showSlide: true,
}); });
} }
} }
@ -409,6 +401,30 @@ class Presentation extends PureComponent {
this.setState({ fitToWidth }); this.setState({ fitToWidth });
} }
zoomChanger(zoom) {
this.setState({ zoom });
}
fitToWidthHandler() {
const {
fitToWidth,
} = this.state;
this.setState({
fitToWidth: !fitToWidth,
zoom: HUNDRED_PERCENT,
});
}
updateLocalPosition(x, y, width, height, zoom) {
this.setState({
localPosition: {
x, y, width, height,
},
zoom,
});
}
calculateSize(viewBoxDimensions) { calculateSize(viewBoxDimensions) {
const { const {
presentationHeight, presentationHeight,
@ -466,39 +482,6 @@ class Presentation extends PureComponent {
}; };
} }
zoomChanger(zoom) {
this.setState({ zoom });
}
fitToWidthHandler() {
const {
fitToWidth,
} = this.state;
this.setState({
fitToWidth: !fitToWidth,
zoom: HUNDRED_PERCENT,
});
}
isPresentationAccessible() {
const {
currentSlide,
slidePosition,
} = this.props;
// sometimes tomcat publishes the slide url, but the actual file is not accessible
return currentSlide && slidePosition;
}
updateLocalPosition(x, y, width, height, zoom) {
this.setState({
localPosition: {
x, y, width, height,
},
zoom,
});
}
panAndZoomChanger(w, h, x, y) { panAndZoomChanger(w, h, x, y) {
const { const {
currentSlide, currentSlide,
@ -509,230 +492,6 @@ class Presentation extends PureComponent {
zoomSlide(currentSlide.num, podId, w, h, x, y); zoomSlide(currentSlide.num, podId, w, h, x, y);
} }
renderPresentationClose() {
const { isFullscreen } = this.state;
const {
layoutType,
fullscreenContext,
layoutContextDispatch,
isIphone,
// <<<<<<< HEAD
// presentationIsOpen,
// } = this.props;
// if (isFullscreen
// =======
} = this.props;
if (!OLD_MINIMIZE_BUTTON_ENABLED
|| !shouldEnableSwapLayout()
|| isFullscreen
// >>>>>>> embed Tldraw into BBB client
|| fullscreenContext
|| layoutType === LAYOUT_TYPE.PRESENTATION_FOCUS) {
return null;
}
}
renderOverlays(slideObj, svgDimensions, viewBoxPosition, viewBoxDimensions, physicalDimensions) {
const {
userIsPresenter,
multiUser,
podId,
currentSlide,
slidePosition,
} = this.props;
const {
zoom,
fitToWidth,
} = this.state;
if (!userIsPresenter && !multiUser) {
return null;
}
// retrieving the pre-calculated data from the slide object
const {
width,
height,
} = slidePosition;
return (
<PresentationOverlayContainer
podId={podId}
userIsPresenter={userIsPresenter}
currentSlideNum={currentSlide.num}
slide={slideObj}
slideWidth={width}
slideHeight={height}
viewBoxX={viewBoxPosition.x}
viewBoxY={viewBoxPosition.y}
viewBoxWidth={viewBoxDimensions.width}
viewBoxHeight={viewBoxDimensions.height}
physicalSlideWidth={physicalDimensions.width}
physicalSlideHeight={physicalDimensions.height}
svgWidth={svgDimensions.width}
svgHeight={svgDimensions.height}
zoom={zoom}
zoomChanger={this.zoomChanger}
updateLocalPosition={this.updateLocalPosition}
panAndZoomChanger={this.panAndZoomChanger}
getSvgRef={this.getSvgRef}
fitToWidth={fitToWidth}
>
<WhiteboardOverlayContainer
getSvgRef={this.getSvgRef}
userIsPresenter={userIsPresenter}
whiteboardId={slideObj.id}
slide={slideObj}
slideWidth={width}
slideHeight={height}
viewBoxX={viewBoxPosition.x}
viewBoxY={viewBoxPosition.y}
viewBoxWidth={viewBoxDimensions.width}
viewBoxHeight={viewBoxDimensions.height}
physicalSlideWidth={physicalDimensions.width}
physicalSlideHeight={physicalDimensions.height}
zoom={zoom}
zoomChanger={this.zoomChanger}
/>
</PresentationOverlayContainer>
);
}
// renders the whole presentation area
renderPresentation(svgDimensions, viewBoxDimensions) {
const {
intl,
podId,
currentSlide,
slidePosition,
userIsPresenter,
presentationIsOpen,
} = this.props;
const {
localPosition,
} = this.state;
if (!this.isPresentationAccessible()) {
return null;
}
// retrieving the pre-calculated data from the slide object
const {
width,
height,
} = slidePosition;
const {
imageUri,
content,
} = currentSlide;
let viewBoxPosition;
if (userIsPresenter && localPosition) {
viewBoxPosition = {
x: localPosition.x,
y: localPosition.y,
};
} else {
viewBoxPosition = {
x: slidePosition.x,
y: slidePosition.y,
};
}
const widthRatio = viewBoxDimensions.width / width;
const heightRatio = viewBoxDimensions.height / height;
const physicalDimensions = {
width: (svgDimensions.width / widthRatio),
height: (svgDimensions.height / heightRatio),
};
const svgViewBox = `${viewBoxPosition.x} ${viewBoxPosition.y} `
+ `${viewBoxDimensions.width} ${Number.isNaN(viewBoxDimensions.height) ? 0 : viewBoxDimensions.height}`;
const slideContent = content ? `${intl.formatMessage(intlMessages.slideContentStart)}
${content}
${intl.formatMessage(intlMessages.slideContentEnd)}` : intl.formatMessage(intlMessages.noSlideContent);
return (
<div
style={{
position: 'absolute',
width: svgDimensions.width < 0 ? 0 : svgDimensions.width,
height: svgDimensions.height < 0 ? 0 : svgDimensions.height,
textAlign: 'center',
display: !presentationIsOpen ? 'none' : 'block',
}}
>
<Styled.VisuallyHidden id="currentSlideText">{slideContent}</Styled.VisuallyHidden>
{/* {this.renderPresentationClose()}
{this.renderPresentationDownload()}
{this.renderPresentationFullscreen()} */}
{this.renderPresentationMenu()}
<Styled.PresentationSvg
key={currentSlide.id}
data-test={!presentationIsOpen ? 'hiddenWhiteboard' : 'whiteboard'}
width={svgDimensions.width < 0 ? 0 : svgDimensions.width}
height={svgDimensions.height < 0 ? 0 : svgDimensions.height}
ref={(ref) => { if (ref != null) { this.svggroup = ref; } }}
viewBox={svgViewBox}
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<clipPath id="viewBox">
<rect x={viewBoxPosition.x} y={viewBoxPosition.y} width="100%" height="100%" fill="none" />
</clipPath>
</defs>
<g clipPath="url(#viewBox)">
<Slide
imageUri={imageUri}
svgWidth={width}
svgHeight={height}
/>
<AnnotationGroupContainer
{...{
width,
height,
}}
published
whiteboardId={currentSlide.id}
/>
<AnnotationGroupContainer
{...{
width,
height,
}}
published={false}
whiteboardId={currentSlide.id}
/>
<CursorWrapperContainer
podId={podId}
whiteboardId={currentSlide.id}
widthRatio={widthRatio}
physicalWidthRatio={svgDimensions.width / width}
slideWidth={width}
slideHeight={height}
/>
</g>
{this.renderOverlays(
currentSlide,
svgDimensions,
viewBoxPosition,
viewBoxDimensions,
physicalDimensions,
)}
</Styled.PresentationSvg>
</div>
);
}
renderPresentationToolbar(svgWidth = 0) { renderPresentationToolbar(svgWidth = 0) {
const { const {
currentSlide, currentSlide,
@ -745,8 +504,14 @@ class Presentation extends PureComponent {
layoutContextDispatch, layoutContextDispatch,
presentationIsOpen, presentationIsOpen,
slidePosition, slidePosition,
addWhiteboardGlobalAccess,
removeWhiteboardGlobalAccess,
multiUserSize,
multiUser,
} = this.props; } = this.props;
const { zoom, fitToWidth } = this.state; const {
zoom, fitToWidth, isPanning, tldrawAPI,
} = this.state;
if (!currentSlide) return null; if (!currentSlide) return null;
@ -771,8 +536,8 @@ class Presentation extends PureComponent {
presentationIsOpen, presentationIsOpen,
}} }}
setIsPanning={this.setIsPanning} setIsPanning={this.setIsPanning}
isPanning={this.state.isPanning} isPanning={isPanning}
curPageId={this.state.tldrawAPI?.getPage()?.id} curPageId={tldrawAPI?.getPage()?.id}
currentSlideNum={currentSlide.num} currentSlideNum={currentSlide.num}
presentationId={currentSlide.presentationId} presentationId={currentSlide.presentationId}
zoomChanger={this.zoomChanger} zoomChanger={this.zoomChanger}
@ -780,66 +545,15 @@ class Presentation extends PureComponent {
isFullscreen={fullscreenContext} isFullscreen={fullscreenContext}
fullscreenAction={ACTIONS.SET_FULLSCREEN_ELEMENT} fullscreenAction={ACTIONS.SET_FULLSCREEN_ELEMENT}
fullscreenRef={this.refPresentationContainer} fullscreenRef={this.refPresentationContainer}
addWhiteboardGlobalAccess={this.props.addWhiteboardGlobalAccess} addWhiteboardGlobalAccess={addWhiteboardGlobalAccess}
removeWhiteboardGlobalAccess={this.props.removeWhiteboardGlobalAccess} removeWhiteboardGlobalAccess={removeWhiteboardGlobalAccess}
multiUserSize={this.props.multiUserSize} multiUserSize={multiUserSize}
multiUser={this.props.multiUser} multiUser={multiUser}
whiteboardId={currentSlide?.id} whiteboardId={currentSlide?.id}
/> />
); );
} }
renderWhiteboardToolbar(svgDimensions) {
const { currentSlide, userIsPresenter } = this.props;
if (!this.isPresentationAccessible()) return null;
return (
<WhiteboardToolbarContainer
whiteboardId={currentSlide.id}
height={svgDimensions.height}
isPresenter={userIsPresenter}
/>
);
}
renderPresentationDownload() {
const { presentationIsDownloadable, downloadPresentationUri } = this.props;
if (!presentationIsDownloadable) return null;
const handleDownloadPresentation = () => {
window.open(downloadPresentationUri);
};
return (
<DownloadPresentationButton
handleDownloadPresentation={handleDownloadPresentation}
dark
/>
);
}
renderPresentationFullscreen() {
const {
intl,
fullscreenElementId,
} = this.props;
const { isFullscreen } = this.state;
if (!ALLOW_FULLSCREEN) return null;
return (
<Styled.PresentationFullscreenButton
fullscreenRef={this.refPresentationContainer}
elementName={intl.formatMessage(intlMessages.presentationLabel)}
elementId={fullscreenElementId}
isFullscreen={isFullscreen}
color="muted"
fullScreenStyle={false}
/>
);
}
renderCurrentPresentationToast() { renderCurrentPresentationToast() {
const { const {
intl, currentPresentation, userIsPresenter, downloadPresentationUri, intl, currentPresentation, userIsPresenter, downloadPresentationUri,
@ -884,11 +598,12 @@ class Presentation extends PureComponent {
fullscreenElementId, fullscreenElementId,
layoutContextDispatch, layoutContextDispatch,
} = this.props; } = this.props;
const { tldrawAPI } = this.state;
return ( return (
<PresentationMenu <PresentationMenu
fullscreenRef={this.refPresentationContainer} fullscreenRef={this.refPresentationContainer}
tldrawAPI={this.state.tldrawAPI} tldrawAPI={tldrawAPI}
elementName={intl.formatMessage(intlMessages.presentationLabel)} elementName={intl.formatMessage(intlMessages.presentationLabel)}
elementId={fullscreenElementId} elementId={fullscreenElementId}
layoutContextDispatch={layoutContextDispatch} layoutContextDispatch={layoutContextDispatch}
@ -896,15 +611,10 @@ class Presentation extends PureComponent {
); );
} }
setTldrawIsMounting(value) {
this.setState({ tldrawIsMounting: value });
}
render() { render() {
const { const {
userIsPresenter, userIsPresenter,
currentSlide, currentSlide,
multiUser,
slidePosition, slidePosition,
presentationBounds, presentationBounds,
fullscreenContext, fullscreenContext,
@ -922,12 +632,12 @@ class Presentation extends PureComponent {
} = this.props; } = this.props;
const { const {
showSlide,
isFullscreen, isFullscreen,
localPosition, localPosition,
fitToWidth, fitToWidth,
zoom, zoom,
tldrawIsMounting, tldrawIsMounting,
isPanning,
} = this.state; } = this.state;
let viewBoxDimensions; let viewBoxDimensions;
@ -953,7 +663,7 @@ class Presentation extends PureComponent {
const svgHeight = svgDimensions.height; const svgHeight = svgDimensions.height;
const svgWidth = svgDimensions.width; const svgWidth = svgDimensions.width;
const toolbarHeight = this.getToolbarHeight(); const toolbarHeight = getToolbarHeight();
const { presentationToolbarMinWidth } = DEFAULT_VALUES; const { presentationToolbarMinWidth } = DEFAULT_VALUES;
@ -968,18 +678,6 @@ class Presentation extends PureComponent {
${currentSlide.content} ${currentSlide.content}
${intl.formatMessage(intlMessages.slideContentEnd)}` : intl.formatMessage(intlMessages.noSlideContent); ${intl.formatMessage(intlMessages.slideContentEnd)}` : intl.formatMessage(intlMessages.noSlideContent);
if (!currentPresentation && this.refPresentationContainer) {
return (
<></>
// <PresentationPlaceholder
// {
// ...presentationBounds
// }
// setPresentationRef={this.setPresentationRef}
// />
);
}
return ( return (
<> <>
<Styled.PresentationContainer <Styled.PresentationContainer
@ -995,7 +693,8 @@ class Presentation extends PureComponent {
display: !presentationIsOpen ? 'none' : 'flex', display: !presentationIsOpen ? 'none' : 'flex',
overflow: 'hidden', overflow: 'hidden',
zIndex: fullscreenContext ? presentationBounds.zIndex : undefined, zIndex: fullscreenContext ? presentationBounds.zIndex : undefined,
background: layoutType === LAYOUT_TYPE.VIDEO_FOCUS && numCameras > 0 && !fullscreenContext background:
layoutType === LAYOUT_TYPE.VIDEO_FOCUS && numCameras > 0 && !fullscreenContext
? colorContentBackground ? colorContentBackground
: null, : null,
}} }}
@ -1023,7 +722,7 @@ class Presentation extends PureComponent {
slidePosition={slidePosition} slidePosition={slidePosition}
getSvgRef={this.getSvgRef} getSvgRef={this.getSvgRef}
setTldrawAPI={this.setTldrawAPI} setTldrawAPI={this.setTldrawAPI}
curPageId={currentSlide?.num.toString()} curPageId={currentSlide?.num.toString() || '0'}
svgUri={currentSlide?.svgUri} svgUri={currentSlide?.svgUri}
intl={intl} intl={intl}
presentationWidth={svgWidth} presentationWidth={svgWidth}
@ -1031,7 +730,7 @@ class Presentation extends PureComponent {
presentationAreaHeight={presentationBounds?.height} presentationAreaHeight={presentationBounds?.height}
presentationAreaWidth={presentationBounds?.width} presentationAreaWidth={presentationBounds?.width}
isViewersCursorLocked={isViewersCursorLocked} isViewersCursorLocked={isViewersCursorLocked}
isPanning={this.state.isPanning} isPanning={isPanning}
zoomChanger={this.zoomChanger} zoomChanger={this.zoomChanger}
fitToWidth={fitToWidth} fitToWidth={fitToWidth}
zoomValue={zoom} zoomValue={zoom}
@ -1058,39 +757,8 @@ class Presentation extends PureComponent {
{this.renderPresentationToolbar(svgWidth)} {this.renderPresentationToolbar(svgWidth)}
</Styled.PresentationToolbar> </Styled.PresentationToolbar>
)} )}
{/*this.renderPresentationToolbar()*/}
</Styled.SvgContainer> </Styled.SvgContainer>
</Styled.Presentation> </Styled.Presentation>
{/*
<Styled.Presentation ref={(ref) => { this.refPresentation = ref; }}>
<Styled.WhiteboardSizeAvailable ref={(ref) => { this.refWhiteboardArea = ref; }} />
<Styled.SvgContainer
style={{
height: svgHeight + toolbarHeight,
}}
>
{showSlide && svgWidth > 0 && svgHeight > 0
? this.renderPresentation(svgDimensions, viewBoxDimensions)
: null}
{showSlide && (userIsPresenter || multiUser)
? this.renderWhiteboardToolbar(svgDimensions)
: null}
{showSlide && userIsPresenter
? (
<Styled.PresentationToolbar
ref={(ref) => { this.refPresentationToolbar = ref; }}
style={
{
width: containerWidth,
}
}
>
{this.renderPresentationToolbar(svgWidth)}
</Styled.PresentationToolbar>
)
: null}
</Styled.SvgContainer>
</Styled.Presentation> */}
</Styled.PresentationContainer> </Styled.PresentationContainer>
</> </>
@ -1110,6 +778,9 @@ Presentation.propTypes = {
num: PropTypes.number.isRequired, num: PropTypes.number.isRequired,
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
imageUri: PropTypes.string.isRequired, imageUri: PropTypes.string.isRequired,
curPageId: PropTypes.string,
svgUri: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
}), }),
slidePosition: PropTypes.shape({ slidePosition: PropTypes.shape({
x: PropTypes.number.isRequired, x: PropTypes.number.isRequired,
@ -1122,9 +793,49 @@ Presentation.propTypes = {
// current multi-user status // current multi-user status
multiUser: PropTypes.bool.isRequired, multiUser: PropTypes.bool.isRequired,
setPresentationIsOpen: PropTypes.func.isRequired, setPresentationIsOpen: PropTypes.func.isRequired,
layoutContextDispatch: PropTypes.func.isRequired,
currentPresentation: PropTypes.shape({
downloadable: PropTypes.bool.isRequired,
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
}),
presentationIsOpen: PropTypes.bool.isRequired,
publishedPoll: PropTypes.bool.isRequired,
presentationBounds: PropTypes.shape({
top: PropTypes.number,
left: PropTypes.number,
right: PropTypes.number,
width: PropTypes.number,
height: PropTypes.number,
zIndex: PropTypes.number,
}),
restoreOnUpdate: PropTypes.bool.isRequired,
numCameras: PropTypes.number.isRequired,
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
isMobile: PropTypes.bool.isRequired,
fullscreenContext: PropTypes.bool.isRequired,
presentationAreaSize: PropTypes.shape({
presentationAreaWidth: PropTypes.number.isRequired,
presentationAreaHeight: PropTypes.number.isRequired,
}),
zoomSlide: PropTypes.func.isRequired,
addWhiteboardGlobalAccess: PropTypes.func.isRequired,
removeWhiteboardGlobalAccess: PropTypes.func.isRequired,
multiUserSize: PropTypes.number.isRequired,
layoutType: PropTypes.string.isRequired,
fullscreenElementId: PropTypes.string.isRequired,
downloadPresentationUri: PropTypes.string,
isViewersCursorLocked: PropTypes.bool.isRequired,
darkTheme: PropTypes.bool.isRequired,
}; };
Presentation.defaultProps = { Presentation.defaultProps = {
currentSlide: undefined, currentSlide: undefined,
slidePosition: undefined, slidePosition: undefined,
currentPresentation: undefined,
presentationAreaSize: undefined,
presentationBounds: undefined,
downloadPresentationUri: undefined,
}; };

View File

@ -1,4 +1,5 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { withTracker } from 'meteor/react-meteor-data'; import { withTracker } from 'meteor/react-meteor-data';
import { notify } from '/imports/ui/services/notification'; import { notify } from '/imports/ui/services/notification';
import PresentationService from './service'; import PresentationService from './service';
@ -15,13 +16,14 @@ import {
layoutSelectOutput, layoutSelectOutput,
layoutDispatch, layoutDispatch,
} from '../layout/context'; } from '../layout/context';
import lockContextContainer from "/imports/ui/components/lock-viewers/context/container"; import lockContextContainer from '/imports/ui/components/lock-viewers/context/container';
import WhiteboardService from '/imports/ui/components/whiteboard/service'; import WhiteboardService from '/imports/ui/components/whiteboard/service';
import { DEVICE_TYPE } from '../layout/enums'; import { DEVICE_TYPE } from '../layout/enums';
import MediaService from '../media/service'; import MediaService from '../media/service';
const PresentationContainer = ({ presentationIsOpen, presentationPodIds, mountPresentation, ...props }) => { const PresentationContainer = ({
presentationIsOpen, presentationPodIds, mountPresentation, ...props
}) => {
const cameraDock = layoutSelectInput((i) => i.cameraDock); const cameraDock = layoutSelectInput((i) => i.cameraDock);
const presentation = layoutSelectOutput((i) => i.presentation); const presentation = layoutSelectOutput((i) => i.presentation);
const layoutType = layoutSelect((i) => i.layoutType); const layoutType = layoutSelect((i) => i.layoutType);
@ -118,8 +120,10 @@ export default lockContextContainer(
currentSlide, currentSlide,
slidePosition, slidePosition,
downloadPresentationUri: PresentationService.downloadPresentationUri(podId), downloadPresentationUri: PresentationService.downloadPresentationUri(podId),
multiUser: (WhiteboardService.hasMultiUserAccess(currentSlide && currentSlide.id, Auth.userID) || WhiteboardService.isMultiUserActive(currentSlide?.id)) multiUser:
&& presentationIsOpen, (WhiteboardService.hasMultiUserAccess(currentSlide && currentSlide.id, Auth.userID)
|| WhiteboardService.isMultiUserActive(currentSlide?.id)
) && presentationIsOpen,
presentationIsDownloadable, presentationIsDownloadable,
mountPresentation: !!currentSlide, mountPresentation: !!currentSlide,
currentPresentation: PresentationService.getCurrentPresentation(podId), currentPresentation: PresentationService.getCurrentPresentation(podId),
@ -139,7 +143,15 @@ export default lockContextContainer(
removeWhiteboardGlobalAccess: WhiteboardService.removeGlobalAccess, removeWhiteboardGlobalAccess: WhiteboardService.removeGlobalAccess,
multiUserSize: WhiteboardService.getMultiUserSize(currentSlide?.id), multiUserSize: WhiteboardService.getMultiUserSize(currentSlide?.id),
isViewersCursorLocked, isViewersCursorLocked,
clearFakeAnnotations: WhiteboardService.clearFakeAnnotations,
setPresentationIsOpen: MediaService.setPresentationIsOpen, setPresentationIsOpen: MediaService.setPresentationIsOpen,
}; };
})(PresentationContainer)); })(PresentationContainer),
);
PresentationContainer.propTypes = {
presentationPodIds: PropTypes.arrayOf(PropTypes.shape({
podId: PropTypes.string.isRequired,
})).isRequired,
presentationIsOpen: PropTypes.bool.isRequired,
mountPresentation: PropTypes.bool.isRequired,
};

View File

@ -1,356 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export default class Cursor extends Component {
static scale(attribute, widthRatio, physicalWidthRatio) {
return (attribute * widthRatio) / physicalWidthRatio;
}
static invertScale(attribute, widthRatio, physicalWidthRatio) {
return (attribute * physicalWidthRatio) / widthRatio;
}
static getCursorCoordinates(cursorX, cursorY, slideWidth, slideHeight) {
// main cursor x and y coordinates
const x = (cursorX / 100) * slideWidth;
const y = (cursorY / 100) * slideHeight;
return {
x,
y,
};
}
static getFillAndLabel(presenter, isMultiUser) {
const obj = {
fill: 'green',
displayLabel: false,
};
if (presenter) {
obj.fill = 'red';
}
if (isMultiUser) {
obj.displayLabel = true;
}
return obj;
}
static getScaledSizes(props, state) {
// TODO: This might need to change for the use case of fit-to-width portrait
// slides in non-presenter view. Some elements are still shrinking.
const scaleFactor = props.widthRatio / props.physicalWidthRatio;
return {
// Adjust the radius of the cursor according to zoom
// and divide it by the physicalWidth ratio, so that svg scaling wasn't applied to the cursor
finalRadius: props.radius * scaleFactor,
// scaling the properties for cursorLabel and the border (rect) around it
cursorLabelText: {
textDY: props.cursorLabelText.textDY * scaleFactor,
textDX: props.cursorLabelText.textDX * scaleFactor,
fontSize: props.cursorLabelText.fontSize * scaleFactor,
},
cursorLabelBox: {
xOffset: props.cursorLabelBox.xOffset * scaleFactor,
yOffset: props.cursorLabelBox.yOffset * scaleFactor,
// making width and height a little bit larger than the size of the text
// received from BBox, so that the text didn't touch the border
width: (state.labelBoxWidth + 3) * scaleFactor,
height: (state.labelBoxHeight + 3) * scaleFactor,
strokeWidth: props.cursorLabelBox.labelBoxStrokeWidth * scaleFactor,
},
};
}
constructor(props) {
super(props);
this.state = {
scaledSizes: null,
labelBoxWidth: 0,
labelBoxHeight: 0,
};
this.setLabelBoxDimensions = this.setLabelBoxDimensions.bind(this);
}
componentDidMount() {
const {
cursorX,
cursorY,
slideWidth,
slideHeight,
presenter,
isMultiUser,
} = this.props;
this.setState({
scaledSizes: Cursor.getScaledSizes(this.props, this.state),
});
this.cursorCoordinate = Cursor.getCursorCoordinates(
cursorX,
cursorY,
slideWidth,
slideHeight,
);
const { fill, displayLabel } = Cursor.getFillAndLabel(presenter, isMultiUser);
this.fill = fill;
this.displayLabel = displayLabel;
// we need to find the BBox of the text, so that we could set a proper border box arount it
}
shouldComponentUpdate(nextProps) {
const { cursorX, cursorY, slideWidth, slideHeight } = this.props;
if (cursorX !== nextProps.cursorX || cursorY !== nextProps.cursorY) {
const cursorCoordinate = Cursor.getCursorCoordinates(
nextProps.cursorX,
nextProps.cursorY,
slideWidth,
slideHeight,
);
this.cursorCoordinate = cursorCoordinate;
}
return true;
}
componentDidUpdate(prevProps, prevState) {
const {
scaledSizes,
} = this.state;
if (!prevState.scaledSizes && scaledSizes) {
this.calculateCursorLabelBoxDimensions();
}
const {
presenter,
isMultiUser,
widthRatio,
physicalWidthRatio,
cursorX,
cursorY,
slideWidth,
slideHeight,
} = this.props;
const {
labelBoxWidth,
labelBoxHeight,
} = this.state;
const {
labelBoxWidth: prevLabelBoxWidth,
labelBoxHeight: prevLabelBoxHeight,
} = prevState;
if (presenter !== prevProps.presenter || isMultiUser !== prevProps.isMultiUser) {
const { fill, displayLabel } = Cursor.getFillAndLabel(
presenter,
isMultiUser,
);
this.displayLabel = displayLabel;
this.fill = fill;
}
if ((widthRatio !== prevProps.widthRatio
|| physicalWidthRatio !== prevProps.physicalWidthRatio)
|| (labelBoxWidth !== prevLabelBoxWidth
|| labelBoxHeight !== prevLabelBoxHeight)) {
this.setState({
scaledSizes: Cursor.getScaledSizes(this.props, this.state),
});
}
}
setLabelBoxDimensions(labelBoxWidth, labelBoxHeight) {
this.setState({
labelBoxWidth,
labelBoxHeight,
});
}
// this function retrieves the text node, measures its BBox and sets the size for the outer box
calculateCursorLabelBoxDimensions() {
let labelBoxWidth = 0;
let labelBoxHeight = 0;
if (this.cursorLabelRef) {
const { width, height } = this.cursorLabelRef.getBBox();
const { widthRatio, physicalWidthRatio, cursorLabelBox } = this.props;
labelBoxWidth = Cursor.invertScale(width, widthRatio, physicalWidthRatio);
labelBoxHeight = Cursor.invertScale(height, widthRatio, physicalWidthRatio);
// if the width of the text node is bigger than the maxSize - set the width to maxWidth
if (labelBoxWidth > cursorLabelBox.maxWidth) {
labelBoxWidth = cursorLabelBox.maxWidth;
}
}
// updating labelBoxWidth and labelBoxHeight in the container, which then passes it down here
this.setLabelBoxDimensions(labelBoxWidth, labelBoxHeight);
}
render() {
const {
scaledSizes,
} = this.state;
const {
cursorId,
userName,
isRTL,
} = this.props;
const {
cursorCoordinate,
fill,
} = this;
if (!scaledSizes) return null;
const {
cursorLabelBox,
cursorLabelText,
finalRadius,
} = scaledSizes;
const {
x,
y,
} = cursorCoordinate;
const boxX = x + cursorLabelBox.xOffset;
const boxY = y + cursorLabelBox.yOffset;
return (
<g
x={x}
y={y}
>
<circle
cx={x}
cy={y}
r={finalRadius}
fill={fill}
fillOpacity="0.6"
/>
{this.displayLabel
? (
<g>
<rect
fill="white"
fillOpacity="0.8"
x={boxX}
y={boxY}
width={cursorLabelBox.width}
height={cursorLabelBox.height}
strokeWidth={cursorLabelBox.strokeWidth}
stroke={fill}
strokeOpacity="0.8"
/>
<text
ref={(ref) => { this.cursorLabelRef = ref; }}
x={x}
y={y}
dy={cursorLabelText.textDY}
dx={cursorLabelText.textDX}
fontFamily="Arial"
fontWeight="600"
fill={fill}
fillOpacity="0.8"
fontSize={cursorLabelText.fontSize}
clipPath={`url(#${cursorId})`}
textAnchor={isRTL ? 'end' : 'start'}
>
{userName}
</text>
<clipPath id={cursorId}>
<rect
x={boxX}
y={boxY}
width={cursorLabelBox.width}
height={cursorLabelBox.height}
/>
</clipPath>
</g>
)
: null }
</g>
);
}
}
Cursor.propTypes = {
// ESLint can't detect where all these propTypes are used, and they are not planning to fix it
// so the next line disables eslint's no-unused-prop-types rule for this file.
/* eslint-disable react/no-unused-prop-types */
// Current presenter status
presenter: PropTypes.bool.isRequired,
// Current multi-user status
isMultiUser: PropTypes.bool.isRequired,
// Defines the id of the current cursor
cursorId: PropTypes.string.isRequired,
// Defines the user name for the cursor label
userName: PropTypes.string.isRequired,
// Defines the cursor x position
cursorX: PropTypes.number.isRequired,
// Defines the cursor y position
cursorY: PropTypes.number.isRequired,
// Slide to view box width ratio
widthRatio: PropTypes.number.isRequired,
// Slide physical size to original size ratio
physicalWidthRatio: PropTypes.number.isRequired,
// Slide width (svg)
slideWidth: PropTypes.number.isRequired,
// Slide height (svg)
slideHeight: PropTypes.number.isRequired,
/**
* Defines the cursor radius (not scaled)
* @defaultValue 5
*/
radius: PropTypes.number,
cursorLabelBox: PropTypes.shape({
labelBoxStrokeWidth: PropTypes.number.isRequired,
xOffset: PropTypes.number.isRequired,
yOffset: PropTypes.number.isRequired,
maxWidth: PropTypes.number.isRequired,
}),
cursorLabelText: PropTypes.shape({
textDY: PropTypes.number.isRequired,
textDX: PropTypes.number.isRequired,
fontSize: PropTypes.number.isRequired,
}),
// Defines the direction the client text should be displayed
isRTL: PropTypes.bool.isRequired,
};
Cursor.defaultProps = {
radius: 5,
cursorLabelText: {
// text Y shift (10 points down)
textDY: 10,
// text X shift (10 points to the right)
textDX: 10,
// Initial label's font-size
fontSize: 12,
},
cursorLabelBox: {
// The thickness of the label box's border
labelBoxStrokeWidth: 1,
// X offset of the label box (8 points to the right)
xOffset: 8,
// Y offset of the label box (-2 points up)
yOffset: -2,
// Maximum width of the box, for the case when the user's name is too long
maxWidth: 65,
},
};

View File

@ -1,78 +0,0 @@
import React, { useContext } from "react";
import PropTypes from "prop-types";
import { withTracker } from "meteor/react-meteor-data";
import Auth from "/imports/ui/services/auth";
import lockContextContainer from "/imports/ui/components/lock-viewers/context/container";
import { UsersContext } from "/imports/ui/components/components-data/users-context/context";
import CursorService from "./service";
import Cursor from "./component";
import WhiteboardService from "/imports/ui/components/whiteboard/service";
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
const CursorContainer = (props) => {
const { cursorX, cursorY, presenter, uid, isViewersCursorLocked } = props;
const usingUsersContext = useContext(UsersContext);
if (cursorX > 0 && cursorY > 0) {
const { users } = usingUsersContext;
const role = users[Auth.meetingID][Auth.userID].role;
const userId = users[Auth.meetingID][Auth.userID].userId;
const showCursor =
role === ROLE_MODERATOR || presenter || (!presenter && uid === userId);
if (!isViewersCursorLocked || (isViewersCursorLocked && showCursor)) {
return (
<Cursor
cursorX={cursorX}
cursorY={cursorY}
setLabelBoxDimensions={this.setLabelBoxDimensions}
{...props}
/>
);
}
}
return null;
};
export default lockContextContainer(
withTracker((params) => {
const { cursorId, userLocks, whiteboardId, presenter } = params;
const isViewersCursorLocked = userLocks?.hideViewersCursor;
const cursor = CursorService.getCurrentCursor(cursorId);
const hasPermission = presenter || WhiteboardService.hasMultiUserAccess(whiteboardId, cursor.userId);
if (cursor&& hasPermission) {
const {
xPercent: cursorX,
yPercent: cursorY,
userName,
userId,
presenter,
} = cursor;
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
return {
cursorX,
cursorY,
userName,
presenter: presenter,
uid: userId,
isRTL,
isViewersCursorLocked,
};
}
return {
cursorX: -1,
cursorY: -1,
userName: "",
};
})(CursorContainer)
);
CursorContainer.propTypes = {
// Defines the 'x' coordinate for the cursor, in percentages of the slide's width
cursorX: PropTypes.number.isRequired,
// Defines the 'y' coordinate for the cursor, in percentages of the slide's height
cursorY: PropTypes.number.isRequired,
};

View File

@ -1,69 +0,0 @@
/*
The purpose of this wrapper is to fetch an array of active cursor iDs only
and map them to the CursorContainer.
The reason for this is that Meteor tracks only the properties defined inside withTracker
and if we fetch the whole array of Cursors right away (let's say in the multi-user mode),
then the whole array would be re-rendered every time one of the Cursors is changed.
To work around this we map only an array of cursor iDs to CursorContainer.
This list will be updated in cases when new users join/leave the meeting.
And then each separate Cursorcontainer will keep
track of that particular Cursor properties, thus all of them will be updated independently.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { withTracker } from 'meteor/react-meteor-data';
import WhiteboardService from '/imports/ui/components/whiteboard/service';
import CursorWrapperService from './service';
import CursorContainer from '../container';
const CursorWrapperContainer = ({ presenterCursorId, multiUserCursorIds, ...rest }) => (
<g>
{Object.keys(presenterCursorId).length > 0
? (
<CursorContainer
key={presenterCursorId._id}
presenter
cursorId={presenterCursorId._id}
{...rest}
/>
)
: null }
{multiUserCursorIds.map((cursorId) => (
<CursorContainer
key={cursorId._id}
cursorId={cursorId._id}
presenter={false}
{...rest}
/>
))}
</g>
);
export default withTracker((params) => {
const { podId, whiteboardId } = params;
const cursorIds = CursorWrapperService.getCurrentCursorIds(podId, whiteboardId);
const { presenterCursorId, multiUserCursorIds } = cursorIds;
const isMultiUser = WhiteboardService.isMultiUserActive(whiteboardId);
return {
presenterCursorId,
multiUserCursorIds,
isMultiUser,
};
})(CursorWrapperContainer);
CursorWrapperContainer.propTypes = {
// Defines the object which contains the id of the presenter's cursor
presenterCursorId: PropTypes.shape({
_id: PropTypes.string,
}),
// Defines an optional array of cursors when multu-user whiteboard is on
multiUserCursorIds: PropTypes.arrayOf(PropTypes.object),
};
CursorWrapperContainer.defaultProps = {
presenterCursorId: {},
multiUserCursorIds: [],
};

View File

@ -1,56 +0,0 @@
import PresentationPods from '/imports/api/presentation-pods';
import Auth from '/imports/ui/services/auth';
import Cursor from '/imports/ui/components/cursor/service';
import WhiteboardService from '/imports/ui/components/whiteboard/service';
const getPresenterCursorId = (whiteboardId, userId) =>
Cursor.findOne(
{
whiteboardId,
userId,
},
{ fields: { _id: 1 } },
);
const getCurrentCursorIds = (podId, whiteboardId) => {
// object to return
const data = {};
// fetching the pod owner's id
const pod = PresentationPods.findOne({ meetingId: Auth.meetingID, podId });
if (pod) {
// fetching the presenter cursor id
data.presenterCursorId = getPresenterCursorId(whiteboardId, pod.currentPresenterId);
}
// checking whether multiUser mode is on or off
const isMultiUser = WhiteboardService.isMultiUserActive(whiteboardId);
// it's a multi-user mode - fetching all the cursors except the presenter's
if (isMultiUser) {
const selector = { whiteboardId };
const filter = {
fields: {
_id: 1,
},
};
// if there is a presenter cursor - excluding it from the query
if (data.presenterCursorId) {
selector._id = {
$ne: data.presenterCursorId._id,
};
}
data.multiUserCursorIds = Cursor.find(selector, filter).fetch();
} else {
// it's not multi-user, assigning an empty array
data.multiUserCursorIds = [];
}
return data;
};
export default {
getCurrentCursorIds,
};

View File

@ -1,19 +0,0 @@
import Cursor from '/imports/ui/components/cursor/service';
import Users from '/imports/api/users';
const getCurrentCursor = (cursorId) => {
const cursor = Cursor.findOne({ _id: cursorId });
if (cursor) {
const { userId } = cursor;
const user = Users.findOne({ userId }, { fields: { name: 1 } });
if (user) {
cursor.userName = user.name;
return cursor;
}
}
return false;
};
export default {
getCurrentCursor,
};

View File

@ -1,35 +0,0 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { TransitionGroup } from 'react-transition-group';
import Settings from '/imports/ui/services/settings';
import Styled from './styles';
export default (props) => {
const { hidePresentation } = props;
const { animations } = Settings.application;
return (
<TransitionGroup>
<Styled.Transition
classNames="transition"
appear
enter={false}
exit={false}
timeout={{ enter: 400 }}
>
<Styled.Content animations={animations}>
<Styled.DefaultContent hideContent={hidePresentation}>
<p>
<FormattedMessage
id="app.home.greeting"
description="Message to greet the user."
defaultMessage="Your presentation will begin shortly..."
/>
<br />
</p>
</Styled.DefaultContent>
</Styled.Content>
</Styled.Transition>
</TransitionGroup>
);
};

View File

@ -1,54 +0,0 @@
import styled from 'styled-components';
import { CSSTransition } from 'react-transition-group';
import { largeUp } from '/imports/ui/stylesheets/styled-components/breakpoints';
import { lineHeightComputed } from '/imports/ui/stylesheets/styled-components/typography';
const Transition = styled(CSSTransition)`
flex-basis: 90%;
@media ${largeUp} {
flex-basis: 90%;
}
`;
const Content = styled.div`
height: 100%;
width: 100%;
&.transition-appear {
opacity: 0.01;
}
&.transition-appear-active {
opacity: 1;
${({ animations }) => animations && `
transition: opacity 700ms ease-in;
`}
}
`;
const DefaultContent = styled.div`
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
padding: ${lineHeightComputed};
border: 0.25rem dashed;
border-radius: 1.5rem;
color: rgba(255, 255, 255, .5);
text-align: center;
overflow: auto;
${({ hideContent }) => hideContent && `
visibility: hidden;
`}
`;
export default {
Transition,
Content,
DefaultContent,
};

View File

@ -1,47 +0,0 @@
import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import PropTypes from 'prop-types';
import Styled from './styles';
const intlMessages = defineMessages({
downloadPresentationButton: {
id: 'app.downloadPresentationButton.label',
description: 'Download presentation label',
},
});
const propTypes = {
intl: PropTypes.object.isRequired,
handleDownloadPresentation: PropTypes.func.isRequired,
dark: PropTypes.bool,
};
const defaultProps = {
dark: false,
};
const DownloadPresentationButton = ({
intl,
handleDownloadPresentation,
dark,
}) => {
return (
<Styled.ButtonWrapper theme={dark ? 'dark' : 'light'}>
<Styled.DownloadButton
data-test="presentationDownload"
color="default"
icon="template_download"
size="sm"
onClick={handleDownloadPresentation}
label={intl.formatMessage(intlMessages.downloadPresentationButton)}
hideLabel
/>
</Styled.ButtonWrapper>
);
};
DownloadPresentationButton.propTypes = propTypes;
DownloadPresentationButton.defaultProps = defaultProps;
export default injectIntl(DownloadPresentationButton);

View File

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

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import PresentationPodsContainer from '../../presentation-pod/container'; import PresentationPodsContainer from '../../presentation-pod/container';
const PresentationArea = ({ const PresentationArea = ({
@ -17,3 +18,10 @@ const PresentationArea = ({
}; };
export default PresentationArea; export default PresentationArea;
PresentationArea.propTypes = {
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
presentationIsOpen: PropTypes.bool.isRequired,
darkTheme: PropTypes.bool.isRequired,
};

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { layoutSelectOutput } from '../../layout/context'; import { layoutSelectOutput } from '../../layout/context';
import PresentationArea from './component'; import PresentationArea from './component';
@ -9,3 +10,8 @@ const PresentationAreaContainer = ({ presentationIsOpen, darkTheme }) => {
}; };
export default PresentationAreaContainer; export default PresentationAreaContainer;
PresentationAreaContainer.propTypes = {
presentationIsOpen: PropTypes.bool.isRequired,
darkTheme: PropTypes.bool.isRequired,
};

View File

@ -5,13 +5,11 @@ import { toPng } from 'html-to-image';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import logger from '/imports/startup/client/logger'; import logger from '/imports/startup/client/logger';
import Styled from './styles'; import Styled from './styles';
import BBBMenu from "/imports/ui/components/common/menu/component"; import BBBMenu from '/imports/ui/components/common/menu/component';
import TooltipContainer from '/imports/ui/components/common/tooltip/container'; import TooltipContainer from '/imports/ui/components/common/tooltip/container';
import { ACTIONS } from '/imports/ui/components/layout/enums'; import { ACTIONS } from '/imports/ui/components/layout/enums';
import browserInfo from '/imports/utils/browserInfo'; import browserInfo from '/imports/utils/browserInfo';
const OLD_MINIMIZE_BUTTON_ENABLED = Meteor.settings.public.presentation.oldMinimizeButton;
const intlMessages = defineMessages({ const intlMessages = defineMessages({
downloading: { downloading: {
id: 'app.presentation.options.downloading', id: 'app.presentation.options.downloading',
@ -54,10 +52,10 @@ const intlMessages = defineMessages({
defaultMessage: 'Snapshot of current slide', defaultMessage: 'Snapshot of current slide',
}, },
whiteboardLabel: { whiteboardLabel: {
id: "app.shortcut-help.whiteboard", id: 'app.shortcut-help.whiteboard',
description: 'used for aria whiteboard options button label', description: 'used for aria whiteboard options button label',
defaultMessage: 'Whiteboard', defaultMessage: 'Whiteboard',
} },
}); });
const propTypes = { const propTypes = {
@ -65,23 +63,36 @@ const propTypes = {
formatMessage: PropTypes.func.isRequired, formatMessage: PropTypes.func.isRequired,
}).isRequired, }).isRequired,
handleToggleFullscreen: PropTypes.func.isRequired, handleToggleFullscreen: PropTypes.func.isRequired,
isDropdownOpen: PropTypes.bool,
isFullscreen: PropTypes.bool, isFullscreen: PropTypes.bool,
elementName: PropTypes.string, elementName: PropTypes.string,
fullscreenRef: PropTypes.instanceOf(Element), fullscreenRef: PropTypes.instanceOf(Element),
screenshotRef: PropTypes.instanceOf(Element),
meetingName: PropTypes.string, meetingName: PropTypes.string,
isIphone: PropTypes.bool, isIphone: PropTypes.bool,
elementId: PropTypes.string,
elementGroup: PropTypes.string,
currentElement: PropTypes.string,
currentGroup: PropTypes.string,
layoutContextDispatch: PropTypes.func.isRequired,
isRTL: PropTypes.bool,
tldrawAPI: PropTypes.shape({
copySvg: PropTypes.func.isRequired,
getShapes: PropTypes.func.isRequired,
currentPageId: PropTypes.string.isRequired,
}),
}; };
const defaultProps = { const defaultProps = {
isDropdownOpen: false,
isIphone: false, isIphone: false,
isFullscreen: false, isFullscreen: false,
isRTL: false,
elementName: '', elementName: '',
meetingName: '', meetingName: '',
fullscreenRef: null, fullscreenRef: null,
screenshotRef: null, elementId: '',
elementGroup: '',
currentElement: '',
currentGroup: '',
tldrawAPI: null,
}; };
const PresentationMenu = (props) => { const PresentationMenu = (props) => {
@ -99,7 +110,7 @@ const PresentationMenu = (props) => {
layoutContextDispatch, layoutContextDispatch,
meetingName, meetingName,
isIphone, isIphone,
isRTL isRTL,
} = props; } = props;
const [state, setState] = useState({ const [state, setState] = useState({
@ -177,7 +188,7 @@ const PresentationMenu = (props) => {
{ {
key: 'list-item-screenshot', key: 'list-item-screenshot',
label: intl.formatMessage(intlMessages.snapshotLabel), label: intl.formatMessage(intlMessages.snapshotLabel),
dataTest: "presentationSnapshot", dataTest: 'presentationSnapshot',
icon: 'video', icon: 'video',
onClick: async () => { onClick: async () => {
setState({ setState({
@ -262,42 +273,42 @@ const PresentationMenu = (props) => {
if (options.length === 0) { if (options.length === 0) {
const undoCtrls = document.getElementById('TD-Styles')?.nextSibling; const undoCtrls = document.getElementById('TD-Styles')?.nextSibling;
if (undoCtrls?.style) { if (undoCtrls?.style) {
undoCtrls.style = "padding:0px"; undoCtrls.style = 'padding:0px';
} }
const styleTool = document.getElementById('TD-Styles')?.parentNode; const styleTool = document.getElementById('TD-Styles')?.parentNode;
if (styleTool?.style) { if (styleTool?.style) {
styleTool.style = "right:0px"; styleTool.style = 'right:0px';
}
return null;
} }
return null
};
return ( return (
<Styled.Right> <Styled.Right>
<BBBMenu <BBBMenu
trigger={ trigger={(
<TooltipContainer title={intl.formatMessage(intlMessages.optionsLabel)}> <TooltipContainer title={intl.formatMessage(intlMessages.optionsLabel)}>
<Styled.DropdownButton <Styled.DropdownButton
state={isDropdownOpen ? 'open' : 'closed'} state={isDropdownOpen ? 'open' : 'closed'}
aria-label={`${intl.formatMessage(intlMessages.whiteboardLabel)} ${intl.formatMessage(intlMessages.optionsLabel)}`} aria-label={`${intl.formatMessage(intlMessages.whiteboardLabel)} ${intl.formatMessage(intlMessages.optionsLabel)}`}
data-test="whiteboardOptionsButton" data-test="whiteboardOptionsButton"
onClick={() => { onClick={() => {
setIsDropdownOpen((isOpen) => !isOpen) setIsDropdownOpen((isOpen) => !isOpen);
}} }}
> >
<Styled.ButtonIcon iconName="more" /> <Styled.ButtonIcon iconName="more" />
</Styled.DropdownButton> </Styled.DropdownButton>
</TooltipContainer> </TooltipContainer>
} )}
opts={{ opts={{
id: "presentation-dropdown-menu", id: 'presentation-dropdown-menu',
keepMounted: true, keepMounted: true,
transitionDuration: 0, transitionDuration: 0,
elevation: 3, elevation: 3,
getContentAnchorEl: null, getContentAnchorEl: null,
fullwidth: "true", fullwidth: 'true',
anchorOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' }, anchorOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' },
transformOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' }, transformOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' },
container: fullscreenRef container: fullscreenRef,
}} }}
actions={options} actions={options}
/> />

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { withTracker } from 'meteor/react-meteor-data'; import { withTracker } from 'meteor/react-meteor-data';
import PresentationMenu from './component'; import PresentationMenu from './component';
import FullscreenService from '/imports/ui/components/common/fullscreen-button/service'; import FullscreenService from '/imports/ui/components/common/fullscreen-button/service';
@ -42,3 +43,7 @@ export default withTracker((props) => {
meetingName: meetingObject.meetingProp.name, meetingName: meetingObject.meetingProp.name,
}; };
})(PresentationMenuContainer); })(PresentationMenuContainer);
PresentationMenuContainer.propTypes = {
elementId: PropTypes.string.isRequired,
};

View File

@ -1,627 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { throttle } from 'lodash';
import SlideCalcUtil, {
HUNDRED_PERCENT, MAX_PERCENT, STEP,
} from '/imports/utils/slideCalcUtils';
import {Meteor} from "meteor/meteor";
// After lots of trial and error on why synching doesn't work properly, I found I had to
// multiply the coordinates by 2. There's something I don't understand probably on the
// canvas coordinate system. (ralam feb 22, 2012)
// maximum value of z-index to prevent other things from overlapping
const MAX_Z_INDEX = (2 ** 31) - 1;
const HAND_TOOL = 'hand';
const MOUSE_INTERVAL = 32;
export default class PresentationOverlay extends Component {
static calculateDistance(touches) {
return Math.sqrt(((touches[0].clientX - touches[1].clientX) ** 2)
+ ((touches[0].clientY - touches[1].clientY) ** 2));
}
static touchCenterPoint(touches) {
let totalX = 0; let
totalY = 0;
for (let i = 0; i < touches.length; i += 1) {
totalX += touches[i].clientX;
totalY += touches[i].clientY;
}
return { x: totalX / touches.length, y: totalY / touches.length };
}
constructor(props) {
super(props);
this.currentMouseX = 0;
this.currentMouseY = 0;
this.prevZoom = props.zoom;
this.state = {
pressed: false,
};
// Mobile Firefox has a bug where e.preventDefault on touchstart doesn't prevent
// onmousedown from triggering right after. Thus we have to track it manually.
// In case if it's fixed one day - there is another issue, React one.
// https://github.com/facebook/react/issues/9809
// Check it to figure if you can add onTouchStart in render(), or should use raw DOM api
this.touchStarted = false;
this.handleTouchStart = this.handleTouchStart.bind(this);
this.handleTouchMove = throttle(this.handleTouchMove.bind(this), MOUSE_INTERVAL);
this.handleTouchEnd = this.handleTouchEnd.bind(this);
this.handleTouchCancel = this.handleTouchCancel.bind(this);
this.mouseDownHandler = this.mouseDownHandler.bind(this);
this.mouseMoveHandler = throttle(this.mouseMoveHandler.bind(this), MOUSE_INTERVAL);
this.mouseUpHandler = this.mouseUpHandler.bind(this);
this.mouseZoomHandler = this.mouseZoomHandler.bind(this);
this.tapedTwice = false;
}
componentDidMount() {
const {
zoom,
slideWidth,
svgWidth,
svgHeight,
userIsPresenter,
} = this.props;
if (userIsPresenter) {
this.viewBoxW = slideWidth / zoom * HUNDRED_PERCENT;
this.viewBoxH = svgHeight / svgWidth * this.viewBoxW;
this.doWidthBoundsDetection();
this.doHeightBoundsDetection();
this.pushSlideUpdate();
}
}
componentDidUpdate(prevProps) {
const {
zoom,
fitToWidth,
svgWidth,
svgHeight,
slideWidth,
userIsPresenter,
slide,
} = this.props;
if (!userIsPresenter) return;
if (zoom !== this.prevZoom) {
this.toolbarZoom();
}
if (fitToWidth !== prevProps.fitToWidth
|| this.checkResize(prevProps.svgWidth, prevProps.svgHeight)
|| slide.id !== prevProps.slide.id) {
this.viewBoxW = slideWidth / zoom * HUNDRED_PERCENT;
this.viewBoxH = svgHeight / svgWidth * this.viewBoxW;
this.doWidthBoundsDetection();
this.doHeightBoundsDetection();
this.pushSlideUpdate();
}
}
componentWillUnmount() {
window.removeEventListener('mousemove', this.mouseMoveHandler);
window.removeEventListener('mouseup', this.mouseUpHandler);
}
getTransformedSvgPoint(clientX, clientY) {
const {
getSvgRef,
} = this.props;
const svgObject = getSvgRef();
// If svgObject is not ready, return origin
if (!svgObject) return { x: 0, y: 0 };
const screenPoint = svgObject.createSVGPoint();
screenPoint.x = clientX;
screenPoint.y = clientY;
// transform a screen point to svg point
const CTM = svgObject.getScreenCTM();
return screenPoint.matrixTransform(CTM.inverse());
}
pushSlideUpdate() {
const {
updateLocalPosition,
panAndZoomChanger,
} = this.props;
if (this.didPositionChange()) {
this.calcViewedRegion();
updateLocalPosition(
this.viewBoxX, this.viewBoxY,
this.viewBoxW, this.viewBoxH,
this.prevZoom,
);
panAndZoomChanger(
this.viewedRegionW, this.viewedRegionH,
this.viewedRegionX, this.viewedRegionY,
);
}
}
checkResize(prevWidth, prevHeight) {
const {
svgWidth,
svgHeight,
} = this.props;
const heightChanged = svgWidth !== prevWidth;
const widthChanged = svgHeight !== prevHeight;
return heightChanged || widthChanged;
}
didPositionChange() {
const {
viewBoxX,
viewBoxY,
viewBoxWidth,
viewBoxHeight,
} = this.props;
return this.viewBoxX !== viewBoxX || this.viewBoxY !== viewBoxY
|| this.viewBoxW !== viewBoxWidth || this.viewBoxH !== viewBoxHeight;
}
panSlide(deltaX, deltaY) {
const {
zoom,
} = this.props;
this.viewBoxX += deltaX;
this.viewBoxY += deltaY;
this.doHeightBoundsDetection();
this.doWidthBoundsDetection();
this.prevZoom = zoom;
this.pushSlideUpdate();
}
toolbarZoom() {
const { zoom } = this.props;
const viewPortCenterX = this.viewBoxW / 2 + this.viewBoxX;
const viewPortCenterY = this.viewBoxH / 2 + this.viewBoxY;
this.doZoomCall(zoom, viewPortCenterX, viewPortCenterY);
}
doWidthBoundsDetection() {
const {
slideWidth,
} = this.props;
const verifyPositionToBound = (this.viewBoxW + this.viewBoxX);
if (this.viewBoxX <= 0) {
this.viewBoxX = 0;
} else if (verifyPositionToBound > slideWidth) {
this.viewBoxX = (slideWidth - this.viewBoxW);
}
}
doHeightBoundsDetection() {
const {
slideHeight,
} = this.props;
const verifyPositionToBound = (this.viewBoxH + this.viewBoxY);
if (this.viewBoxY < 0) {
this.viewBoxY = 0;
} else if (verifyPositionToBound > slideHeight) {
this.viewBoxY = (slideHeight - this.viewBoxH);
}
}
calcViewedRegion() {
const {
slideWidth,
slideHeight,
} = this.props;
this.viewedRegionW = SlideCalcUtil.calcViewedRegionWidth(this.viewBoxW, slideWidth);
this.viewedRegionH = SlideCalcUtil.calcViewedRegionHeight(this.viewBoxH, slideHeight);
this.viewedRegionX = SlideCalcUtil.calcViewedRegionX(this.viewBoxX, slideWidth);
this.viewedRegionY = SlideCalcUtil.calcViewedRegionY(this.viewBoxY, slideHeight);
}
// receives an svg coordinate and changes the values to percentages of the slide's width/height
svgCoordinateToPercentages(svgPoint) {
const {
slideWidth,
slideHeight,
} = this.props;
const point = {
x: (svgPoint.x / slideWidth) * 100,
y: (svgPoint.y / slideHeight) * 100,
};
return point;
}
doZoomCall(zoom, mouseX, mouseY) {
const {
svgWidth,
svgHeight,
slideWidth,
} = this.props;
const relXcoordInViewport = (mouseX - this.viewBoxX) / this.viewBoxW;
const relYcoordInViewport = (mouseY - this.viewBoxY) / this.viewBoxH;
this.viewBoxW = slideWidth / zoom * HUNDRED_PERCENT;
this.viewBoxH = svgHeight / svgWidth * this.viewBoxW;
this.viewBoxX = mouseX - (relXcoordInViewport * this.viewBoxW);
this.viewBoxY = mouseY - (relYcoordInViewport * this.viewBoxH);
this.doWidthBoundsDetection();
this.doHeightBoundsDetection();
this.prevZoom = zoom;
this.pushSlideUpdate();
}
mouseZoomHandler(e) {
const {
zoom,
userIsPresenter,
} = this.props;
if (!userIsPresenter) return;
let newZoom = zoom;
if (e.deltaY < 0) {
newZoom += STEP;
}
if (e.deltaY > 0) {
newZoom -= STEP;
}
if (newZoom <= HUNDRED_PERCENT) {
newZoom = HUNDRED_PERCENT;
} else if (newZoom >= MAX_PERCENT) {
newZoom = MAX_PERCENT;
}
if (newZoom === zoom) return;
const svgPosition = this.getTransformedSvgPoint(e.clientX, e.clientY);
this.doZoomCall(newZoom, svgPosition.x, svgPosition.y);
}
pinchStartHandler(event) {
if (!this.pinchGesture) return;
this.prevDiff = PresentationOverlay.calculateDistance(event.touches);
}
pinchMoveHandler(event) {
const {
zoom,
} = this.props;
if (!this.pinchGesture) return;
if (event.touches.length < 2) return;
const touchCenterPoint = PresentationOverlay.touchCenterPoint(event.touches);
const currDiff = PresentationOverlay.calculateDistance(event.touches);
if (currDiff > 0) {
let newZoom = zoom + (currDiff - this.prevDiff);
if (newZoom <= HUNDRED_PERCENT) {
newZoom = HUNDRED_PERCENT;
} else if (newZoom >= MAX_PERCENT) {
newZoom = MAX_PERCENT;
}
const svgPosition = this.getTransformedSvgPoint(touchCenterPoint.x, touchCenterPoint.y);
this.doZoomCall(newZoom, svgPosition.x, svgPosition.y);
}
this.prevDiff = currDiff;
}
panStartHandler(event) {
if (this.pinchGesture) return;
const touchCenterPoint = PresentationOverlay.touchCenterPoint(event.touches);
this.currentMouseX = touchCenterPoint.x;
this.currentMouseY = touchCenterPoint.y;
}
panMoveHandler(event) {
const {
slideHeight,
physicalSlideHeight,
} = this.props;
if (this.pinchGesture) return;
const touchCenterPoint = PresentationOverlay.touchCenterPoint(event.touches);
const physicalRatio = slideHeight / physicalSlideHeight;
const mouseDeltaX = physicalRatio * (this.currentMouseX - touchCenterPoint.x);
const mouseDeltaY = physicalRatio * (this.currentMouseY - touchCenterPoint.y);
this.currentMouseX = touchCenterPoint.x;
this.currentMouseY = touchCenterPoint.y;
this.panSlide(mouseDeltaX, mouseDeltaY);
}
tapHandler(event) {
const { annotationTool } = this.props;
if (event.touches.length === 2) return;
if (!this.tapedTwice) {
this.tapedTwice = true;
setTimeout(() => (this.tapedTwice = false), 300);
return;
}
event.preventDefault();
const sizeDefault = this.prevZoom === HUNDRED_PERCENT;
if (sizeDefault && annotationTool === HAND_TOOL) {
const touchCenterPoint = PresentationOverlay.touchCenterPoint(event.touches);
this.currentMouseX = touchCenterPoint.x;
this.currentMouseY = touchCenterPoint.y;
this.doZoomCall(200, touchCenterPoint.x, touchCenterPoint.y);
return;
}
this.doZoomCall(HUNDRED_PERCENT, 0, 0);
}
handleTouchStart(event) {
const {
annotationTool,
} = this.props;
if (annotationTool !== HAND_TOOL) return;
// to prevent default behavior (scrolling) on devices (in Safari), when you draw a text box
window.addEventListener('touchend', this.handleTouchEnd, { passive: false });
window.addEventListener('touchmove', this.handleTouchMove, { passive: false });
window.addEventListener('touchcancel', this.handleTouchCancel, true);
this.touchStarted = true;
const numberTouches = event.touches.length;
if (numberTouches === 2) {
this.pinchGesture = true;
this.pinchStartHandler(event);
} else if (numberTouches === 1) {
this.pinchGesture = false;
this.panStartHandler(event);
}
// / TODO Figure out what to do with this later
this.tapHandler(event);
}
handleTouchMove(event) {
const {
annotationTool,
userIsPresenter,
} = this.props;
if (annotationTool !== HAND_TOOL || !userIsPresenter) return;
event.preventDefault();
if (this.pinchGesture) {
this.pinchMoveHandler(event);
} else {
this.panMoveHandler(event);
}
}
handleTouchEnd(event) {
event.preventDefault();
// resetting the touchStarted flag
this.touchStarted = false;
window.removeEventListener('touchend', this.handleTouchEnd, { passive: false });
window.removeEventListener('touchmove', this.handleTouchMove, { passive: false });
window.removeEventListener('touchcancel', this.handleTouchCancel, true);
}
handleTouchCancel(event) {
event.preventDefault();
window.removeEventListener('touchend', this.handleTouchEnd, { passive: false });
window.removeEventListener('touchmove', this.handleTouchMove, { passive: false });
window.removeEventListener('touchcancel', this.handleTouchCancel, true);
}
mouseDownHandler(event) {
const {
annotationTool,
userIsPresenter,
} = this.props;
if (annotationTool !== HAND_TOOL || !userIsPresenter) return;
const isLeftClick = event.button === 0;
if (isLeftClick) {
this.currentMouseX = event.clientX;
this.currentMouseY = event.clientY;
this.setState({
pressed: true,
});
window.addEventListener('mousemove', this.mouseMoveHandler, { passive: false });
window.addEventListener('mouseup', this.mouseUpHandler, { passive: false });
}
}
mouseMoveHandler(event) {
const {
slideHeight,
annotationTool,
physicalSlideHeight,
} = this.props;
const {
pressed,
} = this.state;
if (annotationTool !== HAND_TOOL) return;
if (pressed) {
const mouseDeltaX = slideHeight / physicalSlideHeight * (this.currentMouseX - event.clientX);
const mouseDeltaY = slideHeight / physicalSlideHeight * (this.currentMouseY - event.clientY);
this.currentMouseX = event.clientX;
this.currentMouseY = event.clientY;
this.panSlide(mouseDeltaX, mouseDeltaY);
}
}
mouseUpHandler(event) {
const {
pressed,
} = this.state;
const isLeftClick = event.button === 0;
if (isLeftClick && pressed) {
this.setState({
pressed: false,
});
window.removeEventListener('mousemove', this.mouseMoveHandler);
window.removeEventListener('mouseup', this.mouseUpHandler);
}
}
render() {
const {
viewBoxX,
viewBoxY,
viewBoxWidth,
viewBoxHeight,
slideWidth,
slideHeight,
children,
userIsPresenter,
} = this.props;
const {
pressed,
} = this.state;
this.viewBoxW = viewBoxWidth;
this.viewBoxH = viewBoxHeight;
this.viewBoxX = viewBoxX;
this.viewBoxY = viewBoxY;
const baseName = Meteor.settings.public.app.cdn + Meteor.settings.public.app.basename + Meteor.settings.public.app.instanceId;
let cursor;
if (!userIsPresenter) {
cursor = undefined;
} else if (pressed) {
cursor = `url('${baseName}/resources/images/whiteboard-cursor/pan-closed.png') 4 8 , default`;
} else {
cursor = `url('${baseName}/resources/images/whiteboard-cursor/pan.png') 4 8, default`;
}
const overlayStyle = {
width: '100%',
height: '100%',
touchAction: 'none',
zIndex: MAX_Z_INDEX,
cursor,
};
return (
<foreignObject
clipPath="url(#viewBox)"
x="0"
y="0"
width={slideWidth}
height={slideHeight}
style={{ zIndex: MAX_Z_INDEX }}
>
<div
role="presentation"
onTouchStart={this.handleTouchStart}
onMouseDown={this.mouseDownHandler}
onWheel={this.mouseZoomHandler}
onBlur={() => {}}
style={overlayStyle}
>
{children}
</div>
</foreignObject>
);
}
}
PresentationOverlay.propTypes = {
// Defines a function which returns a reference to the main svg object
getSvgRef: PropTypes.func.isRequired,
// Defines the current zoom level (100 -> 400)
zoom: PropTypes.number.isRequired,
// Defines the width of the parent SVG. Used with svgHeight for aspect ratio
svgWidth: PropTypes.number.isRequired,
// Defines the height of the parent SVG. Used with svgWidth for aspect ratio
svgHeight: PropTypes.number.isRequired,
// Defines the calculated slide width (in svg coordinate system)
slideWidth: PropTypes.number.isRequired,
// Defines the calculated slide height (in svg coordinate system)
slideHeight: PropTypes.number.isRequired,
// Defines the local X value for the viewbox. Needed for pan/zoom
viewBoxX: PropTypes.number.isRequired,
// Defines the local Y value for the viewbox. Needed for pan/zoom
viewBoxY: PropTypes.number.isRequired,
// Defines the local width of the view box
viewBoxWidth: PropTypes.number.isRequired,
// Defines the local height of the view box
viewBoxHeight: PropTypes.number.isRequired,
// Defines the height of the slide in page coordinates for mouse movement
physicalSlideHeight: PropTypes.number.isRequired,
// Defines whether the local user has rights to change the slide position/dimensions
userIsPresenter: PropTypes.bool.isRequired,
// Defines whether the presentation area is in fitToWidth mode or not
fitToWidth: PropTypes.bool.isRequired,
// Defines the slide data. There's more in there, but we don't need it here
slide: PropTypes.shape({
// Defines the slide id. Used to tell if we changed slides
id: PropTypes.string.isRequired,
}).isRequired,
// Defines a function to send the new viewbox position and size for presenter rendering
updateLocalPosition: PropTypes.func.isRequired,
// Defines a function to send the new percent based position and size to other users
panAndZoomChanger: PropTypes.func.isRequired,
// Defines the currently selected annotation tool. When "hand" we can pan
annotationTool: PropTypes.string.isRequired,
// As a child we expect only a WhiteboardOverlay at this point
children: PropTypes.element.isRequired,
};

View File

@ -1,28 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { withTracker } from 'meteor/react-meteor-data';
import PresentationOverlay from './component';
import WhiteboardToolbarService from '../../whiteboard/whiteboard-toolbar/service';
const PresentationOverlayContainer = ({ children, ...rest }) => (
<PresentationOverlay {...rest}>
{children}
</PresentationOverlay>
);
export default withTracker(() => {
const drawSettings = WhiteboardToolbarService.getCurrentDrawSettings();
const tool = drawSettings ? drawSettings.whiteboardAnnotationTool : '';
return {
annotationTool: tool,
};
})(PresentationOverlayContainer);
PresentationOverlayContainer.propTypes = {
children: PropTypes.node,
};
PresentationOverlayContainer.defaultProps = {
children: undefined,
};

View File

@ -1,57 +0,0 @@
import React, { useEffect } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import Styled from './styles';
import { ACTIONS } from '/imports/ui/components/layout/enums';
const intlMessages = defineMessages({
presentationPlaceholderText: {
id: 'app.presentation.placeholder',
description: 'Presentation placeholder text',
},
});
const PresentationPlaceholder = ({
fullscreenContext,
intl,
setPresentationRef,
top,
left,
right,
height,
width,
zIndex,
layoutContextDispatch,
}) => {
useEffect(() => {
layoutContextDispatch({
type: ACTIONS.SET_PRESENTATION_NUM_CURRENT_SLIDE,
value: 0,
});
return () => {
layoutContextDispatch({
type: ACTIONS.SET_PRESENTATION_NUM_CURRENT_SLIDE,
value: 1,
});
}
}, []);
return <Styled.Placeholder
ref={(ref) => setPresentationRef(ref)}
data-test="presentationPlaceholder"
style={{
top,
left,
right,
width,
height,
zIndex: fullscreenContext ? zIndex : undefined,
display: width ? 'flex' : 'none',
}}
>
<span>
{intl.formatMessage(intlMessages.presentationPlaceholderText)}
</span>
</Styled.Placeholder>
};
export default injectIntl(PresentationPlaceholder);

View File

@ -1,29 +0,0 @@
import styled from 'styled-components';
import { colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
import { smPaddingX } from '/imports/ui/stylesheets/styled-components/general';
import { fontSizeLarger } from '/imports/ui/stylesheets/styled-components/typography';
const Placeholder = styled.div`
display: flex;
flex-direction: column;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 2px ${colorWhite} dashed;
justify-content: center;
align-items: center;
color: ${colorWhite};
overflow: hidden;
& > span {
margin: ${smPaddingX};
font-size: ${fontSizeLarger};
text-align: center;
}
`;
export default {
Placeholder,
};

View File

@ -1,18 +1,15 @@
import Presentations from '/imports/api/presentations'; import Presentations, { UploadingPresentations } from '/imports/api/presentations';
import React, { useState } from 'react'; import React from 'react';
import { useTracker } from 'meteor/react-meteor-data'; import { useTracker } from 'meteor/react-meteor-data';
import Icon from '/imports/ui/components/common/icon/component'; import Icon from '/imports/ui/components/common/icon/component';
import { makeCall } from '/imports/ui/services/api'; import { makeCall } from '/imports/ui/services/api';
import Styled from '/imports/ui/components/presentation/presentation-uploader/styles'; import Styled from '/imports/ui/components/presentation/presentation-uploader/styles';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { defineMessages } from 'react-intl'; import { defineMessages } from 'react-intl';
import _ from 'lodash';
import { UploadingPresentations } from '/imports/api/presentations';
const TIMEOUT_CLOSE_TOAST = 1; //second const TIMEOUT_CLOSE_TOAST = 1; // second
const intlMessages = defineMessages({ const intlMessages = defineMessages({
item: { item: {
id: 'app.presentationUploder.item', id: 'app.presentationUploder.item',
description: 'single item label', description: 'single item label',
@ -50,7 +47,7 @@ const intlMessages = defineMessages({
description: 'error that file exceed the size limit', description: 'error that file exceed the size limit',
}, },
CONVERSION_TIMEOUT: { CONVERSION_TIMEOUT: {
id:'app.presentationUploder.conversion.conversionTimeout', id: 'app.presentationUploder.conversion.conversionTimeout',
description: 'warns the user that the presentation timed out in the back-end in specific page of the document', description: 'warns the user that the presentation timed out in the back-end in specific page of the document',
}, },
FILE_TOO_LARGE: { FILE_TOO_LARGE: {
@ -112,11 +109,11 @@ const intlMessages = defineMessages({
}); });
function renderPresentationItemStatus(item, intl) { function renderPresentationItemStatus(item, intl) {
if ((("progress" in item) && item.progress === 0) || (("upload" in item) && item.upload.progress === 0 && !item.upload.error)) { if ((('progress' in item) && item.progress === 0) || (('upload' in item) && item.upload.progress === 0 && !item.upload.error)) {
return intl.formatMessage(intlMessages.fileToUpload); return intl.formatMessage(intlMessages.fileToUpload);
} }
if (("progress" in item) && item.progress < 100 && !("conversion" in item)) { if (('progress' in item) && item.progress < 100 && !('conversion' in item)) {
return intl.formatMessage(intlMessages.uploadProcess, { return intl.formatMessage(intlMessages.uploadProcess, {
0: Math.floor(item.progress).toString(), 0: Math.floor(item.progress).toString(),
}); });
@ -124,22 +121,21 @@ function renderPresentationItemStatus(item, intl) {
const constraint = {}; const constraint = {};
if (("upload" in item) && (item.upload.done && item.upload.error)) { if (('upload' in item) && (item.upload.done && item.upload.error)) {
if (item.conversion.status === 'FILE_TOO_LARGE' || item.upload.status !== 413) { if (item.conversion.status === 'FILE_TOO_LARGE' || item.upload.status !== 413) {
constraint['0'] = ((item.conversion.maxFileSize) / 1000 / 1000).toFixed(2); constraint['0'] = ((item.conversion.maxFileSize) / 1000 / 1000).toFixed(2);
} else { } else if (item.progress < 100) {
if (item.progress < 100) {
const errorMessage = intlMessages.badConnectionError; const errorMessage = intlMessages.badConnectionError;
return intl.formatMessage(errorMessage); return intl.formatMessage(errorMessage);
} }
}
const errorMessage = intlMessages[item.upload.status] || intlMessages.genericError; const errorMessage = intlMessages[item.upload.status] || intlMessages.genericError;
return intl.formatMessage(errorMessage, constraint); return intl.formatMessage(errorMessage, constraint);
} }
if (("conversion" in item) && (!item.conversion.done && item.conversion.error)) { if (('conversion' in item) && (!item.conversion.done && item.conversion.error)) {
const errorMessage = intlMessages[item.conversion.status] || intlMessages.genericConversionStatus; const errorMessage = intlMessages[item.conversion.status]
|| intlMessages.genericConversionStatus;
switch (item.conversion.status) { switch (item.conversion.status) {
case 'CONVERSION_TIMEOUT': case 'CONVERSION_TIMEOUT':
@ -166,9 +162,9 @@ function renderPresentationItemStatus(item, intl) {
return intl.formatMessage(errorMessage, constraint); return intl.formatMessage(errorMessage, constraint);
} }
if ((("conversion" in item) && (!item.conversion.done && !item.conversion.error)) || (("progress" in item) && item.progress == 100)) { if ((('conversion' in item) && (!item.conversion.done && !item.conversion.error)) || (('progress' in item) && item.progress === 100)) {
let conversionStatusMessage let conversionStatusMessage;
if ("conversion" in item) { if ('conversion' in item) {
if (item.conversion.pagesCompleted < item.conversion.numPages) { if (item.conversion.pagesCompleted < item.conversion.numPages) {
return intl.formatMessage(intlMessages.conversionProcessingSlides, { return intl.formatMessage(intlMessages.conversionProcessingSlides, {
0: item.conversion.pagesCompleted, 0: item.conversion.pagesCompleted,
@ -188,10 +184,9 @@ function renderPresentationItemStatus(item, intl) {
} }
function renderToastItem(item, intl) { function renderToastItem(item, intl) {
const isUploading = ('progress' in item) && item.progress <= 100;
const isUploading = ("progress" in item) && item.progress <= 100; const isConverting = ('conversion' in item) && !item.conversion.done;
const isConverting = ("conversion" in item) && !item.conversion.done; const hasError = ((('conversion' in item) && item.conversion.error) || (('upload' in item) && item.upload.error));
const hasError = ((("conversion" in item) && item.conversion.error) || (("upload" in item) && item.upload.error));
const isProcessing = (isUploading || isConverting) && !hasError; const isProcessing = (isUploading || isConverting) && !hasError;
let icon = isProcessing ? 'blank' : 'check'; let icon = isProcessing ? 'blank' : 'check';
@ -215,7 +210,7 @@ function renderToastItem(item, intl) {
<Styled.ToastItemIcon <Styled.ToastItemIcon
done={!isProcessing && !hasError} done={!isProcessing && !hasError}
error={hasError} error={hasError}
loading={ isProcessing } loading={isProcessing}
iconName={icon} iconName={icon}
/> />
</Styled.StatusIcon> </Styled.StatusIcon>
@ -230,16 +225,15 @@ function renderToastItem(item, intl) {
} }
const renderToastList = (presentations, intl) => { const renderToastList = (presentations, intl) => {
let converted = 0; let converted = 0;
let presentationsSorted = presentations const presentationsSorted = presentations
.sort((a, b) => a.uploadTimestamp - b.uploadTimestamp) .sort((a, b) => a.uploadTimestamp - b.uploadTimestamp)
.sort((a, b) => { .sort((a, b) => {
const presADone = a.conversion ? a.conversion.done : false; const presADone = a.conversion ? a.conversion.done : false;
const presBDone = b.conversion ? b.conversion.done : false; const presBDone = b.conversion ? b.conversion.done : false;
return presADone - presBDone return presADone - presBDone;
}); });
presentationsSorted presentationsSorted
@ -275,7 +269,7 @@ const renderToastList = (presentations, intl) => {
} }
return ( return (
<Styled.ToastWrapper data-test="presentationUploadProgressToast" > <Styled.ToastWrapper data-test="presentationUploadProgressToast">
<Styled.UploadToastHeader> <Styled.UploadToastHeader>
<Styled.UploadIcon iconName="upload" /> <Styled.UploadIcon iconName="upload" />
<Styled.UploadToastTitle>{toastHeading}</Styled.UploadToastTitle> <Styled.UploadToastTitle>{toastHeading}</Styled.UploadToastTitle>
@ -289,86 +283,99 @@ const renderToastList = (presentations, intl) => {
</Styled.InnerToast> </Styled.InnerToast>
</Styled.ToastWrapper> </Styled.ToastWrapper>
); );
} };
function handleDismissToast(toastId) { function handleDismissToast(toastId) {
return toast.dismiss(toastId); return toast.dismiss(toastId);
} }
const alreadyRenderedPresList = [];
let alreadyRenderedPresList = []; const enteredConversion = {};
let enteredConversion = {};
export const PresentationUploaderToast = ({ intl }) => { export const PresentationUploaderToast = ({ intl }) => {
useTracker(() => { useTracker(() => {
const presentationsRenderedFalseAndConversionFalse = Presentations.find({ $or: [{ renderedInToast: false }, { 'conversion.done': false }] }).fetch();
const presentationsRenderedFalseAndConversionFalse = Presentations.find({ $or: [{renderedInToast: false}, {"conversion.done": false}] }).fetch(); const convertingPresentations = presentationsRenderedFalseAndConversionFalse
.filter((p) => !p.renderedInToast);
const convertingPresentations = presentationsRenderedFalseAndConversionFalse.filter(p => !p.renderedInToast ); // removing ones with errors.
// If presentation has an error status - we don't want to have it pending as uploading
let conversionInterrupted = false; convertingPresentations.forEach((p) => {
if ('conversion' in p && p.conversion.error) {
// removing ones with errors. If presentation has an error status - we don't want to have it pending as uploading UploadingPresentations.remove(
convertingPresentations.map(p => { { $or: [{ temporaryPresentationId: p.temporaryPresentationId }, { id: p.id }] },
if ("conversion" in p && p.conversion.error){ );
UploadingPresentations.remove({$or: [{temporaryPresentationId: p.temporaryPresentationId }, {id: p.id}]});
conversionInterrupted = true;
} }
}); });
let toRemoveFromUploadingPresentations = []; const toRemoveFromUploadingPresentations = [];
// main goal of this mapping is to sort out what doesn't need to be displayed
UploadingPresentations.find().fetch().map(p => { // main goal of this mapping is to sort out what doesn't need to be displayed UploadingPresentations.find().fetch().forEach((p) => {
if ( if (
( "upload" in p && p.upload.done ) // if presentation is marked as done - it's potentially to be removed ('upload' in p && p.upload.done) // if presentation is marked as done - it's potentially to be removed
&& !p.subscriptionId // at upload stage or already converted && !p.subscriptionId // at upload stage or already converted
) { ) {
if(convertingPresentations[0]) { //there are presentations being converted if (convertingPresentations[0]) { // there are presentations being converted
convertingPresentations.forEach(cp => { convertingPresentations.forEach((cp) => {
if (cp.temporaryPresentationId == p.temporaryPresentationId) { // if this presentation is being converted we don't want it to be marked as still uploading // if this presentation is being converted
toRemoveFromUploadingPresentations.push({temporaryPresentationId: p.temporaryPresentationId, id: p.id}); // we don't want it to be marked as still uploading
if (cp.temporaryPresentationId === p.temporaryPresentationId) {
toRemoveFromUploadingPresentations
.push({ temporaryPresentationId: p.temporaryPresentationId, id: p.id });
} }
}); });
} else if (!enteredConversion[p.temporaryPresentationId]) { // upload stage is done and pesentation is entering conversion stage // upload stage is done and pesentation is entering conversion stage
enteredConversion[p.temporaryPresentationId] = true; // we mark that it has entered conversion stage } else if (!enteredConversion[p.temporaryPresentationId]) {
// we mark that it has entered conversion stage
enteredConversion[p.temporaryPresentationId] = true;
} else { } else {
// presentation doesn't normally enter conversion twice // presentation doesn't normally enter conversion twice so we remove
// so we remove the inconsistencies between UploadingPresentation and Presentation (corner case) // the inconsistencies between UploadingPresentation and Presentation (corner case)
presentationsAlreadyRenderedIds = Presentations.find({renderedInToast: true}).fetch().map(p => { const presentationsAlreadyRenderedIds = Presentations
return { .find({ renderedInToast: true }).fetch().map((pr) => (
id: p.id, {
temporaryPresentationId: p.temporaryPresentationId, id: pr.id,
temporaryPresentationId: pr.temporaryPresentationId,
} }
));
presentationsAlreadyRenderedIds.forEach((pr) => {
UploadingPresentations.remove({
$or: [{ temporaryPresentationId: pr.temporaryPresentationId }, { id: pr.id }],
});
}); });
presentationsAlreadyRenderedIds.forEach(p => {
UploadingPresentations.remove({$or: [{temporaryPresentationId: p.temporaryPresentationId},
{id: p.id}]})
})
} }
} }
}); });
toRemoveFromUploadingPresentations.forEach(p => UploadingPresentations.remove({$or: [{temporaryPresentationId: p.temporaryPresentationId }, {id: p.id}]})); toRemoveFromUploadingPresentations.forEach((p) => {
UploadingPresentations
.remove({ $or: [{ temporaryPresentationId: p.temporaryPresentationId }, { id: p.id }] });
});
const uploadingPresentations = UploadingPresentations.find().fetch(); const uploadingPresentations = UploadingPresentations.find().fetch();
let presentationsToConvert = convertingPresentations.concat(uploadingPresentations); let presentationsToConvert = convertingPresentations.concat(uploadingPresentations);
// Updating or populating the "state" presentation list // Updating or populating the "state" presentation list
presentationsToConvert.map(p => { presentationsToConvert.map((p) => (
return { {
filename: p.name || p.filename, filename: p.name || p.filename,
temporaryPresentationId: p.temporaryPresentationId, temporaryPresentationId: p.temporaryPresentationId,
presentationId: p.id, presentationId: p.id,
hasError: p.conversion?.error || p.upload?.error, hasError: p.conversion?.error || p.upload?.error,
lastModifiedUploader: p.lastModifiedUploader, lastModifiedUploader: p.lastModifiedUploader,
} }
}).forEach(p => { )).forEach((p) => {
const docIndexAlreadyInList = alreadyRenderedPresList.findIndex(pres => { const docIndexAlreadyInList = alreadyRenderedPresList.findIndex((pres) => (
return (pres.temporaryPresentationId === p.temporaryPresentationId || pres.presentationId === p.presentationId (pres.temporaryPresentationId === p.temporaryPresentationId
|| (pres.lastModifiedUploader !== undefined && !pres.lastModifiedUploader && pres.filename === p.filename))}); || pres.presentationId === p.presentationId
|| (
pres.lastModifiedUploader !== undefined
&& !pres.lastModifiedUploader && pres.filename === p.filename
)
)
));
if (docIndexAlreadyInList === -1) { if (docIndexAlreadyInList === -1) {
alreadyRenderedPresList.push({ alreadyRenderedPresList.push({
filename: p.filename, filename: p.filename,
@ -379,13 +386,14 @@ export const PresentationUploaderToast = ({ intl }) => {
hasError: p.hasError, hasError: p.hasError,
}); });
} else { } else {
alreadyRenderedPresList[docIndexAlreadyInList].temporaryPresentationId = p.temporaryPresentationId; const presAlreadyRendered = alreadyRenderedPresList[docIndexAlreadyInList];
alreadyRenderedPresList[docIndexAlreadyInList].presentationId = p.presentationId; presAlreadyRendered.temporaryPresentationId = p.temporaryPresentationId;
alreadyRenderedPresList[docIndexAlreadyInList].lastModifiedUploader = p.lastModifiedUploader; presAlreadyRendered.presentationId = p.presentationId;
alreadyRenderedPresList[docIndexAlreadyInList].hasError = p.hasError; presAlreadyRendered.lastModifiedUploader = p.lastModifiedUploader;
presAlreadyRendered.hasError = p.hasError;
} }
}) });
let activeToast = Session.get("presentationUploaderToastId"); let activeToast = Session.get('presentationUploaderToastId');
const showToast = presentationsToConvert.length > 0; const showToast = presentationsToConvert.length > 0;
if (showToast && !activeToast) { if (showToast && !activeToast) {
@ -394,51 +402,51 @@ export const PresentationUploaderToast = ({ intl }) => {
autoClose: false, autoClose: false,
newestOnTop: true, newestOnTop: true,
closeOnClick: true, closeOnClick: true,
className: "presentationUploaderToast toastClass", className: 'presentationUploaderToast toastClass',
onClose: () => { onClose: () => {
presentationsToConvert = []; presentationsToConvert = [];
if (alreadyRenderedPresList.every((pres) => pres.rendered)) { if (alreadyRenderedPresList.every((pres) => pres.rendered)) {
makeCall('setPresentationRenderedInToast').then(() => { makeCall('setPresentationRenderedInToast').then(() => {
Session.set("presentationUploaderToastId", null); Session.set('presentationUploaderToastId', null);
}); });
alreadyRenderedPresList.length = 0; alreadyRenderedPresList.length = 0;
} }
}, },
}); });
Session.set("presentationUploaderToastId", activeToast); Session.set('presentationUploaderToastId', activeToast);
} else if (!showToast && activeToast) { } else if (!showToast && activeToast) {
handleDismissToast(activeToast); handleDismissToast(activeToast);
Session.set("presentationUploaderToastId", null); Session.set('presentationUploaderToastId', null);
} else { } else {
toast.update(activeToast, { toast.update(activeToast, {
render: renderToastList(presentationsToConvert, intl), render: renderToastList(presentationsToConvert, intl),
}); });
} }
let temporaryPresentationIdListToSetAsRendered = presentationsToConvert.filter(p => const temporaryPresentationIdListToSetAsRendered = presentationsToConvert.filter((p) => (
("conversion" in p && (p.conversion.done || p.conversion.error))) 'conversion' in p && (p.conversion.done || p.conversion.error)
));
temporaryPresentationIdListToSetAsRendered = temporaryPresentationIdListToSetAsRendered.map(p => { temporaryPresentationIdListToSetAsRendered.forEach((p) => {
index = alreadyRenderedPresList.findIndex(pres => (pres.temporaryPresentationId === p.temporaryPresentationId || pres.presentationId === p.id)); const index = alreadyRenderedPresList.findIndex((pres) => (
pres.temporaryPresentationId === p.temporaryPresentationId || pres.presentationId === p.id
));
if (index !== -1) { if (index !== -1) {
alreadyRenderedPresList[index].rendered = true; alreadyRenderedPresList[index].rendered = true;
} }
return p.temporaryPresentationId
}); });
if (alreadyRenderedPresList.every((pres) => pres.rendered && !pres.hasError)) { if (alreadyRenderedPresList.every((pres) => pres.rendered && !pres.hasError)) {
setTimeout(() => { setTimeout(() => {
makeCall('setPresentationRenderedInToast'); makeCall('setPresentationRenderedInToast');
alreadyRenderedPresList.length = 0; alreadyRenderedPresList.length = 0;
}, TIMEOUT_CLOSE_TOAST * 1000); }, TIMEOUT_CLOSE_TOAST * 1000);
} }
}, []); }, []);
return null; return null;
} };
export default { export default {
handleDismissToast, handleDismissToast,
renderPresentationItemStatus, renderPresentationItemStatus,
} };

View File

@ -265,8 +265,6 @@ class PresentationToolbar extends PureComponent {
slidePosition, slidePosition,
multiUserSize, multiUserSize,
multiUser, multiUser,
setIsPanning,
isPanning,
} = this.props; } = this.props;
const { isMobile } = deviceInfo; const { isMobile } = deviceInfo;
@ -276,14 +274,12 @@ class PresentationToolbar extends PureComponent {
const prevSlideAriaLabel = startOfSlides const prevSlideAriaLabel = startOfSlides
? intl.formatMessage(intlMessages.previousSlideLabel) ? intl.formatMessage(intlMessages.previousSlideLabel)
: `${intl.formatMessage(intlMessages.previousSlideLabel)} (${ : `${intl.formatMessage(intlMessages.previousSlideLabel)} (${currentSlideNum <= 1 ? '' : currentSlideNum - 1
currentSlideNum <= 1 ? '' : currentSlideNum - 1
})`; })`;
const nextSlideAriaLabel = endOfSlides const nextSlideAriaLabel = endOfSlides
? intl.formatMessage(intlMessages.nextSlideLabel) ? intl.formatMessage(intlMessages.nextSlideLabel)
: `${intl.formatMessage(intlMessages.nextSlideLabel)} (${ : `${intl.formatMessage(intlMessages.nextSlideLabel)} (${currentSlideNum >= 1 ? currentSlideNum + 1 : ''
currentSlideNum >= 1 ? currentSlideNum + 1 : ''
})`; })`;
return ( return (
@ -400,20 +396,6 @@ class PresentationToolbar extends PureComponent {
/> />
</TooltipContainer> </TooltipContainer>
) : null} ) : null}
<Styled.FitToWidthButton
role="button"
data-test="panButton"
aria-label={intl.formatMessage(intlMessages.pan)}
color="light"
disabled={(zoom <= HUNDRED_PERCENT && !fitToWidth)}
icon="hand"
size="md"
circle
onClick={setIsPanning}
label={intl.formatMessage(intlMessages.pan)}
hideLabel
panning={isPanning}
/>
<Styled.FitToWidthButton <Styled.FitToWidthButton
role="button" role="button"
data-test="fitToWidthButton" data-test="fitToWidthButton"
@ -467,6 +449,25 @@ PresentationToolbar.propTypes = {
fullscreenAction: PropTypes.string.isRequired, fullscreenAction: PropTypes.string.isRequired,
isFullscreen: PropTypes.bool.isRequired, isFullscreen: PropTypes.bool.isRequired,
layoutContextDispatch: PropTypes.func.isRequired, layoutContextDispatch: PropTypes.func.isRequired,
setIsPanning: PropTypes.func.isRequired,
multiUser: PropTypes.bool.isRequired,
whiteboardId: PropTypes.string.isRequired,
removeWhiteboardGlobalAccess: PropTypes.func.isRequired,
addWhiteboardGlobalAccess: PropTypes.func.isRequired,
fullscreenRef: PropTypes.instanceOf(Element),
handleToggleFullScreen: PropTypes.func.isRequired,
isPollingEnabled: PropTypes.bool.isRequired,
amIPresenter: PropTypes.bool.isRequired,
currentSlidHasContent: PropTypes.bool.isRequired,
parseCurrentSlideContent: PropTypes.func.isRequired,
startPoll: PropTypes.func.isRequired,
currentSlide: PropTypes.shape().isRequired,
slidePosition: PropTypes.shape().isRequired,
multiUserSize: PropTypes.number.isRequired,
};
PresentationToolbar.defaultProps = {
fullscreenRef: null,
}; };
export default injectWbResizeEvent(injectIntl(PresentationToolbar)); export default injectWbResizeEvent(injectIntl(PresentationToolbar));

View File

@ -77,4 +77,9 @@ PresentationToolbarContainer.propTypes = {
nextSlide: PropTypes.func.isRequired, nextSlide: PropTypes.func.isRequired,
previousSlide: PropTypes.func.isRequired, previousSlide: PropTypes.func.isRequired,
skipToSlide: PropTypes.func.isRequired, skipToSlide: PropTypes.func.isRequired,
layoutSwapped: PropTypes.bool,
};
PresentationToolbarContainer.defaultProps = {
layoutSwapped: false,
}; };

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages } from 'react-intl'; import { defineMessages } from 'react-intl';
import { safeMatch } from '/imports/utils/string-utils'; import { safeMatch } from '/imports/utils/string-utils';
import { isUrlValid, startWatching } from '/imports/ui/components/external-video-player/service'; import { isUrlValid, startWatching } from '/imports/ui/components/external-video-player/service';
@ -65,3 +66,18 @@ export const SmartMediaShare = (props) => {
}; };
export default SmartMediaShare; export default SmartMediaShare;
SmartMediaShare.propTypes = {
currentSlide: PropTypes.shape({
content: PropTypes.string.isRequired,
}),
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
isMobile: PropTypes.bool.isRequired,
isRTL: PropTypes.bool.isRequired,
};
SmartMediaShare.defaultProps = {
currentSlide: undefined,
};

View File

@ -235,7 +235,10 @@ class ZoomTool extends PureComponent {
} }
const propTypes = { const propTypes = {
intl: PropTypes.object.isRequired, intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
formatNumber: PropTypes.func.isRequired,
}).isRequired,
zoomValue: PropTypes.number.isRequired, zoomValue: PropTypes.number.isRequired,
change: PropTypes.func.isRequired, change: PropTypes.func.isRequired,
minBound: PropTypes.number.isRequired, minBound: PropTypes.number.isRequired,

View File

@ -15,28 +15,41 @@ import { registerTitleView, unregisterTitleView } from '/imports/utils/dom-utils
import Styled from './styles'; import Styled from './styles';
import Settings from '/imports/ui/services/settings'; import Settings from '/imports/ui/services/settings';
import Radio from '/imports/ui/components/common/radio/component'; import Radio from '/imports/ui/components/common/radio/component';
import { isPresentationEnabled } from '/imports/ui/services/features';
const { isMobile } = deviceInfo; const { isMobile } = deviceInfo;
const propTypes = { const propTypes = {
allowDownloadable: PropTypes.bool.isRequired, allowDownloadable: PropTypes.bool.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
fileUploadConstraintsHint: PropTypes.bool.isRequired, fileUploadConstraintsHint: PropTypes.bool.isRequired,
fileSizeMax: PropTypes.number.isRequired, fileSizeMax: PropTypes.number.isRequired,
filePagesMax: PropTypes.number.isRequired, filePagesMax: PropTypes.number.isRequired,
handleSave: PropTypes.func.isRequired, handleSave: PropTypes.func.isRequired,
dispatchTogglePresentationDownloadable: PropTypes.func.isRequired, dispatchTogglePresentationDownloadable: PropTypes.func.isRequired,
fileValidMimeTypes: PropTypes.arrayOf(PropTypes.object).isRequired, fileValidMimeTypes: PropTypes.arrayOf(PropTypes.shape).isRequired,
presentations: PropTypes.arrayOf(PropTypes.shape({ presentations: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
filename: PropTypes.string.isRequired, filename: PropTypes.string.isRequired,
isCurrent: PropTypes.bool.isRequired, isCurrent: PropTypes.bool.isRequired,
conversion: PropTypes.object, conversion: PropTypes.shape,
upload: PropTypes.object, upload: PropTypes.shape,
})).isRequired, })).isRequired,
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
handleFiledrop: PropTypes.func.isRequired,
selectedToBeNextCurrent: PropTypes.string,
renderPresentationItemStatus: PropTypes.func.isRequired,
externalUploadData: PropTypes.shape({
presentationUploadExternalDescription: PropTypes.string.isRequired,
presentationUploadExternalUrl: PropTypes.string.isRequired,
}).isRequired,
isPresenter: PropTypes.bool.isRequired,
exportPresentationToChat: PropTypes.func.isRequired,
}; };
const defaultProps = { const defaultProps = {
selectedToBeNextCurrent: '',
}; };
const intlMessages = defineMessages({ const intlMessages = defineMessages({
@ -147,7 +160,7 @@ const intlMessages = defineMessages({
id: 'app.presentationUploder.conversion.timeout', id: 'app.presentationUploder.conversion.timeout',
}, },
CONVERSION_TIMEOUT: { CONVERSION_TIMEOUT: {
id:'app.presentationUploder.conversion.conversionTimeout', id: 'app.presentationUploder.conversion.conversionTimeout',
description: 'warns the user that the presentation timed out in the back-end in specific page of the document', description: 'warns the user that the presentation timed out in the back-end in specific page of the document',
}, },
GENERATING_THUMBNAIL: { GENERATING_THUMBNAIL: {
@ -304,6 +317,8 @@ const EXPORT_STATUSES = {
EXPORTED: 'EXPORTED', EXPORTED: 'EXPORTED',
}; };
const handleDismissToast = (id) => toast.dismiss(id);
class PresentationUploader extends Component { class PresentationUploader extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -311,7 +326,6 @@ class PresentationUploader extends Component {
this.state = { this.state = {
presentations: [], presentations: [],
disableActions: false, disableActions: false,
toUploadCount: 0,
presExporting: new Set(), presExporting: new Set(),
}; };
@ -319,8 +333,9 @@ class PresentationUploader extends Component {
this.hasError = null; this.hasError = null;
this.exportToastId = null; this.exportToastId = null;
const { handleFiledrop } = this.props;
// handlers // handlers
this.handleFiledrop = this.props.handleFiledrop; this.handleFiledrop = handleFiledrop;
this.handleConfirm = this.handleConfirm.bind(this); this.handleConfirm = this.handleConfirm.bind(this);
this.handleDismiss = this.handleDismiss.bind(this); this.handleDismiss = this.handleDismiss.bind(this);
this.handleRemove = this.handleRemove.bind(this); this.handleRemove = this.handleRemove.bind(this);
@ -353,24 +368,28 @@ class PresentationUploader extends Component {
}); });
if (propPresentations.length > prevPropPresentations.length) { if (propPresentations.length > prevPropPresentations.length) {
shouldUpdateState = true; shouldUpdateState = true;
const propsDiffs = propPresentations.filter(p => const propsDiffs = propPresentations.filter(
!prevPropPresentations.some(presentation => p.id === presentation.id (p) => !prevPropPresentations.some(
|| p.temporaryPresentationId === presentation.temporaryPresentationId)); (presentation) => p.id === presentation.id
|| p.temporaryPresentationId === presentation.temporaryPresentationId,
),
);
propsDiffs.forEach(p => { propsDiffs.forEach((p) => {
const index = presState.findIndex(pres => { const index = presState.findIndex(
return pres.temporaryPresentationId === p.temporaryPresentationId || pres.id === p.id; (pres) => pres.temporaryPresentationId === p.temporaryPresentationId || pres.id === p.id,
}); );
if (index === -1) { if (index === -1) {
presState.push(p); presState.push(p);
} }
}) });
} }
const presStateFiltered = presState.filter((presentation) => { const presStateFiltered = presState.filter((presentation) => {
const currentPropPres = propPresentations.find((pres) => pres.id === presentation.id); const currentPropPres = propPresentations.find((pres) => pres.id === presentation.id);
const prevPropPres = prevPropPresentations.find((pres) => pres.id === presentation.id); const prevPropPres = prevPropPresentations.find((pres) => pres.id === presentation.id);
const hasConversionError = presentation?.conversion?.error; const hasConversionError = presentation?.conversion?.error;
const finishedConversion = presentation?.conversion?.done || currentPropPres?.conversion?.done; const finishedConversion = presentation?.conversion?.done
|| currentPropPres?.conversion?.done;
const hasTemporaryId = presentation.id.startsWith(presentation.filename); const hasTemporaryId = presentation.id.startsWith(presentation.filename);
if (hasConversionError || (!finishedConversion && hasTemporaryId)) return true; if (hasConversionError || (!finishedConversion && hasTemporaryId)) return true;
@ -380,18 +399,19 @@ class PresentationUploader extends Component {
shouldUpdateState = true; shouldUpdateState = true;
} }
const modPresentation = presentation;
if (currentPropPres.isCurrent !== prevPropPres?.isCurrent) { if (currentPropPres.isCurrent !== prevPropPres?.isCurrent) {
presentation.isCurrent = currentPropPres.isCurrent; modPresentation.isCurrent = currentPropPres.isCurrent;
} }
presentation.conversion = currentPropPres.conversion; modPresentation.conversion = currentPropPres.conversion;
presentation.isRemovable = currentPropPres.isRemovable; modPresentation.isRemovable = currentPropPres.isRemovable;
return true; return true;
}).filter(presentation => { }).filter((presentation) => {
const duplicated = presentations.find( const duplicated = presentations.find(
(pres) => pres.filename === presentation.filename (pres) => pres.filename === presentation.filename
&& pres.id !== presentation.id && pres.id !== presentation.id,
); );
if (duplicated if (duplicated
&& duplicated.id.startsWith(presentation.filename) && duplicated.id.startsWith(presentation.filename)
@ -404,7 +424,7 @@ class PresentationUploader extends Component {
if (shouldUpdateState) { if (shouldUpdateState) {
this.setState({ this.setState({
presentations: _.uniqBy(presStateFiltered, 'id') presentations: _.uniqBy(presStateFiltered, 'id'),
}); });
} }
@ -415,8 +435,7 @@ class PresentationUploader extends Component {
// Updates presentation list when chat modal opens to avoid missing presentations // Updates presentation list when chat modal opens to avoid missing presentations
if (isOpen && !prevProps.isOpen) { if (isOpen && !prevProps.isOpen) {
registerTitleView(intl.formatMessage(intlMessages.uploadViewTitle)); registerTitleView(intl.formatMessage(intlMessages.uploadViewTitle));
const focusableElements = const focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
const modal = document.getElementById('upload-modal'); const modal = document.getElementById('upload-modal');
const firstFocusableElement = modal?.querySelectorAll(focusableElements)[0]; const firstFocusableElement = modal?.querySelectorAll(focusableElements)[0];
const focusableContent = modal?.querySelectorAll(focusableElements); const focusableContent = modal?.querySelectorAll(focusableElements);
@ -424,20 +443,18 @@ class PresentationUploader extends Component {
firstFocusableElement.focus(); firstFocusableElement.focus();
modal.addEventListener('keydown', function(e) { modal.addEventListener('keydown', (e) => {
let tab = e.key === 'Tab' || e.keyCode === TAB; const tab = e.key === 'Tab' || e.keyCode === TAB;
if (!tab) return; if (!tab) return;
if (e.shiftKey) { if (e.shiftKey) {
if (document.activeElement === firstFocusableElement) { if (document.activeElement === firstFocusableElement) {
lastFocusableElement.focus(); lastFocusableElement.focus();
e.preventDefault(); e.preventDefault();
} }
} else { } else if (document.activeElement === lastFocusableElement) {
if (document.activeElement === lastFocusableElement) {
firstFocusableElement.focus(); firstFocusableElement.focus();
e.preventDefault(); e.preventDefault();
} }
}
}); });
} }
@ -448,7 +465,7 @@ class PresentationUploader extends Component {
if (this.exportToastId) { if (this.exportToastId) {
if (!prevProps.isOpen && isOpen) { if (!prevProps.isOpen && isOpen) {
this.handleDismissToast(this.exportToastId); handleDismissToast(this.exportToastId);
} }
toast.update(this.exportToastId, { toast.update(this.exportToastId, {
@ -458,18 +475,14 @@ class PresentationUploader extends Component {
} }
componentWillUnmount() { componentWillUnmount() {
let id = Session.get("presentationUploaderToastId"); const id = Session.get('presentationUploaderToastId');
if (id) { if (id) {
toast.dismiss(id); toast.dismiss(id);
Session.set("presentationUploaderToastId", null); Session.set('presentationUploaderToastId', null);
} }
Session.set('showUploadPresentationView', false); Session.set('showUploadPresentationView', false);
} }
handleDismissToast(id) {
return toast.dismiss(id);
}
handleRemove(item, withErr = false) { handleRemove(item, withErr = false) {
if (withErr) { if (withErr) {
const { presentations } = this.props; const { presentations } = this.props;
@ -541,11 +554,6 @@ class PresentationUploader extends Component {
this.setState({ presentations: presentationsUpdated }); this.setState({ presentations: presentationsUpdated });
} }
deepMergeUpdateFileKey(id, key, value) {
const applyValue = (toUpdate) => update(toUpdate, { $merge: value });
this.updateFileKey(id, key, applyValue, '$apply');
}
handleConfirm() { handleConfirm() {
const { const {
handleSave, handleSave,
@ -556,12 +564,20 @@ class PresentationUploader extends Component {
const { disableActions, presentations } = this.state; const { disableActions, presentations } = this.state;
const presentationsToSave = presentations; const presentationsToSave = presentations;
if (!isPresentationEnabled()) {
this.setState(
{ presentations: [] },
Session.set('showUploadPresentationView', false),
);
return null;
}
this.setState({ disableActions: true }); this.setState({ disableActions: true });
presentations.forEach(item => { presentations.forEach((item) => {
if (item.upload.done) { if (item.upload.done) {
const didDownloadableStateChange = propPresentations.some( const didDownloadableStateChange = propPresentations.some(
(p) => p.id === item.id && p.isDownloadable !== item.isDownloadable (p) => p.id === item.id && p.isDownloadable !== item.isDownloadable,
); );
if (didDownloadableStateChange) { if (didDownloadableStateChange) {
dispatchTogglePresentationDownloadable(item, item.isDownloadable); dispatchTogglePresentationDownloadable(item, item.isDownloadable);
@ -577,7 +593,6 @@ class PresentationUploader extends Component {
if (!hasError) { if (!hasError) {
this.setState({ this.setState({
disableActions: false, disableActions: false,
toUploadCount: 0,
}); });
return; return;
} }
@ -619,14 +634,6 @@ class PresentationUploader extends Component {
); );
} }
getPresentationsToShow() {
const { presentations, presExporting } = this.state;
return Array.from(presExporting)
.map((id) => presentations.find((p) => p.id === id))
.filter((p) => p);
}
handleSendToChat(item) { handleSendToChat(item) {
const { const {
exportPresentationToChat, exportPresentationToChat,
@ -640,9 +647,11 @@ class PresentationUploader extends Component {
notify(intl.formatMessage(intlMessages.linkAvailable, { 0: item.filename }), 'success'); notify(intl.formatMessage(intlMessages.linkAvailable, { 0: item.filename }), 'success');
} }
if ([EXPORT_STATUSES.RUNNING, if ([
EXPORT_STATUSES.RUNNING,
EXPORT_STATUSES.COLLECTING, EXPORT_STATUSES.COLLECTING,
EXPORT_STATUSES.PROCESSING].includes(exportation.status)) { EXPORT_STATUSES.PROCESSING,
].includes(exportation.status)) {
this.setState((prevState) => { this.setState((prevState) => {
prevState.presExporting.add(item.id); prevState.presExporting.add(item.id);
return { return {
@ -682,6 +691,19 @@ class PresentationUploader extends Component {
Session.set('showUploadPresentationView', false); Session.set('showUploadPresentationView', false);
} }
getPresentationsToShow() {
const { presentations, presExporting } = this.state;
return Array.from(presExporting)
.map((id) => presentations.find((p) => p.id === id))
.filter((p) => p);
}
deepMergeUpdateFileKey(id, key, value) {
const applyValue = (toUpdate) => update(toUpdate, { $merge: value });
this.updateFileKey(id, key, applyValue, '$apply');
}
updateFileKey(id, key, value, operation = '$set') { updateFileKey(id, key, value, operation = '$set') {
this.setState(({ presentations }) => { this.setState(({ presentations }) => {
const fileIndex = presentations.findIndex((f) => f.id === id); const fileIndex = presentations.findIndex((f) => f.id === id);
@ -788,12 +810,12 @@ class PresentationUploader extends Component {
const shouldDismiss = isAllExported && this.exportToastId; const shouldDismiss = isAllExported && this.exportToastId;
if (shouldDismiss) { if (shouldDismiss) {
this.handleDismissToast(this.exportToastId); handleDismissToast(this.exportToastId);
if (presExporting.size) { if (presExporting.size) {
this.setState({ presExporting: new Set() }); this.setState({ presExporting: new Set() });
} }
return; return null;
} }
const presToShowSorted = [ const presToShowSorted = [
@ -908,24 +930,29 @@ class PresentationUploader extends Component {
renderDownloadableWithAnnotationsHint() { renderDownloadableWithAnnotationsHint() {
const { const {
intl, intl,
allowDownloadable allowDownloadable,
} = this.props; } = this.props;
return allowDownloadable ? ( return allowDownloadable ? (
<Styled.ExportHint> <Styled.ExportHint>
{intl.formatMessage(intlMessages.exportHint)} {intl.formatMessage(intlMessages.exportHint)}
</Styled.ExportHint>) </Styled.ExportHint>
)
: null; : null;
} }
renderPresentationItem(item) { renderPresentationItem(item) {
const { disableActions } = this.state; const { disableActions } = this.state;
const { const {
intl, intl,
selectedToBeNextCurrent, selectedToBeNextCurrent,
allowDownloadable allowDownloadable,
renderPresentationItemStatus,
} = this.props; } = this.props;
const isActualCurrent = selectedToBeNextCurrent ? item.id === selectedToBeNextCurrent : item.isCurrent; const isActualCurrent = selectedToBeNextCurrent
? item.id === selectedToBeNextCurrent
: item.isCurrent;
const isUploading = !item.upload.done && item.upload.progress > 0; const isUploading = !item.upload.done && item.upload.progress > 0;
const isConverting = !item.conversion.done && item.upload.done; const isConverting = !item.conversion.done && item.upload.done;
const hasError = item.conversion.error || item.upload.error; const hasError = item.conversion.error || item.upload.error;
@ -987,7 +1014,7 @@ class PresentationUploader extends Component {
: null : null
} }
<Styled.TableItemStatus colSpan={hasError ? 2 : 0}> <Styled.TableItemStatus colSpan={hasError ? 2 : 0}>
{this.props.renderPresentationItemStatus(item, intl)} {renderPresentationItemStatus(item, intl)}
</Styled.TableItemStatus> </Styled.TableItemStatus>
{hasError ? null : ( {hasError ? null : (
<Styled.TableItemActions notDownloadable={!allowDownloadable}> <Styled.TableItemActions notDownloadable={!allowDownloadable}>
@ -1015,8 +1042,7 @@ class PresentationUploader extends Component {
onClick={() => this.handleRemove(item)} onClick={() => this.handleRemove(item)}
animations={animations} animations={animations}
/> />
) : null ) : null}
}
</Styled.TableItemActions> </Styled.TableItemActions>
)} )}
</Styled.PresentationItem> </Styled.PresentationItem>
@ -1051,10 +1077,10 @@ class PresentationUploader extends Component {
// Error handling is being done in the onDrop prop. // Error handling is being done in the onDrop prop.
<Styled.UploaderDropzone <Styled.UploaderDropzone
multiple multiple
activeClassName={"dropzoneActive"} activeClassName="dropzoneActive"
accept={fileValidMimeTypes.map((fileValid) => fileValid.extension)} accept={fileValidMimeTypes.map((fileValid) => fileValid.extension)}
disablepreview="true" disablepreview="true"
onDrop={(files, files2) => this.handleFiledrop(files, files2, this)} onDrop={(files, files2) => this.handleFiledrop(files, files2, this, intl, intlMessages)}
> >
<Styled.DropzoneIcon iconName="upload" /> <Styled.DropzoneIcon iconName="upload" />
<Styled.DropzoneMessage> <Styled.DropzoneMessage>
@ -1071,7 +1097,9 @@ class PresentationUploader extends Component {
renderExternalUpload() { renderExternalUpload() {
const { externalUploadData, intl } = this.props; const { externalUploadData, intl } = this.props;
const { presentationUploadExternalDescription, presentationUploadExternalUrl } = externalUploadData; const {
presentationUploadExternalDescription, presentationUploadExternalUrl,
} = externalUploadData;
if (!presentationUploadExternalDescription || !presentationUploadExternalUrl) return null; if (!presentationUploadExternalDescription || !presentationUploadExternalUrl) return null;
@ -1091,7 +1119,7 @@ class PresentationUploader extends Component {
aria-describedby={intl.formatMessage(intlMessages.externalUploadLabel)} aria-describedby={intl.formatMessage(intlMessages.externalUploadLabel)}
/> />
</Styled.ExternalUpload> </Styled.ExternalUpload>
) );
} }
renderPicDropzone() { renderPicDropzone() {
@ -1121,7 +1149,7 @@ class PresentationUploader extends Component {
accept="image/*" accept="image/*"
disablepreview="true" disablepreview="true"
data-test="fileUploadDropZone" data-test="fileUploadDropZone"
onDrop={(files, files2) => this.handleFiledrop(files, files2, this)} onDrop={(files, files2) => this.handleFiledrop(files, files2, this, intl, intlMessages)}
> >
<Styled.DropzoneIcon iconName="upload" /> <Styled.DropzoneIcon iconName="upload" />
<Styled.DropzoneMessage> <Styled.DropzoneMessage>
@ -1151,9 +1179,11 @@ class PresentationUploader extends Component {
if (item.id.indexOf(item.filename) !== -1 && item.upload.progress === 0) hasNewUpload = true; if (item.id.indexOf(item.filename) !== -1 && item.upload.progress === 0) hasNewUpload = true;
}); });
return (<> return (
<>
<PresentationUploaderToast intl={intl} /> <PresentationUploaderToast intl={intl} />
{isOpen ? ( {isOpen
? (
<Styled.UploaderModal id="upload-modal"> <Styled.UploaderModal id="upload-modal">
<Styled.ModalInner> <Styled.ModalInner>
<Styled.ModalHeader> <Styled.ModalHeader>
@ -1188,8 +1218,10 @@ class PresentationUploader extends Component {
{this.renderExternalUpload()} {this.renderExternalUpload()}
</Styled.ModalInner> </Styled.ModalInner>
</Styled.UploaderModal> </Styled.UploaderModal>
) : null )
}</>) : null}
</>
);
} }
} }

View File

@ -8,7 +8,7 @@ import PresUploaderToast from '/imports/ui/components/presentation/presentation-
import PresentationUploader from './component'; import PresentationUploader from './component';
import { UsersContext } from '/imports/ui/components/components-data/users-context/context'; import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
import Auth from '/imports/ui/services/auth'; import Auth from '/imports/ui/services/auth';
import { isDownloadPresentationWithAnnotationsEnabled } from '/imports/ui/services/features'; import { isDownloadPresentationWithAnnotationsEnabled, isPresentationEnabled } from '/imports/ui/services/features';
const PRESENTATION_CONFIG = Meteor.settings.public.presentation; const PRESENTATION_CONFIG = Meteor.settings.public.presentation;
@ -33,6 +33,7 @@ export default withTracker(() => {
dispatchTogglePresentationDownloadable, dispatchTogglePresentationDownloadable,
exportPresentationToChat, exportPresentationToChat,
} = Service; } = Service;
const isOpen = isPresentationEnabled() && (Session.get('showUploadPresentationView') || false);
return { return {
presentations: currentPresentations, presentations: currentPresentations,
@ -49,7 +50,7 @@ export default withTracker(() => {
dispatchEnableDownloadable, dispatchEnableDownloadable,
dispatchTogglePresentationDownloadable, dispatchTogglePresentationDownloadable,
exportPresentationToChat, exportPresentationToChat,
isOpen: Session.get('showUploadPresentationView') || false, isOpen,
selectedToBeNextCurrent: Session.get('selectedToBeNextCurrent') || null, selectedToBeNextCurrent: Session.get('selectedToBeNextCurrent') || null,
externalUploadData: Service.getExternalUploadData(), externalUploadData: Service.getExternalUploadData(),
handleFiledrop: Service.handleFiledrop, handleFiledrop: Service.handleFiledrop,

View File

@ -1,4 +1,4 @@
import Presentations from '/imports/api/presentations'; import Presentations, { UploadingPresentations } from '/imports/api/presentations';
import PresentationUploadToken from '/imports/api/presentation-upload-token'; import PresentationUploadToken from '/imports/api/presentation-upload-token';
import Auth from '/imports/ui/services/auth'; import Auth from '/imports/ui/services/auth';
import Poll from '/imports/api/polls/'; import Poll from '/imports/api/polls/';
@ -8,8 +8,9 @@ import logger from '/imports/startup/client/logger';
import _ from 'lodash'; import _ from 'lodash';
import update from 'immutability-helper'; import update from 'immutability-helper';
import { Random } from 'meteor/random'; import { Random } from 'meteor/random';
import { UploadingPresentations } from '/imports/api/presentations';
import Meetings from '/imports/api/meetings'; import Meetings from '/imports/api/meetings';
import { isPresentationEnabled } from '/imports/ui/services/features';
import { notify } from '/imports/ui/services/notification';
const CONVERSION_TIMEOUT = 300000; const CONVERSION_TIMEOUT = 300000;
const TOKEN_TIMEOUT = 5000; const TOKEN_TIMEOUT = 5000;
@ -22,11 +23,11 @@ const futch = (url, opts = {}, onProgress) => new Promise((res, rej) => {
xhr.open(opts.method || 'get', url); xhr.open(opts.method || 'get', url);
Object.keys(opts.headers || {}) Object.keys(opts.headers || {})
.forEach(k => xhr.setRequestHeader(k, opts.headers[k])); .forEach((k) => xhr.setRequestHeader(k, opts.headers[k]));
xhr.onload = (e) => { xhr.onload = (e) => {
if (e.target.status !== 200) { if (e.target.status !== 200) {
return rej({ code: e.target.status, message: e.target.statusText }); return rej(new Error({ code: e.target.status, message: e.target.statusText }));
} }
return res(e.target.responseText); return res(e.target.responseText);
@ -58,7 +59,6 @@ const getPresentations = () => Presentations
const uploadTimestamp = id.split('-').pop(); const uploadTimestamp = id.split('-').pop();
return { return {
id, id,
filename: name, filename: name,
@ -85,7 +85,7 @@ const observePresentationConversion = (
) => new Promise((resolve) => { ) => new Promise((resolve) => {
// The token is placed as an id before the original one is generated // The token is placed as an id before the original one is generated
// in the back-end; // in the back-end;
const tokenId = PresentationUploadToken.findOne({temporaryPresentationId})?.authzToken; const tokenId = PresentationUploadToken.findOne({ temporaryPresentationId })?.authzToken;
const conversionTimeout = setTimeout(() => { const conversionTimeout = setTimeout(() => {
onConversion({ onConversion({
@ -105,12 +105,13 @@ const observePresentationConversion = (
query.observe({ query.observe({
added: (doc) => { added: (doc) => {
if (doc.temporaryPresentationId !== temporaryPresentationId && doc.id !== tokenId) return; if (doc.temporaryPresentationId !== temporaryPresentationId && doc.id !== tokenId) return;
if (doc.conversion.status === 'FILE_TOO_LARGE' || doc.conversion.status === 'UNSUPPORTED_DOCUMENT' if (doc.conversion.status === 'FILE_TOO_LARGE' || doc.conversion.status === 'UNSUPPORTED_DOCUMENT'
|| doc.conversion.status === 'CONVERSION_TIMEOUT' || doc.conversion.status === "IVALID_MIME_TYPE") { || doc.conversion.status === 'CONVERSION_TIMEOUT' || doc.conversion.status === 'IVALID_MIME_TYPE') {
Presentations.update({id: tokenId}, {$set: {temporaryPresentationId, renderedInToast: false}}) Presentations.update(
{ id: tokenId }, { $set: { temporaryPresentationId, renderedInToast: false } },
);
onConversion(doc.conversion); onConversion(doc.conversion);
c.stop(); c.stop();
clearTimeout(conversionTimeout); clearTimeout(conversionTimeout);
@ -146,7 +147,7 @@ const requestPresentationUploadToken = (
let computation = null; let computation = null;
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
computation.stop(); computation.stop();
reject({ code: 408, message: 'requestPresentationUploadToken timeout' }); reject(new Error({ code: 408, message: 'requestPresentationUploadToken timeout' }));
}, TOKEN_TIMEOUT); }, TOKEN_TIMEOUT);
Tracker.autorun((c) => { Tracker.autorun((c) => {
@ -169,7 +170,7 @@ const requestPresentationUploadToken = (
} }
if (PresentationToken.failed) { if (PresentationToken.failed) {
reject({ code: 401, message: `requestPresentationUploadToken token ${PresentationToken.authzToken} failed` }); reject(new Error({ code: 401, message: `requestPresentationUploadToken token ${PresentationToken.authzToken} failed` }));
} }
}); });
}); });
@ -184,7 +185,7 @@ const uploadAndConvertPresentation = (
onProgress, onProgress,
onConversion, onConversion,
) => { ) => {
const temporaryPresentationId = _.uniqueId(Random.id(20)) const temporaryPresentationId = _.uniqueId(Random.id(20));
const data = new FormData(); const data = new FormData();
data.append('fileUpload', file); data.append('fileUpload', file);
@ -215,33 +216,38 @@ const uploadAndConvertPresentation = (
lastModifiedUploader: true, lastModifiedUploader: true,
upload: { upload: {
done: false, done: false,
error: false error: false,
}, },
uploadTimestamp: new Date() uploadTimestamp: new Date(),
} },
}) });
return requestPresentationUploadToken(temporaryPresentationId, podId, meetingId, file.name) return requestPresentationUploadToken(temporaryPresentationId, podId, meetingId, file.name)
.then((token) => { .then((token) => {
makeCall('setUsedToken', token); makeCall('setUsedToken', token);
UploadingPresentations.upsert({ UploadingPresentations.upsert({
temporaryPresentationId temporaryPresentationId,
}, { }, {
$set: { $set: {
id: token, id: token,
} },
}) });
return futch(endpoint.replace('upload', `${token}/upload`), opts, (e) => { return futch(endpoint.replace('upload', `${token}/upload`), opts, (e) => {
onProgress(e); onProgress(e);
let pr = (e.loaded / e.total) * 100; const pr = (e.loaded / e.total) * 100;
if (pr != 100) UploadingPresentations.upsert({ temporaryPresentationId }, {$set: {progress: pr}}); if (pr !== 100) {
else UploadingPresentations.upsert({ temporaryPresentationId }, {$set: { UploadingPresentations.upsert({ temporaryPresentationId }, { $set: { progress: pr } });
} else {
UploadingPresentations.upsert({ temporaryPresentationId }, {
$set: {
progress: pr, progress: pr,
upload: { upload: {
done: true, done: true,
error: false, error: false,
},
},
});
} }
}});
}); });
}) })
.then(() => observePresentationConversion(meetingId, temporaryPresentationId, onConversion)) .then(() => observePresentationConversion(meetingId, temporaryPresentationId, onConversion))
@ -324,17 +330,22 @@ const persistPresentationChanges = (oldState, newState, uploadEndpoint, podId) =
.then(removePresentations.bind(null, presentationsToRemove, podId)); .then(removePresentations.bind(null, presentationsToRemove, podId));
}; };
const handleSavePresentation = (presentations = [], isFromPresentationUploaderInterface = true, newPres = {}) => { const handleSavePresentation = (
presentations = [], isFromPresentationUploaderInterface = true, newPres = {},
) => {
if (!isPresentationEnabled()) {
return null;
}
const currentPresentations = getPresentations(); const currentPresentations = getPresentations();
if (!isFromPresentationUploaderInterface) { if (!isFromPresentationUploaderInterface) {
if (presentations.length === 0) { if (presentations.length === 0) {
presentations = [...currentPresentations]; presentations = [...currentPresentations];
} }
presentations = presentations.map(p => update(p, { presentations = presentations.map((p) => update(p, {
isCurrent: { isCurrent: {
$set: false $set: false,
} },
})); }));
newPres.isCurrent = true; newPres.isCurrent = true;
presentations.push(newPres); presentations.push(newPres);
@ -343,8 +354,9 @@ const handleSavePresentation = (presentations = [], isFromPresentationUploaderIn
currentPresentations, currentPresentations,
presentations, presentations,
PRESENTATION_CONFIG.uploadEndpoint, PRESENTATION_CONFIG.uploadEndpoint,
'DEFAULT_PRESENTATION_POD' 'DEFAULT_PRESENTATION_POD',
)} );
};
const getExternalUploadData = () => { const getExternalUploadData = () => {
const { meetingProp } = Meetings.findOne( const { meetingProp } = Meetings.findOne(
@ -352,7 +364,7 @@ const getExternalUploadData = () => {
{ {
fields: { fields: {
'meetingProp.presentationUploadExternalDescription': 1, 'meetingProp.presentationUploadExternalDescription': 1,
'meetingProp.presentationUploadExternalUrl': 1 'meetingProp.presentationUploadExternalUrl': 1,
}, },
}, },
); );
@ -362,7 +374,7 @@ const getExternalUploadData = () => {
return { return {
presentationUploadExternalDescription, presentationUploadExternalDescription,
presentationUploadExternalUrl, presentationUploadExternalUrl,
} };
}; };
const exportPresentationToChat = (presentationId, observer) => { const exportPresentationToChat = (presentationId, observer) => {
@ -372,11 +384,12 @@ const exportPresentationToChat = (presentationId, observer) => {
const cursor = Presentations.find({ id: presentationId }); const cursor = Presentations.find({ id: presentationId });
const checkStatus = (exportation) => { const checkStatus = (exportation) => {
const shouldStop = lastStatus.status === 'PROCESSING' && exportation.status === 'EXPORTED'; const shouldStop = lastStatus.status === 'RUNNING' && exportation.status === 'EXPORTED';
if (shouldStop) { if (shouldStop) {
observer(exportation, true); observer(exportation, true);
return c.stop(); c.stop();
return;
} }
observer(exportation, false); observer(exportation, false);
@ -396,16 +409,17 @@ const exportPresentationToChat = (presentationId, observer) => {
makeCall('exportPresentationToChat', presentationId); makeCall('exportPresentationToChat', presentationId);
}; };
function handleFiledrop(files, files2, that) { function handleFiledrop(files, files2, that, intl, intlMessages) {
if(that){ if (that) {
const { fileValidMimeTypes, intl } = that.props; const { fileValidMimeTypes } = that.props;
const { toUploadCount } = that.state; const { toUploadCount } = that.state;
const validMimes = fileValidMimeTypes.map((fileValid) => fileValid.mime); const validMimes = fileValidMimeTypes.map((fileValid) => fileValid.mime);
const validExtentions = fileValidMimeTypes.map((fileValid) => fileValid.extension); const validExtentions = fileValidMimeTypes.map((fileValid) => fileValid.extension);
const [accepted, rejected] = _.partition(files const [accepted, rejected] = _.partition(
.concat(files2), (f) => ( files.concat(files2), (f) => (
validMimes.includes(f.type) || validExtentions.includes(`.${f.name.split('.').pop()}`) validMimes.includes(f.type) || validExtentions.includes(`.${f.name.split('.').pop()}`)
)); ),
);
const presentationsToUpload = accepted.map((file) => { const presentationsToUpload = accepted.map((file) => {
const id = _.uniqueId(file.name); const id = _.uniqueId(file.name);

View File

@ -1,7 +1,6 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
const injectWbResizeEvent = WrappedComponent => const injectWbResizeEvent = (WrappedComponent) => class Resize extends Component {
class Resize extends Component {
componentDidMount() { componentDidMount() {
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
} }
@ -15,6 +14,6 @@ const injectWbResizeEvent = WrappedComponent =>
<WrappedComponent {...this.props} /> <WrappedComponent {...this.props} />
); );
} }
}; };
export default injectWbResizeEvent; export default injectWbResizeEvent;

View File

@ -3,10 +3,11 @@ import PropTypes from 'prop-types';
const Slide = ({ imageUri, svgWidth, svgHeight }) => ( const Slide = ({ imageUri, svgWidth, svgHeight }) => (
<g> <g>
{imageUri ? {imageUri
// some pdfs lose a white background color during the conversion to svg // some pdfs lose a white background color during the conversion to svg
// their background color is transparent // their background color is transparent
// that's why we have a white rectangle covering the whole slide area by default // that's why we have a white rectangle covering the whole slide area by default
? (
<g> <g>
<rect <rect
x="0" x="0"
@ -25,6 +26,7 @@ const Slide = ({ imageUri, svgWidth, svgHeight }) => (
style={{ WebkitTapHighlightColor: 'transparent' }} style={{ WebkitTapHighlightColor: 'transparent' }}
/> />
</g> </g>
)
: null} : null}
</g> </g>
); );

View File

@ -51,7 +51,7 @@ export default withTracker(() => {
return { return {
isGloballyBroadcasting: isGloballyBroadcasting(), isGloballyBroadcasting: isGloballyBroadcasting(),
toggleSwapLayout: MediaService.toggleSwapLayout, toggleSwapLayout: MediaService.toggleSwapLayout,
hidePresentation: getFromUserSettings('bbb_hide_presentation', LAYOUT_CONFIG.hidePresentation), hidePresentationOnJoin: getFromUserSettings('bbb_hide_presentation_on_join', LAYOUT_CONFIG.hidePresentationOnJoin),
enableVolumeControl: shouldEnableVolumeControl(), enableVolumeControl: shouldEnableVolumeControl(),
}; };
})(ScreenshareContainer); })(ScreenshareContainer);

View File

@ -5,7 +5,6 @@ import logger from '/imports/startup/client/logger';
import GroupChat from '/imports/api/group-chat'; import GroupChat from '/imports/api/group-chat';
import Annotations from '/imports/api/annotations'; import Annotations from '/imports/api/annotations';
import Users from '/imports/api/users'; import Users from '/imports/api/users';
import AnnotationsTextService from '/imports/ui/components/whiteboard/annotations/text/service';
import { Annotations as AnnotationsLocal } from '/imports/ui/components/whiteboard/service'; import { Annotations as AnnotationsLocal } from '/imports/ui/components/whiteboard/service';
import { import {
localCollectionRegistry, localCollectionRegistry,
@ -156,9 +155,8 @@ export default withTracker(() => {
usersPersistentDataHandler = Meteor.subscribe('users-persistent-data'); usersPersistentDataHandler = Meteor.subscribe('users-persistent-data');
const annotationsHandler = Meteor.subscribe('annotations', { const annotationsHandler = Meteor.subscribe('annotations', {
onReady: () => { onReady: () => {
const activeTextShapeId = AnnotationsTextService.activeTextShapeId(); AnnotationsLocal.remove({});
AnnotationsLocal.remove({ id: { $ne: `${activeTextShapeId}-fake` } }); Annotations.find({}, { reactive: false }).forEach((a) => {
Annotations.find({ id: { $ne: activeTextShapeId } }, { reactive: false }).forEach((a) => {
try { try {
AnnotationsLocal.insert(a); AnnotationsLocal.insert(a);
} catch (e) { } catch (e) {

View File

@ -4,7 +4,7 @@ import { defineMessages, injectIntl } from 'react-intl';
import Icon from '/imports/ui/components/common/icon/component'; import Icon from '/imports/ui/components/common/icon/component';
import Styled from './styles'; import Styled from './styles';
import { ACTIONS, PANELS } from '../../../layout/enums'; import { ACTIONS, PANELS } from '../../../layout/enums';
import BreakoutRemainingTime from '/imports/ui/components/breakout-room/breakout-remaining-time/container'; import MeetingRemainingTime from '../../../notifications-bar/meeting-remaining-time/container';
const intlMessages = defineMessages({ const intlMessages = defineMessages({
breakoutTitle: { breakoutTitle: {
@ -66,7 +66,7 @@ const BreakoutRoomItem = ({
{intl.formatMessage(intlMessages.breakoutTitle)} {intl.formatMessage(intlMessages.breakoutTitle)}
</Styled.BreakoutTitle> </Styled.BreakoutTitle>
<Styled.BreakoutDuration> <Styled.BreakoutDuration>
<BreakoutRemainingTime <MeetingRemainingTime
messageDuration={intlMessages.breakoutTimeRemaining} messageDuration={intlMessages.breakoutTimeRemaining}
breakoutRoom={breakoutRoom} breakoutRoom={breakoutRoom}
/> />

View File

@ -321,6 +321,7 @@ class UserOptions extends PureComponent {
key: this.learningDashboardId, key: this.learningDashboardId,
onClick: () => { openLearningDashboardUrl(locale); }, onClick: () => { openLearningDashboardUrl(locale); },
dividerTop: true, dividerTop: true,
dataTest: 'learningDashboard'
}); });
} }
} }

View File

@ -432,6 +432,7 @@ const VirtualBgSelector = ({
aria-label={intl.formatMessage(intlMessages.virtualBackgroundSettingsLabel)} aria-label={intl.formatMessage(intlMessages.virtualBackgroundSettingsLabel)}
isVisualEffects={isVisualEffects} isVisualEffects={isVisualEffects}
brightnessEnabled={ENABLE_CAMERA_BRIGHTNESS} brightnessEnabled={ENABLE_CAMERA_BRIGHTNESS}
data-test="virtualBackground"
> >
{shouldEnableBackgroundUpload() && ( {shouldEnableBackgroundUpload() && (
<> <>
@ -443,7 +444,6 @@ const VirtualBgSelector = ({
{Object.values(backgrounds) {Object.values(backgrounds)
.sort((a, b) => b.lastActivityDate - a.lastActivityDate) .sort((a, b) => b.lastActivityDate - a.lastActivityDate)
.slice(0, isVisualEffects ? undefined : 3)
.map((background, index) => { .map((background, index) => {
if (background.custom !== false) { if (background.custom !== false) {
return renderCustomButton(background, index); return renderCustomButton(background, index);

View File

@ -4,7 +4,6 @@ import { withModalMounter } from '/imports/ui/components/common/modal/service';
import { withTracker } from 'meteor/react-meteor-data'; import { withTracker } from 'meteor/react-meteor-data';
import MediaService from '/imports/ui/components/media/service'; import MediaService from '/imports/ui/components/media/service';
import Auth from '/imports/ui/services/auth'; import Auth from '/imports/ui/services/auth';
import breakoutService from '/imports/ui/components/breakout-room/service';
import VideoService from '/imports/ui/components/video-provider/service'; import VideoService from '/imports/ui/components/video-provider/service';
import { UsersContext } from '../components-data/users-context/context'; import { UsersContext } from '../components-data/users-context/context';
import { import {
@ -56,8 +55,6 @@ const WebcamContainer = ({
: null; : null;
}; };
let userWasInBreakout = false;
export default withModalMounter(withTracker((props) => { export default withModalMounter(withTracker((props) => {
const { current_presentation: hasPresentation } = MediaService.getPresentationInfo(); const { current_presentation: hasPresentation } = MediaService.getPresentationInfo();
const data = { const data = {
@ -65,31 +62,6 @@ export default withModalMounter(withTracker((props) => {
isMeteorConnected: Meteor.status().connected, isMeteorConnected: Meteor.status().connected,
}; };
const userIsInBreakout = breakoutService.getBreakoutUserIsIn(Auth.userID);
let deviceIds = Session.get('deviceIds');
if (!userIsInBreakout && userWasInBreakout && deviceIds && deviceIds !== '') {
/* 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) */
const canConnect = Session.get('canConnect');
deviceIds = deviceIds.split(',');
if (canConnect) {
const deviceId = deviceIds.shift();
Session.set('canConnect', false);
Session.set('WebcamDeviceId', deviceId);
Session.set('deviceIds', deviceIds.join(','));
VideoService.joinVideo(deviceId);
}
} else {
userWasInBreakout = userIsInBreakout;
}
const { streams: usersVideo } = VideoService.getVideoStreams(); const { streams: usersVideo } = VideoService.getVideoStreams();
data.usersVideo = usersVideo; data.usersVideo = usersVideo;
data.swapLayout = !hasPresentation || props.isLayoutSwapped; data.swapLayout = !hasPresentation || props.isLayoutSwapped;

View File

@ -1,104 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import StaticAnnotation from './static-annotation/component';
import ReactiveAnnotationContainer from './reactive-annotation/container';
import Ellipse from '../annotations/ellipse/component';
import Line from '../annotations/line/component';
import Poll from '../annotations/poll/component';
import Rectangle from '../annotations/rectangle/component';
import Text from '../annotations/text/container';
import Triangle from '../annotations/triangle/component';
import Pencil from '../annotations/pencil/component';
const ANNOTATION_CONFIG = Meteor.settings.public.whiteboard.annotations;
const DRAW_END = ANNOTATION_CONFIG.status.end;
export default class AnnotationFactory extends Component {
static renderStaticAnnotation(annotationInfo, slideWidth, slideHeight, drawObject, whiteboardId) {
return (
<StaticAnnotation
key={annotationInfo._id}
shapeId={annotationInfo._id}
drawObject={drawObject}
slideWidth={slideWidth}
slideHeight={slideHeight}
whiteboardId={whiteboardId}
/>
);
}
static renderReactiveAnnotation(annotationInfo, slideWidth, slideHeight, drawObject, whiteboardId) {
return (
<ReactiveAnnotationContainer
key={annotationInfo._id}
shapeId={annotationInfo._id}
drawObject={drawObject}
slideWidth={slideWidth}
slideHeight={slideHeight}
whiteboardId={whiteboardId}
/>
);
}
constructor() {
super();
this.renderAnnotation = this.renderAnnotation.bind(this);
}
renderAnnotation(annotationInfo) {
const drawObject = this.props.annotationSelector[annotationInfo.annotationType];
if (annotationInfo.status === DRAW_END) {
return AnnotationFactory.renderStaticAnnotation(
annotationInfo,
this.props.slideWidth,
this.props.slideHeight,
drawObject,
this.props.whiteboardId,
);
}
return AnnotationFactory.renderReactiveAnnotation(
annotationInfo,
this.props.slideWidth,
this.props.slideHeight,
drawObject,
this.props.whiteboardId,
);
}
render() {
const { annotationsInfo } = this.props;
return (
<g>
{annotationsInfo
? annotationsInfo.map(annotationInfo => this.renderAnnotation(annotationInfo))
: null }
</g>
);
}
}
AnnotationFactory.propTypes = {
whiteboardId: PropTypes.string.isRequired,
// initial width and height of the slide are required
// to calculate the coordinates for each annotation
slideWidth: PropTypes.number.isRequired,
slideHeight: PropTypes.number.isRequired,
// array of annotations, optional
annotationsInfo: PropTypes.arrayOf(PropTypes.object).isRequired,
annotationSelector: PropTypes.objectOf(PropTypes.func).isRequired,
};
AnnotationFactory.defaultProps = {
annotationSelector: {
ellipse: Ellipse,
line: Line,
poll_result: Poll,
rectangle: Rectangle,
text: Text,
triangle: Triangle,
pencil: Pencil,
},
};

View File

@ -1,30 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
const ReactiveAnnotation = (props) => {
const Component = props.drawObject;
return (
<Component
version={props.annotation.version}
annotation={props.annotation.annotationInfo}
slideWidth={props.slideWidth}
slideHeight={props.slideHeight}
whiteboardId={props.whiteboardId}
/>
);
};
ReactiveAnnotation.propTypes = {
whiteboardId: PropTypes.string.isRequired,
annotation: PropTypes.objectOf(PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.object,
])).isRequired,
drawObject: PropTypes.func.isRequired,
slideWidth: PropTypes.number.isRequired,
slideHeight: PropTypes.number.isRequired,
};
export default ReactiveAnnotation;

View File

@ -1,65 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { withTracker } from 'meteor/react-meteor-data';
import ReactiveAnnotationService from './service';
import ReactiveAnnotation from './component';
import Auth from '/imports/ui/services/auth';
import Users from '/imports/api/users';
import getFromUserSettings from '/imports/ui/services/users-settings';
const ROLE_VIEWER = Meteor.settings.public.user.role_viewer;
const ReactiveAnnotationContainer = (props) => {
const { annotation, drawObject } = props;
if (annotation && drawObject) {
return (
<ReactiveAnnotation
annotation={props.annotation}
slideWidth={props.slideWidth}
slideHeight={props.slideHeight}
drawObject={props.drawObject}
whiteboardId={props.whiteboardId}
/>
);
}
return null;
};
export default withTracker((params) => {
const { shapeId } = params;
const unsentAnnotation = ReactiveAnnotationService.getUnsentAnnotationById(shapeId);
const isUnsentAnnotation = unsentAnnotation !== undefined;
const annotation = isUnsentAnnotation
? unsentAnnotation : ReactiveAnnotationService.getAnnotationById(shapeId);
const isViewer = Users.findOne({ meetingId: Auth.meetingID, userId: Auth.userID }, {
fields: {
role: 1,
},
}).role === ROLE_VIEWER;
const restoreOnUpdate = getFromUserSettings(
'bbb_force_restore_presentation_on_new_events',
Meteor.settings.public.presentation.restoreOnUpdate,
);
return {
annotation,
};
})(ReactiveAnnotationContainer);
ReactiveAnnotationContainer.propTypes = {
whiteboardId: PropTypes.string.isRequired,
annotation: PropTypes.objectOf(PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.object,
])),
drawObject: PropTypes.func.isRequired,
slideWidth: PropTypes.number.isRequired,
slideHeight: PropTypes.number.isRequired,
};
ReactiveAnnotationContainer.defaultProps = {
annotation: undefined,
};

View File

@ -1,14 +0,0 @@
import { Annotations, UnsentAnnotations } from '/imports/ui/components/whiteboard/service';
const getAnnotationById = _id => Annotations.findOne({
_id,
});
const getUnsentAnnotationById = _id => UnsentAnnotations.findOne({
_id,
});
export default {
getAnnotationById,
getUnsentAnnotationById,
};

View File

@ -1,33 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import StaticAnnotationService from './service';
export default class StaticAnnotation extends React.Component {
// completed annotations should never update
shouldComponentUpdate() {
return false;
}
render() {
const annotation = StaticAnnotationService.getAnnotationById(this.props.shapeId);
const Component = this.props.drawObject;
return (
<Component
version={annotation.version}
annotation={annotation.annotationInfo}
slideWidth={this.props.slideWidth}
slideHeight={this.props.slideHeight}
whiteboardId={this.props.whiteboardId}
/>
);
}
}
StaticAnnotation.propTypes = {
whiteboardId: PropTypes.string.isRequired,
shapeId: PropTypes.string.isRequired,
drawObject: PropTypes.func.isRequired,
slideWidth: PropTypes.number.isRequired,
slideHeight: PropTypes.number.isRequired,
};

View File

@ -1,9 +0,0 @@
import { Annotations } from '/imports/ui/components/whiteboard/service';
const getAnnotationById = _id => Annotations.findOne({
_id,
});
export default {
getAnnotationById,
};

View File

@ -1,29 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import AnnotationFactory from '../annotation-factory/component';
const AnnotationGroup = props => (
<AnnotationFactory
annotationsInfo={props.annotationsInfo}
slideWidth={props.slideWidth}
slideHeight={props.slideHeight}
whiteboardId={props.whiteboardId}
/>
);
AnnotationGroup.propTypes = {
whiteboardId: PropTypes.string.isRequired,
// initial width and height of the slide are required
// to calculate the coordinates for each annotation
slideWidth: PropTypes.number.isRequired,
slideHeight: PropTypes.number.isRequired,
// array of annotations, optional
annotationsInfo: PropTypes.arrayOf(PropTypes.shape({
status: PropTypes.string.isRequired,
_id: PropTypes.string.isRequired,
annotationType: PropTypes.string.isRequired,
})).isRequired,
};
export default AnnotationGroup;

View File

@ -1,45 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { withTracker } from 'meteor/react-meteor-data';
import AnnotationGroupService from './service';
import AnnotationGroup from './component';
const AnnotationGroupContainer = ({
annotationsInfo, width, height, whiteboardId,
}) => (
<AnnotationGroup
annotationsInfo={annotationsInfo}
slideWidth={width}
slideHeight={height}
whiteboardId={whiteboardId}
/>
);
export default withTracker((params) => {
const {
whiteboardId,
published,
} = params;
const fetchFunc = published
? AnnotationGroupService.getCurrentAnnotationsInfo : AnnotationGroupService.getUnsentAnnotations;
const annotationsInfo = fetchFunc(whiteboardId);
return {
annotationsInfo,
};
})(AnnotationGroupContainer);
AnnotationGroupContainer.propTypes = {
whiteboardId: PropTypes.string.isRequired,
// initial width and height of the slide; required to calculate the annotations' coordinates
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
// array of annotations, optional
annotationsInfo: PropTypes.arrayOf(PropTypes.shape({
status: PropTypes.string.isRequired,
_id: PropTypes.string.isRequired,
annotationType: PropTypes.string.isRequired,
})).isRequired,
};

View File

@ -1,38 +0,0 @@
import { Annotations, UnsentAnnotations } from '/imports/ui/components/whiteboard/service';
const getCurrentAnnotationsInfo = (whiteboardId) => {
if (!whiteboardId) {
return null;
}
return Annotations.find(
{
whiteboardId,
},
{
sort: { position: 1 },
fields: { status: 1, _id: 1, annotationType: 1 },
},
).fetch();
};
const getUnsentAnnotations = (whiteboardId) => {
if (!whiteboardId) {
return null;
}
return UnsentAnnotations.find(
{
whiteboardId,
},
{
sort: { position: 1 },
fields: { status: 1, _id: 1, annotationType: 1 },
},
).fetch();
};
export default {
getCurrentAnnotationsInfo,
getUnsentAnnotations,
};

Some files were not shown because too many files have changed in this diff Show More