[mirotalk] - t(chat): add emoji reactions to chat messages

This commit is contained in:
Miroslav Pejic
2026-04-22 20:46:19 +02:00
parent 13cbe0562d
commit 627e9299c1
8 changed files with 494 additions and 32 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# ====================================================
# MiroTalk P2P v.1.8.07 - Environment Configuration
# MiroTalk P2P v.1.8.08 - Environment Configuration
# ====================================================
# App environment
+1 -1
View File
@@ -2,7 +2,7 @@
/**
* ==============================================
* MiroTalk P2P v.1.8.07 - Configuration File
* MiroTalk P2P v.1.8.08 - Configuration File
* ==============================================
*
* This file is the central configuration source.
+1 -1
View File
@@ -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 CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
* @version 1.8.07
* @version 1.8.08
*
*/
+10 -10
View File
@@ -1,24 +1,24 @@
{
"name": "mirotalk",
"version": "1.8.07",
"version": "1.8.08",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mirotalk",
"version": "1.8.07",
"version": "1.8.08",
"license": "AGPL-3.0",
"dependencies": {
"@mattermost/client": "11.6.0",
"@ngrok/ngrok": "1.7.0",
"@sentry/node": "^10.49.0",
"axios": "^1.15.1",
"axios": "^1.15.2",
"chokidar": "^5.0.0",
"colors": "^1.4.0",
"compression": "^1.8.1",
"cors": "^2.8.6",
"crypto-js": "^4.2.0",
"dompurify": "^3.4.0",
"dompurify": "^3.4.1",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"express-openid-connect": "^2.20.2",
@@ -1725,9 +1725,9 @@
}
},
"node_modules/axios": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.1.tgz",
"integrity": "sha512-WOG+Jj8ZOvR0a3rAn+Tuf1UQJRxw5venr6DgdbJzngJE3qG7X0kL83CZGpdHMxEm+ZK3seAbvFsw4FfOfP9vxg==",
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz",
"integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
@@ -2508,9 +2508,9 @@
}
},
"node_modules/dompurify": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.0.tgz",
"integrity": "sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==",
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.1.tgz",
"integrity": "sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "mirotalk",
"version": "1.8.07",
"version": "1.8.08",
"description": "A free WebRTC browser-based video call",
"main": "server.js",
"scripts": {
@@ -47,13 +47,13 @@
"@mattermost/client": "11.6.0",
"@ngrok/ngrok": "1.7.0",
"@sentry/node": "^10.49.0",
"axios": "^1.15.1",
"axios": "^1.15.2",
"chokidar": "^5.0.0",
"colors": "^1.4.0",
"compression": "^1.8.1",
"cors": "^2.8.6",
"crypto-js": "^4.2.0",
"dompurify": "^3.4.0",
"dompurify": "^3.4.1",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"express-openid-connect": "^2.20.2",
+202 -3
View File
@@ -1604,11 +1604,12 @@ body {
.private-msg-bubble,
.msg-bubble {
position: relative;
width: auto;
max-width: min(78%, 720px);
padding: 12px 14px;
border-radius: 20px;
overflow: hidden;
overflow: visible;
}
.msg-caption-bubble {
@@ -1648,18 +1649,216 @@ body {
border-top: 1px solid rgba(255, 255, 255, 0.12);
}
.msg-text button {
.msg-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
}
.msg-footer {
display: flex;
flex-direction: column;
margin-top: 6px;
}
.msg-actions button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
padding: 0;
margin-right: 6px;
border-radius: 10px;
background: var(--body-bg) !important;
}
.reaction-picker {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
padding: 6px;
border-radius: 12px;
border: var(--border);
background: rgba(18, 18, 20, 0.95);
box-shadow: 0 14px 24px rgba(0, 0, 0, 0.22);
}
.reaction-emoji-btn {
width: 32px;
height: 32px;
border: none;
border-radius: 10px;
padding: 0;
font-size: 18px;
line-height: 1;
background: rgba(255, 255, 255, 0.12);
cursor: pointer;
}
.reaction-emoji-btn:hover {
transform: translateY(-1px);
background: rgba(255, 255, 255, 0.22);
}
.reaction-emoji-btn:focus {
outline: 2px solid rgba(102, 170, 255, 0.6);
outline-offset: 2px;
}
.reaction-toggle-btn {
font-size: 16px;
}
.message-reactions {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 6px;
}
.reaction-badge {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 32px;
height: 24px;
padding: 1px 5px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.2);
font-size: 11px;
line-height: 1.2;
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
}
.reaction-badge:hover {
background: rgba(255, 255, 255, 0.18);
border-color: rgba(255, 255, 255, 0.32);
transform: translateY(-1px);
}
.reaction-badge:active {
transform: translateY(1px) scale(0.97);
}
.reaction-badge.my-reaction {
background: rgba(53, 125, 255, 0.3);
border-color: rgba(102, 170, 255, 0.8);
font-weight: 500;
}
.reaction-badge.my-reaction:hover {
background: rgba(53, 125, 255, 0.4);
border-color: rgba(102, 170, 255, 1);
}
.reaction-badge::before {
content: '';
position: absolute;
inset: -1px;
border-radius: 999px;
border: 1px solid rgba(102, 170, 255, 0.5);
opacity: 0;
transition: opacity 0.2s ease;
}
.reaction-badge.my-reaction::before {
opacity: 1;
}
.reaction-badge:hover::after {
content: attr(data-peers);
position: absolute;
left: 50%;
bottom: calc(100% + 8px);
transform: translateX(-50%);
max-width: 280px;
width: max-content;
padding: 6px 10px;
border-radius: 8px;
font-size: 12px;
font-weight: 500;
line-height: 1.4;
color: #fff;
background: rgba(0, 0, 0, 0.95);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
pointer-events: none;
z-index: 1000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
}
/* Mobile optimizations for emoji reactions */
@media (max-width: 768px) {
.reaction-emoji-btn {
width: 44px;
height: 44px;
font-size: 22px;
}
.msg-actions button {
width: 40px;
height: 40px;
}
.reaction-picker {
gap: 10px;
padding: 10px;
}
.reaction-badge {
min-width: 30px;
height: 22px;
padding: 1px 4px;
font-size: 11px;
}
}
@media (max-width: 768px) and (hover: none) {
.reaction-emoji-btn:hover {
transform: none;
background: rgba(255, 255, 255, 0.12);
}
.reaction-emoji-btn:active {
transform: scale(1.15) translateY(-3px);
background: rgba(255, 255, 255, 0.25);
}
.reaction-badge:hover {
transform: none;
}
.reaction-badge:active {
transform: scale(0.95);
}
}
@media (max-width: 600px) {
.reaction-emoji-btn {
width: 48px;
height: 48px;
}
.msg-actions {
gap: 8px;
}
.reaction-badge {
min-width: 28px;
height: 20px;
padding: 0px 3px;
font-size: 11px;
}
}
.msg-text iframe {
width: 100%;
}
+1 -1
View File
@@ -109,7 +109,7 @@ let brand = {
},
about: {
imageUrl: '../images/mirotalk-logo.gif',
title: 'WebRTC P2P v1.8.07',
title: 'WebRTC P2P v1.8.08',
html: `
<button
id="support-button"
+275 -12
View File
@@ -15,7 +15,7 @@
* @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
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
* @version 1.8.07
* @version 1.8.08
*
*/
@@ -666,6 +666,7 @@ let isSpeechSynthesisSupported = 'speechSynthesis' in window;
let transcripts = []; // collect all the transcripts to save it later if you need
let chatMessages = []; // collect chat messages to save it later if want
let chatGPTcontext = []; // keep chatGPT messages context
const CHAT_REACTION_EMOJIS = ['👍', '❤️', '😂', '😮', '😢', '🔥'];
const CHAT_GPT_PEER_ID = 'chatgpt';
const CHAT_GPT_NAME = 'ChatGPT';
let activeConversation = {
@@ -2908,6 +2909,9 @@ async function handleRTCDataChannels(peer_id) {
case 'chat':
handleDataChannelChat(dataMessage);
break;
case 'chatReaction':
handleDataChannelChatReaction(dataMessage);
break;
case 'speech':
handleDataChannelSpeechTranscript(dataMessage);
break;
@@ -10224,15 +10228,17 @@ async function sendChatMessage() {
return cleanMessageInput();
}
const msgId = createChatMessageId();
if (activeConversation.type === 'private' && activeConversation.peerId === CHAT_GPT_PEER_ID) {
appendMessage(myPeerName, rightChatAvatar, 'right', msg, true, null, CHAT_GPT_NAME);
appendMessage(myPeerName, rightChatAvatar, 'right', msg, true, msgId, CHAT_GPT_NAME);
await getChatGPTmessage(msg);
} else if (activeConversation.type === 'private' && activeConversation.peerName) {
emitMsg(myPeerName, myPeerAvatar, activeConversation.peerName, msg, true, myPeerId);
appendMessage(myPeerName, rightChatAvatar, 'right', msg, true, null, activeConversation.peerName);
emitMsg(myPeerName, myPeerAvatar, activeConversation.peerName, msg, true, myPeerId, msgId);
appendMessage(myPeerName, rightChatAvatar, 'right', msg, true, msgId, activeConversation.peerName);
} else {
emitMsg(myPeerName, myPeerAvatar, 'toAll', msg, false, myPeerId);
appendMessage(myPeerName, rightChatAvatar, 'right', msg, false);
emitMsg(myPeerName, myPeerAvatar, 'toAll', msg, false, myPeerId, msgId);
appendMessage(myPeerName, rightChatAvatar, 'right', msg, false, msgId);
}
cleanMessageInput();
}
@@ -10251,7 +10257,7 @@ function handleDataChannelChat(dataMessage) {
const msgTo = filterXSS(dataMessage.to);
const msg = filterXSS(dataMessage.msg);
const msgPrivate = filterXSS(dataMessage.privateMsg);
const msgId = filterXSS(dataMessage.id);
const msgId = filterXSS(dataMessage.msg_id || '');
// We check if the message is from real peer
const from_peer_name = allPeers[msgFromId]['peer_name'];
@@ -10305,6 +10311,218 @@ function cleanMessageInput() {
checkLineBreaks();
}
/**
* Create a unique message id used by chat reactions.
* @returns {string}
*/
function createChatMessageId() {
return `m-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
}
/**
* Ensure a safe, non-empty message id.
* @param {string|null} msgId
* @returns {string}
*/
function normalizeChatMessageId(msgId) {
const raw = String(msgId || '').trim();
const safe = raw.replace(/[^a-zA-Z0-9:_-]/g, '');
return safe || createChatMessageId();
}
/**
* Find rendered chat message element by message id.
* @param {string} msgId
* @returns {HTMLElement|null}
*/
function getChatMessageElement(msgId) {
const normalizedId = normalizeChatMessageId(msgId);
return msgerChat?.querySelector(`.msg[data-msg-id="${normalizedId}"]`) || null;
}
/**
* Toggle emoji reaction picker for a message bubble.
* @param {string} msgId
* @param {HTMLElement|null} triggerElement
*/
function toggleReactionPicker(msgId, triggerElement = null) {
const messageElement = getChatMessageElement(msgId);
if (!messageElement) return;
const footer = messageElement.querySelector('.msg-footer');
if (!footer) return;
const existingPicker = footer.querySelector('.reaction-picker');
if (existingPicker) {
existingPicker.remove();
triggerElement?.blur();
return;
}
// Close any other open pickers
msgerChat.querySelectorAll('.reaction-picker').forEach((picker) => picker.remove());
const picker = document.createElement('div');
picker.className = 'reaction-picker';
picker.setAttribute('role', 'group');
picker.setAttribute('aria-label', 'Reaction emoji picker');
const buttons = [];
CHAT_REACTION_EMOJIS.forEach((emoji, index) => {
const button = document.createElement('button');
button.type = 'button';
button.className = 'reaction-emoji-btn';
button.textContent = emoji;
button.setAttribute('data-emoji', emoji);
button.setAttribute('aria-label', `React with ${emoji}`);
button.onclick = (e) => sendChatReaction(msgId, emoji, e);
button.onkeydown = (e) => {
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
e.preventDefault();
buttons[(index + 1) % buttons.length]?.focus();
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
e.preventDefault();
buttons[(index - 1 + buttons.length) % buttons.length]?.focus();
} else if (e.key === 'Escape') {
e.preventDefault();
picker.remove();
triggerElement?.focus();
}
};
buttons.push(button);
picker.appendChild(button);
});
footer.appendChild(picker);
// Focus first button
setTimeout(() => picker.querySelector('.reaction-emoji-btn')?.focus(), 0);
}
/**
* Send reaction to peers and update local bubble.
* @param {string} msgId
* @param {string} emoji
* @param {Event} event
*/
function sendChatReaction(msgId, emoji, event) {
if (event) {
event.preventDefault();
event.stopPropagation();
}
const normalizedMsgId = normalizeChatMessageId(msgId);
if (!normalizedMsgId || !emoji) return;
const messageElement = getChatMessageElement(normalizedMsgId);
if (!messageElement) return;
const currentBadge = messageElement.querySelector(`.reaction-badge[data-emoji="${emoji}"]`);
const currentPeers = currentBadge?.dataset.peers
? currentBadge.dataset.peers
.split(',')
.map((peer) => peer.trim())
.filter(Boolean)
: [];
const action = currentPeers.includes(myPeerName) ? 'remove' : 'add';
applyReactionToElement(messageElement, emoji, myPeerName, action);
sendToDataChannel({
type: 'chatReaction',
msg_id: normalizedMsgId,
emoji: emoji,
peer_name: myPeerName,
fromId: myPeerId,
action: action,
});
msgerChat.querySelectorAll('.reaction-picker').forEach((picker) => picker.remove());
}
/**
* Handle incoming data channel chat reactions.
* @param {object} dataMessage
*/
function handleDataChannelChatReaction(dataMessage) {
if (!dataMessage) return;
const fromId = filterXSS(dataMessage.fromId || '');
const peerName = filterXSS(dataMessage.peer_name || '');
if (fromId && allPeers[fromId]?.peer_name && allPeers[fromId].peer_name !== peerName) {
console.log('Fake reaction detected', {
realFrom: allPeers[fromId].peer_name,
fakeFrom: peerName,
msgId: dataMessage.msg_id,
emoji: dataMessage.emoji,
});
return;
}
handleChatReaction(dataMessage);
}
/**
* Apply reaction updates to a message element with smart badge display.
* @param {HTMLElement} messageElement
* @param {string} emoji
* @param {string} peerName
* @param {string} action
*/
function applyReactionToElement(messageElement, emoji, peerName, action = 'add') {
if (!messageElement || !emoji || !peerName) return;
const footer = messageElement.querySelector('.msg-footer');
if (!footer) return;
let reactionsContainer = footer.querySelector('.message-reactions');
if (!reactionsContainer && action === 'add') {
reactionsContainer = document.createElement('div');
reactionsContainer.className = 'message-reactions';
footer.appendChild(reactionsContainer);
}
if (!reactionsContainer) return;
let badge = reactionsContainer.querySelector(`.reaction-badge[data-emoji="${emoji}"]`);
let peers = badge?.dataset.peers
? badge.dataset.peers
.split(',')
.map((peer) => peer.trim())
.filter(Boolean)
: [];
if (action === 'remove') {
peers = peers.filter((peer) => peer !== peerName);
} else if (!peers.includes(peerName)) {
peers.push(peerName);
}
if (peers.length === 0) {
if (badge) badge.remove();
if (!reactionsContainer.querySelector('.reaction-badge')) reactionsContainer.remove();
return;
}
if (!badge) {
badge = document.createElement('span');
badge.className = 'reaction-badge';
badge.dataset.emoji = emoji;
const msgId = messageElement.dataset.msgId || '';
badge.dataset.msgId = msgId;
badge.onclick = (event) => sendChatReaction(msgId, emoji, event);
reactionsContainer.appendChild(badge);
}
badge.dataset.peers = peers.join(', ');
badge.textContent = `${emoji} ${peers.length}`;
badge.classList.toggle('my-reaction', peers.includes(myPeerName));
}
/**
* Paste from clipboard to input txt message
*/
@@ -10413,6 +10631,7 @@ function appendMessage(from, img, side, msg, privateMsg, msgId = null, to = '')
const getImg = getFrom === CHAT_GPT_NAME && getSide === 'left' ? images.chatgpt : filterXSS(img);
const getMsg = filterXSS(msg);
const getPrivateMsg = filterXSS(privateMsg);
const normalizedMsgId = normalizeChatMessageId(msgId);
// collect chat messages to save it later
const conversationPeer = getPrivateMsg ? (getSide === 'left' ? getFrom : getTo) : '';
@@ -10431,7 +10650,7 @@ function appendMessage(from, img, side, msg, privateMsg, msgId = null, to = '')
let msgHTML = `
<div id="msg-${chatMessagesId}" class="msg ${getSide}-msg" data-sender="${getFrom}" data-chat-type="${
getPrivateMsg ? 'private' : 'public'
}" data-chat-peer="${conversationPeer}">
}" data-chat-peer="${conversationPeer}" data-msg-id="${normalizedMsgId}">
<img class="msg-img" src="${getImg}" />
<div class=${msgBubble}>
<div class="msg-info">
@@ -10441,6 +10660,8 @@ function appendMessage(from, img, side, msg, privateMsg, msgId = null, to = '')
<div class="msg-text">
<span id="message-${chatMessagesId}"></span>
<hr/>
<div class="msg-footer">
<div class="msg-actions">
`;
msgHTML += `
<button
@@ -10455,6 +10676,13 @@ function appendMessage(from, img, side, msg, privateMsg, msgId = null, to = '')
style="color:#fff; border:none; background:transparent;"
onclick="copyToClipboard('message-${chatMessagesId}')"
></button>`;
msgHTML += `
<button
id="msg-reaction-${chatMessagesId}"
class="reaction-toggle-btn"
style="color:#fff; border:none; background:transparent;"
onclick="toggleReactionPicker('${normalizedMsgId}', this)"
>😊</button>`;
if (isSpeechSynthesisSupported) {
msgHTML += `
<button
@@ -10465,6 +10693,9 @@ function appendMessage(from, img, side, msg, privateMsg, msgId = null, to = '')
></button>`;
}
msgHTML += `
</div>
<div class="message-reactions"></div>
</div>
</div>
</div>
</div>
@@ -10490,6 +10721,7 @@ function appendMessage(from, img, side, msg, privateMsg, msgId = null, to = '')
if (!isMobileDevice) {
setTippy(getId('msg-delete-' + chatMessagesId), 'Delete', 'top');
setTippy(getId('msg-copy-' + chatMessagesId), 'Copy', 'top');
setTippy(getId('msg-reaction-' + chatMessagesId), 'React', 'top');
setTippy(getId('msg-speech-' + chatMessagesId), 'Speech', 'top');
}
chatMessagesId++;
@@ -11267,8 +11499,9 @@ function addMsgerPrivateBtn(
}
const toPeerName = msgerPrivateBtn.dataset.value;
emitMsg(myPeerName, myPeerAvatar, toPeerName, pMsg, true, myPeerId);
appendMessage(myPeerName, rightChatAvatar, 'right', pMsg, true, null, toPeerName);
const msgId = createChatMessageId();
emitMsg(myPeerName, myPeerAvatar, toPeerName, pMsg, true, myPeerId, msgId);
appendMessage(myPeerName, rightChatAvatar, 'right', pMsg, true, msgId, toPeerName);
msgerPrivateMsgInput.value = '';
if (!shouldDockParticipantsPanel()) {
syncParticipantsPanelVisibility(false);
@@ -11532,7 +11765,7 @@ function getFormatDate(date) {
* @param {boolean} privateMsg if is a private message
* @param {string} id peer_id
*/
function emitMsg(from, fromAvatar, to, msg, privateMsg, id) {
function emitMsg(from, fromAvatar, to, msg, privateMsg, id, msgId = '') {
if (!msg) return;
// sanitize all params
@@ -11543,6 +11776,7 @@ function emitMsg(from, fromAvatar, to, msg, privateMsg, id) {
const getMsg = filterXSS(msg);
const getPrivateMsg = filterXSS(privateMsg);
const getId = filterXSS(id);
const getMsgId = normalizeChatMessageId(filterXSS(msgId));
const chatMessage = {
type: 'chat',
@@ -11550,6 +11784,7 @@ function emitMsg(from, fromAvatar, to, msg, privateMsg, id) {
fromAvatar: getFromAvatar,
fromId: getFromId,
id: getId,
msg_id: getMsgId,
to: getTo,
msg: getMsg,
privateMsg: getPrivateMsg,
@@ -12352,6 +12587,34 @@ function handleMessage(message) {
}
}
/**
* Handle incoming chat reactions.
* @param {object} data
*/
function handleChatReaction(data) {
if (!data) return;
const rawMsgId = String(data.msg_id || '').trim();
const msgId = rawMsgId.replace(/[^a-zA-Z0-9:_-]/g, '');
const emoji = filterXSS(data.emoji || '');
const peerName = filterXSS(data.peer_name || '');
const action = data.action === 'remove' ? 'remove' : 'add';
if (!msgId || !emoji || !peerName) return;
if (!CHAT_REACTION_EMOJIS.includes(emoji)) return;
const messageElement = getChatMessageElement(msgId);
if (!messageElement) return;
applyReactionToElement(messageElement, emoji, peerName, action);
}
document.addEventListener('click', (event) => {
const target = event.target;
if (target?.closest('.reaction-picker') || target?.closest('.reaction-toggle-btn')) return;
msgerChat?.querySelectorAll('.reaction-picker').forEach((picker) => picker.remove());
});
/**
* Handle room emoji reaction
* @param {object} message
@@ -15002,7 +15265,7 @@ function showAbout() {
Swal.fire({
background: swBg,
position: 'center',
title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.8.07',
title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.8.08',
imageUrl: brand.about?.imageUrl && brand.about.imageUrl.trim() !== '' ? brand.about.imageUrl : images.about,
customClass: { image: 'img-about' },
html: `