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

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

View File

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

View File

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

2
.gitignore vendored
View File

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

View File

@ -68,7 +68,12 @@ trait PresentationUploadTokenReqMsgHdlr extends RightsManagementTrait {
log.info("handlePresentationUploadTokenReqMsg" + liveMeeting.props.meetingProp.intId +
" 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."

View File

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

View File

@ -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",

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}

View File

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

View File

@ -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" />
:&nbsp;
{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

View File

@ -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">

View File

@ -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
? (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -110,12 +110,25 @@ enableUFWRules() {
ufw allow "Nginx Full"
ufw allow 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

View File

@ -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

View File

@ -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) {

View File

@ -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({

View File

@ -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));

View File

@ -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);
}
}
}

View File

@ -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',

View File

@ -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}`);

View File

@ -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,

View File

@ -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;
}

View File

@ -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 {...{

View File

@ -14,7 +14,7 @@ import ExternalVideoService from '/imports/ui/components/external-video-player/s
import CaptionsService from '/imports/ui/components/captions/service';
import { 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 },

View File

@ -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' : ''}`}

View File

@ -5,6 +5,7 @@ import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
import BBBMenu from '/imports/ui/components/common/menu/component';
import 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',
);
}
}}
/>
);

View File

@ -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(),
};

View File

@ -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,
}}

View File

@ -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'),

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import { Session } from 'meteor/session';
import logger from '/imports/startup/client/logger';
import 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

View File

@ -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}

View File

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

View File

@ -215,7 +215,7 @@ class VideoPlayer extends Component {
componentWillUnmount() {
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,

View File

@ -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"

View File

@ -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;

View File

@ -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({

View File

@ -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();
}

View File

@ -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,

View File

@ -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');

View File

@ -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`

View File

@ -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({

View File

@ -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}

View File

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

View File

@ -4,9 +4,10 @@ import { defineMessages, injectIntl } from 'react-intl';
import injectNotify from '/imports/ui/components/common/toast/inject-notify/component';
import 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');

View File

@ -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 && (

View File

@ -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;

View File

@ -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,
};

View File

@ -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,
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import React from 'react';
import 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,
};

View File

@ -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,
};

View File

@ -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}
/>

View File

@ -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,
};

View File

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

View File

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

View File

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

View File

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

View File

@ -1,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,
};

View File

@ -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));

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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,

View File

@ -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}
</>
);
}
}

View File

@ -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,

View File

@ -1,4 +1,4 @@
import Presentations from '/imports/api/presentations';
import Presentations, { UploadingPresentations } from '/imports/api/presentations';
import PresentationUploadToken from '/imports/api/presentation-upload-token';
import 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');
}
}
}

View File

@ -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;

View File

@ -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>
);

View File

@ -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);

View File

@ -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) {

View File

@ -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}
/>

View File

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

View File

@ -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);

View File

@ -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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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