From 77790a279c08fcc1b17abcf1c4c49ba02c1dc5ea Mon Sep 17 00:00:00 2001 From: Timo <6156589+Shik3i@users.noreply.github.com> Date: Sun, 26 Jul 2026 01:02:20 +0200 Subject: [PATCH] fix(chat): preserve mixed-version compatibility --- docs/CHAT.md | 13 ++++++ docs/PROTOCOL.md | 19 ++++++-- extension/background.js | 31 +++++++++---- extension/chat-overlay-contract.test.mjs | 14 ++++++ extension/chat-overlay.js | 7 +-- extension/locales/de.json | 2 + extension/locales/en.json | 2 + extension/locales/es.json | 2 + extension/locales/fr.json | 2 + extension/locales/it.json | 2 + extension/locales/ja.json | 2 + extension/locales/ko.json | 2 + extension/locales/nl.json | 2 + extension/locales/pl.json | 2 + extension/locales/pt-BR.json | 2 + extension/locales/pt.json | 2 + extension/locales/ru.json | 2 + extension/locales/tr.json | 2 + extension/locales/uk.json | 2 + extension/locales/zh.json | 2 + extension/popup.html | 7 +++ extension/popup.js | 10 +++- scripts/test-server-ws.mjs | 39 +++++++++++++++- server/index.js | 40 +++++++++++++++- server/package-lock.json | 58 ++++++++++++++++++------ shared/constants.js | 3 +- 26 files changed, 236 insertions(+), 35 deletions(-) diff --git a/docs/CHAT.md b/docs/CHAT.md index e6ad00b..4b0f6b4 100644 --- a/docs/CHAT.md +++ b/docs/CHAT.md @@ -67,6 +67,19 @@ the stamped value as AAD. - It follows all eucalyptus, cyber, and graphite light/dark theme combinations. - Without a key, the panel stays closed and a disabled chat control explains that a current invite link is required. +- Chat display is a local option and defaults to off. Enabling or disabling it never + deletes the room chat secret, so it can be enabled later without creating a room. - Without the relay `chat` capability, no chat control is shown. +## Mixed-version rollout + +- New extensions announce `chat-v1` in `join_room.clientCapabilities`. +- Old non-chat extensions omit the optional field and receive no `chat_message` + events. Their playback and room protocol remains unchanged. +- The first chat beta omitted the capability. When it sends one valid v1 chat + frame, the relay treats that socket as chat-capable for the rest of the + connection. +- New extensions accept the first beta's unversioned server `chat` capability as + well as `chat-v1`, so a server-first or extension-first rollout degrades safely. + Peer removal and role management are outside chat scope. diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md index 4a18209..9d119eb 100644 --- a/docs/PROTOCOL.md +++ b/docs/PROTOCOL.md @@ -54,6 +54,7 @@ Payload: "password": "string, max 128, optional", "tabTitle": "string, max 100, optional", "mediaTitle": "string, max 100, optional", + "clientCapabilities": ["chat-v1"], "protocolVersion": "string, max 16" } ``` @@ -82,7 +83,7 @@ Payload: "hostPeerId": "string or null", "controlMode": "everyone | host-only", "controllers": ["peerId"], - "capabilities": ["host-control", "co-host", "chat"] + "capabilities": ["host-control", "co-host", "chat", "chat-v1"] } ``` @@ -91,8 +92,17 @@ for every later room update. ## Ephemeral encrypted chat -Relays advertise chat support with `"chat"` in `room_data.capabilities`. Clients -must not infer support from another field. +Relays advertise chat support with `"chat-v1"` in `room_data.capabilities` and keep +the initial beta's `"chat"` flag during the transition. New clients announce +`"chat-v1"` in optional `join_room.clientCapabilities` (at most the first 16 entries +are inspected; unknown or malformed values are ignored). Old clients omit the field +and continue using the pre-chat protocol unchanged. + +The relay sends `chat_message` only to sockets that announced `"chat-v1"`. As a +transition for the first chat beta, a socket that sends a valid v1 ciphertext is +marked capable for the rest of that connection. This prevents old non-chat +extensions from receiving unknown events while preserving the first beta's send +path. ### `chat_message` @@ -106,7 +116,7 @@ Client to relay: authentication tag. The relay validates only canonical base64url and byte bounds. It cannot inspect plaintext. -Relay to every current room peer, including the sender: +Relay to every chat-capable current room peer, including the sender: ```json { @@ -413,5 +423,6 @@ If sender and target are still in the same room, the relay emits: - `host-control` - `co-host` - `chat` +- `chat-v1` Clients should treat a missing or unknown capabilities list as unsupported. diff --git a/extension/background.js b/extension/background.js index bb00ec1..3403995 100644 --- a/extension/background.js +++ b/extension/background.js @@ -91,6 +91,10 @@ let chatReceiveQueue = Promise.resolve(); const chatSendLimiter = createChatSendLimiter(); const webJoinCoordinator = createLatestTaskQueue(); function serverSupports(cap) { return Array.isArray(serverCapabilities) && serverCapabilities.includes(cap); } +function serverSupportsChat() { + return serverSupports(CAPABILITIES.CHAT_V1) || serverSupports(CAPABILITIES.CHAT); +} +const CLIENT_CAPABILITIES = Object.freeze([CAPABILITIES.CHAT_V1]); function invalidateChatSession() { chatSessionGeneration++; @@ -394,7 +398,7 @@ async function getSettings() { // (username) must NEVER come from storage.sync — syncing them across devices // both leaks them and resurrects dead rooms on reinstall (a fresh install // has empty local storage but sync survives in the user's Google account). - const data = await chrome.storage.local.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'chatKey', 'username', 'sendTabTitle', 'mediaTitlePrivacyMode', 'titlePrivacyMode']); + const data = await chrome.storage.local.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'chatKey', 'chatEnabled', 'username', 'sendTabTitle', 'mediaTitlePrivacyMode', 'titlePrivacyMode']); let username = data.username; if (!username) { username = generateUsername(); @@ -412,6 +416,7 @@ async function getSettings() { roomId, password: data.password || '', chatKey, + chatEnabled: data.chatEnabled === true, username, sendTabTitle: normalizeSendTabTitle(data.sendTabTitle, legacyTitlePrivacyMode), mediaTitlePrivacyMode @@ -748,6 +753,7 @@ async function connect() { peerId, username: settings.username, tabTitle: sharedTitles.tabTitle, + clientCapabilities: CLIENT_CAPABILITIES, protocolVersion: PROTOCOL_VERSION }); } @@ -1193,7 +1199,7 @@ async function handleServerEvent(event, data) { chrome.runtime.sendMessage({ type: 'ROOM_LIST', rooms: data.rooms }).catch(() => {}); break; case EVENTS.CHAT_MESSAGE: { - if (!currentRoom || !serverSupports(CAPABILITIES.CHAT) || !currentTabId) break; + if (!currentRoom || !serverSupportsChat() || !currentTabId) break; const generation = chatSessionGeneration; const roomId = currentRoom.roomId; const tabId = Number(currentTabId); @@ -1203,7 +1209,7 @@ async function handleServerEvent(event, data) { chatReceiveQueue = chatReceiveQueue.catch(() => {}).then(async () => { if (!isCurrentSession()) return; const settings = await getSettings(); - if (!settings.chatKey || settings.roomId !== roomId || !isCurrentSession()) return; + if (!settings.chatEnabled || !settings.chatKey || settings.roomId !== roomId || !isCurrentSession()) return; const chatKey = settings.chatKey; try { const text = await decryptChatMessage({ @@ -2547,7 +2553,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { }); chrome.storage.onChanged.addListener((changes, area) => { - if (area !== 'local' || (!changes.roomId && !changes.chatKey)) return; + if (area !== 'local' || (!changes.roomId && !changes.chatKey && !changes.chatEnabled)) return; if (changes.chatKey) chatSecretGuard = validateChatSecret(changes.chatKey.newValue); invalidateChatSession(); if (currentTabId) chrome.tabs.sendMessage(Number(currentTabId), { type: 'CHAT_CONTEXT_UPDATE' }).catch(() => {}); @@ -2593,6 +2599,7 @@ async function handleAsyncMessage(message, sender, sendResponse) { peerId, username: settings.username, tabTitle: sharedTitles.tabTitle, + clientCapabilities: CLIENT_CAPABILITIES, protocolVersion: PROTOCOL_VERSION }); } @@ -2611,6 +2618,7 @@ async function handleAsyncMessage(message, sender, sendResponse) { await retryPendingTarget(); } const pendingTarget = await readPendingTarget(); + const settings = await getSettings(); const isConnected = socket && socket.readyState === WebSocket.OPEN && isNamespaceJoined; const isReconnecting = !isConnected && reconnectAttempts > 0; let status = isConnected ? 'connected' : (isConnecting || (socket && socket.readyState === WebSocket.CONNECTING) ? 'connecting' : (isReconnecting ? 'reconnecting' : 'disconnected')); @@ -2641,8 +2649,9 @@ async function handleAsyncMessage(message, sender, sendResponse) { amController: amController(), hostControlSupported: serverSupports(CAPABILITIES.HOST_CONTROL), coHostSupported: serverSupports(CAPABILITIES.CO_HOST), - chatSupported: serverSupports(CAPABILITIES.CHAT), - hasChatKey: !!(await getSettings()).chatKey + chatSupported: serverSupportsChat(), + hasChatKey: !!settings.chatKey, + chatEnabled: settings.chatEnabled }); } else if (message.type === 'GET_CHAT_CONTEXT') { const senderTabId = sender.tab?.id; @@ -2658,7 +2667,8 @@ async function handleAsyncMessage(message, sender, sendResponse) { return value === key ? '' : value; }; sendResponse({ - supported: serverSupports(CAPABILITIES.CHAT), + supported: serverSupportsChat(), + enabled: settings.chatEnabled, hasKey: !!settings.chatKey, connected: !!(socket && socket.readyState === WebSocket.OPEN && isNamespaceJoined), peerId, @@ -2686,7 +2696,7 @@ async function handleAsyncMessage(message, sender, sendResponse) { sendResponse({ status: 'invalid_tab' }); return; } - if (!serverSupports(CAPABILITIES.CHAT)) { + if (!serverSupportsChat()) { sendResponse({ status: 'unsupported' }); return; } @@ -2702,6 +2712,10 @@ async function handleAsyncMessage(message, sender, sendResponse) { currentRoom?.roomId === roomId && Number(currentTabId) === tabId && socket === socketSnapshot && socketSnapshot.readyState === WebSocket.OPEN && isNamespaceJoined; const settings = await getSettings(); + if (!settings.chatEnabled) { + sendResponse({ status: 'disabled' }); + return; + } if (!settings.chatKey || settings.roomId !== roomId || !isCurrentSession()) { sendResponse({ status: settings.chatKey ? 'session_changed' : 'missing_key' }); return; @@ -2945,6 +2959,7 @@ async function handleAsyncMessage(message, sender, sendResponse) { peerId, username: settings.username, tabTitle: sharedTitles.tabTitle, + clientCapabilities: CLIENT_CAPABILITIES, protocolVersion: PROTOCOL_VERSION }); } diff --git a/extension/chat-overlay-contract.test.mjs b/extension/chat-overlay-contract.test.mjs index 8868fa4..8559857 100644 --- a/extension/chat-overlay-contract.test.mjs +++ b/extension/chat-overlay-contract.test.mjs @@ -9,6 +9,8 @@ const backgroundSource = fs.readFileSync(path.join(extensionDir, 'background.js' const popupSource = fs.readFileSync(path.join(extensionDir, 'popup.js'), 'utf8'); const localeDir = path.join(extensionDir, 'locales'); const chatKeys = [ + 'LABEL_CHAT_ENABLED', + 'LABEL_CHAT_ENABLED_TOOLTIP', 'CHAT_TITLE', 'CHAT_LIVE_ONLY', 'CHAT_OPEN', @@ -60,6 +62,18 @@ describe('chat overlay contract', () => { expect(backgroundSource).toContain("status: 'rate_limited'"); }); + it('keeps chat hidden by default without discarding the room chat key', () => { + expect(popupSource).toContain('localData.chatEnabled === true'); + expect(backgroundSource).toContain('chatEnabled: data.chatEnabled === true'); + expect(backgroundSource).toContain('clientCapabilities: CLIENT_CAPABILITIES'); + expect(overlaySource).toContain('all:initial;display:none;position:fixed'); + expect(overlaySource).toContain("host.style.display = supported && optedIn ? '' : 'none'"); + expect(overlaySource).toContain('supported && optedIn && hasKey && connected'); + expect(popupSource).toContain("chrome.storage.local.set({ chatEnabled: elements.chatEnabled.checked })"); + expect(popupSource).toMatch(/if \(isCreating\) \{[\s\S]*?type: 'CREATE_CHAT_KEY'[\s\S]*?chatKey = normalizeChatKey/); + expect(popupSource).not.toMatch(/chatEnabled[\s\S]{0,120}chatKey:\s*''/); + }); + it('keeps unavailable chat controls discoverable to assistive technology', () => { expect(overlaySource).toContain("launcher.setAttribute('aria-disabled'"); expect(overlaySource).not.toContain('launcher.disabled ='); diff --git a/extension/chat-overlay.js b/extension/chat-overlay.js index d2e7ed2..0870bd3 100644 --- a/extension/chat-overlay.js +++ b/extension/chat-overlay.js @@ -25,7 +25,7 @@ const host = document.createElement('div'); host.id = 'koalasync-chat-overlay-host'; - host.style.cssText = 'all:initial;position:fixed;inset:0;z-index:2147483647;pointer-events:none;contain:layout style paint;transform:translateZ(0);'; + host.style.cssText = 'all:initial;display:none;position:fixed;inset:0;z-index:2147483647;pointer-events:none;contain:layout style paint;transform:translateZ(0);'; const shadow = host.attachShadow({ mode: 'open' }); const style = document.createElement('style'); style.textContent = ` @@ -324,11 +324,12 @@ const previousRoomId = context?.roomId; context = next || null; const supported = !!context?.supported; + const optedIn = !!context?.enabled; const hasKey = !!context?.hasKey; const connected = !!context?.connected; - context = context ? { ...context, enabled: supported && hasKey && connected } : null; + context = context ? { ...context, enabled: supported && optedIn && hasKey && connected } : null; if (previousRoomId && previousRoomId !== context?.roomId) clearMessages(); - host.style.display = supported ? '' : 'none'; + host.style.display = supported && optedIn ? '' : 'none'; launcher.setAttribute('aria-disabled', String(!context?.enabled)); if (!context?.enabled) setOpened(false); applyStrings(); diff --git a/extension/locales/de.json b/extension/locales/de.json index e282de9..bbbd366 100644 --- a/extension/locales/de.json +++ b/extension/locales/de.json @@ -246,6 +246,8 @@ "LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "Ändere deinen Benutzernamen, dein Design und deine Spracheinstellungen.", "LABEL_SETTINGS_GROUP_SYNC": "Wiedergabe & Sync", "LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "Passe Wiedergabe-, Benachrichtigungs- und Audioeinstellungen an.", + "LABEL_CHAT_ENABLED": "Raum-Chat aktivieren", + "LABEL_CHAT_ENABLED_TOOLTIP": "Verschlüsselten Raum-Chat im ausgewählten Video-Tab anzeigen.", "CHAT_TITLE": "Raum-Chat", "CHAT_LIVE_ONLY": "Nur live. Kein Verlauf.", "CHAT_OPEN": "Raum-Chat öffnen", diff --git a/extension/locales/en.json b/extension/locales/en.json index e9a8452..6d7e478 100644 --- a/extension/locales/en.json +++ b/extension/locales/en.json @@ -246,6 +246,8 @@ "LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "Change your username, theme, and language preferences.", "LABEL_SETTINGS_GROUP_SYNC": "Playback & Sync", "LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "Customize playback, notifications, and audio preferences.", + "LABEL_CHAT_ENABLED": "Enable Room Chat", + "LABEL_CHAT_ENABLED_TOOLTIP": "Show encrypted room chat on the selected video tab.", "CHAT_TITLE": "Room Chat", "CHAT_LIVE_ONLY": "Live only. No history.", "CHAT_OPEN": "Open room chat", diff --git a/extension/locales/es.json b/extension/locales/es.json index 8d4decb..a88b151 100644 --- a/extension/locales/es.json +++ b/extension/locales/es.json @@ -246,6 +246,8 @@ "LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "Cambia tu nombre de usuario, tema y preferencias de idioma.", "LABEL_SETTINGS_GROUP_SYNC": "Reproducción y Sincronización", "LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "Personaliza la reproducción, las notificaciones y las preferencias de audio.", + "LABEL_CHAT_ENABLED": "Activar chat de la sala", + "LABEL_CHAT_ENABLED_TOOLTIP": "Mostrar el chat cifrado de la sala en la pestaña de vídeo seleccionada.", "CHAT_TITLE": "Chat de la sala", "CHAT_LIVE_ONLY": "Solo en directo. Sin historial.", "CHAT_OPEN": "Abrir chat de la sala", diff --git a/extension/locales/fr.json b/extension/locales/fr.json index e7634ff..b8c63f2 100644 --- a/extension/locales/fr.json +++ b/extension/locales/fr.json @@ -246,6 +246,8 @@ "LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "Modifiez votre nom d'utilisateur, votre thème et vos préférences linguistiques.", "LABEL_SETTINGS_GROUP_SYNC": "Lecture & Sync", "LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "Personnalisez la lecture, les notifications et les préférences audio.", + "LABEL_CHAT_ENABLED": "Activer le chat du salon", + "LABEL_CHAT_ENABLED_TOOLTIP": "Afficher le chat chiffré du salon dans l'onglet vidéo sélectionné.", "CHAT_TITLE": "Chat du salon", "CHAT_LIVE_ONLY": "En direct uniquement. Aucun historique.", "CHAT_OPEN": "Ouvrir le chat du salon", diff --git a/extension/locales/it.json b/extension/locales/it.json index 26f2505..7fbe1f3 100644 --- a/extension/locales/it.json +++ b/extension/locales/it.json @@ -246,6 +246,8 @@ "LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "Modifica il tuo nome utente, tema e preferenze di lingua.", "LABEL_SETTINGS_GROUP_SYNC": "Riproduzione & Sync", "LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "Personalizza la riproduzione, le notifiche e le preferenze audio.", + "LABEL_CHAT_ENABLED": "Attiva chat della stanza", + "LABEL_CHAT_ENABLED_TOOLTIP": "Mostra la chat crittografata della stanza nella scheda video selezionata.", "CHAT_TITLE": "Chat della stanza", "CHAT_LIVE_ONLY": "Solo in diretta. Nessuna cronologia.", "CHAT_OPEN": "Apri la chat della stanza", diff --git a/extension/locales/ja.json b/extension/locales/ja.json index 32d9d6e..708f970 100644 --- a/extension/locales/ja.json +++ b/extension/locales/ja.json @@ -246,6 +246,8 @@ "LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "ユーザー名、テーマ、言語設定を変更します。", "LABEL_SETTINGS_GROUP_SYNC": "再生と同期", "LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "再生、通知、オーディオ設定をカスタマイズします。", + "LABEL_CHAT_ENABLED": "ルームチャットを有効化", + "LABEL_CHAT_ENABLED_TOOLTIP": "選択した動画タブに暗号化されたルームチャットを表示します。", "CHAT_TITLE": "ルームチャット", "CHAT_LIVE_ONLY": "ライブのみ。履歴はありません。", "CHAT_OPEN": "ルームチャットを開く", diff --git a/extension/locales/ko.json b/extension/locales/ko.json index b731626..57b4a76 100644 --- a/extension/locales/ko.json +++ b/extension/locales/ko.json @@ -246,6 +246,8 @@ "LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "사용자 이름, 테마 및 언어 설정을 변경합니다.", "LABEL_SETTINGS_GROUP_SYNC": "재생 및 동기화", "LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "재생, 알림 및 오디오 설정을 맞춤설정합니다.", + "LABEL_CHAT_ENABLED": "방 채팅 사용", + "LABEL_CHAT_ENABLED_TOOLTIP": "선택한 동영상 탭에 암호화된 방 채팅을 표시합니다.", "CHAT_TITLE": "방 채팅", "CHAT_LIVE_ONLY": "실시간 전용. 기록 없음.", "CHAT_OPEN": "방 채팅 열기", diff --git a/extension/locales/nl.json b/extension/locales/nl.json index 4e68a1d..c5f67b7 100644 --- a/extension/locales/nl.json +++ b/extension/locales/nl.json @@ -246,6 +246,8 @@ "LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "Wijzig uw gebruikersnaam, thema en taalvoorkeuren.", "LABEL_SETTINGS_GROUP_SYNC": "Afspelen & Sync", "LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "Pas afspelen, meldingen en audiovoorkeuren aan.", + "LABEL_CHAT_ENABLED": "Kamerchat inschakelen", + "LABEL_CHAT_ENABLED_TOOLTIP": "Versleutelde kamerchat tonen in het geselecteerde videotabblad.", "CHAT_TITLE": "Kamerchat", "CHAT_LIVE_ONLY": "Alleen live. Geen geschiedenis.", "CHAT_OPEN": "Kamerchat openen", diff --git a/extension/locales/pl.json b/extension/locales/pl.json index 4043ed2..e8ab859 100644 --- a/extension/locales/pl.json +++ b/extension/locales/pl.json @@ -246,6 +246,8 @@ "LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "Zmień swoją nazwę użytkownika, motyw i preferencje językowe.", "LABEL_SETTINGS_GROUP_SYNC": "Odtwarzanie i Sync", "LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "Dostosuj odtwarzanie, powiadomienia i preferencje audio.", + "LABEL_CHAT_ENABLED": "Włącz czat pokoju", + "LABEL_CHAT_ENABLED_TOOLTIP": "Pokaż zaszyfrowany czat pokoju na wybranej karcie wideo.", "CHAT_TITLE": "Czat pokoju", "CHAT_LIVE_ONLY": "Tylko na żywo. Bez historii.", "CHAT_OPEN": "Otwórz czat pokoju", diff --git a/extension/locales/pt-BR.json b/extension/locales/pt-BR.json index 4d8a906..fe4d327 100644 --- a/extension/locales/pt-BR.json +++ b/extension/locales/pt-BR.json @@ -246,6 +246,8 @@ "LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "Altere o seu nome de usuário, tema e preferências de idioma.", "LABEL_SETTINGS_GROUP_SYNC": "Reprodução e Sincronização", "LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "Personalize a reprodução, as notificações e as preferências de áudio.", + "LABEL_CHAT_ENABLED": "Ativar chat da sala", + "LABEL_CHAT_ENABLED_TOOLTIP": "Mostrar o chat criptografado da sala na guia de vídeo selecionada.", "CHAT_TITLE": "Chat da sala", "CHAT_LIVE_ONLY": "Somente ao vivo. Sem histórico.", "CHAT_OPEN": "Abrir chat da sala", diff --git a/extension/locales/pt.json b/extension/locales/pt.json index 05e7328..494f363 100644 --- a/extension/locales/pt.json +++ b/extension/locales/pt.json @@ -246,6 +246,8 @@ "LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "Altere o seu nome de utilizador, tema e preferências de idioma.", "LABEL_SETTINGS_GROUP_SYNC": "Reprodução e Sincronização", "LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "Personalize a reprodução, as notificações e as preferências de áudio.", + "LABEL_CHAT_ENABLED": "Ativar chat da sala", + "LABEL_CHAT_ENABLED_TOOLTIP": "Mostrar o chat encriptado da sala no separador de vídeo selecionado.", "CHAT_TITLE": "Chat da sala", "CHAT_LIVE_ONLY": "Apenas em direto. Sem histórico.", "CHAT_OPEN": "Abrir chat da sala", diff --git a/extension/locales/ru.json b/extension/locales/ru.json index 462c7a9..373b433 100644 --- a/extension/locales/ru.json +++ b/extension/locales/ru.json @@ -246,6 +246,8 @@ "LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "Измените имя пользователя, тему и настройки языка.", "LABEL_SETTINGS_GROUP_SYNC": "Воспроизведение и Синхронизация", "LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "Настройте воспроизведение, уведомления и параметры звука.", + "LABEL_CHAT_ENABLED": "Включить чат комнаты", + "LABEL_CHAT_ENABLED_TOOLTIP": "Показывать зашифрованный чат комнаты на выбранной вкладке с видео.", "CHAT_TITLE": "Чат комнаты", "CHAT_LIVE_ONLY": "Только в реальном времени. Без истории.", "CHAT_OPEN": "Открыть чат комнаты", diff --git a/extension/locales/tr.json b/extension/locales/tr.json index 2bbc9da..d0fc2ce 100644 --- a/extension/locales/tr.json +++ b/extension/locales/tr.json @@ -246,6 +246,8 @@ "LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "Kullanıcı adınızı, temanızı ve dil tercihlerinizi değiştirin.", "LABEL_SETTINGS_GROUP_SYNC": "Oynatma ve Eşitleme", "LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "Oynatma, bildirim ve ses tercihlerini özelleştirin.", + "LABEL_CHAT_ENABLED": "Oda sohbetini etkinleştir", + "LABEL_CHAT_ENABLED_TOOLTIP": "Şifreli oda sohbetini seçili video sekmesinde göster.", "CHAT_TITLE": "Oda Sohbeti", "CHAT_LIVE_ONLY": "Yalnızca canlı. Geçmiş yok.", "CHAT_OPEN": "Oda sohbetini aç", diff --git a/extension/locales/uk.json b/extension/locales/uk.json index 3224a5e..84850f5 100644 --- a/extension/locales/uk.json +++ b/extension/locales/uk.json @@ -246,6 +246,8 @@ "LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "Змініть ім'я користувача, тему та мовні налаштування.", "LABEL_SETTINGS_GROUP_SYNC": "Відтворення та Синхронізація", "LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "Налаштуйте відтворення, сповіщення та параметри звуку.", + "LABEL_CHAT_ENABLED": "Увімкнути чат кімнати", + "LABEL_CHAT_ENABLED_TOOLTIP": "Показувати зашифрований чат кімнати на вибраній вкладці з відео.", "CHAT_TITLE": "Чат кімнати", "CHAT_LIVE_ONLY": "Лише наживо. Без історії.", "CHAT_OPEN": "Відкрити чат кімнати", diff --git a/extension/locales/zh.json b/extension/locales/zh.json index 9c9f439..9aa97ae 100644 --- a/extension/locales/zh.json +++ b/extension/locales/zh.json @@ -246,6 +246,8 @@ "LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "更改您的用户名、主题和语言首选项。", "LABEL_SETTINGS_GROUP_SYNC": "播放与同步", "LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "自定义播放、通知和音频首选项。", + "LABEL_CHAT_ENABLED": "启用房间聊天", + "LABEL_CHAT_ENABLED_TOOLTIP": "在选定的视频标签页中显示加密的房间聊天。", "CHAT_TITLE": "房间聊天", "CHAT_LIVE_ONLY": "仅实时显示,无历史记录。", "CHAT_OPEN": "打开房间聊天", diff --git a/extension/popup.html b/extension/popup.html index 0c106f9..de0a429 100644 --- a/extension/popup.html +++ b/extension/popup.html @@ -1625,6 +1625,13 @@ +
+ + +