diff --git a/GameEngine/Database.php b/GameEngine/Database.php index c7a7614e..dc9f9063 100755 --- a/GameEngine/Database.php +++ b/GameEngine/Database.php @@ -497,8 +497,13 @@ class MYSQLi_DB implements IDbConnection { )); } - // we will operate in UTF8 - mysqli_query($this->dblink,"SET NAMES 'UTF8'"); + // Faza 2 Global Chat (09.09.2026): FIX critic - linia asta rula "SET NAMES + // 'UTF8'" (3 octeti/caracter) IMEDIAT dupa connect(), suprascriind + // silentios mysqli_set_charset('utf8mb4') facut in connect() (vezi + // DatabaseConnectionCore::connect()) - fara aceasta corectie, emoji-urile + // din chat_global tot ar fi picat sau ar fi fost trunchiate la INSERT, + // desi coloana + conexiunea "pareau" corect setate pe utf8mb4. + mysqli_query($this->dblink,"SET NAMES 'utf8mb4'"); } }; diff --git a/GameEngine/Database/DatabaseConnectionCore.php b/GameEngine/Database/DatabaseConnectionCore.php index 97ebadf1..b7ad1414 100644 --- a/GameEngine/Database/DatabaseConnectionCore.php +++ b/GameEngine/Database/DatabaseConnectionCore.php @@ -75,6 +75,12 @@ trait DatabaseConnectionCore { } if ($this->dblink instanceof \mysqli) { + // FIX (Faza 2 Global Chat, 09.09.2026): fara asta, conexiunea foloseste + // charset-ul implicit al serverului MySQL, care poate sa nu fie utf8mb4 - + // emoji-urile (caractere pe 4 octeti) ar pica cu eroare sau ar fi trunchiate + // silentios la INSERT in chat_global.msg (acum utf8mb4). utf8mb4 e superset + // peste utf8, deci restul tabelelor (ramase utf8) nu sunt afectate negativ. + @mysqli_set_charset($this->dblink, 'utf8mb4'); return true; } diff --git a/GameEngine/Database/DatabaseGlobalChatQueries.php b/GameEngine/Database/DatabaseGlobalChatQueries.php index 12a536fb..7e1bab22 100644 --- a/GameEngine/Database/DatabaseGlobalChatQueries.php +++ b/GameEngine/Database/DatabaseGlobalChatQueries.php @@ -12,6 +12,10 @@ ## Cerut de Catalin, 07.09.2026: chat general vizibil/scris de orice ## ## jucator logat, cu moderare pentru Admin (access 9) si MH (access 8). ## ## ## +## Faza 2 (09.09.2026): stergere mesaj (mod), editare mesaj propriu (user), ## +## sondaje (chat_global_polls / chat_global_poll_votes) si suport emoji ## +## (chat_global trecut pe utf8mb4 - vezi struct.sql). ## +## ## ## License: TravianZ Project ## ## Copyright: TravianZ (c) 2010-2026. All rights reserved. ## ## URLs: https://travianz.org ## @@ -79,9 +83,10 @@ trait DatabaseGlobalChatQueries { * singur JOIN (nu o interogare per mesaj) - reflecta mereu alianta * curenta a jucatorului, nu una "inghetata" la momentul postarii. */ - function getGlobalChatMessages($limit = 30, $sinceId = 0) { + function getGlobalChatMessages($limit = 30, $sinceId = 0, $viewerUid = 0) { $limit = (int) $limit; $sinceId = (int) $sinceId; + $viewerUid = (int) $viewerUid; $where = $sinceId > 0 ? "WHERE c.id > $sinceId" : ""; $order = $sinceId > 0 ? "ORDER BY c.id ASC" : "ORDER BY c.id DESC"; @@ -90,7 +95,10 @@ trait DatabaseGlobalChatQueries { // browser pe mobil), la revenire nu vrem un SELECT nemarginit $limitSql = "LIMIT " . ($sinceId > 0 ? 200 : $limit); - $q = "SELECT c.id, c.id_user, c.date, c.msg, u.username, u.access, a.tag AS ally_tag, a.id AS ally_id + // Faza 2: c.type/c.poll_id/c.deleted/c.edited - vezi editGlobalChatMessage(), + // deleteGlobalChatMessage() si createGlobalChatPoll() mai jos + $q = "SELECT c.id, c.id_user, c.date, c.msg, c.type, c.poll_id, c.deleted, c.edited, + u.username, u.access, a.tag AS ally_tag, a.id AS ally_id FROM " . TB_PREFIX . "chat_global c LEFT JOIN " . TB_PREFIX . "users u ON u.id = c.id_user LEFT JOIN " . TB_PREFIX . "alidata a ON a.id = u.alliance AND u.alliance > 0 @@ -106,9 +114,303 @@ trait DatabaseGlobalChatQueries { $rows = array_reverse($rows); } + foreach ($rows as &$row) { + if ((int) $row['deleted'] === 1) { + // golim textul server-side (nu doar in client) - un mesaj sters de + // moderator (de obicei vulgar) nu trebuie sa mai ajunga deloc, sub + // nicio forma, in raspunsul JSON catre ceilalti clienti + $row['msg'] = ''; + } elseif ($row['type'] === 'poll' && $row['poll_id']) { + $row['poll'] = $this->getGlobalChatPollData($row['poll_id'], $viewerUid); + } + } + unset($row); + return $rows; } + /** + * Editeaza un mesaj propriu (text simplu, nu sondaje) - vezi + * deleteGlobalChatMessage() pentru actiunea de moderare (mesajul altcuiva). + * Fara limita de timp la editare (nu a fost ceruta); usor de adaugat o + * fereastra (ex. doar primele 5 minute) daca se doreste ulterior. + * + * @return array ['ok'=>bool, 'reason'=>string|null] + */ + function editGlobalChatMessage($uid, $msgId, $newMsg) { + $uid = (int) $uid; + $msgId = (int) $msgId; + $newMsg = trim((string) $newMsg); + + if ($uid <= 0 || $msgId <= 0) { + return ['ok' => false, 'reason' => 'invalid']; + } + + if ($newMsg === '') { + return ['ok' => false, 'reason' => 'empty']; + } + + $newMsg = function_exists('mb_substr') ? mb_substr($newMsg, 0, 250) : substr($newMsg, 0, 250); + + $row = mysqli_fetch_assoc($this->query( + "SELECT id_user, type, deleted FROM " . TB_PREFIX . "chat_global WHERE id = $msgId" + )); + + if (!$row) { + return ['ok' => false, 'reason' => 'notfound']; + } + + // doar propriul mesaj, doar daca nu a fost deja sters de un moderator, + // si doar mesaje text (ancora unui sondaj nu se editeaza ca text simplu) + if ((int) $row['id_user'] !== $uid || (int) $row['deleted'] === 1 || $row['type'] !== 'text') { + return ['ok' => false, 'reason' => 'forbidden']; + } + + list($emsg) = $this->escape_input($newMsg); + $now = time(); + + $this->query( + "UPDATE " . TB_PREFIX . "chat_global SET msg = '$emsg', edited = 1, updated_at = $now WHERE id = $msgId" + ); + + return ['ok' => true]; + } + + /** + * Sterge (soft-delete) un mesaj din chat-ul general - actiune de moderare. + * Textul e golit aici (nu doar ascuns in client), acelasi motiv ca la + * golirea din getGlobalChatMessages() mai sus. Verificarea de rang (MH/ + * Admin, access minim) se face in ajax.php, la fel ca la mute/block - DAR + * spre deosebire de mute/block, aici NU se compara rangul cu al autorului: + * stergerea vizeaza continutul (un mesaj vulgar), nu persoana, deci orice + * MH/Admin poate sterge orice mesaj, inclusiv al altui MH, ca sa poata + * curata rapid chat-ul. + * + * @return bool + */ + function deleteGlobalChatMessage($msgId) { + $msgId = (int) $msgId; + $now = time(); + + return $this->query( + "UPDATE " . TB_PREFIX . "chat_global SET msg = '', deleted = 1, updated_at = $now WHERE id = $msgId AND deleted = 0" + ) ? true : false; + } + + /** + * Mesaje editate/sterse de la ultimul cec al clientului ($sinceTs, unix) - + * necesar ca sa se poata actualiza "in loc" mesajele deja randate la un + * client care are panoul de chat deschis de mai mult timp: poll-ul normal + * de mesaje noi (getGlobalChatMessages, dupa id) nu ar mai prinde o + * editare/stergere pe un mesaj vechi, deja afisat, cu id mai mic decat + * ultimul id vazut de client. + * + * Include si datele proaspete de sondaj pentru randurile de tip 'poll' + * (ex. cand cineva voteaza, votul nu schimba mesajul in sine, dar + * voteGlobalChatPoll() atinge acest rand exact ca sa fie prins aici - + * altfel doar cel care voteaza ar vedea rezultatul actualizat). + * + * @return array ['rows'=>array, 'now'=>int] - 'now' devine noul watermark pe client + */ + function getGlobalChatUpdates($sinceTs, $viewerUid = 0, $limit = 100) { + $sinceTs = (int) $sinceTs; + $viewerUid = (int) $viewerUid; + $now = time(); + $limit = (int) $limit; + + $rows = $this->mysqli_fetch_all($this->query( + "SELECT id, msg, type, poll_id, deleted, edited FROM " . TB_PREFIX . "chat_global + WHERE updated_at > $sinceTs AND updated_at <= $now + ORDER BY updated_at ASC + LIMIT $limit" + )); + + foreach ($rows as &$row) { + if ((int) $row['deleted'] === 1) { + $row['msg'] = ''; + } elseif ($row['type'] === 'poll' && $row['poll_id']) { + $row['poll'] = $this->getGlobalChatPollData($row['poll_id'], $viewerUid); + } + } + unset($row); + + return ['rows' => $rows, 'now' => $now]; + } + + /** + * Creeaza un sondaj in chat-ul general: un rand-ancora in chat_global + * (type='poll', ca sa-si pastreze locul cronologic in flux de mesaje) + + * randul cu intrebarea/optiunile in chat_global_polls. Reutilizeaza + * acelasi mute-check ca postGlobalChatMessage (un user mutat nu poate + * nici posta mesaje, nici crea sondaje). + * + * @param array $options 2-6 optiuni (string-uri) - restul sunt ignorate + * @return array ['ok'=>bool, 'reason'=>string|null] + */ + function createGlobalChatPoll($uid, $question, $options) { + $uid = (int) $uid; + $question = trim((string) $question); + $question = function_exists('mb_substr') ? mb_substr($question, 0, 200) : substr($question, 0, 200); + + if ($uid <= 0 || $question === '') { + return ['ok' => false, 'reason' => 'invalid']; + } + + $clean = []; + foreach ((array) $options as $opt) { + $opt = trim((string) $opt); + if ($opt === '') { + continue; + } + $clean[] = function_exists('mb_substr') ? mb_substr($opt, 0, 60) : substr($opt, 0, 60); + if (count($clean) >= 6) { + break; + } + } + + if (count($clean) < 2) { + return ['ok' => false, 'reason' => 'notenoughoptions']; + } + + $mutedUntil = $this->getGlobalChatMuteStatus($uid); + if ($mutedUntil !== null) { + return ['ok' => false, 'reason' => 'muted', 'mutedUntil' => $mutedUntil]; + } + + list($euid) = $this->escape_input($uid); + list($eq) = $this->escape_input($question); + $now = time(); + + // 1) randul-ancora in chat_global (msg = intrebarea - fallback util daca + // ceva citeste chat_global fara sa stie de chat_global_polls) + $this->query( + "INSERT INTO " . TB_PREFIX . "chat_global (id_user, date, msg, type) VALUES ($euid, $now, '$eq', 'poll')" + ); + $chatId = mysqli_insert_id($this->dblink); + + // 2) intrebarea + optiunile (JSON) + list($eOptions) = $this->escape_input(json_encode(array_values($clean))); + $this->query( + "INSERT INTO " . TB_PREFIX . "chat_global_polls (chat_id, id_user, question, options, created) + VALUES ($chatId, $euid, '$eq', '$eOptions', $now)" + ); + $pollId = mysqli_insert_id($this->dblink); + + // 3) leaga ancora de randul de sondaj + $this->query( + "UPDATE " . TB_PREFIX . "chat_global SET poll_id = $pollId WHERE id = $chatId" + ); + + return ['ok' => true]; + } + + /** + * Inregistreaza votul unui user la un sondaj din chat-ul general. Userul + * isi poate schimba optiunea (ON DUPLICATE KEY UPDATE) - nu exista un + * "vot definitiv", nefiind cerut. + * + * @return array ['ok'=>bool, 'reason'=>string|null] + */ + function voteGlobalChatPoll($uid, $pollId, $optionIndex) { + $uid = (int) $uid; + $pollId = (int) $pollId; + $optionIndex = (int) $optionIndex; + + if ($uid <= 0 || $pollId <= 0 || $optionIndex < 0) { + return ['ok' => false, 'reason' => 'invalid']; + } + + $poll = mysqli_fetch_assoc($this->query( + "SELECT chat_id, options FROM " . TB_PREFIX . "chat_global_polls WHERE id = $pollId" + )); + + if (!$poll) { + return ['ok' => false, 'reason' => 'notfound']; + } + + $options = json_decode($poll['options'], true); + if (!is_array($options) || !isset($options[$optionIndex])) { + return ['ok' => false, 'reason' => 'invalid']; + } + + $now = time(); + $this->query( + "INSERT INTO " . TB_PREFIX . "chat_global_poll_votes (poll_id, id_user, option_index, voted_at) + VALUES ($pollId, $uid, $optionIndex, $now) + ON DUPLICATE KEY UPDATE option_index = $optionIndex, voted_at = $now" + ); + + // atinge randul-ancora din chat_global ca votul sa fie prins de + // getGlobalChatUpdates() (watermark-ul de editari/stergeri) - altfel + // doar userul care a votat ar vedea rezultatul actualizat, ceilalti + // clienti cu sondajul deja afisat ar ramane cu numaratoarea veche + $chatId = (int) $poll['chat_id']; + $this->query( + "UPDATE " . TB_PREFIX . "chat_global SET updated_at = $now WHERE id = $chatId" + ); + + return ['ok' => true]; + } + + /** + * Rezultatele unui sondaj + optiunea aleasa de $viewerUid (daca a votat). + * Apelata per-rand din getGlobalChatMessages() pentru mesajele de tip + * 'poll' - sondajele sunt rare fata de mesajele normale, deci interogarile + * suplimentare (N+1) sunt neglijabile aici. + */ + function getGlobalChatPollData($pollId, $viewerUid) { + $pollId = (int) $pollId; + $viewerUid = (int) $viewerUid; + + $poll = mysqli_fetch_assoc($this->query( + "SELECT id, question, options FROM " . TB_PREFIX . "chat_global_polls WHERE id = $pollId" + )); + + if (!$poll) { + return null; + } + + $options = json_decode($poll['options'], true); + if (!is_array($options)) { + $options = []; + } + + $counts = array_fill(0, count($options), 0); + $voteRows = $this->mysqli_fetch_all($this->query( + "SELECT option_index, COUNT(*) AS c FROM " . TB_PREFIX . "chat_global_poll_votes + WHERE poll_id = $pollId GROUP BY option_index" + )); + + $total = 0; + foreach ($voteRows as $vr) { + $idx = (int) $vr['option_index']; + if (isset($counts[$idx])) { + $counts[$idx] = (int) $vr['c']; + } + $total += (int) $vr['c']; + } + + $myVote = null; + if ($viewerUid > 0) { + $mv = mysqli_fetch_assoc($this->query( + "SELECT option_index FROM " . TB_PREFIX . "chat_global_poll_votes + WHERE poll_id = $pollId AND id_user = $viewerUid" + )); + if ($mv) { + $myVote = (int) $mv['option_index']; + } + } + + return [ + 'id' => (int) $poll['id'], + 'question' => $poll['question'], + 'options' => $options, + 'counts' => $counts, + 'total' => $total, + 'myVote' => $myVote, + ]; + } + /** * Timestamp-ul (unix) pana la care userul e mutat din chat-ul general, * sau null daca nu e mutat (inclusiv daca o mutare veche a expirat). diff --git a/GameEngine/Lang/en.php b/GameEngine/Lang/en.php index 5b57607a..fbe8f2c9 100755 --- a/GameEngine/Lang/en.php +++ b/GameEngine/Lang/en.php @@ -4423,3 +4423,19 @@ tz_def('GCHAT_BLOCK', 'Block'); tz_def('GCHAT_UNMUTE', 'Unmute'); tz_def('GCHAT_ADMIN_BADGE', 'Admin'); tz_def('GCHAT_MH_BADGE', 'MH'); +// Phase 2 (09.09.2026): edit/delete message, polls, emoji +tz_def('GCHAT_EDIT', 'Edit'); +tz_def('GCHAT_SAVE', 'Save'); +tz_def('GCHAT_CANCEL', 'Cancel'); +tz_def('GCHAT_EDITED_TAG', '(edited)'); +tz_def('GCHAT_DELETE', 'Delete'); +tz_def('GCHAT_CONFIRM_DELETE', 'Delete this message?'); +tz_def('GCHAT_DELETED_PLACEHOLDER', 'Message removed by a moderator.'); +tz_def('GCHAT_POLL_TITLE', 'Create a poll'); +tz_def('GCHAT_POLL_QUESTION_PLACEHOLDER', 'Question...'); +tz_def('GCHAT_POLL_OPTION_PLACEHOLDER', 'Option'); +tz_def('GCHAT_POLL_ADD_OPTION', '+ Option'); +tz_def('GCHAT_POLL_CREATE', 'Create poll'); +tz_def('GCHAT_POLL_VOTES_WORD', 'votes'); +tz_def('GCHAT_EMOJI_TITLE', 'Emoji'); +tz_def('GCHAT_ERROR_GENERIC', 'Something went wrong.'); diff --git a/GameEngine/Lang/ro.php b/GameEngine/Lang/ro.php index db5ab125..8810d1c7 100644 --- a/GameEngine/Lang/ro.php +++ b/GameEngine/Lang/ro.php @@ -4160,3 +4160,19 @@ tz_def('GCHAT_BLOCK', 'Blocheaza'); tz_def('GCHAT_UNMUTE', 'Anuleaza mute'); tz_def('GCHAT_ADMIN_BADGE', 'Admin'); tz_def('GCHAT_MH_BADGE', 'MH'); +// Faza 2 (09.09.2026): editare/stergere mesaj, sondaje, emoji +tz_def('GCHAT_EDIT', 'Editeaza'); +tz_def('GCHAT_SAVE', 'Salveaza'); +tz_def('GCHAT_CANCEL', 'Anuleaza'); +tz_def('GCHAT_EDITED_TAG', '(editat)'); +tz_def('GCHAT_DELETE', 'Sterge'); +tz_def('GCHAT_CONFIRM_DELETE', 'Stergi acest mesaj?'); +tz_def('GCHAT_DELETED_PLACEHOLDER', 'Mesaj sters de un moderator.'); +tz_def('GCHAT_POLL_TITLE', 'Creeaza un sondaj'); +tz_def('GCHAT_POLL_QUESTION_PLACEHOLDER', 'Intrebare...'); +tz_def('GCHAT_POLL_OPTION_PLACEHOLDER', 'Optiune'); +tz_def('GCHAT_POLL_ADD_OPTION', '+ Optiune'); +tz_def('GCHAT_POLL_CREATE', 'Creeaza sondaj'); +tz_def('GCHAT_POLL_VOTES_WORD', 'voturi'); +tz_def('GCHAT_EMOJI_TITLE', 'Emoji'); +tz_def('GCHAT_ERROR_GENERIC', 'A aparut o eroare.'); diff --git a/Templates/GlobalChat/widget.tpl b/Templates/GlobalChat/widget.tpl index d7d67f27..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,13 +384,28 @@ 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; var unread = 0; var isOpen = false; 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); @@ -250,38 +420,177 @@ function escapeForLog(s) { return s; } // (folosim textContent peste tot mai jos - nu innerHTML pe input de utilizator) - function renderModActions(row) { - if (!viewerIsMod || row.id_user === myUid || (row.access || 0) >= ACCESS_MH) { - return null; - } + function appendModActionLinks(parentSpan, row) { + if (!viewerIsMod) { return; } - var span = document.createElement('span'); - span.className = 'gchat_mod_actions'; + var targetAccess = parseInt(row.access, 10) || 0; + var targetUid = parseInt(row.id_user, 10) || 0; 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'; @@ -315,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) { @@ -338,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'; @@ -346,7 +750,8 @@ } function applyViewerState(viewer) { - viewerIsMod = !!viewer.isMod; + viewerAccess = parseInt(viewer.access, 10) || 0; + viewerIsMod = viewerAccess >= ACCESS_MH; if (viewer.mutedUntil) { input.disabled = true; @@ -360,15 +765,21 @@ } } + // 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(); }) .then(function (data) { if (!data || !data.ok) { return; } - appendMessages(data.messages || []); applyViewerState(data.viewer || {}); + appendMessages(data.messages || []); }) .catch(function () { /* hiccup de retea - reincercam la urmatorul tick */ }); + + fetchUpdatesNow(); } function loadInitial() { @@ -383,8 +794,14 @@ empty.textContent = GCHAT_TXT.empty; messagesBox.appendChild(empty); } - appendMessages(data.messages || []); applyViewerState(data.viewer || {}); + // 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); }); } @@ -442,13 +859,118 @@ showNotice(''); poll(); } else if (data && data.reason === 'muted') { - applyViewerState({ isMod: viewerIsMod, mutedUntil: data.mutedUntil }); + applyViewerState({ + access: viewerAccess, + mutedUntil: data.mutedUntil + }); } else if (data && data.reason === 'ratelimit') { showNotice(GCHAT_TXT.ratelimit); } }); }); + 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); diff --git a/ajax.php b/ajax.php index 53eca02a..94320ede 100644 --- a/ajax.php +++ b/ajax.php @@ -111,6 +111,7 @@ switch(isset($_GET['f']) ? $_GET['f'] : '') { // Chat general (server-wide), cerut de Catalin 07.09.2026. Separat de // alliance chat (Chat.php, SAJAX) - vazut/scris de orice user logat, // moderat de MH (access 8) si Admin (access 9). + // Faza 2 (09.09.2026): sterge/editeaza mesaj, sondaje, mai jos. case 'gchat_poll': header('Content-Type: application/json'); if (!isset($_SESSION)) { @@ -126,11 +127,37 @@ switch(isset($_GET['f']) ? $_GET['f'] : '') { $sinceId = (int) ($_GET['sinceId'] ?? 0); echo json_encode([ 'ok' => 1, - 'messages' => $database->getGlobalChatMessages(30, $sinceId), + 'messages' => $database->getGlobalChatMessages(30, $sinceId, $uid), 'viewer' => $database->getGlobalChatViewerInfo($uid), ]); break; + // Faza 2 (09.09.2026): editari/stergeri intamplate de la ultimul cec al + // clientului - separat de 'gchat_poll' de mai sus (acela e long-polling + // pentru mesaje NOI dupa id; asta e watermark pe timp, pentru mesaje VECHI + // deja afisate care s-au schimbat - denumiri diferite intentionat, ca sa + // nu se confunde cele doua mecanisme). + case 'gchat_updates': + header('Content-Type: application/json'); + if (!isset($_SESSION)) { + session_start(); + } + include_once($autoprefix.'GameEngine/Database.php'); + $uid = (int) ($_SESSION['id_user'] ?? 0); + if (!$uid) { + http_response_code(403); + echo json_encode(['ok' => 0, 'reason' => 'notloggedin']); + break; + } + $sinceTs = (int) ($_GET['sinceTs'] ?? 0); + $result = $database->getGlobalChatUpdates($sinceTs, $uid); + echo json_encode([ + 'ok' => 1, + 'rows' => $result['rows'], + 'now' => $result['now'], + ]); + break; + case 'gchat_send': header('Content-Type: application/json'); if (!isset($_SESSION)) { @@ -146,6 +173,81 @@ switch(isset($_GET['f']) ? $_GET['f'] : '') { echo json_encode($database->postGlobalChatMessage($uid, $_POST['msg'] ?? '')); break; + // Faza 2 (09.09.2026): userul isi editeaza propriul mesaj. Verificarea de + // proprietate (doar mesajul lui) se face in Database::editGlobalChatMessage(). + case 'gchat_edit': + header('Content-Type: application/json'); + if (!isset($_SESSION)) { + session_start(); + } + include_once($autoprefix.'GameEngine/Database.php'); + $uid = (int) ($_SESSION['id_user'] ?? 0); + if (!$uid) { + http_response_code(403); + echo json_encode(['ok' => 0, 'reason' => 'notloggedin']); + break; + } + echo json_encode($database->editGlobalChatMessage($uid, $_POST['id'] ?? 0, $_POST['msg'] ?? '')); + break; + + // Faza 2 (09.09.2026): MH/Admin sterg un mesaj (de obicei vulgar). Spre + // deosebire de mute/block mai jos, aici NU se compara rangul cu al + // autorului mesajului - vezi comentariul din Database::deleteGlobalChatMessage(). + case 'gchat_delete': + header('Content-Type: application/json'); + if (!isset($_SESSION)) { + session_start(); + } + include_once($autoprefix.'GameEngine/Database.php'); + $modUid = (int) ($_SESSION['id_user'] ?? 0); + $msgId = (int) ($_POST['id'] ?? 0); + if (!$modUid || !$msgId) { + http_response_code(403); + echo json_encode(['ok' => 0, 'reason' => 'notloggedin']); + break; + } + $modAccess = (int) $database->getUserField($modUid, 'access', 0); + if ($modAccess < MULTIHUNTER) { + http_response_code(403); + echo json_encode(['ok' => 0, 'reason' => 'forbidden']); + break; + } + echo json_encode(['ok' => $database->deleteGlobalChatMessage($msgId) ? 1 : 0]); + break; + + // Faza 2 (09.09.2026): creare sondaj in chat-ul general. $_POST['options'] + // vine ca array (options[]=...&options[]=...) din formularul de creare. + case 'gchat_poll_create': + header('Content-Type: application/json'); + if (!isset($_SESSION)) { + session_start(); + } + include_once($autoprefix.'GameEngine/Database.php'); + $uid = (int) ($_SESSION['id_user'] ?? 0); + if (!$uid) { + http_response_code(403); + echo json_encode(['ok' => 0, 'reason' => 'notloggedin']); + break; + } + $options = isset($_POST['options']) && is_array($_POST['options']) ? $_POST['options'] : []; + echo json_encode($database->createGlobalChatPoll($uid, $_POST['question'] ?? '', $options)); + break; + + case 'gchat_poll_vote': + header('Content-Type: application/json'); + if (!isset($_SESSION)) { + session_start(); + } + include_once($autoprefix.'GameEngine/Database.php'); + $uid = (int) ($_SESSION['id_user'] ?? 0); + if (!$uid) { + http_response_code(403); + echo json_encode(['ok' => 0, 'reason' => 'notloggedin']); + break; + } + echo json_encode($database->voteGlobalChatPoll($uid, $_POST['pollId'] ?? 0, $_POST['option'] ?? -1)); + break; + case 'gchat_mute': case 'gchat_block': case 'gchat_unmute': diff --git a/var/db/struct.sql b/var/db/struct.sql index 77bf3334..3caecfb0 100644 --- a/var/db/struct.sql +++ b/var/db/struct.sql @@ -583,14 +583,25 @@ CREATE TABLE IF NOT EXISTS `%PREFIX%chat` ( -- de toti jucatorii de pe server, indiferent de alianta) -- +-- NOTA (Faza 2, 09.09.2026): tabela + coloanele noi de mai jos folosesc +-- utf8mb4 (nu utf8 ca restul proiectului) - utf8 clasic din MySQL e de fapt +-- utf8mb3 (max 3 octeti/caracter) si NU poate stoca emoji moderne (necesita +-- 4 octeti). utf8mb4 e superset, deci JOIN-urile pe id-uri numerice cu +-- `users`/`alidata` (ramase utf8) raman perfect functionale. CREATE TABLE IF NOT EXISTS `%PREFIX%chat_global` ( `id` int(20) NOT NULL AUTO_INCREMENT, `id_user` int(11) NOT NULL, `date` int(11) NOT NULL, `msg` varchar(250) NOT NULL, + `type` varchar(10) NOT NULL DEFAULT 'text', + `poll_id` int(20) NULL DEFAULT NULL, + `deleted` tinyint(1) NOT NULL DEFAULT 0, + `edited` tinyint(1) NOT NULL DEFAULT 0, + `updated_at` int(11) NULL DEFAULT NULL, PRIMARY KEY (`id`), - KEY `id_user_date` (`id_user`,`date`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; + KEY `id_user_date` (`id_user`,`date`), + KEY `updated_at` (`updated_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- -------------------------------------------------------- @@ -610,6 +621,43 @@ CREATE TABLE IF NOT EXISTS `%PREFIX%chat_mutes` ( PRIMARY KEY (`id_user`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +-- -------------------------------------------------------- + +-- +-- Sondaje in chat-ul general (Faza 2, 09.09.2026). `chat_id` leaga sondajul +-- de randul-ancora din chat_global (type='poll'), care ii pastreaza pozitia +-- cronologica in fluxul de mesaje. `options` = array JSON de string-uri +-- (2-6 optiuni, validat in PHP la creare). +-- + +CREATE TABLE IF NOT EXISTS `%PREFIX%chat_global_polls` ( + `id` int(20) NOT NULL AUTO_INCREMENT, + `chat_id` int(20) NOT NULL, + `id_user` int(11) NOT NULL, + `question` varchar(200) NOT NULL, + `options` text NOT NULL, + `created` int(11) NOT NULL, + PRIMARY KEY (`id`), + KEY `chat_id` (`chat_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- -------------------------------------------------------- + +-- +-- Voturi pe sondaje - un singur vot per user per sondaj (id_user in PRIMARY +-- KEY alaturi de poll_id); un vot nou suprascrie vechiul vot al aceluiasi +-- user via ON DUPLICATE KEY UPDATE (userul isi poate schimba optiunea). +-- + +CREATE TABLE IF NOT EXISTS `%PREFIX%chat_global_poll_votes` ( + `poll_id` int(20) NOT NULL, + `id_user` int(11) NOT NULL, + `option_index` tinyint(3) NOT NULL, + `voted_at` int(11) NOT NULL, + PRIMARY KEY (`poll_id`,`id_user`), + KEY `poll_id` (`poll_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + -- --------------------------------------------------------