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: