Enable noUncheckedIndexedAccess and fix all 98 call sites it surfaced across 18 files. Every fix is behaviour-preserving — the flag flagged reads TypeScript couldn't prove in-bounds, all of which were already guarded by a length/index check, a findIndex result, or a non-empty invariant: - read-once so a ternary's narrowing sticks (Feed overrides, linkify mention); - non-null assertion AFTER an existing guard (PlayerModal/VideoEditor queue+cut-list indexing, modules step, useUndoable stacks) with a comment stating the invariant; - nullish fallback where undefined is a real possibility (GlassTuner slider ?? def, ConfigPanel active group ?? [], descriptionLinks id ?? null); - optional chaining where the entry genuinely can be absent (PlexBrowse IO entry); - a non-empty tuple type for the RELATIVE_UNITS constant (encodes "always has [0]"). The single highest-leverage fix: PlayerModal's `active` became `queue?.[index] ?? video` (always Video), clearing 34 of the 45 errors in that file at once. Verified: tsc/eslint/prettier clean, 56 vitest + 34 pytest + 17/17 e2e green (the e2e suite exercises the feed/player/channel components touched here).
79 lines
3.3 KiB
TypeScript
79 lines
3.3 KiB
TypeScript
import { Fragment, type ReactNode } from "react";
|
|
|
|
// Known platform → profile base URL, for "<platform>: @handle" mentions in descriptions.
|
|
const PLATFORMS: Record<string, string> = {
|
|
x: "https://x.com/",
|
|
twitter: "https://x.com/",
|
|
instagram: "https://instagram.com/",
|
|
insta: "https://instagram.com/",
|
|
ig: "https://instagram.com/",
|
|
facebook: "https://facebook.com/",
|
|
fb: "https://facebook.com/",
|
|
tiktok: "https://www.tiktok.com/@",
|
|
youtube: "https://www.youtube.com/@",
|
|
yt: "https://www.youtube.com/@",
|
|
twitch: "https://www.twitch.tv/",
|
|
threads: "https://www.threads.net/@",
|
|
telegram: "https://t.me/",
|
|
soundcloud: "https://soundcloud.com/",
|
|
};
|
|
|
|
// Two link shapes, combined into one scanner:
|
|
// URL — explicit http(s):// URLs, www.* hosts, or domain.tld/path bare URLs (required path +
|
|
// letters-only TLD keep version numbers / file names out). Non-http gets an https:// href.
|
|
// MENTION — "<platform>: @handle" where <platform> is one of the known keys above. The colon/arrow
|
|
// separator is REQUIRED, so a stray "x" in prose won't match. Only the @handle is linked
|
|
// (to the platform's profile URL); the "<platform>: " label stays as text. Bare @handles
|
|
// with no platform, and #hashtags, are left plain.
|
|
const URL_SOURCE =
|
|
"(?:https?://|www\\.)[^\\s<]+[^\\s<.,!?;:)\\]}\"']|[a-z0-9-]+(?:\\.[a-z0-9-]+)*\\.[a-z]{2,}/[^\\s<]*[^\\s<.,!?;:)\\]}\"']";
|
|
const MENTION_SOURCE =
|
|
"\\b(" +
|
|
Object.keys(PLATFORMS).join("|") +
|
|
")\\b[ \\t]*[::►▶▸]+[ \\t]*@([a-z0-9_](?:[a-z0-9_.]{0,28}[a-z0-9_])?)";
|
|
const TOKEN_RE = new RegExp("(" + URL_SOURCE + ")|" + MENTION_SOURCE, "gi");
|
|
|
|
/**
|
|
* Turn links inside a plain-text string into clickable <a>s that open in a new tab (safe rel):
|
|
* bare URLs (http(s)/www/domain.tld/path) and "<platform>: @handle" social mentions. Everything
|
|
* else is returned as text, so it composes with `whitespace-pre-wrap` (line breaks preserved).
|
|
*/
|
|
export function linkify(text: string): ReactNode[] {
|
|
const out: ReactNode[] = [];
|
|
let last = 0;
|
|
let key = 0;
|
|
TOKEN_RE.lastIndex = 0;
|
|
const link = (href: string, label: string) => (
|
|
<a
|
|
key={key++}
|
|
href={href}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-accent hover:underline break-all"
|
|
>
|
|
{label}
|
|
</a>
|
|
);
|
|
let m: RegExpExecArray | null;
|
|
while ((m = TOKEN_RE.exec(text)) !== null) {
|
|
if (m.index > last) out.push(<Fragment key={key++}>{text.slice(last, m.index)}</Fragment>);
|
|
if (m[1]) {
|
|
// Plain URL.
|
|
const raw = m[1];
|
|
out.push(link(/^https?:\/\//i.test(raw) ? raw : `https://${raw}`, raw));
|
|
} else {
|
|
// Reached only when the MENTION alternative matched, so its groups are present: m[2] is the
|
|
// platform (always one of PLATFORMS' own keys — the pattern is built from them) and m[3] the
|
|
// handle. "<platform>: @handle" — keep the "<platform>: " prefix as text, link only the handle.
|
|
const platform = m[2]!;
|
|
const handle = m[3]!;
|
|
const base = PLATFORMS[platform.toLowerCase()]!;
|
|
out.push(<Fragment key={key++}>{m[0].slice(0, m[0].length - handle.length - 1)}</Fragment>);
|
|
out.push(link(base + handle, "@" + handle));
|
|
}
|
|
last = m.index + m[0].length;
|
|
}
|
|
if (last < text.length) out.push(<Fragment key={key++}>{text.slice(last)}</Fragment>);
|
|
return out;
|
|
}
|