Merge remote-tracking branch 'upstream/develop' into develop

This commit is contained in:
Gustavo Trott 2021-11-29 11:11:33 -03:00
commit 56fedc8921
148 changed files with 2329 additions and 1061 deletions

View File

@ -6,14 +6,14 @@ BigBlueButton supports real-time sharing of audio, video, slides (with whiteboar
Presenters can record and playback content for later sharing with others.
We designed BigBlueButton for online learning (though it can be used for many [other applications](http://www.c4isrnet.com/story/military-tech/disa/2015/02/11/disa-to-save-12m-defense-collaboration-services/23238997/)). The educational use cases for BigBlueButton are
We designed BigBlueButton for online learning, (though it can be used for many [other applications](https://www.c4isrnet.com/it-networks/2015/02/11/disa-to-replace-dco-with-new-collaboration-services-tool/) as well). The educational use cases for BigBlueButton are
* Online tutoring (one-to-one)
* Flipped classrooms (recording content ahead of your session)
* Group collaboration (many-to-many)
* Online classes (one-to-many)
You can install on a Ubuntu 18.04 64-bit server. We provide [bbb-install.sh](https://github.com/bigbluebutton/bbb-install) to let you have a server up and running within 30 minutes (or your money back 😉).
You can install on a Ubuntu 18.04 64-bit server. We provide [bbb-install.sh](https://github.com/bigbluebutton/bbb-install) to let you have a server up and running within 30 minutes (or your money back 😉).
For full technical documentation BigBlueButton -- including architecture, features, API, and GreenLight (the default front-end) -- see [https://docs.bigbluebutton.org/](https://docs.bigbluebutton.org/).

View File

@ -12,7 +12,7 @@ We actively support BigBlueButton through the community forums and through secur
We have released 2.3 to the community and all our support efforts are now transitioned to 2.3. Also, BigBlueButton 2.2 is running on Ubuntu 16.04 which is now end of life.
As such, we highly recommend that all administrators deploy 2.3 going forward as it is built upon Ubuntu 18.04. You'll find [many improvements](/2.3/new.html) in this newer version.
As such, we highly recommend that all administrators deploy 2.3 going forward as it is built upon Ubuntu 18.04. You'll find [many improvements](https://docs.bigbluebutton.org/2.3/new.html) in this newer version.
## Reporting a Vulnerability

View File

@ -1,6 +1,7 @@
package org.bigbluebutton.endpoint.redis
import akka.actor.{Actor, ActorLogging, ActorSystem, Props}
import org.bigbluebutton.common2.domain.PresentationVO
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.common2.util.JsonUtil
import org.bigbluebutton.core.OutMessageGateway
@ -23,6 +24,7 @@ case class Meeting(
users: Map[String, User] = Map(),
polls: Map[String, Poll] = Map(),
screenshares: Vector[Screenshare] = Vector(),
presentationSlides: Vector[PresentationSlide] = Vector(),
createdOn: Long = System.currentTimeMillis(),
endedOn: Long = 0,
)
@ -72,6 +74,12 @@ case class Screenshare(
stoppedOn: Long = 0,
)
case class PresentationSlide(
presentationId: String,
pageNum: Long,
setOn: Long = System.currentTimeMillis(),
)
object LearningDashboardActor {
def props(
@ -92,6 +100,7 @@ class LearningDashboardActor(
private var meetings: Map[String, Meeting] = Map()
private var meetingsLastJsonHash : Map[String,String] = Map()
private var meetingPresentations : Map[String,Map[String,PresentationVO]] = Map()
system.scheduler.schedule(10.seconds, 10.seconds, self, SendPeriodicReport)
@ -108,6 +117,12 @@ class LearningDashboardActor(
// Chat
case m: GroupChatMessageBroadcastEvtMsg => handleGroupChatMessageBroadcastEvtMsg(m)
// Presentation
case m: PresentationConversionCompletedEvtMsg => handlePresentationConversionCompletedEvtMsg(m)
case m: SetCurrentPageEvtMsg => handleSetCurrentPageEvtMsg(m)
case m: RemovePresentationEvtMsg => handleRemovePresentationEvtMsg(m)
case m: SetCurrentPresentationEvtMsg => handleSetCurrentPresentationEvtMsg(m)
// User
case m: UserJoinedMeetingEvtMsg => handleUserJoinedMeetingEvtMsg(m)
case m: UserLeftMeetingEvtMsg => handleUserLeftMeetingEvtMsg(m)
@ -151,6 +166,77 @@ class LearningDashboardActor(
}
}
private def handlePresentationConversionCompletedEvtMsg(msg: PresentationConversionCompletedEvtMsg) {
for {
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
} yield {
val updatedPresentations = meetingPresentations.get(meeting.intId).getOrElse(Map()) + (msg.body.presentation.id -> msg.body.presentation)
meetingPresentations += (meeting.intId -> updatedPresentations)
if(msg.body.presentation.current == true) {
for {
page <- msg.body.presentation.pages.find(p => p.current == true)
} yield {
this.setPresentationSlide(meeting.intId, msg.body.presentation.id,page.num)
}
}
}
}
private def handleSetCurrentPageEvtMsg(msg: SetCurrentPageEvtMsg) {
for {
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
presentations <- meetingPresentations.get(meeting.intId)
presentation <- presentations.get(msg.body.presentationId)
page <- presentation.pages.find(p => p.id == msg.body.pageId)
} yield {
this.setPresentationSlide(meeting.intId, msg.body.presentationId,page.num)
}
}
private def handleRemovePresentationEvtMsg(msg: RemovePresentationEvtMsg) {
for {
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
} yield {
if(meeting.presentationSlides.last.presentationId == msg.body.presentationId) {
this.setPresentationSlide(meeting.intId, "",0)
}
}
}
private def handleSetCurrentPresentationEvtMsg(msg: SetCurrentPresentationEvtMsg) {
for {
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
} yield {
val presPreviousSlides: Vector[PresentationSlide] = meeting.presentationSlides.filter(p => p.presentationId == msg.body.presentationId);
if(presPreviousSlides.length > 0) {
//Set last page showed for this presentation
this.setPresentationSlide(meeting.intId, msg.body.presentationId,presPreviousSlides.last.pageNum)
} else {
//If none page was showed yet, set the current page (page 1 by default)
for {
presentations <- meetingPresentations.get(meeting.intId)
presentation <- presentations.get(msg.body.presentationId)
page <- presentation.pages.find(s => s.current == true)
} yield {
this.setPresentationSlide(meeting.intId, msg.body.presentationId,page.num)
}
}
}
}
private def setPresentationSlide(meetingId: String, presentationId: String, pageNum: Long) {
for {
meeting <- meetings.values.find(m => m.intId == meetingId)
} yield {
if (meeting.presentationSlides.length == 0 ||
meeting.presentationSlides.last.presentationId != presentationId ||
meeting.presentationSlides.last.pageNum != pageNum) {
val updatedMeeting = meeting.copy(presentationSlides = meeting.presentationSlides :+ PresentationSlide(presentationId, pageNum))
meetings += (updatedMeeting.intId -> updatedMeeting)
}
}
}
private def handleUserJoinedMeetingEvtMsg(msg: UserJoinedMeetingEvtMsg): Unit = {
for {
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
@ -403,6 +489,7 @@ class LearningDashboardActor(
//Send report one last time
sendReport(updatedMeeting)
meetingPresentations = meetingPresentations.-(updatedMeeting.intId)
meetings = meetings.-(updatedMeeting.intId)
log.info(" removed for meeting {}.",updatedMeeting.intId)
}
@ -426,7 +513,7 @@ class LearningDashboardActor(
meetingsLastJsonHash += (meeting.intId -> activityJsonHash)
log.info("Activity Report sent for meeting {}",meeting.intId)
log.info("Learning Dashboard data sent for meeting {}",meeting.intId)
}
}

View File

@ -1,42 +1,3 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.col-text-left {
text-align: left;
}

View File

@ -1,12 +1,16 @@
import React from 'react';
import './App.css';
import './bbb-icons.css';
import { FormattedMessage, FormattedDate, injectIntl } from 'react-intl';
import {
FormattedMessage, FormattedDate, injectIntl, FormattedTime,
} from 'react-intl';
import { emojiConfigs } from './services/EmojiService';
import Card from './components/Card';
import UsersTable from './components/UsersTable';
import StatusTable from './components/StatusTable';
import PollsTable from './components/PollsTable';
import ErrorMessage from './components/ErrorMessage';
import { makeUserCSVData, tsToHHmmss } from './services/UserService';
class App extends React.Component {
constructor(props) {
@ -19,6 +23,7 @@ class App extends React.Component {
learningDashboardAccessToken: '',
ldAccessTokenCopied: false,
sessionToken: '',
lastUpdated: null,
};
}
@ -29,6 +34,34 @@ class App extends React.Component {
}, 10000);
}
handleSaveSessionData(e) {
const { target: downloadButton } = e;
const { intl } = this.props;
const { activitiesJson } = this.state;
const {
name: meetingName, createdOn, users, polls,
} = activitiesJson;
const link = document.createElement('a');
const data = makeUserCSVData(users, polls, intl);
const filename = `LearningDashboard_${meetingName}_${new Date(createdOn).toISOString().substr(0, 10)}.csv`.replace(/ /g, '-');
downloadButton.setAttribute('disabled', 'true');
downloadButton.style.cursor = 'not-allowed';
link.setAttribute('href', `data:application/octet-stream,${encodeURIComponent(data)}`);
link.setAttribute('download', filename);
link.style.display = 'none';
document.body.appendChild(link);
link.click();
downloadButton.innerHTML = intl.formatMessage({ id: 'app.learningDashboard.sessionDataDownloadedLabel', defaultMessage: 'Downloaded!' });
setTimeout(() => {
downloadButton.innerHTML = intl.formatMessage({ id: 'app.learningDashboard.downloadSessionDataLabel', defaultMessage: 'Download Session Data' });
downloadButton.removeAttribute('disabled');
downloadButton.style.cursor = 'pointer';
downloadButton.focus();
}, 3000);
document.body.removeChild(link);
}
setDashboardParams() {
let learningDashboardAccessToken = '';
let meetingId = '';
@ -67,6 +100,39 @@ class App extends React.Component {
this.fetchActivitiesJson);
}
fetchMostUsedEmojis() {
const { activitiesJson } = this.state;
if (!activitiesJson) { return []; }
// Icon elements
const emojis = [...Object.keys(emojiConfigs)];
const icons = {};
emojis.forEach((emoji) => {
icons[emoji] = (<i className={`${emojiConfigs[emoji].icon} bbb-icon-card`} />);
});
// Count each emoji
const emojiCount = {};
emojis.forEach((emoji) => {
emojiCount[emoji] = 0;
});
const allEmojisUsed = Object
.values(activitiesJson.users || {})
.map((user) => user.emojis || [])
.flat(1);
allEmojisUsed.forEach((emoji) => {
emojiCount[emoji.name] += 1;
});
// Get the three most used
const mostUsedEmojis = Object
.entries(emojiCount)
.sort(([, countA], [, countB]) => countA - countB)
.reverse()
.slice(0, 3);
return mostUsedEmojis.map(([emoji]) => icons[emoji]);
}
fetchActivitiesJson() {
const { learningDashboardAccessToken, meetingId, sessionToken } = this.state;
@ -74,7 +140,7 @@ class App extends React.Component {
fetch(`${meetingId}/${learningDashboardAccessToken}/learning_dashboard_data.json`)
.then((response) => response.json())
.then((json) => {
this.setState({ activitiesJson: json, loading: false });
this.setState({ activitiesJson: json, loading: false, lastUpdated: Date.now() });
document.title = `Learning Dashboard - ${json.name}`;
}).catch(() => {
this.setState({ loading: false });
@ -86,7 +152,7 @@ class App extends React.Component {
.then((json) => {
if (json.response.returncode === 'SUCCESS') {
const jsonData = JSON.parse(json.response.data);
this.setState({ activitiesJson: jsonData, loading: false });
this.setState({ activitiesJson: jsonData, loading: false, lastUpdated: Date.now() });
document.title = `Learning Dashboard - ${jsonData.name}`;
} else {
// When meeting is ended the sessionToken stop working, check for new cookies
@ -119,25 +185,21 @@ class App extends React.Component {
render() {
const {
activitiesJson, tab, sessionToken, loading,
activitiesJson, tab, sessionToken, loading, lastUpdated,
learningDashboardAccessToken, ldAccessTokenCopied,
} = this.state;
const { intl } = this.props;
document.title = `${intl.formatMessage({ id: 'app.learningDashboard.dashboardTitle', defaultMessage: 'Learning Dashboard' })} - ${activitiesJson.name}`;
function totalOfRaiseHand() {
function totalOfEmojis() {
if (activitiesJson && activitiesJson.users) {
return Object.values(activitiesJson.users)
.reduce((prevVal, elem) => prevVal + elem.emojis.filter((emoji) => emoji.name === 'raiseHand').length, 0);
.reduce((prevVal, elem) => prevVal + elem.emojis.length, 0);
}
return 0;
}
function tsToHHmmss(ts) {
return (new Date(ts).toISOString().substr(11, 8));
}
function totalOfActivity() {
const minTime = Object.values(activitiesJson.users || {}).reduce((prevVal, elem) => {
if (prevVal === 0 || elem.registeredOn < prevVal) return elem.registeredOn;
@ -257,7 +319,7 @@ class App extends React.Component {
</span>
)
: null
}
}
<br />
<span className="text-sm font-medium">{activitiesJson.name || ''}</span>
</h1>
@ -309,7 +371,7 @@ class App extends React.Component {
}
number={Object.values(activitiesJson.users || {})
.filter((u) => activitiesJson.endedOn > 0 || u.leftOn === 0).length}
cardClass="border-pink-500"
cardClass={tab === 'overview' ? 'border-pink-500' : 'hover:border-pink-500'}
iconClass="bg-pink-50 text-pink-500"
onClick={() => {
this.setState({ tab: 'overview' });
@ -331,52 +393,6 @@ class App extends React.Component {
</svg>
</Card>
</div>
<div aria-hidden="true" className="cursor-pointer" onClick={() => { this.setState({ tab: 'polling' }); }}>
<Card
name={intl.formatMessage({ id: 'app.learningDashboard.indicators.polls', defaultMessage: 'Polls' })}
number={Object.values(activitiesJson.polls || {}).length}
cardClass="border-blue-500"
iconClass="bg-blue-100 text-blue-500"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"
/>
</svg>
</Card>
</div>
<div aria-hidden="true" className="cursor-pointer" onClick={() => { this.setState({ tab: 'status_timeline' }); }}>
<Card
name={intl.formatMessage({ id: 'app.learningDashboard.indicators.raiseHand', defaultMessage: 'Raise Hand' })}
number={totalOfRaiseHand()}
cardClass="border-purple-500"
iconClass="bg-purple-200 text-purple-500"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M7 11.5V14m0-2.5v-6a1.5 1.5 0 113 0m-3 6a1.5 1.5 0 00-3 0v2a7.5 7.5 0 0015 0v-5a1.5 1.5 0 00-3 0m-6-3V11m0-5.5v-1a1.5 1.5 0 013 0v1m0 0V11m0-5.5a1.5 1.5 0 013 0v3m0 0V11"
/>
</svg>
</Card>
</div>
<div aria-hidden="true" className="cursor-pointer" onClick={() => { this.setState({ tab: 'overview_activityscore' }); }}>
<Card
name={intl.formatMessage({ id: 'app.learningDashboard.indicators.activityScore', defaultMessage: 'Activity Score' })}
@ -384,7 +400,7 @@ class App extends React.Component {
minimumFractionDigits: 0,
maximumFractionDigits: 1,
})}
cardClass="border-green-500"
cardClass={tab === 'overview_activityscore' ? 'border-green-500' : 'hover:border-green-500'}
iconClass="bg-green-200 text-green-500"
>
<svg
@ -409,13 +425,46 @@ class App extends React.Component {
</svg>
</Card>
</div>
<div aria-hidden="true" className="cursor-pointer" onClick={() => { this.setState({ tab: 'status_timeline' }); }}>
<Card
name={intl.formatMessage({ id: 'app.learningDashboard.indicators.timeline', defaultMessage: 'Timeline' })}
number={totalOfEmojis()}
cardClass={tab === 'status_timeline' ? 'border-purple-500' : 'hover:border-purple-500'}
iconClass="bg-purple-200 text-purple-500"
>
{this.fetchMostUsedEmojis()}
</Card>
</div>
<div aria-hidden="true" className="cursor-pointer" onClick={() => { this.setState({ tab: 'polling' }); }}>
<Card
name={intl.formatMessage({ id: 'app.learningDashboard.indicators.polls', defaultMessage: 'Polls' })}
number={Object.values(activitiesJson.polls || {}).length}
cardClass={tab === 'polling' ? 'border-blue-500' : 'hover:border-blue-500'}
iconClass="bg-blue-100 text-blue-500"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"
/>
</svg>
</Card>
</div>
</div>
<h1 className="block my-1 pr-2 text-xl font-semibold">
<h1 className="block my-2 pr-2 text-xl font-semibold">
{ tab === 'overview' || tab === 'overview_activityscore'
? <FormattedMessage id="app.learningDashboard.usersTable.title" defaultMessage="Overview" />
: null }
{ tab === 'status_timeline'
? <FormattedMessage id="app.learningDashboard.statusTimelineTable.title" defaultMessage="Status Timeline" />
? <FormattedMessage id="app.learningDashboard.statusTimelineTable.title" defaultMessage="Timeline" />
: null }
{ tab === 'polling'
? <FormattedMessage id="app.learningDashboard.pollsTable.title" defaultMessage="Polling" />
@ -441,6 +490,44 @@ class App extends React.Component {
: null }
</div>
</div>
<hr className="my-8" />
<div className="flex justify-between mb-8 text-xs text-gray-700 dark:text-gray-400 whitespace-nowrap flex-col sm:flex-row">
<div className="flex flex-col justify-center mb-4 sm:mb-0">
<p>
{
lastUpdated && (
<>
<FormattedMessage
id="app.learningDashboard.lastUpdatedLabel"
defaultMessage="Last updated at"
/>
&nbsp;
<FormattedTime
value={lastUpdated}
/>
&nbsp;
<FormattedDate
value={lastUpdated}
year="numeric"
month="long"
day="numeric"
/>
</>
)
}
</p>
</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"
onClick={this.handleSaveSessionData.bind(this)}
>
<FormattedMessage
id="app.learningDashboard.downloadSessionDataLabel"
defaultMessage="Download Session Data"
/>
</button>
</div>
</div>
);
}

View File

@ -24,6 +24,15 @@
-moz-osx-font-smoothing: grayscale;
}
.bbb-icon-card {
font-size: 1.25rem;
font-weight: bold;
}
.bbb-icon-timeline {
font-weight: bold;
}
.icon-bbb-screenshare-fullscreen:before {
content: "\e92a";
}

View File

@ -5,8 +5,36 @@ function Card(props) {
number, name, children, iconClass, cardClass,
} = props;
let icons;
try {
React.Children.only(children);
icons = (
<div className={`p-2 text-orange-500 rounded-full ${iconClass}`}>
{ children }
</div>
);
} catch (e) {
icons = (
<div className="flex">
{
React.Children.map(children, (child, index) => {
let offset = 4 / (index + 1);
offset = index === (React.Children.count(children) - 1) ? 0 : offset;
return (
<div className={`flex justify-center transform translate-x-${offset} border-2 border-white p-2 text-orange-500 rounded-full z-${index * 10} ${iconClass}`}>
{ child }
</div>
);
})
}
</div>
);
}
return (
<div className={`flex items-center justify-between p-4 bg-white rounded-md shadow border-l-8 ${cardClass}`}>
<div className={`flex items-start justify-between p-3 bg-white rounded shadow border-l-4 border-white ${cardClass}`}>
<div className="w-70">
<p className="text-lg font-semibold text-gray-700">
{ number }
@ -15,9 +43,7 @@ function Card(props) {
{ name }
</p>
</div>
<div className={`p-3 mr-4 text-orange-500 rounded-full ${iconClass}`}>
{ children }
</div>
{icons}
</div>
);
}

View File

@ -14,6 +14,41 @@ class PollsTable extends React.Component {
return '';
}
if (typeof polls === 'object' && Object.values(polls).length === 0) {
return (
<div className="flex flex-col items-center py-24">
<div className="mb-1 p-3 text-orange-500 rounded-full bg-blue-100 text-blue-500">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"
/>
</svg>
</div>
<p className="text-lg font-semibold text-gray-700">
<FormattedMessage
id="app.learningDashboard.pollsTable.noPollsCreatedHeading"
defaultMessage="No polls have been created"
/>
</p>
<p className="mb-2 text-sm font-medium text-gray-600">
<FormattedMessage
id="app.learningDashboard.pollsTable.noPollsCreatedMessage"
defaultMessage="Once a poll has been sent to users, their results will appear in this list."
/>
</p>
</div>
);
}
return (
<table className="w-full whitespace-nowrap">
<thead>

View File

@ -1,9 +1,26 @@
import React from 'react';
import { FormattedMessage, injectIntl } from 'react-intl';
import { getUserEmojisSummary, emojiConfigs } from '../services/EmojiService';
import { emojiConfigs, filterUserEmojis } from '../services/EmojiService';
import UserAvatar from './UserAvatar';
class StatusTable extends React.Component {
componentDidMount() {
// This code is needed to prevent the emoji in the first cell
// after the username from overflowing
const emojis = document.getElementsByClassName('emojiOnFirstCell');
for (let i = 0; i < emojis.length; i += 1) {
const emojiStyle = window.getComputedStyle(emojis[i]);
let offsetLeft = emojiStyle
.left
.replace(/px/g, '')
.trim();
offsetLeft = Number(offsetLeft);
if (offsetLeft < 0) {
emojis[i].style.offsetLeft = '0px';
}
}
}
render() {
const spanMinutes = 10 * 60000; // 10 minutes default
const { allUsers, intl } = this.props;
@ -32,7 +49,7 @@ class StatusTable extends React.Component {
<table className="w-full whitespace-nowrap">
<thead>
<tr className="text-xs font-semibold tracking-wide text-gray-500 uppercase border-b bg-gray-100">
<th className="px-4 py-3 col-text-left">
<th className="px-4 py-3 col-text-left sticky left-0 z-30 bg-inherit">
<FormattedMessage id="app.learningDashboard.user" defaultMessage="User" />
<svg
xmlns="http://www.w3.org/2000/svg"
@ -58,8 +75,8 @@ class StatusTable extends React.Component {
return 0;
})
.map((user) => (
<tr className="text-gray-700">
<td className="px-4 py-3">
<tr className="text-gray-700 bg-inherit">
<td className="bg-inherit sticky left-0 z-30 px-4 py-3">
<div className="flex items-center text-sm">
<div className="relative hidden w-8 h-8 rounded-full md:block">
<UserAvatar user={user} />
@ -71,81 +88,97 @@ class StatusTable extends React.Component {
</div>
</td>
{ periods.map((period) => {
const userEmojisInPeriod = getUserEmojisSummary(user,
const userEmojisInPeriod = filterUserEmojis(user,
null,
period,
period + spanMinutes);
const { registeredOn, leftOn } = user;
const boundaryLeft = period;
const boundaryRight = period + spanMinutes - 1;
return (
<td className="px-4 py-3 text-sm col-text-left">
<td className="relative px-4 py-3 text-sm col-text-left">
{
user.registeredOn > period && user.registeredOn < period + spanMinutes
(registeredOn >= boundaryLeft && registeredOn <= boundaryRight)
|| (leftOn >= boundaryLeft && leftOn <= boundaryRight)
|| (boundaryLeft > registeredOn && boundaryRight < leftOn)
|| (boundaryLeft >= registeredOn && leftOn === 0)
? (
<span title={intl.formatDate(user.registeredOn, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 text-xs text-green-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
(function makeLineThrough() {
let roundedLeft = registeredOn >= boundaryLeft
&& registeredOn <= boundaryRight ? 'rounded-l' : '';
let roundedRight = leftOn > boundaryLeft
&& leftOn < boundaryRight ? 'rounded-r' : '';
let offsetLeft = 0;
let offsetRight = 0;
if (registeredOn >= boundaryLeft && registeredOn <= boundaryRight) {
offsetLeft = ((registeredOn - boundaryLeft) * 100) / spanMinutes;
}
if (leftOn >= boundaryLeft && leftOn <= boundaryRight) {
offsetRight = ((boundaryRight - leftOn) * 100) / spanMinutes;
}
let width = '';
if (offsetLeft === 0 && offsetRight >= 99) {
width = 'w-1.5';
}
if (offsetRight === 0 && offsetLeft >= 99) {
width = 'w-1.5';
}
if (offsetLeft && offsetRight) {
const variation = offsetLeft - offsetRight;
if (
variation > -1 && variation < 1
) {
width = 'w-1.5';
}
}
const isRTL = document.dir === 'rtl';
if (isRTL) {
const aux = roundedRight;
if (roundedLeft !== '') roundedRight = 'rounded-r';
else roundedRight = '';
if (aux !== '') roundedLeft = 'rounded-l';
else roundedLeft = '';
}
// height / 2
const redress = '(0.375rem / 2)';
return (
<div
className={`h-1.5 ${width} bg-gray-200 absolute inset-x-0 z-10 ${roundedLeft} ${roundedRight}`}
style={{
top: `calc(50% - ${redress})`,
left: `${isRTL ? offsetRight : offsetLeft}%`,
right: `${isRTL ? offsetLeft : offsetRight}%`,
}}
/>
</svg>
</span>
);
})()
) : null
}
{ Object.keys(userEmojisInPeriod)
.map((emoji) => (
<div className="text-sm text-gray-800">
<i className={`${emojiConfigs[emoji].icon} text-sm`} />
&nbsp;
{ userEmojisInPeriod[emoji] }
&nbsp;
<FormattedMessage
id={emojiConfigs[emoji].intlId}
defaultMessage={emojiConfigs[emoji].defaultMessage}
/>
{ userEmojisInPeriod.map((emoji) => {
const offset = ((emoji.sentOn - period) * 100) / spanMinutes;
const origin = document.dir === 'rtl' ? 'right' : 'left';
const onFirstCell = period === firstRegisteredOnTime;
// font-size / 2 + padding right/left + border-width
const redress = '(0.875rem / 2 + 0.25rem + 2px)';
return (
<div
className={`flex absolute p-1 border-white border-2 rounded-full text-sm z-20 bg-purple-500 text-purple-200 ${onFirstCell ? 'emojiOnFirstCell' : ''}`}
role="status"
style={{
top: `calc(50% - ${redress})`,
[origin]: `calc(${offset}% - ${redress})`,
}}
title={intl.formatMessage({
id: emojiConfigs[emoji.name].intlId,
defaultMessage: emojiConfigs[emoji.name].defaultMessage,
})}
>
<i className={`${emojiConfigs[emoji.name].icon} text-sm bbb-icon-timeline`} />
</div>
)) }
{
user.leftOn > period && user.leftOn < period + spanMinutes
? (
<span title={intl.formatDate(user.leftOn, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 text-red-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
</span>
) : null
}
);
})}
</td>
);
}) }

View File

@ -3,6 +3,7 @@ import {
FormattedMessage, FormattedDate, FormattedNumber, injectIntl,
} from 'react-intl';
import { getUserEmojisSummary, emojiConfigs } from '../services/EmojiService';
import { getActivityScore, getSumOfTime, tsToHHmmss } from '../services/UserService';
import UserAvatar from './UserAvatar';
class UsersTable extends React.Component {
@ -30,57 +31,6 @@ class UsersTable extends React.Component {
const { activityscoreOrder } = this.state;
function getSumOfTime(eventsArr) {
return eventsArr.reduce((prevVal, elem) => {
if (elem.stoppedOn > 0) return prevVal + (elem.stoppedOn - elem.startedOn);
return prevVal + (new Date().getTime() - elem.startedOn);
}, 0);
}
function getActivityScore(user) {
if (user.isModerator) return 0;
const allUsersArr = Object.values(allUsers || {}).filter((currUser) => !currUser.isModerator);
let userPoints = 0;
// Calculate points of Talking
const usersTalkTime = allUsersArr.map((currUser) => currUser.talk.totalTime);
const maxTalkTime = Math.max(...usersTalkTime);
if (maxTalkTime > 0) {
userPoints += (user.talk.totalTime / maxTalkTime) * 2;
}
// Calculate points of Chatting
const usersTotalOfMessages = allUsersArr.map((currUser) => currUser.totalOfMessages);
const maxMessages = Math.max(...usersTotalOfMessages);
if (maxMessages > 0) {
userPoints += (user.totalOfMessages / maxMessages) * 2;
}
// Calculate points of Raise hand
const usersRaiseHand = allUsersArr.map((currUser) => currUser.emojis.filter((emoji) => emoji.name === 'raiseHand').length);
const maxRaiseHand = Math.max(...usersRaiseHand);
const userRaiseHand = user.emojis.filter((emoji) => emoji.name === 'raiseHand').length;
if (maxRaiseHand > 0) {
userPoints += (userRaiseHand / maxRaiseHand) * 2;
}
// Calculate points of Emojis
const usersEmojis = allUsersArr.map((currUser) => currUser.emojis.filter((emoji) => emoji.name !== 'raiseHand').length);
const maxEmojis = Math.max(...usersEmojis);
const userEmojis = user.emojis.filter((emoji) => emoji.name !== 'raiseHand').length;
if (maxEmojis > 0) {
userPoints += (userEmojis / maxEmojis) * 2;
}
// Calculate points of Polls
if (totalOfPolls > 0) {
userPoints += (Object.values(user.answers || {}).length / totalOfPolls) * 2;
}
return userPoints;
}
const usersEmojisSummary = {};
Object.values(allUsers || {}).forEach((user) => {
usersEmojisSummary[user.intId] = getUserEmojisSummary(user, 'raiseHand');
@ -91,13 +41,9 @@ class UsersTable extends React.Component {
return Math.ceil((totalUserOnlineTime / totalOfActivityTime) * 100);
}
function tsToHHmmss(ts) {
return (new Date(ts).toISOString().substr(11, 8));
}
const usersActivityScore = {};
Object.values(allUsers || {}).forEach((user) => {
usersActivityScore[user.intId] = getActivityScore(user);
usersActivityScore[user.intId] = getActivityScore(user, allUsers, totalOfPolls);
});
return (
@ -187,49 +133,48 @@ class UsersTable extends React.Component {
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
return 0;
})
.map((user) => (
<tr key={user} className="text-gray-700">
<td className="px-4 py-3 col-text-left text-sm">
<div className="inline-block relative w-8 h-8 rounded-full">
{/* <img className="object-cover w-full h-full rounded-full" */}
{/* src="" */}
{/* alt="" loading="lazy" /> */}
<UserAvatar user={user} />
<div
className="absolute inset-0 rounded-full shadow-inner"
aria-hidden="true"
/>
</div>
&nbsp;&nbsp;&nbsp;
<div className="inline-block">
<p className="font-semibold">
{user.name}
</p>
<p className="text-xs text-gray-600 dark:text-gray-400">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
/>
</svg>
<FormattedDate
value={user.registeredOn}
month="short"
day="numeric"
hour="2-digit"
minute="2-digit"
second="2-digit"
.map((user) => {
const opacity = user.leftOn > 0 ? 'opacity-75' : '';
return (
<tr key={user} className="text-gray-700">
<td className={`px-4 py-3 col-text-left text-sm ${opacity}`}>
<div className="inline-block relative w-8 h-8 rounded-full">
<UserAvatar user={user} />
<div
className="absolute inset-0 rounded-full shadow-inner"
aria-hidden="true"
/>
</p>
{
</div>
&nbsp;&nbsp;&nbsp;
<div className="inline-block">
<p className="font-semibold">
{user.name}
</p>
<p className="text-xs text-gray-600 dark:text-gray-400">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
/>
</svg>
<FormattedDate
value={user.registeredOn}
month="short"
day="numeric"
hour="2-digit"
minute="2-digit"
second="2-digit"
/>
</p>
{
user.leftOn > 0
? (
<p className="text-xs text-gray-600 dark:text-gray-400">
@ -259,154 +204,154 @@ class UsersTable extends React.Component {
</p>
)
: null
}
</div>
</td>
<td className="px-4 py-3 text-sm text-center items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5.636 18.364a9 9 0 010-12.728m12.728 0a9 9 0 010 12.728m-9.9-2.829a5 5 0 010-7.07m7.072 0a5 5 0 010 7.07M13 12a1 1 0 11-2 0 1 1 0 012 0z"
/>
</svg>
&nbsp;
{ tsToHHmmss(
(user.leftOn > 0
? user.leftOn
: (new Date()).getTime()) - user.registeredOn,
) }
<br />
<div
className="bg-gray-200 transition-colors duration-500 rounded-full overflow-hidden"
title={`${getOnlinePercentage(user.registeredOn, user.leftOn).toString()}%`}
>
}
</div>
</td>
<td className={`px-4 py-3 text-sm text-center items-center ${opacity}`}>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5.636 18.364a9 9 0 010-12.728m12.728 0a9 9 0 010 12.728m-9.9-2.829a5 5 0 010-7.07m7.072 0a5 5 0 010 7.07M13 12a1 1 0 11-2 0 1 1 0 012 0z"
/>
</svg>
&nbsp;
{ tsToHHmmss(
(user.leftOn > 0
? user.leftOn
: (new Date()).getTime()) - user.registeredOn,
) }
<br />
<div
aria-label=" "
className="bg-gradient-to-br from-green-100 to-green-600 transition-colors duration-900 h-1.5"
style={{ width: `${getOnlinePercentage(user.registeredOn, user.leftOn).toString()}%` }}
role="progressbar"
/>
</div>
</td>
<td className="px-4 py-3 text-sm text-center">
{ user.talk.totalTime > 0
? (
<span className="text-center">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"
className="bg-gray-200 transition-colors duration-500 rounded-full overflow-hidden"
title={`${getOnlinePercentage(user.registeredOn, user.leftOn).toString()}%`}
>
<div
aria-label=" "
className="bg-gradient-to-br from-green-100 to-green-600 transition-colors duration-900 h-1.5"
style={{ width: `${getOnlinePercentage(user.registeredOn, user.leftOn).toString()}%` }}
role="progressbar"
/>
</div>
</td>
<td className={`px-4 py-3 text-sm text-center items-center ${opacity}`}>
{ user.talk.totalTime > 0
? (
<span className="text-center">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"
/>
</svg>
&nbsp;
{ tsToHHmmss(user.talk.totalTime) }
</span>
) : null }
</td>
<td className={`px-4 py-3 text-sm text-center ${opacity}`}>
{ getSumOfTime(user.webcams) > 0
? (
<span className="text-center">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
&nbsp;
{ tsToHHmmss(getSumOfTime(user.webcams)) }
</span>
) : null }
</td>
<td className={`px-4 py-3 text-sm text-center ${opacity}`}>
{ user.totalOfMessages > 0
? (
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
/>
</svg>
&nbsp;
{user.totalOfMessages}
</span>
) : null }
</td>
<td className={`px-4 py-3 text-sm col-text-left ${opacity}`}>
{
Object.keys(usersEmojisSummary[user.intId] || {}).map((emoji) => (
<div className="text-xs whitespace-nowrap">
<i className={`${emojiConfigs[emoji].icon} text-sm`} />
&nbsp;
{ usersEmojisSummary[user.intId][emoji] }
&nbsp;
<FormattedMessage
id={emojiConfigs[emoji].intlId}
defaultMessage={emojiConfigs[emoji].defaultMessage}
/>
</svg>
&nbsp;
{ tsToHHmmss(user.talk.totalTime) }
</span>
) : null }
</td>
<td className="px-4 py-3 text-sm text-center">
{ getSumOfTime(user.webcams) > 0
? (
<span className="text-center">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
&nbsp;
{ tsToHHmmss(getSumOfTime(user.webcams)) }
</span>
) : null }
</td>
<td className="px-4 py-3 text-sm text-center">
{ user.totalOfMessages > 0
? (
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
/>
</svg>
&nbsp;
{user.totalOfMessages}
</span>
) : null }
</td>
<td className="px-4 py-3 text-sm col-text-left">
</div>
))
}
</td>
<td className={`px-4 py-3 text-sm text-center ${opacity}`}>
{ user.emojis.filter((emoji) => emoji.name === 'raiseHand').length > 0
? (
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M7 11.5V14m0-2.5v-6a1.5 1.5 0 113 0m-3 6a1.5 1.5 0 00-3 0v2a7.5 7.5 0 0015 0v-5a1.5 1.5 0 00-3 0m-6-3V11m0-5.5v-1a1.5 1.5 0 013 0v1m0 0V11m0-5.5a1.5 1.5 0 013 0v3m0 0V11"
/>
</svg>
&nbsp;
{user.emojis.filter((emoji) => emoji.name === 'raiseHand').length}
</span>
) : null }
</td>
{
Object.keys(usersEmojisSummary[user.intId] || {}).map((emoji) => (
<div className="text-xs whitespace-nowrap">
<i className={`${emojiConfigs[emoji].icon} text-sm`} />
&nbsp;
{ usersEmojisSummary[user.intId][emoji] }
&nbsp;
<FormattedMessage
id={emojiConfigs[emoji].intlId}
defaultMessage={emojiConfigs[emoji].defaultMessage}
/>
</div>
))
}
</td>
<td className="px-4 py-3 text-sm text-center">
{ user.emojis.filter((emoji) => emoji.name === 'raiseHand').length > 0
? (
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M7 11.5V14m0-2.5v-6a1.5 1.5 0 113 0m-3 6a1.5 1.5 0 00-3 0v2a7.5 7.5 0 0015 0v-5a1.5 1.5 0 00-3 0m-6-3V11m0-5.5v-1a1.5 1.5 0 013 0v1m0 0V11m0-5.5a1.5 1.5 0 013 0v3m0 0V11"
/>
</svg>
&nbsp;
{user.emojis.filter((emoji) => emoji.name === 'raiseHand').length}
</span>
) : null }
</td>
{
!user.isModerator ? (
<td className="px-4 py-3 text-sm text-center items">
<td className={`px-4 py-3 text-sm text-center items ${opacity}`}>
<svg viewBox="0 0 82 12" width="82" height="12" className="flex-none m-auto inline">
<rect width="12" height="12" fill={usersActivityScore[user.intId] > 0 ? '#A7F3D0' : '#e4e4e7'} />
<rect width="12" height="12" x="14" fill={usersActivityScore[user.intId] > 2 ? '#6EE7B7' : '#e4e4e7'} />
@ -422,23 +367,24 @@ class UsersTable extends React.Component {
</td>
) : <td />
}
<td className="px-4 py-3 text-xs text-center">
{
user.leftOn > 0
? (
<span className="px-2 py-1 font-semibold leading-tight text-red-700 bg-red-100 rounded-full">
<FormattedMessage id="app.learningDashboard.usersTable.userStatusOffline" defaultMessage="Offline" />
</span>
)
: (
<span className="px-2 py-1 font-semibold leading-tight text-green-700 bg-green-100 rounded-full">
<FormattedMessage id="app.learningDashboard.usersTable.userStatusOnline" defaultMessage="Online" />
</span>
)
}
</td>
</tr>
))
<td className="px-4 py-3 text-xs text-center">
{
user.leftOn > 0
? (
<span className="px-2 py-1 font-semibold leading-tight text-red-700 bg-red-100 rounded-full">
<FormattedMessage id="app.learningDashboard.usersTable.userStatusOffline" defaultMessage="Offline" />
</span>
)
: (
<span className="px-2 py-1 font-semibold leading-tight text-green-700 bg-green-100 rounded-full">
<FormattedMessage id="app.learningDashboard.usersTable.userStatusOnline" defaultMessage="Online" />
</span>
)
}
</td>
</tr>
);
})
) : (
<tr className="text-gray-700">
<td colSpan="8" className="px-4 py-3 text-sm text-center">

View File

@ -1,4 +1,34 @@
/* ./src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind utilities;
@layer utilities {
.bg-inherit {
background-color: inherit;
}
}
.translate-x-0 {
--tw-translate-x: 0px;
}
.translate-x-2 {
--tw-translate-x: 0.5rem;
}
.translate-x-4 {
--tw-translate-x: 1rem;
}
[dir="rtl"] .translate-x-0 {
--tw-translate-x: 0px;
}
[dir="rtl"] .translate-x-2 {
--tw-translate-x: -0.5rem;
}
[dir="rtl"] .translate-x-4 {
--tw-translate-x: -1rem;
}

View File

@ -60,3 +60,15 @@ export function getUserEmojisSummary(user, skipNames = null, start = null, end =
});
return userEmojis;
}
export function filterUserEmojis(user, skipNames = null, start = null, end = null) {
const userEmojis = [];
user.emojis.forEach((emoji) => {
if (typeof emojiConfigs[emoji.name] === 'undefined') return;
if (skipNames != null && skipNames.split(',').indexOf(emoji.name) > -1) return;
if (start != null && emoji.sentOn < start) return;
if (end != null && emoji.sentOn > end) return;
userEmojis.push(emoji);
});
return userEmojis;
}

View File

@ -0,0 +1,202 @@
import { emojiConfigs, filterUserEmojis } from './EmojiService';
export function getActivityScore(user, allUsers, totalOfPolls) {
if (user.isModerator) return 0;
const allUsersArr = Object.values(allUsers || {}).filter((currUser) => !currUser.isModerator);
let userPoints = 0;
// Calculate points of Talking
const usersTalkTime = allUsersArr.map((currUser) => currUser.talk.totalTime);
const maxTalkTime = Math.max(...usersTalkTime);
if (maxTalkTime > 0) {
userPoints += (user.talk.totalTime / maxTalkTime) * 2;
}
// Calculate points of Chatting
const usersTotalOfMessages = allUsersArr.map((currUser) => currUser.totalOfMessages);
const maxMessages = Math.max(...usersTotalOfMessages);
if (maxMessages > 0) {
userPoints += (user.totalOfMessages / maxMessages) * 2;
}
// Calculate points of Raise hand
const usersRaiseHand = allUsersArr.map((currUser) => currUser.emojis.filter((emoji) => emoji.name === 'raiseHand').length);
const maxRaiseHand = Math.max(...usersRaiseHand);
const userRaiseHand = user.emojis.filter((emoji) => emoji.name === 'raiseHand').length;
if (maxRaiseHand > 0) {
userPoints += (userRaiseHand / maxRaiseHand) * 2;
}
// Calculate points of Emojis
const usersEmojis = allUsersArr.map((currUser) => currUser.emojis.filter((emoji) => emoji.name !== 'raiseHand').length);
const maxEmojis = Math.max(...usersEmojis);
const userEmojis = user.emojis.filter((emoji) => emoji.name !== 'raiseHand').length;
if (maxEmojis > 0) {
userPoints += (userEmojis / maxEmojis) * 2;
}
// Calculate points of Polls
if (totalOfPolls > 0) {
userPoints += (Object.values(user.answers || {}).length / totalOfPolls) * 2;
}
return userPoints;
}
export function getSumOfTime(eventsArr) {
return eventsArr.reduce((prevVal, elem) => {
if (elem.stoppedOn > 0) return prevVal + (elem.stoppedOn - elem.startedOn);
return prevVal + (new Date().getTime() - elem.startedOn);
}, 0);
}
export function tsToHHmmss(ts) {
return (new Date(ts).toISOString().substr(11, 8));
}
const tableHeaderFields = [
{
id: 'name',
defaultMessage: 'Name',
},
{
id: 'moderator',
defaultMessage: 'Moderator',
},
{
id: 'activityScore',
defaultMessage: 'Activity Score',
},
{
id: 'colTalk',
defaultMessage: 'Talk Time',
},
{
id: 'colWebcam',
defaultMessage: 'Webcam Time',
},
{
id: 'colMessages',
defaultMessage: 'Messages',
},
{
id: 'colEmojis',
defaultMessage: 'Emojis',
},
{
id: 'pollVotes',
defaultMessage: 'Poll Votes',
},
{
id: 'colRaiseHands',
defaultMessage: 'Raise Hands',
},
{
id: 'join',
defaultMessage: 'Join',
},
{
id: 'left',
defaultMessage: 'Left',
},
{
id: 'duration',
defaultMessage: 'Duration',
},
];
export function makeUserCSVData(users, polls, intl) {
const userRecords = {};
const userValues = Object.values(users || {});
const pollValues = Object.values(polls || {});
const skipEmojis = Object
.keys(emojiConfigs)
.filter((emoji) => emoji !== 'raiseHand')
.join(',');
for (let i = 0; i < userValues.length; i += 1) {
const user = userValues[i];
const webcam = getSumOfTime(user.webcams);
const duration = user.leftOn > 0
? user.leftOn - user.registeredOn
: (new Date()).getTime() - user.registeredOn;
const userData = {
name: user.name,
moderator: user.isModerator.toString().toUpperCase(),
activityScore: intl.formatNumber(
getActivityScore(user, userValues, Object.values(polls || {}).length),
{
minimumFractionDigits: 0,
maximumFractionDigits: 1,
},
),
talk: user.talk.totalTime > 0 ? tsToHHmmss(user.talk.totalTime) : '-',
webcam: webcam > 0 ? tsToHHmmss(webcam) : '-',
messages: user.totalOfMessages,
raiseHand: filterUserEmojis(user, 'raiseHand').length,
answers: Object.keys(user.answers).length,
emojis: filterUserEmojis(user, skipEmojis).length,
registeredOn: intl.formatDate(user.registeredOn, {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
}),
leftOn: intl.formatDate(user.leftOn, {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
}),
duration: tsToHHmmss(duration),
};
for (let j = 0; j < pollValues.length; j += 1) {
userData[`Poll_${j}`] = user.answers[pollValues[j].pollId] || '-';
}
const userFields = Object
.values(userData)
.map((data) => `"${data}"`);
userRecords[user.intId] = userFields.join(',');
}
const tableHeaderFieldsTranslated = tableHeaderFields
.map(({ id, defaultMessage }) => intl.formatMessage({
id: `app.learningDashboard.usersTable.${id}`,
defaultMessage,
}));
let header = tableHeaderFieldsTranslated.join(',');
let anonymousRecord = `"${intl.formatMessage({
id: 'app.learningDashboard.pollsTable.anonymousRowName',
defaultMessage: 'Anonymous',
})}"`;
// Skip the fields for the anonymous record
for (let k = 0; k < header.split(',').length - 1; k += 1) {
// Empty fields
anonymousRecord += ',""';
}
for (let i = 0; i < pollValues.length; i += 1) {
// Add the poll question headers
header += `,${pollValues[i].question || `Poll ${i + 1}`}`;
// Add the anonymous answers
anonymousRecord += `,"${pollValues[i].anonymousAnswers.join('\r\n')}"`;
}
userRecords.Anonymous = anonymousRecord;
return [
header,
Object.values(userRecords).join('\r\n'),
].join('\r\n');
}

View File

@ -8,4 +8,4 @@ module.exports = {
extend: {},
},
plugins: [],
}
};

View File

@ -0,0 +1,92 @@
import AbstractCollection from '/imports/ui/services/LocalCollectionSynchronizer/LocalCollectionSynchronizer';
// Collections
import Presentations from '/imports/api/presentations';
import PresentationPods from '/imports/api/presentation-pods';
import PresentationUploadToken from '/imports/api/presentation-upload-token';
import Screenshare from '/imports/api/screenshare';
import UserInfos from '/imports/api/users-infos';
import Polls, { CurrentPoll } from '/imports/api/polls';
import UsersPersistentData from '/imports/api/users-persistent-data';
import UserSettings from '/imports/api/users-settings';
import VideoStreams from '/imports/api/video-streams';
import VoiceUsers from '/imports/api/voice-users';
import WhiteboardMultiUser from '/imports/api/whiteboard-multi-user';
import Note from '/imports/api/note';
import GroupChat from '/imports/api/group-chat';
import ConnectionStatus from '/imports/api/connection-status';
import Captions from '/imports/api/captions';
import AuthTokenValidation from '/imports/api/auth-token-validation';
import Annotations from '/imports/api/annotations';
import Breakouts from '/imports/api/breakouts';
import guestUsers from '/imports/api/guest-users';
import Meetings, { RecordMeetings, ExternalVideoMeetings, MeetingTimeRemaining } from '/imports/api/meetings';
import { UsersTyping } from '/imports/api/group-chat-msg';
import Users, { CurrentUser } from '/imports/api/users';
import { Slides, SlidePositions } from '/imports/api/slides';
// Custom Publishers
export const localCurrentPollSync = new AbstractCollection(CurrentPoll, CurrentPoll);
export const localCurrentUserSync = new AbstractCollection(CurrentUser, CurrentUser);
export const localSlidesSync = new AbstractCollection(Slides, Slides);
export const localSlidePositionsSync = new AbstractCollection(SlidePositions, SlidePositions);
export const localPollsSync = new AbstractCollection(Polls, Polls);
export const localPresentationsSync = new AbstractCollection(Presentations, Presentations);
export const localPresentationPodsSync = new AbstractCollection(PresentationPods, PresentationPods);
export const localPresentationUploadTokenSync = new AbstractCollection(PresentationUploadToken, PresentationUploadToken);
export const localScreenshareSync = new AbstractCollection(Screenshare, Screenshare);
export const localUserInfosSync = new AbstractCollection(UserInfos, UserInfos);
export const localUsersPersistentDataSync = new AbstractCollection(UsersPersistentData, UsersPersistentData);
export const localUserSettingsSync = new AbstractCollection(UserSettings, UserSettings);
export const localVideoStreamsSync = new AbstractCollection(VideoStreams, VideoStreams);
export const localVoiceUsersSync = new AbstractCollection(VoiceUsers, VoiceUsers);
export const localWhiteboardMultiUserSync = new AbstractCollection(WhiteboardMultiUser, WhiteboardMultiUser);
export const localNoteSync = new AbstractCollection(Note, Note);
export const localGroupChatSync = new AbstractCollection(GroupChat, GroupChat);
export const localConnectionStatusSync = new AbstractCollection(ConnectionStatus, ConnectionStatus);
export const localCaptionsSync = new AbstractCollection(Captions, Captions);
export const localAuthTokenValidationSync = new AbstractCollection(AuthTokenValidation, AuthTokenValidation);
export const localAnnotationsSync = new AbstractCollection(Annotations, Annotations);
export const localRecordMeetingsSync = new AbstractCollection(RecordMeetings, RecordMeetings);
export const localExternalVideoMeetingsSync = new AbstractCollection(ExternalVideoMeetings, ExternalVideoMeetings);
export const localMeetingTimeRemainingSync = new AbstractCollection(MeetingTimeRemaining, MeetingTimeRemaining);
export const localUsersTypingSync = new AbstractCollection(UsersTyping, UsersTyping);
export const localBreakoutsSync = new AbstractCollection(Breakouts, Breakouts);
export const localGuestUsersSync = new AbstractCollection(guestUsers, guestUsers);
export const localMeetingsSync = new AbstractCollection(Meetings, Meetings);
export const localUsersSync = new AbstractCollection(Users, Users);
const collectionMirrorInitializer = () => {
localCurrentPollSync.setupListeners();
localCurrentUserSync.setupListeners();
localSlidesSync.setupListeners();
localSlidePositionsSync.setupListeners();
localPollsSync.setupListeners();
localPresentationsSync.setupListeners();
localPresentationPodsSync.setupListeners();
localPresentationUploadTokenSync.setupListeners();
localScreenshareSync.setupListeners();
localUserInfosSync.setupListeners();
localUsersPersistentDataSync.setupListeners();
localUserSettingsSync.setupListeners();
localVideoStreamsSync.setupListeners();
localVoiceUsersSync.setupListeners();
localWhiteboardMultiUserSync.setupListeners();
localNoteSync.setupListeners();
localGroupChatSync.setupListeners();
localConnectionStatusSync.setupListeners();
localCaptionsSync.setupListeners();
localAuthTokenValidationSync.setupListeners();
localAnnotationsSync.setupListeners();
localRecordMeetingsSync.setupListeners();
localExternalVideoMeetingsSync.setupListeners();
localMeetingTimeRemainingSync.setupListeners();
localUsersTypingSync.setupListeners();
localBreakoutsSync.setupListeners();
localGuestUsersSync.setupListeners();
localMeetingsSync.setupListeners();
localUsersSync.setupListeners();
};
export default collectionMirrorInitializer;
// const localUsersSync = new AbstractCollection(CurrentUser, CurrentUser);

View File

@ -17,8 +17,6 @@
*/
/* eslint no-unused-vars: 0 */
import '../imports/ui/services/collection-hooks/collection-hooks';
import React from 'react';
import { Meteor } from 'meteor/meteor';
import { render } from 'react-dom';
@ -32,16 +30,15 @@ import ContextProviders from '/imports/ui/components/context-providers/component
import ChatAdapter from '/imports/ui/components/components-data/chat-context/adapter';
import UsersAdapter from '/imports/ui/components/components-data/users-context/adapter';
import GroupChatAdapter from '/imports/ui/components/components-data/group-chat-context/adapter';
import { liveDataEventBrokerInitializer } from '/imports/ui/services/LiveDataEventBroker/LiveDataEventBroker';
import '/imports/ui/local-collections/meetings-collection/meetings';
import '/imports/ui/local-collections/breakouts-collection/breakouts';
import '/imports/ui/local-collections/guest-users-collection/guest-users';
import '/imports/ui/local-collections/users-collection/users';
import collectionMirrorInitializer from './collection-mirror-initializer';
import('/imports/api/audio/client/bridge/bridge-whitelist').catch(() => {
// bridge loading
});
collectionMirrorInitializer();
liveDataEventBrokerInitializer();
Meteor.startup(() => {
// Logs all uncaught exceptions to the client logger
window.addEventListener('error', (e) => {

View File

@ -1,6 +1,10 @@
import { Meteor } from 'meteor/meteor';
const Annotations = new Mongo.Collection('annotations');
const collectionOptions = Meteor.isClient ? {
connection: null,
} : {};
const Annotations = new Mongo.Collection('annotations', collectionOptions);
if (Meteor.isServer) {
// types of queries for the annotations (Total):

View File

@ -1,6 +1,10 @@
import { Meteor } from 'meteor/meteor';
const AuthTokenValidation = new Mongo.Collection('auth-token-validation');
const collectionOptions = Meteor.isClient ? {
connection: null,
} : {};
const AuthTokenValidation = new Mongo.Collection('auth-token-validation', collectionOptions);
if (Meteor.isServer) {
AuthTokenValidation._ensureIndex({ meetingId: 1, userId: 1 });

View File

@ -1,6 +1,10 @@
import { Meteor } from 'meteor/meteor';
const Breakouts = new Mongo.Collection('breakouts');
const collectionOptions = Meteor.isClient ? {
connection: null,
} : {};
const Breakouts = new Mongo.Collection('breakouts', collectionOptions);
if (Meteor.isServer) {
// types of queries for the breakouts:

View File

@ -1,6 +1,10 @@
import { Meteor } from 'meteor/meteor';
const Captions = new Mongo.Collection('captions');
const collectionOptions = Meteor.isClient ? {
connection: null,
} : {};
const Captions = new Mongo.Collection('captions', collectionOptions);
if (Meteor.isServer) {
Captions._ensureIndex({ meetingId: 1, padId: 1 });

View File

@ -1,6 +1,10 @@
import { Meteor } from 'meteor/meteor';
const ConnectionStatus = new Mongo.Collection('connection-status');
const collectionOptions = Meteor.isClient ? {
connection: null,
} : {};
const ConnectionStatus = new Mongo.Collection('connection-status', collectionOptions);
if (Meteor.isServer) {
ConnectionStatus._ensureIndex({ meetingId: 1, userId: 1 });

View File

@ -1,7 +1,11 @@
import { Meteor } from 'meteor/meteor';
const collectionOptions = Meteor.isClient ? {
connection: null,
} : {};
const GroupChatMsg = new Mongo.Collection('group-chat-msg');
const UsersTyping = new Mongo.Collection('users-typing');
const UsersTyping = new Mongo.Collection('users-typing', collectionOptions);
if (Meteor.isServer) {
GroupChatMsg._ensureIndex({ meetingId: 1, chatId: 1 });

View File

@ -1,6 +1,10 @@
import { Meteor } from 'meteor/meteor';
const GroupChat = new Mongo.Collection('group-chat');
const collectionOptions = Meteor.isClient ? {
connection: null,
} : {};
const GroupChat = new Mongo.Collection('group-chat', collectionOptions);
if (Meteor.isServer) {
GroupChat._ensureIndex({

View File

@ -1,5 +1,9 @@
import { Mongo } from 'meteor/mongo';
const GuestUsers = new Mongo.Collection('guestUsers');
const collectionOptions = Meteor.isClient ? {
connection: null,
} : {};
const GuestUsers = new Mongo.Collection('guestUsers', collectionOptions);
export default GuestUsers;

View File

@ -1,9 +1,13 @@
import { Meteor } from 'meteor/meteor';
const Meetings = new Mongo.Collection('meetings');
const RecordMeetings = new Mongo.Collection('record-meetings');
const ExternalVideoMeetings = new Mongo.Collection('external-video-meetings');
const MeetingTimeRemaining = new Mongo.Collection('meeting-time-remaining');
const collectionOptions = Meteor.isClient ? {
connection: null,
} : {};
const Meetings = new Mongo.Collection('meetings', collectionOptions);
const RecordMeetings = new Mongo.Collection('record-meetings', collectionOptions);
const ExternalVideoMeetings = new Mongo.Collection('external-video-meetings', collectionOptions);
const MeetingTimeRemaining = new Mongo.Collection('meeting-time-remaining', collectionOptions);
if (Meteor.isServer) {
// types of queries for the meetings:

View File

@ -1,6 +1,10 @@
import { Meteor } from 'meteor/meteor';
const Note = new Mongo.Collection('note');
const collectionOptions = Meteor.isClient ? {
connection: null,
} : {};
const Note = new Mongo.Collection('note', collectionOptions);
if (Meteor.isServer) {
Note._ensureIndex({ meetingId: 1, noteId: 1 });

View File

@ -1,7 +1,11 @@
import { Meteor } from 'meteor/meteor';
const Polls = new Mongo.Collection('polls');
export const CurrentPoll = new Mongo.Collection('current-poll');
const collectionOptions = Meteor.isClient ? {
connection: null,
} : {};
const Polls = new Mongo.Collection('polls', collectionOptions);
export const CurrentPoll = new Mongo.Collection('current-poll', { connection: null });
if (Meteor.isServer) {
// We can have just one active poll per meeting

View File

@ -1,6 +1,10 @@
import { Meteor } from 'meteor/meteor';
const PresentationPods = new Mongo.Collection('presentation-pods');
const collectionOptions = Meteor.isClient ? {
connection: null,
} : {};
const PresentationPods = new Mongo.Collection('presentation-pods', collectionOptions);
if (Meteor.isServer) {
// types of queries for the presentation pods:

View File

@ -1,3 +1,7 @@
const PresentationUploadToken = new Mongo.Collection('presentation-upload-token');
const collectionOptions = Meteor.isClient ? {
connection: null,
} : {};
const PresentationUploadToken = new Mongo.Collection('presentation-upload-token', collectionOptions);
export default PresentationUploadToken;

View File

@ -1,6 +1,10 @@
import { Meteor } from 'meteor/meteor';
const Presentations = new Mongo.Collection('presentations');
const collectionOptions = Meteor.isClient ? {
connection: null,
} : {};
const Presentations = new Mongo.Collection('presentations', collectionOptions);
if (Meteor.isServer) {
// types of queries for the presentations:

View File

@ -1,6 +1,10 @@
import { Meteor } from 'meteor/meteor';
const Screenshare = new Mongo.Collection('screenshare');
const collectionOptions = Meteor.isClient ? {
connection: null,
} : {};
const Screenshare = new Mongo.Collection('screenshare', collectionOptions);
if (Meteor.isServer) {
// types of queries for the screenshare:

View File

@ -1,7 +1,11 @@
import { Meteor } from 'meteor/meteor';
const Slides = new Mongo.Collection('slides');
const SlidePositions = new Mongo.Collection('slide-positions');
const collectionOptions = Meteor.isClient ? {
connection: null,
} : {};
const Slides = new Mongo.Collection('slides', collectionOptions);
const SlidePositions = new Mongo.Collection('slide-positions', collectionOptions);
if (Meteor.isServer) {
// types of queries for the slides:

View File

@ -1,6 +1,10 @@
import { Meteor } from 'meteor/meteor';
const UserInfos = new Mongo.Collection('users-infos');
const collectionOptions = Meteor.isClient ? {
connection: null,
} : {};
const UserInfos = new Mongo.Collection('users-infos', collectionOptions);
if (Meteor.isServer) {
UserInfos._ensureIndex({ meetingId: 1, userId: 1 });

View File

@ -1,6 +1,10 @@
import { Meteor } from 'meteor/meteor';
const UsersPersistentData = new Mongo.Collection('users-persistent-data');
const collectionOptions = Meteor.isClient ? {
connection: null,
} : {};
const UsersPersistentData = new Mongo.Collection('users-persistent-data', collectionOptions);
if (Meteor.isServer) {
UsersPersistentData._ensureIndex({ meetingId: 1, userId: 1 });

View File

@ -1,6 +1,10 @@
import { Meteor } from 'meteor/meteor';
const UserSettings = new Mongo.Collection('users-settings');
const collectionOptions = Meteor.isClient ? {
connection: null,
} : {};
const UserSettings = new Mongo.Collection('users-settings', collectionOptions);
if (Meteor.isServer) {
UserSettings._ensureIndex({

View File

@ -1,13 +1,17 @@
import { Meteor } from 'meteor/meteor';
const Users = new Mongo.Collection('users');
export const CurrentUser = new Mongo.Collection('current-user');
const collectionOptions = Meteor.isClient ? {
connection: null,
} : {};
const Users = new Mongo.Collection('users', collectionOptions);
export const CurrentUser = new Mongo.Collection('current-user', { connection: null });
if (Meteor.isServer) {
// types of queries for the users:
// 1. meetingId
// 2. meetingId, userId
// { connection: Meteor.isClient ? null : true }
Users._ensureIndex({ meetingId: 1, userId: 1 });
}

View File

@ -1,6 +1,10 @@
import { Meteor } from 'meteor/meteor';
const VideoStreams = new Mongo.Collection('video-streams');
const collectionOptions = Meteor.isClient ? {
connection: null,
} : {};
const VideoStreams = new Mongo.Collection('video-streams', collectionOptions);
if (Meteor.isServer) {
// types of queries for the video users:

View File

@ -1,6 +1,10 @@
import { Meteor } from 'meteor/meteor';
const VoiceUsers = new Mongo.Collection('voiceUsers');
const collectionOptions = Meteor.isClient ? {
connection: null,
} : {};
const VoiceUsers = new Mongo.Collection('voiceUsers', collectionOptions);
if (Meteor.isServer) {
// types of queries for the voice users:

View File

@ -1,6 +1,10 @@
import { Meteor } from 'meteor/meteor';
const WhiteboardMultiUser = new Mongo.Collection('whiteboard-multi-user');
const collectionOptions = Meteor.isClient ? {
connection: null,
} : {};
const WhiteboardMultiUser = new Mongo.Collection('whiteboard-multi-user', collectionOptions);
if (Meteor.isServer) {
// types of queries for the whiteboard-multi-user:

View File

@ -8,14 +8,14 @@ import MeetingEnded from '/imports/ui/components/meeting-ended/component';
import LoadingScreen from '/imports/ui/components/loading-screen/component';
import Settings from '/imports/ui/services/settings';
import logger from '/imports/startup/client/logger';
import Users from '/imports/ui/local-collections/users-collection/users';
import Users from '/imports/api/users';
import { Session } from 'meteor/session';
import { FormattedMessage } from 'react-intl';
import { Meteor } from 'meteor/meteor';
import { RecordMeetings } from '../../api/meetings';
import Meetings from '/imports/ui/local-collections/meetings-collection/meetings';
import Meetings from '/imports/api/meetings';
import AppService from '/imports/ui/components/app/service';
import Breakouts from '/imports/ui/local-collections/breakouts-collection/breakouts';
import Breakouts from '/imports/api/breakouts';
import AudioService from '/imports/ui/components/audio/service';
import { notify } from '/imports/ui/services/notification';
import deviceInfo from '/imports/utils/deviceInfo';
@ -218,6 +218,7 @@ class Base extends Component {
if (approved && loading) this.updateLoadingState(false);
if (prevProps.ejected || ejected) {
console.log(' if (prevProps.ejected || ejected) {');
Session.set('codeError', '403');
Session.set('isMeetingEnded', true);
}
@ -227,6 +228,10 @@ class Base extends Component {
this.setMeetingExisted(false);
}
if ((prevProps.isMeteorConnected !== isMeteorConnected) && !isMeteorConnected) {
Session.set('globalIgnoreDeletes', true);
}
const enabled = HTML.classList.contains('animationsEnabled');
const disabled = HTML.classList.contains('animationsDisabled');

View File

@ -2,7 +2,7 @@ import styled from 'styled-components';
import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
import Modal from '/imports/ui/components/modal/simple/component';
import {
colorBackground,
colorGrayDark,
colorWhite,
colorGrayLabel,
colorGrayLight,
@ -16,7 +16,7 @@ const ReaderMenuModal = styled(Modal)`
const Title = styled.header`
display: block;
color: ${colorBackground};
color: ${colorGrayDark};
font-size: 1.4rem;
text-align: center;
`;

View File

@ -29,6 +29,8 @@ const ActionsBarContainer = (props) => {
const currentUser = { userId: Auth.userID, emoji: users[Auth.meetingID][Auth.userID].emoji };
const amIPresenter = users[Auth.meetingID][Auth.userID].presenter;
return (
<ActionsBar {
...{
@ -36,6 +38,7 @@ const ActionsBarContainer = (props) => {
currentUser,
layoutContextDispatch,
actionsBarStyle,
amIPresenter,
}
}
/>
@ -49,11 +52,10 @@ const RAISE_HAND_BUTTON_ENABLED = Meteor.settings.public.app.raiseHandActionButt
const OLD_MINIMIZE_BUTTON_ENABLED = Meteor.settings.public.presentation.oldMinimizeButton;
export default withTracker(() => ({
amIPresenter: Service.amIPresenter(),
amIModerator: Service.amIModerator(),
stopExternalVideoShare: ExternalVideoService.stopWatching,
enableVideo: getFromUserSettings('bbb_enable_video', Meteor.settings.public.kurento.enableVideo),
isLayoutSwapped: getSwapLayout()&& shouldEnableSwapLayout(),
isLayoutSwapped: getSwapLayout() && shouldEnableSwapLayout(),
toggleSwapLayout: MediaService.toggleSwapLayout,
handleTakePresenter: Service.takePresenterRole,
currentSlidHasContent: PresentationService.currentSlidHasContent(),

View File

@ -183,7 +183,7 @@ const ScreenshareButton = ({
if (isSafari && !ScreenshareBridgeService.HAS_DISPLAY_MEDIA) {
renderScreenshareUnavailableModal();
} else {
shareScreen(handleFailure);
shareScreen(amIPresenter, handleFailure);
}
}}
id={isVideoBroadcasting ? 'unshare-screen-button' : 'share-screen-button'}

View File

@ -1,7 +1,7 @@
import styled from 'styled-components';
import Modal from '/imports/ui/components/modal/simple/component';
import Button from '/imports/ui/components/button/component';
import { colorBackground, colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
import { colorGrayDark, colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
import {
jumboPaddingY,
minModalHeight,
@ -19,7 +19,7 @@ const ScreenShareModal = styled(Modal)`
const Title = styled.h3`
font-weight: ${headingsFontWeight};
font-size: ${fontSizeLarge};
color: ${colorBackground};
color: ${colorGrayDark};
white-space: normal;
padding-bottom: ${mdPaddingX};
`;

View File

@ -1,8 +1,8 @@
import Auth from '/imports/ui/services/auth';
import Users from '/imports/ui/local-collections/users-collection/users';
import Users from '/imports/api/users';
import { makeCall } from '/imports/ui/services/api';
import Meetings from '/imports/ui/local-collections/meetings-collection/meetings';
import Breakouts from '/imports/ui/local-collections/breakouts-collection/breakouts';
import Meetings from '/imports/api/meetings';
import Breakouts from '/imports/api/breakouts';
import { getVideoUrl } from '/imports/ui/components/external-video-player/service';
const USER_CONFIG = Meteor.settings.public.user;
@ -13,27 +13,16 @@ const getBreakouts = () => Breakouts.find({ parentMeetingId: Auth.meetingID })
.fetch()
.sort((a, b) => a.sequence - b.sequence);
const currentBreakoutUsers = user => !Breakouts.findOne({
const currentBreakoutUsers = (user) => !Breakouts.findOne({
'joinedUsers.userId': new RegExp(`^${user.userId}`),
});
const filterBreakoutUsers = filter => users => users.filter(filter);
const filterBreakoutUsers = (filter) => (users) => users.filter(filter);
const getUsersNotAssigned = filterBreakoutUsers(currentBreakoutUsers);
const takePresenterRole = () => makeCall('assignPresenter', Auth.userID);
const amIPresenter = () => {
const currentUser = Users.findOne({ userId: Auth.userID },
{ fields: { presenter: 1 } });
if (!currentUser) {
return false;
}
return currentUser.presenter;
};
const amIModerator = () => {
const currentUser = Users.findOne({ userId: Auth.userID },
{ fields: { role: 1 } });
@ -45,11 +34,9 @@ const amIModerator = () => {
return currentUser.role === ROLE_MODERATOR;
};
const isMe = intId => intId === Auth.userID;
const isMe = (intId) => intId === Auth.userID;
export default {
amIPresenter,
amIModerator,
isMe,
currentUser: () => Users.findOne({ meetingId: Auth.meetingID, userId: Auth.userID },

View File

@ -3,8 +3,8 @@ import { withTracker } from 'meteor/react-meteor-data';
import { defineMessages, injectIntl } from 'react-intl';
import PropTypes from 'prop-types';
import Auth from '/imports/ui/services/auth';
import Users from '/imports/ui/local-collections/users-collection/users';
import Meetings from '/imports/ui/local-collections/meetings-collection/meetings';
import Users from '/imports/api/users';
import Meetings from '/imports/api/meetings';
import { notify } from '/imports/ui/services/notification';
import CaptionsContainer from '/imports/ui/components/captions/container';
import CaptionsService from '/imports/ui/components/captions/service';
@ -13,11 +13,11 @@ import deviceInfo from '/imports/utils/deviceInfo';
import UserInfos from '/imports/api/users-infos';
import Settings from '/imports/ui/services/settings';
import MediaService from '/imports/ui/components/media/service';
import {
layoutSelect,
layoutSelectInput,
layoutSelectOutput,
layoutDispatch
import {
layoutSelect,
layoutSelectInput,
layoutSelectOutput,
layoutDispatch,
} from '../layout/context';
import {
@ -119,10 +119,6 @@ const currentUserEmoji = (currentUser) => (currentUser
);
export default injectIntl(withModalMounter(withTracker(({ intl, baseControls }) => {
if (Auth.connectionID !== Meteor.connection._lastSessionId) {
endMeeting('403');
}
Users.find({ userId: Auth.userID, meetingId: Auth.meetingID }).observe({
removed() {
endMeeting('403');
@ -168,7 +164,7 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
const { viewScreenshare } = Settings.dataSaving;
const shouldShowExternalVideo = MediaService.shouldShowExternalVideo();
const shouldShowScreenshare = MediaService.shouldShowScreenshare()
&& (viewScreenshare || MediaService.isUserPresenter());
&& (viewScreenshare || currentUser?.presenter);
let customStyleUrl = getFromUserSettings('bbb_custom_style_url', false);
if (!customStyleUrl && CUSTOM_STYLE_URL) {

View File

@ -1,5 +1,5 @@
import Breakouts from '/imports/ui/local-collections/breakouts-collection/breakouts';
import Meetings from '/imports/ui/local-collections/meetings-collection/meetings';
import Breakouts from '/imports/api/breakouts';
import Meetings from '/imports/api/meetings';
import Settings from '/imports/ui/services/settings';
import Auth from '/imports/ui/services/auth/index';
import deviceInfo from '/imports/utils/deviceInfo';

View File

@ -1,6 +1,7 @@
import styled from 'styled-components';
import { barsPadding } from '/imports/ui/stylesheets/styled-components/general';
import { FlexColumn } from '/imports/ui/stylesheets/styled-components/placeholders';
import { colorBackground } from '/imports/ui/stylesheets/styled-components/palette';
const CaptionsWrapper = styled.div`
height: auto;
@ -17,7 +18,7 @@ const ActionsBar = styled.section`
`;
const Layout = styled(FlexColumn)`
background-color: #06172a;
background-color: ${colorBackground};
`;
export default {

View File

@ -4,7 +4,7 @@ import { withModalMounter } from '/imports/ui/components/modal/service';
import browserInfo from '/imports/utils/browserInfo';
import getFromUserSettings from '/imports/ui/services/users-settings';
import AudioModal from './component';
import Meetings from '/imports/ui/local-collections/meetings-collection/meetings';
import Meetings from '/imports/api/meetings';
import Auth from '/imports/ui/services/auth';
import lockContextContainer from '/imports/ui/components/lock-viewers/context/container';
import AudioError from '/imports/ui/services/audio-manager/error-codes';

View File

@ -4,7 +4,7 @@ import Modal from '/imports/ui/components/modal/simple/component';
import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
import {
colorPrimary,
colorBackground,
colorGrayDark,
} from '/imports/ui/stylesheets/styled-components/palette';
import {
mdPaddingY,
@ -113,7 +113,7 @@ const Title = styled.h2`
text-align: center;
font-weight: 400;
font-size: 1.3rem;
color: ${colorBackground};
color: ${colorGrayDark};
white-space: normal;
@media ${smallOnly} {

View File

@ -5,7 +5,7 @@ import { Session } from 'meteor/session';
import { withModalMounter } from '/imports/ui/components/modal/service';
import { injectIntl, defineMessages } from 'react-intl';
import _ from 'lodash';
import Breakouts from '/imports/ui/local-collections/breakouts-collection/breakouts';
import Breakouts from '/imports/api/breakouts';
import AppService from '/imports/ui/components/app/service';
import { notify } from '/imports/ui/services/notification';
import getFromUserSettings from '/imports/ui/services/users-settings';

View File

@ -1,8 +1,8 @@
import Users from '/imports/ui/local-collections/users-collection/users';
import Users from '/imports/api/users';
import Auth from '/imports/ui/services/auth';
import { debounce, throttle } from 'lodash';
import AudioManager from '/imports/ui/services/audio-manager';
import Meetings from '/imports/ui/local-collections/meetings-collection/meetings';
import Meetings from '/imports/api/meetings';
import { makeCall } from '/imports/ui/services/api';
import VoiceUsers from '/imports/api/voice-users';
import logger from '/imports/startup/client/logger';

View File

@ -17,7 +17,7 @@ class AuthenticatedHandler extends Component {
}
static updateStatus(status, lastStatus) {
return status.retryCount > 0 && lastStatus !== STATUS_CONNECTING ? status.status : lastStatus;
return lastStatus !== STATUS_CONNECTING ? status.status : lastStatus;
}
static addReconnectObservable() {
@ -27,6 +27,7 @@ class AuthenticatedHandler extends Component {
lastStatus = AuthenticatedHandler.updateStatus(Meteor.status(), lastStatus);
if (AuthenticatedHandler.shouldAuthenticate(Meteor.status(), lastStatus)) {
Session.set('userWillAuth', true);
Auth.authenticate(true);
lastStatus = Meteor.status().status;
}

View File

@ -7,7 +7,6 @@ import PropTypes from 'prop-types';
import AudioService from '../audio/service';
import VideoService from '../video-provider/service';
import { screenshareHasEnded } from '/imports/ui/components/screenshare/service';
import UserListService from '/imports/ui/components/user-list/service';
import Styled from './styles';
const intlMessages = defineMessages({
@ -102,6 +101,7 @@ class BreakoutJoinConfirmation extends Component {
isFreeJoin,
voiceUserJoined,
requestJoinURL,
amIPresenter,
} = this.props;
const { selectValue } = this.state;
@ -122,7 +122,7 @@ class BreakoutJoinConfirmation extends Component {
VideoService.storeDeviceIds();
VideoService.exitVideo();
if (UserListService.amIPresenter()) screenshareHasEnded();
if (amIPresenter) screenshareHasEnded();
if (url === '') {
logger.error({
logCode: 'breakoutjoinconfirmation_redirecting_to_url',

View File

@ -1,17 +1,23 @@
import React from 'react';
import React, { useContext } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import Breakouts from '/imports/ui/local-collections/breakouts-collection/breakouts';
import Breakouts from '/imports/api/breakouts';
import Auth from '/imports/ui/services/auth';
import { makeCall } from '/imports/ui/services/api';
import breakoutService from '/imports/ui/components/breakout-room/service';
import AudioManager from '/imports/ui/services/audio-manager';
import BreakoutJoinConfirmationComponent from './component';
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
const BreakoutJoinConfirmationContrainer = (props) => (
<BreakoutJoinConfirmationComponent
const BreakoutJoinConfirmationContrainer = (props) => {
const usingUsersContext = useContext(UsersContext);
const { users } = usingUsersContext;
const amIPresenter = users[Auth.meetingID][Auth.userID].presenter;
return <BreakoutJoinConfirmationComponent
{...props}
amIPresenter={amIPresenter}
/>
);
};
const getURL = (breakoutId) => {
const currentUserId = Auth.userID;

View File

@ -10,7 +10,6 @@ import BreakoutRoomContainer from './breakout-remaining-time/container';
import VideoService from '/imports/ui/components/video-provider/service';
import { PANELS, ACTIONS } from '../layout/enums';
import { screenshareHasEnded } from '/imports/ui/components/screenshare/service';
import UserListService from '/imports/ui/components/user-list/service';
import AudioManager from '/imports/ui/services/audio-manager';
import Settings from '/imports/ui/services/settings';
@ -249,6 +248,7 @@ class BreakoutRoom extends PureComponent {
const {
isMicrophoneUser,
amIModerator,
amIPresenter,
intl,
isUserInBreakoutRoom,
exitAudio,
@ -323,7 +323,7 @@ class BreakoutRoom extends PureComponent {
}, 'joining breakout room closed audio in the main room');
VideoService.storeDeviceIds();
VideoService.exitVideo();
if (UserListService.amIPresenter()) screenshareHasEnded();
if (amIPresenter) screenshareHasEnded();
}}
disabled={disable}
/>

View File

@ -1,15 +1,23 @@
import React from 'react';
import React, { useContext } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import AudioService from '/imports/ui/components/audio/service';
import AudioManager from '/imports/ui/services/audio-manager';
import BreakoutComponent from './component';
import Service from './service';
import { layoutDispatch } from '../layout/context';
import Auth from '/imports/ui/services/auth';
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
const BreakoutContainer = (props) => {
const layoutContextDispatch = layoutDispatch();
const usingUsersContext = useContext(UsersContext);
const { users } = usingUsersContext;
const amIPresenter = users[Auth.meetingID][Auth.userID].presenter;
return <BreakoutComponent {...{ layoutContextDispatch, ...props }} />;
return <BreakoutComponent
amIPresenter={amIPresenter}
{...{ layoutContextDispatch, ...props }}
/>;
};
export default withTracker((props) => {

View File

@ -1,9 +1,9 @@
import Breakouts from '/imports/ui/local-collections/breakouts-collection/breakouts';
import Breakouts from '/imports/api/breakouts';
import { MeetingTimeRemaining } from '/imports/api/meetings';
import Meetings from '/imports/ui/local-collections/meetings-collection/meetings';
import Meetings from '/imports/api/meetings';
import { makeCall } from '/imports/ui/services/api';
import Auth from '/imports/ui/services/auth';
import Users from '/imports/ui/local-collections/users-collection/users';
import Users from '/imports/api/users';
import UserListService from '/imports/ui/components/user-list/service';
import fp from 'lodash/fp';

View File

@ -161,6 +161,7 @@ export default class ButtonBase extends React.Component {
'animations',
'small',
'full',
'iconRight',
];
return (

View File

@ -155,11 +155,6 @@ export default class Button extends BaseButton {
const remainingProps = this._cleanProps(otherProps);
/* TODO: We can change this and make the button with flexbox to avoid html
changes */
const renderLeftFuncName = !iconRight ? 'renderIcon' : 'renderLabel';
const renderRightFuncName = !iconRight ? 'renderLabel' : 'renderIcon';
return (
<Styled.Button
size={size}
@ -168,10 +163,11 @@ export default class Button extends BaseButton {
circle={circle}
block={block}
className={className}
iconRight={iconRight}
{...remainingProps}
>
{this[renderLeftFuncName]()}
{this[renderRightFuncName]()}
{this.renderIcon()}
{this.renderLabel()}
</Styled.Button>
);
}

View File

@ -599,9 +599,55 @@ const ButtonSpan = styled.span`
`;
const Button = styled(BaseButton)`
border: none;
overflow: visible;
display: inline-block;
border-radius: ${borderSize};
font-weight: ${btnFontWeight};
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: middle;
cursor: pointer;
user-select: none;
&:-moz-focusring {
outline: none;
}
&:hover,
&:focus {
outline: transparent;
outline-style: dotted;
outline-width: ${borderSize};
text-decoration: none;
}
&:active,
&:focus {
outline: transparent;
outline-width: ${borderSize};
outline-style: solid;
}
&:active {
background-image: none;
}
&[aria-disabled="true"] {
cursor: not-allowed;
opacity: .65;
box-shadow: none;
}
&,
&:active {
&:focus {
span:first-of-type::before {
border-radius: ${borderSize};
}
}
}
${({ size }) => size === 'sm' && `
font-size: calc(${fontSizeSmall} * .85);
@ -967,6 +1013,11 @@ const Button = styled(BaseButton)`
display: block;
width: 100%;
`}
${({ iconRight }) => iconRight && `
display: flex;
flex-direction: row-reverse;
`}
`;
export default {

View File

@ -1,6 +1,6 @@
import _ from 'lodash';
import Captions from '/imports/api/captions';
import Users from '/imports/ui/local-collections/users-collection/users';
import Users from '/imports/api/users';
import Auth from '/imports/ui/services/auth';
import { makeCall } from '/imports/ui/services/api';
import { Meteor } from 'meteor/meteor';

View File

@ -4,7 +4,7 @@ import { borderSize, borderSizeLarge } from '/imports/ui/stylesheets/styled-comp
import {
colorWhite,
colorLink,
colorBackground,
colorGrayDark,
colorGrayLighter,
colorGrayLabel,
colorPrimary,
@ -29,7 +29,7 @@ const Title = styled(HeaderElipsis)`
text-align: center;
font-weight: 400;
font-size: 1.3rem;
color: ${colorBackground};
color: ${colorGrayDark};
white-space: normal;
flex: 1;
margin: 0;

View File

@ -1,7 +1,7 @@
import React, { useContext } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import Auth from '/imports/ui/services/auth';
import Meetings from '/imports/ui/local-collections/meetings-collection/meetings';
import Meetings from '/imports/api/meetings';
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
import ChatDropdown from './component';

View File

@ -2,8 +2,8 @@ import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import Auth from '/imports/ui/services/auth';
import { UsersTyping } from '/imports/api/group-chat-msg';
import Users from '/imports/ui/local-collections/users-collection/users';
import Meetings from '/imports/ui/local-collections/meetings-collection/meetings';
import Users from '/imports/api/users';
import Meetings from '/imports/api/meetings';
import TypingIndicator from './component';
const CHAT_CONFIG = Meteor.settings.public.chat;

View File

@ -1,5 +1,5 @@
import Users from '/imports/api/users';
import Meetings from '/imports/ui/local-collections/meetings-collection/meetings';
import Meetings from '/imports/api/meetings';
import GroupChat from '/imports/api/group-chat';
import Auth from '/imports/ui/services/auth';
import UnreadMessages from '/imports/ui/services/unread-messages';

View File

@ -5,7 +5,7 @@ import { UsersContext } from '../users-context/context';
import { makeCall } from '/imports/ui/services/api';
import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
import Auth from '/imports/ui/services/auth';
import CollectionEventsBroker from '/imports/ui/services/collection-hooks-callbacks/collection-hooks-callbacks';
import CollectionEventsBroker from '/imports/ui/services/LiveDataEventBroker/LiveDataEventBroker';
let prevUserData = {};
let currentUserData = {};

View File

@ -3,7 +3,7 @@ import React, {
useReducer,
} from 'react';
import Users from '/imports/ui/local-collections/users-collection/users';
import Users from '/imports/api/users';
import Auth from '/imports/ui/services/auth';
import Storage from '/imports/ui/services/storage/session';
import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';

View File

@ -1,6 +1,6 @@
import { useContext, useEffect } from 'react';
import { CurrentUser } from '/imports/api/users';
import Users from '/imports/ui/local-collections/users-collection/users';
import Users from '/imports/api/users';
import UsersPersistentData from '/imports/api/users-persistent-data';
import { UsersContext, ACTIONS } from './context';
import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';

View File

@ -46,6 +46,7 @@ const SignalBars = styled.div`
const Bar = styled.div`
width: 20%;
border-radius: .46875em;
`;
const FirstBar = styled(Bar)`

View File

@ -6,6 +6,7 @@ import Icon from '/imports/ui/components/connection-status/icon/component';
import Switch from '/imports/ui/components/switch/component';
import Service from '../service';
import Styled from './styles';
import ConnectionStatusHelper from '../status-helper/container';
const NETWORK_MONITORING_INTERVAL_MS = 2000;
const MIN_TIMEOUT = 3000;
@ -91,6 +92,42 @@ const intlMessages = defineMessages({
id: 'app.connection-status.lostPackets',
description: 'Number of lost packets',
},
audioUploadRate: {
id: 'app.connection-status.audioUploadRate',
description: 'Label for audio current upload rate',
},
audioDownloadRate: {
id: 'app.connection-status.audioDownloadRate',
description: 'Label for audio current download rate',
},
videoUploadRate: {
id: 'app.connection-status.videoUploadRate',
description: 'Label for video current upload rate',
},
videoDownloadRate: {
id: 'app.connection-status.videoDownloadRate',
description: 'Label for video current download rate',
},
connectionStats: {
id: 'app.connection-status.connectionStats',
description: 'Label for Connection Stats tab',
},
myLogs: {
id: 'app.connection-status.myLogs',
description: 'Label for My Logs tab',
},
sessionLogs: {
id: 'app.connection-status.sessionLogs',
description: 'Label for Session Logs tab',
},
next: {
id: 'app.connection-status.next',
description: 'Label for the next page of the connection stats tab',
},
prev: {
id: 'app.connection-status.prev',
description: 'Label for the previous page of the connection stats tab',
},
});
const propTypes = {
@ -121,6 +158,8 @@ class ConnectionStatusComponent extends PureComponent {
this.help = Service.getHelp();
this.state = {
selectedTab: '1',
dataPage: '1',
dataSaving: props.dataSaving,
hasNetworkData: false,
networkData: {
@ -142,9 +181,10 @@ class ConnectionStatusComponent extends PureComponent {
};
this.displaySettingsStatus = this.displaySettingsStatus.bind(this);
this.rateInterval = null;
this.audioLabel = (intl.formatMessage(intlMessages.audioLabel)).charAt(0);
this.videoLabel = (intl.formatMessage(intlMessages.videoLabel)).charAt(0);
this.audioUploadLabel = intl.formatMessage(intlMessages.audioUploadRate);
this.audioDownloadLabel = intl.formatMessage(intlMessages.audioDownloadRate);
this.videoUploadLabel = intl.formatMessage(intlMessages.videoUploadRate);
this.videoDownloadLabel = intl.formatMessage(intlMessages.videoDownloadRate);
}
async componentDidMount() {
@ -222,13 +262,13 @@ class ConnectionStatusComponent extends PureComponent {
const { intl } = this.props;
return (
<Styled.Item data-test="connectionStatusItemEmpty">
<Styled.Item last data-test="connectionStatusItemEmpty">
<Styled.Left>
<Styled.Name>
<Styled.FullName>
<Styled.Text>
{intl.formatMessage(intlMessages.empty)}
</Styled.Text>
</Styled.Name>
</Styled.FullName>
</Styled.Left>
</Styled.Item>
);
@ -280,15 +320,23 @@ class ConnectionStatusComponent extends PureComponent {
intl,
} = this.props;
const { selectedTab } = this.state;
if (isConnectionStatusEmpty(connectionStatus)) return this.renderEmpty();
return connectionStatus.map((conn, index) => {
let connections = connectionStatus;
if (selectedTab === '2') {
connections = connections.filter(conn => conn.you);
if (isConnectionStatusEmpty(connections)) return this.renderEmpty();
}
return connections.map((conn, index) => {
const dateTime = new Date(conn.timestamp);
return (
<Styled.Item
key={index}
even={(index + 1) % 2 === 0}
last={(index + 1) === connections.length}
data-test="connectionStatusItemUser"
>
<Styled.Left>
@ -411,13 +459,15 @@ class ConnectionStatusComponent extends PureComponent {
}
const {
audioLabel,
videoLabel,
audioUploadLabel,
audioDownloadLabel,
videoUploadLabel,
videoDownloadLabel,
} = this;
const { intl } = this.props;
const { intl, closeModal } = this.props;
const { networkData } = this.state;
const { networkData, dataSaving, dataPage } = this.state;
const {
audioCurrentUploadRate,
@ -447,29 +497,103 @@ class ConnectionStatusComponent extends PureComponent {
}
}
function handlePaginationClick(action) {
if (action === 'next') {
this.setState({ dataPage: '2' });
}
else {
this.setState({ dataPage: '1' });
}
}
return (
<Styled.NetworkDataContainer>
<Styled.NetworkData>
{`${audioLabel}: ${audioCurrentUploadRate} k`}
</Styled.NetworkData>
<Styled.NetworkData>
{`${audioLabel}: ${audioCurrentDownloadRate} k`}
</Styled.NetworkData>
<Styled.NetworkData>
{`${videoLabel}: ${videoCurrentUploadRate} k`}
</Styled.NetworkData>
<Styled.NetworkData>
{`${videoLabel}: ${videoCurrentDownloadRate} k`}
</Styled.NetworkData>
<Styled.NetworkData>
{`${intl.formatMessage(intlMessages.jitter)}: ${jitter} ms`}
</Styled.NetworkData>
<Styled.NetworkData>
{`${intl.formatMessage(intlMessages.lostPackets)}: ${packetsLost}`}
</Styled.NetworkData>
<Styled.NetworkData>
{`${intl.formatMessage(intlMessages.usingTurn)}: ${isUsingTurn}`}
</Styled.NetworkData>
<Styled.Prev>
<Styled.ButtonLeft
role="button"
disabled={dataPage === '1'}
aria-label={`${intl.formatMessage(intlMessages.prev)} ${intl.formatMessage(intlMessages.ariaTitle)}`}
onClick={handlePaginationClick.bind(this, 'prev')}
>
<Styled.Chevron
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</Styled.Chevron>
</Styled.ButtonLeft>
</Styled.Prev>
<Styled.Helper page={dataPage}>
<ConnectionStatusHelper closeModal={() => closeModal(dataSaving, intl)} />
</Styled.Helper>
<Styled.NetworkDataContent page={dataPage}>
<Styled.DataColumn>
<Styled.NetworkData>
<div>{`${audioUploadLabel}`}</div>
<div>{`${audioCurrentUploadRate}k ↑`}</div>
</Styled.NetworkData>
<Styled.NetworkData>
<div>{`${videoUploadLabel}`}</div>
<div>{`${videoCurrentUploadRate}k ↑`}</div>
</Styled.NetworkData>
<Styled.NetworkData>
<div>{`${intl.formatMessage(intlMessages.jitter)}`}</div>
<div>{`${jitter} ms`}</div>
</Styled.NetworkData>
<Styled.NetworkData>
<div>{`${intl.formatMessage(intlMessages.usingTurn)}`}</div>
<div>{`${isUsingTurn}`}</div>
</Styled.NetworkData>
</Styled.DataColumn>
<Styled.DataColumn>
<Styled.NetworkData>
<div>{`${audioDownloadLabel}`}</div>
<div>{`${audioCurrentDownloadRate}k ↓`}</div>
</Styled.NetworkData>
<Styled.NetworkData>
<div>{`${videoDownloadLabel}`}</div>
<div>{`${videoCurrentDownloadRate}k ↓`}</div>
</Styled.NetworkData>
<Styled.NetworkData>
<div>{`${intl.formatMessage(intlMessages.lostPackets)}`}</div>
<div>{`${packetsLost}`}</div>
</Styled.NetworkData>
<Styled.NetworkData invisible>
<div>Content Hidden</div>
<div>0</div>
</Styled.NetworkData>
</Styled.DataColumn>
</Styled.NetworkDataContent>
<Styled.Next>
<Styled.ButtonRight
role="button"
disabled={dataPage === '2'}
aria-label={`${intl.formatMessage(intlMessages.next)} ${intl.formatMessage(intlMessages.ariaTitle)}`}
onClick={handlePaginationClick.bind(this, 'next')}
>
<Styled.Chevron
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</Styled.Chevron>
</Styled.ButtonRight>
</Styled.Next>
</Styled.NetworkDataContainer>
);
}
@ -503,13 +627,69 @@ class ConnectionStatusComponent extends PureComponent {
);
}
/**
* The navigation bar.
* @returns {Object} The component to be renderized.
*/
renderNavigation() {
const { intl } = this.props;
const handleTabClick = (event) => {
const activeTabElement = document.querySelector('.activeConnectionStatusTab');
const { target } = event;
if (activeTabElement) {
activeTabElement.classList.remove('activeConnectionStatusTab');
}
target.classList.add('activeConnectionStatusTab');
this.setState({
selectedTab: target.dataset.tab,
});
}
return (
<Styled.Navigation>
<div
data-tab="1"
className="activeConnectionStatusTab"
onClick={handleTabClick}
onKeyDown={handleTabClick}
role="button"
>
{intl.formatMessage(intlMessages.connectionStats)}
</div>
<div
data-tab="2"
onClick={handleTabClick}
onKeyDown={handleTabClick}
role="button"
>
{intl.formatMessage(intlMessages.myLogs)}
</div>
{Service.isModerator()
&& (
<div
data-tab="3"
onClick={handleTabClick}
onKeyDown={handleTabClick}
role="button"
>
{intl.formatMessage(intlMessages.sessionLogs)}
</div>
)
}
</Styled.Navigation>
);
}
render() {
const {
closeModal,
intl,
} = this.props;
const { dataSaving } = this.state;
const { dataSaving, selectedTab } = this.state;
return (
<Styled.ConnectionStatusModal
@ -523,25 +703,18 @@ class ConnectionStatusComponent extends PureComponent {
{intl.formatMessage(intlMessages.title)}
</Styled.Title>
</Styled.Header>
<Styled.Description>
{intl.formatMessage(intlMessages.description)}
{' '}
{this.help
&& (
<a href={this.help} target="_blank" rel="noopener noreferrer">
{`(${intl.formatMessage(intlMessages.more)})`}
</a>
)
{this.renderNavigation()}
<Styled.Main>
<Styled.Body>
{selectedTab === '1'
? this.renderNetworkData()
: this.renderConnections()
}
</Styled.Body>
{selectedTab === '1' &&
this.renderCopyDataButton()
}
</Styled.Description>
{this.renderNetworkData()}
{this.renderCopyDataButton()}
{this.renderDataSaving()}
<Styled.Content>
<Styled.Wrapper>
{this.renderConnections()}
</Styled.Wrapper>
</Styled.Content>
</Styled.Main>
</Styled.Container>
</Styled.ConnectionStatusModal>
);

View File

@ -5,28 +5,37 @@ import {
colorGray,
colorGrayDark,
colorGrayLabel,
colorGrayLightest,
colorPrimary,
} from '/imports/ui/stylesheets/styled-components/palette';
import {
smPaddingX,
smPaddingY,
lgPaddingY,
lgPaddingX,
modalMargin,
titlePositionLeft,
mdPaddingX,
borderSizeLarge,
jumboPaddingY,
} from '/imports/ui/stylesheets/styled-components/general';
import {
fontSizeSmall,
fontSizeLarge,
fontSizeXL,
} from '/imports/ui/stylesheets/styled-components/typography';
import { hasPhoneDimentions } from '/imports/ui/stylesheets/styled-components/breakpoints';
import {
hasPhoneDimentions,
mediumDown,
hasPhoneWidth,
} from '/imports/ui/stylesheets/styled-components/breakpoints';
const Item = styled.div`
display: flex;
width: 100%;
height: 4rem;
border-bottom: 1px solid ${colorGrayLightest};
${({ even }) => even && `
background-color: ${colorOffWhite};
${({ last }) => last && `
border: none;
`}
`;
@ -37,11 +46,19 @@ const Left = styled.div`
`;
const Name = styled.div`
display: grid;
width: 100%;
display: flex;
width: 27.5%;
height: 100%;
align-items: center;
justify-content: center;
justify-content: flex-start;
@media ${hasPhoneDimentions} {
width: 100%;
}
`;
const FullName = styled(Name)`
width: 100%;
`;
const Text = styled.div`
@ -51,8 +68,13 @@ const Text = styled.div`
text-overflow: ellipsis;
${({ offline }) => offline && `
font-style: italic;
font-style: italic;
`}
[dir="rtl"] & {
padding: 0;
padding-right: .5rem;
}
`;
const ToggleLabel = styled.span`
@ -65,7 +87,6 @@ const ToggleLabel = styled.span`
const Avatar = styled.div`
display: flex;
width: 4rem;
height: 100%;
justify-content: center;
align-items: center;
@ -87,6 +108,7 @@ const Time = styled.div`
align-items: center;
width: 100%;
height: 100%;
justify-content: flex-end;
`;
const DataSaving = styled.div`
@ -149,30 +171,46 @@ const Label = styled.span`
const NetworkDataContainer = styled.div`
width: 100%;
height: 100%;
display: flex;
background-color: ${colorOffWhite};
@media ${mediumDown} {
justify-content: space-between;
}
`;
const NetworkData = styled.div`
float: left;
font-size: ${fontSizeSmall};
margin-left: ${smPaddingX};
${({ invisible }) => invisible && `
visibility: hidden;
`}
& :first-child {
font-weight: 600;
}
`;
const CopyContainer = styled.div`
width: 100%;
display: flex;
justify-content: flex-end;
border: none;
border-top: 1px solid ${colorOffWhite};
padding: ${jumboPaddingY} 0 0;
`;
const ConnectionStatusModal = styled(Modal)`
padding: ${smPaddingY};
padding: 1.5rem;
border-radius: 7.5px;
@media ${hasPhoneDimentions} {
padding: 1rem;
}
`;
const Container = styled.div`
margin: 0 ${modalMargin} ${lgPaddingX};
@media ${hasPhoneDimentions} {
margin: 0 1rem;
}
padding: 0 calc(${mdPaddingX} / 2 + ${borderSizeLarge});
`;
const Header = styled.div`
@ -184,16 +222,14 @@ const Header = styled.div`
`;
const Title = styled.h2`
left: ${titlePositionLeft};
right: auto;
color: ${colorGrayDark};
font-weight: bold;
font-size: ${fontSizeLarge};
text-align: center;
font-weight: 500;
font-size: ${fontSizeXL};
text-align: left;
margin: 0;
[dir="rtl"] & {
left: auto;
right: ${titlePositionLeft};
text-align: right;
}
`;
@ -219,14 +255,11 @@ const Status = styled.div`
`;
const Copy = styled.span`
float: right;
text-decoration: underline;
cursor: pointer;
margin-right: ${smPaddingX};
color: ${colorPrimary};
[dir="rtl"] & {
margin-left: ${smPaddingX};
float: left;
&:hover {
text-decoration: underline;
}
${({ disabled }) => disabled && `
@ -234,6 +267,175 @@ const Copy = styled.span`
`}
`;
const Helper = styled.div`
width: 12.5rem;
height: 100%;
border-radius: .5rem;
background-color: ${colorOffWhite};
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
@media ${mediumDown} {
${({ page }) => page === '1'
? 'display: flex;'
: 'display: none;'}
}
`;
const NetworkDataContent = styled.div`
margin: 0;
display: flex;
justify-content: space-around;
flex-grow: 1;
@media ${mediumDown} {
${({ page }) => page === '2'
? 'display: flex;'
: 'display: none;'}
}
`;
const DataColumn = styled.div`
display: flex;
flex-direction: column;
justify-content: space-between;
@media ${hasPhoneWidth} {
flex-grow: 1;
}
`;
const Main = styled.div`
height: 19.5rem;
display: flex;
flex-direction: column;
`;
const Body = styled.div`
padding: ${jumboPaddingY} 0;
margin: 0;
flex-grow: 1;
overflow: auto;
position: relative;
`;
const Navigation = styled.div`
display: flex;
border: none;
border-bottom: 1px solid ${colorOffWhite};
user-select: none;
overflow-y: auto;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
& :not(:last-child) {
margin: 0;
margin-right: ${lgPaddingX};
}
.activeConnectionStatusTab {
border: none;
border-bottom: 2px solid ${colorPrimary};
color: ${colorPrimary};
}
& * {
cursor: pointer;
white-space: nowrap;
}
[dir="rtl"] & {
& :not(:last-child) {
margin: 0;
margin-left: ${lgPaddingX};
}
}
`;
const Prev = styled.div`
display: none;
margin: 0 .5rem 0 .25rem;
@media ${mediumDown} {
display: flex;
flex-direction: column;
justify-content: center;
}
@media ${hasPhoneWidth} {
margin: 0;
}
`;
const Next = styled(Prev)`
margin: 0 .25rem 0 .5rem;
@media ${hasPhoneWidth} {
margin: 0;
}
`;
const Button = styled.button`
flex: 0;
margin: 0;
padding: 0;
border: none;
background: none;
color: inherit;
border-radius: 50%;
cursor: pointer;
&:disabled {
cursor: not-allowed;
opacity: .5;
}
&:hover {
opacity: .75;
}
&:focus {
outline: none;
}
@media ${hasPhoneWidth} {
position: absolute;
bottom: 0;
padding: .25rem;
}
`;
const ButtonLeft = styled(Button)`
left: calc(50% - 2rem);
[dir="rtl"] & {
left: calc(50%);
}
`;
const ButtonRight = styled(Button)`
right: calc(50% - 2rem);
[dir="rtl"] & {
right: calc(50%);
}
`;
const Chevron = styled.svg`
display: flex;
width: 1rem;
height: 1rem;
[dir="rtl"] & {
transform: rotate(180deg);
}
`;
export default {
Item,
Left,
@ -262,4 +464,16 @@ export default {
Wrapper,
Status,
Copy,
Helper,
NetworkDataContent,
Main,
Body,
Navigation,
FullName,
DataColumn,
Prev,
Next,
ButtonLeft,
ButtonRight,
Chevron,
};

View File

@ -1,6 +1,6 @@
import { defineMessages } from 'react-intl';
import ConnectionStatus from '/imports/api/connection-status';
import Users from '/imports/ui/local-collections/users-collection/users';
import Users from '/imports/api/users';
import UsersPersistentData from '/imports/api/users-persistent-data';
import Auth from '/imports/ui/services/auth';
import Settings from '/imports/ui/services/settings';
@ -570,6 +570,7 @@ const calculateBitsPerSecondFromMultipleData = (currentData, previousData) => {
};
export default {
isModerator,
getConnectionStatus,
getStats,
getHelp,

View File

@ -0,0 +1,89 @@
import React, { Fragment, PureComponent } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import Styled from './styles';
import { withModalMounter } from '/imports/ui/components/modal/service';
import Icon from '/imports/ui/components/connection-status/icon/component';
import SettingsMenuContainer from '/imports/ui/components/settings/container';
const intlMessages = defineMessages({
label: {
id: 'app.connection-status.label',
description: 'Connection status label',
},
settings: {
id: 'app.connection-status.settings',
description: 'Connection settings label',
},
});
class ConnectionStatusIcon extends PureComponent {
renderIcon(level = 'normal') {
return(
<Styled.IconWrapper>
<Icon
level={level}
grayscale
/>
</Styled.IconWrapper>
);
}
openAdjustSettings() {
const {
closeModal,
mountModal,
} = this.props;
closeModal();
mountModal(<SettingsMenuContainer selectedTab={2} />);
}
render() {
const {
intl,
stats,
} = this.props;
let color;
switch (stats) {
case 'warning':
color = 'success';
break;
case 'danger':
color = 'warning';
break;
case 'critical':
color = 'danger';
break;
default:
color = 'success';
}
const level = stats ? stats : 'normal';
return (
<Fragment>
<Styled.StatusIconWrapper color={color}>
{this.renderIcon(level)}
</Styled.StatusIconWrapper>
<Styled.Label>
{intl.formatMessage(intlMessages.label)}
</Styled.Label>
{level === 'critical' || level === 'danger'
&& (
<div>
<Styled.Settings
onClick={this.openAdjustSettings.bind(this)}
role="button"
>
{intl.formatMessage(intlMessages.settings)}
</Styled.Settings>
</div>
)
}
</Fragment>
);
}
}
export default withModalMounter(injectIntl(ConnectionStatusIcon));

View File

@ -0,0 +1,12 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import ConnectionStatusService from '../service';
import ConnectionStatusIconComponent from './component';
const connectionStatusIconContainer = props => <ConnectionStatusIconComponent {...props} />;
export default withTracker(() => {
return {
stats: ConnectionStatusService.getStats(),
};
})(connectionStatusIconContainer);

View File

@ -0,0 +1,45 @@
import styled from 'styled-components';
import {
colorPrimary,
colorSuccess,
colorWarning,
colorDanger,
} from '/imports/ui/stylesheets/styled-components/palette';
const StatusIconWrapper = styled.div`
border-radius: 50%;
padding: 1.5rem;
${(color) => {
let bgColor = colorSuccess;
backgroundColor = color === 'warning' ? colorWarning : bgColor;
backgroundColor = color === 'danger' ? colorDanger : bgColor;
return `background-color: ${bgColor};`
}}
`;
const IconWrapper = styled.div`
width: 2.25rem;
height: 2.25rem;
`;
const Label = styled.div`
font-weight: 600;
margin: .25rem 0 .5rem;
`;
const Settings = styled.span`
color: ${colorPrimary};
cursor: pointer;
&:hover {
text-decoration: underline;
}
`;
export default {
StatusIconWrapper,
IconWrapper,
Label,
Settings,
};

View File

@ -1,6 +1,6 @@
import Users from '/imports/ui/local-collections/users-collection/users';
import Users from '/imports/api/users';
import Auth from '/imports/ui/services/auth';
import Meetings from '/imports/ui/local-collections/meetings-collection/meetings';
import Meetings from '/imports/api/meetings';
const getMeetingTitle = () => {
const meeting = Meetings.findOne({

View File

@ -24,6 +24,7 @@ class JoinHandler extends Component {
this.state = {
joined: false,
hasAlreadyJoined: false,
};
}
@ -38,8 +39,8 @@ class JoinHandler extends Component {
connected,
status,
} = Meteor.status();
if (status === 'connecting') {
const { hasAlreadyJoined } = this.state;
if (status === 'connecting' && !hasAlreadyJoined) {
this.setState({ joined: false });
}
@ -83,6 +84,7 @@ class JoinHandler extends Component {
}
async fetchToken() {
const { hasAlreadyJoined } = this.state;
if (!this._isMounted) return;
const urlParams = new URLSearchParams(window.location.search);
@ -94,7 +96,9 @@ class JoinHandler extends Component {
}
// Old credentials stored in memory were being used when joining a new meeting
Auth.clearCredentials();
if (!hasAlreadyJoined) {
Auth.clearCredentials();
}
const logUserInfo = () => {
const userInfo = window.navigator;
@ -215,7 +219,10 @@ class JoinHandler extends Component {
},
}, 'User faced an error on main.joinRouteHandler.');
}
this.setState({ joined: true });
this.setState({
joined: true,
hasAlreadyJoined: true,
});
}
render() {

View File

@ -1,6 +1,6 @@
import Users from '/imports/api/users';
import Auth from '/imports/ui/services/auth';
import Meetings from '../../../api/meetings';
import Meetings from '/imports/api/meetings';
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;

View File

@ -1,7 +1,7 @@
import React, { useContext } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import { withModalMounter } from '/imports/ui/components/modal/service';
import Meetings from '/imports/ui/local-collections/meetings-collection/meetings';
import Meetings from '/imports/api/meetings';
import Auth from '/imports/ui/services/auth';
import LockViewersComponent from './component';
import { updateLockSettings, updateWebcamsOnlyForModerator } from './service';

View File

@ -1,5 +1,5 @@
import { withTracker } from 'meteor/react-meteor-data';
import Meetings from '/imports/ui/local-collections/meetings-collection/meetings';
import Meetings from '/imports/api/meetings';
import Auth from '/imports/ui/services/auth';
import { LockStruct } from './context';
import { withUsersConsumer } from '/imports/ui/components/components-data/users-context/context';

View File

@ -1,5 +1,5 @@
import { withTracker } from 'meteor/react-meteor-data';
import Meetings from '/imports/ui/local-collections/meetings-collection/meetings';
import Meetings from '/imports/api/meetings';
import Auth from '/imports/ui/services/auth';
import LockViewersNotifyComponent from './component';

View File

@ -1,8 +1,6 @@
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 Auth from '/imports/ui/services/auth';
import Users from '/imports/ui/local-collections/users-collection/users';
import Settings from '/imports/ui/services/settings';
import getFromUserSettings from '/imports/ui/services/users-settings';
import { ACTIONS } from '../layout/enums';
@ -21,9 +19,6 @@ const getPresentationInfo = () => {
};
};
const isUserPresenter = () => Users.findOne({ userId: Auth.userID },
{ fields: { presenter: 1 } }).presenter;
function shouldShowWhiteboard() {
return true;
}
@ -91,7 +86,6 @@ export default {
shouldShowScreenshare,
shouldShowExternalVideo,
shouldShowOverlay,
isUserPresenter,
isVideoBroadcasting,
toggleSwapLayout,
shouldEnableSwapLayout,

View File

@ -10,8 +10,8 @@ import logoutRouteHandler from '/imports/utils/logoutRouteHandler';
import Rating from './rating/component';
import Styled from './styles';
import logger from '/imports/startup/client/logger';
import Users from '/imports/ui/local-collections/users-collection/users';
import Meetings from '/imports/ui/local-collections/meetings-collection/meetings';
import Users from '/imports/api/users';
import Meetings from '/imports/api/meetings';
import AudioManager from '/imports/ui/services/audio-manager';
import { meetingIsBreakout } from '/imports/ui/components/app/service';

View File

@ -1,7 +1,7 @@
import React, { useContext } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import Meetings from '/imports/ui/local-collections/meetings-collection/meetings';
import Users from '/imports/ui/local-collections/users-collection/users';
import Meetings from '/imports/api/meetings';
import Users from '/imports/api/users';
import Auth from '/imports/ui/services/auth';
import { withModalMounter } from '/imports/ui/components/modal/service';
import { makeCall } from '/imports/ui/services/api';

View File

@ -69,7 +69,7 @@ class RemoveUserModal extends Component {
</Styled.Description>
<Styled.Footer>
<Styled.ConfirmButtom
<Styled.ConfirmButton
color="primary"
label={intl.formatMessage(messages.yesLabel)}
onClick={() => {

View File

@ -69,7 +69,7 @@ const Footer = styled.div`
display:flex;
`;
const ConfirmButtom = styled(Button)`
const ConfirmButton = styled(Button)`
padding-right: ${jumboPaddingY};
padding-left: ${jumboPaddingY};
margin: 0 ${smPaddingX} 0 0;
@ -79,7 +79,7 @@ const ConfirmButtom = styled(Button)`
}
`;
const DismissButtom = styled(ConfirmButtom)`
const DismissButton = styled(ConfirmButton)`
box-shadow: 0 0 0 1px ${colorGray};
`;
@ -91,6 +91,6 @@ export default {
Description,
BanUserCheckBox,
Footer,
ConfirmButtom,
DismissButtom,
ConfirmButton,
DismissButton,
};

View File

@ -81,7 +81,7 @@ class ModalSimple extends Component {
{...otherProps}
>
<Styled.Header hideBorder={hideBorder}>
<Styled.Title>{title}</Styled.Title>
<Styled.Title hasLeftMargin={shouldShowCloseButton}>{title}</Styled.Title>
{shouldShowCloseButton ? (
<Styled.DismissButton
label={intl.formatMessage(intlMessages.modalClose)}

View File

@ -45,6 +45,10 @@ const Title = styled.h1`
font-size: ${fontSizeLarge};
text-align: center;
align-self: flex-end;
${({ hasLeftMargin }) => hasLeftMargin && `
margin-left: 35px;
`}
`;
const DismissButton = styled(Button)`
@ -61,7 +65,7 @@ const Content = styled.div`
overflow: auto;
color: ${colorText};
font-weight: normal;
padding: ${lineHeightComputed} 0;
padding: 0;
`;
export default {

View File

@ -1,7 +1,7 @@
import React, { useContext } from 'react';
import { Meteor } from 'meteor/meteor';
import { withTracker } from 'meteor/react-meteor-data';
import Meetings from '/imports/ui/local-collections/meetings-collection/meetings';
import Meetings from '/imports/api/meetings';
import Auth from '/imports/ui/services/auth';
import getFromUserSettings from '/imports/ui/services/users-settings';
import userListService from '/imports/ui/components/user-list/service';

View File

@ -1,6 +1,6 @@
import Auth from '/imports/ui/services/auth';
import { makeCall } from '/imports/ui/services/api';
import RecordMeetings from '/imports/api/meetings';
import Meetings from '/imports/api/meetings';
const processOutsideToggleRecording = (e) => {
switch (e.data) {
@ -9,7 +9,7 @@ const processOutsideToggleRecording = (e) => {
break;
}
case 'c_recording_status': {
const recordingState = (RecordMeetings.findOne({ meetingId: Auth.meetingID })).recording;
const recordingState = (Meetings.findOne({ meetingId: Auth.meetingID })).recording;
const recordingMessage = recordingState ? 'recordingStarted' : 'recordingStopped';
this.window.parent.postMessage({ response: recordingMessage }, '*');
break;

View File

@ -1,5 +1,5 @@
import Users from '/imports/ui/local-collections/users-collection/users';
import Meetings from '/imports/ui/local-collections/meetings-collection/meetings';
import Users from '/imports/api/users';
import Meetings from '/imports/api/meetings';
import Note from '/imports/api/note';
import { makeCall } from '/imports/ui/services/api';
import Auth from '/imports/ui/services/auth';

View File

@ -5,7 +5,7 @@ import { defineMessages, injectIntl } from 'react-intl';
import _ from 'lodash';
import Auth from '/imports/ui/services/auth';
import { MeetingTimeRemaining } from '/imports/api/meetings';
import Meetings from '/imports/ui/local-collections/meetings-collection/meetings';
import Meetings from '/imports/api/meetings';
import BreakoutRemainingTime from '/imports/ui/components/breakout-room/breakout-remaining-time/container';
import Styled from './styles';
import { layoutSelectInput, layoutDispatch } from '../layout/context';

View File

@ -18,6 +18,7 @@ const PollContainer = ({ ...props }) => {
const usingUsersContext = useContext(UsersContext);
const { users } = usingUsersContext;
const amIPresenter = users[Auth.meetingID][Auth.userID].presenter;
const usernames = {};
@ -25,7 +26,13 @@ const PollContainer = ({ ...props }) => {
usernames[user.userId] = { userId: user.userId, name: user.name };
});
return <Poll {...{ layoutContextDispatch, ...props }} usernames={usernames} />;
return (
<Poll
{...{ layoutContextDispatch, ...props }}
usernames={usernames}
amIPresenter={amIPresenter}
/>
);
};
export default withTracker(() => {
@ -50,7 +57,6 @@ export default withTracker(() => {
return {
currentSlide,
amIPresenter: Service.amIPresenter(),
pollTypes,
startPoll,
startCustomPoll,

View File

@ -1,4 +1,3 @@
import Users from '/imports/ui/local-collections/users-collection/users';
import Auth from '/imports/ui/services/auth';
import { CurrentPoll } from '/imports/api/polls';
import caseInsensitiveReducer from '/imports/utils/caseInsensitiveReducer';
@ -212,10 +211,6 @@ const checkPollType = (
};
export default {
amIPresenter: () => Users.findOne(
{ userId: Auth.userID },
{ fields: { presenter: 1 } },
).presenter,
pollTypes,
currentPoll: () => CurrentPoll.findOne({ meetingId: Auth.meetingID }),
pollAnswerIds,

View File

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { withTracker } from 'meteor/react-meteor-data';
import Users from '/imports/ui/local-collections/users-collection/users';
import Users from '/imports/api/users';
import Auth from '/imports/ui/services/auth';
import PollingService from './service';
import PollService from '/imports/ui/components/poll/service';

View File

@ -20,6 +20,7 @@ import Icon from '/imports/ui/components/icon/component';
import PollingContainer from '/imports/ui/components/polling/container';
import { ACTIONS, LAYOUT_TYPE } from '../layout/enums';
import DEFAULT_VALUES from '../layout/defaultValues';
import { colorBackground } from '/imports/ui/stylesheets/styled-components/palette';
const intlMessages = defineMessages({
presentationLabel: {
@ -186,7 +187,7 @@ class Presentation extends PureComponent {
this.currentPresentationToastId = toast(this.renderCurrentPresentationToast(), {
onClose: () => { this.currentPresentationToastId = null; },
autoClose: shouldCloseToast,
className: "actionToast",
className: 'actionToast',
});
}
}
@ -693,13 +694,14 @@ class Presentation extends PureComponent {
}
renderWhiteboardToolbar(svgDimensions) {
const { currentSlide } = this.props;
const { currentSlide, userIsPresenter } = this.props;
if (!this.isPresentationAccessible()) return null;
return (
<WhiteboardToolbarContainer
whiteboardId={currentSlide.id}
height={svgDimensions.height}
isPresenter={userIsPresenter}
/>
);
}
@ -854,7 +856,7 @@ class Presentation extends PureComponent {
width: presentationBounds.width,
height: presentationBounds.height,
zIndex: fullscreenContext ? presentationBounds.zIndex : undefined,
backgroundColor: '#06172A',
backgroundColor: colorBackground,
}}
>
{isFullscreen && <PollingContainer />}

View File

@ -11,7 +11,7 @@ import Presentation from '/imports/ui/components/presentation/component';
import PresentationToolbarService from './presentation-toolbar/service';
import { UsersContext } from '../components-data/users-context/context';
import Auth from '/imports/ui/services/auth';
import Meetings from '/imports/ui/local-collections/meetings-collection/meetings';
import Meetings from '/imports/api/meetings';
import getFromUserSettings from '/imports/ui/services/users-settings';
import {
layoutSelect,
@ -25,7 +25,7 @@ import { DEVICE_TYPE } from '../layout/enums';
const ROLE_VIEWER = Meteor.settings.public.user.role_viewer;
const PresentationContainer = ({ presentationPodIds, mountPresentation, ...props }) => {
const { layoutSwapped, podId } = props;
const { layoutSwapped } = props;
const cameraDock = layoutSelectInput((i) => i.cameraDock);
const presentation = layoutSelectOutput((i) => i.presentation);
@ -44,8 +44,7 @@ const PresentationContainer = ({ presentationPodIds, mountPresentation, ...props
const usingUsersContext = useContext(UsersContext);
const { users } = usingUsersContext;
const currentUser = users[Auth.meetingID][Auth.userID];
const userIsPresenter = (podId === 'DEFAULT_PRESENTATION_POD') ? currentUser.presenter : props.isPresenter;
const userIsPresenter = currentUser.presenter;
return (
<Presentation
@ -123,7 +122,6 @@ export default withTracker(({ podId }) => {
currentSlide,
slidePosition,
downloadPresentationUri: PresentationService.downloadPresentationUri(podId),
isPresenter: PresentationService.isPresenter(podId),
multiUser: WhiteboardService.hasMultiUserAccess(currentSlide && currentSlide.id, Auth.userID)
&& !layoutSwapped,
presentationIsDownloadable,

View File

@ -1,5 +1,5 @@
import Cursor from '/imports/ui/components/cursor/service';
import Users from '/imports/ui/local-collections/users-collection/users';
import Users from '/imports/api/users';
const getCurrentCursor = (cursorId) => {
const cursor = Cursor.findOne({ _id: cursorId });

View File

@ -1,21 +1,24 @@
import React from 'react';
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { withTracker } from 'meteor/react-meteor-data';
import PresentationService from '/imports/ui/components/presentation/service';
import MediaService from '/imports/ui/components/media/service';
import Service from '/imports/ui/components/actions-bar/service';
import PollService from '/imports/ui/components/poll/service';
import { makeCall } from '/imports/ui/services/api';
import PresentationToolbar from './component';
import PresentationToolbarService from './service';
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
import Auth from '/imports/ui/services/auth';
const POLLING_ENABLED = Meteor.settings.public.poll.enabled;
const PresentationToolbarContainer = (props) => {
const {
userIsPresenter,
layoutSwapped,
} = props;
const usingUsersContext = useContext(UsersContext);
const { users } = usingUsersContext;
const currentUser = users[Auth.meetingID][Auth.userID];
const userIsPresenter = currentUser.presenter;
const { layoutSwapped } = props;
if (userIsPresenter && !layoutSwapped) {
// Only show controls if user is presenter and layout isn't swapped
@ -23,6 +26,7 @@ const PresentationToolbarContainer = (props) => {
return (
<PresentationToolbar
{...props}
amIPresenter={userIsPresenter}
/>
);
}
@ -44,9 +48,7 @@ export default withTracker((params) => {
};
return {
amIPresenter: Service.amIPresenter(),
layoutSwapped: MediaService.getSwapLayout() && MediaService.shouldEnableSwapLayout(),
userIsPresenter: PresentationService.isPresenter(podId),
numberOfSlides: PresentationToolbarService.getNumberOfSlides(podId, presentationId),
nextSlide: PresentationToolbarService.nextSlide,
previousSlide: PresentationToolbarService.previousSlide,
@ -65,9 +67,6 @@ PresentationToolbarContainer.propTypes = {
zoom: PropTypes.number.isRequired,
zoomChanger: PropTypes.func.isRequired,
// Is the user a presenter
userIsPresenter: PropTypes.bool.isRequired,
// Total number of slides in this presentation
numberOfSlides: PropTypes.number.isRequired,

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