diff --git a/__tests__/components/drops/view/part/DropPartMarkdown.test.tsx b/__tests__/components/drops/view/part/DropPartMarkdown.test.tsx
index 3d7f221a00..aaf4c09eae 100644
--- a/__tests__/components/drops/view/part/DropPartMarkdown.test.tsx
+++ b/__tests__/components/drops/view/part/DropPartMarkdown.test.tsx
@@ -1,15 +1,55 @@
import { render, screen } from "@testing-library/react";
-import DropPartMarkdown from "../../../../../components/drops/view/part/DropPartMarkdown";
+import type { ReactElement } from "react";
-jest.mock("../../../../../hooks/isMobileScreen", () => () => false);
-jest.mock("../../../../../contexts/EmojiContext", () => ({
- useEmoji: () => ({ emojiMap: [] }),
-}));
-jest.mock("react-tweet", () => ({
- Tweet: ({ id }: any) =>
tweet:{id}
,
-}));
+type DropPartMarkdownType = typeof import("../../../../../components/drops/view/part/DropPartMarkdown").default;
describe("DropPartMarkdown", () => {
+ let DropPartMarkdown: DropPartMarkdownType;
+ let dynamicSpy: jest.Mock;
+
+ const loadComponent = async (): Promise => {
+ jest.resetModules();
+ dynamicSpy = jest.fn();
+
+ jest.doMock("../../../../../hooks/isMobileScreen", () => () => false);
+ jest.doMock("../../../../../contexts/EmojiContext", () => ({
+ useEmoji: () => ({
+ emojiMap: [],
+ findNativeEmoji: () => undefined,
+ }),
+ }));
+ jest.doMock("react-tweet", () => ({
+ Tweet: ({ id }: any) => tweet:{id}
,
+ }));
+ jest.doMock("next/dynamic", () => ({
+ __esModule: true,
+ default: (loader: any, options: any) => {
+ dynamicSpy(loader, options);
+ const LoadingComponent = options?.loading;
+ const DynamicComponent = (props: any): ReactElement | null =>
+ LoadingComponent ? : null;
+
+ (DynamicComponent as any).__loader = loader;
+ (DynamicComponent as any).__options = options;
+
+ return DynamicComponent;
+ },
+ }));
+
+ DropPartMarkdown = (
+ await import("../../../../../components/drops/view/part/DropPartMarkdown")
+ ).default;
+ };
+
+ beforeEach(async () => {
+ await loadComponent();
+ });
+
+ afterEach(() => {
+ jest.resetModules();
+ delete process.env.BASE_ENDPOINT;
+ });
+
it("renders gif embeds", () => {
const content = "Check this ";
render(
@@ -57,4 +97,27 @@ describe("DropPartMarkdown", () => {
expect(a).not.toHaveAttribute("target");
expect(a).toHaveAttribute("href", "/page");
});
+
+ it("lazy loads tweet embeds with a loading skeleton", async () => {
+ const content = "Check this [tweet](https://twitter.com/user/status/1234567890)";
+
+ render(
+
+ );
+
+ expect(screen.getByTestId("tweet-embed-loading")).toBeInTheDocument();
+
+ expect(dynamicSpy).toHaveBeenCalledTimes(1);
+ const [loader, options] = dynamicSpy.mock.calls[0];
+ expect(options?.ssr).toBe(false);
+
+ const TweetComponent = await loader();
+ const { getByText } = render();
+ expect(getByText("tweet:abc123")).toBeInTheDocument();
+ });
});
diff --git a/components/drops/view/part/DropPartMarkdown.tsx b/components/drops/view/part/DropPartMarkdown.tsx
index 0289b22202..2a4a22ab69 100644
--- a/components/drops/view/part/DropPartMarkdown.tsx
+++ b/components/drops/view/part/DropPartMarkdown.tsx
@@ -14,17 +14,15 @@ import Markdown, { ExtraProps } from "react-markdown";
import rehypeExternalLinks from "rehype-external-links";
import rehypeSanitize from "rehype-sanitize";
import remarkGfm from "remark-gfm";
+import dynamic from "next/dynamic";
import { getRandomObjectId } from "../../../../helpers/AllowlistToolHelpers";
-
-// Component definitions moved outside for better performance
-const BreakComponent = () =>
;
import DropListItemContentPart, {
DropListItemContentPartProps,
} from "../item/content/DropListItemContentPart";
import { ApiDropMentionedUser } from "../../../../generated/models/ApiDropMentionedUser";
import { ApiDropReferencedNFT } from "../../../../generated/models/ApiDropReferencedNFT";
-import { Tweet } from "react-tweet";
+import type { TweetProps } from "react-tweet";
import DropPartMarkdownImage from "./DropPartMarkdownImage";
import WaveDropQuoteWithDropId from "../../../waves/drops/WaveDropQuoteWithDropId";
@@ -42,6 +40,25 @@ import WaveItemChat from "../../../waves/list/WaveItemChat";
import DropItemChat from "../../../waves/drops/DropItemChat";
import ChatItemHrefButtons from "../../../waves/ChatItemHrefButtons";
+const BreakComponent = () =>
;
+
+const TweetEmbedSkeleton = () => (
+
+ Loading tweet…
+
+);
+
+const TweetEmbed = dynamic(
+ () => import("react-tweet").then((mod) => mod.Tweet),
+ {
+ ssr: false,
+ loading: TweetEmbedSkeleton,
+ }
+);
+
export interface DropPartMarkdownProps {
readonly mentionedUsers: Array;
readonly referencedNfts: Array;
@@ -322,7 +339,7 @@ function DropPartMarkdown({
const renderTweetEmbed = (result: { href: string; tweetId: string }) => (