- Channels: create, edit, delete, link and move (drag and drop for channels and people), right-click on free space in the channel list - Permission editor with inherited rules, allow/deny per permission, groups and members; menus grey out actions the server does not allow - Notification sounds for connection, channel, chat, mute and push-to-talk events; custom files and TeamSpeak 3 style sound packs (settings.ini), stored in IndexedDB - Color schemes: Dark, Light, Midnight, Frost and Windows 95 - Public server list with live UDP ping, fetched through Chromium's network stack - Kick, ban and connection-lost card with Reconnect - Right-click menus for text fields, links, images and message authors - Formatting toolbar in the message box, shared with the description editor - Resizable channel list, menu headers, registration from the self menu only - Fix: client-side pacing now mirrors Murmur's whole-second leaky bucket exactly, so quick actions are no longer dropped silently Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
64 lines
2.8 KiB
TypeScript
64 lines
2.8 KiB
TypeScript
import { native } from './native.ts';
|
|
import { menu, type MenuItem } from './menu.svelte.ts';
|
|
|
|
// Right-click menu for text fields, messages, links and images. Elements with their own menu
|
|
// (users, channels, servers) call preventDefault, so Electron does not report those.
|
|
export function startTextMenu(): void {
|
|
if (!native) return;
|
|
const api = native;
|
|
|
|
api.onContextMenu(p => {
|
|
// Edit commands act on the focused element; keep it focused through the menu
|
|
const field = p.isEditable ? (document.activeElement as HTMLElement | null) : null;
|
|
const edit = (action: string, arg?: unknown) => { field?.focus(); api.editAction(action, arg); };
|
|
const items: MenuItem[] = [];
|
|
const add = (group: MenuItem[]) => {
|
|
if (!group.length) return;
|
|
if (items.length) items.push({ separator: true, label: '' });
|
|
items.push(...group);
|
|
};
|
|
|
|
if (p.misspelledWord) {
|
|
add([
|
|
...p.suggestions.map(s => ({ label: s, action: () => edit('replaceMisspelling', s) })),
|
|
...(p.suggestions.length ? [] : [{ label: 'No suggestions', disabled: true }]),
|
|
{ label: 'Add to dictionary', action: () => api.editAction('addWord', p.misspelledWord) }
|
|
]);
|
|
}
|
|
|
|
if (p.isEditable) {
|
|
const f = p.editFlags;
|
|
add([
|
|
{ label: 'Undo', disabled: !f.canUndo, action: () => edit('undo') },
|
|
{ label: 'Redo', disabled: !f.canRedo, action: () => edit('redo') }
|
|
]);
|
|
add([
|
|
{ label: 'Cut', disabled: !f.canCut, action: () => edit('cut') },
|
|
{ label: 'Copy', disabled: !f.canCopy, action: () => edit('copy') },
|
|
{ label: 'Paste', disabled: !f.canPaste, action: () => edit('paste') }
|
|
]);
|
|
add([{ label: 'Select all', disabled: !f.canSelectAll, action: () => edit('selectAll') }]);
|
|
} else {
|
|
if (p.selectionText.trim()) add([{ label: 'Copy', action: () => api.editAction('copy') }]);
|
|
if (p.linkURL && /^https?:/i.test(p.linkURL)) {
|
|
add([
|
|
{ label: 'Open link', action: () => window.open(p.linkURL, '_blank', 'noopener') },
|
|
{ label: 'Copy link address', action: () => navigator.clipboard.writeText(p.linkURL) }
|
|
]);
|
|
}
|
|
if (p.mediaType === 'image' && p.srcURL) {
|
|
add([
|
|
{ label: 'Copy image', action: () => api.editAction('copyImageAt', { x: p.x, y: p.y }) },
|
|
{ label: 'Save image...', action: () => api.editAction('saveImage', p.srcURL) }
|
|
]);
|
|
}
|
|
// The message under the pointer, as plain text
|
|
const msg = document.elementFromPoint(p.x, p.y)?.closest('.msg, .system');
|
|
const text = msg?.querySelector('.html')?.textContent?.trim();
|
|
if (text) add([{ label: 'Copy message text', action: () => navigator.clipboard.writeText(text) }]);
|
|
}
|
|
|
|
if (items.length) menu.showAt(p.x, p.y, items, true);
|
|
});
|
|
}
|