From a3cda7fd7a86b671b154a96bbaf5a5cb719d19b7 Mon Sep 17 00:00:00 2001 From: alice Date: Wed, 13 May 2026 00:47:33 +0000 Subject: [PATCH] Convert state-dependent utility helpers to TypeScript --- ...ge.js => get-translate-target-language.ts} | 20 +++-- ...ntent-links.js => handle-content-links.ts} | 88 ++++++++++++++----- .../{show-compose.js => show-compose.ts} | 4 +- src/utils/{useTitle.js => useTitle.ts} | 23 +++-- 4 files changed, 96 insertions(+), 39 deletions(-) rename src/utils/{get-translate-target-language.js => get-translate-target-language.ts} (54%) rename src/utils/{handle-content-links.js => handle-content-links.ts} (59%) rename src/utils/{show-compose.js => show-compose.ts} (82%) rename src/utils/{useTitle.js => useTitle.ts} (56%) diff --git a/src/utils/get-translate-target-language.js b/src/utils/get-translate-target-language.ts similarity index 54% rename from src/utils/get-translate-target-language.js rename to src/utils/get-translate-target-language.ts index 8a66ec6e..88ebf52c 100644 --- a/src/utils/get-translate-target-language.js +++ b/src/utils/get-translate-target-language.ts @@ -1,15 +1,19 @@ -import languages from '../data/translang-languages'; +import languages from '../data/translang-languages.json'; import { getDtfLocale } from './dtf-locale'; import localeMatch from './locale-match'; import states from './states'; -const translationTargetLanguages = Object.entries(languages.tl).map( - ([code, { name }]) => ({ - code, - name, - }), -); +// Preserve the original JS shape: values in `languages.tl` are strings, but the +// existing code destructures `{ name }` from each one (yielding `undefined`). +// `name` is unused downstream — only `code` is — so we keep the bug as-is and +// type the entry value as `{ name: string }` to match the runtime destructure. +const translationTargetLanguages = Object.entries( + languages.tl as unknown as Record, +).map(([code, { name }]) => ({ + code, + name, +})); const locales = [...navigator.languages]; const dtfLocale = getDtfLocale(); @@ -24,7 +28,7 @@ const localeTargetLanguages = () => 'en', ); -function getTranslateTargetLanguage(fromSettings = false) { +function getTranslateTargetLanguage(fromSettings = false): string | false { if (fromSettings) { const { contentTranslationTargetLanguage } = states.settings; if (contentTranslationTargetLanguage) { diff --git a/src/utils/handle-content-links.js b/src/utils/handle-content-links.ts similarity index 59% rename from src/utils/handle-content-links.js rename to src/utils/handle-content-links.ts index 717d54ff..36540094 100644 --- a/src/utils/handle-content-links.js +++ b/src/utils/handle-content-links.ts @@ -3,20 +3,50 @@ import states from './states'; const supportsHover = window.matchMedia('(hover: hover)').matches; -function handleContentLinks(opts) { +interface MentionLike { + url?: string; + acct?: string; + username?: string; + [key: string]: unknown; +} + +interface HandleContentLinksOpts { + mentions?: MentionLike[]; + instance?: string; + previewMode?: boolean; + statusURL?: string; +} + +// The handler is attached to elements rendering arbitrary status content; the +// element types vary (anchors, images, spans), so we keep DOM access loose and +// rely on the existing runtime guards (closest, contains, optional chaining). +type LinkClickTarget = HTMLElement & + Partial & + Partial; + +function handleContentLinks( + opts?: HandleContentLinksOpts, +): (e: MouseEvent) => void { const { mentions = [], instance, previewMode, statusURL } = opts || {}; - return (e) => { + return (e: MouseEvent) => { // If cmd/ctrl/shift/alt key is pressed or middle-click, let the browser handle it - if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.which === 2) { + if ( + e.metaKey || + e.ctrlKey || + e.shiftKey || + e.altKey || + (e as unknown as { which?: number }).which === 2 + ) { return; } - let { target } = e; + let target = e.target as LinkClickTarget | null; // Experiment opening custom emoji in a modal // TODO: Rename this function because it's not just for links - if (target.closest('.shortcode-emoji')) { - const { naturalWidth, naturalHeight, width, height } = target; + if (target!.closest('.shortcode-emoji')) { + const { naturalWidth, naturalHeight, width, height } = + target as HTMLImageElement; const kindaLargeRatio = 2; const kindaLarge = naturalWidth > width * kindaLargeRatio || @@ -28,8 +58,10 @@ function handleContentLinks(opts) { mediaAttachments: [ { type: 'image', - url: target.src, - description: target.title || target.alt, + url: (target as HTMLImageElement).src, + description: + (target as HTMLImageElement).title || + (target as HTMLImageElement).alt, }, ], }; @@ -37,19 +69,19 @@ function handleContentLinks(opts) { } } - target = target.closest('a'); + target = target!.closest('a') as LinkClickTarget | null; if (!target) return; // Only handle links inside, not itself or anything outside - if (!e.currentTarget.contains(target)) return; + if (!(e.currentTarget as Node | null)?.contains(target)) return; - const { href } = target; + const { href } = target as HTMLAnchorElement; if (isLeafletUrl(href)) { e.preventDefault(); e.stopPropagation(); states.showEmbedModal = { iframeUrl: href, url: href, - title: target.innerText.trim() || href, + title: (target as HTMLAnchorElement).innerText.trim() || href, }; return; } @@ -57,8 +89,9 @@ function handleContentLinks(opts) { const prevText = target.previousSibling?.textContent; const textBeforeLinkIsAt = prevText?.endsWith('@') || prevText?.endsWith('@'); + const targetInnerText = (target as HTMLAnchorElement).innerText; const textStartsWithAt = - target.innerText.startsWith('@') || target.innerText.startsWith('@'); + targetInnerText.startsWith('@') || targetInnerText.startsWith('@'); if ( ((target.classList.contains('u-url') || target.classList.contains('mention')) && @@ -66,7 +99,8 @@ function handleContentLinks(opts) { (textBeforeLinkIsAt && !textStartsWithAt) ) { const targetText = ( - target.querySelector('span') || target + (target.querySelector('span') as HTMLElement | null) || + (target as HTMLElement) ).innerText.trim(); const username = targetText.replace(/^[@@]/, ''); // Only fallback to acct/username check if url doesn't match @@ -101,27 +135,41 @@ function handleContentLinks(opts) { if (target.classList.contains('hashtag') || textBeforeLinkIsHash) { e.preventDefault(); e.stopPropagation(); - const tag = target.innerText.replace(/^[##]/, '').trim(); + const tag = targetInnerText.replace(/^[##]/, '').trim(); const hashURL = instance ? `#/${instance}/t/${tag}` : `#/t/${tag}`; console.log({ hashURL }); location.hash = hashURL; return; - } else if (states.unfurledLinks[href]?.url && statusURL !== href) { + } else if ( + (states.unfurledLinks as Record)[ + href + ]?.url && + statusURL !== href + ) { // If unfurled AND not self-referential e.preventDefault(); e.stopPropagation(); states.prevLocation = { pathname: location.hash.replace(/^#/, ''), }; - location.hash = `#${states.unfurledLinks[href].url}`; + location.hash = `#${ + ( + states.unfurledLinks as Record< + string, + { url?: string } | undefined + > + )[href]!.url + }`; return; } } try { const urlObj = URL.parse(href); - const domain = urlObj.hostname.replace(/^www\./i, ''); - const containsDomain = target.innerText + // Preserve original JS: don't null-check; rely on the try/catch to absorb + // a TypeError if URL.parse returns null. + const domain = urlObj!.hostname.replace(/^www\./i, ''); + const containsDomain = targetInnerText .toLowerCase() .includes(domain.toLowerCase()); // Only show this on non-hover devices (touch-only) @@ -130,7 +178,7 @@ function handleContentLinks(opts) { if (!containsDomain && !supportsHover) { e.preventDefault(); e.stopPropagation(); - const linkText = target.innerText.trim(); + const linkText = targetInnerText.trim(); states.showOpenLink = { url: href, linkText, diff --git a/src/utils/show-compose.js b/src/utils/show-compose.ts similarity index 82% rename from src/utils/show-compose.js rename to src/utils/show-compose.ts index a77f0bf0..0e9e2a14 100644 --- a/src/utils/show-compose.js +++ b/src/utils/show-compose.ts @@ -6,11 +6,11 @@ import states from './states'; const TOAST_DURATION = 5_000; // 5 seconds -export default function showCompose(opts) { +export default function showCompose(opts?: unknown): void { if (!opts) opts = true; if (states.showCompose) { - if (states.composerState.minimized) { + if ((states.composerState as { minimized?: unknown }).minimized) { showToast({ duration: TOAST_DURATION, text: t`A draft post is currently minimized. Post or discard it before creating a new one.`, diff --git a/src/utils/useTitle.js b/src/utils/useTitle.ts similarity index 56% rename from src/utils/useTitle.js rename to src/utils/useTitle.ts index d4c8aa0d..1d3cdb88 100644 --- a/src/utils/useTitle.js +++ b/src/utils/useTitle.ts @@ -4,24 +4,29 @@ import { subscribeKey } from 'valtio/utils'; import states from './states'; -const { PHANPY_CLIENT_NAME: CLIENT_NAME } = import.meta.env; +const { PHANPY_CLIENT_NAME: CLIENT_NAME } = import.meta.env as { + PHANPY_CLIENT_NAME: string; +}; -export default function useTitle(title, path) { +export default function useTitle( + title: string | null | undefined, + path: string | string[], +): void { function setTitle() { const { currentLocation } = states; const hasPaths = Array.isArray(path); - let paths = hasPaths ? path : []; + let paths: string[] = hasPaths ? (path as string[]) : []; // Workaround for matchPath not working for optional path segments // https://github.com/remix-run/react-router/discussions/9862 - if (!hasPaths && /:?\w+\?/.test(path)) { - paths.push(path.replace(/(:\w+)\?/g, '$1')); - paths.push(path.replace(/\/?:\w+\?/g, '')); + if (!hasPaths && /:?\w+\?/.test(path as string)) { + paths.push((path as string).replace(/(:\w+)\?/g, '$1')); + paths.push((path as string).replace(/\/?:\w+\?/g, '')); } - let matched = false; + let matched: unknown = false; if (paths.length) { - matched = paths.some((p) => matchPath(p, currentLocation)); + matched = paths.some((p) => matchPath(p, currentLocation as string)); } else if (path) { - matched = matchPath(path, currentLocation); + matched = matchPath(path as string, currentLocation as string); } console.debug('setTitle', { title, path, currentLocation, paths, matched }); if (matched) { -- 2.51.2