From d4572e609e65b1298b73b75013de9728317c465d Mon Sep 17 00:00:00 2001 From: HadiKhai Date: Fri, 6 Dec 2024 15:02:26 +0200 Subject: [PATCH 1/2] feat: xmtp refactor and unread count --- .../NotificationBadge.module.css | 27 + .../ui/src/lib/ui/NotificationBadge/index.tsx | 30 + packages/@justweb3/ui/src/lib/ui/index.ts | 1 + .../stories/ui/notification-badge.stories.tsx | 57 ++ .../lib/components/JustWeb3Button/index.tsx | 35 +- .../lib/components/AllMessageSheet/index.tsx | 56 -- .../src/lib/components/Chat/index.tsx | 618 ----------------- .../{ChatButton => ChatMenuButton}/index.tsx | 48 +- .../ChatSheet/Chat/ChatHeader/index.tsx | 157 +++++ .../ChatMessagesList/DateDivider/index.tsx | 43 ++ .../ChatMessagesList/EmojiSelector/index.tsx | 85 +++ .../ChatMessagesList}/MessageCard/index.tsx | 50 +- .../ChatSheet/Chat/ChatMessagesList/index.tsx | 102 +++ .../Chat/ChatReactionOverlay/index.tsx | 29 + .../Chat/ChatRequestControl/index.tsx | 47 ++ .../ChatTextField/AttachmentButtons/index.tsx | 45 ++ .../ChatTextField/AttachmentPreview/index.tsx | 220 ++++++ .../Chat/ChatTextField/MessageInput/index.tsx | 72 ++ .../Chat/ChatTextField/ReplyPreview/index.tsx | 184 +++++ .../VoiceNoteRecording/index.tsx | 115 ++++ .../ChatSheet/Chat/ChatTextField/index.tsx | 225 ++++++ .../MessageSkeletonCard/index.tsx | 0 .../Chat/LoadingMessagesList/index.tsx | 25 + .../Chat/VideoPlayerPreview/index.tsx | 119 ++++ .../Chat/VoiceNotePreview}/index.tsx | 48 +- .../lib/components/ChatSheet/Chat/index.tsx | 257 +++++++ .../NewChat/NewChatTextField/index.tsx | 68 ++ .../NewChat}/index.tsx | 15 +- .../src/lib/components/ChatSheet/index.tsx | 213 ++---- .../src/lib/components/CustomPlayer/index.tsx | 106 --- .../components/CustomVoicePreview/index.tsx | 86 --- .../lib/components/EmojiSelector/index.tsx | 82 --- .../ChatList}/MessageItem/index.tsx | 87 ++- .../{ => InboxSheet}/ChatList/index.tsx | 24 +- .../src/lib/components/InboxSheet/index.tsx | 274 ++++++++ .../src/lib/components/MessageSheet/index.tsx | 29 - .../lib/components/MessageTextField/index.tsx | 651 ------------------ .../lib/components/NewMessageSheet/index.tsx | 37 - .../index.tsx | 4 +- .../src/lib/content-types/readReceipt.ts | 120 ++++ .../lib/hooks/useGetAudioDuration/index.tsx | 71 +- .../src/lib/hooks/useReadReceipt/index.tsx | 14 + .../src/lib/hooks/useSendMessage/index.ts | 52 +- .../xmtp-plugin/src/lib/plugins/index.tsx | 8 +- .../providers/JustWeb3XMTPProvider/index.tsx | 279 +++++++- 45 files changed, 2900 insertions(+), 2015 deletions(-) create mode 100644 packages/@justweb3/ui/src/lib/ui/NotificationBadge/NotificationBadge.module.css create mode 100644 packages/@justweb3/ui/src/lib/ui/NotificationBadge/index.tsx create mode 100644 packages/@justweb3/ui/src/stories/ui/notification-badge.stories.tsx delete mode 100644 packages/@justweb3/xmtp-plugin/src/lib/components/AllMessageSheet/index.tsx delete mode 100644 packages/@justweb3/xmtp-plugin/src/lib/components/Chat/index.tsx rename packages/@justweb3/xmtp-plugin/src/lib/components/{ChatButton => ChatMenuButton}/index.tsx (64%) create mode 100644 packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatHeader/index.tsx create mode 100644 packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatMessagesList/DateDivider/index.tsx create mode 100644 packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatMessagesList/EmojiSelector/index.tsx rename packages/@justweb3/xmtp-plugin/src/lib/components/{ => ChatSheet/Chat/ChatMessagesList}/MessageCard/index.tsx (93%) create mode 100644 packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatMessagesList/index.tsx create mode 100644 packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatReactionOverlay/index.tsx create mode 100644 packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatRequestControl/index.tsx create mode 100644 packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatTextField/AttachmentButtons/index.tsx create mode 100644 packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatTextField/AttachmentPreview/index.tsx create mode 100644 packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatTextField/MessageInput/index.tsx create mode 100644 packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatTextField/ReplyPreview/index.tsx create mode 100644 packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatTextField/VoiceNoteRecording/index.tsx create mode 100644 packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatTextField/index.tsx rename packages/@justweb3/xmtp-plugin/src/lib/components/{ => ChatSheet/Chat/LoadingMessagesList}/MessageSkeletonCard/index.tsx (100%) create mode 100644 packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/LoadingMessagesList/index.tsx create mode 100644 packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/VideoPlayerPreview/index.tsx rename packages/@justweb3/xmtp-plugin/src/lib/components/{VoiceMessageCard => ChatSheet/Chat/VoiceNotePreview}/index.tsx (81%) create mode 100644 packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/index.tsx create mode 100644 packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/NewChat/NewChatTextField/index.tsx rename packages/@justweb3/xmtp-plugin/src/lib/components/{NewConversation => ChatSheet/NewChat}/index.tsx (96%) delete mode 100644 packages/@justweb3/xmtp-plugin/src/lib/components/CustomPlayer/index.tsx delete mode 100644 packages/@justweb3/xmtp-plugin/src/lib/components/CustomVoicePreview/index.tsx delete mode 100644 packages/@justweb3/xmtp-plugin/src/lib/components/EmojiSelector/index.tsx rename packages/@justweb3/xmtp-plugin/src/lib/components/{ => InboxSheet/ChatList}/MessageItem/index.tsx (56%) rename packages/@justweb3/xmtp-plugin/src/lib/components/{ => InboxSheet}/ChatList/index.tsx (60%) create mode 100644 packages/@justweb3/xmtp-plugin/src/lib/components/InboxSheet/index.tsx delete mode 100644 packages/@justweb3/xmtp-plugin/src/lib/components/MessageSheet/index.tsx delete mode 100644 packages/@justweb3/xmtp-plugin/src/lib/components/MessageTextField/index.tsx delete mode 100644 packages/@justweb3/xmtp-plugin/src/lib/components/NewMessageSheet/index.tsx rename packages/@justweb3/xmtp-plugin/src/lib/components/{ChatWithProfileButton => ProfileChatButton}/index.tsx (96%) create mode 100644 packages/@justweb3/xmtp-plugin/src/lib/content-types/readReceipt.ts create mode 100644 packages/@justweb3/xmtp-plugin/src/lib/hooks/useReadReceipt/index.tsx diff --git a/packages/@justweb3/ui/src/lib/ui/NotificationBadge/NotificationBadge.module.css b/packages/@justweb3/ui/src/lib/ui/NotificationBadge/NotificationBadge.module.css new file mode 100644 index 00000000..d5c0f5a4 --- /dev/null +++ b/packages/@justweb3/ui/src/lib/ui/NotificationBadge/NotificationBadge.module.css @@ -0,0 +1,27 @@ +.notificationIcon { + position: relative; + display: inline-block; + +} + +.icon { + font-size: 24px; /* Adjust icon size */ + line-height: 40px; /* Center the icon vertically */ + text-align: center; +} + +.badge { + position: absolute; + top: 0; + right: 4px; + background-color: red; + color: white; + font-size: 8px; + font-weight: bold; + line-height: 1; + border-radius: 100px; + padding: 2px 4px; + transform: translate(50%, -50%); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + white-space: nowrap; +} diff --git a/packages/@justweb3/ui/src/lib/ui/NotificationBadge/index.tsx b/packages/@justweb3/ui/src/lib/ui/NotificationBadge/index.tsx new file mode 100644 index 00000000..b83fe06d --- /dev/null +++ b/packages/@justweb3/ui/src/lib/ui/NotificationBadge/index.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import styles from './NotificationBadge.module.css'; +import { SPAN } from '../Text'; + +interface NotificationBadgeProps { + count: number; + icon?: React.ReactNode; + maxCount?: number; +} + +export const NotificationBadge: React.FC = ({ + count, + icon, + maxCount = 99, +}) => { + return ( +
+ {/*
{icon}
*/} + {icon} + {/*{count > 0 && {count}}*/} + {count > 0 && ( + + {count > maxCount ? `${maxCount}+` : count} + + )} +
+ ); +}; + +export default NotificationBadge; diff --git a/packages/@justweb3/ui/src/lib/ui/index.ts b/packages/@justweb3/ui/src/lib/ui/index.ts index 3cd6c64c..53e68816 100644 --- a/packages/@justweb3/ui/src/lib/ui/index.ts +++ b/packages/@justweb3/ui/src/lib/ui/index.ts @@ -18,3 +18,4 @@ export * from './Sheet'; export * from './Label'; export * from './Checkbox'; export * from './Skeleton'; +export * from './NotificationBadge'; diff --git a/packages/@justweb3/ui/src/stories/ui/notification-badge.stories.tsx b/packages/@justweb3/ui/src/stories/ui/notification-badge.stories.tsx new file mode 100644 index 00000000..7477aa44 --- /dev/null +++ b/packages/@justweb3/ui/src/stories/ui/notification-badge.stories.tsx @@ -0,0 +1,57 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { NotificationBadge } from '../../lib/ui'; +import React from 'react'; + +const meta: Meta = { + component: NotificationBadge, + title: 'Design System/UI/NotificationBadge', + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +const ChatIcon = () => { + return ( + + + + ); +}; + +export const Normal: Story = { + args: { + count: 1, + icon: , + }, +}; + +export const Zero: Story = { + args: { + count: 0, + icon: , + }, +}; + +export const Many: Story = { + args: { + count: 50, + icon: , + }, +}; + +export const ManyWithMax: Story = { + args: { + count: 1000, + icon: , + }, +}; diff --git a/packages/@justweb3/widget/src/lib/components/JustWeb3Button/index.tsx b/packages/@justweb3/widget/src/lib/components/JustWeb3Button/index.tsx index c16067a0..2c10fd20 100644 --- a/packages/@justweb3/widget/src/lib/components/JustWeb3Button/index.tsx +++ b/packages/@justweb3/widget/src/lib/components/JustWeb3Button/index.tsx @@ -35,12 +35,14 @@ import styles from './JustWeb3Button.module.css'; export interface JustWeb3Buttonrops { children: ReactNode; + style?: React.CSSProperties; logout?: () => void; } export const JustWeb3Button: FC = ({ children, logout, + style, }) => { const [openMApps, setOpenMApps] = useState(false); const { plugins, mApps, config } = useContext(JustWeb3Context); @@ -89,6 +91,9 @@ export const JustWeb3Button: FC = ({ = ({ onClick={() => { handleOpenSignInDialog(true); }} + style={{ + ...style, + }} left={} right={ = ({ style={{ backgroundColor: 'var(--justweb3-background-color)', color: 'var(--justweb3-primary-color)', + ...style, }} contentStyle={{ alignItems: 'start', @@ -187,32 +196,6 @@ export const JustWeb3Button: FC = ({ const connectedEnsProfileContent = ( - {/**/} - {/* */} - {/* Profile Overview*/} - {/*

*/} - {/* */} - {/* }*/} - {/* onClick={() => {*/} - {/* openEnsProfile(connectedEns?.ens, connectedEns?.chainId);*/} - {/* }}*/} - {/* >*/} - {/* View Full Profile*/} - {/* */} - {/*
*/} - {/* Profile */} | string | null; - openChat: boolean; - closeChat: () => void; - onChangePeer: ( - peer: CachedConversation | string - ) => void; -} - -export const AllMessageSheet: React.FC = ({ - peer, - closeChat, - openChat, - onChangePeer, -}) => { - const isPeerConversation = useMemo(() => { - return typeof peer !== 'string'; - }, [peer]); - - return ( - !open && closeChat()}> - - - {isPeerConversation ? 'Messages' : 'New Conversation'} - - - {peer !== null && - (isPeerConversation ? ( - } - onBack={closeChat} - /> - ) : ( - { - onChangePeer(conversation); - }} - /> - ))} - - - ); -}; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/Chat/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/Chat/index.tsx deleted file mode 100644 index 3f454e35..00000000 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/Chat/index.tsx +++ /dev/null @@ -1,618 +0,0 @@ -import { - useEnsAvatar, - useMountedAccount, - usePrimaryName, - useRecords, -} from '@justaname.id/react'; -import { - ArrowIcon, - Avatar, - BlockedAccountIcon, - Button, - Flex, - LoadingSpinner, - P, - Popover, - PopoverContent, - PopoverTrigger, - TuneIcon, -} from '@justweb3/ui'; -import { - CachedConversation, - ContentTypeMetadata, - useCanMessage, - useConsent, - useMessages, - useStreamMessages, -} from '@xmtp/react-sdk'; -import React, { useEffect, useMemo } from 'react'; -import { useSendReactionMessage } from '../../hooks'; -import { typeLookup } from '../../utils/attachments'; -import { - filterReactionsMessages, - MessageWithReaction, -} from '../../utils/filterReactionsMessages'; -import { formatAddress } from '../../utils/formatAddress'; -import { groupMessagesByDate } from '../../utils/groupMessageByDate'; -import EmojiSelector from '../EmojiSelector'; -import MessageCard from '../MessageCard'; -import { MessageSkeletonCard } from '../MessageSkeletonCard'; -import MessageTextField from '../MessageTextField'; -import { useJustWeb3 } from '@justweb3/widget'; - -export interface ChatProps { - conversation: CachedConversation; - onBack: () => void; -} - -export const Chat: React.FC = ({ conversation, onBack }) => { - const { openEnsProfile } = useJustWeb3(); - const [replyMessage, setReplyMessage] = - React.useState(null); - const [reactionMessage, setReactionMessage] = - React.useState(null); - const [isRequest, setIsRequest] = React.useState(false); - const [isRequestChangeLoading, setIsRequestChangeLoading] = - React.useState(false); - const { entries, allow, refreshConsentList, deny } = useConsent(); - const { mutateAsync: sendReaction } = useSendReactionMessage(conversation); - - const { primaryName } = usePrimaryName({ - address: conversation.peerAddress as `0x${string}`, - }); - const { records } = useRecords({ - ens: primaryName, - }); - const { sanitizeEnsImage } = useEnsAvatar(); - - const { address } = useMountedAccount(); - - const [canMessage, setCanMessage] = React.useState(true); - - const { messages, isLoading } = useMessages(conversation); - - // Queries - - const blockAddress = async (peerAddress: string) => { - setIsRequestChangeLoading(true); - await refreshConsentList(); - await deny([peerAddress]); - await refreshConsentList(); - setIsRequestChangeLoading(false); - onBack(); - }; - - const { canMessage: canMessageFn, isLoading: isCanMessageLoading } = - useCanMessage(); - - useEffect(() => { - if (isCanMessageLoading) return; - canMessageFn(conversation.peerAddress).then((result) => { - setCanMessage(result); - }); - }, [isCanMessageLoading, conversation, canMessageFn]); - - useStreamMessages(conversation); - - // Memo - const filteredMessages = useMemo(() => { - const messagesWithoutRead = messages.filter( - (message) => !(message.contentType === 'xmtp.org/readReceipt:1.0') - ); - const res = filterReactionsMessages(messagesWithoutRead); - return res; - }, [messages]); - - const groupedMessages = useMemo(() => { - return groupMessagesByDate(filteredMessages ?? []); - }, [filteredMessages]); - - useEffect(() => { - const convoConsentState = entries[conversation.peerAddress]?.permissionType; - if (convoConsentState === 'unknown' || convoConsentState === undefined) { - setIsRequest(true); - } else { - setIsRequest(false); - } - }, [entries, conversation.peerAddress]); - - const isMessagesSenderOnly = useMemo(() => { - return filteredMessages.every( - (message) => message.senderAddress === address - ); - }, [filteredMessages, address]); - - const handleAllowAddress = async () => { - setIsRequestChangeLoading(true); - await refreshConsentList(); - await allow([conversation.peerAddress]); - void refreshConsentList(); - setIsRequest(false); - setIsRequestChangeLoading(false); - }; - - const handleEmojiSelect = (emoji: string) => { - if (!reactionMessage) return; - sendReaction({ - action: 'added', - content: emoji, - referenceId: reactionMessage.id, - }); - }; - - useEffect(() => { - if (messages.length == 0) return; - setTimeout(() => { - const lastMessageId = messages[messages.length - 1]?.id; - const element = document.getElementById(lastMessageId); - if (element) { - element.scrollIntoView({ behavior: 'smooth' }); - } - }, 500); - - // await checkMessageIfRead(); - }, [messages, conversation]); - - const isStringContent = - typeof replyMessage?.content === 'string' || - typeof replyMessage?.content?.content === 'string'; - - const mimeType = replyMessage?.content?.mimeType; - const type = mimeType ? typeLookup[mimeType.split('/')?.[1]] : null; - - const computeHeight = useMemo(() => { - const additionalHeight = []; - const height = '100vh - 50px - 3rem - 1.5rem - 73px - 15px'; - if (isRequest) { - additionalHeight.push('-40px'); - } - if (replyMessage) { - if (isStringContent) { - additionalHeight.push('46px'); - } else if (mimeType === 'audio/wav') { - additionalHeight.push('61px'); - } else if (type === 'video' || type === 'image') { - additionalHeight.push('116px'); - } else { - additionalHeight.push('47px'); - } - } - - if (isMessagesSenderOnly) { - additionalHeight.push('59px'); - } - - return `calc( ${height} ${ - additionalHeight.length > 0 ? ' - ' + additionalHeight.join(' - ') : '' - } )`; - }, [ - replyMessage, - isMessagesSenderOnly, - isStringContent, - mimeType, - type, - isRequest, - ]); - - return ( - -
{ - setReactionMessage(null); - const replica = document.getElementById( - `${reactionMessage?.id}-replica` - ); - replica?.remove(); - }} - >
- - - - - - { - if (primaryName) { - openEnsProfile(primaryName); - } - }} - > - - -

- {primaryName - ? primaryName - : formatAddress(conversation.peerAddress)} -

- {primaryName && ( -

- {formatAddress(conversation.peerAddress)} -

- )} -
-
-
- - - - - - - - blockAddress(conversation.peerAddress)} - > - -

- Block -

-
-
-
-
-
-
- - {isCanMessageLoading || isLoading ? ( - - {[...Array(8)].map((_, index) => ( - - ))} - - ) : ( - - {canMessage ? ( - - {groupedMessages && - Object.keys(groupedMessages).map((date, index) => ( - - -
-

- {date} -

-
- - {groupedMessages[date].map((message) => ( - setReplyMessage(msg)} - message={message} - peerAddress={conversation.peerAddress} - key={`message-${message.id}`} - onReaction={(message) => { - setReactionMessage(message); - - const element = document.getElementById( - message.id.toString() - ); - if (!element) return; - const replica = element?.cloneNode( - true - ) as HTMLElement; - replica.id = `${message.id}-replica`; - replica.style.position = 'absolute'; - replica.style.bottom = '310px'; - replica.style.minHeight = '20px'; - replica.style.left = '4.2vw'; - replica.style.zIndex = '90'; - element?.parentElement?.appendChild(replica); - replica.classList.add('replica-animate'); - }} - /> - ))} - - ))} - {reactionMessage && ( -
- { - handleEmojiSelect(emoji); - setReactionMessage(null); - const replica = document.getElementById( - `${reactionMessage?.id}-replica` - ); - replica?.remove(); - }} - /> -
- )} - - ) : ( -
-

Cannot message {conversation.peerAddress}

-
- )} - - )} - - {isRequest ? ( - isRequestChangeLoading ? ( - - - - ) : ( - - - - - ) - ) : ( - - {isMessagesSenderOnly && ( - -

- Message in user’s Requests -

-

- This user has not accepted your message request yet -

-
- )} - setReplyMessage(null)} - conversation={conversation} - replyMessage={replyMessage} - peerAddress={conversation.peerAddress} - /> -
- )} - - - ); -}; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatButton/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatMenuButton/index.tsx similarity index 64% rename from packages/@justweb3/xmtp-plugin/src/lib/components/ChatButton/index.tsx rename to packages/@justweb3/xmtp-plugin/src/lib/components/ChatMenuButton/index.tsx index 4c39d872..6e209868 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatButton/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatMenuButton/index.tsx @@ -1,15 +1,26 @@ import { useMountedAccount } from '@justaname.id/react'; -import { ArrowIcon, ClickableItem } from '@justweb3/ui'; +import { ArrowIcon, ClickableItem, SPAN } from '@justweb3/ui'; import { Client, ClientOptions, useClient } from '@xmtp/react-sdk'; import { useEthersSigner } from '../../hooks'; import { XmtpEnvironment } from '../../plugins'; import { loadKeys, storeKeys, wipeKeys } from '../../utils/xmtp'; +import { useJustWeb3XMTP } from '../../providers/JustWeb3XMTPProvider'; +import { useMemo } from 'react'; -export interface ChatButtonProps { +export interface ChatMenuButtonProps { handleOpen: (open: boolean) => void; env: XmtpEnvironment; } -export const ChatButton: React.FC = ({ handleOpen, env }) => { +export const ChatMenuButton: React.FC = ({ + handleOpen, + env, +}) => { + const { conversationsInfo } = useJustWeb3XMTP(); + const totalUnreadCount = useMemo(() => { + return conversationsInfo + .filter((conversation) => conversation.consent === 'allowed') + .reduce((acc, curr) => acc + curr.unreadCount, 0); + }, [conversationsInfo]); const { initialize } = useClient(); const { client } = useClient(); const walletClient = useEthersSigner(); @@ -74,7 +85,36 @@ export const ChatButton: React.FC = ({ handleOpen, env }) => { width: '100%', }} onClick={handleChat} - right={} + right={ + <> + {totalUnreadCount > 0 && ( +
+ + {totalUnreadCount} + +
+ )} + + + } /> ); }; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatHeader/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatHeader/index.tsx new file mode 100644 index 00000000..2d90fed0 --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatHeader/index.tsx @@ -0,0 +1,157 @@ +import { + ArrowIcon, + Avatar, + BlockedAccountIcon, + Flex, + P, + Popover, + PopoverContent, + PopoverTrigger, + TuneIcon, +} from '@justweb3/ui'; +import { formatAddress } from '../../../../utils/formatAddress'; +import { useEnsAvatar } from '@justaname.id/react'; + +export interface ChatHeaderProps { + primaryName: string | undefined; + peerAddress: string; + onBack: () => void; + openEnsProfile: (ens: string) => void; + records: any; + blockAddressHandler: (peerAddress: string) => void; +} + +export const ChatHeader: React.FC = ({ + primaryName, + peerAddress, + onBack, + openEnsProfile, + records, + blockAddressHandler, +}) => { + const { sanitizeEnsImage } = useEnsAvatar(); + + return ( + + + + + + { + if (primaryName) openEnsProfile(primaryName); + }} + > + + +

+ {primaryName || formatAddress(peerAddress)} +

+ {primaryName && ( +

+ {formatAddress(peerAddress)} +

+ )} +
+
+
+ + + + + + + + + blockAddressHandler(peerAddress)} + > + +

+ Block +

+
+
+
+
+
+
+ ); +}; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatMessagesList/DateDivider/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatMessagesList/DateDivider/index.tsx new file mode 100644 index 00000000..6b0b7307 --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatMessagesList/DateDivider/index.tsx @@ -0,0 +1,43 @@ +import { Flex, P } from '@justweb3/ui'; + +interface DateDividerProps { + date: string; +} + +export const DateDivider: React.FC = ({ date }) => ( + +
+

+ {date} +

+
+ +); diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatMessagesList/EmojiSelector/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatMessagesList/EmojiSelector/index.tsx new file mode 100644 index 00000000..4dd8f6ba --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatMessagesList/EmojiSelector/index.tsx @@ -0,0 +1,85 @@ +import { Flex, Input } from '@justweb3/ui'; +import React, { useMemo } from 'react'; +import { useDebounce } from '@justweb3/widget'; +import { EmojiObject, emojis } from '../../../../../utils/emojis'; + +interface EmojiSelectorProps { + onEmojiSelect: (emoji: string) => void; +} + +export const EmojiSelector: React.FC = ({ + onEmojiSelect, +}) => { + const [searchValue, setSearchValue] = React.useState(''); + + const { debouncedValue: debouncedSearch } = useDebounce( + searchValue, + 50 + ); + + const onEmojiClickHandler = (emoji: EmojiObject) => { + onEmojiSelect(emoji.name); + }; + + const filteredEmojis = useMemo(() => { + if (debouncedSearch.length === 0) return emojis; + const filteredEmojis = emojis.filter((emoji) => + emoji.name.toLowerCase().includes(debouncedSearch.toLowerCase()) + ); + return filteredEmojis; + }, [debouncedSearch]); + + return ( + + setSearchValue(e.target.value)} + /> +
+ {filteredEmojis.map((emoji, index) => ( + + ))} +
+
+ ); +}; + +export default EmojiSelector; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/MessageCard/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatMessagesList/MessageCard/index.tsx similarity index 93% rename from packages/@justweb3/xmtp-plugin/src/lib/components/MessageCard/index.tsx rename to packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatMessagesList/MessageCard/index.tsx index 0630fb58..8ad5ce6c 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/MessageCard/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatMessagesList/MessageCard/index.tsx @@ -9,15 +9,15 @@ import { } from '@justweb3/ui'; import { CachedConversation, DecodedMessage } from '@xmtp/react-sdk'; import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { useSendReactionMessage } from '../../hooks'; -import { typeLookup } from '../../utils/attachments'; -import { calculateFileSize } from '../../utils/calculateFileSize'; -import { findEmojiByName } from '../../utils/emojis'; -import { MessageWithReaction } from '../../utils/filterReactionsMessages'; -import { formatAddress } from '../../utils/formatAddress'; -import { formatMessageSentTime } from '../../utils/messageTimeFormat'; -import { CustomPlayer } from '../CustomPlayer'; -import VoiceMessageCard from '../VoiceMessageCard'; +import { useSendReactionMessage } from '../../../../../hooks'; +import { typeLookup } from '../../../../../utils/attachments'; +import { calculateFileSize } from '../../../../../utils/calculateFileSize'; +import { findEmojiByName } from '../../../../../utils/emojis'; +import { MessageWithReaction } from '../../../../../utils/filterReactionsMessages'; +import { formatAddress } from '../../../../../utils/formatAddress'; +import { formatMessageSentTime } from '../../../../../utils/messageTimeFormat'; +import VoiceNotePreview from '../../VoiceNotePreview'; +import { VideoPlayerPreview } from '../../VideoPlayerPreview'; interface MessageCardProps { message: MessageWithReaction; @@ -107,7 +107,7 @@ const MeasureAndHyphenateText: React.FC<{ ); }; -const MessageCard: React.FC = ({ +export const MessageCard: React.FC = ({ message, peerAddress, onReply, @@ -259,17 +259,17 @@ const MessageCard: React.FC = ({ senderAddress: message.senderAddress, content: typeLookup[attachmentExtention] === 'image' || - typeLookup[attachmentExtention] === 'video' + typeLookup[attachmentExtention] === 'video' ? { - data: message.content.data, - mimeType: message.content.mimeType, - filename: message.content.filename, - url: URL.createObjectURL( - new Blob([message.content.data], { - type: message.content.mimeType, - }) - ), - } + data: message.content.data, + mimeType: message.content.mimeType, + filename: message.content.filename, + url: URL.createObjectURL( + new Blob([message.content.data], { + type: message.content.mimeType, + }) + ), + } : message.content, contentType: message.contentType, })} @@ -317,7 +317,7 @@ const MessageCard: React.FC = ({ {repliedMessage?.senderAddress === address ? 'YOU' : primaryName ?? - formatAddress(repliedMessage?.senderAddress ?? '')} + formatAddress(repliedMessage?.senderAddress ?? '')}

{isReplyText || isReplyReply ? ( @@ -340,7 +340,7 @@ const MessageCard: React.FC = ({ : repliedMessage.content}

) : isReplyVoice ? ( - = ({ }} /> ) : typeLookup[replyAttachmentExtention] === 'video' ? ( - = ({ ) : ( {isVoice ? ( - + ) : ( {typeLookup[attachmentExtention] === 'image' ? ( @@ -469,7 +469,7 @@ const MessageCard: React.FC = ({
) : typeLookup[attachmentExtention] === 'video' ? ( - ; + setReplyMessage: (msg: MessageWithReaction | null) => void; + setReactionMessage: (msg: MessageWithReaction | null) => void; + reactionMessage: MessageWithReaction | null; + handleEmojiSelect: (emoji: string) => void; + computeHeight: string; +} + +export const ChatMessagesList: React.FC = ({ + canMessage, + groupedMessages, + conversation, + setReplyMessage, + setReactionMessage, + reactionMessage, + handleEmojiSelect, + computeHeight, +}) => { + if (!canMessage) { + return ( + +

Cannot message {conversation.peerAddress}

+
+ ); + } + + return ( + + + {groupedMessages && + Object.keys(groupedMessages).map((date, index) => ( + + + {groupedMessages[date].map((message) => ( + { + setReactionMessage(msg); + const element = document.getElementById(msg.id); + if (!element) return; + const replica = element.cloneNode(true) as HTMLElement; + replica.id = `${msg.id}-replica`; + replica.style.position = 'absolute'; + replica.style.bottom = '310px'; + replica.style.minHeight = '20px'; + replica.style.left = '4.2vw'; + replica.style.zIndex = '90'; + element.parentElement?.appendChild(replica); + replica.classList.add('replica-animate'); + }} + /> + ))} + + ))} + + {reactionMessage && ( +
+ +
+ )} +
+
+ ); +}; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatReactionOverlay/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatReactionOverlay/index.tsx new file mode 100644 index 00000000..46ede4df --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatReactionOverlay/index.tsx @@ -0,0 +1,29 @@ +import { MessageWithReaction } from '../../../utils/filterReactionsMessages'; + +interface ChatReactionOverlayProps { + reactionMessage: MessageWithReaction | null; + onOverlayClick: () => void; +} + +export const ChatReactionOverlay: React.FC = ({ + reactionMessage, + onOverlayClick, +}) => ( +
+); diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatRequestControl/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatRequestControl/index.tsx new file mode 100644 index 00000000..26d1497b --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatRequestControl/index.tsx @@ -0,0 +1,47 @@ +import { Button, Flex, LoadingSpinner } from '@justweb3/ui'; + +interface ChatRequestControlsProps { + isRequestChangeLoading: boolean; + blockAddressHandler: (peerAddress: string) => void; + peerAddress: string; + handleAllowAddress: () => void; +} + +export const ChatRequestControls: React.FC = ({ + isRequestChangeLoading, + blockAddressHandler, + peerAddress, + handleAllowAddress, +}) => { + if (isRequestChangeLoading) { + return ( + + + + ); + } + + return ( + + + + + ); +}; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatTextField/AttachmentButtons/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatTextField/AttachmentButtons/index.tsx new file mode 100644 index 00000000..5958e761 --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatTextField/AttachmentButtons/index.tsx @@ -0,0 +1,45 @@ +// ChatTextField/AttachmentButtons.tsx +import React, { RefObject } from 'react'; +import { AddFolderIcon, AddImageIcon, AddVideoIcon, Flex } from '@justweb3/ui'; + +export interface AttachmentButtonsProps { + onButtonClick: (contentType: 'image' | 'video' | 'application') => void; + acceptedTypes: string | string[] | undefined; + onAttachmentChange: (e: React.ChangeEvent) => void; +} + +export const AttachmentButtons: React.FC< + AttachmentButtonsProps & { inputFileRef: RefObject } +> = ({ onButtonClick, acceptedTypes, onAttachmentChange, inputFileRef }) => ( + + onButtonClick('image')} + style={{ cursor: 'pointer' }} + /> + onButtonClick('video')} + style={{ cursor: 'pointer' }} + /> + onButtonClick('application')} + style={{ cursor: 'pointer' }} + /> + + +); diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatTextField/AttachmentPreview/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatTextField/AttachmentPreview/index.tsx new file mode 100644 index 00000000..71abf76e --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatTextField/AttachmentPreview/index.tsx @@ -0,0 +1,220 @@ +// ChatTextField/AttachmentPreview.tsx +import React from 'react'; +import { + Button, + DocumentIcon, + Flex, + P, + SendIcon, + StopIcon, +} from '@justweb3/ui'; +import { VideoPlayerPreview } from '../../VideoPlayerPreview'; +import { VoiceNoteRecording } from './../VoiceNoteRecording'; +import { typeLookup } from '../../../../../utils/attachments'; +import { Attachment } from '@xmtp/content-type-remote-attachment'; + +export interface AttachmentPreviewProps { + attachment: Attachment | undefined; + attachmentPreview: string | undefined; + disabled?: boolean; + onCancelAttachment: () => void; + onSendAttachment: () => void; + recording: boolean; + recordingValue: string | null; + stopRecording: () => void; + pause: () => void; + reset: () => void; +} + +export const AttachmentPreview: React.FC = ({ + attachment, + attachmentPreview, + disabled, + onCancelAttachment, + onSendAttachment, + recording, + recordingValue, + stopRecording, + pause, + reset, +}) => { + const attachmentExtention = attachment?.mimeType.split('/')?.[1] || ''; + + if (attachmentPreview) { + return ( + + {attachment?.mimeType !== 'audio/wav' && ( +
+ )} + {attachment?.mimeType === 'audio/wav' ? ( + + ) : ( + + {typeLookup[attachmentExtention] === 'image' ? ( + {attachment?.filename} + ) : typeLookup[attachmentExtention] === 'video' ? ( + + ) : ( + + +

+ {attachment?.filename ?? 'Cannot preview'} +

+
+ )} + + + + +
+ )} + { + if (!disabled) onSendAttachment(); + }} + /> + + ); + } else if (recording) { + return ( + +

+ {recordingValue} +

+

+ RECORDING... +

+ { + stopRecording(); + pause(); + reset(); + }} + /> +
+ ); + } + + return null; +}; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatTextField/MessageInput/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatTextField/MessageInput/index.tsx new file mode 100644 index 00000000..c9c61261 --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatTextField/MessageInput/index.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { Input, MicIcon, SendIcon } from '@justweb3/ui'; + +interface MessageInputProps { + replyMessage: boolean; + disabled?: boolean; + messageValue: string; + setMessageValue: (value: string) => void; + handleSendMessage: () => void; + startRecording: () => void; + start: () => void; +} + +export const MessageInput: React.FC = ({ + replyMessage, + disabled, + messageValue, + setMessageValue, + handleSendMessage, + startRecording, + start, +}) => { + return ( + { + if (disabled) return; + startRecording(); + start(); + }} + /> + ) + } + right={ + + } + disabled={disabled} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleSendMessage(); + } + }} + onChange={(e) => setMessageValue(e.target.value)} + /> + ); +}; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatTextField/ReplyPreview/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatTextField/ReplyPreview/index.tsx new file mode 100644 index 00000000..7445ea5a --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatTextField/ReplyPreview/index.tsx @@ -0,0 +1,184 @@ +// ChatTextField/ReplyPreview.tsx +import React, { useMemo } from 'react'; +import { CloseIcon, DocumentIcon, Flex, P } from '@justweb3/ui'; +import VoiceNotePreview from '../../VoiceNotePreview'; +import { VideoPlayerPreview } from '../../VideoPlayerPreview'; +import { formatAddress } from '../../../../../utils/formatAddress'; +import { typeLookup } from '../../../../../utils/attachments'; +import { MessageWithReaction } from '../../../../../utils/filterReactionsMessages'; + +export interface ReplyPreviewProps { + replyMessage: MessageWithReaction | null; + onCancelReply: () => void; + isSender: boolean; + primaryName: string | null | undefined; + navigateToRepliedMessage: () => void; +} + +export const ReplyPreview: React.FC = ({ + replyMessage, + onCancelReply, + isSender, + primaryName, + navigateToRepliedMessage, +}) => { + const isReplyText = useMemo(() => { + if (!replyMessage) return false; + return typeof replyMessage.content === 'string'; + }, [replyMessage]); + + const isReplyReply = useMemo(() => { + if (!replyMessage) return false; + return !!replyMessage.content.reference; + }, [replyMessage]); + + const isReplyVoice = useMemo( + () => replyMessage?.content.mimeType === 'audio/wav', + [replyMessage] + ); + + const replyAttachmentExtension = useMemo(() => { + if (!isReplyText && replyMessage && !isReplyReply) { + return replyMessage.content.mimeType.split('/')?.[1] || ''; + } + }, [isReplyText, replyMessage, isReplyReply]); + + const isReplyVideoOrImage = useMemo(() => { + return ( + typeLookup[replyAttachmentExtension] === 'image' || + typeLookup[replyAttachmentExtension] === 'video' + ); + }, [replyAttachmentExtension]); + + if (!replyMessage) return null; + + return ( + + +

