import React from 'react';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import TabUnstyled from '@mui/base/TabUnstyled';
import TabsListUnstyled from '@mui/base/TabsListUnstyled';
import TabPanelUnstyled from '@mui/base/TabPanelUnstyled';
import TabsUnstyled from '@mui/base/TabsUnstyled';
import './App.css';
import './bbb-icons.css';
import {
FormattedMessage, FormattedDate, injectIntl, FormattedTime,
} from 'react-intl';
import { emojiConfigs } from './services/EmojiService';
import CardBody from './components/Card';
import UsersTable from './components/UsersTable';
import UserDetails from './components/UserDetails/component';
import { UserDetailsContext } from './components/UserDetails/context';
import StatusTable from './components/StatusTable';
import PollsTable from './components/PollsTable';
import ErrorMessage from './components/ErrorMessage';
import { makeUserCSVData, tsToHHmmss } from './services/UserService';
const TABS = {
OVERVIEW: 0,
OVERVIEW_ACTIVITY_SCORE: 1,
TIMELINE: 2,
POLLING: 3,
};
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: true,
invalidSessionCount: 0,
activitiesJson: {},
tab: 0,
meetingId: '',
learningDashboardAccessToken: '',
ldAccessTokenCopied: false,
sessionToken: '',
lastUpdated: null,
};
}
componentDidMount() {
this.setDashboardParams(() => {
this.fetchActivitiesJson();
});
}
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:text/csv;charset=UTF-8,${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(callback) {
let learningDashboardAccessToken = '';
let meetingId = '';
let sessionToken = '';
const urlSearchParams = new URLSearchParams(window.location.search);
const params = Object.fromEntries(urlSearchParams.entries());
if (typeof params.meeting !== 'undefined') {
meetingId = params.meeting;
}
if (typeof params.sessionToken !== 'undefined') {
sessionToken = params.sessionToken;
}
if (typeof params.report !== 'undefined') {
learningDashboardAccessToken = params.report;
} else {
const cookieName = `ld-${params.meeting}`;
const cDecoded = decodeURIComponent(document.cookie);
const cArr = cDecoded.split('; ');
cArr.forEach((val) => {
if (val.indexOf(`${cookieName}=`) === 0) {
learningDashboardAccessToken = val.substring((`${cookieName}=`).length);
}
});
// Extend AccessToken lifetime by 7d (in each access)
if (learningDashboardAccessToken !== '') {
const cookieExpiresDate = new Date();
cookieExpiresDate.setTime(cookieExpiresDate.getTime() + (3600000 * 24 * 7));
const value = `ld-${meetingId}=${learningDashboardAccessToken};`;
const expire = `expires=${cookieExpiresDate.toGMTString()};`;
const args = 'path=/;SameSite=None;Secure';
document.cookie = `${value} ${expire} ${args}`;
}
}
this.setState({ learningDashboardAccessToken, meetingId, sessionToken }, () => {
if (typeof callback === 'function') callback();
});
}
fetchMostUsedEmojis() {
const { activitiesJson } = this.state;
if (!activitiesJson) { return []; }
// Icon elements
const emojis = [...Object.keys(emojiConfigs)];
const icons = {};
emojis.forEach((emoji) => {
icons[emoji] = ();
});
// 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)
.filter(([, count]) => count)
.sort(([, countA], [, countB]) => countA - countB)
.reverse()
.slice(0, 3);
return mostUsedEmojis.map(([emoji]) => icons[emoji]);
}
updateModalUser() {
const { user, dispatch, isOpen } = this.context;
const { activitiesJson } = this.state;
const { users } = activitiesJson;
if (isOpen && users[user.userKey]) {
dispatch({
type: 'changeUser',
user: users[user.userKey],
});
}
}
fetchActivitiesJson() {
const {
learningDashboardAccessToken, meetingId, sessionToken, invalidSessionCount,
} = this.state;
if (learningDashboardAccessToken !== '') {
fetch(`${meetingId}/${learningDashboardAccessToken}/learning_dashboard_data.json`)
.then((response) => response.json())
.then((json) => {
this.setState({
activitiesJson: json,
loading: false,
invalidSessionCount: 0,
lastUpdated: Date.now(),
});
this.updateModalUser();
}).catch(() => {
this.setState({ loading: false, invalidSessionCount: invalidSessionCount + 1 });
});
} else if (sessionToken !== '') {
const url = new URL('/bigbluebutton/api/learningDashboard', window.location);
fetch(`${url}?sessionToken=${sessionToken}`, { credentials: 'include' })
.then((response) => response.json())
.then((json) => {
if (json.response.returncode === 'SUCCESS') {
const jsonData = JSON.parse(json.response.data);
this.setState({
activitiesJson: jsonData,
loading: false,
invalidSessionCount: 0,
lastUpdated: Date.now(),
});
this.updateModalUser();
} else {
// When meeting is ended the sessionToken stop working, check for new cookies
this.setDashboardParams();
this.setState({ loading: false, invalidSessionCount: invalidSessionCount + 1 });
}
})
.catch(() => {
this.setState({ loading: false, invalidSessionCount: invalidSessionCount + 1 });
});
} else {
this.setState({ loading: false });
}
setTimeout(() => {
this.fetchActivitiesJson();
}, 10000 * (2 ** invalidSessionCount));
}
render() {
const {
activitiesJson, tab, sessionToken, loading, lastUpdated,
learningDashboardAccessToken, ldAccessTokenCopied,
} = this.state;
const { intl } = this.props;
document.title = `${intl.formatMessage({ id: 'app.learningDashboard.bigbluebuttonTitle', defaultMessage: 'BigBlueButton' })} - ${intl.formatMessage({ id: 'app.learningDashboard.dashboardTitle', defaultMessage: 'Learning Analytics Dashboard' })} - ${activitiesJson.name}`;
function totalOfEmojis() {
if (activitiesJson && activitiesJson.users) {
return Object.values(activitiesJson.users)
.reduce((prevVal, elem) => prevVal + elem.emojis.length, 0);
}
return 0;
}
function totalOfActivity() {
const usersTimes = Object.values(activitiesJson.users || {}).reduce((prev, user) => ([
...prev,
...Object.values(user.intIds),
]), []);
const minTime = Object.values(usersTimes || {}).reduce((prevVal, elem) => {
if (prevVal === 0 || elem.registeredOn < prevVal) return elem.registeredOn;
return prevVal;
}, 0);
const maxTime = Object.values(usersTimes || {}).reduce((prevVal, elem) => {
if (elem.leftOn === 0) return (new Date()).getTime();
if (elem.leftOn > prevVal) return elem.leftOn;
return prevVal;
}, 0);
return maxTime - minTime;
}
function getAverageActivityScore() {
let meetingAveragePoints = 0;
const allUsers = Object.values(activitiesJson.users || {})
.filter((currUser) => !currUser.isModerator);
const nrOfUsers = allUsers.length;
if (nrOfUsers === 0) return meetingAveragePoints;
// Calculate points of Talking
const usersTalkTime = allUsers.map((currUser) => currUser.talk.totalTime);
const maxTalkTime = Math.max(...usersTalkTime);
const totalTalkTime = usersTalkTime.reduce((prev, val) => prev + val, 0);
if (totalTalkTime > 0) {
meetingAveragePoints += ((totalTalkTime / nrOfUsers) / maxTalkTime) * 2;
}
// Calculate points of Chatting
const usersTotalOfMessages = allUsers.map((currUser) => currUser.totalOfMessages);
const maxMessages = Math.max(...usersTotalOfMessages);
const totalMessages = usersTotalOfMessages.reduce((prev, val) => prev + val, 0);
if (maxMessages > 0) {
meetingAveragePoints += ((totalMessages / nrOfUsers) / maxMessages) * 2;
}
// Calculate points of Raise hand
const usersRaiseHand = allUsers.map((currUser) => currUser.emojis.filter((emoji) => emoji.name === 'raiseHand').length);
const maxRaiseHand = Math.max(...usersRaiseHand);
const totalRaiseHand = usersRaiseHand.reduce((prev, val) => prev + val, 0);
if (maxRaiseHand > 0) {
meetingAveragePoints += ((totalRaiseHand / nrOfUsers) / maxRaiseHand) * 2;
}
// Calculate points of Emojis
const usersEmojis = allUsers.map((currUser) => currUser.emojis.filter((emoji) => emoji.name !== 'raiseHand').length);
const maxEmojis = Math.max(...usersEmojis);
const totalEmojis = usersEmojis.reduce((prev, val) => prev + val, 0);
if (maxEmojis > 0) {
meetingAveragePoints += ((totalEmojis / nrOfUsers) / maxEmojis) * 2;
}
// Calculate points of Polls
const totalOfPolls = Object.values(activitiesJson.polls || {}).length;
if (totalOfPolls > 0) {
const totalAnswers = allUsers
.reduce((prevVal, currUser) => prevVal + Object.values(currUser.answers || {}).length, 0);
meetingAveragePoints += ((totalAnswers / nrOfUsers) / totalOfPolls) * 2;
}
return meetingAveragePoints;
}
function getErrorMessage() {
if (learningDashboardAccessToken === '' && sessionToken === '') {
return intl.formatMessage({ id: 'app.learningDashboard.errors.invalidToken', defaultMessage: 'Invalid session token' });
}
if (activitiesJson === {} || typeof activitiesJson.name === 'undefined') {
return intl.formatMessage({ id: 'app.learningDashboard.errors.dataUnavailable', defaultMessage: 'Data is no longer available' });
}
return '';
}
if (loading === false && getErrorMessage() !== '') return
{
lastUpdated && (
<>