diff --git a/__tests__/components/drops/view/part/DropPartMarkdown.test.tsx b/__tests__/components/drops/view/part/DropPartMarkdown.test.tsx index c596ca3c4e..5bf532e1b7 100644 --- a/__tests__/components/drops/view/part/DropPartMarkdown.test.tsx +++ b/__tests__/components/drops/view/part/DropPartMarkdown.test.tsx @@ -321,6 +321,11 @@ describe("DropPartMarkdown", () => { expect(fallbackLink).toHaveAttribute("target", "_blank"); expect(fallbackLink).toHaveTextContent(/Tweet unavailable/i); expect(fallbackLink).toHaveTextContent(/Open on X/i); + const tweetWrapper = fallbackLink.parentElement; + if (!tweetWrapper) { + throw new Error("Expected tweet fallback wrapper"); + } + expect(tweetWrapper).toHaveClass("tw-w-full", "lg:tw-max-w-[480px]"); }); it("renders a fallback link when the tweet embed throws", async () => { @@ -349,6 +354,11 @@ describe("DropPartMarkdown", () => { "href", "https://twitter.com/foo/status/1111111111" ); + const tweetWrapper = fallbackLink.parentElement; + if (!tweetWrapper) { + throw new Error("Expected tweet fallback wrapper"); + } + expect(tweetWrapper).toHaveClass("tw-w-full", "lg:tw-max-w-[480px]"); } finally { consoleErrorSpy.mockRestore(); } diff --git a/__tests__/components/drops/view/part/dropPartMarkdown/handlers/gif.test.tsx b/__tests__/components/drops/view/part/dropPartMarkdown/handlers/gif.test.tsx new file mode 100644 index 0000000000..0fc2198683 --- /dev/null +++ b/__tests__/components/drops/view/part/dropPartMarkdown/handlers/gif.test.tsx @@ -0,0 +1,67 @@ +import { render, screen } from "@testing-library/react"; + +import { createGifHandler } from "@/components/drops/view/part/dropPartMarkdown/handlers/gif"; +import { renderGifEmbed } from "@/components/drops/view/part/dropPartMarkdown/renderers"; + +jest.mock("@/components/drops/view/part/dropPartMarkdown/renderers", () => ({ + renderGifEmbed: jest.fn((url: string, options?: { fixedSize?: boolean }) => ( +
+ )), +})); + +const mockRenderGifEmbed = renderGifEmbed as jest.MockedFunction< + typeof renderGifEmbed +>; + +describe("createGifHandler", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("matches only Tenor GIF URLs", () => { + const handler = createGifHandler(); + + expect(handler.match("https://media.tenor.com/abc/tenor.gif")).toBe(true); + expect(handler.match("https://media.tenor.com/abc/tenor.jpg")).toBe(false); + expect(handler.match("https://media.giphy.com/media/abc/giphy.gif")).toBe( + false + ); + expect(handler.match("https://example.com/image.gif")).toBe(false); + }); + + it("renders fixed-size GIFs for chat/default variant", () => { + const handler = createGifHandler({ linkPreviewVariant: "chat" }); + const element = handler.render("https://media.tenor.com/abc/tenor.gif"); + + render(<>{element}); + + expect(screen.getByTestId("gif-embed")).toHaveAttribute( + "data-fixed-size", + "true" + ); + expect(mockRenderGifEmbed).toHaveBeenCalledWith( + "https://media.tenor.com/abc/tenor.gif", + { fixedSize: true } + ); + }); + + it("renders non-fixed GIFs for home variant", () => { + const handler = createGifHandler({ linkPreviewVariant: "home" }); + const element = handler.render("https://media.tenor.com/abc/tenor.gif"); + + render(<>{element}); + + expect(screen.getByTestId("gif-embed")).toHaveAttribute( + "data-fixed-size", + "false" + ); + expect(mockRenderGifEmbed).toHaveBeenCalledWith( + "https://media.tenor.com/abc/tenor.gif", + { fixedSize: false } + ); + }); +}); diff --git a/__tests__/components/drops/view/part/dropPartMarkdown/linkHandlersRegistry.test.tsx b/__tests__/components/drops/view/part/dropPartMarkdown/linkHandlersRegistry.test.tsx deleted file mode 100644 index 5aee531469..0000000000 --- a/__tests__/components/drops/view/part/dropPartMarkdown/linkHandlersRegistry.test.tsx +++ /dev/null @@ -1,541 +0,0 @@ -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"; - -type EnsureStableSeizeLinkWithSetter = typeof ensureStableSeizeLink & { - __setCurrentHref?: ((href?: string) => void) | undefined; -}; - -jest.mock( - "@/components/drops/view/part/dropPartMarkdown/youtubePreview", - () => ({ - __esModule: true, - default: ({ href }: { href: string }) => ( -
- ), - }) -); - -jest.mock("@/components/drops/view/part/DropPartMarkdownImage", () => ({ - __esModule: true, - default: ({ src }: { src: string }) => ( - - ), -})); - -jest.mock("@/components/drops/view/part/dropPartMarkdown/renderers", () => ({ - renderGifEmbed: jest.fn((url: string) => ( -
- )), - renderSeizeQuote: jest.fn(() =>
), - renderTweetEmbed: jest.fn((href: string) => ( -
- )), -})); - -jest.mock("@/src/components/waves/ArtBlocksTokenCard", () => ({ - __esModule: true, - default: ({ href }: { href: string }) => ( -
- ), -})); - -jest.mock("@/components/waves/pepe/PepeCard", () => ({ - __esModule: true, - default: ({ href, kind }: { href: string; kind: string }) => ( -
- ), -})); - -jest.mock("@/components/drops/view/part/dropPartMarkdown/tiktok", () => ({ - parseTikTokLink: jest.requireActual( - "@/components/drops/view/part/dropPartMarkdown/tiktok" - ).parseTikTokLink, -})); - -jest.mock("@/components/waves/TikTokCard", () => ({ - __esModule: true, - default: ({ href }: { href: string }) => ( -
- ), -})); - -jest.mock("@/components/waves/LinkPreviewCard", () => ({ - __esModule: true, - default: ({ - href, - renderFallback, - }: { - href: string; - renderFallback: () => ReactNode; - }) => ( -
- {renderFallback()} -
- ), -})); - -const mockEnsLinkPreview = jest.fn(({ href }: { href: string }) => ( -
-)); - -jest.mock("@/components/waves/ens/EnsLinkPreview", () => ({ - __esModule: true, - default: (props: any) => mockEnsLinkPreview(props), -})); - -jest.mock("@/components/waves/FarcasterCard", () => ({ - __esModule: true, - default: ({ href }: { href: string }) => ( -
- ), -})); - -jest.mock("@/components/waves/WikimediaCard", () => ({ - __esModule: true, - default: ({ href }: { href: string }) => ( -
- ), -})); - -jest.mock("@/components/waves/ChatItemHrefButtons", () => ({ - __esModule: true, - default: ({ href }: { href: string }) => ( -
- ), -})); - -jest.mock("@/components/groups/page/list/card/GroupCardChat", () => ({ - __esModule: true, - default: ({ groupId }: { groupId: string }) => ( -
- ), -})); - -jest.mock("@/components/waves/list/WaveItemChat", () => ({ - __esModule: true, - default: ({ waveId }: { waveId: string }) => ( -
- ), -})); - -jest.mock("@/components/waves/drops/DropItemChat", () => ({ - __esModule: true, - 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); - - const ensureWithSetter = - ensureStableSeizeLink as EnsureStableSeizeLinkWithSetter; - ensureWithSetter.__setCurrentHref = (href?: string) => { - currentHrefOverride = href; - }; - - return { - ...actual, - ensureStableSeizeLink, - }; -}); - -const onQuoteClick = jest.fn(); -const QUOTE_WAVE_ID = "123e4567-e89b-12d3-a456-426614174000"; -const QUOTE_SERIAL_NO = "5"; -const QUOTE_HREF = `https://6529.io/waves/${QUOTE_WAVE_ID}?serialNo=${QUOTE_SERIAL_NO}`; -const QUOTE_CYCLE_KEY = `${QUOTE_WAVE_ID}:${QUOTE_SERIAL_NO}`; - -const baseRenderer = () => createLinkRenderer({ onQuoteClick }); - -type EnsureStableSeizeLinkWithSetter = typeof ensureStableSeizeLink & { - __setCurrentHref?: ((href?: string) => void) | undefined; -}; - -const setEnsureCurrentHref = (href?: string) => { - const ensureWithSetter = - ensureStableSeizeLink as EnsureStableSeizeLinkWithSetter; - ensureWithSetter.__setCurrentHref?.(href); -}; - -describe("createLinkRenderer", () => { - const FALLBACK_BASE_ENDPOINT = "https://6529.io"; - const originalBaseEndpointEnv = publicEnv.BASE_ENDPOINT; - const originalProcessBaseEndpoint = process.env["BASE_ENDPOINT"]; - - beforeEach(() => { - jest.clearAllMocks(); - publicEnv.BASE_ENDPOINT = FALLBACK_BASE_ENDPOINT; - process.env["BASE_ENDPOINT"] = FALLBACK_BASE_ENDPOINT; - setEnsureCurrentHref(); - }); - - afterEach(() => { - publicEnv.BASE_ENDPOINT = originalBaseEndpointEnv; - if (originalProcessBaseEndpoint === undefined) { - delete process.env["BASE_ENDPOINT"]; - } else { - process.env["BASE_ENDPOINT"] = originalProcessBaseEndpoint; - } - setEnsureCurrentHref(); - }); - - it("renders DropPartMarkdownImage for img elements", () => { - const { renderImage } = baseRenderer(); - const element = renderImage({ src: "https://example.com/image.png" }); - expect(element).not.toBeNull(); - const { getByTestId } = render(<>{element}); - expect(getByTestId("markdown-image")).toHaveAttribute( - "src", - "https://example.com/image.png" - ); - }); - - it("returns null for invalid image sources", () => { - const { renderImage } = baseRenderer(); - expect(renderImage({ src: undefined })).toBeNull(); - }); - - it("uses TikTok handler for supported URLs", () => { - const { renderAnchor } = baseRenderer(); - const element = renderAnchor({ - href: "https://www.tiktok.com/@creator/video/123456", - } as any); - render(<>{element}); - expect(screen.getByTestId("tiktok-card")).toHaveAttribute( - "data-href", - "https://www.tiktok.com/@creator/video/123456" - ); - expect(screen.queryByTestId("opengraph")).not.toBeInTheDocument(); - }); - - it("uses YouTube handler for supported URLs", () => { - const { renderAnchor } = baseRenderer(); - const element = renderAnchor({ href: "https://youtu.be/video123" } as any); - render(<>{element}); - expect(screen.getByTestId("youtube-preview")).toHaveAttribute( - "data-href", - "https://youtu.be/video123" - ); - expect(screen.queryByTestId("opengraph")).not.toBeInTheDocument(); - }); - - it("renders seize quote previews when matching internal links", () => { - const { renderAnchor } = baseRenderer(); - const element = renderAnchor({ href: QUOTE_HREF } as any); - render(<>{element}); - expect(screen.getByTestId("seize-quote")).toBeInTheDocument(); - }); - - it("renders seize group previews", () => { - const { renderAnchor } = baseRenderer(); - const element = renderAnchor({ - href: "https://6529.io/network?group=test-group", - } as any); - render(<>{element}); - expect(screen.getByTestId("group-card")).toHaveAttribute( - "data-group", - "test-group" - ); - }); - - it("renders seize drop previews", () => { - const { renderAnchor } = baseRenderer(); - const element = renderAnchor({ - href: "https://6529.io/waves/abc?drop=def", - } as any); - render(<>{element}); - expect(screen.getByTestId("drop-card")).toHaveAttribute("data-drop", "def"); - }); - - it("falls back to a simple anchor when link targets the current drop", () => { - const { renderAnchor } = createLinkRenderer({ - onQuoteClick, - currentDropId: "def", - }); - const element = renderAnchor({ - href: "https://6529.io/waves/abc?drop=def", - } as any); - const { container } = render(<>{element}); - - expect(screen.queryByTestId("drop-card")).toBeNull(); - expect( - container.querySelector('a[href="/waves/abc?drop=def"]') - ).not.toBeNull(); - }); - - it("falls back to a simple anchor when quote link exceeds max embed depth", () => { - const { renderAnchor } = createLinkRenderer({ - onQuoteClick, - embedDepth: 4, - maxEmbedDepth: 4, - }); - const element = renderAnchor({ href: QUOTE_HREF } as any); - const { container } = render(<>{element}); - - expect(screen.queryByTestId("seize-quote")).toBeNull(); - expect( - container.querySelector(`a[href="/waves/${QUOTE_WAVE_ID}?serialNo=5"]`) - ).not.toBeNull(); - }); - - it("falls back to a simple anchor when quote link creates a cycle", () => { - const { renderAnchor } = createLinkRenderer({ - onQuoteClick, - quotePath: [QUOTE_CYCLE_KEY], - }); - const element = renderAnchor({ href: QUOTE_HREF } as any); - const { container } = render(<>{element}); - - expect(screen.queryByTestId("seize-quote")).toBeNull(); - expect( - container.querySelector(`a[href="/waves/${QUOTE_WAVE_ID}?serialNo=5"]`) - ).not.toBeNull(); - }); - - it("falls back to a simple anchor when drop link matches embed path", () => { - const { renderAnchor } = createLinkRenderer({ - onQuoteClick, - embedPath: ["def"], - }); - const element = renderAnchor({ - href: "https://6529.io/waves/abc?drop=def", - } as any); - const { container } = render(<>{element}); - - expect(screen.queryByTestId("drop-card")).toBeNull(); - expect( - container.querySelector('a[href="/waves/abc?drop=def"]') - ).not.toBeNull(); - }); - - 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" - ); - }); - - 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/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" - ); - }); - - it.each([ - ["standard status link", "https://twitter.com/user/status/987654321"], - ["mobile twitter link", "https://mobile.twitter.com/user/status/987654321"], - ["i/web link", "https://twitter.com/i/web/status/987654321"], - ["x.com link", "https://x.com/user/status/987654321"], - ["hashbang link", "https://twitter.com/#!/user/status/987654321"], - ])("renders Twitter embeds for %s", (_, tweetHref) => { - const { renderAnchor } = baseRenderer(); - const element = renderAnchor({ href: tweetHref } as any); - render(<>{element}); - expect(screen.getByTestId("tweet")).toHaveAttribute("data-href", tweetHref); - expect(screen.queryByTestId("opengraph")).not.toBeInTheDocument(); - }); - - it("renders Wikimedia cards", () => { - const { renderAnchor } = baseRenderer(); - const element = renderAnchor({ - href: "https://en.wikipedia.org/wiki/Test", - } as any); - render(<>{element}); - expect(screen.getByTestId("wikimedia-card")).toHaveAttribute( - "data-href", - "https://en.wikipedia.org/wiki/Test" - ); - }); - - it("renders gif embeds", () => { - const { renderAnchor } = baseRenderer(); - const element = renderAnchor({ - href: "https://media.tenor.com/test.gif", - } as any); - render(<>{element}); - expect(screen.getByTestId("gif")).toHaveAttribute( - "data-url", - "https://media.tenor.com/test.gif" - ); - }); - - it("renders Art Blocks previews", () => { - const { renderAnchor } = baseRenderer(); - const element = renderAnchor({ - href: "https://www.artblocks.io/token/662000", - } as any); - render(<>{element}); - expect(screen.getByTestId("artblocks-card")).toHaveAttribute( - "data-href", - "https://www.artblocks.io/token/662000" - ); - expect(screen.getByTestId("chat-buttons")).toHaveAttribute( - "data-href", - "https://www.artblocks.io/token/662000" - ); - }); - - it("renders Pepe cards", () => { - const { renderAnchor } = baseRenderer(); - const element = renderAnchor({ - href: "https://pepe.wtf/asset/test", - } as any); - render(<>{element}); - expect(screen.getByTestId("pepe-card")).toHaveAttribute( - "data-kind", - "asset" - ); - }); - - it("renders Farcaster cards", () => { - const { renderAnchor } = baseRenderer(); - const element = renderAnchor({ - href: "https://warpcast.com/alice/0x123", - } as any); - render(<>{element}); - expect(screen.getByTestId("farcaster-card")).toHaveAttribute( - "data-href", - "https://warpcast.com/alice/0x123" - ); - expect(screen.queryByTestId("opengraph")).toBeNull(); - }); - - it("renders ENS previews", () => { - const { renderAnchor } = baseRenderer(); - const element = renderAnchor({ href: "vitalik.eth" } as any); - render(<>{element}); - expect(mockEnsLinkPreview).toHaveBeenCalledWith({ href: "vitalik.eth" }); - expect(screen.getByTestId("ens-link-preview")).toHaveAttribute( - "data-href", - "vitalik.eth" - ); - }); - - it("renders Open Graph preview for generic external links", () => { - const { renderAnchor } = baseRenderer(); - const element = renderAnchor({ href: "https://example.org/post" } as any); - render(<>{element}); - expect(screen.getByTestId("opengraph")).toHaveAttribute( - "data-href", - "https://example.org/post" - ); - }); - - it("falls back to standard anchor for unsupported protocols", () => { - const { renderAnchor } = baseRenderer(); - const element = renderAnchor({ - href: "ftp://example.org/resource", - children: "ftp", - } as any); - render(<>{element}); - expect(screen.getByText("ftp" as any)).toHaveAttribute( - "href", - "ftp://example.org/resource" - ); - }); - - it("identifies block-level smart links", () => { - const { isSmartLink } = baseRenderer(); - expect(isSmartLink("https://youtu.be/video123")).toBe(true); - expect(isSmartLink("https://example.org/post")).toBe(true); - expect(isSmartLink("https://example.org")).toBe(true); - expect(isSmartLink("ftp://example.org/resource")).toBe(false); - }); - - it("returns null when href is missing", () => { - const { renderAnchor } = baseRenderer(); - expect(renderAnchor({ children: "missing" } as any)).toBeNull(); - }); - - describe("hideLinkPreviews option", () => { - it("renders fallback anchor instead of OpenGraph when hideLinkPreviews is true", () => { - const { renderAnchor } = createLinkRenderer({ - onQuoteClick, - hideLinkPreviews: true, - }); - const element = renderAnchor({ - href: "https://example.org/post", - children: "link text", - } as any); - const { container } = render(<>{element}); - expect(screen.queryByTestId("opengraph")).toBeNull(); - expect( - container.querySelector('a[href="https://example.org/post"]') - ).not.toBeNull(); - }); - - it("isSmartLink returns false when hideLinkPreviews is true", () => { - const { isSmartLink } = createLinkRenderer({ - onQuoteClick, - hideLinkPreviews: true, - }); - expect(isSmartLink("https://youtu.be/video123")).toBe(false); - expect(isSmartLink("https://example.org/post")).toBe(false); - }); - - it("renders fallback anchor instead of YouTube preview when hideLinkPreviews is true", () => { - const { renderAnchor } = createLinkRenderer({ - onQuoteClick, - hideLinkPreviews: true, - }); - const element = renderAnchor({ - href: "https://youtu.be/video123", - children: "YouTube link", - } as any); - const { container } = render(<>{element}); - expect(screen.queryByTestId("youtube-preview")).toBeNull(); - expect( - container.querySelector('a[href="https://youtu.be/video123"]') - ).not.toBeNull(); - }); - - it("renders normal preview when hideLinkPreviews is false", () => { - const { renderAnchor } = createLinkRenderer({ - onQuoteClick, - hideLinkPreviews: false, - }); - const element = renderAnchor({ - href: "https://example.org/post", - } as any); - render(<>{element}); - expect(screen.getByTestId("opengraph")).toBeInTheDocument(); - }); - }); -}); diff --git a/components/brain/my-stream/MyStreamWaveChat.tsx b/components/brain/my-stream/MyStreamWaveChat.tsx index cb5d4c1c9d..0beacefa2d 100644 --- a/components/brain/my-stream/MyStreamWaveChat.tsx +++ b/components/brain/my-stream/MyStreamWaveChat.tsx @@ -227,7 +227,6 @@ const MyStreamWaveChat: React.FC = ({ key={wave.id} waveId={wave.id} onReply={handleReply} - activeDrop={activeDrop} initialDrop={scrollTarget} dividerSerialNo={dividerTarget} diff --git a/components/brain/notifications/NotificationItems.tsx b/components/brain/notifications/NotificationItems.tsx index e0eb176f3e..8fdb77b6b2 100644 --- a/components/brain/notifications/NotificationItems.tsx +++ b/components/brain/notifications/NotificationItems.tsx @@ -29,7 +29,7 @@ function NotificationItemsComponent({ items.map((item, index) => { const keySuffix = isGroupedReactionsItem(item) ? `group-${item.drop.id}` - : item.id ?? `fallback-${index}`; + : (item.id ?? `fallback-${index}`); return { item, key: `notification-${keySuffix}`, diff --git a/components/brain/notifications/NotificationsWrapper.tsx b/components/brain/notifications/NotificationsWrapper.tsx index 100ac742da..926e167a87 100644 --- a/components/brain/notifications/NotificationsWrapper.tsx +++ b/components/brain/notifications/NotificationsWrapper.tsx @@ -3,11 +3,8 @@ import { useCallback } from "react"; import type { ExtendedDrop } from "@/helpers/waves/drop.helpers"; import type { NotificationDisplayItem } from "@/types/feed.types"; -import type { - ActiveDropState} from "@/types/dropInteractionTypes"; -import { - ActiveDropAction -} from "@/types/dropInteractionTypes"; +import type { ActiveDropState } from "@/types/dropInteractionTypes"; +import { ActiveDropAction } from "@/types/dropInteractionTypes"; import type { DropInteractionParams } from "@/components/waves/drops/Drop"; import NotificationItems from "./NotificationItems"; import { useRouter } from "next/navigation"; @@ -58,7 +55,7 @@ export default function NotificationsWrapper({ } const isDirectMessage = hasChatScope(drop.wave) - ? drop.wave.chat?.scope?.group?.is_direct_message ?? false + ? (drop.wave.chat?.scope?.group?.is_direct_message ?? false) : false; const href = getWaveRoute({ diff --git a/components/brain/notifications/all-drops/NotificationAllDrops.tsx b/components/brain/notifications/all-drops/NotificationAllDrops.tsx index 4011603c31..74945a506d 100644 --- a/components/brain/notifications/all-drops/NotificationAllDrops.tsx +++ b/components/brain/notifications/all-drops/NotificationAllDrops.tsx @@ -64,7 +64,7 @@ export default function NotificationAllDrops({ }; return ( -
+
-
+
+
{headerSection}
@@ -62,8 +62,8 @@ export default function NotificationPriorityAlert({ const isDirectMessage = getIsDirectMessage(drop.wave); return ( -
-
+
+
{headerSection} +
} > - + {actionText} diff --git a/components/drops/view/Drops.tsx b/components/drops/view/Drops.tsx index 8e492b71e0..1ed375f5c1 100644 --- a/components/drops/view/Drops.tsx +++ b/components/drops/view/Drops.tsx @@ -179,7 +179,6 @@ export default function Drops() { drops={drops} showWaveInfo={true} onReply={() => {}} - onReplyClick={() => {}} serialNo={null} targetDropRef={null} diff --git a/components/drops/view/DropsList.tsx b/components/drops/view/DropsList.tsx index 2e3a6b1eb2..02a56f141f 100644 --- a/components/drops/view/DropsList.tsx +++ b/components/drops/view/DropsList.tsx @@ -223,7 +223,6 @@ const DropsList = memo( showWaveInfo={getItemData.showWaveInfo} activeDrop={getItemData.activeDrop} onReply={getItemData.handleReply} - location={location} showReplyAndQuote={getItemData.showReplyAndQuote} onQuoteClick={getItemData.onQuoteClick} diff --git a/components/drops/view/part/dropPartMarkdown/handlers/gif.tsx b/components/drops/view/part/dropPartMarkdown/handlers/gif.tsx index 9a6cf254e0..1c4c17ec1e 100644 --- a/components/drops/view/part/dropPartMarkdown/handlers/gif.tsx +++ b/components/drops/view/part/dropPartMarkdown/handlers/gif.tsx @@ -1,5 +1,6 @@ import { renderGifEmbed } from "../renderers"; import type { LinkHandler } from "../linkTypes"; +import type { LinkPreviewVariant } from "@/components/waves/LinkPreviewContext"; const TENOR_HOST = "media.tenor.com"; @@ -18,8 +19,13 @@ const isTenorGif = (href: string): boolean => { } }; -export const createGifHandler = (): LinkHandler => ({ +export const createGifHandler = (options?: { + readonly linkPreviewVariant?: LinkPreviewVariant; +}): LinkHandler => ({ match: isTenorGif, - render: (href) => renderGifEmbed(href), + render: (href) => + renderGifEmbed(href, { + fixedSize: options?.linkPreviewVariant !== "home", + }), display: "block", }); diff --git a/components/drops/view/part/dropPartMarkdown/handlers/index.ts b/components/drops/view/part/dropPartMarkdown/handlers/index.ts index 0a1645c168..226aa57eb1 100644 --- a/components/drops/view/part/dropPartMarkdown/handlers/index.ts +++ b/components/drops/view/part/dropPartMarkdown/handlers/index.ts @@ -11,9 +11,11 @@ import { createTwitterHandler } from "./twitter"; import { createWikimediaHandler } from "./wikimedia"; import { createYoutubeHandler } from "./youtube"; import type { TweetPreviewMode } from "@/components/tweets/TweetPreviewModeContext"; +import type { LinkPreviewVariant } from "@/components/waves/LinkPreviewContext"; export const createLinkHandlers = (options?: { readonly tweetPreviewMode?: TweetPreviewMode; + readonly linkPreviewVariant?: LinkPreviewVariant; }): LinkHandler[] => [ createYoutubeHandler(), createTikTokHandler(), @@ -22,7 +24,7 @@ export const createLinkHandlers = (options?: { createCompoundHandler(), createTwitterHandler(options), createWikimediaHandler(), - createGifHandler(), + createGifHandler(options), createArtBlocksHandler(), createPepeHandler(), createFarcasterHandler(), diff --git a/components/drops/view/part/dropPartMarkdown/linkHandlers.tsx b/components/drops/view/part/dropPartMarkdown/linkHandlers.tsx index 9944bcb0aa..4ab10a21b4 100644 --- a/components/drops/view/part/dropPartMarkdown/linkHandlers.tsx +++ b/components/drops/view/part/dropPartMarkdown/linkHandlers.tsx @@ -67,7 +67,7 @@ export const createLinkRenderer = ({ onQuoteClick, currentDropId, hideLinkPreviews = false, - tweetPreviewMode, + tweetPreviewMode = "auto", embedPath, quotePath, embedDepth = 0, @@ -81,9 +81,10 @@ export const createLinkRenderer = ({ embedDepth, maxEmbedDepth, }); - const handlers = createLinkHandlers( - tweetPreviewMode ? { tweetPreviewMode } : undefined - ); + const handlers = createLinkHandlers({ + tweetPreviewMode, + linkPreviewVariant: "chat", + }); const renderImage: LinkRenderer["renderImage"] = ({ src }) => { if (typeof src !== "string") { @@ -124,10 +125,7 @@ export const createLinkRenderer = ({ const tryRenderOpenGraph = () => { try { - const ogContent = renderOpenGraph(); - if (ogContent) { - return ogContent; - } + return renderOpenGraph(); } catch { // swallow and fall back to default anchor } @@ -152,9 +150,6 @@ export const createLinkRenderer = ({ const renderFromHandler = (handler: LinkHandler): ReactElement | null => { try { const rendered = handler.render(stableHref); - if (rendered === null || rendered === undefined) { - throw new Error("Link handler returned no content"); - } return renderHandlerContent(rendered); } catch { const ogContent = tryRenderOpenGraph(); diff --git a/components/drops/view/part/dropPartMarkdown/renderers.tsx b/components/drops/view/part/dropPartMarkdown/renderers.tsx index 669323ee0a..1041a6edd8 100644 --- a/components/drops/view/part/dropPartMarkdown/renderers.tsx +++ b/components/drops/view/part/dropPartMarkdown/renderers.tsx @@ -41,7 +41,7 @@ const renderTweetEmbed = ( const renderFallback = () => ; return ( -
+
renderFallback()}> ( - {url} -); +const CHAT_GIF_HEIGHT = 180; + +interface GifEmbedOptions { + readonly fixedSize?: boolean | undefined; +} + +const renderGifEmbed = ( + url: string, + options?: GifEmbedOptions +): ReactElement => { + if (options?.fixedSize) { + return ( + // Use fixed height to prevent vertical layout shift while preserving GIF aspect ratio. + Embedded GIF + ); + } + + return ( + // Markdown-style GIF embeds intentionally keep for animation support. + Embedded GIF + ); +}; const renderSeizeQuote = ( quoteLinkInfo: SeizeQuoteLinkInfo, diff --git a/components/header/header-search/HeaderSearchModal.tsx b/components/header/header-search/HeaderSearchModal.tsx index 91ab2ba22e..d79638359e 100644 --- a/components/header/header-search/HeaderSearchModal.tsx +++ b/components/header/header-search/HeaderSearchModal.tsx @@ -1041,7 +1041,6 @@ export default function HeaderSearchModal({ location={DropLocation.WAVE} dropViewDropId={null} onReply={() => {}} - onReplyClick={() => {}} onQuoteClick={() => {}} /> diff --git a/components/memes/drops/MemeParticipationDrop.tsx b/components/memes/drops/MemeParticipationDrop.tsx index ec2aabcd2c..872b61eda0 100644 --- a/components/memes/drops/MemeParticipationDrop.tsx +++ b/components/memes/drops/MemeParticipationDrop.tsx @@ -3,7 +3,7 @@ import DropListItemContentMedia from "@/components/drops/view/item/content/media/DropListItemContentMedia"; import { MobileVotingModal, VotingModal } from "@/components/voting"; import VotingModalButton from "@/components/voting/VotingModalButton"; -import type { DropInteractionParams} from "@/components/waves/drops/Drop"; +import type { DropInteractionParams } from "@/components/waves/drops/Drop"; import { DropLocation } from "@/components/waves/drops/Drop"; import DropMobileMenuHandler from "@/components/waves/drops/DropMobileMenuHandler"; import WaveDropReactions from "@/components/waves/drops/WaveDropReactions"; @@ -81,34 +81,38 @@ export default function MemeParticipationDrop({ return (
+ }`} + >
+ }`} + > + onReply={handleOnReply} + > <>
-
+
{artworkMedia && (
+ }`} + > -
+
{canShowVote && ( -
+
e.stopPropagation()}> -
+
diff --git a/components/utils/select/dropdown/CommonDropdownItemsDefaultWrapper.tsx b/components/utils/select/dropdown/CommonDropdownItemsDefaultWrapper.tsx index 37ee27bad7..5594083173 100644 --- a/components/utils/select/dropdown/CommonDropdownItemsDefaultWrapper.tsx +++ b/components/utils/select/dropdown/CommonDropdownItemsDefaultWrapper.tsx @@ -2,9 +2,7 @@ import { createPortal } from "react-dom"; import { AnimatePresence, motion } from "framer-motion"; -import type { - ReactNode, - RefObject} from "react"; +import type { ReactNode, RefObject } from "react"; import { useCallback, useEffect, @@ -116,7 +114,7 @@ export default function CommonDropdownItemsDefaultWrapper({ transition={{ duration: 0.2 }} >
-
    +
      {children}
diff --git a/components/waves/SmartLinkPreview.tsx b/components/waves/SmartLinkPreview.tsx index e777c5e1c7..83591acb34 100644 --- a/components/waves/SmartLinkPreview.tsx +++ b/components/waves/SmartLinkPreview.tsx @@ -84,7 +84,13 @@ export default function SmartLinkPreview({ const { variant: contextVariant } = useLinkPreviewContext(); const resolvedVariant = variant ?? contextVariant; const stableHref = ensureStableSeizeLink(href); - const handlers = useMemo(() => createLinkHandlers(), []); + const handlers = useMemo( + () => + createLinkHandlers({ + linkPreviewVariant: resolvedVariant, + }), + [resolvedVariant] + ); const parsedUrl = parseUrl(stableHref); const fallbackRenderer = renderFallback ?? (() => ); diff --git a/components/waves/drops/WaveDropActionsMore.tsx b/components/waves/drops/WaveDropActionsMore.tsx index c177c06ec1..962ac5ce31 100644 --- a/components/waves/drops/WaveDropActionsMore.tsx +++ b/components/waves/drops/WaveDropActionsMore.tsx @@ -101,8 +101,16 @@ export default function WaveDropActionsMore({ isDropdownItem={true} onMarkUnread={closeDropdown} /> - - + + {mediaInfo && ( = ({ {...(!isTemporaryDrop ? { "data-tooltip-id": `reply-${drop.id}` } : {})} > setDebouncedQuery(query), - 250, - [query] - ); + useDebounce(() => setDebouncedQuery(query), 250, [query]); useEffect(() => { if (!isOpen) { @@ -93,7 +89,7 @@ export default function WaveDropsSearchModal({ document.body, }} > -
+
@@ -101,23 +97,23 @@ export default function WaveDropsSearchModal({ ref={modalRef} aria-modal="true" aria-labelledby="wave-drops-search-input" - className="tw-w-full tw-h-full sm:tw-h-[70vh] tw-max-w-[min(100vw,900px)] tw-transform sm:tw-rounded-xl tw-bg-iron-950 tw-text-left tw-shadow-xl tw-transition-all tw-overflow-hidden inset-safe-area tw-flex tw-flex-col tw-min-h-0 tw-border tw-border-solid tw-border-iron-800" + className="inset-safe-area tw-flex tw-h-full tw-min-h-0 tw-w-full tw-max-w-[min(100vw,900px)] tw-transform tw-flex-col tw-overflow-hidden tw-border tw-border-solid tw-border-iron-800 tw-bg-iron-950 tw-text-left tw-shadow-xl tw-transition-all sm:tw-h-[70vh] sm:tw-rounded-xl" > -
+
-
+
Search in
-
+
{wave.name}
@@ -126,7 +122,7 @@ export default function WaveDropsSearchModal({ type="button" onClick={onClose} aria-label="Close search" - className="tw-hidden sm:tw-inline-flex tw-items-center tw-justify-center tw-size-9 tw-rounded-xl tw-border tw-border-solid tw-border-iron-700 tw-bg-iron-900 tw-text-iron-200 hover:tw-border-iron-500 hover:tw-bg-iron-800 hover:tw-text-white tw-transition tw-duration-150" + className="tw-hidden tw-size-9 tw-items-center tw-justify-center tw-rounded-xl tw-border tw-border-solid tw-border-iron-700 tw-bg-iron-900 tw-text-iron-200 tw-transition tw-duration-150 hover:tw-border-iron-500 hover:tw-bg-iron-800 hover:tw-text-white sm:tw-inline-flex" > @@ -146,7 +142,10 @@ export default function WaveDropsSearchModal({ clipRule="evenodd" /> -
-
+
{isLoading && (
Loading… @@ -186,7 +185,7 @@ export default function WaveDropsSearchModal({ )} {!isLoading && !isError && !meetsMinLength && ( -
+
Type at least {MIN_QUERY_LENGTH} characters to search.
)} @@ -195,7 +194,7 @@ export default function WaveDropsSearchModal({ !isError && meetsMinLength && results.length === 0 && ( -
+
No matches found.
)} @@ -224,7 +223,7 @@ export default function WaveDropsSearchModal({ onSelectSerialNo(serialNo); onClose(); }} - className="tw-w-full tw-text-left tw-rounded-xl tw-border tw-border-solid tw-border-iron-800 tw-bg-iron-950/50 desktop-hover:hover:tw-border-iron-600 desktop-hover:hover:tw-bg-iron-900/40 tw-transition tw-duration-150 disabled:tw-opacity-60 disabled:tw-cursor-not-allowed" + className="tw-w-full tw-rounded-xl tw-border tw-border-solid tw-border-iron-800 tw-bg-iron-950/50 tw-text-left tw-transition tw-duration-150 disabled:tw-cursor-not-allowed disabled:tw-opacity-60 desktop-hover:hover:tw-border-iron-600 desktop-hover:hover:tw-bg-iron-900/40" >
{}} - onReplyClick={() => {}} onQuoteClick={() => {}} /> @@ -252,7 +250,7 @@ export default function WaveDropsSearchModal({ type="button" onClick={() => fetchNextPage()} disabled={isFetchingNextPage} - className="tw-inline-flex tw-items-center tw-rounded-xl tw-border tw-border-solid tw-border-iron-700 tw-bg-iron-900 tw-px-4 tw-py-2 tw-text-sm tw-font-medium tw-text-iron-200 hover:tw-border-iron-500 hover:tw-bg-iron-800 hover:tw-text-white disabled:tw-opacity-50 disabled:tw-cursor-not-allowed tw-transition tw-duration-150" + className="tw-inline-flex tw-items-center tw-rounded-xl tw-border tw-border-solid tw-border-iron-700 tw-bg-iron-900 tw-px-4 tw-py-2 tw-text-sm tw-font-medium tw-text-iron-200 tw-transition tw-duration-150 hover:tw-border-iron-500 hover:tw-bg-iron-800 hover:tw-text-white disabled:tw-cursor-not-allowed disabled:tw-opacity-50" > {isFetchingNextPage ? "Loading…" : "Load more"} diff --git a/components/waves/header/WaveHeaderDescription.tsx b/components/waves/header/WaveHeaderDescription.tsx index b699cafa53..0753c94d24 100644 --- a/components/waves/header/WaveHeaderDescription.tsx +++ b/components/waves/header/WaveHeaderDescription.tsx @@ -186,7 +186,6 @@ const WaveHeaderDescription: React.FC = ({ showReplyAndQuote={false} location={DropLocation.WAVE} onReply={() => {}} - previousDrop={null} nextDrop={null} onQuoteClick={() => {}}