feat: auto-pause chat on scroll, resume at bottom

This commit is contained in:
2026-04-10 13:21:18 -07:00
parent ad6e9ef270
commit 215dfc827e
2 changed files with 238 additions and 218 deletions
+214 -216
View File
@@ -1,216 +1,214 @@
import Store from 'electron-store'; import Store from 'electron-store';
export interface Keybind { export interface Keybind {
key: string; key: string;
ctrl: boolean; ctrl: boolean;
shift: boolean; shift: boolean;
alt: boolean; alt: boolean;
} }
export interface SavedAccount { export interface SavedAccount {
label: string; label: string;
username: string; username: string;
password: string; password: string;
} }
export interface AppConfig { export interface AppConfig {
window: { window: {
width: number; width: number;
height: number; height: number;
x: number | undefined; x: number | undefined;
y: number | undefined; y: number | undefined;
maximized: boolean; maximized: boolean;
fullscreen: boolean; fullscreen: boolean;
}; };
performance: { performance: {
fpsUnlocked: boolean; fpsUnlocked: boolean;
hardwareAccel: boolean; hardwareAccel: boolean;
gpuPreference: 'high-performance' | 'low-power' | 'default'; gpuPreference: 'high-performance' | 'low-power' | 'default';
cpuThrottleGame: number; cpuThrottleGame: number;
cpuThrottleMenu: number; cpuThrottleMenu: number;
processPriority: string; processPriority: string;
}; };
game: { game: {
lastServer: string; lastServer: string;
socialTabBehaviour: 'New Window' | 'Same Window'; socialTabBehaviour: 'New Window' | 'Same Window';
joinAsSpectator: boolean; joinAsSpectator: boolean;
rawInput: boolean; rawInput: boolean;
betterChat: boolean; betterChat: boolean;
chatHistorySize: number; chatHistorySize: number;
showPing: boolean; showPing: boolean;
hpEnemyCounter: boolean; hpEnemyCounter: boolean;
}; };
swapper: { swapper: {
enabled: boolean; enabled: boolean;
path: string; path: string;
}; };
matchmaker: { matchmaker: {
enabled: boolean; enabled: boolean;
regions: string[]; regions: string[];
gamemodes: string[]; gamemodes: string[];
maps: string[]; maps: string[];
minPlayers: number; minPlayers: number;
maxPlayers: number; maxPlayers: number;
minRemainingTime: number; minRemainingTime: number;
openServerBrowser: boolean; openServerBrowser: boolean;
autoJoin: boolean; autoJoin: boolean;
}; };
keybinds: { keybinds: {
reload: Keybind; reload: Keybind;
newMatch: Keybind; newMatch: Keybind;
copyGameLink: Keybind; copyGameLink: Keybind;
joinFromClipboard: Keybind; joinFromClipboard: Keybind;
devTools: Keybind; devTools: Keybind;
matchmaker: Keybind; matchmaker: Keybind;
matchmakerAccept: Keybind; matchmakerAccept: Keybind;
matchmakerCancel: Keybind; matchmakerCancel: Keybind;
pauseChat: Keybind; fullscreenToggle: Keybind;
fullscreenToggle: Keybind; };
}; userscripts: {
userscripts: { enabled: boolean;
enabled: boolean; path: string;
path: string; };
}; ui: {
ui: { showExitButton: boolean;
showExitButton: boolean; deathscreenAnimation: boolean;
deathscreenAnimation: boolean; hideMenuPopups: boolean;
hideMenuPopups: boolean; menuTimer: boolean;
menuTimer: boolean; doublePing: boolean;
doublePing: boolean; cssTheme: string;
cssTheme: string; loadingTheme: string;
loadingTheme: string; backgroundUrl: string;
backgroundUrl: string; showChangelog: boolean;
showChangelog: boolean; lastSeenVersion: string;
lastSeenVersion: string; };
}; discord: {
discord: { enabled: boolean;
enabled: boolean; };
}; translator: {
translator: { enabled: boolean;
enabled: boolean; targetLanguage: string;
targetLanguage: string; showLanguageTag: boolean;
showLanguageTag: boolean; };
}; advanced: {
advanced: { removeUselessFeatures: boolean;
removeUselessFeatures: boolean; gpuRasterizing: boolean;
gpuRasterizing: boolean; helpfulFlags: boolean;
helpfulFlags: boolean; increaseLimits: boolean;
increaseLimits: boolean; lowLatency: boolean;
lowLatency: boolean; experimentalFlags: boolean;
experimentalFlags: boolean; angleBackend: string;
angleBackend: string; verboseLogging: boolean;
verboseLogging: boolean; };
}; accounts: SavedAccount[];
accounts: SavedAccount[]; tabWindow: {
tabWindow: { width: number;
width: number; height: number;
height: number; x: number | undefined;
x: number | undefined; y: number | undefined;
y: number | undefined; maximized: boolean;
maximized: boolean; };
}; }
}
export const DEFAULT_KEYBINDS: AppConfig['keybinds'] = {
export const DEFAULT_KEYBINDS: AppConfig['keybinds'] = { reload: { key: 'F5', ctrl: false, shift: false, alt: false },
reload: { key: 'F5', ctrl: false, shift: false, alt: false }, newMatch: { key: 'F4', ctrl: false, shift: false, alt: false },
newMatch: { key: 'F4', ctrl: false, shift: false, alt: false }, copyGameLink: { key: 'l', ctrl: true, shift: false, alt: false },
copyGameLink: { key: 'l', ctrl: true, shift: false, alt: false }, joinFromClipboard: { key: 'j', ctrl: true, shift: false, alt: false },
joinFromClipboard: { key: 'j', ctrl: true, shift: false, alt: false }, devTools: { key: 'F12', ctrl: false, shift: false, alt: false },
devTools: { key: 'F12', ctrl: false, shift: false, alt: false }, matchmaker: { key: 'F6', ctrl: false, shift: false, alt: false },
matchmaker: { key: 'F6', ctrl: false, shift: false, alt: false }, matchmakerAccept: { key: 'Enter', ctrl: false, shift: false, alt: false },
matchmakerAccept: { key: 'Enter', ctrl: false, shift: false, alt: false }, matchmakerCancel: { key: 'Escape', ctrl: false, shift: false, alt: false },
matchmakerCancel: { key: 'Escape', ctrl: false, shift: false, alt: false }, fullscreenToggle: { key: 'F11', ctrl: false, shift: false, alt: false },
pauseChat: { key: 'F10', ctrl: false, shift: false, alt: false }, };
fullscreenToggle: { key: 'F11', ctrl: false, shift: false, alt: false },
};
export const config = new Store<AppConfig>({
name: 'krunker-civilian-config',
export const config = new Store<AppConfig>({ defaults: {
name: 'krunker-civilian-config', window: {
defaults: { width: 1600,
window: { height: 900,
width: 1600, x: undefined,
height: 900, y: undefined,
x: undefined, maximized: false,
y: undefined, fullscreen: false,
maximized: false, },
fullscreen: false, performance: {
}, fpsUnlocked: true,
performance: { hardwareAccel: true,
fpsUnlocked: true, gpuPreference: 'high-performance',
hardwareAccel: true, cpuThrottleGame: 1,
gpuPreference: 'high-performance', cpuThrottleMenu: 1.5,
cpuThrottleGame: 1, processPriority: 'Normal',
cpuThrottleMenu: 1.5, },
processPriority: 'Normal', game: {
}, lastServer: '',
game: { socialTabBehaviour: 'New Window',
lastServer: '', joinAsSpectator: false,
socialTabBehaviour: 'New Window', rawInput: true,
joinAsSpectator: false, betterChat: true,
rawInput: true, chatHistorySize: 200,
betterChat: true, showPing: true,
chatHistorySize: 200, hpEnemyCounter: true,
showPing: true, },
hpEnemyCounter: true, swapper: {
}, enabled: true,
swapper: { path: '',
enabled: true, },
path: '', matchmaker: {
}, enabled: true,
matchmaker: { regions: [],
enabled: true, gamemodes: [],
regions: [], maps: [],
gamemodes: [], minPlayers: 1,
maps: [], maxPlayers: 6,
minPlayers: 1, minRemainingTime: 120,
maxPlayers: 6, openServerBrowser: true,
minRemainingTime: 120, autoJoin: false,
openServerBrowser: true, },
autoJoin: false, keybinds: DEFAULT_KEYBINDS,
}, userscripts: {
keybinds: DEFAULT_KEYBINDS, enabled: true,
userscripts: { path: '',
enabled: true, },
path: '', ui: {
}, showExitButton: true,
ui: { deathscreenAnimation: true,
showExitButton: true, hideMenuPopups: false,
deathscreenAnimation: true, menuTimer: true,
hideMenuPopups: false, doublePing: true,
menuTimer: true, cssTheme: 'disabled',
doublePing: true, loadingTheme: 'disabled',
cssTheme: 'disabled', backgroundUrl: '',
loadingTheme: 'disabled', showChangelog: true,
backgroundUrl: '', lastSeenVersion: '',
showChangelog: true, },
lastSeenVersion: '', discord: {
}, enabled: false,
discord: { },
enabled: false, translator: {
}, enabled: true,
translator: { targetLanguage: 'en',
enabled: true, showLanguageTag: true,
targetLanguage: 'en', },
showLanguageTag: true, advanced: {
}, removeUselessFeatures: true,
advanced: { gpuRasterizing: false,
removeUselessFeatures: true, helpfulFlags: true,
gpuRasterizing: false, increaseLimits: false,
helpfulFlags: true, lowLatency: false,
increaseLimits: false, experimentalFlags: false,
lowLatency: false, angleBackend: 'default',
experimentalFlags: false, verboseLogging: false,
angleBackend: 'default', },
verboseLogging: false, accounts: [],
}, tabWindow: {
accounts: [], width: 1280,
tabWindow: { height: 720,
width: 1280, x: undefined,
height: 720, y: undefined,
x: undefined, maximized: true,
y: undefined, },
maximized: true, },
}, });
},
});
+24 -2
View File
@@ -16,8 +16,11 @@ let observer: MutationObserver | null = null;
let historyMax = 0; let historyMax = 0;
let betterChatEnabled = false; let betterChatEnabled = false;
let reInsertGuard = false; let reInsertGuard = false;
let scrollPaused = false;
let _con: SavedConsole | null = null; let _con: SavedConsole | null = null;
const SCROLL_BOTTOM_THRESHOLD = 30; // px from bottom to consider "at bottom"
function isChatMessage(node: Node): node is HTMLElement { function isChatMessage(node: Node): node is HTMLElement {
return node.nodeType === 1 && (node as HTMLElement).id?.startsWith('chatMsg_'); return node.nodeType === 1 && (node as HTMLElement).id?.startsWith('chatMsg_');
} }
@@ -83,18 +86,37 @@ function handleMutations(mutations: MutationRecord[]): void {
} }
} }
// Auto-scroll unless paused // Auto-scroll to bottom unless the user has scrolled up
if (chatList && !chatList.classList.contains('kpc-chat-paused')) { if (chatList && !scrollPaused) {
chatList.scrollTop = chatList.scrollHeight; chatList.scrollTop = chatList.scrollHeight;
} }
} }
function isNearBottom(el: HTMLElement): boolean {
return el.scrollHeight - el.scrollTop - el.clientHeight <= SCROLL_BOTTOM_THRESHOLD;
}
function updatePauseState(): void {
if (!chatList) return;
const atBottom = isNearBottom(chatList);
if (scrollPaused && atBottom) {
scrollPaused = false;
chatList.classList.remove('kpc-chat-paused');
} else if (!scrollPaused && !atBottom) {
scrollPaused = true;
chatList.classList.add('kpc-chat-paused');
}
}
function tryAttach(): boolean { function tryAttach(): boolean {
chatList = document.getElementById('chatList'); chatList = document.getElementById('chatList');
if (!chatList) return false; if (!chatList) return false;
observer = new MutationObserver(handleMutations); observer = new MutationObserver(handleMutations);
observer.observe(chatList, { childList: true }); observer.observe(chatList, { childList: true });
chatList.addEventListener('scroll', updatePauseState, { passive: true });
_con?.log('[KCC-Chat] Observer attached to #chatList'); _con?.log('[KCC-Chat] Observer attached to #chatList');
return true; return true;
} }