Skip to content

Commit

Permalink
Merge pull request #439 from xmtp-labs/nm/frameInfo-from-client
Browse files Browse the repository at this point in the history
Frames updates
  • Loading branch information
neekolas authored Feb 24, 2024
2 parents a510755 + e8a7fc0 commit 8dbc532
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 138 deletions.
56 changes: 33 additions & 23 deletions package-lock.json

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

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +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.3.2",
"@xmtp/frames-client": "0.4.2",
"@xmtp/react-sdk": "^5.0.1",
"buffer": "^6.0.3",
"date-fns": "^2.29.3",
Expand Down Expand Up @@ -77,6 +77,7 @@
},
"devDependencies": {
"@babel/core": "^7.20.12",
"@open-frames/proxy-client": "0.2.0",
"@storybook/addon-essentials": "^7.1.0-alpha.29",
"@storybook/addon-interactions": "^7.1.0-alpha.29",
"@storybook/addon-links": "^7.1.0-alpha.29",
Expand Down
62 changes: 43 additions & 19 deletions src/component-library/components/Frame/Frame.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,68 @@
import type { FrameButton } from "../../../helpers/getFrameInfo";
import { GhostButton } from "../GhostButton/GhostButton";
import type { FrameButton } from "../../../helpers/frameInfo";

type FrameProps = {
image: string;
title: string;
textInput?: string;
buttons: FrameButton[];
handleClick: (
buttonNumber: number,
action: FrameButton["action"],
action?: FrameButton["action"],
) => Promise<void>;
onTextInputChange: (value: string) => void;
frameButtonUpdating: number;
interactionsEnabled: boolean;
};

export const Frame = ({
image,
title,
buttons,
textInput,
handleClick,
onTextInputChange,
frameButtonUpdating,
interactionsEnabled,
}: FrameProps) => (
<div className="px-4 md:px-8">
<img src={image} className="max-h-80 rounded-lg" alt={title} />
<div className="flex flex-col items-center">
{buttons?.map((button, index) => {
if (!button) {
return null;
{!!textInput && interactionsEnabled && (
<input
type="text"
className="w-full mt-4 mb-4 h-10 px-3 border rounded-md"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
onChange={(e) =>
onTextInputChange((e.target as HTMLInputElement).value)
}
const handlePress = () => {
void handleClick(index + 1, button.action);
};
return (
<GhostButton
key={button.text}
label={button.text}
onClick={handlePress}
isLoading={frameButtonUpdating === index + 1}
isDisabled={frameButtonUpdating > 0}
/>
);
})}
placeholder={textInput}
/>
)}
<div className="flex flex-col items-center">
{interactionsEnabled ? (
buttons.map((button) => {
if (!button) {
return null;
}
const handlePress = () => {
void handleClick(button.buttonIndex, button.action);
};
return (
<GhostButton
key={button.label}
label={button.label}
onClick={handlePress}
isLoading={frameButtonUpdating === button.buttonIndex}
isDisabled={frameButtonUpdating > 0}
/>
);
})
) : (
<span>Frame interactions not supported</span>
)}
</div>
</div>
);
62 changes: 36 additions & 26 deletions src/controllers/FullMessageController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,27 @@ import type { CachedConversation, CachedMessageWithId } from "@xmtp/react-sdk";
import { useClient } from "@xmtp/react-sdk";
import { FramesClient } from "@xmtp/frames-client";
import { useEffect, useState } from "react";
import type { GetMetadataResponse } from "@open-frames/proxy-client";
import { FullMessage } from "../component-library/components/FullMessage/FullMessage";
import { classNames, shortAddress } from "../helpers";
import MessageContentController from "./MessageContentController";
import { useXmtpStore } from "../store/xmtp";
import { Frame } from "../component-library/components/Frame/Frame";
import type { FrameButton } from "../helpers/getFrameInfo";
import { getFrameInfo } from "../helpers/getFrameInfo";
import { readMetadata } from "../helpers/openFrames";
import type { FrameButton } from "../helpers/frameInfo";
import {
getFrameTitle,
getOrderedButtons,
isValidFrame,
isXmtpFrame,
} from "../helpers/frameInfo";

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

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

export const FullMessageController = ({
message,
conversation,
Expand All @@ -33,39 +32,46 @@ export const FullMessageController = ({

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

const [frameInfo, setFrameInfo] = useState<FrameInfo | undefined>(undefined);
const [frameMetadata, setFrameMetadata] = useState<
GetMetadataResponse | undefined
>(undefined);
const [frameButtonUpdating, setFrameButtonUpdating] = useState<number>(0);
const [textInputValue, setTextInputValue] = useState<string>("");

const handleFrameButtonClick = async (
buttonIndex: number,
action: FrameButton["action"],
action: FrameButton["action"] = "post",
) => {
if (!frameInfo || !client) {
if (!frameMetadata || !client || !frameMetadata?.frameInfo?.buttons) {
return;
}
const { frameInfo, url: frameUrl } = frameMetadata;
if (!frameInfo.buttons) {
return;
}
const frameUrl = frameInfo.image;
const button = frameInfo.buttons[buttonIndex];
const button = frameInfo.buttons[`${buttonIndex}`];

setFrameButtonUpdating(buttonIndex);

const framesClient = new FramesClient(client);

const postUrl = button.target || frameInfo.postUrl || frameUrl;
const payload = await framesClient.signFrameAction({
frameUrl,
inputText: textInputValue || undefined,
buttonIndex,
conversationTopic: conversationTopic as string,
participantAccountAddresses: [client.address, conversation.peerAddress],
});
if (action === "post" || !action) {

if (action === "post") {
const updatedFrameMetadata = await framesClient.proxy.post(
button?.target || frameInfo.postUrl,
postUrl,
payload,
);
const updatedFrameInfo = getFrameInfo(updatedFrameMetadata.extractedTags);
setFrameInfo(updatedFrameInfo);
setFrameMetadata(updatedFrameMetadata);
} else if (action === "post_redirect") {
const { redirectedTo } = await framesClient.proxy.postRedirect(
button?.target || frameInfo.postUrl,
postUrl,
payload,
);
window.open(redirectedTo, "_blank");
Expand All @@ -88,8 +94,7 @@ export const FullMessageController = ({
if (isUrl) {
const metadata = await readMetadata(word);
if (metadata) {
const info = getFrameInfo(metadata.extractedTags);
setFrameInfo(info);
setFrameMetadata(metadata);
}
}
}),
Expand All @@ -103,6 +108,8 @@ export const FullMessageController = ({
? "items-end justify-end"
: "items-start justify-start";

const showFrame = isValidFrame(frameMetadata);

return (
<div
className={classNames(
Expand All @@ -124,13 +131,16 @@ export const FullMessageController = ({
isSelf={client?.address === message.senderAddress}
/>
</FullMessage>
{frameInfo?.image && (
{showFrame && (
<Frame
image={frameInfo.image}
title={frameInfo.title}
buttons={frameInfo.buttons}
image={frameMetadata?.frameInfo?.image.content}
title={getFrameTitle(frameMetadata)}
buttons={getOrderedButtons(frameMetadata)}
handleClick={handleFrameButtonClick}
frameButtonUpdating={frameButtonUpdating}
interactionsEnabled={isXmtpFrame(frameMetadata)}
textInput={frameMetadata?.frameInfo?.textInput?.content}
onTextInputChange={setTextInputValue}
/>
)}
</div>
Expand Down
Loading

0 comments on commit 8dbc532

Please sign in to comment.