Farcaster Editor
A fully featured, performant, and open source Cast creator for your React app.
Why should you use this and not build your own cast creator?
- We've spent over 100 hours on this open source library to get it just right for your Farcaster app.
- It supports: @ mentions, channels, link highlighting & automatic embeds, image, url and video embeds, max lengths in bytes, open graph previews, textcuts
- You can easily use Mods that allow users to add Rich Embeds like image uploads (to imgur or IFPS), video uploads or choosing a GIF.
- It's open source (opens in a new tab), MIT licensed and designed to put you in full control - you choose which if any Mods you want to use with it.
- You can and are free to use this even if you aren't integrating Mod - it doesn't depend on any of Mod's core SDKs.
- It's lightweight, treeshakeable and performant, built on tiptap and prosemirror. It's extendable with your own Mods or tiptap plugins if you'd like.
- It's unopinionated about UI and works with your UI library
Step 1A: Simple Example (no image upload or other Mods)
import { useEditor, EditorContent } from "@mod-protocol/react-editor";
import { EmbedsEditor } from "@mod-protocol/react-ui-shadcn/dist/lib/embeds";
import { Embed, ModManifest, fetchUrlMetadata, handleAddEmbed, handleOpenFile, handleSetInput } from "@mod-protocol/core";
import { Channel, getFarcasterChannels, getFarcasterMentions } from "@mod-protocol/farcaster";
import { MentionList } from "@mod-protocol/react-ui-shadcn/dist/components/mention-list";
import { ChannelList } from "@mod-protocol/react-ui-shadcn/dist/components/channel-list";
import { ChannelPicker } from "@mod-protocol/react-ui-shadcn/dist/components/channel-picker";
import { CastLengthUIIndicator } from "@mod-protocol/react-ui-shadcn/dist/components/cast-length-ui-indicator";
import { RichEmbed } from "@mod-protocol/react";
import { defaultRichEmbedMod } from "@mod-protocol/mod-registry";
// Optionally replace with your API_URL here
const API_URL =
process.env.NEXT_PUBLIC_API_URL ?? "https://api.modprotocol.org/api";
// use our API_URL, self host our API, or use your own API.
const getUrlMetadata = fetchUrlMetadata(API_URL);
const getResults = getFarcasterMentions(API_URL);
const getChannels = getFarcasterChannels(API_URL);
export default function EditorExample() {
const { editor, getEmbeds, setEmbeds, handleSubmit, getText, setChannel, getChannel } = useEditor({
fetchUrlMetadata: getUrlMetadata,
onError: (error) => console.error(error),
onSubmit: (cast) => console.log(cast), // submit to your hub
linkClassName: "text-blue-600",
renderChannelsSuggestionConfig: createRenderMentionsSuggestionConfig({
getResults: (query) => getChannels(query, true),
RenderList: ChannelList,
}),
renderMentionsSuggestionConfig: createRenderMentionsSuggestionConfig({
getResults: getResults,
RenderList: MentionList,
}),
});
return (
<>
<EditorContent
editor={editor}
autoFocus
className="w-full h-full min-h-[200px]"
/>
<EmbedsEditor embeds={getEmbeds()} setEmbeds={setEmbeds} RichEmbed={({ embed }) => (
<RichEmbed
api={API_URL}
defaultRichEmbedMod={defaultRichEmbedMod}
mods={[defaultRichEmbedMod]}
embed={embed}
renderers={renderers}
/>
)} />
<ChannelPicker
getChannels={getChannels}
onSelect={setChannel}
value={getChannel()}
/>
<CastLengthUIIndicator getText={getText} />
<Button type="submit">Cast</Button>
</>
);
Step 1B: Full Example Usage (next.js)
"use client";
import * as React from "react";
import { getAddress } from "viem";
import {
Channel,
formatPlaintextToHubCastMessage,
getFarcasterChannels,
getFarcasterMentions,
getMentionFidsByUsernames,
} from "@mod-protocol/farcaster";
import { CreationMod, RichEmbed } from "@mod-protocol/react";
import { useEditor, EditorContent } from "@mod-protocol/react-editor";
import {
creationMods,
defaultRichEmbedMod,
creationModsExperimental,
} from "@mod-protocol/mod-registry";
import {
Embed,
EthPersonalSignActionResolverInit,
ModManifest,
fetchUrlMetadata,
handleAddEmbed,
handleOpenFile,
handleSetInput,
} from "@mod-protocol/core";
import { SiweMessage } from "siwe";
import { useAccount, useSignMessage } from "wagmi";
// 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";
import { CastLengthUIIndicator } from "@mod-protocol/react-ui-shadcn/dist/components/cast-length-ui-indicator";
import { ChannelPicker } from "@mod-protocol/react-ui-shadcn/dist/components/channel-picker";
import { EmbedsEditor } from "@mod-protocol/react-ui-shadcn/dist/lib/embeds";
import { MentionList } from "@mod-protocol/react-ui-shadcn/dist/components/mention-list";
import { ChannelList } from "@mod-protocol/react-ui-shadcn/dist/components/channel-list";
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 { renderers } from "@mod-protocol/react-ui-shadcn/dist/renderers";
import { useExperimentalMods } from "./use-experimental-mods";
// Optionally replace with your API_URL here
const API_URL =
process.env.NEXT_PUBLIC_API_URL ?? "https://api.modprotocol.org/api";
const getMentions = getFarcasterMentions(API_URL);
const getChannels = getFarcasterChannels(API_URL);
const getMentionFids = getMentionFidsByUsernames(API_URL);
const getUrlMetadata = fetchUrlMetadata(API_URL);
const onError = (err) => console.error(err.message);
const onSubmit = async ({
text,
embeds,
channel,
}: {
text: string;
embeds: Embed[];
channel: Channel;
}) => {
const formattedCast = await formatPlaintextToHubCastMessage({
text,
embeds,
parentUrl: channel.parent_url,
getMentionFidsByUsernames: getMentionFids,
});
window.alert(
`This is a demo, and doesn't do anything.\n\nCast text:\n${text}\nEmbeds:\n${embeds
.map((embed) => (embed as any).url)
.join(", ")}\nChannel:\n${channel.name}`
);
console.log(formattedCast);
// TODO submit the cast to a hub
return true;
};
export default function EditorExample() {
const experimentalMods = useExperimentalMods();
const {
editor,
getText,
getEmbeds,
setEmbeds,
setText,
setChannel,
getChannel,
addEmbed,
handleSubmit,
} = useEditor({
fetchUrlMetadata: getUrlMetadata,
onError,
onSubmit,
linkClassName: "text-blue-600",
renderChannelsSuggestionConfig: createRenderMentionsSuggestionConfig({
getResults: (query) => getChannels(query, true),
RenderList: ChannelList,
}),
renderMentionsSuggestionConfig: createRenderMentionsSuggestionConfig({
getResults: getMentions,
RenderList: MentionList,
}),
});
const { address: unchecksummedAddress } = useAccount();
const checksummedAddress = React.useMemo(() => {
if (!unchecksummedAddress) return null;
return getAddress(unchecksummedAddress);
}, [unchecksummedAddress]);
const { signMessageAsync } = useSignMessage();
const getAuthSig = React.useCallback(
async (
{
data: { statement, version, chainId },
}: EthPersonalSignActionResolverInit,
{ onSuccess, onError }
): Promise<void> => {
if (!checksummedAddress) {
window.alert("please connect your wallet");
return;
}
try {
const siweMessage = new SiweMessage({
domain: process.env.NEXT_PUBLIC_HOST,
address: checksummedAddress,
statement,
uri: process.env.NEXT_PUBLIC_URL,
version,
chainId: Number(chainId),
});
const messageToSign = siweMessage.prepareMessage();
// Sign the message and format the authSig
const signature = await signMessageAsync({ message: messageToSign });
const authSig = {
signature,
// derivedVia: "web3.eth.personal.sign",
signedMessage: messageToSign,
address: checksummedAddress,
};
onSuccess(authSig);
} catch (err) {
console.error(err);
onError(err);
}
},
[signMessageAsync, checksummedAddress]
);
const [currentMod, setCurrentMod] = React.useState<ModManifest | null>(null);
const user = React.useMemo(() => {
return {
wallet: {
address: checksummedAddress,
},
};
}, [checksummedAddress]);
return (
<form onSubmit={handleSubmit}>
<div className="p-2 border border-input rounded-md">
<EditorContent
editor={editor}
autoFocus
className="w-full h-full min-h-[200px]"
/>
<EmbedsEditor
embeds={getEmbeds()}
setEmbeds={setEmbeds}
RichEmbed={({ embed }) => (
<RichEmbed
api={API_URL}
defaultRichEmbedMod={defaultRichEmbedMod}
mods={[defaultRichEmbedMod]}
embed={embed}
renderers={renderers}
/>
)}
/>
</div>
<div className="flex flex-row pt-2 gap-1">
<ChannelPicker
getChannels={getChannels}
onSelect={setChannel}
value={getChannel()}
/>
<Popover
open={!!currentMod}
onOpenChange={(op: boolean) => {
if (!op) setCurrentMod(null);
}}
>
<PopoverTrigger></PopoverTrigger>
<ModsSearch
mods={experimentalMods ? creationModsExperimental : creationMods}
onSelect={setCurrentMod}
/>
<PopoverContent className="w-[400px] ml-2" align="start">
<div className="space-y-4">
<h4 className="font-medium leading-none">{currentMod?.name}</h4>
<hr />
<CreationMod
input={getText()}
embeds={getEmbeds()}
api={API_URL}
user={user}
variant="creation"
manifest={currentMod}
renderers={renderers}
onOpenFileAction={handleOpenFile}
onExitAction={() => setCurrentMod(null)}
onSetInputAction={handleSetInput(setText)}
onAddEmbedAction={handleAddEmbed(addEmbed)}
onEthPersonalSignAction={getAuthSig}
/>
</div>
</PopoverContent>
</Popover>
{/* CastLengthUIIndicator only shows text when the user is low on characters */}
<CastLengthUIIndicator getText={getText} />
<div className="grow"></div>
<button type="submit">Cast</button>
</div>
</form>
);
}
Step 2: Styling setup
If you're using @mod-protocol/react-ui-shadcn
, follow the configuration steps for @mod-protocol/react-ui-shadcn