From 149e77b75f17db914fb0c3668294c8d5ebb31cdb Mon Sep 17 00:00:00 2001 From: daria-github Date: Thu, 22 Feb 2024 12:39:48 -0800 Subject: [PATCH 01/14] remove mobile logic, redirect mobile users --- .../HeaderDropdown/HeaderDropdown.tsx | 7 +---- .../components/MessageInput/MessageInput.tsx | 10 ++----- .../components/Mobile/Mobile.tsx | 28 +++++++++++++++++++ src/controllers/AddressInputController.tsx | 13 ++------- src/controllers/HeaderDropdownController.tsx | 4 --- src/locales/en_US.json | 5 ++++ src/pages/inbox.tsx | 12 ++++---- 7 files changed, 44 insertions(+), 35 deletions(-) create mode 100644 src/component-library/components/Mobile/Mobile.tsx diff --git a/src/component-library/components/HeaderDropdown/HeaderDropdown.tsx b/src/component-library/components/HeaderDropdown/HeaderDropdown.tsx index b6922fc6..c598f859 100644 --- a/src/component-library/components/HeaderDropdown/HeaderDropdown.tsx +++ b/src/component-library/components/HeaderDropdown/HeaderDropdown.tsx @@ -14,16 +14,11 @@ interface HeaderDropdownProps { * What is the recipient input? */ recipientInput: string; - /** - * Boolean to determine if screen width is mobile size - */ - isMobileView?: boolean; } export const HeaderDropdown = ({ onClick, recipientInput, - isMobileView, }: HeaderDropdownProps) => { const { t } = useTranslation(); @@ -61,7 +56,7 @@ export const HeaderDropdown = ({ {t(`consent.${name}`)} ))} - {(recipientInput || isMobileView) && ( + {recipientInput && ( onClick?.()} label={} diff --git a/src/component-library/components/MessageInput/MessageInput.tsx b/src/component-library/components/MessageInput/MessageInput.tsx index d6ae5e8a..569be872 100644 --- a/src/component-library/components/MessageInput/MessageInput.tsx +++ b/src/component-library/components/MessageInput/MessageInput.tsx @@ -31,7 +31,7 @@ import { ContentTypeScreenEffect } from "@xmtp/experimental-content-type-screen- import { IconButton } from "../IconButton/IconButton"; import { useAttachmentChange } from "../../../hooks/useAttachmentChange"; import { typeLookup, type contentTypes } from "../../../helpers/attachments"; -import { TAILWIND_MD_BREAKPOINT, classNames } from "../../../helpers"; +import { classNames } from "../../../helpers"; import type { RecipientAddress } from "../../../store/xmtp"; import { useXmtpStore } from "../../../store/xmtp"; import { useVoiceRecording } from "../../../hooks/useVoiceRecording"; @@ -39,7 +39,6 @@ import { useRecordingTimer } from "../../../hooks/useRecordingTimer"; import "react-tooltip/dist/react-tooltip.css"; import { useLongPress } from "../../../hooks/useLongPress"; import { EffectDialog } from "../EffectDialog/EffectDialog"; -import useWindowSize from "../../../hooks/useWindowSize"; type InputProps = { /** @@ -96,8 +95,6 @@ export const MessageInput = ({ setAttachmentPreview, setIsDragActive, }: InputProps) => { - const [width] = useWindowSize(); - const { getCachedByPeerAddress } = useConversation(); // For effects const { sendMessage: _sendMessage } = _useSendMessage(); @@ -246,10 +243,7 @@ export const MessageInput = ({ ]); const handleLongPress = () => { - // Don't run effect on mobile - if (width > TAILWIND_MD_BREAKPOINT) { - setOpenEffectDialog(true); - } + setOpenEffectDialog(true); }; const handleSendEffect = (effectType: string) => { diff --git a/src/component-library/components/Mobile/Mobile.tsx b/src/component-library/components/Mobile/Mobile.tsx new file mode 100644 index 00000000..e34c6b9d --- /dev/null +++ b/src/component-library/components/Mobile/Mobile.tsx @@ -0,0 +1,28 @@ +import { DeviceMobileIcon } from "@heroicons/react/solid"; +import { useTranslation } from "react-i18next"; + +export const Mobile = () => { + const { t } = useTranslation(); + return ( +
+ +

{t("mobile.mobile_detected")}

+

{t("mobile.group_chat_cta")}

+ + https://testflight.apple.com/join/xEJOvzEx + +

{t("mobile.reference_app_cta")}

+ + https://github.com/xmtp-labs/xmtp-inbox-mobile + +
+ ); +}; diff --git a/src/controllers/AddressInputController.tsx b/src/controllers/AddressInputController.tsx index 97e9c141..aaf5b3ea 100644 --- a/src/controllers/AddressInputController.tsx +++ b/src/controllers/AddressInputController.tsx @@ -1,8 +1,7 @@ import { useEffect } from "react"; import { useConversation, useConsent } from "@xmtp/react-sdk"; import { AddressInput } from "../component-library/components/AddressInput/AddressInput"; -import { getRecipientInputSubtext, shortAddress } from "../helpers"; -import useWindowSize from "../hooks/useWindowSize"; +import { getRecipientInputSubtext } from "../helpers"; import { useXmtpStore } from "../store/xmtp"; import { useAddressInput } from "../hooks/useAddressInput"; @@ -29,8 +28,6 @@ export const AddressInputController = () => { // manage address input state useAddressInput(); - const size = useWindowSize(); - useEffect(() => { const selectConversation = async () => { // if there's a valid network address, look for an existing conversation @@ -80,13 +77,7 @@ export const AddressInputController = () => { : "" } resolvedAddress={{ - displayAddress: - recipientName ?? - (size[0] < 700 - ? recipientAddress - ? shortAddress(recipientAddress) - : "" - : recipientAddress ?? ""), + displayAddress: recipientName ?? recipientAddress ?? "", walletAddress: recipientName ? recipientAddress ?? undefined : undefined, diff --git a/src/controllers/HeaderDropdownController.tsx b/src/controllers/HeaderDropdownController.tsx index 9dc79911..d63c9017 100644 --- a/src/controllers/HeaderDropdownController.tsx +++ b/src/controllers/HeaderDropdownController.tsx @@ -1,6 +1,4 @@ import { HeaderDropdown } from "../component-library/components/HeaderDropdown/HeaderDropdown"; -import { TAILWIND_MD_BREAKPOINT } from "../helpers"; -import useWindowSize from "../hooks/useWindowSize"; import { useXmtpStore } from "../store/xmtp"; export const HeaderDropdownController = () => { @@ -8,7 +6,6 @@ export const HeaderDropdownController = () => { const setConversationTopic = useXmtpStore((s) => s.setConversationTopic); const recipientInput = useXmtpStore((s) => s.recipientInput); const setStartedFirstMessage = useXmtpStore((s) => s.setStartedFirstMessage); - const [width] = useWindowSize(); return ( { setConversationTopic(); setStartedFirstMessage(true); }} - isMobileView={width <= TAILWIND_MD_BREAKPOINT} /> ); }; diff --git a/src/locales/en_US.json b/src/locales/en_US.json index c781f874..725040c9 100644 --- a/src/locales/en_US.json +++ b/src/locales/en_US.json @@ -62,6 +62,11 @@ "collapse_header": "Collapse", "messages_header": "Messages" }, + "mobile": { + "mobile_detected": "It looks like you may be on a mobile device!", + "group_chat_cta": "Download the Converse TestFlight to try group chat:", + "reference_app_cta": "In addition, please note that our React Native XMTP Reference Application is now live:" + }, "status_messaging": { "error_1_header": "Sorry, the app encountered an error", "error_1_subheader": "Not to worry. Let’s try again. If the error persists, <0>we're here to help!", diff --git a/src/pages/inbox.tsx b/src/pages/inbox.tsx index 67aea152..1d258d3b 100644 --- a/src/pages/inbox.tsx +++ b/src/pages/inbox.tsx @@ -23,6 +23,7 @@ import { ConversationListController } from "../controllers/ConversationListContr import { useAttachmentChange } from "../hooks/useAttachmentChange"; import useSelectedConversation from "../hooks/useSelectedConversation"; import { ReplyThread } from "../component-library/components/ReplyThread/ReplyThread"; +import { Mobile } from "../component-library/components/Mobile/Mobile"; const Inbox: React.FC<{ children?: React.ReactNode }> = () => { const navigate = useNavigate(); @@ -118,7 +119,9 @@ const Inbox: React.FC<{ children?: React.ReactNode }> = () => { } }; - return ( + return size[0] < TAILWIND_MD_BREAKPOINT ? ( + + ) : ( // Controller for drag-and-drop area
= () => { onDrop={onAttachmentChange}>
- {size[0] > TAILWIND_MD_BREAKPOINT || - (!recipientAddress && !startedFirstMessage) ? ( + {!recipientAddress && !startedFirstMessage ? ( <>
@@ -141,9 +143,7 @@ const Inbox: React.FC<{ children?: React.ReactNode }> = () => { ) : null}
- {size[0] > TAILWIND_MD_BREAKPOINT || - recipientAddress || - startedFirstMessage ? ( + {recipientAddress || startedFirstMessage ? (
{!conversations.length && !loadingConversations && From 959854dfeb310c21baa7deefc4ba433618dde27c Mon Sep 17 00:00:00 2001 From: daria-github Date: Thu, 22 Feb 2024 13:03:36 -0800 Subject: [PATCH 02/14] remove unneeded consent code --- .../components/AddressInput/AddressInput.tsx | 14 ++---- .../components/LearnMore/LearnMore.tsx | 3 -- .../components/MessageInput/MessageInput.tsx | 2 +- .../ConversationListController.tsx | 48 ++----------------- .../MessagePreviewCardController.tsx | 4 -- src/pages/inbox.tsx | 19 +++----- 6 files changed, 15 insertions(+), 75 deletions(-) diff --git a/src/component-library/components/AddressInput/AddressInput.tsx b/src/component-library/components/AddressInput/AddressInput.tsx index 5e3076f9..c223cf15 100644 --- a/src/component-library/components/AddressInput/AddressInput.tsx +++ b/src/component-library/components/AddressInput/AddressInput.tsx @@ -70,9 +70,9 @@ export const AddressInput = ({ !resolvedAddress?.displayAddress ? "bg-indigo-50 border-b border-indigo-500" : "border-b border-gray-200", - "flex items-center px-2 md:px-4 py-3 border-l-0 z-10 max-md:h-fit md:max-h-sm w-full h-16", + "flex items-center px-2 md:px-4 py-3 border-l-0 z-10 max-h-sm w-full h-16", )}> -
+
)}
{subtext ? t(subtext) diff --git a/src/component-library/components/LearnMore/LearnMore.tsx b/src/component-library/components/LearnMore/LearnMore.tsx index 8f890e30..6b57b10e 100644 --- a/src/component-library/components/LearnMore/LearnMore.tsx +++ b/src/component-library/components/LearnMore/LearnMore.tsx @@ -9,14 +9,11 @@ interface LearnMoreProps { description: string; tags: React.ReactNode; }>; - version: string; setStartedFirstMessage: () => void; } export const LearnMore = ({ highlightedCompanies = [], - // eslint-disable-next-line @typescript-eslint/no-unused-vars - version, setStartedFirstMessage, }: LearnMoreProps) => { const { t } = useTranslation(); diff --git a/src/component-library/components/MessageInput/MessageInput.tsx b/src/component-library/components/MessageInput/MessageInput.tsx index 569be872..ea2ca668 100644 --- a/src/component-library/components/MessageInput/MessageInput.tsx +++ b/src/component-library/components/MessageInput/MessageInput.tsx @@ -123,7 +123,7 @@ export const MessageInput = ({ textAreaRef?.current?.scrollHeight <= 32 ? "max-h-8" : "max-h-40" - } min-h-8 outline-none border-none focus:ring-0 resize-none mx-2 p-1 w-full max-md:text-[16px] md:text-md text-gray-900`; + } min-h-8 outline-none border-none focus:ring-0 resize-none mx-2 p-1 w-full text-md text-gray-900`; useLayoutEffect(() => { const MIN_TEXTAREA_HEIGHT = 32; diff --git a/src/controllers/ConversationListController.tsx b/src/controllers/ConversationListController.tsx index ad6fa0a6..e87884e6 100644 --- a/src/controllers/ConversationListController.tsx +++ b/src/controllers/ConversationListController.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo } from "react"; -import { useClient, useConsent, useDb } from "@xmtp/react-sdk"; +import { useConsent, useDb } from "@xmtp/react-sdk"; import type { CachedConversation } from "@xmtp/react-sdk"; import type { ActiveTab } from "../store/xmtp"; import { useXmtpStore } from "../store/xmtp"; @@ -27,11 +27,8 @@ export const ConversationListController = ({ const { isAllowed, isDenied } = useConsent(); const { db } = useDb(); - // const [messages, setMessages] = useState([]); - // const messagesDb = db.table("messages"); useStreamAllMessages(); - const { client: walletAddress } = useClient(); const recipientInput = useXmtpStore((s) => s.recipientInput); const changedConsentCount = useXmtpStore((s) => s.changedConsentCount); @@ -45,50 +42,21 @@ export const ConversationListController = ({ } }; void runUpdate(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isLoaded, activeTab, changedConsentCount]); - - // To-do: remove if not needed after consent goes out - // useEffect(() => { - // // This may make more sense to come from the React SDK, but we're pulling from here for now - // const fetchMessages = async () => - // messagesDb - // .where("senderAddress") - // .equals(walletAddress?.address as string) - // .toArray() - // .then((dbMessages: CachedMessage[]) => { - // setMessages(dbMessages); - // }) - // .catch((error: Error) => { - // console.error("Error querying messages:", error); - // }); - - // void fetchMessages(); - // }, [conversations.length, messagesDb, walletAddress?.address]); + }, [isLoaded, activeTab, changedConsentCount, conversations, db]); const messagesToPass = useMemo(() => { const conversationsWithTab = conversations.map( (conversation: CachedConversation) => { - const tab = isAllowed(conversation.peerAddress) - ? "messages" - : isDenied(conversation.peerAddress) - ? "blocked" - : "requests"; return ( ); }, ); const sortedConvos = conversationsWithTab.filter( (item: NodeWithConsent) => { - // To-do: remove commented out code in this block if not needed after consent goes out - // const hasSentMessages = messages.find( - // (message) => message?.conversationTopic === item.props.convo.topic, - // ); const isAddressBlocked = isDenied(item.props.convo.peerAddress); const isAddressAllowed = isAllowed(item.props.convo.peerAddress); @@ -105,17 +73,7 @@ export const ConversationListController = ({ }, ); return sortedConvos; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - conversations, - // messages, - isLoading, - walletAddress, - db, - changedConsentCount, - isAllowed, - isDenied, - ]); + }, [activeTab, conversations, isAllowed, isDenied]); return ( { const { t } = useTranslation(); const { allow } = useConsent(); diff --git a/src/pages/inbox.tsx b/src/pages/inbox.tsx index 1d258d3b..d456c034 100644 --- a/src/pages/inbox.tsx +++ b/src/pages/inbox.tsx @@ -131,17 +131,13 @@ const Inbox: React.FC<{ children?: React.ReactNode }> = () => { onDrop={onAttachmentChange}>
- {!recipientAddress && !startedFirstMessage ? ( - <> - -
- - -
- - ) : null} + +
+ + +
{recipientAddress || startedFirstMessage ? (
@@ -149,7 +145,6 @@ const Inbox: React.FC<{ children?: React.ReactNode }> = () => { !loadingConversations && !startedFirstMessage ? ( setStartedFirstMessage(true)} /> ) : ( From c5b32e2ad93c57d857d897b123bfe6e5fba9141b Mon Sep 17 00:00:00 2001 From: daria-github Date: Thu, 22 Feb 2024 13:05:48 -0800 Subject: [PATCH 03/14] lint fix --- src/controllers/ConversationListController.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/controllers/ConversationListController.tsx b/src/controllers/ConversationListController.tsx index e87884e6..7c574fd6 100644 --- a/src/controllers/ConversationListController.tsx +++ b/src/controllers/ConversationListController.tsx @@ -46,14 +46,12 @@ export const ConversationListController = ({ const messagesToPass = useMemo(() => { const conversationsWithTab = conversations.map( - (conversation: CachedConversation) => { - return ( - - ); - }, + (conversation: CachedConversation) => ( + + ), ); const sortedConvos = conversationsWithTab.filter( (item: NodeWithConsent) => { From ce2b6271d2cac006a4281b6a59d9fd926fd7c279 Mon Sep 17 00:00:00 2001 From: daria-github Date: Thu, 22 Feb 2024 15:22:47 -0800 Subject: [PATCH 04/14] test fix --- src/pages/inbox.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/inbox.tsx b/src/pages/inbox.tsx index d456c034..3c127ba1 100644 --- a/src/pages/inbox.tsx +++ b/src/pages/inbox.tsx @@ -139,7 +139,7 @@ const Inbox: React.FC<{ children?: React.ReactNode }> = () => { />
- {recipientAddress || startedFirstMessage ? ( + {
{!conversations.length && !loadingConversations && @@ -194,7 +194,7 @@ const Inbox: React.FC<{ children?: React.ReactNode }> = () => {
)}
- ) : null} + }
); From 3896095f91e2e90936800d64662aa26c4e02cb6c Mon Sep 17 00:00:00 2001 From: daria-github Date: Thu, 22 Feb 2024 15:26:10 -0800 Subject: [PATCH 05/14] remove unused var --- src/pages/inbox.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/inbox.tsx b/src/pages/inbox.tsx index 3c127ba1..3bc8f456 100644 --- a/src/pages/inbox.tsx +++ b/src/pages/inbox.tsx @@ -49,7 +49,6 @@ const Inbox: React.FC<{ children?: React.ReactNode }> = () => { }, [client]); const activeTab = useXmtpStore((s) => s.activeTab); - const recipientAddress = useXmtpStore((s) => s.recipientAddress); const setActiveMessage = useXmtpStore((s) => s.setActiveMessage); const size = useWindowSize(); From d565cd56872f6c7851e52f6041faa75d6d0a4734 Mon Sep 17 00:00:00 2001 From: Nicholas Molnar <65710+neekolas@users.noreply.github.com> Date: Fri, 23 Feb 2024 13:20:29 -0800 Subject: [PATCH 06/14] Frames updates --- package-lock.json | 56 ++++++++------- package.json | 3 +- .../components/Frame/Frame.tsx | 62 ++++++++++++----- src/controllers/FullMessageController.tsx | 62 ++++++++++------- src/helpers/frameInfo.ts | 56 +++++++++++++++ src/helpers/getFrameInfo.ts | 69 ------------------- 6 files changed, 170 insertions(+), 138 deletions(-) create mode 100644 src/helpers/frameInfo.ts delete mode 100644 src/helpers/getFrameInfo.ts diff --git a/package-lock.json b/package-lock.json index 9e978ed3..1a5a3ef0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +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.3.2", + "@xmtp/frames-client": "0.4.2", "@xmtp/react-sdk": "^5.0.1", "buffer": "^6.0.3", "date-fns": "^2.29.3", @@ -56,6 +56,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", @@ -4777,6 +4778,25 @@ "node": ">= 8" } }, + "node_modules/@open-frames/proxy-client": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@open-frames/proxy-client/-/proxy-client-0.2.0.tgz", + "integrity": "sha512-elfy9da998KR65tQDt+Z/nvNnFeXHhYAxlIjI3lftJbg5Vt6zwAtoWvV3r10B9nTcaA4/MWipZezD5pGV5tazg==", + "dependencies": { + "@open-frames/proxy-types": "0.1.0" + } + }, + "node_modules/@open-frames/proxy-types": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@open-frames/proxy-types/-/proxy-types-0.1.0.tgz", + "integrity": "sha512-fFcUmUmnGn3JkiBL3az6sqcs62pOnKPWc3tKar6LV9VciHaRXC059kCxpLh2mcYVfpZ0HgRILBtttEupIrAPDw==", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "typescript": "^5.3.3" + } + }, "node_modules/@perma/map": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@perma/map/-/map-1.0.3.tgz", @@ -14602,12 +14622,13 @@ } }, "node_modules/@xmtp/frames-client": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@xmtp/frames-client/-/frames-client-0.3.2.tgz", - "integrity": "sha512-61rxA7YcNUUKndQ9e5X44LNVwWJCrrZR6sBGOWLckfMK00LqRasoiLgom1PlR34c17a59PPZmagxoNQ2QCto7A==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@xmtp/frames-client/-/frames-client-0.4.2.tgz", + "integrity": "sha512-47fy35WGJ/QDzPKjuCCc45GLvLrDb92Jh5sDXY6B/HmWrssb8dZtrV5kw77kKsdGVhnqTQTRUM3WjMC6l83T0w==", "dependencies": { "@noble/hashes": "^1.3.3", - "@xmtp/proto": "3.41.0-beta.5", + "@open-frames/proxy-client": "^0.2.0", + "@xmtp/proto": "3.44.0", "long": "^5.2.3" }, "peerDependencies": { @@ -14625,21 +14646,10 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@xmtp/frames-client/node_modules/@xmtp/proto": { - "version": "3.41.0-beta.5", - "resolved": "https://registry.npmjs.org/@xmtp/proto/-/proto-3.41.0-beta.5.tgz", - "integrity": "sha512-vx5zqLpAVPjTEdyqY/woXrgvWMKjbTwwco+x9WE+T1iVlv+472yp2DwFJRLpfeQByC1cHl7XQyuO2Q+8t8HL4Q==", - "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", - "integrity": "sha512-LTyoa1K5TgHv6ekGmwVTaMwfnhxZhglZLh/6zS3dgygGYgn2KvFvmkt+ssNSRdK8LAaJ2ZpqV/OTDjB/tEHjCA==", + "version": "3.44.0", + "resolved": "https://registry.npmjs.org/@xmtp/proto/-/proto-3.44.0.tgz", + "integrity": "sha512-5M23JsxONb/qluZF6QMjoXHT67xDRR7pWwHTd1tfw59jS+jXTxm/+v8/UeSOoK47PBSJhDD5I90bAeA5NrLRig==", "dependencies": { "long": "^5.2.0", "protobufjs": "^7.0.0", @@ -31123,15 +31133,15 @@ } }, "node_modules/typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=12.20" + "node": ">=14.17" } }, "node_modules/ufo": { diff --git a/package.json b/package.json index 02fe1e81..2e8ef0f8 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/src/component-library/components/Frame/Frame.tsx b/src/component-library/components/Frame/Frame.tsx index 151771d5..e49106b8 100644 --- a/src/component-library/components/Frame/Frame.tsx +++ b/src/component-library/components/Frame/Frame.tsx @@ -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"] | undefined, ) => Promise; + onTextInputChange: (value: string) => void; frameButtonUpdating: number; + interactionsEnabled: boolean; }; export const Frame = ({ image, title, buttons, + textInput, handleClick, + onTextInputChange, frameButtonUpdating, + interactionsEnabled, }: FrameProps) => (
{title} -
- {buttons?.map((button, index) => { - if (!button) { - return null; + {!!textInput && interactionsEnabled && ( + + onTextInputChange((e.target as HTMLInputElement).value) } - const handlePress = () => { - void handleClick(index + 1, button.action); - }; - return ( - 0} - /> - ); - })} + placeholder={textInput} + /> + )} +
+ {interactionsEnabled ? ( + buttons.map((button) => { + if (!button) { + return null; + } + const handlePress = () => { + void handleClick(button.buttonIndex, button.action); + }; + return ( + 0} + /> + ); + }) + ) : ( + Frame interactions not supported + )}
); diff --git a/src/controllers/FullMessageController.tsx b/src/controllers/FullMessageController.tsx index e7bdee78..b08687ed 100644 --- a/src/controllers/FullMessageController.tsx +++ b/src/controllers/FullMessageController.tsx @@ -2,14 +2,20 @@ 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; @@ -17,13 +23,6 @@ interface FullMessageControllerProps { isReply?: boolean; } -export type FrameInfo = { - image: string; - title: string; - buttons: FrameButton[]; - postUrl: string; -}; - export const FullMessageController = ({ message, conversation, @@ -33,39 +32,46 @@ export const FullMessageController = ({ const conversationTopic = useXmtpStore((state) => state.conversationTopic); - const [frameInfo, setFrameInfo] = useState(undefined); + const [frameMetadata, setFrameMetadata] = useState< + GetMetadataResponse | undefined + >(undefined); const [frameButtonUpdating, setFrameButtonUpdating] = useState(0); + const [textInputValue, setTextInputValue] = useState(""); 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"); @@ -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); } } }), @@ -103,6 +108,8 @@ export const FullMessageController = ({ ? "items-end justify-end" : "items-start justify-start"; + const showFrame = isValidFrame(frameMetadata); + return (
- {frameInfo?.image && ( + {showFrame && ( )}
diff --git a/src/helpers/frameInfo.ts b/src/helpers/frameInfo.ts new file mode 100644 index 00000000..9fb25763 --- /dev/null +++ b/src/helpers/frameInfo.ts @@ -0,0 +1,56 @@ +import type { + GetMetadataResponse, + OpenFrameButton, +} from "@open-frames/proxy-client"; +import { PROTOCOL_VERSION } from "@xmtp/frames-client"; + +const OG_TITLE_TAG = "og:title"; + +export type FrameButton = OpenFrameButton & { buttonIndex: number }; + +export function getOrderedButtons( + metadata: GetMetadataResponse, +): FrameButton[] { + const buttonMap = metadata?.frameInfo?.buttons; + + if (!buttonMap) { + return []; + } + + return Object.keys(buttonMap) + .sort() + .map((key) => { + const button = buttonMap[key]; + return { + ...button, + buttonIndex: parseInt(key, 10), + }; + }); +} + +export function isXmtpFrame(metadata: GetMetadataResponse): boolean { + const minXmtpVersion = metadata.frameInfo?.acceptedClients?.xmtp; + + return ( + !!minXmtpVersion && + minXmtpVersion.length > 0 && + minXmtpVersion <= PROTOCOL_VERSION + ); +} + +export function isValidFrame( + metadata: GetMetadataResponse | undefined, +): metadata is Required { + if (!metadata) { + return false; + } + + // NOTE: This is more lenient than the Farcaster spec, which lists the og:image tag as required + const hasImage = !!metadata.frameInfo?.image || !!metadata.frameInfo?.ogImage; + + return !!metadata?.frameInfo && hasImage; +} + +export function getFrameTitle(metadata: GetMetadataResponse): string { + return metadata.extractedTags[OG_TITLE_TAG] || ""; +} diff --git a/src/helpers/getFrameInfo.ts b/src/helpers/getFrameInfo.ts deleted file mode 100644 index 139ae563..00000000 --- a/src/helpers/getFrameInfo.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { getButtonIndex, mediaUrl } from "./openFrames"; - -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"; - -const ALLOWED_ACTIONS = ["post", "post_redirect", "link", "mint"] as const; - -export type FrameButton = { - text?: string; - action?: (typeof ALLOWED_ACTIONS)[number]; - target?: string; -}; - -function addButtonTag( - existingButtons: FrameButton[], - key: string, - value: string, -): FrameButton[] { - const buttons = existingButtons; - const buttonIndex = getButtonIndex(key); - if (buttonIndex) { - buttons[buttonIndex] = buttons[buttonIndex] || {}; - if ( - key.endsWith(":action") && - ALLOWED_ACTIONS.includes(value as (typeof ALLOWED_ACTIONS)[number]) - ) { - buttons[buttonIndex].action = value as (typeof ALLOWED_ACTIONS)[number]; - } else if (key.endsWith(":target")) { - buttons[buttonIndex].target = value; - } else if (key.endsWith(buttonIndex.toString())) { - buttons[buttonIndex].text = value; - } - } - return buttons; -} - -export function getFrameInfo(extractedTags: Record) { - let buttons: FrameButton[] = []; - let image = ""; - let postUrl = ""; - let title = ""; - for (const key in extractedTags) { - if (key) { - if (key.startsWith(BUTTON_PREFIX)) { - buttons = addButtonTag(buttons, key, extractedTags[key]); - } - if (key.startsWith(IMAGE_PREFIX)) { - const imageUrl = extractedTags[key]; - image = imageUrl.startsWith("data:") - ? extractedTags[key] - : mediaUrl(imageUrl); - } - if (key.startsWith(POST_URL_PREFIX)) { - postUrl = extractedTags[key]; - } - if (key.startsWith(TITLE_PREFIX)) { - title = extractedTags[key]; - } - } - } - return { - buttons: buttons.filter((b) => b), - image, - postUrl, - title, - }; -} From daabd7cb8e8e7047ddf533512424d15afb632a68 Mon Sep 17 00:00:00 2001 From: Nicholas Molnar <65710+neekolas@users.noreply.github.com> Date: Fri, 23 Feb 2024 15:01:36 -0800 Subject: [PATCH 07/14] Make optional argument --- src/component-library/components/Frame/Frame.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/component-library/components/Frame/Frame.tsx b/src/component-library/components/Frame/Frame.tsx index e49106b8..bb81389f 100644 --- a/src/component-library/components/Frame/Frame.tsx +++ b/src/component-library/components/Frame/Frame.tsx @@ -8,7 +8,7 @@ type FrameProps = { buttons: FrameButton[]; handleClick: ( buttonNumber: number, - action: FrameButton["action"] | undefined, + action?: FrameButton["action"], ) => Promise; onTextInputChange: (value: string) => void; frameButtonUpdating: number; From eb679d9c1cefa2f8a0a0a2c2a1feecb586b1f862 Mon Sep 17 00:00:00 2001 From: Nicholas Molnar <65710+neekolas@users.noreply.github.com> Date: Sun, 25 Feb 2024 10:49:22 -0800 Subject: [PATCH 08/14] Redesign Frames buttons --- .../components/Frame/Frame.tsx | 121 +++++++++++++++--- 1 file changed, 101 insertions(+), 20 deletions(-) diff --git a/src/component-library/components/Frame/Frame.tsx b/src/component-library/components/Frame/Frame.tsx index bb81389f..3706fc6c 100644 --- a/src/component-library/components/Frame/Frame.tsx +++ b/src/component-library/components/Frame/Frame.tsx @@ -1,5 +1,7 @@ -import { GhostButton } from "../GhostButton/GhostButton"; +import { ArrowCircleRightIcon } from "@heroicons/react/outline"; import type { FrameButton } from "../../../helpers/frameInfo"; +import { classNames } from "../../../helpers"; +import { ButtonLoader } from "../Loaders/ButtonLoader"; type FrameProps = { image: string; @@ -15,6 +17,97 @@ type FrameProps = { interactionsEnabled: boolean; }; +const FrameButtonContainer = ({ + button, + isFullWidth, + isLoading, + isDisabled, + testId = "", + clickHandler, +}: { + testId?: string; + button: FrameButton; + isFullWidth: boolean; + isLoading: boolean; + isDisabled: boolean; + clickHandler: () => void; +}) => { + const columnWidth = isFullWidth ? "col-span-2" : "col-span-1"; + const isExternal = ["post_redirect", "link"].includes(button.action || ""); + + const icon = isExternal ? : null; + return ( + + ); +}; + +const ButtonsContainer = ({ + frameButtonUpdating, + buttons, + handleClick, +}: Pick) => { + if (buttons.length < 1 || buttons.length > 4) { + return null; + } + // If there is only one button make it full-width + const gridColumns = buttons.length === 1 ? "grid-cols-1" : "grid-cols-2"; + return ( +
+ {buttons.map((button, index) => { + const clickHandler = () => { + void handleClick(button.buttonIndex, button.action); + }; + const isFullWidth = buttons.length === 3 && index === 2; + return ( + 0} + clickHandler={clickHandler} + /> + ); + })} +
+ ); +}; + export const Frame = ({ image, title, @@ -25,7 +118,7 @@ export const Frame = ({ frameButtonUpdating, interactionsEnabled, }: FrameProps) => ( -
+
{title} {!!textInput && interactionsEnabled && ( )}
- {interactionsEnabled ? ( - buttons.map((button) => { - if (!button) { - return null; - } - const handlePress = () => { - void handleClick(button.buttonIndex, button.action); - }; - return ( - 0} - /> - ); - }) + {interactionsEnabled && buttons.length ? ( + ) : ( Frame interactions not supported )} From 9cbc046eb0f1818113eca62f8d27eeead31ad130 Mon Sep 17 00:00:00 2001 From: Nicholas Molnar <65710+neekolas@users.noreply.github.com> Date: Sun, 25 Feb 2024 10:58:05 -0800 Subject: [PATCH 09/14] Don't rely on button state --- .../components/Frame/Frame.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/component-library/components/Frame/Frame.tsx b/src/component-library/components/Frame/Frame.tsx index 3706fc6c..8816a83c 100644 --- a/src/component-library/components/Frame/Frame.tsx +++ b/src/component-library/components/Frame/Frame.tsx @@ -18,7 +18,8 @@ type FrameProps = { }; const FrameButtonContainer = ({ - button, + label, + isExternalLink, isFullWidth, isLoading, isDisabled, @@ -26,16 +27,16 @@ const FrameButtonContainer = ({ clickHandler, }: { testId?: string; - button: FrameButton; + label: string; + isExternalLink: boolean; isFullWidth: boolean; isLoading: boolean; isDisabled: boolean; clickHandler: () => void; }) => { const columnWidth = isFullWidth ? "col-span-2" : "col-span-1"; - const isExternal = ["post_redirect", "link"].includes(button.action || ""); - const icon = isExternal ? : null; + const icon = isExternalLink ? : null; return ( @@ -97,7 +98,10 @@ const ButtonsContainer = ({ 0} clickHandler={clickHandler} From e237849e06085b24cfe5ab00478c9539182f3782 Mon Sep 17 00:00:00 2001 From: daria-github Date: Mon, 26 Feb 2024 15:33:21 -0800 Subject: [PATCH 10/14] link updates --- .../components/Mobile/Mobile.tsx | 64 ++++++++++++------- src/locales/en_US.json | 5 -- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/src/component-library/components/Mobile/Mobile.tsx b/src/component-library/components/Mobile/Mobile.tsx index e34c6b9d..7365e9ec 100644 --- a/src/component-library/components/Mobile/Mobile.tsx +++ b/src/component-library/components/Mobile/Mobile.tsx @@ -1,28 +1,44 @@ import { DeviceMobileIcon } from "@heroicons/react/solid"; -import { useTranslation } from "react-i18next"; -export const Mobile = () => { - const { t } = useTranslation(); - return ( -
+const LinkEle = ({ url, text }: { url: string; text: string }) => ( + + {text} + +); + +export const Mobile = () => ( +
+
-

{t("mobile.mobile_detected")}

-

{t("mobile.group_chat_cta")}

- - https://testflight.apple.com/join/xEJOvzEx - -

{t("mobile.reference_app_cta")}

- - https://github.com/xmtp-labs/xmtp-inbox-mobile - +

Looks like you're on mobile!

- ); -}; +

For mobile-friendly XMTP chat:

+
    +
  • + Try group chat on the dev network in Converse Preview: +
  • + + | + +
  • + Try subscription notifications and 1:1 chats in Coinbase Wallet: +
  • + | + +
  • + Devs: Build on the open source reference implementation: +
  • + +
+
+); diff --git a/src/locales/en_US.json b/src/locales/en_US.json index 725040c9..c781f874 100644 --- a/src/locales/en_US.json +++ b/src/locales/en_US.json @@ -62,11 +62,6 @@ "collapse_header": "Collapse", "messages_header": "Messages" }, - "mobile": { - "mobile_detected": "It looks like you may be on a mobile device!", - "group_chat_cta": "Download the Converse TestFlight to try group chat:", - "reference_app_cta": "In addition, please note that our React Native XMTP Reference Application is now live:" - }, "status_messaging": { "error_1_header": "Sorry, the app encountered an error", "error_1_subheader": "Not to worry. Let’s try again. If the error persists, <0>we're here to help!", From 65594d35d85cfef2140e03a9bcb3bde73bf0cd4d Mon Sep 17 00:00:00 2001 From: daria-github Date: Mon, 26 Feb 2024 15:43:28 -0800 Subject: [PATCH 11/14] fixed styling --- .../components/Mobile/Mobile.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/component-library/components/Mobile/Mobile.tsx b/src/component-library/components/Mobile/Mobile.tsx index 7365e9ec..374c17c3 100644 --- a/src/component-library/components/Mobile/Mobile.tsx +++ b/src/component-library/components/Mobile/Mobile.tsx @@ -28,17 +28,16 @@ export const Mobile = () => ( text="Android" />
  • - Try subscription notifications and 1:1 chats in Coinbase Wallet: + Try subscription notifications and 1:1 chats in +
  • - | -
  • - Devs: Build on the open source reference implementation: + Devs: Build on the open source +
  • -
    ); From 98a67f09ee8ce0f57a7780618b0e2be5609a5d99 Mon Sep 17 00:00:00 2001 From: daria-github Date: Mon, 26 Feb 2024 15:55:37 -0800 Subject: [PATCH 12/14] update mobile view for onboarding screen --- src/pages/index.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/pages/index.tsx b/src/pages/index.tsx index e8b96c6a..780c9b90 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -4,9 +4,16 @@ import { useConnectModal } from "@rainbow-me/rainbowkit"; import { useClient } from "@xmtp/react-sdk"; import { useNavigate } from "react-router-dom"; import { OnboardingStep } from "../component-library/components/OnboardingStep/OnboardingStep"; -import { classNames, isAppEnvDemo, wipeKeys } from "../helpers"; +import { + TAILWIND_MD_BREAKPOINT, + classNames, + isAppEnvDemo, + wipeKeys, +} from "../helpers"; import useInitXmtpClient from "../hooks/useInitXmtpClient"; import { useXmtpStore } from "../store/xmtp"; +import { Mobile } from "../component-library/components/Mobile/Mobile"; +import useWindowSize from "../hooks/useWindowSize"; const OnboardingPage = () => { const navigate = useNavigate(); @@ -47,7 +54,11 @@ const OnboardingPage = () => { } }, [status]); - return ( + const size = useWindowSize(); + + return size[0] < TAILWIND_MD_BREAKPOINT ? ( + + ) : (
    Date: Wed, 28 Feb 2024 11:34:15 -0800 Subject: [PATCH 13/14] design fixes --- .../components/Mobile/Mobile.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/component-library/components/Mobile/Mobile.tsx b/src/component-library/components/Mobile/Mobile.tsx index 374c17c3..44f3c519 100644 --- a/src/component-library/components/Mobile/Mobile.tsx +++ b/src/component-library/components/Mobile/Mobile.tsx @@ -1,24 +1,24 @@ -import { DeviceMobileIcon } from "@heroicons/react/solid"; +import xmtpIcon from "../../../../public/xmtp-icon.png"; const LinkEle = ({ url, text }: { url: string; text: string }) => ( {text} ); export const Mobile = () => ( -
    +
    - -

    Looks like you're on mobile!

    + XMTP logo +

    Looks like you're on mobile!

    -

    For mobile-friendly XMTP chat:

    -
      -
    • +

      For mobile-friendly chat:

      +
        +
      • Try group chat on the dev network in Converse Preview:
      • @@ -27,12 +27,12 @@ export const Mobile = () => ( url="https://drive.google.com/file/d/1rUtCmtIB6VzHNW8PDJ1TMBRuI2OEOdcg/view?usp=drive_link" text="Android" /> -
      • +
      • Try subscription notifications and 1:1 chats in
      • -
      • - Devs: Build on the open source +
      • + Devs, build on the open source Date: Wed, 28 Feb 2024 12:01:02 -0800 Subject: [PATCH 14/14] type error fix --- src/component-library/components/Mobile/Mobile.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/component-library/components/Mobile/Mobile.tsx b/src/component-library/components/Mobile/Mobile.tsx index 44f3c519..54ec710a 100644 --- a/src/component-library/components/Mobile/Mobile.tsx +++ b/src/component-library/components/Mobile/Mobile.tsx @@ -13,7 +13,11 @@ const LinkEle = ({ url, text }: { url: string; text: string }) => ( export const Mobile = () => (
        - XMTP logo + XMTP logo

        Looks like you're on mobile!

        For mobile-friendly chat: