diff --git a/package.json b/package.json index 14e09b75..3ea58a1b 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,15 @@ "@types/webscopeio__react-textarea-autocomplete": "^4.7.2", "@webscopeio/react-textarea-autocomplete": "^4.9.2", "bech32": "^2.0.0", + "dexie": "^3.2.2", + "dexie-react-hooks": "^1.1.1", "light-bolt11-decoder": "^2.1.0", "qr-code-styling": "^1.6.0-rc.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-intersection-observer": "^9.4.1", "react-markdown": "^8.0.4", + "react-query": "^3.39.2", "react-redux": "^8.0.5", "react-router-dom": "^6.5.0", "react-scripts": "5.0.1", diff --git a/src/db.ts b/src/db.ts new file mode 100644 index 00000000..203982d2 --- /dev/null +++ b/src/db.ts @@ -0,0 +1,16 @@ +import Dexie, { Table } from 'dexie'; + +import type { User } from './nostr/types'; + +export class SnortDB extends Dexie { + users!: Table; + + constructor() { + super('snortDB'); + this.version(1).stores({ + users: '++pubkey, name, display_name, picture, nip05' // Primary key and indexed props + }); + } +} + +export const db = new SnortDB(); diff --git a/src/element/Nip05.js b/src/element/Nip05.js index 94f2bb95..0001e1ca 100644 --- a/src/element/Nip05.js +++ b/src/element/Nip05.js @@ -1,45 +1,55 @@ -import { useState, useEffect } from "react"; +import { useQuery } from "react-query"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCheck, faSpinner, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons"; import './Nip05.css' -export function useIsVerified(nip05, pubkey) { - const [isVerified, setIsVerified] = useState(false) - const [couldNotVerify, setCouldNotVerify] = useState(false) - const [name, domain] = nip05 ? nip05.split('@') : [] - - useEffect(() => { - if (!nip05 || !pubkey) { - return - } - setCouldNotVerify(false) - setIsVerified(false) - - fetch(`https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(name)}`) +function fetchNip05Pubkey(name, domain) { + if (!name || !domain) { + return Promise.resolve(null) + } + return fetch(`https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(name)}`) .then((res) => res.json()) .then(({ names }) => { - if (names && names[name]) { - setIsVerified(names[name] === pubkey) - } + const match = Object.keys(names).find(n => { + return n.toLowerCase() === name.toLowerCase() + }) + return names[match] }) - .catch((err) => { - setCouldNotVerify(true) - console.error("Couldn't verifiy nip05") - }) - }, [nip05, pubkey]) - - return { name, domain: domain?.toLowerCase(), isVerified, couldNotVerify } } -const Nip05 = ({ name, domain, isVerified, couldNotVerify }) => { +const VERIFICATION_CACHE_TIME = 24 * 60 * 60 * 1000 +const VERIFICATION_STALE_TIMEOUT = 10 * 60 * 1000 + +export function useIsVerified(nip05, pubkey) { + const [name, domain] = nip05 ? nip05.split('@') : [] + const address = domain && `${name}@${domain.toLowerCase()}` + const { isLoading, isError, isSuccess, isIdle, data } = useQuery( + ['nip05', nip05], + () => fetchNip05Pubkey(name, domain), + { + retry: false, + cacheTime: VERIFICATION_CACHE_TIME, + staleTime: VERIFICATION_STALE_TIMEOUT + }, + ) + const isVerified = isSuccess && data === pubkey + const cantVerify = isSuccess && data !== pubkey + return { isVerified, couldNotVerify: isError || cantVerify } +} + +const Nip05 = ({ nip05, pubkey, defaultUsername = '' }) => { + const [name, domain] = nip05 ? nip05.split('@') : [] const isDefaultUser = name === '_' + const { isVerified, couldNotVerify } = useIsVerified(nip05, pubkey) return (
ev.stopPropagation()}> - {!isDefaultUser &&
{name}
} -
+
+ {isDefaultUser ? defaultUsername : name} +
+
{domain}
diff --git a/src/element/Note.js b/src/element/Note.js index 7745ae2e..6a5c27e3 100644 --- a/src/element/Note.js +++ b/src/element/Note.js @@ -9,6 +9,7 @@ import Text from "./Text"; import { eventLink, hexToBech32 } from "../Util"; import NoteFooter from "./NoteFooter"; import NoteTime from "./NoteTime"; +import Nip05 from "./Nip05"; export default function Note(props) { const navigate = useNavigate(); diff --git a/src/element/ProfileImage.css b/src/element/ProfileImage.css index f75504f4..31b24614 100644 --- a/src/element/ProfileImage.css +++ b/src/element/ProfileImage.css @@ -17,4 +17,18 @@ .pfp a:hover { text-decoration: underline; + text-decoration-color: var(--gray-superlight); +} + +.pfp .profile-name { + display: flex; + flex-direction: column; + font-weight: bold; + margin-bottom: .2em; +} + +.pfp .nip05 { + font-size: 16px; + margin: 0; + margin-top: -.2em; } diff --git a/src/element/ProfileImage.js b/src/element/ProfileImage.js index 828c583c..e4feeb77 100644 --- a/src/element/ProfileImage.js +++ b/src/element/ProfileImage.js @@ -6,6 +6,7 @@ import { Link, useNavigate } from "react-router-dom"; import useProfile from "../feed/ProfileFeed"; import { hexToBech32, profileLink } from "../Util"; import LazyImage from "./LazyImage"; +import Nip05 from "./Nip05"; export default function ProfileImage({ pubkey, subHeader, showUsername = true, className, link }) { const navigate = useNavigate(); @@ -26,10 +27,21 @@ export default function ProfileImage({ pubkey, subHeader, showUsername = true, c
navigate(link ?? profileLink(pubkey))} /> {showUsername && (
- {name} + +
+
{name}
+ {user?.nip05 && ( + + )} +
+ {subHeader ? <>{subHeader} : null}
)}
) -} \ No newline at end of file +} diff --git a/src/element/Textarea.tsx b/src/element/Textarea.tsx index f4d290d3..35d591eb 100644 --- a/src/element/Textarea.tsx +++ b/src/element/Textarea.tsx @@ -1,9 +1,10 @@ -import { Component } from "react"; +import { useSelector } from "react-redux"; +import { useLiveQuery } from "dexie-react-hooks"; import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete"; // @ts-expect-error -import Nip05, { useIsVerified } from "./Nip05"; +import Nip05 from "./Nip05"; import "@webscopeio/react-textarea-autocomplete/style.css"; import "./Textarea.css"; // @ts-expect-error @@ -11,10 +12,11 @@ import Nostrich from "../nostrich.jpg"; // @ts-expect-error import { hexToBech32 } from "../Util"; import type { User } from "../nostr/types"; +import { db } from "../db"; -function searchUsers(query: string, users: Record) { +function searchUsers(query: string, users: User[]) { const q = query.toLowerCase() - return Object.values(users).filter(({ name, display_name, about, nip05 }) => { + return users.filter(({ name, display_name, about, nip05 }: User) => { return name?.toLowerCase().includes(q) || display_name?.toLowerCase().includes(q) || about?.toLowerCase().includes(q) @@ -23,7 +25,6 @@ function searchUsers(query: string, users: Record) { } const UserItem = ({ pubkey, display_name, picture, nip05, ...rest }: User) => { - const { isVerified, couldNotVerify, name, domain } = useIsVerified(nip05, pubkey) return (
@@ -31,35 +32,45 @@ const UserItem = ({ pubkey, display_name, picture, nip05, ...rest }: User) => {
{display_name || rest.name} - +
) } -export default class Textarea extends Component { - render() { - // @ts-expect-error - const { users, onChange, ...rest } = this.props +function normalizeUser({ pubkey, picture, nip05, name, display_name }: User) { + return { pubkey, nip05, name, picture, display_name } +} + +const Textarea = ({ users, onChange, ...rest }: any) => { + const normalizedUsers = Object.keys(users).reduce((acc, pk) => { + return {...acc, [pk]: normalizeUser(users[pk]) } + }, {}) + const dbUsers = useLiveQuery( + () => db.users.toArray().then(usrs => { + return usrs.reduce((acc, usr) => { + return { ...acc, [usr.pubkey]: normalizeUser(usr)} + }, {}) + }) + ) + const allUsers: User[] = Object.values({...normalizedUsers, ...dbUsers}) + return ( Loading....} placeholder="Say something!" - ref={rta => { - // @ts-expect-error - this.rta = rta; - }} onChange={onChange} trigger={{ "@": { afterWhitespace: true, - dataProvider: token => searchUsers(token, users), + dataProvider: token => dbUsers ? searchUsers(token, allUsers) : [], component: (props: any) => , output: (item: any) => `@${hexToBech32("npub", item.pubkey)}` } }} /> ) - } } + +export default Textarea diff --git a/src/feed/LoginFeed.js b/src/feed/LoginFeed.js index 4d4034cf..5cadfc40 100644 --- a/src/feed/LoginFeed.js +++ b/src/feed/LoginFeed.js @@ -4,6 +4,7 @@ import EventKind from "../nostr/EventKind"; import { Subscriptions } from "../nostr/Subscriptions"; import { addDirectMessage, addNotifications, setFollows, setRelays } from "../state/Login"; import { setUserData } from "../state/Users"; +import { db } from "../db"; import useSubscription from "./Subscription"; import { mapEventToProfile } from "./UsersFeed"; @@ -41,7 +42,8 @@ export default function useLoginFeed() { useEffect(() => { let contactList = notes.filter(a => a.kind === EventKind.ContactList); let notifications = notes.filter(a => a.kind === EventKind.TextNote); - let metadata = notes.filter(a => a.kind === EventKind.SetMetadata).map(a => mapEventToProfile(a)); + let metadata = notes.filter(a => a.kind === EventKind.SetMetadata) + let profiles = metadata.map(a => mapEventToProfile(a)); let dms = notes.filter(a => a.kind === EventKind.DirectMessage); for (let cl of contactList) { @@ -60,7 +62,11 @@ export default function useLoginFeed() { } } dispatch(addNotifications(notifications)); - dispatch(setUserData(metadata)); + dispatch(setUserData(profiles)); + const userMetadata = metadata.map(ev => { + return {...JSON.parse(ev.content), pubkey: ev.pubkey } + }) + db.users.bulkPut(metadata); dispatch(addDirectMessage(dms)); }, [notes]); } \ No newline at end of file diff --git a/src/feed/UsersFeed.js b/src/feed/UsersFeed.js index f9d87e3c..68db3f05 100644 --- a/src/feed/UsersFeed.js +++ b/src/feed/UsersFeed.js @@ -2,6 +2,7 @@ import { useEffect, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; import { ProfileCacheExpire } from "../Const"; import EventKind from "../nostr/EventKind"; +import { db } from "../db"; import { Subscriptions } from "../nostr/Subscriptions"; import { setUserData } from "../state/Users"; import useSubscription from "./Subscription"; @@ -34,7 +35,12 @@ export default function useUsersCache() { const results = useSubscription(sub); useEffect(() => { - dispatch(setUserData(results.notes.map(a => mapEventToProfile(a)))); + const userData = results.notes.map(a => mapEventToProfile(a)); + dispatch(setUserData(userData)); + const profiles = results.notes.map(ev => { + return {...JSON.parse(ev.content), pubkey: ev.pubkey } + }); + db.users.bulkPut(profiles); }, [results]); return results; diff --git a/src/index.js b/src/index.js index e834baa9..22c8323a 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,7 @@ import './index.css'; import React from 'react'; +import { QueryClient, QueryClientProvider } from 'react-query'; import ReactDOM from 'react-dom/client'; import { Provider } from 'react-redux' import { @@ -28,6 +29,11 @@ import ChatPage from './pages/ChatPage'; */ export const System = new NostrSystem(); +/** + * HTTP query provider + */ +const HTTP = new QueryClient() + const router = createBrowserRouter([ { element: , @@ -81,7 +87,9 @@ const root = ReactDOM.createRoot(document.getElementById('root')); root.render( - + + + ); diff --git a/src/nostr/types.tsx b/src/nostr/types.tsx index 9f9df606..8c02463b 100644 --- a/src/nostr/types.tsx +++ b/src/nostr/types.tsx @@ -1,4 +1,4 @@ -export type User = { +export interface User { name?: string about?: string display_name?: string diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index 3d46a706..a1ca626b 100644 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -13,7 +13,7 @@ import { extractLnAddress, parseId, hexToBech32 } from "../Util"; import Timeline from "../element/Timeline"; import Text from '../element/Text' import LNURLTip from "../element/LNURLTip"; -import Nip05, { useIsVerified } from "../element/Nip05"; +import Nip05 from "../element/Nip05"; import Copy from "../element/Copy"; import ProfilePreview from "../element/ProfilePreview"; import FollowersList from "../element/FollowersList"; @@ -37,9 +37,9 @@ export default function ProfilePage() { const [showLnQr, setShowLnQr] = useState(false); const [tab, setTab] = useState(ProfileTab.Notes); const about = Text({ content: user?.about }) - const { name, domain, isVerified, couldNotVerify } = useIsVerified(user?.nip05, user?.pubkey) const avatarUrl = (user?.picture?.length ?? 0) === 0 ? Nostrich : user?.picture const backgroundImage = `url(${avatarUrl})` + const domain = user?.nip05 && user.nip05.split('@')[1] useEffect(() => { setTab(ProfileTab.Notes); @@ -50,7 +50,7 @@ export default function ProfilePage() {

{user?.display_name || user?.name || 'Nostrich'}

- {user?.nip05 && } + {user?.nip05 && }
) } @@ -59,7 +59,7 @@ export default function ProfilePage() { const lnurl = extractLnAddress(user?.lud16 || user?.lud06 || ""); return (
-

{about}

+
{about}
{user?.website && (
@@ -103,7 +103,7 @@ export default function ProfilePage() { function avatar() { return (
-
+
) diff --git a/src/state/Users.js b/src/state/Users.js index d61b95cb..5f334b12 100644 --- a/src/state/Users.js +++ b/src/state/Users.js @@ -1,5 +1,6 @@ import { createSlice } from '@reduxjs/toolkit' import { ProfileCacheExpire } from '../Const'; +import { db } from '../db'; const UsersSlice = createSlice({ name: "Users", @@ -67,7 +68,13 @@ const UsersSlice = createSlice({ }; } state.users[x.pubkey] = x; - window.localStorage.setItem(`user:${x.pubkey}`, JSON.stringify(x)); + db.users.put({ + pubkey: x.pubkey, + name: x.name, + display_name: x.display_name, + nip05: x.nip05, + picture: x.picture, + }) state.users = { ...state.users diff --git a/yarn.lock b/yarn.lock index 1b412655..eb869882 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1024,7 +1024,7 @@ "@babel/helper-validator-option" "^7.18.6" "@babel/plugin-transform-typescript" "^7.18.6" -"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.20.7", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.20.7", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.20.7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.7.tgz#fcb41a5a70550e04a7b708037c7c32f7f356d8fd" integrity sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ== @@ -2868,6 +2868,11 @@ bfj@^7.0.2: hoopy "^0.1.4" tryer "^1.0.1" +big-integer@^1.6.16: + version "1.6.51" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" + integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== + big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -2943,6 +2948,20 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" +broadcast-channel@^3.4.1: + version "3.7.0" + resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-3.7.0.tgz#2dfa5c7b4289547ac3f6705f9c00af8723889937" + integrity sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg== + dependencies: + "@babel/runtime" "^7.7.2" + detect-node "^2.1.0" + js-sha3 "0.8.0" + microseconds "0.2.0" + nano-time "1.0.0" + oblivious-set "1.0.0" + rimraf "3.0.2" + unload "2.2.0" + browser-process-hrtime@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" @@ -3682,7 +3701,7 @@ detect-newline@^3.0.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== -detect-node@^2.0.4: +detect-node@^2.0.4, detect-node@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== @@ -3704,6 +3723,16 @@ detective@^5.2.1: defined "^1.0.0" minimist "^1.2.6" +dexie-react-hooks@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/dexie-react-hooks/-/dexie-react-hooks-1.1.1.tgz#ff405cc89e5d899ddbac5e40d593f83f9a74106a" + integrity sha512-Cam5JP6PxHN564RvWEoe8cqLhosW0O4CAZ9XEVYeGHJBa6KEJlOpd9CUpV3kmU9dm2MrW97/lk7qkf1xpij7gA== + +dexie@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/dexie/-/dexie-3.2.2.tgz#fa6f2a3c0d6ed0766f8d97a03720056f88fe0e01" + integrity sha512-q5dC3HPmir2DERlX+toCBbHQXW5MsyrFqPFcovkH9N2S/UW/H3H5AWAB6iEOExeraAu+j+zRDG+zg/D7YhH0qg== + didyoumean@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" @@ -5974,6 +6003,11 @@ js-sdsl@^4.1.4: resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.2.0.tgz#278e98b7bea589b8baaf048c20aeb19eb7ad09d0" integrity sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ== +js-sha3@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" + integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -6288,6 +6322,14 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" +match-sorter@^6.0.2: + version "6.3.1" + resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.3.1.tgz#98cc37fda756093424ddf3cbc62bfe9c75b92bda" + integrity sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw== + dependencies: + "@babel/runtime" "^7.12.5" + remove-accents "0.4.2" + mdast-util-definitions@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-5.1.1.tgz#2c1d684b28e53f84938bb06317944bee8efa79db" @@ -6580,6 +6622,11 @@ micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.2" picomatch "^2.3.1" +microseconds@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.2.0.tgz#233b25f50c62a65d861f978a4a4f8ec18797dc39" + integrity sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA== + mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" @@ -6668,6 +6715,13 @@ multicast-dns@^7.2.5: dns-packet "^5.2.2" thunky "^1.0.2" +nano-time@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef" + integrity sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA== + dependencies: + big-integer "^1.6.16" + nanoid@^3.3.4: version "3.3.4" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" @@ -6840,6 +6894,11 @@ object.values@^1.1.0, object.values@^1.1.6: define-properties "^1.1.4" es-abstract "^1.20.4" +oblivious-set@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/oblivious-set/-/oblivious-set-1.0.0.tgz#c8316f2c2fb6ff7b11b6158db3234c49f733c566" + integrity sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw== + obuf@^1.0.0, obuf@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" @@ -7893,6 +7952,15 @@ react-markdown@^8.0.4: unist-util-visit "^4.0.0" vfile "^5.0.0" +react-query@^3.39.2: + version "3.39.2" + resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.39.2.tgz#9224140f0296f01e9664b78ed6e4f69a0cc9216f" + integrity sha512-F6hYDKyNgDQfQOuR1Rsp3VRzJnWHx6aRnnIZHMNGGgbL3SBgpZTDg8MQwmxOgpCAoqZJA+JSNCydF1xGJqKOCA== + dependencies: + "@babel/runtime" "^7.5.5" + broadcast-channel "^3.4.1" + match-sorter "^6.0.2" + react-redux@^8.0.5: version "8.0.5" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.0.5.tgz#e5fb8331993a019b8aaf2e167a93d10af469c7bd" @@ -8140,6 +8208,11 @@ remark-rehype@^10.0.0: mdast-util-to-hast "^12.1.0" unified "^10.0.0" +remove-accents@0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5" + integrity sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA== + renderkid@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-3.0.0.tgz#5fd823e4d6951d37358ecc9a58b1f06836b6268a" @@ -8232,7 +8305,7 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@^3.0.0, rimraf@^3.0.2: +rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -9216,6 +9289,14 @@ universalify@^2.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== +unload@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/unload/-/unload-2.2.0.tgz#ccc88fdcad345faa06a92039ec0f80b488880ef7" + integrity sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA== + dependencies: + "@babel/runtime" "^7.6.2" + detect-node "^2.0.4" + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"