diff --git a/package.json b/package.json index f89c7089c5..e643a23934 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "icons:optimize": "svgo -f ./assets/icons" }, "dependencies": { - "@atproto/api": "^0.15.26", + "@atproto/api": "^0.16.2", "@bitdrift/react-native": "^0.6.8", "@braintree/sanitize-url": "^6.0.2", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", @@ -219,7 +219,7 @@ "zod": "^3.20.2" }, "devDependencies": { - "@atproto/dev-env": "^0.3.155", + "@atproto/dev-env": "^0.3.160", "@babel/core": "^7.26.0", "@babel/preset-env": "^7.26.0", "@babel/runtime": "^7.26.0", diff --git a/src/state/queries/list-memberships.ts b/src/state/queries/list-memberships.ts index 410a613ada..0d93f91d28 100644 --- a/src/state/queries/list-memberships.ts +++ b/src/state/queries/list-memberships.ts @@ -1,93 +1,32 @@ -/** - * NOTE - * - * This query is a temporary solution to our lack of server API for - * querying user membership in an API. It is extremely inefficient. - * - * THIS SHOULD ONLY BE USED IN MODALS FOR MODIFYING A USER'S LIST MEMBERSHIP! - * Use the list-members query for rendering a list's members. - * - * It works by fetching *all* of the user's list item records and querying - * or manipulating that cache. For users with large lists, it will fall - * down completely, so be very conservative about how you use it. - * - * -prf - */ +import {type AppBskyGraphGetListsWithMembership, AtUri} from '@atproto/api' +import { + type InfiniteData, + useInfiniteQuery, + useMutation, + useQueryClient, +} from '@tanstack/react-query' -import {AtUri} from '@atproto/api' -import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' - -import {STALE} from '#/state/queries' import {RQKEY as LIST_MEMBERS_RQKEY} from '#/state/queries/list-members' import {useAgent, useSession} from '#/state/session' -// sanity limit is SANITY_PAGE_LIMIT*PAGE_SIZE total records -const SANITY_PAGE_LIMIT = 1000 -const PAGE_SIZE = 100 -// ...which comes 100,000k list members - const RQKEY_ROOT = 'list-memberships' -export const RQKEY = () => [RQKEY_ROOT] +export const RQKEY = (did: string) => [RQKEY_ROOT, did] -export interface ListMembersip { - membershipUri: string - listUri: string - actorDid: string -} - -/** - * This API is dangerous! Read the note above! - */ -export function useDangerousListMembershipsQuery() { - const {currentAccount} = useSession() +export function useGetListsWithMembership(did: string) { const agent = useAgent() - return useQuery({ - staleTime: STALE.MINUTES.FIVE, - queryKey: RQKEY(), - async queryFn() { - if (!currentAccount) { - return [] - } - let cursor - let arr: ListMembersip[] = [] - for (let i = 0; i < SANITY_PAGE_LIMIT; i++) { - const res = await agent.app.bsky.graph.listitem.list({ - repo: currentAccount.did, - limit: PAGE_SIZE, - cursor, - }) - arr = arr.concat( - res.records.map(r => ({ - membershipUri: r.uri, - listUri: r.value.list, - actorDid: r.value.subject, - })), - ) - cursor = res.cursor - if (!cursor) { - break - } - } - return arr - }, - }) -} -/** - * Returns undefined for pending, false for not a member, and string for a member (the URI of the membership record) - */ -export function getMembership( - memberships: ListMembersip[] | undefined, - list: string, - actor: string, -): string | false | undefined { - if (!memberships) { - return undefined - } - const membership = memberships.find( - m => m.listUri === list && m.actorDid === actor, - ) - return membership ? membership.membershipUri : false + return useInfiniteQuery({ + queryKey: RQKEY(did), + initialPageParam: undefined as string | undefined, + queryFn: async ({pageParam}) => { + const {data} = await agent.app.bsky.graph.getListsWithMembership({ + actor: did, + cursor: pageParam, + }) + return data + }, + getNextPageParam: lastPage => lastPage.cursor, + }) } export function useListMembershipAddMutation({ @@ -124,26 +63,26 @@ export function useListMembershipAddMutation({ return res }, onSuccess: (data, variables) => { - // manually update the cache; a refetch is too expensive - let memberships = queryClient.getQueryData(RQKEY()) - if (memberships) { - memberships = memberships - // avoid dups - .filter( - m => - !( - m.actorDid === variables.actorDid && - m.listUri === variables.listUri - ), - ) - .concat([ - { - ...variables, - membershipUri: data.uri, - }, - ]) - queryClient.setQueryData(RQKEY(), memberships) - } + queryClient.setQueryData( + RQKEY(variables.actorDid), + ( + old?: InfiniteData, + ) => { + if (!old) return old + return { + ...old, + pages: old.pages.map(page => ({ + ...page, + listsWithMembership: page.listsWithMembership.map(list => ({ + ...list, + listItem: + list.list.uri === variables.listUri ? data : list.listItem, + })), + })), + } + }, + ) + // invalidate the members queries (used for rendering the listings) // use a timeout to wait for the appview (see above) setTimeout(() => { @@ -187,18 +126,28 @@ export function useListMembershipRemoveMutation({ // -prf }, onSuccess: (data, variables) => { - // manually update the cache; a refetch is too expensive - let memberships = queryClient.getQueryData(RQKEY()) - if (memberships) { - memberships = memberships.filter( - m => - !( - m.actorDid === variables.actorDid && - m.listUri === variables.listUri - ), - ) - queryClient.setQueryData(RQKEY(), memberships) - } + queryClient.setQueryData( + RQKEY(variables.actorDid), + ( + old?: InfiniteData, + ) => { + if (!old) return old + return { + ...old, + pages: old.pages.map(page => ({ + ...page, + listsWithMembership: page.listsWithMembership.map(list => ({ + ...list, + listItem: + list.list.uri === variables.listUri + ? undefined + : list.listItem, + })), + })), + } + }, + ) + // invalidate the members queries (used for rendering the listings) // use a timeout to wait for the appview (see above) setTimeout(() => { diff --git a/src/view/com/modals/UserAddRemoveLists.tsx b/src/view/com/modals/UserAddRemoveLists.tsx index b0b76644f0..7c20b50c7a 100644 --- a/src/view/com/modals/UserAddRemoveLists.tsx +++ b/src/view/com/modals/UserAddRemoveLists.tsx @@ -5,7 +5,7 @@ import { useWindowDimensions, View, } from 'react-native' -import {AppBskyGraphDefs as GraphDefs} from '@atproto/api' +import {type AppBskyGraphDefs as GraphDefs} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -18,7 +18,7 @@ import {isAndroid, isMobileWeb, isWeb} from '#/platform/detection' import {useModalControls} from '#/state/modals' import { getMembership, - ListMembersip, + type ListMembersip, useDangerousListMembershipsQuery, useListMembershipAddMutation, useListMembershipRemoveMutation, diff --git a/yarn.lock b/yarn.lock index bc7e35f67d..f825040548 100644 --- a/yarn.lock +++ b/yarn.lock @@ -55,18 +55,18 @@ resolved "https://registry.yarnpkg.com/@atproto-labs/simple-store/-/simple-store-0.2.0.tgz#f39098747dabf8a245d0ed6edc50f362aa4d95f8" integrity sha512-0bRbAlI8Ayh03wRwncAMEAyUKtZ+AuTS1jgPrfym1WVOAOiottI/ZmgccqLl6w5MbxVcClNQF7WYGKvGwGoIhA== -"@atproto-labs/xrpc-utils@0.0.17": - version "0.0.17" - resolved "https://registry.yarnpkg.com/@atproto-labs/xrpc-utils/-/xrpc-utils-0.0.17.tgz#c9ff68943a20957ec6e41ed347c73072c53d8755" - integrity sha512-2kEfhe3F4GxW5grpfXxMo4fxHuEdDhj5D10YDJ0aC2BvYab9Y/67DDor7a63IptTgJooKNSweXochCUqOw4I8w== +"@atproto-labs/xrpc-utils@0.0.18": + version "0.0.18" + resolved "https://registry.yarnpkg.com/@atproto-labs/xrpc-utils/-/xrpc-utils-0.0.18.tgz#b4d31867cccff0e846798048b00648bb37e090f0" + integrity sha512-Cwrlx2JcLe0jxCK8b3GCT3HRGaH3yPhyyt+3n4JykJapCaGBKqa6FHGs9hK2Fx6lOyPF7TnV5qUPUsJ1qGEUVA== dependencies: "@atproto/xrpc" "^0.7.1" - "@atproto/xrpc-server" "^0.9.0" + "@atproto/xrpc-server" "^0.9.1" -"@atproto/api@^0.15.26": - version "0.15.26" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.15.26.tgz#452019d6d0753d4caa0f7941e8e87e9f8bfbee52" - integrity sha512-AdXGjeCpLZiP9YMGi4YOdK1ayqkBhklmGfSG8UefqR6tTHth59PZvYs5KiwLnFhedt2Xljt3eUlhkn14Y48wEA== +"@atproto/api@^0.16.2": + version "0.16.2" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.16.2.tgz#1b2870e9a03d88f00a27602281755fa82ec824dd" + integrity sha512-sSTg31J8ws8DNaoiizp+/uJideRxRaJsq+Nyl8rnSxGw0w3oCvoeRU19iRWh2t0jZEmiRJAGkveGu23NKmPYEQ== dependencies: "@atproto/common-web" "^0.4.2" "@atproto/lexicon" "^0.4.12" @@ -94,23 +94,23 @@ multiformats "^9.9.0" uint8arrays "3.0.0" -"@atproto/bsky@^0.0.172": - version "0.0.172" - resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.172.tgz#963e3e2bc661e05c03fcb58788b2621d1afa6a3e" - integrity sha512-P/oDJ4i4TuRBV8pQg8hht147jl+OruDYAXdVmUaHRMWPnry+godnfp6vzph9W9Y03DN+G320Z63dCfvFZquSUQ== +"@atproto/bsky@^0.0.177": + version "0.0.177" + resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.177.tgz#efc78272aabf005657680d6e230888fff1f951c8" + integrity sha512-vg+jG2RknyaVoqzI3D8Djeabg9KzEnC6b3SB+8HDtaJxYmKJ9GH2crMNjcphdygigpls1vn2tJIvG8noKLu6tw== dependencies: "@atproto-labs/fetch-node" "0.1.9" - "@atproto-labs/xrpc-utils" "0.0.17" - "@atproto/api" "^0.15.26" + "@atproto-labs/xrpc-utils" "0.0.18" + "@atproto/api" "^0.16.2" "@atproto/common" "^0.4.11" "@atproto/crypto" "^0.4.4" "@atproto/did" "^0.1.5" "@atproto/identity" "^0.4.8" "@atproto/lexicon" "^0.4.12" "@atproto/repo" "^0.8.5" - "@atproto/sync" "^0.1.29" + "@atproto/sync" "^0.1.30" "@atproto/syntax" "^0.4.0" - "@atproto/xrpc-server" "^0.9.0" + "@atproto/xrpc-server" "^0.9.1" "@bufbuild/protobuf" "^1.5.0" "@connectrpc/connect" "^1.1.4" "@connectrpc/connect-express" "^1.1.4" @@ -129,6 +129,7 @@ jose "^5.0.1" key-encoder "^2.0.3" kysely "^0.22.0" + leo-profanity "^1.8.0" multiformats "^9.9.0" murmurhash "^2.0.1" p-queue "^6.6.2" @@ -221,23 +222,23 @@ "@noble/hashes" "^1.6.1" uint8arrays "3.0.0" -"@atproto/dev-env@^0.3.155": - version "0.3.155" - resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.155.tgz#26f865b92e9241d3e39c1c81e38c8422f2df26e2" - integrity sha512-KNrArdTAfrZQsRsFnwLC1Vgqh27brD5G2ZI0E3qF9BXQw2iiCH1r46IAhhggugL6xo3VXYucmM6/HGX+bKyavw== +"@atproto/dev-env@^0.3.160": + version "0.3.160" + resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.160.tgz#eb777e4f32525d276b4dec20a23001765286a478" + integrity sha512-pEGLoWQ2q4muMlmJ7IiSWI1iWbhOc1PlGqxn6Ru1kd3xR+opuF36OXMeUU1aIqnIrhefjW6Mk+9RYHJAfK9ltg== dependencies: - "@atproto/api" "^0.15.26" - "@atproto/bsky" "^0.0.172" + "@atproto/api" "^0.16.2" + "@atproto/bsky" "^0.0.177" "@atproto/bsync" "^0.0.20" "@atproto/common-web" "^0.4.2" "@atproto/crypto" "^0.4.4" "@atproto/identity" "^0.4.8" "@atproto/lexicon" "^0.4.12" - "@atproto/ozone" "^0.1.131" - "@atproto/pds" "^0.4.161" - "@atproto/sync" "^0.1.29" + "@atproto/ozone" "^0.1.135" + "@atproto/pds" "^0.4.165" + "@atproto/sync" "^0.1.30" "@atproto/syntax" "^0.4.0" - "@atproto/xrpc-server" "^0.9.0" + "@atproto/xrpc-server" "^0.9.1" "@did-plc/lib" "^0.0.1" "@did-plc/server" "^0.0.1" dotenv "^16.0.3" @@ -350,19 +351,19 @@ "@atproto/jwk" "0.4.0" zod "^3.23.8" -"@atproto/ozone@^0.1.131": - version "0.1.131" - resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.131.tgz#b097c0274f424afd6af4e891cf78c563395b65b3" - integrity sha512-xGp0KPK89SXwtXHYza5kYNkwizCdXvn1sq0+5gwR6U8qNrvw0ibxBVMgtHTmqo9633tGD0c0woza1j+r5adm1w== +"@atproto/ozone@^0.1.135": + version "0.1.135" + resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.135.tgz#f317d6541e60a5a659b55c5afd13d19be832796c" + integrity sha512-q5z5Kw596OG7XuhfMtUpICW8u0kZZ+x+wxoEtG5rpBHlFMod13oJOCk+Lo82WFt3dztYIuhg3oSu3JI0NKpW0g== dependencies: - "@atproto/api" "^0.15.26" + "@atproto/api" "^0.16.2" "@atproto/common" "^0.4.11" "@atproto/crypto" "^0.4.4" "@atproto/identity" "^0.4.8" "@atproto/lexicon" "^0.4.12" "@atproto/syntax" "^0.4.0" "@atproto/xrpc" "^0.7.1" - "@atproto/xrpc-server" "^0.9.0" + "@atproto/xrpc-server" "^0.9.1" "@did-plc/lib" "^0.0.1" compression "^1.7.4" cors "^2.8.5" @@ -380,14 +381,14 @@ undici "^6.14.1" ws "^8.12.0" -"@atproto/pds@^0.4.161": - version "0.4.161" - resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.161.tgz#1f74675d5d5e4ca56361c89926571646b4c641ee" - integrity sha512-az1PGUCwIEx/b1neWBh9lgv27nbtczinGazI/ccG5A0AOMps1MXpTIJgEz5yJ1c6mSrhZlP0sL1EO6uC89irNA== +"@atproto/pds@^0.4.165": + version "0.4.165" + resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.165.tgz#2728a8e738a498ef0f471e6f7a3e535601138966" + integrity sha512-rK0sF8hfvOCCXjtm+b0Ic3A2fnq4b7Q8c/vEOaMK3ZqwS8VwFeq9CtopotAMr0swDSupjxiH5aW3NqmNYMvbUg== dependencies: "@atproto-labs/fetch-node" "0.1.9" - "@atproto-labs/xrpc-utils" "0.0.17" - "@atproto/api" "^0.15.26" + "@atproto-labs/xrpc-utils" "0.0.18" + "@atproto/api" "^0.16.2" "@atproto/aws" "^0.2.25" "@atproto/common" "^0.4.11" "@atproto/crypto" "^0.4.4" @@ -397,7 +398,7 @@ "@atproto/repo" "^0.8.5" "@atproto/syntax" "^0.4.0" "@atproto/xrpc" "^0.7.1" - "@atproto/xrpc-server" "^0.9.0" + "@atproto/xrpc-server" "^0.9.1" "@did-plc/lib" "^0.0.4" "@hapi/address" "^5.1.1" better-sqlite3 "^10.0.0" @@ -442,17 +443,17 @@ varint "^6.0.0" zod "^3.23.8" -"@atproto/sync@^0.1.29": - version "0.1.29" - resolved "https://registry.yarnpkg.com/@atproto/sync/-/sync-0.1.29.tgz#fb628e9f8a3562caa72fab6d3888790fc08ab29e" - integrity sha512-tKIhbY4rfCCfinBapnGf7X276dron9A7NT7VFB1Wa9NqODjoJPGDBaRG8s9WmeeIknMgJquhRJOrYu0hQUupTQ== +"@atproto/sync@^0.1.30": + version "0.1.30" + resolved "https://registry.yarnpkg.com/@atproto/sync/-/sync-0.1.30.tgz#38faadc82b7cd62a2835eb3664d386df1bd5de91" + integrity sha512-IbMT/4dklCKy0pVMlrJff4CTdaX/sWwcUrMIxv/kurCzpSQXaC0JtiA0DRfZCIc9n7FMSX+/96vfUNgZttEbOA== dependencies: "@atproto/common" "^0.4.11" "@atproto/identity" "^0.4.8" "@atproto/lexicon" "^0.4.12" "@atproto/repo" "^0.8.5" "@atproto/syntax" "^0.4.0" - "@atproto/xrpc-server" "^0.9.0" + "@atproto/xrpc-server" "^0.9.1" multiformats "^9.9.0" p-queue "^6.6.2" ws "^8.12.0" @@ -462,10 +463,10 @@ resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.4.0.tgz#bec71552087bb24c208a06ef418c0040b65542f2" integrity sha512-b9y5ceHS8YKOfP3mdKmwAx5yVj9294UN7FG2XzP6V5aKUdFazEYRnR9m5n5ZQFKa3GNvz7de9guZCJ/sUTcOAA== -"@atproto/xrpc-server@^0.9.0": - version "0.9.0" - resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.9.0.tgz#d4b2f4194327eac456381b313280ac5c1c27ddd3" - integrity sha512-vREyUFx4EiOtPYfPHVF8x6vQThi/72ZkGSwxfFkFpUZp5PXCjagk3vFw0NH8GbbtQeSAPfdgrcZunfJJgLt4SQ== +"@atproto/xrpc-server@^0.9.1": + version "0.9.1" + resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.9.1.tgz#cf7b35d520ce2841f54aa25eecaaffa218c0d44a" + integrity sha512-AJfxsKrZgKL/5362Rc0oUEjlgpDCmY/soeyLHHjid8J6clbErAdJVCuFwW4T40aHGFY1J13a29ucwbSfOROx6w== dependencies: "@atproto/common" "^0.4.11" "@atproto/crypto" "^0.4.4" @@ -11877,6 +11878,11 @@ freeport-async@^2.0.0: resolved "https://registry.yarnpkg.com/freeport-async/-/freeport-async-2.0.0.tgz#6adf2ec0c629d11abff92836acd04b399135bab4" integrity sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ== +french-badwords-list@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/french-badwords-list/-/french-badwords-list-1.0.7.tgz#8ed2442cb20fa2781c38b5a75c944f8e91a73c08" + integrity sha512-H1ziKs2PJh2+UXZ9oCGJ/rRQpsI9NBykGf2Sc7WaKaj1OnWFuBXfsvANTdRcfVmOghGQaUmRyZ1hJOPbDpy04Q== + fresh@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" @@ -14088,6 +14094,14 @@ launch-editor@^2.6.0: picocolors "^1.0.0" shell-quote "^1.7.3" +leo-profanity@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/leo-profanity/-/leo-profanity-1.8.0.tgz#2ce6522e27c093d02253cdb9755b74cc7ffccf90" + integrity sha512-TATupbZhT3oFCtsiEXzgXcLvW6cDPhDtN+0yQAibjwBdSw3WJFLKM0DZ18eEW8KeFTT10x8gCP+DDZz0cI4Bfg== + optionalDependencies: + french-badwords-list "^1.0.7" + russian-bad-words "^0.5.0" + leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -17575,6 +17589,11 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +russian-bad-words@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/russian-bad-words/-/russian-bad-words-0.5.0.tgz#d4abcdc55f7af37606cdbac44bcb9e0bde6c3855" + integrity sha512-euNvEYki6iYYpkNbeudW+lEMMYGEmN7EBwVF8ezlbv0bZoQpVYB7W10cCeUIGV7Ed50sJynLQ0c559q5iI0ejQ== + rxjs@^6.6.0: version "6.6.7" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"