From c5c2f81163e568f9507336de2bf324cca5af9785 Mon Sep 17 00:00:00 2001 From: daria-github Date: Fri, 2 Feb 2024 17:02:14 -0800 Subject: [PATCH] updated frames get/post logic --- package-lock.json | 44 ++++++ package.json | 1 + .../components/Frame/Frame.tsx | 45 +++++++ src/controllers/FullMessageController.tsx | 126 +++++++++++++++--- src/helpers/getFrameInfo.ts | 34 +++++ 5 files changed, 234 insertions(+), 16 deletions(-) create mode 100644 src/component-library/components/Frame/Frame.tsx create mode 100644 src/helpers/getFrameInfo.ts diff --git a/package-lock.json b/package-lock.json index ccca33b5..404ca1bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,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", @@ -14936,6 +14937,28 @@ "@xmtp/xmtp-js": "^11.1.1" } }, + "node_modules/@xmtp/frames-client": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@xmtp/frames-client/-/frames-client-0.1.4.tgz", + "integrity": "sha512-MVGkkYTrO6QZ/jjVkS/XFxRfdb+cGy0RaNN42mN6LnRzJ1mha9So8jaIhcnO17CXiibqZBEeA+JU10Tr1q0iGg==", + "dependencies": { + "@xmtp/proto": "3.41.0-beta.2" + }, + "peerDependencies": { + "@xmtp/xmtp-js": ">9.3.1" + } + }, + "node_modules/@xmtp/frames-client/node_modules/@xmtp/proto": { + "version": "3.41.0-beta.2", + "resolved": "https://registry.npmjs.org/@xmtp/proto/-/proto-3.41.0-beta.2.tgz", + "integrity": "sha512-oRhyjnm7M4tnMZSpMMQQGs9VQbp3lndj/AUEJYcB3fzIgzHpwttfnHy5bMliR9ktv0B2EaldyLOlWRX/RroTBA==", + "dependencies": { + "long": "^5.2.0", + "protobufjs": "^7.0.0", + "rxjs": "^7.8.0", + "undici": "^5.8.1" + } + }, "node_modules/@xmtp/proto": { "version": "3.36.0", "resolved": "https://registry.npmjs.org/@xmtp/proto/-/proto-3.36.0.tgz", @@ -44435,6 +44458,27 @@ "@xmtp/xmtp-js": "^11.1.2" } }, + "@xmtp/frames-client": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@xmtp/frames-client/-/frames-client-0.1.4.tgz", + "integrity": "sha512-MVGkkYTrO6QZ/jjVkS/XFxRfdb+cGy0RaNN42mN6LnRzJ1mha9So8jaIhcnO17CXiibqZBEeA+JU10Tr1q0iGg==", + "requires": { + "@xmtp/proto": "3.41.0-beta.2" + }, + "dependencies": { + "@xmtp/proto": { + "version": "3.41.0-beta.2", + "resolved": "https://registry.npmjs.org/@xmtp/proto/-/proto-3.41.0-beta.2.tgz", + "integrity": "sha512-oRhyjnm7M4tnMZSpMMQQGs9VQbp3lndj/AUEJYcB3fzIgzHpwttfnHy5bMliR9ktv0B2EaldyLOlWRX/RroTBA==", + "requires": { + "long": "^5.2.0", + "protobufjs": "^7.0.0", + "rxjs": "^7.8.0", + "undici": "^5.8.1" + } + } + } + }, "@xmtp/proto": { "version": "3.36.0", "resolved": "https://registry.npmjs.org/@xmtp/proto/-/proto-3.36.0.tgz", diff --git a/package.json b/package.json index 4d2f75d4..e1f206b5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/component-library/components/Frame/Frame.tsx b/src/component-library/components/Frame/Frame.tsx new file mode 100644 index 00000000..ca5fbccb --- /dev/null +++ b/src/component-library/components/Frame/Frame.tsx @@ -0,0 +1,45 @@ +import { GhostButton } from "../GhostButton/GhostButton"; + +type FrameProps = { + info: { + image: string; + title: string; + buttons: string[]; + postUrl: string; + }; + handleClick: (buttonNumber: number) => Promise; + frameButtonUpdating: number; +}; + +export const Frame = ({ + info, + handleClick, + frameButtonUpdating, +}: FrameProps) => { + const { buttons, image, title } = info; + + return ( + <> + {title} +
+ {buttons?.map((button, index) => { + if (!button) { + return null; + } + const handlePress = () => { + void handleClick(index + 1); + }; + return ( + 0} + /> + ); + })} +
+ + ); +}; diff --git a/src/controllers/FullMessageController.tsx b/src/controllers/FullMessageController.tsx index 4874dd9b..95da68b5 100644 --- a/src/controllers/FullMessageController.tsx +++ b/src/controllers/FullMessageController.tsx @@ -1,9 +1,16 @@ 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; @@ -11,29 +18,116 @@ interface FullMessageControllerProps { 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(undefined); + const [frameButtonUpdating, setFrameButtonUpdating] = useState(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 ( - - + - + conversation={conversation} + key={message.xmtpID} + from={{ + displayAddress: recipientName ?? shortAddress(message.senderAddress), + isSelf: client?.address === message.senderAddress, + }} + datetime={message.sentAt}> + + + {frameInfo && ( + + )} + ); }; diff --git a/src/helpers/getFrameInfo.ts b/src/helpers/getFrameInfo.ts new file mode 100644 index 00000000..d09b8d3f --- /dev/null +++ b/src/helpers/getFrameInfo.ts @@ -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) { + 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, + }; +}