Merge remote-tracking branch 'upstream/develop' into develop
This commit is contained in:
commit
56fedc8921
@ -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/).
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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"
|
||||
/>
|
||||
|
||||
<FormattedTime
|
||||
value={lastUpdated}
|
||||
/>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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`} />
|
||||
|
||||
{ userEmojisInPeriod[emoji] }
|
||||
|
||||
<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>
|
||||
);
|
||||
}) }
|
||||
|
@ -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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
{ 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>
|
||||
|
||||
{ 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>
|
||||
|
||||
{ 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>
|
||||
|
||||
{ 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>
|
||||
|
||||
{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`} />
|
||||
|
||||
{ usersEmojisSummary[user.intId][emoji] }
|
||||
|
||||
<FormattedMessage
|
||||
id={emojiConfigs[emoji].intlId}
|
||||
defaultMessage={emojiConfigs[emoji].defaultMessage}
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{ 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>
|
||||
|
||||
{ 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>
|
||||
|
||||
{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>
|
||||
|
||||
{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`} />
|
||||
|
||||
{ usersEmojisSummary[user.intId][emoji] }
|
||||
|
||||
<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>
|
||||
|
||||
{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">
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
202
bbb-learning-dashboard/src/services/UserService.js
Normal file
202
bbb-learning-dashboard/src/services/UserService.js
Normal 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');
|
||||
}
|
@ -8,4 +8,4 @@ module.exports = {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
};
|
||||
|
92
bigbluebutton-html5/client/collection-mirror-initializer.js
Normal file
92
bigbluebutton-html5/client/collection-mirror-initializer.js
Normal 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);
|
@ -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) => {
|
||||
|
@ -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):
|
||||
|
@ -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 });
|
||||
|
@ -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:
|
||||
|
@ -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 });
|
||||
|
@ -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 });
|
||||
|
@ -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 });
|
||||
|
@ -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({
|
||||
|
@ -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;
|
||||
|
@ -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:
|
||||
|
@ -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 });
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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 });
|
||||
|
@ -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 });
|
||||
|
@ -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({
|
||||
|
@ -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 });
|
||||
}
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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');
|
||||
|
||||
|
@ -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;
|
||||
`;
|
||||
|
@ -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(),
|
||||
|
@ -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'}
|
||||
|
@ -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};
|
||||
`;
|
||||
|
@ -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 },
|
||||
|
@ -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) {
|
||||
|
@ -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';
|
||||
|
@ -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 {
|
||||
|
@ -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';
|
||||
|
@ -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} {
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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;
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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) => {
|
||||
|
@ -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';
|
||||
|
||||
|
@ -161,6 +161,7 @@ export default class ButtonBase extends React.Component {
|
||||
'animations',
|
||||
'small',
|
||||
'full',
|
||||
'iconRight',
|
||||
];
|
||||
|
||||
return (
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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';
|
||||
|
@ -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 = {};
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -46,6 +46,7 @@ const SignalBars = styled.div`
|
||||
|
||||
const Bar = styled.div`
|
||||
width: 20%;
|
||||
border-radius: .46875em;
|
||||
`;
|
||||
|
||||
const FirstBar = styled(Bar)`
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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));
|
@ -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);
|
@ -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,
|
||||
};
|
@ -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({
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
@ -69,7 +69,7 @@ class RemoveUserModal extends Component {
|
||||
</Styled.Description>
|
||||
|
||||
<Styled.Footer>
|
||||
<Styled.ConfirmButtom
|
||||
<Styled.ConfirmButton
|
||||
color="primary"
|
||||
label={intl.formatMessage(messages.yesLabel)}
|
||||
onClick={() => {
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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)}
|
||||
|
@ -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 {
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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';
|
||||
|
@ -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 />}
|
||||
|
@ -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,
|
||||
|
@ -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 });
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user