+ {isSender + ? 'YOU' + : primaryName ?? formatAddress(replyMessage.senderAddress)} +

+ {isReplyText || isReplyReply ? ( +

+ {isReplyReply ? replyMessage.content.content : replyMessage.content} +

+ ) : isReplyVoice ? ( + + ) : typeLookup[replyAttachmentExtension] === 'image' ? ( + {replyMessage.content.filename} + ) : typeLookup[replyAttachmentExtension] === 'video' ? ( + + ) : ( + + +

+ {replyMessage.content.filename} +

+
+ )} +
+ +
+ ); +}; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatTextField/VoiceNoteRecording/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatTextField/VoiceNoteRecording/index.tsx new file mode 100644 index 00000000..33cf8749 --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatTextField/VoiceNoteRecording/index.tsx @@ -0,0 +1,115 @@ +import { CloseIcon, Flex, P, PauseIcon, PlayIcon } from '@justweb3/ui'; +import React, { useEffect, useRef, useState } from 'react'; +import { formatTime } from '../../../../../utils/formatVoiceTime'; +import { useGetAudioDuration } from '../../../../../hooks/useGetAudioDuration'; + +interface VoiceNoteRecordingProps { + audioUrl: string; + onCancel: () => void; +} + +export const VoiceNoteRecording: React.FC = ({ + audioUrl, + onCancel, +}) => { + const [playing, setPlaying] = useState(false); + const [currentTime, setCurrentTime] = useState(0); + const audioRef = useRef(new Audio()); + + const audioDuration = useGetAudioDuration(audioUrl); + + useEffect(() => { + const audio = new Audio(); + audioRef.current = audio; + + const onTimeUpdate = () => { + setCurrentTime(audio.currentTime); + }; + + const onEnded = () => { + setPlaying(false); + setCurrentTime(0); + }; + + audio.src = audioUrl; + audio.addEventListener('timeupdate', onTimeUpdate); + audio.addEventListener('ended', onEnded); + + return () => { + audio.removeEventListener('timeupdate', onTimeUpdate); + audio.removeEventListener('ended', onEnded); + }; + }, [audioUrl]); + + const handlePlayPause = () => { + const audio = audioRef.current; + if (!audio) return; + + if (playing) { + audio.pause(); + } else { + audio.play(); + } + setPlaying(!playing); + }; + + return ( + + {playing ? ( + + ) : ( + + )} +

+ {playing || currentTime > 0 + ? formatTime(currentTime) + : formatTime(audioDuration ?? 0)} +

+ +
+ ); +}; + +export default VoiceNoteRecording; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatTextField/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatTextField/index.tsx new file mode 100644 index 00000000..6698b6ab --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatTextField/index.tsx @@ -0,0 +1,225 @@ +// ChatTextField/index.tsx +import React, { + Dispatch, + SetStateAction, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { + CachedConversation, + ContentTypeMetadata, + useClient, +} from '@xmtp/react-sdk'; +import { useMountedAccount, usePrimaryName } from '@justaname.id/react'; +import { Flex, P } from '@justweb3/ui'; +import { AttachmentButtons } from './AttachmentButtons'; +import { ReplyPreview } from './ReplyPreview'; +import { AttachmentPreview } from './AttachmentPreview'; +import { MessageInput } from './MessageInput'; +import { + useAttachmentChange, + useRecordingTimer, + useRecordVoice, + useSendAttachment, + useSendMessages, + useSendReplyMessage, +} from '../../../../hooks'; +import { AttachmentType, typeLookup } from '../../../../utils/attachments'; +import type { Attachment } from '@xmtp/content-type-remote-attachment'; +import { MessageWithReaction } from '../../../../utils/filterReactionsMessages'; + +export interface ChatTextFieldProps { + isMessagesSenderOnly: boolean; + replyMessage: MessageWithReaction | null; + conversation: CachedConversation; + peerAddress: string; + onCancelReply: () => void; + disabled?: boolean; + style?: React.CSSProperties; +} + +export const ChatTextField: React.FC = ({ + isMessagesSenderOnly, + replyMessage, + conversation, + peerAddress, + onCancelReply, + disabled, + style, +}) => { + const [messageValue, setMessageValue] = useState(''); + const [attachment, setAttachment] = useState(); + const [attachmentPreview, setAttachmentPreview] = useState< + string | undefined + >(); + const { client } = useClient(); + const { address } = useMountedAccount(); + const { mutateAsync: sendMessage } = useSendMessages(conversation); + const { mutateAsync: sendReply } = useSendReplyMessage(conversation); + const { mutateAsync: sendAttachment } = useSendAttachment(conversation); + const { primaryName } = usePrimaryName({ + address: peerAddress as `0x${string}`, + }); + + const [acceptedTypes, setAcceptedTypes]: [ + string | string[] | undefined, + Dispatch> + ] = useState(); + const inputFile = useRef(null); + + const { onAttachmentChange } = useAttachmentChange({ + setAttachment, + setAttachmentPreview, + }); + + const { recording, startRecording, stopRecording } = useRecordVoice({ + setAttachment, + setAttachmentPreview, + }); + const { start, pause, reset, recordingValue } = useRecordingTimer({ + stopRecording, + status: recording ? 'recording' : 'idle', + }); + + const handleSendMessage = async () => { + if (!client || disabled || messageValue.length === 0) return; + + if (replyMessage) { + await sendReply({ + message: messageValue, + referenceId: replyMessage.id, + }); + onCancelReply && onCancelReply(); + } else { + await sendMessage(messageValue); + } + + setMessageValue(''); + }; + + const handleSendAttachment = async () => { + if (!client || !attachment || disabled) return; + await sendAttachment(attachment); + setMessageValue(''); + setAttachment(undefined); + setAttachmentPreview(undefined); + }; + + const handleCancelAttachment = () => { + setAttachment(undefined); + setAttachmentPreview(undefined); + }; + + const onButtonClick = (contentType: AttachmentType) => { + if (contentType === 'application') { + setAcceptedTypes('all'); + } else { + const acceptedFileTypeList = Object.keys(typeLookup).reduce( + (acc: string[], key: string) => { + if (typeLookup[key] === contentType) acc.push(`.${key}`); + return acc; + }, + [] + ); + setAcceptedTypes([...acceptedFileTypeList]); + } + }; + + const isSender = useMemo( + () => address === replyMessage?.senderAddress, + [replyMessage, address] + ); + + const navigateToRepliedMessage = () => { + if (!replyMessage) return; + const element = document.getElementById(replyMessage.id.toString()); + if (element) { + element.scrollIntoView({ + block: 'end', + behavior: 'smooth', + }); + } + }; + + useEffect(() => { + if (acceptedTypes) { + inputFile?.current?.click(); + } + }, [acceptedTypes]); + + return ( + + {isMessagesSenderOnly && ( + +

+ Message in user’s Requests +

+

+ This user has not accepted your message request yet +

