diff --git a/.changeset/thirty-carpets-rush.md b/.changeset/thirty-carpets-rush.md new file mode 100644 index 0000000000..b46984a954 --- /dev/null +++ b/.changeset/thirty-carpets-rush.md @@ -0,0 +1,7 @@ +--- +"@lens-protocol/react": minor +"@lens-protocol/react-web": minor +"@lens-protocol/domain": patch +--- + +**Added** support for `attributes` and `image` for non-collectable publications. Affects `useCreatePost`, `useCreateComment`, `useCreateEncryptedPost`, and `useCreateEncryptedComment` diff --git a/packages/domain/src/use-cases/publications/CreateComment.ts b/packages/domain/src/use-cases/publications/CreateComment.ts index a1f64dfedf..6797697dcd 100644 --- a/packages/domain/src/use-cases/publications/CreateComment.ts +++ b/packages/domain/src/use-cases/publications/CreateComment.ts @@ -9,7 +9,15 @@ import { } from '../../entities'; import { DelegableSigning } from '../transactions/DelegableSigning'; import { ReferencePolicyConfig } from './ReferencePolicyConfig'; -import { CollectPolicyConfig, ContentFocus, ContentWarning, Locale, MediaObject } from './types'; +import { + CollectPolicyConfig, + ContentFocus, + ContentWarning, + Locale, + MediaObject, + MetadataAttribute, + MetadataImage, +} from './types'; /** * @alpha @@ -71,6 +79,27 @@ export type BaseCommentRequest = { * These are not the same as #hashtag in the publication content. Use these if you don't want to clutter the publication content with tags. */ tags?: string[]; + /** + * A list of attributes for the collect NFT. + * + * This is the NFT description visible on marketplaces like OpenSea. + */ + attributes?: MetadataAttribute[]; + /** + * The collect NFT image. + * + * This is the NFT image visible on marketplaces like OpenSea. + * + * DO NOT use this as primary storage for publication media. Use the `media` property instead. + * In the case your publication has many media consider to use this field as a static representation + * of the collect NFT. For example if the publication is an album of audio files this could be + * used as album cover image. If the publication is a gallery of images, this could be the gallery + * cover image. + * + * DO NOT use this as media cover image. + * For individual media cover image (e.g. the video thumbnail image) use the `media[n].cover` (see {@link MediaObject}). + */ + image?: MetadataImage; }; export type CreateTextualCommentRequest = BaseCommentRequest & { diff --git a/packages/domain/src/use-cases/publications/CreatePost.ts b/packages/domain/src/use-cases/publications/CreatePost.ts index 0d9cf7dd3b..e90a103abf 100644 --- a/packages/domain/src/use-cases/publications/CreatePost.ts +++ b/packages/domain/src/use-cases/publications/CreatePost.ts @@ -3,7 +3,15 @@ import { Url, invariant } from '@lens-protocol/shared-kernel'; import { AppId, DecryptionCriteria, ProfileId, TransactionKind } from '../../entities'; import { DelegableSigning } from '../transactions/DelegableSigning'; import { ReferencePolicyConfig } from './ReferencePolicyConfig'; -import { CollectPolicyConfig, ContentFocus, ContentWarning, Locale, MediaObject } from './types'; +import { + CollectPolicyConfig, + ContentFocus, + ContentWarning, + Locale, + MediaObject, + MetadataAttribute, + MetadataImage, +} from './types'; /** * @alpha @@ -61,6 +69,27 @@ export type BasePostRequest = { * These are not the same as #hashtag in the publication content. Use these if you don't want to clutter the publication content with tags. */ tags?: string[]; + /** + * A list of attributes for the collect NFT. + * + * This is the NFT description visible on marketplaces like OpenSea. + */ + attributes?: MetadataAttribute[]; + /** + * The collect NFT image. + * + * This is the NFT image visible on marketplaces like OpenSea. + * + * DO NOT use this as primary storage for publication media. Use the `media` property instead. + * In the case your publication has many media consider to use this field as a static representation + * of the collect NFT. For example if the publication is an album of audio files this could be + * used as album cover image. If the publication is a gallery of images, this could be the gallery + * cover image. + * + * DO NOT use this as media cover image. + * For individual media cover image (e.g. the video thumbnail image) use the `media[n].cover` (see {@link MediaObject}). + */ + image?: MetadataImage; }; export type CreateTextualPostRequest = BasePostRequest & { diff --git a/packages/domain/src/use-cases/publications/__helpers__/mocks.ts b/packages/domain/src/use-cases/publications/__helpers__/mocks.ts index afc0a4a760..ee22d40adf 100644 --- a/packages/domain/src/use-cases/publications/__helpers__/mocks.ts +++ b/packages/domain/src/use-cases/publications/__helpers__/mocks.ts @@ -23,8 +23,9 @@ import { ContentFocus, FreeCollectPolicyConfig, MediaObject, - NftAttribute, - NftAttributeDisplayType, + MetadataAttribute, + MetadataAttributeDisplayType, + MetadataImage, NftMetadata, NoCollectPolicyConfig, SimpleChargeCollectPolicyConfig, @@ -62,25 +63,33 @@ export function mockMediaObject(overrides?: Partial): MediaObject { }; } -export function mockDateNftAttribute(): NftAttribute { +export function mockMetadataImage(overrides?: Partial): MetadataImage { return { - displayType: NftAttributeDisplayType.Date, + url: faker.image.imageUrl(), + mimeType: ImageType.JPEG, + ...overrides, + }; +} + +export function mockDateMetadataAttribute(): MetadataAttribute { + return { + displayType: MetadataAttributeDisplayType.Date, traitType: faker.lorem.word(), value: faker.date.past(), }; } -export function mockNumberNftAttribute(): NftAttribute { +export function mockNumberMetadataAttribute(): MetadataAttribute { return { - displayType: NftAttributeDisplayType.Number, + displayType: MetadataAttributeDisplayType.Number, traitType: faker.lorem.word(), value: faker.datatype.number(), }; } -export function mockStringNftAttribute(): NftAttribute { +export function mockStringMetadataAttribute(): MetadataAttribute { return { - displayType: NftAttributeDisplayType.String, + displayType: MetadataAttributeDisplayType.String, traitType: faker.lorem.word(), value: faker.lorem.word(), }; diff --git a/packages/domain/src/use-cases/publications/types.ts b/packages/domain/src/use-cases/publications/types.ts index 36b6478a26..5524d14ac3 100644 --- a/packages/domain/src/use-cases/publications/types.ts +++ b/packages/domain/src/use-cases/publications/types.ts @@ -53,42 +53,53 @@ export enum ContentWarning { SPOILER = 'Spoiler', } -export enum NftAttributeDisplayType { +export enum MetadataAttributeDisplayType { Number = 'Number', String = 'String', Date = 'Date', } -export type NftAttribute = +export type MetadataAttribute = | { - displayType: NftAttributeDisplayType.Date; + displayType: MetadataAttributeDisplayType.Date; value: Date; traitType: string; } | { - displayType: NftAttributeDisplayType.Number; + displayType: MetadataAttributeDisplayType.Number; value: number; traitType: string; } | { - displayType: NftAttributeDisplayType.String; + displayType: MetadataAttributeDisplayType.String; value: string; traitType: string; }; +export type MetadataImage = { + url: Url; + mimeType: ImageType; +}; + export type NftMetadata = { /** - * The name of the NFT. + * The name of the collect NFT. + * + * This is the NFT name visible on marketplaces like OpenSea. */ name: string; /** - * The description of the NFT. + * The description of the collect NFT. + * + * This is the NFT description visible on marketplaces like OpenSea. */ description?: string; /** * A list of attributes for the NFT. + * + * @deprecated Use the `attributes` field at the request object top level. */ - attributes: NftAttribute[]; + attributes?: MetadataAttribute[]; /** * This is the URL that will appear below the asset's image on OpenSea and other marketplaces. * It will allow users to leave OpenSea and view the item on the external site. @@ -96,11 +107,15 @@ export type NftMetadata = { externalUrl?: Url; /** * Legacy to support OpenSea schema, store any NFT image here. + * + * @deprecated Use the `image` field at the request object top level. */ image?: Url; /** * This is the mime type of the image. This is used if you are uploading more advanced * cover images as sometimes IPFS does not emit the content header so this solves that. + * + * @deprecated Use the `image` field at the request object top level. */ imageMimeType?: ImageType; }; diff --git a/packages/react/src/deprecated.ts b/packages/react/src/deprecated.ts index 59b8fa55c2..08223dea80 100644 --- a/packages/react/src/deprecated.ts +++ b/packages/react/src/deprecated.ts @@ -38,6 +38,10 @@ import { UnknownFollowModuleSettings, WhoReactedResult, } from '@lens-protocol/api-bindings'; +import { + MetadataAttribute, + MetadataAttributeDisplayType, +} from '@lens-protocol/domain/use-cases/publications'; import { AnyTransactionRequest } from '@lens-protocol/domain/use-cases/transactions'; import { LoginError } from '@lens-protocol/domain/use-cases/wallets'; @@ -298,3 +302,13 @@ export enum ReactionType { UPVOTE = ReactionTypes.Upvote, DOWNVOTE = ReactionTypes.Downvote, } + +/** + * @deprecated Use {@link MetadataAttribute} instead. + */ +export type NftAttribute = MetadataAttribute; + +/** + * @deprecated Use {@link MetadataAttributeDisplayType} instead. + */ +export type NftAttributeDisplayType = MetadataAttributeDisplayType; diff --git a/packages/react/src/transactions/adapters/schemas/__tests__/validators.spec.ts b/packages/react/src/transactions/adapters/schemas/__tests__/validators.spec.ts index a11a19d58e..df2b2940c2 100644 --- a/packages/react/src/transactions/adapters/schemas/__tests__/validators.spec.ts +++ b/packages/react/src/transactions/adapters/schemas/__tests__/validators.spec.ts @@ -279,83 +279,201 @@ describe(`Given the validator helpers`, () => { }); }); - it('should provide an actionable error message in case of "collect" policy misconfiguration', () => { - expect(() => - validateCreatePostRequest({ - contentFocus: ContentFocus.TEXT_ONLY, - content: '', - collect: { - type: CollectPolicyType.CHARGE, - fee: '1', - }, - delegate: false, - locale: 'en', - kind: TransactionKind.CREATE_POST, - offChain: false, - profileId: mockProfileId(), - reference: { - type: ReferencePolicyType.ANYONE, - }, - }), - ).toThrowErrorMatchingInlineSnapshot(` - "fix the following issues - · "collect" expected to match one of the following groups: - · "collect.fee": value not instance of Amount - · "collect.followersOnly": Required - · "collect.metadata": Required - · "collect.mirrorReward": Required - · "collect.recipient": Required - · "collect.timeLimited": Required - OR: - · "collect.fee": value not instance of Amount - · "collect.followersOnly": Required - · "collect.metadata": Required - · "collect.mirrorReward": Required - · "collect.recipients": Required - OR: - · "collect.fee": value not instance of Amount - · "collect.followersOnly": Required - · "collect.metadata": Required - · "collect.mirrorReward": Required - · "collect.recipient": Required - · "collect.vault": Required - OR: - · "collect.fee": value not instance of Amount - · "collect.followersOnly": Required - · "collect.metadata": Required - · "collect.mirrorReward": Required - · "collect.recipient": Required - · "collect.depositToAave": Invalid literal value, expected true - OR: - · "collect.type": Invalid literal value, expected "FREE" - · "collect.metadata": Required - · "collect.followersOnly": Required - OR: - · "collect.type": Invalid literal value, expected "NO_COLLECT"" - `); + describe('with invalid "collect" policy', () => { + it('should provide an actionable error message', () => { + expect(() => + validateCreatePostRequest({ + contentFocus: ContentFocus.TEXT_ONLY, + content: '', + collect: { + type: CollectPolicyType.CHARGE, + fee: '1', + }, + delegate: false, + locale: 'en', + kind: TransactionKind.CREATE_POST, + offChain: false, + profileId: mockProfileId(), + reference: { + type: ReferencePolicyType.ANYONE, + }, + }), + ).toThrowErrorMatchingInlineSnapshot(` + "fix the following issues + · "collect" expected to match one of the following groups: + · "collect.fee": value not instance of Amount + · "collect.followersOnly": Required + · "collect.metadata": Required + · "collect.mirrorReward": Required + · "collect.recipient": Required + · "collect.timeLimited": Required + OR: + · "collect.fee": value not instance of Amount + · "collect.followersOnly": Required + · "collect.metadata": Required + · "collect.mirrorReward": Required + · "collect.recipients": Required + OR: + · "collect.fee": value not instance of Amount + · "collect.followersOnly": Required + · "collect.metadata": Required + · "collect.mirrorReward": Required + · "collect.recipient": Required + · "collect.vault": Required + OR: + · "collect.fee": value not instance of Amount + · "collect.followersOnly": Required + · "collect.metadata": Required + · "collect.mirrorReward": Required + · "collect.recipient": Required + · "collect.depositToAave": Invalid literal value, expected true + OR: + · "collect.type": Invalid literal value, expected "FREE" + · "collect.metadata": Required + · "collect.followersOnly": Required + OR: + · "collect.type": Invalid literal value, expected "NO_COLLECT"" + `); + }); }); - it('should provide an actionable error message in case of "reference" policy misconfiguration', () => { - expect(() => - validateCreatePostRequest({ - contentFocus: ContentFocus.TEXT_ONLY, - content: '', - collect: { - type: CollectPolicyType.NO_COLLECT, - }, - delegate: false, - locale: 'en', - kind: TransactionKind.CREATE_POST, - offChain: false, - profileId: mockProfileId(), - reference: { - type: ReferencePolicyType.DEGREES_OF_SEPARATION, - }, - }), - ).toThrowErrorMatchingInlineSnapshot(` - "fix the following issues - · "reference.params": Required" - `); + describe('with invalid "collect.metadata" for collectable publications', () => { + it('should provide an actionable error message in case of "collect.metadata" misconfiguration', () => { + expect(() => + validateCreatePostRequest({ + contentFocus: ContentFocus.TEXT_ONLY, + content: '', + collect: { + type: CollectPolicyType.FREE, + metadata: {}, + }, + delegate: false, + locale: 'en', + kind: TransactionKind.CREATE_POST, + offChain: false, + profileId: mockProfileId(), + reference: { + type: ReferencePolicyType.ANYONE, + }, + }), + ).toThrowErrorMatchingInlineSnapshot(` + "fix the following issues + · "collect" expected to match one of the following groups: + · "collect.type": Invalid literal value, expected "CHARGE" + · "collect.fee": value not instance of Amount + · "collect.followersOnly": Required + · "collect.metadata.name": Required + · "collect.mirrorReward": Required + · "collect.recipient": Required + · "collect.timeLimited": Required + OR: + · "collect.type": Invalid literal value, expected "CHARGE" + · "collect.fee": value not instance of Amount + · "collect.followersOnly": Required + · "collect.metadata.name": Required + · "collect.mirrorReward": Required + · "collect.recipients": Required + OR: + · "collect.type": Invalid literal value, expected "CHARGE" + · "collect.fee": value not instance of Amount + · "collect.followersOnly": Required + · "collect.metadata.name": Required + · "collect.mirrorReward": Required + · "collect.recipient": Required + · "collect.vault": Required + OR: + · "collect.type": Invalid literal value, expected "CHARGE" + · "collect.fee": value not instance of Amount + · "collect.followersOnly": Required + · "collect.metadata.name": Required + · "collect.mirrorReward": Required + · "collect.recipient": Required + · "collect.depositToAave": Invalid literal value, expected true + OR: + · "collect.metadata.name": Required + · "collect.followersOnly": Required + OR: + · "collect.type": Invalid literal value, expected "NO_COLLECT"" + `); + }); + }); + + describe('with invalid "attributes"', () => { + it('should provide an actionable error message', () => { + expect(() => + validateCreatePostRequest({ + attributes: {}, + contentFocus: ContentFocus.TEXT_ONLY, + content: '', + collect: { + type: CollectPolicyType.NO_COLLECT, + }, + delegate: false, + locale: 'en', + kind: TransactionKind.CREATE_POST, + offChain: false, + profileId: mockProfileId(), + reference: { + type: ReferencePolicyType.ANYONE, + }, + }), + ).toThrowErrorMatchingInlineSnapshot(` + "fix the following issues + · "attributes": Expected array, received object" + `); + }); + }); + + describe('with invalid "image"', () => { + it('should provide an actionable error message', () => { + expect(() => + validateCreatePostRequest({ + contentFocus: ContentFocus.TEXT_ONLY, + content: '', + collect: { + type: CollectPolicyType.NO_COLLECT, + }, + delegate: false, + image: {}, + locale: 'en', + kind: TransactionKind.CREATE_POST, + offChain: false, + profileId: mockProfileId(), + reference: { + type: ReferencePolicyType.ANYONE, + }, + }), + ).toThrowErrorMatchingInlineSnapshot(` + "fix the following issues + · "image.mimeType": Required + · "image.url": Required" + `); + }); + }); + + describe('with invalid "attributes"', () => { + it('should provide an actionable error message', () => { + expect(() => + validateCreatePostRequest({ + contentFocus: ContentFocus.TEXT_ONLY, + content: '', + collect: { + type: CollectPolicyType.NO_COLLECT, + }, + delegate: false, + locale: 'en', + kind: TransactionKind.CREATE_POST, + offChain: false, + profileId: mockProfileId(), + reference: { + type: ReferencePolicyType.DEGREES_OF_SEPARATION, + }, + }), + ).toThrowErrorMatchingInlineSnapshot(` + "fix the following issues + · "reference.params": Required" + `); + }); }); }); diff --git a/packages/react/src/transactions/adapters/schemas/publications.ts b/packages/react/src/transactions/adapters/schemas/publications.ts index 1ba46f8ff0..4a3208f0d3 100644 --- a/packages/react/src/transactions/adapters/schemas/publications.ts +++ b/packages/react/src/transactions/adapters/schemas/publications.ts @@ -10,7 +10,7 @@ import { CollectPolicyType, CollectType, ContentFocus, - NftAttributeDisplayType, + MetadataAttributeDisplayType, ReferencePolicyType, ContentWarning, ImageType, @@ -121,19 +121,19 @@ function decryptionCriteriaSchema( .optional(); } -const NftAttributeSchema = z.discriminatedUnion('displayType', [ +const MetadataAttributeSchema = z.discriminatedUnion('displayType', [ z.object({ - displayType: z.literal(NftAttributeDisplayType.Date), + displayType: z.literal(MetadataAttributeDisplayType.Date), value: z.coerce.date(), traitType: z.string(), }), z.object({ - displayType: z.literal(NftAttributeDisplayType.Number), + displayType: z.literal(MetadataAttributeDisplayType.Number), value: z.number(), traitType: z.string(), }), z.object({ - displayType: z.literal(NftAttributeDisplayType.String), + displayType: z.literal(MetadataAttributeDisplayType.String), value: z.string(), traitType: z.string(), }), @@ -142,7 +142,7 @@ const NftAttributeSchema = z.discriminatedUnion('displayType', [ const NftMetadataSchema = z.object({ name: z.string(), description: z.string().optional(), - attributes: z.array(NftAttributeSchema), + attributes: z.array(MetadataAttributeSchema).optional(), externalUrl: z.string().optional(), image: z.string().optional(), imageMimeType: z.nativeEnum(ImageType).optional(), @@ -242,13 +242,18 @@ function collectPolicyConfigSchema( ]); } -const MediaSchema = z.object({ +const MediaObjectSchema = z.object({ altTag: z.string().optional(), cover: z.string().optional(), mimeType: z.nativeEnum(SupportedFileType), url: z.string(), }); +const MetadataImageSchema = z.object({ + mimeType: z.nativeEnum(ImageType), + url: z.string(), +}); + const AnyoneReferencePolicyConfigSchema = z.object({ type: z.literal(ReferencePolicyType.ANYONE), }); @@ -279,10 +284,12 @@ function createCommonPublicationRequestSchema { }); }); + it(`should support 'attributes' and 'image'`, () => { + const request = mockPublication({ + attributes: [dateNftAttribute, numberNftAttribute, stringNftAttribute], + image, + }); + + const metadata = createPublicationMetadata(request); + + expect(metadata).toMatchObject({ + attributes: [ + { + displayType: PublicationMetadataDisplayTypes[dateNftAttribute.displayType], + traitType: dateNftAttribute.traitType, + value: dateNftAttribute.value.toString(), + }, + { + displayType: PublicationMetadataDisplayTypes[numberNftAttribute.displayType], + traitType: numberNftAttribute.traitType, + value: numberNftAttribute.value.toString(), + }, + { + displayType: PublicationMetadataDisplayTypes[stringNftAttribute.displayType], + traitType: stringNftAttribute.traitType, + value: stringNftAttribute.value.toString(), + }, + ], + image: image.url, + imageMimeType: image.mimeType, + }); + }); + describe.each<{ description: string; request: CreatePostRequest | CreateCommentRequest; @@ -215,6 +248,20 @@ describe(`Given the ${createPublicationMetadata.name} helper`, () => { const metadata = createPublicationMetadata(request); + expect(metadata).toMatchObject({ + name: collectPolicyConfig.metadata.name, + description: collectPolicyConfig.metadata.description, + external_url: nftMetadata.externalUrl, + }); + }); + + it('should be backwards compatible with the deprecated `collect.metadata.attributes`, `collect.metadata.image`, and `collect.metadata.imageMimeType`', () => { + const request = mockPublication({ + collect: collectPolicyConfig, + }); + + const metadata = createPublicationMetadata(request); + expect(metadata).toMatchObject({ attributes: [ { @@ -233,9 +280,6 @@ describe(`Given the ${createPublicationMetadata.name} helper`, () => { value: stringNftAttribute.value.toString(), }, ], - name: collectPolicyConfig.metadata.name, - description: collectPolicyConfig.metadata.description, - external_url: nftMetadata.externalUrl, image: nftMetadata.image, imageMimeType: nftMetadata.imageMimeType, }); @@ -251,7 +295,6 @@ describe(`Given the ${createPublicationMetadata.name} helper`, () => { const metadata = createPublicationMetadata(request); expect(metadata).toMatchObject({ - attributes: [], name: 'none', // although "name" is not needed when a publication is not collectable, our Publication Metadata V2 schema requires it ¯\_(ツ)_/¯ }); }); diff --git a/packages/react/src/transactions/infrastructure/createPublicationMetadata.ts b/packages/react/src/transactions/infrastructure/createPublicationMetadata.ts index ff47da5ac7..3025428f12 100644 --- a/packages/react/src/transactions/infrastructure/createPublicationMetadata.ts +++ b/packages/react/src/transactions/infrastructure/createPublicationMetadata.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import { PublicationMetadata, PublicationMainFocus, @@ -12,8 +13,7 @@ import { CreateCommentRequest, CreatePostRequest, MediaObject, - NftAttribute, - NoCollectPolicyConfig, + MetadataAttribute, } from '@lens-protocol/domain/use-cases/publications'; import { assertNever, Overwrite } from '@lens-protocol/shared-kernel'; import { v4 } from 'uuid'; @@ -25,12 +25,7 @@ type CollectablePublicationRequest = Overwrite< { collect: CollectablePolicyConfig } >; -type NonCollectablePublicationRequest = Overwrite< - CreatePublicationRequest, - { collect: NoCollectPolicyConfig } ->; - -function mapMetaAttributes(attributes: NftAttribute[]) { +function mapMetadataAttributes(attributes: MetadataAttribute[]) { return attributes.map(({ displayType, value, ...rest }) => { return { ...rest, @@ -58,13 +53,25 @@ function essentialFields(request: CreatePublicationRequest) { } as const; } +function metadataImage(request: CreatePublicationRequest) { + if (!request.image) { + return undefined; + } + return { + image: request.image.url, + imageMimeType: request.image.mimeType, + } as const; +} + function optionalFields(request: CreatePublicationRequest) { return { + attributes: mapMetadataAttributes(request.attributes ?? []), ...(request.appId && { appId: request.appId }), ...(request.contentWarning && { contentWarning: PublicationContentWarning[request.contentWarning], }), ...(request.tags && { tags: request.tags }), + ...metadataImage(request), } as const; } @@ -94,42 +101,54 @@ function contentFields(request: CreatePublicationRequest) { } } -function resolveNonCollectableMetadata( - request: NonCollectablePublicationRequest, -): PublicationMetadata { +function resolveNonCollectableMetadata() { return { - ...essentialFields(request), - ...optionalFields(request), - ...contentFields(request), name: 'none', // although "name" is not needed when a publication is not collectable, Publication Metadata V2 schema requires it ¯\_(ツ)_/¯ - attributes: [], - }; + } as const; } -function resolveCollectableMetadata(request: CollectablePublicationRequest): PublicationMetadata { +function resolveDeprecatedCollectableMetadata(request: CollectablePublicationRequest) { + return { + attributes: mapMetadataAttributes(request.collect.metadata.attributes ?? []), + image: request.collect.metadata.image, + imageMimeType: request.collect.metadata.imageMimeType, + } as const; +} + +function resolveCollectableMetadata(request: CollectablePublicationRequest) { + if ('attributes' in request.collect.metadata && 'attributes' in request) { + console.warn( + 'Both "attributes" and "collect.metadata.attributes" are provided. Using "collect.metadata.attributes"', + ); + } + if ('image' in request.collect.metadata && 'image' in request) { + console.warn( + 'Both "image" and "collect.metadata.image" are provided. Using "collect.metadata.image"', + ); + } + + if ('imageMimeType' in request.collect.metadata && 'image' in request) { + console.warn( + 'Both "image" and "collect.metadata.imageMimeType" are provided. Using "collect.metadata.image"', + ); + } + return { - ...essentialFields(request), - ...optionalFields(request), - ...contentFields(request), - attributes: mapMetaAttributes(request.collect.metadata.attributes), description: request.collect.metadata.description, name: request.collect.metadata.name, + + ...resolveDeprecatedCollectableMetadata(request), + ...(request.collect.metadata.externalUrl && { external_url: request.collect.metadata.externalUrl, }), - ...(request.collect.metadata.image && { image: request.collect.metadata.image }), - ...(request.collect.metadata.imageMimeType && { - imageMimeType: request.collect.metadata.imageMimeType, - }), - }; + } as const; } -export function createPublicationMetadata( - request: CreatePostRequest | CreateCommentRequest, -): PublicationMetadata { +function resolveCollectMetadata(request: CreatePublicationRequest) { switch (request.collect.type) { case CollectPolicyType.NO_COLLECT: - return resolveNonCollectableMetadata(request as NonCollectablePublicationRequest); + return resolveNonCollectableMetadata(); case CollectPolicyType.CHARGE: case CollectPolicyType.FREE: @@ -139,3 +158,12 @@ export function createPublicationMetadata( assertNever(request.collect, `Unexpected collect policy type`); } } + +export function createPublicationMetadata(request: CreatePublicationRequest): PublicationMetadata { + return { + ...essentialFields(request), + ...optionalFields(request), + ...contentFields(request), + ...resolveCollectMetadata(request), + }; +} diff --git a/packages/react/src/transactions/useCreateComment.ts b/packages/react/src/transactions/useCreateComment.ts index 0453adf75e..9880648a65 100644 --- a/packages/react/src/transactions/useCreateComment.ts +++ b/packages/react/src/transactions/useCreateComment.ts @@ -14,6 +14,8 @@ import { ContentWarning, Locale, MediaObject, + MetadataAttribute, + MetadataImage, ReferencePolicyConfig, ReferencePolicyType, } from '@lens-protocol/domain/use-cases/publications'; @@ -75,6 +77,27 @@ export type CreateCommentBaseArgs = { * These are not the same as #hashtag in the publication content. Use these if you don't want to clutter the publication content with tags. */ tags?: string[]; + /** + * A list of attributes for the collect NFT. + * + * This is the NFT description visible on marketplaces like OpenSea. + */ + attributes?: MetadataAttribute[]; + /** + * The collect NFT image. + * + * This is the NFT image visible on marketplaces like OpenSea. + * + * DO NOT use this as primary storage for publication media. Use the `media` property instead. + * In the case your publication has many media consider to use this field as a static representation + * of the collect NFT. For example if the publication is an album of audio files this could be + * used as album cover image. If the publication is a gallery of images, this could be the gallery + * cover image. + * + * DO NOT use this as media cover image. + * For individual media cover image (e.g. the video thumbnail image) use the `media[n].cover` (see {@link MediaObject}). + */ + image?: MetadataImage; }; export type CreateTextualCommentArgs = CreateCommentBaseArgs & { diff --git a/packages/react/src/transactions/useCreatePost.ts b/packages/react/src/transactions/useCreatePost.ts index 58ff3a69bd..994408f800 100644 --- a/packages/react/src/transactions/useCreatePost.ts +++ b/packages/react/src/transactions/useCreatePost.ts @@ -14,6 +14,8 @@ import { CreatePostRequest, Locale, MediaObject, + MetadataAttribute, + MetadataImage, ReferencePolicyConfig, ReferencePolicyType, } from '@lens-protocol/domain/use-cases/publications'; @@ -79,6 +81,27 @@ export type CreatePostBaseArgs = { * These are not the same as #hashtag in the publication content. Use these if you don't want to clutter the publication content with tags. */ tags?: string[]; + /** + * A list of attributes for the collect NFT. + * + * This is the NFT description visible on marketplaces like OpenSea. + */ + attributes?: MetadataAttribute[]; + /** + * The collect NFT image. + * + * This is the NFT image visible on marketplaces like OpenSea. + * + * DO NOT use this as primary storage for publication media. Use the `media` property instead. + * In the case your publication has many media consider to use this field as a static representation + * of the collect NFT. For example if the publication is an album of audio files this could be + * used as album cover image. If the publication is a gallery of images, this could be the gallery + * cover image. + * + * DO NOT use this as media cover image. + * For individual media cover image (e.g. the video thumbnail image) use the `media[n].cover` (see {@link MediaObject}). + */ + image?: MetadataImage; }; export type CreateTextualPostArgs = CreatePostBaseArgs & {