diff --git a/Client/tauri-client/src/components/AdminActions.ts b/Client/tauri-client/src/components/AdminActions.ts index 4432b556..40c390c0 100644 --- a/Client/tauri-client/src/components/AdminActions.ts +++ b/Client/tauri-client/src/components/AdminActions.ts @@ -60,53 +60,73 @@ function withConfirmation( let confirming = false; const originalLabel = item.textContent ?? ""; - item.addEventListener("click", (e) => { - e.stopPropagation(); - if (confirming) { - confirming = false; - setText(item, originalLabel); - onConfirm(); - } else { - confirming = true; - setText(item, confirmLabel); - } - }, { signal }); + item.addEventListener( + "click", + (e) => { + e.stopPropagation(); + if (confirming) { + confirming = false; + setText(item, originalLabel); + onConfirm(); + } else { + confirming = true; + setText(item, confirmLabel); + } + }, + { signal }, + ); } // --------------------------------------------------------------------------- // Member Context Menu // --------------------------------------------------------------------------- -export function createMemberContextMenu( - options: MemberContextMenuOptions, -): ContextMenuResult { +export function createMemberContextMenu(options: MemberContextMenuOptions): ContextMenuResult { const ac = new AbortController(); const menu = createElement("div", { class: "context-menu" }); // Role submenu trigger - const roleItem = createElement("div", { - class: "context-menu__item", - }, "Change Role"); + const roleItem = createElement( + "div", + { + class: "context-menu__item", + }, + "Change Role", + ); const roleSub = createElement("div", { class: "context-menu__submenu" }); for (const role of options.availableRoles) { - const cls = role === options.currentRole - ? "context-menu__item context-menu__item--active" - : "context-menu__item"; - const roleOption = createMenuItem(role, cls, () => { - if (role !== options.currentRole) { - void options.onChangeRole(role); - } - }, ac.signal); + const cls = + role === options.currentRole + ? "context-menu__item context-menu__item--active" + : "context-menu__item"; + const roleOption = createMenuItem( + role, + cls, + () => { + if (role !== options.currentRole) { + void options.onChangeRole(role); + } + }, + ac.signal, + ); roleSub.appendChild(roleOption); } - roleItem.addEventListener("mouseenter", () => { - roleSub.style.display = ""; - }, { signal: ac.signal }); - roleItem.addEventListener("mouseleave", () => { - roleSub.style.display = "none"; - }, { signal: ac.signal }); + roleItem.addEventListener( + "mouseenter", + () => { + roleSub.style.display = ""; + }, + { signal: ac.signal }, + ); + roleItem.addEventListener( + "mouseleave", + () => { + roleSub.style.display = "none"; + }, + { signal: ac.signal }, + ); roleSub.style.display = "none"; appendChildren(roleItem, roleSub); @@ -115,21 +135,39 @@ export function createMemberContextMenu( menu.appendChild(createSeparator()); // Kick with confirmation - const kickItem = createElement("div", { - class: "context-menu__item context-menu__item--danger", - }, "Kick"); - withConfirmation(kickItem, "Are you sure?", () => { - void options.onKick(); - }, ac.signal); + const kickItem = createElement( + "div", + { + class: "context-menu__item context-menu__item--danger", + }, + "Kick", + ); + withConfirmation( + kickItem, + "Are you sure?", + () => { + void options.onKick(); + }, + ac.signal, + ); menu.appendChild(kickItem); // Ban with confirmation - const banItem = createElement("div", { - class: "context-menu__item context-menu__item--danger", - }, "Ban"); - withConfirmation(banItem, "Are you sure?", () => { - void options.onBan(); - }, ac.signal); + const banItem = createElement( + "div", + { + class: "context-menu__item context-menu__item--danger", + }, + "Ban", + ); + withConfirmation( + banItem, + "Are you sure?", + () => { + void options.onBan(); + }, + ac.signal, + ); menu.appendChild(banItem); function destroy(): void { @@ -144,9 +182,7 @@ export function createMemberContextMenu( // Channel Context Menu // --------------------------------------------------------------------------- -export function createChannelContextMenu( - options: ChannelContextMenuOptions, -): ContextMenuResult { +export function createChannelContextMenu(options: ChannelContextMenuOptions): ContextMenuResult { const ac = new AbortController(); const menu = createElement("div", { class: "context-menu" }); @@ -171,12 +207,21 @@ export function createChannelContextMenu( menu.appendChild(createSeparator()); // Delete Channel with confirmation - const deleteItem = createElement("div", { - class: "context-menu__item context-menu__item--danger", - }, "Delete Channel"); - withConfirmation(deleteItem, "Are you sure?", () => { - void options.onDelete(); - }, ac.signal); + const deleteItem = createElement( + "div", + { + class: "context-menu__item context-menu__item--danger", + }, + "Delete Channel", + ); + withConfirmation( + deleteItem, + "Are you sure?", + () => { + void options.onDelete(); + }, + ac.signal, + ); menu.appendChild(deleteItem); function destroy(): void { diff --git a/Client/tauri-client/src/components/CertMismatchModal.ts b/Client/tauri-client/src/components/CertMismatchModal.ts index ad5c6162..73638c87 100644 --- a/Client/tauri-client/src/components/CertMismatchModal.ts +++ b/Client/tauri-client/src/components/CertMismatchModal.ts @@ -18,9 +18,7 @@ export interface CertMismatchModalOptions { readonly onReject: () => void; } -export function createCertMismatchModal( - options: CertMismatchModalOptions, -): MountableComponent { +export function createCertMismatchModal(options: CertMismatchModalOptions): MountableComponent { const { host, storedFingerprint, newFingerprint, onAccept, onReject } = options; let overlay: HTMLDivElement | null = null; const ac = new AbortController(); @@ -52,8 +50,8 @@ export function createCertMismatchModal( setText( desc, "The server's TLS certificate fingerprint has changed. " + - "This could mean the server regenerated its certificate, " + - "or it could indicate a security issue.", + "This could mean the server regenerated its certificate, " + + "or it could indicate a security issue.", ); const details = createElement("div", { class: "cert-details" }); @@ -110,11 +108,7 @@ export function createCertMismatchModal( return { mount, destroy }; } -function buildRow( - label: string, - value: string, - isFingerprint: boolean, -): HTMLDivElement { +function buildRow(label: string, value: string, isFingerprint: boolean): HTMLDivElement { const row = createElement("div", { class: "cert-row" }); const labelEl = createElement("span", { class: "cert-label" }); setText(labelEl, label); diff --git a/Client/tauri-client/src/components/ConnectedOverlay.ts b/Client/tauri-client/src/components/ConnectedOverlay.ts index b8b0de7c..ceea74bb 100644 --- a/Client/tauri-client/src/components/ConnectedOverlay.ts +++ b/Client/tauri-client/src/components/ConnectedOverlay.ts @@ -27,8 +27,14 @@ const READY_DELAY_MS = 800; function serverIconColor(name: string): string { const palette = [ - "#5865f2", "#57f287", "#fee75c", "#eb459e", - "#ed4245", "#f0b232", "#2ecc71", "#e74c3c", + "#5865f2", + "#57f287", + "#fee75c", + "#eb459e", + "#ed4245", + "#f0b232", + "#2ecc71", + "#e74c3c", ] as const; let hash = 0; for (let i = 0; i < name.length; i++) { @@ -37,14 +43,15 @@ function serverIconColor(name: string): string { return palette[Math.abs(hash) % palette.length] ?? palette[0]; } -export function createConnectedOverlay( - options: ConnectedOverlayOptions, -): ConnectedOverlayControl { +export function createConnectedOverlay(options: ConnectedOverlayOptions): ConnectedOverlayControl { const { serverName, username, motd, onReady } = options; const ac = new AbortController(); // Root overlay (hidden by default, .visible to show) - const overlay = createElement("div", { class: "connected-overlay", "data-testid": "connected-overlay" }); + const overlay = createElement("div", { + class: "connected-overlay", + "data-testid": "connected-overlay", + }); // Server icon with check badge const iconWrap = createElement("div", { class: "connected-icon-wrap" }); @@ -70,13 +77,21 @@ export function createConnectedOverlay( appendChildren(iconWrap, srvIcon, checkBadge); // Text elements - const connectedText = createElement("div", { - class: "connected-text", - }, "Connected!"); + const connectedText = createElement( + "div", + { + class: "connected-text", + }, + "Connected!", + ); - const userText = createElement("div", { - class: "connected-user", - }, `Logged in as ${username}`); + const userText = createElement( + "div", + { + class: "connected-user", + }, + `Logged in as ${username}`, + ); const motdEl = createElement("div", { class: "connected-motd" }); if (motd) { diff --git a/Client/tauri-client/src/components/CreateChannelModal.ts b/Client/tauri-client/src/components/CreateChannelModal.ts index 4c46e1fe..4622835b 100644 --- a/Client/tauri-client/src/components/CreateChannelModal.ts +++ b/Client/tauri-client/src/components/CreateChannelModal.ts @@ -14,11 +14,7 @@ export interface CreateChannelModalOptions { /** The category this channel will be created under. */ readonly category: string; /** Called when the user submits the form. */ - readonly onCreate: (data: { - name: string; - type: ChannelType; - category: string; - }) => Promise; + readonly onCreate: (data: { name: string; type: ChannelType; category: string }) => Promise; /** Called when the modal is closed without creating. */ readonly onClose: () => void; } @@ -29,18 +25,14 @@ export function isVoiceCategory(category: string): boolean { } /** Returns the allowed channel types for a given category. */ -export function allowedTypesForCategory( - category: string, -): readonly ChannelType[] { +export function allowedTypesForCategory(category: string): readonly ChannelType[] { if (isVoiceCategory(category)) { return ["voice"] as const; } return ["text", "announcement"] as const; } -export function createCreateChannelModal( - options: CreateChannelModalOptions, -): MountableComponent { +export function createCreateChannelModal(options: CreateChannelModalOptions): MountableComponent { const { category, onCreate, onClose } = options; const ac = new AbortController(); let overlay: HTMLDivElement | null = null; @@ -72,11 +64,7 @@ export function createCreateChannelModal( // Category (read-only display) const categoryGroup = createElement("div", { class: "form-group" }); - const categoryLabel = createElement( - "label", - { class: "form-label" }, - "Category", - ); + const categoryLabel = createElement("label", { class: "form-label" }, "Category"); const categoryDisplay = createElement("div", { class: "form-input", style: "opacity: 0.7; cursor: default;", @@ -104,11 +92,7 @@ export function createCreateChannelModal( }); for (const t of allowedTypes) { - const opt = createElement( - "option", - { value: t }, - t.charAt(0).toUpperCase() + t.slice(1), - ); + const opt = createElement("option", { value: t }, t.charAt(0).toUpperCase() + t.slice(1)); typeSelect.appendChild(opt); } appendChildren(typeGroup, typeLabel, typeSelect); @@ -166,10 +150,7 @@ export function createCreateChannelModal( }); } catch (err) { errorEl.style.display = "block"; - setText( - errorEl, - err instanceof Error ? err.message : "Failed to create channel", - ); + setText(errorEl, err instanceof Error ? err.message : "Failed to create channel"); createBtn.removeAttribute("disabled"); setText(createBtn, "Create Channel"); } diff --git a/Client/tauri-client/src/components/DeleteChannelModal.ts b/Client/tauri-client/src/components/DeleteChannelModal.ts index 8ee3c524..a125696a 100644 --- a/Client/tauri-client/src/components/DeleteChannelModal.ts +++ b/Client/tauri-client/src/components/DeleteChannelModal.ts @@ -14,9 +14,7 @@ export interface DeleteChannelModalOptions { readonly onClose: () => void; } -export function createDeleteChannelModal( - options: DeleteChannelModalOptions, -): MountableComponent { +export function createDeleteChannelModal(options: DeleteChannelModalOptions): MountableComponent { const { channelName, onConfirm, onClose } = options; const ac = new AbortController(); let overlay: HTMLDivElement | null = null; @@ -88,10 +86,7 @@ export function createDeleteChannelModal( await onConfirm(); } catch (err) { errorEl.style.display = "block"; - setText( - errorEl, - err instanceof Error ? err.message : "Failed to delete channel", - ); + setText(errorEl, err instanceof Error ? err.message : "Failed to delete channel"); deleteBtn.removeAttribute("disabled"); setText(deleteBtn, "Delete Channel"); } diff --git a/Client/tauri-client/src/components/DmSidebar.ts b/Client/tauri-client/src/components/DmSidebar.ts index 9c27182a..75819595 100644 --- a/Client/tauri-client/src/components/DmSidebar.ts +++ b/Client/tauri-client/src/components/DmSidebar.ts @@ -8,11 +8,7 @@ * dm-name, dm-close, dm-unread. */ -import { - createElement, - setText, - appendChildren, -} from "@lib/dom"; +import { createElement, setText, appendChildren } from "@lib/dom"; import { createIcon } from "@lib/icons"; import type { MountableComponent } from "@lib/safe-render"; @@ -111,16 +107,20 @@ function renderDmItem( item.appendChild(unreadDot); } - item.addEventListener("click", () => { - const parent = item.parentElement; - if (parent !== null) { - for (const sibling of parent.querySelectorAll(".dm-item.active")) { - sibling.classList.remove("active"); + item.addEventListener( + "click", + () => { + const parent = item.parentElement; + if (parent !== null) { + for (const sibling of parent.querySelectorAll(".dm-item.active")) { + sibling.classList.remove("active"); + } } - } - item.classList.add("active"); - onSelect(convo.userId); - }, { signal }); + item.classList.add("active"); + onSelect(convo.userId); + }, + { signal }, + ); return item; } @@ -142,8 +142,11 @@ export function createDmSidebar(options: DmSidebarOptions): MountableComponent { }); const arrow = createElement("span", { class: "dm-back-arrow" }, "\u2190"); const backInfo = createElement("div", { class: "dm-back-info" }); - const backTitle = createElement("div", { class: "dm-back-title" }, - `Back to ${options.serverName ?? "Server"}`); + const backTitle = createElement( + "div", + { class: "dm-back-title" }, + `Back to ${options.serverName ?? "Server"}`, + ); const backSub = createElement("div", { class: "dm-back-subtitle" }, "Return to channels"); appendChildren(backInfo, backTitle, backSub); appendChildren(backHeader, arrow, backInfo); diff --git a/Client/tauri-client/src/components/EditChannelModal.ts b/Client/tauri-client/src/components/EditChannelModal.ts index 6da4b1ed..3a60ad06 100644 --- a/Client/tauri-client/src/components/EditChannelModal.ts +++ b/Client/tauri-client/src/components/EditChannelModal.ts @@ -20,9 +20,7 @@ export interface EditChannelModalOptions { readonly onClose: () => void; } -export function createEditChannelModal( - options: EditChannelModalOptions, -): MountableComponent { +export function createEditChannelModal(options: EditChannelModalOptions): MountableComponent { const { channelName, channelType, onSave, onClose } = options; const ac = new AbortController(); let overlay: HTMLDivElement | null = null; @@ -120,10 +118,7 @@ export function createEditChannelModal( await onSave({ name }); } catch (err) { errorEl.style.display = "block"; - setText( - errorEl, - err instanceof Error ? err.message : "Failed to update channel", - ); + setText(errorEl, err instanceof Error ? err.message : "Failed to update channel"); saveBtn.removeAttribute("disabled"); setText(saveBtn, "Save Changes"); } diff --git a/Client/tauri-client/src/components/EmojiPicker.ts b/Client/tauri-client/src/components/EmojiPicker.ts index 741a02dc..25f85381 100644 --- a/Client/tauri-client/src/components/EmojiPicker.ts +++ b/Client/tauri-client/src/components/EmojiPicker.ts @@ -35,128 +35,457 @@ const CATEGORIES: readonly EmojiCategory[] = [ { name: "Smileys", emoji: [ - "๐Ÿ˜€", "๐Ÿ˜ƒ", "๐Ÿ˜„", "๐Ÿ˜", "๐Ÿ˜†", "๐Ÿ˜…", "๐Ÿคฃ", "๐Ÿ˜‚", "๐Ÿ™‚", "๐Ÿ˜Š", - "๐Ÿ˜‡", "๐Ÿฅฐ", "๐Ÿ˜", "๐Ÿคฉ", "๐Ÿ˜˜", "๐Ÿ˜—", "๐Ÿ˜‹", "๐Ÿ˜›", "๐Ÿ˜œ", "๐Ÿคช", - "๐Ÿ˜", "๐Ÿค‘", "๐Ÿค—", "๐Ÿคญ", "๐Ÿคซ", "๐Ÿค”", "๐Ÿค", "๐Ÿคจ", "๐Ÿ˜", "๐Ÿ˜‘", - "๐Ÿ˜ถ", "๐Ÿ˜", "๐Ÿ˜’", "๐Ÿ™„", "๐Ÿ˜ฌ", "๐Ÿคฅ", "๐Ÿ˜Œ", "๐Ÿ˜”", "๐Ÿ˜ช", "๐Ÿคค", - "๐Ÿ˜ด", "๐Ÿ˜ท", "๐Ÿค’", "๐Ÿค•", "๐Ÿคข", "๐Ÿคฎ", "๐Ÿฅต", "๐Ÿฅถ", "๐Ÿฅด", "๐Ÿ˜ต", - "๐Ÿคฏ", "๐Ÿค ", "๐Ÿฅณ", "๐Ÿ˜Ž", "๐Ÿค“", "๐Ÿง", "๐Ÿ˜•", "๐Ÿ˜Ÿ", "๐Ÿ™", "๐Ÿ˜ฎ", - "๐Ÿ˜ฒ", "๐Ÿ˜ณ", "๐Ÿฅบ", "๐Ÿ˜ข", "๐Ÿ˜ญ", "๐Ÿ˜ค", "๐Ÿ˜ ", "๐Ÿ˜ก", "๐Ÿคฌ", "๐Ÿ’€", + "๐Ÿ˜€", + "๐Ÿ˜ƒ", + "๐Ÿ˜„", + "๐Ÿ˜", + "๐Ÿ˜†", + "๐Ÿ˜…", + "๐Ÿคฃ", + "๐Ÿ˜‚", + "๐Ÿ™‚", + "๐Ÿ˜Š", + "๐Ÿ˜‡", + "๐Ÿฅฐ", + "๐Ÿ˜", + "๐Ÿคฉ", + "๐Ÿ˜˜", + "๐Ÿ˜—", + "๐Ÿ˜‹", + "๐Ÿ˜›", + "๐Ÿ˜œ", + "๐Ÿคช", + "๐Ÿ˜", + "๐Ÿค‘", + "๐Ÿค—", + "๐Ÿคญ", + "๐Ÿคซ", + "๐Ÿค”", + "๐Ÿค", + "๐Ÿคจ", + "๐Ÿ˜", + "๐Ÿ˜‘", + "๐Ÿ˜ถ", + "๐Ÿ˜", + "๐Ÿ˜’", + "๐Ÿ™„", + "๐Ÿ˜ฌ", + "๐Ÿคฅ", + "๐Ÿ˜Œ", + "๐Ÿ˜”", + "๐Ÿ˜ช", + "๐Ÿคค", + "๐Ÿ˜ด", + "๐Ÿ˜ท", + "๐Ÿค’", + "๐Ÿค•", + "๐Ÿคข", + "๐Ÿคฎ", + "๐Ÿฅต", + "๐Ÿฅถ", + "๐Ÿฅด", + "๐Ÿ˜ต", + "๐Ÿคฏ", + "๐Ÿค ", + "๐Ÿฅณ", + "๐Ÿ˜Ž", + "๐Ÿค“", + "๐Ÿง", + "๐Ÿ˜•", + "๐Ÿ˜Ÿ", + "๐Ÿ™", + "๐Ÿ˜ฎ", + "๐Ÿ˜ฒ", + "๐Ÿ˜ณ", + "๐Ÿฅบ", + "๐Ÿ˜ข", + "๐Ÿ˜ญ", + "๐Ÿ˜ค", + "๐Ÿ˜ ", + "๐Ÿ˜ก", + "๐Ÿคฌ", + "๐Ÿ’€", ], }, { name: "People", emoji: [ - "๐Ÿ‘‹", "๐Ÿคš", "๐Ÿ–", "โœ‹", "๐Ÿ––", "๐Ÿ‘Œ", "๐ŸคŒ", "๐Ÿค", "โœŒ๏ธ", "๐Ÿคž", - "๐ŸคŸ", "๐Ÿค˜", "๐Ÿค™", "๐Ÿ‘ˆ", "๐Ÿ‘‰", "๐Ÿ‘†", "๐Ÿ‘‡", "โ˜๏ธ", "๐Ÿ‘", "๐Ÿ‘Ž", - "โœŠ", "๐Ÿ‘Š", "๐Ÿค›", "๐Ÿคœ", "๐Ÿ‘", "๐Ÿ™Œ", "๐Ÿ‘", "๐Ÿคฒ", "๐Ÿค", "๐Ÿ™", + "๐Ÿ‘‹", + "๐Ÿคš", + "๐Ÿ–", + "โœ‹", + "๐Ÿ––", + "๐Ÿ‘Œ", + "๐ŸคŒ", + "๐Ÿค", + "โœŒ๏ธ", + "๐Ÿคž", + "๐ŸคŸ", + "๐Ÿค˜", + "๐Ÿค™", + "๐Ÿ‘ˆ", + "๐Ÿ‘‰", + "๐Ÿ‘†", + "๐Ÿ‘‡", + "โ˜๏ธ", + "๐Ÿ‘", + "๐Ÿ‘Ž", + "โœŠ", + "๐Ÿ‘Š", + "๐Ÿค›", + "๐Ÿคœ", + "๐Ÿ‘", + "๐Ÿ™Œ", + "๐Ÿ‘", + "๐Ÿคฒ", + "๐Ÿค", + "๐Ÿ™", ], }, { name: "Nature", emoji: [ - "๐Ÿถ", "๐Ÿฑ", "๐Ÿญ", "๐Ÿน", "๐Ÿฐ", "๐ŸฆŠ", "๐Ÿป", "๐Ÿผ", "๐Ÿจ", "๐Ÿฏ", - "๐Ÿฆ", "๐Ÿฎ", "๐Ÿท", "๐Ÿธ", "๐Ÿต", "๐Ÿ”", "๐Ÿง", "๐Ÿฆ", "๐Ÿค", "๐Ÿฆ„", - "๐ŸŒธ", "๐ŸŒน", "๐ŸŒบ", "๐ŸŒป", "๐ŸŒผ", "๐ŸŒท", "๐ŸŒฑ", "๐ŸŒฒ", "๐ŸŒณ", "๐Ÿ€", + "๐Ÿถ", + "๐Ÿฑ", + "๐Ÿญ", + "๐Ÿน", + "๐Ÿฐ", + "๐ŸฆŠ", + "๐Ÿป", + "๐Ÿผ", + "๐Ÿจ", + "๐Ÿฏ", + "๐Ÿฆ", + "๐Ÿฎ", + "๐Ÿท", + "๐Ÿธ", + "๐Ÿต", + "๐Ÿ”", + "๐Ÿง", + "๐Ÿฆ", + "๐Ÿค", + "๐Ÿฆ„", + "๐ŸŒธ", + "๐ŸŒน", + "๐ŸŒบ", + "๐ŸŒป", + "๐ŸŒผ", + "๐ŸŒท", + "๐ŸŒฑ", + "๐ŸŒฒ", + "๐ŸŒณ", + "๐Ÿ€", ], }, { name: "Food", emoji: [ - "๐ŸŽ", "๐ŸŠ", "๐Ÿ‹", "๐ŸŒ", "๐Ÿ‰", "๐Ÿ‡", "๐Ÿ“", "๐Ÿ’", "๐Ÿ‘", "๐Ÿ", - "๐Ÿฅ", "๐Ÿ”", "๐ŸŸ", "๐Ÿ•", "๐ŸŒญ", "๐Ÿฟ", "๐Ÿง€", "๐Ÿฅš", "๐Ÿณ", "๐Ÿฅ“", - "โ˜•", "๐Ÿต", "๐Ÿบ", "๐Ÿป", "๐Ÿฅ‚", "๐Ÿท", "๐Ÿธ", "๐Ÿน", "๐Ÿพ", "๐Ÿง", + "๐ŸŽ", + "๐ŸŠ", + "๐Ÿ‹", + "๐ŸŒ", + "๐Ÿ‰", + "๐Ÿ‡", + "๐Ÿ“", + "๐Ÿ’", + "๐Ÿ‘", + "๐Ÿ", + "๐Ÿฅ", + "๐Ÿ”", + "๐ŸŸ", + "๐Ÿ•", + "๐ŸŒญ", + "๐Ÿฟ", + "๐Ÿง€", + "๐Ÿฅš", + "๐Ÿณ", + "๐Ÿฅ“", + "โ˜•", + "๐Ÿต", + "๐Ÿบ", + "๐Ÿป", + "๐Ÿฅ‚", + "๐Ÿท", + "๐Ÿธ", + "๐Ÿน", + "๐Ÿพ", + "๐Ÿง", ], }, { name: "Objects", emoji: [ - "โšฝ", "๐Ÿ€", "๐Ÿˆ", "โšพ", "๐ŸŽพ", "๐ŸŽฎ", "๐ŸŽฒ", "๐ŸŽฏ", "๐ŸŽต", "๐ŸŽถ", - "๐Ÿ’ก", "๐Ÿ”ฅ", "โญ", "๐ŸŒŸ", "๐Ÿ’ซ", "โœจ", "๐Ÿ’ฅ", "โค๏ธ", "๐Ÿงก", "๐Ÿ’›", - "๐Ÿ’š", "๐Ÿ’™", "๐Ÿ’œ", "๐Ÿ–ค", "๐Ÿค", "๐Ÿ’ฏ", "๐Ÿ’ข", "๐Ÿ’ฌ", "๐Ÿ‘โ€๐Ÿ—จ", "๐Ÿ—จ", + "โšฝ", + "๐Ÿ€", + "๐Ÿˆ", + "โšพ", + "๐ŸŽพ", + "๐ŸŽฎ", + "๐ŸŽฒ", + "๐ŸŽฏ", + "๐ŸŽต", + "๐ŸŽถ", + "๐Ÿ’ก", + "๐Ÿ”ฅ", + "โญ", + "๐ŸŒŸ", + "๐Ÿ’ซ", + "โœจ", + "๐Ÿ’ฅ", + "โค๏ธ", + "๐Ÿงก", + "๐Ÿ’›", + "๐Ÿ’š", + "๐Ÿ’™", + "๐Ÿ’œ", + "๐Ÿ–ค", + "๐Ÿค", + "๐Ÿ’ฏ", + "๐Ÿ’ข", + "๐Ÿ’ฌ", + "๐Ÿ‘โ€๐Ÿ—จ", + "๐Ÿ—จ", ], }, { name: "Symbols", emoji: [ - "โœ…", "โŒ", "โ“", "โ—", "โ€ผ๏ธ", "โ‰๏ธ", "๐Ÿ’ค", "๐Ÿ’ฎ", "โ™ป๏ธ", "๐Ÿ”ฐ", - "โš ๏ธ", "๐Ÿšซ", "๐Ÿ”ด", "๐ŸŸ ", "๐ŸŸก", "๐ŸŸข", "๐Ÿ”ต", "๐ŸŸฃ", "โšซ", "โšช", + "โœ…", + "โŒ", + "โ“", + "โ—", + "โ€ผ๏ธ", + "โ‰๏ธ", + "๐Ÿ’ค", + "๐Ÿ’ฎ", + "โ™ป๏ธ", + "๐Ÿ”ฐ", + "โš ๏ธ", + "๐Ÿšซ", + "๐Ÿ”ด", + "๐ŸŸ ", + "๐ŸŸก", + "๐ŸŸข", + "๐Ÿ”ต", + "๐ŸŸฃ", + "โšซ", + "โšช", ], }, ]; /** Emoji name lookup for search. Maps emoji character โ†’ searchable keywords. */ const EMOJI_NAMES: Readonly> = { - "๐Ÿ˜€": "grinning face happy smile", "๐Ÿ˜ƒ": "smiley face happy smile", "๐Ÿ˜„": "smile happy grin", - "๐Ÿ˜": "beaming grin teeth smile", "๐Ÿ˜†": "laughing happy squint smile", "๐Ÿ˜…": "sweat smile nervous", - "๐Ÿคฃ": "rofl laughing rolling floor", "๐Ÿ˜‚": "joy tears laughing cry happy", "๐Ÿ™‚": "slightly smiling", - "๐Ÿ˜Š": "blush happy smile shy", "๐Ÿ˜‡": "innocent angel halo", "๐Ÿฅฐ": "love hearts face smiling", - "๐Ÿ˜": "heart eyes love", "๐Ÿคฉ": "star struck excited", "๐Ÿ˜˜": "kiss blowing wink", - "๐Ÿ˜—": "kissing face", "๐Ÿ˜‹": "yummy delicious tongue food", "๐Ÿ˜›": "tongue out", - "๐Ÿ˜œ": "wink tongue playful", "๐Ÿคช": "zany crazy wild", "๐Ÿ˜": "squinting tongue", - "๐Ÿค‘": "money face rich dollar", "๐Ÿค—": "hugging hug hands", "๐Ÿคญ": "hand over mouth oops giggle", - "๐Ÿคซ": "shushing quiet secret shh", "๐Ÿค”": "thinking hmm wonder", "๐Ÿค": "zipper mouth shut secret", - "๐Ÿคจ": "raised eyebrow skeptical", "๐Ÿ˜": "neutral face blank", "๐Ÿ˜‘": "expressionless blank", - "๐Ÿ˜ถ": "no mouth silent mute", "๐Ÿ˜": "smirk smug", "๐Ÿ˜’": "unamused bored annoyed", - "๐Ÿ™„": "eye roll whatever", "๐Ÿ˜ฌ": "grimace awkward teeth", "๐Ÿคฅ": "lying pinocchio nose", - "๐Ÿ˜Œ": "relieved calm peaceful", "๐Ÿ˜”": "pensive sad thoughtful", "๐Ÿ˜ช": "sleepy tired", - "๐Ÿคค": "drooling hungry", "๐Ÿ˜ด": "sleeping zzz tired", "๐Ÿ˜ท": "mask sick medical face", - "๐Ÿค’": "thermometer sick fever", "๐Ÿค•": "bandage hurt injured", "๐Ÿคข": "nauseous sick green", - "๐Ÿคฎ": "vomiting throw up sick", "๐Ÿฅต": "hot face overheated", "๐Ÿฅถ": "cold face freezing", - "๐Ÿฅด": "woozy drunk dizzy", "๐Ÿ˜ต": "dizzy spiral knocked out", "๐Ÿคฏ": "mind blown exploding head", - "๐Ÿค ": "cowboy hat yeehaw", "๐Ÿฅณ": "party celebration birthday", "๐Ÿ˜Ž": "sunglasses cool", - "๐Ÿค“": "nerd glasses geek", "๐Ÿง": "monocle detective inspect", "๐Ÿ˜•": "confused puzzled", - "๐Ÿ˜Ÿ": "worried concerned", "๐Ÿ™": "frowning sad", "๐Ÿ˜ฎ": "open mouth surprised", - "๐Ÿ˜ฒ": "astonished shocked wow", "๐Ÿ˜ณ": "flushed embarrassed", "๐Ÿฅบ": "pleading puppy eyes please", - "๐Ÿ˜ข": "crying sad tear", "๐Ÿ˜ญ": "sobbing crying loud", "๐Ÿ˜ค": "steam nose angry huffing", - "๐Ÿ˜ ": "angry mad", "๐Ÿ˜ก": "rage furious red", "๐Ÿคฌ": "cursing swearing symbols angry", + "๐Ÿ˜€": "grinning face happy smile", + "๐Ÿ˜ƒ": "smiley face happy smile", + "๐Ÿ˜„": "smile happy grin", + "๐Ÿ˜": "beaming grin teeth smile", + "๐Ÿ˜†": "laughing happy squint smile", + "๐Ÿ˜…": "sweat smile nervous", + "๐Ÿคฃ": "rofl laughing rolling floor", + "๐Ÿ˜‚": "joy tears laughing cry happy", + "๐Ÿ™‚": "slightly smiling", + "๐Ÿ˜Š": "blush happy smile shy", + "๐Ÿ˜‡": "innocent angel halo", + "๐Ÿฅฐ": "love hearts face smiling", + "๐Ÿ˜": "heart eyes love", + "๐Ÿคฉ": "star struck excited", + "๐Ÿ˜˜": "kiss blowing wink", + "๐Ÿ˜—": "kissing face", + "๐Ÿ˜‹": "yummy delicious tongue food", + "๐Ÿ˜›": "tongue out", + "๐Ÿ˜œ": "wink tongue playful", + "๐Ÿคช": "zany crazy wild", + "๐Ÿ˜": "squinting tongue", + "๐Ÿค‘": "money face rich dollar", + "๐Ÿค—": "hugging hug hands", + "๐Ÿคญ": "hand over mouth oops giggle", + "๐Ÿคซ": "shushing quiet secret shh", + "๐Ÿค”": "thinking hmm wonder", + "๐Ÿค": "zipper mouth shut secret", + "๐Ÿคจ": "raised eyebrow skeptical", + "๐Ÿ˜": "neutral face blank", + "๐Ÿ˜‘": "expressionless blank", + "๐Ÿ˜ถ": "no mouth silent mute", + "๐Ÿ˜": "smirk smug", + "๐Ÿ˜’": "unamused bored annoyed", + "๐Ÿ™„": "eye roll whatever", + "๐Ÿ˜ฌ": "grimace awkward teeth", + "๐Ÿคฅ": "lying pinocchio nose", + "๐Ÿ˜Œ": "relieved calm peaceful", + "๐Ÿ˜”": "pensive sad thoughtful", + "๐Ÿ˜ช": "sleepy tired", + "๐Ÿคค": "drooling hungry", + "๐Ÿ˜ด": "sleeping zzz tired", + "๐Ÿ˜ท": "mask sick medical face", + "๐Ÿค’": "thermometer sick fever", + "๐Ÿค•": "bandage hurt injured", + "๐Ÿคข": "nauseous sick green", + "๐Ÿคฎ": "vomiting throw up sick", + "๐Ÿฅต": "hot face overheated", + "๐Ÿฅถ": "cold face freezing", + "๐Ÿฅด": "woozy drunk dizzy", + "๐Ÿ˜ต": "dizzy spiral knocked out", + "๐Ÿคฏ": "mind blown exploding head", + "๐Ÿค ": "cowboy hat yeehaw", + "๐Ÿฅณ": "party celebration birthday", + "๐Ÿ˜Ž": "sunglasses cool", + "๐Ÿค“": "nerd glasses geek", + "๐Ÿง": "monocle detective inspect", + "๐Ÿ˜•": "confused puzzled", + "๐Ÿ˜Ÿ": "worried concerned", + "๐Ÿ™": "frowning sad", + "๐Ÿ˜ฎ": "open mouth surprised", + "๐Ÿ˜ฒ": "astonished shocked wow", + "๐Ÿ˜ณ": "flushed embarrassed", + "๐Ÿฅบ": "pleading puppy eyes please", + "๐Ÿ˜ข": "crying sad tear", + "๐Ÿ˜ญ": "sobbing crying loud", + "๐Ÿ˜ค": "steam nose angry huffing", + "๐Ÿ˜ ": "angry mad", + "๐Ÿ˜ก": "rage furious red", + "๐Ÿคฌ": "cursing swearing symbols angry", "๐Ÿ’€": "skull dead death skeleton", - "๐Ÿ‘‹": "wave hello hi bye hand", "๐Ÿคš": "raised back hand", "๐Ÿ–": "hand fingers splayed five", - "โœ‹": "raised hand stop high five", "๐Ÿ––": "vulcan spock", "๐Ÿ‘Œ": "ok okay perfect", - "๐ŸคŒ": "pinched fingers italian", "๐Ÿค": "pinching small little", "โœŒ๏ธ": "peace victory two", - "๐Ÿคž": "crossed fingers luck hope", "๐ŸคŸ": "love you gesture rock", - "๐Ÿค˜": "rock on horns metal", "๐Ÿค™": "call me hang loose shaka", "๐Ÿ‘ˆ": "pointing left", - "๐Ÿ‘‰": "pointing right", "๐Ÿ‘†": "pointing up", "๐Ÿ‘‡": "pointing down", "โ˜๏ธ": "index pointing up", - "๐Ÿ‘": "thumbs up like good yes", "๐Ÿ‘Ž": "thumbs down dislike bad no", - "โœŠ": "raised fist power", "๐Ÿ‘Š": "fist bump punch", "๐Ÿค›": "left fist bump", - "๐Ÿคœ": "right fist bump", "๐Ÿ‘": "clap applause bravo", "๐Ÿ™Œ": "raising hands hooray celebrate", - "๐Ÿ‘": "open hands jazz", "๐Ÿคฒ": "palms up together prayer", "๐Ÿค": "handshake deal agreement", + "๐Ÿ‘‹": "wave hello hi bye hand", + "๐Ÿคš": "raised back hand", + "๐Ÿ–": "hand fingers splayed five", + "โœ‹": "raised hand stop high five", + "๐Ÿ––": "vulcan spock", + "๐Ÿ‘Œ": "ok okay perfect", + "๐ŸคŒ": "pinched fingers italian", + "๐Ÿค": "pinching small little", + "โœŒ๏ธ": "peace victory two", + "๐Ÿคž": "crossed fingers luck hope", + "๐ŸคŸ": "love you gesture rock", + "๐Ÿค˜": "rock on horns metal", + "๐Ÿค™": "call me hang loose shaka", + "๐Ÿ‘ˆ": "pointing left", + "๐Ÿ‘‰": "pointing right", + "๐Ÿ‘†": "pointing up", + "๐Ÿ‘‡": "pointing down", + "โ˜๏ธ": "index pointing up", + "๐Ÿ‘": "thumbs up like good yes", + "๐Ÿ‘Ž": "thumbs down dislike bad no", + "โœŠ": "raised fist power", + "๐Ÿ‘Š": "fist bump punch", + "๐Ÿค›": "left fist bump", + "๐Ÿคœ": "right fist bump", + "๐Ÿ‘": "clap applause bravo", + "๐Ÿ™Œ": "raising hands hooray celebrate", + "๐Ÿ‘": "open hands jazz", + "๐Ÿคฒ": "palms up together prayer", + "๐Ÿค": "handshake deal agreement", "๐Ÿ™": "pray thanks please folded hands", - "๐Ÿถ": "dog puppy pet", "๐Ÿฑ": "cat kitten pet", "๐Ÿญ": "mouse rat", "๐Ÿน": "hamster", - "๐Ÿฐ": "rabbit bunny", "๐ŸฆŠ": "fox", "๐Ÿป": "bear", "๐Ÿผ": "panda bear", - "๐Ÿจ": "koala", "๐Ÿฏ": "tiger", "๐Ÿฆ": "lion king", "๐Ÿฎ": "cow moo", - "๐Ÿท": "pig oink", "๐Ÿธ": "frog toad", "๐Ÿต": "monkey face", "๐Ÿ”": "chicken hen", - "๐Ÿง": "penguin", "๐Ÿฆ": "bird", "๐Ÿค": "chick baby bird", "๐Ÿฆ„": "unicorn magic", - "๐ŸŒธ": "cherry blossom flower pink", "๐ŸŒน": "rose flower red", "๐ŸŒบ": "hibiscus flower", - "๐ŸŒป": "sunflower", "๐ŸŒผ": "blossom flower", "๐ŸŒท": "tulip flower", - "๐ŸŒฑ": "seedling sprout plant", "๐ŸŒฒ": "evergreen tree pine", "๐ŸŒณ": "tree deciduous", "๐Ÿ€": "four leaf clover luck", - "๐ŸŽ": "red apple fruit", "๐ŸŠ": "orange tangerine fruit", "๐Ÿ‹": "lemon fruit", "๐ŸŒ": "banana fruit", - "๐Ÿ‰": "watermelon fruit", "๐Ÿ‡": "grapes fruit", "๐Ÿ“": "strawberry fruit", "๐Ÿ’": "cherries fruit", - "๐Ÿ‘": "peach fruit butt", "๐Ÿ": "pineapple fruit", "๐Ÿฅ": "kiwi fruit", - "๐Ÿ”": "hamburger burger food", "๐ŸŸ": "fries french food", "๐Ÿ•": "pizza food slice", - "๐ŸŒญ": "hot dog food", "๐Ÿฟ": "popcorn snack movie", "๐Ÿง€": "cheese wedge", - "๐Ÿฅš": "egg", "๐Ÿณ": "cooking fried egg", "๐Ÿฅ“": "bacon", - "โ˜•": "coffee hot drink", "๐Ÿต": "tea hot drink", "๐Ÿบ": "beer mug drink", - "๐Ÿป": "clinking beers cheers drink", "๐Ÿฅ‚": "champagne toast celebrate drink", - "๐Ÿท": "wine glass drink red", "๐Ÿธ": "cocktail martini drink", "๐Ÿน": "tropical drink", - "๐Ÿพ": "bottle popping champagne celebrate", "๐Ÿง": "cupcake dessert sweet", - "โšฝ": "soccer football ball sport", "๐Ÿ€": "basketball ball sport", "๐Ÿˆ": "football american sport", - "โšพ": "baseball ball sport", "๐ŸŽพ": "tennis ball sport", "๐ŸŽฎ": "video game controller gaming", - "๐ŸŽฒ": "dice game random", "๐ŸŽฏ": "bullseye target dart", "๐ŸŽต": "music note", - "๐ŸŽถ": "music notes", "๐Ÿ’ก": "light bulb idea", "๐Ÿ”ฅ": "fire hot flame lit", - "โญ": "star yellow", "๐ŸŒŸ": "glowing star sparkle", "๐Ÿ’ซ": "dizzy star shooting", - "โœจ": "sparkles magic shine", "๐Ÿ’ฅ": "boom collision crash", "โค๏ธ": "red heart love", - "๐Ÿงก": "orange heart love", "๐Ÿ’›": "yellow heart love", "๐Ÿ’š": "green heart love", - "๐Ÿ’™": "blue heart love", "๐Ÿ’œ": "purple heart love", "๐Ÿ–ค": "black heart dark love", - "๐Ÿค": "white heart love", "๐Ÿ’ฏ": "hundred percent perfect score", "๐Ÿ’ข": "anger symbol mad", - "๐Ÿ’ฌ": "speech bubble chat talk", "๐Ÿ‘โ€๐Ÿ—จ": "eye speech bubble witness", "๐Ÿ—จ": "speech balloon left", - "โœ…": "check mark yes done complete", "โŒ": "cross mark no wrong cancel", - "โ“": "question mark red", "โ—": "exclamation mark red alert", "โ€ผ๏ธ": "double exclamation", - "โ‰๏ธ": "exclamation question", "๐Ÿ’ค": "sleeping zzz tired", "๐Ÿ’ฎ": "white flower", - "โ™ป๏ธ": "recycle green environment", "๐Ÿ”ฐ": "beginner new japanese", "โš ๏ธ": "warning caution alert", - "๐Ÿšซ": "prohibited forbidden no", "๐Ÿ”ด": "red circle", "๐ŸŸ ": "orange circle", - "๐ŸŸก": "yellow circle", "๐ŸŸข": "green circle", "๐Ÿ”ต": "blue circle", - "๐ŸŸฃ": "purple circle", "โšซ": "black circle", "โšช": "white circle", + "๐Ÿถ": "dog puppy pet", + "๐Ÿฑ": "cat kitten pet", + "๐Ÿญ": "mouse rat", + "๐Ÿน": "hamster", + "๐Ÿฐ": "rabbit bunny", + "๐ŸฆŠ": "fox", + "๐Ÿป": "bear", + "๐Ÿผ": "panda bear", + "๐Ÿจ": "koala", + "๐Ÿฏ": "tiger", + "๐Ÿฆ": "lion king", + "๐Ÿฎ": "cow moo", + "๐Ÿท": "pig oink", + "๐Ÿธ": "frog toad", + "๐Ÿต": "monkey face", + "๐Ÿ”": "chicken hen", + "๐Ÿง": "penguin", + "๐Ÿฆ": "bird", + "๐Ÿค": "chick baby bird", + "๐Ÿฆ„": "unicorn magic", + "๐ŸŒธ": "cherry blossom flower pink", + "๐ŸŒน": "rose flower red", + "๐ŸŒบ": "hibiscus flower", + "๐ŸŒป": "sunflower", + "๐ŸŒผ": "blossom flower", + "๐ŸŒท": "tulip flower", + "๐ŸŒฑ": "seedling sprout plant", + "๐ŸŒฒ": "evergreen tree pine", + "๐ŸŒณ": "tree deciduous", + "๐Ÿ€": "four leaf clover luck", + "๐ŸŽ": "red apple fruit", + "๐ŸŠ": "orange tangerine fruit", + "๐Ÿ‹": "lemon fruit", + "๐ŸŒ": "banana fruit", + "๐Ÿ‰": "watermelon fruit", + "๐Ÿ‡": "grapes fruit", + "๐Ÿ“": "strawberry fruit", + "๐Ÿ’": "cherries fruit", + "๐Ÿ‘": "peach fruit butt", + "๐Ÿ": "pineapple fruit", + "๐Ÿฅ": "kiwi fruit", + "๐Ÿ”": "hamburger burger food", + "๐ŸŸ": "fries french food", + "๐Ÿ•": "pizza food slice", + "๐ŸŒญ": "hot dog food", + "๐Ÿฟ": "popcorn snack movie", + "๐Ÿง€": "cheese wedge", + "๐Ÿฅš": "egg", + "๐Ÿณ": "cooking fried egg", + "๐Ÿฅ“": "bacon", + "โ˜•": "coffee hot drink", + "๐Ÿต": "tea hot drink", + "๐Ÿบ": "beer mug drink", + "๐Ÿป": "clinking beers cheers drink", + "๐Ÿฅ‚": "champagne toast celebrate drink", + "๐Ÿท": "wine glass drink red", + "๐Ÿธ": "cocktail martini drink", + "๐Ÿน": "tropical drink", + "๐Ÿพ": "bottle popping champagne celebrate", + "๐Ÿง": "cupcake dessert sweet", + "โšฝ": "soccer football ball sport", + "๐Ÿ€": "basketball ball sport", + "๐Ÿˆ": "football american sport", + "โšพ": "baseball ball sport", + "๐ŸŽพ": "tennis ball sport", + "๐ŸŽฎ": "video game controller gaming", + "๐ŸŽฒ": "dice game random", + "๐ŸŽฏ": "bullseye target dart", + "๐ŸŽต": "music note", + "๐ŸŽถ": "music notes", + "๐Ÿ’ก": "light bulb idea", + "๐Ÿ”ฅ": "fire hot flame lit", + "โญ": "star yellow", + "๐ŸŒŸ": "glowing star sparkle", + "๐Ÿ’ซ": "dizzy star shooting", + "โœจ": "sparkles magic shine", + "๐Ÿ’ฅ": "boom collision crash", + "โค๏ธ": "red heart love", + "๐Ÿงก": "orange heart love", + "๐Ÿ’›": "yellow heart love", + "๐Ÿ’š": "green heart love", + "๐Ÿ’™": "blue heart love", + "๐Ÿ’œ": "purple heart love", + "๐Ÿ–ค": "black heart dark love", + "๐Ÿค": "white heart love", + "๐Ÿ’ฏ": "hundred percent perfect score", + "๐Ÿ’ข": "anger symbol mad", + "๐Ÿ’ฌ": "speech bubble chat talk", + "๐Ÿ‘โ€๐Ÿ—จ": "eye speech bubble witness", + "๐Ÿ—จ": "speech balloon left", + "โœ…": "check mark yes done complete", + "โŒ": "cross mark no wrong cancel", + "โ“": "question mark red", + "โ—": "exclamation mark red alert", + "โ€ผ๏ธ": "double exclamation", + "โ‰๏ธ": "exclamation question", + "๐Ÿ’ค": "sleeping zzz tired", + "๐Ÿ’ฎ": "white flower", + "โ™ป๏ธ": "recycle green environment", + "๐Ÿ”ฐ": "beginner new japanese", + "โš ๏ธ": "warning caution alert", + "๐Ÿšซ": "prohibited forbidden no", + "๐Ÿ”ด": "red circle", + "๐ŸŸ ": "orange circle", + "๐ŸŸก": "yellow circle", + "๐ŸŸข": "green circle", + "๐Ÿ”ต": "blue circle", + "๐ŸŸฃ": "purple circle", + "โšซ": "black circle", + "โšช": "white circle", }; const MAX_RECENT = 20; @@ -224,9 +553,7 @@ export function createEmojiPicker(options: EmojiPickerOptions): { // Build categories with recent + custom function getAllCategories(): readonly EmojiCategory[] { const recent = getRecentEmoji(); - const cats: EmojiCategory[] = [ - { name: "Recent", emoji: recent }, - ]; + const cats: EmojiCategory[] = [{ name: "Recent", emoji: recent }]; // Custom server emoji if (options.customEmoji && options.customEmoji.length > 0) { @@ -292,9 +619,13 @@ export function createEmojiPicker(options: EmojiPickerOptions): { // If nothing rendered at all, show empty state if (scrollArea.children.length === 0) { - const empty = createElement("div", { - style: "padding: 24px; text-align: center; color: var(--text-faint); font-size: 13px;", - }, "No emoji found"); + const empty = createElement( + "div", + { + style: "padding: 24px; text-align: center; color: var(--text-faint); font-size: 13px;", + }, + "No emoji found", + ); scrollArea.appendChild(empty); } } @@ -303,17 +634,25 @@ export function createEmojiPicker(options: EmojiPickerOptions): { renderAllCategories(getAllCategories()); // Search handler - searchInput.addEventListener("input", () => { - searchQuery = searchInput.value.trim(); - renderAllCategories(getAllCategories()); - }, { signal }); + searchInput.addEventListener( + "input", + () => { + searchQuery = searchInput.value.trim(); + renderAllCategories(getAllCategories()); + }, + { signal }, + ); // Close on Escape - root.addEventListener("keydown", (e) => { - if (e.key === "Escape") { - options.onClose(); - } - }, { signal }); + root.addEventListener( + "keydown", + (e) => { + if (e.key === "Escape") { + options.onClose(); + } + }, + { signal }, + ); // Focus search on mount requestAnimationFrame(() => searchInput.focus()); diff --git a/Client/tauri-client/src/components/FileUpload.ts b/Client/tauri-client/src/components/FileUpload.ts index 6c550ff7..abaff490 100644 --- a/Client/tauri-client/src/components/FileUpload.ts +++ b/Client/tauri-client/src/components/FileUpload.ts @@ -7,9 +7,16 @@ import type { MountableComponent } from "@lib/safe-render"; /** Default allowed MIME types for file uploads. */ const DEFAULT_ALLOWED_TYPES = [ - "image/jpeg", "image/png", "image/gif", "image/webp", "image/avif", - "video/mp4", "video/webm", - "audio/mpeg", "audio/ogg", "audio/wav", + "image/jpeg", + "image/png", + "image/gif", + "image/webp", + "image/avif", + "video/mp4", + "video/webm", + "audio/mpeg", + "audio/ogg", + "audio/wav", "application/pdf", "text/plain", ]; @@ -85,7 +92,9 @@ export function createFileUpload(options: FileUploadOptions): FileUploadComponen return; } if (file.size > maxBytes) { - showError(`File too large (${formatSize(file.size)}). Max ${options.maxSizeMb ?? DEFAULT_MAX_SIZE_MB} MB.`); + showError( + `File too large (${formatSize(file.size)}). Max ${options.maxSizeMb ?? DEFAULT_MAX_SIZE_MB} MB.`, + ); return; } showPreview(file); @@ -105,8 +114,13 @@ export function createFileUpload(options: FileUploadOptions): FileUploadComponen function buildDom(): void { root = createElement("div", { class: "file-upload" }); - dropzone = createElement("div", { class: "file-upload__dropzone file-upload__dropzone--hidden" }); - appendChildren(dropzone, createElement("span", { class: "file-upload__droptext" }, "Drop files here")); + dropzone = createElement("div", { + class: "file-upload__dropzone file-upload__dropzone--hidden", + }); + appendChildren( + dropzone, + createElement("span", { class: "file-upload__droptext" }, "Drop files here"), + ); const allowed = options.allowedMimeTypes ?? DEFAULT_ALLOWED_TYPES; fileInput = createElement("input", { @@ -135,38 +149,64 @@ export function createFileUpload(options: FileUploadOptions): FileUploadComponen } function attachListeners(): void { - fileInput.addEventListener("change", () => { - const file = fileInput.files?.[0]; - if (file) { void handleFile(file); fileInput.value = ""; } - }, { signal }); + fileInput.addEventListener( + "change", + () => { + const file = fileInput.files?.[0]; + if (file) { + void handleFile(file); + fileInput.value = ""; + } + }, + { signal }, + ); - cancelBtn.addEventListener("click", () => { - if (uploadAbort !== null) uploadAbort.abort(); - resetPreview(); - }, { signal }); + cancelBtn.addEventListener( + "click", + () => { + if (uploadAbort !== null) uploadAbort.abort(); + resetPreview(); + }, + { signal }, + ); let dragCounter = 0; - root!.addEventListener("dragenter", (e) => { - e.preventDefault(); - dragCounter++; - dropzone.classList.remove("file-upload__dropzone--hidden"); - }, { signal }); + root!.addEventListener( + "dragenter", + (e) => { + e.preventDefault(); + dragCounter++; + dropzone.classList.remove("file-upload__dropzone--hidden"); + }, + { signal }, + ); - root!.addEventListener("dragleave", (e) => { - e.preventDefault(); - dragCounter--; - if (dragCounter <= 0) { dragCounter = 0; dropzone.classList.add("file-upload__dropzone--hidden"); } - }, { signal }); + root!.addEventListener( + "dragleave", + (e) => { + e.preventDefault(); + dragCounter--; + if (dragCounter <= 0) { + dragCounter = 0; + dropzone.classList.add("file-upload__dropzone--hidden"); + } + }, + { signal }, + ); root!.addEventListener("dragover", (e) => e.preventDefault(), { signal }); - root!.addEventListener("drop", (e) => { - e.preventDefault(); - dragCounter = 0; - dropzone.classList.add("file-upload__dropzone--hidden"); - const file = e.dataTransfer?.files[0]; - if (file) void handleFile(file); - }, { signal }); + root!.addEventListener( + "drop", + (e) => { + e.preventDefault(); + dragCounter = 0; + dropzone.classList.add("file-upload__dropzone--hidden"); + const file = e.dataTransfer?.files[0]; + if (file) void handleFile(file); + }, + { signal }, + ); } function mount(container: Element): void { @@ -182,7 +222,9 @@ export function createFileUpload(options: FileUploadOptions): FileUploadComponen root = null; } - function openPicker(): void { fileInput.click(); } + function openPicker(): void { + fileInput.click(); + } return { mount, destroy, openPicker }; } diff --git a/Client/tauri-client/src/components/GifPicker.ts b/Client/tauri-client/src/components/GifPicker.ts index d84fab45..5538b6c3 100644 --- a/Client/tauri-client/src/components/GifPicker.ts +++ b/Client/tauri-client/src/components/GifPicker.ts @@ -88,10 +88,14 @@ export function createGifPicker(options: GifPickerOptions): { }); item.appendChild(img); - item.addEventListener("click", () => { - options.onSelect(gif.fullUrl); - options.onClose(); - }, { signal }); + item.addEventListener( + "click", + () => { + options.onSelect(gif.fullUrl); + options.onClose(); + }, + { signal }, + ); grid.appendChild(item); } @@ -109,9 +113,8 @@ export function createGifPicker(options: GifPickerOptions): { showLoading(); try { - const gifs = query.length > 0 - ? await searchGifs(query, GIF_LIMIT) - : await getTrendingGifs(GIF_LIMIT); + const gifs = + query.length > 0 ? await searchGifs(query, GIF_LIMIT) : await getTrendingGifs(GIF_LIMIT); // Only render if this is still the latest request if (requestId === currentRequestId) { @@ -130,20 +133,28 @@ export function createGifPicker(options: GifPickerOptions): { // โ”€โ”€ Event handlers โ”€โ”€ - searchInput.addEventListener("input", () => { - if (debounceTimer !== null) { - clearTimeout(debounceTimer); - } - debounceTimer = setTimeout(() => { - void loadGifs(searchInput.value.trim()); - }, DEBOUNCE_MS); - }, { signal }); + searchInput.addEventListener( + "input", + () => { + if (debounceTimer !== null) { + clearTimeout(debounceTimer); + } + debounceTimer = setTimeout(() => { + void loadGifs(searchInput.value.trim()); + }, DEBOUNCE_MS); + }, + { signal }, + ); - root.addEventListener("keydown", (e) => { - if (e.key === "Escape") { - options.onClose(); - } - }, { signal }); + root.addEventListener( + "keydown", + (e) => { + if (e.key === "Escape") { + options.onClose(); + } + }, + { signal }, + ); // Focus search on mount requestAnimationFrame(() => searchInput.focus()); diff --git a/Client/tauri-client/src/components/InviteManager.ts b/Client/tauri-client/src/components/InviteManager.ts index 6ad7fa27..17c270b0 100644 --- a/Client/tauri-client/src/components/InviteManager.ts +++ b/Client/tauri-client/src/components/InviteManager.ts @@ -39,9 +39,8 @@ function maskCode(code: string): string { } function formatInviteInfo(invite: InviteItem): string { - const uses = invite.maxUses !== null - ? `${invite.uses}/${invite.maxUses} uses` - : `${invite.uses} uses`; + const uses = + invite.maxUses !== null ? `${invite.uses}/${invite.maxUses} uses` : `${invite.uses} uses`; return `Created by ${invite.createdBy} \u00B7 ${uses}`; } @@ -49,9 +48,7 @@ function formatInviteInfo(invite: InviteItem): string { // Factory // --------------------------------------------------------------------------- -export function createInviteManager( - options: InviteManagerOptions, -): MountableComponent { +export function createInviteManager(options: InviteManagerOptions): MountableComponent { const ac = new AbortController(); let root: HTMLDivElement | null = null; let listEl: HTMLDivElement | null = null; @@ -80,21 +77,32 @@ export function createInviteManager( const copyBtn = createElement("button", { class: "invite-item__copy" }); copyBtn.appendChild(createIcon("external-link", 14)); copyBtn.appendChild(document.createTextNode(" Copy")); - copyBtn.addEventListener("click", () => { - options.onCopyLink(invite.code); - }, { signal: ac.signal }); + copyBtn.addEventListener( + "click", + () => { + options.onCopyLink(invite.code); + }, + { signal: ac.signal }, + ); const revokeBtn = createElement("button", { class: "invite-item__revoke" }); revokeBtn.appendChild(createIcon("trash-2", 14)); revokeBtn.appendChild(document.createTextNode(" Revoke")); - revokeBtn.addEventListener("click", () => { - void options.onRevokeInvite(invite.code).then(() => { - invites = invites.filter((i) => i.code !== invite.code); - renderList(); - }).catch(() => { - options.onError?.("Failed to revoke invite"); - }); - }, { signal: ac.signal }); + revokeBtn.addEventListener( + "click", + () => { + void options + .onRevokeInvite(invite.code) + .then(() => { + invites = invites.filter((i) => i.code !== invite.code); + renderList(); + }) + .catch(() => { + options.onError?.("Failed to revoke invite"); + }); + }, + { signal: ac.signal }, + ); appendChildren(actions, copyBtn, revokeBtn); appendChildren(headerRow, code, actions); @@ -135,29 +143,44 @@ export function createInviteManager( const createBtn = createElement("button", { class: "invite-manager__create btn-modal-save" }); createBtn.appendChild(createIcon("external-link", 14)); createBtn.appendChild(document.createTextNode(" Create Invite")); - createBtn.addEventListener("click", () => { - void options.onCreateInvite().then((newInvite) => { - invites = [...invites, newInvite]; - renderList(); - }).catch(() => { - options.onError?.("Failed to create invite"); - }); - }, { signal: ac.signal }); + createBtn.addEventListener( + "click", + () => { + void options + .onCreateInvite() + .then((newInvite) => { + invites = [...invites, newInvite]; + renderList(); + }) + .catch(() => { + options.onError?.("Failed to create invite"); + }); + }, + { signal: ac.signal }, + ); footer.appendChild(createBtn); // Escape key - document.addEventListener("keydown", (e: KeyboardEvent) => { - if (e.key === "Escape") { - options.onClose(); - } - }, { signal: ac.signal }); + document.addEventListener( + "keydown", + (e: KeyboardEvent) => { + if (e.key === "Escape") { + options.onClose(); + } + }, + { signal: ac.signal }, + ); // Click overlay to close - root.addEventListener("click", (e) => { - if (e.target === root) { - options.onClose(); - } - }, { signal: ac.signal }); + root.addEventListener( + "click", + (e) => { + if (e.target === root) { + options.onClose(); + } + }, + { signal: ac.signal }, + ); appendChildren(modal, header, body, footer); root.appendChild(modal); diff --git a/Client/tauri-client/src/components/MemberList.ts b/Client/tauri-client/src/components/MemberList.ts index 4558a187..df2189e3 100644 --- a/Client/tauri-client/src/components/MemberList.ts +++ b/Client/tauri-client/src/components/MemberList.ts @@ -35,21 +35,31 @@ const ROLE_GROUPS: readonly { /** Status priority for sorting: lower = higher priority (shown first). */ function statusPriority(status: UserStatus): number { switch (status) { - case "online": return 0; - case "idle": return 1; - case "dnd": return 2; - case "offline": return 3; - default: return 99; + case "online": + return 0; + case "idle": + return 1; + case "dnd": + return 2; + case "offline": + return 3; + default: + return 99; } } function statusColor(status: UserStatus): string { switch (status) { - case "online": return "var(--green)"; - case "idle": return "var(--yellow)"; - case "dnd": return "var(--red)"; - case "offline": return "var(--text-micro)"; - default: return "#747f8d"; + case "online": + return "var(--green)"; + case "idle": + return "var(--yellow)"; + case "dnd": + return "var(--red)"; + case "offline": + return "var(--text-micro)"; + default: + return "#747f8d"; } } @@ -95,53 +105,54 @@ function createMemberItem( }); avatar.appendChild(statusDot); - const name = createElement( - "span", - { class: "mi-name", style: `color: ${colorVar}` }, - ); + const name = createElement("span", { class: "mi-name", style: `color: ${colorVar}` }); setText(name, member.username); appendChildren(item, avatar, name); // Context menu for admin actions - item.addEventListener("contextmenu", (e) => { - e.preventDefault(); + item.addEventListener( + "contextmenu", + (e) => { + e.preventDefault(); - // Don't show context menu for yourself - const currentUserId = authStore.getState().user?.id ?? 0; - if (member.id === currentUserId) return; + // Don't show context menu for yourself + const currentUserId = authStore.getState().user?.id ?? 0; + if (member.id === currentUserId) return; - // Only admins and owners can use admin actions - const role = opts.currentUserRole.toLowerCase(); - if (role !== "owner" && role !== "admin") return; + // Only admins and owners can use admin actions + const role = opts.currentUserRole.toLowerCase(); + if (role !== "owner" && role !== "admin") return; - closeActiveMenu(); - document.removeEventListener("mousedown", handleOutsideClick); + closeActiveMenu(); + document.removeEventListener("mousedown", handleOutsideClick); - const availableRoles = ["admin", "moderator", "member"]; + const availableRoles = ["admin", "moderator", "member"]; - activeMenu = createMemberContextMenu({ - userId: member.id, - username: member.username, - currentRole: member.role.toLowerCase(), - availableRoles, - onKick: () => opts.onKick(member.id, member.username), - onBan: () => opts.onBan(member.id, member.username), - onChangeRole: (newRole: string) => opts.onChangeRole(member.id, member.username, newRole), - }); + activeMenu = createMemberContextMenu({ + userId: member.id, + username: member.username, + currentRole: member.role.toLowerCase(), + availableRoles, + onKick: () => opts.onKick(member.id, member.username), + onBan: () => opts.onBan(member.id, member.username), + onChangeRole: (newRole: string) => opts.onChangeRole(member.id, member.username, newRole), + }); - // Position at mouse - activeMenu.element.style.position = "fixed"; - activeMenu.element.style.left = `${e.clientX}px`; - activeMenu.element.style.top = `${e.clientY}px`; - activeMenu.element.style.zIndex = "1000"; - document.body.appendChild(activeMenu.element); + // Position at mouse + activeMenu.element.style.position = "fixed"; + activeMenu.element.style.left = `${e.clientX}px`; + activeMenu.element.style.top = `${e.clientY}px`; + activeMenu.element.style.zIndex = "1000"; + document.body.appendChild(activeMenu.element); - // Close on outside click (deferred so this click doesn't close it) - setTimeout(() => { - document.addEventListener("mousedown", handleOutsideClick); - }, 0); - }, { signal }); + // Close on outside click (deferred so this click doesn't close it) + setTimeout(() => { + document.addEventListener("mousedown", handleOutsideClick); + }, 0); + }, + { signal }, + ); return item; } diff --git a/Client/tauri-client/src/components/MessageInput.ts b/Client/tauri-client/src/components/MessageInput.ts index 3b1da1b8..27a31e3c 100644 --- a/Client/tauri-client/src/components/MessageInput.ts +++ b/Client/tauri-client/src/components/MessageInput.ts @@ -12,7 +12,11 @@ import { createGifPicker } from "@components/GifPicker"; export interface MessageInputOptions { readonly channelId: number; readonly channelName: string; - readonly onSend: (content: string, replyTo: number | null, attachments: readonly string[]) => void; + readonly onSend: ( + content: string, + replyTo: number | null, + attachments: readonly string[], + ) => void; readonly onUploadFile?: (file: File) => Promise<{ id: string; url: string; filename: string }>; readonly onTyping: () => void; readonly onEditMessage: (messageId: number, content: string) => void; @@ -40,14 +44,14 @@ const ALLOWED_TYPES = [ "application/json", ]; -export function createMessageInput( - options: MessageInputOptions, -): MessageInputComponent { +export function createMessageInput(options: MessageInputOptions): MessageInputComponent { const ac = new AbortController(); const signal = ac.signal; let root: HTMLDivElement | null = null; - let state = { replyTo: null as { messageId: number; username: string } | null, - editing: null as { messageId: number } | null }; + let state = { + replyTo: null as { messageId: number; username: string } | null, + editing: null as { messageId: number } | null, + }; let lastTypingTime = 0; let lastSendTime = 0; @@ -58,7 +62,8 @@ export function createMessageInput( let attachmentPreviewBar: HTMLDivElement | null = null; /** Pending attachment IDs to send with the next message. */ - const pendingAttachments: { id: string; filename: string; readonly previewEl: HTMLDivElement }[] = []; + const pendingAttachments: { id: string; filename: string; readonly previewEl: HTMLDivElement }[] = + []; /** Count of file uploads currently in flight. */ let pendingUploadCount = 0; /** References to picker close functions, set by mount() for destroy() to call. */ @@ -72,9 +77,15 @@ export function createMessageInput( replyBar.classList.add("visible"); } - function hideReplyBar(): void { replyBar?.classList.remove("visible"); } - function showEditBar(): void { editBar?.classList.add("visible"); } - function hideEditBar(): void { editBar?.classList.remove("visible"); } + function hideReplyBar(): void { + replyBar?.classList.remove("visible"); + } + function showEditBar(): void { + editBar?.classList.add("visible"); + } + function hideEditBar(): void { + editBar?.classList.remove("visible"); + } function autoResize(): void { if (textarea === null) return; @@ -102,11 +113,18 @@ export function createMessageInput( function showUploadError(message: string): void { if (attachmentPreviewBar === null) return; - const errEl = createElement("div", { - class: "attachment-upload-error", - }, message); + const errEl = createElement( + "div", + { + class: "attachment-upload-error", + }, + message, + ); attachmentPreviewBar.appendChild(errEl); - const t = setTimeout(() => { activeTimers.delete(t); errEl.remove(); }, 4000); + const t = setTimeout(() => { + activeTimers.delete(t); + errEl.remove(); + }, 4000); activeTimers.add(t); } @@ -203,15 +221,17 @@ export function createMessageInput( alt: file.name, }); item.appendChild(img); - readFileAsDataUrl(file).then((dataUrl) => { - if (signal.aborted) return; - img.src = dataUrl; - }).catch(() => { - if (signal.aborted) return; - // Fallback: show filename - const nameEl = createElement("span", { class: "attachment-preview-name" }, file.name); - img.replaceWith(nameEl); - }); + readFileAsDataUrl(file) + .then((dataUrl) => { + if (signal.aborted) return; + img.src = dataUrl; + }) + .catch(() => { + if (signal.aborted) return; + // Fallback: show filename + const nameEl = createElement("span", { class: "attachment-preview-name" }, file.name); + img.replaceWith(nameEl); + }); } else { const icon = createElement("div", { class: "attachment-preview-file" }); icon.appendChild(createIcon("file-text", 16)); @@ -229,10 +249,14 @@ export function createMessageInput( "data-testid": "attachment-remove", }); removeBtn.appendChild(createIcon("x", 14)); - removeBtn.addEventListener("click", (e) => { - e.stopPropagation(); - removePreviewItem(tempId); - }, { signal }); + removeBtn.addEventListener( + "click", + (e) => { + e.stopPropagation(); + removePreviewItem(tempId); + }, + { signal }, + ); item.appendChild(removeBtn); attachmentPreviewBar.appendChild(item); @@ -289,7 +313,10 @@ export function createMessageInput( function cancelEdit(): void { state = { ...state, editing: null }; hideEditBar(); - if (textarea !== null) { textarea.value = ""; autoResize(); } + if (textarea !== null) { + textarea.value = ""; + autoResize(); + } } function mount(container: Element): void { @@ -318,8 +345,11 @@ export function createMessageInput( attachmentPreviewBar = createElement("div", { class: "attachment-preview-bar" }); const inputBox = createElement("div", { class: "message-input-box" }); - const attachBtn = createElement("button", - { class: "input-btn attach-btn", "aria-label": "Attach file" }, "+"); + const attachBtn = createElement( + "button", + { class: "input-btn attach-btn", "aria-label": "Attach file" }, + "+", + ); // File picker via attach button if (options.onUploadFile !== undefined) { @@ -328,13 +358,17 @@ export function createMessageInput( style: "display: none;", accept: "image/*,video/*,audio/*,.pdf,.txt,.zip,.rar,.7z", }); - fileInput.addEventListener("change", () => { - const file = fileInput.files?.[0]; - if (file != null) { - void handlePasteFile(file); - } - fileInput.value = ""; - }, { signal }); + fileInput.addEventListener( + "change", + () => { + const file = fileInput.files?.[0]; + if (file != null) { + void handlePasteFile(file); + } + fileInput.value = ""; + }, + { signal }, + ); attachBtn.addEventListener("click", () => fileInput.click(), { signal }); root?.appendChild(fileInput); } else { @@ -342,42 +376,73 @@ export function createMessageInput( attachBtn.title = "File uploads not available"; } textarea = createElement("textarea", { - class: "msg-textarea", placeholder: `Message #${options.channelName}`, rows: "1", + class: "msg-textarea", + placeholder: `Message #${options.channelName}`, + rows: "1", "data-testid": "msg-textarea", }); - const emojiBtn = createElement("button", - { class: "input-btn emoji-btn", "aria-label": "Emoji" }); + const emojiBtn = createElement("button", { + class: "input-btn emoji-btn", + "aria-label": "Emoji", + }); emojiBtn.appendChild(createIcon("smile", 20)); - const gifBtn = createElement("button", - { class: "input-btn gif-btn", "aria-label": "GIF" }, "GIF"); - const sendBtn = createElement("button", - { class: "input-btn send-btn", "aria-label": "Send message", "data-testid": "send-btn" }); + const gifBtn = createElement( + "button", + { class: "input-btn gif-btn", "aria-label": "GIF" }, + "GIF", + ); + const sendBtn = createElement("button", { + class: "input-btn send-btn", + "aria-label": "Send message", + "data-testid": "send-btn", + }); sendBtn.appendChild(createIcon("send", 20)); - textarea.addEventListener("input", () => { autoResize(); maybeEmitTyping(); }, { signal }); - textarea.addEventListener("keydown", (e: KeyboardEvent) => { - if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); } - if (e.key === "Escape") { - if (state.editing !== null) { cancelEdit(); } - else if (state.replyTo !== null) { clearReply(); } - } - if (e.key === "ArrowUp" && textarea !== null && textarea.value.length === 0) { - root?.dispatchEvent(new CustomEvent("edit-last-message", { bubbles: true })); - } - }, { signal }); + textarea.addEventListener( + "input", + () => { + autoResize(); + maybeEmitTyping(); + }, + { signal }, + ); + textarea.addEventListener( + "keydown", + (e: KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + if (e.key === "Escape") { + if (state.editing !== null) { + cancelEdit(); + } else if (state.replyTo !== null) { + clearReply(); + } + } + if (e.key === "ArrowUp" && textarea !== null && textarea.value.length === 0) { + root?.dispatchEvent(new CustomEvent("edit-last-message", { bubbles: true })); + } + }, + { signal }, + ); // Clipboard paste: detect images/files - textarea.addEventListener("paste", (e: ClipboardEvent) => { - const items = e.clipboardData?.items; - if (items === undefined) return; - for (const item of items) { - if (item.kind !== "file") continue; - const file = item.getAsFile(); - if (file === null) continue; - e.preventDefault(); - void handlePasteFile(file); - } - }, { signal }); + textarea.addEventListener( + "paste", + (e: ClipboardEvent) => { + const items = e.clipboardData?.items; + if (items === undefined) return; + for (const item of items) { + if (item.kind !== "file") continue; + const file = item.getAsFile(); + if (file === null) continue; + e.preventDefault(); + void handlePasteFile(file); + } + }, + { signal }, + ); sendBtn.addEventListener("click", handleSend, { signal }); @@ -398,7 +463,11 @@ export function createMessageInput( if (emojiPicker === null) return; const target = e.target as Node; // Close if click is outside both the picker and the emoji button - if (!emojiPicker.element.contains(target) && target !== emojiBtn && !emojiBtn.contains(target)) { + if ( + !emojiPicker.element.contains(target) && + target !== emojiBtn && + !emojiBtn.contains(target) + ) { closeEmojiPicker(); } } @@ -494,7 +563,10 @@ export function createMessageInput( gifBtn.addEventListener("click", toggleGifPicker, { signal }); // Store picker cleanup for destroy() - cleanupPickers = () => { closeEmojiPicker(); closeGifPicker(); }; + cleanupPickers = () => { + closeEmojiPicker(); + closeGifPicker(); + }; appendChildren(inputBox, attachBtn, textarea, emojiBtn, gifBtn, sendBtn); appendChildren(root, replyBar, editBar, attachmentPreviewBar, inputBox); diff --git a/Client/tauri-client/src/components/MessageList.ts b/Client/tauri-client/src/components/MessageList.ts index 23b41fa8..140829e7 100644 --- a/Client/tauri-client/src/components/MessageList.ts +++ b/Client/tauri-client/src/components/MessageList.ts @@ -11,12 +11,7 @@ import type { Message } from "@stores/messages.store"; import { membersStore } from "@stores/members.store"; const log = createLogger("message-list"); -import { - shouldGroup, - isSameDay, - renderDayDivider, - renderMessage, -} from "./message-list/renderers"; +import { shouldGroup, isSameDay, renderDayDivider, renderMessage } from "./message-list/renderers"; import { FenwickTree } from "./message-list/fenwick"; // -- Options ------------------------------------------------------------------ @@ -120,9 +115,7 @@ function renderEmptyState(channelName: string, channelType?: string): HTMLDivEle icon.textContent = isDm ? "@" : "#"; const title = createElement("h2", { class: "channel-welcome-title" }); - title.textContent = isDm - ? channelName - : `Welcome to #${channelName}!`; + title.textContent = isDm ? channelName : `Welcome to #${channelName}!`; const text = createElement("p", { class: "channel-welcome-text" }); text.textContent = isDm @@ -277,7 +270,8 @@ export function createMessageList(options: MessageListOptions): MessageListCompo let renderWindowResetTimer = 0; function renderWindow(): void { - if (root === null || contentContainer === null || topSpacer === null || bottomSpacer === null) return; + if (root === null || contentContainer === null || topSpacer === null || bottomSpacer === null) + return; const scrollTop = root.scrollTop; const clientHeight = root.clientHeight; @@ -465,9 +459,9 @@ export function createMessageList(options: MessageListOptions): MessageListCompo // Load older messages when near top if ( - root.scrollTop < SCROLL_TOP_THRESHOLD - && !loadingOlder - && hasMoreMessages(options.channelId) + root.scrollTop < SCROLL_TOP_THRESHOLD && + !loadingOlder && + hasMoreMessages(options.channelId) ) { loadingOlder = true; options.onScrollTop(); @@ -499,10 +493,14 @@ export function createMessageList(options: MessageListOptions): MessageListCompo scrollToBottomBtn = createElement("button", { class: "scroll-to-bottom-btn" }); scrollToBottomBtn.textContent = "โ†“"; - scrollToBottomBtn.addEventListener("click", () => { - scrollToBottom(); - updateScrollToBottomBtn(); - }, { signal: ac.signal }); + scrollToBottomBtn.addEventListener( + "click", + () => { + scrollToBottom(); + updateScrollToBottomBtn(); + }, + { signal: ac.signal }, + ); root.appendChild(topSpacer); root.appendChild(contentContainer); @@ -555,21 +553,29 @@ export function createMessageList(options: MessageListOptions): MessageListCompo const initialScrollRaf = requestAnimationFrame(() => scrollToBottom()); ac.signal.addEventListener("abort", () => cancelAnimationFrame(initialScrollRaf)); - unsubscribers.push(messagesStore.subscribeSelector( - (s) => s.messagesByChannel, - () => { renderAll(); }, - )); + unsubscribers.push( + messagesStore.subscribeSelector( + (s) => s.messagesByChannel, + () => { + renderAll(); + }, + ), + ); // Only re-render when member roles change, not on presence/typing updates. // Extract a role-only map so shallowEqual ignores status changes. - unsubscribers.push(membersStore.subscribeSelector( - (s) => { - const roles = new Map(); - for (const [id, m] of s.members) roles.set(id, m.role); - return roles; - }, - () => { renderAll(); }, - )); + unsubscribers.push( + membersStore.subscribeSelector( + (s) => { + const roles = new Map(); + for (const [id, m] of s.members) roles.set(id, m.role); + return roles; + }, + () => { + renderAll(); + }, + ), + ); } function destroy(): void { @@ -591,11 +597,16 @@ export function createMessageList(options: MessageListOptions): MessageListCompo renderWindowResetTimer = 0; } unsubLoadingReset(); - for (const unsub of unsubscribers) { unsub(); } + for (const unsub of unsubscribers) { + unsub(); + } unsubscribers.length = 0; heightCache.clear(); tree = null; - if (root !== null) { root.remove(); root = null; } + if (root !== null) { + root.remove(); + root = null; + } contentContainer = null; topSpacer = null; bottomSpacer = null; @@ -618,7 +629,9 @@ export function createMessageList(options: MessageListOptions): MessageListCompo const el = contentContainer.children[localIdx] as HTMLElement | undefined; if (el !== undefined) { el.classList.add("highlight-flash"); - setTimeout(() => { el.classList.remove("highlight-flash"); }, 1500); + setTimeout(() => { + el.classList.remove("highlight-flash"); + }, 1500); } } diff --git a/Client/tauri-client/src/components/PinnedMessages.ts b/Client/tauri-client/src/components/PinnedMessages.ts index ae84ef82..6aea5926 100644 --- a/Client/tauri-client/src/components/PinnedMessages.ts +++ b/Client/tauri-client/src/components/PinnedMessages.ts @@ -3,10 +3,7 @@ * with avatars, hover actions, and entry animation. */ -import { - createElement, - appendChildren, -} from "@lib/dom"; +import { createElement, appendChildren } from "@lib/dom"; import { createIcon } from "@lib/icons"; import type { MountableComponent } from "@lib/safe-render"; @@ -92,9 +89,7 @@ function renderEmptyState(): HTMLDivElement { return empty; } -export function createPinnedMessages( - options: PinnedMessagesOptions, -): MountableComponent { +export function createPinnedMessages(options: PinnedMessagesOptions): MountableComponent { const ac = new AbortController(); let root: HTMLDivElement | null = null; diff --git a/Client/tauri-client/src/components/QuickSwitchOverlay.ts b/Client/tauri-client/src/components/QuickSwitchOverlay.ts index 70c9fb1b..bf7e0303 100644 --- a/Client/tauri-client/src/components/QuickSwitchOverlay.ts +++ b/Client/tauri-client/src/components/QuickSwitchOverlay.ts @@ -39,17 +39,24 @@ export function createQuickSwitchOverlay(options: QuickSwitchOverlayOptions): Mo }); // Close on backdrop click (not on modal content) - root.addEventListener("click", (e) => { - if (e.target === root) options.onClose(); - }, { signal: ac.signal }); + root.addEventListener( + "click", + (e) => { + if (e.target === root) options.onClose(); + }, + { signal: ac.signal }, + ); const modal = createElement("div", { class: "quick-switch-modal" }); // Header const header = createElement("div", { class: "quick-switch-header" }); const title = createElement("h2", {}, "Switch Server"); - const subtitle = createElement("p", { class: "quick-switch-subtitle" }, - "You\u2019ll disconnect from the current server."); + const subtitle = createElement( + "p", + { class: "quick-switch-subtitle" }, + "You\u2019ll disconnect from the current server.", + ); appendChildren(header, title, subtitle); // Server list @@ -68,8 +75,11 @@ export function createQuickSwitchOverlay(options: QuickSwitchOverlayOptions): Mo const info = createElement("div", { class: "quick-switch-info" }); const nameEl = createElement("div", { class: "quick-switch-name" }, profile.name); - const hostEl = createElement("div", { class: "quick-switch-host" }, - `${profile.host}${isCurrent ? " \u00B7 Connected" : ""}`); + const hostEl = createElement( + "div", + { class: "quick-switch-host" }, + `${profile.host}${isCurrent ? " \u00B7 Connected" : ""}`, + ); appendChildren(info, nameEl, hostEl); if (isCurrent) { @@ -77,9 +87,13 @@ export function createQuickSwitchOverlay(options: QuickSwitchOverlayOptions): Mo appendChildren(item, icon, info, dot); } else { appendChildren(item, icon, info); - item.addEventListener("click", () => { - options.onSwitch(profile.host, profile.name); - }, { signal: ac.signal }); + item.addEventListener( + "click", + () => { + options.onSwitch(profile.host, profile.name); + }, + { signal: ac.signal }, + ); } list.appendChild(item); @@ -93,7 +107,11 @@ export function createQuickSwitchOverlay(options: QuickSwitchOverlayOptions): Mo const addIcon = createElement("div", { class: "quick-switch-icon add" }, "+"); const addInfo = createElement("div", { class: "quick-switch-info" }); const addName = createElement("div", { class: "quick-switch-name" }, "Add new server"); - const addHost = createElement("div", { class: "quick-switch-host" }, "Connect to another OwnCord server"); + const addHost = createElement( + "div", + { class: "quick-switch-host" }, + "Connect to another OwnCord server", + ); appendChildren(addInfo, addName, addHost); appendChildren(addItem, addIcon, addInfo); addItem.addEventListener("click", () => options.onAddServer(), { signal: ac.signal }); @@ -107,9 +125,13 @@ export function createQuickSwitchOverlay(options: QuickSwitchOverlayOptions): Mo container.appendChild(root); // Escape key closes overlay - document.addEventListener("keydown", (e) => { - if (e.key === "Escape") options.onClose(); - }, { signal: ac.signal }); + document.addEventListener( + "keydown", + (e) => { + if (e.key === "Escape") options.onClose(); + }, + { signal: ac.signal }, + ); } function destroy(): void { diff --git a/Client/tauri-client/src/components/QuickSwitcher.ts b/Client/tauri-client/src/components/QuickSwitcher.ts index 4594a5bd..01670e07 100644 --- a/Client/tauri-client/src/components/QuickSwitcher.ts +++ b/Client/tauri-client/src/components/QuickSwitcher.ts @@ -68,10 +68,14 @@ export function createQuickSwitcher(options: QuickSwitcherOptions): MountableCom appendChildren(item, ...parts); - item.addEventListener("click", () => { - options.onSelectChannel(ch.id); - options.onClose(); - }, { signal }); + item.addEventListener( + "click", + () => { + options.onSelectChannel(ch.id); + options.onClose(); + }, + { signal }, + ); resultsDiv.appendChild(item); } @@ -144,7 +148,8 @@ export function createQuickSwitcher(options: QuickSwitcherOptions): MountableCom // Overlay backdrop root = createElement("div", { class: "quick-switcher-overlay", - style: "position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 1000; display: flex; justify-content: center; padding-top: 20vh;", + style: + "position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 1000; display: flex; justify-content: center; padding-top: 20vh;", }); // Modal container @@ -175,10 +180,7 @@ export function createQuickSwitcher(options: QuickSwitcherOptions): MountableCom document.addEventListener("keydown", handleGlobalKeydown, { signal }); // Subscribe to store changes - unsubscribe = channelsStore.subscribeSelector( - (s) => s.channels, - refreshFromStore, - ); + unsubscribe = channelsStore.subscribeSelector((s) => s.channels, refreshFromStore); // Auto-focus requestAnimationFrame(() => input.focus()); diff --git a/Client/tauri-client/src/components/SearchOverlay.ts b/Client/tauri-client/src/components/SearchOverlay.ts index 9f4181bb..35553864 100644 --- a/Client/tauri-client/src/components/SearchOverlay.ts +++ b/Client/tauri-client/src/components/SearchOverlay.ts @@ -13,7 +13,11 @@ import type { SearchResultItem } from "@lib/types"; // --------------------------------------------------------------------------- export interface SearchOverlayOptions { - readonly onSearch: (query: string, channelId?: number, signal?: AbortSignal) => Promise; + readonly onSearch: ( + query: string, + channelId?: number, + signal?: AbortSignal, + ) => Promise; readonly onSelectResult: (result: SearchResultItem) => void; readonly onClose: () => void; readonly currentChannelId?: number; @@ -49,8 +53,11 @@ export function createSearchOverlay(options: SearchOverlayOptions): MountableCom function formatTimestamp(ts: string): string { try { const d = new Date(ts); - return d.toLocaleDateString(undefined, { month: "short", day: "numeric" }) - + " " + d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" }); + return ( + d.toLocaleDateString(undefined, { month: "short", day: "numeric" }) + + " " + + d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" }) + ); } catch { return ts; } @@ -65,9 +72,7 @@ export function createSearchOverlay(options: SearchOverlayOptions): MountableCom const isActive = i === activeIndex; const item = createElement("div", { - class: isActive - ? "search-result-item search-result-item--active" - : "search-result-item", + class: isActive ? "search-result-item search-result-item--active" : "search-result-item", role: "option", "aria-selected": isActive ? "true" : "false", "data-testid": `search-result-${i}`, @@ -87,10 +92,14 @@ export function createSearchOverlay(options: SearchOverlayOptions): MountableCom appendChildren(item, header, content); - item.addEventListener("click", () => { - options.onSelectResult(r); - options.onClose(); - }, { signal }); + item.addEventListener( + "click", + () => { + options.onSelectResult(r); + options.onClose(); + }, + { signal }, + ); resultsDiv.appendChild(item); } @@ -122,7 +131,8 @@ export function createSearchOverlay(options: SearchOverlayOptions): MountableCom setStatus("Searching..."); - options.onSearch(query, options.currentChannelId, searchAbort.signal) + options + .onSearch(query, options.currentChannelId, searchAbort.signal) .then((items) => { results = items; activeIndex = 0; diff --git a/Client/tauri-client/src/components/SettingsOverlay.ts b/Client/tauri-client/src/components/SettingsOverlay.ts index 7127f3bb..336e7c00 100644 --- a/Client/tauri-client/src/components/SettingsOverlay.ts +++ b/Client/tauri-client/src/components/SettingsOverlay.ts @@ -43,7 +43,16 @@ export interface SettingsOverlayOptions { isAuthenticated?: boolean; } -export type TabName = "Account" | "Appearance" | "Notifications" | "Text & Images" | "Accessibility" | "Voice & Audio" | "Keybinds" | "Advanced" | "Logs"; +export type TabName = + | "Account" + | "Appearance" + | "Notifications" + | "Text & Images" + | "Accessibility" + | "Voice & Audio" + | "Keybinds" + | "Advanced" + | "Logs"; const TAB_ICONS: Record = { Account: "user", @@ -92,8 +101,14 @@ export function applyStoredAppearance(): void { "compact-mode", loadPref("compactMode", false), ); - document.documentElement.classList.toggle("reduced-motion", loadPref("reducedMotion", false)); - document.documentElement.classList.toggle("high-contrast", loadPref("highContrast", false)); + document.documentElement.classList.toggle( + "reduced-motion", + loadPref("reducedMotion", false), + ); + document.documentElement.classList.toggle( + "high-contrast", + loadPref("highContrast", false), + ); document.documentElement.classList.toggle("large-font", loadPref("largeFont", false)); syncOsMotionListener(loadPref("syncOsMotion", false)); @@ -178,14 +193,26 @@ export function createSettingsOverlay( // User profile section at top of sidebar const user = authStore.getState().user; const profileSection = createElement("div", { class: "settings-sidebar-profile" }); - const avatarEl = createElement("div", { class: "settings-sidebar-avatar" }, - (user?.username ?? "U").charAt(0).toUpperCase()); + const avatarEl = createElement( + "div", + { class: "settings-sidebar-avatar" }, + (user?.username ?? "U").charAt(0).toUpperCase(), + ); const profileInfo = createElement("div", {}); - const profileName = createElement("div", { class: "settings-sidebar-name" }, - user?.username ?? "Unknown"); - const editProfileLink = createElement("div", { class: "settings-sidebar-edit" }, "Edit Profile"); + const profileName = createElement( + "div", + { class: "settings-sidebar-name" }, + user?.username ?? "Unknown", + ); + const editProfileLink = createElement( + "div", + { class: "settings-sidebar-edit" }, + "Edit Profile", + ); if (authenticated) { - editProfileLink.addEventListener("click", () => setActiveTab("Account"), { signal: ac.signal }); + editProfileLink.addEventListener("click", () => setActiveTab("Account"), { + signal: ac.signal, + }); } else { editProfileLink.style.display = "none"; } @@ -214,7 +241,16 @@ export function createSettingsOverlay( const appSettingsCat = createElement("div", { class: "settings-cat" }, "App Settings"); sidebar.appendChild(appSettingsCat); - const appTabs: readonly TabName[] = ["Appearance", "Notifications", "Text & Images", "Accessibility", "Voice & Audio", "Keybinds", "Advanced", "Logs"]; + const appTabs: readonly TabName[] = [ + "Appearance", + "Notifications", + "Text & Images", + "Accessibility", + "Voice & Audio", + "Keybinds", + "Advanced", + "Logs", + ]; for (const name of appTabs) { const btn = createElement("button", { class: `settings-nav-item${name === activeTab ? " active" : ""}`, @@ -248,27 +284,39 @@ export function createSettingsOverlay( const closeWrap = createElement("div", { class: "settings-close-wrap" }); const closeBtn = createElement("button", { class: "settings-close-btn" }); closeBtn.appendChild(createIcon("x", 18)); - closeBtn.addEventListener("click", () => { - options.onClose(); - }, { signal: ac.signal }); + closeBtn.addEventListener( + "click", + () => { + options.onClose(); + }, + { signal: ac.signal }, + ); const escLabel = createElement("div", { class: "settings-esc-label" }, "ESC"); appendChildren(closeWrap, closeBtn, escLabel); // Escape key - document.addEventListener("keydown", (e: KeyboardEvent) => { - if (e.key === "Escape" && root?.classList.contains("open")) { - options.onClose(); - } - }, { signal: ac.signal }); + document.addEventListener( + "keydown", + (e: KeyboardEvent) => { + if (e.key === "Escape" && root?.classList.contains("open")) { + options.onClose(); + } + }, + { signal: ac.signal }, + ); // Inner panel (Discord-style centered card) const panel = createElement("div", { class: "settings-panel" }); appendChildren(panel, sidebar, contentArea, closeWrap); // Click backdrop (outside panel) to close - root.addEventListener("click", (e: MouseEvent) => { - if (e.target === root) options.onClose(); - }, { signal: ac.signal }); + root.addEventListener( + "click", + (e: MouseEvent) => { + if (e.target === root) options.onClose(); + }, + { signal: ac.signal }, + ); root.appendChild(panel); renderActiveTab(); diff --git a/Client/tauri-client/src/components/Toast.ts b/Client/tauri-client/src/components/Toast.ts index d5346524..baab3875 100644 --- a/Client/tauri-client/src/components/Toast.ts +++ b/Client/tauri-client/src/components/Toast.ts @@ -93,6 +93,7 @@ export function createToastContainer(): ToastContainer { } function clear(): void { + // oxlint-disable-next-line no-useless-spread -- snapshot needed: removeToast splices the array during iteration for (const entry of [...toasts]) { removeToast(entry); } diff --git a/Client/tauri-client/src/components/TypingIndicator.ts b/Client/tauri-client/src/components/TypingIndicator.ts index 8a8882b4..0e516b82 100644 --- a/Client/tauri-client/src/components/TypingIndicator.ts +++ b/Client/tauri-client/src/components/TypingIndicator.ts @@ -25,9 +25,7 @@ function formatTypingText(users: readonly Member[]): string { return "Several people are typing..."; } -export function createTypingIndicator( - options: TypingIndicatorOptions, -): MountableComponent { +export function createTypingIndicator(options: TypingIndicatorOptions): MountableComponent { const disposable = new Disposable(); let root: HTMLDivElement | null = null; @@ -65,7 +63,9 @@ export function createTypingIndicator( disposable.onStoreChange( membersStore, (s) => s.typingUsers, - () => { updateFromState(); }, + () => { + updateFromState(); + }, ); container.appendChild(root); diff --git a/Client/tauri-client/src/components/UpdateNotifier.ts b/Client/tauri-client/src/components/UpdateNotifier.ts index 8e937fba..0fad5058 100644 --- a/Client/tauri-client/src/components/UpdateNotifier.ts +++ b/Client/tauri-client/src/components/UpdateNotifier.ts @@ -32,17 +32,26 @@ export function createUpdateNotifier(options: UpdateNotifierOptions): MountableC banner = createElement("div", { class: "update-banner" }); - const text = createElement("span", { class: "update-banner-text" }, - `Update v${version} available`); + const text = createElement( + "span", + { class: "update-banner-text" }, + `Update v${version} available`, + ); - const updateBtn = createElement("button", { class: "update-banner-btn update-banner-install" }, - "Update Now"); + const updateBtn = createElement( + "button", + { class: "update-banner-btn update-banner-install" }, + "Update Now", + ); updateBtn.addEventListener("click", () => { void installUpdate(); }); - const laterBtn = createElement("button", { class: "update-banner-btn update-banner-later" }, - "Later"); + const laterBtn = createElement( + "button", + { class: "update-banner-btn update-banner-later" }, + "Later", + ); laterBtn.addEventListener("click", () => { dismissed = true; removeBanner(); @@ -57,8 +66,11 @@ export function createUpdateNotifier(options: UpdateNotifierOptions): MountableC // Replace banner content with progress indicator while (banner.firstChild) banner.removeChild(banner.firstChild); - const progress = createElement("span", { class: "update-banner-text" }, - "Downloading update..."); + const progress = createElement( + "span", + { class: "update-banner-text" }, + "Downloading update...", + ); banner.appendChild(progress); try { @@ -67,10 +79,16 @@ export function createUpdateNotifier(options: UpdateNotifierOptions): MountableC } catch (err) { log.error("Update install failed", { error: String(err) }); while (banner.firstChild) banner.removeChild(banner.firstChild); - const errorText = createElement("span", { class: "update-banner-text" }, - "Update failed. Please try again later."); - const dismissBtn = createElement("button", { class: "update-banner-btn update-banner-later" }, - "Dismiss"); + const errorText = createElement( + "span", + { class: "update-banner-text" }, + "Update failed. Please try again later.", + ); + const dismissBtn = createElement( + "button", + { class: "update-banner-btn update-banner-later" }, + "Dismiss", + ); dismissBtn.addEventListener("click", () => { dismissed = true; removeBanner(); @@ -89,7 +107,9 @@ export function createUpdateNotifier(options: UpdateNotifierOptions): MountableC function mount(target: Element): void { container = target; // Delay the check slightly so the main UI renders first - setTimeout(() => { void performCheck(); }, 3000); + setTimeout(() => { + void performCheck(); + }, 3000); } function destroy(): void { diff --git a/Client/tauri-client/src/components/VideoGrid.ts b/Client/tauri-client/src/components/VideoGrid.ts index 1a56d1ff..b6545e19 100644 --- a/Client/tauri-client/src/components/VideoGrid.ts +++ b/Client/tauri-client/src/components/VideoGrid.ts @@ -26,9 +26,13 @@ export interface VideoGridComponent extends MountableComponent { } /** Create a fresh volume icon element. */ -function volumeIcon(): SVGSVGElement { return createIcon("volume-2", 16); } +function volumeIcon(): SVGSVGElement { + return createIcon("volume-2", 16); +} /** Create a fresh volume-x (muted) icon element. */ -function volumeXIcon(): SVGSVGElement { return createIcon("volume-x", 16); } +function volumeXIcon(): SVGSVGElement { + return createIcon("volume-x", 16); +} /** Replace a button's icon child with a new one. */ function setButtonIcon(btn: HTMLButtonElement, icon: SVGSVGElement): void { while (btn.firstChild) btn.removeChild(btn.firstChild); @@ -191,7 +195,12 @@ export function createVideoGrid(): VideoGridComponent { applyGridSizes(); } - function addStream(userId: number, username: string, stream: MediaStream, config?: TileConfig): void { + function addStream( + userId: number, + username: string, + stream: MediaStream, + config?: TileConfig, + ): void { if (root === null) return; // If a cell already exists for this user, update it in place @@ -358,7 +367,9 @@ export function createVideoGrid(): VideoGridComponent { container.appendChild(root); // Observe container size changes to recalculate tile layout - resizeObserver = new ResizeObserver(() => { scheduleResize(); }); + resizeObserver = new ResizeObserver(() => { + scheduleResize(); + }); resizeObserver.observe(root); } @@ -384,5 +395,13 @@ export function createVideoGrid(): VideoGridComponent { } } - return { mount, destroy, addStream, removeStream, hasStreams, setFocusedTile, getFocusedTileId: getFocusedTileIdFn }; + return { + mount, + destroy, + addStream, + removeStream, + hasStreams, + setFocusedTile, + getFocusedTileId: getFocusedTileIdFn, + }; } diff --git a/Client/tauri-client/src/components/VoiceChannel.ts b/Client/tauri-client/src/components/VoiceChannel.ts index d34b6186..a7870b41 100644 --- a/Client/tauri-client/src/components/VoiceChannel.ts +++ b/Client/tauri-client/src/components/VoiceChannel.ts @@ -77,10 +77,14 @@ export function createVoiceChannel(options: VoiceChannelOptions): VoiceChannelRe const menu = createElement("div", { class: "context-menu" }); // Header - const header = createElement("div", { - class: "context-menu-item", - style: "font-weight:600;cursor:default;pointer-events:none", - }, username); + const header = createElement( + "div", + { + class: "context-menu-item", + style: "font-weight:600;cursor:default;pointer-events:none", + }, + username, + ); menu.appendChild(header); const sep = createElement("div", { class: "context-menu-sep" }); @@ -88,10 +92,14 @@ export function createVoiceChannel(options: VoiceChannelOptions): VoiceChannelRe // Volume label const currentVol = getUserVolume(userId); - const volLabel = createElement("div", { - class: "context-menu-item", - style: "font-size:12px;color:var(--text-muted);cursor:default;pointer-events:none", - }, `User Volume: ${currentVol}%`); + const volLabel = createElement( + "div", + { + class: "context-menu-item", + style: "font-size:12px;color:var(--text-muted);cursor:default;pointer-events:none", + }, + `User Volume: ${currentVol}%`, + ); menu.appendChild(volLabel); // Volume slider (0-200%, like Discord) @@ -106,10 +114,14 @@ export function createVoiceChannel(options: VoiceChannelOptions): VoiceChannelRe value: String(currentVol), style: "flex:1", }); - const valLabel = createElement("span", { - class: "slider-val", - style: "min-width:40px;text-align:right;font-size:12px;color:var(--text-muted)", - }, `${currentVol}%`); + const valLabel = createElement( + "span", + { + class: "slider-val", + style: "min-width:40px;text-align:right;font-size:12px;color:var(--text-muted)", + }, + `${currentVol}%`, + ); slider.addEventListener("input", () => { const val = Number(slider.value); @@ -142,18 +154,20 @@ export function createVoiceChannel(options: VoiceChannelOptions): VoiceChannelRe const dismissSignal = menuDismissAc.signal; setTimeout(() => { if (dismissSignal.aborted) return; - document.addEventListener("mousedown", (e: MouseEvent) => { - if (!menu.contains(e.target as Node)) { - closeContextMenu(); - } - }, { signal: dismissSignal }); + document.addEventListener( + "mousedown", + (e: MouseEvent) => { + if (!menu.contains(e.target as Node)) { + closeContextMenu(); + } + }, + { signal: dismissSignal }, + ); }, 0); } function createUserRow(user: VoiceUser, username: string): HTMLDivElement { - const classes = user.speaking - ? "voice-user-item speaking" - : "voice-user-item"; + const classes = user.speaking ? "voice-user-item speaking" : "voice-user-item"; const row = createElement("div", { class: classes }); const initial = username.length > 0 ? username.charAt(0).toUpperCase() : "?"; @@ -180,11 +194,15 @@ export function createVoiceChannel(options: VoiceChannelOptions): VoiceChannelRe // Right-click for per-user volume (skip for own user) const currentUser = authStore.getState().user; if (currentUser === null || currentUser.id !== user.userId) { - row.addEventListener("contextmenu", (e) => { - e.preventDefault(); - e.stopPropagation(); - showVolumeMenu(user.userId, username, e.clientX, e.clientY); - }, { signal: ac.signal }); + row.addEventListener( + "contextmenu", + (e) => { + e.preventDefault(); + e.stopPropagation(); + showVolumeMenu(user.userId, username, e.clientX, e.clientY); + }, + { signal: ac.signal }, + ); } return row; @@ -227,8 +245,18 @@ export function createVoiceChannel(options: VoiceChannelOptions): VoiceChannelRe // Initial render and subscribe update(); - unsubs.push(voiceStore.subscribeSelector((s) => s.voiceUsers, () => update())); - unsubs.push(membersStore.subscribeSelector((s) => s.members, () => update())); + unsubs.push( + voiceStore.subscribeSelector( + (s) => s.voiceUsers, + () => update(), + ), + ); + unsubs.push( + membersStore.subscribeSelector( + (s) => s.members, + () => update(), + ), + ); function destroy(): void { closeContextMenu(); diff --git a/Client/tauri-client/src/components/channel-sidebar/drag-reorder.ts b/Client/tauri-client/src/components/channel-sidebar/drag-reorder.ts index f31cfc88..1485a8cf 100644 --- a/Client/tauri-client/src/components/channel-sidebar/drag-reorder.ts +++ b/Client/tauri-client/src/components/channel-sidebar/drag-reorder.ts @@ -32,97 +32,105 @@ export function ensureGlobalDragListeners(): void { } globalDragAc = new AbortController(); - document.addEventListener("mousemove", (e) => { - if (activeDrag === null) { - return; - } - // Clear old indicators - activeDrag.containerEl.querySelectorAll(".channel-drop-indicator").forEach((x) => { - x.classList.remove("channel-drop-indicator"); - }); + document.addEventListener( + "mousemove", + (e) => { + if (activeDrag === null) { + return; + } + // Clear old indicators + activeDrag.containerEl.querySelectorAll(".channel-drop-indicator").forEach((x) => { + x.classList.remove("channel-drop-indicator"); + }); - // Find which channel item we're hovering over - const items = activeDrag.containerEl.querySelectorAll("[data-drag-channel-id]"); - for (const item of items) { - const rect = item.getBoundingClientRect(); - if (e.clientY >= rect.top && e.clientY <= rect.bottom) { - const targetId = Number((item as HTMLElement).dataset.dragChannelId); - if (targetId !== activeDrag.channelId) { - item.classList.add("channel-drop-indicator"); + // Find which channel item we're hovering over + const items = activeDrag.containerEl.querySelectorAll("[data-drag-channel-id]"); + for (const item of items) { + const rect = item.getBoundingClientRect(); + if (e.clientY >= rect.top && e.clientY <= rect.bottom) { + const targetId = Number((item as HTMLElement).dataset.dragChannelId); + if (targetId !== activeDrag.channelId) { + item.classList.add("channel-drop-indicator"); + } + break; } - break; } - } - }, { signal: globalDragAc.signal }); + }, + { signal: globalDragAc.signal }, + ); - document.addEventListener("mouseup", (e) => { - if (activeDrag === null) { - return; - } - const drag = activeDrag; - activeDrag = null; - - // Clean up visual state - drag.sourceEl.classList.remove("dragging"); - document.body.classList.remove("channel-reordering"); - drag.containerEl.querySelectorAll(".channel-drop-indicator").forEach((x) => { - x.classList.remove("channel-drop-indicator"); - }); - - // Find drop target - const items = drag.containerEl.querySelectorAll("[data-drag-channel-id]"); - let dropTargetId: number | null = null; - let dropBefore = false; - for (const item of items) { - const rect = item.getBoundingClientRect(); - if (e.clientY >= rect.top && e.clientY <= rect.bottom) { - dropTargetId = Number((item as HTMLElement).dataset.dragChannelId); - dropBefore = e.clientY < rect.top + rect.height / 2; - break; + document.addEventListener( + "mouseup", + (e) => { + if (activeDrag === null) { + return; } - } + const drag = activeDrag; + activeDrag = null; - if (dropTargetId === null || dropTargetId === drag.channelId) { - return; - } + // Clean up visual state + drag.sourceEl.classList.remove("dragging"); + document.body.classList.remove("channel-reordering"); + drag.containerEl.querySelectorAll(".channel-drop-indicator").forEach((x) => { + x.classList.remove("channel-drop-indicator"); + }); - // Compute new order - const orderedIds = drag.channels.map((ch) => ch.id); - const dragIdx = orderedIds.indexOf(drag.channelId); - if (dragIdx === -1) { - return; - } - const withoutDrag = orderedIds.filter((id) => id !== drag.channelId); - - const targetIdx = withoutDrag.indexOf(dropTargetId); - if (targetIdx === -1) { - return; - } - const insertIdx = dropBefore ? targetIdx : targetIdx + 1; - const reorderedIds = [ - ...withoutDrag.slice(0, insertIdx), - drag.channelId, - ...withoutDrag.slice(insertIdx), - ]; - - // Build reorder data and update store immediately - const reorders: ChannelReorderData[] = []; - for (let i = 0; i < reorderedIds.length; i++) { - const id = reorderedIds[i]; - if (id === undefined) { - continue; + // Find drop target + const items = drag.containerEl.querySelectorAll("[data-drag-channel-id]"); + let dropTargetId: number | null = null; + let dropBefore = false; + for (const item of items) { + const rect = item.getBoundingClientRect(); + if (e.clientY >= rect.top && e.clientY <= rect.bottom) { + dropTargetId = Number((item as HTMLElement).dataset.dragChannelId); + dropBefore = e.clientY < rect.top + rect.height / 2; + break; + } } - const ch = drag.channels.find((c) => c.id === id); - if (ch !== undefined && ch.position !== i) { - reorders.push({ channelId: id, newPosition: i }); - updateChannelPosition(id, i); - } - } - if (reorders.length > 0) { - drag.onReorder(reorders); - } - }, { signal: globalDragAc.signal }); + if (dropTargetId === null || dropTargetId === drag.channelId) { + return; + } + + // Compute new order + const orderedIds = drag.channels.map((ch) => ch.id); + const dragIdx = orderedIds.indexOf(drag.channelId); + if (dragIdx === -1) { + return; + } + const withoutDrag = orderedIds.filter((id) => id !== drag.channelId); + + const targetIdx = withoutDrag.indexOf(dropTargetId); + if (targetIdx === -1) { + return; + } + const insertIdx = dropBefore ? targetIdx : targetIdx + 1; + const reorderedIds = [ + ...withoutDrag.slice(0, insertIdx), + drag.channelId, + ...withoutDrag.slice(insertIdx), + ]; + + // Build reorder data and update store immediately + const reorders: ChannelReorderData[] = []; + for (let i = 0; i < reorderedIds.length; i++) { + const id = reorderedIds[i]; + if (id === undefined) { + continue; + } + const ch = drag.channels.find((c) => c.id === id); + if (ch !== undefined && ch.position !== i) { + reorders.push({ channelId: id, newPosition: i }); + updateChannelPosition(id, i); + } + } + + if (reorders.length > 0) { + drag.onReorder(reorders); + } + }, + { signal: globalDragAc.signal }, + ); } /** Make a channel element draggable via mousedown (admin/owner only). */ diff --git a/Client/tauri-client/src/components/channel-sidebar/volume-menu.ts b/Client/tauri-client/src/components/channel-sidebar/volume-menu.ts index 423df641..d6989c46 100644 --- a/Client/tauri-client/src/components/channel-sidebar/volume-menu.ts +++ b/Client/tauri-client/src/components/channel-sidebar/volume-menu.ts @@ -22,20 +22,28 @@ export function showUserVolumeMenu( const menu = createElement("div", { class: "context-menu user-vol-menu" }); - const header = createElement("div", { - class: "context-menu-item", - style: "font-weight:600;cursor:default;pointer-events:none", - }, username); + const header = createElement( + "div", + { + class: "context-menu-item", + style: "font-weight:600;cursor:default;pointer-events:none", + }, + username, + ); menu.appendChild(header); const sep = createElement("div", { class: "context-menu-sep" }); menu.appendChild(sep); const currentVol = getUserVolume(userId); - const volLabel = createElement("div", { - class: "context-menu-item", - style: "font-size:12px;color:var(--text-muted);cursor:default;pointer-events:none", - }, `User Volume: ${currentVol}%`); + const volLabel = createElement( + "div", + { + class: "context-menu-item", + style: "font-size:12px;color:var(--text-muted);cursor:default;pointer-events:none", + }, + `User Volume: ${currentVol}%`, + ); menu.appendChild(volLabel); const sliderRow = createElement("div", { @@ -49,10 +57,14 @@ export function showUserVolumeMenu( value: String(currentVol), style: "flex:1", }); - const valLabel = createElement("span", { - class: "slider-val", - style: "min-width:40px;text-align:right;font-size:12px;color:var(--text-muted)", - }, `${currentVol}%`); + const valLabel = createElement( + "span", + { + class: "slider-val", + style: "min-width:40px;text-align:right;font-size:12px;color:var(--text-muted)", + }, + `${currentVol}%`, + ); slider.addEventListener("input", () => { const val = Number(slider.value); @@ -82,12 +94,16 @@ export function showUserVolumeMenu( (menu as HTMLElement & { _dismissAc?: AbortController })._dismissAc = dismissAc; setTimeout(() => { if (dismissAc.signal.aborted) return; - document.addEventListener("mousedown", (e: MouseEvent) => { - if (!menu.contains(e.target as Node)) { - menu.remove(); - dismissAc.abort(); - } - }, { signal: dismissAc.signal }); + document.addEventListener( + "mousedown", + (e: MouseEvent) => { + if (!menu.contains(e.target as Node)) { + menu.remove(); + dismissAc.abort(); + } + }, + { signal: dismissAc.signal }, + ); }, 0); // Also clean up if the parent component is destroyed diff --git a/Client/tauri-client/src/components/message-input/file-upload.ts b/Client/tauri-client/src/components/message-input/file-upload.ts index ee423a45..f96786fd 100644 --- a/Client/tauri-client/src/components/message-input/file-upload.ts +++ b/Client/tauri-client/src/components/message-input/file-upload.ts @@ -53,14 +53,16 @@ export function buildPreviewItem( alt: file.name, }); item.appendChild(img); - readFileAsDataUrl(file).then((dataUrl) => { - if (signal.aborted) return; - img.src = dataUrl; - }).catch(() => { - if (signal.aborted) return; - const nameEl = createElement("span", { class: "attachment-preview-name" }, file.name); - img.replaceWith(nameEl); - }); + readFileAsDataUrl(file) + .then((dataUrl) => { + if (signal.aborted) return; + img.src = dataUrl; + }) + .catch(() => { + if (signal.aborted) return; + const nameEl = createElement("span", { class: "attachment-preview-name" }, file.name); + img.replaceWith(nameEl); + }); } else { const icon = createElement("div", { class: "attachment-preview-file" }); icon.appendChild(createIcon("file-text", 16)); @@ -78,10 +80,14 @@ export function buildPreviewItem( "data-testid": "attachment-remove", }); removeBtn.appendChild(createIcon("x", 14)); - removeBtn.addEventListener("click", (e) => { - e.stopPropagation(); - onRemove(); - }, { signal }); + removeBtn.addEventListener( + "click", + (e) => { + e.stopPropagation(); + onRemove(); + }, + { signal }, + ); item.appendChild(removeBtn); return item; diff --git a/Client/tauri-client/src/components/message-input/picker-toggle.ts b/Client/tauri-client/src/components/message-input/picker-toggle.ts index 7b4a5a94..bee5f7ca 100644 --- a/Client/tauri-client/src/components/message-input/picker-toggle.ts +++ b/Client/tauri-client/src/components/message-input/picker-toggle.ts @@ -33,7 +33,11 @@ export function createPickerToggle(opts: PickerToggleOptions): PickerToggleHandl function handleClickOutside(e: MouseEvent): void { if (instance === null) return; const target = e.target as Node; - if (!instance.element.contains(target) && target !== opts.triggerEl && !opts.triggerEl.contains(target)) { + if ( + !instance.element.contains(target) && + target !== opts.triggerEl && + !opts.triggerEl.contains(target) + ) { close(); } } diff --git a/Client/tauri-client/src/components/message-list/attachments.ts b/Client/tauri-client/src/components/message-list/attachments.ts index ea273c87..20d797b6 100644 --- a/Client/tauri-client/src/components/message-list/attachments.ts +++ b/Client/tauri-client/src/components/message-list/attachments.ts @@ -3,10 +3,7 @@ * Also owns the server host state and URL resolution used by other modules. */ -import { - createElement, - appendChildren, -} from "@lib/dom"; +import { createElement, appendChildren } from "@lib/dom"; import { createIcon } from "@lib/icons"; import { observeMedia } from "@lib/media-visibility"; import { loadPref } from "@components/settings/helpers"; @@ -81,9 +78,18 @@ export function clearAttachmentCaches(): void { // loaded in , , or