[mirotalk] - t(chat): add emoji reactions to chat messages
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
||||
# ====================================================
|
||||
# MiroTalk P2P v.1.8.07 - Environment Configuration
|
||||
# MiroTalk P2P v.1.8.08 - Environment Configuration
|
||||
# ====================================================
|
||||
|
||||
# App environment
|
||||
|
||||
@@ -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
@@ -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
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
Generated
+10
-10
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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: `
|
||||
|
||||
Reference in New Issue
Block a user