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