diff --git a/__tests__/components/waves/OpenGraphPreview.test.tsx b/__tests__/components/waves/OpenGraphPreview.test.tsx index e0f7d6741b..c24221c82c 100644 --- a/__tests__/components/waves/OpenGraphPreview.test.tsx +++ b/__tests__/components/waves/OpenGraphPreview.test.tsx @@ -139,4 +139,23 @@ describe('OpenGraphPreview', () => { ); }); + it('wraps long unbroken segments to keep layout consistent', () => { + (removeBaseEndpoint as jest.Mock).mockReturnValue('/article'); + + const longUrl = `https://example.com/${'a'.repeat(48)}`; + + render( + + ); + + const wrappedSegment = screen.getByText(longUrl); + expect(wrappedSegment.tagName).toBe('SPAN'); + expect(wrappedSegment).toHaveClass('tw-break-all'); + }); + }); diff --git a/codex/tickets/TKT-0009.md b/codex/tickets/TKT-0009.md index d4d03f4964..09462bf416 100644 --- a/codex/tickets/TKT-0009.md +++ b/codex/tickets/TKT-0009.md @@ -37,3 +37,4 @@ title: Refactor Brain notifications shell for modular clarity - 2025-10-15T08:35:49Z – Outlined modular breakdown: main shell in `index.tsx`, hook layer (`useNotificationsController`, `useNotificationsScroll`), presentational states (`NotificationsContent`, `NotificationsStateMessage`), and error utilities. - 2025-10-15T09:44:55Z – Refactored Notifications into modular files, attempted type-check/lint (failing due to pre-existing repository issues). - 2025-10-15T10:56:58Z – Hardened mark-all-as-read success handler against context cleanup failures. +- 2025-10-16T07:48:38Z – Removed unicode regex flag from `OpenGraphPreview` to restore compatibility with current TS target; type-check attempt still blocked by existing test typing failures. diff --git a/components/waves/LinkHandlerFrame.tsx b/components/waves/LinkHandlerFrame.tsx index dfc6068f24..3248730ff3 100644 --- a/components/waves/LinkHandlerFrame.tsx +++ b/components/waves/LinkHandlerFrame.tsx @@ -1,5 +1,7 @@ import type { ReactNode } from "react"; +import { removeBaseEndpoint } from "@/helpers/Helpers"; + import ChatItemHrefButtons from "./ChatItemHrefButtons"; interface LinkHandlerFrameProps { @@ -15,13 +17,22 @@ export default function LinkHandlerFrame({ hideLink = false, relativeHref, }: LinkHandlerFrameProps) { + const effectiveRelativeHref = + relativeHref ?? + (() => { + const relative = removeBaseEndpoint(href); + return relative?.startsWith("/") ? relative : undefined; + })(); + return ( -
-
{children}
+
+
+ {children} +
); diff --git a/components/waves/LinkPreviewCard.tsx b/components/waves/LinkPreviewCard.tsx index e0ca062a5c..b353ba7462 100644 --- a/components/waves/LinkPreviewCard.tsx +++ b/components/waves/LinkPreviewCard.tsx @@ -86,8 +86,8 @@ export default function LinkPreviewCard({ return ( -
-
+
+
{fallbackContent}
@@ -102,7 +102,7 @@ export default function LinkPreviewCard({ if (state.type === "ens") { return ( -
+
diff --git a/components/waves/OpenGraphPreview.tsx b/components/waves/OpenGraphPreview.tsx index d4bcfe11d6..628cca6a43 100644 --- a/components/waves/OpenGraphPreview.tsx +++ b/components/waves/OpenGraphPreview.tsx @@ -1,4 +1,4 @@ -import { type ReactNode } from "react"; +import { Fragment, type ReactElement, type ReactNode } from "react"; import Image from "next/image"; import Link from "next/link"; @@ -51,6 +51,7 @@ const IMAGE_KEYS = [ "secureUrl", ]; const IMAGE_COLLECTION_KEYS = ["images", "ogImages", "og_images", "thumbnails"]; +const LONG_UNBROKEN_SEGMENT_THRESHOLD = 32; function readFirstString( data: OpenGraphPreviewData | null | undefined, @@ -135,6 +136,45 @@ function extractImageUrl( return undefined; } +function wrapLongUnbrokenSegments(value: string | undefined): ReactNode { + if (!value) { + return value ?? ""; + } + + const tokens = value.split(/(\s+)/); + let mutated = false; + let offset = 0; + + const nodes = tokens + .map((token) => { + if (!token) { + return null; + } + + const key = `og-wrap-${offset}`; + offset += token.length; + + const trimmed = token.trim(); + if (trimmed.length >= LONG_UNBROKEN_SEGMENT_THRESHOLD) { + mutated = true; + return ( + + {token} + + ); + } + + return {token}; + }) + .filter((token): token is ReactElement => token !== null); + + if (!mutated) { + return value; + } + + return nodes; +} + function extractDomainFromUrl(url: string | undefined): string | undefined { if (!url) { return undefined; @@ -197,8 +237,10 @@ export function LinkPreviewCardLayout({ const relativeHref = getRelativeHref(href); return ( -
-
{children}
+
+
+ {children} +
); @@ -232,7 +274,7 @@ export default function OpenGraphPreview({ return (
+ className="tw-w-full tw-rounded-xl tw-border tw-border-solid tw-border-iron-700 tw-bg-iron-900/40 tw-p-4">
@@ -256,7 +298,7 @@ export default function OpenGraphPreview({ return (

@@ -266,8 +308,8 @@ export default function OpenGraphPreview({ href={effectiveHref} target={linkTarget} rel={linkRel} - className="tw-text-sm tw-font-semibold tw-text-iron-100 tw-no-underline tw-transition tw-duration-200 hover:tw-text-white"> - {domain ?? href} + className="tw-break-words tw-[overflow-wrap:anywhere] tw-text-sm tw-font-semibold tw-text-iron-100 tw-no-underline tw-transition tw-duration-200 hover:tw-text-white"> + {wrapLongUnbrokenSegments(domain ?? href)}

@@ -278,7 +320,7 @@ export default function OpenGraphPreview({ return (
{imageUrl && ( @@ -304,19 +346,19 @@ export default function OpenGraphPreview({
{domain && ( - {domain} + {wrapLongUnbrokenSegments(domain)} )} - {title ?? domain ?? href} + className="tw-break-words tw-[overflow-wrap:anywhere] tw-text-lg tw-font-semibold tw-leading-snug tw-text-iron-100 tw-no-underline tw-transition tw-duration-200 hover:tw-text-white"> + {wrapLongUnbrokenSegments(title ?? domain ?? href)} {description && ( -

- {description} +

+ {wrapLongUnbrokenSegments(description)}

)}