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 ; const usersCount = Object.values(activitiesJson.users || {}) .filter((u) => activitiesJson.endedOn > 0 || Object.values(u.intIds)[Object.values(u.intIds).length - 1].leftOn === 0) .length; return (

{ ldAccessTokenCopied === true ? ( ) : null }
{activitiesJson.name || ''}

   { activitiesJson.endedOn > 0 ? ( ) : null } { activitiesJson.endedOn === 0 ? ( ) : null }

:  {tsToHHmmss(totalOfActivity())}

{ this.setState({ tab: v }); }} > {this.fetchMostUsedEmojis()}


{ lastUpdated && ( <>     ) }

); } } App.contextType = UserDetailsContext; export default injectIntl(App);