mirror of
https://github.com/Shadowss/TravianZ.git
synced 2026-09-10 04:27:14 +00:00
Add Global Chat Options
Add Global Chat Options 1. emoji 2. Poll 3. Delete message 4. Edit message
This commit is contained in:
@@ -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'");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.');
|
||||
|
||||
@@ -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.');
|
||||
|
||||
+550
-28
@@ -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 @@
|
||||
|
||||
<div id="gchat_notice" style="display:none"></div>
|
||||
|
||||
<div id="gchat_emoji_panel" style="display:none"></div>
|
||||
|
||||
<div id="gchat_poll_form" style="display:none">
|
||||
<input id="gchat_poll_question" type="text" maxlength="200" placeholder="<?php echo GCHAT_POLL_QUESTION_PLACEHOLDER; ?>"/>
|
||||
<div id="gchat_poll_options"></div>
|
||||
<div class="gchat_poll_form_actions">
|
||||
<button id="gchat_poll_add_option" type="button"><?php echo GCHAT_POLL_ADD_OPTION; ?></button>
|
||||
<button id="gchat_poll_submit" type="button"><?php echo GCHAT_POLL_CREATE; ?></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="gchat_form" autocomplete="off">
|
||||
<button id="gchat_emoji_btn" type="button" class="gchat_toolbar_btn" title="<?php echo GCHAT_EMOJI_TITLE; ?>">🙂</button>
|
||||
<button id="gchat_poll_btn" type="button" class="gchat_toolbar_btn" title="<?php echo GCHAT_POLL_TITLE; ?>">📊</button>
|
||||
<input id="gchat_input" type="text" maxlength="250" placeholder="<?php echo GCHAT_PLACEHOLDER; ?>"/>
|
||||
<button id="gchat_send_btn" type="submit" title="<?php echo GCHAT_SEND; ?>">►</button>
|
||||
</form>
|
||||
@@ -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: <?php echo json_encode(GCHAT_BLOCK); ?>,
|
||||
unmute: <?php echo json_encode(GCHAT_UNMUTE); ?>,
|
||||
adminBadge: <?php echo json_encode(GCHAT_ADMIN_BADGE); ?>,
|
||||
mhBadge: <?php echo json_encode(GCHAT_MH_BADGE); ?>
|
||||
mhBadge: <?php echo json_encode(GCHAT_MH_BADGE); ?>,
|
||||
edit: <?php echo json_encode(GCHAT_EDIT); ?>,
|
||||
save: <?php echo json_encode(GCHAT_SAVE); ?>,
|
||||
cancel: <?php echo json_encode(GCHAT_CANCEL); ?>,
|
||||
editedTag: <?php echo json_encode(GCHAT_EDITED_TAG); ?>,
|
||||
deleteMsg: <?php echo json_encode(GCHAT_DELETE); ?>,
|
||||
confirmDelete: <?php echo json_encode(GCHAT_CONFIRM_DELETE); ?>,
|
||||
deletedPlaceholder: <?php echo json_encode(GCHAT_DELETED_PLACEHOLDER); ?>,
|
||||
pollOptionPlaceholder: <?php echo json_encode(GCHAT_POLL_OPTION_PLACEHOLDER); ?>,
|
||||
pollVotesWord: <?php echo json_encode(GCHAT_POLL_VOTES_WORD); ?>,
|
||||
errorGeneric: <?php echo json_encode(GCHAT_ERROR_GENERIC); ?>
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -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':
|
||||
|
||||
+50
-2
@@ -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;
|
||||
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
|
||||
Reference in New Issue
Block a user