From 87d98d25c545b3d45cf331ed974d39ece001bf9c Mon Sep 17 00:00:00 2001 From: Philippe Schommers Date: Thu, 25 Aug 2022 19:51:34 +0200 Subject: [PATCH 01/13] feat: add profile protocol --- src/protos/Profile.proto | 8 +++ src/services/eip-712.ts | 124 +++++++++++++++++++++++++++++++++++++++ src/services/profile.ts | 96 ++++++++++++++++++++++++++++++ src/services/waku.ts | 76 ++++++++++++++++++++++++ 4 files changed, 304 insertions(+) create mode 100644 src/protos/Profile.proto create mode 100644 src/services/eip-712.ts create mode 100644 src/services/profile.ts create mode 100644 src/services/waku.ts diff --git a/src/protos/Profile.proto b/src/protos/Profile.proto new file mode 100644 index 0000000..bc2ce1a --- /dev/null +++ b/src/protos/Profile.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +message Profile { + bytes address = 1; + string username = 2; + bytes pictureHash = 3; + bytes signature = 99; +} \ No newline at end of file diff --git a/src/services/eip-712.ts b/src/services/eip-712.ts new file mode 100644 index 0000000..ddbe303 --- /dev/null +++ b/src/services/eip-712.ts @@ -0,0 +1,124 @@ +import { utils } from 'js-waku' +import { getAddress } from '@ethersproject/address' +import { verifyTypedData, Wallet } from '@ethersproject/wallet' + +// Types +import type { + TypedDataDomain, + TypedDataField, + Signer, +} from '@ethersproject/abstract-signer' +import type { SignatureLike } from '@ethersproject/bytes' +import type { Uint8ArrayList } from 'uint8arraylist' + +// Custom types +export type EIP712Config = { + domain: TypedDataDomain + types: Record +} + +type Proto = { + encode: (obj: ProtoType) => Uint8Array + decode: (buf: Uint8Array | Uint8ArrayList) => ProtoType +} + +type SignedPayload = Values & { signature: Uint8Array } + +type VerifyPayloadConfig = { + formatValue: (payload: ProtoType, address: string) => Record + getSigner: (payload: ProtoType) => string | Uint8Array + getSignature?: (payload: ProtoType) => SignatureLike +} + +// Defaults +const defaultGetSignature = (data: Record): SignatureLike => + data.signature as SignatureLike + +const getSignerString = (signer: string | Uint8Array) => { + if (typeof signer === 'string') { + return signer + } + return getAddress('0x' + utils.bytesToHex(signer)) +} + +const getVerifyPayloadConfig = ( + config: VerifyPayloadConfig +) => { + return { + ...config, + getSignature: config.getSignature || defaultGetSignature, + } +} + +export const verifyPayload = >( + { domain, types }: EIP712Config, + config: VerifyPayloadConfig, + payload: ProtoType +) => { + const { getSigner, formatValue, getSignature } = + getVerifyPayloadConfig(config) + const address = getSignerString(getSigner(payload)) + const recovered = verifyTypedData( + domain, + types, + formatValue(payload, address), + getSignature(payload) + ) + return recovered === address +} + +export const decodeSignedPayload = < + Values extends Record, + ProtoType extends SignedPayload +>( + eip712Config: EIP712Config, + config: VerifyPayloadConfig, + proto: Proto, + payload: Uint8Array +): ProtoType | false => { + try { + const decoded = proto.decode(payload) + return verifyPayload(eip712Config, config, decoded) && decoded + } catch (err) { + return false + } +} + +export const createSignedPayload = async < + Values extends Record +>( + config: EIP712Config, + formatData: (signer: Uint8Array) => Values, + signer: Signer +): Promise => { + const address = utils.hexToBytes(await signer.getAddress()) + + // Data to sign and in the Waku message + const data = formatData(address) + + // Check if the signer is a Wallet + if (!(signer instanceof Wallet)) { + throw new Error('not implemented yet') + } + + // Sign the message + const signatureHex = await signer._signTypedData( + config.domain, + config.types, + data + ) + const signature = utils.hexToBytes(signatureHex) + + // Return the data with signature + return { ...data, signature } +} + +export const createSignedProto = async >( + config: EIP712Config, + formatData: (signer: Uint8Array) => Values, + proto: Proto>, + signer: Signer +): Promise => { + const payload = await createSignedPayload(config, formatData, signer) + return proto.encode(payload as SignedPayload) +} diff --git a/src/services/profile.ts b/src/services/profile.ts new file mode 100644 index 0000000..9d1c6d6 --- /dev/null +++ b/src/services/profile.ts @@ -0,0 +1,96 @@ +import { useState } from 'react' +import { PageDirection, WakuMessage } from 'js-waku' + +// Types +import type { Waku } from 'js-waku' +import type { Signer } from 'ethers' + +// Protos +import { Profile } from '../protos/Profile' + +// Services +import { + postWakuMessage, + useWakuStoreQuery, + WakuMessageWithPayload, +} from './waku' +import { createSignedProto, decodeSignedPayload, EIP712Config } from './eip-712' + +type CreateProfile = { + username: string + pictureHash: Uint8Array +} + +// EIP-712 +const eip712Config: EIP712Config = { + domain: { + name: 'Swarm.City', + version: '1', + salt: '0xe3dd854eb9d23c94680b3ec632b9072842365d9a702ab0df7da8bc398ee52c7d', // keccak256('profile') + }, + types: { + Reply: [ + { name: 'address', type: 'address' }, + { name: 'username', type: 'string' }, + { name: 'pictureHash', type: 'bytes' }, + ], + }, +} + +export const getProfileTopic = (address: string) => { + return `/swarmcity/1/profile-${address}/proto` +} + +export const createProfile = async ( + waku: Waku, + connector: { getSigner: () => Promise }, + input: CreateProfile +) => { + const signer = await connector.getSigner() + const payload = await createSignedProto( + eip712Config, + (signer: Uint8Array) => ({ address: signer, ...input }), + Profile, + signer + ) + + return postWakuMessage(waku, connector, getProfileTopic, payload) +} + +const decodeMessage = (message: WakuMessageWithPayload): Profile | false => { + return decodeSignedPayload( + eip712Config, + { + formatValue: (profile, address) => ({ ...profile, address }), + getSigner: (profile) => profile.address, + }, + Profile, + message.payload + ) +} + +export const useProfile = (waku: Waku | undefined, address: string) => { + const [lastUpdate, setLastUpdate] = useState(Date.now()) + const [profile, setProfile] = useState() + + const callback = (messages: WakuMessage[]) => { + for (const message of messages) { + const profile = decodeMessage(message as WakuMessageWithPayload) + if (profile) { + setProfile(profile) + setLastUpdate(Date.now()) + return false + } + } + } + + const state = useWakuStoreQuery( + waku, + callback, + () => getProfileTopic(address), + [address], + { pageDirection: PageDirection.BACKWARD } + ) + + return { ...state, lastUpdate, profile } +} diff --git a/src/services/waku.ts b/src/services/waku.ts new file mode 100644 index 0000000..d1199b3 --- /dev/null +++ b/src/services/waku.ts @@ -0,0 +1,76 @@ +import { useEffect, useState } from 'react' +import { waitForRemotePeer, WakuMessage } from 'js-waku' +import { Wallet } from 'ethers' + +// Types +import type { Waku } from 'js-waku' +import type { DependencyList } from 'react' +import type { QueryOptions } from 'js-waku/lib/waku_store' +import type { Signer } from 'ethers' + +// Custom types +export type WakuMessageWithPayload = WakuMessage & { get payload(): Uint8Array } + +export const useWaku = (waku: Waku | undefined) => { + const [waiting, setWaiting] = useState(true) + + useEffect(() => { + if (!waku) { + return + } + + waitForRemotePeer(waku).then(() => setWaiting(false)) + }, [waku]) + + return waiting +} + +export const useWakuStoreQuery = ( + waku: Waku | undefined, + callback: QueryOptions['callback'], + getTopic: () => string, + dependencies: DependencyList, + options: Omit = {} +) => { + const waiting = useWaku(waku) + const [loading, setLoading] = useState(false) + + useEffect(() => { + if (!waku || waiting) { + return + } + + waku.store + .queryHistory([getTopic()], { callback, ...options }) + .then(() => setLoading(false)) + }, [waiting, ...dependencies]) + + return { waiting, loading } +} + +export const postWakuMessage = async ( + waku: Waku, + connector: { getSigner: () => Promise }, + getTopic: (address: string) => string, + payload: Uint8Array +) => { + const promise = waitForRemotePeer(waku) + + // Get signer + const signer = await connector.getSigner() + const address = await signer.getAddress() + + if (!(signer instanceof Wallet)) { + throw new Error('not implemented yet') + } + + // Wait for peers + // TODO: Should probably be moved somewhere else so the UI can access the state + await promise + + // Post the metadata on Waku + const message = await WakuMessage.fromBytes(payload, getTopic(address)) + + // Send the message + await waku.relay.send(message) +} From 977a4457929186f975416d8bb8cc14d21c74c08e Mon Sep 17 00:00:00 2001 From: Philippe Schommers Date: Thu, 25 Aug 2022 20:04:23 +0200 Subject: [PATCH 02/13] chore: remove unnecessary typescript cast --- src/services/eip-712.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/eip-712.ts b/src/services/eip-712.ts index ddbe303..2a34dab 100644 --- a/src/services/eip-712.ts +++ b/src/services/eip-712.ts @@ -120,5 +120,5 @@ export const createSignedProto = async >( signer: Signer ): Promise => { const payload = await createSignedPayload(config, formatData, signer) - return proto.encode(payload as SignedPayload) + return proto.encode(payload) } From c923c08c5c2ca06ce5371220e2fd493e735403ec Mon Sep 17 00:00:00 2001 From: Philippe Schommers Date: Sun, 28 Aug 2022 18:11:01 +0200 Subject: [PATCH 03/13] chore: deduplicate `useWaku` --- src/hooks/use-waku.tsx | 19 +++++++++++++++++-- src/pages/marketplaces/item.tsx | 6 +++--- src/pages/marketplaces/list-item.tsx | 4 ++-- src/pages/marketplaces/marketplace.tsx | 4 ++-- src/services/profile.ts | 3 +-- src/services/waku.ts | 20 ++++---------------- 6 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/hooks/use-waku.tsx b/src/hooks/use-waku.tsx index 09056b3..037d3af 100644 --- a/src/hooks/use-waku.tsx +++ b/src/hooks/use-waku.tsx @@ -1,5 +1,5 @@ import { multiaddr } from '@multiformats/multiaddr' -import { Waku } from 'js-waku' +import { waitForRemotePeer, Waku } from 'js-waku' import { createWaku, CreateOptions } from 'js-waku/lib/create_waku' import { createContext, @@ -43,10 +43,25 @@ export const WakuProvider = ({ ) } -export const useWaku = () => { +export const useWakuContext = () => { const context = useContext(WakuContext) if (!context) { throw new Error('no context') } return context } + +export const useWaku = () => { + const { waku } = useWakuContext() + const [waiting, setWaiting] = useState(true) + + useEffect(() => { + if (!waku) { + return + } + + waitForRemotePeer(waku).then(() => setWaiting(false)) + }, [waku]) + + return { waku, waiting } +} diff --git a/src/pages/marketplaces/item.tsx b/src/pages/marketplaces/item.tsx index abe6e7e..da4f091 100644 --- a/src/pages/marketplaces/item.tsx +++ b/src/pages/marketplaces/item.tsx @@ -5,7 +5,7 @@ import { useParams, useNavigate } from 'react-router' import { useAccount } from 'wagmi' // Hooks -import { useWaku } from '../../hooks/use-waku' +import { useWakuContext } from '../../hooks/use-waku' // Services import { @@ -24,7 +24,7 @@ type ReplyFormProps = { const ReplyForm = ({ item, marketplace, decimals }: ReplyFormProps) => { const [text, setText] = useState('') - const { waku } = useWaku() + const { waku } = useWakuContext() const { connector } = useAccount() const postReply = async (event: FormEvent) => { @@ -64,7 +64,7 @@ export const MarketplaceItem = () => { const itemId = BigNumber.from(itemIdString) const { address } = useAccount() - const { waku } = useWaku() + const { waku } = useWakuContext() const { decimals } = useMarketplaceTokenDecimals(id) const contract = useMarketplaceContract(id) const { connector } = useAccount() diff --git a/src/pages/marketplaces/list-item.tsx b/src/pages/marketplaces/list-item.tsx index 951a687..a0cb07f 100644 --- a/src/pages/marketplaces/list-item.tsx +++ b/src/pages/marketplaces/list-item.tsx @@ -2,7 +2,7 @@ import { useState } from 'react' import { useParams } from 'react-router' // Hooks -import { useWaku } from '../../hooks/use-waku' +import { useWakuContext } from '../../hooks/use-waku' // Types import type { FormEvent } from 'react' @@ -17,7 +17,7 @@ export const MarketplaceListItem = () => { const [description, setDescription] = useState() const [price, setPrice] = useState() - const { waku } = useWaku() + const { waku } = useWakuContext() const { connector } = useAccount() const onSubmit = async (event: FormEvent) => { diff --git a/src/pages/marketplaces/marketplace.tsx b/src/pages/marketplaces/marketplace.tsx index c1ef074..04f417f 100644 --- a/src/pages/marketplaces/marketplace.tsx +++ b/src/pages/marketplaces/marketplace.tsx @@ -4,7 +4,7 @@ import { Link, useParams } from 'react-router-dom' import { useAccount } from 'wagmi' // Hooks -import { useWaku } from '../../hooks/use-waku' +import { useWakuContext } from '../../hooks/use-waku' // Routes import { MARKETPLACE_ADD } from '../../routes' @@ -55,7 +55,7 @@ export const Marketplace = () => { } const { address } = useAccount() - const { waku } = useWaku() + const { waku } = useWakuContext() const { loading, waiting, items, lastUpdate } = useMarketplaceItems(waku, id) const { decimals } = useMarketplaceTokenDecimals(id) const name = useMarketplaceName(id) diff --git a/src/services/profile.ts b/src/services/profile.ts index 9d1c6d6..3f4ecb1 100644 --- a/src/services/profile.ts +++ b/src/services/profile.ts @@ -69,7 +69,7 @@ const decodeMessage = (message: WakuMessageWithPayload): Profile | false => { ) } -export const useProfile = (waku: Waku | undefined, address: string) => { +export const useProfile = (address: string) => { const [lastUpdate, setLastUpdate] = useState(Date.now()) const [profile, setProfile] = useState() @@ -85,7 +85,6 @@ export const useProfile = (waku: Waku | undefined, address: string) => { } const state = useWakuStoreQuery( - waku, callback, () => getProfileTopic(address), [address], diff --git a/src/services/waku.ts b/src/services/waku.ts index d1199b3..13a6ac4 100644 --- a/src/services/waku.ts +++ b/src/services/waku.ts @@ -8,31 +8,19 @@ import type { DependencyList } from 'react' import type { QueryOptions } from 'js-waku/lib/waku_store' import type { Signer } from 'ethers' +// Hooks +import { useWaku } from '../hooks/use-waku' + // Custom types export type WakuMessageWithPayload = WakuMessage & { get payload(): Uint8Array } -export const useWaku = (waku: Waku | undefined) => { - const [waiting, setWaiting] = useState(true) - - useEffect(() => { - if (!waku) { - return - } - - waitForRemotePeer(waku).then(() => setWaiting(false)) - }, [waku]) - - return waiting -} - export const useWakuStoreQuery = ( - waku: Waku | undefined, callback: QueryOptions['callback'], getTopic: () => string, dependencies: DependencyList, options: Omit = {} ) => { - const waiting = useWaku(waku) + const { waku, waiting } = useWaku() const [loading, setLoading] = useState(false) useEffect(() => { From f9af0751b0aa6ad7a9a7bffd05f1852ac479f28d Mon Sep 17 00:00:00 2001 From: Philippe Schommers Date: Sun, 28 Aug 2022 19:44:27 +0200 Subject: [PATCH 04/13] feat: add hook to keep profile in sync --- src/pages/marketplaces/index.tsx | 6 +++++ src/services/eip-712.ts | 38 +++++++++++++++++++----------- src/services/profile.ts | 40 ++++++++++++++++++++++++++++++-- src/services/waku.ts | 4 ++++ src/store.ts | 3 +++ 5 files changed, 76 insertions(+), 15 deletions(-) diff --git a/src/pages/marketplaces/index.tsx b/src/pages/marketplaces/index.tsx index 2673a92..287d2f9 100644 --- a/src/pages/marketplaces/index.tsx +++ b/src/pages/marketplaces/index.tsx @@ -21,6 +21,9 @@ import { formatBalance } from '../../lib/tools' import avatarDefault from '../../assets/imgs/avatar.svg?url' import exit from '../../assets/imgs/exit.svg?url' +// Services +import { useSyncProfile } from '../../services/profile' + export const Marketplaces = () => { const [profile, setProfile] = useStore.profile() @@ -33,6 +36,9 @@ export const Marketplaces = () => { watch: true, }) + // Keep the profile in sync + useSyncProfile() + if (!profile?.address) { return } diff --git a/src/services/eip-712.ts b/src/services/eip-712.ts index 2a34dab..27291c8 100644 --- a/src/services/eip-712.ts +++ b/src/services/eip-712.ts @@ -1,4 +1,4 @@ -import { utils } from 'js-waku' +import { arrayify } from '@ethersproject/bytes' import { getAddress } from '@ethersproject/address' import { verifyTypedData, Wallet } from '@ethersproject/wallet' @@ -38,7 +38,7 @@ const getSignerString = (signer: string | Uint8Array) => { if (typeof signer === 'string') { return signer } - return getAddress('0x' + utils.bytesToHex(signer)) + return getAddress('0x' + arrayify(signer)) } const getVerifyPayloadConfig = ( @@ -85,16 +85,19 @@ export const decodeSignedPayload = < } export const createSignedPayload = async < - Values extends Record + Data extends Record, + DataToSigner extends Record >( config: EIP712Config, - formatData: (signer: Uint8Array) => Values, + formatData: (signer: Uint8Array) => Data, + formatDataToSign: (signer: string) => DataToSigner, signer: Signer -): Promise => { - const address = utils.hexToBytes(await signer.getAddress()) +): Promise => { + const address = await signer.getAddress() // Data to sign and in the Waku message - const data = formatData(address) + const data = formatData(arrayify(address)) + const values = formatDataToSign(address) // Check if the signer is a Wallet if (!(signer instanceof Wallet)) { @@ -105,20 +108,29 @@ export const createSignedPayload = async < const signatureHex = await signer._signTypedData( config.domain, config.types, - data + values ) - const signature = utils.hexToBytes(signatureHex) + const signature = arrayify(signatureHex) // Return the data with signature return { ...data, signature } } -export const createSignedProto = async >( +export const createSignedProto = async < + Data extends Record, + DataToSigner extends Record +>( config: EIP712Config, - formatData: (signer: Uint8Array) => Values, - proto: Proto>, + formatData: (signer: Uint8Array) => Data, + formatDataToSign: (signer: string) => DataToSigner, + proto: Proto>, signer: Signer ): Promise => { - const payload = await createSignedPayload(config, formatData, signer) + const payload = await createSignedPayload( + config, + formatData, + formatDataToSign, + signer + ) return proto.encode(payload) } diff --git a/src/services/profile.ts b/src/services/profile.ts index 3f4ecb1..8f32bef 100644 --- a/src/services/profile.ts +++ b/src/services/profile.ts @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' import { PageDirection, WakuMessage } from 'js-waku' // Types @@ -13,8 +13,12 @@ import { postWakuMessage, useWakuStoreQuery, WakuMessageWithPayload, + wrapSigner, } from './waku' import { createSignedProto, decodeSignedPayload, EIP712Config } from './eip-712' +import { useWaku } from '../hooks/use-waku' +import { useAccount } from 'wagmi' +import { useStore } from '../store' type CreateProfile = { username: string @@ -50,11 +54,12 @@ export const createProfile = async ( const payload = await createSignedProto( eip712Config, (signer: Uint8Array) => ({ address: signer, ...input }), + (signer: string) => ({ address: signer, ...input }), Profile, signer ) - return postWakuMessage(waku, connector, getProfileTopic, payload) + return postWakuMessage(waku, wrapSigner(signer), getProfileTopic, payload) } const decodeMessage = (message: WakuMessageWithPayload): Profile | false => { @@ -93,3 +98,34 @@ export const useProfile = (address: string) => { return { ...state, lastUpdate, profile } } + +export const useSyncProfile = () => { + const { waku, waiting } = useWaku() + const { address, connector } = useAccount() + const [profile] = useStore.profile() + const [lastProfileSync, setLastProfileSync] = useStore.lastProfileSync() + + useEffect(() => { + if (!waku || !connector || !profile || waiting) { + return + } + + // Only update the profile once a day + if (Date.now() - lastProfileSync.getTime() < 24 * 60 * 60) { + return + } + + if (address !== profile.address) { + console.error( + `Profile address (${profile.address}) differs from signer address (${address})` + ) + return + } + + // TODO: Remove cast after Partial is removed from Partial in store + createProfile(waku, connector, { + username: profile.username as string, + pictureHash: new Uint8Array([]), + }).then(() => setLastProfileSync(new Date())) + }, [waku, connector, waiting, profile]) +} diff --git a/src/services/waku.ts b/src/services/waku.ts index 13a6ac4..8866052 100644 --- a/src/services/waku.ts +++ b/src/services/waku.ts @@ -62,3 +62,7 @@ export const postWakuMessage = async ( // Send the message await waku.relay.send(message) } + +export const wrapSigner = (signer: Signer) => ({ + getSigner: async () => signer, +}) diff --git a/src/store.ts b/src/store.ts index 03fbf41..35648ec 100644 --- a/src/store.ts +++ b/src/store.ts @@ -8,13 +8,16 @@ import type { Profile } from './types/profile' type Store = { profile: Partial | undefined + lastProfileSync: Date } export const { useStore, getStore, withStore } = createStore( { profile: readLocalStore('profile'), + lastProfileSync: new Date(readLocalStore('lastProfileSync') || 0), }, ({ store, prevStore }) => { updateLocalStore(store, prevStore, 'profile') + updateLocalStore(store, prevStore, 'lastProfileSync') } ) From c8293b4836253734f239ecb2c97193f34e43df3c Mon Sep 17 00:00:00 2001 From: Philippe Schommers Date: Sun, 28 Aug 2022 21:47:49 +0200 Subject: [PATCH 05/13] fix: `getSignerString` in EIP 712 verification --- src/services/eip-712.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/eip-712.ts b/src/services/eip-712.ts index 27291c8..f29e212 100644 --- a/src/services/eip-712.ts +++ b/src/services/eip-712.ts @@ -1,4 +1,4 @@ -import { arrayify } from '@ethersproject/bytes' +import { arrayify, hexlify } from '@ethersproject/bytes' import { getAddress } from '@ethersproject/address' import { verifyTypedData, Wallet } from '@ethersproject/wallet' @@ -38,7 +38,7 @@ const getSignerString = (signer: string | Uint8Array) => { if (typeof signer === 'string') { return signer } - return getAddress('0x' + arrayify(signer)) + return getAddress(hexlify(signer)) } const getVerifyPayloadConfig = ( From 4a2feb81d983e8c2cc5d3f749c4246ccce1410ed Mon Sep 17 00:00:00 2001 From: Philippe Schommers Date: Tue, 30 Aug 2022 15:43:48 +0200 Subject: [PATCH 06/13] feat: add profile picture sync --- package-lock.json | 14 +- package.json | 2 +- src/components/modals/create-avatar.tsx | 19 +-- src/lib/store.ts | 18 +++ src/pages/create-account/choose-password.tsx | 1 + src/pages/marketplaces/index.tsx | 5 +- src/protos/Profile.proto | 3 +- src/protos/ProfilePicture.proto | 6 + src/services/profile-picture.ts | 57 ++++++++ src/services/profile.ts | 138 +++++++++++++------ src/services/waku.ts | 49 +++++-- src/store.ts | 15 +- src/types/profile.ts | 2 + 13 files changed, 249 insertions(+), 80 deletions(-) create mode 100644 src/protos/ProfilePicture.proto create mode 100644 src/services/profile-picture.ts diff --git a/package-lock.json b/package-lock.json index a210f32..c27288e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "classnames": "^2.3.1", "ethers": "^5.6.9", "eventemitter3": "^4.0.7", - "js-waku": "^0.25.0-rc.1", + "js-waku": "^0.24.0-e3bef47", "protons-runtime": "^3.1.0", "qrcode.react": "^3.1.0", "react": "^18.2.0", @@ -6860,9 +6860,9 @@ "license": "MIT" }, "node_modules/js-waku": { - "version": "0.25.0-rc.1", - "resolved": "https://registry.npmjs.org/js-waku/-/js-waku-0.25.0-rc.1.tgz", - "integrity": "sha512-bBBgp1xGYxel8wYKAT9wk+NpMymYipDJjEZ79LfDJSfsxybBQgA1ZJaI2jARGfFnfNtcVr7N7QDZTXINwMEqsQ==", + "version": "0.24.0-e3bef47", + "resolved": "https://registry.npmjs.org/js-waku/-/js-waku-0.24.0-e3bef47.tgz", + "integrity": "sha512-TWZvQJuVBVDlBJfbyXBQGj5TyIXgPN3liM3PD8xDmMy9p8Fdn7KFpdjGdUho3ANLXRiOEpwCSGjO7OvFPH/87A==", "dependencies": { "@chainsafe/libp2p-gossipsub": "^3.4.0", "@chainsafe/libp2p-noise": "^7.0.1", @@ -15661,9 +15661,9 @@ "version": "4.0.0" }, "js-waku": { - "version": "0.25.0-rc.1", - "resolved": "https://registry.npmjs.org/js-waku/-/js-waku-0.25.0-rc.1.tgz", - "integrity": "sha512-bBBgp1xGYxel8wYKAT9wk+NpMymYipDJjEZ79LfDJSfsxybBQgA1ZJaI2jARGfFnfNtcVr7N7QDZTXINwMEqsQ==", + "version": "0.24.0-e3bef47", + "resolved": "https://registry.npmjs.org/js-waku/-/js-waku-0.24.0-e3bef47.tgz", + "integrity": "sha512-TWZvQJuVBVDlBJfbyXBQGj5TyIXgPN3liM3PD8xDmMy9p8Fdn7KFpdjGdUho3ANLXRiOEpwCSGjO7OvFPH/87A==", "requires": { "@chainsafe/libp2p-gossipsub": "^3.4.0", "@chainsafe/libp2p-noise": "^7.0.1", diff --git a/package.json b/package.json index e3304dd..230a7d7 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "classnames": "^2.3.1", "ethers": "^5.6.9", "eventemitter3": "^4.0.7", - "js-waku": "^0.25.0-rc.1", + "js-waku": "^0.24.0-e3bef47", "protons-runtime": "^3.1.0", "qrcode.react": "^3.1.0", "react": "^18.2.0", diff --git a/src/components/modals/create-avatar.tsx b/src/components/modals/create-avatar.tsx index 73606d1..dc06e02 100644 --- a/src/components/modals/create-avatar.tsx +++ b/src/components/modals/create-avatar.tsx @@ -1,6 +1,6 @@ // Store import { useRef, useState } from 'react' -import { useStore } from '../../store' +import { setStore } from '../../store' import cancel from '../../assets/imgs/cancel.svg?url' import checkMarkBlue from '../../assets/imgs/checkMarkBlue.svg?url' import iconRotate from '../../assets/imgs/iconRotate.svg?url' @@ -16,11 +16,8 @@ interface Props { export const CreateAvatar = ({ children }: Props) => { const [avatar, setAvatar] = useState('') - const cropperRef = useRef(null) - const [shown, setShown] = useState() - - const [profile, setProfile] = useStore.profile() + const cropperRef = useRef(null) const onFileChange = async (event: ChangeEvent) => { if (!(event.target instanceof HTMLInputElement)) { @@ -85,9 +82,15 @@ export const CreateAvatar = ({ children }: Props) => { className="btn-icon" onClick={(e) => { e.stopPropagation() - updateAvatar().then((newAvatar) => - setProfile({ ...profile, avatar: newAvatar }) - ) + updateAvatar().then((newAvatar) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + setStore.profile.avatar(newAvatar) + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + setStore.profile.lastUpdate(new Date()) + }) setShown(false) }} > diff --git a/src/lib/store.ts b/src/lib/store.ts index 9609be9..f4255c2 100644 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -29,3 +29,21 @@ export function readLocalStore(key: string, prefix?: string): T | undefined { return undefined } } + +// TODO: Improve types +export function readLocalStoreAndRevive< + T extends Record, + K extends keyof T, + V extends T[K] +>( + key: string, + prefix?: string, + reviver?: (key: string, value: string) => V +): T | undefined { + try { + const json = localStorage.getItem(getKey(key, prefix)) + return json && JSON.parse(json, reviver) + } catch (err) { + return undefined + } +} diff --git a/src/pages/create-account/choose-password.tsx b/src/pages/create-account/choose-password.tsx index c120667..68651d2 100644 --- a/src/pages/create-account/choose-password.tsx +++ b/src/pages/create-account/choose-password.tsx @@ -24,6 +24,7 @@ export const ChoosePassword = () => { ...profile, encryptedWallet, address: wallet.address, + lastUpdate: new Date(), }) setLoading(false) navigate(ACCOUNT_CREATED) diff --git a/src/pages/marketplaces/index.tsx b/src/pages/marketplaces/index.tsx index 287d2f9..b693727 100644 --- a/src/pages/marketplaces/index.tsx +++ b/src/pages/marketplaces/index.tsx @@ -13,6 +13,7 @@ import { MarketplaceItem } from './item' // Components import { Redirect } from '../../components/redirect' +import { CreateAvatar } from '../../components/modals/create-avatar' // Lib import { formatBalance } from '../../lib/tools' @@ -53,7 +54,9 @@ export const Marketplaces = () => {
- user avatar + + user avatar +
{profile?.username} diff --git a/src/protos/Profile.proto b/src/protos/Profile.proto index bc2ce1a..f9e5db6 100644 --- a/src/protos/Profile.proto +++ b/src/protos/Profile.proto @@ -3,6 +3,7 @@ syntax = "proto3"; message Profile { bytes address = 1; string username = 2; - bytes pictureHash = 3; + optional bytes pictureHash = 3; + string date = 4; bytes signature = 99; } \ No newline at end of file diff --git a/src/protos/ProfilePicture.proto b/src/protos/ProfilePicture.proto new file mode 100644 index 0000000..2df1982 --- /dev/null +++ b/src/protos/ProfilePicture.proto @@ -0,0 +1,6 @@ +syntax = "proto3"; + +message ProfilePicture { + bytes data = 1; + string type = 2; +} \ No newline at end of file diff --git a/src/services/profile-picture.ts b/src/services/profile-picture.ts new file mode 100644 index 0000000..ab3e4e2 --- /dev/null +++ b/src/services/profile-picture.ts @@ -0,0 +1,57 @@ +// Types +import type { Waku } from 'js-waku' + +// Protos +import { ProfilePicture } from '../protos/ProfilePicture' + +// Services +import { + postWakuMessage, + useLatestTopicData, + WakuMessageWithPayload, +} from './waku' +import { bufferToHex } from '../lib/tools' + +type CreateProfilePicture = { + dataUri: string +} + +export const getProfilePictureTopic = (hash: string) => { + return `/swarmcity/1/profile-picture-${hash}/proto` +} + +export const createProfilePicture = async ( + waku: Waku, + { dataUri }: CreateProfilePicture +) => { + const blob = await (await fetch(dataUri)).blob() + const buffer = await blob.arrayBuffer() + const hash = await crypto.subtle.digest('SHA-256', buffer) + const message = postWakuMessage( + waku, + getProfilePictureTopic(bufferToHex(hash)), + ProfilePicture.encode({ + data: new Uint8Array(buffer), + type: blob.type, + }) + ) + return { hash, message } +} + +const decodeMessage = ( + message: WakuMessageWithPayload +): ProfilePicture | false => { + try { + return ProfilePicture.decode(message.payload) + } catch (err) { + return false + } +} + +export const useProfilePicture = (hash: string) => { + const { data, ...state } = useLatestTopicData( + getProfilePictureTopic(hash), + decodeMessage + ) + return { ...state, picture: data } +} diff --git a/src/services/profile.ts b/src/services/profile.ts index 8f32bef..6a12e10 100644 --- a/src/services/profile.ts +++ b/src/services/profile.ts @@ -1,28 +1,33 @@ -import { useEffect, useState } from 'react' -import { PageDirection, WakuMessage } from 'js-waku' +import { useEffect } from 'react' +import { useAccount } from 'wagmi' + +// Store +import { setStore, useStore } from '../store' // Types import type { Waku } from 'js-waku' import type { Signer } from 'ethers' +import type { Profile } from '../types/profile' // Protos -import { Profile } from '../protos/Profile' +import { Profile as ProfileProto } from '../protos/Profile' // Services import { postWakuMessage, - useWakuStoreQuery, + useLatestTopicData, WakuMessageWithPayload, - wrapSigner, } from './waku' import { createSignedProto, decodeSignedPayload, EIP712Config } from './eip-712' +import { createProfilePicture } from './profile-picture' + +// Hooks import { useWaku } from '../hooks/use-waku' -import { useAccount } from 'wagmi' -import { useStore } from '../store' type CreateProfile = { username: string - pictureHash: Uint8Array + pictureHash?: Uint8Array + date: string } // EIP-712 @@ -33,10 +38,11 @@ const eip712Config: EIP712Config = { salt: '0xe3dd854eb9d23c94680b3ec632b9072842365d9a702ab0df7da8bc398ee52c7d', // keccak256('profile') }, types: { - Reply: [ + Profile: [ { name: 'address', type: 'address' }, { name: 'username', type: 'string' }, { name: 'pictureHash', type: 'bytes' }, + { name: 'date', type: 'string' }, ], }, } @@ -51,67 +57,80 @@ export const createProfile = async ( input: CreateProfile ) => { const signer = await connector.getSigner() + const topic = getProfileTopic(await signer.getAddress()) const payload = await createSignedProto( eip712Config, (signer: Uint8Array) => ({ address: signer, ...input }), (signer: string) => ({ address: signer, ...input }), - Profile, + ProfileProto, signer ) - return postWakuMessage(waku, wrapSigner(signer), getProfileTopic, payload) + return postWakuMessage(waku, topic, payload) } -const decodeMessage = (message: WakuMessageWithPayload): Profile | false => { +const decodeMessage = ( + message: WakuMessageWithPayload +): ProfileProto | false => { return decodeSignedPayload( eip712Config, { formatValue: (profile, address) => ({ ...profile, address }), getSigner: (profile) => profile.address, }, - Profile, + ProfileProto, message.payload ) } export const useProfile = (address: string) => { - const [lastUpdate, setLastUpdate] = useState(Date.now()) - const [profile, setProfile] = useState() - - const callback = (messages: WakuMessage[]) => { - for (const message of messages) { - const profile = decodeMessage(message as WakuMessageWithPayload) - if (profile) { - setProfile(profile) - setLastUpdate(Date.now()) - return false - } - } + const { data, ...state } = useLatestTopicData( + getProfileTopic(address), + decodeMessage + ) + return { ...state, profile: data } +} + +const postPicture = async (waku: Waku, dataUri?: string) => { + if (!dataUri) { + return } - const state = useWakuStoreQuery( - callback, - () => getProfileTopic(address), - [address], - { pageDirection: PageDirection.BACKWARD } - ) + const { hash } = await createProfilePicture(waku, { dataUri }) + return new Uint8Array(hash) +} + +// TODO: Fix teaful issues +const updateProfile = async ( + waku: Waku, + connector: { getSigner: () => Promise }, + profile: Profile +) => { + const pictureHash = await postPicture(waku, profile.avatar) - return { ...state, lastUpdate, profile } + await createProfile(waku, connector, { + username: profile.username, + date: profile.lastUpdate.toISOString(), + pictureHash, + }) + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + setStore.profile.lastSync(new Date()) } export const useSyncProfile = () => { const { waku, waiting } = useWaku() const { address, connector } = useAccount() const [profile] = useStore.profile() - const [lastProfileSync, setLastProfileSync] = useStore.lastProfileSync() + const { + loading, + profile: wakuProfile, + payload, + } = useProfile(profile?.address ?? '') useEffect(() => { - if (!waku || !connector || !profile || waiting) { - return - } - - // Only update the profile once a day - if (Date.now() - lastProfileSync.getTime() < 24 * 60 * 60) { + if (!waku || !connector || !profile || !address || waiting || loading) { return } @@ -122,10 +141,39 @@ export const useSyncProfile = () => { return } - // TODO: Remove cast after Partial is removed from Partial in store - createProfile(waku, connector, { - username: profile.username as string, - pictureHash: new Uint8Array([]), - }).then(() => setLastProfileSync(new Date())) - }, [waku, connector, waiting, profile]) + const wakuDate = new Date(wakuProfile?.date ?? 0) + const profileDate = new Date(profile?.lastUpdate ?? 0) + + // If remote is more recent + if (wakuDate > profileDate) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + setStore.profile.username(wakuProfile.username) + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + setStore.profile.lastUpdate(new Date(wakuProfile.date)) + + return + } + + // If the local profile is more recent, store it + if (profileDate > wakuDate) { + // TODO: Remove cast after Partial is removed from Partial in store + updateProfile(waku, connector, profile as Profile) + return + } + + // If both profiles are in sync, only update the profile once a day + if ( + payload && + Date.now() - (profile.lastSync?.getTime() ?? 0) > 24 * 60 * 60 + ) { + postWakuMessage(waku, getProfileTopic(address), payload).then(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + setStore.profile.lastSync(new Date()) + }) + } + }, [waku, connector, waiting, profile?.lastUpdate, loading]) } diff --git a/src/services/waku.ts b/src/services/waku.ts index 8866052..e9c273c 100644 --- a/src/services/waku.ts +++ b/src/services/waku.ts @@ -1,6 +1,5 @@ import { useEffect, useState } from 'react' -import { waitForRemotePeer, WakuMessage } from 'js-waku' -import { Wallet } from 'ethers' +import { waitForRemotePeer, WakuMessage, PageDirection } from 'js-waku' // Types import type { Waku } from 'js-waku' @@ -21,7 +20,7 @@ export const useWakuStoreQuery = ( options: Omit = {} ) => { const { waku, waiting } = useWaku() - const [loading, setLoading] = useState(false) + const [loading, setLoading] = useState(true) useEffect(() => { if (!waku || waiting) { @@ -38,31 +37,53 @@ export const useWakuStoreQuery = ( export const postWakuMessage = async ( waku: Waku, - connector: { getSigner: () => Promise }, - getTopic: (address: string) => string, + topic: string, payload: Uint8Array ) => { const promise = waitForRemotePeer(waku) - // Get signer - const signer = await connector.getSigner() - const address = await signer.getAddress() - - if (!(signer instanceof Wallet)) { - throw new Error('not implemented yet') - } - // Wait for peers // TODO: Should probably be moved somewhere else so the UI can access the state await promise // Post the metadata on Waku - const message = await WakuMessage.fromBytes(payload, getTopic(address)) + const message = await WakuMessage.fromBytes(payload, topic) // Send the message await waku.relay.send(message) + + // Return message + return message } export const wrapSigner = (signer: Signer) => ({ getSigner: async () => signer, }) + +export const useLatestTopicData = ( + topic: string, + decodeMessage: (message: WakuMessageWithPayload) => Data | false +) => { + const [lastUpdate, setLastUpdate] = useState(Date.now()) + const [data, setData] = useState() + const [payload, setPayload] = useState() + + const callback = (messages: WakuMessage[]) => { + for (const message of messages) { + const data = decodeMessage(message as WakuMessageWithPayload) + if (data) { + setData(data) + setPayload(message.payload) + setLastUpdate(Date.now()) + return false + } + } + } + + const state = useWakuStoreQuery(callback, () => topic, [topic], { + pageDirection: PageDirection.BACKWARD, + pageSize: 1, + }) + + return { ...state, lastUpdate, data, payload } +} diff --git a/src/store.ts b/src/store.ts index 35648ec..5e97a3e 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1,7 +1,11 @@ import createStore from 'teaful' // Lib -import { readLocalStore, updateLocalStore } from './lib/store' +import { + readLocalStore, + readLocalStoreAndRevive, + updateLocalStore, +} from './lib/store' // Types import type { Profile } from './types/profile' @@ -11,9 +15,14 @@ type Store = { lastProfileSync: Date } -export const { useStore, getStore, withStore } = createStore( +export const { useStore, getStore, withStore, setStore } = createStore( { - profile: readLocalStore('profile'), + profile: readLocalStoreAndRevive('profile', undefined, (key, value) => { + if (key === 'lastUpdate' || key === 'lastSync') { + return new Date(value) + } + return value + }), lastProfileSync: new Date(readLocalStore('lastProfileSync') || 0), }, ({ store, prevStore }) => { diff --git a/src/types/profile.ts b/src/types/profile.ts index 250cc38..1c79ab3 100644 --- a/src/types/profile.ts +++ b/src/types/profile.ts @@ -3,4 +3,6 @@ export type Profile = { address: string encryptedWallet: string avatar?: string + lastUpdate: Date + lastSync?: Date } From aff1fc1b353782a6a61b0c9ad475fa3328022095 Mon Sep 17 00:00:00 2001 From: Philippe Schommers Date: Tue, 30 Aug 2022 16:17:12 +0200 Subject: [PATCH 07/13] fix: default to loading in `useGetWakuItems` --- src/pages/marketplaces/services/marketplace-items.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/marketplaces/services/marketplace-items.tsx b/src/pages/marketplaces/services/marketplace-items.tsx index 3a361c3..0885c04 100644 --- a/src/pages/marketplaces/services/marketplace-items.tsx +++ b/src/pages/marketplaces/services/marketplace-items.tsx @@ -135,7 +135,7 @@ export const useGetWakuItems = ( marketplace: string ) => { const [waiting, setWaiting] = useState(true) - const [loading, setLoading] = useState(false) + const [loading, setLoading] = useState(true) const [items, setItems] = useState([]) const [lastUpdate, setLastUpdate] = useState(Date.now()) From bd2a2d88374f5a9280ce7fcf37e05172a7cc6e69 Mon Sep 17 00:00:00 2001 From: Philippe Schommers Date: Tue, 30 Aug 2022 16:55:26 +0200 Subject: [PATCH 08/13] feat: display profiles in item details --- src/lib/tools.ts | 8 +++ src/pages/marketplaces/item.tsx | 59 +++++++++++++++++-- .../services/marketplace-item.tsx | 4 +- 3 files changed, 64 insertions(+), 7 deletions(-) diff --git a/src/lib/tools.ts b/src/lib/tools.ts index ac95006..7687f41 100644 --- a/src/lib/tools.ts +++ b/src/lib/tools.ts @@ -35,3 +35,11 @@ export const bufferToHex = (buffer: ArrayBuffer) => { .map((x) => x.toString(16).padStart(2, '0')) .join('') } + +export const displayAddress = (address: string) => { + return address.substring(0, 6) + '..' + address.substring(38) +} + +export const dataUriToBlob = async (dataUri: string) => { + return await (await fetch(dataUri)).blob() +} diff --git a/src/pages/marketplaces/item.tsx b/src/pages/marketplaces/item.tsx index da4f091..8137033 100644 --- a/src/pages/marketplaces/item.tsx +++ b/src/pages/marketplaces/item.tsx @@ -7,13 +7,28 @@ import { useAccount } from 'wagmi' // Hooks import { useWakuContext } from '../../hooks/use-waku' +// Lib +import { bufferToHex, displayAddress } from '../../lib/tools' + // Services import { useMarketplaceContract, useMarketplaceTokenDecimals, } from './services/marketplace' -import { createReply, useItemReplies } from './services/marketplace-item' +import { + createReply, + ItemReplyClean, + useItemReplies, +} from './services/marketplace-item' import { Item, useMarketplaceItems } from './services/marketplace-items' +import { useProfile } from '../../services/profile' +import { useProfilePicture } from '../../services/profile-picture' + +// Assets +import avatarDefault from '../../assets/imgs/avatar.svg?url' + +// Protos +import { ProfilePicture as ProfilePictureProto } from '../../protos/ProfilePicture' type ReplyFormProps = { item: Item @@ -55,6 +70,43 @@ const ReplyForm = ({ item, marketplace, decimals }: ReplyFormProps) => { ) } +const ProfilePicture = ({ picture }: { picture?: ProfilePictureProto }) => { + const avatar = useMemo(() => { + if (!picture) { + return avatarDefault + } + + const blob = new Blob([picture.data], { type: picture?.type }) + return URL.createObjectURL(blob) + }, [picture]) + + return +} + +const formatFrom = (address: string, username?: string) => { + if (!username) { + return displayAddress(address) + } + + return `${username} (${displayAddress(address)})` +} + +const Reply = ({ reply }: { reply: ItemReplyClean }) => { + const data = useProfile(reply.from) + const { profile } = data + const { picture } = useProfilePicture( + profile?.pictureHash ? bufferToHex(profile.pictureHash) : '' + ) + + return ( +
  • +

    From: {formatFrom(reply.from, profile?.username)}

    + +

    {reply.text}

    +
  • + ) +} + export const MarketplaceItem = () => { const { id, item: itemIdString } = useParams<{ id: string; item: string }>() if (!id || !itemIdString) { @@ -114,10 +166,7 @@ export const MarketplaceItem = () => { {replies.length ? (
      {replies.map((reply) => ( -
    • -

      From: {reply.from}

      -

      {reply.text}

      -
    • + ))}
    ) : ( diff --git a/src/pages/marketplaces/services/marketplace-item.tsx b/src/pages/marketplaces/services/marketplace-item.tsx index 0eb7511..bdac2f3 100644 --- a/src/pages/marketplaces/services/marketplace-item.tsx +++ b/src/pages/marketplaces/services/marketplace-item.tsx @@ -15,11 +15,11 @@ import type { BigNumber, Signer } from 'ethers' // Protos import { ItemReply } from '../../../protos/ItemReply' -type CreateReply = { +export type CreateReply = { text: string } -type ItemReplyClean = { +export type ItemReplyClean = { marketplace: string item: bigint text: string From f4f54a5c0ad046aff397689cebe0aa6209814da2 Mon Sep 17 00:00:00 2001 From: Philippe Schommers Date: Tue, 30 Aug 2022 17:17:28 +0200 Subject: [PATCH 09/13] fix: wait for peers with right protocols --- src/hooks/use-waku.tsx | 7 +++++-- src/services/profile.ts | 4 ++-- src/services/waku.ts | 9 +++++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/hooks/use-waku.tsx b/src/hooks/use-waku.tsx index 037d3af..ca40b78 100644 --- a/src/hooks/use-waku.tsx +++ b/src/hooks/use-waku.tsx @@ -9,6 +9,9 @@ import { useState, } from 'react' +// Types +import type { Protocols } from 'js-waku' + // Config const DEFAULT_SETTINGS: CreateOptions = {} @@ -51,7 +54,7 @@ export const useWakuContext = () => { return context } -export const useWaku = () => { +export const useWaku = (protocols?: Protocols[]) => { const { waku } = useWakuContext() const [waiting, setWaiting] = useState(true) @@ -60,7 +63,7 @@ export const useWaku = () => { return } - waitForRemotePeer(waku).then(() => setWaiting(false)) + waitForRemotePeer(waku, protocols).then(() => setWaiting(false)) }, [waku]) return { waku, waiting } diff --git a/src/services/profile.ts b/src/services/profile.ts index 6a12e10..e109407 100644 --- a/src/services/profile.ts +++ b/src/services/profile.ts @@ -5,7 +5,7 @@ import { useAccount } from 'wagmi' import { setStore, useStore } from '../store' // Types -import type { Waku } from 'js-waku' +import { Protocols, Waku } from 'js-waku' import type { Signer } from 'ethers' import type { Profile } from '../types/profile' @@ -120,7 +120,7 @@ const updateProfile = async ( } export const useSyncProfile = () => { - const { waku, waiting } = useWaku() + const { waku, waiting } = useWaku([Protocols.Relay]) const { address, connector } = useAccount() const [profile] = useStore.profile() const { diff --git a/src/services/waku.ts b/src/services/waku.ts index e9c273c..3e3725c 100644 --- a/src/services/waku.ts +++ b/src/services/waku.ts @@ -1,5 +1,10 @@ import { useEffect, useState } from 'react' -import { waitForRemotePeer, WakuMessage, PageDirection } from 'js-waku' +import { + waitForRemotePeer, + WakuMessage, + PageDirection, + Protocols, +} from 'js-waku' // Types import type { Waku } from 'js-waku' @@ -19,7 +24,7 @@ export const useWakuStoreQuery = ( dependencies: DependencyList, options: Omit = {} ) => { - const { waku, waiting } = useWaku() + const { waku, waiting } = useWaku([Protocols.Store]) const [loading, setLoading] = useState(true) useEffect(() => { From e8135d7c1ec8266f2690aa12ce105474487e495c Mon Sep 17 00:00:00 2001 From: Philippe Schommers Date: Tue, 30 Aug 2022 17:41:01 +0200 Subject: [PATCH 10/13] fix: cropper style in header bar --- src/css/style.css | 4 ++-- src/pages/marketplaces/index.tsx | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/css/style.css b/src/css/style.css index 9c3e445..8915333 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -7846,7 +7846,7 @@ body #app figure.avatar.avatar-sm { width: auto; align-items: center; } -body #app figure.avatar.avatar-sm img { +body #app figure.avatar.avatar-sm img.avatar-img { width: 40px; height: auto; margin-right: 10px; @@ -8587,7 +8587,7 @@ body #app .account-wallet .flex-space { body #app .account-wallet .flex-space figure.avatar .username { font-size: 14px; } - body #app .account-wallet .flex-space figure.avatar.avatar-sm img { + body #app .account-wallet .flex-space figure.avatar.avatar-sm img.avatar-img { width: 60px; } } diff --git a/src/pages/marketplaces/index.tsx b/src/pages/marketplaces/index.tsx index b693727..6fc23de 100644 --- a/src/pages/marketplaces/index.tsx +++ b/src/pages/marketplaces/index.tsx @@ -55,7 +55,11 @@ export const Marketplaces = () => {
    - user avatar + user avatar
    From 0a97c809f3e9debc48ecc1b71b8e909c8a362853 Mon Sep 17 00:00:00 2001 From: Philippe Schommers Date: Tue, 30 Aug 2022 17:52:52 +0200 Subject: [PATCH 11/13] fix: early return from `useLatestTopicData` --- src/services/waku.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/waku.ts b/src/services/waku.ts index 3e3725c..72037a5 100644 --- a/src/services/waku.ts +++ b/src/services/waku.ts @@ -80,7 +80,7 @@ export const useLatestTopicData = ( setData(data) setPayload(message.payload) setLastUpdate(Date.now()) - return false + return true } } } From 270d0ce395483fbd134dd3c0e4d365387839b4d1 Mon Sep 17 00:00:00 2001 From: Philippe Schommers Date: Wed, 31 Aug 2022 11:18:58 +0200 Subject: [PATCH 12/13] refactor: improve types for `readLocalStore` --- src/lib/store.ts | 20 ++++---------------- src/store.ts | 8 ++------ 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/src/lib/store.ts b/src/lib/store.ts index f4255c2..3d7344d 100644 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -21,24 +21,12 @@ export function updateLocalStore>( } } -export function readLocalStore(key: string, prefix?: string): T | undefined { - try { - const json = localStorage.getItem(getKey(key, prefix)) - return json ? (JSON.parse(json) as T) : undefined - } catch (err) { - return undefined - } -} - -// TODO: Improve types -export function readLocalStoreAndRevive< - T extends Record, - K extends keyof T, - V extends T[K] ->( +export function readLocalStore( key: string, prefix?: string, - reviver?: (key: string, value: string) => V + reviver?: T extends Record + ? (key: keyof T, value: string) => T[K] + : undefined ): T | undefined { try { const json = localStorage.getItem(getKey(key, prefix)) diff --git a/src/store.ts b/src/store.ts index 5e97a3e..4fa3c50 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1,11 +1,7 @@ import createStore from 'teaful' // Lib -import { - readLocalStore, - readLocalStoreAndRevive, - updateLocalStore, -} from './lib/store' +import { readLocalStore, updateLocalStore } from './lib/store' // Types import type { Profile } from './types/profile' @@ -17,7 +13,7 @@ type Store = { export const { useStore, getStore, withStore, setStore } = createStore( { - profile: readLocalStoreAndRevive('profile', undefined, (key, value) => { + profile: readLocalStore('profile', undefined, (key, value) => { if (key === 'lastUpdate' || key === 'lastSync') { return new Date(value) } From 0f108abd1a11f832117500bc2909e569bd9918f0 Mon Sep 17 00:00:00 2001 From: Philippe Schommers Date: Wed, 31 Aug 2022 12:40:02 +0200 Subject: [PATCH 13/13] chore: remove `lastProfileSync` from store --- src/store.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/store.ts b/src/store.ts index 4fa3c50..3a2f3f8 100644 --- a/src/store.ts +++ b/src/store.ts @@ -8,7 +8,6 @@ import type { Profile } from './types/profile' type Store = { profile: Partial | undefined - lastProfileSync: Date } export const { useStore, getStore, withStore, setStore } = createStore( @@ -19,10 +18,8 @@ export const { useStore, getStore, withStore, setStore } = createStore( } return value }), - lastProfileSync: new Date(readLocalStore('lastProfileSync') || 0), }, ({ store, prevStore }) => { updateLocalStore(store, prevStore, 'profile') - updateLocalStore(store, prevStore, 'lastProfileSync') } )