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.{" "}
+
+
+ )}