diff --git a/__tests__/components/waves/drops/WaveDropActionsCopyLink.test.tsx b/__tests__/components/waves/drops/WaveDropActionsCopyLink.test.tsx
index 8850b9e54b..c456e42e8b 100644
--- a/__tests__/components/waves/drops/WaveDropActionsCopyLink.test.tsx
+++ b/__tests__/components/waves/drops/WaveDropActionsCopyLink.test.tsx
@@ -1,12 +1,18 @@
import WaveDropActionsCopyLink from "@/components/waves/drops/WaveDropActionsCopyLink";
+import { ApiDropType } from "@/generated/models/ApiDropType";
import "@testing-library/jest-dom";
import { fireEvent, render } from "@testing-library/react";
+const mockIsMemesWave = jest.fn();
+
jest.mock("@/config/env", () => ({
publicEnv: {
BASE_ENDPOINT: "https://base",
},
}));
+jest.mock("@/contexts/SeizeSettingsContext", () => ({
+ useSeizeSettings: () => ({ isMemesWave: mockIsMemesWave }),
+}));
const writeText = jest.fn().mockResolvedValue(undefined);
Object.assign(navigator, { clipboard: { writeText } });
@@ -14,15 +20,35 @@ Object.assign(navigator, { clipboard: { writeText } });
describe("WaveDropActionsCopyLink", () => {
beforeEach(() => {
jest.clearAllMocks();
+ mockIsMemesWave.mockReturnValue(false);
});
- it("copies drop link when clicked", () => {
- const drop: any = { id: "d1", wave: { id: "w1" }, serial_no: 5 };
+ it("copies serial jump links for non-memes drops", () => {
+ const drop: any = {
+ id: "d1",
+ wave: { id: "w1" },
+ serial_no: 5,
+ drop_type: ApiDropType.Chat,
+ };
const { getByRole } = render();
fireEvent.click(getByRole("button"));
expect(writeText).toHaveBeenCalledWith("https://base/waves/w1?serialNo=5");
});
+ it("copies canonical drop links for memes submissions", () => {
+ mockIsMemesWave.mockReturnValue(true);
+
+ const drop: any = {
+ id: "d1",
+ wave: { id: "w1" },
+ serial_no: 5,
+ drop_type: ApiDropType.Participatory,
+ };
+ const { getByRole } = render();
+ fireEvent.click(getByRole("button"));
+ expect(writeText).toHaveBeenCalledWith("https://base/waves/w1?drop=d1");
+ });
+
it("disables button for temporary drop", () => {
const drop: any = { id: "temp-1", wave: { id: "w1" }, serial_no: 1 };
const { getByRole } = render();
diff --git a/__tests__/components/waves/drops/WaveDropMobileMenu.test.tsx b/__tests__/components/waves/drops/WaveDropMobileMenu.test.tsx
index 4eeaa33a11..f53fa42673 100644
--- a/__tests__/components/waves/drops/WaveDropMobileMenu.test.tsx
+++ b/__tests__/components/waves/drops/WaveDropMobileMenu.test.tsx
@@ -5,6 +5,9 @@ import { useDropInteractionRules } from "@/hooks/drops/useDropInteractionRules";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
+const mockIsMemesWave = jest.fn();
+const writeText = jest.fn().mockResolvedValue(undefined);
+
jest.mock("@/hooks/drops/useDropInteractionRules", () => ({
useDropInteractionRules: jest.fn(),
}));
@@ -44,7 +47,7 @@ jest.mock(
);
jest.mock("@/contexts/SeizeSettingsContext", () => ({
- useSeizeSettings: () => ({ isMemesWave: jest.fn().mockReturnValue(true) }),
+ useSeizeSettings: () => ({ isMemesWave: mockIsMemesWave }),
}));
jest.mock("@/contexts/EmojiContext", () => ({
useEmoji: () => ({
@@ -57,20 +60,21 @@ jest.mock("@/contexts/EmojiContext", () => ({
}),
EmojiProvider: ({ children }: any) => children,
}));
-
-jest.doMock("@/config/env", () => ({
+jest.mock("@/config/env", () => ({
publicEnv: { BASE_ENDPOINT: "https://base" },
}));
beforeAll(() => {
Object.assign(navigator, {
- clipboard: { writeText: jest.fn().mockResolvedValue(undefined) },
+ clipboard: { writeText },
});
});
const mockedUseDropInteractionRules = jest.mocked(useDropInteractionRules);
beforeEach(() => {
+ writeText.mockClear();
+ mockIsMemesWave.mockReturnValue(false);
mockedUseDropInteractionRules.mockReturnValue({
canShowVote: true,
canVote: true,
@@ -83,7 +87,7 @@ beforeEach(() => {
});
});
-test("copies link and shows feedback", async () => {
+test("copies serial jump links for non-memes drops", async () => {
const drop = {
id: "1",
serial_no: 1,
@@ -113,10 +117,47 @@ test("copies link and shows feedback", async () => {
);
await userEvent.click(screen.getByText("Copy link"));
- expect(navigator.clipboard.writeText).toHaveBeenCalled();
+ expect(writeText).toHaveBeenCalledWith("https://base/waves/w?serialNo=1");
+});
+
+test("copies canonical drop links for memes submissions", async () => {
+ mockIsMemesWave.mockReturnValue(true);
+
+ const drop = {
+ id: "1",
+ serial_no: 1,
+ wave: { id: "w" },
+ drop_type: ApiDropType.Participatory,
+ author: { handle: "alice" },
+ } as any;
+ render(
+
+
+
+ );
+ await userEvent.click(screen.getByText("Copy link"));
+ expect(writeText).toHaveBeenCalledWith("https://base/waves/w?drop=1");
});
test("hides follow and clap when author and memes wave", () => {
+ mockIsMemesWave.mockReturnValue(true);
+
const drop = {
id: "1",
serial_no: 1,
diff --git a/components/user/layout/userPageVisibility.ts b/components/user/layout/userPageVisibility.ts
index f33cfabc82..2a326d85a6 100644
--- a/components/user/layout/userPageVisibility.ts
+++ b/components/user/layout/userPageVisibility.ts
@@ -1,6 +1,6 @@
"use client";
-export const normalizeCountry = (
+const normalizeCountry = (
country: string | null | undefined
): string | null => {
if (typeof country !== "string") {
diff --git a/components/waves/drops/WaveDropActionsCopyLink.tsx b/components/waves/drops/WaveDropActionsCopyLink.tsx
index 0cf6e3b2c0..9e3a561ebd 100644
--- a/components/waves/drops/WaveDropActionsCopyLink.tsx
+++ b/components/waves/drops/WaveDropActionsCopyLink.tsx
@@ -1,7 +1,7 @@
"use client";
-import { publicEnv } from "@/config/env";
-import { getWaveRoute } from "@/helpers/navigation.helpers";
+import { useSeizeSettings } from "@/contexts/SeizeSettingsContext";
+import { getCopiedDropLink } from "@/helpers/waves/drop-copy-link.helpers";
import { isWaveDirectMessage } from "@/helpers/waves/wave.helpers";
import React, { useState } from "react";
import { Tooltip } from "react-tooltip";
@@ -20,6 +20,7 @@ const WaveDropActionsCopyLink: React.FC = ({
onCopy,
}) => {
const [copied, setCopied] = useState(false);
+ const { isMemesWave } = useSeizeSettings();
const myStream = useMyStreamOptional();
const directMessageWaves = myStream?.directMessages.list ?? [];
@@ -49,12 +50,11 @@ const WaveDropActionsCopyLink: React.FC = ({
waveDetails,
directMessageWaves
);
- const dropLink = `${publicEnv.BASE_ENDPOINT}${getWaveRoute({
- waveId: drop.wave.id,
- serialNo: drop.serial_no,
+ const dropLink = getCopiedDropLink({
+ drop,
isDirectMessage,
- isApp: false,
- })}`;
+ isMemesWave,
+ });
navigator.clipboard.writeText(dropLink).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
diff --git a/components/waves/drops/WaveDropMobileMenu.tsx b/components/waves/drops/WaveDropMobileMenu.tsx
index 8a14af8bcf..c0ac592697 100644
--- a/components/waves/drops/WaveDropMobileMenu.tsx
+++ b/components/waves/drops/WaveDropMobileMenu.tsx
@@ -2,10 +2,10 @@
import { AuthContext } from "@/components/auth/Auth";
import CommonDropdownItemsMobileWrapper from "@/components/utils/select/dropdown/CommonDropdownItemsMobileWrapper";
-import { publicEnv } from "@/config/env";
+import { useSeizeSettings } from "@/contexts/SeizeSettingsContext";
import type { ApiDrop } from "@/generated/models/ApiDrop";
import { ApiDropType } from "@/generated/models/ApiDropType";
-import { getWaveRoute } from "@/helpers/navigation.helpers";
+import { getCopiedDropLink } from "@/helpers/waves/drop-copy-link.helpers";
import type { ExtendedDrop } from "@/helpers/waves/drop.helpers";
import { DropSize } from "@/helpers/waves/drop.helpers";
import { useDropInteractionRules } from "@/hooks/drops/useDropInteractionRules";
@@ -50,6 +50,7 @@ const WaveDropMobileMenu: FC = ({
showCopyOption = true,
}) => {
const { connectedProfile, activeProfileProxy } = useContext(AuthContext);
+ const { isMemesWave } = useSeizeSettings();
const isTemporaryDrop = drop.id.startsWith("temp-");
const { canDelete, canSetPinnedDrop } = useDropInteractionRules(drop);
@@ -83,12 +84,11 @@ const WaveDropMobileMenu: FC = ({
};
const isDirectMessage =
waveDetails.chat?.scope?.group?.is_direct_message ?? false;
- const dropLink = `${publicEnv.BASE_ENDPOINT}${getWaveRoute({
- waveId: drop.wave.id,
- serialNo: drop.serial_no,
+ const dropLink = getCopiedDropLink({
+ drop,
isDirectMessage,
- isApp: false,
- })}`;
+ isMemesWave,
+ });
if (typeof navigator.clipboard.writeText === "function") {
void navigator.clipboard.writeText(dropLink).then(() => {
diff --git a/components/waves/memes/submission/utils/buildPreviewDrop.ts b/components/waves/memes/submission/utils/buildPreviewDrop.ts
index 7018ef4505..fa654860f9 100644
--- a/components/waves/memes/submission/utils/buildPreviewDrop.ts
+++ b/components/waves/memes/submission/utils/buildPreviewDrop.ts
@@ -117,7 +117,9 @@ export const buildPreviewDrop = ({
forbid_negative_votes: wave.voting.forbid_negative_votes,
pinned: wave.pinned,
submission_type: wave.participation.submission_strategy?.type ?? null,
+ selections: wave.selections,
},
+ selections: [],
author: {
id: connectedProfile?.id ?? "preview-user",
handle: connectedProfile?.handle ?? "preview-user",
diff --git a/components/waves/utils/getOptimisticDrop.ts b/components/waves/utils/getOptimisticDrop.ts
index ad3248ede6..8c83e13661 100644
--- a/components/waves/utils/getOptimisticDrop.ts
+++ b/components/waves/utils/getOptimisticDrop.ts
@@ -66,7 +66,9 @@ export const getOptimisticDrop = (
authenticated_user_admin: false,
forbid_negative_votes: wave.voting.forbid_negative_votes,
submission_type: wave.participation.submission_strategy?.type ?? null,
+ selections: wave.selections,
},
+ selections: [],
author: {
id: connectedProfile.id,
handle: connectedProfile.handle,
diff --git a/docs/waves/drop-actions/feature-open-and-copy-links.md b/docs/waves/drop-actions/feature-open-and-copy-links.md
index 807373e2ff..ce0d7132cb 100644
--- a/docs/waves/drop-actions/feature-open-and-copy-links.md
+++ b/docs/waves/drop-actions/feature-open-and-copy-links.md
@@ -9,8 +9,9 @@ Drop-level `Open` / `Open drop` actions and link-card `Open link` /
current thread.
- Preview cards and quote cards expose a `Link actions` button with
`Copy link` and `Open link`.
-- `Copy link` copies an absolute share URL that targets the drop with
- `serialNo={serialNo}`.
+- `Copy link` copies an absolute share URL that targets the drop with:
+ - `serialNo={serialNo}` for most drops
+ - `drop={dropId}` for memes submission drops
- Link-card `Copy link` copies the original referenced URL shown by that card.
## Location in the Site
@@ -59,6 +60,8 @@ Drop-level `Open` / `Open drop` actions and link-card `Open link` /
- Desktop copy can still infer DM routes from stream context when DM scope data
on the drop is incomplete.
- Mobile copy uses Clipboard API when available and falls back to textarea copy.
+- Memes submission drop copy links use canonical wave drop URLs and open the
+ single-drop overlay when opened.
## Edge Cases
@@ -81,8 +84,9 @@ Drop-level `Open` / `Open drop` actions and link-card `Open link` /
## Limitations / Notes
-- `Open` (`drop` query), copied drop links (`serialNo` query), and link-card
- `Open link` are different navigation mechanisms.
+- `Open` (`drop` query), most copied drop links (`serialNo` query), memes
+ submission copied links (`drop` query), and link-card `Open link` are
+ different navigation mechanisms.
- Opening a copied link does not force single-drop overlay mode.
- `Copy link` is unavailable for temporary drops.
- Link-card actions depend on the preview or quote card being rendered. Once a
diff --git a/generated/models/ApiDrop.ts b/generated/models/ApiDrop.ts
index f8edf00621..fdbef2e5a8 100644
--- a/generated/models/ApiDrop.ts
+++ b/generated/models/ApiDrop.ts
@@ -26,6 +26,7 @@ import { ApiMentionedWave } from '../models/ApiMentionedWave';
import { ApiProfileMin } from '../models/ApiProfileMin';
import { ApiReplyToDropResponse } from '../models/ApiReplyToDropResponse';
import { ApiWaveMin } from '../models/ApiWaveMin';
+import { ApiWaveSelection } from '../models/ApiWaveSelection';
import { HttpFile } from '../http/http';
export class ApiDrop {
@@ -65,6 +66,7 @@ export class ApiDrop {
'raters_count': number;
'context_profile_context': ApiDropContextProfileContext | null;
'subscribed_actions': Array;
+ 'selections': Array;
'is_signed': boolean;
'reactions': Array;
'boosts': number;
@@ -220,6 +222,12 @@ export class ApiDrop {
"type": "Array",
"format": ""
},
+ {
+ "name": "selections",
+ "baseName": "selections",
+ "type": "Array",
+ "format": ""
+ },
{
"name": "is_signed",
"baseName": "is_signed",
diff --git a/generated/models/ApiDropWithoutWave.ts b/generated/models/ApiDropWithoutWave.ts
index 098947cea1..31974f475e 100644
--- a/generated/models/ApiDropWithoutWave.ts
+++ b/generated/models/ApiDropWithoutWave.ts
@@ -25,6 +25,7 @@ import { ApiDropWinningContext } from '../models/ApiDropWinningContext';
import { ApiMentionedWave } from '../models/ApiMentionedWave';
import { ApiProfileMin } from '../models/ApiProfileMin';
import { ApiReplyToDropResponse } from '../models/ApiReplyToDropResponse';
+import { ApiWaveSelection } from '../models/ApiWaveSelection';
import { HttpFile } from '../http/http';
export class ApiDropWithoutWave {
@@ -63,6 +64,7 @@ export class ApiDropWithoutWave {
'raters_count': number;
'context_profile_context': ApiDropContextProfileContext | null;
'subscribed_actions': Array;
+ 'selections': Array;
'is_signed': boolean;
'reactions': Array;
'boosts': number;
@@ -212,6 +214,12 @@ export class ApiDropWithoutWave {
"type": "Array",
"format": ""
},
+ {
+ "name": "selections",
+ "baseName": "selections",
+ "type": "Array",
+ "format": ""
+ },
{
"name": "is_signed",
"baseName": "is_signed",
diff --git a/generated/models/ApiWave.ts b/generated/models/ApiWave.ts
index f7308538af..eaaa341527 100644
--- a/generated/models/ApiWave.ts
+++ b/generated/models/ApiWave.ts
@@ -19,6 +19,7 @@ import { ApiWaveContributorOverview } from '../models/ApiWaveContributorOverview
import { ApiWaveDecisionPause } from '../models/ApiWaveDecisionPause';
import { ApiWaveMetrics } from '../models/ApiWaveMetrics';
import { ApiWaveParticipationConfig } from '../models/ApiWaveParticipationConfig';
+import { ApiWaveSelection } from '../models/ApiWaveSelection';
import { ApiWaveSubscriptionTargetAction } from '../models/ApiWaveSubscriptionTargetAction';
import { ApiWaveVisibilityConfig } from '../models/ApiWaveVisibilityConfig';
import { ApiWaveVotingConfig } from '../models/ApiWaveVotingConfig';
@@ -57,6 +58,7 @@ export class ApiWave {
'subscribed_actions': Array;
'metrics': ApiWaveMetrics;
'pauses': Array;
+ 'selections': Array;
'pinned': boolean;
static readonly discriminator: string | undefined = undefined;
@@ -166,6 +168,12 @@ export class ApiWave {
"type": "Array",
"format": ""
},
+ {
+ "name": "selections",
+ "baseName": "selections",
+ "type": "Array",
+ "format": ""
+ },
{
"name": "pinned",
"baseName": "pinned",
diff --git a/generated/models/ApiWaveMin.ts b/generated/models/ApiWaveMin.ts
index 634befdaa2..a80862c717 100644
--- a/generated/models/ApiWaveMin.ts
+++ b/generated/models/ApiWaveMin.ts
@@ -13,6 +13,7 @@
import { ApiWaveCreditType } from '../models/ApiWaveCreditType';
import { ApiWaveParticipationSubmissionStrategyType } from '../models/ApiWaveParticipationSubmissionStrategyType';
+import { ApiWaveSelection } from '../models/ApiWaveSelection';
import { HttpFile } from '../http/http';
export class ApiWaveMin {
@@ -39,6 +40,7 @@ export class ApiWaveMin {
'voting_credit_type': ApiWaveCreditType;
'admin_drop_deletion_enabled': boolean;
'forbid_negative_votes': boolean;
+ 'selections': Array;
'pinned': boolean;
static readonly discriminator: string | undefined = undefined;
@@ -166,6 +168,12 @@ export class ApiWaveMin {
"type": "boolean",
"format": ""
},
+ {
+ "name": "selections",
+ "baseName": "selections",
+ "type": "Array",
+ "format": ""
+ },
{
"name": "pinned",
"baseName": "pinned",
diff --git a/generated/models/ApiWaveSelection.ts b/generated/models/ApiWaveSelection.ts
new file mode 100644
index 0000000000..4c4d7e2a81
--- /dev/null
+++ b/generated/models/ApiWaveSelection.ts
@@ -0,0 +1,44 @@
+// @ts-nocheck
+/**
+ * 6529.io API
+ * This is the API interface description. Brief terminology overview and an authentication example can be found at https://6529.io/about/api.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+import { HttpFile } from '../http/http';
+
+export class ApiWaveSelection {
+ 'id': string;
+ 'title': string;
+
+ static readonly discriminator: string | undefined = undefined;
+
+ static readonly mapping: {[index: string]: string} | undefined = undefined;
+
+ static readonly attributeTypeMap: Array<{name: string, baseName: string, type: string, format: string}> = [
+ {
+ "name": "id",
+ "baseName": "id",
+ "type": "string",
+ "format": ""
+ },
+ {
+ "name": "title",
+ "baseName": "title",
+ "type": "string",
+ "format": ""
+ } ];
+
+ static getAttributeTypeMap() {
+ return ApiWaveSelection.attributeTypeMap;
+ }
+
+ public constructor() {
+ }
+}
diff --git a/generated/models/ApiWaveSelectionDropRequest.ts b/generated/models/ApiWaveSelectionDropRequest.ts
new file mode 100644
index 0000000000..f2ff4fd404
--- /dev/null
+++ b/generated/models/ApiWaveSelectionDropRequest.ts
@@ -0,0 +1,37 @@
+// @ts-nocheck
+/**
+ * 6529.io API
+ * This is the API interface description. Brief terminology overview and an authentication example can be found at https://6529.io/about/api.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+import { HttpFile } from '../http/http';
+
+export class ApiWaveSelectionDropRequest {
+ 'drop_id': string;
+
+ static readonly discriminator: string | undefined = undefined;
+
+ static readonly mapping: {[index: string]: string} | undefined = undefined;
+
+ static readonly attributeTypeMap: Array<{name: string, baseName: string, type: string, format: string}> = [
+ {
+ "name": "drop_id",
+ "baseName": "drop_id",
+ "type": "string",
+ "format": ""
+ } ];
+
+ static getAttributeTypeMap() {
+ return ApiWaveSelectionDropRequest.attributeTypeMap;
+ }
+
+ public constructor() {
+ }
+}
diff --git a/generated/models/ApiWaveSelectionRequest.ts b/generated/models/ApiWaveSelectionRequest.ts
new file mode 100644
index 0000000000..ca949888e6
--- /dev/null
+++ b/generated/models/ApiWaveSelectionRequest.ts
@@ -0,0 +1,37 @@
+// @ts-nocheck
+/**
+ * 6529.io API
+ * This is the API interface description. Brief terminology overview and an authentication example can be found at https://6529.io/about/api.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+import { HttpFile } from '../http/http';
+
+export class ApiWaveSelectionRequest {
+ 'title': string;
+
+ static readonly discriminator: string | undefined = undefined;
+
+ static readonly mapping: {[index: string]: string} | undefined = undefined;
+
+ static readonly attributeTypeMap: Array<{name: string, baseName: string, type: string, format: string}> = [
+ {
+ "name": "title",
+ "baseName": "title",
+ "type": "string",
+ "format": ""
+ } ];
+
+ static getAttributeTypeMap() {
+ return ApiWaveSelectionRequest.attributeTypeMap;
+ }
+
+ public constructor() {
+ }
+}
diff --git a/generated/models/ObjectSerializer.ts b/generated/models/ObjectSerializer.ts
index af569fc3a1..458de8bfaf 100644
--- a/generated/models/ObjectSerializer.ts
+++ b/generated/models/ObjectSerializer.ts
@@ -232,6 +232,9 @@ export * from '../models/ApiWaveParticipationSubmissionStrategyIdentityConf';
export * from '../models/ApiWaveParticipationSubmissionStrategyType';
export * from '../models/ApiWaveRequiredMetadata';
export * from '../models/ApiWaveScope';
+export * from '../models/ApiWaveSelection';
+export * from '../models/ApiWaveSelectionDropRequest';
+export * from '../models/ApiWaveSelectionRequest';
export * from '../models/ApiWaveSubscriptionActions';
export * from '../models/ApiWaveSubscriptionTargetAction';
export * from '../models/ApiWaveType';
@@ -373,7 +376,7 @@ import { ApiCreateWaveOutcome } from '../models/ApiCreateWaveOutcome';
import { ApiCreateWaveOutcomeDistributionItem } from '../models/ApiCreateWaveOutcomeDistributionItem';
import { ApiDistributionAirdropsCsvUploadRequest } from '../models/ApiDistributionAirdropsCsvUploadRequest';
import { ApiDistributionAirdropsUploadResponse } from '../models/ApiDistributionAirdropsUploadResponse';
-import { ApiDrop } from '../models/ApiDrop';
+import { ApiDrop } from '../models/ApiDrop';
import { ApiDropAndDropVote } from '../models/ApiDropAndDropVote';
import { ApiDropBoost } from '../models/ApiDropBoost';
import { ApiDropBoostsPage } from '../models/ApiDropBoostsPage';
@@ -397,7 +400,7 @@ import { ApiDropTraceItem } from '../models/ApiDropTraceItem';
import { ApiDropType } from '../models/ApiDropType';
import { ApiDropVote } from '../models/ApiDropVote';
import { ApiDropWinningContext } from '../models/ApiDropWinningContext';
-import { ApiDropWithoutWave } from '../models/ApiDropWithoutWave';
+import { ApiDropWithoutWave } from '../models/ApiDropWithoutWave';
import { ApiDropWithoutWavesPageWithoutCount } from '../models/ApiDropWithoutWavesPageWithoutCount';
import { ApiDropsLeaderboardPage } from '../models/ApiDropsLeaderboardPage';
import { ApiDropsPage } from '../models/ApiDropsPage';
@@ -524,7 +527,7 @@ import { ApiWaveDropsFeed } from '../models/ApiWaveDropsFeed';
import { ApiWaveLog } from '../models/ApiWaveLog';
import { ApiWaveMetadataType } from '../models/ApiWaveMetadataType';
import { ApiWaveMetrics } from '../models/ApiWaveMetrics';
-import { ApiWaveMin } from '../models/ApiWaveMin';
+import { ApiWaveMin } from '../models/ApiWaveMin';
import { ApiWaveOutcome } from '../models/ApiWaveOutcome';
import { ApiWaveOutcomeCredit } from '../models/ApiWaveOutcomeCredit';
import { ApiWaveOutcomeDistributionItem } from '../models/ApiWaveOutcomeDistributionItem';
@@ -541,6 +544,9 @@ import { ApiWaveParticipationSubmissionStrategyIdentityConf } from '../models/
import { ApiWaveParticipationSubmissionStrategyType } from '../models/ApiWaveParticipationSubmissionStrategyType';
import { ApiWaveRequiredMetadata } from '../models/ApiWaveRequiredMetadata';
import { ApiWaveScope } from '../models/ApiWaveScope';
+import { ApiWaveSelection } from '../models/ApiWaveSelection';
+import { ApiWaveSelectionDropRequest } from '../models/ApiWaveSelectionDropRequest';
+import { ApiWaveSelectionRequest } from '../models/ApiWaveSelectionRequest';
import { ApiWaveSubscriptionActions } from '../models/ApiWaveSubscriptionActions';
import { ApiWaveSubscriptionTargetAction } from '../models/ApiWaveSubscriptionTargetAction';
import { ApiWaveType } from '../models/ApiWaveType';
@@ -880,6 +886,9 @@ let typeMap: {[index: string]: any} = {
"ApiWaveParticipationSubmissionStrategyIdentityConf": ApiWaveParticipationSubmissionStrategyIdentityConf,
"ApiWaveRequiredMetadata": ApiWaveRequiredMetadata,
"ApiWaveScope": ApiWaveScope,
+ "ApiWaveSelection": ApiWaveSelection,
+ "ApiWaveSelectionDropRequest": ApiWaveSelectionDropRequest,
+ "ApiWaveSelectionRequest": ApiWaveSelectionRequest,
"ApiWaveSubscriptionActions": ApiWaveSubscriptionActions,
"ApiWaveVisibilityConfig": ApiWaveVisibilityConfig,
"ApiWaveVoter": ApiWaveVoter,
diff --git a/helpers/waves/drop-copy-link.helpers.ts b/helpers/waves/drop-copy-link.helpers.ts
new file mode 100644
index 0000000000..b291d960ef
--- /dev/null
+++ b/helpers/waves/drop-copy-link.helpers.ts
@@ -0,0 +1,39 @@
+import { publicEnv } from "@/config/env";
+import type { ApiDrop } from "@/generated/models/ApiDrop";
+import { ApiDropType } from "@/generated/models/ApiDropType";
+import { getWaveRoute } from "@/helpers/navigation.helpers";
+
+const isMemesSubmissionCopyLinkDrop = ({
+ drop,
+ isMemesWave,
+}: {
+ drop: ApiDrop;
+ isMemesWave: (waveId: string | undefined | null) => boolean;
+}): boolean =>
+ isMemesWave(drop.wave.id) && drop.drop_type === ApiDropType.Participatory;
+
+export const getCopiedDropLink = ({
+ drop,
+ isDirectMessage,
+ isMemesWave,
+}: {
+ drop: ApiDrop;
+ isDirectMessage: boolean;
+ isMemesWave: (waveId: string | undefined | null) => boolean;
+}): string => {
+ if (isMemesSubmissionCopyLinkDrop({ drop, isMemesWave })) {
+ return `${publicEnv.BASE_ENDPOINT}${getWaveRoute({
+ waveId: drop.wave.id,
+ extraParams: { drop: drop.id },
+ isDirectMessage: false,
+ isApp: false,
+ })}`;
+ }
+
+ return `${publicEnv.BASE_ENDPOINT}${getWaveRoute({
+ waveId: drop.wave.id,
+ serialNo: drop.serial_no,
+ isDirectMessage,
+ isApp: false,
+ })}`;
+};
diff --git a/hooks/useWaveDropsSearch.ts b/hooks/useWaveDropsSearch.ts
index 62482427c4..4277afbaaf 100644
--- a/hooks/useWaveDropsSearch.ts
+++ b/hooks/useWaveDropsSearch.ts
@@ -38,6 +38,7 @@ const toWaveMin = (wave: ApiWave): ApiWaveMin => {
forbid_negative_votes: wave.voting.forbid_negative_votes,
pinned: wave.pinned,
submission_type: wave.participation.submission_strategy?.type ?? null,
+ selections: wave.selections,
};
};
diff --git a/openapi.yaml b/openapi.yaml
index fa0bcae7f1..6cb08f3035 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -1009,6 +1009,12 @@ paths:
required: false
schema:
type: string
+ - name: selection_id
+ in: query
+ description: Only include drops that belong to the given selection
+ required: false
+ schema:
+ type: string
- name: serial_no_less_than
in: query
description: Used to find older drops
@@ -5643,6 +5649,12 @@ paths:
required: false
schema:
$ref: "#/components/schemas/ApiDropType"
+ - name: selection_id
+ in: query
+ description: Only include drops that belong to the given selection
+ required: false
+ schema:
+ type: string
responses:
"200":
description: successful operation
@@ -5890,6 +5902,122 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ApiDropsLeaderboardPage"
+ /waves/{id}/selections:
+ get:
+ tags:
+ - Waves
+ summary: List selections configured for wave
+ operationId: listWaveSelections
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/ApiWaveSelection"
+ post:
+ tags:
+ - Waves
+ summary: Create selection for wave
+ operationId: createWaveSelection
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ApiWaveSelectionRequest"
+ responses:
+ "201":
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ApiWaveSelection"
+ /waves/{id}/selections/{selectionId}:
+ delete:
+ tags:
+ - Waves
+ summary: Delete selection from wave
+ operationId: deleteWaveSelection
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: selectionId
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: successful operation
+ /waves/{id}/selections/{selectionId}/drops:
+ post:
+ tags:
+ - Waves
+ summary: Add drop to selection
+ operationId: addDropToWaveSelection
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: selectionId
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ApiWaveSelectionDropRequest"
+ responses:
+ "201":
+ description: successful operation
+ /waves/{id}/selections/{selectionId}/drops/{dropId}:
+ delete:
+ tags:
+ - Waves
+ summary: Remove drop from selection
+ operationId: removeDropFromWaveSelection
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: selectionId
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: dropId
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: successful operation
/waves/{id}/curation-groups:
get:
tags:
@@ -8249,6 +8377,7 @@ components:
- title
- context_profile_context
- subscribed_actions
+ - selections
- rank
- is_signed
- reactions
@@ -8340,6 +8469,10 @@ components:
type: array
items:
$ref: "#/components/schemas/ApiDropSubscriptionTargetAction"
+ selections:
+ type: array
+ items:
+ $ref: "#/components/schemas/ApiWaveSelection"
is_signed:
type: boolean
reactions:
@@ -8715,6 +8848,7 @@ components:
- raters_count
- title
- subscribed_actions
+ - selections
- context_profile_context
- rank
- is_signed
@@ -8805,6 +8939,10 @@ components:
type: array
items:
$ref: "#/components/schemas/ApiDropSubscriptionTargetAction"
+ selections:
+ type: array
+ items:
+ $ref: "#/components/schemas/ApiWaveSelection"
is_signed:
type: boolean
reactions:
@@ -11221,6 +11359,7 @@ components:
- subscribed_actions
- metrics
- pauses
+ - selections
- pinned
properties:
id:
@@ -11273,6 +11412,10 @@ components:
type: array
items:
$ref: "#/components/schemas/ApiWaveDecisionPause"
+ selections:
+ type: array
+ items:
+ $ref: "#/components/schemas/ApiWaveSelection"
pinned:
type: boolean
ApiWaveChatConfig:
@@ -11627,6 +11770,7 @@ components:
- admin_group_id
- admin_drop_deletion_enabled
- forbid_negative_votes
+ - selections
- pinned
properties:
id:
@@ -11683,6 +11827,10 @@ components:
type: boolean
forbid_negative_votes:
type: boolean
+ selections:
+ type: array
+ items:
+ $ref: "#/components/schemas/ApiWaveSelection"
pinned:
type: boolean
ApiWaveOutcome:
@@ -11872,6 +12020,32 @@ components:
anyOf:
- type: "null"
- $ref: "#/components/schemas/ApiGroup"
+ ApiWaveSelection:
+ type: object
+ required:
+ - id
+ - title
+ properties:
+ id:
+ type: string
+ title:
+ type: string
+ ApiWaveSelectionDropRequest:
+ type: object
+ required:
+ - drop_id
+ properties:
+ drop_id:
+ type: string
+ ApiWaveSelectionRequest:
+ type: object
+ required:
+ - title
+ properties:
+ title:
+ type: string
+ minLength: 1
+ maxLength: 250
ApiWavesOverviewType:
type: string
enum: