[mirotalksfu] - #22 add real-time polls
هذا الالتزام موجود في:
@@ -61,6 +61,9 @@ module.exports = class Room {
|
|||||||
this.rtmpFileStreamer = null;
|
this.rtmpFileStreamer = null;
|
||||||
this.rtmpUrlStreamer = null;
|
this.rtmpUrlStreamer = null;
|
||||||
this.rtmp = config.server.rtmp || false;
|
this.rtmp = config.server.rtmp || false;
|
||||||
|
|
||||||
|
// Polls
|
||||||
|
this.polls = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ####################################################
|
// ####################################################
|
||||||
@@ -87,10 +90,30 @@ module.exports = class Room {
|
|||||||
survey: this.survey,
|
survey: this.survey,
|
||||||
redirect: this.redirect,
|
redirect: this.redirect,
|
||||||
videoAIEnabled: this.videoAIEnabled,
|
videoAIEnabled: this.videoAIEnabled,
|
||||||
|
thereIsPolls: this.thereIsPolls(),
|
||||||
peers: JSON.stringify([...this.peers]),
|
peers: JSON.stringify([...this.peers]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ##############################################
|
||||||
|
// POLLS
|
||||||
|
// ##############################################
|
||||||
|
|
||||||
|
thereIsPolls() {
|
||||||
|
return this.polls.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPolls() {
|
||||||
|
return this.polls;
|
||||||
|
}
|
||||||
|
|
||||||
|
convertPolls(polls) {
|
||||||
|
return polls.map((poll) => {
|
||||||
|
const voters = poll.voters ? Object.fromEntries(poll.voters.entries()) : {};
|
||||||
|
return { ...poll, voters };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ##############################################
|
// ##############################################
|
||||||
// RTMP from FILE
|
// RTMP from FILE
|
||||||
// ##############################################
|
// ##############################################
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ dependencies: {
|
|||||||
* @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon
|
* @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon
|
||||||
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
|
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
|
||||||
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
||||||
* @version 1.4.99
|
* @version 1.5.10
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -2385,6 +2385,97 @@ function startServer() {
|
|||||||
log.debug('endRTMPfromURL - rtmpUrlStreamsCount ---->', rtmpUrlStreamsCount);
|
log.debug('endRTMPfromURL - rtmpUrlStreamsCount ---->', rtmpUrlStreamsCount);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on('createPoll', (dataObject) => {
|
||||||
|
if (!roomList.has(socket.room_id)) return;
|
||||||
|
|
||||||
|
const data = checkXSS(dataObject);
|
||||||
|
|
||||||
|
const { question, options } = data;
|
||||||
|
|
||||||
|
const room = roomList.get(socket.room_id);
|
||||||
|
|
||||||
|
const newPoll = {
|
||||||
|
question: question,
|
||||||
|
options: options,
|
||||||
|
voters: new Map(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const roomPolls = room.getPolls();
|
||||||
|
|
||||||
|
roomPolls.push(newPoll);
|
||||||
|
room.sendToAll('updatePolls', room.convertPolls(roomPolls));
|
||||||
|
log.debug('[Poll] createPoll', roomPolls);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('vote', (dataObject) => {
|
||||||
|
if (!roomList.has(socket.room_id)) return;
|
||||||
|
|
||||||
|
const data = checkXSS(dataObject);
|
||||||
|
|
||||||
|
const room = roomList.get(socket.room_id);
|
||||||
|
|
||||||
|
const roomPolls = room.getPolls();
|
||||||
|
|
||||||
|
const poll = roomPolls[data.pollIndex];
|
||||||
|
if (poll) {
|
||||||
|
const peer_name = getPeerName(room, false) || socket.id;
|
||||||
|
poll.voters.set(peer_name, data.option);
|
||||||
|
room.sendToAll('updatePolls', room.convertPolls(roomPolls));
|
||||||
|
log.debug('[Poll] vote', roomPolls);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('updatePoll', () => {
|
||||||
|
if (!roomList.has(socket.room_id)) return;
|
||||||
|
|
||||||
|
const room = roomList.get(socket.room_id);
|
||||||
|
|
||||||
|
const roomPolls = room.getPolls();
|
||||||
|
|
||||||
|
if (roomPolls.length > 0) {
|
||||||
|
room.sendToAll('updatePolls', room.convertPolls(roomPolls));
|
||||||
|
log.debug('[Poll] updatePoll', roomPolls);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('editPoll', (dataObject) => {
|
||||||
|
if (!roomList.has(socket.room_id)) return;
|
||||||
|
|
||||||
|
const data = checkXSS(dataObject);
|
||||||
|
|
||||||
|
const { index, question, options } = data;
|
||||||
|
|
||||||
|
const room = roomList.get(socket.room_id);
|
||||||
|
|
||||||
|
const roomPolls = room.getPolls();
|
||||||
|
|
||||||
|
if (roomPolls[index]) {
|
||||||
|
roomPolls[index].question = question;
|
||||||
|
roomPolls[index].options = options;
|
||||||
|
room.sendToAll('updatePolls', roomPolls);
|
||||||
|
log.debug('[Poll] editPoll', roomPolls);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('deletePoll', async (data) => {
|
||||||
|
if (!roomList.has(socket.room_id)) return;
|
||||||
|
|
||||||
|
const { index, peer_name, peer_uuid } = checkXSS(data);
|
||||||
|
|
||||||
|
const isPresenter = await isPeerPresenter(socket.room_id, socket.id, peer_name, peer_uuid);
|
||||||
|
|
||||||
|
if (!isPresenter) return;
|
||||||
|
|
||||||
|
const room = roomList.get(socket.room_id);
|
||||||
|
|
||||||
|
const roomPolls = room.getPolls();
|
||||||
|
|
||||||
|
if (roomPolls[index]) {
|
||||||
|
roomPolls.splice(index, 1);
|
||||||
|
room.sendToAll('updatePolls', roomPolls);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
socket.on('disconnect', async () => {
|
socket.on('disconnect', async () => {
|
||||||
if (!roomList.has(socket.room_id)) return;
|
if (!roomList.has(socket.room_id)) return;
|
||||||
|
|
||||||
|
|||||||
@@ -355,6 +355,7 @@ module.exports = {
|
|||||||
startScreenButton: true,
|
startScreenButton: true,
|
||||||
swapCameraButton: true,
|
swapCameraButton: true,
|
||||||
chatButton: true,
|
chatButton: true,
|
||||||
|
pollButton: true,
|
||||||
raiseHandButton: true,
|
raiseHandButton: true,
|
||||||
transcriptionButton: true,
|
transcriptionButton: true,
|
||||||
whiteboardButton: true,
|
whiteboardButton: true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mirotalksfu",
|
"name": "mirotalksfu",
|
||||||
"version": "1.4.99",
|
"version": "1.5.10",
|
||||||
"description": "WebRTC SFU browser-based video calls",
|
"description": "WebRTC SFU browser-based video calls",
|
||||||
"main": "Server.js",
|
"main": "Server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/integrations": "7.114.0",
|
"@sentry/integrations": "7.114.0",
|
||||||
"@sentry/node": "8.17.0",
|
"@sentry/node": "8.18.0",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"body-parser": "1.20.2",
|
"body-parser": "1.20.2",
|
||||||
"colors": "1.4.0",
|
"colors": "1.4.0",
|
||||||
|
|||||||
167
public/css/Polls.css
Normal file
167
public/css/Polls.css
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
.pls-container {
|
||||||
|
z-index: 5;
|
||||||
|
position: absolute;
|
||||||
|
background: var(--body-bg);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: var(--border);
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-header {
|
||||||
|
display: inline-flex;
|
||||||
|
text-align: left;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
width: 100%;
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-creation {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
max-height: 480px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-h1,
|
||||||
|
.poll-h2,
|
||||||
|
.poll-h3 {
|
||||||
|
margin-top: 5px;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form label {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form input {
|
||||||
|
padding: 10px;
|
||||||
|
border: var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-btns {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-btn {
|
||||||
|
padding: 10px 15px;
|
||||||
|
background: var(--body-bg);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-btn:hover {
|
||||||
|
background: var(--btns-bg-color) !important;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-btn {
|
||||||
|
padding: 10px 15px;
|
||||||
|
background: #dc3545;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-btn:hover {
|
||||||
|
background: #c82333;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.polls-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll {
|
||||||
|
background: var(--body-bg);
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll .options {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll .options div {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll ul li {
|
||||||
|
background: var(--body-bg);
|
||||||
|
padding: 8px;
|
||||||
|
border: var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options input {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options label {
|
||||||
|
margin-left: 5px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-btn {
|
||||||
|
padding: 10px 15px;
|
||||||
|
background: var(--body-bg);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-btn:hover {
|
||||||
|
background: var(--btns-bg-color) !important;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pollCloseBtn {
|
||||||
|
position: absolute;
|
||||||
|
float: right;
|
||||||
|
right: 20px;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
@@ -1504,6 +1504,8 @@ z-index:
|
|||||||
- 3 control buttons
|
- 3 control buttons
|
||||||
- 4 whiteboard
|
- 4 whiteboard
|
||||||
- 5 chat group
|
- 5 chat group
|
||||||
|
- 5 polls
|
||||||
|
- 5 transcription
|
||||||
- 6 settings
|
- 6 settings
|
||||||
- 7 participants/lobby
|
- 7 participants/lobby
|
||||||
- 8 send receive progress
|
- 8 send receive progress
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ if (location.href.substr(0, 5) !== 'https') location.href = 'https' + location.h
|
|||||||
* @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon
|
* @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon
|
||||||
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
|
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
|
||||||
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
||||||
* @version 1.4.99
|
* @version 1.5.10
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -176,6 +176,18 @@ const initMicrophoneSelect = getId('initMicrophoneSelect');
|
|||||||
const speakerSelect = getId('speakerSelect');
|
const speakerSelect = getId('speakerSelect');
|
||||||
const initSpeakerSelect = getId('initSpeakerSelect');
|
const initSpeakerSelect = getId('initSpeakerSelect');
|
||||||
|
|
||||||
|
// ####################################################
|
||||||
|
// POLLS
|
||||||
|
// ####################################################
|
||||||
|
|
||||||
|
const createPollForm = getId('createPollForm');
|
||||||
|
const pollsContainer = getId('pollsContainer');
|
||||||
|
const addOptionButton = getId('addOptionButton');
|
||||||
|
const delOptionButton = getId('delOptionButton');
|
||||||
|
const optionsContainer = getId('optionsContainer');
|
||||||
|
const selectedOptions = {};
|
||||||
|
let pollOpen = false;
|
||||||
|
|
||||||
// ####################################################
|
// ####################################################
|
||||||
// DYNAMIC SETTINGS
|
// DYNAMIC SETTINGS
|
||||||
// ####################################################
|
// ####################################################
|
||||||
@@ -333,6 +345,7 @@ function initClient() {
|
|||||||
setTippy('chatShowParticipantsList', 'Toggle participants list', 'bottom');
|
setTippy('chatShowParticipantsList', 'Toggle participants list', 'bottom');
|
||||||
setTippy('chatMaxButton', 'Maximize', 'bottom');
|
setTippy('chatMaxButton', 'Maximize', 'bottom');
|
||||||
setTippy('chatMinButton', 'Minimize', 'bottom');
|
setTippy('chatMinButton', 'Minimize', 'bottom');
|
||||||
|
setTippy('pollCloseBtn', 'Close', 'bottom');
|
||||||
setTippy('participantsSaveBtn', 'Save participants info', 'bottom');
|
setTippy('participantsSaveBtn', 'Save participants info', 'bottom');
|
||||||
setTippy('participantsRaiseHandBtn', 'Toggle raise hands', 'bottom');
|
setTippy('participantsRaiseHandBtn', 'Toggle raise hands', 'bottom');
|
||||||
setTippy('participantsUnreadMessagesBtn', 'Toggle unread messages', 'bottom');
|
setTippy('participantsUnreadMessagesBtn', 'Toggle unread messages', 'bottom');
|
||||||
@@ -372,6 +385,7 @@ function refreshMainButtonsToolTipPlacement() {
|
|||||||
setTippy('emojiRoomButton', 'Toggle emoji reaction', placement);
|
setTippy('emojiRoomButton', 'Toggle emoji reaction', placement);
|
||||||
setTippy('swapCameraButton', 'Swap the camera', placement);
|
setTippy('swapCameraButton', 'Swap the camera', placement);
|
||||||
setTippy('chatButton', 'Toggle the chat', placement);
|
setTippy('chatButton', 'Toggle the chat', placement);
|
||||||
|
setTippy('pollButton', 'Toggle the poll', placement);
|
||||||
setTippy('transcriptionButton', 'Toggle transcription', placement);
|
setTippy('transcriptionButton', 'Toggle transcription', placement);
|
||||||
setTippy('whiteboardButton', 'Toggle the whiteboard', placement);
|
setTippy('whiteboardButton', 'Toggle the whiteboard', placement);
|
||||||
setTippy('settingsButton', 'Toggle the settings', placement);
|
setTippy('settingsButton', 'Toggle the settings', placement);
|
||||||
@@ -1267,6 +1281,7 @@ function roomIsReady() {
|
|||||||
hide(tabRecordingBtn);
|
hide(tabRecordingBtn);
|
||||||
}
|
}
|
||||||
BUTTONS.main.chatButton && show(chatButton);
|
BUTTONS.main.chatButton && show(chatButton);
|
||||||
|
BUTTONS.main.pollButton && show(pollButton);
|
||||||
BUTTONS.main.raiseHandButton && show(raiseHandButton);
|
BUTTONS.main.raiseHandButton && show(raiseHandButton);
|
||||||
BUTTONS.main.emojiRoomButton && show(emojiRoomButton);
|
BUTTONS.main.emojiRoomButton && show(emojiRoomButton);
|
||||||
!BUTTONS.chat.chatSaveButton && hide(chatSaveButton);
|
!BUTTONS.chat.chatSaveButton && hide(chatSaveButton);
|
||||||
@@ -1300,9 +1315,11 @@ function roomIsReady() {
|
|||||||
hide(transcriptionTogglePinBtn);
|
hide(transcriptionTogglePinBtn);
|
||||||
hide(transcriptionMaxBtn);
|
hide(transcriptionMaxBtn);
|
||||||
hide(transcriptionMinBtn);
|
hide(transcriptionMinBtn);
|
||||||
|
rc.pollMaximize();
|
||||||
} else {
|
} else {
|
||||||
rc.makeDraggable(emojiPickerContainer, emojiPickerHeader);
|
rc.makeDraggable(emojiPickerContainer, emojiPickerHeader);
|
||||||
rc.makeDraggable(chatRoom, chatHeader);
|
rc.makeDraggable(chatRoom, chatHeader);
|
||||||
|
rc.makeDraggable(pollRoom, pollHeader);
|
||||||
rc.makeDraggable(mySettings, mySettingsHeader);
|
rc.makeDraggable(mySettings, mySettingsHeader);
|
||||||
rc.makeDraggable(whiteboard, whiteboardHeader);
|
rc.makeDraggable(whiteboard, whiteboardHeader);
|
||||||
rc.makeDraggable(sendFileDiv, imgShareSend);
|
rc.makeDraggable(sendFileDiv, imgShareSend);
|
||||||
@@ -1555,6 +1572,22 @@ function handleButtons() {
|
|||||||
rc.toggleShowParticipants();
|
rc.toggleShowParticipants();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// Polls
|
||||||
|
pollButton.onclick = () => {
|
||||||
|
rc.togglePoll();
|
||||||
|
};
|
||||||
|
pollCloseBtn.onclick = () => {
|
||||||
|
rc.togglePoll();
|
||||||
|
};
|
||||||
|
addOptionButton.onclick = () => {
|
||||||
|
rc.pollAddOptions();
|
||||||
|
};
|
||||||
|
delOptionButton.onclick = () => {
|
||||||
|
rc.pollDeleteOptions();
|
||||||
|
};
|
||||||
|
createPollForm.onsubmit = (e) => {
|
||||||
|
rc.pollCreateForm(e);
|
||||||
|
};
|
||||||
transcriptionButton.onclick = () => {
|
transcriptionButton.onclick = () => {
|
||||||
transcription.toggle();
|
transcription.toggle();
|
||||||
};
|
};
|
||||||
@@ -4240,7 +4273,7 @@ function showAbout() {
|
|||||||
imageUrl: image.about,
|
imageUrl: image.about,
|
||||||
customClass: { image: 'img-about' },
|
customClass: { image: 'img-about' },
|
||||||
position: 'center',
|
position: 'center',
|
||||||
title: 'WebRTC SFU v1.4.99',
|
title: 'WebRTC SFU v1.5.10',
|
||||||
html: `
|
html: `
|
||||||
<br />
|
<br />
|
||||||
<div id="about">
|
<div id="about">
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
* @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon
|
* @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon
|
||||||
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
|
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
|
||||||
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
||||||
* @version 1.4.99
|
* @version 1.5.10
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -551,6 +551,10 @@ class RoomClient {
|
|||||||
elemDisplay('tabRTMPStreamingBtn', false);
|
elemDisplay('tabRTMPStreamingBtn', false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// There is polls
|
||||||
|
if (room.thereIsPolls) {
|
||||||
|
this.socket.emit('updatePoll');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PARTICIPANTS
|
// PARTICIPANTS
|
||||||
@@ -869,6 +873,7 @@ class RoomClient {
|
|||||||
this.socket.on('errorRTMP', this.handleErrorRTMP);
|
this.socket.on('errorRTMP', this.handleErrorRTMP);
|
||||||
this.socket.on('endRTMPfromURL', this.handleEndRTMPfromURL);
|
this.socket.on('endRTMPfromURL', this.handleEndRTMPfromURL);
|
||||||
this.socket.on('errorRTMPfromURL', this.handleErrorRTMPfromURL);
|
this.socket.on('errorRTMPfromURL', this.handleErrorRTMPfromURL);
|
||||||
|
this.socket.on('updatePolls', this.handleUpdatePolls);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ####################################################
|
// ####################################################
|
||||||
@@ -1039,6 +1044,10 @@ class RoomClient {
|
|||||||
this.errorRTMPfromURL(data);
|
this.errorRTMPfromURL(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleUpdatePolls = (data) => {
|
||||||
|
this.pollsUpdate(data);
|
||||||
|
};
|
||||||
|
|
||||||
// ####################################################
|
// ####################################################
|
||||||
// SERVER AWAY/MAINTENANCE
|
// SERVER AWAY/MAINTENANCE
|
||||||
// ####################################################
|
// ####################################################
|
||||||
@@ -7730,6 +7739,216 @@ class RoomClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ##############################################
|
||||||
|
// POOLS
|
||||||
|
// ##############################################
|
||||||
|
|
||||||
|
togglePoll() {
|
||||||
|
const pollRoom = this.getId('pollRoom');
|
||||||
|
pollRoom.classList.toggle('show');
|
||||||
|
if (!pollOpen) {
|
||||||
|
this.pollCenter();
|
||||||
|
this.sound('open');
|
||||||
|
}
|
||||||
|
pollOpen = !pollOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
pollCenter() {
|
||||||
|
const pollRoom = this.getId('pollRoom');
|
||||||
|
pollRoom.style.position = 'fixed';
|
||||||
|
pollRoom.style.transform = 'translate(-50%, -50%)';
|
||||||
|
pollRoom.style.top = '50%';
|
||||||
|
pollRoom.style.left = '50%';
|
||||||
|
}
|
||||||
|
|
||||||
|
pollMaximize() {
|
||||||
|
const pollRoom = this.getId('pollRoom');
|
||||||
|
pollRoom.style.maxHeight = '100vh';
|
||||||
|
pollRoom.style.maxWidth = '100vw';
|
||||||
|
}
|
||||||
|
|
||||||
|
pollsUpdate(polls) {
|
||||||
|
if (!pollOpen) this.togglePoll();
|
||||||
|
|
||||||
|
pollsContainer.innerHTML = '';
|
||||||
|
polls.forEach((poll, index) => {
|
||||||
|
const pollDiv = document.createElement('div');
|
||||||
|
pollDiv.className = 'poll';
|
||||||
|
|
||||||
|
const question = document.createElement('h3');
|
||||||
|
question.className = 'poll-h3';
|
||||||
|
question.textContent = poll.question;
|
||||||
|
pollDiv.appendChild(question);
|
||||||
|
|
||||||
|
const options = document.createElement('div');
|
||||||
|
options.className = 'options';
|
||||||
|
|
||||||
|
poll.options.forEach((option) => {
|
||||||
|
const optionDiv = document.createElement('div');
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'radio';
|
||||||
|
input.name = `poll${index}`;
|
||||||
|
input.value = option;
|
||||||
|
if (selectedOptions[index] === option) {
|
||||||
|
input.checked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.addEventListener('change', () => {
|
||||||
|
selectedOptions[index] = option;
|
||||||
|
this.socket.emit('vote', { pollIndex: index, option });
|
||||||
|
});
|
||||||
|
|
||||||
|
const label = document.createElement('label');
|
||||||
|
label.textContent = option;
|
||||||
|
|
||||||
|
optionDiv.appendChild(input);
|
||||||
|
optionDiv.appendChild(label);
|
||||||
|
options.appendChild(optionDiv);
|
||||||
|
});
|
||||||
|
pollDiv.appendChild(options);
|
||||||
|
|
||||||
|
// Only the presenters
|
||||||
|
// if (isPresenter) {
|
||||||
|
const pollButtonsDiv = document.createElement('div');
|
||||||
|
pollButtonsDiv.className = 'poll-btns';
|
||||||
|
|
||||||
|
// Toggle voters button
|
||||||
|
const toggleButton = document.createElement('button');
|
||||||
|
const toggleButtonIcon = document.createElement('i');
|
||||||
|
toggleButtonIcon.className = 'fas fa-users';
|
||||||
|
toggleButton.textContent = ' Toggle Voters';
|
||||||
|
toggleButton.className = 'view-btn';
|
||||||
|
// Append the icon to the button
|
||||||
|
toggleButton.insertBefore(toggleButtonIcon, toggleButton.firstChild);
|
||||||
|
toggleButton.addEventListener('click', () => {
|
||||||
|
votersList.style.display === 'none'
|
||||||
|
? (votersList.style.display = 'block')
|
||||||
|
: (votersList.style.display = 'none');
|
||||||
|
});
|
||||||
|
pollButtonsDiv.appendChild(toggleButton);
|
||||||
|
|
||||||
|
// Edit poll button using swal
|
||||||
|
const editPollButton = document.createElement('button');
|
||||||
|
const editPollButtonIcon = document.createElement('i');
|
||||||
|
editPollButtonIcon.className = 'fas fa-pen-to-square';
|
||||||
|
editPollButton.textContent = ' Edit Poll';
|
||||||
|
editPollButton.className = 'poll-btn';
|
||||||
|
editPollButton.insertBefore(editPollButtonIcon, editPollButton.firstChild);
|
||||||
|
editPollButton.addEventListener('click', () => {
|
||||||
|
Swal.fire({
|
||||||
|
allowOutsideClick: false,
|
||||||
|
allowEscapeKey: false,
|
||||||
|
background: swalBackground,
|
||||||
|
title: 'Edit Poll',
|
||||||
|
html: this.createPollInputs(poll),
|
||||||
|
focusConfirm: false,
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'Save',
|
||||||
|
cancelButtonText: 'Cancel',
|
||||||
|
cancelButtonColor: '#dc3545',
|
||||||
|
preConfirm: () => {
|
||||||
|
const newQuestion = document.getElementById('swal-input-question').value;
|
||||||
|
const newOptions = this.getPollOptions(poll.options.length);
|
||||||
|
this.socket.emit('editPoll', {
|
||||||
|
index,
|
||||||
|
question: newQuestion,
|
||||||
|
options: newOptions,
|
||||||
|
peer_name: this.peer_name,
|
||||||
|
peer_uuid: this.peer_uuid,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
showClass: { popup: 'animate__animated animate__fadeInDown' },
|
||||||
|
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
pollButtonsDiv.appendChild(editPollButton);
|
||||||
|
|
||||||
|
// Delete poll button
|
||||||
|
const deletePollButton = document.createElement('button');
|
||||||
|
const deletePollButtonIcon = document.createElement('i');
|
||||||
|
deletePollButtonIcon.className = 'fas fa-minus';
|
||||||
|
deletePollButton.textContent = ' Delete Poll';
|
||||||
|
deletePollButton.className = 'del-btn';
|
||||||
|
deletePollButton.insertBefore(deletePollButtonIcon, deletePollButton.firstChild);
|
||||||
|
deletePollButton.addEventListener('click', () => {
|
||||||
|
this.socket.emit('deletePoll', { index, peer_name: this.peer_name, peer_uuid: this.peer_uuid });
|
||||||
|
});
|
||||||
|
pollButtonsDiv.appendChild(deletePollButton);
|
||||||
|
|
||||||
|
// Append buttons to poll
|
||||||
|
pollDiv.appendChild(pollButtonsDiv);
|
||||||
|
|
||||||
|
// Create voter lists
|
||||||
|
const votersList = document.createElement('ul');
|
||||||
|
votersList.style.display = 'none';
|
||||||
|
for (const [user, vote] of Object.entries(poll.voters)) {
|
||||||
|
const voter = document.createElement('li');
|
||||||
|
voter.textContent = `${user}: ${vote}`;
|
||||||
|
votersList.appendChild(voter);
|
||||||
|
}
|
||||||
|
pollDiv.appendChild(votersList);
|
||||||
|
// }
|
||||||
|
|
||||||
|
pollsContainer.appendChild(pollDiv);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pollCreateForm(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const question = e.target.question.value;
|
||||||
|
const optionInputs = document.querySelectorAll('.option-input');
|
||||||
|
const options = Array.from(optionInputs).map((input) => input.value.trim());
|
||||||
|
|
||||||
|
this.socket.emit('createPoll', { question, options });
|
||||||
|
|
||||||
|
e.target.reset();
|
||||||
|
optionsContainer.innerHTML = '';
|
||||||
|
const initialOptionInput = document.createElement('input');
|
||||||
|
initialOptionInput.type = 'text';
|
||||||
|
initialOptionInput.name = 'option';
|
||||||
|
initialOptionInput.className = 'option-input';
|
||||||
|
initialOptionInput.required = true;
|
||||||
|
optionsContainer.appendChild(initialOptionInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
pollAddOptions() {
|
||||||
|
const optionInput = document.createElement('input');
|
||||||
|
optionInput.type = 'text';
|
||||||
|
optionInput.name = 'option';
|
||||||
|
optionInput.className = 'option-input';
|
||||||
|
optionInput.required = true;
|
||||||
|
optionsContainer.appendChild(optionInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
pollDeleteOptions() {
|
||||||
|
const optionInputs = document.querySelectorAll('.option-input');
|
||||||
|
if (optionInputs.length > 1) {
|
||||||
|
optionsContainer.removeChild(optionInputs[optionInputs.length - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createPollInputs(poll) {
|
||||||
|
const questionInput = `<input id="swal-input-question" class="swal2-input" value="${poll.question}">`;
|
||||||
|
const optionsInputs = poll.options
|
||||||
|
.map(
|
||||||
|
(option, i) => `
|
||||||
|
<input id="swal-input-option${i}" class="swal2-input" value="${option}">
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
return questionInput + optionsInputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPollOptions(optionCount) {
|
||||||
|
const options = [];
|
||||||
|
for (let i = 0; i < optionCount; i++) {
|
||||||
|
options.push(document.getElementById(`swal-input-option${i}`).value);
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
sleep(ms) {
|
sleep(ms) {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ let BUTTONS = {
|
|||||||
startScreenButton: true,
|
startScreenButton: true,
|
||||||
swapCameraButton: true,
|
swapCameraButton: true,
|
||||||
chatButton: true,
|
chatButton: true,
|
||||||
|
pollButton: true,
|
||||||
raiseHandButton: true,
|
raiseHandButton: true,
|
||||||
transcriptionButton: true,
|
transcriptionButton: true,
|
||||||
whiteboardButton: true,
|
whiteboardButton: true,
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ const commands = {
|
|||||||
chatOn: 'open the chat',
|
chatOn: 'open the chat',
|
||||||
chatSend: 'send',
|
chatSend: 'send',
|
||||||
chatOff: 'close the chat',
|
chatOff: 'close the chat',
|
||||||
|
pollOn: 'open the poll',
|
||||||
|
pollOff: 'close the poll',
|
||||||
toggleTr: 'toggle transcription',
|
toggleTr: 'toggle transcription',
|
||||||
whiteboardOn: 'open the whiteboard',
|
whiteboardOn: 'open the whiteboard',
|
||||||
whiteboardOff: 'close the whiteboard',
|
whiteboardOff: 'close the whiteboard',
|
||||||
@@ -186,6 +188,14 @@ function execVoiceCommands(transcript) {
|
|||||||
printCommand(commands.chatOn);
|
printCommand(commands.chatOn);
|
||||||
chatButton.click();
|
chatButton.click();
|
||||||
break;
|
break;
|
||||||
|
case commands.pollOn:
|
||||||
|
printCommand(commands.pollOn);
|
||||||
|
pollButton.click();
|
||||||
|
break;
|
||||||
|
case commands.pollOff:
|
||||||
|
printCommand(commands.pollOff);
|
||||||
|
pollCloseBtn.click();
|
||||||
|
break;
|
||||||
case commands.chatSend:
|
case commands.chatSend:
|
||||||
printCommand(commands.chatSend);
|
printCommand(commands.chatSend);
|
||||||
chatSendButton.click();
|
chatSendButton.click();
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
<link rel="stylesheet" href="../css/Room.css" />
|
<link rel="stylesheet" href="../css/Room.css" />
|
||||||
<link rel="stylesheet" href="../css/VideoGrid.css" />
|
<link rel="stylesheet" href="../css/VideoGrid.css" />
|
||||||
<link rel="stylesheet" href="../css/GroupChat.css" />
|
<link rel="stylesheet" href="../css/GroupChat.css" />
|
||||||
|
<link rel="stylesheet" href="../css/Polls.css" />
|
||||||
|
|
||||||
<!-- https://cdnjs.com/libraries/font-awesome -->
|
<!-- https://cdnjs.com/libraries/font-awesome -->
|
||||||
|
|
||||||
@@ -178,6 +179,7 @@ access to use this app.
|
|||||||
<button id="lowerHandButton" class="hidden"><i id="lowerHandIcon" class="fas fa-hand-paper"></i></button>
|
<button id="lowerHandButton" class="hidden"><i id="lowerHandIcon" class="fas fa-hand-paper"></i></button>
|
||||||
<button id="emojiRoomButton" class="hidden"><i class="fas fa-face-smile"></i></button>
|
<button id="emojiRoomButton" class="hidden"><i class="fas fa-face-smile"></i></button>
|
||||||
<button id="chatButton" class="hidden"><i class="fas fa-comments"></i></button>
|
<button id="chatButton" class="hidden"><i class="fas fa-comments"></i></button>
|
||||||
|
<button id="pollButton" class="hidden"><i class="fas fa-square-poll-horizontal"></i></button>
|
||||||
<button id="transcriptionButton" class="hidden"><i class="fas fa-closed-captioning"></i></button>
|
<button id="transcriptionButton" class="hidden"><i class="fas fa-closed-captioning"></i></button>
|
||||||
<button id="whiteboardButton" class="hidden"><i class="fas fa-chalkboard-teacher"></i></button>
|
<button id="whiteboardButton" class="hidden"><i class="fas fa-chalkboard-teacher"></i></button>
|
||||||
<button id="settingsButton" class="hidden"><i class="fas fa-cogs"></i></button>
|
<button id="settingsButton" class="hidden"><i class="fas fa-cogs"></i></button>
|
||||||
@@ -1436,6 +1438,40 @@ access to use this app.
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section id="pollRoom" class="pls-container fadein hidden">
|
||||||
|
<div id="pollHeader" class="poll-header">
|
||||||
|
<h2 class="poll-h2">Create a Poll</h2>
|
||||||
|
<button id="pollCloseBtn" class="fas fa-times"></button>
|
||||||
|
</div>
|
||||||
|
<div id="pollForm" class="poll-creation">
|
||||||
|
<form id="createPollForm" class="form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="question">Question:</label>
|
||||||
|
<input type="text" id="question" name="question" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="options">Options:</label>
|
||||||
|
<div id="optionsContainer">
|
||||||
|
<input type="text" name="option" class="option-input" required />
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<div class="poll-btns">
|
||||||
|
<button type="button" id="addOptionButton" class="poll-btn">
|
||||||
|
<i class="fas fa-plus"></i> Add Option
|
||||||
|
</button>
|
||||||
|
<button type="button" id="delOptionButton" class="del-btn">
|
||||||
|
<i class="fas fa-minus"></i> Delete Option
|
||||||
|
</button>
|
||||||
|
<button type="submit" id="addPollButton" class="poll-btn">
|
||||||
|
<i class="fas fa-square-poll-horizontal"></i> Create Poll
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div id="pollsContainer" class="polls-container"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section id="transcriptionRoom" class="transcription-room fadein">
|
<section id="transcriptionRoom" class="transcription-room fadein">
|
||||||
<section id="transcriptionSection" class="transcription">
|
<section id="transcriptionSection" class="transcription">
|
||||||
<header id="transcriptionHeader" class="transcription-header">
|
<header id="transcriptionHeader" class="transcription-header">
|
||||||
|
|||||||
المرجع في مشكلة جديدة
حظر مستخدم