diff --git a/Templates/GlobalChat/widget.tpl b/Templates/GlobalChat/widget.tpl index 0eebc1fd..2338a984 100644 --- a/Templates/GlobalChat/widget.tpl +++ b/Templates/GlobalChat/widget.tpl @@ -14,6 +14,9 @@ ## - MH si Admin evidentiati fata de restul jucatorilor ## ## - format afisare: [TAG] Nume, sau doar Nume daca nu are alianta ## ## ## +## Faza 2 (09.09.2026): stergere mesaj (mod), editare mesaj propriu (user), ## +## sondaje, emoji, si fix la badge-ul de mesaje necitite (vezi loadInitial). ## +## ## ## Nu depinde de MooTools/jQuery (evita coliziuni cu $ / $$ din unx.js / ## ## mt-full.js) - JS vanilla, prefixat "gchat_" peste tot. ## ## ## @@ -36,7 +39,20 @@ + + + +
+ +
@@ -147,8 +163,11 @@ .gchat_badge_role.admin { background: #b8290a; } .gchat_badge_role.mh { background: #1a5aa8; } - .gchat_mod_actions { margin-left: 4px; } - .gchat_mod_actions a { + /* Faza 2: actiuni (edit propriu + moderare) - o singura clasa flat, + ca sa nu se dubleze margin-left cand cele doua se combina pe acelasi rand. + Inlocuieste .gchat_mod_actions din Faza 1 (nu mai e generata de JS). */ + .gchat_actions { margin-left: 4px; } + .gchat_actions a { font-size: 10px; color: #888; text-decoration: none; @@ -156,7 +175,59 @@ margin-right: 4px; cursor: pointer; } - .gchat_mod_actions a:hover { color: #b8290a; } + .gchat_actions a:hover { color: #b8290a; } + + .gchat_text_deleted { color: #999; font-style: italic; } + .gchat_edited_tag { color: #999; font-size: 10px; } + + .gchat_edit_bar { display: flex; gap: 4px; margin-top: 3px; } + .gchat_edit_bar input { + flex: 1; + font-size: 11px; + padding: 2px 4px; + border: 1px solid #ccc; + border-radius: 3px; + } + .gchat_edit_bar a { + font-size: 10px; + color: #6b8f47; + cursor: pointer; + align-self: center; + text-decoration: none; + } + + /* sondaje */ + .gchat_poll_block { + margin-top: 3px; + padding: 5px 6px; + background: #f2f0e6; + border: 1px solid #ddd6bd; + border-radius: 4px; + } + .gchat_poll_question { font-weight: bold; margin-bottom: 4px; } + .gchat_poll_option { + position: relative; + display: block; + width: 100%; + text-align: left; + margin-bottom: 3px; + padding: 3px 6px; + border: 1px solid #ccc; + border-radius: 3px; + background: #fff; + cursor: pointer; + overflow: hidden; + font-size: 11px; + } + .gchat_poll_bar { + position: absolute; + left: 0; top: 0; bottom: 0; + background: #cfe0bb; + z-index: 0; + } + .gchat_poll_option_label { position: relative; z-index: 1; } + .gchat_poll_option_mine { border-color: #6b8f47; } + .gchat_poll_total { font-size: 10px; color: #888; margin-top: 2px; } #gchat_notice { padding: 4px 8px; @@ -190,6 +261,80 @@ #gchat_input:disabled { background: #eee; color: #999; } + .gchat_toolbar_btn { + border: none; + background: none; + font-size: 16px; + line-height: 1; + cursor: pointer; + padding: 0 4px; + flex-shrink: 0; + } + + #gchat_emoji_panel { + display: none; + grid-template-columns: repeat(8, 1fr); + gap: 2px; + padding: 6px; + max-height: 110px; + overflow-y: auto; + border-top: 1px solid #ddd; + flex-shrink: 0; + } + .gchat_emoji_item { + border: none; + background: none; + font-size: 16px; + cursor: pointer; + padding: 2px; + border-radius: 3px; + } + .gchat_emoji_item:hover { background: #eee; } + + #gchat_poll_form { + display: none; + padding: 6px 8px; + border-top: 1px solid #ddd; + flex-shrink: 0; + background: #f7f5ef; + } + #gchat_poll_form input[type="text"] { + width: 100%; + box-sizing: border-box; + margin-bottom: 4px; + padding: 4px 6px; + font-size: 12px; + border: 1px solid #ccc; + border-radius: 3px; + } + .gchat_poll_option_row { + display: flex; + align-items: center; + gap: 4px; + margin-bottom: 4px; + } + .gchat_poll_option_row input { flex: 1; } + .gchat_poll_option_remove { + border: none; + background: #ddd; + border-radius: 3px; + width: 20px; + height: 20px; + cursor: pointer; + flex-shrink: 0; + } + .gchat_poll_form_actions { display: flex; justify-content: space-between; gap: 6px; } + .gchat_poll_form_actions button { + flex: 1; + padding: 4px; + font-size: 11px; + border: none; + border-radius: 3px; + cursor: pointer; + } + #gchat_poll_add_option { background: #ddd; } + #gchat_poll_submit { background: #6b8f47; color: #fff; } + @media (max-width: 480px) { #gchat_panel { right: 8px; bottom: 66px; } #gchat_bubble { right: 8px; bottom: 8px; } @@ -212,7 +357,17 @@ block: , unmute: , adminBadge: , - mhBadge: + mhBadge: , + edit: , + save: , + cancel: , + editedTag: , + deleteMsg: , + confirmDelete: , + deletedPlaceholder: , + pollOptionPlaceholder: , + pollVotesWord: , + errorGeneric: }; var ACCESS_ADMIN = 9, ACCESS_MH = 8; @@ -229,6 +384,12 @@ var notice = document.getElementById('gchat_notice'); var form = document.getElementById('gchat_form'); var input = document.getElementById('gchat_input'); + var emojiBtn = document.getElementById('gchat_emoji_btn'); + var emojiPanel = document.getElementById('gchat_emoji_panel'); + var pollBtn = document.getElementById('gchat_poll_btn'); + var pollForm = document.getElementById('gchat_poll_form'); + var pollQuestionInput = document.getElementById('gchat_poll_question'); + var pollOptionsBox = document.getElementById('gchat_poll_options'); var myUid = parseInt(root.getAttribute('data-uid'), 10) || 0; var lastId = 0; @@ -237,6 +398,14 @@ var viewerIsMod = false; var viewerAccess = 0; var pollTimer = null; + // Faza 2: watermark de timp pentru editari/stergeri/voturi pe mesaje deja + // afisate (separat de 'lastId', care e watermark pe mesaje NOI) - initializat + // la "acum", nu la 0, ca sa nu tragem tot istoricul de mutatii de la + // pornirea serverului (irelevant - contam doar ce s-a schimbat de cand + // pagina curenta a fost incarcata) + var lastMutationTs = Math.floor(Date.now() / 1000); + var MAX_POLL_OPTIONS = 6; + var EMOJI_LIST = ['😀','😂','😅','😊','😍','😎','🤔','😴','😭','😡','👍','👎','👏','🙏','💪','🔥','⭐','❤️','💯','🎉','⚔️','🛡️','🏰','🌾','🪵','⛏️','🧱','⏳','🐎','🏆']; function fmtTime(unixTs) { var d = new Date(unixTs * 1000); @@ -251,41 +420,177 @@ function escapeForLog(s) { return s; } // (folosim textContent peste tot mai jos - nu innerHTML pe input de utilizator) - function renderModActions(row) { + function appendModActionLinks(parentSpan, row) { + if (!viewerIsMod) { return; } + var targetAccess = parseInt(row.access, 10) || 0; var targetUid = parseInt(row.id_user, 10) || 0; - if (!viewerIsMod || targetUid === myUid || targetAccess >= viewerAccess) { - return null; - } - - var span = document.createElement('span'); - span.className = 'gchat_mod_actions'; - function actionLink(label, handler) { var a = document.createElement('a'); a.textContent = label; a.addEventListener('click', handler); - span.appendChild(a); + parentSpan.appendChild(a); } - actionLink(GCHAT_TXT.mute5, function () { doModerate('gchat_mute', row.id_user, 5); }); - actionLink(GCHAT_TXT.mute30, function () { doModerate('gchat_mute', row.id_user, 30); }); - actionLink(GCHAT_TXT.mute60, function () { doModerate('gchat_mute', row.id_user, 60); }); - actionLink(GCHAT_TXT.mute1440, function () { doModerate('gchat_mute', row.id_user, 1440); }); - actionLink(GCHAT_TXT.block, function () { - if (window.confirm(GCHAT_TXT.confirmBlock)) { - doModerate('gchat_block', row.id_user, 0); + // mute/block: doar pe alt user, cu rang strict mai mic (neschimbat) + if (targetUid !== myUid && targetAccess < viewerAccess) { + actionLink(GCHAT_TXT.mute5, function () { doModerate('gchat_mute', row.id_user, 5); }); + actionLink(GCHAT_TXT.mute30, function () { doModerate('gchat_mute', row.id_user, 30); }); + actionLink(GCHAT_TXT.mute60, function () { doModerate('gchat_mute', row.id_user, 60); }); + actionLink(GCHAT_TXT.mute1440, function () { doModerate('gchat_mute', row.id_user, 1440); }); + actionLink(GCHAT_TXT.block, function () { + if (window.confirm(GCHAT_TXT.confirmBlock)) { + doModerate('gchat_block', row.id_user, 0); + } + }); + actionLink(GCHAT_TXT.unmute, function () { doModerate('gchat_unmute', row.id_user, 0); }); + } + + // Faza 2: stergere mesaj - NU e limitata de rang (vezi comentariul din + // Database::deleteGlobalChatMessage) - orice MH/Admin poate sterge orice + // mesaj, inclusiv al altui MH sau al lui insusi, ca sa poata curata + // rapid continut vulgar + actionLink(GCHAT_TXT.deleteMsg, function () { + if (window.confirm(GCHAT_TXT.confirmDelete)) { + deleteMessage(row.id); } }); - actionLink(GCHAT_TXT.unmute, function () { doModerate('gchat_unmute', row.id_user, 0); }); + } - return span; + function deleteMessage(id) { + fetch(AJAX_URL + '?f=gchat_delete', { + method: 'POST', + credentials: 'same-origin', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: 'id=' + encodeURIComponent(id) + }).then(function (r) { return r.json(); }) + .then(function (data) { + if (data && data.ok) { + fetchUpdatesNow(); + } else { + showNotice(GCHAT_TXT.errorGeneric); + } + }); + } + + function startEdit(line, row, textSpan) { + var actions = line.querySelector('.gchat_actions'); + var original = textSpan.textContent; + + var editInput = document.createElement('input'); + editInput.type = 'text'; + editInput.className = 'gchat_edit_input'; + editInput.maxLength = 250; + editInput.value = original; + + var saveBtn = document.createElement('a'); + saveBtn.textContent = GCHAT_TXT.save; + var cancelBtn = document.createElement('a'); + cancelBtn.textContent = GCHAT_TXT.cancel; + + var editBar = document.createElement('div'); + editBar.className = 'gchat_edit_bar'; + editBar.appendChild(editInput); + editBar.appendChild(saveBtn); + editBar.appendChild(cancelBtn); + + textSpan.style.display = 'none'; + if (actions) { actions.style.display = 'none'; } + line.appendChild(editBar); + editInput.focus(); + + function cleanup() { + editBar.remove(); + textSpan.style.display = ''; + if (actions) { actions.style.display = ''; } + } + + cancelBtn.addEventListener('click', cleanup); + + saveBtn.addEventListener('click', function () { + var newMsg = editInput.value.trim(); + if (!newMsg) { return; } + + fetch(AJAX_URL + '?f=gchat_edit', { + method: 'POST', + credentials: 'same-origin', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: 'id=' + encodeURIComponent(row.id) + '&msg=' + encodeURIComponent(newMsg) + }).then(function (r) { return r.json(); }) + .then(function (data) { + cleanup(); + if (data && data.ok) { + fetchUpdatesNow(); + } else { + showNotice(GCHAT_TXT.errorGeneric); + } + }); + }); + } + + function renderPollBlock(pollData, msgId) { + var wrap = document.createElement('div'); + wrap.className = 'gchat_poll_block'; + if (!pollData) { return wrap; } + + var q = document.createElement('div'); + q.className = 'gchat_poll_question'; + q.textContent = pollData.question; + wrap.appendChild(q); + + var total = pollData.total || 0; + (pollData.options || []).forEach(function (optText, idx) { + var count = (pollData.counts && pollData.counts[idx]) || 0; + var pct = total > 0 ? Math.round((count / total) * 100) : 0; + var isMine = pollData.myVote !== null && pollData.myVote !== undefined + && parseInt(pollData.myVote, 10) === idx; + + var optBtn = document.createElement('button'); + optBtn.type = 'button'; + optBtn.className = 'gchat_poll_option' + (isMine ? ' gchat_poll_option_mine' : ''); + + var bar = document.createElement('span'); + bar.className = 'gchat_poll_bar'; + bar.style.width = pct + '%'; + optBtn.appendChild(bar); + + var label = document.createElement('span'); + label.className = 'gchat_poll_option_label'; + label.textContent = optText + ' \u2014 ' + pct + '% (' + count + ')'; + optBtn.appendChild(label); + + optBtn.addEventListener('click', function () { + fetch(AJAX_URL + '?f=gchat_poll_vote', { + method: 'POST', + credentials: 'same-origin', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: 'pollId=' + encodeURIComponent(pollData.id) + '&option=' + encodeURIComponent(idx) + }).then(function (r) { return r.json(); }) + .then(function (data) { + if (data && data.ok) { + fetchUpdatesNow(); + } else { + showNotice(GCHAT_TXT.errorGeneric); + } + }); + }); + + wrap.appendChild(optBtn); + }); + + var totalLine = document.createElement('div'); + totalLine.className = 'gchat_poll_total'; + totalLine.textContent = total + ' ' + GCHAT_TXT.pollVotesWord; + wrap.appendChild(totalLine); + + return wrap; } function renderMessage(row) { var line = document.createElement('div'); line.className = 'gchat_msg'; + line.id = 'gchat_msg_' + row.id; var time = document.createElement('span'); time.className = 'gchat_time'; @@ -319,18 +624,113 @@ line.appendChild(document.createTextNode(': ')); + // Faza 2: mesaj sters de moderator - text golit server-side, aici doar + // afisam un placeholder; nicio actiune (nimic de moderat pe un mesaj deja sters) + if (parseInt(row.deleted, 10) === 1) { + var delText = document.createElement('span'); + delText.className = 'gchat_text gchat_text_deleted'; + delText.textContent = GCHAT_TXT.deletedPlaceholder; + line.appendChild(delText); + return line; + } + + // Faza 2: sondaj - randare separata, doar cu actiunea de stergere (mod) + if (row.type === 'poll') { + line.appendChild(renderPollBlock(row.poll, row.id)); + + var pollActions = document.createElement('span'); + pollActions.className = 'gchat_actions'; + appendModActionLinks(pollActions, row); + if (pollActions.childNodes.length) { line.appendChild(pollActions); } + + return line; + } + var text = document.createElement('span'); text.className = 'gchat_text'; text.textContent = row.msg; line.appendChild(text); - var mod = renderModActions(row); - if (mod) { line.appendChild(mod); } + var editedTag = document.createElement('span'); + editedTag.className = 'gchat_edited_tag'; + editedTag.textContent = ' ' + GCHAT_TXT.editedTag; + editedTag.style.display = parseInt(row.edited, 10) === 1 ? 'inline' : 'none'; + line.appendChild(editedTag); + + var actions = document.createElement('span'); + actions.className = 'gchat_actions'; + + var targetUid = parseInt(row.id_user, 10) || 0; + if (targetUid === myUid) { + var editLink = document.createElement('a'); + editLink.textContent = GCHAT_TXT.edit; + editLink.addEventListener('click', function () { startEdit(line, row, text); }); + actions.appendChild(editLink); + } + + appendModActionLinks(actions, row); + + if (actions.childNodes.length) { line.appendChild(actions); } return line; } - function appendMessages(rows) { + function applyMutation(row) { + var line = document.getElementById('gchat_msg_' + row.id); + if (!line) { return; } + + if (parseInt(row.deleted, 10) === 1) { + var textEl = line.querySelector('.gchat_text'); + var pollEl = line.querySelector('.gchat_poll_block'); + var actionsEl = line.querySelector('.gchat_actions'); + var editedTagEl = line.querySelector('.gchat_edited_tag'); + if (pollEl) { pollEl.remove(); } + if (actionsEl) { actionsEl.remove(); } + if (editedTagEl) { editedTagEl.remove(); } + if (textEl) { + textEl.textContent = GCHAT_TXT.deletedPlaceholder; + textEl.className = 'gchat_text gchat_text_deleted'; + } else { + var span = document.createElement('span'); + span.className = 'gchat_text gchat_text_deleted'; + span.textContent = GCHAT_TXT.deletedPlaceholder; + line.appendChild(span); + } + return; + } + + if (row.type === 'poll' && row.poll) { + var existingPoll = line.querySelector('.gchat_poll_block'); + var freshPoll = renderPollBlock(row.poll, row.id); + if (existingPoll) { + existingPoll.replaceWith(freshPoll); + } else { + line.appendChild(freshPoll); + } + return; + } + + var textEl2 = line.querySelector('.gchat_text'); + var editedTagEl2 = line.querySelector('.gchat_edited_tag'); + if (textEl2) { textEl2.textContent = row.msg; } + if (editedTagEl2 && parseInt(row.edited, 10) === 1) { + editedTagEl2.style.display = 'inline'; + } + } + + function fetchUpdatesNow() { + fetch(AJAX_URL + '?f=gchat_updates&sinceTs=' + lastMutationTs, { credentials: 'same-origin' }) + .then(function (r) { return r.json(); }) + .then(function (data) { + if (!data || !data.ok) { return; } + (data.rows || []).forEach(applyMutation); + lastMutationTs = data.now; + }) + .catch(function () { /* reincercam la urmatorul tick */ }); + } + + function appendMessages(rows, countUnread) { + if (countUnread === undefined) { countUnread = true; } var wasAtBottom = messagesBox.scrollTop + messagesBox.clientHeight >= messagesBox.scrollHeight - 4; rows.forEach(function (row) { @@ -342,7 +742,7 @@ messagesBox.scrollTop = messagesBox.scrollHeight; } - if (rows.length && !isOpen) { + if (countUnread && rows.length && !isOpen) { unread += rows.length; badge.textContent = unread > 99 ? '99+' : String(unread); badge.style.display = 'inline-block'; @@ -365,6 +765,10 @@ } } + // NOTA: numele functiei 'poll()' vine de la mecanismul de long-polling + // pentru mesaje NOI (dupa id) - nu are legatura cu sondajele (chat_global_polls, + // "gchat_poll_create/vote" mai jos); denumire mostenita din Faza 1, pastrata + // ca sa nu umblam degeaba prin tot fisierul. function poll() { fetch(AJAX_URL + '?f=gchat_poll&sinceId=' + lastId, { credentials: 'same-origin' }) .then(function (r) { return r.json(); }) @@ -374,6 +778,8 @@ appendMessages(data.messages || []); }) .catch(function () { /* hiccup de retea - reincercam la urmatorul tick */ }); + + fetchUpdatesNow(); } function loadInitial() { @@ -389,7 +795,13 @@ messagesBox.appendChild(empty); } applyViewerState(data.viewer || {}); - appendMessages(data.messages || []); + // FIX (Faza 2, 09.09.2026): istoricul incarcat la deschiderea/ + // reincarcarea paginii NU e "necitit" - inainte se aduna la + // contorul de mesaje noi de fiecare data cand pagina se + // (re)incarca, motiv pentru care badge-ul arata numarul total + // de mesaje din istoric (ex. 17), nu doar cele aparute cat timp + // panoul a stat efectiv inchis. + appendMessages(data.messages || [], false); }); } @@ -457,6 +869,108 @@ }); }); + function insertAtCursor(field, text) { + var start = field.selectionStart != null ? field.selectionStart : field.value.length; + var end = field.selectionEnd != null ? field.selectionEnd : field.value.length; + field.value = field.value.slice(0, start) + text + field.value.slice(end); + var pos = start + text.length; + field.focus(); + if (field.setSelectionRange) { field.setSelectionRange(pos, pos); } + } + + function addPollOptionRow(value) { + if (pollOptionsBox.children.length >= MAX_POLL_OPTIONS) { return; } + + var row = document.createElement('div'); + row.className = 'gchat_poll_option_row'; + + var optInput = document.createElement('input'); + optInput.type = 'text'; + optInput.maxLength = 60; + optInput.placeholder = GCHAT_TXT.pollOptionPlaceholder; + optInput.value = value || ''; + row.appendChild(optInput); + + // primele 2 optiuni sunt obligatorii (minim necesar la un sondaj) - + // fara buton de stergere pe ele + if (pollOptionsBox.children.length >= 2) { + var removeBtn = document.createElement('button'); + removeBtn.type = 'button'; + removeBtn.className = 'gchat_poll_option_remove'; + removeBtn.textContent = '\u00d7'; + removeBtn.addEventListener('click', function () { row.remove(); }); + row.appendChild(removeBtn); + } + + pollOptionsBox.appendChild(row); + } + + function resetPollForm() { + pollQuestionInput.value = ''; + pollOptionsBox.innerHTML = ''; + addPollOptionRow(''); + addPollOptionRow(''); + } + + function closePollForm() { + pollForm.style.display = 'none'; + } + + EMOJI_LIST.forEach(function (em) { + var b = document.createElement('button'); + b.type = 'button'; + b.className = 'gchat_emoji_item'; + b.textContent = em; + b.addEventListener('click', function () { insertAtCursor(input, em); }); + emojiPanel.appendChild(b); + }); + + emojiBtn.addEventListener('click', function () { + pollForm.style.display = 'none'; + emojiPanel.style.display = emojiPanel.style.display === 'none' ? 'grid' : 'none'; + }); + + pollBtn.addEventListener('click', function () { + emojiPanel.style.display = 'none'; + var opening = pollForm.style.display === 'none'; + pollForm.style.display = opening ? 'block' : 'none'; + if (opening) { resetPollForm(); } + }); + + document.getElementById('gchat_poll_add_option').addEventListener('click', function () { + addPollOptionRow(''); + }); + + document.getElementById('gchat_poll_submit').addEventListener('click', function () { + var question = pollQuestionInput.value.trim(); + var options = Array.prototype.map.call( + pollOptionsBox.querySelectorAll('input'), + function (inp) { return inp.value.trim(); } + ).filter(function (v) { return v !== ''; }); + + if (!question || options.length < 2) { return; } + + var body = 'question=' + encodeURIComponent(question); + options.forEach(function (opt) { body += '&options[]=' + encodeURIComponent(opt); }); + + fetch(AJAX_URL + '?f=gchat_poll_create', { + method: 'POST', + credentials: 'same-origin', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: body + }).then(function (r) { return r.json(); }) + .then(function (data) { + if (data && data.ok) { + closePollForm(); + poll(); + } else if (data && data.reason === 'muted') { + applyViewerState({ access: viewerAccess, mutedUntil: data.mutedUntil }); + } else { + showNotice(GCHAT_TXT.errorGeneric); + } + }); + }); + loadInitial(); pollTimer = window.setInterval(poll, 2500);