From f1dc85be95ab61b8489d31308f1a8da5b4a5d11d Mon Sep 17 00:00:00 2001 From: ragnep Date: Tue, 21 Oct 2025 14:15:11 +0300 Subject: [PATCH 1/3] drop preview url fix Signed-off-by: ragnep --- .../linkHandlersRegistry.test.tsx | 73 ++++++++++++- __tests__/helpers/SeizeLinkParser.test.ts | 71 ++++++++++++ .../part/dropPartMarkdown/handlers/seize.tsx | 16 ++- .../part/dropPartMarkdown/linkHandlers.tsx | 39 ++++--- helpers/SeizeLinkParser.ts | 103 +++++++++++++++++- 5 files changed, 282 insertions(+), 20 deletions(-) diff --git a/__tests__/components/drops/view/part/dropPartMarkdown/linkHandlersRegistry.test.tsx b/__tests__/components/drops/view/part/dropPartMarkdown/linkHandlersRegistry.test.tsx index b7505fd40a..2a09efdf81 100644 --- a/__tests__/components/drops/view/part/dropPartMarkdown/linkHandlersRegistry.test.tsx +++ b/__tests__/components/drops/view/part/dropPartMarkdown/linkHandlersRegistry.test.tsx @@ -3,6 +3,7 @@ import type { ReactNode } from "react"; import { render, screen } from "@testing-library/react"; import { createLinkRenderer } from "@/components/drops/view/part/dropPartMarkdown/linkHandlers"; +import { ensureStableSeizeLink } from "@/helpers/SeizeLinkParser"; import { publicEnv } from "@/config/env"; jest.mock("@/components/drops/view/part/dropPartMarkdown/youtubePreview", () => ({ @@ -10,6 +11,14 @@ jest.mock("@/components/drops/view/part/dropPartMarkdown/youtubePreview", () => default: ({ href }: { href: string }) => (
), +j jest.mock("@/components/waves/drops/DropItemChat", () => ({ + __esModule: true, +- default: ({ dropId }: { dropId: string }) =>
, ++ default: ({ dropId, href }: { dropId: string; href: string }) => ( ++
++
++
++ ), })); jest.mock("@/components/drops/view/part/DropPartMarkdownImage", () => ({ @@ -100,13 +109,41 @@ jest.mock("@/components/waves/list/WaveItemChat", () => ({ jest.mock("@/components/waves/drops/DropItemChat", () => ({ __esModule: true, - default: ({ dropId }: { dropId: string }) =>
, + default: ({ dropId, href }: { dropId: string; href: string }) => ( +
+
+
+ ), })); +jest.mock("@/helpers/SeizeLinkParser", () => { + const actual = jest.requireActual("@/helpers/SeizeLinkParser"); + let currentHrefOverride: string | undefined; + + const ensureStableSeizeLink = (href: string) => + actual.ensureStableSeizeLink(href, currentHrefOverride); + + (ensureStableSeizeLink as any).__setCurrentHref = (href?: string) => { + currentHrefOverride = href; + }; + + return { + ...actual, + ensureStableSeizeLink, + }; +}); + const onQuoteClick = jest.fn(); const baseRenderer = () => createLinkRenderer({ onQuoteClick }); +const setEnsureCurrentHref = (href?: string) => { + const setter = (ensureStableSeizeLink as any).__setCurrentHref; + if (typeof setter === "function") { + setter(href); + } +}; + describe("createLinkRenderer", () => { const FALLBACK_BASE_ENDPOINT = "https://6529.io"; const originalBaseEndpointEnv = publicEnv.BASE_ENDPOINT; @@ -116,6 +153,7 @@ describe("createLinkRenderer", () => { jest.clearAllMocks(); publicEnv.BASE_ENDPOINT = FALLBACK_BASE_ENDPOINT; process.env.BASE_ENDPOINT = FALLBACK_BASE_ENDPOINT; + setEnsureCurrentHref(undefined); }); afterEach(() => { @@ -125,6 +163,7 @@ describe("createLinkRenderer", () => { } else { process.env.BASE_ENDPOINT = originalProcessBaseEndpoint; } + setEnsureCurrentHref(undefined); }); it("renders DropPartMarkdownImage for img elements", () => { @@ -182,6 +221,38 @@ describe("createLinkRenderer", () => { expect(screen.getByTestId("drop-card")).toHaveAttribute("data-drop", "def"); }); + it("normalizes root drop links using current location context", () => { + setEnsureCurrentHref("https://6529.io/messages?wave=current-wave"); + + const { renderAnchor } = baseRenderer(); + const element = renderAnchor({ href: "https://6529.io/?drop=drop-123" } as any); + render(<>{element}); + + expect(screen.getByTestId("drop-card")).toHaveAttribute("data-drop", "drop-123"); + expect(screen.getByTestId("chat-buttons")).toHaveAttribute( + "data-href", + "https://6529.io/messages?wave=current-wave&drop=drop-123" + ); + setEnsureCurrentHref(undefined); + }); + + it("normalizes drop links shared from other paths", () => { + setEnsureCurrentHref("https://6529.io/messages?wave=current-wave"); + + const { renderAnchor } = baseRenderer(); + const element = renderAnchor({ + href: "https://6529.io/waves?wave=other-wave&drop=drop-456", + } as any); + render(<>{element}); + + expect(screen.getByTestId("drop-card")).toHaveAttribute("data-drop", "drop-456"); + expect(screen.getByTestId("chat-buttons")).toHaveAttribute( + "data-href", + "https://6529.io/messages?wave=current-wave&drop=drop-456" + ); + setEnsureCurrentHref(undefined); + }); + it.each([ ["standard status link", "https://twitter.com/user/status/987654321"], ["mobile twitter link", "https://mobile.twitter.com/user/status/987654321"], diff --git a/__tests__/helpers/SeizeLinkParser.test.ts b/__tests__/helpers/SeizeLinkParser.test.ts index 6ed545bf1e..4af5965568 100644 --- a/__tests__/helpers/SeizeLinkParser.test.ts +++ b/__tests__/helpers/SeizeLinkParser.test.ts @@ -1,6 +1,7 @@ describe("SeizeLinkParser with mocked BASE_ENDPOINT", () => { let parseSeizeQuoteLink: any; let parseSeizeQueryLink: any; + let ensureStableSeizeLink: any; beforeAll(() => { jest.resetModules(); @@ -13,6 +14,7 @@ describe("SeizeLinkParser with mocked BASE_ENDPOINT", () => { ({ parseSeizeQuoteLink, parseSeizeQueryLink, + ensureStableSeizeLink, } = require("@/helpers/SeizeLinkParser")); }); @@ -79,4 +81,73 @@ describe("SeizeLinkParser with mocked BASE_ENDPOINT", () => { expect(res).toBeNull(); }); }); + + describe("ensureStableSeizeLink", () => { + it("returns original href for non-base URLs", () => { + const incoming = "https://othersite.com/?drop=drop-id"; + const current = "https://site.com/messages?wave=abc"; + expect(ensureStableSeizeLink(incoming, current)).toBe(incoming); + }); + + it("returns original href when drop param missing", () => { + const incoming = "https://site.com/"; + const current = "https://site.com/messages?wave=abc"; + expect(ensureStableSeizeLink(incoming, current)).toBe(incoming); + }); + + it("rewrites root drop link to current path with drop param", () => { + const incoming = "https://site.com/?drop=drop-id"; + const current = "https://site.com/messages?wave=abc"; + expect(ensureStableSeizeLink(incoming, current)).toBe( + "https://site.com/messages?wave=abc&drop=drop-id" + ); + }); + + it("handles relative drop links", () => { + const incoming = "?drop=drop-id"; + const current = "https://site.com/messages"; + expect(ensureStableSeizeLink(incoming, current)).toBe( + "https://site.com/messages?drop=drop-id" + ); + }); + + it("preserves existing query params and replaces drop", () => { + const incoming = "https://site.com/?drop=new-drop"; + const current = "https://site.com/messages?wave=abc&drop=old-drop"; + expect(ensureStableSeizeLink(incoming, current)).toBe( + "https://site.com/messages?wave=abc&drop=new-drop" + ); + }); + + it("rebases drop links from other paths onto current location", () => { + const incoming = "https://site.com/waves?wave=abc&drop=def"; + const current = "https://site.com/messages?wave=xyz"; + expect(ensureStableSeizeLink(incoming, current)).toBe( + "https://site.com/messages?wave=xyz&drop=def" + ); + }); + + it("refreshes cached origin when BASE_ENDPOINT changes", () => { + const { publicEnv } = require("@/config/env"); + + expect( + ensureStableSeizeLink( + "https://site.com/?drop=drop-1", + "https://site.com/messages" + ) + ).toBe("https://site.com/messages?drop=drop-1"); + + publicEnv.BASE_ENDPOINT = "https://other.com"; + try { + expect( + ensureStableSeizeLink( + "https://other.com/?drop=drop-2", + "https://other.com/messages" + ) + ).toBe("https://other.com/messages?drop=drop-2"); + } finally { + publicEnv.BASE_ENDPOINT = "https://site.com"; + } + }); + }); }); diff --git a/components/drops/view/part/dropPartMarkdown/handlers/seize.tsx b/components/drops/view/part/dropPartMarkdown/handlers/seize.tsx index 3fff42ffe0..0ef205bb3a 100644 --- a/components/drops/view/part/dropPartMarkdown/handlers/seize.tsx +++ b/components/drops/view/part/dropPartMarkdown/handlers/seize.tsx @@ -4,6 +4,7 @@ import { ApiDrop } from "@/generated/models/ApiDrop"; import { parseSeizeQuoteLink, parseSeizeQueryLink, + getSeizeBaseOrigin, type SeizeQuoteLinkInfo, } from "@/helpers/SeizeLinkParser"; @@ -91,12 +92,21 @@ const createSeizeWaveHandler = (): LinkHandler => ); const getDropId = (href: string): string | null => { - const result = parseSeizeQueryLink(href, "/waves", ["wave", "drop"], true); - if (!result || typeof result.drop !== "string") { + const baseOrigin = getSeizeBaseOrigin(); + if (!baseOrigin) { return null; } - return result.drop; + try { + const url = new URL(href, baseOrigin); + if (url.origin !== baseOrigin) { + return null; + } + const dropId = url.searchParams.get("drop"); + return dropId ?? null; + } catch { + return null; + } }; const createSeizeDropHandler = (): LinkHandler => diff --git a/components/drops/view/part/dropPartMarkdown/linkHandlers.tsx b/components/drops/view/part/dropPartMarkdown/linkHandlers.tsx index 1071d62e4d..b06d04f187 100644 --- a/components/drops/view/part/dropPartMarkdown/linkHandlers.tsx +++ b/components/drops/view/part/dropPartMarkdown/linkHandlers.tsx @@ -8,6 +8,7 @@ import { ErrorBoundary } from "react-error-boundary"; import { ExtraProps } from "react-markdown"; import { ApiDrop } from "@/generated/models/ApiDrop"; +import { ensureStableSeizeLink } from "@/helpers/SeizeLinkParser"; import LinkPreviewCard from "@/components/waves/LinkPreviewCard"; import DropPartMarkdownImage from "../DropPartMarkdownImage"; @@ -68,21 +69,28 @@ export const createLinkRenderer = ({ const renderAnchor: LinkRenderer["renderAnchor"] = (props) => { const { href } = props; - if (!href || !isValidLink(href)) { + if (!href) { return null; } - const parsedUrl = parseUrl(href); - const renderFallbackAnchor = () => renderExternalOrInternalLink(href, props); - const matchSeize = findMatch(seizeHandlers, href); + const stableHref = ensureStableSeizeLink(href); + if (!isValidLink(stableHref)) { + return null; + } + + const parsedUrl = parseUrl(stableHref); + const anchorProps = { ...props, href: stableHref }; + const renderFallbackAnchor = () => + renderExternalOrInternalLink(stableHref, anchorProps); + const matchSeize = findMatch(seizeHandlers, stableHref); const renderOpenGraph = () => { - if (!shouldUseOpenGraphPreview(href, parsedUrl)) { + if (!shouldUseOpenGraphPreview(stableHref, parsedUrl)) { return renderFallbackAnchor(); } return ( renderFallbackAnchor()} /> ); @@ -117,7 +125,7 @@ export const createLinkRenderer = ({ const renderFromHandler = (handler: LinkHandler): ReactElement | null => { try { - const rendered = handler.render(href); + const rendered = handler.render(stableHref); if (rendered === null || rendered === undefined) { throw new Error("Link handler returned no content"); } @@ -138,7 +146,7 @@ export const createLinkRenderer = ({ } } - const matchExternal = findMatch(handlers, href); + const matchExternal = findMatch(handlers, stableHref); if (matchExternal) { const rendered = renderFromHandler(matchExternal); @@ -156,22 +164,27 @@ export const createLinkRenderer = ({ }; const isSmartLink = (href: string): boolean => { - if (!href || !isValidLink(href)) { + if (!href) { + return false; + } + + const stableHref = ensureStableSeizeLink(href); + if (!isValidLink(stableHref)) { return false; } - const parsedUrl = parseUrl(href); - const seizeMatch = findMatch(seizeHandlers, href); + const parsedUrl = parseUrl(stableHref); + const seizeMatch = findMatch(seizeHandlers, stableHref); if (seizeMatch) { return seizeMatch.display === "block"; } - const match = findMatch(handlers, href); + const match = findMatch(handlers, stableHref); if (match) { return match.display === "block"; } - return shouldUseOpenGraphPreview(href, parsedUrl); + return shouldUseOpenGraphPreview(stableHref, parsedUrl); }; return { diff --git a/helpers/SeizeLinkParser.ts b/helpers/SeizeLinkParser.ts index f29845b452..cb22dd1360 100644 --- a/helpers/SeizeLinkParser.ts +++ b/helpers/SeizeLinkParser.ts @@ -1,5 +1,36 @@ import { publicEnv } from "@/config/env"; +let cachedBaseEndpoint: string | undefined; +let cachedBaseUrl: URL | null | undefined; + +const getBaseUrl = (): URL | null => { + const current = publicEnv.BASE_ENDPOINT; + + if (!current) { + cachedBaseEndpoint = undefined; + cachedBaseUrl = null; + return null; + } + + if (cachedBaseEndpoint === current && cachedBaseUrl !== undefined) { + return cachedBaseUrl; + } + + cachedBaseEndpoint = current; + try { + cachedBaseUrl = new URL(current); + } catch { + cachedBaseUrl = null; + } + + return cachedBaseUrl; +}; + +export const getSeizeBaseOrigin = (): string | null => { + const baseUrl = getBaseUrl(); + return baseUrl ? baseUrl.origin : null; +}; + export interface SeizeQuoteLinkInfo { waveId: string; serialNo?: string; @@ -24,10 +55,19 @@ export function parseSeizeQueryLink( exact: boolean = false ): Record | null { try { - const url = new URL(href); + const baseUrl = getBaseUrl(); + if (!baseUrl) { + return null; + } - const allowedOrigins = new Set([new URL(publicEnv.BASE_ENDPOINT).origin]); - if (!allowedOrigins.has(url.origin)) return null; + let url: URL; + try { + url = new URL(href); + } catch { + url = new URL(href, baseUrl.origin); + } + + if (url.origin !== baseUrl.origin) return null; if (url.pathname !== path) return null; if (exact) { @@ -56,3 +96,60 @@ export function parseSeizeQueryLink( return null; } } + +export const ensureStableSeizeLink = ( + href: string, + currentHref?: string +): string => { + try { + const baseUrl = getBaseUrl(); + if (!baseUrl) { + return href; + } + + let targetUrl: URL; + try { + targetUrl = new URL(href); + } catch { + targetUrl = new URL(href, baseUrl.origin); + } + + if (targetUrl.origin !== baseUrl.origin) { + return href; + } + + const dropId = targetUrl.searchParams.get("drop"); + if (!dropId) { + return href; + } + + const resolvedCurrentHref = + currentHref ?? + (typeof window !== "undefined" ? window.location.href : undefined); + if (!resolvedCurrentHref) { + return href; + } + + let currentUrl: URL; + try { + currentUrl = new URL(resolvedCurrentHref); + } catch { + return href; + } + + if (currentUrl.origin !== baseUrl.origin) { + return href; + } + + const params = new URLSearchParams(currentUrl.search); + params.set("drop", dropId); + const query = params.toString(); + const hash = currentUrl.hash ?? ""; + + const path = currentUrl.pathname || "/"; + + return `${baseUrl.origin}${path}${query ? `?${query}` : ""}${hash}`; + } catch { + return href; + } +}; From 54b3914bdb692edfb39386f456ab85607f86cd2f Mon Sep 17 00:00:00 2001 From: ragnep Date: Tue, 21 Oct 2025 14:24:46 +0300 Subject: [PATCH 2/3] wip Signed-off-by: ragnep --- .../dropPartMarkdown/linkHandlersRegistry.test.tsx | 12 +++--------- helpers/SeizeLinkParser.ts | 13 +++++++++---- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/__tests__/components/drops/view/part/dropPartMarkdown/linkHandlersRegistry.test.tsx b/__tests__/components/drops/view/part/dropPartMarkdown/linkHandlersRegistry.test.tsx index 2a09efdf81..53f13c4606 100644 --- a/__tests__/components/drops/view/part/dropPartMarkdown/linkHandlersRegistry.test.tsx +++ b/__tests__/components/drops/view/part/dropPartMarkdown/linkHandlersRegistry.test.tsx @@ -11,19 +11,13 @@ jest.mock("@/components/drops/view/part/dropPartMarkdown/youtubePreview", () => default: ({ href }: { href: string }) => (
), -j jest.mock("@/components/waves/drops/DropItemChat", () => ({ - __esModule: true, -- default: ({ dropId }: { dropId: string }) =>
, -+ default: ({ dropId, href }: { dropId: string; href: string }) => ( -+
-+
-+
-+ ), })); jest.mock("@/components/drops/view/part/DropPartMarkdownImage", () => ({ __esModule: true, - default: ({ src }: { src: string }) => , + default: ({ src }: { src: string }) => ( + + ), })); jest.mock("@/components/drops/view/part/dropPartMarkdown/renderers", () => ({ diff --git a/helpers/SeizeLinkParser.ts b/helpers/SeizeLinkParser.ts index cb22dd1360..8d352c1a72 100644 --- a/helpers/SeizeLinkParser.ts +++ b/helpers/SeizeLinkParser.ts @@ -123,10 +123,13 @@ export const ensureStableSeizeLink = ( return href; } + const globalWindow = + typeof globalThis !== "undefined" + ? (globalThis as typeof globalThis & { window?: Window }).window + : undefined; const resolvedCurrentHref = - currentHref ?? - (typeof window !== "undefined" ? window.location.href : undefined); - if (!resolvedCurrentHref) { + currentHref ?? globalWindow?.location?.href ?? undefined; + if (resolvedCurrentHref === undefined || resolvedCurrentHref === "") { return href; } @@ -148,7 +151,9 @@ export const ensureStableSeizeLink = ( const path = currentUrl.pathname || "/"; - return `${baseUrl.origin}${path}${query ? `?${query}` : ""}${hash}`; + const querySuffix = query ? `?${query}` : ""; + + return `${baseUrl.origin}${path}${querySuffix}${hash}`; } catch { return href; } From e0733437495eea37c025a71de4fcc71b05b76e6e Mon Sep 17 00:00:00 2001 From: ragnep Date: Tue, 21 Oct 2025 14:47:40 +0300 Subject: [PATCH 3/3] wip Signed-off-by: ragnep --- .../dropPartMarkdown/linkHandlersRegistry.test.tsx | 8 ++++---- helpers/SeizeLinkParser.ts | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/__tests__/components/drops/view/part/dropPartMarkdown/linkHandlersRegistry.test.tsx b/__tests__/components/drops/view/part/dropPartMarkdown/linkHandlersRegistry.test.tsx index 53f13c4606..1ed8abd6dc 100644 --- a/__tests__/components/drops/view/part/dropPartMarkdown/linkHandlersRegistry.test.tsx +++ b/__tests__/components/drops/view/part/dropPartMarkdown/linkHandlersRegistry.test.tsx @@ -147,7 +147,7 @@ describe("createLinkRenderer", () => { jest.clearAllMocks(); publicEnv.BASE_ENDPOINT = FALLBACK_BASE_ENDPOINT; process.env.BASE_ENDPOINT = FALLBACK_BASE_ENDPOINT; - setEnsureCurrentHref(undefined); + setEnsureCurrentHref(); }); afterEach(() => { @@ -157,7 +157,7 @@ describe("createLinkRenderer", () => { } else { process.env.BASE_ENDPOINT = originalProcessBaseEndpoint; } - setEnsureCurrentHref(undefined); + setEnsureCurrentHref(); }); it("renders DropPartMarkdownImage for img elements", () => { @@ -227,7 +227,7 @@ describe("createLinkRenderer", () => { "data-href", "https://6529.io/messages?wave=current-wave&drop=drop-123" ); - setEnsureCurrentHref(undefined); + setEnsureCurrentHref(); }); it("normalizes drop links shared from other paths", () => { @@ -244,7 +244,7 @@ describe("createLinkRenderer", () => { "data-href", "https://6529.io/messages?wave=current-wave&drop=drop-456" ); - setEnsureCurrentHref(undefined); + setEnsureCurrentHref(); }); it.each([ diff --git a/helpers/SeizeLinkParser.ts b/helpers/SeizeLinkParser.ts index 8d352c1a72..bec5b7d80d 100644 --- a/helpers/SeizeLinkParser.ts +++ b/helpers/SeizeLinkParser.ts @@ -123,12 +123,12 @@ export const ensureStableSeizeLink = ( return href; } - const globalWindow = - typeof globalThis !== "undefined" - ? (globalThis as typeof globalThis & { window?: Window }).window - : undefined; - const resolvedCurrentHref = - currentHref ?? globalWindow?.location?.href ?? undefined; + let globalWindow: Window | undefined; + if (typeof globalThis === "object" && "window" in globalThis) { + globalWindow = (globalThis as typeof globalThis & { window?: Window }).window; + } + + const resolvedCurrentHref = currentHref ?? globalWindow?.location?.href; if (resolvedCurrentHref === undefined || resolvedCurrentHref === "") { return href; }