Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9f89823
v3.18.8: fix purple optimistic messages — track color per platform
jouki Apr 12, 2026
101525a
v3.18.9: fix Twitch UC button — insert into header, not toggle wrapper
jouki Apr 12, 2026
043d67a
v3.18.10: Twitch UC button — second child of header + collapse chat
jouki Apr 12, 2026
6967b74
v3.18.11: Twitch UC button absolute positioned — no layout disruption
jouki Apr 12, 2026
8f2b77a
v3.18.12: Twitch button back to inline flex with order:1
jouki Apr 12, 2026
e17792b
v3.18.13: Twitch UC button — orange text 'UC' instead of img
jouki Apr 12, 2026
3612fd4
v3.18.14: revert to icon-based UC button on Twitch
jouki Apr 12, 2026
f975fb1
v3.18.15: fix Twitch button — strict selector + polling, no MutationO…
jouki Apr 12, 2026
1b6e1e7
v3.18.16: insert UC button before #chat-room-header-label wrapper
jouki Apr 12, 2026
3cd14c8
v3.18.17: persist _platformColors in config
jouki Apr 12, 2026
554749f
v3.18.18: track platform color from _lastSentText echo + username
jouki Apr 12, 2026
ec3b88f
v3.18.19: move color tracking BEFORE content dedup
jouki Apr 12, 2026
0d06086
v3.18.20: retroactively apply color to older messages
jouki Apr 12, 2026
b86b7f5
v3.18.21: always override color on retroactive apply + config.usernam…
jouki Apr 12, 2026
01a2444
v3.18.22: use _chatUsers color map for optimistic messages
jouki Apr 12, 2026
fe42f90
v3.18.23: color is username-bound, not message-bound
jouki Apr 12, 2026
8a730f7
v3.19.0: username→color mapping persisted across reloads
jouki Apr 12, 2026
a5955a0
fix: version 3.18.24 not 3.19.0
jouki Apr 12, 2026
391fa08
v3.18.25: platform:username color keys + save color without nickname
jouki Apr 12, 2026
dd41e15
v3.18.30: fix color save, settings UI improvements, backend rate limit
jouki Apr 12, 2026
a59d348
v3.18.33: fix @mention dedup, reply highlight, reply text in cache, r…
jouki Apr 12, 2026
814490b
v3.18.34: fix reply context missing message text
jouki Apr 12, 2026
eb7f692
v3.18.35: use Twitch display-name, fix nickname save without custom nick
jouki Apr 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const EnvSchema = z.object({
HOST: z.string().default('0.0.0.0'),
DATABASE_URL: z.string().url(),
LOG_LEVEL: z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal']).default('info'),
NICKNAME_RATE_LIMIT_SECS: z.coerce.number().int().positive().default(300),
NICKNAME_RATE_LIMIT_SECS: z.coerce.number().int().positive().default(10),
});

export const config = EnvSchema.parse(process.env);
Expand Down
96 changes: 28 additions & 68 deletions extension/content/twitch.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,103 +19,63 @@
btn.setAttribute('aria-label', 'Otevřít UnityChat');
btn.title = 'Otevřít UnityChat';
Object.assign(btn.style, {
display: 'inline-flex',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '30px',
height: '30px',
padding: '4px',
margin: '0 4px',
minWidth: '30px',
padding: '0',
margin: '0 2px',
background: 'transparent',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
flexShrink: '0',
transition: 'background 0.15s ease, transform 0.15s ease'
transition: 'background 0.15s ease'
});

const img = document.createElement('img');
img.src = chrome.runtime.getURL('icons/icon48.png');
img.alt = 'UnityChat';
Object.assign(img.style, {
width: '22px',
height: '22px',
display: 'block',
filter: 'drop-shadow(0 0 6px rgba(255, 140, 0, 0.55))',
pointerEvents: 'none'
});
img.alt = 'UC';
Object.assign(img.style, { width: '20px', height: '20px', display: 'block', pointerEvents: 'none' });
btn.appendChild(img);

btn.addEventListener('mouseenter', () => {
btn.style.background = 'rgba(255, 140, 0, 0.12)';
img.style.filter = 'drop-shadow(0 0 10px rgba(255, 160, 20, 0.8))';
});
btn.addEventListener('mouseleave', () => {
btn.style.background = 'transparent';
img.style.filter = 'drop-shadow(0 0 6px rgba(255, 140, 0, 0.55))';
});
btn.addEventListener('mouseenter', () => { btn.style.background = 'rgba(255,140,0,0.15)'; });
btn.addEventListener('mouseleave', () => { btn.style.background = 'transparent'; });
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
// Collapse vanilla Twitch chat
const collapseBtn = document.querySelector('[data-a-target="right-column__toggle-collapse-btn"]');
if (collapseBtn) collapseBtn.click();
// Open UnityChat side panel
chrome.runtime.sendMessage({ type: 'OPEN_SIDE_PANEL' }).catch(() => {});
});
return btn;
}

function findChatHeader() {
// Twitch uses several header classes across layouts; try them in order.
const selectors = [
'.stream-chat-header',
'[data-a-target="stream-chat-header"]',
'.chat-room__header',
'.chat-shell__header',
'.chat-header'
];
for (const sel of selectors) {
const el = document.querySelector(sel);
if (el) return el;
}
// Fallback: climb from the collapse toggle button.
const toggle = document.querySelector('[data-a-target="right-column__toggle-collapse-btn"]');
if (toggle) {
// Walk up until we find a row-ish container (flex parent with siblings).
let n = toggle.parentElement;
for (let i = 0; i < 5 && n; i++) {
if (n.childElementCount >= 2) return n;
n = n.parentElement;
}
}
return null;
return document.querySelector('.stream-chat-header');
}

