diff --git a/GameEngine/Database/DatabaseGlobalChatQueries.php b/GameEngine/Database/DatabaseGlobalChatQueries.php index 7e1bab22..d8e3e175 100644 --- a/GameEngine/Database/DatabaseGlobalChatQueries.php +++ b/GameEngine/Database/DatabaseGlobalChatQueries.php @@ -97,7 +97,8 @@ trait DatabaseGlobalChatQueries { // 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, + // Faza 3: c.report_id - vezi shareGlobalChatReport() mai jos + $q = "SELECT c.id, c.id_user, c.date, c.msg, c.type, c.poll_id, c.report_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 @@ -475,4 +476,118 @@ trait DatabaseGlobalChatQueries { "DELETE FROM " . TB_PREFIX . "chat_mutes WHERE id_user = $targetUid" ) ? true : false; } + + /** + * Faza 3 (09.09.2026): cautare useri dupa inceputul username-ului, pentru + * autocomplete la @mentiuni. Exclude id 1-3 (Nature/Natars/conturi de + * sistem - vezi addNotice() din DatabaseMessageQueries.php, care le trateaza + * deja ca speciale). Scapam manual '%'/'_' din query ca sa nu functioneze + * ca wildcard-uri LIKE neintentionate daca cineva tasteaza chiar aceste + * caractere dupa @. + */ + function searchGlobalChatUsers($prefix, $limit = 8) { + $prefix = trim((string) $prefix); + $limit = (int) $limit; + + if ($prefix === '') { + return []; + } + + $likeSafe = str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $prefix); + list($eprefix) = $this->escape_input($likeSafe); + + return $this->mysqli_fetch_all($this->query( + "SELECT id, username FROM " . TB_PREFIX . "users + WHERE username LIKE '$eprefix%' AND id > 3 + ORDER BY username ASC + LIMIT $limit" + )); + } + + /** + * Faza 3 (09.09.2026): distribuie un raport de lupta propriu in chat-ul + * general - de atac SAU de aparare (cand te ataca altcineva pe tine, + * raportul TAU are tot ntype in acelasi set - vezi + * AutomationBattleResolution::[cod care apeleaza addNotice], unde + * atacatorul si apararea primesc fiecare propriul rand in `ndata`, cu + * ntype-uri complementare din SETUL de mai jos - t=3/TZ_ATTACKS din + * Message::noticeType() include deja ambele roluri, nu doar atacul). + * Include si perechea 22/23 (scenariu de atac separat, gasit tot in + * AutomationBattleResolution.php - nu e in intervalul 1-7 dar e tot + * un raport de lupta atac+aparare). + * + * Exclude intentionat intariri (8,15,16,17) si comert (10-13): acelea + * dezvaluie logistica/loturi, nepotrivite pentru difuzare pe tot + * serverul (chat-ul general nu are granita de alianta, deci un raport + * distribuit aici e vizibil oricui, inclusiv unui inamic) - acelasi + * filtru ca share-ul de alianta din berichte.php. + * + * NU atingem tabela ndata (folosita de tot sistemul de mesaje) - in loc, + * adaugam id-ul raportului intr-o allowlist separata (chat_global_shared_reports), + * verificata suplimentar in berichte.php la afisarea unui raport individual. + * + * @return array ['ok'=>bool, 'reason'=>string|null] + */ + function shareGlobalChatReport($uid, $noticeId) { + $uid = (int) $uid; + $noticeId = (int) $noticeId; + + if ($uid <= 0 || $noticeId <= 0) { + return ['ok' => false, 'reason' => 'invalid']; + } + + $notice = $this->getNotice2($noticeId, null); + + if (!$notice) { + return ['ok' => false, 'reason' => 'notfound']; + } + + // tine sincron cu acelasi set din berichte.php ($tzShareableNtypes) + $shareableNtypes = [1, 2, 3, 4, 5, 6, 7, 22, 23]; + if ((int) $notice['uid'] !== $uid || !in_array((int) $notice['ntype'], $shareableNtypes, true)) { + return ['ok' => false, 'reason' => 'forbidden']; + } + + $mutedUntil = $this->getGlobalChatMuteStatus($uid); + if ($mutedUntil !== null) { + return ['ok' => false, 'reason' => 'muted', 'mutedUntil' => $mutedUntil]; + } + + $now = time(); + list($eNoticeId, $euid) = $this->escape_input($noticeId, $uid); + + // allowlist: acest raport devine vizibil oricui e logat (vezi berichte.php) + $this->query( + "INSERT INTO " . TB_PREFIX . "chat_global_shared_reports (notice_id, shared_by, created) + VALUES ($eNoticeId, $euid, $now) + ON DUPLICATE KEY UPDATE shared_by = $euid, created = $now" + ); + + // mesajul-ancora in chat - topicul raportului (deja text afisabil, + // acelasi camp folosit si in lista de rapoarte din berichte.php) + $topic = trim((string) ($notice['topic'] ?? '')); + $topic = function_exists('mb_substr') ? mb_substr($topic, 0, 250) : substr($topic, 0, 250); + list($eTopic) = $this->escape_input($topic); + + $this->query( + "INSERT INTO " . TB_PREFIX . "chat_global (id_user, date, msg, type, report_id) + VALUES ($euid, $now, '$eTopic', 'report', $eNoticeId)" + ); + + return ['ok' => true]; + } + + /** + * Verifica daca un raport a fost distribuit in chat-ul general - folosita + * de berichte.php ca o conditie suplimentara de acces (vezi acolo). + */ + function isGlobalChatSharedReport($noticeId) { + $noticeId = (int) $noticeId; + + $row = mysqli_fetch_assoc($this->query( + "SELECT notice_id FROM " . TB_PREFIX . "chat_global_shared_reports WHERE notice_id = $noticeId" + )); + + return $row ? true : false; + } } diff --git a/GameEngine/Lang/en.php b/GameEngine/Lang/en.php index fbe8f2c9..770cb714 100755 --- a/GameEngine/Lang/en.php +++ b/GameEngine/Lang/en.php @@ -4439,3 +4439,7 @@ 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.'); +// Phase 3 (09.09.2026): @username mentions, share battle reports +tz_def('GCHAT_SHARE_REPORT', 'Share to chat'); +tz_def('GCHAT_SHARE_REPORT_OK', 'Shared to chat!'); +tz_def('GCHAT_REPORT_FALLBACK', 'Battle report'); diff --git a/GameEngine/Lang/ro.php b/GameEngine/Lang/ro.php index 8810d1c7..f5c869f0 100644 --- a/GameEngine/Lang/ro.php +++ b/GameEngine/Lang/ro.php @@ -4176,3 +4176,7 @@ 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.'); +// Faza 3 (09.09.2026): mentiuni @username, distribuire rapoarte de lupta +tz_def('GCHAT_SHARE_REPORT', 'Distribuie in chat'); +tz_def('GCHAT_SHARE_REPORT_OK', 'Distribuit in chat!'); +tz_def('GCHAT_REPORT_FALLBACK', 'Raport de lupta'); diff --git a/Templates/GlobalChat/share_report_button.tpl b/Templates/GlobalChat/share_report_button.tpl new file mode 100644 index 00000000..6cd1ae8f --- /dev/null +++ b/Templates/GlobalChat/share_report_button.tpl @@ -0,0 +1,59 @@ + +
+ + +
+ diff --git a/Templates/GlobalChat/widget.tpl b/Templates/GlobalChat/widget.tpl index 2338a984..05855125 100644 --- a/Templates/GlobalChat/widget.tpl +++ b/Templates/GlobalChat/widget.tpl @@ -24,7 +24,7 @@ ## Copyright: TravianZ (c) 2010-2026. All rights reserved. ## ################################################################################# ?> -
+
@@ -50,6 +50,8 @@
+ +
@@ -335,6 +337,50 @@ #gchat_poll_add_option { background: #ddd; } #gchat_poll_submit { background: #6b8f47; color: #fff; } + /* Faza 3: mentiuni @username */ + #gchat_mention_list { + display: none; + position: absolute; + left: 8px; + right: 8px; + bottom: 40px; + background: #fff; + border: 1px solid #ccc; + border-radius: 4px; + max-height: 120px; + overflow-y: auto; + box-shadow: 0 -2px 8px rgba(0,0,0,.15); + z-index: 10; + } + .gchat_mention_item { + padding: 4px 8px; + cursor: pointer; + font-size: 12px; + } + .gchat_mention_item:hover, .gchat_mention_item.active { background: #eef2e8; } + + .gchat_mention { color: #1a5aa8; font-weight: bold; } + .gchat_msg_mentioned { + background: #fff6d5; + border-radius: 3px; + padding: 2px 3px; + margin: -2px -3px 4px -3px; + } + + /* Faza 3: rapoarte de lupta distribuite in chat */ + .gchat_report_block { margin-top: 2px; } + .gchat_report_link { + display: inline-block; + padding: 3px 6px; + background: #f2f0e6; + border: 1px solid #ddd6bd; + border-radius: 4px; + color: #6b4a1f; + text-decoration: none; + font-size: 11px; + } + .gchat_report_link:hover { text-decoration: underline; } + @media (max-width: 480px) { #gchat_panel { right: 8px; bottom: 66px; } #gchat_bubble { right: 8px; bottom: 8px; } @@ -367,7 +413,8 @@ deletedPlaceholder: , pollOptionPlaceholder: , pollVotesWord: , - errorGeneric: + errorGeneric: , + reportFallback: }; var ACCESS_ADMIN = 9, ACCESS_MH = 8; @@ -390,8 +437,10 @@ var pollForm = document.getElementById('gchat_poll_form'); var pollQuestionInput = document.getElementById('gchat_poll_question'); var pollOptionsBox = document.getElementById('gchat_poll_options'); + var mentionList = document.getElementById('gchat_mention_list'); var myUid = parseInt(root.getAttribute('data-uid'), 10) || 0; + var myUsername = root.getAttribute('data-username') || ''; var lastId = 0; var unread = 0; var isOpen = false; @@ -407,6 +456,12 @@ var MAX_POLL_OPTIONS = 6; var EMOJI_LIST = ['😀','😂','😅','😊','😍','😎','🤔','😴','😭','😡','👍','👎','👏','🙏','💪','🔥','⭐','❤️','💯','🎉','⚔️','🛡️','🏰','🌾','🪵','⛏️','🧱','⏳','🐎','🏆']; + // Faza 3: stare pentru autocomplete la @mentiuni + var mentionMatches = []; + var mentionActiveIndex = -1; + var mentionTokenStart = -1; + var mentionTimer = null; + function fmtTime(unixTs) { var d = new Date(unixTs * 1000); function pad(n) { return (n < 10 ? '0' : '') + n; } @@ -587,6 +642,44 @@ return wrap; } + /** + * Faza 3: evidentiaza "@cuvant" in text (stil, nu neaparat un user real - + * verificare simpla, client-side, fara sa validam impotriva bazei de date + * la fiecare randare). Daca vreunul dintre ele e chiar username-ul + * vizualizatorului, intoarce true - renderMessage() adauga atunci un fundal + * pe tot randul, ca "notificare" vizuala (nu exista notificari separate, + * gen sunet/browser - in linie cu restul widget-ului, minimalist). + */ + function renderTextWithMentions(container, text) { + var re = /@([A-Za-z0-9_.\-]{2,32})/g; + var lastIndex = 0; + var match; + var mentionsMe = false; + + while ((match = re.exec(text)) !== null) { + if (match.index > lastIndex) { + container.appendChild(document.createTextNode(text.slice(lastIndex, match.index))); + } + + var mSpan = document.createElement('span'); + mSpan.className = 'gchat_mention'; + mSpan.textContent = '@' + match[1]; + container.appendChild(mSpan); + + if (myUsername && match[1].toLowerCase() === myUsername.toLowerCase()) { + mentionsMe = true; + } + + lastIndex = re.lastIndex; + } + + if (lastIndex < text.length) { + container.appendChild(document.createTextNode(text.slice(lastIndex))); + } + + return mentionsMe; + } + function renderMessage(row) { var line = document.createElement('div'); line.className = 'gchat_msg'; @@ -646,9 +739,34 @@ return line; } + // Faza 3: raport de lupta distribuit - link catre berichte.php, cu + // topicul raportului (deja in row.msg) ca text vizibil + if (row.type === 'report') { + var reportBlock = document.createElement('div'); + reportBlock.className = 'gchat_report_block'; + + var reportLink = document.createElement('a'); + reportLink.href = 'berichte.php?id=' + encodeURIComponent(row.report_id); + reportLink.target = '_blank'; + reportLink.rel = 'noopener'; + reportLink.className = 'gchat_report_link'; + reportLink.textContent = '\u2694\ufe0f ' + (row.msg || GCHAT_TXT.reportFallback); + reportBlock.appendChild(reportLink); + line.appendChild(reportBlock); + + var reportActions = document.createElement('span'); + reportActions.className = 'gchat_actions'; + appendModActionLinks(reportActions, row); + if (reportActions.childNodes.length) { line.appendChild(reportActions); } + + return line; + } + var text = document.createElement('span'); text.className = 'gchat_text'; - text.textContent = row.msg; + if (renderTextWithMentions(text, row.msg)) { + line.classList.add('gchat_msg_mentioned'); + } line.appendChild(text); var editedTag = document.createElement('span'); @@ -712,7 +830,11 @@ var textEl2 = line.querySelector('.gchat_text'); var editedTagEl2 = line.querySelector('.gchat_edited_tag'); - if (textEl2) { textEl2.textContent = row.msg; } + if (textEl2) { + textEl2.textContent = ''; + var mentionsMe2 = renderTextWithMentions(textEl2, row.msg); + line.classList.toggle('gchat_msg_mentioned', mentionsMe2); + } if (editedTagEl2 && parseInt(row.edited, 10) === 1) { editedTagEl2.style.display = 'inline'; } @@ -878,6 +1000,125 @@ if (field.setSelectionRange) { field.setSelectionRange(pos, pos); } } + // Faza 3: autocomplete la @mentiuni + function currentMentionToken() { + var pos = input.selectionStart; + var value = input.value; + if (pos == null) { return null; } + + var atPos = value.lastIndexOf('@', pos - 1); + if (atPos === -1) { return null; } + + // "@" trebuie sa fie inceput de cuvant (spatiu inainte, sau chiar + // inceputul mesajului) - altfel un email sau "cuvant@altceva" ar + // declansa gresit autocomplete-ul + if (atPos > 0 && !/\s/.test(value.charAt(atPos - 1))) { return null; } + + var token = value.slice(atPos + 1, pos); + if (/[\s@]/.test(token)) { return null; } + + return { start: atPos, query: token }; + } + + function closeMentionList() { + mentionList.style.display = 'none'; + mentionList.innerHTML = ''; + mentionMatches = []; + mentionActiveIndex = -1; + mentionTokenStart = -1; + } + + function renderMentionList() { + mentionList.innerHTML = ''; + + mentionMatches.forEach(function (u, idx) { + var item = document.createElement('div'); + item.className = 'gchat_mention_item' + (idx === mentionActiveIndex ? ' active' : ''); + item.textContent = u.username; + // mousedown (nu click) - trebuie sa apuce inaintea lui 'blur' de pe input + item.addEventListener('mousedown', function (e) { + e.preventDefault(); + selectMention(u.username); + }); + mentionList.appendChild(item); + }); + + mentionList.style.display = mentionMatches.length ? 'block' : 'none'; + } + + function selectMention(username) { + if (mentionTokenStart === -1) { return; } + + var pos = input.selectionStart; + var value = input.value; + var before = value.slice(0, mentionTokenStart); + var after = value.slice(pos); + var inserted = '@' + username + ' '; + + input.value = before + inserted + after; + var newPos = (before + inserted).length; + input.focus(); + if (input.setSelectionRange) { input.setSelectionRange(newPos, newPos); } + + closeMentionList(); + } + + input.addEventListener('input', function () { + var token = currentMentionToken(); + if (!token || token.query.length < 1) { + closeMentionList(); + return; + } + + mentionTokenStart = token.start; + + if (mentionTimer) { window.clearTimeout(mentionTimer); } + mentionTimer = window.setTimeout(function () { + fetch(AJAX_URL + '?f=gchat_search_users&q=' + encodeURIComponent(token.query), { credentials: 'same-origin' }) + .then(function (r) { return r.json(); }) + .then(function (data) { + if (!data || !data.ok) { return; } + + // token-ul s-ar fi putut schimba intre timp (cerere async) - + // verificam ca inca suntem pe acelasi "@cuvant" inainte sa + // afisam rezultate posibil deja irelevante + var stillSame = currentMentionToken(); + if (!stillSame || stillSame.start !== mentionTokenStart) { return; } + + mentionMatches = data.users || []; + mentionActiveIndex = mentionMatches.length ? 0 : -1; + renderMentionList(); + }) + .catch(function () { closeMentionList(); }); + }, 150); + }); + + input.addEventListener('keydown', function (e) { + if (!mentionMatches.length) { return; } + + if (e.key === 'ArrowDown') { + e.preventDefault(); + mentionActiveIndex = (mentionActiveIndex + 1) % mentionMatches.length; + renderMentionList(); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + mentionActiveIndex = (mentionActiveIndex - 1 + mentionMatches.length) % mentionMatches.length; + renderMentionList(); + } else if (e.key === 'Enter' || e.key === 'Tab') { + if (mentionActiveIndex >= 0) { + e.preventDefault(); + selectMention(mentionMatches[mentionActiveIndex].username); + } + } else if (e.key === 'Escape') { + closeMentionList(); + } + }); + + input.addEventListener('blur', function () { + // delay ca sa apuce mousedown-ul pe un item din lista inainte sa o inchidem + window.setTimeout(closeMentionList, 150); + }); + function addPollOptionRow(value) { if (pollOptionsBox.children.length >= MAX_POLL_OPTIONS) { return; } diff --git a/ajax.php b/ajax.php index 94320ede..2cb3cde7 100644 --- a/ajax.php +++ b/ajax.php @@ -248,6 +248,40 @@ switch(isset($_GET['f']) ? $_GET['f'] : '') { echo json_encode($database->voteGlobalChatPoll($uid, $_POST['pollId'] ?? 0, $_POST['option'] ?? -1)); break; + // Faza 3 (09.09.2026): autocomplete la @mentiuni - cautare useri dupa + // inceputul username-ului. GET, nu necesita alta permisiune decat login. + case 'gchat_search_users': + 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(['ok' => 1, 'users' => $database->searchGlobalChatUsers($_GET['q'] ?? '')]); + break; + + // Faza 3 (09.09.2026): distribuie un raport de lupta propriu in chat. + // Verificarea de tip+proprietate se face in Database::shareGlobalChatReport(). + case 'gchat_share_report': + 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->shareGlobalChatReport($uid, $_POST['id'] ?? 0)); + break; + case 'gchat_mute': case 'gchat_block': case 'gchat_unmute': diff --git a/berichte.php b/berichte.php index 4f7b01b2..61cd0ed5 100644 --- a/berichte.php +++ b/berichte.php @@ -157,6 +157,8 @@ if (isset($_GET['t']) && (int) $_GET['t'] === 3) { 0 && $_GET['aid'] == $session->alliance && $database->getNotice2($_GET['id'], 'ally') == $session->alliance) { $type = $database->getNotice2($_GET['id'], 'ntype'); @@ -170,9 +172,40 @@ if (isset($_GET['id'])) elseif($database->getNotice2(preg_replace("/[^a-zA-Z0-9_-]/", "", $_GET['id']), 'uid') == $session->uid) { $type = ($message->readingNotice['ntype'] == 9) ? $message->readingNotice['archive'] : $message->readingNotice['ntype']; + + // Faza 3 Global Chat (09.09.2026): doar proprietarul vazand propriul + // raport poate primi butonul de distribuire, si doar rapoarte de + // LUPTA - atac SAU aparare (cand te ataca altcineva pe tine, raportul + // TAU are tot un ntype din acelasi set - t=3/TZ_ATTACKS din + // Message::noticeType() include deja ambele roluri). Verificam ntype-ul + // ORIGINAL, nefiltrat (nu $type, care mai sus poate fi deja remapat pe + // arhiva). Tine sincron cu $shareableNtypes din + // Database::shareGlobalChatReport(). + $tzShareableNtypes = [1, 2, 3, 4, 5, 6, 7, 22, 23]; + $tzOwnNtype = $message->readingNotice['ntype'] ?? null; + if ($tzOwnNtype !== null && in_array((int) $tzOwnNtype, $tzShareableNtypes, true)) { + $tzShareEligible = true; + } + } + elseif ($database->isGlobalChatSharedReport($_GET['id'])) + { + // Faza 3 Global Chat (09.09.2026): raport distribuit explicit in + // chat-ul general de catre proprietarul lui - vizibil oricui e logat, + // indiferent de alianta. Acelasi filtru de siguranta ca la share-ul + // de alianta mai sus (10-17 raman ascunse), desi in practica + // Database::shareGlobalChatReport() deja refuza sa distribuie orice + // in afara de ntype 1-7 - dublam verificarea aici din prudenta. + $type = $database->getNotice2($_GET['id'], 'ntype'); + if ($type >= 10 && $type <= 17) unset($type); } if(isset($type)) include("Templates/Notice/".$message->getReportType($type).".tpl"); + + // Faza 3 Global Chat: butonul "Distribuie in chat", doar cand eligibil (vezi mai sus) + if ($tzShareEligible) { + include("Templates/GlobalChat/share_report_button.tpl"); + } + unset($type); } else include("Templates/Notice/all.tpl"); diff --git a/var/db/struct.sql b/var/db/struct.sql index 3caecfb0..02598564 100644 --- a/var/db/struct.sql +++ b/var/db/struct.sql @@ -595,6 +595,7 @@ CREATE TABLE IF NOT EXISTS `%PREFIX%chat_global` ( `msg` varchar(250) NOT NULL, `type` varchar(10) NOT NULL DEFAULT 'text', `poll_id` int(20) NULL DEFAULT NULL, + `report_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, @@ -658,6 +659,24 @@ CREATE TABLE IF NOT EXISTS `%PREFIX%chat_global_poll_votes` ( KEY `poll_id` (`poll_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +-- -------------------------------------------------------- + +-- +-- Faza 3 (09.09.2026): allowlist de rapoarte distribuite in chat-ul general. +-- NU atingem tabela `ndata` (folosita de tot sistemul de mesaje/rapoarte) - +-- in loc, un raport prezent aici e vizibil oricui e logat, indiferent de +-- alianta (vezi berichte.php, verificarea de acces). notice_id e PRIMARY +-- KEY: un raport se distribuie o singura data in allowlist (re-distribuirea +-- doar posteaza un mesaj nou in chat, vezi Database::shareGlobalChatReport). +-- + +CREATE TABLE IF NOT EXISTS `%PREFIX%chat_global_shared_reports` ( + `notice_id` int(11) NOT NULL, + `shared_by` int(11) NOT NULL, + `created` int(11) NOT NULL, + PRIMARY KEY (`notice_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + -- --------------------------------------------------------