[mirotalk] - refactor(chat): migrate room UI markup to templates and CSS, fix private/chatgpt entries, and keep sender info visible
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
# ====================================================
|
# ====================================================
|
||||||
# MiroTalk P2P v.1.8.28 - Environment Configuration
|
# MiroTalk P2P v.1.8.30 - Environment Configuration
|
||||||
# ====================================================
|
# ====================================================
|
||||||
|
|
||||||
# App environment
|
# App environment
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* ==============================================
|
* ==============================================
|
||||||
* MiroTalk P2P v.1.8.28 - Configuration File
|
* MiroTalk P2P v.1.8.30 - Configuration File
|
||||||
* ==============================================
|
* ==============================================
|
||||||
*
|
*
|
||||||
* This file is the central configuration source.
|
* This file is the central configuration source.
|
||||||
|
|||||||
+1
-1
@@ -45,7 +45,7 @@ dependencies: {
|
|||||||
* @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon
|
* @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon
|
||||||
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
|
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
|
||||||
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
||||||
* @version 1.8.28
|
* @version 1.8.30
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
Generated
+7
-7
@@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "mirotalk",
|
"name": "mirotalk",
|
||||||
"version": "1.8.28",
|
"version": "1.8.30",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mirotalk",
|
"name": "mirotalk",
|
||||||
"version": "1.8.28",
|
"version": "1.8.30",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mattermost/client": "11.6.0",
|
"@mattermost/client": "11.6.0",
|
||||||
"@ngrok/ngrok": "1.7.0",
|
"@ngrok/ngrok": "1.7.0",
|
||||||
"@sentry/node": "^10.51.0",
|
"@sentry/node": "^10.51.0",
|
||||||
"axios": "^1.15.2",
|
"axios": "^1.16.0",
|
||||||
"chokidar": "^5.0.0",
|
"chokidar": "^5.0.0",
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
"compression": "^1.8.1",
|
"compression": "^1.8.1",
|
||||||
@@ -1718,12 +1718,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.15.2",
|
"version": "1.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz",
|
||||||
"integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==",
|
"integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.11",
|
"follow-redirects": "^1.16.0",
|
||||||
"form-data": "^4.0.5",
|
"form-data": "^4.0.5",
|
||||||
"proxy-from-env": "^2.1.0"
|
"proxy-from-env": "^2.1.0"
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mirotalk",
|
"name": "mirotalk",
|
||||||
"version": "1.8.28",
|
"version": "1.8.30",
|
||||||
"description": "A free WebRTC browser-based video call",
|
"description": "A free WebRTC browser-based video call",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
"@mattermost/client": "11.6.0",
|
"@mattermost/client": "11.6.0",
|
||||||
"@ngrok/ngrok": "1.7.0",
|
"@ngrok/ngrok": "1.7.0",
|
||||||
"@sentry/node": "^10.51.0",
|
"@sentry/node": "^10.51.0",
|
||||||
"axios": "^1.15.2",
|
"axios": "^1.16.0",
|
||||||
"chokidar": "^5.0.0",
|
"chokidar": "^5.0.0",
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
"compression": "^1.8.1",
|
"compression": "^1.8.1",
|
||||||
|
|||||||
+18
-3
@@ -1996,15 +1996,15 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.msg-grouped .msg-img {
|
.msg-grouped .msg-img {
|
||||||
visibility: hidden;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-grouped .msg-info-name {
|
.msg-grouped .msg-info-name {
|
||||||
display: none;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-grouped .msg-info {
|
.msg-grouped .msg-info {
|
||||||
justify-content: flex-end;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msger-copy-txt {
|
.msger-copy-txt {
|
||||||
@@ -2902,6 +2902,16 @@ button {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.share-room-modal-highlight {
|
||||||
|
color: rgb(8, 189, 89);
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-room-modal-description {
|
||||||
|
background: transparent;
|
||||||
|
color: #fff;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
#qrRoomPopupContainer {
|
#qrRoomPopupContainer {
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -2930,6 +2940,11 @@ button {
|
|||||||
height: 256px;
|
height: 256px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.kicked-out-modal-alert-title,
|
||||||
|
.kicked-out-modal-alert-time {
|
||||||
|
color: #ff2d00;
|
||||||
|
}
|
||||||
|
|
||||||
/*--------------------------------------------------------------
|
/*--------------------------------------------------------------
|
||||||
# My settings
|
# My settings
|
||||||
--------------------------------------------------------------*/
|
--------------------------------------------------------------*/
|
||||||
|
|||||||
+1
-1
@@ -109,7 +109,7 @@ let brand = {
|
|||||||
},
|
},
|
||||||
about: {
|
about: {
|
||||||
imageUrl: '../images/mirotalk-logo.gif',
|
imageUrl: '../images/mirotalk-logo.gif',
|
||||||
title: 'WebRTC P2P v1.8.28',
|
title: 'WebRTC P2P v1.8.30',
|
||||||
html: `
|
html: `
|
||||||
<button
|
<button
|
||||||
id="support-button"
|
id="support-button"
|
||||||
|
|||||||
+179
-192
@@ -15,7 +15,7 @@
|
|||||||
* @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon
|
* @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon
|
||||||
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
|
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
|
||||||
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
||||||
* @version 1.8.28
|
* @version 1.8.30
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -1052,7 +1052,11 @@ function getInfo() {
|
|||||||
})
|
})
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
extraInfo.innerHTML = `<div class="extra-info-grid">${rows}</div>`;
|
extraInfo.innerHTML = renderRoomTemplate('tpl-extra-info-grid', {
|
||||||
|
html: {
|
||||||
|
rows,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return parserResult;
|
return parserResult;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1611,7 +1615,11 @@ function roomIsBusy() {
|
|||||||
imageUrl: images.forbidden,
|
imageUrl: images.forbidden,
|
||||||
position: 'center',
|
position: 'center',
|
||||||
title: 'Room is busy',
|
title: 'Room is busy',
|
||||||
html: `The room is limited to ${thisMaxRoomParticipants} users. <br/> Please try again later`,
|
html: renderRoomTemplate('tpl-room-busy-message', {
|
||||||
|
text: {
|
||||||
|
maxUsers: String(thisMaxRoomParticipants),
|
||||||
|
},
|
||||||
|
}),
|
||||||
showDenyButton: false,
|
showDenyButton: false,
|
||||||
confirmButtonText: `OK`,
|
confirmButtonText: `OK`,
|
||||||
showClass: { popup: 'animate__animated animate__fadeInDown' },
|
showClass: { popup: 'animate__animated animate__fadeInDown' },
|
||||||
@@ -1840,7 +1848,14 @@ function renderDynamicThemeCards() {
|
|||||||
card.className = 'theme-card';
|
card.className = 'theme-card';
|
||||||
card.dataset.theme = name;
|
card.dataset.theme = name;
|
||||||
card.dataset.index = index;
|
card.dataset.index = index;
|
||||||
card.innerHTML = `<i class="${iconClass}"></i><span>${option.textContent}</span>`;
|
card.innerHTML = renderRoomTemplate('tpl-theme-card-content', {
|
||||||
|
text: {
|
||||||
|
label: option.textContent,
|
||||||
|
},
|
||||||
|
attrs: {
|
||||||
|
iconClass,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Apply dynamic icon color via inline style
|
// Apply dynamic icon color via inline style
|
||||||
const icon = card.querySelector('i');
|
const icon = card.querySelector('i');
|
||||||
@@ -2118,7 +2133,7 @@ function userNameAlreadyInRoom() {
|
|||||||
imageUrl: images.forbidden,
|
imageUrl: images.forbidden,
|
||||||
position: 'center',
|
position: 'center',
|
||||||
title: 'Username',
|
title: 'Username',
|
||||||
html: `The Username is already in use. <br/> Please try with another one`,
|
html: renderRoomTemplate('tpl-username-in-use-message'),
|
||||||
showDenyButton: false,
|
showDenyButton: false,
|
||||||
confirmButtonText: `OK`,
|
confirmButtonText: `OK`,
|
||||||
showClass: { popup: 'animate__animated animate__fadeInDown' },
|
showClass: { popup: 'animate__animated animate__fadeInDown' },
|
||||||
@@ -8370,14 +8385,11 @@ function shareRoomMeetingURL(checkScreen = false) {
|
|||||||
background: swBg,
|
background: swBg,
|
||||||
position: 'center',
|
position: 'center',
|
||||||
title: 'Share the room',
|
title: 'Share the room',
|
||||||
html: `
|
html: renderRoomTemplate('tpl-share-room-modal', {
|
||||||
<div id="qrRoomContainer">
|
text: {
|
||||||
<canvas id="qrRoom"></canvas>
|
roomURL,
|
||||||
</div>
|
},
|
||||||
<br/>
|
}),
|
||||||
<p style="color:rgb(8, 189, 89);">Join from your mobile device</p>
|
|
||||||
<p style="background:transparent; color:white; font-family: Arial, Helvetica, sans-serif;">No need for apps, simply capture the QR code with your mobile camera Or Invite someone else to join by sending them the following URL</p>
|
|
||||||
<p style="color:rgb(8, 189, 89);">${roomURL}</p>`,
|
|
||||||
showDenyButton: true,
|
showDenyButton: true,
|
||||||
showCancelButton: true,
|
showCancelButton: true,
|
||||||
cancelButtonColor: 'red',
|
cancelButtonColor: 'red',
|
||||||
@@ -9731,7 +9743,11 @@ async function downloadRecordedStream() {
|
|||||||
</ul>
|
</ul>
|
||||||
<br/>
|
<br/>
|
||||||
`;
|
`;
|
||||||
lastRecordingInfo.innerHTML = `<br/>Last recording info: ${recordingInfo}`;
|
lastRecordingInfo.innerHTML = renderRoomTemplate('tpl-last-recording-info', {
|
||||||
|
html: {
|
||||||
|
recordingInfo,
|
||||||
|
},
|
||||||
|
});
|
||||||
recordingTime.innerText = '';
|
recordingTime.innerText = '';
|
||||||
|
|
||||||
msgHTML(
|
msgHTML(
|
||||||
@@ -10719,17 +10735,15 @@ function handleSpeechTranscript(config) {
|
|||||||
// parser decodes back to " in attribute context (double-decode XSS).
|
// parser decodes back to " in attribute context (double-decode XSS).
|
||||||
// Use a temporary id and setAttribute instead.
|
// Use a temporary id and setAttribute instead.
|
||||||
const captionAvatarTmpId = `capt-av-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
const captionAvatarTmpId = `capt-av-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
||||||
const msgHTML = `
|
const msgHTML = renderRoomTemplate('tpl-caption-message', {
|
||||||
<div class="msg left-msg">
|
text: {
|
||||||
<img class="msg-img" id="${captionAvatarTmpId}" />
|
captionInfoText: `${peer_name} : ${time_stamp}`,
|
||||||
<div class="msg-caption-bubble">
|
captionText: text_data,
|
||||||
<div class="msg-info">
|
},
|
||||||
<div class="msg-info-name">${peer_name} : ${time_stamp}</div>
|
attrs: {
|
||||||
</div>
|
captionAvatarTmpId,
|
||||||
<div class="msg-text">${text_data}</div>
|
},
|
||||||
</div>
|
});
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
captionChat.insertAdjacentHTML('beforeend', msgHTML);
|
captionChat.insertAdjacentHTML('beforeend', msgHTML);
|
||||||
const captionAvatarEl = document.getElementById(captionAvatarTmpId);
|
const captionAvatarEl = document.getElementById(captionAvatarTmpId);
|
||||||
if (captionAvatarEl) {
|
if (captionAvatarEl) {
|
||||||
@@ -10810,23 +10824,7 @@ function appendMessage(from, img, side, msg, privateMsg, msgId = null, to = '')
|
|||||||
// getImg is a user-controlled URL; use a temporary id and setAttribute
|
// getImg is a user-controlled URL; use a temporary id and setAttribute
|
||||||
// after insertion to avoid double-decode XSS via insertAdjacentHTML.
|
// after insertion to avoid double-decode XSS via insertAdjacentHTML.
|
||||||
const msgAvatarTmpId = `msg-av-${chatMessagesId}`;
|
const msgAvatarTmpId = `msg-av-${chatMessagesId}`;
|
||||||
let msgHTML = `
|
let messageActionsHTML = `
|
||||||
<div id="msg-${chatMessagesId}" class="msg ${getSide}-msg" data-sender="${getFrom}" data-chat-type="${
|
|
||||||
getPrivateMsg ? 'private' : 'public'
|
|
||||||
}" data-chat-peer="${conversationPeer}" data-msg-id="${normalizedMsgId}">
|
|
||||||
<img class="msg-img" id="${msgAvatarTmpId}" />
|
|
||||||
<div class=${msgBubble}>
|
|
||||||
<div class="msg-info">
|
|
||||||
<div class="msg-info-name">${getFrom}</div>
|
|
||||||
<div class="msg-info-time">${time}</div>
|
|
||||||
</div>
|
|
||||||
<div class="msg-text">
|
|
||||||
<span id="message-${chatMessagesId}"></span>
|
|
||||||
<hr/>
|
|
||||||
<div class="msg-footer">
|
|
||||||
<div class="msg-actions">
|
|
||||||
`;
|
|
||||||
msgHTML += `
|
|
||||||
<button
|
<button
|
||||||
id="msg-delete-${chatMessagesId}"
|
id="msg-delete-${chatMessagesId}"
|
||||||
class="${className.trash}"
|
class="${className.trash}"
|
||||||
@@ -10839,7 +10837,7 @@ function appendMessage(from, img, side, msg, privateMsg, msgId = null, to = '')
|
|||||||
style="color:#fff; border:none; background:transparent;"
|
style="color:#fff; border:none; background:transparent;"
|
||||||
onclick="copyToClipboard('message-${chatMessagesId}')"
|
onclick="copyToClipboard('message-${chatMessagesId}')"
|
||||||
></button>`;
|
></button>`;
|
||||||
msgHTML += `
|
messageActionsHTML += `
|
||||||
<button
|
<button
|
||||||
id="msg-reaction-${chatMessagesId}"
|
id="msg-reaction-${chatMessagesId}"
|
||||||
class="reaction-toggle-btn"
|
class="reaction-toggle-btn"
|
||||||
@@ -10847,7 +10845,7 @@ function appendMessage(from, img, side, msg, privateMsg, msgId = null, to = '')
|
|||||||
onclick="toggleReactionPicker('${normalizedMsgId}', this)"
|
onclick="toggleReactionPicker('${normalizedMsgId}', this)"
|
||||||
>😊</button>`;
|
>😊</button>`;
|
||||||
if (isSpeechSynthesisSupported) {
|
if (isSpeechSynthesisSupported) {
|
||||||
msgHTML += `
|
messageActionsHTML += `
|
||||||
<button
|
<button
|
||||||
id="msg-speech-${chatMessagesId}"
|
id="msg-speech-${chatMessagesId}"
|
||||||
class="${className.speech}"
|
class="${className.speech}"
|
||||||
@@ -10855,14 +10853,26 @@ function appendMessage(from, img, side, msg, privateMsg, msgId = null, to = '')
|
|||||||
onclick="speechElementText(false, '${getFrom}', 'message-${chatMessagesId}')"
|
onclick="speechElementText(false, '${getFrom}', 'message-${chatMessagesId}')"
|
||||||
></button>`;
|
></button>`;
|
||||||
}
|
}
|
||||||
msgHTML += `
|
|
||||||
</div>
|
const msgHTML = renderRoomTemplate('tpl-msger-chat-message', {
|
||||||
<div class="message-reactions"></div>
|
text: {
|
||||||
</div>
|
senderName: getFrom,
|
||||||
</div>
|
messageTime: time,
|
||||||
</div>
|
},
|
||||||
</div>
|
html: {
|
||||||
`;
|
messageActions: messageActionsHTML,
|
||||||
|
},
|
||||||
|
attrs: {
|
||||||
|
messageContainerId: `msg-${chatMessagesId}`,
|
||||||
|
messageContainerClass: `msg ${getSide}-msg`,
|
||||||
|
chatType: getPrivateMsg ? 'private' : 'public',
|
||||||
|
chatPeer: conversationPeer,
|
||||||
|
messageId: normalizedMsgId,
|
||||||
|
messageAvatarTmpId: msgAvatarTmpId,
|
||||||
|
messageBubbleClass: msgBubble,
|
||||||
|
messageTextId: `message-${chatMessagesId}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
msgerChat.insertAdjacentHTML('beforeend', msgHTML);
|
msgerChat.insertAdjacentHTML('beforeend', msgHTML);
|
||||||
const msgAvatarEl = document.getElementById(msgAvatarTmpId);
|
const msgAvatarEl = document.getElementById(msgAvatarTmpId);
|
||||||
@@ -11069,8 +11079,9 @@ function resolvePeerNameById(peerId = '') {
|
|||||||
if (peerId === CHAT_GPT_PEER_ID) return CHAT_GPT_NAME;
|
if (peerId === CHAT_GPT_PEER_ID) return CHAT_GPT_NAME;
|
||||||
|
|
||||||
const privateChatButton = getId(peerId + '_pMsgBtn');
|
const privateChatButton = getId(peerId + '_pMsgBtn');
|
||||||
if (privateChatButton?.value) {
|
const privatePeerName = privateChatButton?.dataset?.value || privateChatButton?.getAttribute('data-value');
|
||||||
return privateChatButton.value;
|
if (privatePeerName) {
|
||||||
|
return privatePeerName;
|
||||||
}
|
}
|
||||||
|
|
||||||
return allPeers[peerId]?.peer_name || '';
|
return allPeers[peerId]?.peer_name || '';
|
||||||
@@ -11104,31 +11115,22 @@ function ensureChatGPTConversationEntry() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatGPTEntry = `
|
const chatGPTEntry = renderRoomTemplate('tpl-chatgpt-participant-entry', {
|
||||||
<div id="${CHAT_GPT_PEER_ID}_pMsgDiv" class="msger-private-chat-entry" data-peer-name="${CHAT_GPT_NAME.toLowerCase()}">
|
text: {
|
||||||
<div
|
participantName: CHAT_GPT_NAME,
|
||||||
id="${CHAT_GPT_PEER_ID}_pMsgBtn"
|
participantSubtitle: 'Ask anything',
|
||||||
class="msger-chat-item"
|
},
|
||||||
role="button"
|
attrs: {
|
||||||
tabindex="0"
|
participantName: CHAT_GPT_NAME,
|
||||||
data-value="${CHAT_GPT_NAME}"
|
entryId: `${CHAT_GPT_PEER_ID}_pMsgDiv`,
|
||||||
data-peer-id="${CHAT_GPT_PEER_ID}"
|
entryPeerName: CHAT_GPT_NAME.toLowerCase(),
|
||||||
title="${CHAT_GPT_NAME}"
|
buttonId: `${CHAT_GPT_PEER_ID}_pMsgBtn`,
|
||||||
>
|
participantPeerId: CHAT_GPT_PEER_ID,
|
||||||
<img
|
avatarId: `${CHAT_GPT_PEER_ID}_pMsgAvatar`,
|
||||||
id="${CHAT_GPT_PEER_ID}_pMsgAvatar"
|
avatarSrc: images.chatgpt,
|
||||||
class="msger-chat-avatar"
|
badgeId: `${CHAT_GPT_PEER_ID}_pMsgBadge`,
|
||||||
src="${images.chatgpt}"
|
},
|
||||||
alt="${CHAT_GPT_NAME}"
|
});
|
||||||
/>
|
|
||||||
<span class="msger-chat-item-copy">
|
|
||||||
<strong>${CHAT_GPT_NAME}</strong>
|
|
||||||
<small>Ask anything</small>
|
|
||||||
</span>
|
|
||||||
<span id="${CHAT_GPT_PEER_ID}_pMsgBadge" class="msger-chat-unread-badge hidden">0</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
msgerCPList.insertAdjacentHTML('afterbegin', chatGPTEntry);
|
msgerCPList.insertAdjacentHTML('afterbegin', chatGPTEntry);
|
||||||
|
|
||||||
@@ -11471,28 +11473,33 @@ async function msgerAddPeers(peers) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const msgerPrivateDiv = `
|
const msgerPrivateDiv = renderRoomTemplate('tpl-msger-private-entry', {
|
||||||
<div id="${peer_id}_pMsgDiv" class="msger-private-chat-entry" data-peer-name="${peer_name.toLowerCase()}">
|
text: {
|
||||||
<div id="${peer_id}_pMsgBtn" class="msger-chat-item" role="button" tabindex="0" data-value="${peer_name}" data-peer-id="${peer_id}" title="${peer_name}">
|
participantName: peer_name,
|
||||||
<img id="${peer_id}_pMsgAvatar" class="msger-chat-avatar" src="${chatAvatar}" alt="${peer_name}" />
|
participantSubtitle: 'Open private conversation',
|
||||||
<span class="msger-chat-item-copy">
|
},
|
||||||
<strong>${peer_name}</strong>
|
html: {
|
||||||
<small>Open private conversation</small>
|
dropdownOptions,
|
||||||
</span>
|
},
|
||||||
<span id="${peer_id}_pMsgBadge" class="msger-chat-unread-badge hidden">0</span>
|
attrs: {
|
||||||
<div id="${peer_id}_pDropdownMenu" class="dropdown-menu-custom msger-participant-dropdown">
|
participantName: peer_name,
|
||||||
<button id="${peer_id}_pDropdownToggle" class="dropdown-toggle" type="button">
|
entryId: `${peer_id}_pMsgDiv`,
|
||||||
<i class="fas fa-ellipsis-vertical"></i>
|
entryPeerName: peer_name.toLowerCase(),
|
||||||
</button>
|
buttonId: `${peer_id}_pMsgBtn`,
|
||||||
<ul id="${peer_id}_pDropdownMenuList" class="dropdown-menu-custom-list app-dropdown-menu msger-participant-dropdown-menu">
|
participantPeerId: peer_id,
|
||||||
${dropdownOptions}
|
avatarTmpId: `${peer_id}_pMsgAvatar`,
|
||||||
</ul>
|
badgeId: `${peer_id}_pMsgBadge`,
|
||||||
</div>
|
dropdownMenuId: `${peer_id}_pDropdownMenu`,
|
||||||
</div>
|
dropdownToggleId: `${peer_id}_pDropdownToggle`,
|
||||||
</div>
|
dropdownListId: `${peer_id}_pDropdownMenuList`,
|
||||||
`;
|
},
|
||||||
|
});
|
||||||
|
|
||||||
msgerCPList.insertAdjacentHTML('beforeend', msgerPrivateDiv);
|
msgerCPList.insertAdjacentHTML('beforeend', msgerPrivateDiv);
|
||||||
|
const participantAvatar = getId(`${peer_id}_pMsgAvatar`);
|
||||||
|
if (participantAvatar) {
|
||||||
|
participantAvatar.setAttribute('src', chatAvatar);
|
||||||
|
}
|
||||||
msgerCPList.scrollTop += 500;
|
msgerCPList.scrollTop += 500;
|
||||||
|
|
||||||
const msgerPrivateBtn = getId(peer_id + '_pMsgBtn');
|
const msgerPrivateBtn = getId(peer_id + '_pMsgBtn');
|
||||||
@@ -11988,17 +11995,11 @@ function emitMsg(from, fromAvatar, to, msg, privateMsg, id, msgId = '') {
|
|||||||
function showAITypingIndicator(aiName) {
|
function showAITypingIndicator(aiName) {
|
||||||
const existing = getId(`ai-typing-${aiName}`);
|
const existing = getId(`ai-typing-${aiName}`);
|
||||||
if (existing) return;
|
if (existing) return;
|
||||||
const typingHTML = `
|
const typingHTML = renderRoomTemplate('tpl-ai-typing-indicator', {
|
||||||
<div id="ai-typing-${aiName}" class="msg left-msg">
|
attrs: {
|
||||||
<div class="ai-typing-indicator">
|
typingIndicatorId: `ai-typing-${aiName}`,
|
||||||
<div class="typing-dots">
|
},
|
||||||
<span></span>
|
});
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
msgerChat.insertAdjacentHTML('beforeend', typingHTML);
|
msgerChat.insertAdjacentHTML('beforeend', typingHTML);
|
||||||
msgerChat.scrollTop = msgerChat.scrollHeight;
|
msgerChat.scrollTop = msgerChat.scrollHeight;
|
||||||
}
|
}
|
||||||
@@ -14053,25 +14054,7 @@ function createStickyNote() {
|
|||||||
Swal.fire({
|
Swal.fire({
|
||||||
background: swBg,
|
background: swBg,
|
||||||
title: 'Create Sticky Note',
|
title: 'Create Sticky Note',
|
||||||
html: `
|
html: renderRoomTemplate('tpl-sticky-note-form'),
|
||||||
<div class="sticky-note-form">
|
|
||||||
<textarea id="stickyNoteText" class="sticky-note-textarea" rows="4" placeholder="Type your note here...">Note</textarea>
|
|
||||||
<div class="sticky-note-colors-row">
|
|
||||||
<div class="sticky-note-color-group">
|
|
||||||
<label for="stickyNoteColor" class="sticky-note-color-label">
|
|
||||||
<i class="fas fa-palette"></i> Background
|
|
||||||
</label>
|
|
||||||
<input id="stickyNoteColor" type="color" value="#FFEB3B" class="sticky-note-color-input">
|
|
||||||
</div>
|
|
||||||
<div class="sticky-note-color-group">
|
|
||||||
<label for="stickyNoteTextColor" class="sticky-note-color-label">
|
|
||||||
<i class="fas fa-font"></i> Text
|
|
||||||
</label>
|
|
||||||
<input id="stickyNoteTextColor" type="color" value="#000000" class="sticky-note-color-input">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
showCancelButton: true,
|
showCancelButton: true,
|
||||||
confirmButtonText: 'Create',
|
confirmButtonText: 'Create',
|
||||||
cancelButtonText: 'Cancel',
|
cancelButtonText: 'Cancel',
|
||||||
@@ -14198,25 +14181,13 @@ async function openFilePickerModal(config) {
|
|||||||
position: 'center',
|
position: 'center',
|
||||||
title: title,
|
title: title,
|
||||||
input: 'file',
|
input: 'file',
|
||||||
html: `
|
html: renderRoomTemplate('tpl-file-picker-modal', {
|
||||||
<div class="mirotalk-file-picker">
|
text: {
|
||||||
<button type="button" id="mirotalkFileDropzone" class="mirotalk-file-dropzone">
|
emptyStateTitle,
|
||||||
<span class="mirotalk-file-dropzone-icon"><i class="fas fa-cloud-upload-alt"></i></span>
|
emptyStateSubtitle,
|
||||||
<span id="mirotalkFileDropzoneTitle" class="mirotalk-file-dropzone-title">${emptyStateTitle}</span>
|
helperText,
|
||||||
<span id="mirotalkFileDropzoneSubtitle" class="mirotalk-file-dropzone-subtitle">${emptyStateSubtitle}</span>
|
},
|
||||||
<span class="mirotalk-file-dropzone-helper">${helperText}</span>
|
}),
|
||||||
<span id="mirotalkFileBrowseBtn" class="mirotalk-file-dropzone-cta">Browse files</span>
|
|
||||||
</button>
|
|
||||||
<div id="mirotalkFilePreview" class="mirotalk-file-preview" hidden>
|
|
||||||
<div class="mirotalk-file-preview-icon"><i class="fas fa-file-alt"></i></div>
|
|
||||||
<div class="mirotalk-file-preview-meta">
|
|
||||||
<strong id="mirotalkFileName">No file selected</strong>
|
|
||||||
<span id="mirotalkFileDetails"></span>
|
|
||||||
</div>
|
|
||||||
<button type="button" id="mirotalkFileRemoveBtn" class="mirotalk-file-preview-remove">Remove</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
inputAttributes: {
|
inputAttributes: {
|
||||||
accept: accept,
|
accept: accept,
|
||||||
'aria-label': title,
|
'aria-label': title,
|
||||||
@@ -15742,11 +15713,11 @@ function handleKickedOut(config) {
|
|||||||
position: 'center',
|
position: 'center',
|
||||||
imageUrl: images.leave,
|
imageUrl: images.leave,
|
||||||
title: 'Kicked out!',
|
title: 'Kicked out!',
|
||||||
html:
|
html: renderRoomTemplate('tpl-kicked-out-modal', {
|
||||||
`<h2 style="color: #FF2D00;">` +
|
text: {
|
||||||
`User ` +
|
peerName: peer_name,
|
||||||
peer_name +
|
},
|
||||||
`</h2> will kick out you after <b style="color: #FF2D00;"></b> milliseconds.`,
|
}),
|
||||||
timer: 5000,
|
timer: 5000,
|
||||||
timerProgressBar: true,
|
timerProgressBar: true,
|
||||||
didOpen: () => {
|
didOpen: () => {
|
||||||
@@ -15776,50 +15747,19 @@ function handleKickedOut(config) {
|
|||||||
function showAbout() {
|
function showAbout() {
|
||||||
playSound('newMessage');
|
playSound('newMessage');
|
||||||
|
|
||||||
|
const aboutHtml = brand.about.html;
|
||||||
|
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
background: swBg,
|
background: swBg,
|
||||||
position: 'center',
|
position: 'center',
|
||||||
title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.8.28',
|
title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.8.30',
|
||||||
imageUrl: brand.about?.imageUrl && brand.about.imageUrl.trim() !== '' ? brand.about.imageUrl : images.about,
|
imageUrl: brand.about?.imageUrl && brand.about.imageUrl.trim() !== '' ? brand.about.imageUrl : images.about,
|
||||||
customClass: { image: 'img-about' },
|
customClass: { image: 'img-about' },
|
||||||
html: `
|
html: renderRoomTemplate('tpl-about-modal', {
|
||||||
<br/>
|
html: {
|
||||||
<div id="about">
|
aboutHtml,
|
||||||
${
|
},
|
||||||
brand.about?.html && brand.about.html.trim() !== ''
|
}),
|
||||||
? brand.about.html
|
|
||||||
: `
|
|
||||||
<button
|
|
||||||
id="support-button"
|
|
||||||
data-umami-event="Support button"
|
|
||||||
onclick="window.open('https://codecanyon.net/user/miroslavpejic85', '_blank')">
|
|
||||||
<i class="${className.heart}"></i> Support
|
|
||||||
</button>
|
|
||||||
<br /><br /><br />
|
|
||||||
Author:
|
|
||||||
<a
|
|
||||||
id="linkedin-button"
|
|
||||||
data-umami-event="Linkedin button"
|
|
||||||
href="https://www.linkedin.com/in/miroslav-pejic-976a07101/"
|
|
||||||
target="_blank">
|
|
||||||
Miroslav Pejic
|
|
||||||
</a>
|
|
||||||
<br /><br />
|
|
||||||
Email:
|
|
||||||
<a
|
|
||||||
id="email-button"
|
|
||||||
data-umami-event="Email button"
|
|
||||||
href="mailto:miroslav.pejic.85@gmail.com?subject=MiroTalk P2P info">
|
|
||||||
miroslav.pejic.85@gmail.com
|
|
||||||
</a>
|
|
||||||
<br /><br />
|
|
||||||
<hr />
|
|
||||||
<span>© 2025 MiroTalk P2P, all rights reserved</span>
|
|
||||||
<hr />
|
|
||||||
`
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
showClass: { popup: 'animate__animated animate__fadeInDown' },
|
showClass: { popup: 'animate__animated animate__fadeInDown' },
|
||||||
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
|
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
|
||||||
});
|
});
|
||||||
@@ -16942,3 +16882,50 @@ function displayElements(elements) {
|
|||||||
function sleep(ms) {
|
function sleep(ms) {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render HTML template with provided data
|
||||||
|
* @param {string} templateId - ID of the <template> element
|
||||||
|
* @param {object} data - Data to populate the template
|
||||||
|
* @param {object} data.text - Key-value pairs for text content (data-template-text)
|
||||||
|
* @param {object} data.html - Key-value pairs for HTML content (data-template-html
|
||||||
|
* @param {object} data.attrs - Key-value pairs for attributes (data-template-attr-*)
|
||||||
|
* @returns {string} Rendered HTML string
|
||||||
|
*/
|
||||||
|
function renderRoomTemplate(templateId, { text = {}, html = {}, attrs = {} } = {}) {
|
||||||
|
const template = document.getElementById(templateId);
|
||||||
|
if (!template || !template.content) return '';
|
||||||
|
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.appendChild(template.content.cloneNode(true));
|
||||||
|
|
||||||
|
wrapper.querySelectorAll('*').forEach((element) => {
|
||||||
|
element.getAttributeNames().forEach((name) => {
|
||||||
|
if (!name.startsWith('data-template-attr-')) return;
|
||||||
|
|
||||||
|
const attrName = name.replace('data-template-attr-', '');
|
||||||
|
const key = element.getAttribute(name);
|
||||||
|
const value = attrs[key];
|
||||||
|
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
element.removeAttribute(attrName);
|
||||||
|
} else {
|
||||||
|
element.setAttribute(attrName, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
element.removeAttribute(name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.querySelectorAll('[data-template-text]').forEach((element) => {
|
||||||
|
const key = element.getAttribute('data-template-text');
|
||||||
|
element.textContent = text[key] ?? '';
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.querySelectorAll('[data-template-html]').forEach((element) => {
|
||||||
|
const key = element.getAttribute('data-template-html');
|
||||||
|
element.innerHTML = html[key] ?? '';
|
||||||
|
});
|
||||||
|
|
||||||
|
return wrapper.innerHTML.trim();
|
||||||
|
}
|
||||||
|
|||||||
@@ -1568,6 +1568,232 @@ access to use this app.
|
|||||||
|
|
||||||
<!-- End Video container -->
|
<!-- End Video container -->
|
||||||
|
|
||||||
|
<!-- Start dynamic room templates -->
|
||||||
|
|
||||||
|
<template id="tpl-caption-message">
|
||||||
|
<div class="msg left-msg">
|
||||||
|
<img class="msg-img" data-template-attr-id="captionAvatarTmpId" />
|
||||||
|
<div class="msg-caption-bubble">
|
||||||
|
<div class="msg-info">
|
||||||
|
<div class="msg-info-name" data-template-text="captionInfoText"></div>
|
||||||
|
</div>
|
||||||
|
<div class="msg-text" data-template-text="captionText"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="tpl-msger-chat-message">
|
||||||
|
<div
|
||||||
|
data-template-attr-id="messageContainerId"
|
||||||
|
data-template-attr-class="messageContainerClass"
|
||||||
|
data-template-attr-data-sender="senderName"
|
||||||
|
data-template-attr-data-chat-type="chatType"
|
||||||
|
data-template-attr-data-chat-peer="chatPeer"
|
||||||
|
data-template-attr-data-msg-id="messageId"
|
||||||
|
>
|
||||||
|
<img class="msg-img" data-template-attr-id="messageAvatarTmpId" />
|
||||||
|
<div data-template-attr-class="messageBubbleClass">
|
||||||
|
<div class="msg-info">
|
||||||
|
<div class="msg-info-name" data-template-text="senderName"></div>
|
||||||
|
<div class="msg-info-time" data-template-text="messageTime"></div>
|
||||||
|
</div>
|
||||||
|
<div class="msg-text">
|
||||||
|
<span data-template-attr-id="messageTextId"></span>
|
||||||
|
<hr />
|
||||||
|
<div class="msg-footer">
|
||||||
|
<div class="msg-actions" data-template-html="messageActions"></div>
|
||||||
|
<div class="message-reactions"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="tpl-ai-typing-indicator">
|
||||||
|
<div data-template-attr-id="typingIndicatorId" class="msg left-msg">
|
||||||
|
<div class="ai-typing-indicator">
|
||||||
|
<div class="typing-dots">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="tpl-chatgpt-participant-entry">
|
||||||
|
<div
|
||||||
|
data-template-attr-id="entryId"
|
||||||
|
class="msger-private-chat-entry"
|
||||||
|
data-template-attr-data-peer-name="entryPeerName"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-template-attr-id="buttonId"
|
||||||
|
class="msger-chat-item"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
data-template-attr-data-value="participantName"
|
||||||
|
data-template-attr-data-peer-id="participantPeerId"
|
||||||
|
data-template-attr-title="participantName"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
data-template-attr-id="avatarId"
|
||||||
|
class="msger-chat-avatar"
|
||||||
|
data-template-attr-src="avatarSrc"
|
||||||
|
data-template-attr-alt="participantName"
|
||||||
|
/>
|
||||||
|
<span class="msger-chat-item-copy">
|
||||||
|
<strong data-template-text="participantName"></strong>
|
||||||
|
<small data-template-text="participantSubtitle"></small>
|
||||||
|
</span>
|
||||||
|
<span data-template-attr-id="badgeId" class="msger-chat-unread-badge hidden">0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="tpl-msger-private-entry">
|
||||||
|
<div
|
||||||
|
data-template-attr-id="entryId"
|
||||||
|
class="msger-private-chat-entry"
|
||||||
|
data-template-attr-data-peer-name="entryPeerName"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-template-attr-id="buttonId"
|
||||||
|
class="msger-chat-item"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
data-template-attr-data-value="participantName"
|
||||||
|
data-template-attr-data-peer-id="participantPeerId"
|
||||||
|
data-template-attr-title="participantName"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
data-template-attr-id="avatarTmpId"
|
||||||
|
class="msger-chat-avatar"
|
||||||
|
data-template-attr-alt="participantName"
|
||||||
|
/>
|
||||||
|
<span class="msger-chat-item-copy">
|
||||||
|
<strong data-template-text="participantName"></strong>
|
||||||
|
<small data-template-text="participantSubtitle"></small>
|
||||||
|
</span>
|
||||||
|
<span data-template-attr-id="badgeId" class="msger-chat-unread-badge hidden">0</span>
|
||||||
|
<div data-template-attr-id="dropdownMenuId" class="dropdown-menu-custom msger-participant-dropdown">
|
||||||
|
<button data-template-attr-id="dropdownToggleId" class="dropdown-toggle" type="button">
|
||||||
|
<i class="fas fa-ellipsis-vertical"></i>
|
||||||
|
</button>
|
||||||
|
<ul
|
||||||
|
data-template-attr-id="dropdownListId"
|
||||||
|
class="dropdown-menu-custom-list app-dropdown-menu msger-participant-dropdown-menu"
|
||||||
|
data-template-html="dropdownOptions"
|
||||||
|
></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="tpl-share-room-modal">
|
||||||
|
<div id="qrRoomContainer">
|
||||||
|
<canvas id="qrRoom"></canvas>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<p class="share-room-modal-highlight">Join from your mobile device</p>
|
||||||
|
<p class="share-room-modal-description">
|
||||||
|
No need for apps, simply capture the QR code with your mobile camera Or Invite someone else to join by
|
||||||
|
sending them the following URL
|
||||||
|
</p>
|
||||||
|
<p class="share-room-modal-highlight" data-template-text="roomURL"></p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="tpl-sticky-note-form">
|
||||||
|
<div class="sticky-note-form">
|
||||||
|
<textarea
|
||||||
|
id="stickyNoteText"
|
||||||
|
class="sticky-note-textarea"
|
||||||
|
rows="4"
|
||||||
|
placeholder="Type your note here..."
|
||||||
|
>
|
||||||
|
Note</textarea
|
||||||
|
>
|
||||||
|
<div class="sticky-note-colors-row">
|
||||||
|
<div class="sticky-note-color-group">
|
||||||
|
<label for="stickyNoteColor" class="sticky-note-color-label">
|
||||||
|
<i class="fas fa-palette"></i> Background
|
||||||
|
</label>
|
||||||
|
<input id="stickyNoteColor" type="color" value="#FFEB3B" class="sticky-note-color-input" />
|
||||||
|
</div>
|
||||||
|
<div class="sticky-note-color-group">
|
||||||
|
<label for="stickyNoteTextColor" class="sticky-note-color-label">
|
||||||
|
<i class="fas fa-font"></i> Text
|
||||||
|
</label>
|
||||||
|
<input id="stickyNoteTextColor" type="color" value="#000000" class="sticky-note-color-input" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="tpl-file-picker-modal">
|
||||||
|
<div class="mirotalk-file-picker">
|
||||||
|
<button type="button" id="mirotalkFileDropzone" class="mirotalk-file-dropzone">
|
||||||
|
<span class="mirotalk-file-dropzone-icon"><i class="fas fa-cloud-upload-alt"></i></span>
|
||||||
|
<span
|
||||||
|
id="mirotalkFileDropzoneTitle"
|
||||||
|
class="mirotalk-file-dropzone-title"
|
||||||
|
data-template-text="emptyStateTitle"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
id="mirotalkFileDropzoneSubtitle"
|
||||||
|
class="mirotalk-file-dropzone-subtitle"
|
||||||
|
data-template-text="emptyStateSubtitle"
|
||||||
|
></span>
|
||||||
|
<span class="mirotalk-file-dropzone-helper" data-template-text="helperText"></span>
|
||||||
|
<span id="mirotalkFileBrowseBtn" class="mirotalk-file-dropzone-cta">Browse files</span>
|
||||||
|
</button>
|
||||||
|
<div id="mirotalkFilePreview" class="mirotalk-file-preview" hidden>
|
||||||
|
<div class="mirotalk-file-preview-icon"><i class="fas fa-file-alt"></i></div>
|
||||||
|
<div class="mirotalk-file-preview-meta">
|
||||||
|
<strong id="mirotalkFileName">No file selected</strong>
|
||||||
|
<span id="mirotalkFileDetails"></span>
|
||||||
|
</div>
|
||||||
|
<button type="button" id="mirotalkFileRemoveBtn" class="mirotalk-file-preview-remove">
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="tpl-kicked-out-modal">
|
||||||
|
<h2 class="kicked-out-modal-alert-title">User <span data-template-text="peerName"></span></h2>
|
||||||
|
will kick out you after <b class="kicked-out-modal-alert-time"></b> milliseconds.
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="tpl-about-modal">
|
||||||
|
<br />
|
||||||
|
<div id="about" data-template-html="aboutHtml"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="tpl-extra-info-grid">
|
||||||
|
<div class="extra-info-grid" data-template-html="rows"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="tpl-theme-card-content">
|
||||||
|
<i data-template-attr-class="iconClass"></i><span data-template-text="label"></span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="tpl-last-recording-info">
|
||||||
|
<br />Last recording info: <span data-template-html="recordingInfo"></span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="tpl-room-busy-message">
|
||||||
|
The room is limited to <span data-template-text="maxUsers"></span> users. <br />
|
||||||
|
Please try again later
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="tpl-username-in-use-message">
|
||||||
|
The Username is already in use. <br />
|
||||||
|
Please try with another one
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- End dynamic room templates -->
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
- JS scripts https://cdn.jsdelivr.net
|
- JS scripts https://cdn.jsdelivr.net
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
require('should');
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const vm = require('vm');
|
||||||
|
const { JSDOM } = require('jsdom');
|
||||||
|
|
||||||
|
function extractNamedFunction(source, functionName) {
|
||||||
|
const signature = `function ${functionName}`;
|
||||||
|
const start = source.indexOf(signature);
|
||||||
|
|
||||||
|
if (start === -1) {
|
||||||
|
throw new Error(`Unable to find ${functionName} in source file`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const paramsStart = source.indexOf('(', start);
|
||||||
|
let paramsDepth = 0;
|
||||||
|
let bodyStart = -1;
|
||||||
|
|
||||||
|
for (let index = paramsStart; index < source.length; index++) {
|
||||||
|
const char = source[index];
|
||||||
|
|
||||||
|
if (char === '(') paramsDepth++;
|
||||||
|
if (char === ')') paramsDepth--;
|
||||||
|
|
||||||
|
if (paramsDepth === 0) {
|
||||||
|
bodyStart = source.indexOf('{', index);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bodyStart === -1) {
|
||||||
|
throw new Error(`Unable to find ${functionName} body in source file`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let depth = 0;
|
||||||
|
|
||||||
|
for (let index = bodyStart; index < source.length; index++) {
|
||||||
|
const char = source[index];
|
||||||
|
|
||||||
|
if (char === '{') depth++;
|
||||||
|
if (char === '}') depth--;
|
||||||
|
|
||||||
|
if (depth === 0) {
|
||||||
|
return source.slice(start, index + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unable to parse ${functionName} from source file`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadNamedFunctionSource(functionName, relativePaths) {
|
||||||
|
for (const relativePath of relativePaths) {
|
||||||
|
const sourcePath = path.join(__dirname, '..', ...relativePath.split('/'));
|
||||||
|
const source = fs.readFileSync(sourcePath, 'utf8');
|
||||||
|
|
||||||
|
try {
|
||||||
|
return extractNamedFunction(source, functionName);
|
||||||
|
} catch (error) {
|
||||||
|
if (!error.message.includes(`Unable to find ${functionName}`)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unable to find ${functionName} in source file`);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('test-RoomTemplates', () => {
|
||||||
|
let renderRoomTemplate;
|
||||||
|
let document;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const dom = new JSDOM('<!doctype html><html><body></body></html>');
|
||||||
|
document = dom.window.document;
|
||||||
|
|
||||||
|
const functionSource = loadNamedFunctionSource('renderRoomTemplate', ['public/js/client.js']);
|
||||||
|
|
||||||
|
renderRoomTemplate = vm.runInNewContext(`(${functionSource})`, { document });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves empty string attributes used by placeholder options', () => {
|
||||||
|
const template = document.createElement('template');
|
||||||
|
template.id = 'breakoutRoomOptionTemplate';
|
||||||
|
template.innerHTML = '<option data-template-attr-value="value" data-template-text="label"></option>';
|
||||||
|
document.body.appendChild(template);
|
||||||
|
|
||||||
|
const rendered = renderRoomTemplate('breakoutRoomOptionTemplate', {
|
||||||
|
text: { label: 'Not assigned' },
|
||||||
|
attrs: { value: '' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const select = document.createElement('select');
|
||||||
|
select.innerHTML = rendered;
|
||||||
|
select.value.should.equal('');
|
||||||
|
select.options.length.should.equal(1);
|
||||||
|
select.options[0].textContent.should.equal('Not assigned');
|
||||||
|
select.options[0].getAttribute('value').should.equal('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('still renders non-empty attributes normally', () => {
|
||||||
|
const template = document.createElement('template');
|
||||||
|
template.id = 'participantTemplate';
|
||||||
|
template.innerHTML = '<div data-template-attr-data-peer-id="peerId" data-template-text="peerName"></div>';
|
||||||
|
document.body.appendChild(template);
|
||||||
|
|
||||||
|
const rendered = renderRoomTemplate('participantTemplate', {
|
||||||
|
text: { peerName: 'Alice' },
|
||||||
|
attrs: { peerId: 'peer-1' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.innerHTML = rendered;
|
||||||
|
|
||||||
|
wrapper.firstElementChild.textContent.should.equal('Alice');
|
||||||
|
wrapper.firstElementChild.getAttribute('data-peer-id').should.equal('peer-1');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user