function injectSidePanelButton() {
if (document.getElementById(UC_BTN_ID)) return;
const header = findChatHeader();
if (document.querySelectorAll('#' + UC_BTN_ID).length > 0) return;
const header = document.querySelector('.stream-chat-header');
if (!header) return;
const btn = buildUcButton();
// Prefer to sit right after the collapse toggle (matches the red-box
// position in the UI). Otherwise prepend into the header row.
const toggle = header.querySelector('[data-a-target="right-column__toggle-collapse-btn"]');
if (toggle && toggle.parentElement) {
toggle.parentElement.insertBefore(btn, toggle.nextSibling);
} else {
header.insertBefore(btn, header.firstChild);
const label = header.querySelector('#chat-room-header-label');
if (!label) return;
// label is inside a wrapper div — insert button before that wrapper
const wrapper = label.parentElement;
if (wrapper && wrapper.parentElement === header) {
header.insertBefore(buildUcButton(), wrapper);
}
}

function startHeaderObserver() {
injectSidePanelButton();
const obs = new MutationObserver(() => {
if (!document.getElementById(UC_BTN_ID)) injectSidePanelButton();
});
obs.observe(document.body, { childList: true, subtree: true });
}

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startHeaderObserver, { once: true });
} else {
startHeaderObserver();
}
// Poll for header — Twitch re-mounts on navigation
setInterval(() => {
if (document.querySelectorAll('#' + UC_BTN_ID).length === 0) {
injectSidePanelButton();
}
}, 2000);

chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === 'PING') {
Expand Down
2 changes: 1 addition & 1 deletion extension/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "UnityChat",
"version": "3.18.7",
"version": "3.18.35",
"description": "Sjednocený chat z Twitch, YouTube a Kick v jednom panelu",
"permissions": [
"sidePanel",
Expand Down
13 changes: 7 additions & 6 deletions extension/sidepanel.css
Original file line number Diff line number Diff line change
Expand Up @@ -238,11 +238,15 @@ body.layout-large .reply-ctx { font-size: 12px; }

/* ---- Nickname setting ---- */

.nickname-row {
.save-row {
display: flex;
gap: 6px;
justify-content: center;
margin-top: 8px;
}
#input-username[readonly] {
opacity: 0.5;
cursor: not-allowed;
}
.nickname-row input { flex: 1; }
.btn-nick {
background: var(--accent-gradient);
color: #000;
Expand Down Expand Up @@ -625,9 +629,6 @@ body.layout-large .reply-ctx { font-size: 12px; }
font-size: 11px;
color: var(--text-muted);
padding: 1px 0 1px 18px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
opacity: 0.7;
}

Expand Down
21 changes: 10 additions & 11 deletions extension/sidepanel.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,31 +23,31 @@
<summary>Kanály</summary>
<div class="setting-row">
<label for="input-channel">Twitch</label>
<input type="text" id="input-channel" value="robdiesalot" spellcheck="false">
<input type="text" id="input-channel" value="robdiesalot" spellcheck="false" autocomplete="off">
</div>
<div class="setting-row">
<label for="input-kick-channel">Kick</label>
<input type="text" id="input-kick-channel" value="robdiesalot" spellcheck="false">
<input type="text" id="input-kick-channel" value="robdiesalot" spellcheck="false" autocomplete="off">
</div>
<div class="setting-row">
<label for="input-yt-channel">YouTube</label>
<input type="text" id="input-yt-channel" value="robdiesalot" placeholder="robdiesalot nebo @robdiesalot" spellcheck="false">
<input type="text" id="input-yt-channel" value="robdiesalot" placeholder="Název kanálu nebo @handle" spellcheck="false" autocomplete="off">
</div>
</details>
<div class="setting-row">
<label for="input-username">Tvoje username</label>
<input type="text" id="input-username" placeholder="Jouki728" spellcheck="false">
<label for="input-username">USERNAME (TWITCH)</label>
<input type="text" id="input-username" placeholder="Detekováno automaticky" spellcheck="false" autocomplete="off" readonly>
</div>
<div class="setting-row">
<label for="input-nickname">UnityChat přezdívka</label>
<div class="nickname-row">
<input type="text" id="input-nickname" placeholder="Tvoje přezdívka" maxlength="30" spellcheck="false">
<button id="btn-nickname" class="btn-nick">Uložit</button>
</div>
<input type="text" id="input-nickname" placeholder="Vlastní přezdívka" maxlength="30" spellcheck="false" autocomplete="off">
<label for="input-color-hex" class="color-label">Barva jména</label>
<div class="color-row">
<input type="color" id="input-color-picker" value="#ff8c00">
<input type="text" id="input-color-hex" placeholder="#ff8c00" maxlength="7" spellcheck="false">
<input type="text" id="input-color-hex" placeholder="#ff8c00" maxlength="7" spellcheck="false" autocomplete="off">
</div>
<div class="save-row">
<button id="btn-nickname" class="btn-nick">Uložit</button>
</div>
<span id="nickname-status" class="nick-status"></span>
</div>
Expand Down Expand Up @@ -99,7 +99,6 @@
<span class="st" id="st-kick" title="Kick"><span class="dot"></span>KI</span>
</div>
<div id="filters">
<button class="fbtn active" data-platform="all">Vše</button>
<button class="fbtn active" data-platform="twitch">TW</button>
<button class="fbtn active" data-platform="youtube">YT</button>
<button class="fbtn active" data-platform="kick">KI</button>
Expand Down
Loading