diff --git a/.changeset/chilled-kids-tease.md b/.changeset/chilled-kids-tease.md new file mode 100644 index 00000000..09be9e49 --- /dev/null +++ b/.changeset/chilled-kids-tease.md @@ -0,0 +1,5 @@ +--- +"@mod-protocol/react-editor": patch +--- + +feat: expose tiptap editor config diff --git a/.changeset/fair-tigers-grab.md b/.changeset/fair-tigers-grab.md new file mode 100644 index 00000000..f2c56e01 --- /dev/null +++ b/.changeset/fair-tigers-grab.md @@ -0,0 +1,8 @@ +--- +"@mod-protocol/react-ui-shadcn": patch +"@miniapps/url-render": patch +"@mod-protocol/react": patch +"@mod-protocol/core": patch +--- + +feat: text component variants: `bold`, `secondary`, `regular` diff --git a/.changeset/modern-ways-juggle.md b/.changeset/modern-ways-juggle.md new file mode 100644 index 00000000..b440b748 --- /dev/null +++ b/.changeset/modern-ways-juggle.md @@ -0,0 +1,6 @@ +--- +"@mod-protocol/core": minor +"api": minor +--- + +chore: users mentioned in NFT metadata can either be an FID or FarcasterUser object depending on context diff --git a/.changeset/small-houses-tie.md b/.changeset/small-houses-tie.md new file mode 100644 index 00000000..723492a5 --- /dev/null +++ b/.changeset/small-houses-tie.md @@ -0,0 +1,5 @@ +--- +"@mod-protocol/react-editor": patch +--- + +fix: placeholder text diff --git a/docs/pages/integrate/creation.mdx b/docs/pages/integrate/creation.mdx index 0284d14a..0ecde562 100644 --- a/docs/pages/integrate/creation.mdx +++ b/docs/pages/integrate/creation.mdx @@ -1,4 +1,4 @@ -import Image from 'next/image' +import Image from "next/image"; # Creation Mods @@ -56,7 +56,14 @@ import { import { CreationMod } from "@mod-protocol/react"; import { useEditor, EditorContent } from "@mod-protocol/react-editor"; import { creationMods } from "@mod-protocol/mod-registry"; -import { Embed, ModManifest, fetchUrlMetadata, handleAddEmbed, handleOpenFile, handleSetInput } from "@mod-protocol/core"; +import { + Embed, + ModManifest, + fetchUrlMetadata, + handleAddEmbed, + handleOpenFile, + handleSetInput, +} from "@mod-protocol/core"; // UI implementation import { createRenderMentionsSuggestionConfig } from "@mod-protocol/react-ui-shadcn/dist/lib/mentions"; import { ModsSearch } from "@mod-protocol/react-ui-shadcn/dist/components/creation-mods-search"; @@ -64,7 +71,11 @@ import { CastLengthUIIndicator } from "@mod-protocol/react-ui-shadcn/dist/compon import { ChannelPicker } from "@mod-protocol/react-ui-shadcn/dist/components/channel-picker"; import { EmbedsEditor } from "@mod-protocol/react-ui-shadcn/dist/lib/embeds"; import { Button } from "@mod-protocol/react-ui-shadcn/dist/components/ui/button"; -import { Popover, PopoverContent, PopoverTrigger } from "@mod-protocol/react-ui-shadcn/dist/components/ui/popover"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@mod-protocol/react-ui-shadcn/dist/components/ui/popover"; import { renderers } from "@mod-protocol/react-ui-shadcn/dist/renderers"; // Optionally replace with your API_URL here if you want to self host by running your own instance of https://github.com/mod-protocol/mod/tree/main/examples/api @@ -112,8 +123,7 @@ export default function EditorExample() { }), }); - const [currentMod, setCurrentMod] = - React.useState(null); + const [currentMod, setCurrentMod] = React.useState(null); return (
@@ -138,15 +148,10 @@ export default function EditorExample() { }} > - +
-

- {currentMod?.name} -

+

{currentMod?.name}


