Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: action mods (+ tipping example) #151

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/brown-baboons-complain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@mod-protocol/react-ui-shadcn": patch
---

feat: add support for action mod types
5 changes: 5 additions & 0 deletions .changeset/little-suits-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@mod-protocol/mod-registry": minor
---

feat: add `tip-eth` mod
5 changes: 5 additions & 0 deletions .changeset/tricky-hairs-confess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@mod-protocol/core": patch
---

feat: add action mod type
8 changes: 5 additions & 3 deletions docs/pages/create-mod/getting-started.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Getting started making a Mod

A Mod consists of a manifest JSON file and optionally a backend which can handle requests from the Mod and make external requests.
A Mod consists of a manifest JSON file and optionally a backend which can handle requests from the Mod and make external requests.
The Manifest contains metadata about the Mod, such as it's name and unique identifier, which Mod Elements it renders onto the page, and conditions under which to render them.

Our convention is to write Mods in TypeScript, for better autocompletion and legibility, with the Mods being built to JSON.
Expand All @@ -26,8 +26,10 @@ There's a couple reasons, depending on what your goals are:

## Types of Mods

There's currently two types of Mods supported:
There's currently three types of Mods supported:

1. Rich-embed Mods
2. Creation Mods
3. Action Mods

We're planning to support more types of Mods in the near future, including Action Mods and Full screen Mods.
We're planning to support more types of Mods in the near future.
43 changes: 23 additions & 20 deletions docs/pages/create-mod/reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ type ModManifest = {
creationEntrypoints?: ModElement[];
/** Interface this Mod exposes, if any, for Content Rendering */
richEmbedEntrypoints?: ModConditionalElement[];
/** Interface this Mod exposes, if any, for Action Execution */
actionEntrypoints?: ModElement[];
/** A definition map of reusable elements, using their id as the key */
elements?: Record<string, ModElement[]>;
/** Permissions requested by the Mod */
Expand All @@ -52,6 +54,7 @@ export type ModElement =
| {
type: "text";
label: string;
variant?: "bold" | "secondary" | "regular";
}
| {
type: "image";
Expand All @@ -60,13 +63,14 @@ export type ModElement =
| {
type: "link";
label: string;
onclick?: ModEvent;
variant?: "link" | "primary" | "secondary" | "destructive";
url: string;
onclick?: ModEvent;
}
| {
type: "button";
label: string;
loadingLabel?: string;
variant?: "primary" | "secondary" | "destructive";
onclick: ModEvent;
}
Expand All @@ -84,35 +88,23 @@ export type ModElement =
onload?: ModEvent;
}
| {
type: "textarea";
ref?: string;
type: "select";
options: Array<{ label: string; value: any }>;
placeholder?: string;
isClearable?: boolean;
onchange?: ModEvent;
onsubmit?: ModEvent;
}
| {
type: "combobox";
ref?: string;
isClearable?: boolean;
placeholder?: string;
optionsRef?: string;
valueRef?: string;
onload?: ModEvent;
onpick?: ModEvent;
onchange?: ModEvent;
}
| {
type: "select";
options: Array<{ label: string; value: any }>;
ref?: string;
type: "textarea";
placeholder?: string;
isClearable?: boolean;
onchange?: ModEvent;
onsubmit?: ModEvent;
}
| {
ref?: string;
type: "input";
ref?: string;
placeholder?: string;
isClearable?: boolean;
onchange?: ModEvent;
Expand All @@ -123,16 +115,27 @@ export type ModElement =
videoSrc: string;
}
| {
ref?: string;
type: "tabs";
ref?: string;
values: string[];
names: string[];
onload?: ModEvent;
onchange?: ModEvent;
}
| ({
| {
type: "combobox";
ref?: string;
isClearable?: boolean;
placeholder?: string;
optionsRef?: string;
valueRef?: string;
onload?: ModEvent;
onpick?: ModEvent;
onchange?: ModEvent;
}
| ({
type: "image-grid-list";
ref?: string;
onload?: ModEvent;
onpick?: ModEvent;
} & (
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/integrate/creation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { CreationMod } from "@mod-protocol/react";
/>
```

if you want to support the user choosing between Mods, you can give them a UI to select a Mod, or use our component.
If you want to support the user choosing between Mods, you can give them a UI to select a Mod, or use our component.

## Full example with the Mod Editor

Expand Down
6 changes: 4 additions & 2 deletions docs/pages/integrate/getting-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,20 @@ You can integrate as much or as little of Mod as you'd like, as Mod supports pro

You also have free choice of which Mods you want to support in your App. You can pick and choose from our open source ones, or even build and bring your own.

There are two kinds of Mods currently:
There are three kinds of Mods currently:

1. [Creation Mods](creation.mdx): Enable users to use Mods when creating a post, such as Mods for adding Gifs, Images, Videos, Polls or using AI.
You can integrate these with or without the Mod Editor, which is a great Farcaster cast creator with batteries included.
2. [Rich-embed Mods](rich-embeds.mdx): Turn urls into rich embeds, with a fallback to an open graph style card embed. These enable Images, Videos, Polls, Games, Minting NFTs,
or any other mini-interaction to happen directly in the interface.
You can integrate these with or without the Mod Metadata Cache.
3. [Post Action Mods](post-actions.mdx): Enable users to use Mods when interacting with posts, such as Mods for tipping, sharing, traslating or other mini-interactions that require the context of the post such as its author and contents.
You can integrate these with or without the Mod Metadata Cache.

## Support

Mod currently only has SDKs for React, and we're working on adding React-native support.

## Boilerplate starter

Fork the [Mod-starter repo](https://github.com/mod-protocol/mod-starter) to get started
Fork the [Mod-starter repo](https://github.com/mod-protocol/mod-starter) to get started
153 changes: 153 additions & 0 deletions docs/pages/integrate/post-actions.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import Image from "next/image";

# Post Action Mods

Post Action Mods enable users to use Mods when interacting with posts, such as Mods for tipping, sharing, traslating or other mini-interactions that require the context of the post such as its author and contents.

Here's an example of where post action Mods will typically be used:

<Image
src="/post-action.png"
alt="Post Action Mod example"
width="400"
height="200"
/>

You can integrate Post Action Mods with or without our [Mod Metadata Cache](../metadata-cache.mdx).

## Integration Example

```tsx
import { ActionMod } from "@mod-protocol/react";

...

<ActionMod
input={text}
embeds={embeds}
api={API_URL}
variant="action"
manifest={currentMod}
renderers={renderers}
onOpenFileAction={handleOpenFile}
onExitAction={hideCurrentMod}
/>
```

## Full Example with Mod Search

```tsx
import { Embed, ModManifest, handleOpenFile } from "@mod-protocol/core";
import { actionMods, actionModsExperimental } from "@mod-protocol/mod-registry";
import { ActionMod } from "@mod-protocol/react";
import { ModsSearch } from "@mod-protocol/react-ui-shadcn/dist/components/creation-mods-search";
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 { KebabHorizontalIcon } from "@primer/octicons-react";
import React, { useMemo } from "react";
import { getAddress } from "viem";
import { useAccount } from "wagmi";
import { API_URL } from "./constants";
import { useExperimentalMods } from "./use-experimental-mods";
import { sendEthTransaction } from "./utils";

export function Actions({
author,
post,
}: {
author: {
farcaster: {
fid: string;
};
};
post: {
id: string;
text: string;
embeds: Embed[];
};
}) {
const experimentalMods = useExperimentalMods();
const [currentMod, setCurrentMod] = React.useState<ModManifest | null>(null);

const { address: unchecksummedAddress } = useAccount();
const checksummedAddress = React.useMemo(() => {
if (!unchecksummedAddress) return null;
return getAddress(unchecksummedAddress);
}, [unchecksummedAddress]);
const user = React.useMemo(() => {
return {
wallet: {
address: checksummedAddress,
},
};
}, [checksummedAddress]);

const onSendEthTransactionAction = useMemo(() => sendEthTransaction, []);

return (
<Popover
open={!!currentMod}
onOpenChange={(op: boolean) => {
if (!op) setCurrentMod(null);
}}
>
<PopoverTrigger></PopoverTrigger>
<ModsSearch
mods={experimentalMods ? actionModsExperimental : actionMods}
onSelect={setCurrentMod}
>
<Button variant="ghost" role="combobox" type="button">
<KebabHorizontalIcon></KebabHorizontalIcon>
</Button>
</ModsSearch>
<PopoverContent className="w-[400px] ml-2" align="start">
<div className="space-y-4">
<h4 className="font-medium leading-none">{currentMod?.name}</h4>
<hr />
<ActionMod
api={API_URL}
user={user}
variant="action"
manifest={currentMod}
renderers={renderers}
onOpenFileAction={handleOpenFile}
onExitAction={() => setCurrentMod(null)}
onSendEthTransactionAction={onSendEthTransactionAction}
author={author}
post={{
text: post.text,
embeds: post.embeds,
}}
/>
</div>
</PopoverContent>
</Popover>
);
}
```

This component can then be included in your post to allow it to be invoked by the user:

```tsx
...
<div className="ml-auto">
<Actions
post={{
id: props.cast.hash,
text: props.cast.text,
embeds: props.cast.embeds,
}}
author={{
farcaster: {
fid: props.cast.fid,
},
}}
/>
</div>
...
```
Binary file added docs/public/post-action.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions examples/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ PRIVATE_KEY="REQUIRED"
CHATGPT_ORGANIZATION_ID="REQUIRED"
NFT_STORAGE_API_KEY="REQUIRED"
ZORA_ADMIN_PRIVATE_KEY="REQUIRED"
HUB_HTTP_ENDPOINT="REQUIRED"
7 changes: 7 additions & 0 deletions examples/api/src/app/api/hello-world/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
return NextResponse.json({
data: "pong",
});
}
Loading