Skip to content

Commit

Permalink
Merge pull request #431 from xmtp-labs/dj/frames
Browse files Browse the repository at this point in the history
Add Frames client get/post logic
  • Loading branch information
daria-github authored Feb 5, 2024
2 parents e57d4ab + c5c2f81 commit ff18531
Show file tree
Hide file tree
Showing 5 changed files with 234 additions and 16 deletions.
44 changes: 44 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@xmtp/content-type-remote-attachment": "^1.1.4",
"@xmtp/content-type-reply": "^1.1.5",
"@xmtp/experimental-content-type-screen-effect": "^1.0.2",
"@xmtp/frames-client": "0.1.4",
"@xmtp/react-sdk": "^4.0.0",
"buffer": "^6.0.3",
"date-fns": "^2.29.3",
Expand Down
45 changes: 45 additions & 0 deletions src/component-library/components/Frame/Frame.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { GhostButton } from "../GhostButton/GhostButton";

type FrameProps = {
info: {
image: string;
title: string;
buttons: string[];
postUrl: string;
};
handleClick: (buttonNumber: number) => Promise<void>;
frameButtonUpdating: number;
};

export const Frame = ({
info,
handleClick,
frameButtonUpdating,
}: FrameProps) => {
const { buttons, image, title } = info;

return (
<>
<img src={image} className="max-h-80 rounded-lg px-8" alt={title} />
<div className="flex flex-col">
{buttons?.map((button, index) => {
if (!button) {
return null;
}
const handlePress = () => {
void handleClick(index + 1);
};
return (
<GhostButton
key={button}
label={button}
onClick={handlePress}
isLoading={frameButtonUpdating === index + 1}
isDisabled={frameButtonUpdating > 0}
/>
);
})}
</div>
</>
);
};
126 changes: 110 additions & 16 deletions src/controllers/FullMessageController.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,133 @@
import type { CachedConversation, CachedMessageWithId } from "@xmtp/react-sdk";
import { useClient } from "@xmtp/react-sdk";
import { Client, useClient } from "@xmtp/react-sdk";
import { FramesClient } from "@xmtp/frames-client";
import { useEffect, useState } from "react";
import type { PrivateKeyAccount, Transport, WalletClient } from "viem";
import type { mainnet } from "wagmi";
import { useWalletClient } from "wagmi";
import { FullMessage } from "../component-library/components/FullMessage/FullMessage";
import { shortAddress } from "../helpers";
import { classNames, shortAddress } from "../helpers";
import MessageContentController from "./MessageContentController";
import { useXmtpStore } from "../store/xmtp";
import { Frame } from "../component-library/components/Frame/Frame";
import { getFrameInfo } from "../helpers/getFrameInfo";

interface FullMessageControllerProps {
message: CachedMessageWithId;
conversation: CachedConversation;
isReply?: boolean;
}

export type FrameInfo = {
image: string;
title: string;
buttons: string[];
postUrl: string;
};

export const FullMessageController = ({
message,
conversation,
isReply,
}: FullMessageControllerProps) => {
const { client } = useClient();
const { data: walletClient } = useWalletClient();

const conversationTopic = useXmtpStore((state) => state.conversationTopic);

const [frameInfo, setFrameInfo] = useState<FrameInfo | undefined>(undefined);
const [frameButtonUpdating, setFrameButtonUpdating] = useState<number>(0);

const handleFrameButtonClick = async (buttonNumber: number) => {
if (!frameInfo) {
return;
}
const url = frameInfo.image;
const messageId = String(message.id);

setFrameButtonUpdating(buttonNumber);

const xmtpClient = await Client.create(
walletClient as WalletClient<
Transport,
typeof mainnet,
PrivateKeyAccount
>,
);
const framesClient = new FramesClient(xmtpClient);

const payload = await framesClient.signFrameAction(
url,
buttonNumber,
conversationTopic as string,
messageId,
);
const updatedFrameMetadata = await FramesClient.postToFrame(
frameInfo.postUrl,
payload,
);
const updatedFrameInfo = getFrameInfo(updatedFrameMetadata.extractedTags);

setFrameInfo(updatedFrameInfo);
setFrameButtonUpdating(0);
};

useEffect(() => {
if (typeof message.content === "string") {
const words = message.content?.split(/(\r?\n|\s+)/);
const urlRegex =
/^(http[s]?:\/\/)?([a-z0-9.-]+\.[a-z0-9]{1,}\/.*|[a-z0-9.-]+\.[a-z0-9]{1,})$/i;

void Promise.all(
words.map(async (word) => {
const isUrl = !!word.match(urlRegex)?.[0];

if (isUrl) {
const metadata = await FramesClient.readMetadata(word);
if (metadata) {
const info = getFrameInfo(metadata.extractedTags);
setFrameInfo(info);
}
}
}),
);
}
}, [message?.content]);

const recipientName = useXmtpStore((s) => s.recipientName);
const alignmentStyles =
client?.address === message.senderAddress
? "items-end justify-end"
: "items-start justify-start";

return (
<FullMessage
isReply={isReply}
message={message}
conversation={conversation}
key={message.xmtpID}
from={{
displayAddress: recipientName ?? shortAddress(message.senderAddress),
isSelf: client?.address === message.senderAddress,
}}
datetime={message.sentAt}>
<MessageContentController
<div
className={classNames(
"flex flex-col w-full px-4 md:px-8",
alignmentStyles,
)}>
<FullMessage
isReply={isReply}
message={message}
isSelf={client?.address === message.senderAddress}
/>
</FullMessage>
conversation={conversation}
key={message.xmtpID}
from={{
displayAddress: recipientName ?? shortAddress(message.senderAddress),
isSelf: client?.address === message.senderAddress,
}}
datetime={message.sentAt}>
<MessageContentController
message={message}
isSelf={client?.address === message.senderAddress}
/>
</FullMessage>
{frameInfo && (
<Frame
info={frameInfo}
handleClick={handleFrameButtonClick}
frameButtonUpdating={frameButtonUpdating}
/>
)}
</div>
);
};
34 changes: 34 additions & 0 deletions src/helpers/getFrameInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const BUTTON_PREFIX = "fc:frame:button:";
const IMAGE_PREFIX = "fc:frame:image";
const POST_URL_PREFIX = "fc:frame:post_url";
const TITLE_PREFIX = "og:title";

export function getFrameInfo(extractedTags: Record<string, string>) {
const buttons: string[] = [];
let image = "";
let postUrl = "";
let title = "";
for (const key in extractedTags) {
if (key) {
if (key.startsWith(BUTTON_PREFIX)) {
const buttonIndex = parseInt(key.replace(BUTTON_PREFIX, ""), 10);
buttons[buttonIndex] = extractedTags[key];
}
if (key.startsWith(IMAGE_PREFIX)) {
image = extractedTags[key];
}
if (key.startsWith(POST_URL_PREFIX)) {
postUrl = extractedTags[key];
}
if (key.startsWith(TITLE_PREFIX)) {
title = extractedTags[key];
}
}
}
return {
buttons,
image,
postUrl,
title,
};
}

0 comments on commit ff18531

Please sign in to comment.