+
+ )} + + + + +
+ + + + + {!attachmentPreview && !recording && ( + + )} +
+
+
+ ); +}; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/MessageSkeletonCard/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/LoadingMessagesList/MessageSkeletonCard/index.tsx similarity index 100% rename from packages/@justweb3/xmtp-plugin/src/lib/components/MessageSkeletonCard/index.tsx rename to packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/LoadingMessagesList/MessageSkeletonCard/index.tsx diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/LoadingMessagesList/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/LoadingMessagesList/index.tsx new file mode 100644 index 00000000..5a65dcdc --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/LoadingMessagesList/index.tsx @@ -0,0 +1,25 @@ +import { Flex } from '@justweb3/ui'; +import { MessageSkeletonCard } from './MessageSkeletonCard'; + +interface LoadingMessagesListProps { + computeHeight: string; +} + +export const LoadingMessagesList: React.FC = ({ + computeHeight, +}) => ( + + {Array.from({ length: 8 }).map((_, index) => ( + + ))} + +); diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/VideoPlayerPreview/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/VideoPlayerPreview/index.tsx new file mode 100644 index 00000000..dd4de5d0 --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/VideoPlayerPreview/index.tsx @@ -0,0 +1,119 @@ +import { DownloadIcon, Flex, PauseIcon, PlayIcon } from '@justweb3/ui'; +import React, { useEffect, useRef, useState } from 'react'; + +export interface VideoPlayerPreviewProps { + style?: React.CSSProperties; + url?: string; + disabled?: boolean; + fileName?: string; +} + +export const VideoPlayerPreview: React.FC = ({ + style, + url = '', + disabled, + fileName, +}) => { + const [playing, setPlaying] = React.useState(false); + const videoRef = useRef(null); + const [hovered, setHovered] = useState(false); + + const togglePlay = () => { + if (disabled) return; + if (videoRef.current) { + if (playing) { + videoRef.current.pause(); + } else { + videoRef.current.play(); + } + setPlaying(!playing); + } + }; + + useEffect(() => { + const handleVisibilityChange = () => { + if (document.hidden && playing && videoRef.current) { + videoRef.current.pause(); + } + }; + document.addEventListener('visibilitychange', handleVisibilityChange); + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange); + if (videoRef.current && playing) { + videoRef.current.pause(); + setPlaying(false); + } + }; + }, [playing]); + + return ( + { + setHovered(true); + }} + onMouseLeave={() => { + setHovered(false); + }} + onClick={togglePlay} + style={{ + aspectRatio: '16/9', + position: 'relative', + // TODO: check background color + background: 'var(--justweb3-background-color)', + cursor: 'pointer', + borderRadius: '10px', + border: '1px solid var(--justweb3-primary-color)', + ...style, + }} + > + + ); +}; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/VoiceMessageCard/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/VoiceNotePreview/index.tsx similarity index 81% rename from packages/@justweb3/xmtp-plugin/src/lib/components/VoiceMessageCard/index.tsx rename to packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/VoiceNotePreview/index.tsx index 0664a88b..819df525 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/VoiceMessageCard/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/VoiceNotePreview/index.tsx @@ -2,18 +2,18 @@ import { Flex, P, PauseIcon, PlayIcon } from '@justweb3/ui'; import * as Slider from '@radix-ui/react-slider'; import { DecodedMessage } from '@xmtp/xmtp-js'; import React, { useEffect, useMemo, useRef, useState } from 'react'; -import useGetAudioDuration from '../../hooks/useGetAudioDuration'; -import { MessageWithReaction } from '../../utils/filterReactionsMessages'; -import { formatTime } from '../../utils/formatVoiceTime'; +import { formatTime } from '../../../../utils/formatVoiceTime'; +import { useGetAudioDuration } from '../../../../hooks/useGetAudioDuration'; +import { MessageWithReaction } from '../../../../utils/filterReactionsMessages'; -interface VoiceMessageCardProps { +interface VoiceNotePreviewProps { message: MessageWithReaction | DecodedMessage; style?: React.CSSProperties; disabled?: boolean; isReceiver: boolean; } -const VoiceMessageCard: React.FC = ({ +const VoiceNotePreview: React.FC = ({ message, style, disabled, @@ -115,12 +115,11 @@ const VoiceMessageCard: React.FC = ({ width="22" height="22" fill={ - disabled ? - 'var(--justweb3-primary-color)' - : - isReceiver - ? 'var(--justweb3-primary-color)' - : 'var(--justweb3-foreground-color-4)' + disabled + ? 'var(--justweb3-primary-color)' + : isReceiver + ? 'var(--justweb3-primary-color)' + : 'var(--justweb3-foreground-color-4)' } style={{ cursor: 'pointer', @@ -193,19 +192,26 @@ const VoiceMessageCard: React.FC = ({ aria-label="Volume" /> - -

{playing || currentTime > 0 ? formatTime(currentTime) : formatTime(duration ?? 0)}

- + +

+ {playing || currentTime > 0 + ? formatTime(currentTime) + : formatTime(duration ?? 0)} +

); }; -export default VoiceMessageCard; +export default VoiceNotePreview; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/index.tsx new file mode 100644 index 00000000..de637057 --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/index.tsx @@ -0,0 +1,257 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { + useMountedAccount, + usePrimaryName, + useRecords, +} from '@justaname.id/react'; +import { Flex } from '@justweb3/ui'; +import { + CachedConversation, + ContentTypeMetadata, + useCanMessage, + useConsent, + useMessages, + useStreamMessages, +} from '@xmtp/react-sdk'; +import { useJustWeb3 } from '@justweb3/widget'; +import { ChatTextField } from './ChatTextField'; +import { ChatMessagesList } from './ChatMessagesList'; +import { LoadingMessagesList } from './LoadingMessagesList'; +import { ChatRequestControls } from './ChatRequestControl'; +import { ChatHeader } from './ChatHeader'; +import { ChatReactionOverlay } from './ChatReactionOverlay'; +import { + filterReactionsMessages, + MessageWithReaction, +} from '../../../utils/filterReactionsMessages'; +import { useSendReactionMessage } from '../../../hooks'; +import { groupMessagesByDate } from '../../../utils/groupMessageByDate'; +import { typeLookup } from '../../../utils/attachments'; +import { ContentTypeReadReceipt } from '@xmtp/content-type-read-receipt'; +import { useReadReceipt } from '../../../hooks/useReadReceipt'; + +export interface ChatProps { + conversation: CachedConversation; + onBack: () => void; +} + +export const Chat: React.FC = ({ conversation, onBack }) => { + const { openEnsProfile } = useJustWeb3(); + const [replyMessage, setReplyMessage] = useState( + null + ); + const [reactionMessage, setReactionMessage] = + useState(null); + const [isRequest, setIsRequest] = useState(false); + const [isRequestChangeLoading, setIsRequestChangeLoading] = + useState(false); + + const { entries, allow, refreshConsentList, deny } = useConsent(); + const { mutateAsync: sendReaction } = useSendReactionMessage(conversation); + const { primaryName } = usePrimaryName({ + address: conversation.peerAddress as `0x${string}`, + }); + const { records } = useRecords({ ens: primaryName }); + const { address } = useMountedAccount(); + const [canMessage, setCanMessage] = useState(true); + const { messages, isLoading } = useMessages(conversation); + const { canMessage: canMessageFn, isLoading: isCanMessageLoading } = + useCanMessage(); + const { mutateAsync: readReceipt, isPending: isReadReceiptSending } = + useReadReceipt(conversation); + useStreamMessages(conversation); + + useEffect(() => { + const lastMessage = messages[messages.length - 1]; + + if (!lastMessage || isReadReceiptSending) return; + + if (lastMessage?.contentType === ContentTypeReadReceipt.toString()) { + return; + } + + readReceipt(); + }, [messages, readReceipt, isReadReceiptSending]); + + // Determine if user can message + useEffect(() => { + if (isCanMessageLoading) return; + canMessageFn(conversation.peerAddress).then(setCanMessage); + }, [isCanMessageLoading, conversation, canMessageFn]); + + // Check if conversation is in "request" state + useEffect(() => { + const convoConsentState = entries[conversation.peerAddress]?.permissionType; + setIsRequest( + convoConsentState === 'unknown' || convoConsentState === undefined + ); + }, [entries, conversation.peerAddress]); + + // Scroll to last message when messages change + useEffect(() => { + if (messages.length === 0) return; + setTimeout(() => { + const lastMessageId = messages[messages.length - 1]?.id; + const element = document.getElementById(lastMessageId); + if (element) { + element.scrollIntoView({ behavior: 'smooth' }); + } + }, 500); + }, [messages, conversation]); + + // Filter out read receipts and group messages by date + const filteredMessages = useMemo(() => { + const withoutRead = messages.filter( + (message) => message.contentType !== 'xmtp.org/readReceipt:1.0' + ); + return filterReactionsMessages(withoutRead); + }, [messages]); + + const groupedMessages = useMemo(() => { + return groupMessagesByDate(filteredMessages ?? []); + }, [filteredMessages]); + + const isMessagesSenderOnly = useMemo(() => { + return filteredMessages.every( + (message) => message.senderAddress === address + ); + }, [filteredMessages, address]); + + const isStringContent = + typeof replyMessage?.content === 'string' || + typeof replyMessage?.content?.content === 'string'; + + const mimeType = replyMessage?.content?.mimeType; + const type = mimeType ? typeLookup[mimeType.split('/')?.[1]] : null; + + const computeHeight = useMemo(() => { + const baseHeight = 'calc(100vh - 50px - 3rem - 1.5rem - 73px - 15px)'; + const adjustments: string[] = []; + + if (isRequest) adjustments.push('40px'); + if (replyMessage) { + if (isStringContent) { + adjustments.push('46px'); + } else if (mimeType === 'audio/wav') { + adjustments.push('61px'); + } else if (type === 'video' || type === 'image') { + adjustments.push('116px'); + } else { + adjustments.push('47px'); + } + } + if (isMessagesSenderOnly) adjustments.push('59px'); + + if (adjustments.length === 0) return baseHeight; + return `${baseHeight}${adjustments.map((val) => ` - ${val}`).join('')}`; + }, [ + replyMessage, + isMessagesSenderOnly, + isStringContent, + mimeType, + type, + isRequest, + ]); + + // Handlers + const blockAddressHandler = async (peerAddress: string) => { + setIsRequestChangeLoading(true); + await refreshConsentList(); + await deny([peerAddress]); + await refreshConsentList(); + setIsRequestChangeLoading(false); + onBack(); + }; + + const handleAllowAddress = async () => { + setIsRequestChangeLoading(true); + await refreshConsentList(); + await allow([conversation.peerAddress]); + await refreshConsentList(); + setIsRequest(false); + setIsRequestChangeLoading(false); + }; + + const handleEmojiSelect = (emoji: string) => { + if (!reactionMessage) return; + sendReaction({ + action: 'added', + content: emoji, + referenceId: reactionMessage.id, + }); + setReactionMessage(null); + const replica = document.getElementById(`${reactionMessage?.id}-replica`); + replica?.remove(); + }; + + return ( + + {/* Overlay for reaction */} + { + setReactionMessage(null); + const replica = document.getElementById( + `${reactionMessage?.id}-replica` + ); + replica?.remove(); + }} + /> + + + + + {isCanMessageLoading || isLoading ? ( + + ) : ( + + )} + + {isRequest ? ( + + ) : ( + setReplyMessage(null)} + /> + )} + + + ); +}; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/NewChat/NewChatTextField/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/NewChat/NewChatTextField/index.tsx new file mode 100644 index 00000000..81b5e15d --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/NewChat/NewChatTextField/index.tsx @@ -0,0 +1,68 @@ +import React, { useState } from 'react'; +import { Flex, Input, LoadingSpinner, SendIcon } from '@justweb3/ui'; + +interface NewChatTextFieldProps { + disabled?: boolean; + onNewConvo: (message: string) => void; + style?: React.CSSProperties; +} + +export const NewChatTextField: React.FC = ({ + disabled, + onNewConvo, + style, +}) => { + const [messageValue, setMessageValue] = useState(''); + const [isNewMessageLoading, setIsNewMessageLoading] = + useState(false); + + const handleSendMessage = async () => { + if (messageValue.trim().length === 0 || disabled) return; + setIsNewMessageLoading(true); + await onNewConvo(messageValue); + setIsNewMessageLoading(false); + setMessageValue(''); + }; + + return ( + + {isNewMessageLoading ? ( + + + + ) : ( + + } + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleSendMessage(); + } + }} + onChange={(e) => setMessageValue(e.target.value)} + /> + )} + + ); +}; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/NewConversation/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/NewChat/index.tsx similarity index 96% rename from packages/@justweb3/xmtp-plugin/src/lib/components/NewConversation/index.tsx rename to packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/NewChat/index.tsx index 8190c568..6d8e2df6 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/NewConversation/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/NewChat/index.tsx @@ -8,7 +8,6 @@ import { useStartConversation, } from '@xmtp/react-sdk'; import React, { useEffect, useMemo } from 'react'; -import MessageTextField from '../MessageTextField'; import { useDebounce } from '@justweb3/widget'; import { useMountedAccount, @@ -25,14 +24,15 @@ import { P, VerificationsIcon, } from '@justweb3/ui'; +import { NewChatTextField } from './NewChatTextField'; -interface NewConversationProps { +interface NewChatProps { onChatStarted: (conversation: CachedConversation) => void; onBack: () => void; selectedAddress?: string; } -const NewConversation: React.FC = ({ +export const NewChat: React.FC = ({ onChatStarted, onBack, selectedAddress, @@ -102,7 +102,7 @@ const NewConversation: React.FC = ({ } }; - const handleNewConversation = async (message: string) => { + const handleNewChat = async (message: string) => { if (!client) return; const peerAddress = isAddressName && !!resolvedAddress ? resolvedAddress : debouncedAddress; @@ -303,10 +303,9 @@ const NewConversation: React.FC = ({ padding: '0px 1.5rem', }} > - = ({ ); }; -export default NewConversation; +export default NewChat; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/index.tsx index 98960a88..a4c57e5e 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/index.tsx @@ -1,184 +1,55 @@ -import { - AddIcon, - Flex, - Sheet, - SheetContent, - SheetTitle, - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from '@justweb3/ui'; -import { - CachedConversation, - ContentTypeMetadata, - useConsent, - useConversations, - useStreamAllMessages, - useStreamConversations, -} from '@xmtp/react-sdk'; -import React, { useEffect, useMemo } from 'react'; -import { ChatList } from '../ChatList'; +import { CachedConversation, ContentTypeMetadata } from '@xmtp/react-sdk'; +import { Sheet, SheetContent, SheetTitle } from '@justweb3/ui'; +import { Chat } from './Chat'; +import { useMemo } from 'react'; +import { NewChat } from './NewChat'; export interface ChatSheetProps { - open?: boolean; - handleOpen?: (open: boolean) => void; - handleOpenChat: ( - conversation: CachedConversation + // peerAddress?: string | null; + peer: CachedConversation | string | null; + openChat: boolean; + closeChat: () => void; + onChangePeer: ( + peer: CachedConversation | string ) => void; - handleNewChat: () => void; } export const ChatSheet: React.FC = ({ - open, - handleOpen, - handleOpenChat, - handleNewChat, + peer, + closeChat, + openChat, + onChangePeer, }) => { - const [tab, setTab] = React.useState('Chats'); - const { conversations, isLoading } = useConversations(); - const [isConsentListLoading, setIsConsentListLoading] = React.useState(true); - const { loadConsentList, entries } = useConsent(); - - const allowedConversations = useMemo(() => { - return conversations.filter( - (convo) => - entries && - entries[convo.peerAddress] && - entries[convo.peerAddress]?.permissionType === 'allowed' - ); - }, [conversations, entries]); - - const blockedConversations = useMemo(() => { - return conversations.filter( - (convo) => - entries && - entries[convo.peerAddress] && - entries[convo.peerAddress]?.permissionType === 'denied' - ); - }, [conversations, entries]); - - const requestConversations = useMemo(() => { - return conversations.filter((convo) => { - if (!entries[convo.peerAddress]) return true; - return entries[convo.peerAddress]?.permissionType === 'unknown'; - }); - }, [conversations, entries]); - - useEffect(() => { - loadConsentList().then(() => { - setIsConsentListLoading(false); - }); - }, [loadConsentList]); - - useStreamConversations(); - useStreamAllMessages(); + const isPeerConversation = useMemo(() => { + return typeof peer !== 'string'; + }, [peer]); return ( - - - Chats - - - - setTab(value)} - style={{ - display: 'flex', - flexDirection: 'column', - marginBottom: '0px', - overflow: 'hidden', - marginTop: '10px', - flex: '1', - }} - > - - - Chats - - - Requests - {requestConversations.length > 0 && ( - - {requestConversations.length} - - )} - - - Blocked - - - {isLoading || isConsentListLoading ? ( -
Loading...
+ !open && closeChat()}> + + + {isPeerConversation ? 'Messages' : 'New Conversation'} + + + {peer !== null && + (isPeerConversation ? ( + } + onBack={closeChat} + /> ) : ( - <> - - - - - - - - - - - )} -
+ { + onChangePeer(conversation); + }} + /> + ))}
); diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/CustomPlayer/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/CustomPlayer/index.tsx deleted file mode 100644 index 1ab77a3c..00000000 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/CustomPlayer/index.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { DownloadIcon, Flex, PauseIcon, PlayIcon } from '@justweb3/ui'; -import React, { useEffect, useRef, useState } from 'react'; - - -export interface CustomPlayerProps { - style?: React.CSSProperties; - url?: string; - disabled?: boolean; - fileName?: string; -} - -export const CustomPlayer: React.FC = ({ - style, - url = '', - disabled, - fileName -}) => { - const [playing, setPlaying] = React.useState(false); - const videoRef = useRef(null); - const [hovered, setHovered] = useState(false) - - const togglePlay = () => { - if (disabled) return; - if (videoRef.current) { - if (playing) { - videoRef.current.pause(); - } else { - videoRef.current.play(); - } - setPlaying(!playing); - } - }; - - useEffect(() => { - const handleVisibilityChange = () => { - if (document.hidden && playing && videoRef.current) { - videoRef.current.pause(); - } - }; - document.addEventListener('visibilitychange', handleVisibilityChange); - return () => { - document.removeEventListener('visibilitychange', handleVisibilityChange); - if (videoRef.current && playing) { - videoRef.current.pause(); - setPlaying(false); - } - }; - }, [playing]); - - return ( - { setHovered(true) }} - onMouseLeave={() => { setHovered(false) }} - onClick={togglePlay} - style={{ - aspectRatio: '16/9', - position: 'relative', - // TODO: check background color - background: 'var(--justweb3-background-color)', - cursor: 'pointer', - borderRadius: '10px', - border: '1px solid var(--justweb3-primary-color)', - ...style - }}> - - ); -} diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/CustomVoicePreview/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/CustomVoicePreview/index.tsx deleted file mode 100644 index f90bd51d..00000000 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/CustomVoicePreview/index.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { CloseIcon, Flex, P, PauseIcon, PlayIcon } from '@justweb3/ui'; -import React, { useEffect, useRef, useState } from 'react'; -import useGetAudioDuration from '../../hooks/useGetAudioDuration'; -import { formatTime } from '../../utils/formatVoiceTime'; - - -interface CustomVoicePreviewProps { - audioUrl: string; - onCancel: () => void; -} - -const CustomVoicePreview: React.FC = ({ - audioUrl, - onCancel -}) => { - const [playing, setPlaying] = useState(false); - const [currentTime, setCurrentTime] = useState(0); - const audioRef = useRef(new Audio()); - - const audioDuration = useGetAudioDuration(audioUrl); - - useEffect(() => { - const audio = new Audio(); - audioRef.current = audio; - - const onTimeUpdate = () => { - setCurrentTime(audio.currentTime); - }; - - const onEnded = () => { - setPlaying(false); - setCurrentTime(0); - }; - - audio.src = audioUrl; - audio.addEventListener('timeupdate', onTimeUpdate); - audio.addEventListener('ended', onEnded); - - return () => { - audio.removeEventListener('timeupdate', onTimeUpdate); - audio.removeEventListener('ended', onEnded); - }; - }, [audioUrl]); - - const handlePlayPause = () => { - const audio = audioRef.current; - if (!audio) return; - - if (playing) { - audio.pause(); - } else { - audio.play(); - } - setPlaying(!playing); - }; - - return ( - - {playing ? - - : - - } -

{playing || currentTime > 0 ? formatTime(currentTime) : formatTime(audioDuration ?? 0)}

- -
- ); -}; - -export default CustomVoicePreview; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/EmojiSelector/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/EmojiSelector/index.tsx deleted file mode 100644 index c57959d1..00000000 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/EmojiSelector/index.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { Flex, Input } from '@justweb3/ui'; -import React, { useMemo } from 'react'; -import { EmojiObject, emojis } from '../../utils/emojis'; -import { useDebounced } from '../../hooks'; - - -interface EmojiSelectorProps { - onEmojiSelect: (emoji: string) => void; -} - -const EmojiSelector: React.FC = ({ - onEmojiSelect, -}) => { - const [searchValue, setSearchValue] = React.useState(""); - - const { - value: debouncedSearch, - } = useDebounced(searchValue, 50); - - const onEmojiClickHandler = (emoji: EmojiObject) => { - onEmojiSelect(emoji.name); - - } - - const filteredEmojis = useMemo(() => { - if (debouncedSearch.length === 0) return emojis; - const filteredEmojis = emojis.filter(emoji => - emoji.name.toLowerCase().includes(debouncedSearch.toLowerCase()) - ); - return filteredEmojis; - }, [debouncedSearch]); - - - return ( - - setSearchValue(e.target.value)} - /> -
- {filteredEmojis.map((emoji, index) => ( - - ))} -
-
- - ); -}; - -export default EmojiSelector; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/MessageItem/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/InboxSheet/ChatList/MessageItem/index.tsx similarity index 56% rename from packages/@justweb3/xmtp-plugin/src/lib/components/MessageItem/index.tsx rename to packages/@justweb3/xmtp-plugin/src/lib/components/InboxSheet/ChatList/MessageItem/index.tsx index 19b1bf23..a994b787 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/MessageItem/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/InboxSheet/ChatList/MessageItem/index.tsx @@ -1,41 +1,50 @@ import { attachmentContentTypeConfig, CachedConversation, + CachedMessage, ContentTypeMetadata, reactionContentTypeConfig, replyContentTypeConfig, useConsent, - useLastMessage, useStreamMessages, } from '@xmtp/react-sdk'; import { useEnsAvatar, useRecords } from '@justaname.id/react'; import { Avatar, Button, Flex, formatText, P, SPAN } from '@justweb3/ui'; import React, { useMemo } from 'react'; -import { formatChatDate } from '../../utils/formatChatDate'; +import { formatChatDate } from '../../../../utils/formatChatDate'; export interface MessageItemProps { conversation: CachedConversation; onClick?: () => void; blocked?: boolean; primaryName?: string | null; + conversationInfo?: { + conversationId: string; + unreadCount: number; + consent: 'allowed' | 'blocked' | 'requested'; + lastMessage: CachedMessage; + }; } -export const MessageItem: React.FC = ({ +const MessageItem: React.FC = ({ conversation, onClick, blocked, primaryName, + conversationInfo, }) => { - const lastMessage = useLastMessage(conversation.topic); useStreamMessages(conversation); - // const { primaryName } = usePrimaryName({ - // address: conversation.peerAddress as `0x${string}`, - // }); + // const { messages } = useMessages(conversation); const { records } = useRecords({ ens: primaryName || undefined, }); const { sanitizeEnsImage } = useEnsAvatar(); + // const unreadMessages = useMemo(() => { + // if (!lastMessage) return false; + // return lastMessage.contentType !== ContentTypeReadReceipt.toString(); + // }, [lastMessage]); + const { allow, refreshConsentList } = useConsent(); const allowUser = async () => { @@ -44,7 +53,10 @@ export const MessageItem: React.FC = ({ await refreshConsentList(); }; + console.log('conversationInfo', conversationInfo); + const lastContent = useMemo(() => { + const lastMessage = conversationInfo?.lastMessage; if (!lastMessage) return ''; if (typeof lastMessage.content === 'string') { @@ -72,7 +84,9 @@ export const MessageItem: React.FC = ({ } return lastMessage.contentFallback; - }, [lastMessage]); + }, [conversationInfo, conversationInfo?.lastMessage]); + + console.log(lastContent); return ( = ({ lineHeight: '12px', }} > - {lastMessage - ? lastMessage.senderAddress !== conversation.peerAddress + {conversationInfo?.lastMessage + ? conversationInfo?.lastMessage.senderAddress !== + conversation.peerAddress ? 'You: ' : '' : ''} - {lastMessage + {conversationInfo?.lastMessage ? lastContent ? lastContent : 'No preview available' @@ -148,11 +163,55 @@ export const MessageItem: React.FC = ({ Unblock ) : ( - - {lastMessage?.sentAt ? formatChatDate(lastMessage.sentAt) : ''} - +
+ + {conversationInfo?.lastMessage?.sentAt + ? formatChatDate(conversationInfo?.lastMessage.sentAt) + : ''} + + {!!conversationInfo?.unreadCount && + conversationInfo?.unreadCount > 0 && ( +
+ + {conversationInfo?.unreadCount} + +
+ )} +
)}
); }; + +const MessageItemMemo = React.memo(MessageItem, (prevProps, nextProps) => { + return JSON.stringify(prevProps) === JSON.stringify(nextProps); +}); + +export { MessageItemMemo as MessageItem }; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatList/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/InboxSheet/ChatList/index.tsx similarity index 60% rename from packages/@justweb3/xmtp-plugin/src/lib/components/ChatList/index.tsx rename to packages/@justweb3/xmtp-plugin/src/lib/components/InboxSheet/ChatList/index.tsx index 3ff94d5f..c9cd170e 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatList/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/InboxSheet/ChatList/index.tsx @@ -1,7 +1,11 @@ import { Flex } from '@justweb3/ui'; -import { CachedConversation, ContentTypeMetadata } from '@xmtp/react-sdk'; +import { + CachedConversation, + CachedMessage, + ContentTypeMetadata, +} from '@xmtp/react-sdk'; import React from 'react'; -import { MessageItem } from '../MessageItem'; +import { MessageItem } from './MessageItem'; import { usePrimaryNameBatch } from '@justaname.id/react'; export interface ChatListProps { @@ -10,12 +14,19 @@ export interface ChatListProps { conversation: CachedConversation ) => void; blockedList?: boolean; + conversationsInfo?: { + conversationId: string; + unreadCount: number; + consent: 'allowed' | 'blocked' | 'requested'; + lastMessage: CachedMessage; + }[]; } export const ChatList: React.FC = ({ conversations, handleOpenChat, blockedList, + conversationsInfo, }) => { const { allPrimaryNames } = usePrimaryNameBatch({ addresses: conversations.map((conversation) => conversation.peerAddress), @@ -33,6 +44,15 @@ export const ChatList: React.FC = ({ item.conversationId === conversation.topic + // )?.unreadCount + // } + // + conversationInfo={conversationsInfo?.find( + (item) => item.conversationId === conversation.topic + )} onClick={() => handleOpenChat(conversation)} key={conversation.topic} blocked={blockedList} diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/InboxSheet/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/InboxSheet/index.tsx new file mode 100644 index 00000000..c4496252 --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/InboxSheet/index.tsx @@ -0,0 +1,274 @@ +import { + AddIcon, + Flex, + Sheet, + SheetContent, + SheetTitle, + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from '@justweb3/ui'; +import { + CachedConversation, + CachedMessage, + ContentTypeMetadata, + useConsent, + useConversations, + useStreamAllMessages, + useStreamConversations, +} from '@xmtp/react-sdk'; +import React, { useEffect, useMemo } from 'react'; +import { ChatList } from './ChatList'; + +export interface InboxSheetProps { + open?: boolean; + handleOpen?: (open: boolean) => void; + handleOpenChat: ( + conversation: CachedConversation + ) => void; + handleNewChat: () => void; + onConversationsUpdated: ({ + allowed, + blocked, + requested, + }: { + allowed: CachedConversation[]; + blocked: CachedConversation[]; + requested: CachedConversation[]; + }) => void; + allConversations: { + allowed: CachedConversation[]; + blocked: CachedConversation[]; + requested: CachedConversation[]; + }; + conversationsInfo?: { + conversationId: string; + unreadCount: number; + consent: 'allowed' | 'blocked' | 'requested'; + lastMessage: CachedMessage; + }[]; +} + +export const InboxSheet: React.FC = ({ + open, + handleOpen, + handleOpenChat, + handleNewChat, + onConversationsUpdated, + allConversations, + conversationsInfo, +}) => { + const [tab, setTab] = React.useState('Chats'); + const { conversations, isLoading } = useConversations(); + + const [isConsentListLoading, setIsConsentListLoading] = React.useState(true); + const { loadConsentList, entries } = useConsent(); + + const allowedConversations = useMemo(() => { + return conversations.filter( + (convo) => + entries && + entries[convo.peerAddress] && + entries[convo.peerAddress]?.permissionType === 'allowed' + ); + }, [conversations, entries]); + + const blockedConversations = useMemo(() => { + return conversations.filter( + (convo) => + entries && + entries[convo.peerAddress] && + entries[convo.peerAddress]?.permissionType === 'denied' + ); + }, [conversations, entries]); + + const requestConversations = useMemo(() => { + return conversations.filter((convo) => { + if (!entries[convo.peerAddress]) return true; + return entries[convo.peerAddress]?.permissionType === 'unknown'; + }); + }, [conversations, entries]); + + useEffect(() => { + let _allowedConversations = [] as CachedConversation[]; + let _blockedConversations = [] as CachedConversation[]; + let _requestConversations = [] as CachedConversation[]; + + if ( + allowedConversations.some( + (convo) => + !allConversations.allowed.some((c) => c.topic === convo.topic) + ) + ) { + _allowedConversations = allowedConversations.filter( + (convo) => + !allConversations.allowed.some((c) => c.topic === convo.topic) + ); + // onConversationsUpdated([...allConversations, ...newConversations]); + } + + if ( + blockedConversations.some( + (convo) => + !allConversations.blocked.some((c) => c.topic === convo.topic) + ) + ) { + _blockedConversations = blockedConversations.filter( + (convo) => + !allConversations.blocked.some((c) => c.topic === convo.topic) + ); + } + + if ( + requestConversations.some( + (convo) => + !allConversations.requested.some((c) => c.topic === convo.topic) + ) + ) { + _requestConversations = requestConversations.filter( + (convo) => + !allConversations.requested.some((c) => c.topic === convo.topic) + ); + } + + if ( + _allowedConversations.length > 0 || + _blockedConversations.length > 0 || + _requestConversations.length > 0 + ) { + onConversationsUpdated({ + allowed: [...allConversations.allowed, ..._allowedConversations], + blocked: [...allConversations.blocked, ..._blockedConversations], + requested: [...allConversations.requested, ..._requestConversations], + }); + } + }, [ + allConversations, + allowedConversations, + blockedConversations, + onConversationsUpdated, + requestConversations, + ]); + + useEffect(() => { + loadConsentList().then(() => { + setIsConsentListLoading(false); + }); + }, [loadConsentList]); + + useStreamConversations(); + useStreamAllMessages(); + + return ( + + + Chats + + + + setTab(value)} + style={{ + display: 'flex', + flexDirection: 'column', + marginBottom: '0px', + overflow: 'hidden', + marginTop: '10px', + flex: '1', + }} + > + + + Chats + + + Requests + {requestConversations.length > 0 && ( + + {requestConversations.length} + + )} + + + Blocked + + + {isLoading || isConsentListLoading ? ( +
Loading...
+ ) : ( + <> + + + + + + + + + + + )} +
+
+
+ ); +}; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/MessageSheet/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/MessageSheet/index.tsx deleted file mode 100644 index f39ebfbf..00000000 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/MessageSheet/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { CachedConversation, ContentTypeMetadata } from '@xmtp/react-sdk'; -import { Sheet, SheetContent, SheetTitle } from '@justweb3/ui'; -import { Chat } from '../Chat'; - -export interface MessageSheetProps { - conversation: CachedConversation | null; - openChat: boolean; - handleOpenChat: ( - conversation: CachedConversation | null - ) => void; -} - -export const MessageSheet: React.FC = ({ - conversation, - handleOpenChat, - openChat, -}) => { - return ( - !open && handleOpenChat(null)} - > - - Messages - {conversation && handleOpenChat(null)} />} - - - ); -}; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/MessageTextField/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/MessageTextField/index.tsx deleted file mode 100644 index 6c6acbfc..00000000 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/MessageTextField/index.tsx +++ /dev/null @@ -1,651 +0,0 @@ -import type { Attachment } from '@xmtp/content-type-remote-attachment'; -import { CachedConversation, useClient } from '@xmtp/react-sdk'; -import React, { - Dispatch, - SetStateAction, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; -import { MessageWithReaction } from '../../utils/filterReactionsMessages'; -import { useMountedAccount, usePrimaryName } from '@justaname.id/react'; -import { - useAttachmentChange, - useRecordingTimer, - useRecordVoice, - useSendAttachment, - useSendMessages, - useSendReplyMessage, -} from '../../hooks'; -import { AttachmentType, typeLookup } from '../../utils/attachments'; -import { - AddFolderIcon, - AddImageIcon, - AddVideoIcon, - Button, - CloseIcon, - DocumentIcon, - Flex, - Input, - LoadingSpinner, - MicIcon, - P, - SendIcon, - StopIcon, -} from '@justweb3/ui'; -import { formatAddress } from '../../utils/formatAddress'; -import VoiceMessageCard from '../VoiceMessageCard'; -import { CustomPlayer } from '../CustomPlayer'; -import CustomVoicePreview from '../CustomVoicePreview'; - -interface MessageTextFieldProps { - newConvo?: boolean; - disabled?: boolean; - conversation?: CachedConversation; - replyMessage?: MessageWithReaction | null; - onCancelReply?: () => void; - onNewConvo?: (message: string) => void; - peerAddress?: string; - style?: React.CSSProperties; -} - -const MessageTextField: React.FC = ({ - newConvo, - disabled, - replyMessage, - onCancelReply, - conversation, - onNewConvo, - peerAddress, - style, -}) => { - const [messageValue, setMessageValue] = React.useState(''); - const [attachment, setAttachment] = React.useState(); - const [attachmentPreview, setAttachmentPreview] = React.useState< - string | undefined - >(); - const [isNewMessageLoading, setIsNewMessageLoading] = - React.useState(false); - // const [selectingAttachment, setSelectingAttachment] = React.useState(false); - const { client } = useClient(); - const { address } = useMountedAccount(); - const { mutateAsync: sendMessage } = useSendMessages(conversation); - const { mutateAsync: sendReply } = useSendReplyMessage(conversation); - const { mutateAsync: sendAttachment } = useSendAttachment(conversation); - - const attachmentExtention = useMemo(() => { - return attachment?.mimeType.split('/')?.[1] || ''; - }, [attachment]); - - const { primaryName } = usePrimaryName({ - address: peerAddress as `0x${string}`, - }); - - // Attachments - const [acceptedTypes, setAcceptedTypes]: [ - string | string[] | undefined, - Dispatch> - ] = useState(); - const inputFile = useRef(null); - const { onAttachmentChange } = useAttachmentChange({ - setAttachment, - setAttachmentPreview, - onError: (error) => { - // showToast("error", error); - }, - }); - - // Recording - const { recording, startRecording, stopRecording } = useRecordVoice({ - setAttachment, - setAttachmentPreview, - }); - const { start, pause, reset, recordingValue } = useRecordingTimer({ - stopRecording, - status: recording ? 'recording' : 'idle', - }); - - // Sending text message - const handleSendMessage = async () => { - if (messageValue.length === 0) return; - if (!client) return; - if (disabled) return; - if (newConvo) { - setIsNewMessageLoading(true); - onNewConvo && onNewConvo(messageValue); - setIsNewMessageLoading(false); - } else { - if (replyMessage) { - sendReply({ - message: messageValue, - referenceId: replyMessage.id, - }); - onCancelReply && onCancelReply(); - } else { - sendMessage(messageValue); - } - } - setMessageValue(''); - }; - - // Sending attachment - const handleSendAttachment = async () => { - if (!client) return; - if (!attachment) return; - if (disabled) return; - if (newConvo) { - onNewConvo && onNewConvo(messageValue); - } else { - sendAttachment(attachment); - } - setMessageValue(''); - setAttachment(undefined); - setAttachmentPreview(undefined); - // setSelectingAttachment(false); - }; - - const handleCancelAttachment = () => { - setAttachment(undefined); - setAttachmentPreview(undefined); - }; - - const onButtonClick = (contentType: AttachmentType) => { - if (contentType === 'application') { - setAcceptedTypes('all'); - } else { - const acceptedFileTypeList = Object.keys(typeLookup).reduce( - (acc: string[], key: string) => { - if (typeLookup[key] === contentType) acc.push(`.${key}`); - return acc; - }, - [] - ); - setAcceptedTypes([...acceptedFileTypeList]); - } - }; - - // Reply message - const isSender = useMemo(() => { - return address === replyMessage?.senderAddress; - }, [replyMessage, address]); - const isReplyVoice = useMemo(() => { - return replyMessage?.content.mimeType === 'audio/wav'; - }, [replyMessage]); - - const isReplyText = useMemo(() => { - if (!replyMessage) return false; - return typeof replyMessage.content === 'string'; - }, [replyMessage]); - - const isReplyReply = useMemo(() => { - if (!replyMessage) return false; - return !!replyMessage.content.reference; - }, [replyMessage]); - - const replyAttachmentExtention = useMemo(() => { - if (!isReplyText && !!replyMessage && !isReplyReply) - return replyMessage.content.mimeType.split('/')?.[1] || ''; - }, [isReplyText, replyMessage, isReplyReply]); - - const navigateToRepliedMessage = () => { - if (!replyMessage) return; - const element = document.getElementById(replyMessage.id.toString()); - if (element) { - element.scrollIntoView({ - block: 'end', - behavior: 'smooth', - }); - } - }; - - useEffect(() => { - if (acceptedTypes) { - inputFile?.current?.click(); - } - }, [acceptedTypes]); - - const isReplyVideoOrImage = useMemo(() => { - return ( - typeLookup[replyAttachmentExtention] === 'image' || - typeLookup[replyAttachmentExtention] === 'video' - ); - }, [replyAttachmentExtention]); - - return ( - - {!replyMessage} - {!newConvo && ( - - onButtonClick('image')} - style={{ - cursor: 'pointer', - }} - /> - onButtonClick('video')} - style={{ - cursor: 'pointer', - }} - /> - onButtonClick('application')} - style={{ - cursor: 'pointer', - }} - /> - - - )} -
- {replyMessage && ( - - -

- {isSender - ? 'YOU' - : primaryName ?? formatAddress(replyMessage.senderAddress)} -

- {isReplyText || isReplyReply ? ( -

- {isReplyReply - ? replyMessage.content.content - : replyMessage.content} -

- ) : isReplyVoice ? ( - - ) : typeLookup[replyAttachmentExtention] === 'image' ? ( - {replyMessage.content.filename} - ) : typeLookup[replyAttachmentExtention] === 'video' ? ( - - ) : ( - - -

- {replyMessage.content.filename} -

-
- )} -
- -
- )} - {attachmentPreview ? ( - - {attachment?.mimeType !== 'audio/wav' && ( -
- )} - {attachment?.mimeType === 'audio/wav' ? ( - - ) : ( - - {typeLookup[attachmentExtention] === 'image' ? ( - {attachment?.filename} - ) : typeLookup[attachmentExtention] === 'video' ? ( - - ) : ( - - -

- {attachment?.filename ?? 'Cannot preview'} -

-
- )} - - - - -
- )} - { - if (disabled) return; - handleSendAttachment(); - }} - /> - - ) : recording ? ( - -

- {recordingValue} -

-

- RECORDING... -

- { - stopRecording(); - pause(); - reset(); - }} - /> -
- ) : isNewMessageLoading ? ( - - - - ) : ( - { - if (disabled) return; - startRecording(); - start(); - }} - /> - ) - } - right={ - { - if (disabled) return; - handleSendMessage(); - }} - /> - } - disabled={disabled} - onKeyDown={(e) => { - if (e.key === 'Enter') { - handleSendMessage(); - } - }} - onChange={(e) => setMessageValue(e.target.value)} - /> - )} -
-
- ); -}; - -export default MessageTextField; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/NewMessageSheet/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/NewMessageSheet/index.tsx deleted file mode 100644 index 69a14e41..00000000 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/NewMessageSheet/index.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Sheet, SheetContent, SheetTitle } from '@justweb3/ui'; -import { CachedConversation } from '@xmtp/react-sdk'; -import NewConversation from '../NewConversation'; - -export interface MessageSheetProps { - openNewChat: boolean; - handleOpenNewChat: (open: boolean) => void; - onChatStarted: (conversation: CachedConversation) => void; - addressOrEns?: string; -} - -export const NewMessageSheet: React.FC = ({ - handleOpenNewChat, - openNewChat, - onChatStarted, - addressOrEns, -}) => { - return ( - !open && handleOpenNewChat(false)} - > - - New Conversation - handleOpenNewChat(false)} - selectedAddress={addressOrEns} - /> - - - ); -}; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatWithProfileButton/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ProfileChatButton/index.tsx similarity index 96% rename from packages/@justweb3/xmtp-plugin/src/lib/components/ChatWithProfileButton/index.tsx rename to packages/@justweb3/xmtp-plugin/src/lib/components/ProfileChatButton/index.tsx index 0e839180..c0965bde 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatWithProfileButton/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ProfileChatButton/index.tsx @@ -8,13 +8,13 @@ import { useEthersSigner } from '../../hooks'; import { loadKeys, storeKeys, wipeKeys } from '../../utils/xmtp'; import { ChainId } from '@justaname.id/sdk'; -export interface ChatWithProfileButtonProps { +export interface ProfileChatButtonProps { ens: string; env: 'local' | 'production' | 'dev'; chainId: ChainId; } -export const ChatWithProfileButton: React.FC = ({ +export const ProfileChatButton: React.FC = ({ ens, env, chainId, diff --git a/packages/@justweb3/xmtp-plugin/src/lib/content-types/readReceipt.ts b/packages/@justweb3/xmtp-plugin/src/lib/content-types/readReceipt.ts new file mode 100644 index 00000000..12c0a2c2 --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/content-types/readReceipt.ts @@ -0,0 +1,120 @@ +import { + ContentTypeReadReceipt, + ReadReceiptCodec, +} from '@xmtp/content-type-read-receipt'; +import { z } from 'zod'; +import { isAfter, parseISO } from 'date-fns'; +import { + CachedConversation, + ContentTypeConfiguration, + ContentTypeMessageProcessor, + ContentTypeMetadataValues, + getCachedConversationByTopic, +} from '@xmtp/react-sdk'; +import { Mutex } from 'async-mutex'; +import { ContentTypeId } from '@xmtp/content-type-primitives'; + +const NAMESPACE = 'readReceipt'; +export type CachedReadReceiptMetadata = { + incoming: string | undefined; + outgoing: string | undefined; +}; +/** + * Retrieve the read receipt from a cached conversation for the given type + * + * @param conversation Cached conversation + * @returns The read receipt date, or `undefined` if the conversation + * has no read receipt for the given type + */ +export const getReadReceipt = ( + conversation: CachedConversation, + type: keyof CachedReadReceiptMetadata +) => { + const metadata = conversation?.metadata?.[NAMESPACE] as + | CachedReadReceiptMetadata + | undefined; + const readReceiptType = metadata?.[type]; + return readReceiptType ? parseISO(readReceiptType) : undefined; +}; +/** + * Check if a cached conversation has a read receipt for the given type + * + * @param conversation Cached conversation + * @returns `true` if the conversation has a read receipt for the given type, + * `false` otherwise + */ +export const hasReadReceipt = ( + conversation: CachedConversation, + type: keyof CachedReadReceiptMetadata +) => getReadReceipt(conversation, type) !== undefined; +const ReadReceiptContentSchema = z.object({}).strict(); +/** + * Validate the content of a read receipt message + * + * @param content Message content + * @returns `true` if the content is valid, `false` otherwise + */ +const isValidReadReceiptContent = (content: unknown) => { + const { success } = ReadReceiptContentSchema.safeParse(content); + return success; +}; +const processReadReceiptMutex = new Mutex(); +/** + * Process a read receipt message + * + * Updates the metadata of its conversation with the timestamp of the + * read receipt. + */ +export const processReadReceipt: ContentTypeMessageProcessor = async ({ + client, + db, + message, + conversation, + updateConversationMetadata, +}) => { + // ensure that only 1 read receipt message is processed at a time to preserve order + await processReadReceiptMutex.runExclusive(async () => { + const contentType = ContentTypeId.fromString(message.contentType); + // always use the latest conversation from the cache + const updatedConversation = await getCachedConversationByTopic( + client.address, + conversation.topic, + db + ); + if (updatedConversation) { + const isIncoming = message.senderAddress !== client.address; + const readReceiptType = isIncoming ? 'incoming' : 'outgoing'; + const readReceiptDate = getReadReceipt( + updatedConversation, + readReceiptType + ); + if ( + ContentTypeReadReceipt.sameAs(contentType) && + conversation && + isValidReadReceiptContent(message.content) && + // ignore read receipts that are older than the current one + (!readReceiptDate || isAfter(message.sentAt, readReceiptDate)) + ) { + const metadata = updatedConversation.metadata?.[NAMESPACE] as + | CachedReadReceiptMetadata + | undefined; + // update conversation metadata with the appropriate read receipt + await updateConversationMetadata({ + ...(metadata ?? {}), + [readReceiptType]: message.sentAt.toISOString(), + } as ContentTypeMetadataValues); + } + } + }); +}; +export const readReceiptContentTypeConfig: ContentTypeConfiguration = { + codecs: [new ReadReceiptCodec()], + contentTypes: [ContentTypeReadReceipt.toString()], + namespace: NAMESPACE, + processors: { + [ContentTypeReadReceipt.toString()]: [processReadReceipt], + }, + validators: { + [ContentTypeReadReceipt.toString()]: isValidReadReceiptContent, + }, +}; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/hooks/useGetAudioDuration/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/hooks/useGetAudioDuration/index.tsx index ce7dc3c6..91b2af54 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/hooks/useGetAudioDuration/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/hooks/useGetAudioDuration/index.tsx @@ -1,41 +1,44 @@ import { useEffect, useState } from 'react'; -const useGetAudioDuration = (url: string) => { - const [duration, setDuration] = useState(null); - - useEffect(() => { - if (!url) { - setDuration(null); - return; +export const useGetAudioDuration = (url: string) => { + const [duration, setDuration] = useState(null); + + useEffect(() => { + if (!url) { + setDuration(null); + return; + } + + const getDuration = (url: string, next: (duration: number) => void) => { + const _player = new Audio(url); + const durationChangeHandler = function ( + this: HTMLAudioElement, + e: Event + ) { + if (this.duration !== Infinity) { + const duration = this.duration; + _player.remove(); // Cleanup + next(duration); } + }; + + _player.addEventListener('durationchange', durationChangeHandler, false); + _player.load(); + _player.currentTime = 24 * 60 * 60; + _player.volume = 0; + }; + + getDuration(url, (duration: number) => { + setDuration(duration); + }); + + return () => { + const _player = new Audio(url); + _player.remove(); + }; + }, [url]); - const getDuration = (url: string, next: (duration: number) => void) => { - const _player = new Audio(url); - const durationChangeHandler = function (this: HTMLAudioElement, e: Event) { - if (this.duration !== Infinity) { - const duration = this.duration; - _player.remove(); // Cleanup - next(duration); - } - }; - - _player.addEventListener('durationchange', durationChangeHandler, false); - _player.load(); - _player.currentTime = 24 * 60 * 60; - _player.volume = 0; - }; - - getDuration(url, (duration: number) => { - setDuration(duration); - }); - - return () => { - const _player = new Audio(url); - _player.remove(); - }; - }, [url]); - - return duration; + return duration; }; export default useGetAudioDuration; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/hooks/useReadReceipt/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/hooks/useReadReceipt/index.tsx new file mode 100644 index 00000000..eb1695f7 --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/hooks/useReadReceipt/index.tsx @@ -0,0 +1,14 @@ +import { useMutation } from '@tanstack/react-query'; +import { CachedConversation, useSendMessage } from '@xmtp/react-sdk'; +import { ContentTypeReadReceipt } from '@xmtp/content-type-read-receipt'; + +export const useReadReceipt = (conversation?: CachedConversation) => { + const { sendMessage } = useSendMessage(); + + return useMutation({ + mutationFn: () => { + if (!conversation) throw new Error('Conversation not found'); + return sendMessage(conversation, {}, ContentTypeReadReceipt); + }, + }); +}; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/hooks/useSendMessage/index.ts b/packages/@justweb3/xmtp-plugin/src/lib/hooks/useSendMessage/index.ts index 4a89f1d9..c7866345 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/hooks/useSendMessage/index.ts +++ b/packages/@justweb3/xmtp-plugin/src/lib/hooks/useSendMessage/index.ts @@ -1,33 +1,33 @@ -import { useMutation } from '@tanstack/react-query' -import { ContentTypeId } from '@xmtp/content-type-primitives' +import { useMutation } from '@tanstack/react-query'; +import { ContentTypeId } from '@xmtp/content-type-primitives'; import { - CachedConversation, - useSendMessage, - DecodedMessage, - SendOptions, -} from '@xmtp/react-sdk' + CachedConversation, + DecodedMessage, + SendOptions, + useSendMessage, +} from '@xmtp/react-sdk'; -export const sendMessages = async ( +export const _sendMessages = async ( + conversation: CachedConversation, + message: string, + sendMessage: ( conversation: CachedConversation, - message: string, - sendMessage: ( - conversation: CachedConversation, - content: T, - contentType?: ContentTypeId, - sendOptions?: Omit, - ) => Promise | undefined>, - contentType?: SendOptions, + content: T, + contentType?: ContentTypeId, + sendOptions?: Omit + ) => Promise | undefined>, + contentType?: SendOptions ) => { - await sendMessage(conversation, message, undefined, contentType) -} + await sendMessage(conversation, message, undefined, contentType); +}; export const useSendMessages = (conversation?: CachedConversation) => { - const { sendMessage } = useSendMessage() + const { sendMessage } = useSendMessage(); - return useMutation({ - mutationFn: (message: string, contentType?: SendOptions) => { - if (!conversation) throw new Error('Conversation not found') - return sendMessages(conversation, message, sendMessage, contentType) - }, - }) -} + return useMutation({ + mutationFn: (message: string, contentType?: SendOptions) => { + if (!conversation) throw new Error('Conversation not found'); + return _sendMessages(conversation, message, sendMessage, contentType); + }, + }); +}; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/plugins/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/plugins/index.tsx index 63c919a9..90d1a0a3 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/plugins/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/plugins/index.tsx @@ -1,7 +1,7 @@ import { JustaPlugin } from '@justweb3/widget'; import { JustWeb3XMTPProvider } from '../providers/JustWeb3XMTPProvider'; -import { ChatButton } from '../components/ChatButton'; -import { ChatWithProfileButton } from '../components/ChatWithProfileButton'; +import { ChatMenuButton } from '../components/ChatMenuButton'; +import { ProfileChatButton } from '../components/ProfileChatButton'; export type XmtpEnvironment = 'local' | 'production' | 'dev'; @@ -21,11 +21,11 @@ export const XMTPPlugin = (env: XmtpEnvironment): JustaPlugin => { ); }, ProfileHeader: (pluginApi, ens, chainId, address) => { - return ; + return ; }, SignInMenu: (pluginApi) => { return ( - pluginApi.setState('xmtpOpen', open)} env={env} /> diff --git a/packages/@justweb3/xmtp-plugin/src/lib/providers/JustWeb3XMTPProvider/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/providers/JustWeb3XMTPProvider/index.tsx index 05d06fc5..1ee5a0a8 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/providers/JustWeb3XMTPProvider/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/providers/JustWeb3XMTPProvider/index.tsx @@ -1,29 +1,42 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { attachmentContentTypeConfig, CachedConversation, + CachedMessage, Client, ClientOptions, + ContentTypeConfiguration, ContentTypeMetadata, reactionContentTypeConfig, replyContentTypeConfig, useClient, + useMessages, + useStreamMessages, XMTPProvider, } from '@xmtp/react-sdk'; -import { ChatSheet } from '../../components/ChatSheet'; +import { InboxSheet } from '../../components/InboxSheet'; import { useEthersSigner } from '../../hooks'; import { useMountedAccount } from '@justaname.id/react'; import { loadKeys, storeKeys, wipeKeys } from '../../utils/xmtp'; -import { AllMessageSheet } from '../../components/AllMessageSheet'; +import { readReceiptContentTypeConfig } from '../../content-types/readReceipt'; +import { ContentTypeReadReceipt } from '@xmtp/content-type-read-receipt'; +import { ChatSheet } from '../../components/ChatSheet'; -const contentTypeConfigs = [ +const contentTypeConfigs: ContentTypeConfiguration[] = [ attachmentContentTypeConfig, reactionContentTypeConfig, replyContentTypeConfig, + readReceiptContentTypeConfig, ]; interface JustWeb3XMTPContextProps { handleOpenChat: (address: string) => void; + conversationsInfo: { + conversationId: string; + unreadCount: number; + consent: 'allowed' | 'blocked' | 'requested'; + lastMessage: CachedMessage; + }[]; } const JustWeb3XMTPContext = React.createContext< @@ -46,6 +59,25 @@ export const JustWeb3XMTPProvider: React.FC = ({ const [isXmtpEnabled, setIsXmtpEnabled] = React.useState(false); const [conversation, setConversation] = React.useState | null>(null); + const [conversations, setConversations] = React.useState<{ + allowed: CachedConversation[]; + blocked: CachedConversation[]; + requested: CachedConversation[]; + }>({ + allowed: [], + blocked: [], + requested: [], + }); + const [conversationsInfo, setConversationsInfo] = React.useState< + { + conversationId: string; + unreadCount: number; + consent: 'allowed' | 'blocked' | 'requested'; + lastMessage: CachedMessage; + }[] + >([]); + + console.log(conversations); const handleXmtpEnabled = (enabled: boolean) => { setIsXmtpEnabled(enabled); }; @@ -61,24 +93,142 @@ export const JustWeb3XMTPProvider: React.FC = ({ } }; + const handleConversationInfo = ( + conversationId: string, + unreadCount: number, + lastMessage: CachedMessage, + consent: 'allowed' | 'blocked' | 'requested' + ) => { + setConversationsInfo((prev) => { + const index = prev.findIndex( + (item) => item.conversationId === conversationId + ); + if (index === -1) { + return [ + ...prev, + { + conversationId, + unreadCount, + lastMessage, + consent, + }, + ]; + } + prev[index].unreadCount = unreadCount; + prev[index].lastMessage = lastMessage; + return [...prev]; + }); + }; + + console.log('Conversations Info:', conversationsInfo); + return ( {isXmtpEnabled && ( - handleOpenChat('')} + allConversations={conversations} + onConversationsUpdated={setConversations} + conversationsInfo={conversationsInfo} /> )} - ( + item.conversationId === conversation.topic + )?.unreadCount + } + lastMessage={ + conversationsInfo.find( + (item) => item.conversationId === conversation.topic + )?.lastMessage + } + handleConversationInfo={( + conversationId, + unreadCount, + lastMessage + ) => + handleConversationInfo( + conversationId, + unreadCount, + lastMessage, + 'allowed' + ) + } + /> + ))} + {conversations.blocked.map((conversation) => ( + item.conversationId === conversation.topic + )?.unreadCount + } + lastMessage={ + conversationsInfo.find( + (item) => item.conversationId === conversation.topic + )?.lastMessage + } + handleConversationInfo={( + conversationId, + unreadCount, + lastMessage + ) => + handleConversationInfo( + conversationId, + unreadCount, + lastMessage, + 'blocked' + ) + } + /> + ))} + {conversations.requested.map((conversation) => ( + item.conversationId === conversation.topic + )?.unreadCount + } + lastMessage={ + conversationsInfo.find( + (item) => item.conversationId === conversation.topic + )?.lastMessage + } + handleConversationInfo={( + conversationId, + unreadCount, + lastMessage + ) => + handleConversationInfo( + conversationId, + unreadCount, + lastMessage, + 'requested' + ) + } + /> + ))} + + { setPeerAddress(null); @@ -99,16 +249,122 @@ interface ChecksProps { env: 'local' | 'production' | 'dev'; } +interface GetConversationInfoProps { + conversation: CachedConversation; + handleConversationInfo: ( + conversationId: string, + unreadCount: number, + lastMessage: CachedMessage + ) => void; + unreadCount?: number; + lastMessage?: CachedMessage; +} + +export const GetConversationInfo: React.FC = ({ + conversation, + handleConversationInfo, + unreadCount, + lastMessage, +}) => { + const { messages } = useMessages(conversation); + + useStreamMessages(conversation); + const _unreadCount = useMemo(() => { + let count = 0; + const _messages = [...messages].reverse(); + for (const message of _messages) { + if (message.contentType === ContentTypeReadReceipt.toString()) { + break; + } + + count++; + } + + return count; + }, [messages]); + + const _lastMessage = useMemo(() => { + const _messages = [...messages]; + let lastMessage = _messages[_messages.length - 1]; + if (lastMessage?.contentType === ContentTypeReadReceipt.toString()) { + lastMessage = _messages[_messages.length - 2]; + } + + console.log('Last Message:', lastMessage); + return lastMessage; + }, [messages]); + + useEffect(() => { + if (unreadCount === _unreadCount && _lastMessage?.id === lastMessage?.id) { + return; + } + + console.log( + 'Updating Conversation Info:', + conversation.topic, + _unreadCount, + _lastMessage + ); + handleConversationInfo(conversation.topic, _unreadCount, _lastMessage); + }, [ + conversation.topic, + handleConversationInfo, + _unreadCount, + unreadCount, + _lastMessage, + lastMessage?.id, + ]); + + return null; +}; + export const Checks: React.FC = ({ open, handleXmtpEnabled, env, }) => { - const { client, initialize, isLoading } = useClient(); + const { client, initialize, isLoading, disconnect } = useClient(); const signer = useEthersSigner(); const { address } = useMountedAccount(); const [isInitializing, setIsInitializing] = React.useState(false); const [rejected, setRejected] = React.useState(false); + + useEffect(() => { + async function reinitializeXmtp() { + if (client && address) { + if (client?.address?.toLowerCase() !== address.toLowerCase()) { + await disconnect(); + + if (!signer) { + return; + } + setIsInitializing(true); + const clientOptions: Partial> = { + appVersion: 'JustWeb3/1.0.0/' + env + '/0', + env: env, + }; + let keys = loadKeys(address ?? '', env); + console.log('Keys:', keys); + if (!keys) { + keys = await Client.getKeys(signer, { + env: env, + skipContactPublishing: false, + // persistConversations: false, + }); + storeKeys(address ?? '', keys, env); + } + + await initialize({ + keys, + options: clientOptions, + signer: signer, + }); + } + } + } + reinitializeXmtp(); + }, [client, address, signer, env, initialize, disconnect]); + useEffect(() => { async function initializeXmtp() { if (isInitializing || isLoading || rejected) return; @@ -134,11 +390,14 @@ export const Checks: React.FC = ({ }); storeKeys(address ?? '', keys, env); } + await initialize({ keys, options: clientOptions, signer: signer, }); + + // _client?.registerCodec(new ReadReceiptCodec()); setIsInitializing(false); } catch (error) { console.error('Failed to initialize XMTP Client:', error); @@ -164,12 +423,6 @@ export const Checks: React.FC = ({ handleXmtpEnabled(!!client); }, [client, handleXmtpEnabled]); - // useEffect(() => { - // if (!address) { - // disconnect(); - // } - // }, [connectedEns?.ens, disconnect]); - return null; }; From 4a3af5ec741d4e6938b1311bd2fd8303c7e86875 Mon Sep 17 00:00:00 2001 From: HadiKhai Date: Mon, 9 Dec 2024 09:48:20 +0200 Subject: [PATCH 2/2] feat: chat optimization --- .gitignore | 4 +- apps/console/next.config.js | 5 +- package.json | 1 + .../lib/hooks/client/useEnsPublicClient.ts | 2 +- .../lib/hooks/primaryName/usePrimaryName.ts | 30 +- .../react/src/lib/hooks/records/useRecords.ts | 6 +- .../src/lib/providers/JustaNameProvider.tsx | 4 +- .../sdk/src/lib/api/axiosController.ts | 1 - .../src/lib/components/FollowButton/index.tsx | 3 + .../NotificationBadge.module.css | 3 +- .../ui/src/lib/ui/Tabs/Tabs.module.css | 3 + .../JustEnsCard/JustEnsCard.module.css | 5 + .../src/lib/components/JustEnsCard/index.tsx | 3 +- .../lib/components/JustWeb3Button/index.tsx | 29 + .../Profile/ContentSection/index.tsx | 5 +- .../src/lib/dialogs/SignInDialog/index.tsx | 1 + .../@justweb3/widget/src/lib/plugins/index.ts | 1 + .../lib/providers/JustWeb3Provider/index.tsx | 16 + .../@justweb3/xmtp-plugin/.storybook/main.ts | 11 +- .../xmtp-plugin/.storybook/preview.tsx | 14 +- .../ChatMessagesList/MessageCard/index.tsx | 1 - .../ChatSheet/Chat/ChatMessagesList/index.tsx | 31 +- .../Chat/ChatReactionOverlay/index.tsx | 2 +- .../Chat/ChatTextField/MessageInput/index.tsx | 1 + .../lib/components/ChatSheet/Chat/index.tsx | 46 +- .../InboxSheet/ChatList/MessageItem/index.tsx | 51 +- .../components/InboxSheet/ChatList/index.tsx | 66 +- .../src/lib/components/InboxSheet/index.tsx | 358 ++++--- .../components/JustWeb3ButtonRight/index.tsx | 84 ++ .../components/ProfileChatButton/index.tsx | 6 + .../src/lib/content-types/readReceipt.ts | 2 + .../src/lib/icons/ChatIcon/index.tsx | 17 + .../xmtp-plugin/src/lib/plugins/index.tsx | 18 +- .../providers/JustWeb3XMTPProvider/index.tsx | 86 +- .../xmtp-plugin/src/stories/xmtp.stories.tsx | 99 +- yarn.lock | 946 +++++++++++++++++- 36 files changed, 1597 insertions(+), 364 deletions(-) create mode 100644 packages/@justweb3/xmtp-plugin/src/lib/components/JustWeb3ButtonRight/index.tsx create mode 100644 packages/@justweb3/xmtp-plugin/src/lib/icons/ChatIcon/index.tsx diff --git a/.gitignore b/.gitignore index 0c426e1a..1af11c4b 100644 --- a/.gitignore +++ b/.gitignore @@ -62,4 +62,6 @@ cache/ .yarn/install-state.gz -storybook-static \ No newline at end of file +storybook-static + +.million \ No newline at end of file diff --git a/apps/console/next.config.js b/apps/console/next.config.js index 55fe2990..a4720cf2 100644 --- a/apps/console/next.config.js +++ b/apps/console/next.config.js @@ -2,6 +2,7 @@ // eslint-disable-next-line @typescript-eslint/no-var-requires const { composePlugins, withNx } = require('@nx/next'); +const MillionLint = require('@million/lint'); /** * @type {import('@nx/next/plugins/with-nx').WithNxOptions} @@ -23,4 +24,6 @@ const plugins = [ withNx, ]; -module.exports = composePlugins(...plugins)(nextConfig); +module.exports = MillionLint.next({ rsc: true })( + composePlugins(...plugins)(nextConfig) +); diff --git a/package.json b/package.json index 8e77fd0f..e3ed50e1 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@hookform/resolvers": "^3.9.0", "@inquirer/prompts": "^4.3.0", "@justaname.id/address-resolution": "^1.1.0", + "@million/lint": "^1.0.13", "@privy-io/react-auth": "^1.82.0", "@privy-io/wagmi": "^0.2.12", "@radix-ui/react-accordion": "^1.2.1", diff --git a/packages/@justaname.id/react/src/lib/hooks/client/useEnsPublicClient.ts b/packages/@justaname.id/react/src/lib/hooks/client/useEnsPublicClient.ts index 039c6fb5..288087d2 100644 --- a/packages/@justaname.id/react/src/lib/hooks/client/useEnsPublicClient.ts +++ b/packages/@justaname.id/react/src/lib/hooks/client/useEnsPublicClient.ts @@ -22,7 +22,7 @@ export const getEnsPublicClient = ( }; export const buildEnsPublicClientKey = (chainId: ChainId | undefined) => [ - 'CLIENT', + 'ENS_PUBLIC_CLIENT', chainId, ]; diff --git a/packages/@justaname.id/react/src/lib/hooks/primaryName/usePrimaryName.ts b/packages/@justaname.id/react/src/lib/hooks/primaryName/usePrimaryName.ts index 594082b5..2c704966 100644 --- a/packages/@justaname.id/react/src/lib/hooks/primaryName/usePrimaryName.ts +++ b/packages/@justaname.id/react/src/lib/hooks/primaryName/usePrimaryName.ts @@ -6,6 +6,7 @@ import { useEnsPublicClient } from '../client/useEnsPublicClient'; import { defaultOptions } from '../../query'; import { getName } from '@ensdomains/ensjs/public'; import { PrimaryNameTaskQueue } from './primary-name-task-queue'; +import { buildPrimaryNameBatchKey } from './usePrimaryNameBatch'; export const buildPrimaryName = ( address: string, @@ -59,6 +60,16 @@ export const usePrimaryName = ( let name = ''; + const primaryNames = queryClient.getQueryData( + buildPrimaryNameBatchKey(_chainId) + ) as Record; + + if (primaryNames && _params?.address) { + if (primaryNames[_params?.address]) { + return primaryNames[_params.address]; + } + } + const primaryNameGetByAddressResponse = await justaname.subnames.getPrimaryNameByAddress({ address: params?.address, @@ -82,25 +93,6 @@ export const usePrimaryName = ( name = reverseResolution.name; } } - - // const reverseResolution = await getName(ensClient, { - // address: params?.address, - // }); - // - // if (reverseResolution && reverseResolution?.name) { - // name = reverseResolution.name; - // } else { - // const primaryNameGetByAddressResponse = - // await justaname.subnames.getPrimaryNameByAddress({ - // address: params?.address, - // chainId: _chainId, - // }); - // - // if (primaryNameGetByAddressResponse) { - // name = primaryNameGetByAddressResponse.name; - // } - // } - // return name; }; diff --git a/packages/@justaname.id/react/src/lib/hooks/records/useRecords.ts b/packages/@justaname.id/react/src/lib/hooks/records/useRecords.ts index 2feb6b87..51b421de 100644 --- a/packages/@justaname.id/react/src/lib/hooks/records/useRecords.ts +++ b/packages/@justaname.id/react/src/lib/hooks/records/useRecords.ts @@ -260,11 +260,7 @@ export const useRecords = (params?: UseRecordsParams): UseRecordsResult => { } return failureCount < 3; }, - queryKey: buildRecordsBySubnameKey( - _ens || '', - _chainId - // params?.standard - ), + queryKey: buildRecordsBySubnameKey(_ens || '', _chainId), queryFn: () => getRecordsInternal( { diff --git a/packages/@justaname.id/react/src/lib/providers/JustaNameProvider.tsx b/packages/@justaname.id/react/src/lib/providers/JustaNameProvider.tsx index 2b49b9cb..b3a690a3 100644 --- a/packages/@justaname.id/react/src/lib/providers/JustaNameProvider.tsx +++ b/packages/@justaname.id/react/src/lib/providers/JustaNameProvider.tsx @@ -68,8 +68,8 @@ export const JustaNameProvider: FC = ({ const { chainId } = useMountedAccount(); const defaultChain = useMemo(() => { - return !chainId - ? undefined + return !chainId === undefined + ? 1 : chainId !== 1 && chainId !== 11155111 ? 1 : chainId; diff --git a/packages/@justaname.id/sdk/src/lib/api/axiosController.ts b/packages/@justaname.id/sdk/src/lib/api/axiosController.ts index 9a2dad0b..4e84ddc5 100644 --- a/packages/@justaname.id/sdk/src/lib/api/axiosController.ts +++ b/packages/@justaname.id/sdk/src/lib/api/axiosController.ts @@ -42,7 +42,6 @@ export const controlledAxiosPromise = >( return res.data.result.data as T; }) .catch((err: AxiosError>) => { - console.error(err); if (err?.response) { if (err?.response?.data?.result) { if (err?.response?.data?.result?.error) { diff --git a/packages/@justweb3/efp-plugin/src/lib/components/FollowButton/index.tsx b/packages/@justweb3/efp-plugin/src/lib/components/FollowButton/index.tsx index e334e222..3206dd24 100644 --- a/packages/@justweb3/efp-plugin/src/lib/components/FollowButton/index.tsx +++ b/packages/@justweb3/efp-plugin/src/lib/components/FollowButton/index.tsx @@ -15,6 +15,9 @@ export const FollowButton: React.FC = ({ ens, address }) => { addressOrEns1: address, addressOrEns2: ownAddress, }); + if (ownAddress === address) { + return null; + } if (!ownAddress) { return ( diff --git a/packages/@justweb3/ui/src/lib/ui/NotificationBadge/NotificationBadge.module.css b/packages/@justweb3/ui/src/lib/ui/NotificationBadge/NotificationBadge.module.css index d5c0f5a4..f0b5f0f9 100644 --- a/packages/@justweb3/ui/src/lib/ui/NotificationBadge/NotificationBadge.module.css +++ b/packages/@justweb3/ui/src/lib/ui/NotificationBadge/NotificationBadge.module.css @@ -1,7 +1,6 @@ .notificationIcon { position: relative; - display: inline-block; - + display: flex; } .icon { diff --git a/packages/@justweb3/ui/src/lib/ui/Tabs/Tabs.module.css b/packages/@justweb3/ui/src/lib/ui/Tabs/Tabs.module.css index dac10440..5510c3f7 100644 --- a/packages/@justweb3/ui/src/lib/ui/Tabs/Tabs.module.css +++ b/packages/@justweb3/ui/src/lib/ui/Tabs/Tabs.module.css @@ -6,6 +6,8 @@ margin-bottom: 10px; gap: 10px; display: flex; + overflow-x: auto; + overflow-y: visible; } /* Underlined variant */ @@ -22,6 +24,7 @@ font-family: var(--justweb3-font-family); border-bottom: 2px solid transparent; + } .underlinedTabs .tabsTrigger[data-state='active'] { diff --git a/packages/@justweb3/widget/src/lib/components/JustEnsCard/JustEnsCard.module.css b/packages/@justweb3/widget/src/lib/components/JustEnsCard/JustEnsCard.module.css index 11751fc2..3b130351 100644 --- a/packages/@justweb3/widget/src/lib/components/JustEnsCard/JustEnsCard.module.css +++ b/packages/@justweb3/widget/src/lib/components/JustEnsCard/JustEnsCard.module.css @@ -70,3 +70,8 @@ display: flex; gap: 5px; } + +.socialIcon { + width: 12px; + height: 12px; +} diff --git a/packages/@justweb3/widget/src/lib/components/JustEnsCard/index.tsx b/packages/@justweb3/widget/src/lib/components/JustEnsCard/index.tsx index 515471f2..5b24a65d 100644 --- a/packages/@justweb3/widget/src/lib/components/JustEnsCard/index.tsx +++ b/packages/@justweb3/widget/src/lib/components/JustEnsCard/index.tsx @@ -214,8 +214,7 @@ export const JustEnsCard: FC = ({ {records.sanitizedRecords.socials.map((social, index) => React.cloneElement(getTextRecordIcon(social.key), { key: `${ens}-${index}-${social.key}`, - width: 12, - height: 12, + className: styles.socialIcon, }) )} diff --git a/packages/@justweb3/widget/src/lib/components/JustWeb3Button/index.tsx b/packages/@justweb3/widget/src/lib/components/JustWeb3Button/index.tsx index 2c10fd20..2b53c6b3 100644 --- a/packages/@justweb3/widget/src/lib/components/JustWeb3Button/index.tsx +++ b/packages/@justweb3/widget/src/lib/components/JustWeb3Button/index.tsx @@ -162,6 +162,24 @@ export const JustWeb3Button: FC = ({ } const connectedEnsBtn = (withDialog: boolean) => { + const right = plugins.map((plugin) => { + const component = plugin.components?.JustWeb3ButtonRight; + if (!component) { + return null; + } + + return ( +
{ + setMobileDialogOpen(false); + }} + > + {component(createPluginApi(plugin.name))} +
+ ); + }); + return ( = ({ color: 'var(--justweb3-primary-color)', ...style, }} + right={ +
+ {right} +
+ } contentStyle={{ alignItems: 'start', }} diff --git a/packages/@justweb3/widget/src/lib/components/Profile/ContentSection/index.tsx b/packages/@justweb3/widget/src/lib/components/Profile/ContentSection/index.tsx index de020496..97d576b5 100644 --- a/packages/@justweb3/widget/src/lib/components/Profile/ContentSection/index.tsx +++ b/packages/@justweb3/widget/src/lib/components/Profile/ContentSection/index.tsx @@ -51,6 +51,9 @@ export interface ContentProps { plugins: JustaPlugin[]; } +const ENS_MAINNET_RESOLVER = '0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41'; +const ENS_SEPOLIA_RESOLVER = '0x8FADE66B79cC9f707aB26799354482EB93a5B7dD'; + const ContentSection: React.FC = ({ fullSubname = '', chainId = 1, @@ -65,7 +68,7 @@ const ContentSection: React.FC = ({ const { accountEnsNames } = useAccountEnsNames(); const [tab, setTab] = React.useState('Main'); const { openEnsProfile } = useJustWeb3(); - + // const { offchainResolvers } = useOffchainResolvers() const isProfileSelf = useMemo(() => { const tempEns = accountEnsNames ?.map((ens) => ens.ens) diff --git a/packages/@justweb3/widget/src/lib/dialogs/SignInDialog/index.tsx b/packages/@justweb3/widget/src/lib/dialogs/SignInDialog/index.tsx index eeb51db4..0bad3cb3 100644 --- a/packages/@justweb3/widget/src/lib/dialogs/SignInDialog/index.tsx +++ b/packages/@justweb3/widget/src/lib/dialogs/SignInDialog/index.tsx @@ -30,6 +30,7 @@ import styles from './SignInDialog.module.css'; import clsx from 'clsx'; const ENS_MAINNET_RESOLVER = '0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41'; +// const BASE_MAINNET_RESOLVER = '0xde9049636F4a1dfE0a64d1bFe3155C0A14C54F31' const ENS_SEPOLIA_RESOLVER = '0x8FADE66B79cC9f707aB26799354482EB93a5B7dD'; interface TransitionElementProps extends React.HTMLAttributes { diff --git a/packages/@justweb3/widget/src/lib/plugins/index.ts b/packages/@justweb3/widget/src/lib/plugins/index.ts index 6337ffd3..5ba874b9 100644 --- a/packages/@justweb3/widget/src/lib/plugins/index.ts +++ b/packages/@justweb3/widget/src/lib/plugins/index.ts @@ -50,6 +50,7 @@ interface PluginComponents { Provider?: PluginProviderComponent; Global?: PluginComponent; SignInMenu?: PluginComponent; + JustWeb3ButtonRight?: PluginComponent; ProfileSection?: PluginRichComponent; ProfileHeader?: PluginRichComponent; ProfileTab?: ProfileTabPluginComponent; diff --git a/packages/@justweb3/widget/src/lib/providers/JustWeb3Provider/index.tsx b/packages/@justweb3/widget/src/lib/providers/JustWeb3Provider/index.tsx index 90058cf0..5e3bf1ad 100644 --- a/packages/@justweb3/widget/src/lib/providers/JustWeb3Provider/index.tsx +++ b/packages/@justweb3/widget/src/lib/providers/JustWeb3Provider/index.tsx @@ -403,6 +403,22 @@ const CheckSession: FC<{ ); const isConnectedPrevious = usePreviousState(isConnected, [isConnected]); + useEffect(() => { + if (isConnecting || isReconnecting || isEnsAuthPending) { + return; + } + + const timeout = setTimeout(() => { + if (!isConnected) { + signOut(); + } + }, 1000); + + return () => { + clearTimeout(timeout); + }; + }, [isConnected, isConnecting, isEnsAuthPending, isReconnecting, signOut]); + useEffect(() => { if (connectedEns && chainId) { if ( diff --git a/packages/@justweb3/xmtp-plugin/.storybook/main.ts b/packages/@justweb3/xmtp-plugin/.storybook/main.ts index 3e575653..73c1c0cc 100644 --- a/packages/@justweb3/xmtp-plugin/.storybook/main.ts +++ b/packages/@justweb3/xmtp-plugin/.storybook/main.ts @@ -13,7 +13,16 @@ const config: StorybookConfig = { viteFinal: async (config) => mergeConfig(config, { - plugins: [react(), nxViteTsPaths()], + plugins: [ + react(), + nxViteTsPaths(), + // MillionLint.vite({ + // filter: { + // // I want a regex to exclude all the react tsx components with Icon at the end + // exclude: /Icon$/, + // }, + // }), + ], define: { 'process.env': process.env, }, diff --git a/packages/@justweb3/xmtp-plugin/.storybook/preview.tsx b/packages/@justweb3/xmtp-plugin/.storybook/preview.tsx index 0b60c255..297516f8 100644 --- a/packages/@justweb3/xmtp-plugin/.storybook/preview.tsx +++ b/packages/@justweb3/xmtp-plugin/.storybook/preview.tsx @@ -1,11 +1,11 @@ import './polyfills'; -import { scan } from 'react-scan'; +// import { scan } from 'react-scan'; -if (typeof window !== 'undefined') { - scan({ - enabled: true, - log: true, // logs render info to console (default: false) - }); -} +// if (typeof window !== 'undefined') { +// scan({ +// enabled: true, +// log: true, // logs render info to console (default: false) +// }); +// } export const decorators = [(Story) => ]; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatMessagesList/MessageCard/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatMessagesList/MessageCard/index.tsx index 8ad5ce6c..ae0491c4 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatMessagesList/MessageCard/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatMessagesList/MessageCard/index.tsx @@ -243,7 +243,6 @@ export const MessageCard: React.FC = ({ overflowWrap: 'break-word', maxWidth: !isImage ? '240px' : 'none', fontSize: '14px', - lineHeight: '14px', padding: isReply ? '4px' : '5px', borderRadius: '14px', backgroundColor: isReceiver diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatMessagesList/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatMessagesList/index.tsx index ebbdee51..2aad68e1 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatMessagesList/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatMessagesList/index.tsx @@ -4,6 +4,7 @@ import { DateDivider } from './DateDivider'; import { EmojiSelector } from './EmojiSelector'; import { MessageCard } from './MessageCard'; import { MessageWithReaction } from '../../../../utils/filterReactionsMessages'; +import { useEffect, useState } from 'react'; interface ChatMessagesListProps { canMessage: boolean; @@ -26,6 +27,19 @@ export const ChatMessagesList: React.FC = ({ handleEmojiSelect, computeHeight, }) => { + const [ref, setRef] = useState(null); + useEffect(() => { + if (ref) { + setTimeout(() => { + const lastGroupChild = ref.lastElementChild as HTMLElement; + + const lastMessage = lastGroupChild?.lastElementChild as HTMLElement; + + lastMessage?.scrollIntoView({ behavior: 'smooth' }); + }, 500); + } + }, [groupedMessages, ref]); + if (!canMessage) { return ( = ({ return ( setRef(el)} > {groupedMessages && Object.keys(groupedMessages).map((date, index) => ( - + {groupedMessages[date].map((message) => ( = ({ borderTopRightRadius: replyMessage ? 0 : '100px', borderTop: replyMessage ? '0px' : '', }} + autoFocus placeholder="Send message..." value={messageValue} left={ diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/index.tsx index de637057..688c55de 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/index.tsx @@ -11,7 +11,6 @@ import { useCanMessage, useConsent, useMessages, - useStreamMessages, } from '@xmtp/react-sdk'; import { useJustWeb3 } from '@justweb3/widget'; import { ChatTextField } from './ChatTextField'; @@ -46,7 +45,7 @@ export const Chat: React.FC = ({ conversation, onBack }) => { const [isRequestChangeLoading, setIsRequestChangeLoading] = useState(false); - const { entries, allow, refreshConsentList, deny } = useConsent(); + const { entries, allow, deny } = useConsent(); const { mutateAsync: sendReaction } = useSendReactionMessage(conversation); const { primaryName } = usePrimaryName({ address: conversation.peerAddress as `0x${string}`, @@ -59,7 +58,6 @@ export const Chat: React.FC = ({ conversation, onBack }) => { useCanMessage(); const { mutateAsync: readReceipt, isPending: isReadReceiptSending } = useReadReceipt(conversation); - useStreamMessages(conversation); useEffect(() => { const lastMessage = messages[messages.length - 1]; @@ -70,8 +68,16 @@ export const Chat: React.FC = ({ conversation, onBack }) => { return; } - readReceipt(); - }, [messages, readReceipt, isReadReceiptSending]); + if (entries[conversation.peerAddress]?.permissionType === 'allowed') { + readReceipt(); + } + }, [ + messages, + readReceipt, + isReadReceiptSending, + entries, + conversation.peerAddress, + ]); // Determine if user can message useEffect(() => { @@ -87,19 +93,6 @@ export const Chat: React.FC = ({ conversation, onBack }) => { ); }, [entries, conversation.peerAddress]); - // Scroll to last message when messages change - useEffect(() => { - if (messages.length === 0) return; - setTimeout(() => { - const lastMessageId = messages[messages.length - 1]?.id; - const element = document.getElementById(lastMessageId); - if (element) { - element.scrollIntoView({ behavior: 'smooth' }); - } - }, 500); - }, [messages, conversation]); - - // Filter out read receipts and group messages by date const filteredMessages = useMemo(() => { const withoutRead = messages.filter( (message) => message.contentType !== 'xmtp.org/readReceipt:1.0' @@ -125,10 +118,10 @@ export const Chat: React.FC = ({ conversation, onBack }) => { const type = mimeType ? typeLookup[mimeType.split('/')?.[1]] : null; const computeHeight = useMemo(() => { - const baseHeight = 'calc(100vh - 50px - 3rem - 1.5rem - 73px - 15px)'; + const baseHeight = '100vh - 50px - 3rem - 1.5rem - 73px - 15px'; const adjustments: string[] = []; - if (isRequest) adjustments.push('40px'); + if (isRequest) return 'calc(100vh - 50px - 3rem - 1.5rem - 15px - 34px)'; if (replyMessage) { if (isStringContent) { adjustments.push('46px'); @@ -142,8 +135,9 @@ export const Chat: React.FC = ({ conversation, onBack }) => { } if (isMessagesSenderOnly) adjustments.push('59px'); - if (adjustments.length === 0) return baseHeight; - return `${baseHeight}${adjustments.map((val) => ` - ${val}`).join('')}`; + return ` calc(${baseHeight}${adjustments + .map((val) => ` - ${val}`) + .join('')}) `; }, [ replyMessage, isMessagesSenderOnly, @@ -156,18 +150,18 @@ export const Chat: React.FC = ({ conversation, onBack }) => { // Handlers const blockAddressHandler = async (peerAddress: string) => { setIsRequestChangeLoading(true); - await refreshConsentList(); + // await refreshConsentList(); await deny([peerAddress]); - await refreshConsentList(); + // await refreshConsentList(); setIsRequestChangeLoading(false); onBack(); }; const handleAllowAddress = async () => { setIsRequestChangeLoading(true); - await refreshConsentList(); + // await refreshConsentList(); await allow([conversation.peerAddress]); - await refreshConsentList(); + // await refreshConsentList(); setIsRequest(false); setIsRequestChangeLoading(false); }; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/InboxSheet/ChatList/MessageItem/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/InboxSheet/ChatList/MessageItem/index.tsx index a994b787..f3640899 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/InboxSheet/ChatList/MessageItem/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/InboxSheet/ChatList/MessageItem/index.tsx @@ -6,7 +6,6 @@ import { reactionContentTypeConfig, replyContentTypeConfig, useConsent, - useStreamMessages, } from '@xmtp/react-sdk'; import { useEnsAvatar, useRecords } from '@justaname.id/react'; import { Avatar, Button, Flex, formatText, P, SPAN } from '@justweb3/ui'; @@ -33,8 +32,6 @@ const MessageItem: React.FC = ({ primaryName, conversationInfo, }) => { - useStreamMessages(conversation); - // const { messages } = useMessages(conversation); const { records } = useRecords({ ens: primaryName || undefined, }); @@ -45,15 +42,15 @@ const MessageItem: React.FC = ({ // return lastMessage.contentType !== ContentTypeReadReceipt.toString(); // }, [lastMessage]); - const { allow, refreshConsentList } = useConsent(); + const { allow, deny } = useConsent(); const allowUser = async () => { - await refreshConsentList(); await allow([conversation.peerAddress]); - await refreshConsentList(); }; - console.log('conversationInfo', conversationInfo); + const ignoreUser = async () => { + await deny([conversation.peerAddress]); + }; const lastContent = useMemo(() => { const lastMessage = conversationInfo?.lastMessage; @@ -80,14 +77,12 @@ const MessageItem: React.FC = ({ if ( replyContentTypeConfig.contentTypes.includes(lastMessage?.contentType) ) { - return lastMessage.contentFallback; + return 'replied "' + lastMessage.content.content + '"'; } return lastMessage.contentFallback; }, [conversationInfo, conversationInfo?.lastMessage]); - console.log(lastContent); - return ( = ({ direction={'column'} style={{ marginLeft: '10px', - maxWidth: blocked - ? 'calc(100% - 120px)' - : 'calc(100% - 50px - 32px - 10px)', + maxWidth: + conversationInfo?.consent === 'requested' + ? 'calc(100% - 132px - 44px)' + : conversationInfo?.consent === 'blocked' + ? 'calc(100% - 120px)' + : 'calc(100% - 50px - 32px - 10px)', justifyContent: 'space-between', }} > @@ -158,10 +156,29 @@ const MessageItem: React.FC = ({ textAlign: 'end', }} > - {blocked ? ( - + {conversationInfo?.consent !== 'allowed' ? ( + + + {conversationInfo?.consent === 'requested' && ( + + )} + ) : (
[]; @@ -14,6 +14,7 @@ export interface ChatListProps { conversation: CachedConversation ) => void; blockedList?: boolean; + primaryNames: PrimaryNameRecord | undefined; conversationsInfo?: { conversationId: string; unreadCount: number; @@ -27,37 +28,50 @@ export const ChatList: React.FC = ({ handleOpenChat, blockedList, conversationsInfo, + primaryNames, }) => { - const { allPrimaryNames } = usePrimaryNameBatch({ - addresses: conversations.map((conversation) => conversation.peerAddress), - }); - return ( - {conversations.map((conversation) => ( - item.conversationId === conversation.topic - // )?.unreadCount - // } - // - conversationInfo={conversationsInfo?.find( - (item) => item.conversationId === conversation.topic - )} - onClick={() => handleOpenChat(conversation)} - key={conversation.topic} - blocked={blockedList} - /> - ))} + {conversationsInfo + ?.sort((a, b) => { + if (a.lastMessage?.sentAt && b.lastMessage?.sentAt) { + // a.lastMessage.sentAt and b.lastMessage.sentA are Date objects + return ( + b.lastMessage.sentAt.getTime() - a.lastMessage.sentAt.getTime() + ); + } + return 0; + }) + .map((conv) => { + const conversation = conversations.find( + (item) => item.topic === conv.conversationId + ); + + if (!conversation) return null; + + return ( + item.conversationId === conversation.topic + // )?.unreadCount + // } + // + conversationInfo={conv} + onClick={() => handleOpenChat(conversation)} + key={conversation.topic} + blocked={blockedList} + /> + ); + })} ); }; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/InboxSheet/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/InboxSheet/index.tsx index c4496252..a8cdbf43 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/InboxSheet/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/InboxSheet/index.tsx @@ -4,6 +4,7 @@ import { Sheet, SheetContent, SheetTitle, + SPAN, Tabs, TabsContent, TabsList, @@ -13,13 +14,18 @@ import { CachedConversation, CachedMessage, ContentTypeMetadata, + Conversation, + useClient, useConsent, useConversations, useStreamAllMessages, + useStreamConsentList, useStreamConversations, } from '@xmtp/react-sdk'; import React, { useEffect, useMemo } from 'react'; import { ChatList } from './ChatList'; +import { useMountedAccount, usePrimaryNameBatch } from '@justaname.id/react'; +import { isEqual } from 'lodash'; export interface InboxSheetProps { open?: boolean; @@ -60,10 +66,43 @@ export const InboxSheet: React.FC = ({ conversationsInfo, }) => { const [tab, setTab] = React.useState('Chats'); - const { conversations, isLoading } = useConversations(); - + const { conversations: cachedConversations, isLoading } = useConversations(); + const { address } = useMountedAccount(); + const conversations = useMemo(() => { + return cachedConversations.filter((convo) => convo.peerAddress !== address); + }, [cachedConversations, address]); const [isConsentListLoading, setIsConsentListLoading] = React.useState(true); - const { loadConsentList, entries } = useConsent(); + const { entries, loadConsentList } = useConsent(); + const { client } = useClient(); + + const [initialConversations, setInitialConversations] = React.useState< + Conversation[] | null + >(null); + useEffect(() => { + if (!client) return; + + const fetchConversations = async () => { + const _conversations = await client.conversations.list(); + setInitialConversations(_conversations); + }; + fetchConversations(); + }, [client]); + + const primaryNameConversations = useMemo(() => { + if (!initialConversations) return; + if (conversations?.length > initialConversations?.length) { + return conversations; + } + + return initialConversations; + }, [conversations, initialConversations]); + const { allPrimaryNames } = usePrimaryNameBatch({ + addresses: primaryNameConversations?.map( + (conversation) => conversation.peerAddress + ), + + enabled: initialConversations !== null, + }); const allowedConversations = useMemo(() => { return conversations.filter( @@ -91,56 +130,39 @@ export const InboxSheet: React.FC = ({ }, [conversations, entries]); useEffect(() => { - let _allowedConversations = [] as CachedConversation[]; - let _blockedConversations = [] as CachedConversation[]; - let _requestConversations = [] as CachedConversation[]; + const allowedConversationsTopic = allowedConversations.map( + (convo) => convo.topic + ); - if ( - allowedConversations.some( - (convo) => - !allConversations.allowed.some((c) => c.topic === convo.topic) - ) - ) { - _allowedConversations = allowedConversations.filter( - (convo) => - !allConversations.allowed.some((c) => c.topic === convo.topic) - ); - // onConversationsUpdated([...allConversations, ...newConversations]); - } + const blockedConversationsTopic = blockedConversations.map( + (convo) => convo.topic + ); - if ( - blockedConversations.some( - (convo) => - !allConversations.blocked.some((c) => c.topic === convo.topic) - ) - ) { - _blockedConversations = blockedConversations.filter( - (convo) => - !allConversations.blocked.some((c) => c.topic === convo.topic) - ); - } + const requestConversationsTopic = requestConversations.map( + (convo) => convo.topic + ); - if ( - requestConversations.some( - (convo) => - !allConversations.requested.some((c) => c.topic === convo.topic) - ) - ) { - _requestConversations = requestConversations.filter( - (convo) => - !allConversations.requested.some((c) => c.topic === convo.topic) - ); - } + const allConversationsAllowedTopic = allConversations.allowed.map( + (convo) => convo.topic + ); + + const allConversationsBlockedTopic = allConversations.blocked.map( + (convo) => convo.topic + ); + + const allConversationsRequestedTopic = allConversations.requested.map( + (convo) => convo.topic + ); if ( - _allowedConversations.length > 0 || - _blockedConversations.length > 0 || - _requestConversations.length > 0 + !isEqual(allowedConversationsTopic, allConversationsAllowedTopic) || + !isEqual(blockedConversationsTopic, allConversationsBlockedTopic) || + !isEqual(requestConversationsTopic, allConversationsRequestedTopic) ) { onConversationsUpdated({ - allowed: [...allConversations.allowed, ..._allowedConversations], - blocked: [...allConversations.blocked, ..._blockedConversations], - requested: [...allConversations.requested, ..._requestConversations], + allowed: allowedConversations, + blocked: blockedConversations, + requested: requestConversations, }); } }, [ @@ -152,13 +174,15 @@ export const InboxSheet: React.FC = ({ ]); useEffect(() => { + if (!isConsentListLoading) return; loadConsentList().then(() => { setIsConsentListLoading(false); }); - }, [loadConsentList]); + }, [loadConsentList, isConsentListLoading]); useStreamConversations(); useStreamAllMessages(); + useStreamConsentList(); return ( @@ -171,7 +195,7 @@ export const InboxSheet: React.FC = ({ width: 45, height: 45, borderRadius: '50%', - backgroundColor: 'var(--justweb3-primary-color', + backgroundColor: 'var(--justweb3-primary-color)', cursor: 'pointer', position: 'absolute', bottom: '2rem', @@ -186,88 +210,172 @@ export const InboxSheet: React.FC = ({ height={35} /> - setTab(value)} - style={{ - display: 'flex', - flexDirection: 'column', - marginBottom: '0px', - overflow: 'hidden', - marginTop: '10px', - flex: '1', - }} - > - - - Chats - - + setTab(value)} + style={{ + display: 'flex', + flexDirection: 'column', + marginBottom: '0px', + // overflow: 'hidden', + maxHeight: 'calc(100vh - 72px - 10px - 28px - 10px)', + minHeight: 'calc(100vh - 72px - 10px - 28px - 10px)', + marginTop: '10px', + flex: '1', + }} + > + + + Chats + + + Requests + {requestConversations.length > 0 && ( +
+ + {requestConversations.length} + +
+ )} +
+ + Blocked + +
+ {isLoading || isConsentListLoading ? ( + // {true ? ( +
+ + Loading... + +
+ ) : ( + <> + + + + + + + + + + + )} +
+ + + - Requests - {requestConversations.length > 0 && ( - - {requestConversations.length} - - )} -
- + - Blocked - -
- {isLoading || isConsentListLoading ? ( -
Loading...
- ) : ( - <> - - + + - - - - - - - - - )} -
+ + + + + + + + + +
); diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/JustWeb3ButtonRight/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/JustWeb3ButtonRight/index.tsx new file mode 100644 index 00000000..5358282e --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/JustWeb3ButtonRight/index.tsx @@ -0,0 +1,84 @@ +import { XmtpEnvironment } from '../../plugins'; +import { NotificationBadge } from '@justweb3/ui'; +import { ChatIcon } from '../../icons/ChatIcon'; +import { useJustWeb3XMTP } from '../../providers/JustWeb3XMTPProvider'; +import { useMemo } from 'react'; +import { Client, ClientOptions, useClient } from '@xmtp/react-sdk'; +import { useEthersSigner } from '../../hooks'; +import { useMountedAccount } from '@justaname.id/react'; +import { loadKeys, storeKeys, wipeKeys } from '../../utils/xmtp'; + +export interface ChatMenuButtonProps { + handleOpen: (open: boolean) => void; + env: XmtpEnvironment; +} + +export const JustWeb3ButtonRight: React.FC = ({ + handleOpen, + env, +}) => { + const { conversationsInfo } = useJustWeb3XMTP(); + const totalUnreadCount = useMemo(() => { + return conversationsInfo + .filter((conversation) => conversation.consent === 'allowed') + .reduce((acc, curr) => acc + curr.unreadCount, 0); + }, [conversationsInfo]); + const { initialize } = useClient(); + const { client } = useClient(); + const walletClient = useEthersSigner(); + const { address } = useMountedAccount(); + + const handleChat = async () => { + if (!client) { + const signer = walletClient; + try { + if (!signer) { + return; + } + const clientOptions: Partial> = { + appVersion: 'JustWeb3/1.0.0', + env: env, + }; + let keys = loadKeys(address ?? '', env); + if (!keys) { + keys = await Client.getKeys(signer, { + env: env, + skipContactPublishing: false, + // persistConversations: false, + }); + storeKeys(address ?? '', keys, env); + } + await initialize({ + keys, + options: clientOptions, + signer: signer, + }).then(() => { + handleOpen(true); + }); + + // handleClient(client) + } catch (error) { + console.error('Failed to initialize XMTP Client:', error); + wipeKeys(address ?? '', env); + } + } else { + handleOpen(true); + } + }; + + return ( + { + e.stopPropagation(); + handleChat(); + }} + /> + } + /> + ); +}; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ProfileChatButton/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ProfileChatButton/index.tsx index c0965bde..902cb71f 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/ProfileChatButton/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ProfileChatButton/index.tsx @@ -12,12 +12,14 @@ export interface ProfileChatButtonProps { ens: string; env: 'local' | 'production' | 'dev'; chainId: ChainId; + address: string; } export const ProfileChatButton: React.FC = ({ ens, env, chainId, + address: profileAddress, }) => { const { closeEnsProfile } = useJustWeb3(); const { handleOpenChat } = useJustWeb3XMTP(); @@ -92,6 +94,10 @@ export const ProfileChatButton: React.FC = ({ } }, [canMessage, canMessageAddress, env, records]); + if (profileAddress === address) { + return null; + } + return (
) => { + return ( + + + + ); +}; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/plugins/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/plugins/index.tsx index 90d1a0a3..22758fb3 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/plugins/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/plugins/index.tsx @@ -2,6 +2,7 @@ import { JustaPlugin } from '@justweb3/widget'; import { JustWeb3XMTPProvider } from '../providers/JustWeb3XMTPProvider'; import { ChatMenuButton } from '../components/ChatMenuButton'; import { ProfileChatButton } from '../components/ProfileChatButton'; +import { JustWeb3ButtonRight } from '../components/JustWeb3ButtonRight'; export type XmtpEnvironment = 'local' | 'production' | 'dev'; @@ -9,6 +10,14 @@ export const XMTPPlugin = (env: XmtpEnvironment): JustaPlugin => { return { name: 'XMTPPlugin', components: { + JustWeb3ButtonRight: (pluginApi) => { + return ( + pluginApi.setState('xmtpOpen', open)} + env={env} + /> + ); + }, Provider: (pluginApi, children) => { return ( { ); }, ProfileHeader: (pluginApi, ens, chainId, address) => { - return ; + return ( + + ); }, SignInMenu: (pluginApi) => { return ( diff --git a/packages/@justweb3/xmtp-plugin/src/lib/providers/JustWeb3XMTPProvider/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/providers/JustWeb3XMTPProvider/index.tsx index 1ee5a0a8..7092f8d0 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/providers/JustWeb3XMTPProvider/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/providers/JustWeb3XMTPProvider/index.tsx @@ -11,7 +11,6 @@ import { replyContentTypeConfig, useClient, useMessages, - useStreamMessages, XMTPProvider, } from '@xmtp/react-sdk'; import { InboxSheet } from '../../components/InboxSheet'; @@ -56,6 +55,7 @@ export const JustWeb3XMTPProvider: React.FC = ({ handleOpen, env, }) => { + // const { isConnected } = useMountedAccount() const [isXmtpEnabled, setIsXmtpEnabled] = React.useState(false); const [conversation, setConversation] = React.useState | null>(null); @@ -77,7 +77,6 @@ export const JustWeb3XMTPProvider: React.FC = ({ }[] >([]); - console.log(conversations); const handleXmtpEnabled = (enabled: boolean) => { setIsXmtpEnabled(enabled); }; @@ -116,12 +115,11 @@ export const JustWeb3XMTPProvider: React.FC = ({ } prev[index].unreadCount = unreadCount; prev[index].lastMessage = lastMessage; + prev[index].consent = consent; return [...prev]; }); }; - console.log('Conversations Info:', conversationsInfo); - return ( = ({ }) => { const { messages } = useMessages(conversation); - useStreamMessages(conversation); const _unreadCount = useMemo(() => { let count = 0; const _messages = [...messages].reverse(); @@ -285,12 +282,17 @@ export const GetConversationInfo: React.FC = ({ const _lastMessage = useMemo(() => { const _messages = [...messages]; - let lastMessage = _messages[_messages.length - 1]; - if (lastMessage?.contentType === ContentTypeReadReceipt.toString()) { - lastMessage = _messages[_messages.length - 2]; + // let lastMessage = _messages[_messages.length - 1]; + let lastMessageIndex = _messages.length - 1; + let lastMessage = _messages[lastMessageIndex]; + while ( + lastMessage?.contentType === ContentTypeReadReceipt.toString() && + lastMessageIndex > 0 + ) { + lastMessageIndex--; + lastMessage = _messages[lastMessageIndex]; } - console.log('Last Message:', lastMessage); return lastMessage; }, [messages]); @@ -299,12 +301,6 @@ export const GetConversationInfo: React.FC = ({ return; } - console.log( - 'Updating Conversation Info:', - conversation.topic, - _unreadCount, - _lastMessage - ); handleConversationInfo(conversation.topic, _unreadCount, _lastMessage); }, [ conversation.topic, @@ -330,44 +326,42 @@ export const Checks: React.FC = ({ const [rejected, setRejected] = React.useState(false); useEffect(() => { + if (!client || !address || isInitializing) return; + if (client.address.toLowerCase() === address.toLowerCase()) return; + async function reinitializeXmtp() { - if (client && address) { - if (client?.address?.toLowerCase() !== address.toLowerCase()) { - await disconnect(); - - if (!signer) { - return; - } - setIsInitializing(true); - const clientOptions: Partial> = { - appVersion: 'JustWeb3/1.0.0/' + env + '/0', - env: env, - }; - let keys = loadKeys(address ?? '', env); - console.log('Keys:', keys); - if (!keys) { - keys = await Client.getKeys(signer, { - env: env, - skipContactPublishing: false, - // persistConversations: false, - }); - storeKeys(address ?? '', keys, env); - } - - await initialize({ - keys, - options: clientOptions, - signer: signer, - }); - } + await disconnect(); + + if (!signer) { + return; } + setIsInitializing(true); + const clientOptions: Partial> = { + appVersion: 'JustWeb3/1.0.0/' + env + '/0', + env: env, + }; + let keys = loadKeys(address ?? '', env); + if (!keys) { + keys = await Client.getKeys(signer, { + env: env, + skipContactPublishing: false, + // persistConversations: false, + }); + storeKeys(address ?? '', keys, env); + } + + await initialize({ + keys, + options: clientOptions, + signer: signer, + }); } reinitializeXmtp(); - }, [client, address, signer, env, initialize, disconnect]); + }, [client, address, signer, env, initialize, disconnect, isInitializing]); useEffect(() => { + if (isInitializing || isLoading || rejected) return; async function initializeXmtp() { - if (isInitializing || isLoading || rejected) return; try { if (client) { return; diff --git a/packages/@justweb3/xmtp-plugin/src/stories/xmtp.stories.tsx b/packages/@justweb3/xmtp-plugin/src/stories/xmtp.stories.tsx index 03c180ca..6dd6e287 100644 --- a/packages/@justweb3/xmtp-plugin/src/stories/xmtp.stories.tsx +++ b/packages/@justweb3/xmtp-plugin/src/stories/xmtp.stories.tsx @@ -1,5 +1,4 @@ import { - JustEnsCard, JustWeb3Button, JustWeb3Provider, JustWeb3ProviderConfig, @@ -93,55 +92,55 @@ export const Example = () => {
-
- {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - - - -
-
- - - - - - -
+ {/*
*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/*
*/} + {/**/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/*
*/}
diff --git a/yarn.lock b/yarn.lock index 902a2f9e..7630baab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -62,6 +62,31 @@ __metadata: languageName: node linkType: hard +"@antfu/ni@npm:^0.21.12": + version: 0.21.12 + resolution: "@antfu/ni@npm:0.21.12" + bin: + na: bin/na.mjs + nci: bin/nci.mjs + ni: bin/ni.mjs + nlx: bin/nlx.mjs + nr: bin/nr.mjs + nu: bin/nu.mjs + nun: bin/nun.mjs + checksum: 10c0/729e03cdde75087bd1e959357fa69caf5c8ecf80087f19ac07ecceeb0def3eabbe87a8bd88a78444bea321cce8e385a1c90dca4b5146c351e32c2a516c2d9ea2 + languageName: node + linkType: hard + +"@axiomhq/js@npm:1.0.0-rc.3": + version: 1.0.0-rc.3 + resolution: "@axiomhq/js@npm:1.0.0-rc.3" + dependencies: + fetch-retry: "npm:^6.0.0" + uuid: "npm:^8.3.2" + checksum: 10c0/bcd02c844c19dcfa49e7e75fffbafffea6caa46756e8f194e41b281c807fde26ccdb2198844672ad078c54ba9609949585ffd2c2d0ed9b5db1d74a77dae5f883 + languageName: node + linkType: hard + "@babel/code-frame@npm:7.10.4, @babel/code-frame@npm:~7.10.4": version: 7.10.4 resolution: "@babel/code-frame@npm:7.10.4" @@ -89,7 +114,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.14.5, @babel/core@npm:^7.18.9, @babel/core@npm:^7.20.0, @babel/core@npm:^7.21.3, @babel/core@npm:^7.22.5, @babel/core@npm:^7.23.0, @babel/core@npm:^7.23.2, @babel/core@npm:^7.23.9, @babel/core@npm:^7.24.4, @babel/core@npm:^7.25.2, @babel/core@npm:^7.7.5": +"@babel/core@npm:7.26.0, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.14.5, @babel/core@npm:^7.18.9, @babel/core@npm:^7.20.0, @babel/core@npm:^7.21.3, @babel/core@npm:^7.22.5, @babel/core@npm:^7.23.0, @babel/core@npm:^7.23.2, @babel/core@npm:^7.23.9, @babel/core@npm:^7.24.4, @babel/core@npm:^7.25.2, @babel/core@npm:^7.7.5": version: 7.26.0 resolution: "@babel/core@npm:7.26.0" dependencies: @@ -1707,7 +1732,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.24.0, @babel/types@npm:^7.24.7, @babel/types@npm:^7.25.4, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": +"@babel/types@npm:7.26.0, @babel/types@npm:^7.0.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.24.0, @babel/types@npm:^7.24.7, @babel/types@npm:^7.25.4, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": version: 7.26.0 resolution: "@babel/types@npm:7.26.0" dependencies: @@ -1798,7 +1823,7 @@ __metadata: languageName: node linkType: hard -"@clack/core@npm:0.3.5, @clack/core@npm:^0.3.5": +"@clack/core@npm:0.3.5, @clack/core@npm:^0.3.3, @clack/core@npm:^0.3.5": version: 0.3.5 resolution: "@clack/core@npm:0.3.5" dependencies: @@ -1808,6 +1833,18 @@ __metadata: languageName: node linkType: hard +"@clack/prompts@npm:^0.7.0": + version: 0.7.0 + resolution: "@clack/prompts@npm:0.7.0" + dependencies: + "@clack/core": "npm:^0.3.3" + is-unicode-supported: "npm:*" + picocolors: "npm:^1.0.0" + sisteransi: "npm:^1.0.5" + checksum: 10c0/fecb3b34308c5cb75807211b28d50caa4b0c5d150d16e733e59bfba3187ac856f050ed44baeca90eb99e047671096ff54402dd2790e9c0e77845a75b04003e2e + languageName: node + linkType: hard + "@clack/prompts@npm:^0.8.2": version: 0.8.2 resolution: "@clack/prompts@npm:0.8.2" @@ -2111,6 +2148,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/aix-ppc64@npm:0.20.2" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/aix-ppc64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/aix-ppc64@npm:0.21.5" @@ -2132,6 +2176,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/android-arm64@npm:0.20.2" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/android-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/android-arm64@npm:0.21.5" @@ -2153,6 +2204,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/android-arm@npm:0.20.2" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@esbuild/android-arm@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/android-arm@npm:0.21.5" @@ -2174,6 +2232,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-x64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/android-x64@npm:0.20.2" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + "@esbuild/android-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/android-x64@npm:0.21.5" @@ -2195,6 +2260,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-arm64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/darwin-arm64@npm:0.20.2" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/darwin-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/darwin-arm64@npm:0.21.5" @@ -2216,6 +2288,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-x64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/darwin-x64@npm:0.20.2" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@esbuild/darwin-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/darwin-x64@npm:0.21.5" @@ -2237,6 +2316,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-arm64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/freebsd-arm64@npm:0.20.2" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/freebsd-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/freebsd-arm64@npm:0.21.5" @@ -2258,6 +2344,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-x64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/freebsd-x64@npm:0.20.2" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/freebsd-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/freebsd-x64@npm:0.21.5" @@ -2279,6 +2372,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/linux-arm64@npm:0.20.2" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/linux-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-arm64@npm:0.21.5" @@ -2300,6 +2400,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/linux-arm@npm:0.20.2" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@esbuild/linux-arm@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-arm@npm:0.21.5" @@ -2321,6 +2428,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ia32@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/linux-ia32@npm:0.20.2" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/linux-ia32@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-ia32@npm:0.21.5" @@ -2342,6 +2456,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-loong64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/linux-loong64@npm:0.20.2" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + "@esbuild/linux-loong64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-loong64@npm:0.21.5" @@ -2363,6 +2484,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-mips64el@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/linux-mips64el@npm:0.20.2" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + "@esbuild/linux-mips64el@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-mips64el@npm:0.21.5" @@ -2384,6 +2512,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ppc64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/linux-ppc64@npm:0.20.2" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/linux-ppc64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-ppc64@npm:0.21.5" @@ -2405,6 +2540,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-riscv64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/linux-riscv64@npm:0.20.2" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + "@esbuild/linux-riscv64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-riscv64@npm:0.21.5" @@ -2426,6 +2568,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-s390x@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/linux-s390x@npm:0.20.2" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + "@esbuild/linux-s390x@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-s390x@npm:0.21.5" @@ -2447,6 +2596,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-x64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/linux-x64@npm:0.20.2" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + "@esbuild/linux-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-x64@npm:0.21.5" @@ -2468,6 +2624,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-x64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/netbsd-x64@npm:0.20.2" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/netbsd-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/netbsd-x64@npm:0.21.5" @@ -2489,6 +2652,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-x64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/openbsd-x64@npm:0.20.2" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/openbsd-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/openbsd-x64@npm:0.21.5" @@ -2510,6 +2680,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/sunos-x64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/sunos-x64@npm:0.20.2" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + "@esbuild/sunos-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/sunos-x64@npm:0.21.5" @@ -2531,6 +2708,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-arm64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/win32-arm64@npm:0.20.2" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/win32-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/win32-arm64@npm:0.21.5" @@ -2552,6 +2736,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-ia32@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/win32-ia32@npm:0.20.2" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/win32-ia32@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/win32-ia32@npm:0.21.5" @@ -2573,6 +2764,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-x64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/win32-x64@npm:0.20.2" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@esbuild/win32-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/win32-x64@npm:0.21.5" @@ -3652,6 +3850,15 @@ __metadata: languageName: node linkType: hard +"@hono/node-server@npm:^1.11.1": + version: 1.13.7 + resolution: "@hono/node-server@npm:1.13.7" + peerDependencies: + hono: ^4 + checksum: 10c0/a50da48fd0b5c647db20a4cefdbec10af957753427bb6b66388f2716a9d3910f192f2f51d0a3ddb9e5e0d6f9057451daabbd84fe8f6190e5ab15c35f86ebaafa + languageName: node + linkType: hard + "@hookform/error-message@npm:^2.0.1": version: 2.0.1 resolution: "@hookform/error-message@npm:2.0.1" @@ -5074,6 +5281,66 @@ __metadata: languageName: node linkType: hard +"@million/install@npm:latest": + version: 1.0.13 + resolution: "@million/install@npm:1.0.13" + dependencies: + "@antfu/ni": "npm:^0.21.12" + "@axiomhq/js": "npm:1.0.0-rc.3" + "@babel/parser": "npm:^7.25.3" + "@babel/types": "npm:7.26.0" + "@clack/prompts": "npm:^0.7.0" + ast-types: "npm:^0.14.2" + cli-high: "npm:^0.4.2" + diff: "npm:^5.1.0" + effect: "npm:^3.8.4" + nanoid: "npm:^5.0.7" + recast: "npm:^0.23.9" + xycolors: "npm:^0.1.2" + bin: + install: bin/index.js + checksum: 10c0/496617a85663b95b196c698cb6b768aef18954d932eaf01760a5f1299fbe91f4010720bc835596a05a2124bf580cb13eb0b04bc6f12d0b4158d3096290b55307 + languageName: node + linkType: hard + +"@million/lint@npm:^1.0.13": + version: 1.0.13 + resolution: "@million/lint@npm:1.0.13" + dependencies: + "@axiomhq/js": "npm:1.0.0-rc.3" + "@babel/core": "npm:7.26.0" + "@babel/types": "npm:7.26.0" + "@hono/node-server": "npm:^1.11.1" + "@million/install": "npm:latest" + "@rollup/pluginutils": "npm:^5.1.0" + "@rrweb/types": "npm:2.0.0-alpha.16" + babel-plugin-syntax-hermes-parser: "npm:^0.21.1" + ci-info: "npm:^4.0.0" + esbuild: "npm:^0.20.1" + faster-babel-types: "npm:^0.1.0" + hono: "npm:^4.5.9" + isomorphic-fetch: "npm:^3.0.0" + nanoid: "npm:^5.0.7" + ohash: "npm:^1.1.4" + pako: "npm:^2.1.0" + pathe: "npm:^1.1.2" + piscina: "npm:^4.4.0" + pretty-ms: "npm:8.0.0" + react-scan: "npm:^0.0.31" + rrweb: "npm:2.0.0-alpha.4" + rrweb-player: "npm:1.0.0-alpha.4" + semver: "npm:^7.6.2" + socket.io: "npm:^4.8.1" + socket.io-client: "npm:^4.7.5" + tmp: "npm:^0.2.3" + unplugin: "npm:^1.6.0" + update-notifier-cjs: "npm:^5.1.6" + bin: + lint: cli.js + checksum: 10c0/bd5d28ed49137edf20fa9c0bf1ecc429d2ef0f5fda63ff4f020861bce9049d9dd3b6d52f9daa5a5816b312486c7919a802e567e3bac7fa2491d582d3a27cdce5 + languageName: node + linkType: hard + "@module-federation/bridge-react-webpack-plugin@npm:0.6.14": version: 0.6.14 resolution: "@module-federation/bridge-react-webpack-plugin@npm:0.6.14" @@ -8725,6 +8992,29 @@ __metadata: languageName: node linkType: hard +"@rrweb/types@npm:2.0.0-alpha.16": + version: 2.0.0-alpha.16 + resolution: "@rrweb/types@npm:2.0.0-alpha.16" + dependencies: + rrweb-snapshot: "npm:^2.0.0-alpha.16" + checksum: 10c0/d2eb1e755c3bed7fda19ebdaca92df5ec83979326d2597c95fcdd1a59e4f0fbd17b6f3f5bc48e6faaf098a4521f13cf1aeaf9247e026d8c02e0843ea313498d0 + languageName: node + linkType: hard + +"@rrweb/types@npm:^2.0.0-alpha.18, @rrweb/types@npm:^2.0.0-alpha.4": + version: 2.0.0-alpha.18 + resolution: "@rrweb/types@npm:2.0.0-alpha.18" + checksum: 10c0/a1adb842f59b782e04576a7e88ffc2e43b8b29460555922e6a15f8ffd4840e554e96c1331653b0a322a3a8f25e73a9ae38ff37d3e60f1da71b6f6c0d001a20e7 + languageName: node + linkType: hard + +"@rrweb/utils@npm:^2.0.0-alpha.18": + version: 2.0.0-alpha.18 + resolution: "@rrweb/utils@npm:2.0.0-alpha.18" + checksum: 10c0/04a24b838de7294254ead08fbdc39daa9e04a86ed519eba3d6a85d485da56da02aa19cd61d82606c538d324060d6a0fc80fc41813d474e2b57ad9b61e74a137c + languageName: node + linkType: hard + "@rtsao/scc@npm:^1.1.0": version: 1.1.0 resolution: "@rtsao/scc@npm:1.1.0" @@ -11304,6 +11594,13 @@ __metadata: languageName: node linkType: hard +"@tsconfig/svelte@npm:^1.0.0": + version: 1.0.13 + resolution: "@tsconfig/svelte@npm:1.0.13" + checksum: 10c0/701da672b70300a023754eca604788fe96542178cbf362122bb1083a81f3ff4f8922213960abb99aaf194977e0582eb76a51695a9dd9b5fd6a458e17fb0c3074 + languageName: node + linkType: hard + "@tufjs/canonical-json@npm:2.0.0": version: 2.0.0 resolution: "@tufjs/canonical-json@npm:2.0.0" @@ -11454,7 +11751,14 @@ __metadata: languageName: node linkType: hard -"@types/cors@npm:^2.8.17": +"@types/cookie@npm:^0.4.1": + version: 0.4.1 + resolution: "@types/cookie@npm:0.4.1" + checksum: 10c0/f96afe12bd51be1ec61410b0641243d93fa3a494702407c787a4c872b5c8bcd39b224471452055e44a9ce42af1a636e87d161994226eaf4c2be9c30f60418409 + languageName: node + linkType: hard + +"@types/cors@npm:^2.8.12, @types/cors@npm:^2.8.17": version: 2.8.17 resolution: "@types/cors@npm:2.8.17" dependencies: @@ -11472,6 +11776,13 @@ __metadata: languageName: node linkType: hard +"@types/css-font-loading-module@npm:0.0.7": + version: 0.0.7 + resolution: "@types/css-font-loading-module@npm:0.0.7" + checksum: 10c0/a74759a14bcc7d60a1a1d863b53b7638d4aa7f88f1d97347426262cc6fe8f9335d8fa80c7e0608cd67e33ff0067608e9b5475a1227a684e1dfad3cac87df1405 + languageName: node + linkType: hard + "@types/debug@npm:^4.1.7": version: 4.1.12 resolution: "@types/debug@npm:4.1.12" @@ -11844,6 +12155,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:>=10.0.0": + version: 22.10.1 + resolution: "@types/node@npm:22.10.1" + dependencies: + undici-types: "npm:~6.20.0" + checksum: 10c0/0fbb6d29fa35d807f0223a4db709c598ac08d66820240a2cd6a8a69b8f0bc921d65b339d850a666b43b4e779f967e6ed6cf6f0fca3575e08241e6b900364c234 + languageName: node + linkType: hard + "@types/node@npm:>=13.7.0": version: 22.9.1 resolution: "@types/node@npm:22.9.1" @@ -14397,6 +14717,13 @@ __metadata: languageName: node linkType: hard +"@xstate/fsm@npm:^1.4.0": + version: 1.6.5 + resolution: "@xstate/fsm@npm:1.6.5" + checksum: 10c0/472fe625b84b9e7102b8774e80c441b8b7dbc9585e700223d9c7a39c583d38ba50636909a5d749d44b361555daa841862f932a29c52b81c8829a74884e715bf4 + languageName: node + linkType: hard + "@xtuc/ieee754@npm:^1.2.0": version: 1.2.0 resolution: "@xtuc/ieee754@npm:1.2.0" @@ -14741,6 +15068,15 @@ __metadata: languageName: node linkType: hard +"ansi-align@npm:^3.0.0": + version: 3.0.1 + resolution: "ansi-align@npm:3.0.1" + dependencies: + string-width: "npm:^4.1.0" + checksum: 10c0/ad8b755a253a1bc8234eb341e0cec68a857ab18bf97ba2bda529e86f6e30460416523e0ec58c32e5c21f0ca470d779503244892873a5895dbd0c39c788e82467 + languageName: node + linkType: hard + "ansi-colors@npm:^4.1.1, ansi-colors@npm:^4.1.3": version: 4.1.3 resolution: "ansi-colors@npm:4.1.3" @@ -15209,6 +15545,15 @@ __metadata: languageName: node linkType: hard +"ast-types@npm:^0.14.2": + version: 0.14.2 + resolution: "ast-types@npm:0.14.2" + dependencies: + tslib: "npm:^2.0.1" + checksum: 10c0/5d66d89b6c07fe092087454b6042dbaf81f2882b176db93861e2b986aafe0bce49e1f1ff59aac775d451c1426ad1e967d250e9e3548f5166ea8a3475e66c169d + languageName: node + linkType: hard + "ast-types@npm:^0.16.1": version: 0.16.1 resolution: "ast-types@npm:0.16.1" @@ -15567,6 +15912,15 @@ __metadata: languageName: node linkType: hard +"babel-plugin-syntax-hermes-parser@npm:^0.21.1": + version: 0.21.1 + resolution: "babel-plugin-syntax-hermes-parser@npm:0.21.1" + dependencies: + hermes-parser: "npm:0.21.1" + checksum: 10c0/0134b435e194654f5ba96b6f48ea8ac37c31e9f52e29261bddeefb0cb98852a13cf58a569dd1f39479098dbaf54a0152bff995faf58b8206eb64c2ffaa800b8d + languageName: node + linkType: hard + "babel-plugin-syntax-trailing-function-commas@npm:^7.0.0-beta.0": version: 7.0.0-beta.0 resolution: "babel-plugin-syntax-trailing-function-commas@npm:7.0.0-beta.0" @@ -15768,6 +16122,13 @@ __metadata: languageName: node linkType: hard +"base64-arraybuffer@npm:^1.0.1": + version: 1.0.2 + resolution: "base64-arraybuffer@npm:1.0.2" + checksum: 10c0/3acac95c70f9406e87a41073558ba85b6be9dbffb013a3d2a710e3f2d534d506c911847d5d9be4de458af6362c676de0a5c4c2d7bdf4def502d00b313368e72f + languageName: node + linkType: hard + "base64-js@npm:^1.2.3, base64-js@npm:^1.3.0, base64-js@npm:^1.3.1, base64-js@npm:^1.5.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" @@ -15775,6 +16136,13 @@ __metadata: languageName: node linkType: hard +"base64id@npm:2.0.0, base64id@npm:~2.0.0": + version: 2.0.0 + resolution: "base64id@npm:2.0.0" + checksum: 10c0/6919efd237ed44b9988cbfc33eca6f173a10e810ce50292b271a1a421aac7748ef232a64d1e6032b08f19aae48dce6ee8f66c5ae2c9e5066c82b884861d4d453 + languageName: node + linkType: hard + "base64url@npm:3.0.1, base64url@npm:^3.0.1": version: 3.0.1 resolution: "base64url@npm:3.0.1" @@ -16013,6 +16381,22 @@ __metadata: languageName: node linkType: hard +"boxen@npm:^5.0.0": + version: 5.1.2 + resolution: "boxen@npm:5.1.2" + dependencies: + ansi-align: "npm:^3.0.0" + camelcase: "npm:^6.2.0" + chalk: "npm:^4.1.0" + cli-boxes: "npm:^2.2.1" + string-width: "npm:^4.2.2" + type-fest: "npm:^0.20.2" + widest-line: "npm:^3.1.0" + wrap-ansi: "npm:^7.0.0" + checksum: 10c0/71f31c2eb3dcacd5fce524ae509e0cc90421752e0bfbd0281fd3352871d106c462a0f810c85f2fdb02f3a9fab2d7a84e9718b4999384d651b76104ebe5d2c024 + languageName: node + linkType: hard + "bplist-creator@npm:0.1.1": version: 0.1.1 resolution: "bplist-creator@npm:0.1.1" @@ -16975,6 +17359,13 @@ __metadata: languageName: node linkType: hard +"cli-boxes@npm:^2.2.1": + version: 2.2.1 + resolution: "cli-boxes@npm:2.2.1" + checksum: 10c0/6111352edbb2f62dbc7bfd58f2d534de507afed7f189f13fa894ce5a48badd94b2aa502fda28f1d7dd5f1eb456e7d4033d09a76660013ef50c7f66e7a034f050 + languageName: node + linkType: hard + "cli-columns@npm:^4.0.0": version: 4.0.0 resolution: "cli-columns@npm:4.0.0" @@ -17012,6 +17403,20 @@ __metadata: languageName: node linkType: hard +"cli-high@npm:^0.4.2": + version: 0.4.3 + resolution: "cli-high@npm:0.4.3" + dependencies: + "@clack/prompts": "npm:^0.7.0" + sugar-high: "npm:^0.7.1" + xycolors: "npm:^0.1.2" + yargs: "npm:^17.7.2" + bin: + cli-high: bin/index.js + checksum: 10c0/0f93a06436d20d7224fd21a815c46dcad10952724ba7dc3d3ac67ff60fa818676238115fac4620547322e92b450cc88f642efb449ad05c71b648e0fe10a7ec17 + languageName: node + linkType: hard + "cli-spinners@npm:2.6.1": version: 2.6.1 resolution: "cli-spinners@npm:2.6.1" @@ -17469,6 +17874,20 @@ __metadata: languageName: node linkType: hard +"configstore@npm:^5.0.1": + version: 5.0.1 + resolution: "configstore@npm:5.0.1" + dependencies: + dot-prop: "npm:^5.2.0" + graceful-fs: "npm:^4.1.2" + make-dir: "npm:^3.0.0" + unique-string: "npm:^2.0.0" + write-file-atomic: "npm:^3.0.0" + xdg-basedir: "npm:^4.0.0" + checksum: 10c0/5af23830e78bdc56cbe92a2f81e87f1d3a39e96e51a0ab2a8bc79bbbc5d4440a48d92833b3fd9c6d34b4a9c4c5853c8487b8e6e68593e7ecbc7434822f7aced3 + languageName: node + linkType: hard + "confusing-browser-globals@npm:^1.0.9": version: 1.0.11 resolution: "confusing-browser-globals@npm:1.0.11" @@ -17627,7 +18046,7 @@ __metadata: languageName: node linkType: hard -"cookie@npm:0.7.2": +"cookie@npm:0.7.2, cookie@npm:~0.7.2": version: 0.7.2 resolution: "cookie@npm:0.7.2" checksum: 10c0/9596e8ccdbf1a3a88ae02cf5ee80c1c50959423e1022e4e60b91dd87c622af1da309253d8abdb258fb5e3eacb4f08e579dc58b4897b8087574eee0fd35dfa5d2 @@ -17706,7 +18125,7 @@ __metadata: languageName: node linkType: hard -"cors@npm:2.8.5, cors@npm:^2.8.5": +"cors@npm:2.8.5, cors@npm:^2.8.5, cors@npm:~2.8.5": version: 2.8.5 resolution: "cors@npm:2.8.5" dependencies: @@ -18560,7 +18979,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:4.3.7, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:~4.3.1, debug@npm:~4.3.2": +"debug@npm:4, debug@npm:4.3.7, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:~4.3.1, debug@npm:~4.3.2, debug@npm:~4.3.4": version: 4.3.7 resolution: "debug@npm:4.3.7" dependencies: @@ -19241,7 +19660,7 @@ __metadata: languageName: node linkType: hard -"dot-prop@npm:^5.1.0": +"dot-prop@npm:^5.1.0, dot-prop@npm:^5.2.0": version: 5.3.0 resolution: "dot-prop@npm:5.3.0" dependencies: @@ -19349,6 +19768,15 @@ __metadata: languageName: node linkType: hard +"effect@npm:^3.8.4": + version: 3.11.3 + resolution: "effect@npm:3.11.3" + dependencies: + fast-check: "npm:^3.21.0" + checksum: 10c0/a343a20d833112e21b27f17f404e97a33240fafe09d638428381d8bb96b4f0d2eccdaebe43453dce4982242134e1c8cfbab1d63e364fea79de02faeb92940c04 + languageName: node + linkType: hard + "ejs@npm:^3.1.10, ejs@npm:^3.1.7": version: 3.1.10 resolution: "ejs@npm:3.1.10" @@ -19541,6 +19969,24 @@ __metadata: languageName: node linkType: hard +"engine.io@npm:~6.6.0": + version: 6.6.2 + resolution: "engine.io@npm:6.6.2" + dependencies: + "@types/cookie": "npm:^0.4.1" + "@types/cors": "npm:^2.8.12" + "@types/node": "npm:>=10.0.0" + accepts: "npm:~1.3.4" + base64id: "npm:2.0.0" + cookie: "npm:~0.7.2" + cors: "npm:~2.8.5" + debug: "npm:~4.3.1" + engine.io-parser: "npm:~5.2.1" + ws: "npm:~8.17.1" + checksum: 10c0/e9ac3cba49badb6905259df3b019fbcbe53e2a389c930fb9fbc10eebc8839554b189706206bba2509a4a3a7d78a32f7e027f73230f31662c7efd215276432dad + languageName: node + linkType: hard + "enhanced-resolve@npm:^5.0.0, enhanced-resolve@npm:^5.15.0, enhanced-resolve@npm:^5.17.1, enhanced-resolve@npm:^5.7.0": version: 5.17.1 resolution: "enhanced-resolve@npm:5.17.1" @@ -20136,6 +20582,86 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:^0.20.1": + version: 0.20.2 + resolution: "esbuild@npm:0.20.2" + dependencies: + "@esbuild/aix-ppc64": "npm:0.20.2" + "@esbuild/android-arm": "npm:0.20.2" + "@esbuild/android-arm64": "npm:0.20.2" + "@esbuild/android-x64": "npm:0.20.2" + "@esbuild/darwin-arm64": "npm:0.20.2" + "@esbuild/darwin-x64": "npm:0.20.2" + "@esbuild/freebsd-arm64": "npm:0.20.2" + "@esbuild/freebsd-x64": "npm:0.20.2" + "@esbuild/linux-arm": "npm:0.20.2" + "@esbuild/linux-arm64": "npm:0.20.2" + "@esbuild/linux-ia32": "npm:0.20.2" + "@esbuild/linux-loong64": "npm:0.20.2" + "@esbuild/linux-mips64el": "npm:0.20.2" + "@esbuild/linux-ppc64": "npm:0.20.2" + "@esbuild/linux-riscv64": "npm:0.20.2" + "@esbuild/linux-s390x": "npm:0.20.2" + "@esbuild/linux-x64": "npm:0.20.2" + "@esbuild/netbsd-x64": "npm:0.20.2" + "@esbuild/openbsd-x64": "npm:0.20.2" + "@esbuild/sunos-x64": "npm:0.20.2" + "@esbuild/win32-arm64": "npm:0.20.2" + "@esbuild/win32-ia32": "npm:0.20.2" + "@esbuild/win32-x64": "npm:0.20.2" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 10c0/66398f9fb2c65e456a3e649747b39af8a001e47963b25e86d9c09d2a48d61aa641b27da0ce5cad63df95ad246105e1d83e7fee0e1e22a0663def73b1c5101112 + languageName: node + linkType: hard + "escalade@npm:^3.1.1, escalade@npm:^3.2.0": version: 3.2.0 resolution: "escalade@npm:3.2.0" @@ -20143,6 +20669,13 @@ __metadata: languageName: node linkType: hard +"escape-goat@npm:^2.0.0": + version: 2.1.1 + resolution: "escape-goat@npm:2.1.1" + checksum: 10c0/fc0ad656f89c05e86a9641a21bdc5ea37b258714c057430b68a834854fa3e5770cda7d41756108863fc68b1e36a0946463017b7553ac39eaaf64815be07816fc + languageName: node + linkType: hard + "escape-html@npm:^1.0.3, escape-html@npm:~1.0.3": version: 1.0.3 resolution: "escape-html@npm:1.0.3" @@ -21463,6 +21996,15 @@ __metadata: languageName: node linkType: hard +"fast-check@npm:^3.21.0": + version: 3.23.1 + resolution: "fast-check@npm:3.23.1" + dependencies: + pure-rand: "npm:^6.1.0" + checksum: 10c0/d61ee4a7a2e1abc5126bf2f1894413f532f686b3d1fc15c67fefb60dcca66024934b69a6454d3eba92e6568ac1abbb9882080e212d255865c3b3bbe52c5bf702 + languageName: node + linkType: hard + "fast-copy@npm:^3.0.0, fast-copy@npm:^3.0.2": version: 3.0.2 resolution: "fast-copy@npm:3.0.2" @@ -21587,6 +22129,15 @@ __metadata: languageName: node linkType: hard +"faster-babel-types@npm:^0.1.0": + version: 0.1.0 + resolution: "faster-babel-types@npm:0.1.0" + peerDependencies: + "@babel/types": ^7 + checksum: 10c0/e73f27146458d8af39582231e2b65643f1d74a3cc184ed68a684c3c74cb8b8d054773c174fb6a0bb7a85523af91f10a11ae6f542766d5be6d7b570680c2bb256 + languageName: node + linkType: hard + "fastest-levenshtein@npm:^1.0.12, fastest-levenshtein@npm:^1.0.16": version: 1.0.16 resolution: "fastest-levenshtein@npm:1.0.16" @@ -21693,6 +22244,20 @@ __metadata: languageName: node linkType: hard +"fetch-retry@npm:^6.0.0": + version: 6.0.0 + resolution: "fetch-retry@npm:6.0.0" + checksum: 10c0/8e275b042ff98041236d30b71966f24c34ff19f957bb0f00e664754bd63d0dfb5122d091e7d5bca21f6370d88a1713d22421b33471305d7b86d6799427278802 + languageName: node + linkType: hard + +"fflate@npm:^0.4.4": + version: 0.4.8 + resolution: "fflate@npm:0.4.8" + checksum: 10c0/29d1eddaaa5deab61b1c6b0d21282adacadbc4d2c01e94d8b1ee784398151673b9c563e53f97a801bc410a1ae55e8de5378114a743430e643e7a0644ba8e5a42 + languageName: node + linkType: hard + "fflate@npm:^0.8.1": version: 0.8.2 resolution: "fflate@npm:0.8.2" @@ -23100,6 +23665,13 @@ __metadata: languageName: node linkType: hard +"has-yarn@npm:^2.1.0": + version: 2.1.0 + resolution: "has-yarn@npm:2.1.0" + checksum: 10c0/b5cab61b4129c2fc0474045b59705371b7f5ddf2aab8ba8725011e52269f017e06f75059a2c8a1d8011e9779c2885ad987263cfc6d1280f611c396b45fd5d74a + languageName: node + linkType: hard + "has@npm:^1.0.3": version: 1.0.4 resolution: "has@npm:1.0.4" @@ -23255,6 +23827,13 @@ __metadata: languageName: node linkType: hard +"hermes-estree@npm:0.21.1": + version: 0.21.1 + resolution: "hermes-estree@npm:0.21.1" + checksum: 10c0/b9bacf41ced8346e4e1d91519530a5facf8cde8662a4ed732b85bb989b11d596832d3fe743e6ccd212eb08ca160ad504d191895cc13b62932f5ba14edb27516f + languageName: node + linkType: hard + "hermes-estree@npm:0.23.1": version: 0.23.1 resolution: "hermes-estree@npm:0.23.1" @@ -23271,6 +23850,15 @@ __metadata: languageName: node linkType: hard +"hermes-parser@npm:0.21.1": + version: 0.21.1 + resolution: "hermes-parser@npm:0.21.1" + dependencies: + hermes-estree: "npm:0.21.1" + checksum: 10c0/a3df443bfef835a982865da16a0b0bda5d86efa699791d5007e14b1b2731cb62cb9b1ac9157581602d35ff0ae437a345e701e03dd6330d9c552e8dafa9776436 + languageName: node + linkType: hard + "hermes-parser@npm:0.23.1": version: 0.23.1 resolution: "hermes-parser@npm:0.23.1" @@ -23330,6 +23918,13 @@ __metadata: languageName: node linkType: hard +"hono@npm:^4.5.9": + version: 4.6.13 + resolution: "hono@npm:4.6.13" + checksum: 10c0/8bf6ed856e3204d8dadc74cddc0f5a8e99e78a12df00e5b57f4d6c416ad7f808e6140688a30606c7d4b2a585b0ad085b7ed8be419f13a4b58e29dfdd54cfdac8 + languageName: node + linkType: hard + "hosted-git-info@npm:^2.1.4": version: 2.8.9 resolution: "hosted-git-info@npm:2.8.9" @@ -23896,6 +24491,13 @@ __metadata: languageName: node linkType: hard +"import-lazy@npm:^2.1.0": + version: 2.1.0 + resolution: "import-lazy@npm:2.1.0" + checksum: 10c0/c5e5f507d26ee23c5b2ed64577155810361ac37863b322cae0c17f16b6a8cdd15adf370288384ddd95ef9de05602fb8d87bf76ff835190eb037333c84db8062c + languageName: node + linkType: hard + "import-lazy@npm:~4.0.0": version: 4.0.0 resolution: "import-lazy@npm:4.0.0" @@ -24304,6 +24906,17 @@ __metadata: languageName: node linkType: hard +"is-ci@npm:^2.0.0": + version: 2.0.0 + resolution: "is-ci@npm:2.0.0" + dependencies: + ci-info: "npm:^2.0.0" + bin: + is-ci: bin.js + checksum: 10c0/17de4e2cd8f993c56c86472dd53dd9e2c7f126d0ee55afe610557046cdd64de0e8feadbad476edc9eeff63b060523b8673d9094ed2ab294b59efb5a66dd05a9a + languageName: node + linkType: hard + "is-ci@npm:^3.0.1": version: 3.0.1 resolution: "is-ci@npm:3.0.1" @@ -24493,7 +25106,7 @@ __metadata: languageName: node linkType: hard -"is-installed-globally@npm:~0.4.0": +"is-installed-globally@npm:^0.4.0, is-installed-globally@npm:~0.4.0": version: 0.4.0 resolution: "is-installed-globally@npm:0.4.0" dependencies: @@ -24571,6 +25184,13 @@ __metadata: languageName: node linkType: hard +"is-npm@npm:^5.0.0": + version: 5.0.0 + resolution: "is-npm@npm:5.0.0" + checksum: 10c0/8ded3ae1119bbbda22395fe1c64d2d79d3b3baeb2635c90f9a9dca4b8ce19a67b55fda178269b63421b257b361892fd545807fb5ac212f06776f544d9fcc3ab0 + languageName: node + linkType: hard + "is-number-object@npm:^1.0.4": version: 1.0.7 resolution: "is-number-object@npm:1.0.7" @@ -24772,6 +25392,13 @@ __metadata: languageName: node linkType: hard +"is-unicode-supported@npm:*, is-unicode-supported@npm:^2.0.0": + version: 2.1.0 + resolution: "is-unicode-supported@npm:2.1.0" + checksum: 10c0/a0f53e9a7c1fdbcf2d2ef6e40d4736fdffff1c9f8944c75e15425118ff3610172c87bf7bc6c34d3903b04be59790bb2212ddbe21ee65b5a97030fc50370545a5 + languageName: node + linkType: hard + "is-unicode-supported@npm:^0.1.0": version: 0.1.0 resolution: "is-unicode-supported@npm:0.1.0" @@ -24786,13 +25413,6 @@ __metadata: languageName: node linkType: hard -"is-unicode-supported@npm:^2.0.0": - version: 2.1.0 - resolution: "is-unicode-supported@npm:2.1.0" - checksum: 10c0/a0f53e9a7c1fdbcf2d2ef6e40d4736fdffff1c9f8944c75e15425118ff3610172c87bf7bc6c34d3903b04be59790bb2212ddbe21ee65b5a97030fc50370545a5 - languageName: node - linkType: hard - "is-valid-path@npm:^0.1.1": version: 0.1.1 resolution: "is-valid-path@npm:0.1.1" @@ -24874,6 +25494,13 @@ __metadata: languageName: node linkType: hard +"is-yarn-global@npm:^0.3.0": + version: 0.3.0 + resolution: "is-yarn-global@npm:0.3.0" + checksum: 10c0/9f1ab6f28e6e7961c4b97e564791d1decf2886a0dbe9b92b2176d76156adbb42b4c06c0f33d7107b270c207cbcfe0b2293b7cc4a0ec6774ac6d37af9503d51e1 + languageName: node + linkType: hard + "is64bit@npm:^2.0.0": version: 2.0.0 resolution: "is64bit@npm:2.0.0" @@ -24918,6 +25545,16 @@ __metadata: languageName: node linkType: hard +"isomorphic-fetch@npm:^3.0.0": + version: 3.0.0 + resolution: "isomorphic-fetch@npm:3.0.0" + dependencies: + node-fetch: "npm:^2.6.1" + whatwg-fetch: "npm:^3.4.1" + checksum: 10c0/511b1135c6d18125a07de661091f5e7403b7640060355d2d704ce081e019bc1862da849482d079ce5e2559b8976d3de7709566063aec1b908369c0b98a2b075b + languageName: node + linkType: hard + "isomorphic-rslog@npm:0.0.4": version: 0.0.4 resolution: "isomorphic-rslog@npm:0.0.4" @@ -26712,6 +27349,7 @@ __metadata: "@hookform/resolvers": "npm:^3.9.0" "@inquirer/prompts": "npm:^4.3.0" "@justaname.id/address-resolution": "npm:^1.1.0" + "@million/lint": "npm:^1.0.13" "@nx/cypress": "npm:19.7.3" "@nx/eslint": "npm:19.7.3" "@nx/eslint-plugin": "npm:19.7.3" @@ -29029,6 +29667,13 @@ __metadata: languageName: node linkType: hard +"mitt@npm:^3.0.0": + version: 3.0.1 + resolution: "mitt@npm:3.0.1" + checksum: 10c0/3ab4fdecf3be8c5255536faa07064d05caa3dd332bd318ff02e04621f7b3069ca1de9106cfe8e7ced675abfc2bec2ce4c4ef321c4a1bb1fb29df8ae090741913 + languageName: node + linkType: hard + "mkdirp@npm:1.0.4, mkdirp@npm:^1.0.3, mkdirp@npm:^1.0.4": version: 1.0.4 resolution: "mkdirp@npm:1.0.4" @@ -29228,6 +29873,15 @@ __metadata: languageName: node linkType: hard +"nanoid@npm:^5.0.7": + version: 5.0.9 + resolution: "nanoid@npm:5.0.9" + bin: + nanoid: bin/nanoid.js + checksum: 10c0/a2d9710525d4998a8a1610bbe6eb9a92c254ebab7c567c1ab429046fe7eed9c4df3508b59fb44c58ffdc98edb28dd6f953715c14b64ea0a3a2ce37420cdfeefd + languageName: node + linkType: hard + "napi-wasm@npm:^1.1.0": version: 1.1.3 resolution: "napi-wasm@npm:1.1.3" @@ -30941,6 +31595,13 @@ __metadata: languageName: node linkType: hard +"parse-ms@npm:^3.0.0": + version: 3.0.0 + resolution: "parse-ms@npm:3.0.0" + checksum: 10c0/056b4a32a9d3749f3f4cfffefb45c45540491deaa8e1d8ad43c2ddde7ba04edd076bd1b298f521238bb5fb084a9b2c4a2ebb78aefa651afbc4c2b0af4232fc54 + languageName: node + linkType: hard + "parse-ms@npm:^4.0.0": version: 4.0.0 resolution: "parse-ms@npm:4.0.0" @@ -31405,6 +32066,18 @@ __metadata: languageName: node linkType: hard +"piscina@npm:^4.4.0": + version: 4.8.0 + resolution: "piscina@npm:4.8.0" + dependencies: + "@napi-rs/nice": "npm:^1.0.1" + dependenciesMeta: + "@napi-rs/nice": + optional: true + checksum: 10c0/963ee0dc0862e936c88357b21b0b4fa32407ab21e9600756504411f368dcfae7478c8a19e13d0dd8afed56a8252a8e5967ee4413aa33dd436751b7ee2804531e + languageName: node + linkType: hard + "pkg-dir@npm:^3.0.0": version: 3.0.0 resolution: "pkg-dir@npm:3.0.0" @@ -32528,6 +33201,15 @@ __metadata: languageName: node linkType: hard +"pretty-ms@npm:8.0.0": + version: 8.0.0 + resolution: "pretty-ms@npm:8.0.0" + dependencies: + parse-ms: "npm:^3.0.0" + checksum: 10c0/e960d633ecca45445cf5c6dffc0f5e4bef6744c92449ab0e8c6c704800675ab71e181c5e02ece5265e02137a33e313d3f3e355fbf8ea30b4b5b23de423329f8d + languageName: node + linkType: hard + "pretty-ms@npm:^9.0.0": version: 9.1.0 resolution: "pretty-ms@npm:9.1.0" @@ -32889,7 +33571,16 @@ __metadata: languageName: node linkType: hard -"pure-rand@npm:^6.0.0": +"pupa@npm:^2.1.1": + version: 2.1.1 + resolution: "pupa@npm:2.1.1" + dependencies: + escape-goat: "npm:^2.0.0" + checksum: 10c0/d2346324780ebae4be847cad052b830e004d816851dd4750fc73faa6cd360f443e358f6b1c83641fd4c904c6055dcb545807f55259a20a52ad86d9477746c724 + languageName: node + linkType: hard + +"pure-rand@npm:^6.0.0, pure-rand@npm:^6.1.0": version: 6.1.0 resolution: "pure-rand@npm:6.1.0" checksum: 10c0/1abe217897bf74dcb3a0c9aba3555fe975023147b48db540aa2faf507aee91c03bf54f6aef0eb2bf59cc259a16d06b28eca37f0dc426d94f4692aeff02fb0e65 @@ -33507,6 +34198,21 @@ __metadata: languageName: node linkType: hard +"react-scan@npm:^0.0.31": + version: 0.0.31 + resolution: "react-scan@npm:0.0.31" + dependencies: + "@clack/core": "npm:^0.3.5" + "@clack/prompts": "npm:^0.8.2" + kleur: "npm:^4.1.5" + mri: "npm:^1.2.0" + playwright: "npm:^1.49.0" + bin: + react-scan: bin/cli.js + checksum: 10c0/4235909e20a3f096382c96e108c293dfd9369e4311a02661adeab366a9964a8cf238ff7bfab5a4ec59cc022e5ecd157acb8e445a4d7120f30ee06421dd82613f + languageName: node + linkType: hard + "react-scan@npm:^0.0.35": version: 0.0.35 resolution: "react-scan@npm:0.0.35" @@ -33806,7 +34512,7 @@ __metadata: languageName: node linkType: hard -"recast@npm:^0.23.1, recast@npm:^0.23.3, recast@npm:^0.23.5": +"recast@npm:^0.23.1, recast@npm:^0.23.3, recast@npm:^0.23.5, recast@npm:^0.23.9": version: 0.23.9 resolution: "recast@npm:0.23.9" dependencies: @@ -33971,6 +34677,24 @@ __metadata: languageName: node linkType: hard +"registry-auth-token@npm:^5.0.1": + version: 5.0.3 + resolution: "registry-auth-token@npm:5.0.3" + dependencies: + "@pnpm/npm-conf": "npm:^2.1.0" + checksum: 10c0/f92313032fae7dca787aa878cc7fa8499ee5da960802777f6b9f168a5d8f24a97fcfa0cf30a604bcf38b050a5db5f034b1e2fec18a3326f41822a6aff9514c85 + languageName: node + linkType: hard + +"registry-url@npm:^5.1.0": + version: 5.1.0 + resolution: "registry-url@npm:5.1.0" + dependencies: + rc: "npm:^1.2.8" + checksum: 10c0/c2c455342b5836cbed5162092eba075c7a02c087d9ce0fde8aeb4dc87a8f4a34a542e58bf4d8ec2d4cb73f04408cb3148ceb1f76647f76b978cfec22047dc6d6 + languageName: node + linkType: hard + "regjsgen@npm:^0.8.0": version: 0.8.0 resolution: "regjsgen@npm:0.8.0" @@ -34572,6 +35296,24 @@ __metadata: languageName: node linkType: hard +"rrdom@npm:^0.1.7": + version: 0.1.7 + resolution: "rrdom@npm:0.1.7" + dependencies: + rrweb-snapshot: "npm:^2.0.0-alpha.4" + checksum: 10c0/2e06c90f1e3d8a329edb9526ceaf25f2ca09b4a8dffd171577751a94fdc0ec0e56ab899be1f8f775fd6bf76a1268507767a2aa336a6c1d8a189022909cde7726 + languageName: node + linkType: hard + +"rrdom@npm:^2.0.0-alpha.18": + version: 2.0.0-alpha.18 + resolution: "rrdom@npm:2.0.0-alpha.18" + dependencies: + rrweb-snapshot: "npm:^2.0.0-alpha.18" + checksum: 10c0/13770f81a475eff96d50bb74d313965a4e3d5b904b9a27dc43cb33fb92dac040fa5acd602db3a20de5366997a4cf3f55c635a5359e58bd26187ef074c0704fe5 + languageName: node + linkType: hard + "rrweb-cssom@npm:^0.6.0": version: 0.6.0 resolution: "rrweb-cssom@npm:0.6.0" @@ -34579,6 +35321,57 @@ __metadata: languageName: node linkType: hard +"rrweb-player@npm:1.0.0-alpha.4": + version: 1.0.0-alpha.4 + resolution: "rrweb-player@npm:1.0.0-alpha.4" + dependencies: + "@tsconfig/svelte": "npm:^1.0.0" + rrweb: "npm:^2.0.0-alpha.4" + checksum: 10c0/cde3a7e0505c97312bc8015bba91f7814770d1d08cff2c2cb3e7a9eaf6a8a2bcc40f5fe5a22a46a351f6992df49a4a1c8be2c85ef461c96d5dd2af69f4fbad75 + languageName: node + linkType: hard + +"rrweb-snapshot@npm:^2.0.0-alpha.16, rrweb-snapshot@npm:^2.0.0-alpha.18, rrweb-snapshot@npm:^2.0.0-alpha.4": + version: 2.0.0-alpha.18 + resolution: "rrweb-snapshot@npm:2.0.0-alpha.18" + dependencies: + postcss: "npm:^8.4.38" + checksum: 10c0/296996f50f8a9c5f5dad2f92cea8fcc670bf364e094db48592919cab8987cdc19ce2e7b9b20653c63160f924e733a213329e87ff092353d4a5364c0a3a66405c + languageName: node + linkType: hard + +"rrweb@npm:2.0.0-alpha.4": + version: 2.0.0-alpha.4 + resolution: "rrweb@npm:2.0.0-alpha.4" + dependencies: + "@rrweb/types": "npm:^2.0.0-alpha.4" + "@types/css-font-loading-module": "npm:0.0.7" + "@xstate/fsm": "npm:^1.4.0" + base64-arraybuffer: "npm:^1.0.1" + fflate: "npm:^0.4.4" + mitt: "npm:^3.0.0" + rrdom: "npm:^0.1.7" + rrweb-snapshot: "npm:^2.0.0-alpha.4" + checksum: 10c0/124de86bc4db3d3254309950a5fef1c0ed2ad283a4db5840c43eba8d4157738f1fd833e15c3201e6754aa0bec2e3f7ed7fdf7a5335262626e15691afc2cf4f24 + languageName: node + linkType: hard + +"rrweb@npm:^2.0.0-alpha.4": + version: 2.0.0-alpha.18 + resolution: "rrweb@npm:2.0.0-alpha.18" + dependencies: + "@rrweb/types": "npm:^2.0.0-alpha.18" + "@rrweb/utils": "npm:^2.0.0-alpha.18" + "@types/css-font-loading-module": "npm:0.0.7" + "@xstate/fsm": "npm:^1.4.0" + base64-arraybuffer: "npm:^1.0.1" + mitt: "npm:^3.0.0" + rrdom: "npm:^2.0.0-alpha.18" + rrweb-snapshot: "npm:^2.0.0-alpha.18" + checksum: 10c0/8f33a64f4648fbba1621ec4ec383ee909435a0621259cf1b38c88074b8e23c875a7837f7e403d176f8399ad25312f6632c16c0879618a327d4b1797f8c4b37b1 + languageName: node + linkType: hard + "run-applescript@npm:^7.0.0": version: 7.0.0 resolution: "run-applescript@npm:7.0.0" @@ -34852,6 +35645,15 @@ __metadata: languageName: node linkType: hard +"semver-diff@npm:^3.1.1": + version: 3.1.1 + resolution: "semver-diff@npm:3.1.1" + dependencies: + semver: "npm:^6.3.0" + checksum: 10c0/7d350f1450b9577d538ef866a9bc4cd97bfbf1f1d92070291495a31d0ec3aa808e826c223e5454ea9877cc06eaa886ffd71bb3a1f331b44bc210f9ff525c68d2 + languageName: node + linkType: hard + "semver-regex@npm:^4.0.5": version: 4.0.5 resolution: "semver-regex@npm:4.0.5" @@ -34897,7 +35699,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.6.3, semver@npm:^7.1.1, semver@npm:^7.1.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.3": +"semver@npm:7.6.3, semver@npm:^7.1.1, semver@npm:^7.1.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.2, semver@npm:^7.6.3": version: 7.6.3 resolution: "semver@npm:7.6.3" bin: @@ -35335,7 +36137,17 @@ __metadata: languageName: node linkType: hard -"socket.io-client@npm:^4.5.1": +"socket.io-adapter@npm:~2.5.2": + version: 2.5.5 + resolution: "socket.io-adapter@npm:2.5.5" + dependencies: + debug: "npm:~4.3.4" + ws: "npm:~8.17.1" + checksum: 10c0/04a5a2a9c4399d1b6597c2afc4492ab1e73430cc124ab02b09e948eabf341180b3866e2b61b5084cb899beb68a4db7c328c29bda5efb9207671b5cb0bc6de44e + languageName: node + linkType: hard + +"socket.io-client@npm:^4.5.1, socket.io-client@npm:^4.7.5": version: 4.8.1 resolution: "socket.io-client@npm:4.8.1" dependencies: @@ -35357,6 +36169,21 @@ __metadata: languageName: node linkType: hard +"socket.io@npm:^4.8.1": + version: 4.8.1 + resolution: "socket.io@npm:4.8.1" + dependencies: + accepts: "npm:~1.3.4" + base64id: "npm:~2.0.0" + cors: "npm:~2.8.5" + debug: "npm:~4.3.2" + engine.io: "npm:~6.6.0" + socket.io-adapter: "npm:~2.5.2" + socket.io-parser: "npm:~4.2.4" + checksum: 10c0/acf931a2bb235be96433b71da3d8addc63eeeaa8acabd33dc8d64e12287390a45f1e9f389a73cf7dc336961cd491679741b7a016048325c596835abbcc017ca9 + languageName: node + linkType: hard + "sockjs@npm:^0.3.24": version: 0.3.24 resolution: "sockjs@npm:0.3.24" @@ -36057,7 +36884,7 @@ __metadata: languageName: node linkType: hard -"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.0.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.2, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -36549,6 +37376,13 @@ __metadata: languageName: node linkType: hard +"sugar-high@npm:^0.7.1": + version: 0.7.5 + resolution: "sugar-high@npm:0.7.5" + checksum: 10c0/0d898ce842bc4c7362bed5b302186a39bed320c251e193e185be6b6ba10b183321748fa9d30d2a89b242b7d3cbcdb8abc26c02d3279851a649c0532cbeddd582 + languageName: node + linkType: hard + "superstruct@npm:^1.0.3": version: 1.0.4 resolution: "superstruct@npm:1.0.4" @@ -37151,7 +37985,7 @@ __metadata: languageName: node linkType: hard -"tmp@npm:~0.2.1, tmp@npm:~0.2.3": +"tmp@npm:^0.2.3, tmp@npm:~0.2.1, tmp@npm:~0.2.3": version: 0.2.3 resolution: "tmp@npm:0.2.3" checksum: 10c0/3e809d9c2f46817475b452725c2aaa5d11985cf18d32a7a970ff25b568438e2c076c2e8609224feef3b7923fa9749b74428e3e634f6b8e520c534eef2fd24125 @@ -37963,6 +38797,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.20.0": + version: 6.20.0 + resolution: "undici-types@npm:6.20.0" + checksum: 10c0/68e659a98898d6a836a9a59e6adf14a5d799707f5ea629433e025ac90d239f75e408e2e5ff086afc3cace26f8b26ee52155293564593fbb4a2f666af57fc59bf + languageName: node + linkType: hard + "undici@npm:^5.8.1": version: 5.28.4 resolution: "undici@npm:5.28.4" @@ -38225,6 +39066,16 @@ __metadata: languageName: node linkType: hard +"unplugin@npm:^1.6.0": + version: 1.16.0 + resolution: "unplugin@npm:1.16.0" + dependencies: + acorn: "npm:^8.14.0" + webpack-virtual-modules: "npm:^0.6.2" + checksum: 10c0/547f6bd5ec1dd7411533e68e73c60d5e9527e68d52aa326442650d084866ed3307ac68719068abae23ceab09db197cad43b382a7e69c2d8ca338b27802392fed + languageName: node + linkType: hard + "unstorage@npm:^1.9.0": version: 1.12.0 resolution: "unstorage@npm:1.12.0" @@ -38325,6 +39176,30 @@ __metadata: languageName: node linkType: hard +"update-notifier-cjs@npm:^5.1.6": + version: 5.1.6 + resolution: "update-notifier-cjs@npm:5.1.6" + dependencies: + boxen: "npm:^5.0.0" + chalk: "npm:^4.1.0" + configstore: "npm:^5.0.1" + has-yarn: "npm:^2.1.0" + import-lazy: "npm:^2.1.0" + is-ci: "npm:^2.0.0" + is-installed-globally: "npm:^0.4.0" + is-npm: "npm:^5.0.0" + is-yarn-global: "npm:^0.3.0" + isomorphic-fetch: "npm:^3.0.0" + pupa: "npm:^2.1.1" + registry-auth-token: "npm:^5.0.1" + registry-url: "npm:^5.1.0" + semver: "npm:^7.3.7" + semver-diff: "npm:^3.1.1" + xdg-basedir: "npm:^4.0.0" + checksum: 10c0/339d9d8c049c2dc45979159b393e6037b9a935e1da3a0b772e1f42437a2fd372e9b5db12c98c2eef4c9a578922b214432085944935816dab06804ca29ab61d99 + languageName: node + linkType: hard + "uqr@npm:^0.1.2": version: 0.1.2 resolution: "uqr@npm:0.1.2" @@ -39520,7 +40395,7 @@ __metadata: languageName: node linkType: hard -"whatwg-fetch@npm:^3.0.0": +"whatwg-fetch@npm:^3.0.0, whatwg-fetch@npm:^3.4.1": version: 3.6.20 resolution: "whatwg-fetch@npm:3.6.20" checksum: 10c0/fa972dd14091321d38f36a4d062298df58c2248393ef9e8b154493c347c62e2756e25be29c16277396046d6eaa4b11bd174f34e6403fff6aaca9fb30fa1ff46d @@ -39696,6 +40571,15 @@ __metadata: languageName: node linkType: hard +"widest-line@npm:^3.1.0": + version: 3.1.0 + resolution: "widest-line@npm:3.1.0" + dependencies: + string-width: "npm:^4.0.0" + checksum: 10c0/b1e623adcfb9df35350dd7fc61295d6d4a1eaa65a406ba39c4b8360045b614af95ad10e05abf704936ed022569be438c4bfa02d6d031863c4166a238c301119f + languageName: node + linkType: hard + "wildcard@npm:^2.0.0": version: 2.0.1 resolution: "wildcard@npm:2.0.1" @@ -39919,6 +40803,13 @@ __metadata: languageName: node linkType: hard +"xdg-basedir@npm:^4.0.0": + version: 4.0.0 + resolution: "xdg-basedir@npm:4.0.0" + checksum: 10c0/1b5d70d58355af90363a4e0a51c992e77fc5a1d8de5822699c7d6e96a6afea9a1e048cb93312be6870f338ca45ebe97f000425028fa149c1e87d1b5b8b212a06 + languageName: node + linkType: hard + "xml-name-validator@npm:^4.0.0": version: 4.0.0 resolution: "xml-name-validator@npm:4.0.0" @@ -39985,6 +40876,13 @@ __metadata: languageName: node linkType: hard +"xycolors@npm:^0.1.2": + version: 0.1.2 + resolution: "xycolors@npm:0.1.2" + checksum: 10c0/a7dd439022f72e7d9c9ec335ccee5583dca63c2bdcc9aa64cd6b1f7de6d6e319fe01aa3c584241fd2f187be3ca30a5527682310493975357333afb6311c7cac1 + languageName: node + linkType: hard + "y18n@npm:^4.0.0": version: 4.0.3 resolution: "y18n@npm:4.0.3" @@ -40093,7 +40991,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^17.3.1, yargs@npm:^17.6.2": +"yargs@npm:^17.3.1, yargs@npm:^17.6.2, yargs@npm:^17.7.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" dependencies: