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 cffac9d0..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} @@ -13,6 +14,9 @@ const nextConfig = { // See: https://github.com/gregberge/svgr svgr: false, }, + experimental: { + serverComponentsExternalPackages: ['@xmtp/user-preferences-bindings-wasm'], + }, }; const plugins = [ @@ -20,4 +24,6 @@ const plugins = [ withNx, ]; -module.exports = composePlugins(...plugins)(nextConfig); +module.exports = MillionLint.next({ rsc: true })( + composePlugins(...plugins)(nextConfig) +); diff --git a/apps/console/src/app/page.tsx b/apps/console/src/app/page.tsx index 197422c9..3de9aa76 100644 --- a/apps/console/src/app/page.tsx +++ b/apps/console/src/app/page.tsx @@ -40,6 +40,7 @@ export default function Page() { + diff --git a/apps/console/src/app/providers.tsx b/apps/console/src/app/providers.tsx index f709020d..b4b11657 100644 --- a/apps/console/src/app/providers.tsx +++ b/apps/console/src/app/providers.tsx @@ -63,6 +63,7 @@ export const Providers: React.FC = (props) => { providerUrl: process.env.NEXT_PUBLIC_SEPOLIA_PROVIDER_URL as string, }, ], + enableAuth: true, openOnWalletConnect: true, allowedEns: 'all', disableOverlay: true, diff --git a/apps/console/src/components/sections/code/CodeSection/index.tsx b/apps/console/src/components/sections/code/CodeSection/index.tsx index c4da154f..31b2bc7d 100644 --- a/apps/console/src/components/sections/code/CodeSection/index.tsx +++ b/apps/console/src/components/sections/code/CodeSection/index.tsx @@ -32,6 +32,11 @@ export const CodeSection: React.FC = ({ mobile }) => { [config] ); + const xmtpPluginEnabled = useMemo( + () => config.plugins?.find((p) => p.name === 'XMTPPlugin'), + [config] + ); + const codeSnippet = useMemo(() => { const plugins = []; @@ -57,6 +62,10 @@ export const CodeSection: React.FC = ({ mobile }) => { ); } + if (xmtpPluginEnabled) { + plugins.push("%%XMTPPlugin('production')%%"); + } + return ` import '@rainbow-me/rainbowkit/styles.css'; import '@justweb3/widget/styles.css'; @@ -97,6 +106,9 @@ ${ ? "import { TalentProtocolPlugin } from '@justweb3/talent-protocol-plugin';" : '' } +${ + xmtpPluginEnabled ? "import { XMTPPlugin } from '@justweb3/xmtp-plugin';" : '' +} export const App: React.FC = () => { const { wallets } = getDefaultWallets(); @@ -163,6 +175,7 @@ export default App;`.trim(); justVerifiedEnabled, poapPluginEnabled, talentProtocolPluginEnabled, + xmtpPluginEnabled, ]); const code = useMemo(() => { @@ -172,9 +185,11 @@ export default App;`.trim(); }, [codeSnippet]); const dependencies = useMemo(() => { - return `yarn add ${justVerifiedEnabled ? '@justverified/plugin' : ''} ${ - poapPluginEnabled ? '@justweb3/poap-plugin' : '' - } ${efpPluginEnabled ? '@justweb3/efp-plugin' : ''} + return `yarn add ${xmtpPluginEnabled ? '@justweb3/xmtp-plugin' : ''} ${ + justVerifiedEnabled ? '@justverified/plugin' : '' + } ${poapPluginEnabled ? '@justweb3/poap-plugin' : ''} ${ + efpPluginEnabled ? '@justweb3/efp-plugin' : '' + } ${talentProtocolPluginEnabled ? '@justweb3/talent-protocol-plugin' : ''} @justweb3/widget viem wagmi @rainbow-me/rainbowkit @tanstack/react-query ethers`; }, [ @@ -182,6 +197,7 @@ export default App;`.trim(); justVerifiedEnabled, poapPluginEnabled, talentProtocolPluginEnabled, + xmtpPluginEnabled, ]); const handleDependenciesCopy = () => { diff --git a/apps/console/src/components/sections/customizer/PluginsSection/Verified/index.tsx b/apps/console/src/components/sections/customizer/PluginsSection/JustVerified/index.tsx similarity index 99% rename from apps/console/src/components/sections/customizer/PluginsSection/Verified/index.tsx rename to apps/console/src/components/sections/customizer/PluginsSection/JustVerified/index.tsx index 1c6ca75e..882d8b18 100644 --- a/apps/console/src/components/sections/customizer/PluginsSection/Verified/index.tsx +++ b/apps/console/src/components/sections/customizer/PluginsSection/JustVerified/index.tsx @@ -45,7 +45,7 @@ const socials: { logo: ReactNode; title: string; credential: Credentials }[] = [ }, ]; -export const Verified = () => { +export const JustVerified = () => { const { handleJustWeb3Config, config } = useContext(JustWeb3Context); const { justVerified, setJustVerified } = useConsole(); diff --git a/apps/console/src/components/sections/customizer/PluginsSection/Verified/socialCard/index.tsx b/apps/console/src/components/sections/customizer/PluginsSection/JustVerified/socialCard/index.tsx similarity index 100% rename from apps/console/src/components/sections/customizer/PluginsSection/Verified/socialCard/index.tsx rename to apps/console/src/components/sections/customizer/PluginsSection/JustVerified/socialCard/index.tsx diff --git a/apps/console/src/components/sections/customizer/PluginsSection/XMTP/index.tsx b/apps/console/src/components/sections/customizer/PluginsSection/XMTP/index.tsx new file mode 100644 index 00000000..aa17765f --- /dev/null +++ b/apps/console/src/components/sections/customizer/PluginsSection/XMTP/index.tsx @@ -0,0 +1,49 @@ +// import { AccordionItem, AccordionTrigger } from '../../../../ui/accordion'; +import { Switch } from '../../../../ui/switch'; +import { useContext } from 'react'; +import { JustWeb3Context } from '@justweb3/widget'; +import { XMTPPlugin } from '@justweb3/xmtp-plugin'; + +export const XMTP = () => { + const { handleJustWeb3Config, config } = useContext(JustWeb3Context); + + const handleEFPConfig = (enabled: boolean) => { + if (enabled) { + handleJustWeb3Config({ + ...config, + plugins: [ + ...(config?.plugins || []).filter( + (plugin) => plugin.name !== XMTPPlugin.name + ), + XMTPPlugin('production'), + ], + }); + } else { + handleJustWeb3Config({ + ...config, + plugins: (config?.plugins || []).filter( + (plugin) => plugin.name !== XMTPPlugin.name + ), + }); + } + }; + + return ( +
+

+ XMTP +

+ plugin.name === XMTPPlugin.name) + } + onClick={(e) => { + e.stopPropagation(); + }} + onCheckedChange={(checked) => { + handleEFPConfig(checked); + }} + /> +
+ ); +}; diff --git a/apps/console/src/components/sections/customizer/PluginsSection/index.tsx b/apps/console/src/components/sections/customizer/PluginsSection/index.tsx index 5d4e59bd..06321f97 100644 --- a/apps/console/src/components/sections/customizer/PluginsSection/index.tsx +++ b/apps/console/src/components/sections/customizer/PluginsSection/index.tsx @@ -1,9 +1,10 @@ import { FC } from 'react'; import { Accordion } from '../../../ui/accordion'; -import { Verified } from './Verified'; +import { JustVerified } from './JustVerified'; import { EFP } from './EFP'; import { POAP } from './POAP'; import { TalentProtocol } from './TalentProtocol'; +import { XMTP } from './XMTP'; export const PluginsSection: FC = () => { return ( @@ -13,12 +14,14 @@ export const PluginsSection: FC = () => {

- +
+
+
); diff --git a/apps/console/src/layout/navbar/index.tsx b/apps/console/src/layout/navbar/index.tsx index e0cc0afb..0c376170 100644 --- a/apps/console/src/layout/navbar/index.tsx +++ b/apps/console/src/layout/navbar/index.tsx @@ -5,7 +5,7 @@ import Link from 'next/link'; export const Navbar = () => { return ( -
+
{/*JustName*/} diff --git a/demo/server/src/main.ts b/demo/server/src/main.ts index b3d71883..06f75887 100644 --- a/demo/server/src/main.ts +++ b/demo/server/src/main.ts @@ -68,9 +68,15 @@ app.post('/api/signin', async (req: Request, res) => { return; } + if (!req.session.nonce) { + res.status(401).json({ message: 'No nonce found.' }); + return; + } + const { data: message, ens } = await justaname.signIn.signIn({ message: req.body.message, signature: req.body.signature, + nonce: req.session.nonce, }); if (!message) { diff --git a/package.json b/package.json index d3e05ddc..e3ed50e1 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ "@hookform/error-message": "^2.0.1", "@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", @@ -99,6 +101,7 @@ "react-native-url-polyfill": "^2.0.0", "react-native-web": "~0.19.9", "react-router-dom": "6.11.2", + "react-scan": "^0.0.35", "react-split": "^2.0.14", "react-timer-hook": "^3.0.8", "react-tiny-popover": "^8.0.4", diff --git a/packages/@justaname.id/react/CHANGELOG.md b/packages/@justaname.id/react/CHANGELOG.md index 5944fd22..18b3f63d 100644 --- a/packages/@justaname.id/react/CHANGELOG.md +++ b/packages/@justaname.id/react/CHANGELOG.md @@ -1,3 +1,122 @@ +## 0.3.158 (2024-12-03) + + +### 🚀 Features + +- sign in optional ([3df90a7](https://github.com/JustaName-id/JustaName-sdk/commit/3df90a7)) + + +### 🧱 Updated Dependencies + +- Updated @justaname.id/sdk to 0.2.155 + + +### ❤️ Thank You + +- HadiKhai + +## 0.3.157 (2024-12-02) + + +### 🧱 Updated Dependencies + +- Updated @justaname.id/sdk to 0.2.154 + +## 0.3.156 (2024-12-01) + + +### 🚀 Features + +- support ethers 5 and ethers 6 ([f82ab13](https://github.com/JustaName-id/JustaName-sdk/commit/f82ab13)) + + +### 🧱 Updated Dependencies + +- Updated @justaname.id/sdk to 0.2.153 + + +### ❤️ Thank You + +- HadiKhai + +## 0.3.155 (2024-11-29) + + +### 🧱 Updated Dependencies + +- Updated @justaname.id/sdk to 0.2.152 + +## 0.3.154 (2024-11-29) + + +### 🧱 Updated Dependencies + +- Updated @justaname.id/sdk to 0.2.151 + +## 0.3.153 (2024-11-29) + + +### 🧱 Updated Dependencies + +- Updated @justaname.id/sdk to 0.2.150 + +## 0.3.152 (2024-11-29) + + +### 🧱 Updated Dependencies + +- Updated @justaname.id/sdk to 0.2.149 + +## 0.3.151 (2024-11-29) + + +### 🧱 Updated Dependencies + +- Updated @justaname.id/sdk to 0.2.148 + +## 0.3.150 (2024-11-29) + + +### 🚀 Features + +- remove mapps from justverified ([2898c521](https://github.com/JustaName-id/JustaName-sdk/commit/2898c521)) + +- efp ([36fcc153](https://github.com/JustaName-id/JustaName-sdk/commit/36fcc153)) + +- efp and storybook for react sdk ([f0c5c7a0](https://github.com/JustaName-id/JustaName-sdk/commit/f0c5c7a0)) + +- console fixes ([358b3a0e](https://github.com/JustaName-id/JustaName-sdk/commit/358b3a0e)) + +- added docs and the sign in should only show ens set to default resolver or justaname resolver only ([a3bcecaa](https://github.com/JustaName-id/JustaName-sdk/commit/a3bcecaa)) + +- enabled in hooks and siwens fix and coinbase fix ([c3fb455e](https://github.com/JustaName-id/JustaName-sdk/commit/c3fb455e)) + +- retryOnMount false ([bec00ba8](https://github.com/JustaName-id/JustaName-sdk/commit/bec00ba8)) + + +### 🩹 Fixes + +- packages ([6dd450f5](https://github.com/JustaName-id/JustaName-sdk/commit/6dd450f5)) + +- lint fix on react and package json of projects ([b29cd296](https://github.com/JustaName-id/JustaName-sdk/commit/b29cd296)) + +- error from useEnsWalletClient ([67043376](https://github.com/JustaName-id/JustaName-sdk/commit/67043376)) + +- update rest call and ui fixes ([81b05f29](https://github.com/JustaName-id/JustaName-sdk/commit/81b05f29)) + +- poap default options ([eb5d1c36](https://github.com/JustaName-id/JustaName-sdk/commit/eb5d1c36)) + + +### 🧱 Updated Dependencies + +- Updated @justaname.id/sdk to 0.2.147 + + +### ❤️ Thank You + +- anthony2399 @anthony23991 +- HadiKhai + ## 0.3.149 (2024-11-15) diff --git a/packages/@justaname.id/react/package.json b/packages/@justaname.id/react/package.json index c7e321d5..e3f639be 100644 --- a/packages/@justaname.id/react/package.json +++ b/packages/@justaname.id/react/package.json @@ -1,15 +1,15 @@ { "name": "@justaname.id/react", - "version": "0.3.149", + "version": "0.3.158", "dependencies": { "@ensdomains/ensjs": "4.0.2", - "@justaname.id/sdk": "0.2.146", + "@justaname.id/sdk": "0.2.155", "axios": "^1.6.0", "qs": "^6.12.0" }, "peerDependencies": { "@tanstack/react-query": "^5.x", - "ethers": "^6.x", + "ethers": "^5.6.8 || ^6.0.8", "react": ">=17", "viem": "2.x", "wagmi": "2.x" diff --git a/packages/@justaname.id/react/src/lib/helpers/ethersCompat.ts b/packages/@justaname.id/react/src/lib/helpers/ethersCompat.ts new file mode 100644 index 00000000..682e9f25 --- /dev/null +++ b/packages/@justaname.id/react/src/lib/helpers/ethersCompat.ts @@ -0,0 +1,51 @@ +// inspired by spruceid siwe: https://github.com/spruceid/siwe/blob/main/packages/siwe/lib/ethersCompat.ts + +import { ethers } from 'ethers'; + +// @ts-expect-error -- compatibility hack +type ProviderV5 = ethers.providers.Provider; +type ProviderV6 = ethers.Provider; +// @ts-expect-error -- compatibility hack +type JsonRpcProviderV5 = ethers.providers.JsonRpcProvider; +type JsonRpcProviderV6 = ethers.JsonRpcProvider; + +export type Provider = ProviderV6 extends undefined ? ProviderV5 : ProviderV6; +export type JsonRpcProvider = JsonRpcProviderV6 extends undefined + ? JsonRpcProviderV5 + : JsonRpcProviderV6; + +interface EthersCompat { + namehash?: (name: string) => string; + getAddress?: (address: string) => string; + JsonRpcProvider?: new (...args: any[]) => JsonRpcProvider; + utils: { + namehash: (name: string) => string; + getAddress: (address: string) => string; + }; + providers: { + JsonRpcProvider: new (...args: any[]) => JsonRpcProvider; + }; +} + +const ethersCompat = ethers as unknown as EthersCompat; + +export const getJsonRpcProvider = ( + providerUrl?: string, + chainId?: number +): JsonRpcProvider => { + if ('JsonRpcProvider' in ethersCompat) { + return new ethersCompat.JsonRpcProvider!(providerUrl, chainId); + } else { + return new ethersCompat.providers.JsonRpcProvider(providerUrl, chainId); + } +}; + +export const namehash: (name: string) => string = + 'namehash' in ethersCompat + ? ethersCompat.namehash! + : ethersCompat.utils.namehash; + +export const getAddress: (address: string) => string = + 'getAddress' in ethersCompat + ? ethersCompat.getAddress! + : ethersCompat.utils.getAddress; diff --git a/packages/@justaname.id/react/src/lib/helpers/isParseable/index.ts b/packages/@justaname.id/react/src/lib/helpers/isParseable/index.ts new file mode 100644 index 00000000..a6d87b9a --- /dev/null +++ b/packages/@justaname.id/react/src/lib/helpers/isParseable/index.ts @@ -0,0 +1,8 @@ +export const isParseable = (value: any): boolean => { + try { + JSON.parse(value); + return true; + } catch (e) { + return false; + } +}; diff --git a/packages/@justaname.id/react/src/lib/helpers/validateEns.ts b/packages/@justaname.id/react/src/lib/helpers/validateEns.ts new file mode 100644 index 00000000..684da86d --- /dev/null +++ b/packages/@justaname.id/react/src/lib/helpers/validateEns.ts @@ -0,0 +1,25 @@ +import { normalize } from 'viem/ens'; + +export const normalizeEns = (name: string | undefined): string | undefined => { + if (typeof name !== 'string' || name.trim() === '') { + return; + } + + try { + const normalized = normalize(name); + + return normalized; + } catch (error) { + return; + } +}; + +export const validateEns = (name: string | undefined): boolean => { + const ensRegex = /(?:^|[^a-zA-Z0-9-_.])(([^\s.]{1,63}\.)+[^\s.]{2,63})/; + + if (typeof name !== 'string' || name.trim() === '') { + return false; + } + + return ensRegex.test(name); +}; 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 aff748e2..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, @@ -13,7 +14,7 @@ export const buildPrimaryName = ( ) => ['PRIMARY_NAME', address, chainId]; export interface UsePrimaryNameParams { - address?: Address; + address?: string; chainId?: ChainId; enabled?: boolean; } @@ -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, @@ -72,7 +83,7 @@ export const usePrimaryName = ( throw new Error('Address is required'); } return getName(ensClient, { - address: params?.address, + address: params?.address as Address, }); }; @@ -82,32 +93,12 @@ 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; }; const query = useQuery({ ...defaultOptions, retry: (_count, error) => { - console.log('Error fetching primary name', error, _count); if (error?.message.includes('PrimaryNameNotFound')) { return false; } @@ -116,7 +107,7 @@ export const usePrimaryName = ( queryKey: buildPrimaryName(params?.address || '', _chainId), queryFn: () => getPrimaryName({ - address: params?.address, + address: params?.address as Address, }), enabled: Boolean(params?.address) && Boolean(ensClient) && Boolean(_enabled), diff --git a/packages/@justaname.id/react/src/lib/hooks/primaryName/usePrimaryNameBatch.ts b/packages/@justaname.id/react/src/lib/hooks/primaryName/usePrimaryNameBatch.ts index ddd06a45..48ff1dce 100644 --- a/packages/@justaname.id/react/src/lib/hooks/primaryName/usePrimaryNameBatch.ts +++ b/packages/@justaname.id/react/src/lib/hooks/primaryName/usePrimaryNameBatch.ts @@ -17,10 +17,10 @@ export const buildPrimaryNameBatchKey = (chainId: ChainId | undefined) => [ chainId, ]; -export type PrimaryNameRecord = Record; +export type PrimaryNameRecord = Record; export interface UsePrimaryNameBatchParams { - addresses?: Address[]; + addresses?: string[]; chainId?: ChainId; enabled?: boolean; } @@ -143,7 +143,7 @@ export const usePrimaryNameBatch = ( ), queryFn: () => getPrimaryNameBatch({ - addresses: params?.addresses, + addresses: params?.addresses as Address[], }), enabled: Boolean(params?.addresses) && Boolean(ensClient) && Boolean(_enabled), 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 58557c3b..51b421de 100644 --- a/packages/@justaname.id/react/src/lib/hooks/records/useRecords.ts +++ b/packages/@justaname.id/react/src/lib/hooks/records/useRecords.ts @@ -18,10 +18,11 @@ import { useMemo } from 'react'; import { Records } from '../../types'; import { defaultOptions } from '../../query'; import { RecordsTaskQueue } from './records-task-queue'; -import { checkEnsValid } from '../../helpers/checkEnsValid'; import { useEnsPublicClient } from '../client/useEnsPublicClient'; import { useOffchainResolvers } from '../offchainResolver'; import { getRecords as getEnsRecords } from '@ensdomains/ensjs/public'; +import { checkEnsValid } from '../../helpers/checkEnsValid'; +import { normalizeEns, validateEns } from '../../helpers/validateEns'; export const buildRecordsBySubnameKey = ( subname: string, @@ -72,6 +73,7 @@ export const useRecords = (params?: UseRecordsParams): UseRecordsResult => { () => params?.chainId || chainId, [params?.chainId, chainId] ); + const _ens = useMemo(() => normalizeEns(params?.ens), [params?.ens]); const { offchainResolvers } = useOffchainResolvers(); const { ensClient } = useEnsPublicClient({ chainId: _chainId, @@ -86,8 +88,18 @@ export const useRecords = (params?: UseRecordsParams): UseRecordsResult => { const getRecords = async ( _params: SubnameRecordsRoute['params'] ): Promise => { + const __ens = normalizeEns(_params.ens); + + if (!__ens) { + throw new Error('Invalid ENS name'); + } + + if (!validateEns(__ens)) { + throw new Error('Invalid ENS name'); + } + const result = await justaname.subnames.getRecords({ - ens: _params.ens, + ens: __ens, providerUrl: _params.providerUrl, chainId: _params.chainId, }); @@ -109,8 +121,18 @@ export const useRecords = (params?: UseRecordsParams): UseRecordsResult => { throw new Error('Public client not found'); } + const __ens = normalizeEns(_params.ens) || _ens; + + if (!__ens) { + throw new Error('Invalid ENS name'); + } + + if (!validateEns(__ens)) { + throw new Error('Invalid ENS name'); + } + const result = await getEnsRecords(ensClient, { - name: _params.ens, + name: __ens, coins: Object.keys(coinTypeMap), texts: [ ...generalKeys, @@ -126,7 +148,7 @@ export const useRecords = (params?: UseRecordsParams): UseRecordsResult => { ); const record = { - ens: _params.ens, + ens: __ens, isJAN: result.resolverAddress === offchainResolver?.resolverAddress, records: { ...result, @@ -152,11 +174,19 @@ export const useRecords = (params?: UseRecordsParams): UseRecordsResult => { forceUpdate = false ): Promise => { const __chainId = _params?.chainId || _chainId; + const __ens = normalizeEns(_params?.ens) || _ens; + if (!__ens) { + throw new Error('Invalid ENS name'); + } + + if (!validateEns(__ens)) { + throw new Error('Invalid ENS name'); + } // const __standard = _params?.standard || params?.standard; const __standard = false; if (!forceUpdate) { const cachedRecords = queryClient.getQueryData( - buildRecordsBySubnameKey(_params?.ens, __chainId, __standard) + buildRecordsBySubnameKey(__ens, __chainId, __standard) ) as Records; if (cachedRecords) { return cachedRecords; @@ -176,13 +206,16 @@ export const useRecords = (params?: UseRecordsParams): UseRecordsResult => { let records: Records; try { records = await getRecords({ - ens: _params.ens, + ens: __ens, chainId: __chainId, providerUrl: __providerUrl, }); } catch (error) { + if (error instanceof Error && error.message.includes('NotFound')) { + throw error; + } records = await getStandardRecords({ - ens: _params.ens, + ens: __ens, chainId: __chainId, providerUrl: __providerUrl, }); @@ -195,7 +228,7 @@ export const useRecords = (params?: UseRecordsParams): UseRecordsResult => { // if (__standard) { // records = await getStandardRecords({ - // ens: _params.ens, + // ens: __ens, // chainId: __chainId, // providerUrl: __providerUrl, // }); @@ -208,7 +241,7 @@ export const useRecords = (params?: UseRecordsParams): UseRecordsResult => { // } queryClient.setQueryData( - buildRecordsBySubnameKey(_params.ens, __chainId, __standard), + buildRecordsBySubnameKey(__ens, __chainId, __standard), records ); return records; @@ -216,23 +249,31 @@ export const useRecords = (params?: UseRecordsParams): UseRecordsResult => { const query = useQuery({ ...defaultOptions, - queryKey: buildRecordsBySubnameKey( - params?.ens || '', - _chainId - // params?.standard - ), + retry: (failureCount, error) => { + if (error instanceof Error) { + if ( + error.message.includes('NotFound') || + error.message.includes('ETH address not found') + ) { + return false; + } + } + return failureCount < 3; + }, + queryKey: buildRecordsBySubnameKey(_ens || '', _chainId), queryFn: () => getRecordsInternal( { - ens: params?.ens || '', + ens: _ens || '', chainId: _chainId, }, true ), enabled: - Boolean(params?.ens) && + Boolean(_ens) && Boolean(_chainId) && Boolean(_providerUrl) && + Boolean(validateEns(_ens)) && Boolean(_enabled), }); diff --git a/packages/@justaname.id/react/src/lib/hooks/resolver/useSetNameHashJustaNameResolver.ts b/packages/@justaname.id/react/src/lib/hooks/resolver/useSetNameHashJustaNameResolver.ts index 6bc1e94f..b20ef502 100644 --- a/packages/@justaname.id/react/src/lib/hooks/resolver/useSetNameHashJustaNameResolver.ts +++ b/packages/@justaname.id/react/src/lib/hooks/resolver/useSetNameHashJustaNameResolver.ts @@ -1,7 +1,6 @@ 'use client'; import { useMutation } from '@tanstack/react-query'; -import { ethers } from 'ethers'; import { useEffect, useMemo } from 'react'; import { useReadContract, @@ -11,6 +10,7 @@ import { } from 'wagmi'; import { useOffchainResolvers } from '../offchainResolver/useOffchainResolvers'; import { useMountedAccount } from '../account/useMountedAccount'; +import { getAddress, namehash } from '../../helpers/ethersCompat'; const REGISTRY_ADDRESS = '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e'; const SEPOLIA_REGISTRAR_ADDRESS = '0xA0a1AbcDAe1a2a4A2EF8e9113Ff0e02DD81DC0C6'; @@ -120,12 +120,14 @@ export const useSetNameHashJustaNameResolver = < T = any >(): UseSetNameHashJustaNameResolver => { const { chainId, address } = useMountedAccount(); - const { offchainResolvers, isOffchainResolversPending } = useOffchainResolvers(); + const { offchainResolvers, isOffchainResolversPending } = + useOffchainResolvers(); const currentResolver = useMemo(() => { if (!chainId || !offchainResolvers || isOffchainResolversPending) return; - return offchainResolvers?.offchainResolvers.find((resolver) => resolver.chainId === chainId) - ?.resolverAddress; + return offchainResolvers?.offchainResolvers.find( + (resolver) => resolver.chainId === chainId + )?.resolverAddress; }, [offchainResolvers, chainId, isOffchainResolversPending]); const { @@ -136,11 +138,7 @@ export const useSetNameHashJustaNameResolver = < address: REGISTRY_ADDRESS, abi: recordExistsABI, functionName: 'recordExists', - args: [ - ethers.namehash( - ethers.getAddress(address ?? '').substring(2) + '.addr.reverse' - ), - ], + args: [namehash(getAddress(address ?? '').substring(2) + '.addr.reverse')], chainId: chainId, }); @@ -149,7 +147,7 @@ export const useSetNameHashJustaNameResolver = < chainId === 1 ? MAINNET_REGISTRAR_ADDRESS : SEPOLIA_REGISTRAR_ADDRESS, abi: setClaimWithResolverABI, functionName: 'claimWithResolver', - args: [ethers.getAddress(address ?? ''), currentResolver], + args: [getAddress(address ?? ''), currentResolver], chainId: chainId, query: { enabled: recordExistsStatus === 'success' && recordExistsConfig === false, @@ -164,11 +162,7 @@ export const useSetNameHashJustaNameResolver = < address: REGISTRY_ADDRESS, abi: getResolverABI, functionName: 'resolver', - args: [ - ethers.namehash( - ethers.getAddress(address ?? '').substring(2) + '.addr.reverse' - ), - ], + args: [namehash(getAddress(address ?? '').substring(2) + '.addr.reverse')], chainId: chainId, query: { enabled: recordExistsStatus === 'success' && recordExistsConfig === true, @@ -180,9 +174,7 @@ export const useSetNameHashJustaNameResolver = < abi: setResolverABI, functionName: 'setResolver', args: [ - ethers.namehash( - ethers.getAddress(address ?? '').substring(2) + '.addr.reverse' - ), + namehash(getAddress(address ?? '').substring(2) + '.addr.reverse'), currentResolver, ], chainId: chainId, diff --git a/packages/@justaname.id/react/src/lib/hooks/signIn/useEnsAuth.ts b/packages/@justaname.id/react/src/lib/hooks/signIn/useEnsAuth.ts index 989783ad..24707001 100644 --- a/packages/@justaname.id/react/src/lib/hooks/signIn/useEnsAuth.ts +++ b/packages/@justaname.id/react/src/lib/hooks/signIn/useEnsAuth.ts @@ -4,6 +4,7 @@ import { useJustaName } from '../../providers'; import { ChainId } from '@justaname.id/sdk'; import { defaultOptions } from '../../query'; import axios from 'axios'; +import { isParseable } from '../../helpers/isParseable'; export const buildEnsAuthKey = (backendUrl: string) => ['ENS_AUTH', backendUrl]; @@ -17,6 +18,7 @@ export interface UseEnsAuthParams { backendUrl?: string; currentEnsRoute?: string; enabled?: boolean; + local?: boolean; } export interface UseEnsAuthReturn { @@ -45,17 +47,28 @@ export const useEnsAuth = ( () => _backendUrl + _currentEnsRoute, [_backendUrl, _currentEnsRoute] ); + const query = useQuery({ ...defaultOptions, retry: 0, queryKey: buildEnsAuthKey(_backendUrl), queryFn: async () => { try { + if (params?.local) { + const response = localStorage.getItem('ENS_AUTH') || ''; + if (isParseable(response)) { + return JSON.parse(response); + } else { + localStorage.removeItem('ENS_AUTH'); + } + return null; + } + const response = await axios.get(currentEnsEndpoint, { headers: { 'Content-Type': 'application/json', }, - withCredentials: true, // Similar to 'credentials: include' in fetch + withCredentials: true, }); return response.data; diff --git a/packages/@justaname.id/react/src/lib/hooks/signIn/useEnsNonce.ts b/packages/@justaname.id/react/src/lib/hooks/signIn/useEnsNonce.ts index def93b8c..314d3a86 100644 --- a/packages/@justaname.id/react/src/lib/hooks/signIn/useEnsNonce.ts +++ b/packages/@justaname.id/react/src/lib/hooks/signIn/useEnsNonce.ts @@ -1,7 +1,12 @@ import { useMemo } from 'react'; import { useJustaName } from '../../providers'; -import { useQuery } from '@tanstack/react-query'; +import { + QueryObserverResult, + RefetchOptions, + useQuery, +} from '@tanstack/react-query'; import { defaultOptions } from '../../query'; +import { useMountedAccount } from '../account'; export const buildNonceKey = ( backendUrl: string, @@ -12,13 +17,15 @@ export const buildNonceKey = ( export interface UseEnsNonceParams { signinNonceRoute?: string; backendUrl?: string; - address?: string; + // address?: string; enabled?: boolean; } export interface UseEnsNonceResult { nonce: string | undefined; - refetchNonce: () => void; + refetchNonce: ( + options?: RefetchOptions | undefined + ) => Promise>; isNoncePending: boolean; isNonceLoading: boolean; isNonceFetching: boolean; @@ -26,6 +33,7 @@ export interface UseEnsNonceResult { export const useEnsNonce = (params?: UseEnsNonceParams): UseEnsNonceResult => { const { backendUrl, routes } = useJustaName(); + const { address } = useMountedAccount(); const _enabled = params?.enabled !== undefined ? params.enabled : true; const _backendUrl = useMemo( () => params?.backendUrl || backendUrl || '', @@ -44,11 +52,7 @@ export const useEnsNonce = (params?: UseEnsNonceParams): UseEnsNonceResult => { ...defaultOptions, retry: 0, staleTime: Infinity, - queryKey: buildNonceKey( - _backendUrl, - _signinNonceRoute, - params?.address || '' - ), + queryKey: buildNonceKey(_backendUrl, _signinNonceRoute, address || ''), queryFn: async () => { const nonceResponse = await fetch(nonceEndpoint, { credentials: 'include', @@ -60,7 +64,7 @@ export const useEnsNonce = (params?: UseEnsNonceParams): UseEnsNonceResult => { return nonceResponse.text(); }, - enabled: Boolean(params?.address) && Boolean(_enabled), + enabled: Boolean(address) && Boolean(_enabled), }); return { diff --git a/packages/@justaname.id/react/src/lib/hooks/signIn/useEnsSignIn.ts b/packages/@justaname.id/react/src/lib/hooks/signIn/useEnsSignIn.ts index 9184d4df..a076079d 100644 --- a/packages/@justaname.id/react/src/lib/hooks/signIn/useEnsSignIn.ts +++ b/packages/@justaname.id/react/src/lib/hooks/signIn/useEnsSignIn.ts @@ -20,6 +20,7 @@ export interface UseEnsSignInParams signinNonceRoute?: string; signinRoute?: string; currentEnsRoute?: string; + local?: boolean; } export interface UseEnsSignInResult { @@ -56,28 +57,48 @@ export const useEnsSignIn = ( () => params?.currentEnsRoute || routes.currentEnsRoute, [routes.currentEnsRoute, params?.currentEnsRoute] ); + const _signinNonceRoute = useMemo( + () => params?.signinNonceRoute || routes.signinNonceRoute, + [routes.signinNonceRoute, params?.signinNonceRoute] + ); const { refreshEnsAuth } = useEnsAuth({ backendUrl: _backendUrl, currentEnsRoute: _currentEnsRoute, + local: params?.local, }); const { nonce, refetchNonce } = useEnsNonce({ - backendUrl: params?.backendUrl, - signinNonceRoute: params?.signinNonceRoute, - address, + backendUrl: _backendUrl, + signinNonceRoute: _signinNonceRoute, + enabled: !params?.local, }); const mutation = useMutation({ mutationFn: async (_params: UseEnsSignInFunctionParams) => { - if (!address) { - throw new Error('No address found'); - } + try { + if (!address) { + throw new Error('No address found'); + } - if (!nonce) { - throw new Error('No nonce found'); - } + if (params?.local) { + localStorage.setItem( + 'ENS_AUTH', + JSON.stringify({ + ens: _params.ens, + chainId, + address, + }) + ); + + refreshEnsAuth(); + + return 'success'; + } + + if (!nonce) { + throw new Error('No nonce found'); + } - try { const message = justaname.signIn.requestSignIn({ ens: _params.ens, ttl: config?.signInTtl, @@ -105,11 +126,15 @@ export const useEnsSignIn = ( credentials: 'include', }); + if (response.status !== 200) { + throw new Error('Failed to sign in'); + } + refreshEnsAuth(); return response.text(); } catch (e) { - refetchNonce(); + await refetchNonce(); throw e; } }, diff --git a/packages/@justaname.id/react/src/lib/hooks/signIn/useEnsSignOut.ts b/packages/@justaname.id/react/src/lib/hooks/signIn/useEnsSignOut.ts index 085fbf29..34cd468b 100644 --- a/packages/@justaname.id/react/src/lib/hooks/signIn/useEnsSignOut.ts +++ b/packages/@justaname.id/react/src/lib/hooks/signIn/useEnsSignOut.ts @@ -16,6 +16,7 @@ export interface UseEnsSignOutParams { signoutRoute?: string; currentEnsRoute?: string; signinNonceRoute?: string; + local?: boolean; } export const useEnsSignOut = ( @@ -43,26 +44,33 @@ export const useEnsSignOut = ( [routes.signinNonceRoute, params?.signinNonceRoute] ); - const { refreshEnsAuth, connectedEns } = useEnsAuth({ + const { refreshEnsAuth } = useEnsAuth({ backendUrl: _backendUrl, currentEnsRoute: _currentEnsRoute, + local: params?.local, }); const { refetchNonce } = useEnsNonce({ backendUrl: _backendUrl, signinNonceRoute: _signinNonceRoute, - address: connectedEns?.address, + enabled: !params?.local, }); const mutation = useMutation({ mutationFn: async () => { + if (params?.local) { + localStorage.removeItem('ENS_AUTH'); + refreshEnsAuth(); + return; + } + await fetch(signoutEndpoint, { method: 'POST', credentials: 'include', }); + await refetchNonce(); refreshEnsAuth(); - refetchNonce(); }, }); 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/CHANGELOG.md b/packages/@justaname.id/sdk/CHANGELOG.md index c520ec0c..0be6bf0c 100644 --- a/packages/@justaname.id/sdk/CHANGELOG.md +++ b/packages/@justaname.id/sdk/CHANGELOG.md @@ -1,3 +1,114 @@ +## 0.2.155 (2024-12-03) + + +### 🚀 Features + +- sign in optional ([3df90a7](https://github.com/JustaName-id/JustaName-sdk/commit/3df90a7)) + + +### 🧱 Updated Dependencies + +- Updated @justaname.id/siwens to 0.0.88 + + +### ❤️ Thank You + +- HadiKhai + +## 0.2.154 (2024-12-02) + + +### 🧱 Updated Dependencies + +- Updated @justaname.id/siwens to 0.0.87 + +## 0.2.153 (2024-12-01) + + +### 🚀 Features + +- support ethers 5 and ethers 6 ([f82ab13](https://github.com/JustaName-id/JustaName-sdk/commit/f82ab13)) + + +### 🧱 Updated Dependencies + +- Updated @justaname.id/siwens to 0.0.86 + + +### ❤️ Thank You + +- HadiKhai + +## 0.2.152 (2024-11-29) + + +### 🧱 Updated Dependencies + +- Updated @justaname.id/siwens to 0.0.85 + +## 0.2.151 (2024-11-29) + + +### 🧱 Updated Dependencies + +- Updated @justaname.id/siwens to 0.0.84 + +## 0.2.150 (2024-11-29) + + +### 🧱 Updated Dependencies + +- Updated @justaname.id/siwens to 0.0.83 + +## 0.2.149 (2024-11-29) + + +### 🧱 Updated Dependencies + +- Updated @justaname.id/siwens to 0.0.82 + +## 0.2.148 (2024-11-29) + + +### 🧱 Updated Dependencies + +- Updated @justaname.id/siwens to 0.0.81 + +## 0.2.147 (2024-11-29) + + +### 🚀 Features + +- efp ([36fcc153](https://github.com/JustaName-id/JustaName-sdk/commit/36fcc153)) + +- console fixes ([358b3a0e](https://github.com/JustaName-id/JustaName-sdk/commit/358b3a0e)) + +- added docs and the sign in should only show ens set to default resolver or justaname resolver only ([a3bcecaa](https://github.com/JustaName-id/JustaName-sdk/commit/a3bcecaa)) + +- sdk signin fix for aa wallets ([52944a72](https://github.com/JustaName-id/JustaName-sdk/commit/52944a72)) + +- enabled in hooks and siwens fix and coinbase fix ([c3fb455e](https://github.com/JustaName-id/JustaName-sdk/commit/c3fb455e)) + +- cache provider url to reduce calls ([d4943d0b](https://github.com/JustaName-id/JustaName-sdk/commit/d4943d0b)) + + +### 🩹 Fixes + +- update rest call and ui fixes ([81b05f29](https://github.com/JustaName-id/JustaName-sdk/commit/81b05f29)) + +- justaname network and widget package json ([6afe04ce](https://github.com/JustaName-id/JustaName-sdk/commit/6afe04ce)) + + +### 🧱 Updated Dependencies + +- Updated @justaname.id/siwens to 0.0.80 + + +### ❤️ Thank You + +- anthony2399 @anthony23991 +- HadiKhai + ## 0.2.146 (2024-11-15) diff --git a/packages/@justaname.id/sdk/package.json b/packages/@justaname.id/sdk/package.json index a03d998a..33222e62 100644 --- a/packages/@justaname.id/sdk/package.json +++ b/packages/@justaname.id/sdk/package.json @@ -1,8 +1,8 @@ { "name": "@justaname.id/sdk", - "version": "0.2.146", + "version": "0.2.155", "dependencies": { - "@justaname.id/siwens": "0.0.79", + "@justaname.id/siwens": "0.0.88", "axios": "^1.6.0", "qs": "^6.12.0" }, @@ -10,7 +10,7 @@ "jest": "^29.4.1" }, "peerDependencies": { - "ethers": ">=6.0.0", + "ethers": "^5.6.8 || ^6.0.8", "siwe": ">=2.0.0", "viem": ">=2.0.0" }, diff --git a/packages/@justaname.id/sdk/src/lib/justaname/index.ts b/packages/@justaname.id/sdk/src/lib/justaname/index.ts index be7d760c..84da9a6e 100644 --- a/packages/@justaname.id/sdk/src/lib/justaname/index.ts +++ b/packages/@justaname.id/sdk/src/lib/justaname/index.ts @@ -14,8 +14,8 @@ import { Subnames, } from '../features'; import { InvalidConfigurationException } from '../errors/InvalidConfiguration.exception'; -import { ethers } from 'ethers'; import { providerUrlChainIdLoadingMap, providerUrlChainIdMap } from '../memory'; +import { getJsonRpcProvider } from '../utils/ethersCompat'; /** * The main class for the JustaName SDK. @@ -162,11 +162,11 @@ export class JustaName { const defaultMainnetProviderUrl = 'https://cloudflare-eth.com'; const defaultTestnetProviderUrl = 'https://rpc.sepolia.org'; - const defaultMainnetProvider = new ethers.JsonRpcProvider( + const defaultMainnetProvider = getJsonRpcProvider( defaultMainnetProviderUrl, 1 ); - const defaultTestnetProvider = new ethers.JsonRpcProvider( + const defaultTestnetProvider = getJsonRpcProvider( defaultTestnetProviderUrl, 11155111 ); @@ -184,7 +184,7 @@ export class JustaName { }, // { // chainId: 31337 as ChainId, - // provider: new ethers.JsonRpcProvider('http://localhost:8545'), + // provider: getJsonRpcProvider('http://localhost:8545'), // providerUrl: 'http://localhost:8545', // }, ] as NetworksWithProvider; @@ -194,7 +194,7 @@ export class JustaName { if (network && network?.providerUrl) { return { chainId: network.chainId, - provider: new ethers.JsonRpcProvider(network.providerUrl), + provider: getJsonRpcProvider(network.providerUrl), providerUrl: network.providerUrl, }; } else { @@ -256,7 +256,7 @@ export class JustaName { } } - const provider = new ethers.JsonRpcProvider(network.providerUrl); + const provider = getJsonRpcProvider(network.providerUrl); provider.getNetwork().then((_network) => { if (network.chainId.toString() !== _network.chainId.toString()) { throw new InvalidConfigurationException( diff --git a/packages/@justaname.id/sdk/src/lib/types/justaname/configuration.ts b/packages/@justaname.id/sdk/src/lib/types/justaname/configuration.ts index 227c8129..344bc654 100644 --- a/packages/@justaname.id/sdk/src/lib/types/justaname/configuration.ts +++ b/packages/@justaname.id/sdk/src/lib/types/justaname/configuration.ts @@ -1,5 +1,5 @@ +import { JsonRpcProvider } from '../../utils/ethersCompat'; import { ChainId } from '../common'; -import { JsonRpcProvider } from 'ethers'; export interface NetworkWithProvider extends Network { diff --git a/packages/@justaname.id/sdk/src/lib/utils/ethersCompat.ts b/packages/@justaname.id/sdk/src/lib/utils/ethersCompat.ts new file mode 100644 index 00000000..682e9f25 --- /dev/null +++ b/packages/@justaname.id/sdk/src/lib/utils/ethersCompat.ts @@ -0,0 +1,51 @@ +// inspired by spruceid siwe: https://github.com/spruceid/siwe/blob/main/packages/siwe/lib/ethersCompat.ts + +import { ethers } from 'ethers'; + +// @ts-expect-error -- compatibility hack +type ProviderV5 = ethers.providers.Provider; +type ProviderV6 = ethers.Provider; +// @ts-expect-error -- compatibility hack +type JsonRpcProviderV5 = ethers.providers.JsonRpcProvider; +type JsonRpcProviderV6 = ethers.JsonRpcProvider; + +export type Provider = ProviderV6 extends undefined ? ProviderV5 : ProviderV6; +export type JsonRpcProvider = JsonRpcProviderV6 extends undefined + ? JsonRpcProviderV5 + : JsonRpcProviderV6; + +interface EthersCompat { + namehash?: (name: string) => string; + getAddress?: (address: string) => string; + JsonRpcProvider?: new (...args: any[]) => JsonRpcProvider; + utils: { + namehash: (name: string) => string; + getAddress: (address: string) => string; + }; + providers: { + JsonRpcProvider: new (...args: any[]) => JsonRpcProvider; + }; +} + +const ethersCompat = ethers as unknown as EthersCompat; + +export const getJsonRpcProvider = ( + providerUrl?: string, + chainId?: number +): JsonRpcProvider => { + if ('JsonRpcProvider' in ethersCompat) { + return new ethersCompat.JsonRpcProvider!(providerUrl, chainId); + } else { + return new ethersCompat.providers.JsonRpcProvider(providerUrl, chainId); + } +}; + +export const namehash: (name: string) => string = + 'namehash' in ethersCompat + ? ethersCompat.namehash! + : ethersCompat.utils.namehash; + +export const getAddress: (address: string) => string = + 'getAddress' in ethersCompat + ? ethersCompat.getAddress! + : ethersCompat.utils.getAddress; diff --git a/packages/@justaname.id/siwens/CHANGELOG.md b/packages/@justaname.id/siwens/CHANGELOG.md index 29c21301..36acbe66 100644 --- a/packages/@justaname.id/siwens/CHANGELOG.md +++ b/packages/@justaname.id/siwens/CHANGELOG.md @@ -1,3 +1,73 @@ +## 0.0.88 (2024-12-03) + + +### 🚀 Features + +- sign in optional ([3df90a7](https://github.com/JustaName-id/JustaName-sdk/commit/3df90a7)) + + +### ❤️ Thank You + +- HadiKhai + +## 0.0.87 (2024-12-02) + +This was a version bump only for @justaname.id/siwens to align it with other projects, there were no code changes. + +## 0.0.86 (2024-12-01) + + +### 🚀 Features + +- support ethers 5 and ethers 6 ([f82ab13](https://github.com/JustaName-id/JustaName-sdk/commit/f82ab13)) + + +### ❤️ Thank You + +- HadiKhai + +## 0.0.85 (2024-11-29) + +This was a version bump only for @justaname.id/siwens to align it with other projects, there were no code changes. + +## 0.0.84 (2024-11-29) + +This was a version bump only for @justaname.id/siwens to align it with other projects, there were no code changes. + +## 0.0.83 (2024-11-29) + +This was a version bump only for @justaname.id/siwens to align it with other projects, there were no code changes. + +## 0.0.82 (2024-11-29) + +This was a version bump only for @justaname.id/siwens to align it with other projects, there were no code changes. + +## 0.0.81 (2024-11-29) + +This was a version bump only for @justaname.id/siwens to align it with other projects, there were no code changes. + +## 0.0.80 (2024-11-29) + + +### 🚀 Features + +- added docs and the sign in should only show ens set to default resolver or justaname resolver only ([a3bcecaa](https://github.com/JustaName-id/JustaName-sdk/commit/a3bcecaa)) + +- sdk signin fix for aa wallets ([52944a72](https://github.com/JustaName-id/JustaName-sdk/commit/52944a72)) + +- enabled in hooks and siwens fix and coinbase fix ([c3fb455e](https://github.com/JustaName-id/JustaName-sdk/commit/c3fb455e)) + + +### 🩹 Fixes + +- update rest call and ui fixes ([81b05f29](https://github.com/JustaName-id/JustaName-sdk/commit/81b05f29)) + + +### ❤️ Thank You + +- anthony2399 @anthony23991 +- HadiKhai + ## 0.0.79 (2024-11-15) This was a version bump only for @justaname.id/siwens to align it with other projects, there were no code changes. diff --git a/packages/@justaname.id/siwens/README.md b/packages/@justaname.id/siwens/README.md index 067c7dee..727d6e1b 100644 --- a/packages/@justaname.id/siwens/README.md +++ b/packages/@justaname.id/siwens/README.md @@ -43,54 +43,60 @@ yarn add @justaname.id/siwens ### Example Usage ```typescript -import { SIWENS, InvalidDomainException, InvalidENSException, InvalidStatementException, InvalidTimeException } f, InvalidDomainException, InvalidENSException, InvalidStatementException, InvalidTimeException } from '@justaname.id/siwens';rom '@justaname.id/siwens'; -import { ethers } from 'ethers'; +import { SIWENS, InvalidENSException } from '@justaname.id/siwens'; +import { Wallet } from 'ethers'; // Define your provider URL (e.g., Infura) -const providerUrl = 'https://mainnet.infura.io/v3/YOUR_INFURA_KEY'; +const infuraProjectId = 'YOUR_INFURA_PROJECT_ID'; +const providerUrl = 'https://mainnet.infura.io/v3/' + infuraProjectId; -const signer = new ethers.Wallet('YOUR_PRIVATE_KEY_ENS_HOLDER') +// const signer = Wallet.createRandom(); +const signer = new Wallet('YOUR_PRIVATE_KEY'); async function signInUser() { const siwens = new SIWENS({ params: { - domain: 'example.eth', // The domain you're authenticating for + domain: 'example.com', // The domain you're authenticating for + uri: 'https://example.com', // The URI of the dApp + chainId: 1, // The chain ID of the network ttl: 3600000, // Time-to-Live (TTL) in milliseconds (1 hour) ens: 'user.example.eth', // The ENS name being used statement: 'Signing into dApp', // Optional custom sign-in statement + address: signer.address }, providerUrl - }); + }); const message = await siwens.prepareMessage(); const signature = await signer.signMessage(message); - return signature; + return {signature, message}; } // Verifying a user's sign-in request -async function verifyUserSignature(signature: string, message: string) { - try { - const siwe = new SIWENS({ - params: message, - providerUrl - }) - - const verification = await siwe.verifySignature({ - signature: signature, - }); - - console.log('ENS Sign-in successful!', verification.ens); - } catch (error) { - if (error instanceof InvalidENSException) { - console.error('ENS Verification Failed:', error.message); - } else { - console.error('Error during verification:', error.message); - } - } +async function verifyUserSignature(signature, message) { + try { + const siwe = new SIWENS({ + params: message, + providerUrl + }) + + const verification = await siwe.verify({ + signature: signature, + }); + + console.log('ENS Sign-in successful!', verification.ens); + } catch (error) { + console.error('Error during verification:', error); + if (error instanceof InvalidENSException) { + console.error('ENS Verification Failed:', error.message); + } else { + console.error('Error during verification:', error); + } + } } // Example usage -signInUser().then(async (signature) => { - await verifyUserSignature(signature, 'Signing into dApp'); +signInUser().then(async ({message, signature}) => { + await verifyUserSignature(signature, message); }); ``` diff --git a/packages/@justaname.id/siwens/package.json b/packages/@justaname.id/siwens/package.json index f9bd6a16..e7616056 100644 --- a/packages/@justaname.id/siwens/package.json +++ b/packages/@justaname.id/siwens/package.json @@ -1,8 +1,8 @@ { "name": "@justaname.id/siwens", - "version": "0.0.79", + "version": "0.0.88", "peerDependencies": { - "ethers": ">=6.0.0", + "ethers": "^5.6.8 || ^6.0.8", "siwe": ">=2.0.0" }, "exports": { diff --git a/packages/@justaname.id/siwens/src/lib/siwens/siwens.ts b/packages/@justaname.id/siwens/src/lib/siwens/siwens.ts index d12c68ca..c85ddcb7 100644 --- a/packages/@justaname.id/siwens/src/lib/siwens/siwens.ts +++ b/packages/@justaname.id/siwens/src/lib/siwens/siwens.ts @@ -1,4 +1,3 @@ -import { JsonRpcProvider } from 'ethers'; import { generateNonce, SiweMessage, @@ -19,6 +18,7 @@ import { extractDataFromStatement, } from '../utils'; import { toASCII, toUnicode } from 'punycode'; +import { getJsonRpcProvider, JsonRpcProvider } from '../utils/ethersCompat'; export interface SiwensResponse extends SiweResponse { ens: string; @@ -50,7 +50,7 @@ export class SIWENS extends SiweMessage { if (!providerUrl) { throw InvalidConfigurationException.providerUrlRequired(); } - this.provider = new JsonRpcProvider(providerUrl); + this.provider = getJsonRpcProvider(providerUrl); this.providerUrl = providerUrl; return; } @@ -91,12 +91,12 @@ export class SIWENS extends SiweMessage { expirationTime, }); this.providerUrl = providerUrl; - this.provider = new JsonRpcProvider(providerUrl); + this.provider = getJsonRpcProvider(providerUrl); } override async verify( params: VerifyParams, - opts: VerifyOpts + opts?: VerifyOpts ): Promise { let verification: SiweResponse; diff --git a/packages/@justaname.id/siwens/src/lib/utils/ethersCompat.ts b/packages/@justaname.id/siwens/src/lib/utils/ethersCompat.ts new file mode 100644 index 00000000..682e9f25 --- /dev/null +++ b/packages/@justaname.id/siwens/src/lib/utils/ethersCompat.ts @@ -0,0 +1,51 @@ +// inspired by spruceid siwe: https://github.com/spruceid/siwe/blob/main/packages/siwe/lib/ethersCompat.ts + +import { ethers } from 'ethers'; + +// @ts-expect-error -- compatibility hack +type ProviderV5 = ethers.providers.Provider; +type ProviderV6 = ethers.Provider; +// @ts-expect-error -- compatibility hack +type JsonRpcProviderV5 = ethers.providers.JsonRpcProvider; +type JsonRpcProviderV6 = ethers.JsonRpcProvider; + +export type Provider = ProviderV6 extends undefined ? ProviderV5 : ProviderV6; +export type JsonRpcProvider = JsonRpcProviderV6 extends undefined + ? JsonRpcProviderV5 + : JsonRpcProviderV6; + +interface EthersCompat { + namehash?: (name: string) => string; + getAddress?: (address: string) => string; + JsonRpcProvider?: new (...args: any[]) => JsonRpcProvider; + utils: { + namehash: (name: string) => string; + getAddress: (address: string) => string; + }; + providers: { + JsonRpcProvider: new (...args: any[]) => JsonRpcProvider; + }; +} + +const ethersCompat = ethers as unknown as EthersCompat; + +export const getJsonRpcProvider = ( + providerUrl?: string, + chainId?: number +): JsonRpcProvider => { + if ('JsonRpcProvider' in ethersCompat) { + return new ethersCompat.JsonRpcProvider!(providerUrl, chainId); + } else { + return new ethersCompat.providers.JsonRpcProvider(providerUrl, chainId); + } +}; + +export const namehash: (name: string) => string = + 'namehash' in ethersCompat + ? ethersCompat.namehash! + : ethersCompat.utils.namehash; + +export const getAddress: (address: string) => string = + 'getAddress' in ethersCompat + ? ethersCompat.getAddress! + : ethersCompat.utils.getAddress; diff --git a/packages/@justverified/plugin/CHANGELOG.md b/packages/@justverified/plugin/CHANGELOG.md index 32447882..67dfe942 100644 --- a/packages/@justverified/plugin/CHANGELOG.md +++ b/packages/@justverified/plugin/CHANGELOG.md @@ -1,3 +1,70 @@ +## 0.0.93 (2024-12-03) + +This was a version bump only for @justverified/plugin to align it with other projects, there were no code changes. + +## 0.0.92 (2024-12-02) + +This was a version bump only for @justverified/plugin to align it with other projects, there were no code changes. + +## 0.0.91 (2024-12-01) + +This was a version bump only for @justverified/plugin to align it with other projects, there were no code changes. + +## 0.0.90 (2024-11-29) + +This was a version bump only for @justverified/plugin to align it with other projects, there were no code changes. + +## 0.0.89 (2024-11-29) + +This was a version bump only for @justverified/plugin to align it with other projects, there were no code changes. + +## 0.0.88 (2024-11-29) + +This was a version bump only for @justverified/plugin to align it with other projects, there were no code changes. + +## 0.0.87 (2024-11-29) + +This was a version bump only for @justverified/plugin to align it with other projects, there were no code changes. + +## 0.0.86 (2024-11-29) + +This was a version bump only for @justverified/plugin to align it with other projects, there were no code changes. + +## 0.0.85 (2024-11-29) + + +### 🚀 Features + +- remove mapps from justverified ([2898c521](https://github.com/JustaName-id/JustaName-sdk/commit/2898c521)) + +- efp ([36fcc153](https://github.com/JustaName-id/JustaName-sdk/commit/36fcc153)) + +- efp and storybook for react sdk ([f0c5c7a0](https://github.com/JustaName-id/JustaName-sdk/commit/f0c5c7a0)) + +- console fixes ([358b3a0e](https://github.com/JustaName-id/JustaName-sdk/commit/358b3a0e)) + +- added docs and the sign in should only show ens set to default resolver or justaname resolver only ([a3bcecaa](https://github.com/JustaName-id/JustaName-sdk/commit/a3bcecaa)) + +- move justweb3 to peer dep in the plugins ([316047cb](https://github.com/JustaName-id/JustaName-sdk/commit/316047cb)) + +- rainbow and privy examples ([fb2aa7dc](https://github.com/JustaName-id/JustaName-sdk/commit/fb2aa7dc)) + +- dialog fixes and logo needs to be fixed ([fad47312](https://github.com/JustaName-id/JustaName-sdk/commit/fad47312)) + +- **widget:** mobile dialogs ([#51](https://github.com/JustaName-id/JustaName-sdk/pull/51)) + + +### 🩹 Fixes + +- update rest call and ui fixes ([81b05f29](https://github.com/JustaName-id/JustaName-sdk/commit/81b05f29)) + + +### ❤️ Thank You + +- Anthony Khoury @anthony23991 +- anthony2399 @anthony23991 +- HadiKhai + ## 0.0.84 (2024-11-15) This was a version bump only for @justverified/plugin to align it with other projects, there were no code changes. diff --git a/packages/@justverified/plugin/package.json b/packages/@justverified/plugin/package.json index ea512de6..27dd8160 100644 --- a/packages/@justverified/plugin/package.json +++ b/packages/@justverified/plugin/package.json @@ -1,6 +1,6 @@ { "name": "@justverified/plugin", - "version": "0.0.84", + "version": "0.0.93", "dependencies": { "axios": "^1.6.0", "lodash": "4.17.21", diff --git a/packages/@justverified/plugin/src/stories/justverified.stories.tsx b/packages/@justverified/plugin/src/stories/justverified.stories.tsx index e541a041..d3074875 100644 --- a/packages/@justverified/plugin/src/stories/justverified.stories.tsx +++ b/packages/@justverified/plugin/src/stories/justverified.stories.tsx @@ -11,7 +11,6 @@ import { ChainId } from '@justaname.id/sdk'; import { Meta, StoryObj } from '@storybook/react'; import { Button } from '@justweb3/ui'; import { - JustEnsCard, JustWeb3Button, JustWeb3Provider, JustWeb3ProviderConfig, @@ -19,6 +18,7 @@ import { } from '@justweb3/widget'; import '@justweb3/widget/styles.css'; import { JustVerifiedPlugin } from '../lib'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; const queryClient = new QueryClient(); @@ -47,13 +47,14 @@ const JustWeb3Config: JustWeb3ProviderConfig = { ], openOnWalletConnect: true, allowedEns: 'all', - dev: import.meta.env.STORYBOOK_APP_DEV === 'true', + // dev: import.meta.env.STORYBOOK_APP_DEV === 'true', disableOverlay: true, + // enableAuth: true, plugins: [ JustVerifiedPlugin( - ['twitter', 'github', 'discord'], + ['twitter', 'github', 'discord'] // 'http://localhost:3009/verifications/v1' - 'https://api-staging.justaname.id/verifications/v1' + // 'https://api-staging.justaname.id/verifications/v1' ), ], }; @@ -110,35 +111,36 @@ export const Example = () => {
- + {/**/} -
- - - - - - - -
-
- - - - - - -
+ {/*
*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/*
*/} + {/*
*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/*
*/}
+ ); diff --git a/packages/@justweb3/efp-plugin/CHANGELOG.md b/packages/@justweb3/efp-plugin/CHANGELOG.md index fa5523e5..8c6a03c5 100644 --- a/packages/@justweb3/efp-plugin/CHANGELOG.md +++ b/packages/@justweb3/efp-plugin/CHANGELOG.md @@ -1,3 +1,63 @@ +## 0.1.54 (2024-12-03) + +This was a version bump only for @justweb3/efp-plugin to align it with other projects, there were no code changes. + +## 0.1.53 (2024-12-02) + +This was a version bump only for @justweb3/efp-plugin to align it with other projects, there were no code changes. + +## 0.1.52 (2024-12-01) + +This was a version bump only for @justweb3/efp-plugin to align it with other projects, there were no code changes. + +## 0.1.51 (2024-11-29) + +This was a version bump only for @justweb3/efp-plugin to align it with other projects, there were no code changes. + +## 0.1.50 (2024-11-29) + +This was a version bump only for @justweb3/efp-plugin to align it with other projects, there were no code changes. + +## 0.1.49 (2024-11-29) + +This was a version bump only for @justweb3/efp-plugin to align it with other projects, there were no code changes. + +## 0.1.48 (2024-11-29) + +This was a version bump only for @justweb3/efp-plugin to align it with other projects, there were no code changes. + +## 0.1.47 (2024-11-29) + +This was a version bump only for @justweb3/efp-plugin to align it with other projects, there were no code changes. + +## 0.1.46 (2024-11-29) + + +### 🚀 Features + +- move justweb3 to peer dep in the plugins ([316047cb](https://github.com/JustaName-id/JustaName-sdk/commit/316047cb)) + +- sdk signin fix for aa wallets ([52944a72](https://github.com/JustaName-id/JustaName-sdk/commit/52944a72)) + +- enabled in hooks and siwens fix and coinbase fix ([c3fb455e](https://github.com/JustaName-id/JustaName-sdk/commit/c3fb455e)) + + +### 🩹 Fixes + +- lint fix on react and package json of projects ([b29cd296](https://github.com/JustaName-id/JustaName-sdk/commit/b29cd296)) + +- update rest call and ui fixes ([81b05f29](https://github.com/JustaName-id/JustaName-sdk/commit/81b05f29)) + +- yarn lock ([98e8b8b2](https://github.com/JustaName-id/JustaName-sdk/commit/98e8b8b2)) + +- justweb3button infinite loading ([465453ca](https://github.com/JustaName-id/JustaName-sdk/commit/465453ca)) + + +### ❤️ Thank You + +- anthony2399 @anthony23991 +- HadiKhai + ## 0.1.45 (2024-11-15) This was a version bump only for @justweb3/efp-plugin to align it with other projects, there were no code changes. diff --git a/packages/@justweb3/efp-plugin/package.json b/packages/@justweb3/efp-plugin/package.json index 1f362b62..49fb3781 100644 --- a/packages/@justweb3/efp-plugin/package.json +++ b/packages/@justweb3/efp-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@justweb3/efp-plugin", - "version": "0.1.45", + "version": "0.1.54", "dependencies": { "axios": "^1.6.0" }, 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/poap-plugin/package.json b/packages/@justweb3/poap-plugin/package.json index bfea4b4d..58373ca4 100644 --- a/packages/@justweb3/poap-plugin/package.json +++ b/packages/@justweb3/poap-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@justweb3/poap-plugin", - "version": "0.0.3", + "version": "0.0.12", "dependencies": { "axios": "^1.6.0" }, diff --git a/packages/@justweb3/talent-protocol-plugin/package.json b/packages/@justweb3/talent-protocol-plugin/package.json index 10fd0f85..c538a7e0 100644 --- a/packages/@justweb3/talent-protocol-plugin/package.json +++ b/packages/@justweb3/talent-protocol-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@justweb3/talent-protocol-plugin", - "version": "0.0.3", + "version": "0.0.12", "dependencies": { "axios": "^1.6.0" }, diff --git a/packages/@justweb3/ui/CHANGELOG.md b/packages/@justweb3/ui/CHANGELOG.md index a31b9d96..ed39886b 100644 --- a/packages/@justweb3/ui/CHANGELOG.md +++ b/packages/@justweb3/ui/CHANGELOG.md @@ -1,3 +1,90 @@ +## 0.0.94 (2024-12-03) + + +### 🚀 Features + +- sign in optional ([3df90a7](https://github.com/JustaName-id/JustaName-sdk/commit/3df90a7)) + + +### ❤️ Thank You + +- HadiKhai + +## 0.0.93 (2024-12-02) + +This was a version bump only for @justweb3/ui to align it with other projects, there were no code changes. + +## 0.0.92 (2024-12-01) + + +### 🚀 Features + +- support ethers 5 and ethers 6 ([f82ab13](https://github.com/JustaName-id/JustaName-sdk/commit/f82ab13)) + + +### ❤️ Thank You + +- HadiKhai + +## 0.0.91 (2024-11-29) + +This was a version bump only for @justweb3/ui to align it with other projects, there were no code changes. + +## 0.0.90 (2024-11-29) + +This was a version bump only for @justweb3/ui to align it with other projects, there were no code changes. + +## 0.0.89 (2024-11-29) + +This was a version bump only for @justweb3/ui to align it with other projects, there were no code changes. + +## 0.0.88 (2024-11-29) + +This was a version bump only for @justweb3/ui to align it with other projects, there were no code changes. + +## 0.0.87 (2024-11-29) + +This was a version bump only for @justweb3/ui to align it with other projects, there were no code changes. + +## 0.0.86 (2024-11-29) + + +### 🚀 Features + +- remove mapps from justverified ([2898c521](https://github.com/JustaName-id/JustaName-sdk/commit/2898c521)) + +- efp ([36fcc153](https://github.com/JustaName-id/JustaName-sdk/commit/36fcc153)) + +- efp and storybook for react sdk ([f0c5c7a0](https://github.com/JustaName-id/JustaName-sdk/commit/f0c5c7a0)) + +- added docs and the sign in should only show ens set to default resolver or justaname resolver only ([a3bcecaa](https://github.com/JustaName-id/JustaName-sdk/commit/a3bcecaa)) + +- move justweb3 to peer dep in the plugins ([316047cb](https://github.com/JustaName-id/JustaName-sdk/commit/316047cb)) + +- sdk signin fix for aa wallets ([52944a72](https://github.com/JustaName-id/JustaName-sdk/commit/52944a72)) + +- rainbow and privy examples ([fb2aa7dc](https://github.com/JustaName-id/JustaName-sdk/commit/fb2aa7dc)) + +- dialog fixes and logo needs to be fixed ([fad47312](https://github.com/JustaName-id/JustaName-sdk/commit/fad47312)) + +- expandableCard ([#54](https://github.com/JustaName-id/JustaName-sdk/pull/54)) + +- packagejson fixes ([873f9967](https://github.com/JustaName-id/JustaName-sdk/commit/873f9967)) + +- **widget:** mobile dialogs ([#51](https://github.com/JustaName-id/JustaName-sdk/pull/51)) + + +### 🩹 Fixes + +- update rest call and ui fixes ([81b05f29](https://github.com/JustaName-id/JustaName-sdk/commit/81b05f29)) + + +### ❤️ Thank You + +- Anthony Khoury @anthony23991 +- anthony2399 @anthony23991 +- HadiKhai + ## 0.0.85 (2024-11-15) This was a version bump only for @justweb3/ui to align it with other projects, there were no code changes. diff --git a/packages/@justweb3/ui/package.json b/packages/@justweb3/ui/package.json index a59b2553..09562f42 100644 --- a/packages/@justweb3/ui/package.json +++ b/packages/@justweb3/ui/package.json @@ -1,6 +1,6 @@ { "name": "@justweb3/ui", - "version": "0.0.85", + "version": "0.0.94", "dependencies": { "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-dialog": "^1.1.1", diff --git a/packages/@justweb3/ui/src/lib/components/ClickableItem/index.tsx b/packages/@justweb3/ui/src/lib/components/ClickableItem/index.tsx index 9be1c6ce..15ace89b 100644 --- a/packages/@justweb3/ui/src/lib/components/ClickableItem/index.tsx +++ b/packages/@justweb3/ui/src/lib/components/ClickableItem/index.tsx @@ -8,7 +8,7 @@ interface ClickableListItemProps { subtitle?: string | React.ReactNode; left?: React.ReactNode; loading?: boolean; - onClick?: () => void; + onClick?: (e: React.MouseEvent) => void; disabled?: boolean; right?: React.ReactNode; onHover?: (hover: boolean) => void; @@ -52,10 +52,12 @@ export const ClickableItem = React.forwardRef<
{ + onClick={(e) => { if (!loading && !disabled && clickable) { - onClick && onClick(); + onClick && onClick(e); } + // e.stopPropagation(); + // e.preventDefault(); }} onPointerEnter={() => { if (!loading && !disabled && clickable) { 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..f0b5f0f9 --- /dev/null +++ b/packages/@justweb3/ui/src/lib/ui/NotificationBadge/NotificationBadge.module.css @@ -0,0 +1,26 @@ +.notificationIcon { + position: relative; + display: flex; +} + +.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/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/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/.storybook/preview.ts b/packages/@justweb3/widget/.storybook/preview.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/@justweb3/widget/CHANGELOG.md b/packages/@justweb3/widget/CHANGELOG.md index 43ac416b..91e4af78 100644 --- a/packages/@justweb3/widget/CHANGELOG.md +++ b/packages/@justweb3/widget/CHANGELOG.md @@ -1,3 +1,153 @@ +## 0.0.94 (2024-12-03) + + +### 🚀 Features + +- sign in optional ([3df90a7](https://github.com/JustaName-id/JustaName-sdk/commit/3df90a7)) + + +### 🧱 Updated Dependencies + +- Updated @justaname.id/sdk to 0.2.155 +- Updated @justaname.id/react to 0.3.158 +- Updated @justweb3/ui to 0.0.94 + + +### ❤️ Thank You + +- HadiKhai + +## 0.0.93 (2024-12-02) + + +### 🧱 Updated Dependencies + +- Updated @justaname.id/sdk to 0.2.154 +- Updated @justaname.id/react to 0.3.157 +- Updated @justweb3/ui to 0.0.93 + +## 0.0.92 (2024-12-01) + + +### 🚀 Features + +- support ethers 5 and ethers 6 ([f82ab13](https://github.com/JustaName-id/JustaName-sdk/commit/f82ab13)) + + +### 🧱 Updated Dependencies + +- Updated @justaname.id/sdk to 0.2.153 +- Updated @justaname.id/react to 0.3.156 +- Updated @justweb3/ui to 0.0.92 + + +### ❤️ Thank You + +- HadiKhai + +## 0.0.91 (2024-11-29) + + +### 🧱 Updated Dependencies + +- Updated @justaname.id/sdk to 0.2.152 +- Updated @justaname.id/react to 0.3.155 +- Updated @justweb3/ui to 0.0.91 + +## 0.0.90 (2024-11-29) + + +### 🧱 Updated Dependencies + +- Updated @justaname.id/sdk to 0.2.151 +- Updated @justaname.id/react to 0.3.154 +- Updated @justweb3/ui to 0.0.90 + +## 0.0.89 (2024-11-29) + + +### 🧱 Updated Dependencies + +- Updated @justaname.id/sdk to 0.2.150 +- Updated @justaname.id/react to 0.3.153 +- Updated @justweb3/ui to 0.0.89 + +## 0.0.88 (2024-11-29) + + +### 🧱 Updated Dependencies + +- Updated @justaname.id/sdk to 0.2.149 +- Updated @justaname.id/react to 0.3.152 +- Updated @justweb3/ui to 0.0.88 + +## 0.0.87 (2024-11-29) + + +### 🧱 Updated Dependencies + +- Updated @justaname.id/sdk to 0.2.148 +- Updated @justaname.id/react to 0.3.151 +- Updated @justweb3/ui to 0.0.87 + +## 0.0.86 (2024-11-29) + + +### 🚀 Features + +- remove mapps from justverified ([2898c521](https://github.com/JustaName-id/JustaName-sdk/commit/2898c521)) + +- efp ([36fcc153](https://github.com/JustaName-id/JustaName-sdk/commit/36fcc153)) + +- efp and storybook for react sdk ([f0c5c7a0](https://github.com/JustaName-id/JustaName-sdk/commit/f0c5c7a0)) + +- console fixes ([358b3a0e](https://github.com/JustaName-id/JustaName-sdk/commit/358b3a0e)) + +- added docs and the sign in should only show ens set to default resolver or justaname resolver only ([a3bcecaa](https://github.com/JustaName-id/JustaName-sdk/commit/a3bcecaa)) + +- sdk signin fix for aa wallets ([52944a72](https://github.com/JustaName-id/JustaName-sdk/commit/52944a72)) + +- rainbow and privy examples ([fb2aa7dc](https://github.com/JustaName-id/JustaName-sdk/commit/fb2aa7dc)) + +- remove currentChain ([43e8b6d6](https://github.com/JustaName-id/JustaName-sdk/commit/43e8b6d6)) + +- dialog fixes and logo needs to be fixed ([fad47312](https://github.com/JustaName-id/JustaName-sdk/commit/fad47312)) + +- expandableCard ([#54](https://github.com/JustaName-id/JustaName-sdk/pull/54)) + +- community members tab ([#55](https://github.com/JustaName-id/JustaName-sdk/pull/55)) + +- enabled in hooks and siwens fix and coinbase fix ([c3fb455e](https://github.com/JustaName-id/JustaName-sdk/commit/c3fb455e)) + +- community button and release action ([0015df77](https://github.com/JustaName-id/JustaName-sdk/commit/0015df77)) + +- **widget:** mobile dialogs ([#51](https://github.com/JustaName-id/JustaName-sdk/pull/51)) + + +### 🩹 Fixes + +- error from useEnsWalletClient ([67043376](https://github.com/JustaName-id/JustaName-sdk/commit/67043376)) + +- update rest call and ui fixes ([81b05f29](https://github.com/JustaName-id/JustaName-sdk/commit/81b05f29)) + +- justweb3button infinite loading ([465453ca](https://github.com/JustaName-id/JustaName-sdk/commit/465453ca)) + +- justaname network and widget package json ([6afe04ce](https://github.com/JustaName-id/JustaName-sdk/commit/6afe04ce)) + + +### 🧱 Updated Dependencies + +- Updated @justaname.id/sdk to 0.2.147 +- Updated @justaname.id/react to 0.3.150 +- Updated @justweb3/ui to 0.0.86 + + +### ❤️ Thank You + +- Anthony Khoury @anthony23991 +- anthony2399 @anthony23991 +- HadiKhai + ## 0.0.85 (2024-11-15) diff --git a/packages/@justweb3/widget/package.json b/packages/@justweb3/widget/package.json index 7a307ae9..303946bf 100644 --- a/packages/@justweb3/widget/package.json +++ b/packages/@justweb3/widget/package.json @@ -1,12 +1,12 @@ { "name": "@justweb3/widget", - "version": "0.0.85", + "version": "0.0.94", "dependencies": { "@ensdomains/address-encoder": "^1.1.2", "@hookform/resolvers": "^3.9.0", - "@justaname.id/react": "0.3.149", - "@justaname.id/sdk": "0.2.146", - "@justweb3/ui": "^0.0.85", + "@justaname.id/react": "0.3.158", + "@justaname.id/sdk": "0.2.155", + "@justweb3/ui": "^0.0.94", "clsx": "1.2.1", "cropperjs": "^1.6.2", "lodash": "4.17.21", 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 f4468019..3b130351 100644 --- a/packages/@justweb3/widget/src/lib/components/JustEnsCard/JustEnsCard.module.css +++ b/packages/@justweb3/widget/src/lib/components/JustEnsCard/JustEnsCard.module.css @@ -1,6 +1,7 @@ .expandableCard { - height: 200px; - width: 400px; + height: 193px; + box-sizing: content-box; + width: 398px; display: flex; flex-direction: column; position: relative; @@ -69,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 64e94c4e..5b24a65d 100644 --- a/packages/@justweb3/widget/src/lib/components/JustEnsCard/index.tsx +++ b/packages/@justweb3/widget/src/lib/components/JustEnsCard/index.tsx @@ -111,8 +111,13 @@ export const JustEnsCard: FC = ({ return (
handleEnsClick()} + ref={ref} + className={styles.expandableCard} + onClick={(e) => { + e.stopPropagation(); + e.preventDefault(); + handleEnsClick(); + }} > = ({ {records.sanitizedRecords.socials.map((social, index) => React.cloneElement(getTextRecordIcon(social.key), { key: `${ens}-${index}-${social.key}`, - width: 12, - height: 12, + className: styles.socialIcon, }) )} @@ -239,7 +243,11 @@ export const JustEnsCard: FC = ({ ) } disabled={!isEns && !primaryName} - onClick={() => handleEnsClick()} + onClick={(event) => { + handleEnsClick(); + event.stopPropagation(); + event.preventDefault(); + }} /> ); }; diff --git a/packages/@justweb3/widget/src/lib/components/JustWeb3Button/index.tsx b/packages/@justweb3/widget/src/lib/components/JustWeb3Button/index.tsx index c16067a0..2b53c6b3 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={ = ({ } 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 ( = ({ style={{ backgroundColor: 'var(--justweb3-background-color)', color: 'var(--justweb3-primary-color)', + ...style, }} + right={ +
+ {right} +
+ } contentStyle={{ alignItems: 'start', }} @@ -187,32 +225,6 @@ export const JustWeb3Button: FC = ({ const connectedEnsProfileContent = ( - {/**/} - {/* */} - {/* Profile Overview*/} - {/*

*/} - {/* */} - {/* }*/} - {/* onClick={() => {*/} - {/* openEnsProfile(connectedEns?.ens, connectedEns?.chainId);*/} - {/* }}*/} - {/* >*/} - {/* View Full Profile*/} - {/* */} - {/*
*/} - {/* Profile */} = ({ fullSubname = '', chainId = 1, @@ -61,12 +68,16 @@ const ContentSection: React.FC = ({ const { accountEnsNames } = useAccountEnsNames(); const [tab, setTab] = React.useState('Main'); const { openEnsProfile } = useJustWeb3(); - + // const { offchainResolvers } = useOffchainResolvers() const isProfileSelf = useMemo(() => { - const isEns = accountEnsNames?.map((ens) => ens.ens).includes(fullSubname); + const tempEns = accountEnsNames + ?.map((ens) => ens.ens) + .find((e) => e === fullSubname); - if (isEns) { - return chainId === connectedWalletChainId; + if (tempEns) { + if (!accountSubnames?.find((subname) => subname.ens === tempEns)) { + return chainId === connectedWalletChainId; + } } return ( @@ -187,13 +198,33 @@ const ContentSection: React.FC = ({ title={'Addresses'} items={sanitized?.allAddresses?.map((address) => { return ( - + + + +
+ +
+
+ +

+ {address.symbol.toUpperCase()}: {address.value} +

+
+
+
); })} /> diff --git a/packages/@justweb3/widget/src/lib/dialogs/ProfileDialog/index.tsx b/packages/@justweb3/widget/src/lib/dialogs/ProfileDialog/index.tsx index beedc810..15146d32 100644 --- a/packages/@justweb3/widget/src/lib/dialogs/ProfileDialog/index.tsx +++ b/packages/@justweb3/widget/src/lib/dialogs/ProfileDialog/index.tsx @@ -350,6 +350,7 @@ export const ProfileDialog: FC = ({ contentStyle={{ width: '100%', height: '100%', + pointerEvents: 'auto', }} fullScreen={minimized} header={ diff --git a/packages/@justweb3/widget/src/lib/dialogs/SignInDialog/index.tsx b/packages/@justweb3/widget/src/lib/dialogs/SignInDialog/index.tsx index 484abef9..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 { @@ -64,6 +65,7 @@ export interface SignInDialogProps { logo?: string; disableOverlay?: boolean; dev?: boolean; + local?: boolean; } export const SignInDialog: FC = ({ @@ -73,6 +75,7 @@ export const SignInDialog: FC = ({ logo, disableOverlay, dev = false, + local, }) => { const { chainId, ensDomains } = useJustaName(); const { isConnected, address } = useMountedAccount(); @@ -152,7 +155,9 @@ export const SignInDialog: FC = ({ : isAddSubnamePendingTestnet; }, [chainId, isAddSubnamePendingMainnet, isAddSubnamePendingTestnet]); - const { signIn } = useEnsSignIn(); + const { signIn } = useEnsSignIn({ + local: local, + }); const { offchainResolvers, isOffchainResolversPending } = useOffchainResolvers(); 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 f91ca993..5e3bf1ad 100644 --- a/packages/@justweb3/widget/src/lib/providers/JustWeb3Provider/index.tsx +++ b/packages/@justweb3/widget/src/lib/providers/JustWeb3Provider/index.tsx @@ -18,6 +18,7 @@ import { UseEnsSignInResult, useEnsSignOut, UseEnsSignOutResult, + useMounted, useMountedAccount, useRecords, UseSubnameUpdateFunctionParams, @@ -46,6 +47,7 @@ export interface JustWeb3ContextProps { ) => Promise; handleJustWeb3Config: (config: JustWeb3ProviderConfig) => void; handleOpenEnsProfile: (ens: string, chainId?: ChainId) => void; + handleCloseEnsProfile: () => void; isSignInOpen: boolean; config: JustWeb3ProviderConfig; plugins: JustaPlugin[]; @@ -57,6 +59,7 @@ export const JustWeb3Context = createContext({ handleOpenSignInDialog: () => {}, handleUpdateRecords: async () => {}, handleOpenEnsProfile: () => {}, + handleCloseEnsProfile: () => {}, handleJustWeb3Config: () => {}, config: {}, plugins: [], @@ -128,6 +131,10 @@ export const JustWeb3Provider: FC = ({ setEnsOpen({ ens, chainId }); }; + const handleCloseEnsProfile = () => { + setEnsOpen(null); + }; + useEffect(() => { if (!updateRecord && updateRecordPromiseResolveRef.current) { updateRecordPromiseResolveRef.current(); @@ -182,6 +189,22 @@ export const JustWeb3Provider: FC = ({ } }; + const mounted = useMounted(); + useEffect(() => { + if (mounted) { + if (window !== undefined) { + handleJustWeb3Config({ + ...initialConfig, + config: { + domain: window?.location?.hostname, + origin: window?.location?.origin, + ...initialConfig.config, + }, + }); + } + } + }, [initialConfig, mounted]); + return ( @@ -195,6 +218,7 @@ export const JustWeb3Provider: FC = ({ handleUpdateRecords: handleUpdateRecords, handleJustWeb3Config, handleOpenEnsProfile, + handleCloseEnsProfile, }} > = ({ : true } handleOpenDialog={handleOpenSignInDialog} + enableAuth={config.enableAuth} /> = ({ logo={config.logo} disableOverlay={config.disableOverlay} dev={config.dev} + local={!config.enableAuth} /> void; connectedEns: UseEnsAuthReturn['connectedEns']; openEnsProfile: (ens: string, chainId?: ChainId) => void; + closeEnsProfile: () => void; updateRecords: ( records: Omit & { ens?: string } ) => Promise; @@ -269,8 +296,13 @@ export interface useJustWeb3 { export const useJustWeb3 = (): useJustWeb3 => { const context = useContext(JustWeb3Context); const justanameContext = useContext(JustaNameContext); - const { signIn, isSignInPending } = useEnsSignIn(); - const { signOut, isSignOutPending } = useEnsSignOut(); + + const { signIn, isSignInPending } = useEnsSignIn({ + local: !context.config.enableAuth, + }); + const { signOut, isSignOutPending } = useEnsSignOut({ + local: !context.config.enableAuth, + }); const { connectedEns, isLoggedIn, @@ -278,8 +310,11 @@ export const useJustWeb3 = (): useJustWeb3 => { isEnsAuthLoading, isEnsAuthFetching, refreshEnsAuth, - } = useEnsAuth(); - const { handleUpdateRecords, handleOpenEnsProfile } = context; + } = useEnsAuth({ + local: !context.config.enableAuth, + }); + const { handleUpdateRecords, handleOpenEnsProfile, handleCloseEnsProfile } = + context; const handleUpdateRecordsInternal = async ( records: Omit & { ens?: string; @@ -336,6 +371,7 @@ export const useJustWeb3 = (): useJustWeb3 => { connectedEns, refreshEnsAuth, openEnsProfile: handleOpenEnsProfile, + closeEnsProfile: handleCloseEnsProfile, chainId: justanameContext?.chainId, }; }; @@ -343,10 +379,16 @@ export const useJustWeb3 = (): useJustWeb3 => { const CheckSession: FC<{ openOnWalletConnect: boolean; handleOpenDialog: (open: boolean) => void; -}> = ({ openOnWalletConnect, handleOpenDialog }) => { - const { connectedEns, isEnsAuthPending } = useEnsAuth(); + enableAuth?: boolean; +}> = ({ openOnWalletConnect, handleOpenDialog, enableAuth }) => { + const { connectedEns, isEnsAuthPending } = useEnsAuth({ + local: !enableAuth, + }); const { getRecords } = useRecords(); - const { signOut } = useEnsSignOut(); + const { signOut } = useEnsSignOut({ + local: !enableAuth, + }); + const { address, isConnected: isConnectedAccount, @@ -361,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/widget/src/lib/providers/MAppProvider/index.tsx b/packages/@justweb3/widget/src/lib/providers/MAppProvider/index.tsx index 383b3512..56cac1bb 100644 --- a/packages/@justweb3/widget/src/lib/providers/MAppProvider/index.tsx +++ b/packages/@justweb3/widget/src/lib/providers/MAppProvider/index.tsx @@ -33,6 +33,7 @@ export interface MAppContextProps { handleOpenAuthorizeMAppDialog: (mAppName: string, open?: boolean) => void; handleOpenRevokeMAppDialog: (mAppName: string, open?: boolean) => void; handleOpenSignInDialog: (open: boolean) => void; + config: JustWeb3ProviderConfig; } export const MAppContext = createContext({ @@ -42,6 +43,7 @@ export const MAppContext = createContext({ handleOpenAuthorizeMAppDialog: () => {}, handleOpenRevokeMAppDialog: () => {}, handleOpenSignInDialog: () => {}, + config: {}, }); interface MAppsProviderProps { @@ -66,7 +68,9 @@ export const MAppsProvider: FC = ({ plugins, config, }) => { - const { isEnsAuthPending, isLoggedIn, connectedEns } = useEnsAuth(); + const { isEnsAuthPending, isLoggedIn, connectedEns } = useEnsAuth({ + local: !config.enableAuth, + }); const [mAppsToEnableOpen, setMAppsToEnableOpen] = useState< { name: string; isOpen: boolean }[] | undefined >(undefined); @@ -159,6 +163,7 @@ export const MAppsProvider: FC = ({ handleOpenRevokeMAppDialog, canEnableMApps, handleOpenSignInDialog, + config, }} > { handleOpenAuthorizeMAppDialog: contextOpenAuthorizeMAppDialog, handleOpenRevokeMAppDialog: contextOpenRevokeMAppDialog, handleOpenSignInDialog, + config, } = useContext(MAppContext); - - const { connectedEns } = useEnsAuth(); + const { connectedEns } = useEnsAuth({ + local: !config.enableAuth, + }); const { isMAppEnabled, isMAppEnabledPending } = useIsMAppEnabled({ ens: connectedEns?.ens || '', mApp, diff --git a/packages/@justweb3/widget/src/lib/providers/PluginProvider/index.tsx b/packages/@justweb3/widget/src/lib/providers/PluginProvider/index.tsx index b6a4e822..5e4d8743 100644 --- a/packages/@justweb3/widget/src/lib/providers/PluginProvider/index.tsx +++ b/packages/@justweb3/widget/src/lib/providers/PluginProvider/index.tsx @@ -49,7 +49,9 @@ export const PluginProvider: FC = ({ mApps, config, }) => { - const { connectedEns, isEnsAuthPending, isLoggedIn } = useEnsAuth(); + const { connectedEns, isEnsAuthPending, isLoggedIn } = useEnsAuth({ + local: !config.enableAuth, + }); const { records } = useRecords({ ens: connectedEns?.ens || '', }); diff --git a/packages/@justweb3/widget/src/lib/types/config/index.ts b/packages/@justweb3/widget/src/lib/types/config/index.ts index 89ce259c..b62a6b59 100644 --- a/packages/@justweb3/widget/src/lib/types/config/index.ts +++ b/packages/@justweb3/widget/src/lib/types/config/index.ts @@ -9,6 +9,7 @@ export interface JustWeb3ProviderConfig allowedEns?: 'all' | 'claimable' | string[]; logo?: string; disableOverlay?: boolean; + enableAuth?: boolean; mApps?: (string | { name: string; openOnConnect: boolean })[]; plugins?: JustaPlugin[]; } 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 940554e4..297516f8 100644 --- a/packages/@justweb3/xmtp-plugin/.storybook/preview.tsx +++ b/packages/@justweb3/xmtp-plugin/.storybook/preview.tsx @@ -1,3 +1,11 @@ import './polyfills'; +// import { scan } from 'react-scan'; + +// 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/Chat/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/Chat/index.tsx deleted file mode 100644 index dffcbedf..00000000 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/Chat/index.tsx +++ /dev/null @@ -1,539 +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, - useClient, - useConsent, - useMessages, - useStreamMessages, -} from '@xmtp/react-sdk'; -import React, { useEffect, useMemo } from 'react'; -import { useSendReactionMessage } from '../../hooks'; -import { - filterReactionsMessages, - MessageWithReaction, -} from '../../utils/filterReactionsMessages'; -import { formatAddress } from '../../utils/formatAddress'; -import { groupMessagesByDate } from '../../utils/groupMessageByDate'; -import MessageCard from '../MessageCard'; -import EmojiSelector from '../EmojiSelector'; -import MessageTextField from '../MessageTextField'; -import { MessageSkeletonCard } from '../MessageSkeletonCard'; - -export interface ChatProps { - conversation: CachedConversation; - onBack: () => void; -} - -export const Chat: React.FC = ({ conversation, onBack }) => { - const [replyMessage, setReplyMessage] = - React.useState(null); - const [reactionMessage, setReactionMessage] = - React.useState(null); - const [emojiSelectorTop, setEmojiSelectorTop] = React.useState(0); - 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 { client } = useClient(); - - 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(); - // TODO: check if this is needed - // onRequestAllowed(); - 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]); - - return ( - -
{ - setReactionMessage(null); - const replica = document.getElementById( - `${reactionMessage?.id}-replica` - ); - replica?.remove(); - }} - >
- - - - - - - - -

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

- {/* {(!!primaryName) && ( -

{formatAddress(conversation.peerAddress)}

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

- Block -

- -
-
-
-
-
-
- - {isCanMessageLoading || isLoading ? ( - - {[...Array(5)].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.top = element?.offsetTop + 'px'; - replica.style.top = '100px'; - replica.style.zIndex = '90'; - element?.parentElement?.appendChild(replica); - replica.classList.add('replica-animate'); - setEmojiSelectorTop( - 100 + element.getBoundingClientRect().height - ); - }} - /> - ))} -
- ))} - {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} - /> -
- )} -
-
- ); -}; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatButton/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatButton/index.tsx deleted file mode 100644 index df728b2d..00000000 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatButton/index.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { ArrowIcon, ClickableItem } from '@justweb3/ui'; -import { useClient } from '@xmtp/react-sdk'; -import { useWalletClient } from 'wagmi'; - -export interface ChatButtonProps { - handleOpen: (open: boolean) => void; -} -export const ChatButton: React.FC = ({ handleOpen }) => { - const { initialize } = useClient(); - const { client } = useClient(); - const { data: walletClient } = useWalletClient(); - - const handleChat = async () => { - if (!client) { - initialize({ - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - signer: walletClient, - options: { - env: 'dev', - }, - }).then(() => { - handleOpen(true); - }); - } else { - handleOpen(true); - } - }; - - return ( - - - - } - style={{ - width: '100%', - }} - onClick={handleChat} - right={} - /> - ); -}; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatList/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatList/index.tsx deleted file mode 100644 index 676a6753..00000000 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatList/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { CachedConversation, ContentTypeMetadata } from '@xmtp/react-sdk'; -import { Flex } from '@justweb3/ui'; -import React from 'react'; -import { MessageItem } from '../MessageItem'; - -export interface ChatListProps { - conversations: CachedConversation[]; - handleOpenChat: ( - conversation: CachedConversation | null - ) => void; -} - -export const ChatList: React.FC = ({ - conversations, - handleOpenChat, -}) => { - return ( - - {conversations.map((conversation) => ( - handleOpenChat(conversation)} - key={conversation.topic} - /> - ))} - - ); -}; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/ChatMenuButton/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatMenuButton/index.tsx new file mode 100644 index 00000000..6e209868 --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatMenuButton/index.tsx @@ -0,0 +1,120 @@ +import { useMountedAccount } from '@justaname.id/react'; +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 ChatMenuButtonProps { + handleOpen: (open: boolean) => void; + env: XmtpEnvironment; +} +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(); + 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 ( + + + + } + style={{ + width: '100%', + }} + onClick={handleChat} + 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/ChatSheet/Chat/ChatMessagesList/MessageCard/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatMessagesList/MessageCard/index.tsx new file mode 100644 index 00000000..ae0491c4 --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatMessagesList/MessageCard/index.tsx @@ -0,0 +1,601 @@ +import { useMountedAccount, usePrimaryName } from '@justaname.id/react'; +import { + DocumentIcon, + DownloadIcon, + Flex, + P, + ReactionIcon, + ReplyIcon, +} 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 VoiceNotePreview from '../../VoiceNotePreview'; +import { VideoPlayerPreview } from '../../VideoPlayerPreview'; + +interface MessageCardProps { + message: MessageWithReaction; + conversation: CachedConversation; + peerAddress: string; + // onReply: (message: DecodedMessage) => void; + // onReaction: (message: DecodedMessage) => void; + onReply: (message: MessageWithReaction) => void; + onReaction: (message: MessageWithReaction) => void; +} + +const MeasureAndHyphenateText: React.FC<{ + text: string; + maxWidth: number; + isReceiver: boolean; +}> = ({ text, maxWidth, isReceiver }) => { + const [processedText, setProcessedText] = useState(''); + + useEffect(() => { + // Function to measure text width + const measureText = (text = '', font = '10px Inter') => { + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + if (!context) return 0; + context.font = font; + return context.measureText(text).width; + }; + + // Function to insert hyphens + const insertHyphens = (text: string) => { + const words = text.split(' '); + let currentLine = ''; + let finalText = ''; + + words.forEach((word) => { + const testLine = currentLine + word + ' '; + const testLineWidth = measureText(testLine); + + if (testLineWidth > maxWidth && currentLine !== '') { + // Check if it's necessary to hyphenate the current word + let hyphenated = false; + for (let i = word.length; i > 0; i--) { + const part = word.substring(0, i); + const testPartWidth = measureText(currentLine + part + '-'); + + if (testPartWidth <= maxWidth) { + finalText += currentLine + part + '-\n'; + currentLine = word.substring(i) + ' '; + hyphenated = true; + break; + } + } + + if (!hyphenated) { + finalText += currentLine + '\n'; + currentLine = word + ' '; + } + } else { + currentLine = testLine; + } + }); + + finalText += currentLine; + return finalText; + }; + + // Process the text + const processed = insertHyphens(text); + setProcessedText(processed); + }, [text, maxWidth]); + + return ( +
+      {processedText}
+    
+ ); +}; + +export const MessageCard: React.FC = ({ + message, + peerAddress, + onReply, + conversation, + onReaction, +}) => { + const { address } = useMountedAccount(); + const [hovered, setHovered] = useState(false); + const [repliedMessage, setRepliedMessage] = + React.useState(null); + const divRef = useRef(null); + const { mutateAsync: sendReaction } = useSendReactionMessage(conversation); + + const { primaryName } = usePrimaryName({ + address: peerAddress as `0x${string}`, + }); + + const isText = useMemo(() => { + return typeof message.content === 'string'; + }, [message.content]); + + useEffect(() => { + function handleMouseEnter() { + setHovered(true); + } + function handleMouseLeave() { + setHovered(false); + } + const divElement = divRef.current; + if (divElement) { + divElement.addEventListener('mouseenter', handleMouseEnter); + divElement.addEventListener('mouseleave', handleMouseLeave); + } + return () => { + if (divElement) { + divElement.removeEventListener('mouseenter', handleMouseEnter); + divElement.removeEventListener('mouseleave', handleMouseLeave); + } + }; + }, []); + + const attachmentExtention = useMemo(() => { + if (!isText && message.content.mimeType) + return message.content.mimeType.split('/')?.[1] || ''; + }, [isText, message.content.mimeType]); + + const isImage = message.content && message.content.data; + const isReply = + message.content && message.contentType === 'xmtp.org/reply:1.0'; + const isReceiver = message.senderAddress === peerAddress; + const isVoice = message.content.mimeType === 'audio/wav'; + + const getMessageDataById = (messageId: string) => { + const messageElement = document.getElementById(messageId); + if (!messageElement) { + return null; + } + + const messageDataString = messageElement.getAttribute('data-message'); + if (!messageDataString) { + return null; + } + try { + const messageData = JSON.parse(messageDataString); + return messageData; + } catch (e) { + console.error('Failed to parse message data:', e); + return null; + } + }; + + const handleEmojiSelect = (emoji: string, action: 'added' | 'removed') => { + sendReaction({ + action: action, + content: emoji, + referenceId: message.id, + }); + }; + + const navigateToRepliedMessage = () => { + if (!repliedMessage) return; + const element = document.getElementById(repliedMessage.id); + if (element) { + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }; + + const isReplyVoice = useMemo(() => { + if (!repliedMessage) return false; + return repliedMessage.content.mimeType === 'audio/wav'; + }, [repliedMessage]); + + const isReplyText = useMemo(() => { + if (!repliedMessage) return false; + return typeof repliedMessage.content === 'string'; + }, [repliedMessage]); + + const isReplyReply = useMemo(() => { + if (!repliedMessage) return false; + return !!repliedMessage.content.reference; + }, [repliedMessage]); + + const replyAttachmentExtention = useMemo(() => { + if (!isReplyText && !!repliedMessage && !isReplyReply) + return repliedMessage.content.mimeType.split('/')?.[1] || ''; + }, [isReplyReply, isReplyText, repliedMessage]); + + useEffect(() => { + if (!isReply || !!repliedMessage) return; + const repliedMsg = getMessageDataById(message.content.reference); + setRepliedMessage(repliedMsg); + }, [isReply, message.content.reference, repliedMessage]); + + return ( + + + + <> + {repliedMessage && isReply ? ( + + + + +

+ {repliedMessage?.senderAddress === address + ? 'YOU' + : primaryName ?? + formatAddress(repliedMessage?.senderAddress ?? '')} +

+ + {isReplyText || isReplyReply ? ( +

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

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

+ {repliedMessage.content.filename} +

+
+ )} +
+
+
+

+ {message.content.content} +

+
+
+
+ ) : isText ? ( + + + + ) : ( + + {isVoice ? ( + + ) : ( + + {typeLookup[attachmentExtention] === 'image' ? ( +
+ {message.content.filename} + + + +
+ ) : typeLookup[attachmentExtention] === 'video' ? ( + + ) : ( + + + +

+ {message.content.filename} +

+

+ {calculateFileSize( + message.content.data?.byteLength ?? 0 + )} +

+
+
+ )} +
+ )} +
+ )} + {message.reactionMessage && ( +

{ + if (!message.reactionMessage) return; + if (message.reactionMessage.senderAddress !== address) return; + handleEmojiSelect('', 'removed'); + }} + style={{ + position: 'absolute', + cursor: 'pointer', + bottom: '-0.5rem', + fontSize: '20px', + right: isReceiver ? '-12px' : 'auto', + left: isReceiver ? 'auto' : '-12px', + }} + > + {findEmojiByName(message.reactionMessage.content.content)} +

+ )} + +
+ + + onReply(message)} + /> + + {message.senderAddress !== address && ( + { + onReaction(message); + }} + /> + )} + +
+

+ {formatMessageSentTime(message.sentAt)} +

+
+ ); +}; + +export default MessageCard; 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 new file mode 100644 index 00000000..2aad68e1 --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatMessagesList/index.tsx @@ -0,0 +1,125 @@ +import { Flex, P } from '@justweb3/ui'; +import { CachedConversation, ContentTypeMetadata } from '@xmtp/react-sdk'; +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; + groupedMessages: { [date: string]: MessageWithReaction[] }; + conversation: CachedConversation; + 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, +}) => { + 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 ( + +

Cannot message {conversation.peerAddress}

+
+ ); + } + + return ( + + setRef(el)} + > + {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..90855359 --- /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..f116404f --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/ChatTextField/MessageInput/index.tsx @@ -0,0 +1,73 @@ +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 86% 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 index 0b18023f..f7855876 100644 --- 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 @@ -18,10 +18,10 @@ export const MessageSkeletonCard: React.FC = ({ isRece maxWidth: '170px', width: '170px', minHeight: '40px', + maxHeight: '40px', fontSize: '14px', display: 'flex', flexDirection: 'column', - padding: '10px', overflowWrap: 'break-word', borderRadius: '10px', border: '1px solid var(--justweb3-primary-color)', @@ -32,6 +32,14 @@ export const MessageSkeletonCard: React.FC = ({ isRece boxShadow: isReceiver ? 'shadow-[-1px_2px_0_0_#3]' : 'shadow-[1px_2px_0_0_#88C6F5]', }}> + + + ) } \ No newline at end of file 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 75% 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 7fe843ae..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 @@ -1,24 +1,23 @@ +import { Flex, P, PauseIcon, PlayIcon } from '@justweb3/ui'; import * as Slider from '@radix-ui/react-slider'; import { DecodedMessage } from '@xmtp/xmtp-js'; -import { Flex, PauseIcon, PlayIcon } from '@justweb3/ui'; import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { MessageWithReaction } from '../../utils/filterReactionsMessages'; -import useGetAudioDuration from '../../hooks/useGetAudioDuration'; +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; - isReply?: boolean; } -const VoiceMessageCard: React.FC = ({ +const VoiceNotePreview: React.FC = ({ message, style, disabled, isReceiver, - isReply, }) => { const [playing, setPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); @@ -116,7 +115,9 @@ const VoiceMessageCard: React.FC = ({ width="22" height="22" fill={ - isReceiver + disabled + ? 'var(--justweb3-primary-color)' + : isReceiver ? 'var(--justweb3-primary-color)' : 'var(--justweb3-foreground-color-4)' } @@ -156,7 +157,7 @@ const VoiceMessageCard: React.FC = ({ > = ({ > = ({ style={{ width: '6px', height: '5px', - backgroundColor: isReceiver + background: isReceiver ? 'var(--justweb3-primary-color)' : 'var(--justweb3-foreground-color-4)', borderRadius: '2.5px', @@ -191,27 +192,26 @@ const VoiceMessageCard: React.FC = ({ aria-label="Volume" /> - {/**/} - {/*

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

*/} - {/* {!isReply && (*/} - {/*

{formatMessageSentTime(message.sentAt)}

*/} - {/* )}*/} - {/*
*/} + +

+ {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..688c55de --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/Chat/index.tsx @@ -0,0 +1,251 @@ +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, +} 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, 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); + + useEffect(() => { + const lastMessage = messages[messages.length - 1]; + + if (!lastMessage || isReadReceiptSending) return; + + if (lastMessage?.contentType === ContentTypeReadReceipt.toString()) { + return; + } + + if (entries[conversation.peerAddress]?.permissionType === 'allowed') { + readReceipt(); + } + }, [ + messages, + readReceipt, + isReadReceiptSending, + entries, + conversation.peerAddress, + ]); + + // 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]); + + 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 = '100vh - 50px - 3rem - 1.5rem - 73px - 15px'; + const adjustments: string[] = []; + + if (isRequest) return 'calc(100vh - 50px - 3rem - 1.5rem - 15px - 34px)'; + 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'); + + return ` calc(${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/ChatSheet/NewChat/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/NewChat/index.tsx new file mode 100644 index 00000000..6d8e2df6 --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ChatSheet/NewChat/index.tsx @@ -0,0 +1,318 @@ +import { + CachedConversation, + toCachedConversation, + useCanMessage, + useClient, + useConsent, + useConversation, + useStartConversation, +} from '@xmtp/react-sdk'; +import React, { useEffect, useMemo } from 'react'; +import { useDebounce } from '@justweb3/widget'; +import { + useMountedAccount, + usePrimaryName, + useRecords, +} from '@justaname.id/react'; + +import { + ArrowIcon, + CloseIcon, + Flex, + Input, + LoadingSpinner, + P, + VerificationsIcon, +} from '@justweb3/ui'; +import { NewChatTextField } from './NewChatTextField'; + +interface NewChatProps { + onChatStarted: (conversation: CachedConversation) => void; + onBack: () => void; + selectedAddress?: string; +} + +export const NewChat: React.FC = ({ + onChatStarted, + onBack, + selectedAddress, +}) => { + // States + const [newAddress, setNewAddress] = React.useState( + selectedAddress ?? '' + ); + const [canMessage, setCanMessage] = React.useState(false); + //Queries + const { client } = useClient(); + const { startConversation } = useStartConversation(); + const { getCachedByPeerAddress } = useConversation(); + const { refreshConsentList, allow } = useConsent(); + const { address } = useMountedAccount(); + const { canMessage: xmtpCanMessage, isLoading } = useCanMessage(); + const { + debouncedValue: debouncedAddress, + isDebouncing: isDebouncingAddress, + } = useDebounce(newAddress, 500); + + const isAddressName = useMemo(() => { + const ethAddressRegex = /^0x[a-fA-F0-9]{40}$/; + return ( + !ethAddressRegex.test(debouncedAddress) && debouncedAddress.length > 0 + ); + }, [debouncedAddress]); + + const { records, isRecordsLoading, isRecordsFetching } = useRecords({ + ens: debouncedAddress, + enabled: isAddressName, + }); + + const { + primaryName: name, + isPrimaryNameLoading, + isPrimaryNameFetching, + } = usePrimaryName({ + address: debouncedAddress as `0x${string}`, + enabled: !isAddressName, + }); + + const resolvedAddress = useMemo(() => { + return records?.sanitizedRecords?.ethAddress?.value; + }, [records]); + + const handleCanMessage = async () => { + if (!client) return; + try { + if (isAddressName) { + if (resolvedAddress) { + const res = await xmtpCanMessage(resolvedAddress); + setCanMessage(res); + } else { + // Resolved address is not available yet; do nothing + return; + } + } else if (debouncedAddress.length === 42) { + const res = await xmtpCanMessage(debouncedAddress); + setCanMessage(res); + } else { + setCanMessage(false); + } + } catch (e) { + console.log('error', e); + setCanMessage(false); + } + }; + + const handleNewChat = async (message: string) => { + if (!client) return; + const peerAddress = + isAddressName && !!resolvedAddress ? resolvedAddress : debouncedAddress; + try { + const conv = await startConversation(peerAddress, message ?? {}); + if (!conv.cachedConversation) { + if (!conv.conversation) { + return; + } else { + const cachedConvo = toCachedConversation( + conv.conversation, + address ?? '' + ); + await allow([conv.conversation.peerAddress]); + await refreshConsentList(); + onChatStarted(cachedConvo); + onBack(); + } + } else { + await allow([conv.cachedConversation.peerAddress]); + await refreshConsentList(); + onChatStarted(conv.cachedConversation); + onBack(); + } + } catch (error) { + const e = error as Error; + console.log('error creating chat', e); + } + }; + + const checkIfConversationExists = async (peerAddress: string) => { + const convoExists = await getCachedByPeerAddress(peerAddress); + if (convoExists) { + onChatStarted(convoExists); + } + }; + + useEffect(() => { + if (debouncedAddress.length === 0) { + setCanMessage(false); + return; + } + if (isAddressName) { + if (!isRecordsLoading && resolvedAddress) { + handleCanMessage(); + } + } else { + if (!isLoading && !isPrimaryNameLoading) { + handleCanMessage(); + } + } + }, [ + debouncedAddress, + isLoading, + isPrimaryNameLoading, + isRecordsLoading, + resolvedAddress, + isAddressName, + ]); + + useEffect(() => { + const checkConversation = async () => { + if (canMessage) { + await checkIfConversationExists( + isAddressName && resolvedAddress ? resolvedAddress : debouncedAddress + ); + } + }; + + checkConversation(); + }, [ + canMessage, + resolvedAddress, + debouncedAddress, + checkIfConversationExists, + isAddressName, + ]); + + const isSearchLoading = useMemo(() => { + return ( + // (isAddressName && isPrimaryNameLoading && debouncedAddress.length > 4) || + // isLoading + isDebouncingAddress || isRecordsFetching || isPrimaryNameFetching + ); + }, [isDebouncingAddress, isRecordsFetching, isPrimaryNameFetching]); + // }, [isAddressName, isPrimaryNameLoading, debouncedAddress.length, isLoading]); + + useEffect(() => { + if (isRecordsLoading || isPrimaryNameLoading) { + return; + } + + if (name) { + setNewAddress(name); + return; + } + }, [name, isRecordsLoading, isPrimaryNameLoading]); + + return ( + + + + + + +

+ To +

+ {isSearchLoading ? ( + + ) : debouncedAddress.length > 0 ? ( + + ) : null} +
+ } + right={ + isSearchLoading ? ( + + ) : ( + + { + setNewAddress(''); + }} + > + + + + ) + } + placeholder={'ENS, Wallet Address...'} + onChange={(e) => setNewAddress(e.target.value)} + style={{ + flex: 1, + height: 22, + maxHeight: 22!, + gap: 5, + }} + /> +
+ + + + + ); +}; + +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 5a794bda..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,136 +1,55 @@ -import { - Sheet, - SheetContent, - SheetTitle, - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from '@justweb3/ui'; -import React, { useEffect, useMemo } from 'react'; -import { - CachedConversation, - ContentTypeMetadata, - useConsent, - useConversations, - useStreamAllMessages, - useStreamConversations, -} from '@xmtp/react-sdk'; -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 | null + // peerAddress?: string | null; + peer: CachedConversation | string | null; + openChat: boolean; + closeChat: () => void; + onChangePeer: ( + peer: CachedConversation | string ) => void; } export const ChatSheet: React.FC = ({ - open, - handleOpen, - handleOpenChat, + 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', - flex: '1', - }} - > - - - Chats - - - Requests - - - 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 32af76c1..00000000 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/CustomPlayer/index.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { Flex, PauseIcon, PlayIcon } from '@justweb3/ui'; -import React, { useEffect, useRef, useState } from 'react'; - - -export interface CustomPlayerProps { - style?: React.CSSProperties; - url?: string; - disabled?: boolean; -} - -export const CustomPlayer: React.FC = ({ - style, - url = '', - disabled -}) => { - 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 49e12de3..00000000 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/CustomVoicePreview/index.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { Flex, P, PauseIcon, PlayIcon, CloseIcon } from '@justweb3/ui'; -import { formatTime } from '../../utils/formatVoiceTime'; -import React, { useEffect, useRef, useState } from 'react'; -import useGetAudioDuration from '../../hooks/useGetAudioDuration'; - - -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 f39022f8..00000000 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/EmojiSelector/index.tsx +++ /dev/null @@ -1,80 +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/InboxSheet/ChatList/MessageItem/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/InboxSheet/ChatList/MessageItem/index.tsx new file mode 100644 index 00000000..f3640899 --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/InboxSheet/ChatList/MessageItem/index.tsx @@ -0,0 +1,234 @@ +import { + attachmentContentTypeConfig, + CachedConversation, + CachedMessage, + ContentTypeMetadata, + reactionContentTypeConfig, + replyContentTypeConfig, + useConsent, +} 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'; + +export interface MessageItemProps { + conversation: CachedConversation; + onClick?: () => void; + blocked?: boolean; + primaryName?: string | null; + conversationInfo?: { + conversationId: string; + unreadCount: number; + consent: 'allowed' | 'blocked' | 'requested'; + lastMessage: CachedMessage; + }; +} + +const MessageItem: React.FC = ({ + conversation, + onClick, + blocked, + primaryName, + conversationInfo, +}) => { + const { records } = useRecords({ + ens: primaryName || undefined, + }); + const { sanitizeEnsImage } = useEnsAvatar(); + + // const unreadMessages = useMemo(() => { + // if (!lastMessage) return false; + // return lastMessage.contentType !== ContentTypeReadReceipt.toString(); + // }, [lastMessage]); + + const { allow, deny } = useConsent(); + + const allowUser = async () => { + await allow([conversation.peerAddress]); + }; + + const ignoreUser = async () => { + await deny([conversation.peerAddress]); + }; + + const lastContent = useMemo(() => { + const lastMessage = conversationInfo?.lastMessage; + if (!lastMessage) return ''; + + if (typeof lastMessage.content === 'string') { + return lastMessage.content; + } + + if ( + attachmentContentTypeConfig.contentTypes.includes( + lastMessage?.contentType + ) + ) { + return lastMessage.content.filename; + } + + if ( + reactionContentTypeConfig.contentTypes.includes(lastMessage?.contentType) + ) { + return lastMessage.contentFallback; + } + + if ( + replyContentTypeConfig.contentTypes.includes(lastMessage?.contentType) + ) { + return 'replied "' + lastMessage.content.content + '"'; + } + + return lastMessage.contentFallback; + }, [conversationInfo, conversationInfo?.lastMessage]); + + return ( + { + if (blocked) return; + onClick && onClick(); + }} + > + + + +

+ {primaryName || formatText(conversation.peerAddress, 4)} +

+ + {conversationInfo?.lastMessage + ? conversationInfo?.lastMessage.senderAddress !== + conversation.peerAddress + ? 'You: ' + : '' + : ''} + {conversationInfo?.lastMessage + ? lastContent + ? lastContent + : 'No preview available' + : ''} + +
+ + + {conversationInfo?.consent !== 'allowed' ? ( + + + {conversationInfo?.consent === 'requested' && ( + + )} + + ) : ( +
+ + {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/InboxSheet/ChatList/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/InboxSheet/ChatList/index.tsx new file mode 100644 index 00000000..dd873405 --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/InboxSheet/ChatList/index.tsx @@ -0,0 +1,77 @@ +import { Flex } from '@justweb3/ui'; +import { + CachedConversation, + CachedMessage, + ContentTypeMetadata, +} from '@xmtp/react-sdk'; +import React from 'react'; +import { MessageItem } from './MessageItem'; +import { PrimaryNameRecord } from '@justaname.id/react'; + +export interface ChatListProps { + conversations: CachedConversation[]; + handleOpenChat: ( + conversation: CachedConversation + ) => void; + blockedList?: boolean; + primaryNames: PrimaryNameRecord | undefined; + conversationsInfo?: { + conversationId: string; + unreadCount: number; + consent: 'allowed' | 'blocked' | 'requested'; + lastMessage: CachedMessage; + }[]; +} + +export const ChatList: React.FC = ({ + conversations, + handleOpenChat, + blockedList, + conversationsInfo, + primaryNames, +}) => { + return ( + + {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 new file mode 100644 index 00000000..a8cdbf43 --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/InboxSheet/index.tsx @@ -0,0 +1,382 @@ +import { + AddIcon, + Flex, + Sheet, + SheetContent, + SheetTitle, + SPAN, + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from '@justweb3/ui'; +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; + 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: 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 { 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( + (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(() => { + const allowedConversationsTopic = allowedConversations.map( + (convo) => convo.topic + ); + + const blockedConversationsTopic = blockedConversations.map( + (convo) => convo.topic + ); + + const requestConversationsTopic = requestConversations.map( + (convo) => 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 ( + !isEqual(allowedConversationsTopic, allConversationsAllowedTopic) || + !isEqual(blockedConversationsTopic, allConversationsBlockedTopic) || + !isEqual(requestConversationsTopic, allConversationsRequestedTopic) + ) { + onConversationsUpdated({ + allowed: allowedConversations, + blocked: blockedConversations, + requested: requestConversations, + }); + } + }, [ + allConversations, + allowedConversations, + blockedConversations, + onConversationsUpdated, + requestConversations, + ]); + + useEffect(() => { + if (!isConsentListLoading) return; + loadConsentList().then(() => { + setIsConsentListLoading(false); + }); + }, [loadConsentList, isConsentListLoading]); + + useStreamConversations(); + useStreamAllMessages(); + useStreamConsentList(); + + return ( + + + 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... + +
+ ) : ( + <> + + + + + + + + + + + )} +
+ + + + POWERED BY + + + + + + + + + + + + + + + + +
+
+
+ ); +}; 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/MessageCard/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/MessageCard/index.tsx deleted file mode 100644 index f63cce4d..00000000 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/MessageCard/index.tsx +++ /dev/null @@ -1,448 +0,0 @@ -import { useEffect, useMemo, useRef } from "react"; -import { CachedConversation, DecodedMessage } from "@xmtp/react-sdk"; -import { MessageWithReaction } from "../../utils/filterReactionsMessages"; -import { useState } from "react"; -import { useMountedAccount } from "@justaname.id/react"; -import React from "react"; -import { useSendReactionMessage } from "../../hooks"; -import { typeLookup } from "../../utils/attachments"; -import { findEmojiByName } from "../../utils/emojis"; -import { formatMessageSentTime } from "../../utils/messageTimeFormat"; -import { DocumentIcon, Flex, P, ReplyIcon, ReactionIcon, DownloadIcon } from "@justweb3/ui"; -import { formatAddress } from "../../utils/formatAddress"; -import { calculateFileSize } from "../../utils/calculateFileSize"; -import VoiceMessageCard from "../VoiceMessageCard"; -import { CustomPlayer } from "../CustomPlayer"; - - -interface MessageCardProps { - message: MessageWithReaction; - conversation: CachedConversation; - peerAddress: string; - // onReply: (message: DecodedMessage) => void; - // onReaction: (message: DecodedMessage) => void; - onReply: (message: MessageWithReaction) => void; - onReaction: (message: MessageWithReaction) => void; -} - -const MeasureAndHyphenateText: React.FC<{ text: string; maxWidth: number, isReceiver: boolean }> = ({ text, maxWidth, isReceiver }) => { - const [processedText, setProcessedText] = useState(''); - - useEffect(() => { - // Function to measure text width - const measureText = (text = '', font = '10px Inter') => { - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - if (!context) return 0; - context.font = font; - return context.measureText(text).width; - }; - - // Function to insert hyphens - const insertHyphens = (text: string) => { - const words = text.split(' '); - let currentLine = ''; - let finalText = ''; - - words.forEach((word) => { - const testLine = currentLine + word + ' '; - const testLineWidth = measureText(testLine); - - if (testLineWidth > maxWidth && currentLine !== '') { - // Check if it's necessary to hyphenate the current word - let hyphenated = false; - for (let i = word.length; i > 0; i--) { - const part = word.substring(0, i); - const testPartWidth = measureText(currentLine + part + '-'); - - if (testPartWidth <= maxWidth) { - finalText += currentLine + part + '-\n'; - currentLine = word.substring(i) + ' '; - hyphenated = true; - break; - } - } - - if (!hyphenated) { - finalText += currentLine + '\n'; - currentLine = word + ' '; - } - } else { - currentLine = testLine; - } - }); - - finalText += currentLine; - return finalText; - }; - - // Process the text - const processed = insertHyphens(text); - setProcessedText(processed); - }, [text, maxWidth]); - - return ( -
-            {processedText}
-        
- ); -}; - -const MessageCard: React.FC = ({ - message, - peerAddress, - onReply, - conversation, - onReaction -}) => { - const { address } = useMountedAccount(); - const [hovered, setHovered] = useState(false) - const [repliedMessage, setRepliedMessage] = React.useState(null); - const divRef = useRef(null); - const { mutateAsync: sendReaction } = useSendReactionMessage(conversation); - - const isText = useMemo(() => { - return typeof message.content === "string" - }, [message.content]) - - - useEffect(() => { - function handleMouseEnter() { - setHovered(true) - } - function handleMouseLeave() { - setHovered(false) - } - const divElement = divRef.current; - if (divElement) { - divElement.addEventListener('mouseenter', handleMouseEnter); - divElement.addEventListener('mouseleave', handleMouseLeave); - } - return () => { - if (divElement) { - divElement.removeEventListener('mouseenter', handleMouseEnter); - divElement.removeEventListener('mouseleave', handleMouseLeave); - } - }; - }, []); - - - const attachmentExtention = useMemo(() => { - if (!isText && message.content.mimeType) - return message.content.mimeType.split("/")?.[1] || ""; - }, [isText, message.content.mimeType]); - - - const isImage = message.content && message.content.data; - const isReply = message.content && message.contentType === "xmtp.org/reply:1.0" - const isReceiver = message.senderAddress === peerAddress; - const isVoice = message.content.mimeType === "audio/wav"; - - - const getMessageDataById = (messageId: string) => { - const messageElement = document.getElementById(messageId); - if (!messageElement) { - return null; - } - - const messageDataString = messageElement.getAttribute('data-message'); - if (!messageDataString) { - return null; - } - try { - const messageData = JSON.parse(messageDataString); - return messageData; - } catch (e) { - console.error('Failed to parse message data:', e); - return null; - } - } - - const handleEmojiSelect = (emoji: string, action: "added" | "removed") => { - sendReaction({ - action: action, - content: emoji, - referenceId: message.id, - }); - } - - const navigateToRepliedMessage = () => { - if (!repliedMessage) return; - const element = document.getElementById(repliedMessage.id); - if (element) { - element.scrollIntoView({ behavior: "smooth", block: "center" }); - } - } - - - const isReplyVoice = useMemo(() => { - if (!repliedMessage) return false; - return repliedMessage.content.mimeType === "audio/wav"; - }, [repliedMessage]); - - const isReplyText = useMemo(() => { - if (!repliedMessage) return false; - return typeof repliedMessage.content === "string" - }, [repliedMessage]) - - const isReplyReply = useMemo(() => { - if (!repliedMessage) return false; - return !!repliedMessage.content.reference - }, [repliedMessage]) - - const replyAttachmentExtention = useMemo(() => { - if (!isReplyText && !!repliedMessage && !isReplyReply) - return repliedMessage.content.mimeType.split("/")?.[1] || ""; - }, [isReplyText, repliedMessage]); - - useEffect(() => { - if (!isReply || !!repliedMessage) return; - const repliedMsg = getMessageDataById(message.content.reference) - setRepliedMessage(repliedMsg); - }, [isReply, message.content.reference, repliedMessage]) - - return ( - - - - <> - { - repliedMessage && isReply ? - - - - -

{repliedMessage?.senderAddress === address ? "YOU" : formatAddress(repliedMessage?.senderAddress ?? "")}

- - {(isReplyText || isReplyReply) ? ( -

{isReplyReply ? repliedMessage.content.content : repliedMessage.content}

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

{repliedMessage.content.filename}

-
- )} -
-
-
-

{message.content.content}

-
-
-
- : - isText ? - - - - : - - {isVoice ? - - : - - {typeLookup[attachmentExtention] === "image" ? - {message.content.filename} - : - typeLookup[attachmentExtention] === "video" ? - - : - - - -

{message.content.filename}

-

{calculateFileSize(message.content.data?.byteLength ?? 0)}

-
-
} - - -
} -
- } - {message.reactionMessage && ( -

{ - if (!message.reactionMessage) return; - if (message.reactionMessage.senderAddress !== address) return; - handleEmojiSelect("", "removed") - }} - style={{ - position: 'absolute', - cursor: 'pointer', - bottom: '-0.5rem', - fontSize: '20px', - right: isReceiver ? '-12px' : 'auto', - left: isReceiver ? 'auto' : '-12px', - }} - >{findEmojiByName(message.reactionMessage.content.content)}

- )} - - {!isVoice && ( -

{formatMessageSentTime(message.sentAt)}

)} -
- - - onReply(message)} /> - - {message.senderAddress !== address && - { - onReaction(message) - } - } /> - } - - -
- -
- ); -}; - -export default MessageCard; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/components/MessageItem/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/MessageItem/index.tsx deleted file mode 100644 index 80ee2e9b..00000000 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/MessageItem/index.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { - attachmentContentTypeConfig, - CachedConversation, - ContentTypeMetadata, - reactionContentTypeConfig, - replyContentTypeConfig, - useLastMessage, - useStreamMessages, -} from '@xmtp/react-sdk'; -import { useEnsAvatar, usePrimaryName, useRecords } from '@justaname.id/react'; -import { Avatar, Flex, formatText, P, SPAN } from '@justweb3/ui'; -import React, { useMemo } from 'react'; -import { formatChatDate } from '../../utils/formatChatDate'; - -export interface MessageItemProps { - conversation: CachedConversation; - onClick?: () => void; -} - -export const MessageItem: React.FC = ({ - conversation, - onClick, -}) => { - const lastMessage = useLastMessage(conversation.topic); - useStreamMessages(conversation); - const { primaryName } = usePrimaryName({ - address: conversation.peerAddress as `0x${string}`, - }); - const { records } = useRecords({ - ens: primaryName, - }); - const { sanitizeEnsImage } = useEnsAvatar(); - - const lastContent = useMemo(() => { - if (!lastMessage) return ''; - - if (typeof lastMessage.content === 'string') { - return lastMessage.content; - } - - if ( - attachmentContentTypeConfig.contentTypes.includes( - lastMessage?.contentType - ) - ) { - return lastMessage.content.filename; - } - - if ( - reactionContentTypeConfig.contentTypes.includes(lastMessage?.contentType) - ) { - return lastMessage.contentFallback; - } - - if ( - replyContentTypeConfig.contentTypes.includes(lastMessage?.contentType) - ) { - return lastMessage.contentFallback; - } - - return lastMessage.contentFallback; - }, [lastMessage]); - - return ( - - - - -

- {primaryName || formatText(conversation.peerAddress, 4)} -

- - {lastMessage - ? lastMessage.senderAddress !== conversation.peerAddress - ? 'You: ' - : '' - : ''} - {lastMessage - ? lastContent - ? lastContent - : 'No preview available' - : ''} - -
- - - - {lastMessage?.sentAt ? formatChatDate(lastMessage.sentAt) : ''} - - -
- ); -}; 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 4026a6af..00000000 --- a/packages/@justweb3/xmtp-plugin/src/lib/components/MessageTextField/index.tsx +++ /dev/null @@ -1,461 +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 } from '@justaname.id/react'; -import { useAttachmentChange, useRecordingTimer, useRecordVoice, useSendAttachment, useSendMessages, useSendReplyMessage } from '../../hooks'; -import { AttachmentType, typeLookup } from '../../utils/attachments'; -import { Button, CloseIcon, Flex, Input, LoadingSpinner, P, AddImageIcon, AddVideoIcon, AddFolderIcon, DocumentIcon, SendIcon, StopIcon, MicIcon } 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; -} - -const MessageTextField: React.FC = ({ - newConvo, - disabled, - replyMessage, - onCancelReply, - conversation, - onNewConvo -}) => { - const [messageValue, setMessageValue] = React.useState(""); - const [attachment, setAttachment] = React.useState(); - const [attachmentPreview, setAttachmentPreview] = React.useState(); - 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]); - - - // 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" : 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" ? - - : - - {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/ProfileChatButton/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/components/ProfileChatButton/index.tsx new file mode 100644 index 00000000..902cb71f --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/components/ProfileChatButton/index.tsx @@ -0,0 +1,136 @@ +import { useJustWeb3XMTP } from '../../providers/JustWeb3XMTPProvider'; +import { useMountedAccount, useRecords } from '@justaname.id/react'; +import { ClientOptions, useCanMessage, useClient } from '@xmtp/react-sdk'; +import { useEffect, useState } from 'react'; +import { useJustWeb3 } from '@justweb3/widget'; +import { Client } from '@xmtp/xmtp-js'; +import { useEthersSigner } from '../../hooks'; +import { loadKeys, storeKeys, wipeKeys } from '../../utils/xmtp'; +import { ChainId } from '@justaname.id/sdk'; + +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(); + const { records } = useRecords({ + ens, + chainId: chainId, + }); + const [canMessageAddress, setCanMessageAddress] = useState( + null + ); + + const { canMessage } = useCanMessage(); + + const { initialize } = useClient(); + const { client } = useClient(); + const walletClient = useEthersSigner(); + const { address } = useMountedAccount(); + + const handleChat = async () => { + if (!records?.sanitizedRecords?.ethAddress?.value) { + return; + } + + if (!client) { + const signer = await 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(() => { + handleOpenChat(ens); + }); + + // handleClient(client) + } catch (error) { + console.error('Failed to initialize XMTP Client:', error); + wipeKeys(address ?? '', env); + } + } else { + handleOpenChat(ens); + } + }; + + useEffect(() => { + if (canMessageAddress === null) { + if (records) { + if (records?.sanitizedRecords?.ethAddress) { + Client.canMessage(records?.sanitizedRecords?.ethAddress?.value, { + env: env, + }).then((canMessage) => { + setCanMessageAddress(canMessage); + }); + } + } + } + }, [canMessage, canMessageAddress, env, records]); + + if (profileAddress === address) { + return null; + } + + return ( +
+ { + if (!canMessageAddress) { + return; + } + closeEnsProfile(); + if (records?.sanitizedRecords?.ethAddress?.value) { + handleChat(); + } + }} + > + + +
+ ); +}; 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..29542327 --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/content-types/readReceipt.ts @@ -0,0 +1,122 @@ +// taken from the xmtp web repository + +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/index.ts b/packages/@justweb3/xmtp-plugin/src/lib/hooks/index.ts index f61e9449..0a628dc4 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/hooks/index.ts +++ b/packages/@justweb3/xmtp-plugin/src/lib/hooks/index.ts @@ -1,8 +1,9 @@ -export * from './sendAttachment'; -export * from './sendMessage'; -export * from './sendReactionMessage'; -export * from './sendReplyMessage'; +export * from './useSendAttachment'; +export * from './useSendMessage'; +export * from './useSendReactionMessage'; +export * from './useSendReplyMessage'; export * from './useAttachmentChange'; export * from './useDebounced'; export * from './useRecordingTimer'; +export * from './useEthersSigner'; export * from './useVoiceRecording'; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/hooks/sendMessage/index.ts b/packages/@justweb3/xmtp-plugin/src/lib/hooks/sendMessage/index.ts deleted file mode 100644 index 4a89f1d9..00000000 --- a/packages/@justweb3/xmtp-plugin/src/lib/hooks/sendMessage/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { useMutation } from '@tanstack/react-query' -import { ContentTypeId } from '@xmtp/content-type-primitives' -import { - CachedConversation, - useSendMessage, - DecodedMessage, - SendOptions, -} from '@xmtp/react-sdk' - -export const sendMessages = async ( - conversation: CachedConversation, - message: string, - sendMessage: ( - conversation: CachedConversation, - content: T, - contentType?: ContentTypeId, - sendOptions?: Omit, - ) => Promise | undefined>, - contentType?: SendOptions, -) => { - await sendMessage(conversation, message, undefined, contentType) -} - -export const useSendMessages = (conversation?: CachedConversation) => { - const { sendMessage } = useSendMessage() - - 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/hooks/useEthersSigner/index.ts b/packages/@justweb3/xmtp-plugin/src/lib/hooks/useEthersSigner/index.ts new file mode 100644 index 00000000..ce6ac392 --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/hooks/useEthersSigner/index.ts @@ -0,0 +1,29 @@ +import { BrowserProvider, JsonRpcSigner } from 'ethers'; +import { useMemo } from 'react'; +import type { Account, Chain, Client, Transport } from 'viem'; +import { useWalletClient } from 'wagmi'; + +export function clientToSigner(client: Client) { + const { account, chain, transport } = client; + if (!account || !chain || !transport) return undefined; + const network = { + chainId: chain?.id, + name: chain?.name, + ensAddress: chain?.contracts?.ensRegistry?.address, + }; + const provider = new BrowserProvider(transport, network); + const signer = new JsonRpcSigner(provider, account.address); + return signer; +} + +/** Hook to convert a viem Wallet Client to an ethers.js Signer. */ +export const useEthersSigner = ({ chainId }: { chainId?: number } = {}): + | JsonRpcSigner + | undefined => { + const { data: client } = useWalletClient({ chainId }); + const signer = useMemo( + () => (client ? clientToSigner(client) : undefined), + [client] + ); + return signer; +}; 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/sendAttachment/index.ts b/packages/@justweb3/xmtp-plugin/src/lib/hooks/useSendAttachment/index.ts similarity index 100% rename from packages/@justweb3/xmtp-plugin/src/lib/hooks/sendAttachment/index.ts rename to packages/@justweb3/xmtp-plugin/src/lib/hooks/useSendAttachment/index.ts diff --git a/packages/@justweb3/xmtp-plugin/src/lib/hooks/useSendMessage/index.ts b/packages/@justweb3/xmtp-plugin/src/lib/hooks/useSendMessage/index.ts new file mode 100644 index 00000000..c7866345 --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/hooks/useSendMessage/index.ts @@ -0,0 +1,33 @@ +import { useMutation } from '@tanstack/react-query'; +import { ContentTypeId } from '@xmtp/content-type-primitives'; +import { + CachedConversation, + DecodedMessage, + SendOptions, + useSendMessage, +} from '@xmtp/react-sdk'; + +export const _sendMessages = async ( + conversation: CachedConversation, + message: string, + sendMessage: ( + conversation: CachedConversation, + content: T, + contentType?: ContentTypeId, + sendOptions?: Omit + ) => Promise | undefined>, + contentType?: SendOptions +) => { + await sendMessage(conversation, message, undefined, contentType); +}; + +export const useSendMessages = (conversation?: CachedConversation) => { + const { sendMessage } = useSendMessage(); + + 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/hooks/sendReactionMessage/index.ts b/packages/@justweb3/xmtp-plugin/src/lib/hooks/useSendReactionMessage/index.ts similarity index 100% rename from packages/@justweb3/xmtp-plugin/src/lib/hooks/sendReactionMessage/index.ts rename to packages/@justweb3/xmtp-plugin/src/lib/hooks/useSendReactionMessage/index.ts diff --git a/packages/@justweb3/xmtp-plugin/src/lib/hooks/sendReplyMessage/index.ts b/packages/@justweb3/xmtp-plugin/src/lib/hooks/useSendReplyMessage/index.ts similarity index 100% rename from packages/@justweb3/xmtp-plugin/src/lib/hooks/sendReplyMessage/index.ts rename to packages/@justweb3/xmtp-plugin/src/lib/hooks/useSendReplyMessage/index.ts diff --git a/packages/@justweb3/xmtp-plugin/src/lib/icons/ChatIcon/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/icons/ChatIcon/index.tsx new file mode 100644 index 00000000..e749a1bc --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/icons/ChatIcon/index.tsx @@ -0,0 +1,17 @@ +export const ChatIcon = (props: React.SVGProps) => { + return ( + + + + ); +}; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/plugins/index.tsx b/packages/@justweb3/xmtp-plugin/src/lib/plugins/index.tsx index 5f0d27c9..22758fb3 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/plugins/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/plugins/index.tsx @@ -1,26 +1,52 @@ import { JustaPlugin } from '@justweb3/widget'; import { JustWeb3XMTPProvider } from '../providers/JustWeb3XMTPProvider'; -import { ChatButton } from '../components/ChatButton'; +import { ChatMenuButton } from '../components/ChatMenuButton'; +import { ProfileChatButton } from '../components/ProfileChatButton'; +import { JustWeb3ButtonRight } from '../components/JustWeb3ButtonRight'; -export const XMTPPlugin: JustaPlugin = { - name: 'XMTPPlugin', - components: { - Provider: (pluginApi, children) => { - return ( - pluginApi.setState('xmtpOpen', open)} - > - {children} - - ); - }, - SignInMenu: (pluginApi) => { - return ( - pluginApi.setState('xmtpOpen', open)} - /> - ); +export type XmtpEnvironment = 'local' | 'production' | 'dev'; + +export const XMTPPlugin = (env: XmtpEnvironment): JustaPlugin => { + return { + name: 'XMTPPlugin', + components: { + JustWeb3ButtonRight: (pluginApi) => { + return ( + pluginApi.setState('xmtpOpen', open)} + env={env} + /> + ); + }, + Provider: (pluginApi, children) => { + return ( + pluginApi.setState('xmtpOpen', open)} + env={env} + > + {children} + + ); + }, + ProfileHeader: (pluginApi, ens, chainId, address) => { + 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 c5e135b3..7092f8d0 100644 --- a/packages/@justweb3/xmtp-plugin/src/lib/providers/JustWeb3XMTPProvider/index.tsx +++ b/packages/@justweb3/xmtp-plugin/src/lib/providers/JustWeb3XMTPProvider/index.tsx @@ -1,24 +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, XMTPProvider, } from '@xmtp/react-sdk'; -import { useJustWeb3 } from '@justweb3/widget'; +import { InboxSheet } from '../../components/InboxSheet'; +import { useEthersSigner } from '../../hooks'; +import { useMountedAccount } from '@justaname.id/react'; +import { loadKeys, storeKeys, wipeKeys } from '../../utils/xmtp'; +import { readReceiptContentTypeConfig } from '../../content-types/readReceipt'; +import { ContentTypeReadReceipt } from '@xmtp/content-type-read-receipt'; import { ChatSheet } from '../../components/ChatSheet'; -import { MessageSheet } from '../../components/MessageSheet'; -const contentTypeConfigs = [ +const contentTypeConfigs: ContentTypeConfiguration[] = [ attachmentContentTypeConfig, reactionContentTypeConfig, replyContentTypeConfig, + readReceiptContentTypeConfig, ]; -interface JustWeb3XMTPContextProps { } +interface JustWeb3XMTPContextProps { + handleOpenChat: (address: string) => void; + conversationsInfo: { + conversationId: string; + unreadCount: number; + consent: 'allowed' | 'blocked' | 'requested'; + lastMessage: CachedMessage; + }[]; +} const JustWeb3XMTPContext = React.createContext< JustWeb3XMTPContextProps | undefined @@ -28,42 +46,195 @@ export interface JustWeb3XMTPProviderProps { children: React.ReactNode; open?: boolean; handleOpen?: (open: boolean) => void; + env: 'local' | 'production' | 'dev'; } export const JustWeb3XMTPProvider: React.FC = ({ children, open, handleOpen, + env, }) => { + // const { isConnected } = useMountedAccount() 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; + }[] + >([]); + const handleXmtpEnabled = (enabled: boolean) => { setIsXmtpEnabled(enabled); }; + const [peerAddress, setPeerAddress] = React.useState(null); const handleOpenChat = ( - conversation: CachedConversation | null + peer: string | CachedConversation + ) => { + if (typeof peer === 'string') { + setPeerAddress(peer); + } else { + setConversation(peer); + } + }; + + const handleConversationInfo = ( + conversationId: string, + unreadCount: number, + lastMessage: CachedMessage, + consent: 'allowed' | 'blocked' | 'requested' ) => { - setConversation(conversation); + 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; + prev[index].consent = consent; + return [...prev]; + }); }; return ( - - - + + {isXmtpEnabled && ( - handleOpenChat('')} + allConversations={conversations} + onConversationsUpdated={setConversations} + conversationsInfo={conversationsInfo} /> )} + + {conversations.allowed.map((conversation) => ( + 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); + setConversation(null); + }} + onChangePeer={handleOpenChat} + peer={conversation ?? peerAddress ?? null} + /> {children} @@ -73,21 +244,188 @@ export const JustWeb3XMTPProvider: React.FC = ({ interface ChecksProps { open?: boolean; handleXmtpEnabled: (enabled: boolean) => void; + env: 'local' | 'production' | 'dev'; +} + +interface GetConversationInfoProps { + conversation: CachedConversation; + handleConversationInfo: ( + conversationId: string, + unreadCount: number, + lastMessage: CachedMessage + ) => void; + unreadCount?: number; + lastMessage?: CachedMessage; } -export const Checks: React.FC = ({ open, handleXmtpEnabled }) => { - const { connectedEns } = useJustWeb3(); - const { disconnect, client } = useClient(); +export const GetConversationInfo: React.FC = ({ + conversation, + handleConversationInfo, + unreadCount, + lastMessage, +}) => { + const { messages } = useMessages(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]; + let lastMessageIndex = _messages.length - 1; + let lastMessage = _messages[lastMessageIndex]; + while ( + lastMessage?.contentType === ContentTypeReadReceipt.toString() && + lastMessageIndex > 0 + ) { + lastMessageIndex--; + lastMessage = _messages[lastMessageIndex]; + } + + return lastMessage; + }, [messages]); useEffect(() => { - handleXmtpEnabled(!!client); - }, [client, handleXmtpEnabled]); + if (unreadCount === _unreadCount && _lastMessage?.id === lastMessage?.id) { + return; + } + + 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, disconnect } = useClient(); + const signer = useEthersSigner(); + const { address } = useMountedAccount(); + const [isInitializing, setIsInitializing] = React.useState(false); + const [rejected, setRejected] = React.useState(false); + + useEffect(() => { + if (!client || !address || isInitializing) return; + if (client.address.toLowerCase() === address.toLowerCase()) return; + + async function reinitializeXmtp() { + 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, isInitializing]); useEffect(() => { - if (!connectedEns?.ens) { - disconnect(); + if (isInitializing || isLoading || rejected) return; + async function initializeXmtp() { + try { + if (client) { + return; + } + + 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, + }); + + // _client?.registerCodec(new ReadReceiptCodec()); + setIsInitializing(false); + } catch (error) { + console.error('Failed to initialize XMTP Client:', error); + wipeKeys(address ?? '', env); + setIsInitializing(false); + setRejected(true); + } } - }, [connectedEns?.ens, disconnect]); + initializeXmtp(); + }, [ + address, + client, + env, + handleXmtpEnabled, + initialize, + isInitializing, + isLoading, + rejected, + signer, + ]); + + useEffect(() => { + handleXmtpEnabled(!!client); + }, [client, handleXmtpEnabled]); return null; }; + +export const useJustWeb3XMTP = () => { + const context = React.useContext(JustWeb3XMTPContext); + if (context === undefined) { + throw new Error( + 'useJustWeb3XMTP must be used within a JustWeb3XMTPProvider' + ); + } + return context; +}; diff --git a/packages/@justweb3/xmtp-plugin/src/lib/utils/xmtp/index.ts b/packages/@justweb3/xmtp-plugin/src/lib/utils/xmtp/index.ts new file mode 100644 index 00000000..7907a5f2 --- /dev/null +++ b/packages/@justweb3/xmtp-plugin/src/lib/utils/xmtp/index.ts @@ -0,0 +1,32 @@ +import { XmtpEnvironment } from '../../plugins'; + +export const buildLocalStorageKey = ( + walletAddress: string, + env: XmtpEnvironment +) => { + return `xmtp:${env}:keys:${walletAddress}`; +}; +const ENCODING = 'binary'; + +export const storeKeys = ( + walletAddress: string, + keys: Uint8Array, + env: XmtpEnvironment +) => { + localStorage.setItem( + buildLocalStorageKey(walletAddress, env), + Buffer.from(keys).toString(ENCODING) + ); +}; + +export const loadKeys = ( + walletAddress: string, + env: XmtpEnvironment +): Uint8Array | null => { + const val = localStorage.getItem(buildLocalStorageKey(walletAddress, env)); + return val ? Buffer.from(val, ENCODING) : null; +}; + +export const wipeKeys = (walletAddress: string, env: XmtpEnvironment) => { + localStorage.removeItem(buildLocalStorageKey(walletAddress, env)); +}; diff --git a/packages/@justweb3/xmtp-plugin/src/stories/xmtp.stories.tsx b/packages/@justweb3/xmtp-plugin/src/stories/xmtp.stories.tsx index fba60a52..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, @@ -15,23 +14,21 @@ import { Meta, StoryObj } from '@storybook/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { WagmiProvider } from 'wagmi'; import { mainnet, sepolia } from 'wagmi/chains'; -import { XMTPPlugin } from '../lib'; -import { ChainId } from '@justaname.id/sdk'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { XMTPPlugin } from '../lib'; import { EFPPlugin } from '@justweb3/efp-plugin'; -import { POAPPlugin } from '@justweb3/poap-plugin'; import { TalentProtocolPlugin } from '@justweb3/talent-protocol-plugin'; -import { JustVerifiedPlugin } from '@justverified/plugin'; +import { POAPPlugin } from '@justweb3/poap-plugin'; const queryClient = new QueryClient(); const JustWeb3Config: JustWeb3ProviderConfig = { - config: { - origin: import.meta.env.STORYBOOK_APP_ORIGIN, - domain: import.meta.env.STORYBOOK_APP_DOMAIN, - signInTtl: 1000 * 60 * 60 * 24, - }, - backendUrl: import.meta.env.STORYBOOK_APP_BACKEND_URL, + // config: { + // origin: import.meta.env.STORYBOOK_APP_ORIGIN, + // domain: import.meta.env.STORYBOOK_APP_DOMAIN, + // signInTtl: 1000 * 60 * 60 * 24, + // }, + // backendUrl: import.meta.env.STORYBOOK_APP_BACKEND_URL, networks: [ { chainId: 1, @@ -42,17 +39,17 @@ const JustWeb3Config: JustWeb3ProviderConfig = { providerUrl: import.meta.env.STORYBOOK_APP_SEPOLIA_PROVIDER_URL, }, ], - ensDomains: [ - { - ensDomain: import.meta.env.STORYBOOK_APP_ENS_DOMAIN, - chainId: parseInt(import.meta.env.STORYBOOK_APP_CHAIN_ID) as ChainId, - }, - ], - openOnWalletConnect: false, - allowedEns: 'all', - dev: import.meta.env.STORYBOOK_APP_DEV === 'true', + // ensDomains: [ + // { + // ensDomain: import.meta.env.STORYBOOK_APP_ENS_DOMAIN, + // chainId: parseInt(import.meta.env.STORYBOOK_APP_CHAIN_ID) as ChainId, + // }, + // ], + // openOnWalletConnect: false, + // // allowedEns: 'all', + // // dev: import.meta.env.STORYBOOK_APP_DEV === 'true', plugins: [ - XMTPPlugin, + XMTPPlugin('production'), EFPPlugin, POAPPlugin({ apiKey: import.meta.env.STORYBOOK_APP_POAP_KEY, @@ -60,7 +57,7 @@ const JustWeb3Config: JustWeb3ProviderConfig = { TalentProtocolPlugin({ apiKey: import.meta.env.STORYBOOK_APP_TALENT_PROTOCOL_API_KEY, }), - JustVerifiedPlugin(['email', 'telegram', 'twitter', 'discord']), + // JustVerifiedPlugin(['email', 'telegram', 'twitter', 'discord']), ], // color: { // primary: '#FF00FF', @@ -95,37 +92,55 @@ export const Example = () => {
-
- - - - - - - - - - - -
-
- - - - - - -
+ {/*
*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/*
*/} + {/**/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/*
*/}
diff --git a/yarn.lock b/yarn.lock index 25fd2303..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,6 +1823,39 @@ __metadata: languageName: node linkType: hard +"@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: + picocolors: "npm:^1.0.0" + sisteransi: "npm:^1.0.5" + checksum: 10c0/b1037226b38696bd95e09beef789ff4e23abb282505ac233c0316c2410c8afb68cf91b67812c883f05ffa6943d6f0593f1ebc17beb94f4a42c13e4657f598c0b + 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" + dependencies: + "@clack/core": "npm:0.3.5" + picocolors: "npm:^1.0.0" + sisteransi: "npm:^1.0.5" + checksum: 10c0/7ac6e7e0fba1c958847e6e3ef9415d9c2184fb424b31691aaef64fd06042d243e859c157b941ca1c9f73e9cb649413fab487a602b981e300acfbec4a111d7562 + languageName: node + linkType: hard + "@coinbase/wallet-sdk@npm:4.0.3": version: 4.0.3 resolution: "@coinbase/wallet-sdk@npm:4.0.3" @@ -2090,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" @@ -2111,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" @@ -2132,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" @@ -2153,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" @@ -2174,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" @@ -2195,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" @@ -2216,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" @@ -2237,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" @@ -2258,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" @@ -2279,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" @@ -2300,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" @@ -2321,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" @@ -2342,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" @@ -2363,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" @@ -2384,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" @@ -2405,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" @@ -2426,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" @@ -2447,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" @@ -2468,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" @@ -2489,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" @@ -2510,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" @@ -2531,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" @@ -2552,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" @@ -3631,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" @@ -4514,44 +4742,53 @@ __metadata: languageName: node linkType: hard -"@justaname.id/react@npm:0.3.149, @justaname.id/react@workspace:packages/@justaname.id/react": +"@justaname.id/address-resolution@npm:^1.1.0": + version: 1.1.0 + resolution: "@justaname.id/address-resolution@npm:1.1.0" + peerDependencies: + ethers: 6.9.0 + checksum: 10c0/f8ffb6eb96ca7497aee179a9714dc45c0dd31fe4a1564eea9faf1b26ff689c58539a20568f17ec107dbdf4d298cf1f1e6593aff0aee24023caf1d07c42d5e263 + languageName: node + linkType: hard + +"@justaname.id/react@npm:0.3.158, @justaname.id/react@workspace:packages/@justaname.id/react": version: 0.0.0-use.local resolution: "@justaname.id/react@workspace:packages/@justaname.id/react" dependencies: "@ensdomains/ensjs": "npm:4.0.2" - "@justaname.id/sdk": "npm:0.2.146" + "@justaname.id/sdk": "npm:0.2.155" axios: "npm:^1.6.0" jest: "npm:^29.4.1" qs: "npm:^6.12.0" peerDependencies: "@tanstack/react-query": ^5.x - ethers: ^6.x + ethers: ^5.6.8 || ^6.0.8 react: ">=17" viem: 2.x wagmi: 2.x languageName: unknown linkType: soft -"@justaname.id/sdk@npm:0.2.146, @justaname.id/sdk@workspace:packages/@justaname.id/sdk": +"@justaname.id/sdk@npm:0.2.155, @justaname.id/sdk@workspace:packages/@justaname.id/sdk": version: 0.0.0-use.local resolution: "@justaname.id/sdk@workspace:packages/@justaname.id/sdk" dependencies: - "@justaname.id/siwens": "npm:0.0.79" + "@justaname.id/siwens": "npm:0.0.88" axios: "npm:^1.6.0" jest: "npm:^29.4.1" qs: "npm:^6.12.0" peerDependencies: - ethers: ">=6.0.0" + ethers: ^5.6.8 || ^6.0.8 siwe: ">=2.0.0" viem: ">=2.0.0" languageName: unknown linkType: soft -"@justaname.id/siwens@npm:0.0.79, @justaname.id/siwens@workspace:packages/@justaname.id/siwens": +"@justaname.id/siwens@npm:0.0.88, @justaname.id/siwens@workspace:packages/@justaname.id/siwens": version: 0.0.0-use.local resolution: "@justaname.id/siwens@workspace:packages/@justaname.id/siwens" peerDependencies: - ethers: ">=6.0.0" + ethers: ^5.6.8 || ^6.0.8 siwe: ">=2.0.0" languageName: unknown linkType: soft @@ -4607,7 +4844,7 @@ __metadata: languageName: unknown linkType: soft -"@justweb3/ui@npm:^0.0.85, @justweb3/ui@workspace:packages/@justweb3/ui": +"@justweb3/ui@npm:^0.0.94, @justweb3/ui@workspace:packages/@justweb3/ui": version: 0.0.0-use.local resolution: "@justweb3/ui@workspace:packages/@justweb3/ui" dependencies: @@ -4635,9 +4872,9 @@ __metadata: dependencies: "@ensdomains/address-encoder": "npm:^1.1.2" "@hookform/resolvers": "npm:^3.9.0" - "@justaname.id/react": "npm:0.3.149" - "@justaname.id/sdk": "npm:0.2.146" - "@justweb3/ui": "npm:^0.0.85" + "@justaname.id/react": "npm:0.3.158" + "@justaname.id/sdk": "npm:0.2.155" + "@justweb3/ui": "npm:^0.0.94" clsx: "npm:1.2.1" cropperjs: "npm:^1.6.2" lodash: "npm:4.17.21" @@ -5044,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" @@ -8695,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" @@ -11274,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" @@ -11424,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: @@ -11442,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" @@ -11814,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" @@ -14367,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" @@ -14711,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" @@ -15179,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" @@ -15537,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" @@ -15738,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" @@ -15745,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" @@ -15983,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" @@ -16945,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" @@ -16982,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" @@ -17439,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" @@ -17597,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 @@ -17676,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: @@ -18530,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: @@ -19211,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: @@ -19319,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" @@ -19511,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" @@ -20106,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" @@ -20113,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" @@ -21433,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" @@ -21557,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" @@ -21663,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" @@ -23070,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" @@ -23225,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" @@ -23241,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" @@ -23300,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" @@ -23866,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" @@ -24274,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" @@ -24463,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: @@ -24541,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" @@ -24742,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" @@ -24756,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" @@ -24844,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" @@ -24888,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" @@ -26681,6 +27348,8 @@ __metadata: "@hookform/error-message": "npm:^2.0.1" "@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" @@ -26842,6 +27511,7 @@ __metadata: react-native-web: "npm:~0.19.9" react-refresh: "npm:^0.10.0" react-router-dom: "npm:6.11.2" + react-scan: "npm:^0.0.35" react-split: "npm:^2.0.14" react-test-renderer: "npm:18.2.0" react-timer-hook: "npm:^3.0.8" @@ -26945,7 +27615,7 @@ __metadata: languageName: node linkType: hard -"kleur@npm:4.1.5": +"kleur@npm:4.1.5, kleur@npm:^4.1.5": version: 4.1.5 resolution: "kleur@npm:4.1.5" checksum: 10c0/e9de6cb49657b6fa70ba2d1448fd3d691a5c4370d8f7bbf1c2f64c24d461270f2117e1b0afe8cb3114f13bbd8e51de158c2a224953960331904e636a5e4c0f2a @@ -28997,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" @@ -29196,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" @@ -30909,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" @@ -31373,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" @@ -31436,6 +32141,15 @@ __metadata: languageName: node linkType: hard +"playwright-core@npm:1.49.0": + version: 1.49.0 + resolution: "playwright-core@npm:1.49.0" + bin: + playwright-core: cli.js + checksum: 10c0/22c1a72fabdcc87bd1cd4d40a032d2c5b94cf94ba7484dc182048c3fa1c8ec26180b559d8cac4ca9870e8fd6bdf5ef9d9f54e7a31fd60d67d098fcffc5e4253b + languageName: node + linkType: hard + "playwright@npm:^1.14.0": version: 1.48.2 resolution: "playwright@npm:1.48.2" @@ -31451,6 +32165,21 @@ __metadata: languageName: node linkType: hard +"playwright@npm:^1.49.0": + version: 1.49.0 + resolution: "playwright@npm:1.49.0" + dependencies: + fsevents: "npm:2.3.2" + playwright-core: "npm:1.49.0" + dependenciesMeta: + fsevents: + optional: true + bin: + playwright: cli.js + checksum: 10c0/e94d662747cd147d0573570fec90dadc013c1097595714036fc8934a075c5a82ab04a49111b03b1f762ea86429bdb7c94460901896901e20970b30ce817cc93f + languageName: node + linkType: hard + "plist@npm:^3.0.5": version: 3.1.0 resolution: "plist@npm:3.1.0" @@ -32472,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" @@ -32833,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 @@ -33451,6 +34198,36 @@ __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" + 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/f0f8d4f489bd07fa659b1f10111befd92e5c12d1fd2d39abeaa22a358069700914c570085ff6c73c9bd541d0c24e8725b65a2454699d1c45a7c8b646339530ec + languageName: node + linkType: hard + "react-shallow-renderer@npm:^16.15.0": version: 16.15.0 resolution: "react-shallow-renderer@npm:16.15.0" @@ -33735,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: @@ -33900,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" @@ -34501,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" @@ -34508,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" @@ -34781,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" @@ -34826,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: @@ -35264,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: @@ -35286,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" @@ -35986,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: @@ -36478,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" @@ -37080,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 @@ -37892,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" @@ -38154,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" @@ -38254,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" @@ -39449,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 @@ -39625,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" @@ -39848,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" @@ -39914,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" @@ -40022,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: