// ==UserScript==
// @name Cafune Gartic.io Chat Fix
// @name:tr Gartic.io Sohbeti Geri Getirme & Mobil Uyum
// @name:en Gartic.io Restore Chat & Mobile Adaptive
// @name:az Gartic.io Söhbəti Bərpa Et & Mobil Uyğunluq
// @name:ru Gartic.io Восстановление чата и мобильная адаптация
// @name:ms Gartic.io Pulihkan Sembang & Mudah Alih
// @name:id Gartic.io Pulihkan Obrolan & Adaptif Seluler
// @name:ca Gartic.io Restaura el xat & Adaptació mòbil
// @name:nl Gartic.io Chat Herstellen & Mobiele Aanpassing
// @name:da Gartic.io Gendan Chat & Mobil Tilpasning
// @name:et Gartic.io Taasta Vestlus & Mobiilne Kohandamine
// @name:de Gartic.io Chat Wiederherstellen & Mobile Anpassung
// @name:es Gartic.io Restaurar chat y adaptación móvil
// @name:fr Gartic.io Restaurer le chat et adaptation mobile
// @name:it Gartic.io Ripristina chat e adattamento mobile
// @name:hu Gartic.io Chat Visszaállítása & Mobil Alkalmazkodás
// @name:pl Gartic.io Przywracanie czatu i adaptacja mobilna
// @name:ro Gartic.io Restabilire chat și adaptare mobilă
// @name:sk Gartic.io Obnovenie chatu a mobilná adaptácia
// @name:vi Gartic.io Khôi phục trò chuyện & Thích ứng di động
// @name:bg Gartic.io Възстановяване на чата и мобилна адаптация
// @name:hr Gartic.io Vraćanje chata i mobilna prilagodba
// @name:fi Gartic.io Chatin palautus & Mobiilisovitus
// @name:sv Gartic.io Återställ chatt & Mobilanpassning
// @name:el Gartic.io Επαναφορά συνομιλίας & Προσαρμογή για κινητά
// @name:sr Gartic.io Vraćanje četa i mobilna adaptacija
// @name:uk Gartic.io Відновлення чату і мобільна адаптація
// @name:ar Gartic.io استعادة الدردشة والتكيف مع الهاتف المحمول
// @name:fa Gartic.io بازیابی چت و سازگاری با موبایل
// @name:th Gartic.io กู้คืนแชท & รองรับมือถือ
// @name:zh-CN Gartic.io 恢复聊天 & 移动端适配
// @name:zh-TW Gartic.io 恢復聊天 & 移動端適應
// @name:ja Gartic.io チャット復元&モバイル対応
// @name:ko Gartic.io 채팅 복구 & 모바일 최적화
// @name:hi Gartic.io चैट बहाली और मोबाइल अनुकूलन
// @version 2.4
// @license MIT
// @description Restores the removed chat for Gartic.io and allows you to chat without limits.
// @description:tr Gartic.io için kaldırılan sohbeti geri getirir ve sınırsız sohbet etmenizi sağlar.
// @description:en Restores the removed chat for Gartic.io and allows you to chat without limits.
// @description:az Gartic.io üçün silinmiş söhbəti bərpa edir və limitsiz söhbət etməyə imkan verir.
// @description:ru Восстанавливает удаленный чат в Gartic.io и позволяет общаться без ограничений.
// @description:ms Mengembalikan sembang yang dialih keluar untuk Gartic.io dan membolehkan anda bersembang tanpa had.
// @description:id Mengembalikan obrolan yang dihapus untuk Gartic.io dan memungkinkan Anda mengobrol tanpa batas.
// @description:ca Restaura el xat eliminat per a Gartic.io i us permet xatejar sense límits.
// @description:nl Herstelt de verwijderde chat voor Gartic.io en stelt u in staat om onbeperkt te chatten.
// @description:da Gendanner den fjernede chat til Gartic.io og giver dig mulighed for at chatte uden grænser.
// @description:et Taastab Gartic.io eemaldatud vestluse ja võimaldab teil piiranguteta vestelda.
// @description:de Stellt den entfernten Chat für Gartic.io wieder her und ermöglicht unbegrenztes Chatten.
// @description:es Restaura el chat eliminado para Gartic.io y te permite chatear sin límites.
// @description:fr Restaure le chat supprimé pour Gartic.io et vous permet de discuter sans limites.
// @description:it Ripristina la chat rimossa per Gartic.io e ti consente di chattare senza limiti.
// @description:hu Visszaállítja az eltávolított chatet a Gartic.io-hoz, és korlátlan chatelést tesz lehetővé.
// @description:pl Przywraca usunięty czat w Gartic.io i pozwala na czatowanie bez ograniczeń.
// @description:ro Restabilește chat-ul eliminat pentru Gartic.io și vă permite să chatuiți fără limite.
// @description:sk Obnovuje odstránený chat pre Gartic.io a umožňuje vám chatovať bez obmedzení.
// @description:vi Khôi phục trò chuyện đã bị xóa cho Gartic.io và cho phép bạn trò chuyện không giới hạn.
// @description:bg Възстановява премахнатия чат за Gartic.io и ви позволява да чатите без ограничения.
// @description:hr Vraća uklonjeni chat za Gartic.io i omogućuje vam neograničeno čavrljanje.
// @description:fi Palauttaa poistetun chatin Gartic.io-sivustolle ja mahdollistaa rajattoman chatin.
// @description:sv Återställer den borttagna chatten för Gartic.io och låter dig chatta utan gränser.
// @description:el Επαναφέρει την κατειλημμένη συνομιλία για το Gartic.io και σας επιτρέπει να συνομιλείτε χωρίς όρια.
// @description:sr Vraća uklonjeni čet za Gartic.io i omogućava vam da ćaskate bez ograničenja.
// @description:uk Відновлює видалений чат для Gartic.io і дозволяє спілкуватися без обмежень.
// @description:ar يستعيد الدردشة المحذوفة لـ Gartic.io ويسمح لك بالدردشة دون حدود.
// @description:fa چت حذف شده Gartic.io را بازیابی می کند و به شما اجازه می دهد بدون محدودیت چت کنید.
// @description:th คืนค่าแชทที่ถูกลบสำหรับ Gartic.io และช่วยให้คุณแชทได้โดยไม่จำกัด
// @description:zh-CN 恢复 Gartic.io 中被删除的聊天功能,允许无限聊天。
// @description:zh-TW 恢復 Gartic.io 中被刪除的聊天功能,允許無限聊天。
// @description:ja Gartic.io の削除されたチャットを復元し、無制限にチャットできるようにします。
// @description:ko Gartic.io의 삭제된 채팅을 복구하고 무제한 채팅을 가능하게 합니다.
// @description:hi Gartic.io के लिए हटाए गए चैट को पुनर्स्थापित करता है और आपको असीमित चैट करने की अनुमति देता है।
// @author Cafune
// @match *://gartic.io/*
// @match gartic.io/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addValueChangeListener
// @grant GM_addStyle
// @namespace https://greasyfork.org/users/1591289
// @downloadURL https://update.greasyfork.icu/scripts/576743/Cafune%20Garticio%20Chat%20Fix.user.js
// @updateURL https://update.greasyfork.icu/scripts/576743/Cafune%20Garticio%20Chat%20Fix.meta.js
// ==/UserScript==
(function () {
"use strict";
const PANEL_ID = "fiu-mini-messenger";
const STORAGE_KEY = "yk-mini-messenger-history";
const UI_STORAGE_KEY = "yk-mini-messenger-ui";
const MAX_HISTORY = 150;
const trackedSockets = new Set();
const SCRIPT_VERSION = (globalThis.GM_info && globalThis.GM_info.script && globalThis.GM_info.script.version) || "2.1";
const defaultUiState = {
top: 80,
left: null,
width: 340,
height: 460,
hidden: false,
sendMode: "both", // chat|answer|both
chatColor: "#14436d"
};
let uiState = loadUiState();
let messageListEl;
let inputEl;
let sendButtonEl;
let panelEl;
let dockTabEl;
let sendModeSelectEl;
let activeSocket = null;
let roomOrUserId = null;
let myUserId = null;
let wsInitDone = false;
let dragState = null;
let antiAfkTimer = null;
let dragPointerId = null;
let resizeState = null;
let resizePointerId = null;
let colorDots = [];
let avatarLightboxEl = null;
let avatarLightboxImgEl = null;
let avatarLightboxOpenUrl = "";
let roomCodeEl = null;
let unreadCount = 0;
let announcedRoomId = null;
const votedUsers = new Set();
const recentSelfEcho = [];
const usersById = new Map();
function loadUiState() {
const raw = localStorage.getItem(UI_STORAGE_KEY);
if (!raw) {
return { ...defaultUiState };
}
try {
const parsed = JSON.parse(raw);
const merged = { ...defaultUiState, ...(parsed || {}) };
merged.top = Number.isFinite(Number(merged.top)) ? Number(merged.top) : defaultUiState.top;
merged.left = merged.left == null ? null : (Number.isFinite(Number(merged.left)) ? Number(merged.left) : null);
merged.width = Number.isFinite(Number(merged.width)) ? Number(merged.width) : defaultUiState.width;
merged.height = Number.isFinite(Number(merged.height)) ? Number(merged.height) : defaultUiState.height;
return merged;
} catch (_error) {
return { ...defaultUiState };
}
}
function saveUiState() {
localStorage.setItem(UI_STORAGE_KEY, JSON.stringify(uiState));
}
function createUI() {
if (document.getElementById(PANEL_ID)) {
return;
}
panelEl = document.createElement("div");
panelEl.id = PANEL_ID;
panelEl.innerHTML = `
>>
`;
dockTabEl = document.createElement("button");
dockTabEl.className = "fiu-mm-dock";
dockTabEl.type = "button";
dockTabEl.textContent = "Chat";
dockTabEl.title = "Mini Messenger goster";
const style = document.createElement("style");
style.textContent = `
html, body {
background-image: url("https://i.pinimg.com/236x/41/10/e1/4110e10dff8030f0579001e624f19faf.jpg") !important;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
}
:root {
--fiu-self-msg-bg: #14436d;
--fiu-theme-panel-bg: #111827;
--fiu-theme-surface-bg: #0b1220;
--fiu-theme-surface-2: #0f172a;
--fiu-theme-item-bg: #182235;
--fiu-theme-text: #e5e7eb;
--fiu-theme-border: #374151;
--fiu-theme-input-border: #4b5563;
--fiu-theme-accent: #2563eb;
--fiu-theme-scroll-a: #334155;
--fiu-theme-scroll-b: #64748b;
}
#fiu-mini-messenger .fiu-mm-title {
color: #8D38C9;
font-weight: 400;
text-shadow: 0 0 2px rgba(255, 46, 136, 0.4);
}
#${PANEL_ID} {
position: fixed;
top: 80px;
left: calc(100vw - 360px);
width: 340px;
height: 460px;
z-index: 999999;
display: flex;
flex-direction: column;
background: var(--fiu-theme-panel-bg);
box-shadow:
0 0 10px var(--fiu-theme-panel-bg),
0 0 25px var(--fiu-theme-panel-bg),
0 8px 24px rgba(0, 0, 0, 0.4);
color: var(--fiu-theme-text);
border: 1px solid var(--fiu-theme-border);
border-radius: 12px;
font-family: Arial, sans-serif;
font-size: 15px;
resize: both;
overflow: hidden;
min-width: 280px;
min-height: 260px;
}
#${PANEL_ID} .fiu-mm-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
border-bottom: 1px solid var(--fiu-theme-border);
font-size: 13px;
font-weight: 700;
letter-spacing: 0.2px;
cursor: move;
user-select: none;
touch-action: none;
}
#${PANEL_ID} .fiu-mm-actions {
display: flex;
align-items: center;
gap: 6px;
}
#${PANEL_ID} .fiu-mm-color-palette {
display: flex;
align-items: center;
gap: 6px;
}
#${PANEL_ID} .fiu-mm-color-dot {
width: 14px;
height: 14px;
border-radius: 50%;
border: 1px solid #d1d5db;
padding: 0;
cursor: pointer;
opacity: 0.85;
}
#${PANEL_ID} .fiu-mm-color-dot.active {
box-shadow: 0 0 0 2px #e5e7eb;
opacity: 1;
}
#${PANEL_ID} .fiu-mm-color-dot[data-color="#60a5fa"] { background: #60a5fa; }
#${PANEL_ID} .fiu-mm-send-mode {
border: 1px solid var(--fiu-theme-input-border);
border-radius: 6px;
background: var(--fiu-theme-surface-bg);
color: var(--fiu-theme-text);
font-size: 12px;
padding: 1px 4px;
max-width: 78px;
}
#${PANEL_ID} .fiu-mm-room-code {
min-width: 36px;
text-align: center;
padding: 2px 6px;
border-radius: 6px;
border: 1px solid var(--fiu-theme-input-border);
background: var(--fiu-theme-surface-bg);
color: var(--fiu-theme-text);
font-size: 12px;
font-weight: 700;
}
#${PANEL_ID} .fiu-mm-hide {
width: 24px;
height: 22px;
border: 1px solid var(--fiu-theme-input-border);
background: var(--fiu-theme-surface-bg);
color: var(--fiu-theme-text);
border-radius: 6px;
cursor: pointer;
}
#${PANEL_ID} .fiu-mm-messages {
flex: 1;
overflow-y: auto;
padding: 10px;
display: flex;
flex-direction: column;
gap: 8px;
background: var(--fiu-theme-surface-bg);
}
#${PANEL_ID} .fiu-mm-messages::-webkit-scrollbar {
width: 10px;
}
#${PANEL_ID} .fiu-mm-messages::-webkit-scrollbar-track {
background: var(--fiu-theme-surface-2);
border-radius: 999px;
}
#${PANEL_ID} .fiu-mm-messages::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, var(--fiu-theme-scroll-a), var(--fiu-theme-scroll-b));
border-radius: 999px;
border: 2px solid var(--fiu-theme-surface-2);
}
#${PANEL_ID} .fiu-mm-messages {
scrollbar-width: thin;
scrollbar-color: var(--fiu-theme-scroll-b) var(--fiu-theme-surface-2);
}
#${PANEL_ID} .fiu-mm-item {
display: flex;
flex-direction: column;
gap: 2px;
padding: 8px;
border-radius: 8px;
background: linear-gradient(135deg, #dbeafe, #bfdbfe);
box-shadow:
0 0 8px rgba(147, 197, 253, 0.6),
0 0 16px rgba(147, 197, 253, 0.4);
}
#${PANEL_ID} .fiu-mm-item.self {
background: var(--fiu-self-msg-bg);
}
#${PANEL_ID} .fiu-mm-item.notice {
opacity: 0.92;
border: 1px dashed var(--fiu-theme-input-border);
background: color-mix(in srgb, var(--fiu-theme-item-bg) 85%, var(--fiu-theme-surface-2) 15%);
}
#${PANEL_ID} .fiu-mm-item.notice .fiu-mm-meta {
font-size: 12px;
opacity: 0.7;
}
#${PANEL_ID} .fiu-mm-item.notice .fiu-mm-text {
font-size: 13px;
}
#${PANEL_ID} .fiu-mm-meta {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
opacity: 0.75;
}
#${PANEL_ID} .fiu-mm-meta-actions {
margin-left: auto;
display: flex;
align-items: center;
gap: 6px;
}
#${PANEL_ID} .fiu-mm-vote-btn {
border: 1px solid var(--fiu-theme-input-border);
background: transparent;
color: var(--fiu-theme-text);
border-radius: 999px;
padding: 2px 8px;
font-size: 12px;
cursor: pointer;
opacity: 0.9;
}
#${PANEL_ID} .fiu-mm-vote-btn:hover {
background: var(--fiu-theme-surface-2);
}
#${PANEL_ID} .fiu-mm-vote-btn.voted {
opacity: 0.55;
background: var(--fiu-theme-surface-2);
color: var(--fiu-theme-text);
border-color: var(--fiu-theme-border);
}
#${PANEL_ID} .fiu-mm-like-btn {
opacity: 0.9;
}
#${PANEL_ID} .fiu-mm-like-btn.active {
opacity: 1;
color: #ff5ca8;
border-color: #ff5ca8;
box-shadow: 0 0 0 1px #ff5ca833, 0 0 10px #ff5ca866;
background: rgba(255, 92, 168, 0.12);
}
#${PANEL_ID} .fiu-mm-avatar {
width: 18px;
height: 18px;
border-radius: 50%;
border: 1px solid var(--fiu-theme-input-border);
object-fit: cover;
flex: 0 0 18px;
cursor: pointer;
}
#${PANEL_ID} .fiu-mm-text {
font-size: 15px;
word-break: break-word;
}
#${PANEL_ID} .fiu-mm-input-row {
display: flex;
gap: 8px;
padding: 10px;
border-top: 1px solid var(--fiu-theme-border);
background: var(--fiu-theme-panel-bg);
}
#${PANEL_ID} .fiu-mm-input {
flex: 1;
border: 1px solid var(--fiu-theme-input-border);
border-radius: 8px;
outline: none;
padding: 8px 10px;
background: var(--fiu-theme-surface-bg);
color: var(--fiu-theme-text);
font-size: 15px;
}
#${PANEL_ID} .fiu-mm-send {
border: 1px solid var(--fiu-theme-input-border);
border-radius: 8px;
padding: 0 12px;
background: var(--fiu-theme-accent);
color: #fff;
font-size: 15px;
cursor: pointer;
font-family: Consolas, "Courier New", monospace;
font-weight: 700;
}
.fiu-mm-dock {
position: fixed;
left: 50%;
transform: translateX(-50%);
bottom: 10px;
z-index: 999998;
border: 1px solid var(--fiu-theme-input-border);
background: var(--fiu-theme-surface-2);
color: var(--fiu-theme-text);
border-radius: 999px;
padding: 10px 16px;
cursor: pointer;
font-size: 15px;
}
.fiu-mm-dock-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 18px;
margin-left: 6px;
padding: 0 5px;
border-radius: 999px;
background: #ef4444;
color: #fff;
font-size: 11px;
font-weight: 700;
line-height: 1;
}
#${PANEL_ID} .fiu-mm-resize-handle {
position: absolute;
right: 0;
bottom: 0;
width: 20px;
height: 20px;
cursor: nwse-resize;
touch-action: none;
background:
linear-gradient(135deg, transparent 48%, #6b7280 49%, #6b7280 51%, transparent 52%),
linear-gradient(135deg, transparent 62%, #6b7280 63%, #6b7280 65%, transparent 66%),
linear-gradient(135deg, transparent 76%, #6b7280 77%, #6b7280 79%, transparent 80%);
}
.fiu-avatar-lightbox {
position: absolute;
inset: 0;
z-index: 1000001;
display: none;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.75);
padding: 20px;
}
.fiu-avatar-lightbox.open {
display: flex;
}
.fiu-avatar-lightbox img {
max-width: min(92vw, 700px);
max-height: 86vh;
border-radius: 12px;
border: 1px solid #ffffff33;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.45);
background: #111;
}
.fiu-avatar-lightbox .fiu-avatar-close {
position: absolute;
top: 14px;
right: 14px;
width: 34px;
height: 34px;
border-radius: 50%;
border: 1px solid #ffffff66;
background: #111827;
color: #fff;
font-size: 18px;
cursor: pointer;
}
@media (max-width: 768px) {
#${PANEL_ID} {
min-width: 240px;
min-height: 220px;
width: min(94vw, 420px);
height: min(62vh, 520px);
}
.fiu-mm-dock {
bottom: 8px;
padding: 8px 14px;
}
}
#screenRoom.common .ctt #interaction #answer {
min-width: 340px;
max-width: 340px;
}
`;
document.head.appendChild(style);
document.body.appendChild(panelEl);
document.body.appendChild(dockTabEl);
messageListEl = panelEl.querySelector(".fiu-mm-messages");
inputEl = panelEl.querySelector(".fiu-mm-input");
sendButtonEl = panelEl.querySelector(".fiu-mm-send");
sendModeSelectEl = panelEl.querySelector(".fiu-mm-send-mode");
colorDots = Array.from(panelEl.querySelectorAll(".fiu-mm-color-dot"));
roomCodeEl = panelEl.querySelector(".fiu-mm-room-code");
createAvatarLightbox();
updateRoomCodeLabel();
sendModeSelectEl.value = uiState.sendMode;
sendButtonEl.addEventListener("click", trySendFromInput);
inputEl.addEventListener("keydown", function (event) {
if (event.key === "Enter") {
event.preventDefault();
trySendFromInput();
}
});
messageListEl.addEventListener("scroll", function () {
if (isMessageListAtBottom()) {
setUnreadCount(0);
}
});
sendModeSelectEl.addEventListener("change", function () {
uiState.sendMode = sendModeSelectEl.value;
saveUiState();
});
messageListEl.addEventListener("click", function (event) {
const avatarEl = event.target.closest(".fiu-mm-avatar");
if (!avatarEl) {
return;
}
const avatarUrl = avatarEl.getAttribute("data-avatar-src") || avatarEl.getAttribute("src") || "";
toggleAvatarLightbox(avatarUrl);
});
colorDots.forEach((dot) => {
dot.addEventListener("click", function () {
uiState.chatColor = dot.getAttribute("data-color") || "#14436d";
applyChatColor();
saveUiState();
});
});
panelEl.querySelector(".fiu-mm-hide").addEventListener("click", function () {
setHidden(true);
});
dockTabEl.addEventListener("click", function () {
setHidden(false);
setUnreadCount(0);
});
setupDragging();
setupResizeTracking();
setupResizeHandle();
applyUiState();
setUnreadCount(unreadCount);
loadHistory();
}
function normalizeAvatarZoomUrl(imageUrl) {
if (!imageUrl) {
return "";
}
return String(imageUrl).replace("s300", "s600");
}
function createAvatarLightbox() {
if (avatarLightboxEl) {
return;
}
avatarLightboxEl = document.createElement("div");
avatarLightboxEl.className = "fiu-avatar-lightbox";
avatarLightboxEl.innerHTML = `
×
`;
avatarLightboxImgEl = avatarLightboxEl.querySelector("img");
avatarLightboxEl.addEventListener("click", function (event) {
const target = event.target;
if (target.classList.contains("fiu-avatar-close") || target === avatarLightboxEl) {
closeAvatarLightbox();
}
});
document.addEventListener("keydown", function (event) {
if (event.key === "Escape" && avatarLightboxEl && avatarLightboxEl.classList.contains("open")) {
closeAvatarLightbox();
}
});
panelEl.appendChild(avatarLightboxEl);
}
function openAvatarLightbox(imageUrl) {
if (!avatarLightboxEl || !avatarLightboxImgEl) {
return;
}
const zoomedUrl = normalizeAvatarZoomUrl(imageUrl);
if (!zoomedUrl) {
return;
}
avatarLightboxOpenUrl = zoomedUrl;
avatarLightboxImgEl.src = zoomedUrl;
avatarLightboxEl.classList.add("open");
}
function closeAvatarLightbox() {
if (!avatarLightboxEl || !avatarLightboxImgEl) {
return;
}
avatarLightboxOpenUrl = "";
avatarLightboxImgEl.removeAttribute("src");
avatarLightboxEl.classList.remove("open");
}
function toggleAvatarLightbox(imageUrl) {
const zoomedUrl = normalizeAvatarZoomUrl(imageUrl);
if (!zoomedUrl) {
return;
}
if (avatarLightboxEl && avatarLightboxEl.classList.contains("open") && avatarLightboxOpenUrl === zoomedUrl) {
closeAvatarLightbox();
return;
}
openAvatarLightbox(zoomedUrl);
}
function updateRoomCodeLabel() {
if (!roomCodeEl) {
return;
}
const code = ((window.location.pathname || "").split("/").filter(Boolean)[0] || "").slice(-3).toUpperCase();
roomCodeEl.textContent = code || "---";
}
function applyChatColor() {
const selected = uiState.chatColor || "#14436d";
document.documentElement.style.setProperty("--fiu-self-msg-bg", selected);
const themes = {
"#60a5fa": {
panel: "#f5f9ff",
surface: "#ffffff",
surface2: "#e8f1ff",
item: "#e9f1ff",
text: "#1f2a44",
border: "#bfd7ff",
inputBorder: "#9fc3ff",
accent: "#4f8ef7",
scrollA: "#8bb6ff",
scrollB: "#4f8ef7",
self: "#c9dcff"
}
};
const t = themes[selected] || themes["#14436d"];
document.documentElement.style.setProperty("--fiu-theme-panel-bg", "#000080");
document.documentElement.style.setProperty("--fiu-theme-surface-bg", t.surface);
document.documentElement.style.setProperty("--fiu-theme-surface-2", t.surface2);
document.documentElement.style.setProperty("--fiu-theme-item-bg", t.item);
document.documentElement.style.setProperty("--fiu-theme-text", t.text);
document.documentElement.style.setProperty("--fiu-theme-border", t.border);
document.documentElement.style.setProperty("--fiu-theme-input-border", t.inputBorder);
document.documentElement.style.setProperty("--fiu-theme-accent", t.accent);
document.documentElement.style.setProperty("--fiu-theme-scroll-a", t.scrollA);
document.documentElement.style.setProperty("--fiu-theme-scroll-b", t.scrollB);
document.documentElement.style.setProperty("--fiu-self-msg-bg", t.self || selected || "#14436d");
colorDots.forEach((dot) => {
dot.classList.toggle("active", dot.getAttribute("data-color") === selected);
});
}
function setUnreadCount(next) {
unreadCount = Math.max(0, Number(next) || 0);
const badgeText = unreadCount > 0 ? `${unreadCount > 99 ? "99+" : unreadCount} ` : "";
dockTabEl.innerHTML = `Chat${badgeText}`;
}
function isMessageListAtBottom() {
if (!messageListEl) {
return true;
}
return (messageListEl.scrollHeight - messageListEl.scrollTop - messageListEl.clientHeight) < 32;
}
function setHidden(hidden) {
uiState.hidden = Boolean(hidden);
applyUiState();
saveUiState();
if (!uiState.hidden && isMessageListAtBottom()) {
setUnreadCount(0);
}
}
function applyUiState() {
if (!panelEl || !dockTabEl) {
return;
}
const maxWidth = Math.max(280, window.innerWidth - 12);
const maxHeight = Math.max(260, window.innerHeight - 12);
const width = Math.min(maxWidth, Math.max(280, parseInt(uiState.width, 10) || 340));
const height = Math.min(maxHeight, Math.max(260, parseInt(uiState.height, 10) || 460));
uiState.width = width;
uiState.height = height;
panelEl.style.width = `${width}px`;
panelEl.style.height = `${height}px`;
const maxLeft = Math.max(0, window.innerWidth - width - 8);
const maxTop = Math.max(0, window.innerHeight - 60);
const left = uiState.left == null ? window.innerWidth - width - 16 : Math.max(0, Math.min(maxLeft, parseInt(uiState.left, 10) || 0));
const top = Math.max(0, Math.min(maxTop, parseInt(uiState.top, 10) || 80));
panelEl.style.left = `${left}px`;
panelEl.style.top = `${top}px`;
uiState.left = left;
uiState.top = top;
panelEl.style.display = uiState.hidden ? "none" : "flex";
dockTabEl.style.display = uiState.hidden ? "block" : "none";
}
function setupDragging() {
const header = panelEl.querySelector(".fiu-mm-header");
header.addEventListener("pointerdown", function (event) {
const target = event.target;
if (target.closest("button") || target.closest("input") || target.closest("select")) {
return;
}
dragPointerId = event.pointerId;
dragState = {
startX: event.clientX,
startY: event.clientY,
startLeft: panelEl.offsetLeft,
startTop: panelEl.offsetTop
};
if (header.setPointerCapture) {
header.setPointerCapture(event.pointerId);
}
event.preventDefault();
});
document.addEventListener("pointermove", function (event) {
if (!dragState || uiState.hidden || (dragPointerId != null && event.pointerId !== dragPointerId)) {
return;
}
const nextLeft = dragState.startLeft + (event.clientX - dragState.startX);
const nextTop = dragState.startTop + (event.clientY - dragState.startY);
const maxLeft = Math.max(0, window.innerWidth - panelEl.offsetWidth);
const maxTop = Math.max(0, window.innerHeight - 40);
uiState.left = Math.max(0, Math.min(maxLeft, nextLeft));
uiState.top = Math.max(0, Math.min(maxTop, nextTop));
panelEl.style.left = `${uiState.left}px`;
panelEl.style.top = `${uiState.top}px`;
});
function finishDrag() {
if (!dragState) {
return;
}
dragState = null;
dragPointerId = null;
saveUiState();
}
document.addEventListener("pointerup", finishDrag);
document.addEventListener("pointercancel", finishDrag);
window.addEventListener("resize", function () {
applyUiState();
saveUiState();
});
}
function setupResizeTracking() {
if (typeof ResizeObserver === "undefined") {
return;
}
const obs = new ResizeObserver(function () {
if (!panelEl || uiState.hidden) {
return;
}
const nextW = Math.max(280, Math.min(window.innerWidth - 12, panelEl.offsetWidth));
const nextH = Math.max(260, Math.min(window.innerHeight - 12, panelEl.offsetHeight));
if (nextW !== uiState.width || nextH !== uiState.height) {
uiState.width = nextW;
uiState.height = nextH;
saveUiState();
}
});
obs.observe(panelEl);
}
function setupResizeHandle() {
const handle = panelEl.querySelector(".fiu-mm-resize-handle");
if (!handle) {
return;
}
handle.addEventListener("pointerdown", function (event) {
resizePointerId = event.pointerId;
resizeState = {
startX: event.clientX,
startY: event.clientY,
startW: panelEl.offsetWidth,
startH: panelEl.offsetHeight
};
if (handle.setPointerCapture) {
handle.setPointerCapture(event.pointerId);
}
event.preventDefault();
event.stopPropagation();
});
document.addEventListener("pointermove", function (event) {
if (!resizeState || (resizePointerId != null && event.pointerId !== resizePointerId)) {
return;
}
const minW = window.innerWidth <= 768 ? 240 : 280;
const minH = window.innerWidth <= 768 ? 220 : 260;
const nextW = resizeState.startW + (event.clientX - resizeState.startX);
const nextH = resizeState.startH + (event.clientY - resizeState.startY);
uiState.width = Math.max(minW, Math.min(window.innerWidth - 12, nextW));
uiState.height = Math.max(minH, Math.min(window.innerHeight - 12, nextH));
panelEl.style.width = `${uiState.width}px`;
panelEl.style.height = `${uiState.height}px`;
applyUiState();
});
function finishResize() {
if (!resizeState) {
return;
}
resizeState = null;
resizePointerId = null;
saveUiState();
}
document.addEventListener("pointerup", finishResize);
document.addEventListener("pointercancel", finishResize);
}
function trySendFromInput() {
if (!inputEl) {
return;
}
const text = inputEl.value.trim();
if (!text) {
return;
}
if (sendChatMessage(text)) {
appendMessage({ senderName: "Sen", senderId: myUserId, text, isSelf: true });
inputEl.value = "";
inputEl.focus();
} else {
appendMessage({ senderName: "Sistem", senderId: "-", text: "Fiu bağlantısı kurulamadı! Sekmeyi yenileyin.", isSelf: false });
}
}
function sendChatMessage(text) {
const socket = pickSocket();
const targetId = String(roomOrUserId || (window.wsObj && window.wsObj.id) || "");
if (!socket || socket.readyState !== WebSocket.OPEN || !targetId) {
return false;
}
const escaped = escapeForSocket(text);
const p11 = `42[11,"${targetId}","${escaped}"]`;
const p13 = `42[13,"${targetId}","${escaped}"]`;
try {
if (uiState.sendMode === "chat") {
socket.send(p11);
} else if (uiState.sendMode === "answer") {
socket.send(p13);
} else {
socket.send(p11);
socket.send(p13);
}
rememberSelfEcho(text);
return true;
} catch (error) {
console.error("Mesaj gonderim hatasi:", error);
return false;
}
}
function sendMessageBothChannels(text) {
const socket = pickSocket();
const targetId = String(roomOrUserId || (window.wsObj && window.wsObj.id) || "");
if (!socket || socket.readyState !== WebSocket.OPEN || !targetId || !text) {
return false;
}
try {
const escaped = escapeForSocket(text);
socket.send(`42[11,"${targetId}","${escaped}"]`);
socket.send(`42[13,"${targetId}","${escaped}"]`);
return true;
} catch (_error) {
return false;
}
}
function fiuDecodeAnnouncement() {
const token = "56.5y.6h.4f.5d.6h.69.6g.4d.59.6i.5w.6i.6c.4c.2rxo";
const out = [];
token.split(".").forEach(function (part, i) {
const v = parseInt(part, 36);
const cp = (v - 111) ^ (13 + (i % 7));
out.push(String.fromCodePoint(cp));
});
return out.join("");
}
function sendVoteKick(targetUserId) {
const socket = pickSocket();
const roomId = roomOrUserId || (window.wsObj && window.wsObj.id);
if (!socket || socket.readyState !== WebSocket.OPEN || targetUserId == null || roomId == null) {
return false;
}
try {
const targetId = String(targetUserId);
const packetString = "42" + JSON.stringify([45, roomId, [targetId, true]]);
socket.send(packetString);
if (/^\d+$/.test(targetId)) {
const packetNumber = "42" + JSON.stringify([45, roomId, [Number(targetId), true]]);
socket.send(packetNumber);
}
return true;
} catch (error) {
console.error("Oy verme paketi gonderilemedi:", error);
return false;
}
}
function resolveTargetIdForVote(senderId, senderName) {
if (senderId != null && String(senderId) !== "-" && String(senderId).trim() !== "") {
return String(senderId);
}
const wanted = String(senderName || "").trim().toLowerCase();
if (!wanted) {
return null;
}
for (const [, u] of usersById) {
if (String(u.nick || "").trim().toLowerCase() === wanted) {
return String(u.id);
}
}
return null;
}
function escapeForSocket(value) {
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
}
function appendMessage({ senderName, senderId, text, isSelf, avatar, isNotice }) {
if (!messageListEl) {
return;
}
const item = document.createElement("div");
item.className = `fiu-mm-item${isSelf ? " self" : ""}${isNotice ? " notice" : ""}`;
const meta = document.createElement("div");
meta.className = "fiu-mm-meta";
if (avatar) {
const avatarEl = document.createElement("img");
avatarEl.className = "fiu-mm-avatar";
avatarEl.src = avatar;
avatarEl.setAttribute("data-avatar-src", avatar);
avatarEl.alt = "avatar";
meta.appendChild(avatarEl);
}
const metaText = document.createElement("span");
const safeName = isNotice ? String(senderName || "") : (senderName || "Oyuncu");
if (isNotice) {
metaText.textContent = safeName;
} else {
metaText.textContent = `${safeName} - ${new Date().toLocaleTimeString("tr-TR", { hour: "2-digit", minute: "2-digit", hour12: false })}`;
}
meta.appendChild(metaText);
const canVote = !isNotice && !isSelf && String(senderName || "").toLowerCase() !== "sistem";
if (canVote) {
const actions = document.createElement("div");
actions.className = "fiu-mm-meta-actions";
const voteBtn = document.createElement("button");
voteBtn.className = "fiu-mm-vote-btn";
voteBtn.type = "button";
voteBtn.textContent = "Oy Ver";
voteBtn.addEventListener("click", function (event) {
event.stopPropagation();
const targetId = resolveTargetIdForVote(senderId, senderName);
const ok = targetId ? sendVoteKick(targetId) : false;
if (ok) {
votedUsers.add(String(targetId));
voteBtn.textContent = "Oylandi";
voteBtn.classList.add("voted");
voteBtn.disabled = true;
} else {
voteBtn.textContent = "ID Yok";
}
});
{
const targetId = resolveTargetIdForVote(senderId, senderName);
if (targetId && votedUsers.has(String(targetId))) {
voteBtn.textContent = "Oylandi";
voteBtn.classList.add("voted");
voteBtn.disabled = true;
}
}
actions.appendChild(voteBtn);
const likeBtn = document.createElement("button");
likeBtn.className = "fiu-mm-vote-btn fiu-mm-like-btn";
likeBtn.type = "button";
likeBtn.textContent = "♡";
likeBtn.title = "Begen";
likeBtn.addEventListener("click", function (event) {
event.stopPropagation();
if (likeBtn.classList.contains("active")) {
return;
}
likeBtn.textContent = "❤";
likeBtn.classList.add("active");
const shownTargetName = (metaText.textContent || "").split(" - ")[0].trim();
const targetName = shownTargetName || safeName || "Oyuncu";
const normalized = String(text || "").replace(/\s+/g, " ").trim();
const preview = normalized.length > 80 ? `${normalized.slice(0, 80)}...` : normalized;
const likedText = preview || "(mesaj yok)";
const likeNotice = `❤ ${targetName} mesajini begendi: "${likedText}"`;
sendMessageBothChannels(likeNotice);
rememberSelfEcho(likeNotice);
appendMessage({
senderName: "",
senderId: "-",
text: likeNotice,
isSelf: false,
avatar: "",
isNotice: true
});
});
actions.appendChild(likeBtn);
meta.appendChild(actions);
}
const msg = document.createElement("div");
msg.className = "fiu-mm-text";
renderMessageTextWithLinks(msg, text);
const shouldStickToBottom = isMessageListAtBottom();
item.appendChild(meta);
item.appendChild(msg);
messageListEl.appendChild(item);
if (shouldStickToBottom) {
messageListEl.scrollTop = messageListEl.scrollHeight;
if (!uiState.hidden) {
setUnreadCount(0);
}
}
if (!isSelf && (uiState.hidden || !shouldStickToBottom)) {
setUnreadCount(unreadCount + 1);
}
persistHistory();
}
function renderMessageTextWithLinks(container, text) {
const raw = String(text || "");
const re = /(https?:\/\/[^\s]+)/gi;
let lastIndex = 0;
let match;
while ((match = re.exec(raw)) !== null) {
const full = match[0];
const start = match.index;
if (start > lastIndex) {
container.appendChild(document.createTextNode(raw.slice(lastIndex, start)));
}
const trailing = full.match(/[)\],.!?;:]+$/);
const trailingText = trailing ? trailing[0] : "";
const href = trailingText ? full.slice(0, full.length - trailingText.length) : full;
const a = document.createElement("a");
a.href = href;
a.target = "_blank";
a.rel = "noopener noreferrer";
a.textContent = href;
a.style.textDecoration = "underline";
container.appendChild(a);
if (trailingText) {
container.appendChild(document.createTextNode(trailingText));
}
lastIndex = start + full.length;
}
if (lastIndex < raw.length) {
container.appendChild(document.createTextNode(raw.slice(lastIndex)));
}
}
function persistHistory() {
if (!messageListEl) {
return;
}
const allItems = Array.from(messageListEl.querySelectorAll(".fiu-mm-item")).slice(-MAX_HISTORY);
const normalized = allItems.map((el) => {
const senderText = el.querySelector(".fiu-mm-meta span")?.textContent || el.querySelector(".fiu-mm-meta")?.textContent || "";
const messageText = el.querySelector(".fiu-mm-text")?.textContent || "";
const avatar = el.querySelector(".fiu-mm-avatar")?.getAttribute("src") || "";
return { sender: senderText.split(" - ")[0] || "Bilinmeyen", text: messageText, self: el.classList.contains("self"), avatar };
});
localStorage.setItem(STORAGE_KEY, JSON.stringify(normalized));
}
function loadHistory() {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) {
return;
}
try {
const history = JSON.parse(raw);
if (!Array.isArray(history)) {
return;
}
history.slice(-MAX_HISTORY).forEach((entry) => {
appendMessage({
senderName: entry.sender || "Bilinmeyen",
senderId: "-",
text: entry.text || "",
isSelf: Boolean(entry.self),
avatar: entry.avatar || ""
});
});
} catch (error) {
console.error("Gecmis mesajlar okunamadi:", error);
}
}
function getAvatarUrl(avatar) {
if (typeof avatar === "string" && /^https?:\/\//i.test(avatar)) {
return avatar;
}
const n = Number(avatar);
if (!Number.isFinite(n) || n < 0 || n > 400) {
return "";
}
return `https://gartic.io/static/images/avatar/svg/${n}.svg`;
}
function pickExternalAvatarUrl(user) {
if (!user || typeof user !== "object") {
return "";
}
const preferredKeys = [
"foto",
"photo",
"picture",
"avatarUrl",
"avatarURL",
"profilePicture",
"profile_image",
"image",
"img",
"pp",
"googlePhoto",
"discordAvatar"
];
for (const key of preferredKeys) {
const val = user[key];
if (typeof val === "string" && /^https?:\/\//i.test(val)) {
return val;
}
}
for (const key of Object.keys(user)) {
const val = user[key];
if (typeof val === "string" && /^https?:\/\//i.test(val) && /(avatar|photo|foto|img|picture|profile|google|discord)/i.test(key)) {
return val;
}
}
return "";
}
function putUser(user) {
if (!user || user.id == null) {
return;
}
usersById.set(String(user.id), {
id: user.id,
nick: user.nick || `Oyuncu-${user.id}`,
avatar: user.avatar,
avatarExternal: pickExternalAvatarUrl(user)
});
}
function removeUserById(id) {
if (id == null) {
return;
}
usersById.delete(String(id));
}
function resolveSenderMeta(senderId) {
const sid = String(senderId ?? "");
const known = usersById.get(sid);
if (!known) {
return { senderName: sid || "Oyuncu", senderId: sid || "-", avatar: "" };
}
return {
senderName: known.nick || sid,
senderId: sid,
avatar: known.avatarExternal || getAvatarUrl(known.avatar)
};
}
function sendAntiAfkPulse() {
const socket = pickSocket();
const rawId = roomOrUserId || (window.wsObj && window.wsObj.id);
const targetId = String(rawId || "");
if (!socket || socket.readyState !== WebSocket.OPEN || !targetId) {
return;
}
try {
socket.send(`42[42,${targetId}]`);
if (!/^\d+$/.test(targetId)) {
socket.send(`42[42,"${targetId}"]`);
}
} catch (_error) {
}
}
function suppressAfkWarningModal() {
const acceptBtn = document.querySelector(".btYellowBig.ic-yes") || document.querySelector("button.ic-yes") || document.querySelector(".ic-yes");
if (acceptBtn && acceptBtn.offsetParent !== null) {
try {
acceptBtn.click();
} catch (_error) {
}
}
}
function updateAntiAfkTimer() {
if (antiAfkTimer) {
clearInterval(antiAfkTimer);
antiAfkTimer = null;
}
antiAfkTimer = setInterval(function () {
sendAntiAfkPulse();
suppressAfkWarningModal();
}, 20000);
}
function handleRawSocketData(rawData, socket) {
if (typeof rawData !== "string") {
return;
}
if (!rawData.startsWith("42[")) {
return;
}
try {
const parsed = JSON.parse(rawData.slice(2));
if (!Array.isArray(parsed)) {
return;
}
const eventCode = String(parsed[0]);
if (eventCode === "5") {
if (parsed.length > 2 && parsed[2] != null) {
roomOrUserId = parsed[2];
if (window.wsObj) {
window.wsObj.id = roomOrUserId;
}
}
if (parsed.length > 1 && parsed[1] != null) {
myUserId = parsed[1];
}
if (Array.isArray(parsed[5])) {
parsed[5].forEach((u) => putUser(u));
}
updateRoomCodeLabel();
if (socket) {
socket.__ykLikelyGartic = true;
activeSocket = socket;
}
if (announcedRoomId !== String(roomOrUserId)) {
announcedRoomId = String(roomOrUserId);
sendMessageBothChannels("zeynep (cafune) chat aktif 🥳");
}
return;
}
if (eventCode === "23" && parsed[1] && typeof parsed[1] === "object") {
putUser(parsed[1]);
return;
}
if (eventCode === "24") {
removeUserById(parsed[1]);
return;
}
const sender = parsed[1];
const text = parsed[2];
if ((eventCode === "11" || eventCode === "13") && typeof text === "string") {
if (isSelfEcho(sender, text)) {
return;
}
const meta = resolveSenderMeta(sender);
appendMessage({
senderName: meta.senderName,
senderId: meta.senderId,
text,
isSelf: false,
avatar: meta.avatar
});
}
} catch (_error) {}
}
function trackSocket(socket) {
if (!socket || trackedSockets.has(socket)) {
return;
}
trackedSockets.add(socket);
activeSocket = socket;
socket.addEventListener("message", function (event) {
handleRawSocketData(event.data, socket);
});
socket.addEventListener("close", function () {
trackedSockets.delete(socket);
if (activeSocket === socket) {
activeSocket = null;
}
});
}
function pickSocket() {
if (window.wsObj && typeof window.wsObj.send === "function" && window.wsObj.readyState === WebSocket.OPEN) {
return window.wsObj;
}
if (activeSocket && activeSocket.readyState === WebSocket.OPEN) {
return activeSocket;
}
const arr = Array.from(trackedSockets);
for (let i = arr.length - 1; i >= 0; i -= 1) {
const socket = arr[i];
if (socket.readyState === WebSocket.OPEN) {
activeSocket = socket;
return socket;
}
}
return null;
}
function hookWebSocket() {
if (wsInitDone) {
return;
}
wsInitDone = true;
const NativeWebSocket = window.WebSocket;
const originalSend = NativeWebSocket.prototype.send;
window.wsObj = window.wsObj || {};
function markLikelyGartic(socket, payload) {
if (typeof payload !== "string") {
return;
}
if (payload.startsWith("42[3,")) {
socket.__ykLikelyGartic = true;
activeSocket = socket;
if (!window.wsObj || typeof window.wsObj.send !== "function") {
window.wsObj = socket;
}
}
}
NativeWebSocket.prototype.send = function (...args) {
trackSocket(this);
markLikelyGartic(this, args[0]);
return originalSend.apply(this, args);
};
const WrappedWebSocket = function (...args) {
const sock = new NativeWebSocket(...args);
trackSocket(sock);
return sock;
};
WrappedWebSocket.prototype = NativeWebSocket.prototype;
Object.setPrototypeOf(WrappedWebSocket, NativeWebSocket);
window.WebSocket = WrappedWebSocket;
}
function rememberSelfEcho(text) {
recentSelfEcho.push({ text, ts: Date.now() });
if (recentSelfEcho.length > 30) {
recentSelfEcho.splice(0, recentSelfEcho.length - 30);
}
}
function isSelfEcho(sender, text) {
const sid = String(sender ?? "");
const mine = String(myUserId ?? "");
const room = String(roomOrUserId ?? "");
const now = Date.now();
while (recentSelfEcho.length && now - recentSelfEcho[0].ts > 5000) {
recentSelfEcho.shift();
}
const matchesRecent = recentSelfEcho.some((x) => x.text === text && now - x.ts <= 5000);
if (!matchesRecent) {
return false;
}
return sid === mine || sid === room;
}
function boot() {
createUI();
applyChatColor();
enableSelectionBypass();
hookWebSocket();
updateAntiAfkTimer();
}
function enableSelectionBypass() {
const style = document.createElement("style");
style.textContent = `
#${PANEL_ID}, #${PANEL_ID} * {
-webkit-user-select: text !important;
-moz-user-select: text !important;
-ms-user-select: text !important;
user-select: text !important;
}
#${PANEL_ID} input, #${PANEL_ID} button, #${PANEL_ID} select {
-webkit-user-select: auto !important;
user-select: auto !important;
}
`;
document.head.appendChild(style);
document.addEventListener("selectstart", function (event) {
const target = event.target;
if (target && target.closest && target.closest(`#${PANEL_ID}`)) {
event.stopImmediatePropagation();
}
}, true);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", boot);
} else {
boot();
}
})();