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'
|
||||
- 'v2.[5-9].x-release'
|
||||
- 'v[3-9].*.x-release'
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '**/*.md'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
permissions:
|
||||
|
4
.github/workflows/deploy-docs.yml
vendored
4
.github/workflows/deploy-docs.yml
vendored
@ -7,8 +7,8 @@ on:
|
||||
- 'v*'
|
||||
- 'develop'
|
||||
paths:
|
||||
- 'docs/'
|
||||
- '.github/'
|
||||
- 'docs/**'
|
||||
- '.github/**'
|
||||
|
||||
# Do not build the docs concurrently
|
||||
concurrency:
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -21,3 +21,5 @@ record-and-playback/.loadpath
|
||||
*~
|
||||
cache/*
|
||||
artifacts/*
|
||||
bbb-presentation-video.zip
|
||||
bbb-presentation-video
|
||||
|
@ -68,7 +68,12 @@ trait PresentationUploadTokenReqMsgHdlr extends RightsManagementTrait {
|
||||
log.info("handlePresentationUploadTokenReqMsg" + liveMeeting.props.meetingProp.intId +
|
||||
" 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)) {
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
val reason = "No permission to request presentation upload token."
|
||||
|
@ -182,7 +182,6 @@ case class PresentationHasInvalidMimeTypeErrorSysPubMsgBody(
|
||||
fileExtension: String,
|
||||
)
|
||||
|
||||
|
||||
object PresentationUploadedFileTimeoutErrorSysPubMsg { val NAME = "PresentationUploadedFileTimeoutErrorSysPubMsg" }
|
||||
case class PresentationUploadedFileTimeoutErrorSysPubMsg(
|
||||
header: BbbClientMsgHeader,
|
||||
|
@ -105,7 +105,6 @@ libraryDependencies ++= Seq(
|
||||
"javax.validation" % "validation-api" % "2.0.1.Final",
|
||||
"org.springframework.boot" % "spring-boot-starter-validation" % "2.7.1",
|
||||
"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.postgresql" % "postgresql" % "42.4.3",
|
||||
"org.hibernate" % "hibernate-core" % "5.6.1.Final",
|
||||
|
@ -20,17 +20,8 @@ package org.bigbluebutton.api;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URI;
|
||||
import java.util.AbstractMap;
|
||||
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.*;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.TreeMap;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
@ -263,7 +254,6 @@ public class MeetingService implements MessageListener {
|
||||
RegisteredUser ru = registeredUser.getValue();
|
||||
|
||||
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) {
|
||||
log.info("Purging user [{}]", registeredUserID);
|
||||
if (meeting.userUnregistered(registeredUserID) != null) {
|
||||
@ -549,6 +539,11 @@ public class MeetingService implements MessageListener {
|
||||
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) {
|
||||
Pageable pageable = null;
|
||||
int o = -1;
|
||||
|
@ -30,6 +30,7 @@ public class RecordingServiceDbImpl implements RecordingService {
|
||||
private RecordingMetadataReaderHelper recordingServiceHelper;
|
||||
private String recordStatusDir;
|
||||
private String captionsDir;
|
||||
private Boolean allowFetchAllRecordings;
|
||||
private String presentationBaseDir;
|
||||
private String defaultServerUrl;
|
||||
private String defaultTextTrackUrl;
|
||||
@ -74,7 +75,7 @@ public class RecordingServiceDbImpl implements RecordingService {
|
||||
@Override
|
||||
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((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");
|
||||
Set<Recording> recordings = new HashSet<>(dataStore.findAll(Recording.class));
|
||||
@ -262,6 +263,10 @@ public class RecordingServiceDbImpl implements RecordingService {
|
||||
captionsDir = dir;
|
||||
}
|
||||
|
||||
public void setAllowFetchAllRecordings(Boolean allowFetchAllRecordings) {
|
||||
this.allowFetchAllRecordings = allowFetchAllRecordings;
|
||||
}
|
||||
|
||||
public void setRecordingServiceHelper(RecordingMetadataReaderHelper r) {
|
||||
recordingServiceHelper = r;
|
||||
}
|
||||
|
@ -62,6 +62,7 @@ public class RecordingServiceFileImpl implements RecordingService {
|
||||
private XmlService xmlService;
|
||||
private String recordStatusDir;
|
||||
private String captionsDir;
|
||||
private Boolean allowFetchAllRecordings;
|
||||
private String presentationBaseDir;
|
||||
private String defaultServerUrl;
|
||||
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) {
|
||||
// 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);
|
||||
ArrayList<RecordingMetadata> recs = filterRecordingsByMetadata(recsList, metadataFilters);
|
||||
@ -434,6 +435,8 @@ public class RecordingServiceFileImpl implements RecordingService {
|
||||
captionsDir = dir;
|
||||
}
|
||||
|
||||
public void setAllowFetchAllRecordings(Boolean allowFetchAllRecordings) { this.allowFetchAllRecordings = allowFetchAllRecordings; }
|
||||
|
||||
public void setRecordingServiceHelper(RecordingMetadataReaderHelper r) {
|
||||
recordingServiceHelper = r;
|
||||
}
|
||||
|
@ -64,6 +64,7 @@ public class SlidesGenerationProgressNotifier {
|
||||
);
|
||||
messagingService.sendDocConversionMsg(invalidMimeType);
|
||||
}
|
||||
|
||||
public void sendUploadFileTimedout(UploadedPresentation pres, int page) {
|
||||
UploadFileTimedoutMessage errorMessage = new UploadFileTimedoutMessage(
|
||||
pres.getPodId(),
|
||||
|
@ -346,7 +346,7 @@ class App extends React.Component {
|
||||
</h1>
|
||||
<div className="mt-3 col-text-right py-1 text-gray-500 inline-block">
|
||||
<p className="font-bold">
|
||||
<div className="inline">
|
||||
<div className="inline" data-test="meetingDateDashboard">
|
||||
<FormattedDate
|
||||
value={activitiesJson.createdOn}
|
||||
year="numeric"
|
||||
@ -359,7 +359,7 @@ class App extends React.Component {
|
||||
activitiesJson.endedOn > 0
|
||||
? (
|
||||
<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>
|
||||
)
|
||||
: null
|
||||
@ -367,14 +367,14 @@ class App extends React.Component {
|
||||
{
|
||||
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" />
|
||||
</span>
|
||||
)
|
||||
: null
|
||||
}
|
||||
</p>
|
||||
<p>
|
||||
<p data-test="meetingDurationTimeDashboard">
|
||||
<FormattedMessage id="app.learningDashboard.indicators.duration" defaultMessage="Duration" />
|
||||
:
|
||||
{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">
|
||||
<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>
|
||||
<CardContent classes={{ root: '!p-0' }}>
|
||||
<CardBody
|
||||
@ -420,7 +420,7 @@ class App extends React.Component {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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>
|
||||
<CardContent classes={{ root: '!p-0' }}>
|
||||
<CardBody
|
||||
@ -430,7 +430,7 @@ class App extends React.Component {
|
||||
maximumFractionDigits: 1,
|
||||
})}
|
||||
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
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -456,7 +456,7 @@ class App extends React.Component {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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>
|
||||
<CardContent classes={{ root: '!p-0' }}>
|
||||
<CardBody
|
||||
@ -470,7 +470,7 @@ class App extends React.Component {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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>
|
||||
<CardContent classes={{ root: '!p-0' }}>
|
||||
<CardBody
|
||||
@ -557,7 +557,7 @@ class App extends React.Component {
|
||||
<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 flex-col justify-center mb-4 sm:mb-0">
|
||||
<p>
|
||||
<p className="text-gray-700">
|
||||
{
|
||||
lastUpdated && (
|
||||
<>
|
||||
@ -583,7 +583,7 @@ class App extends React.Component {
|
||||
</div>
|
||||
<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)}
|
||||
>
|
||||
<FormattedMessage
|
||||
|
@ -303,6 +303,10 @@ const UserDatailsComponent = (props) => {
|
||||
return [];
|
||||
}
|
||||
|
||||
const Duration = new Date(getSumOfTime(Object.values(user.intIds)))
|
||||
.toISOString()
|
||||
.substring(11, 19);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 flex flex-row z-50">
|
||||
<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
|
||||
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"
|
||||
style={{
|
||||
right: `calc(${document.dir === 'ltr' ? userEndOffsetTime : userStartOffsetTime}% + 10px)`,
|
||||
left: `calc(${document.dir === 'ltr' ? userStartOffsetTime : userEndOffsetTime}% + 10px)`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
aria-describedby={`online-indicator-desc-${user.userKey}`}
|
||||
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 className="mx-3 inline-block text-white">
|
||||
{ Duration }
|
||||
</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 className="flex flex-row justify-between font-light text-gray-700">
|
||||
<div>
|
||||
<div><FormattedMessage id="app.learningDashboard.userDetails.startTime" defaultMessage="Start Time" /></div>
|
||||
<div>
|
||||
<div aria-label={`${intl.formatMessage({ id: 'app.learningDashboard.userDetails.startTime', defaultMessage: 'Joined' })} ${new Date(createdOn).toISOString().substring(11, 19)}`}>
|
||||
<div aria-hidden="true"><FormattedMessage id="app.learningDashboard.userDetails.startTime" defaultMessage="Start Time" /></div>
|
||||
<div aria-hidden="true">
|
||||
<FormattedTime value={createdOn} />
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
{ endedOn === 0 ? (
|
||||
<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 className="p-6 flex flex-row justify-between text-gray-700">
|
||||
<div>
|
||||
<div className="text-gray-900 font-medium">
|
||||
{ new Date(getSumOfTime(Object.values(user.intIds)))
|
||||
.toISOString()
|
||||
.substring(11, 19) }
|
||||
<div aria-label={`Duration ${Duration}`}>
|
||||
<div aria-hidden="true" className="text-gray-900 font-medium">
|
||||
{ Duration }
|
||||
</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 className="font-medium">
|
||||
<div aria-label={`${intl.formatMessage({ id: 'app.learningDashboard.userDetails.joined', defaultMessage: 'Joined' })} ${new Date(joinTime).toISOString().substring(11, 19)}`}>
|
||||
<div aria-hidden="true" className="font-medium">
|
||||
<FormattedTime value={joinTime} />
|
||||
</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 className="font-medium">
|
||||
|
@ -233,7 +233,7 @@ class UsersTable extends React.Component {
|
||||
const opacity = user.leftOn > 0 ? 'opacity-75' : '';
|
||||
return (
|
||||
<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">
|
||||
<UserAvatar user={user} />
|
||||
<div
|
||||
@ -253,7 +253,7 @@ class UsersTable extends React.Component {
|
||||
</button>
|
||||
{ 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
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4 inline"
|
||||
@ -279,7 +279,7 @@ class UsersTable extends React.Component {
|
||||
</p>
|
||||
{ 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
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4 inline"
|
||||
@ -315,7 +315,7 @@ class UsersTable extends React.Component {
|
||||
)) }
|
||||
</div>
|
||||
</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
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4 inline"
|
||||
@ -360,7 +360,7 @@ class UsersTable extends React.Component {
|
||||
}())
|
||||
}
|
||||
</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
|
||||
? (
|
||||
<span className="text-center">
|
||||
@ -383,7 +383,7 @@ class UsersTable extends React.Component {
|
||||
</span>
|
||||
) : null }
|
||||
</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
|
||||
? (
|
||||
<span className="text-center">
|
||||
@ -406,7 +406,7 @@ class UsersTable extends React.Component {
|
||||
</span>
|
||||
) : null }
|
||||
</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
|
||||
? (
|
||||
<span>
|
||||
@ -429,7 +429,7 @@ class UsersTable extends React.Component {
|
||||
</span>
|
||||
) : null }
|
||||
</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) => (
|
||||
<div className="text-xs whitespace-nowrap">
|
||||
@ -445,7 +445,7 @@ class UsersTable extends React.Component {
|
||||
))
|
||||
}
|
||||
</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
|
||||
? (
|
||||
<span>
|
||||
@ -470,7 +470,7 @@ class UsersTable extends React.Component {
|
||||
</td>
|
||||
{
|
||||
!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">
|
||||
<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'} />
|
||||
@ -490,7 +490,7 @@ class UsersTable extends React.Component {
|
||||
</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
|
||||
? (
|
||||
|
@ -3,6 +3,12 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
.text-gray-700 {
|
||||
color: #374151 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.bg-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
|
||||
|
||||
|
@ -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 16384:32768/udp
|
||||
|
||||
# Check if coturn is running on this server and, if so, open firewall port
|
||||
if systemctl status coturn > /dev/null; then
|
||||
echo " - Local turnserver detected -- opening port 3478"
|
||||
ufw allow 3478
|
||||
# echo " - Forcing FireFox to use turn server"
|
||||
# yq w -i $HTML5_CONFIG public.kurento.forceRelayOnFirefox true
|
||||
# Check if haproxy is running on this server and, if so, open port 3478 on ufw
|
||||
|
||||
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
|
||||
# echo " - Forcing FireFox to use turn server"
|
||||
# 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
|
||||
|
||||
ufw --force enable
|
||||
@ -255,9 +268,9 @@ notCalled() {
|
||||
# apply-config.sh.
|
||||
#
|
||||
# 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
|
||||
#!/bin/bash
|
||||
|
||||
|
@ -181,12 +181,14 @@ NCPU=$(nproc --all)
|
||||
|
||||
BBB_USER=bigbluebutton
|
||||
|
||||
TURN=$SERVLET_DIR/WEB-INF/classes/spring/turn-stun-servers.xml
|
||||
TURN_ETC_CONFIG=/etc/bigbluebutton/turn-stun-servers.xml
|
||||
if [ -f "$TURN_ETC_CONFIG" ]; then
|
||||
if [ $EUID == 0 ]; then
|
||||
TURN=$SERVLET_DIR/WEB-INF/classes/spring/turn-stun-servers.xml
|
||||
TURN_ETC_CONFIG=/etc/bigbluebutton/turn-stun-servers.xml
|
||||
if [ -f "$TURN_ETC_CONFIG" ]; then
|
||||
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
|
||||
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
|
||||
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 {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
.overrideSelect {
|
||||
background-color: #FFF !important;
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
.select {
|
||||
background-color: rgba(66, 133, 244, 1) !important;
|
||||
color: #FFF !important;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
document.addEventListener('gesturestart', function (e) {
|
||||
|
@ -7,7 +7,7 @@ const collectionOptions = Meteor.isClient ? {
|
||||
const AuthTokenValidation = new Mongo.Collection('auth-token-validation', collectionOptions);
|
||||
|
||||
if (Meteor.isServer) {
|
||||
AuthTokenValidation._ensureIndex({ meetingId: 1, userId: 1 });
|
||||
AuthTokenValidation._ensureIndex({ connectionId: 1 });
|
||||
}
|
||||
|
||||
export const ValidationStates = Object.freeze({
|
||||
|
@ -32,7 +32,7 @@ export default function handlePresentationConversionUpdate({ body }, meetingId)
|
||||
|
||||
check(meetingId, String);
|
||||
check(presentationId, Match.Maybe(String));
|
||||
check(podId, String);
|
||||
check(podId, Match.Maybe(String));
|
||||
check(status, String);
|
||||
check(temporaryPresentationId, Match.Maybe(String));
|
||||
|
||||
|
@ -50,6 +50,14 @@ export default function setCurrentPresentation(meetingId, podId, presentationId)
|
||||
|
||||
const oldPresentation = Presentations.findOne(oldCurrent.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) {
|
||||
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',
|
||||
enableVideo: 'bbb_enable_video',
|
||||
forceListenOnly: 'bbb_force_listen_only',
|
||||
hidePresentation: 'bbb_hide_presentation',
|
||||
hidePresentationOnJoin: 'bbb_hide_presentation',
|
||||
listenOnlyMode: 'bbb_listen_only_mode',
|
||||
multiUserPenOnly: 'bbb_multi_user_pen_only',
|
||||
multiUserTools: 'bbb_multi_user_tools',
|
||||
@ -56,7 +56,7 @@ const currentParameters = [
|
||||
'bbb_custom_style',
|
||||
'bbb_custom_style_url',
|
||||
// LAYOUT
|
||||
'bbb_hide_presentation',
|
||||
'bbb_hide_presentation_on_join',
|
||||
'bbb_show_participants_on_login',
|
||||
'bbb_show_public_chat_on_login',
|
||||
'bbb_hide_actions_bar',
|
||||
|
@ -16,7 +16,7 @@ export default function userLeftFlagUpdated(meetingId, userId, left) {
|
||||
try {
|
||||
const numberAffected = Users.update(selector, modifier);
|
||||
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) {
|
||||
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 { isChatEnabled } from '/imports/ui/services/features';
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
import BBBStorage from '/imports/ui/services/storage';
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
|
||||
const USER_WAS_EJECTED = 'userWasEjected';
|
||||
|
||||
const HTML = document.getElementsByTagName('html')[0];
|
||||
|
||||
@ -256,6 +258,7 @@ class Base extends Component {
|
||||
meetingEndedReason,
|
||||
meetingIsBreakout,
|
||||
subscriptionsReady,
|
||||
userWasEjected,
|
||||
} = this.props;
|
||||
|
||||
if ((loading || !subscriptionsReady) && !meetingHasEnded && meetingExist) {
|
||||
@ -270,7 +273,7 @@ class Base extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (ejected) {
|
||||
if (ejected || userWasEjected) {
|
||||
return (
|
||||
<MeetingEnded
|
||||
code="403"
|
||||
@ -280,7 +283,7 @@ class Base extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (meetingHasEnded && !meetingIsBreakout) {
|
||||
if ((meetingHasEnded && !meetingIsBreakout) || userWasEjected) {
|
||||
return (
|
||||
<MeetingEnded
|
||||
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.
|
||||
if (codeError !== '680') {
|
||||
return (<ErrorScreen code={codeError} callback={() => Base.setExitReason('error')} />);
|
||||
@ -385,6 +388,10 @@ export default withTracker(() => {
|
||||
const currentConnectionId = User?.currentConnectionId;
|
||||
const { connectionID, connectionAuthTime } = Auth;
|
||||
const connectionIdUpdateTime = User?.connectionIdUpdateTime;
|
||||
|
||||
if (ejected) {
|
||||
BBBStorage.setItem(USER_WAS_EJECTED, ejected);
|
||||
}
|
||||
|
||||
if (currentConnectionId && currentConnectionId !== connectionID && connectionIdUpdateTime > connectionAuthTime) {
|
||||
Session.set('codeError', '409');
|
||||
@ -397,6 +404,7 @@ export default withTracker(() => {
|
||||
const { streams: usersVideo } = VideoService.getVideoStreams();
|
||||
|
||||
return {
|
||||
userWasEjected: BBBStorage.getItem(USER_WAS_EJECTED),
|
||||
approved,
|
||||
ejected,
|
||||
ejectedReason,
|
||||
|
@ -11,6 +11,8 @@ import BBBMenu from '/imports/ui/components/common/menu/component';
|
||||
import Styled from './styles';
|
||||
import { colorPrimary } from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import { PANELS, ACTIONS, LAYOUT_TYPE } from '../../layout/enums';
|
||||
import { isPresentationEnabled } from '/imports/ui/services/features';
|
||||
import {isLayoutsEnabled} from '/imports/ui/services/features';
|
||||
|
||||
const propTypes = {
|
||||
amIPresenter: PropTypes.bool.isRequired,
|
||||
@ -154,9 +156,9 @@ class ActionsDropdown extends PureComponent {
|
||||
|
||||
const actions = [];
|
||||
|
||||
if (amIPresenter) {
|
||||
if (amIPresenter && isPresentationEnabled()) {
|
||||
actions.push({
|
||||
icon: "presentation",
|
||||
icon: "upload",
|
||||
dataTest: "managePresentations",
|
||||
label: formatMessage(presentationLabel),
|
||||
key: this.presentationItemId,
|
||||
@ -218,22 +220,26 @@ class ActionsDropdown extends PureComponent {
|
||||
})
|
||||
}
|
||||
|
||||
if (amIPresenter && showPushLayout) {
|
||||
if (amIPresenter && showPushLayout && isLayoutsEnabled()) {
|
||||
actions.push({
|
||||
icon: 'send',
|
||||
label: intl.formatMessage(intlMessages.propagateLayoutLabel),
|
||||
key: 'propagate layout',
|
||||
onClick: amIPresenter ? setMeetingLayout : setPushLayout,
|
||||
dataTest: 'propagateLayout',
|
||||
});
|
||||
}
|
||||
|
||||
actions.push({
|
||||
icon: 'send',
|
||||
label: intl.formatMessage(intlMessages.layoutModal),
|
||||
key: 'layoutModal',
|
||||
onClick: () => mountModal(<LayoutModalContainer {...this.props} />),
|
||||
});
|
||||
|
||||
if (isLayoutsEnabled()){
|
||||
actions.push({
|
||||
icon: 'send',
|
||||
label: intl.formatMessage(intlMessages.layoutModal),
|
||||
key: 'layoutModal',
|
||||
onClick: () => mountModal(<LayoutModalContainer {...this.props} />),
|
||||
dataTest: 'layoutModal',
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ import AudioControlsContainer from '../audio/audio-controls/container';
|
||||
import JoinVideoOptionsContainer from '../video-provider/video-button/container';
|
||||
import PresentationOptionsContainer from './presentation-options/component';
|
||||
import RaiseHandDropdownContainer from './raise-hand/container';
|
||||
import { isPresentationEnabled } from '/imports/ui/services/features';
|
||||
|
||||
class ActionsBar extends PureComponent {
|
||||
render() {
|
||||
@ -21,6 +22,7 @@ class ActionsBar extends PureComponent {
|
||||
handleTakePresenter,
|
||||
intl,
|
||||
isSharingVideo,
|
||||
isSharedNotesPinned,
|
||||
hasScreenshare,
|
||||
stopExternalVideoShare,
|
||||
isCaptionsAvailable,
|
||||
@ -39,6 +41,8 @@ class ActionsBar extends PureComponent {
|
||||
setPushLayout,
|
||||
} = this.props;
|
||||
|
||||
const shouldShowOptionsButton = (isPresentationEnabled() && isThereCurrentPresentation)
|
||||
|| isSharingVideo || hasScreenshare || isSharedNotesPinned;
|
||||
return (
|
||||
<Styled.ActionsBar
|
||||
style={
|
||||
@ -90,14 +94,18 @@ class ActionsBar extends PureComponent {
|
||||
/>
|
||||
</Styled.Center>
|
||||
<Styled.Right>
|
||||
<PresentationOptionsContainer
|
||||
presentationIsOpen={presentationIsOpen}
|
||||
setPresentationIsOpen={setPresentationIsOpen}
|
||||
layoutContextDispatch={layoutContextDispatch}
|
||||
hasPresentation={isThereCurrentPresentation}
|
||||
hasExternalVideo={isSharingVideo}
|
||||
hasScreenshare={hasScreenshare}
|
||||
/>
|
||||
{ shouldShowOptionsButton ?
|
||||
<PresentationOptionsContainer
|
||||
presentationIsOpen={presentationIsOpen}
|
||||
setPresentationIsOpen={setPresentationIsOpen}
|
||||
layoutContextDispatch={layoutContextDispatch}
|
||||
hasPresentation={isThereCurrentPresentation}
|
||||
hasExternalVideo={isSharingVideo}
|
||||
hasScreenshare={hasScreenshare}
|
||||
hasPinnedSharedNotes={isSharedNotesPinned}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
{isRaiseHandButtonEnabled
|
||||
? (
|
||||
<RaiseHandDropdownContainer {...{
|
||||
|
@ -14,7 +14,7 @@ import ExternalVideoService from '/imports/ui/components/external-video-player/s
|
||||
import CaptionsService from '/imports/ui/components/captions/service';
|
||||
import { layoutSelectOutput, layoutDispatch } from '../layout/context';
|
||||
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';
|
||||
|
||||
@ -57,10 +57,11 @@ export default withTracker(() => ({
|
||||
currentSlidHasContent: PresentationService.currentSlidHasContent(),
|
||||
parseCurrentSlideContent: PresentationService.parseCurrentSlideContent,
|
||||
isSharingVideo: Service.isSharingVideo(),
|
||||
isSharedNotesPinned: Service.isSharedNotesPinned(),
|
||||
hasScreenshare: isVideoBroadcasting(),
|
||||
isCaptionsAvailable: CaptionsService.isCaptionsAvailable(),
|
||||
isMeteorConnected: Meteor.status().connected,
|
||||
isPollingEnabled: isPollingEnabled(),
|
||||
isPollingEnabled: isPollingEnabled() && isPresentationEnabled(),
|
||||
isSelectRandomUserEnabled: SELECT_RANDOM_USER_ENABLED,
|
||||
isRaiseHandButtonEnabled: RAISE_HAND_BUTTON_ENABLED,
|
||||
isThereCurrentPresentation: Presentations.findOne({ meetingId: Auth.meetingID, current: true },
|
||||
|
@ -37,6 +37,7 @@ const PresentationOptionsContainer = ({
|
||||
hasPresentation,
|
||||
hasExternalVideo,
|
||||
hasScreenshare,
|
||||
hasPinnedSharedNotes,
|
||||
}) => {
|
||||
let buttonType = 'presentation';
|
||||
if (hasExternalVideo) {
|
||||
@ -46,7 +47,7 @@ const PresentationOptionsContainer = ({
|
||||
buttonType = 'desktop';
|
||||
}
|
||||
|
||||
const isThereCurrentPresentation = hasExternalVideo || hasScreenshare || hasPresentation;
|
||||
const isThereCurrentPresentation = hasExternalVideo || hasScreenshare || hasPresentation || hasPinnedSharedNotes;
|
||||
return (
|
||||
<Button
|
||||
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 Button from '/imports/ui/components/common/button/component';
|
||||
import Styled from './styles';
|
||||
import { EMOJI_STATUSES } from '/imports/utils/statuses';
|
||||
|
||||
const propTypes = {
|
||||
intl: PropTypes.shape({
|
||||
@ -22,15 +23,27 @@ const intlMessages = defineMessages({
|
||||
id: 'app.actionsBar.emojiMenu.statusTriggerLabel',
|
||||
description: 'label for option to show emoji menu',
|
||||
},
|
||||
clearStatusLabel: {
|
||||
id: 'app.actionsBar.emojiMenu.noneLabel',
|
||||
description: 'label for status clearing',
|
||||
},
|
||||
});
|
||||
|
||||
class RaiseHandDropdown extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isHandRaised: false,
|
||||
};
|
||||
}
|
||||
|
||||
getAvailableActions() {
|
||||
const {
|
||||
userId,
|
||||
getEmojiList,
|
||||
setEmojiStatus,
|
||||
intl,
|
||||
currentUser,
|
||||
} = this.props;
|
||||
|
||||
const actions = [];
|
||||
@ -41,6 +54,11 @@ class RaiseHandDropdown extends PureComponent {
|
||||
key: s,
|
||||
label: intl.formatMessage({ id: `app.actionsBar.emojiMenu.${s}Label` }),
|
||||
onClick: () => {
|
||||
if (currentUser.emoji === 'raiseHand') {
|
||||
this.setState({
|
||||
isHandRaised: true,
|
||||
});
|
||||
}
|
||||
setEmojiStatus(userId, s);
|
||||
},
|
||||
icon: getEmojiList[s],
|
||||
@ -57,30 +75,51 @@ class RaiseHandDropdown extends PureComponent {
|
||||
shortcuts,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isHandRaised,
|
||||
} = this.state;
|
||||
|
||||
const label = currentUser.emoji !== 'raiseHand' && currentUser.emoji !== 'none'
|
||||
? intlMessages.clearStatusLabel
|
||||
: {id: `app.actionsBar.emojiMenu.${
|
||||
currentUser.emoji === 'raiseHand'
|
||||
? 'lowerHandLabel'
|
||||
: 'raiseHandLabel'
|
||||
}`,
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon="hand"
|
||||
label={intl.formatMessage({
|
||||
id: `app.actionsBar.emojiMenu.${
|
||||
currentUser.emoji === 'raiseHand'
|
||||
? 'lowerHandLabel'
|
||||
: 'raiseHandLabel'
|
||||
}`,
|
||||
})}
|
||||
icon={EMOJI_STATUSES[currentUser.emoji === 'none'
|
||||
? 'raiseHand' : currentUser.emoji]}
|
||||
label={intl.formatMessage(
|
||||
label,
|
||||
)}
|
||||
accessKey={shortcuts.raisehand}
|
||||
color={currentUser.emoji === 'raiseHand' ? 'primary' : 'default'}
|
||||
color={currentUser.emoji !== 'none' ? 'primary' : 'default'}
|
||||
data-test={currentUser.emoji === 'raiseHand' ? 'lowerHandLabel' : 'raiseHandLabel'}
|
||||
ghost={currentUser.emoji !== 'raiseHand'}
|
||||
ghost={currentUser.emoji === 'none'}
|
||||
emoji={currentUser.emoji}
|
||||
hideLabel
|
||||
circle
|
||||
size="lg"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEmojiStatus(
|
||||
currentUser.userId,
|
||||
currentUser.emoji === 'raiseHand' ? 'none' : 'raiseHand',
|
||||
);
|
||||
if (currentUser.emoji !== 'none'
|
||||
&& currentUser.emoji !== 'raiseHand') {
|
||||
setEmojiStatus(
|
||||
currentUser.userId,
|
||||
isHandRaised ? 'raiseHand' : 'none',
|
||||
);
|
||||
} else {
|
||||
this.setState({
|
||||
isHandRaised: false,
|
||||
});
|
||||
setEmojiStatus(
|
||||
currentUser.userId,
|
||||
currentUser.emoji === 'raiseHand' ? 'none' : 'raiseHand',
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -4,6 +4,7 @@ import { makeCall } from '/imports/ui/services/api';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import Breakouts from '/imports/api/breakouts';
|
||||
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';
|
||||
|
||||
const USER_CONFIG = Meteor.settings.public.user;
|
||||
@ -73,5 +74,6 @@ export default {
|
||||
getLastBreakouts,
|
||||
getUsersNotJoined,
|
||||
takePresenterRole,
|
||||
isSharedNotesPinned: () => NotesService.isSharedNotesPinned(),
|
||||
isSharingVideo: () => getVideoUrl(),
|
||||
};
|
||||
|
@ -488,6 +488,7 @@ class App extends Component {
|
||||
pushLayoutMeeting,
|
||||
selectedLayout,
|
||||
setMeetingLayout,
|
||||
setPushLayout,
|
||||
shouldShowScreenshare,
|
||||
shouldShowExternalVideo,
|
||||
} = this.props;
|
||||
@ -517,6 +518,7 @@ class App extends Component {
|
||||
pushLayoutMeeting,
|
||||
selectedLayout,
|
||||
setMeetingLayout,
|
||||
setPushLayout,
|
||||
shouldShowScreenshare,
|
||||
shouldShowExternalVideo,
|
||||
}}
|
||||
|
@ -15,6 +15,7 @@ import UserInfos from '/imports/api/users-infos';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import MediaService from '/imports/ui/components/media/service';
|
||||
import LayoutService from '/imports/ui/components/layout/service';
|
||||
import { isPresentationEnabled } from '/imports/ui/services/features';
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
layoutSelect,
|
||||
@ -59,6 +60,8 @@ const AppContainer = (props) => {
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
const layoutType = useRef(null);
|
||||
|
||||
const {
|
||||
actionsbar,
|
||||
selectedLayout,
|
||||
@ -92,12 +95,23 @@ const AppContainer = (props) => {
|
||||
|
||||
const { sidebarContentPanel, isOpen: sidebarContentIsOpen } = sidebarContent;
|
||||
const { sidebarNavPanel, isOpen: sidebarNavigationIsOpen } = sidebarNavigation;
|
||||
const { isOpen: presentationIsOpen } = presentation;
|
||||
const shouldShowPresentation = propsShouldShowPresentation
|
||||
&& (presentationIsOpen || presentationRestoreOnUpdate);
|
||||
const { isOpen } = presentation;
|
||||
const presentationIsOpen = isOpen;
|
||||
|
||||
const shouldShowPresentation = (propsShouldShowPresentation
|
||||
&& (presentationIsOpen || presentationRestoreOnUpdate)) && isPresentationEnabled();
|
||||
|
||||
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';
|
||||
// this is not exactly right yet
|
||||
let presentationVideoRate;
|
||||
@ -132,6 +146,9 @@ const AppContainer = (props) => {
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
MediaService.buildLayoutWhenPresentationAreaIsDisabled(layoutContextDispatch)});
|
||||
|
||||
return currentUserId
|
||||
? (
|
||||
<App
|
||||
@ -308,7 +325,7 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
|
||||
'bbb_force_restore_presentation_on_new_events',
|
||||
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),
|
||||
isModalOpen: !!getModal(),
|
||||
ignorePollNotifications: Session.get('ignorePollNotifications'),
|
||||
|
@ -69,7 +69,7 @@ const Select = ({
|
||||
|
||||
if (voices.length === 0) {
|
||||
return (
|
||||
<div
|
||||
<div data-test="speechRecognition"
|
||||
style={{
|
||||
fontSize: '.75rem',
|
||||
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 Styled from './styles';
|
||||
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 VideoService from '/imports/ui/components/video-provider/service';
|
||||
import { PANELS, ACTIONS } from '../layout/enums';
|
||||
@ -495,7 +495,7 @@ class BreakoutRoom extends PureComponent {
|
||||
ref={(ref) => this.durationContainerRef = ref}
|
||||
>
|
||||
<Styled.Duration>
|
||||
<BreakoutRoomContainer
|
||||
<MeetingRemainingTime
|
||||
messageDuration={intlMessages.breakoutDuration}
|
||||
breakoutRoom={breakoutRooms[0]}
|
||||
fromBreakoutPanel
|
||||
|
@ -158,6 +158,16 @@ class TimeWindowList extends PureComponent {
|
||||
) {
|
||||
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) {
|
||||
@ -219,6 +229,8 @@ class TimeWindowList extends PureComponent {
|
||||
<span
|
||||
style={style}
|
||||
key={`span-${key}-${index}`}
|
||||
role="listitem"
|
||||
data-test="msgListItem"
|
||||
>
|
||||
<TimeWindowChatItem
|
||||
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() {
|
||||
const {
|
||||
layoutContextDispatch,
|
||||
hidePresentation,
|
||||
hidePresentationOnJoin,
|
||||
} = this.props;
|
||||
|
||||
window.removeEventListener('beforeunload', this.onBeforeUnload);
|
||||
@ -232,7 +232,7 @@ class VideoPlayer extends Component {
|
||||
value: false,
|
||||
});
|
||||
|
||||
if (hidePresentation) {
|
||||
if (hidePresentationOnJoin) {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_PRESENTATION_IS_OPEN,
|
||||
value: false,
|
||||
|
@ -105,6 +105,7 @@ class ExternalVideoModal extends Component {
|
||||
<label htmlFor="video-modal-input">
|
||||
{intl.formatMessage(intlMessages.input)}
|
||||
<input
|
||||
autoFocus
|
||||
id="video-modal-input"
|
||||
onChange={this.updateVideoUrlHandler}
|
||||
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 { INITIAL_INPUT_STATE } from '/imports/ui/components/layout/initState';
|
||||
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 windowHeight = () => window.document.documentElement.clientHeight;
|
||||
@ -34,6 +35,7 @@ const SmartLayout = (props) => {
|
||||
const navbarInput = layoutSelectInput((i) => i.navBar);
|
||||
const externalVideoInput = layoutSelectInput((i) => i.externalVideo);
|
||||
const screenShareInput = layoutSelectInput((i) => i.screenShare);
|
||||
const sharedNotesInput = layoutSelectInput((i) => i.sharedNotes);
|
||||
const layoutContextDispatch = layoutDispatch();
|
||||
|
||||
const prevDeviceType = usePrevious(deviceType);
|
||||
@ -262,11 +264,12 @@ const SmartLayout = (props) => {
|
||||
const { isOpen, currentSlide } = presentationInput;
|
||||
const { hasExternalVideo } = externalVideoInput;
|
||||
const { hasScreenShare } = screenShareInput;
|
||||
const { isPinned: isSharedNotesPinned } = sharedNotesInput;
|
||||
const mediaBounds = {};
|
||||
const { element: fullscreenElement } = fullscreen;
|
||||
const { num: currentSlideNumber } = currentSlide;
|
||||
|
||||
if (!isOpen || (currentSlideNumber === 0 && !hasExternalVideo && !hasScreenShare)) {
|
||||
if (!isOpen || ((isPresentationEnabled() && currentSlideNumber === 0) && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned)) {
|
||||
mediaBounds.width = 0;
|
||||
mediaBounds.height = 0;
|
||||
mediaBounds.top = 0;
|
||||
|
@ -14,6 +14,7 @@ const LayoutModalComponent = (props) => {
|
||||
const {
|
||||
intl,
|
||||
closeModal,
|
||||
isModerator,
|
||||
isPresenter,
|
||||
showToggleLabel,
|
||||
application,
|
||||
@ -101,7 +102,7 @@ const LayoutModalComponent = (props) => {
|
||||
};
|
||||
|
||||
const renderPushLayoutsOptions = () => {
|
||||
if (!isPresenter) {
|
||||
if (!isModerator && !isPresenter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -142,6 +143,7 @@ const LayoutModalComponent = (props) => {
|
||||
onClick={() => handleSwitchLayout(layout)}
|
||||
active={(layout === selectedLayout).toString()}
|
||||
aria-describedby="layout-btn-desc"
|
||||
data-test={`${layout}Layout`}
|
||||
/>
|
||||
</Styled.ButtonLayoutContainer>
|
||||
))}
|
||||
@ -186,6 +188,7 @@ const propTypes = {
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
closeModal: PropTypes.func.isRequired,
|
||||
isModerator: PropTypes.bool.isRequired,
|
||||
isPresenter: PropTypes.bool.isRequired,
|
||||
showToggleLabel: PropTypes.bool.isRequired,
|
||||
application: PropTypes.shape({
|
||||
|
@ -8,7 +8,7 @@ import { LAYOUT_TYPE, ACTIONS } from '../enums';
|
||||
import { isMobile } from '../utils';
|
||||
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 precision = 0.01;
|
||||
@ -39,6 +39,7 @@ const propTypes = {
|
||||
pushLayoutMeeting: PropTypes.bool,
|
||||
selectedLayout: PropTypes.string,
|
||||
setMeetingLayout: PropTypes.func,
|
||||
setPushLayout: PropTypes.func,
|
||||
shouldShowScreenshare: PropTypes.bool,
|
||||
shouldShowExternalVideo: PropTypes.bool,
|
||||
};
|
||||
@ -73,7 +74,7 @@ class PushLayoutEngine extends React.Component {
|
||||
}
|
||||
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);
|
||||
|
||||
if (selectedLayout === 'custom') {
|
||||
@ -136,6 +137,7 @@ class PushLayoutEngine extends React.Component {
|
||||
pushLayoutMeeting,
|
||||
selectedLayout,
|
||||
setMeetingLayout,
|
||||
setPushLayout,
|
||||
} = this.props;
|
||||
|
||||
const meetingLayoutDidChange = meetingLayout !== prevProps.meetingLayout;
|
||||
@ -240,9 +242,13 @@ class PushLayoutEngine extends React.Component {
|
||||
|| focusedCamera !== prevProps.focusedCamera
|
||||
|| !equalDouble(presentationVideoRate, prevProps.presentationVideoRate);
|
||||
|
||||
if ((pushLayout && layoutChanged) // change layout sizes / states
|
||||
|| (pushLayout !== prevProps.pushLayout) // push layout once after presenter toggles / special case where we set pushLayout to false in all viewers
|
||||
) {
|
||||
if (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) {
|
||||
setMeetingLayout();
|
||||
}
|
||||
|
@ -1,12 +1,15 @@
|
||||
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 getFromUserSettings from '/imports/ui/services/users-settings';
|
||||
import { isExternalVideoEnabled, isScreenSharingEnabled } from '/imports/ui/services/features';
|
||||
import { ACTIONS } from '../layout/enums';
|
||||
import UserService from '/imports/ui/components/user-list/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 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 {
|
||||
buildLayoutWhenPresentationAreaIsDisabled,
|
||||
getPresentationInfo,
|
||||
shouldShowWhiteboard,
|
||||
shouldShowScreenshare,
|
||||
|
@ -178,6 +178,8 @@ class NavBar extends Component {
|
||||
ariaLabel += hasNotification ? (` ${intl.formatMessage(intlMessages.newMessages)}`) : '';
|
||||
|
||||
const isExpanded = sidebarNavigation.isOpen;
|
||||
const { isPhone } = deviceInfo;
|
||||
|
||||
|
||||
const { acs } = this.state;
|
||||
|
||||
@ -214,7 +216,7 @@ class NavBar extends Component {
|
||||
&& <Styled.ArrowLeft iconName="left_arrow" />}
|
||||
<Styled.NavbarToggleButton
|
||||
onClick={this.handleToggleUserList}
|
||||
color='dark'
|
||||
color={isPhone && isExpanded ? 'primary' : 'dark'}
|
||||
size='md'
|
||||
circle
|
||||
hideLabel
|
||||
@ -258,4 +260,5 @@ class NavBar extends Component {
|
||||
|
||||
NavBar.propTypes = propTypes;
|
||||
NavBar.defaultProps = defaultProps;
|
||||
export default withShortcutHelper(withModalMounter(injectIntl(NavBar)), 'toggleUserList');
|
||||
export default withShortcutHelper(withModalMounter(injectIntl(NavBar)), 'toggleUserList');
|
||||
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
colorBackground,
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
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';
|
||||
|
||||
const Navbar = styled.header`
|
||||
@ -39,6 +39,9 @@ const ArrowLeft = styled(Icon)`
|
||||
font-size: 40%;
|
||||
color: ${colorWhite};
|
||||
left: .25rem;
|
||||
@media ${smallOnly} {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const ArrowRight = styled(Icon)`
|
||||
@ -46,6 +49,9 @@ const ArrowRight = styled(Icon)`
|
||||
font-size: 40%;
|
||||
color: ${colorWhite};
|
||||
right: .0125rem;
|
||||
@media ${smallOnly} {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const Center = styled.div`
|
||||
|
@ -9,6 +9,7 @@ import { PANELS, ACTIONS, LAYOUT_TYPE } from '../layout/enums';
|
||||
import browserInfo from '/imports/utils/browserInfo';
|
||||
import Header from '/imports/ui/components/common/control-header/component';
|
||||
import NotesDropdown from '/imports/ui/components/notes/notes-dropdown/container';
|
||||
import { isPresentationEnabled } from '../../services/features';
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
|
||||
@ -96,8 +97,8 @@ const Notes = ({
|
||||
useEffect(() => {
|
||||
if (
|
||||
isOnMediaArea
|
||||
&& sidebarContent.isOpen
|
||||
&& sidebarContent.sidebarContentPanel === PANELS.SHARED_NOTES
|
||||
&& (sidebarContent.isOpen || !isPresentationEnabled())
|
||||
&& (sidebarContent.sidebarContentPanel === PANELS.SHARED_NOTES || !isPresentationEnabled())
|
||||
) {
|
||||
if (layoutType === LAYOUT_TYPE.VIDEO_FOCUS) {
|
||||
layoutContextDispatch({
|
||||
@ -125,6 +126,10 @@ const Notes = ({
|
||||
type: ACTIONS.SET_NOTES_IS_PINNED,
|
||||
value: true,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_PRESENTATION_IS_OPEN,
|
||||
value: true,
|
||||
});
|
||||
|
||||
return () => {
|
||||
layoutContextDispatch({
|
||||
|
@ -6,7 +6,7 @@ import _ from 'lodash';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import { MeetingTimeRemaining } 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 { layoutSelectInput, layoutDispatch } from '../layout/context';
|
||||
import { ACTIONS } from '../layout/enums';
|
||||
@ -176,7 +176,7 @@ export default injectIntl(withTracker(({ intl }) => {
|
||||
|
||||
if (currentBreakout) {
|
||||
data.message = (
|
||||
<BreakoutRemainingTime
|
||||
<MeetingRemainingTime
|
||||
breakoutRoom={currentBreakout}
|
||||
messageDuration={intlMessages.breakoutTimeRemaining}
|
||||
timeEndedMessage={intlMessages.breakoutWillClose}
|
||||
@ -197,7 +197,7 @@ export default injectIntl(withTracker(({ intl }) => {
|
||||
|
||||
if (underThirtyMin && !isBreakout) {
|
||||
data.message = (
|
||||
<BreakoutRemainingTime
|
||||
<MeetingRemainingTime
|
||||
breakoutRoom={meetingTimeRemaining}
|
||||
messageDuration={intlMessages.meetingTimeRemaining}
|
||||
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 humanizeSeconds from '/imports/utils/humanizeSeconds';
|
||||
import _ from 'lodash';
|
||||
import BreakoutRemainingTimeComponent from './component';
|
||||
import MeetingRemainingTimeComponent from './component';
|
||||
import BreakoutService from '/imports/ui/components/breakout-room/service';
|
||||
import { Text, Time } from './styles';
|
||||
import { meetingIsBreakout } from '/imports/ui/components/app/service';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
failedMessage: {
|
||||
@ -37,6 +38,10 @@ const intlMessages = defineMessages({
|
||||
id: 'app.meeting.alertBreakoutEndsUnderMinutes',
|
||||
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;
|
||||
@ -66,17 +71,17 @@ class breakoutRemainingTimeContainer extends React.Component {
|
||||
const time = words.pop();
|
||||
const text = words.join(' ');
|
||||
return (
|
||||
<BreakoutRemainingTimeComponent>
|
||||
<MeetingRemainingTimeComponent>
|
||||
<Text>{text}</Text>
|
||||
<br />
|
||||
<Time data-test="breakoutRemainingTime">{time}</Time>
|
||||
</BreakoutRemainingTimeComponent>
|
||||
</MeetingRemainingTimeComponent>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<BreakoutRemainingTimeComponent>
|
||||
<MeetingRemainingTimeComponent>
|
||||
{message}
|
||||
</BreakoutRemainingTimeComponent>
|
||||
</MeetingRemainingTimeComponent>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -137,8 +142,11 @@ export default injectNotify(injectIntl(withTracker(({
|
||||
|
||||
if (alertsInSeconds.includes(time) && time !== lastAlertTime && displayAlerts) {
|
||||
const timeInMinutes = time / 60;
|
||||
const msg = { id: `${intlMessages.alertBreakoutEndsUnderMinutes.id}${timeInMinutes === 1 ? 'Singular' : 'Plural'}` };
|
||||
const alertMessage = intl.formatMessage(msg, { 0: timeInMinutes })
|
||||
const message = meetingIsBreakout()
|
||||
? intlMessages.alertBreakoutEndsUnderMinutes
|
||||
: intlMessages.alertMeetingEndsUnderMinutes;
|
||||
const msg = { id: `${message.id}${timeInMinutes === 1 ? 'Singular' : 'Plural'}` };
|
||||
const alertMessage = intl.formatMessage(msg, { 0: timeInMinutes });
|
||||
|
||||
lastAlertTime = time;
|
||||
notify(alertMessage, 'info', 'rooms');
|
@ -54,6 +54,7 @@ class Polling extends Component {
|
||||
checkedAnswers: [],
|
||||
};
|
||||
|
||||
this.pollingContainer = null;
|
||||
this.play = this.play.bind(this);
|
||||
this.handleUpdateResponseInput = this.handleUpdateResponseInput.bind(this);
|
||||
this.renderButtonAnswers = this.renderButtonAnswers.bind(this);
|
||||
@ -65,6 +66,7 @@ class Polling extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
this.play();
|
||||
this.pollingContainer && this.pollingContainer?.focus();
|
||||
}
|
||||
|
||||
play() {
|
||||
@ -302,7 +304,9 @@ class Polling extends Component {
|
||||
<Styled.PollingContainer
|
||||
autoWidth={stackOptions}
|
||||
data-test="pollingContainer"
|
||||
role="alert"
|
||||
role="complementary"
|
||||
ref={el => this.pollingContainer = el}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{
|
||||
question.length > 0 && (
|
||||
|
@ -144,7 +144,7 @@ const QText = styled.div`
|
||||
padding-right: ${smPaddingX};
|
||||
`;
|
||||
|
||||
const PollingContainer = styled.div`
|
||||
const PollingContainer = styled.aside`
|
||||
pointer-events:auto;
|
||||
min-width: ${pollWidth};
|
||||
position: absolute;
|
||||
@ -161,6 +161,10 @@ const PollingContainer = styled.div`
|
||||
bottom: ${pollBottomOffset};
|
||||
right: ${jumboPaddingX};
|
||||
|
||||
&:focus {
|
||||
border: 1px solid ${colorPrimary};
|
||||
}
|
||||
|
||||
[dir="rtl"] & {
|
||||
left: ${jumboPaddingX};
|
||||
right: auto;
|
||||
|
@ -1,24 +1,14 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import WhiteboardOverlayContainer from '/imports/ui/components/whiteboard/whiteboard-overlay/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 { SPACE } from '/imports/utils/keyCodes';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Session } from 'meteor/session';
|
||||
import PresentationToolbarContainer from './presentation-toolbar/container';
|
||||
import PresentationPlaceholder from './presentation-placeholder/component';
|
||||
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 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 Icon from '/imports/ui/components/common/icon/component';
|
||||
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 browserInfo from '/imports/utils/browserInfo';
|
||||
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({
|
||||
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 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 {
|
||||
constructor() {
|
||||
super();
|
||||
@ -72,7 +70,6 @@ class Presentation extends PureComponent {
|
||||
this.state = {
|
||||
presentationWidth: 0,
|
||||
presentationHeight: 0,
|
||||
showSlide: false,
|
||||
zoom: 100,
|
||||
fitToWidth: false,
|
||||
isFullscreen: false,
|
||||
@ -128,24 +125,6 @@ class Presentation extends PureComponent {
|
||||
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() {
|
||||
this.getInitialPresentationSizes();
|
||||
this.refPresentationContainer.addEventListener('keydown', this.handlePanShortcut);
|
||||
@ -180,7 +159,6 @@ class Presentation extends PureComponent {
|
||||
presentationIsOpen,
|
||||
currentSlide,
|
||||
publishedPoll,
|
||||
isViewer,
|
||||
setPresentationIsOpen,
|
||||
restoreOnUpdate,
|
||||
layoutContextDispatch,
|
||||
@ -189,10 +167,11 @@ class Presentation extends PureComponent {
|
||||
numCameras,
|
||||
intl,
|
||||
multiUser,
|
||||
clearFakeAnnotations,
|
||||
} = this.props;
|
||||
|
||||
const { presentationWidth, presentationHeight, zoom, isPanning, fitToWidth } = this.state;
|
||||
const {
|
||||
presentationWidth, presentationHeight, zoom, isPanning, fitToWidth,
|
||||
} = this.state;
|
||||
const {
|
||||
numCameras: prevNumCameras,
|
||||
presentationBounds: prevPresentationBounds,
|
||||
@ -200,7 +179,6 @@ class Presentation extends PureComponent {
|
||||
} = prevProps;
|
||||
|
||||
if (prevMultiUser && !multiUser) {
|
||||
clearFakeAnnotations();
|
||||
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 positionChanged = slidePosition
|
||||
.viewBoxHeight !== prevProps.slidePosition.viewBoxHeight
|
||||
@ -276,8 +254,8 @@ class Presentation extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
if ((presentationBounds !== prevPresentationBounds) ||
|
||||
(!presentationWidth && !presentationHeight)) this.onResize();
|
||||
if ((presentationBounds !== prevPresentationBounds)
|
||||
|| (!presentationWidth && !presentationHeight)) this.onResize();
|
||||
} else if (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();
|
||||
}
|
||||
}
|
||||
@ -320,10 +299,19 @@ class Presentation extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
setTldrawAPI(api) {
|
||||
this.setState({
|
||||
tldrawAPI: api,
|
||||
});
|
||||
handlePanShortcut(e) {
|
||||
const { userIsPresenter } = this.props;
|
||||
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() {
|
||||
@ -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) {
|
||||
this.refPresentationContainer = ref;
|
||||
}
|
||||
@ -357,16 +361,6 @@ class Presentation extends PureComponent {
|
||||
return this.svggroup;
|
||||
}
|
||||
|
||||
getToolbarHeight() {
|
||||
let height = 0;
|
||||
const toolbarEl = document.getElementById('presentationToolbarWrapper');
|
||||
if (toolbarEl) {
|
||||
const { clientHeight } = toolbarEl;
|
||||
height = clientHeight;
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
getPresentationSizesAvailable() {
|
||||
const {
|
||||
presentationBounds,
|
||||
@ -380,7 +374,7 @@ class Presentation extends PureComponent {
|
||||
if (newPresentationAreaSize) {
|
||||
presentationSizes.presentationWidth = newPresentationAreaSize.presentationAreaWidth;
|
||||
presentationSizes.presentationHeight = newPresentationAreaSize
|
||||
.presentationAreaHeight - (this.getToolbarHeight() || 0);
|
||||
.presentationAreaHeight - (getToolbarHeight() || 0);
|
||||
return presentationSizes;
|
||||
}
|
||||
|
||||
@ -396,11 +390,9 @@ class Presentation extends PureComponent {
|
||||
const presentationSizes = this.getPresentationSizesAvailable();
|
||||
if (Object.keys(presentationSizes).length > 0) {
|
||||
// setting the state of the available space for the svg
|
||||
// and set the showSlide to true to start rendering the slide
|
||||
this.setState({
|
||||
presentationHeight: presentationSizes.presentationHeight,
|
||||
presentationWidth: presentationSizes.presentationWidth,
|
||||
showSlide: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -409,6 +401,30 @@ class Presentation extends PureComponent {
|
||||
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) {
|
||||
const {
|
||||
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) {
|
||||
const {
|
||||
currentSlide,
|
||||
@ -509,230 +492,6 @@ class Presentation extends PureComponent {
|
||||
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) {
|
||||
const {
|
||||
currentSlide,
|
||||
@ -745,8 +504,14 @@ class Presentation extends PureComponent {
|
||||
layoutContextDispatch,
|
||||
presentationIsOpen,
|
||||
slidePosition,
|
||||
addWhiteboardGlobalAccess,
|
||||
removeWhiteboardGlobalAccess,
|
||||
multiUserSize,
|
||||
multiUser,
|
||||
} = this.props;
|
||||
const { zoom, fitToWidth } = this.state;
|
||||
const {
|
||||
zoom, fitToWidth, isPanning, tldrawAPI,
|
||||
} = this.state;
|
||||
|
||||
if (!currentSlide) return null;
|
||||
|
||||
@ -771,8 +536,8 @@ class Presentation extends PureComponent {
|
||||
presentationIsOpen,
|
||||
}}
|
||||
setIsPanning={this.setIsPanning}
|
||||
isPanning={this.state.isPanning}
|
||||
curPageId={this.state.tldrawAPI?.getPage()?.id}
|
||||
isPanning={isPanning}
|
||||
curPageId={tldrawAPI?.getPage()?.id}
|
||||
currentSlideNum={currentSlide.num}
|
||||
presentationId={currentSlide.presentationId}
|
||||
zoomChanger={this.zoomChanger}
|
||||
@ -780,66 +545,15 @@ class Presentation extends PureComponent {
|
||||
isFullscreen={fullscreenContext}
|
||||
fullscreenAction={ACTIONS.SET_FULLSCREEN_ELEMENT}
|
||||
fullscreenRef={this.refPresentationContainer}
|
||||
addWhiteboardGlobalAccess={this.props.addWhiteboardGlobalAccess}
|
||||
removeWhiteboardGlobalAccess={this.props.removeWhiteboardGlobalAccess}
|
||||
multiUserSize={this.props.multiUserSize}
|
||||
multiUser={this.props.multiUser}
|
||||
addWhiteboardGlobalAccess={addWhiteboardGlobalAccess}
|
||||
removeWhiteboardGlobalAccess={removeWhiteboardGlobalAccess}
|
||||
multiUserSize={multiUserSize}
|
||||
multiUser={multiUser}
|
||||
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() {
|
||||
const {
|
||||
intl, currentPresentation, userIsPresenter, downloadPresentationUri,
|
||||
@ -884,11 +598,12 @@ class Presentation extends PureComponent {
|
||||
fullscreenElementId,
|
||||
layoutContextDispatch,
|
||||
} = this.props;
|
||||
const { tldrawAPI } = this.state;
|
||||
|
||||
return (
|
||||
<PresentationMenu
|
||||
fullscreenRef={this.refPresentationContainer}
|
||||
tldrawAPI={this.state.tldrawAPI}
|
||||
tldrawAPI={tldrawAPI}
|
||||
elementName={intl.formatMessage(intlMessages.presentationLabel)}
|
||||
elementId={fullscreenElementId}
|
||||
layoutContextDispatch={layoutContextDispatch}
|
||||
@ -896,15 +611,10 @@ class Presentation extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
setTldrawIsMounting(value) {
|
||||
this.setState({ tldrawIsMounting: value });
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
userIsPresenter,
|
||||
currentSlide,
|
||||
multiUser,
|
||||
slidePosition,
|
||||
presentationBounds,
|
||||
fullscreenContext,
|
||||
@ -922,12 +632,12 @@ class Presentation extends PureComponent {
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
showSlide,
|
||||
isFullscreen,
|
||||
localPosition,
|
||||
fitToWidth,
|
||||
zoom,
|
||||
tldrawIsMounting,
|
||||
isPanning,
|
||||
} = this.state;
|
||||
|
||||
let viewBoxDimensions;
|
||||
@ -953,7 +663,7 @@ class Presentation extends PureComponent {
|
||||
const svgHeight = svgDimensions.height;
|
||||
const svgWidth = svgDimensions.width;
|
||||
|
||||
const toolbarHeight = this.getToolbarHeight();
|
||||
const toolbarHeight = getToolbarHeight();
|
||||
|
||||
const { presentationToolbarMinWidth } = DEFAULT_VALUES;
|
||||
|
||||
@ -968,18 +678,6 @@ class Presentation extends PureComponent {
|
||||
${currentSlide.content}
|
||||
${intl.formatMessage(intlMessages.slideContentEnd)}` : intl.formatMessage(intlMessages.noSlideContent);
|
||||
|
||||
if (!currentPresentation && this.refPresentationContainer) {
|
||||
return (
|
||||
<></>
|
||||
// <PresentationPlaceholder
|
||||
// {
|
||||
// ...presentationBounds
|
||||
// }
|
||||
// setPresentationRef={this.setPresentationRef}
|
||||
// />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Styled.PresentationContainer
|
||||
@ -995,9 +693,10 @@ class Presentation extends PureComponent {
|
||||
display: !presentationIsOpen ? 'none' : 'flex',
|
||||
overflow: 'hidden',
|
||||
zIndex: fullscreenContext ? presentationBounds.zIndex : undefined,
|
||||
background: layoutType === LAYOUT_TYPE.VIDEO_FOCUS && numCameras > 0 && !fullscreenContext
|
||||
? colorContentBackground
|
||||
: null,
|
||||
background:
|
||||
layoutType === LAYOUT_TYPE.VIDEO_FOCUS && numCameras > 0 && !fullscreenContext
|
||||
? colorContentBackground
|
||||
: null,
|
||||
}}
|
||||
>
|
||||
<Styled.Presentation ref={(ref) => { this.refPresentation = ref; }}>
|
||||
@ -1023,7 +722,7 @@ class Presentation extends PureComponent {
|
||||
slidePosition={slidePosition}
|
||||
getSvgRef={this.getSvgRef}
|
||||
setTldrawAPI={this.setTldrawAPI}
|
||||
curPageId={currentSlide?.num.toString()}
|
||||
curPageId={currentSlide?.num.toString() || '0'}
|
||||
svgUri={currentSlide?.svgUri}
|
||||
intl={intl}
|
||||
presentationWidth={svgWidth}
|
||||
@ -1031,7 +730,7 @@ class Presentation extends PureComponent {
|
||||
presentationAreaHeight={presentationBounds?.height}
|
||||
presentationAreaWidth={presentationBounds?.width}
|
||||
isViewersCursorLocked={isViewersCursorLocked}
|
||||
isPanning={this.state.isPanning}
|
||||
isPanning={isPanning}
|
||||
zoomChanger={this.zoomChanger}
|
||||
fitToWidth={fitToWidth}
|
||||
zoomValue={zoom}
|
||||
@ -1058,40 +757,9 @@ class Presentation extends PureComponent {
|
||||
{this.renderPresentationToolbar(svgWidth)}
|
||||
</Styled.PresentationToolbar>
|
||||
)}
|
||||
{/*this.renderPresentationToolbar()*/}
|
||||
</Styled.SvgContainer>
|
||||
</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,
|
||||
id: PropTypes.string.isRequired,
|
||||
imageUri: PropTypes.string.isRequired,
|
||||
curPageId: PropTypes.string,
|
||||
svgUri: PropTypes.string.isRequired,
|
||||
content: PropTypes.string.isRequired,
|
||||
}),
|
||||
slidePosition: PropTypes.shape({
|
||||
x: PropTypes.number.isRequired,
|
||||
@ -1122,9 +793,49 @@ Presentation.propTypes = {
|
||||
// current multi-user status
|
||||
multiUser: PropTypes.bool.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 = {
|
||||
currentSlide: undefined,
|
||||
slidePosition: undefined,
|
||||
currentPresentation: undefined,
|
||||
presentationAreaSize: undefined,
|
||||
presentationBounds: undefined,
|
||||
downloadPresentationUri: undefined,
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import { notify } from '/imports/ui/services/notification';
|
||||
import PresentationService from './service';
|
||||
@ -15,13 +16,14 @@ import {
|
||||
layoutSelectOutput,
|
||||
layoutDispatch,
|
||||
} 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 { DEVICE_TYPE } from '../layout/enums';
|
||||
import MediaService from '../media/service';
|
||||
|
||||
const PresentationContainer = ({ presentationIsOpen, presentationPodIds, mountPresentation, ...props }) => {
|
||||
|
||||
const PresentationContainer = ({
|
||||
presentationIsOpen, presentationPodIds, mountPresentation, ...props
|
||||
}) => {
|
||||
const cameraDock = layoutSelectInput((i) => i.cameraDock);
|
||||
const presentation = layoutSelectOutput((i) => i.presentation);
|
||||
const layoutType = layoutSelect((i) => i.layoutType);
|
||||
@ -66,80 +68,90 @@ const APP_CONFIG = Meteor.settings.public.app;
|
||||
const PRELOAD_NEXT_SLIDE = APP_CONFIG.preloadNextSlides;
|
||||
const fetchedpresentation = {};
|
||||
|
||||
export default lockContextContainer(
|
||||
export default lockContextContainer(
|
||||
withTracker(({ podId, presentationIsOpen, userLocks }) => {
|
||||
const currentSlide = PresentationService.getCurrentSlide(podId);
|
||||
const presentationIsDownloadable = PresentationService.isPresentationDownloadable(podId);
|
||||
const isViewersCursorLocked = userLocks?.hideViewersCursor;
|
||||
const currentSlide = PresentationService.getCurrentSlide(podId);
|
||||
const presentationIsDownloadable = PresentationService.isPresentationDownloadable(podId);
|
||||
const isViewersCursorLocked = userLocks?.hideViewersCursor;
|
||||
|
||||
let slidePosition;
|
||||
if (currentSlide) {
|
||||
const {
|
||||
presentationId,
|
||||
id: slideId,
|
||||
} = currentSlide;
|
||||
slidePosition = PresentationService.getSlidePosition(podId, presentationId, slideId);
|
||||
if (PRELOAD_NEXT_SLIDE && !fetchedpresentation[presentationId]) {
|
||||
fetchedpresentation[presentationId] = {
|
||||
canFetch: true,
|
||||
fetchedSlide: {},
|
||||
};
|
||||
}
|
||||
const currentSlideNum = currentSlide.num;
|
||||
const presentation = fetchedpresentation[presentationId];
|
||||
|
||||
if (PRELOAD_NEXT_SLIDE
|
||||
&& !presentation.fetchedSlide[currentSlide.num + PRELOAD_NEXT_SLIDE]
|
||||
&& presentation.canFetch) {
|
||||
const slidesToFetch = Slides.find({
|
||||
podId,
|
||||
let slidePosition;
|
||||
if (currentSlide) {
|
||||
const {
|
||||
presentationId,
|
||||
num: {
|
||||
$in: Array(PRELOAD_NEXT_SLIDE).fill(1).map((v, idx) => currentSlideNum + (idx + 1)),
|
||||
},
|
||||
}).fetch();
|
||||
id: slideId,
|
||||
} = currentSlide;
|
||||
slidePosition = PresentationService.getSlidePosition(podId, presentationId, slideId);
|
||||
if (PRELOAD_NEXT_SLIDE && !fetchedpresentation[presentationId]) {
|
||||
fetchedpresentation[presentationId] = {
|
||||
canFetch: true,
|
||||
fetchedSlide: {},
|
||||
};
|
||||
}
|
||||
const currentSlideNum = currentSlide.num;
|
||||
const presentation = fetchedpresentation[presentationId];
|
||||
|
||||
const promiseImageGet = slidesToFetch
|
||||
.filter((s) => !fetchedpresentation[presentationId].fetchedSlide[s.num])
|
||||
.map(async (slide) => {
|
||||
if (presentation.canFetch) presentation.canFetch = false;
|
||||
const image = await fetch(slide.imageUri);
|
||||
if (image.ok) {
|
||||
presentation.fetchedSlide[slide.num] = true;
|
||||
}
|
||||
if (PRELOAD_NEXT_SLIDE
|
||||
&& !presentation.fetchedSlide[currentSlide.num + PRELOAD_NEXT_SLIDE]
|
||||
&& presentation.canFetch) {
|
||||
const slidesToFetch = Slides.find({
|
||||
podId,
|
||||
presentationId,
|
||||
num: {
|
||||
$in: Array(PRELOAD_NEXT_SLIDE).fill(1).map((v, idx) => currentSlideNum + (idx + 1)),
|
||||
},
|
||||
}).fetch();
|
||||
|
||||
const promiseImageGet = slidesToFetch
|
||||
.filter((s) => !fetchedpresentation[presentationId].fetchedSlide[s.num])
|
||||
.map(async (slide) => {
|
||||
if (presentation.canFetch) presentation.canFetch = false;
|
||||
const image = await fetch(slide.imageUri);
|
||||
if (image.ok) {
|
||||
presentation.fetchedSlide[slide.num] = true;
|
||||
}
|
||||
});
|
||||
Promise.all(promiseImageGet).then(() => {
|
||||
presentation.canFetch = true;
|
||||
});
|
||||
Promise.all(promiseImageGet).then(() => {
|
||||
presentation.canFetch = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
currentSlide,
|
||||
slidePosition,
|
||||
downloadPresentationUri: PresentationService.downloadPresentationUri(podId),
|
||||
multiUser: (WhiteboardService.hasMultiUserAccess(currentSlide && currentSlide.id, Auth.userID) || WhiteboardService.isMultiUserActive(currentSlide?.id))
|
||||
&& presentationIsOpen,
|
||||
presentationIsDownloadable,
|
||||
mountPresentation: !!currentSlide,
|
||||
currentPresentation: PresentationService.getCurrentPresentation(podId),
|
||||
notify,
|
||||
zoomSlide: PresentationToolbarService.zoomSlide,
|
||||
podId,
|
||||
publishedPoll: Meetings.findOne({ meetingId: Auth.meetingID }, {
|
||||
fields: {
|
||||
publishedPoll: 1,
|
||||
},
|
||||
}).publishedPoll,
|
||||
restoreOnUpdate: getFromUserSettings(
|
||||
'bbb_force_restore_presentation_on_new_events',
|
||||
Meteor.settings.public.presentation.restoreOnUpdate,
|
||||
),
|
||||
addWhiteboardGlobalAccess: WhiteboardService.addGlobalAccess,
|
||||
removeWhiteboardGlobalAccess: WhiteboardService.removeGlobalAccess,
|
||||
multiUserSize: WhiteboardService.getMultiUserSize(currentSlide?.id),
|
||||
isViewersCursorLocked,
|
||||
clearFakeAnnotations: WhiteboardService.clearFakeAnnotations,
|
||||
setPresentationIsOpen: MediaService.setPresentationIsOpen,
|
||||
};
|
||||
})(PresentationContainer));
|
||||
return {
|
||||
currentSlide,
|
||||
slidePosition,
|
||||
downloadPresentationUri: PresentationService.downloadPresentationUri(podId),
|
||||
multiUser:
|
||||
(WhiteboardService.hasMultiUserAccess(currentSlide && currentSlide.id, Auth.userID)
|
||||
|| WhiteboardService.isMultiUserActive(currentSlide?.id)
|
||||
) && presentationIsOpen,
|
||||
presentationIsDownloadable,
|
||||
mountPresentation: !!currentSlide,
|
||||
currentPresentation: PresentationService.getCurrentPresentation(podId),
|
||||
notify,
|
||||
zoomSlide: PresentationToolbarService.zoomSlide,
|
||||
podId,
|
||||
publishedPoll: Meetings.findOne({ meetingId: Auth.meetingID }, {
|
||||
fields: {
|
||||
publishedPoll: 1,
|
||||
},
|
||||
}).publishedPoll,
|
||||
restoreOnUpdate: getFromUserSettings(
|
||||
'bbb_force_restore_presentation_on_new_events',
|
||||
Meteor.settings.public.presentation.restoreOnUpdate,
|
||||
),
|
||||
addWhiteboardGlobalAccess: WhiteboardService.addGlobalAccess,
|
||||
removeWhiteboardGlobalAccess: WhiteboardService.removeGlobalAccess,
|
||||
multiUserSize: WhiteboardService.getMultiUserSize(currentSlide?.id),
|
||||
isViewersCursorLocked,
|
||||
setPresentationIsOpen: MediaService.setPresentationIsOpen,
|
||||
};
|
||||
})(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 PropTypes from 'prop-types';
|
||||
import PresentationPodsContainer from '../../presentation-pod/container';
|
||||
|
||||
const PresentationArea = ({
|
||||
@ -17,3 +18,10 @@ const 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 PropTypes from 'prop-types';
|
||||
import { layoutSelectOutput } from '../../layout/context';
|
||||
import PresentationArea from './component';
|
||||
|
||||
@ -9,3 +10,8 @@ const PresentationAreaContainer = ({ presentationIsOpen, darkTheme }) => {
|
||||
};
|
||||
|
||||
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 logger from '/imports/startup/client/logger';
|
||||
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 { ACTIONS } from '/imports/ui/components/layout/enums';
|
||||
import browserInfo from '/imports/utils/browserInfo';
|
||||
|
||||
const OLD_MINIMIZE_BUTTON_ENABLED = Meteor.settings.public.presentation.oldMinimizeButton;
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
downloading: {
|
||||
id: 'app.presentation.options.downloading',
|
||||
@ -54,10 +52,10 @@ const intlMessages = defineMessages({
|
||||
defaultMessage: 'Snapshot of current slide',
|
||||
},
|
||||
whiteboardLabel: {
|
||||
id: "app.shortcut-help.whiteboard",
|
||||
id: 'app.shortcut-help.whiteboard',
|
||||
description: 'used for aria whiteboard options button label',
|
||||
defaultMessage: 'Whiteboard',
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
@ -65,23 +63,36 @@ const propTypes = {
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
handleToggleFullscreen: PropTypes.func.isRequired,
|
||||
isDropdownOpen: PropTypes.bool,
|
||||
isFullscreen: PropTypes.bool,
|
||||
elementName: PropTypes.string,
|
||||
fullscreenRef: PropTypes.instanceOf(Element),
|
||||
screenshotRef: PropTypes.instanceOf(Element),
|
||||
meetingName: PropTypes.string,
|
||||
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 = {
|
||||
isDropdownOpen: false,
|
||||
isIphone: false,
|
||||
isFullscreen: false,
|
||||
isRTL: false,
|
||||
elementName: '',
|
||||
meetingName: '',
|
||||
fullscreenRef: null,
|
||||
screenshotRef: null,
|
||||
elementId: '',
|
||||
elementGroup: '',
|
||||
currentElement: '',
|
||||
currentGroup: '',
|
||||
tldrawAPI: null,
|
||||
};
|
||||
|
||||
const PresentationMenu = (props) => {
|
||||
@ -99,7 +110,7 @@ const PresentationMenu = (props) => {
|
||||
layoutContextDispatch,
|
||||
meetingName,
|
||||
isIphone,
|
||||
isRTL
|
||||
isRTL,
|
||||
} = props;
|
||||
|
||||
const [state, setState] = useState({
|
||||
@ -177,7 +188,7 @@ const PresentationMenu = (props) => {
|
||||
{
|
||||
key: 'list-item-screenshot',
|
||||
label: intl.formatMessage(intlMessages.snapshotLabel),
|
||||
dataTest: "presentationSnapshot",
|
||||
dataTest: 'presentationSnapshot',
|
||||
icon: 'video',
|
||||
onClick: async () => {
|
||||
setState({
|
||||
@ -262,42 +273,42 @@ const PresentationMenu = (props) => {
|
||||
if (options.length === 0) {
|
||||
const undoCtrls = document.getElementById('TD-Styles')?.nextSibling;
|
||||
if (undoCtrls?.style) {
|
||||
undoCtrls.style = "padding:0px";
|
||||
undoCtrls.style = 'padding:0px';
|
||||
}
|
||||
const styleTool = document.getElementById('TD-Styles')?.parentNode;
|
||||
if (styleTool?.style) {
|
||||
styleTool.style = "right:0px";
|
||||
styleTool.style = 'right:0px';
|
||||
}
|
||||
return null
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Styled.Right>
|
||||
<BBBMenu
|
||||
trigger={
|
||||
<BBBMenu
|
||||
trigger={(
|
||||
<TooltipContainer title={intl.formatMessage(intlMessages.optionsLabel)}>
|
||||
<Styled.DropdownButton
|
||||
state={isDropdownOpen ? 'open' : 'closed'}
|
||||
aria-label={`${intl.formatMessage(intlMessages.whiteboardLabel)} ${intl.formatMessage(intlMessages.optionsLabel)}`}
|
||||
data-test="whiteboardOptionsButton"
|
||||
onClick={() => {
|
||||
setIsDropdownOpen((isOpen) => !isOpen)
|
||||
setIsDropdownOpen((isOpen) => !isOpen);
|
||||
}}
|
||||
>
|
||||
<Styled.ButtonIcon iconName="more" />
|
||||
>
|
||||
<Styled.ButtonIcon iconName="more" />
|
||||
</Styled.DropdownButton>
|
||||
</TooltipContainer>
|
||||
}
|
||||
)}
|
||||
opts={{
|
||||
id: "presentation-dropdown-menu",
|
||||
id: 'presentation-dropdown-menu',
|
||||
keepMounted: true,
|
||||
transitionDuration: 0,
|
||||
elevation: 3,
|
||||
getContentAnchorEl: null,
|
||||
fullwidth: "true",
|
||||
fullwidth: 'true',
|
||||
anchorOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' },
|
||||
transformOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' },
|
||||
container: fullscreenRef
|
||||
container: fullscreenRef,
|
||||
}}
|
||||
actions={options}
|
||||
/>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import PresentationMenu from './component';
|
||||
import FullscreenService from '/imports/ui/components/common/fullscreen-button/service';
|
||||
@ -42,3 +43,7 @@ export default withTracker((props) => {
|
||||
meetingName: meetingObject.meetingProp.name,
|
||||
};
|
||||
})(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,444 +1,452 @@
|
||||
import Presentations from '/imports/api/presentations';
|
||||
import React, { useState } from 'react';
|
||||
import Presentations, { UploadingPresentations } from '/imports/api/presentations';
|
||||
import React from 'react';
|
||||
import { useTracker } from 'meteor/react-meteor-data';
|
||||
import Icon from '/imports/ui/components/common/icon/component';
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
import Styled from '/imports/ui/components/presentation/presentation-uploader/styles';
|
||||
import { toast } from 'react-toastify';
|
||||
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({
|
||||
|
||||
item: {
|
||||
id: 'app.presentationUploder.item',
|
||||
description: 'single item label',
|
||||
},
|
||||
itemPlural: {
|
||||
id: 'app.presentationUploder.itemPlural',
|
||||
description: 'plural item label',
|
||||
},
|
||||
uploading: {
|
||||
id: 'app.presentationUploder.uploading',
|
||||
description: 'uploading label for toast notification',
|
||||
},
|
||||
uploadStatus: {
|
||||
id: 'app.presentationUploder.uploadStatus',
|
||||
description: 'upload status for toast notification',
|
||||
},
|
||||
completed: {
|
||||
id: 'app.presentationUploder.completed',
|
||||
description: 'uploads complete label for toast notification',
|
||||
},
|
||||
GENERATING_THUMBNAIL: {
|
||||
id: 'app.presentationUploder.conversion.generatingThumbnail',
|
||||
description: 'indicatess that it is generating thumbnails',
|
||||
},
|
||||
GENERATING_SVGIMAGES: {
|
||||
id: 'app.presentationUploder.conversion.generatingSvg',
|
||||
description: 'warns that it is generating svg images',
|
||||
},
|
||||
GENERATED_SLIDE: {
|
||||
id: 'app.presentationUploder.conversion.generatedSlides',
|
||||
description: 'warns that were slides generated',
|
||||
},
|
||||
413: {
|
||||
id: 'app.presentationUploder.upload.413',
|
||||
description: 'error that file exceed the size limit',
|
||||
},
|
||||
CONVERSION_TIMEOUT: {
|
||||
id:'app.presentationUploder.conversion.conversionTimeout',
|
||||
description: 'warns the user that the presentation timed out in the back-end in specific page of the document',
|
||||
},
|
||||
FILE_TOO_LARGE: {
|
||||
id: 'app.presentationUploder.upload.413',
|
||||
description: 'error that file exceed the size limit',
|
||||
},
|
||||
IVALID_MIME_TYPE: {
|
||||
id: 'app.presentationUploder.conversion.invalidMimeType',
|
||||
description: 'warns user that the file\'s mime type is not supported or it doesn\'t match the extension',
|
||||
},
|
||||
PAGE_COUNT_EXCEEDED: {
|
||||
id: 'app.presentationUploder.conversion.pageCountExceeded',
|
||||
description: 'warns the user that the conversion failed because of the page count',
|
||||
},
|
||||
PDF_HAS_BIG_PAGE: {
|
||||
id: 'app.presentationUploder.conversion.pdfHasBigPage',
|
||||
description: 'warns the user that the conversion failed because of the pdf page siz that exceeds the allowed limit',
|
||||
},
|
||||
OFFICE_DOC_CONVERSION_INVALID: {
|
||||
id: 'app.presentationUploder.conversion.officeDocConversionInvalid',
|
||||
description: '',
|
||||
},
|
||||
OFFICE_DOC_CONVERSION_FAILED: {
|
||||
id: 'app.presentationUploder.conversion.officeDocConversionFailed',
|
||||
description: 'warns the user that the conversion failed because of wrong office file',
|
||||
},
|
||||
UNSUPPORTED_DOCUMENT: {
|
||||
id: 'app.presentationUploder.conversion.unsupportedDocument',
|
||||
description: 'warns the user that the file extension is not supported',
|
||||
},
|
||||
204: {
|
||||
id: 'app.presentationUploder.conversion.204',
|
||||
description: 'error indicating that the file has no content to capture',
|
||||
},
|
||||
fileToUpload: {
|
||||
id: 'app.presentationUploder.fileToUpload',
|
||||
description: 'message used in the file selected for upload',
|
||||
},
|
||||
uploadProcess: {
|
||||
id: 'app.presentationUploder.upload.progress',
|
||||
description: 'message that indicates the percentage of the upload',
|
||||
},
|
||||
badConnectionError: {
|
||||
id: 'app.presentationUploder.connectionClosedError',
|
||||
description: 'message indicating that the connection was closed',
|
||||
},
|
||||
conversionProcessingSlides: {
|
||||
id: 'app.presentationUploder.conversion.conversionProcessingSlides',
|
||||
description: 'indicates how many slides were converted',
|
||||
},
|
||||
genericError: {
|
||||
id: 'app.presentationUploder.genericError',
|
||||
description: 'generic error while uploading/converting',
|
||||
},
|
||||
genericConversionStatus: {
|
||||
id: 'app.presentationUploder.conversion.genericConversionStatus',
|
||||
description: 'indicates that file is being converted',
|
||||
},
|
||||
});
|
||||
item: {
|
||||
id: 'app.presentationUploder.item',
|
||||
description: 'single item label',
|
||||
},
|
||||
itemPlural: {
|
||||
id: 'app.presentationUploder.itemPlural',
|
||||
description: 'plural item label',
|
||||
},
|
||||
uploading: {
|
||||
id: 'app.presentationUploder.uploading',
|
||||
description: 'uploading label for toast notification',
|
||||
},
|
||||
uploadStatus: {
|
||||
id: 'app.presentationUploder.uploadStatus',
|
||||
description: 'upload status for toast notification',
|
||||
},
|
||||
completed: {
|
||||
id: 'app.presentationUploder.completed',
|
||||
description: 'uploads complete label for toast notification',
|
||||
},
|
||||
GENERATING_THUMBNAIL: {
|
||||
id: 'app.presentationUploder.conversion.generatingThumbnail',
|
||||
description: 'indicatess that it is generating thumbnails',
|
||||
},
|
||||
GENERATING_SVGIMAGES: {
|
||||
id: 'app.presentationUploder.conversion.generatingSvg',
|
||||
description: 'warns that it is generating svg images',
|
||||
},
|
||||
GENERATED_SLIDE: {
|
||||
id: 'app.presentationUploder.conversion.generatedSlides',
|
||||
description: 'warns that were slides generated',
|
||||
},
|
||||
413: {
|
||||
id: 'app.presentationUploder.upload.413',
|
||||
description: 'error that file exceed the size limit',
|
||||
},
|
||||
CONVERSION_TIMEOUT: {
|
||||
id: 'app.presentationUploder.conversion.conversionTimeout',
|
||||
description: 'warns the user that the presentation timed out in the back-end in specific page of the document',
|
||||
},
|
||||
FILE_TOO_LARGE: {
|
||||
id: 'app.presentationUploder.upload.413',
|
||||
description: 'error that file exceed the size limit',
|
||||
},
|
||||
IVALID_MIME_TYPE: {
|
||||
id: 'app.presentationUploder.conversion.invalidMimeType',
|
||||
description: 'warns user that the file\'s mime type is not supported or it doesn\'t match the extension',
|
||||
},
|
||||
PAGE_COUNT_EXCEEDED: {
|
||||
id: 'app.presentationUploder.conversion.pageCountExceeded',
|
||||
description: 'warns the user that the conversion failed because of the page count',
|
||||
},
|
||||
PDF_HAS_BIG_PAGE: {
|
||||
id: 'app.presentationUploder.conversion.pdfHasBigPage',
|
||||
description: 'warns the user that the conversion failed because of the pdf page siz that exceeds the allowed limit',
|
||||
},
|
||||
OFFICE_DOC_CONVERSION_INVALID: {
|
||||
id: 'app.presentationUploder.conversion.officeDocConversionInvalid',
|
||||
description: '',
|
||||
},
|
||||
OFFICE_DOC_CONVERSION_FAILED: {
|
||||
id: 'app.presentationUploder.conversion.officeDocConversionFailed',
|
||||
description: 'warns the user that the conversion failed because of wrong office file',
|
||||
},
|
||||
UNSUPPORTED_DOCUMENT: {
|
||||
id: 'app.presentationUploder.conversion.unsupportedDocument',
|
||||
description: 'warns the user that the file extension is not supported',
|
||||
},
|
||||
204: {
|
||||
id: 'app.presentationUploder.conversion.204',
|
||||
description: 'error indicating that the file has no content to capture',
|
||||
},
|
||||
fileToUpload: {
|
||||
id: 'app.presentationUploder.fileToUpload',
|
||||
description: 'message used in the file selected for upload',
|
||||
},
|
||||
uploadProcess: {
|
||||
id: 'app.presentationUploder.upload.progress',
|
||||
description: 'message that indicates the percentage of the upload',
|
||||
},
|
||||
badConnectionError: {
|
||||
id: 'app.presentationUploder.connectionClosedError',
|
||||
description: 'message indicating that the connection was closed',
|
||||
},
|
||||
conversionProcessingSlides: {
|
||||
id: 'app.presentationUploder.conversion.conversionProcessingSlides',
|
||||
description: 'indicates how many slides were converted',
|
||||
},
|
||||
genericError: {
|
||||
id: 'app.presentationUploder.genericError',
|
||||
description: 'generic error while uploading/converting',
|
||||
},
|
||||
genericConversionStatus: {
|
||||
id: 'app.presentationUploder.conversion.genericConversionStatus',
|
||||
description: 'indicates that file is being converted',
|
||||
},
|
||||
});
|
||||
|
||||
function renderPresentationItemStatus(item, intl) {
|
||||
if ((("progress" in item) && item.progress === 0) || (("upload" in item) && item.upload.progress === 0 && !item.upload.error)) {
|
||||
return intl.formatMessage(intlMessages.fileToUpload);
|
||||
}
|
||||
if ((('progress' in item) && item.progress === 0) || (('upload' in item) && item.upload.progress === 0 && !item.upload.error)) {
|
||||
return intl.formatMessage(intlMessages.fileToUpload);
|
||||
}
|
||||
|
||||
if (("progress" in item) && item.progress < 100 && !("conversion" in item)) {
|
||||
return intl.formatMessage(intlMessages.uploadProcess, {
|
||||
0: Math.floor(item.progress).toString(),
|
||||
});
|
||||
}
|
||||
if (('progress' in item) && item.progress < 100 && !('conversion' in item)) {
|
||||
return intl.formatMessage(intlMessages.uploadProcess, {
|
||||
0: Math.floor(item.progress).toString(),
|
||||
});
|
||||
}
|
||||
|
||||
const constraint = {};
|
||||
const constraint = {};
|
||||
|
||||
if (("upload" in item) && (item.upload.done && item.upload.error)) {
|
||||
if (item.conversion.status === 'FILE_TOO_LARGE' || item.upload.status !== 413) {
|
||||
constraint['0'] = ((item.conversion.maxFileSize) / 1000 / 1000).toFixed(2);
|
||||
} else {
|
||||
if (item.progress < 100) {
|
||||
const errorMessage = intlMessages.badConnectionError;
|
||||
return intl.formatMessage(errorMessage);
|
||||
}
|
||||
}
|
||||
if (('upload' in item) && (item.upload.done && item.upload.error)) {
|
||||
if (item.conversion.status === 'FILE_TOO_LARGE' || item.upload.status !== 413) {
|
||||
constraint['0'] = ((item.conversion.maxFileSize) / 1000 / 1000).toFixed(2);
|
||||
} else if (item.progress < 100) {
|
||||
const errorMessage = intlMessages.badConnectionError;
|
||||
return intl.formatMessage(errorMessage);
|
||||
}
|
||||
|
||||
const errorMessage = intlMessages[item.upload.status] || intlMessages.genericError;
|
||||
return intl.formatMessage(errorMessage, constraint);
|
||||
}
|
||||
const errorMessage = intlMessages[item.upload.status] || intlMessages.genericError;
|
||||
return intl.formatMessage(errorMessage, constraint);
|
||||
}
|
||||
|
||||
if (("conversion" in item) && (!item.conversion.done && item.conversion.error)) {
|
||||
const errorMessage = intlMessages[item.conversion.status] || intlMessages.genericConversionStatus;
|
||||
if (('conversion' in item) && (!item.conversion.done && item.conversion.error)) {
|
||||
const errorMessage = intlMessages[item.conversion.status]
|
||||
|| intlMessages.genericConversionStatus;
|
||||
|
||||
switch (item.conversion.status) {
|
||||
case 'CONVERSION_TIMEOUT':
|
||||
constraint['0'] = item.conversion.numberPageError;
|
||||
constraint['1'] = item.conversion.maxNumberOfAttempts;
|
||||
break;
|
||||
case 'FILE_TOO_LARGE':
|
||||
constraint['0'] = ((item.conversion.maxFileSize) / 1000 / 1000).toFixed(2);
|
||||
break;
|
||||
case 'PAGE_COUNT_EXCEEDED':
|
||||
constraint['0'] = item.conversion.maxNumberPages;
|
||||
break;
|
||||
case 'PDF_HAS_BIG_PAGE':
|
||||
constraint['0'] = (item.conversion.bigPageSize / 1000 / 1000).toFixed(2);
|
||||
break;
|
||||
case 'IVALID_MIME_TYPE':
|
||||
constraint['0'] = item.conversion.fileExtension;
|
||||
constraint['1'] = item.conversion.fileMime;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
switch (item.conversion.status) {
|
||||
case 'CONVERSION_TIMEOUT':
|
||||
constraint['0'] = item.conversion.numberPageError;
|
||||
constraint['1'] = item.conversion.maxNumberOfAttempts;
|
||||
break;
|
||||
case 'FILE_TOO_LARGE':
|
||||
constraint['0'] = ((item.conversion.maxFileSize) / 1000 / 1000).toFixed(2);
|
||||
break;
|
||||
case 'PAGE_COUNT_EXCEEDED':
|
||||
constraint['0'] = item.conversion.maxNumberPages;
|
||||
break;
|
||||
case 'PDF_HAS_BIG_PAGE':
|
||||
constraint['0'] = (item.conversion.bigPageSize / 1000 / 1000).toFixed(2);
|
||||
break;
|
||||
case 'IVALID_MIME_TYPE':
|
||||
constraint['0'] = item.conversion.fileExtension;
|
||||
constraint['1'] = item.conversion.fileMime;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
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)) {
|
||||
let conversionStatusMessage
|
||||
if ("conversion" in item) {
|
||||
if (item.conversion.pagesCompleted < item.conversion.numPages) {
|
||||
return intl.formatMessage(intlMessages.conversionProcessingSlides, {
|
||||
0: item.conversion.pagesCompleted,
|
||||
1: item.conversion.numPages,
|
||||
});
|
||||
}
|
||||
if ((('conversion' in item) && (!item.conversion.done && !item.conversion.error)) || (('progress' in item) && item.progress === 100)) {
|
||||
let conversionStatusMessage;
|
||||
if ('conversion' in item) {
|
||||
if (item.conversion.pagesCompleted < item.conversion.numPages) {
|
||||
return intl.formatMessage(intlMessages.conversionProcessingSlides, {
|
||||
0: item.conversion.pagesCompleted,
|
||||
1: item.conversion.numPages,
|
||||
});
|
||||
}
|
||||
|
||||
conversionStatusMessage = intlMessages[item.conversion.status]
|
||||
|| intlMessages.genericConversionStatus;
|
||||
} else {
|
||||
conversionStatusMessage = intlMessages.genericConversionStatus;
|
||||
}
|
||||
return intl.formatMessage(conversionStatusMessage);
|
||||
}
|
||||
conversionStatusMessage = intlMessages[item.conversion.status]
|
||||
|| intlMessages.genericConversionStatus;
|
||||
} else {
|
||||
conversionStatusMessage = intlMessages.genericConversionStatus;
|
||||
}
|
||||
return intl.formatMessage(conversionStatusMessage);
|
||||
}
|
||||
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
function renderToastItem(item, intl) {
|
||||
const isUploading = ('progress' in item) && item.progress <= 100;
|
||||
const isConverting = ('conversion' in item) && !item.conversion.done;
|
||||
const hasError = ((('conversion' in item) && item.conversion.error) || (('upload' in item) && item.upload.error));
|
||||
const isProcessing = (isUploading || isConverting) && !hasError;
|
||||
|
||||
const isUploading = ("progress" in item) && item.progress <= 100;
|
||||
const isConverting = ("conversion" in item) && !item.conversion.done;
|
||||
const hasError = ((("conversion" in item) && item.conversion.error) || (("upload" in item) && item.upload.error));
|
||||
const isProcessing = (isUploading || isConverting) && !hasError;
|
||||
let icon = isProcessing ? 'blank' : 'check';
|
||||
if (hasError) icon = 'circle_close';
|
||||
|
||||
let icon = isProcessing ? 'blank' : 'check';
|
||||
if (hasError) icon = 'circle_close';
|
||||
|
||||
return (
|
||||
<Styled.UploadRow
|
||||
key={item.id || item.temporaryPresentationId}
|
||||
onClick={() => {
|
||||
if (hasError || isProcessing) Session.set('showUploadPresentationView', true);
|
||||
}}
|
||||
>
|
||||
<Styled.FileLine>
|
||||
<span>
|
||||
<Icon iconName="file" />
|
||||
</span>
|
||||
<Styled.ToastFileName>
|
||||
<span>{item.filename || item.name}</span>
|
||||
</Styled.ToastFileName>
|
||||
<Styled.StatusIcon>
|
||||
<Styled.ToastItemIcon
|
||||
done={!isProcessing && !hasError}
|
||||
error={hasError}
|
||||
loading={ isProcessing }
|
||||
iconName={icon}
|
||||
/>
|
||||
</Styled.StatusIcon>
|
||||
</Styled.FileLine>
|
||||
<Styled.StatusInfo>
|
||||
<Styled.StatusInfoSpan data-test="presentationStatusInfo" styles={hasError ? 'error' : 'info'}>
|
||||
{renderPresentationItemStatus(item, intl)}
|
||||
</Styled.StatusInfoSpan>
|
||||
</Styled.StatusInfo>
|
||||
</Styled.UploadRow>
|
||||
);
|
||||
return (
|
||||
<Styled.UploadRow
|
||||
key={item.id || item.temporaryPresentationId}
|
||||
onClick={() => {
|
||||
if (hasError || isProcessing) Session.set('showUploadPresentationView', true);
|
||||
}}
|
||||
>
|
||||
<Styled.FileLine>
|
||||
<span>
|
||||
<Icon iconName="file" />
|
||||
</span>
|
||||
<Styled.ToastFileName>
|
||||
<span>{item.filename || item.name}</span>
|
||||
</Styled.ToastFileName>
|
||||
<Styled.StatusIcon>
|
||||
<Styled.ToastItemIcon
|
||||
done={!isProcessing && !hasError}
|
||||
error={hasError}
|
||||
loading={isProcessing}
|
||||
iconName={icon}
|
||||
/>
|
||||
</Styled.StatusIcon>
|
||||
</Styled.FileLine>
|
||||
<Styled.StatusInfo>
|
||||
<Styled.StatusInfoSpan data-test="presentationStatusInfo" styles={hasError ? 'error' : 'info'}>
|
||||
{renderPresentationItemStatus(item, intl)}
|
||||
</Styled.StatusInfoSpan>
|
||||
</Styled.StatusInfo>
|
||||
</Styled.UploadRow>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const renderToastList = (presentations, intl) => {
|
||||
|
||||
let converted = 0;
|
||||
let converted = 0;
|
||||
|
||||
let presentationsSorted = presentations
|
||||
.sort((a, b) => a.uploadTimestamp - b.uploadTimestamp)
|
||||
.sort((a, b) => {
|
||||
const presADone = a.conversion ? a.conversion.done : false;
|
||||
const presBDone = b.conversion ? b.conversion.done : false;
|
||||
const presentationsSorted = presentations
|
||||
.sort((a, b) => a.uploadTimestamp - b.uploadTimestamp)
|
||||
.sort((a, b) => {
|
||||
const presADone = a.conversion ? a.conversion.done : false;
|
||||
const presBDone = b.conversion ? b.conversion.done : false;
|
||||
|
||||
return presADone - presBDone
|
||||
});
|
||||
return presADone - presBDone;
|
||||
});
|
||||
|
||||
presentationsSorted
|
||||
.forEach((p) => {
|
||||
const presDone = p.conversion ? p.conversion.done : false;
|
||||
if (presDone) converted += 1;
|
||||
return p;
|
||||
});
|
||||
presentationsSorted
|
||||
.forEach((p) => {
|
||||
const presDone = p.conversion ? p.conversion.done : false;
|
||||
if (presDone) converted += 1;
|
||||
return p;
|
||||
});
|
||||
|
||||
let toastHeading = '';
|
||||
const itemLabel = presentationsSorted.length > 1
|
||||
? intl.formatMessage(intlMessages.itemPlural)
|
||||
: intl.formatMessage(intlMessages.item);
|
||||
let toastHeading = '';
|
||||
const itemLabel = presentationsSorted.length > 1
|
||||
? intl.formatMessage(intlMessages.itemPlural)
|
||||
: intl.formatMessage(intlMessages.item);
|
||||
|
||||
if (converted === 0) {
|
||||
toastHeading = intl.formatMessage(intlMessages.uploading, {
|
||||
0: presentationsSorted.length,
|
||||
1: itemLabel,
|
||||
});
|
||||
}
|
||||
if (converted === 0) {
|
||||
toastHeading = intl.formatMessage(intlMessages.uploading, {
|
||||
0: presentationsSorted.length,
|
||||
1: itemLabel,
|
||||
});
|
||||
}
|
||||
|
||||
if (converted > 0 && converted !== presentationsSorted.length) {
|
||||
toastHeading = intl.formatMessage(intlMessages.uploadStatus, {
|
||||
0: converted,
|
||||
1: presentationsSorted.length,
|
||||
});
|
||||
}
|
||||
if (converted > 0 && converted !== presentationsSorted.length) {
|
||||
toastHeading = intl.formatMessage(intlMessages.uploadStatus, {
|
||||
0: converted,
|
||||
1: presentationsSorted.length,
|
||||
});
|
||||
}
|
||||
|
||||
if (converted === presentationsSorted.length) {
|
||||
toastHeading = intl.formatMessage(intlMessages.completed, {
|
||||
0: converted,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Styled.ToastWrapper data-test="presentationUploadProgressToast" >
|
||||
<Styled.UploadToastHeader>
|
||||
<Styled.UploadIcon iconName="upload" />
|
||||
<Styled.UploadToastTitle>{toastHeading}</Styled.UploadToastTitle>
|
||||
</Styled.UploadToastHeader>
|
||||
<Styled.InnerToast>
|
||||
<div>
|
||||
<div>
|
||||
{presentationsSorted.map((item) => renderToastItem(item, intl))}
|
||||
</div>
|
||||
</div>
|
||||
</Styled.InnerToast>
|
||||
</Styled.ToastWrapper>
|
||||
);
|
||||
}
|
||||
if (converted === presentationsSorted.length) {
|
||||
toastHeading = intl.formatMessage(intlMessages.completed, {
|
||||
0: converted,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Styled.ToastWrapper data-test="presentationUploadProgressToast">
|
||||
<Styled.UploadToastHeader>
|
||||
<Styled.UploadIcon iconName="upload" />
|
||||
<Styled.UploadToastTitle>{toastHeading}</Styled.UploadToastTitle>
|
||||
</Styled.UploadToastHeader>
|
||||
<Styled.InnerToast>
|
||||
<div>
|
||||
<div>
|
||||
{presentationsSorted.map((item) => renderToastItem(item, intl))}
|
||||
</div>
|
||||
</div>
|
||||
</Styled.InnerToast>
|
||||
</Styled.ToastWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
function handleDismissToast(toastId) {
|
||||
return toast.dismiss(toastId);
|
||||
return toast.dismiss(toastId);
|
||||
}
|
||||
|
||||
const alreadyRenderedPresList = [];
|
||||
|
||||
let alreadyRenderedPresList = [];
|
||||
|
||||
let enteredConversion = {};
|
||||
const enteredConversion = {};
|
||||
|
||||
export const PresentationUploaderToast = ({ intl }) => {
|
||||
useTracker(() => {
|
||||
const presentationsRenderedFalseAndConversionFalse = Presentations.find({ $or: [{ renderedInToast: false }, { 'conversion.done': false }] }).fetch();
|
||||
|
||||
useTracker(() => {
|
||||
|
||||
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);
|
||||
|
||||
let conversionInterrupted = false;
|
||||
|
||||
// removing ones with errors. If presentation has an error status - we don't want to have it pending as uploading
|
||||
convertingPresentations.map(p => {
|
||||
if ("conversion" in p && p.conversion.error){
|
||||
UploadingPresentations.remove({$or: [{temporaryPresentationId: p.temporaryPresentationId }, {id: p.id}]});
|
||||
conversionInterrupted = true;
|
||||
}
|
||||
});
|
||||
// removing ones with errors.
|
||||
// If presentation has an error status - we don't want to have it pending as uploading
|
||||
convertingPresentations.forEach((p) => {
|
||||
if ('conversion' in p && p.conversion.error) {
|
||||
UploadingPresentations.remove(
|
||||
{ $or: [{ temporaryPresentationId: p.temporaryPresentationId }, { id: p.id }] },
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let toRemoveFromUploadingPresentations = [];
|
||||
const toRemoveFromUploadingPresentations = [];
|
||||
// main goal of this mapping is to sort out what doesn't need to be displayed
|
||||
UploadingPresentations.find().fetch().forEach((p) => {
|
||||
if (
|
||||
('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
|
||||
) {
|
||||
if (convertingPresentations[0]) { // there are presentations being converted
|
||||
convertingPresentations.forEach((cp) => {
|
||||
// if this presentation is being converted
|
||||
// we don't want it to be marked as still uploading
|
||||
if (cp.temporaryPresentationId === p.temporaryPresentationId) {
|
||||
toRemoveFromUploadingPresentations
|
||||
.push({ temporaryPresentationId: p.temporaryPresentationId, id: p.id });
|
||||
}
|
||||
});
|
||||
// upload stage is done and pesentation is entering conversion stage
|
||||
} else if (!enteredConversion[p.temporaryPresentationId]) {
|
||||
// we mark that it has entered conversion stage
|
||||
enteredConversion[p.temporaryPresentationId] = true;
|
||||
} else {
|
||||
// presentation doesn't normally enter conversion twice so we remove
|
||||
// the inconsistencies between UploadingPresentation and Presentation (corner case)
|
||||
const presentationsAlreadyRenderedIds = Presentations
|
||||
.find({ renderedInToast: true }).fetch().map((pr) => (
|
||||
{
|
||||
id: pr.id,
|
||||
temporaryPresentationId: pr.temporaryPresentationId,
|
||||
}
|
||||
));
|
||||
presentationsAlreadyRenderedIds.forEach((pr) => {
|
||||
UploadingPresentations.remove({
|
||||
$or: [{ temporaryPresentationId: pr.temporaryPresentationId }, { id: pr.id }],
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
UploadingPresentations.find().fetch().map(p => { // main goal of this mapping is to sort out what doesn't need to be displayed
|
||||
if (
|
||||
( "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
|
||||
) {
|
||||
if(convertingPresentations[0]) { //there are presentations being converted
|
||||
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
|
||||
toRemoveFromUploadingPresentations.push({temporaryPresentationId: p.temporaryPresentationId, id: p.id});
|
||||
}
|
||||
});
|
||||
} else if (!enteredConversion[p.temporaryPresentationId]) { // upload stage is done and pesentation is entering conversion stage
|
||||
enteredConversion[p.temporaryPresentationId] = true; // we mark that it has entered conversion stage
|
||||
} else {
|
||||
// presentation doesn't normally enter conversion twice
|
||||
// so we remove the inconsistencies between UploadingPresentation and Presentation (corner case)
|
||||
presentationsAlreadyRenderedIds = Presentations.find({renderedInToast: true}).fetch().map(p => {
|
||||
return {
|
||||
id: p.id,
|
||||
temporaryPresentationId: p.temporaryPresentationId,
|
||||
}
|
||||
});
|
||||
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);
|
||||
// Updating or populating the "state" presentation list
|
||||
presentationsToConvert.map((p) => (
|
||||
{
|
||||
filename: p.name || p.filename,
|
||||
temporaryPresentationId: p.temporaryPresentationId,
|
||||
presentationId: p.id,
|
||||
hasError: p.conversion?.error || p.upload?.error,
|
||||
lastModifiedUploader: p.lastModifiedUploader,
|
||||
}
|
||||
)).forEach((p) => {
|
||||
const docIndexAlreadyInList = alreadyRenderedPresList.findIndex((pres) => (
|
||||
(pres.temporaryPresentationId === p.temporaryPresentationId
|
||||
|| pres.presentationId === p.presentationId
|
||||
|| (
|
||||
pres.lastModifiedUploader !== undefined
|
||||
&& !pres.lastModifiedUploader && pres.filename === p.filename
|
||||
)
|
||||
)
|
||||
));
|
||||
if (docIndexAlreadyInList === -1) {
|
||||
alreadyRenderedPresList.push({
|
||||
filename: p.filename,
|
||||
temporaryPresentationId: p.temporaryPresentationId,
|
||||
presentationId: p.presentationId,
|
||||
rendered: false,
|
||||
lastModifiedUploader: p.lastModifiedUploader,
|
||||
hasError: p.hasError,
|
||||
});
|
||||
} else {
|
||||
const presAlreadyRendered = alreadyRenderedPresList[docIndexAlreadyInList];
|
||||
presAlreadyRendered.temporaryPresentationId = p.temporaryPresentationId;
|
||||
presAlreadyRendered.presentationId = p.presentationId;
|
||||
presAlreadyRendered.lastModifiedUploader = p.lastModifiedUploader;
|
||||
presAlreadyRendered.hasError = p.hasError;
|
||||
}
|
||||
});
|
||||
let activeToast = Session.get('presentationUploaderToastId');
|
||||
const showToast = presentationsToConvert.length > 0;
|
||||
|
||||
let presentationsToConvert = convertingPresentations.concat(uploadingPresentations);
|
||||
// Updating or populating the "state" presentation list
|
||||
presentationsToConvert.map(p => {
|
||||
return {
|
||||
filename: p.name || p.filename,
|
||||
temporaryPresentationId: p.temporaryPresentationId,
|
||||
presentationId: p.id,
|
||||
hasError: p.conversion?.error || p.upload?.error,
|
||||
lastModifiedUploader: p.lastModifiedUploader,
|
||||
}
|
||||
}).forEach(p => {
|
||||
const docIndexAlreadyInList = alreadyRenderedPresList.findIndex(pres => {
|
||||
return (pres.temporaryPresentationId === p.temporaryPresentationId || pres.presentationId === p.presentationId
|
||||
|| (pres.lastModifiedUploader !== undefined && !pres.lastModifiedUploader && pres.filename === p.filename))});
|
||||
if (docIndexAlreadyInList === -1) {
|
||||
alreadyRenderedPresList.push({
|
||||
filename: p.filename,
|
||||
temporaryPresentationId: p.temporaryPresentationId,
|
||||
presentationId: p.presentationId,
|
||||
rendered: false,
|
||||
lastModifiedUploader: p.lastModifiedUploader,
|
||||
hasError: p.hasError,
|
||||
});
|
||||
} else {
|
||||
alreadyRenderedPresList[docIndexAlreadyInList].temporaryPresentationId = p.temporaryPresentationId;
|
||||
alreadyRenderedPresList[docIndexAlreadyInList].presentationId = p.presentationId;
|
||||
alreadyRenderedPresList[docIndexAlreadyInList].lastModifiedUploader = p.lastModifiedUploader;
|
||||
alreadyRenderedPresList[docIndexAlreadyInList].hasError = p.hasError;
|
||||
}
|
||||
})
|
||||
let activeToast = Session.get("presentationUploaderToastId");
|
||||
const showToast = presentationsToConvert.length > 0;
|
||||
if (showToast && !activeToast) {
|
||||
activeToast = toast.info(() => renderToastList(presentationsToConvert, intl), {
|
||||
hideProgressBar: true,
|
||||
autoClose: false,
|
||||
newestOnTop: true,
|
||||
closeOnClick: true,
|
||||
className: 'presentationUploaderToast toastClass',
|
||||
onClose: () => {
|
||||
presentationsToConvert = [];
|
||||
if (alreadyRenderedPresList.every((pres) => pres.rendered)) {
|
||||
makeCall('setPresentationRenderedInToast').then(() => {
|
||||
Session.set('presentationUploaderToastId', null);
|
||||
});
|
||||
alreadyRenderedPresList.length = 0;
|
||||
}
|
||||
},
|
||||
});
|
||||
Session.set('presentationUploaderToastId', activeToast);
|
||||
} else if (!showToast && activeToast) {
|
||||
handleDismissToast(activeToast);
|
||||
Session.set('presentationUploaderToastId', null);
|
||||
} else {
|
||||
toast.update(activeToast, {
|
||||
render: renderToastList(presentationsToConvert, intl),
|
||||
});
|
||||
}
|
||||
|
||||
if (showToast && !activeToast) {
|
||||
activeToast = toast.info(() => renderToastList(presentationsToConvert, intl), {
|
||||
hideProgressBar: true,
|
||||
autoClose: false,
|
||||
newestOnTop: true,
|
||||
closeOnClick: true,
|
||||
className: "presentationUploaderToast toastClass",
|
||||
onClose: () => {
|
||||
presentationsToConvert = [];
|
||||
if (alreadyRenderedPresList.every((pres) => pres.rendered)) {
|
||||
makeCall('setPresentationRenderedInToast').then(() => {
|
||||
Session.set("presentationUploaderToastId", null);
|
||||
});
|
||||
alreadyRenderedPresList.length = 0;
|
||||
}
|
||||
},
|
||||
});
|
||||
Session.set("presentationUploaderToastId", activeToast);
|
||||
} else if (!showToast && activeToast) {
|
||||
handleDismissToast(activeToast);
|
||||
Session.set("presentationUploaderToastId", null);
|
||||
} else {
|
||||
toast.update(activeToast, {
|
||||
render: renderToastList(presentationsToConvert, intl),
|
||||
});
|
||||
}
|
||||
const temporaryPresentationIdListToSetAsRendered = presentationsToConvert.filter((p) => (
|
||||
'conversion' in p && (p.conversion.done || p.conversion.error)
|
||||
));
|
||||
|
||||
let temporaryPresentationIdListToSetAsRendered = presentationsToConvert.filter(p =>
|
||||
("conversion" in p && (p.conversion.done || p.conversion.error)))
|
||||
|
||||
temporaryPresentationIdListToSetAsRendered = temporaryPresentationIdListToSetAsRendered.map(p => {
|
||||
index = alreadyRenderedPresList.findIndex(pres => (pres.temporaryPresentationId === p.temporaryPresentationId || pres.presentationId === p.id));
|
||||
if (index !== -1) {
|
||||
alreadyRenderedPresList[index].rendered = true;
|
||||
}
|
||||
return p.temporaryPresentationId
|
||||
});
|
||||
temporaryPresentationIdListToSetAsRendered.forEach((p) => {
|
||||
const index = alreadyRenderedPresList.findIndex((pres) => (
|
||||
pres.temporaryPresentationId === p.temporaryPresentationId || pres.presentationId === p.id
|
||||
));
|
||||
if (index !== -1) {
|
||||
alreadyRenderedPresList[index].rendered = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (alreadyRenderedPresList.every((pres) => pres.rendered && !pres.hasError)) {
|
||||
|
||||
setTimeout(() => {
|
||||
makeCall('setPresentationRenderedInToast');
|
||||
alreadyRenderedPresList.length = 0;
|
||||
}, TIMEOUT_CLOSE_TOAST * 1000);
|
||||
}
|
||||
|
||||
}, []);
|
||||
return null;
|
||||
}
|
||||
if (alreadyRenderedPresList.every((pres) => pres.rendered && !pres.hasError)) {
|
||||
setTimeout(() => {
|
||||
makeCall('setPresentationRenderedInToast');
|
||||
alreadyRenderedPresList.length = 0;
|
||||
}, TIMEOUT_CLOSE_TOAST * 1000);
|
||||
}
|
||||
}, []);
|
||||
return null;
|
||||
};
|
||||
|
||||
export default {
|
||||
handleDismissToast,
|
||||
renderPresentationItemStatus,
|
||||
}
|
||||
handleDismissToast,
|
||||
renderPresentationItemStatus,
|
||||
};
|
||||
|
@ -265,8 +265,6 @@ class PresentationToolbar extends PureComponent {
|
||||
slidePosition,
|
||||
multiUserSize,
|
||||
multiUser,
|
||||
setIsPanning,
|
||||
isPanning,
|
||||
} = this.props;
|
||||
|
||||
const { isMobile } = deviceInfo;
|
||||
@ -276,14 +274,12 @@ class PresentationToolbar extends PureComponent {
|
||||
|
||||
const prevSlideAriaLabel = startOfSlides
|
||||
? intl.formatMessage(intlMessages.previousSlideLabel)
|
||||
: `${intl.formatMessage(intlMessages.previousSlideLabel)} (${
|
||||
currentSlideNum <= 1 ? '' : currentSlideNum - 1
|
||||
: `${intl.formatMessage(intlMessages.previousSlideLabel)} (${currentSlideNum <= 1 ? '' : currentSlideNum - 1
|
||||
})`;
|
||||
|
||||
const nextSlideAriaLabel = endOfSlides
|
||||
? intl.formatMessage(intlMessages.nextSlideLabel)
|
||||
: `${intl.formatMessage(intlMessages.nextSlideLabel)} (${
|
||||
currentSlideNum >= 1 ? currentSlideNum + 1 : ''
|
||||
: `${intl.formatMessage(intlMessages.nextSlideLabel)} (${currentSlideNum >= 1 ? currentSlideNum + 1 : ''
|
||||
})`;
|
||||
|
||||
return (
|
||||
@ -312,8 +308,8 @@ class PresentationToolbar extends PureComponent {
|
||||
role="button"
|
||||
aria-label={prevSlideAriaLabel}
|
||||
aria-describedby={
|
||||
startOfSlides ? 'noPrevSlideDesc' : 'prevSlideDesc'
|
||||
}
|
||||
startOfSlides ? 'noPrevSlideDesc' : 'prevSlideDesc'
|
||||
}
|
||||
disabled={startOfSlides || !isMeteorConnected}
|
||||
color="light"
|
||||
circle
|
||||
@ -346,8 +342,8 @@ class PresentationToolbar extends PureComponent {
|
||||
role="button"
|
||||
aria-label={nextSlideAriaLabel}
|
||||
aria-describedby={
|
||||
endOfSlides ? 'noNextSlideDesc' : 'nextSlideDesc'
|
||||
}
|
||||
endOfSlides ? 'noNextSlideDesc' : 'nextSlideDesc'
|
||||
}
|
||||
disabled={endOfSlides || !isMeteorConnected}
|
||||
color="light"
|
||||
circle
|
||||
@ -364,10 +360,10 @@ class PresentationToolbar extends PureComponent {
|
||||
data-test={multiUser ? 'turnMultiUsersWhiteboardOff' : 'turnMultiUsersWhiteboardOn'}
|
||||
role="button"
|
||||
aria-label={
|
||||
multiUser
|
||||
? intl.formatMessage(intlMessages.toolbarMultiUserOff)
|
||||
: intl.formatMessage(intlMessages.toolbarMultiUserOn)
|
||||
}
|
||||
multiUser
|
||||
? intl.formatMessage(intlMessages.toolbarMultiUserOff)
|
||||
: intl.formatMessage(intlMessages.toolbarMultiUserOn)
|
||||
}
|
||||
color="light"
|
||||
disabled={!isMeteorConnected}
|
||||
icon={multiUser ? 'multi_whiteboard' : 'whiteboard'}
|
||||
@ -375,10 +371,10 @@ class PresentationToolbar extends PureComponent {
|
||||
circle
|
||||
onClick={() => this.handleSwitchWhiteboardMode(!multiUser)}
|
||||
label={
|
||||
multiUser
|
||||
? intl.formatMessage(intlMessages.toolbarMultiUserOff)
|
||||
: intl.formatMessage(intlMessages.toolbarMultiUserOn)
|
||||
}
|
||||
multiUser
|
||||
? intl.formatMessage(intlMessages.toolbarMultiUserOff)
|
||||
: intl.formatMessage(intlMessages.toolbarMultiUserOn)
|
||||
}
|
||||
hideLabel
|
||||
/>
|
||||
{multiUser ? (
|
||||
@ -400,33 +396,19 @@ class PresentationToolbar extends PureComponent {
|
||||
/>
|
||||
</TooltipContainer>
|
||||
) : 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
|
||||
role="button"
|
||||
data-test="fitToWidthButton"
|
||||
aria-describedby={fitToWidth ? 'fitPageDesc' : 'fitWidthDesc'}
|
||||
aria-label={
|
||||
fitToWidth
|
||||
? `${intl.formatMessage(
|
||||
intlMessages.presentationLabel,
|
||||
)} ${intl.formatMessage(intlMessages.fitToPage)}`
|
||||
: `${intl.formatMessage(
|
||||
intlMessages.presentationLabel,
|
||||
)} ${intl.formatMessage(intlMessages.fitToWidth)}`
|
||||
}
|
||||
fitToWidth
|
||||
? `${intl.formatMessage(
|
||||
intlMessages.presentationLabel,
|
||||
)} ${intl.formatMessage(intlMessages.fitToPage)}`
|
||||
: `${intl.formatMessage(
|
||||
intlMessages.presentationLabel,
|
||||
)} ${intl.formatMessage(intlMessages.fitToWidth)}`
|
||||
}
|
||||
color="light"
|
||||
disabled={!isMeteorConnected}
|
||||
icon="fit_to_width"
|
||||
@ -467,6 +449,25 @@ PresentationToolbar.propTypes = {
|
||||
fullscreenAction: PropTypes.string.isRequired,
|
||||
isFullscreen: PropTypes.bool.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));
|
||||
|
@ -77,4 +77,9 @@ PresentationToolbarContainer.propTypes = {
|
||||
nextSlide: PropTypes.func.isRequired,
|
||||
previousSlide: PropTypes.func.isRequired,
|
||||
skipToSlide: PropTypes.func.isRequired,
|
||||
layoutSwapped: PropTypes.bool,
|
||||
};
|
||||
|
||||
PresentationToolbarContainer.defaultProps = {
|
||||
layoutSwapped: false,
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages } from 'react-intl';
|
||||
import { safeMatch } from '/imports/utils/string-utils';
|
||||
import { isUrlValid, startWatching } from '/imports/ui/components/external-video-player/service';
|
||||
@ -65,3 +66,18 @@ export const SmartMediaShare = (props) => {
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
@ -161,7 +161,7 @@ class ZoomTool extends PureComponent {
|
||||
}
|
||||
|
||||
const stateZoomPct = intl.formatNumber((stateZoomValue / 100), { style: 'percent' });
|
||||
|
||||
|
||||
return (
|
||||
[
|
||||
(
|
||||
@ -235,7 +235,10 @@ class ZoomTool extends PureComponent {
|
||||
}
|
||||
|
||||
const propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
formatNumber: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
zoomValue: PropTypes.number.isRequired,
|
||||
change: PropTypes.func.isRequired,
|
||||
minBound: PropTypes.number.isRequired,
|
||||
|
@ -15,28 +15,41 @@ import { registerTitleView, unregisterTitleView } from '/imports/utils/dom-utils
|
||||
import Styled from './styles';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import Radio from '/imports/ui/components/common/radio/component';
|
||||
import { isPresentationEnabled } from '/imports/ui/services/features';
|
||||
|
||||
const { isMobile } = deviceInfo;
|
||||
const propTypes = {
|
||||
allowDownloadable: PropTypes.bool.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
fileUploadConstraintsHint: PropTypes.bool.isRequired,
|
||||
fileSizeMax: PropTypes.number.isRequired,
|
||||
filePagesMax: PropTypes.number.isRequired,
|
||||
handleSave: PropTypes.func.isRequired,
|
||||
dispatchTogglePresentationDownloadable: PropTypes.func.isRequired,
|
||||
fileValidMimeTypes: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fileValidMimeTypes: PropTypes.arrayOf(PropTypes.shape).isRequired,
|
||||
presentations: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
filename: PropTypes.string.isRequired,
|
||||
isCurrent: PropTypes.bool.isRequired,
|
||||
conversion: PropTypes.object,
|
||||
upload: PropTypes.object,
|
||||
conversion: PropTypes.shape,
|
||||
upload: PropTypes.shape,
|
||||
})).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 = {
|
||||
selectedToBeNextCurrent: '',
|
||||
};
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
@ -147,9 +160,9 @@ const intlMessages = defineMessages({
|
||||
id: 'app.presentationUploder.conversion.timeout',
|
||||
},
|
||||
CONVERSION_TIMEOUT: {
|
||||
id:'app.presentationUploder.conversion.conversionTimeout',
|
||||
description: 'warns the user that the presentation timed out in the back-end in specific page of the document',
|
||||
},
|
||||
id: 'app.presentationUploder.conversion.conversionTimeout',
|
||||
description: 'warns the user that the presentation timed out in the back-end in specific page of the document',
|
||||
},
|
||||
GENERATING_THUMBNAIL: {
|
||||
id: 'app.presentationUploder.conversion.generatingThumbnail',
|
||||
description: 'indicatess that it is generating thumbnails',
|
||||
@ -304,6 +317,8 @@ const EXPORT_STATUSES = {
|
||||
EXPORTED: 'EXPORTED',
|
||||
};
|
||||
|
||||
const handleDismissToast = (id) => toast.dismiss(id);
|
||||
|
||||
class PresentationUploader extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -311,7 +326,6 @@ class PresentationUploader extends Component {
|
||||
this.state = {
|
||||
presentations: [],
|
||||
disableActions: false,
|
||||
toUploadCount: 0,
|
||||
presExporting: new Set(),
|
||||
};
|
||||
|
||||
@ -319,8 +333,9 @@ class PresentationUploader extends Component {
|
||||
this.hasError = null;
|
||||
this.exportToastId = null;
|
||||
|
||||
const { handleFiledrop } = this.props;
|
||||
// handlers
|
||||
this.handleFiledrop = this.props.handleFiledrop;
|
||||
this.handleFiledrop = handleFiledrop;
|
||||
this.handleConfirm = this.handleConfirm.bind(this);
|
||||
this.handleDismiss = this.handleDismiss.bind(this);
|
||||
this.handleRemove = this.handleRemove.bind(this);
|
||||
@ -353,24 +368,28 @@ class PresentationUploader extends Component {
|
||||
});
|
||||
if (propPresentations.length > prevPropPresentations.length) {
|
||||
shouldUpdateState = true;
|
||||
const propsDiffs = propPresentations.filter(p =>
|
||||
!prevPropPresentations.some(presentation => p.id === presentation.id
|
||||
|| p.temporaryPresentationId === presentation.temporaryPresentationId));
|
||||
const propsDiffs = propPresentations.filter(
|
||||
(p) => !prevPropPresentations.some(
|
||||
(presentation) => p.id === presentation.id
|
||||
|| p.temporaryPresentationId === presentation.temporaryPresentationId,
|
||||
),
|
||||
);
|
||||
|
||||
propsDiffs.forEach(p => {
|
||||
const index = presState.findIndex(pres => {
|
||||
return pres.temporaryPresentationId === p.temporaryPresentationId || pres.id === p.id;
|
||||
});
|
||||
propsDiffs.forEach((p) => {
|
||||
const index = presState.findIndex(
|
||||
(pres) => pres.temporaryPresentationId === p.temporaryPresentationId || pres.id === p.id,
|
||||
);
|
||||
if (index === -1) {
|
||||
presState.push(p);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
const presStateFiltered = presState.filter((presentation) => {
|
||||
const currentPropPres = propPresentations.find((pres) => pres.id === presentation.id);
|
||||
const prevPropPres = prevPropPresentations.find((pres) => pres.id === presentation.id);
|
||||
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);
|
||||
|
||||
if (hasConversionError || (!finishedConversion && hasTemporaryId)) return true;
|
||||
@ -380,31 +399,32 @@ class PresentationUploader extends Component {
|
||||
shouldUpdateState = true;
|
||||
}
|
||||
|
||||
const modPresentation = presentation;
|
||||
if (currentPropPres.isCurrent !== prevPropPres?.isCurrent) {
|
||||
presentation.isCurrent = currentPropPres.isCurrent;
|
||||
modPresentation.isCurrent = currentPropPres.isCurrent;
|
||||
}
|
||||
|
||||
presentation.conversion = currentPropPres.conversion;
|
||||
presentation.isRemovable = currentPropPres.isRemovable;
|
||||
modPresentation.conversion = currentPropPres.conversion;
|
||||
modPresentation.isRemovable = currentPropPres.isRemovable;
|
||||
|
||||
return true;
|
||||
}).filter(presentation => {
|
||||
}).filter((presentation) => {
|
||||
const duplicated = presentations.find(
|
||||
(pres) => pres.filename === presentation.filename
|
||||
&& pres.id !== presentation.id
|
||||
&& pres.id !== presentation.id,
|
||||
);
|
||||
if (duplicated
|
||||
&& duplicated.id.startsWith(presentation.filename)
|
||||
&& !presentation.id.startsWith(presentation.filename)
|
||||
&& presentation?.conversion?.done === duplicated?.conversion?.done) {
|
||||
return false; // Prioritizing propPresentations (the one with id from back-end)
|
||||
return false; // Prioritizing propPresentations (the one with id from back-end)
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (shouldUpdateState) {
|
||||
this.setState({
|
||||
presentations: _.uniqBy(presStateFiltered, 'id')
|
||||
presentations: _.uniqBy(presStateFiltered, 'id'),
|
||||
});
|
||||
}
|
||||
|
||||
@ -415,28 +435,25 @@ class PresentationUploader extends Component {
|
||||
// Updates presentation list when chat modal opens to avoid missing presentations
|
||||
if (isOpen && !prevProps.isOpen) {
|
||||
registerTitleView(intl.formatMessage(intlMessages.uploadViewTitle));
|
||||
const focusableElements =
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
||||
const focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
||||
const modal = document.getElementById('upload-modal');
|
||||
const firstFocusableElement = modal?.querySelectorAll(focusableElements)[0];
|
||||
const focusableContent = modal?.querySelectorAll(focusableElements);
|
||||
const lastFocusableElement = focusableContent[focusableContent.length - 1];
|
||||
|
||||
|
||||
firstFocusableElement.focus();
|
||||
|
||||
modal.addEventListener('keydown', function(e) {
|
||||
let tab = e.key === 'Tab' || e.keyCode === TAB;
|
||||
|
||||
modal.addEventListener('keydown', (e) => {
|
||||
const tab = e.key === 'Tab' || e.keyCode === TAB;
|
||||
if (!tab) return;
|
||||
if (e.shiftKey) {
|
||||
if (document.activeElement === firstFocusableElement) {
|
||||
lastFocusableElement.focus();
|
||||
e.preventDefault();
|
||||
}
|
||||
} else {
|
||||
if (document.activeElement === lastFocusableElement) {
|
||||
firstFocusableElement.focus();
|
||||
e.preventDefault();
|
||||
}
|
||||
} else if (document.activeElement === lastFocusableElement) {
|
||||
firstFocusableElement.focus();
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -448,7 +465,7 @@ class PresentationUploader extends Component {
|
||||
|
||||
if (this.exportToastId) {
|
||||
if (!prevProps.isOpen && isOpen) {
|
||||
this.handleDismissToast(this.exportToastId);
|
||||
handleDismissToast(this.exportToastId);
|
||||
}
|
||||
|
||||
toast.update(this.exportToastId, {
|
||||
@ -458,18 +475,14 @@ class PresentationUploader extends Component {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
let id = Session.get("presentationUploaderToastId");
|
||||
const id = Session.get('presentationUploaderToastId');
|
||||
if (id) {
|
||||
toast.dismiss(id);
|
||||
Session.set("presentationUploaderToastId", null);
|
||||
Session.set('presentationUploaderToastId', null);
|
||||
}
|
||||
Session.set('showUploadPresentationView', false);
|
||||
}
|
||||
|
||||
handleDismissToast(id) {
|
||||
return toast.dismiss(id);
|
||||
}
|
||||
|
||||
handleRemove(item, withErr = false) {
|
||||
if (withErr) {
|
||||
const { presentations } = this.props;
|
||||
@ -541,11 +554,6 @@ class PresentationUploader extends Component {
|
||||
this.setState({ presentations: presentationsUpdated });
|
||||
}
|
||||
|
||||
deepMergeUpdateFileKey(id, key, value) {
|
||||
const applyValue = (toUpdate) => update(toUpdate, { $merge: value });
|
||||
this.updateFileKey(id, key, applyValue, '$apply');
|
||||
}
|
||||
|
||||
handleConfirm() {
|
||||
const {
|
||||
handleSave,
|
||||
@ -556,12 +564,20 @@ class PresentationUploader extends Component {
|
||||
const { disableActions, presentations } = this.state;
|
||||
const presentationsToSave = presentations;
|
||||
|
||||
if (!isPresentationEnabled()) {
|
||||
this.setState(
|
||||
{ presentations: [] },
|
||||
Session.set('showUploadPresentationView', false),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
this.setState({ disableActions: true });
|
||||
|
||||
presentations.forEach(item => {
|
||||
presentations.forEach((item) => {
|
||||
if (item.upload.done) {
|
||||
const didDownloadableStateChange = propPresentations.some(
|
||||
(p) => p.id === item.id && p.isDownloadable !== item.isDownloadable
|
||||
(p) => p.id === item.id && p.isDownloadable !== item.isDownloadable,
|
||||
);
|
||||
if (didDownloadableStateChange) {
|
||||
dispatchTogglePresentationDownloadable(item, item.isDownloadable);
|
||||
@ -577,7 +593,6 @@ class PresentationUploader extends Component {
|
||||
if (!hasError) {
|
||||
this.setState({
|
||||
disableActions: false,
|
||||
toUploadCount: 0,
|
||||
});
|
||||
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) {
|
||||
const {
|
||||
exportPresentationToChat,
|
||||
@ -640,9 +647,11 @@ class PresentationUploader extends Component {
|
||||
notify(intl.formatMessage(intlMessages.linkAvailable, { 0: item.filename }), 'success');
|
||||
}
|
||||
|
||||
if ([EXPORT_STATUSES.RUNNING,
|
||||
if ([
|
||||
EXPORT_STATUSES.RUNNING,
|
||||
EXPORT_STATUSES.COLLECTING,
|
||||
EXPORT_STATUSES.PROCESSING].includes(exportation.status)) {
|
||||
EXPORT_STATUSES.PROCESSING,
|
||||
].includes(exportation.status)) {
|
||||
this.setState((prevState) => {
|
||||
prevState.presExporting.add(item.id);
|
||||
return {
|
||||
@ -664,8 +673,8 @@ class PresentationUploader extends Component {
|
||||
const presToShow = this.getPresentationsToShow();
|
||||
const isAnyRunning = presToShow.some(
|
||||
(p) => p.exportation.status === EXPORT_STATUSES.RUNNING
|
||||
|| p.exportation.status === EXPORT_STATUSES.COLLECTING
|
||||
|| p.exportation.status === EXPORT_STATUSES.PROCESSING,
|
||||
|| p.exportation.status === EXPORT_STATUSES.COLLECTING
|
||||
|| p.exportation.status === EXPORT_STATUSES.PROCESSING,
|
||||
);
|
||||
if (!isAnyRunning) {
|
||||
this.setState({ presExporting: new Set() });
|
||||
@ -682,6 +691,19 @@ class PresentationUploader extends Component {
|
||||
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') {
|
||||
this.setState(({ presentations }) => {
|
||||
const fileIndex = presentations.findIndex((f) => f.id === id);
|
||||
@ -788,12 +810,12 @@ class PresentationUploader extends Component {
|
||||
const shouldDismiss = isAllExported && this.exportToastId;
|
||||
|
||||
if (shouldDismiss) {
|
||||
this.handleDismissToast(this.exportToastId);
|
||||
handleDismissToast(this.exportToastId);
|
||||
|
||||
if (presExporting.size) {
|
||||
this.setState({ presExporting: new Set() });
|
||||
}
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
const presToShowSorted = [
|
||||
@ -830,8 +852,8 @@ class PresentationUploader extends Component {
|
||||
renderToastExportItem(item) {
|
||||
const { status } = item.exportation;
|
||||
const loading = (status === EXPORT_STATUSES.RUNNING
|
||||
|| status === EXPORT_STATUSES.COLLECTING
|
||||
|| status === EXPORT_STATUSES.PROCESSING);
|
||||
|| status === EXPORT_STATUSES.COLLECTING
|
||||
|| status === EXPORT_STATUSES.PROCESSING);
|
||||
const done = status === EXPORT_STATUSES.EXPORTED;
|
||||
let icon;
|
||||
|
||||
@ -908,24 +930,29 @@ class PresentationUploader extends Component {
|
||||
renderDownloadableWithAnnotationsHint() {
|
||||
const {
|
||||
intl,
|
||||
allowDownloadable
|
||||
allowDownloadable,
|
||||
} = this.props;
|
||||
|
||||
return allowDownloadable ? (
|
||||
<Styled.ExportHint>
|
||||
{intl.formatMessage(intlMessages.exportHint)}
|
||||
</Styled.ExportHint>)
|
||||
<Styled.ExportHint>
|
||||
{intl.formatMessage(intlMessages.exportHint)}
|
||||
</Styled.ExportHint>
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
renderPresentationItem(item) {
|
||||
const { disableActions } = this.state;
|
||||
const {
|
||||
intl,
|
||||
selectedToBeNextCurrent,
|
||||
allowDownloadable
|
||||
allowDownloadable,
|
||||
renderPresentationItemStatus,
|
||||
} = 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 isConverting = !item.conversion.done && item.upload.done;
|
||||
const hasError = item.conversion.error || item.upload.error;
|
||||
@ -987,7 +1014,7 @@ class PresentationUploader extends Component {
|
||||
: null
|
||||
}
|
||||
<Styled.TableItemStatus colSpan={hasError ? 2 : 0}>
|
||||
{this.props.renderPresentationItemStatus(item, intl)}
|
||||
{renderPresentationItemStatus(item, intl)}
|
||||
</Styled.TableItemStatus>
|
||||
{hasError ? null : (
|
||||
<Styled.TableItemActions notDownloadable={!allowDownloadable}>
|
||||
@ -1015,8 +1042,7 @@ class PresentationUploader extends Component {
|
||||
onClick={() => this.handleRemove(item)}
|
||||
animations={animations}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
) : null}
|
||||
</Styled.TableItemActions>
|
||||
)}
|
||||
</Styled.PresentationItem>
|
||||
@ -1051,10 +1077,10 @@ class PresentationUploader extends Component {
|
||||
// Error handling is being done in the onDrop prop.
|
||||
<Styled.UploaderDropzone
|
||||
multiple
|
||||
activeClassName={"dropzoneActive"}
|
||||
activeClassName="dropzoneActive"
|
||||
accept={fileValidMimeTypes.map((fileValid) => fileValid.extension)}
|
||||
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.DropzoneMessage>
|
||||
@ -1071,7 +1097,9 @@ class PresentationUploader extends Component {
|
||||
renderExternalUpload() {
|
||||
const { externalUploadData, intl } = this.props;
|
||||
|
||||
const { presentationUploadExternalDescription, presentationUploadExternalUrl } = externalUploadData;
|
||||
const {
|
||||
presentationUploadExternalDescription, presentationUploadExternalUrl,
|
||||
} = externalUploadData;
|
||||
|
||||
if (!presentationUploadExternalDescription || !presentationUploadExternalUrl) return null;
|
||||
|
||||
@ -1091,7 +1119,7 @@ class PresentationUploader extends Component {
|
||||
aria-describedby={intl.formatMessage(intlMessages.externalUploadLabel)}
|
||||
/>
|
||||
</Styled.ExternalUpload>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
renderPicDropzone() {
|
||||
@ -1121,7 +1149,7 @@ class PresentationUploader extends Component {
|
||||
accept="image/*"
|
||||
disablepreview="true"
|
||||
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.DropzoneMessage>
|
||||
@ -1151,45 +1179,49 @@ class PresentationUploader extends Component {
|
||||
if (item.id.indexOf(item.filename) !== -1 && item.upload.progress === 0) hasNewUpload = true;
|
||||
});
|
||||
|
||||
return (<>
|
||||
<PresentationUploaderToast intl={intl} />
|
||||
{isOpen ? (
|
||||
<Styled.UploaderModal id="upload-modal">
|
||||
<Styled.ModalInner>
|
||||
<Styled.ModalHeader>
|
||||
<Styled.Title>{intl.formatMessage(intlMessages.title)}</Styled.Title>
|
||||
<Styled.ActionWrapper>
|
||||
<Styled.DismissButton
|
||||
color="secondary"
|
||||
onClick={this.handleDismiss}
|
||||
label={intl.formatMessage(intlMessages.dismissLabel)}
|
||||
aria-describedby={intl.formatMessage(intlMessages.dismissDesc)}
|
||||
/>
|
||||
<Styled.ConfirmButton
|
||||
data-test="confirmManagePresentation"
|
||||
color="primary"
|
||||
onClick={() => this.handleConfirm()}
|
||||
disabled={disableActions}
|
||||
label={hasNewUpload
|
||||
? intl.formatMessage(intlMessages.uploadLabel)
|
||||
: intl.formatMessage(intlMessages.confirmLabel)}
|
||||
/>
|
||||
</Styled.ActionWrapper>
|
||||
</Styled.ModalHeader>
|
||||
return (
|
||||
<>
|
||||
<PresentationUploaderToast intl={intl} />
|
||||
{isOpen
|
||||
? (
|
||||
<Styled.UploaderModal id="upload-modal">
|
||||
<Styled.ModalInner>
|
||||
<Styled.ModalHeader>
|
||||
<Styled.Title>{intl.formatMessage(intlMessages.title)}</Styled.Title>
|
||||
<Styled.ActionWrapper>
|
||||
<Styled.DismissButton
|
||||
color="secondary"
|
||||
onClick={this.handleDismiss}
|
||||
label={intl.formatMessage(intlMessages.dismissLabel)}
|
||||
aria-describedby={intl.formatMessage(intlMessages.dismissDesc)}
|
||||
/>
|
||||
<Styled.ConfirmButton
|
||||
data-test="confirmManagePresentation"
|
||||
color="primary"
|
||||
onClick={() => this.handleConfirm()}
|
||||
disabled={disableActions}
|
||||
label={hasNewUpload
|
||||
? intl.formatMessage(intlMessages.uploadLabel)
|
||||
: intl.formatMessage(intlMessages.confirmLabel)}
|
||||
/>
|
||||
</Styled.ActionWrapper>
|
||||
</Styled.ModalHeader>
|
||||
|
||||
<Styled.ModalHint>
|
||||
{`${intl.formatMessage(intlMessages.message)}`}
|
||||
{fileUploadConstraintsHint ? this.renderExtraHint() : null}
|
||||
</Styled.ModalHint>
|
||||
{this.renderPresentationList()}
|
||||
{this.renderDownloadableWithAnnotationsHint()}
|
||||
{isMobile ? this.renderPicDropzone() : null}
|
||||
{this.renderDropzone()}
|
||||
{this.renderExternalUpload()}
|
||||
</Styled.ModalInner>
|
||||
</Styled.UploaderModal>
|
||||
) : null
|
||||
}</>)
|
||||
<Styled.ModalHint>
|
||||
{`${intl.formatMessage(intlMessages.message)}`}
|
||||
{fileUploadConstraintsHint ? this.renderExtraHint() : null}
|
||||
</Styled.ModalHint>
|
||||
{this.renderPresentationList()}
|
||||
{this.renderDownloadableWithAnnotationsHint()}
|
||||
{isMobile ? this.renderPicDropzone() : null}
|
||||
{this.renderDropzone()}
|
||||
{this.renderExternalUpload()}
|
||||
</Styled.ModalInner>
|
||||
</Styled.UploaderModal>
|
||||
)
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,7 @@ import PresUploaderToast from '/imports/ui/components/presentation/presentation-
|
||||
import PresentationUploader from './component';
|
||||
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
|
||||
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;
|
||||
|
||||
@ -33,6 +33,7 @@ export default withTracker(() => {
|
||||
dispatchTogglePresentationDownloadable,
|
||||
exportPresentationToChat,
|
||||
} = Service;
|
||||
const isOpen = isPresentationEnabled() && (Session.get('showUploadPresentationView') || false);
|
||||
|
||||
return {
|
||||
presentations: currentPresentations,
|
||||
@ -49,7 +50,7 @@ export default withTracker(() => {
|
||||
dispatchEnableDownloadable,
|
||||
dispatchTogglePresentationDownloadable,
|
||||
exportPresentationToChat,
|
||||
isOpen: Session.get('showUploadPresentationView') || false,
|
||||
isOpen,
|
||||
selectedToBeNextCurrent: Session.get('selectedToBeNextCurrent') || null,
|
||||
externalUploadData: Service.getExternalUploadData(),
|
||||
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 Auth from '/imports/ui/services/auth';
|
||||
import Poll from '/imports/api/polls/';
|
||||
@ -8,8 +8,9 @@ import logger from '/imports/startup/client/logger';
|
||||
import _ from 'lodash';
|
||||
import update from 'immutability-helper';
|
||||
import { Random } from 'meteor/random';
|
||||
import { UploadingPresentations } from '/imports/api/presentations';
|
||||
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 TOKEN_TIMEOUT = 5000;
|
||||
@ -22,11 +23,11 @@ const futch = (url, opts = {}, onProgress) => new Promise((res, rej) => {
|
||||
xhr.open(opts.method || 'get', url);
|
||||
|
||||
Object.keys(opts.headers || {})
|
||||
.forEach(k => xhr.setRequestHeader(k, opts.headers[k]));
|
||||
.forEach((k) => xhr.setRequestHeader(k, opts.headers[k]));
|
||||
|
||||
xhr.onload = (e) => {
|
||||
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);
|
||||
@ -58,7 +59,6 @@ const getPresentations = () => Presentations
|
||||
|
||||
const uploadTimestamp = id.split('-').pop();
|
||||
|
||||
|
||||
return {
|
||||
id,
|
||||
filename: name,
|
||||
@ -85,7 +85,7 @@ const observePresentationConversion = (
|
||||
) => new Promise((resolve) => {
|
||||
// The token is placed as an id before the original one is generated
|
||||
// in the back-end;
|
||||
const tokenId = PresentationUploadToken.findOne({temporaryPresentationId})?.authzToken;
|
||||
const tokenId = PresentationUploadToken.findOne({ temporaryPresentationId })?.authzToken;
|
||||
|
||||
const conversionTimeout = setTimeout(() => {
|
||||
onConversion({
|
||||
@ -105,12 +105,13 @@ const observePresentationConversion = (
|
||||
|
||||
query.observe({
|
||||
added: (doc) => {
|
||||
|
||||
if (doc.temporaryPresentationId !== temporaryPresentationId && doc.id !== tokenId) return;
|
||||
|
||||
if (doc.conversion.status === 'FILE_TOO_LARGE' || doc.conversion.status === 'UNSUPPORTED_DOCUMENT'
|
||||
|| doc.conversion.status === 'CONVERSION_TIMEOUT' || doc.conversion.status === "IVALID_MIME_TYPE") {
|
||||
Presentations.update({id: tokenId}, {$set: {temporaryPresentationId, renderedInToast: false}})
|
||||
if (doc.conversion.status === 'FILE_TOO_LARGE' || doc.conversion.status === 'UNSUPPORTED_DOCUMENT'
|
||||
|| doc.conversion.status === 'CONVERSION_TIMEOUT' || doc.conversion.status === 'IVALID_MIME_TYPE') {
|
||||
Presentations.update(
|
||||
{ id: tokenId }, { $set: { temporaryPresentationId, renderedInToast: false } },
|
||||
);
|
||||
onConversion(doc.conversion);
|
||||
c.stop();
|
||||
clearTimeout(conversionTimeout);
|
||||
@ -146,7 +147,7 @@ const requestPresentationUploadToken = (
|
||||
let computation = null;
|
||||
const timeout = setTimeout(() => {
|
||||
computation.stop();
|
||||
reject({ code: 408, message: 'requestPresentationUploadToken timeout' });
|
||||
reject(new Error({ code: 408, message: 'requestPresentationUploadToken timeout' }));
|
||||
}, TOKEN_TIMEOUT);
|
||||
|
||||
Tracker.autorun((c) => {
|
||||
@ -169,7 +170,7 @@ const requestPresentationUploadToken = (
|
||||
}
|
||||
|
||||
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,
|
||||
onConversion,
|
||||
) => {
|
||||
const temporaryPresentationId = _.uniqueId(Random.id(20))
|
||||
const temporaryPresentationId = _.uniqueId(Random.id(20));
|
||||
|
||||
const data = new FormData();
|
||||
data.append('fileUpload', file);
|
||||
@ -205,43 +206,48 @@ const uploadAndConvertPresentation = (
|
||||
// If the presentation is from sharedNotes I don't want to
|
||||
// insert another one, I just need to update it.
|
||||
UploadingPresentations.upsert({
|
||||
filename: file.name,
|
||||
lastModifiedUploader: false,
|
||||
}, {
|
||||
$set: {
|
||||
temporaryPresentationId,
|
||||
progress: 0,
|
||||
filename: file.name,
|
||||
lastModifiedUploader: false,
|
||||
}, {
|
||||
$set: {
|
||||
temporaryPresentationId,
|
||||
progress: 0,
|
||||
filename: file.name,
|
||||
lastModifiedUploader: true,
|
||||
upload: {
|
||||
done: false,
|
||||
error: false
|
||||
},
|
||||
uploadTimestamp: new Date()
|
||||
}
|
||||
})
|
||||
lastModifiedUploader: true,
|
||||
upload: {
|
||||
done: false,
|
||||
error: false,
|
||||
},
|
||||
uploadTimestamp: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return requestPresentationUploadToken(temporaryPresentationId, podId, meetingId, file.name)
|
||||
.then((token) => {
|
||||
makeCall('setUsedToken', token);
|
||||
UploadingPresentations.upsert({
|
||||
temporaryPresentationId
|
||||
}, {
|
||||
$set: {
|
||||
id: token,
|
||||
}
|
||||
})
|
||||
temporaryPresentationId,
|
||||
}, {
|
||||
$set: {
|
||||
id: token,
|
||||
},
|
||||
});
|
||||
return futch(endpoint.replace('upload', `${token}/upload`), opts, (e) => {
|
||||
onProgress(e);
|
||||
let pr = (e.loaded / e.total) * 100;
|
||||
if (pr != 100) UploadingPresentations.upsert({ temporaryPresentationId }, {$set: {progress: pr}});
|
||||
else UploadingPresentations.upsert({ temporaryPresentationId }, {$set: {
|
||||
progress: pr,
|
||||
upload: {
|
||||
done: true,
|
||||
error: false,
|
||||
}
|
||||
}});
|
||||
const pr = (e.loaded / e.total) * 100;
|
||||
if (pr !== 100) {
|
||||
UploadingPresentations.upsert({ temporaryPresentationId }, { $set: { progress: pr } });
|
||||
} else {
|
||||
UploadingPresentations.upsert({ temporaryPresentationId }, {
|
||||
$set: {
|
||||
progress: pr,
|
||||
upload: {
|
||||
done: true,
|
||||
error: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(() => observePresentationConversion(meetingId, temporaryPresentationId, onConversion))
|
||||
@ -324,27 +330,33 @@ const persistPresentationChanges = (oldState, newState, uploadEndpoint, 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();
|
||||
if (!isFromPresentationUploaderInterface) {
|
||||
|
||||
if (presentations.length === 0) {
|
||||
presentations = [...currentPresentations];
|
||||
}
|
||||
presentations = presentations.map(p => update(p, {
|
||||
presentations = presentations.map((p) => update(p, {
|
||||
isCurrent: {
|
||||
$set: false
|
||||
}
|
||||
$set: false,
|
||||
},
|
||||
}));
|
||||
newPres.isCurrent = true;
|
||||
presentations.push(newPres);
|
||||
}
|
||||
return persistPresentationChanges(
|
||||
currentPresentations,
|
||||
presentations,
|
||||
PRESENTATION_CONFIG.uploadEndpoint,
|
||||
'DEFAULT_PRESENTATION_POD'
|
||||
)}
|
||||
currentPresentations,
|
||||
presentations,
|
||||
PRESENTATION_CONFIG.uploadEndpoint,
|
||||
'DEFAULT_PRESENTATION_POD',
|
||||
);
|
||||
};
|
||||
|
||||
const getExternalUploadData = () => {
|
||||
const { meetingProp } = Meetings.findOne(
|
||||
@ -352,7 +364,7 @@ const getExternalUploadData = () => {
|
||||
{
|
||||
fields: {
|
||||
'meetingProp.presentationUploadExternalDescription': 1,
|
||||
'meetingProp.presentationUploadExternalUrl': 1
|
||||
'meetingProp.presentationUploadExternalUrl': 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
@ -362,7 +374,7 @@ const getExternalUploadData = () => {
|
||||
return {
|
||||
presentationUploadExternalDescription,
|
||||
presentationUploadExternalUrl,
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const exportPresentationToChat = (presentationId, observer) => {
|
||||
@ -372,11 +384,12 @@ const exportPresentationToChat = (presentationId, observer) => {
|
||||
const cursor = Presentations.find({ id: presentationId });
|
||||
|
||||
const checkStatus = (exportation) => {
|
||||
const shouldStop = lastStatus.status === 'PROCESSING' && exportation.status === 'EXPORTED';
|
||||
const shouldStop = lastStatus.status === 'RUNNING' && exportation.status === 'EXPORTED';
|
||||
|
||||
if (shouldStop) {
|
||||
observer(exportation, true);
|
||||
return c.stop();
|
||||
c.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
observer(exportation, false);
|
||||
@ -396,71 +409,72 @@ const exportPresentationToChat = (presentationId, observer) => {
|
||||
makeCall('exportPresentationToChat', presentationId);
|
||||
};
|
||||
|
||||
function handleFiledrop(files, files2, that) {
|
||||
if(that){
|
||||
const { fileValidMimeTypes, intl } = that.props;
|
||||
const { toUploadCount } = that.state;
|
||||
const validMimes = fileValidMimeTypes.map((fileValid) => fileValid.mime);
|
||||
const validExtentions = fileValidMimeTypes.map((fileValid) => fileValid.extension);
|
||||
const [accepted, rejected] = _.partition(files
|
||||
.concat(files2), (f) => (
|
||||
validMimes.includes(f.type) || validExtentions.includes(`.${f.name.split('.').pop()}`)
|
||||
));
|
||||
function handleFiledrop(files, files2, that, intl, intlMessages) {
|
||||
if (that) {
|
||||
const { fileValidMimeTypes } = that.props;
|
||||
const { toUploadCount } = that.state;
|
||||
const validMimes = fileValidMimeTypes.map((fileValid) => fileValid.mime);
|
||||
const validExtentions = fileValidMimeTypes.map((fileValid) => fileValid.extension);
|
||||
const [accepted, rejected] = _.partition(
|
||||
files.concat(files2), (f) => (
|
||||
validMimes.includes(f.type) || validExtentions.includes(`.${f.name.split('.').pop()}`)
|
||||
),
|
||||
);
|
||||
|
||||
const presentationsToUpload = accepted.map((file) => {
|
||||
const id = _.uniqueId(file.name);
|
||||
const presentationsToUpload = accepted.map((file) => {
|
||||
const id = _.uniqueId(file.name);
|
||||
|
||||
return {
|
||||
file,
|
||||
isDownloadable: false, // by default new presentations are set not to be downloadable
|
||||
isRemovable: true,
|
||||
id,
|
||||
filename: file.name,
|
||||
isCurrent: false,
|
||||
conversion: { done: false, error: false },
|
||||
upload: { done: false, error: false, progress: 0 },
|
||||
exportation: { error: false },
|
||||
onProgress: (event) => {
|
||||
if (!event.lengthComputable) {
|
||||
that.deepMergeUpdateFileKey(id, 'upload', {
|
||||
progress: 100,
|
||||
done: true,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
file,
|
||||
isDownloadable: false, // by default new presentations are set not to be downloadable
|
||||
isRemovable: true,
|
||||
id,
|
||||
filename: file.name,
|
||||
isCurrent: false,
|
||||
conversion: { done: false, error: false },
|
||||
upload: { done: false, error: false, progress: 0 },
|
||||
exportation: { error: false },
|
||||
onProgress: (event) => {
|
||||
if (!event.lengthComputable) {
|
||||
that.deepMergeUpdateFileKey(id, 'upload', {
|
||||
progress: 100,
|
||||
done: true,
|
||||
progress: (event.loaded / event.total) * 100,
|
||||
done: event.loaded === event.total,
|
||||
});
|
||||
},
|
||||
onConversion: (conversion) => {
|
||||
that.deepMergeUpdateFileKey(id, 'conversion', conversion);
|
||||
},
|
||||
onUpload: (upload) => {
|
||||
that.deepMergeUpdateFileKey(id, 'upload', upload);
|
||||
},
|
||||
onDone: (newId) => {
|
||||
that.updateFileKey(id, 'id', newId);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
that.setState(({ presentations }) => ({
|
||||
presentations: presentations.concat(presentationsToUpload),
|
||||
toUploadCount: (toUploadCount + presentationsToUpload.length),
|
||||
}), () => {
|
||||
// after the state is set (files have been dropped),
|
||||
// make the first of the new presentations current
|
||||
if (presentationsToUpload && presentationsToUpload.length) {
|
||||
that.handleCurrentChange(presentationsToUpload[0].id);
|
||||
}
|
||||
});
|
||||
|
||||
that.deepMergeUpdateFileKey(id, 'upload', {
|
||||
progress: (event.loaded / event.total) * 100,
|
||||
done: event.loaded === event.total,
|
||||
});
|
||||
},
|
||||
onConversion: (conversion) => {
|
||||
that.deepMergeUpdateFileKey(id, 'conversion', conversion);
|
||||
},
|
||||
onUpload: (upload) => {
|
||||
that.deepMergeUpdateFileKey(id, 'upload', upload);
|
||||
},
|
||||
onDone: (newId) => {
|
||||
that.updateFileKey(id, 'id', newId);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
that.setState(({ presentations }) => ({
|
||||
presentations: presentations.concat(presentationsToUpload),
|
||||
toUploadCount: (toUploadCount + presentationsToUpload.length),
|
||||
}), () => {
|
||||
// after the state is set (files have been dropped),
|
||||
// make the first of the new presentations current
|
||||
if (presentationsToUpload && presentationsToUpload.length) {
|
||||
that.handleCurrentChange(presentationsToUpload[0].id);
|
||||
if (rejected.length > 0) {
|
||||
notify(intl.formatMessage(intlMessages.rejectedError), 'error');
|
||||
}
|
||||
});
|
||||
|
||||
if (rejected.length > 0) {
|
||||
notify(intl.formatMessage(intlMessages.rejectedError), 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,20 +1,19 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
const injectWbResizeEvent = WrappedComponent =>
|
||||
class Resize extends Component {
|
||||
componentDidMount() {
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}
|
||||
const injectWbResizeEvent = (WrappedComponent) => class Resize extends Component {
|
||||
componentDidMount() {
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}
|
||||
componentWillUnmount() {
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<WrappedComponent {...this.props} />
|
||||
);
|
||||
}
|
||||
};
|
||||
render() {
|
||||
return (
|
||||
<WrappedComponent {...this.props} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default injectWbResizeEvent;
|
||||
|
@ -3,28 +3,30 @@ import PropTypes from 'prop-types';
|
||||
|
||||
const Slide = ({ imageUri, svgWidth, svgHeight }) => (
|
||||
<g>
|
||||
{imageUri ?
|
||||
{imageUri
|
||||
// some pdfs lose a white background color during the conversion to svg
|
||||
// their background color is transparent
|
||||
// that's why we have a white rectangle covering the whole slide area by default
|
||||
<g>
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width={svgWidth}
|
||||
height={svgHeight}
|
||||
fill="white"
|
||||
/>
|
||||
<image
|
||||
x="0"
|
||||
y="0"
|
||||
width={svgWidth}
|
||||
height={svgHeight}
|
||||
xlinkHref={imageUri}
|
||||
strokeWidth="0.8"
|
||||
style={{ WebkitTapHighlightColor: 'transparent' }}
|
||||
/>
|
||||
</g>
|
||||
? (
|
||||
<g>
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width={svgWidth}
|
||||
height={svgHeight}
|
||||
fill="white"
|
||||
/>
|
||||
<image
|
||||
x="0"
|
||||
y="0"
|
||||
width={svgWidth}
|
||||
height={svgHeight}
|
||||
xlinkHref={imageUri}
|
||||
strokeWidth="0.8"
|
||||
style={{ WebkitTapHighlightColor: 'transparent' }}
|
||||
/>
|
||||
</g>
|
||||
)
|
||||
: null}
|
||||
</g>
|
||||
);
|
||||
|
@ -51,7 +51,7 @@ export default withTracker(() => {
|
||||
return {
|
||||
isGloballyBroadcasting: isGloballyBroadcasting(),
|
||||
toggleSwapLayout: MediaService.toggleSwapLayout,
|
||||
hidePresentation: getFromUserSettings('bbb_hide_presentation', LAYOUT_CONFIG.hidePresentation),
|
||||
hidePresentationOnJoin: getFromUserSettings('bbb_hide_presentation_on_join', LAYOUT_CONFIG.hidePresentationOnJoin),
|
||||
enableVolumeControl: shouldEnableVolumeControl(),
|
||||
};
|
||||
})(ScreenshareContainer);
|
||||
|
@ -5,7 +5,6 @@ import logger from '/imports/startup/client/logger';
|
||||
import GroupChat from '/imports/api/group-chat';
|
||||
import Annotations from '/imports/api/annotations';
|
||||
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 {
|
||||
localCollectionRegistry,
|
||||
@ -156,9 +155,8 @@ export default withTracker(() => {
|
||||
usersPersistentDataHandler = Meteor.subscribe('users-persistent-data');
|
||||
const annotationsHandler = Meteor.subscribe('annotations', {
|
||||
onReady: () => {
|
||||
const activeTextShapeId = AnnotationsTextService.activeTextShapeId();
|
||||
AnnotationsLocal.remove({ id: { $ne: `${activeTextShapeId}-fake` } });
|
||||
Annotations.find({ id: { $ne: activeTextShapeId } }, { reactive: false }).forEach((a) => {
|
||||
AnnotationsLocal.remove({});
|
||||
Annotations.find({}, { reactive: false }).forEach((a) => {
|
||||
try {
|
||||
AnnotationsLocal.insert(a);
|
||||
} catch (e) {
|
||||
|
@ -4,7 +4,7 @@ import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Icon from '/imports/ui/components/common/icon/component';
|
||||
import Styled from './styles';
|
||||
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({
|
||||
breakoutTitle: {
|
||||
@ -66,7 +66,7 @@ const BreakoutRoomItem = ({
|
||||
{intl.formatMessage(intlMessages.breakoutTitle)}
|
||||
</Styled.BreakoutTitle>
|
||||
<Styled.BreakoutDuration>
|
||||
<BreakoutRemainingTime
|
||||
<MeetingRemainingTime
|
||||
messageDuration={intlMessages.breakoutTimeRemaining}
|
||||
breakoutRoom={breakoutRoom}
|
||||
/>
|
||||
|
@ -321,6 +321,7 @@ class UserOptions extends PureComponent {
|
||||
key: this.learningDashboardId,
|
||||
onClick: () => { openLearningDashboardUrl(locale); },
|
||||
dividerTop: true,
|
||||
dataTest: 'learningDashboard'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -432,6 +432,7 @@ const VirtualBgSelector = ({
|
||||
aria-label={intl.formatMessage(intlMessages.virtualBackgroundSettingsLabel)}
|
||||
isVisualEffects={isVisualEffects}
|
||||
brightnessEnabled={ENABLE_CAMERA_BRIGHTNESS}
|
||||
data-test="virtualBackground"
|
||||
>
|
||||
{shouldEnableBackgroundUpload() && (
|
||||
<>
|
||||
@ -443,7 +444,6 @@ const VirtualBgSelector = ({
|
||||
|
||||
{Object.values(backgrounds)
|
||||
.sort((a, b) => b.lastActivityDate - a.lastActivityDate)
|
||||
.slice(0, isVisualEffects ? undefined : 3)
|
||||
.map((background, index) => {
|
||||
if (background.custom !== false) {
|
||||
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 MediaService from '/imports/ui/components/media/service';
|
||||
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 { UsersContext } from '../components-data/users-context/context';
|
||||
import {
|
||||
@ -56,8 +55,6 @@ const WebcamContainer = ({
|
||||
: null;
|
||||
};
|
||||
|
||||
let userWasInBreakout = false;
|
||||
|
||||
export default withModalMounter(withTracker((props) => {
|
||||
const { current_presentation: hasPresentation } = MediaService.getPresentationInfo();
|
||||
const data = {
|
||||
@ -65,31 +62,6 @@ export default withModalMounter(withTracker((props) => {
|
||||
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();
|
||||
data.usersVideo = usersVideo;
|
||||
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