diff --git a/__tests__/components/waves/CreateDrop.test.tsx b/__tests__/components/waves/CreateDrop.test.tsx index 8290805353..6a621f4e79 100644 --- a/__tests__/components/waves/CreateDrop.test.tsx +++ b/__tests__/components/waves/CreateDrop.test.tsx @@ -38,21 +38,11 @@ jest.mock("@/contexts/wave/MyStreamContext", () => ({ jest.mock("@/components/waves/CreateDropStormParts", () => () => (
)); +const PREFILL_URL = + "https://opensea.io/item/ethereum/0x1234567890abcdef1234567890abcdef12345678/123"; + jest.mock("@/components/waves/CreateDropContent", () => (props: any) => ( - -)); -jest.mock("@/components/waves/CreateCurationDropContent", () => ({ - __esModule: true, - default: (props: any) => ( +
+ +
+)); +jest.mock("@/components/waves/CreateCurationDropContent", () => ({ + __esModule: true, + default: (props: any) => ( +
+ +
{props.initialUrl ?? ""}
+
), })); @@ -172,4 +189,82 @@ describe("CreateDrop", () => { type: "success", }); }); + + it("switches to curation mode with a prefilled url seed", async () => { + useWaveMock.mockReturnValue({ isCurationWave: true } as any); + + render( + + + {}} + onDropAddedToQueue={jest.fn()} + wave={wave} + dropId={null} + fixedDropMode={"BOTH" as any} + privileges={{} as any} + /> + + + ); + + await userEvent.click(screen.getByText("switch to drop")); + + await waitFor(() => + expect(screen.getByTestId("initial-url")).toHaveTextContent(PREFILL_URL) + ); + }); + + it("resets to default mode when wave scope changes", async () => { + useWaveMock.mockReturnValue({ isCurationWave: true } as any); + const nextWave = { ...wave, id: "2" }; + + const { rerender } = render( + + + {}} + onDropAddedToQueue={jest.fn()} + wave={wave} + dropId={null} + fixedDropMode={"BOTH" as any} + privileges={{} as any} + /> + + + ); + + await userEvent.click(screen.getByText("switch to drop")); + await waitFor(() => + expect(screen.getByText("submit curation")).toBeInTheDocument() + ); + + rerender( + + + {}} + onDropAddedToQueue={jest.fn()} + wave={nextWave} + dropId={null} + fixedDropMode={"BOTH" as any} + privileges={{} as any} + /> + + + ); + + await waitFor(() => + expect(screen.getByText("switch to drop")).toBeInTheDocument() + ); + }); }); diff --git a/components/waves/CreateCurationDropContent.tsx b/components/waves/CreateCurationDropContent.tsx index a93cc37be3..c8bba69cc2 100644 --- a/components/waves/CreateCurationDropContent.tsx +++ b/components/waves/CreateCurationDropContent.tsx @@ -51,6 +51,7 @@ interface CreateCurationDropContentProps { readonly wave: ApiWave; readonly dropId: string | null; readonly isDropMode: boolean; + readonly initialUrl: string | null; readonly submitDrop: (dropRequest: DropMutationBody) => void; readonly curationComposerVariant?: CurationComposerVariant | undefined; } @@ -148,6 +149,7 @@ const CreateCurationDropContent: React.FC = ({ wave, dropId, isDropMode, + initialUrl, submitDrop, curationComposerVariant = "default", }) => { @@ -159,7 +161,7 @@ const CreateCurationDropContent: React.FC = ({ const { processIncomingDrop } = useMyStream(); const { signDrop } = useDropSignature(); - const [urlValue, setUrlValue] = useState(""); + const [urlValue, setUrlValue] = useState(() => initialUrl ?? ""); const [submitting, setSubmitting] = useState(false); const [showLiveValidation, setShowLiveValidation] = useState(false); const [isSupportedUrlsModalOpen, setIsSupportedUrlsModalOpen] = @@ -480,7 +482,7 @@ const CreateCurationDropContent: React.FC = ({
) : ( -
+
= ({ />
-
+
onCancelReplyQuote()); const [isStormMode, setIsStormMode] = useState(false); const [drop, setDrop] = useState(null); + const [dropModeOverride, setDropModeOverride] = useState<{ + scopeKey: string; + value: boolean; + } | null>(null); + const [curationPrefillSeed, setCurationPrefillSeed] = useState<{ + scopeKey: string; + url: string; + } | null>(null); const { processDropRemoved, processIncomingDrop } = useMyStream(); const { isCurationWave } = useWave(wave); - const getIsDropMode = () => { + const getDefaultIsDropMode = () => { if (fixedDropMode === DropMode.CHAT) { return false; } @@ -82,21 +83,41 @@ export default function CreateDrop({ return false; }; - const [isDropMode, setIsDropMode] = useState(getIsDropMode()); + const activeDropScope = + activeDrop === null + ? "none" + : `${activeDrop.action}:${activeDrop.drop.id}:${activeDrop.partId}`; + const modeScopeKey = `${wave.id}:${fixedDropMode}:${wave.chat.authenticated_user_eligible}:${wave.participation.authenticated_user_eligible}:${activeDropScope}`; + const modeScopeEpochRef = useRef(0); + const lastModeScopeKeyRef = useRef(modeScopeKey); + if (lastModeScopeKeyRef.current !== modeScopeKey) { + lastModeScopeKeyRef.current = modeScopeKey; + modeScopeEpochRef.current += 1; + } + const modeScopeToken = `${modeScopeKey}:${modeScopeEpochRef.current}`; + const defaultIsDropMode = getDefaultIsDropMode(); + const isDropMode = + dropModeOverride?.scopeKey === modeScopeToken + ? dropModeOverride.value + : defaultIsDropMode; + const initialCurationUrl = + curationPrefillSeed?.scopeKey === modeScopeToken + ? curationPrefillSeed.url + : null; const isCurationDropMode = isCurationWave && isDropMode; - useEffect(() => setIsDropMode(getIsDropMode()), [wave, activeDrop]); - const onDropModeChange = useCallback( + const canSwitchDropMode = useCallback( (newIsDropMode: boolean) => { if (fixedDropMode !== DropMode.BOTH) { - return; + return false; } + if (newIsDropMode && !wave.participation.authenticated_user_eligible) { setToast({ message: "You are not eligible to drop in this wave", type: "error", }); - return; + return false; } if (!newIsDropMode && !wave.chat.authenticated_user_eligible) { @@ -104,12 +125,34 @@ export default function CreateDrop({ message: "You are not eligible to chat in this wave", type: "error", }); + return false; + } + + return true; + }, + [fixedDropMode, setToast, wave] + ); + + const onDropModeChange = useCallback( + (newIsDropMode: boolean) => { + if (!canSwitchDropMode(newIsDropMode)) { return; } + setCurationPrefillSeed(null); + setDropModeOverride({ scopeKey: modeScopeToken, value: newIsDropMode }); + }, + [canSwitchDropMode, modeScopeToken] + ); - setIsDropMode(newIsDropMode); + const onSwitchToDropModeWithUrl = useCallback( + (url: string) => { + if (!canSwitchDropMode(true)) { + return; + } + setCurationPrefillSeed({ scopeKey: modeScopeToken, url }); + setDropModeOverride({ scopeKey: modeScopeToken, value: true }); }, - [wave] + [canSwitchDropMode, modeScopeToken] ); const onRemovePart = useCallback((partIndex: number) => { @@ -252,6 +295,7 @@ export default function CreateDrop({ setDrop, setIsStormMode, onDropModeChange, + onSwitchToDropModeWithUrl, submitDrop, privileges, }), @@ -265,6 +309,7 @@ export default function CreateDrop({ setDrop, setIsStormMode, onDropModeChange, + onSwitchToDropModeWithUrl, submitDrop, privileges, ] @@ -298,6 +343,7 @@ export default function CreateDrop({ wave={wave} dropId={dropId} isDropMode={isDropMode} + initialUrl={initialCurationUrl} submitDrop={submitDrop} curationComposerVariant={curationComposerVariant} /> diff --git a/components/waves/CreateDropContent.tsx b/components/waves/CreateDropContent.tsx index 3fde7dc975..e7b49fbb31 100644 --- a/components/waves/CreateDropContent.tsx +++ b/components/waves/CreateDropContent.tsx @@ -73,6 +73,7 @@ import { import type { MissingRequirements } from "./utils/getMissingRequirements"; import { getMissingRequirements } from "./utils/getMissingRequirements"; import { getOptimisticDrop } from "./utils/getOptimisticDrop"; +import { normalizeCurationDropInput } from "./utils/validateCurationDropUrl"; // Use next/dynamic for lazy loading with SSR support const TermsSignatureFlow = dynamic( @@ -116,6 +117,7 @@ interface CreateDropContentProps { >; readonly setIsStormMode: React.Dispatch>; readonly onDropModeChange: (newIsDropMode: boolean) => void; + readonly onSwitchToDropModeWithUrl: (url: string) => void; readonly submitDrop: (dropRequest: DropMutationBody) => void; readonly privileges: DropPrivileges; } @@ -345,6 +347,7 @@ const CreateDropContent: React.FC = ({ setDrop, setIsStormMode, onDropModeChange, + onSwitchToDropModeWithUrl, submitDrop, privileges, }) => { @@ -360,7 +363,7 @@ const CreateDropContent: React.FC = ({ const { addOptimisticDrop } = useContext(ReactQueryWrapperContext); const { processIncomingDrop } = useMyStream(); const { signDrop } = useDropSignature(); - const { isMemesWave } = useWave(wave); + const { isMemesWave, isCurationWave } = useWave(wave); const [submitting, setSubmitting] = useState(false); const [editorState, setEditorState] = useState(null); @@ -478,6 +481,14 @@ const CreateDropContent: React.FC = ({ const getCanAddPart = () => getHaveMarkdownOrFile() && !getIsDropLimit(); const canSubmit = getCanSubmit(); const canAddPart = getCanAddPart(); + const normalizedCurationDropUrl = useMemo(() => { + if (!isCurationWave || isDropMode) { + return null; + } + return normalizeCurationDropInput(getMarkdown ?? ""); + }, [getMarkdown, isCurationWave, isDropMode]); + const showCurationDropModeWarning = + !isDropMode && !!normalizedCurationDropUrl && isCurationWave; const [referencedNfts, setReferencedNfts] = useState([]); @@ -908,6 +919,13 @@ const CreateDropContent: React.FC = ({ await prepareAndSubmitDrop(createGifDrop(gif)); }; + const onSwitchToDropMode = useCallback(() => { + if (!normalizedCurationDropUrl) { + return; + } + onSwitchToDropModeWithUrl(normalizedCurationDropUrl); + }, [normalizedCurationDropUrl, onSwitchToDropModeWithUrl]); + const focusInputWithDelay = (delay: number) => { setTimeout(() => { createDropInputRef.current?.focus(); @@ -1182,6 +1200,18 @@ const CreateDropContent: React.FC = ({ onMentionedWave={onMentionedWave} onDrop={onDrop} /> + {showCurationDropModeWarning && ( +
+ This looks like a curation URL.{" "} + +
+ )}