['infura-ipfs-upload', 'livepeer-video'].includes(mod.slug)} + mods={creationMods.filter(mod => ['infura-ipfs-upload', 'livepeer-video'].includes(mod.slug))} onSelect={setCurrentMod} /> -``` \ No newline at end of file +``` diff --git a/examples/api/src/app/api/cast-embeds-metadata/by-url/route.ts b/examples/api/src/app/api/cast-embeds-metadata/by-url/route.ts index 5d7941dc..acb349dd 100644 --- a/examples/api/src/app/api/cast-embeds-metadata/by-url/route.ts +++ b/examples/api/src/app/api/cast-embeds-metadata/by-url/route.ts @@ -51,6 +51,7 @@ export async function POST(request: NextRequest) { "nft_collections.name as collection_name", "nft_collections.open_sea_url as collection_open_sea_url", "nft_collections.owner_count as collection_owner_count", + "nft_collections.creator_fid as collection_creator_fid", // NFT metadata "nft_metadata.token_id as nft_token_id", diff --git a/examples/api/src/app/api/cast-embeds-metadata/route.ts b/examples/api/src/app/api/cast-embeds-metadata/route.ts index 62ea20c8..5149d5a3 100644 --- a/examples/api/src/app/api/cast-embeds-metadata/route.ts +++ b/examples/api/src/app/api/cast-embeds-metadata/route.ts @@ -41,7 +41,9 @@ function formatRow(row): EmbedWithCastHash { name: row.collection_name, openSeaUrl: row.collection_open_sea_url || undefined, ownerCount: row.collection_owner_count || undefined, - creator: undefined, // TODO: Look up farcaster user by FID + creator: row.collection_creator_fid + ? { fid: row.collection_creator_fid } + : undefined, }, }; } @@ -132,6 +134,7 @@ export async function POST(request: NextRequest) { "nft_collections.name as collection_name", "nft_collections.open_sea_url as collection_open_sea_url", "nft_collections.owner_count as collection_owner_count", + "nft_collections.creator_fid as collection_creator_fid", // NFT metadata "nft_metadata.token_id as nft_token_id", diff --git a/examples/api/src/app/api/open-graph/lib/url-handlers/local-fetch.ts b/examples/api/src/app/api/open-graph/lib/url-handlers/local-fetch.ts index 3f2fe8b8..244b263a 100644 --- a/examples/api/src/app/api/open-graph/lib/url-handlers/local-fetch.ts +++ b/examples/api/src/app/api/open-graph/lib/url-handlers/local-fetch.ts @@ -125,6 +125,11 @@ async function localFetchHandler(url: string): Promise { return null; } + if (!(urlMetadata.image || urlMetadata.logo)) { + console.error(`[Local Fetch] No image or logo found for ${url}`); + return null; + } + return urlMetadata; } diff --git a/examples/api/src/app/api/open-graph/lib/util.ts b/examples/api/src/app/api/open-graph/lib/util.ts index 956c0891..01a19218 100644 --- a/examples/api/src/app/api/open-graph/lib/util.ts +++ b/examples/api/src/app/api/open-graph/lib/util.ts @@ -35,15 +35,18 @@ async function fetchUserData(address: string): Promise { } export function toUrlMetadata(nftMetadata: NFTMetadata): UrlMetadata { + // If the creator is an object, it's a FarcasterUser + const creator = nftMetadata?.collection?.creator; + return { image: nftMetadata.mediaUrl ? { url: nftMetadata.mediaUrl } : undefined, description: nftMetadata.collection.description, alt: nftMetadata.collection.name, title: nftMetadata.collection.name, - publisher: nftMetadata.collection.creator?.displayName, - logo: nftMetadata.collection.creator + publisher: creator?.displayName, + logo: creator ? { - url: nftMetadata.collection.creator.pfp.url, + url: creator.pfp.url, } : undefined, nft: nftMetadata, diff --git a/examples/nextjs-shadcn/src/app/editor-example.tsx b/examples/nextjs-shadcn/src/app/editor-example.tsx index 2e624bc9..0906d3d2 100644 --- a/examples/nextjs-shadcn/src/app/editor-example.tsx +++ b/examples/nextjs-shadcn/src/app/editor-example.tsx @@ -11,9 +11,9 @@ import { getFarcasterMentions, getMentionFidsByUsernames, } from "@mod-protocol/farcaster"; -import { CreationMod } from "@mod-protocol/react"; +import { CreationMod, RichEmbed } from "@mod-protocol/react"; import { useEditor, EditorContent } from "@mod-protocol/react-editor"; -import { creationMods } from "@mod-protocol/mod-registry"; +import { creationMods, defaultRichEmbedMod } from "@mod-protocol/mod-registry"; import { Embed, EthPersonalSignActionResolverInit, @@ -164,7 +164,19 @@ export default function EditorExample() { autoFocus className="w-full h-full min-h-[200px]" /> - + ( + + )} + />
; tokenId?: string; mediaUrl?: string; mintUrl?: string; @@ -47,7 +47,7 @@ export type NFTMetadata = { description?: string; imageUrl?: string; openSeaUrl?: string; - creator?: FarcasterUser; + creator?: Partial; }; }; diff --git a/packages/core/src/manifest.ts b/packages/core/src/manifest.ts index cedd013a..3e47b32c 100644 --- a/packages/core/src/manifest.ts +++ b/packages/core/src/manifest.ts @@ -181,6 +181,7 @@ export type ModElement = | { type: "text"; label: string; + variant?: "bold" | "secondary" | "regular"; } | { type: "image"; diff --git a/packages/core/src/renderer.ts b/packages/core/src/renderer.ts index b7f7760a..743decbd 100644 --- a/packages/core/src/renderer.ts +++ b/packages/core/src/renderer.ts @@ -21,6 +21,7 @@ export type ModElementRef = | { type: "text"; label: string; + variant?: "bold" | "secondary" | "regular"; events?: undefined; } | { @@ -157,25 +158,26 @@ export type ModElementRef = }; }; -export type CreationContext = { - input: any; +export type BaseContext = { user?: { wallet?: { - address: string; + address?: string; + }; + farcaster?: { + fid?: string; }; }; +}; + +export type CreationContext = BaseContext & { + input: any; embeds: Embed[]; /** The url of the api hosting the mod backends. (including /api) **/ api: string; }; -// Render Mods only are triggered by a single embed right now -export type RichEmbedContext = { - user?: { - wallet?: { - address: string; - }; - }; +// Render Mini-apps only are triggered by a single embed right now +export type RichEmbedContext = BaseContext & { embed: Embed; api: string; }; @@ -1114,6 +1116,7 @@ export class Renderer { { type: "text", label: this.replaceInlineContext(el.label), + variant: el.variant, }, key ); diff --git a/packages/mod-registry/src/index.ts b/packages/mod-registry/src/index.ts index 57f32541..a9d20260 100644 --- a/packages/mod-registry/src/index.ts +++ b/packages/mod-registry/src/index.ts @@ -9,20 +9,22 @@ import NFTMinter from "@mods/nft-minter"; import UrlRender from "@mods/url-render"; import ImageRender from "@mods/image-render"; import ChatGPTShorten from "@mods/chatgpt-shorten"; -// import ZoraNftMinter from "@mods/zora-nft-minter"; -// import DALLE from "@mods/dall-e"; +import ZoraNftMinter from "@mods/zora-nft-minter"; +import ImgurUpload from "@mods/imgur-upload"; +import DALLE from "@mods/dall-e"; + +/** All - Stable, suitable for use */ export const allMods = [ + ImgurUpload, InfuraIPFSUpload, LivepeerVideo, GiphyPicker, VideoRender, - // ZoraNftMinter, NFTMinter, ImageRender, ChatGPTShorten, ChatGPT, - // DALLE, ]; export const creationMods: ModManifest[] = allMods.filter( @@ -35,5 +37,34 @@ export const richEmbedMods: ModManifest[] = allMods.filter( manifest.richEmbedEntrypoints && manifest.richEmbedEntrypoints.length !== 0 ); +/** All + Experimental - Potentially unstable, unsuitable for production use */ + +export const allModsExperimental = [ + ImgurUpload, + InfuraIPFSUpload, + LivepeerVideo, + GiphyPicker, + VideoRender, + ZoraNftMinter, + NFTMinter, + ImageRender, + ChatGPTShorten, + ChatGPT, + DALLE, +]; + +export const creationModsExperimental: ModManifest[] = + allModsExperimental.filter( + (manifest) => + manifest.creationEntrypoints && manifest.creationEntrypoints.length !== 0 + ); + +export const richEmbedModsExperimental: ModManifest[] = + allModsExperimental.filter( + (manifest) => + manifest.richEmbedEntrypoints && + manifest.richEmbedEntrypoints.length !== 0 + ); + /** When no renderMod matches an embed, this one will be used **/ export const defaultRichEmbedMod: ModManifest = UrlRender; diff --git a/packages/react-editor/src/create-editor-config.tsx b/packages/react-editor/src/create-editor-config.tsx index f552f081..ca27bb97 100644 --- a/packages/react-editor/src/create-editor-config.tsx +++ b/packages/react-editor/src/create-editor-config.tsx @@ -10,13 +10,14 @@ import { clipboardTextParser, transformPastedHTML, } from "./extension-clipboard"; +import { EditorOptions } from "@tiptap/core"; export type EditorConfig = { placeholderText: string; - onAddLink: (link: Link) => void; renderMentionsSuggestionConfig: Pick; linkClassName: string; + editorOptions?: Partial; }; export function createEditorConfig({ @@ -24,12 +25,12 @@ export function createEditorConfig({ renderMentionsSuggestionConfig, linkClassName, onAddLink, -}: EditorConfig) { + editorOptions = {}, +}: EditorConfig): Partial { return { editorProps: { attributes: { // min-height allows clicking in the box and creating focus on the input - // TODO: configurable/options style: "outline: 0; min-height: 200px;", }, // attributes: { @@ -39,6 +40,7 @@ export function createEditorConfig({ transformPastedHTML: transformPastedHTML, // Prevents weird paste new line inconsistencies when coming from textedit, plaintext, ... clipboardTextParser: clipboardTextParser, + ...editorOptions.editorProps, }, extensions: [ // you can find pro extensions at https://embed-pro.tiptap.dev/preview/Nodes/Emoji?inline=false&hideSource=true @@ -49,6 +51,9 @@ export function createEditorConfig({ TipTapParagraph, TipTapPlaceholder.configure({ placeholder: placeholderText, + // https://github.com/ueberdosis/tiptap/issues/2659#issuecomment-1230954941 + emptyEditorClass: + "cursor-text before:content-[attr(data-placeholder)] before:absolute before:text-foreground-secondary before:opacity-50 before-pointer-events-none", }), // TipTapCustomImagePlugin, diff --git a/packages/react-editor/src/use-editor.tsx b/packages/react-editor/src/use-editor.tsx index 21f4d05b..8ef9a898 100644 --- a/packages/react-editor/src/use-editor.tsx +++ b/packages/react-editor/src/use-editor.tsx @@ -72,6 +72,7 @@ export function useEditor({ linkClassName, renderMentionsSuggestionConfig, maxEmbeds = FARCASTER_MAX_EMBEDS, + editorOptions, }: useEditorParameter): useEditorReturn { const [embeds, setEmbeds] = useState([]); @@ -184,8 +185,15 @@ export function useEditor({ placeholderText, onAddLink, renderMentionsSuggestionConfig, + editorOptions, }), - [linkClassName, placeholderText, renderMentionsSuggestionConfig, onAddLink] + [ + linkClassName, + placeholderText, + renderMentionsSuggestionConfig, + onAddLink, + editorOptions, + ] ); const editor: Editor | null = useTipTapEditor(editorConfig); useEffect(() => { diff --git a/packages/react-ui-shadcn/package.json b/packages/react-ui-shadcn/package.json index abc529f6..a5e3559a 100644 --- a/packages/react-ui-shadcn/package.json +++ b/packages/react-ui-shadcn/package.json @@ -8,10 +8,11 @@ "dev": "npm run build -- --watch" }, "dependencies": { - "@mod-protocol/core": "*", - "@mod-protocol/farcaster": "*", - "@mod-protocol/react": "*", - "@mod-protocol/react-editor": "*", + "@mod-protocol/core": "^0.0.2", + "@mod-protocol/farcaster": "^0.0.1", + "@mod-protocol/react": "^0.0.2", + "@mod-protocol/react-editor": "^0.0.2", + "@mod-protocol/mod-registry": "*", "@primer/octicons-react": "^19.5.0", "@radix-ui/react-aspect-ratio": "^1.0.3", "@radix-ui/react-avatar": "^1.0.4", diff --git a/packages/react-ui-shadcn/src/lib/embeds.tsx b/packages/react-ui-shadcn/src/lib/embeds.tsx index 71c192a8..bf90198b 100644 --- a/packages/react-ui-shadcn/src/lib/embeds.tsx +++ b/packages/react-ui-shadcn/src/lib/embeds.tsx @@ -10,26 +10,25 @@ import { Button } from "components/ui/button"; import { Skeleton } from "components/ui/skeleton"; import { VideoRenderer } from "../renderers/video"; -function shorten(str?: string, maxLength = 64) { - if (!str) return str; - if (str.length > maxLength) return str.slice(0, maxLength - 3) + "..."; - return str; -} - -export const EmbedsEditor = (props: { +export const EmbedsEditor = ({ + embeds, + setEmbeds, + RichEmbed, +}: { embeds: Embed[]; setEmbeds: (e: Embed[]) => void; + RichEmbed: (props: { embed: Embed }) => JSX.Element; }) => { return ( <> - {props.embeds.map((embed, i) => ( + {embeds.map((embed, i) => (
))} diff --git a/packages/react-ui-shadcn/src/renderers/card.tsx b/packages/react-ui-shadcn/src/renderers/card.tsx index 8675e648..57b46ae2 100644 --- a/packages/react-ui-shadcn/src/renderers/card.tsx +++ b/packages/react-ui-shadcn/src/renderers/card.tsx @@ -17,15 +17,17 @@ export const CardRenderer = ( onClick, } = props; return ( -
+
{ + // Prevent double event firing when clicking child components + e.stopPropagation(); + onClick(); + }} + > {imageSrc ? (
- Image {topLeftBadge ? ( {topLeftBadge} ) : null} @@ -44,6 +46,11 @@ export const CardRenderer = ( {bottomRightBadge} ) : null} + Image
) : null} diff --git a/packages/react-ui-shadcn/src/renderers/container.tsx b/packages/react-ui-shadcn/src/renderers/container.tsx index b29d2f3f..5728980a 100644 --- a/packages/react-ui-shadcn/src/renderers/container.tsx +++ b/packages/react-ui-shadcn/src/renderers/container.tsx @@ -5,7 +5,7 @@ export const ContainerRenderer = ( props: React.ComponentProps ) => { return ( -
+
{props.children}
); diff --git a/packages/react-ui-shadcn/src/renderers/horizontal-layout.tsx b/packages/react-ui-shadcn/src/renderers/horizontal-layout.tsx index 7f2b1635..8e305fd4 100644 --- a/packages/react-ui-shadcn/src/renderers/horizontal-layout.tsx +++ b/packages/react-ui-shadcn/src/renderers/horizontal-layout.tsx @@ -5,7 +5,7 @@ export const HorizontalLayoutRenderer = ( props: React.ComponentProps ) => { return ( -
+
{props.children}
); diff --git a/packages/react-ui-shadcn/src/renderers/text.tsx b/packages/react-ui-shadcn/src/renderers/text.tsx index 562b4730..6e0f5d21 100644 --- a/packages/react-ui-shadcn/src/renderers/text.tsx +++ b/packages/react-ui-shadcn/src/renderers/text.tsx @@ -1,9 +1,25 @@ import React from "react"; import { Renderers } from "@mod-protocol/react"; +import { cva } from "class-variance-authority"; +import { cn } from "../lib/utils"; + +const textVariants = cva("my-0 flex-1 text-sm", { + variants: { + variant: { + bold: "font-bold", + regular: "", + secondary: "text-muted-foreground", + }, + }, + defaultVariants: { + variant: "regular", + }, +}); export const TextRenderer = ( props: React.ComponentProps ) => { - const { label } = props; - return

{label}

; + const { label, variant = "regular" } = props; + + return

{label}

; }; diff --git a/packages/react/src/index.tsx b/packages/react/src/index.tsx index 02aab639..c4437d10 100644 --- a/packages/react/src/index.tsx +++ b/packages/react/src/index.tsx @@ -46,7 +46,10 @@ export type Renderers = { onChange: (value: string) => void; onPick: (value: any) => void; }>; - Text: React.ComponentType<{ label: string }>; + Text: React.ComponentType<{ + label: string; + variant?: "bold" | "regular" | "secondary"; + }>; Link: React.ComponentType<{ label: string; url: string; diff --git a/packages/react/src/render-embed.tsx b/packages/react/src/render-embed.tsx index 494f2168..a5fa4ce1 100644 --- a/packages/react/src/render-embed.tsx +++ b/packages/react/src/render-embed.tsx @@ -1,14 +1,12 @@ "use client"; -import React from "react"; import { - RichEmbedContext, - Embed, ModManifest, + RichEmbedContext, canRenderEntrypointWithContext, } from "@mod-protocol/core"; -import { ResolverTypes, RenderMod, Renderers } from "."; +import { RenderMod, Renderers, ResolverTypes } from "."; type Props = RichEmbedContext & { renderers: Renderers; @@ -61,16 +59,15 @@ export function RichEmbed(props: Props) { return getMatchingMods([props.defaultRichEmbedMod]); }); - return matchingMods.length - ? matchingMods.map((mod, index) => ( - - )) - : null; + const mod = matchingMods[0]; + + return mod ? ( + + ) : null; } diff --git a/tsconfig.json b/tsconfig.json index 98527d65..9bb22b39 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,11 +9,10 @@ "esModuleInterop": true, "preserveSymlinks": true, "incremental": true, + "composite": true, "declarationMap": true, "paths": { - "@mods/*": [ - "./mods/*" - ], + "@mods/*": ["./mods/*"] }, "jsx": "react-jsx", "module": "ESNext", @@ -31,22 +30,14 @@ "sourceMap": false, "strictNullChecks": true, "target": "es2019", - "types": [ - "node" - ], - "lib": [ - "es2021", - "dom", - "esnext" - ], + "types": ["node"], + "lib": ["es2021", "dom", "esnext"], "strict": true, "noFallthroughCasesInSwitch": true, "noUncheckedIndexedAccess": true }, - "exclude": [ - "_" - ], + "exclude": ["_"], "typeAcquisition": { "enable": true } -} \ No newline at end of file +}