Merge branch 'v2.6.x-release' of github.com:bigbluebutton/bigbluebutton into merge-26-27
This commit is contained in:
commit
2c5cd8f2a0
3
.github/workflows/automated-tests.yml
vendored
3
.github/workflows/automated-tests.yml
vendored
@ -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:
|
||||||
|
4
.github/workflows/deploy-docs.yml
vendored
4
.github/workflows/deploy-docs.yml
vendored
@ -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
2
.gitignore
vendored
@ -21,3 +21,5 @@ record-and-playback/.loadpath
|
|||||||
*~
|
*~
|
||||||
cache/*
|
cache/*
|
||||||
artifacts/*
|
artifacts/*
|
||||||
|
bbb-presentation-video.zip
|
||||||
|
bbb-presentation-video
|
||||||
|
@ -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."
|
||||||
|
@ -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,
|
||||||
|
@ -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",
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
|
@ -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" />
|
||||||
:
|
:
|
||||||
{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
|
||||||
|
@ -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">
|
||||||
|
@ -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
|
||||||
? (
|
? (
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
BIGBLUEBUTTON_RELEASE=2.6.0-rc.4
|
BIGBLUEBUTTON_RELEASE=2.6.0-rc.7
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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) {
|
||||||
|
@ -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({
|
||||||
|
@ -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));
|
||||||
|
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -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',
|
||||||
|
@ -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}`);
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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 {...{
|
||||||
|
@ -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 },
|
||||||
|
@ -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' : ''}`}
|
||||||
|
@ -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',
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -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(),
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
}}
|
}}
|
||||||
|
@ -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'),
|
||||||
|
@ -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',
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
|
|
||||||
const BreakoutRemainingTime = props => (
|
|
||||||
<span data-test="timeRemaining">
|
|
||||||
{props.children}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
export default BreakoutRemainingTime;
|
|
@ -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
|
||||||
|
@ -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}
|
||||||
|
@ -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;
|
|
@ -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,
|
||||||
|
@ -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"
|
||||||
|
@ -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;
|
||||||
|
@ -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({
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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');
|
||||||
|
|
||||||
|
@ -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`
|
||||||
|
@ -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({
|
||||||
|
@ -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}
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const MeetingRemainingTime = (props) => (
|
||||||
|
<span data-test="timeRemaining">
|
||||||
|
{ props.children }
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default MeetingRemainingTime;
|
@ -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');
|
@ -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 && (
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
|
};
|
||||||
|
@ -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,
|
|
||||||
},
|
|
||||||
};
|
|
@ -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,
|
|
||||||
};
|
|
@ -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: [],
|
|
||||||
};
|
|
@ -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,
|
|
||||||
};
|
|
@ -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,
|
|
||||||
};
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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,
|
|
||||||
};
|
|
@ -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);
|
|
@ -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,
|
|
||||||
};
|
|
@ -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,
|
||||||
|
};
|
||||||
|
@ -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,
|
||||||
|
};
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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,
|
||||||
|
};
|
||||||
|
@ -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,
|
|
||||||
};
|
|
@ -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,
|
|
||||||
};
|
|
@ -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);
|
|
@ -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,
|
|
||||||
};
|
|
@ -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,
|
||||||
}
|
};
|
||||||
|
@ -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));
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
|
};
|
||||||
|
@ -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,
|
||||||
|
@ -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}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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);
|
||||||
|
@ -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) {
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
|
||||||
},
|
|
||||||
};
|
|
@ -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;
|
|
@ -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,
|
|
||||||
};
|
|
@ -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,
|
|
||||||
};
|
|
@ -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,
|
|
||||||
};
|
|
@ -1,9 +0,0 @@
|
|||||||
import { Annotations } from '/imports/ui/components/whiteboard/service';
|
|
||||||
|
|
||||||
const getAnnotationById = _id => Annotations.findOne({
|
|
||||||
_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default {
|
|
||||||
getAnnotationById,
|
|
||||||
};
|
|
@ -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;
|
|
@ -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,
|
|
||||||
};
|
|
@ -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
Loading…
Reference in New Issue
Block a user