[mirotalksfu] - add collaborative powerful rich text editor

هذا الالتزام موجود في:
Miroslav Pejic
2024-08-11 23:56:10 +02:00
الأصل 8ef849a80e
التزام 8ab09c9ded
10 ملفات معدلة مع 439 إضافات و8 حذوفات

عرض الملف

@@ -61,6 +61,7 @@
- Speech recognition, execute the app features simply with your voice.
- Push-to-talk functionality, similar to a walkie-talkie.
- Advanced collaborative whiteboard for teachers.
- Advanced collaborative powerful rich text editor.
- Real-time sharing of YouTube embed videos, video files (MP4, WebM, OGG), and audio files (MP3).
- Real-time polls, allows users to create and participate in live polls, providing instant feedback and results.
- Integrated RTMP server, fully compatible with **[OBS](https://obsproject.com)**.

عرض الملف

@@ -43,7 +43,7 @@ dependencies: {
* @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
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
* @version 1.5.47
* @version 1.5.50
*
*/
@@ -2512,6 +2512,42 @@ function startServer() {
}
});
// Room collaborative editor
socket.on('editorChange', (dataObject) => {
if (!roomList.has(socket.room_id)) return;
//const data = checkXSS(dataObject);
const data = dataObject;
const room = roomList.get(socket.room_id);
room.broadCast(socket.id, 'editorChange', data);
});
socket.on('editorActions', (dataObject) => {
if (!roomList.has(socket.room_id)) return;
const data = checkXSS(dataObject);
const room = roomList.get(socket.room_id);
log.debug('editorActions', data);
room.broadCast(socket.id, 'editorActions', data);
});
socket.on('editorUpdate', (dataObject) => {
if (!roomList.has(socket.room_id)) return;
//const data = checkXSS(dataObject);
const data = dataObject;
const room = roomList.get(socket.room_id);
room.broadCast(socket.id, 'editorUpdate', data);
});
socket.on('disconnect', async () => {
if (!roomList.has(socket.room_id)) return;

عرض الملف

@@ -356,6 +356,7 @@ module.exports = {
swapCameraButton: true,
chatButton: true,
pollButton: true,
editorButton: true,
raiseHandButton: true,
transcriptionButton: true,
whiteboardButton: true,

عرض الملف

@@ -1,6 +1,6 @@
{
"name": "mirotalksfu",
"version": "1.5.47",
"version": "1.5.50",
"description": "WebRTC SFU browser-based video calls",
"main": "Server.js",
"scripts": {

79
public/css/Editor.css Normal file
عرض الملف

@@ -0,0 +1,79 @@
.editor-container {
z-index: 5;
position: absolute;
padding: 20px;
width: 100%;
height: 100%;
border-radius: 10px;
background: var(--body-bg);
border: var(--border);
box-shadow: var(--box-shadow);
overflow-x: hidden;
overflow-y: auto;
}
.editor-header {
display: inline-flex;
text-align: left;
margin-bottom: 30px;
width: 100%;
cursor: move;
}
.editor-header-btns {
display: flex;
gap: 10px;
position: absolute;
float: right;
right: 20px;
}
.editor {
height: 85vh !important;
color: white !important;
border-radius: 0px 0px 10px 10px; /* Top-left, top-right, bottom-right, bottom-left */
border: var(--border) !important;
box-shadow: var(--box-shadow) !important;
}
.ql-toolbar {
background: rgba(0, 0, 0, 0.4) !important;
color: white !important;
border-radius: 10px 10px 0px 0px; /* Top-left, top-right, bottom-right, bottom-left */
border: var(--border) !important;
}
/* Optional: Customize code block appearance */
.ql-snow .ql-code-block-container {
padding: 4px !important;
font-family: monospace;
margin-bottom: 20px !important;
border-radius: 4px !important;
/* border: var(--border) !important; */
background: var(--select-bg) !important;
box-shadow: var(--box-shadow) !important;
}
.ql-snow .ql-code-block {
font-family: monospace;
}
.ql-toolbar button:hover {
background: var(--body-bg) !important;
}
.ql-picker-options {
color: white;
border: none !important;
border-radius: 10px;
background: var(--body-bg);
box-shadow: var(--box-shadow) !important;
}
.ql-ui {
padding: 5px;
height: 30px;
color: white;
border-radius: 10px;
background: var(--body-bg);
}

عرض الملف

@@ -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 CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
* @version 1.5.47
* @version 1.5.50
*
*/
@@ -252,6 +252,8 @@ let transcription;
let showFreeAvatars = true;
let quill = null;
// ####################################################
// INIT ROOM
// ####################################################
@@ -338,6 +340,14 @@ function initClient() {
setTippy('pollMinButton', 'Minimize', 'bottom');
setTippy('pollSaveButton', 'Save results', 'bottom');
setTippy('pollCloseBtn', 'Close', 'bottom');
setTippy('editorLockBtn', 'Toggle Lock editor', 'bottom');
setTippy('editorUnlockBtn', 'Toggle Lock editor', 'bottom');
setTippy('editorUndoBtn', 'Undo', 'bottom');
setTippy('editorRedoBtn', 'Redo', 'bottom');
setTippy('editorCopyBtn', 'Copy', 'bottom');
setTippy('editorSaveBtn', 'Save', 'bottom');
setTippy('editorCloseBtn', 'Close', 'bottom');
setTippy('editorCleanBtn', 'Clean', 'bottom');
setTippy('pollAddOptionBtn', 'Add option', 'top');
setTippy('pollDelOptionBtn', 'Delete option', 'top');
setTippy('participantsSaveBtn', 'Save participants info', 'bottom');
@@ -375,6 +385,7 @@ function refreshMainButtonsToolTipPlacement() {
setTippy('emojiRoomButton', 'Toggle emoji reaction', placement);
setTippy('chatButton', 'Toggle the chat', placement);
setTippy('pollButton', 'Toggle the poll', placement);
setTippy('editorButton', 'Toggle the editor', placement);
setTippy('transcriptionButton', 'Toggle transcription', placement);
setTippy('whiteboardButton', 'Toggle the whiteboard', placement);
setTippy('snapshotRoomButton', 'Snapshot screen, window, or tab', placement);
@@ -1185,7 +1196,7 @@ function copyRoomURL() {
userLog('info', 'Meeting URL copied to clipboard 👍', 'top-end');
}
function copyToClipboard(txt) {
function copyToClipboard(txt, showTxt = true) {
let tmpInput = document.createElement('input');
document.body.appendChild(tmpInput);
tmpInput.value = txt;
@@ -1193,7 +1204,9 @@ function copyToClipboard(txt) {
tmpInput.setSelectionRange(0, 99999); // For mobile devices
navigator.clipboard.writeText(tmpInput.value);
document.body.removeChild(tmpInput);
userLog('info', `${txt} copied to clipboard 👍`, 'top-end');
showTxt
? userLog('info', `${txt} copied to clipboard 👍`, 'top-end')
: userLog('info', `Copied to clipboard 👍`, 'top-end');
}
function shareRoomByEmail() {
@@ -1284,6 +1297,7 @@ function roomIsReady() {
}
BUTTONS.main.chatButton && show(chatButton);
BUTTONS.main.pollButton && show(pollButton);
BUTTONS.main.editorButton && show(editorButton);
BUTTONS.main.raiseHandButton && show(raiseHandButton);
BUTTONS.main.emojiRoomButton && show(emojiRoomButton);
!BUTTONS.chat.chatSaveButton && hide(chatSaveButton);
@@ -1375,6 +1389,7 @@ function roomIsReady() {
handleInputs();
handleChatEmojiPicker();
handleRoomEmojiPicker();
handleEditor();
loadSettingsFromLocalStorage();
startSessionTimer();
document.body.addEventListener('mousemove', (e) => {
@@ -1619,6 +1634,39 @@ function handleButtons() {
pollCreateForm.onsubmit = (e) => {
rc.pollCreateNewForm(e);
};
editorButton.onclick = () => {
rc.toggleEditor();
if (isPresenter && !rc.editorIsLocked()) {
rc.editorSendAction('toggle');
}
};
editorCloseBtn.onclick = () => {
rc.toggleEditor();
if (isPresenter && !rc.editorIsLocked()) {
rc.editorSendAction('toggle');
}
};
editorLockBtn.onclick = () => {
rc.toggleLockUnlockEditor();
};
editorUnlockBtn.onclick = () => {
rc.toggleLockUnlockEditor();
};
editorCleanBtn.onclick = () => {
rc.editorClean();
};
editorCopyBtn.onclick = () => {
rc.editorCopy();
};
editorSaveBtn.onclick = () => {
rc.editorSave();
};
editorUndoBtn.onclick = () => {
rc.editorUndo();
};
editorRedoBtn.onclick = () => {
rc.editorRedo();
};
transcriptionButton.onclick = () => {
transcription.toggle();
};
@@ -2667,6 +2715,50 @@ function handleRoomEmojiPicker() {
}
}
// ####################################################
// ROOM EDITOR
// ####################################################
function handleEditor() {
const toolbarOptions = [
[{ header: [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'link', 'code-block'],
[{ list: 'ordered' }, { list: 'bullet' }, { list: 'check' }],
[{ indent: '+1' }, { indent: '-1' }], // { align: [] }
//...
];
quill = new Quill('#editor', {
modules: {
toolbar: {
container: toolbarOptions,
},
syntax: true,
},
theme: 'snow',
});
applySyntaxHighlighting();
quill.on('text-change', (delta, oldDelta, source) => {
if (!isPresenter && rc.editorIsLocked()) {
return;
}
console.log('text-change', { delta, oldDelta, source });
applySyntaxHighlighting();
if (rc.thereAreParticipants() && source === 'user') {
socket.emit('editorChange', delta);
}
});
}
function applySyntaxHighlighting() {
const codeBlocks = document.querySelectorAll('.ql-syntax');
codeBlocks.forEach((block) => {
hljs.highlightElement(block);
});
}
// ####################################################
// LOAD SETTINGS FROM LOCAL STORAGE
// ####################################################
@@ -4344,7 +4436,7 @@ function showAbout() {
imageUrl: image.about,
customClass: { image: 'img-about' },
position: 'center',
title: 'WebRTC SFU v1.5.47',
title: 'WebRTC SFU v1.5.50',
html: `
<br />
<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 CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
* @version 1.5.47
* @version 1.5.50
*
*/
@@ -68,6 +68,7 @@ const icons = {
theme: '<i class="fas fa-fill-drip"></i>',
recSync: '<i class="fa-solid fa-cloud-arrow-up"></i>',
refresh: '<i class="fas fa-rotate"></i>',
editor: '<i class="fas fa-pen-to-square"></i>',
};
const image = {
@@ -261,6 +262,8 @@ class RoomClient {
this.isChatEmojiOpen = false;
this.isPollOpen = false;
this.isPollPinned = false;
this.isEditorOpen = false;
this.isEditorLocked = false;
this.isSpeechSynthesisSupported = isSpeechSynthesisSupported;
this.speechInMessages = false;
this.showChatOnMessage = true;
@@ -905,6 +908,9 @@ class RoomClient {
this.socket.on('endRTMPfromURL', this.handleEndRTMPfromURL);
this.socket.on('errorRTMPfromURL', this.handleErrorRTMPfromURL);
this.socket.on('updatePolls', this.handleUpdatePolls);
this.socket.on('editorChange', this.handleEditorChange);
this.socket.on('editorActions', this.handleEditorActions);
this.socket.on('editorUpdate', this.handleEditorUpdate);
}
// ####################################################
@@ -944,6 +950,7 @@ class RoomClient {
if (isBroadcastingEnabled) {
if (isParticipantsListOpen) getRoomParticipants();
wbUpdate();
this.editorUpdate();
} else {
adaptAspectRatio(participantsCount);
}
@@ -1079,6 +1086,18 @@ class RoomClient {
this.pollsUpdate(data);
};
handleEditorChange = (data) => {
this.handleEditorData(data);
};
handleEditorActions = (data) => {
this.handleEditorActionsData(data);
};
handleEditorUpdate = (data) => {
this.handleEditorUpdateData(data);
};
// ####################################################
// SERVER AWAY/MAINTENANCE
// ####################################################
@@ -2166,6 +2185,8 @@ class RoomClient {
try {
wbUpdate();
this.editorUpdate();
const { consumer, stream, kind } = await this.getConsumeStream(producer_id, peer_info.peer_id, type);
console.log('CONSUMER MEDIA TYPE ----> ' + type);
@@ -2608,6 +2629,8 @@ class RoomClient {
//
wbUpdate();
this.editorUpdate();
this.handleHideMe();
}
@@ -4656,6 +4679,160 @@ class RoomClient {
return `Poll_${roomName}_${dateTime}.txt`;
}
// ####################################################
// EDITOR
// ####################################################
toggleEditor() {
editorRoom.classList.toggle('show');
if (!this.isEditorOpen) {
this.sound('open');
}
this.isEditorOpen = !this.isEditorOpen;
}
toggleLockUnlockEditor() {
this.isEditorLocked = !this.isEditorLocked;
const btnToShow = this.isEditorLocked ? editorLockBtn : editorUnlockBtn;
const btnToHide = this.isEditorLocked ? editorUnlockBtn : editorLockBtn;
const btnColor = this.isEditorLocked ? 'red' : 'white';
const action = this.isEditorLocked ? 'lock' : 'unlock';
show(btnToShow);
hide(btnToHide);
setColor(editorLockBtn, btnColor);
this.editorSendAction(action);
if (this.isEditorLocked) {
userLog('info', 'The Editor is locked. \n The participants cannot interact with it.', 'top-right');
sound('locked');
}
}
editorCenter() {
editorRoom.style.position = 'fixed';
editorRoom.style.transform = 'translate(-50%, -50%)';
editorRoom.style.top = '50%';
editorRoom.style.left = '50%';
}
editorUpdate() {
if (this.isEditorOpen && (!isRulesActive || isPresenter)) {
console.log('IsPresenter: update editor content to the participants in the room');
const content = quill.getContents(); // Get content in Delta format
this.socket.emit('editorUpdate', content);
const action = this.isEditorLocked ? 'lock' : 'unlock';
this.editorSendAction(action);
}
}
handleEditorUpdateData(data) {
this.editorOpen();
quill.setContents(data);
}
handleEditorData(data) {
this.editorOpen();
quill.updateContents(data);
}
editorOpen() {
if (!this.isEditorOpen) {
this.sound('open');
this.toggleEditor();
}
}
handleEditorActionsData(data) {
const { peer_name, action } = data;
switch (action) {
case 'toggle':
this.toggleEditor();
this.userLog('info', `${icons.editor} ${peer_name} toggle editor`, 'top-end', 6000);
break;
case 'clean':
quill.setText('');
this.userLog('info', `${icons.editor} ${peer_name} cleared editor`, 'top-end', 6000);
break;
case 'lock':
this.isEditorLocked = true;
quill.enable(false);
this.userLog('info', `${icons.editor} ${peer_name} locked the editor`, 'top-end', 6000);
break;
case 'unlock':
this.isEditorLocked = false;
quill.enable(true);
this.userLog('info', `${icons.editor} ${peer_name} unlocked the editor`, 'top-end', 6000);
break;
default:
break;
}
}
editorIsLocked() {
return this.isEditorLocked;
}
editorUndo() {
quill.history.undo();
}
editorRedo() {
quill.history.redo();
}
editorCopy() {
const content = quill.getText();
if (content.trim().length === 0) {
return this.userLog('info', 'Nothing to copy', 'top-end');
}
copyToClipboard(content, false);
}
editorClean() {
if (!isPresenter && this.editorIsLocked()) {
userLog('info', 'The Editor is locked. \n You cannot interact with it.', 'top-right');
return;
}
const content = quill.getText();
if (content.trim().length === 0) {
return this.userLog('info', 'Nothing to clear', 'top-end');
}
Swal.fire({
background: swalBackground,
position: 'center',
title: 'Clear the editor content?',
imageUrl: image.delete,
showDenyButton: true,
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
quill.setText('');
this.editorSendAction('clean');
this.sound('delete');
}
});
}
editorSave() {
const content = quill.getText();
if (content.trim().length === 0) {
return this.userLog('info', 'No data to save!', 'top-end');
}
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const file = 'Room_' + this.room_id + getDataTimeString() + '_editor.txt';
this.saveBlobToFile(blob, file);
}
editorSendAction(action) {
this.socket.emit('editorActions', { peer_name: this.peer_name, action: action });
}
// ####################################################
// RECORDING
// ####################################################

عرض الملف

@@ -134,6 +134,7 @@ function handleRules(isPresenter) {
//BUTTONS.consumerVideo.muteAudioButton = false;
//BUTTONS.consumerVideo.muteVideoButton = false;
BUTTONS.whiteboard.whiteboardLockButton = false;
//...
} else {
// ##################################
@@ -144,6 +145,8 @@ function handleRules(isPresenter) {
BUTTONS.settings.lockRoomButton = BUTTONS.settings.lockRoomButton && !isRoomLocked;
BUTTONS.settings.unlockRoomButton = BUTTONS.settings.lockRoomButton && isRoomLocked;
BUTTONS.settings.sendEmailInvitation = true;
show(editorUnlockBtn);
//...
// ##################################
@@ -267,6 +270,7 @@ function handleRulesBroadcasting() {
//elemDisplay('snapshotRoomButton', false);
//elemDisplay('emojiRoomButton', false);
//elemDisplay('pollButton', false);
//elemDisplay('editorButton', false);
elemDisplay('transcriptionButton', false);
elemDisplay('lockRoomButton', false);
elemDisplay('unlockRoomButton', false);

عرض الملف

@@ -30,6 +30,8 @@ const commands = {
chatOff: 'close the chat',
pollOn: 'open the poll',
pollOff: 'close the poll',
editorOn: 'open the editor',
editorOff: 'close the editor',
toggleTr: 'toggle transcription',
whiteboardOn: 'open the whiteboard',
whiteboardOff: 'close the whiteboard',
@@ -197,6 +199,14 @@ function execVoiceCommands(transcript) {
printCommand(commands.pollOff);
pollCloseBtn.click();
break;
case commands.editorOn:
printCommand(commands.editorOn);
editorButton.click();
break;
case commands.editorOff:
printCommand(commands.editorOff);
editorCloseBtn.click();
break;
case commands.chatSend:
printCommand(commands.chatSend);
chatSendButton.click();

عرض الملف

@@ -43,6 +43,7 @@
<link rel="stylesheet" href="../css/VideoGrid.css" />
<link rel="stylesheet" href="../css/GroupChat.css" />
<link rel="stylesheet" href="../css/Polls.css" />
<link rel="stylesheet" href="../css/Editor.css" />
<!-- https://cdnjs.com/libraries/font-awesome -->
@@ -67,6 +68,17 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/themes/monolith.min.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/themes/nano.min.css" />
<!-- Highlight https://github.com/highlightjs/highlight.js -->
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/atom-one-dark.min.css"
/>
<!-- Quill https://github.com/slab/quill -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.snow.css" />
<script defer src="../js/Brand.js"></script>
<!-- Modern or es5 bundle -->
@@ -124,6 +136,8 @@
<script defer src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<script defer src="https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/highlight.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.js"></script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
@@ -169,8 +183,9 @@ access to use this app.
<button id="stopRecButton" class="hidden"><i class="fas fa-record-vinyl cr"></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="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="pollButton" class="hidden"><i class="fas fa-square-poll-horizontal"></i></button>
<button id="editorButton" class="hidden"><i class="fas fa-pen-to-square"></i></button>
<button id="whiteboardButton" class="hidden"><i class="fas fa-chalkboard-teacher"></i></button>
<button id="snapshotRoomButton" class="hidden"><i class="fas fas fa-camera-retro"></i></button>
<button id="settingsButton" class="hidden"><i class="fas fa-cogs"></i></button>
@@ -1526,6 +1541,22 @@ access to use this app.
</div>
</section>
<section id="editorRoom" class="editor-container fadein hidden">
<div id="editorHeader" class="editor-header">
<div class="editor-header-btns">
<button id="editorUnlockBtn" class="fa-solid fa-lock-open hidden"></button>
<button id="editorLockBtn" class="fa-solid fa-lock hidden"></button>
<button id="editorUndoBtn" class="fas fa-undo"></button>
<button id="editorRedoBtn" class="fas fa-redo"></button>
<button id="editorCopyBtn" class="fas fa-paste"></button>
<button id="editorSaveBtn" class="fas fa-floppy-disk"></button>
<button id="editorCleanBtn" class="fas fa-trash"></button>
<button id="editorCloseBtn" class="fas fa-times"></button>
</div>
</div>
<div class="editor" id="editor"></div>
</section>
<section id="transcriptionRoom" class="transcription-room fadein">
<section id="transcriptionSection" class="transcription">
<header id="transcriptionHeader" class="transcription-header">