csr tangled client in solid-js
15

Configure Feed

Select the types of activity you want to include in your feed.

create atproto-query that uses atproto-ui in a tanstack query compatible format

authored by

nekomimi.pet and committed by
Tangled
(Jul 1, 2026, 8:06 PM UTC) 5e284cd5 9aaba609

+567 -103
+94 -60
package-lock.json
··· 15 15 "@atcute/microcosm": "^2.0.0", 16 16 "@atcute/oauth-browser-client": "^4.0.0", 17 17 "@atcute/tangled": "^2.0.1", 18 + "@atproto-ui/core": "^0.13.0", 19 + "@atproto-ui/solid": "^0.13.1", 18 20 "@solidjs/router": "^0.16.1", 19 - "@tanstack/solid-query": "^5.100.10", 20 21 "clsx": "^2.1.1", 21 22 "dompurify": "^3.4.3", 22 23 "fflate": "^0.8.2", ··· 33 34 } 34 35 }, 35 36 "node_modules/@atcute/atproto": { 36 - "version": "4.0.0", 37 - "resolved": "https://registry.npmjs.org/@atcute/atproto/-/atproto-4.0.0.tgz", 38 - "integrity": "sha512-Rd21kW0tl/QEXhyPkJXRpUV8HYs7sMOmmXRu2z1sKMZbm1tqvHz1L3CQeTFcC/Xf/5cg7S6ITGIKRP0++zu0aw==", 37 + "version": "4.0.2", 38 + "resolved": "https://registry.npmjs.org/@atcute/atproto/-/atproto-4.0.2.tgz", 39 + "integrity": "sha512-hLnvjiIOStpdUm0cEN+R5YydvbV0d6ap17Iv+t7i/nhSCN3TGMya7M0ftCWtCo+xoQ1EU6HK74R8jqXWlyrM0w==", 40 + "license": "0BSD", 41 + "dependencies": { 42 + "@atcute/lexicons": "^2.0.0" 43 + }, 44 + "peerDependencies": { 45 + "@atcute/lexicons": "^2.0.0" 46 + } 47 + }, 48 + "node_modules/@atcute/bluesky": { 49 + "version": "4.0.9", 50 + "resolved": "https://registry.npmjs.org/@atcute/bluesky/-/bluesky-4.0.9.tgz", 51 + "integrity": "sha512-xSecUOyy0T0zsKAZ2GT/0YgzfwiPraI48hpX8KfJh7dAFekFFq7b9/tHN0rzwhqDQxAcyOH5I0kgjpmX2jL4yw==", 39 52 "license": "0BSD", 40 53 "dependencies": { 54 + "@atcute/atproto": "^4.0.2", 41 55 "@atcute/lexicons": "^2.0.0" 42 56 }, 43 57 "peerDependencies": { ··· 62 76 "resolved": "https://registry.npmjs.org/@atcute/identity/-/identity-2.0.0.tgz", 63 77 "integrity": "sha512-YXFsggO7eJYifqkN85+kUXJE2a1iI9AyuzPTDjtS/4WE1Zs1/XiPkWmwZlAgtp+pYhVtjm3mJqy/h/mZ0OnIVw==", 64 78 "license": "0BSD", 79 + "peer": true, 65 80 "dependencies": { 66 81 "@atcute/lexicons": "^2.0.0", 67 82 "valibot": "^1.4.0" ··· 207 222 } 208 223 }, 209 224 "node_modules/@atcute/tangled": { 210 - "version": "2.0.1", 211 - "resolved": "https://registry.npmjs.org/@atcute/tangled/-/tangled-2.0.1.tgz", 212 - "integrity": "sha512-x+B77ukgl/YYfy/oXDyPesqlT765i3goDm6lDQEgJCQwSRcczps8XC26pBVUKSPGcSMTo+p76vaeXLpi3wB+FQ==", 225 + "version": "2.0.7", 226 + "resolved": "https://registry.npmjs.org/@atcute/tangled/-/tangled-2.0.7.tgz", 227 + "integrity": "sha512-1L9WG3x5RZRXspHiA2gSXbD+lusYt350RVEqqdrkQYryPnYyaN5864d4KI4UjVwG2H3cDWamMqH8DUCdVughGw==", 213 228 "license": "0BSD", 214 229 "dependencies": { 215 - "@atcute/atproto": "^4.0.0", 230 + "@atcute/atproto": "^4.0.2", 216 231 "@atcute/lexicons": "^2.0.0" 217 232 }, 218 233 "peerDependencies": { ··· 243 258 "unicode-segmenter": "^0.14.5" 244 259 } 245 260 }, 261 + "node_modules/@atproto-ui/core": { 262 + "version": "0.13.0", 263 + "resolved": "https://registry.npmjs.org/@atproto-ui/core/-/core-0.13.0.tgz", 264 + "integrity": "sha512-vV/vsc7q/yi38L/wUw6lj5fdGPwBhP3+QsYfvbigpQelFYUTfnrphd70SGEObwYRZjOce9pBzTIv1YUamdAHow==", 265 + "dependencies": { 266 + "@atcute/atproto": "^4.0.0", 267 + "@atcute/bluesky": "^4.0.2", 268 + "@atcute/client": "^5.0.0", 269 + "@atcute/identity": "^2.0.0", 270 + "@atcute/identity-resolver": "^2.0.0", 271 + "@atcute/lexicons": "^2.0.0", 272 + "@atcute/tangled": "^2.0.2" 273 + } 274 + }, 275 + "node_modules/@atproto-ui/solid": { 276 + "version": "0.13.1", 277 + "resolved": "https://registry.npmjs.org/@atproto-ui/solid/-/solid-0.13.1.tgz", 278 + "integrity": "sha512-b6LCcPM1S8En35FP7fohQQxNDoULXNj6kyIpoxK+0T15XAYrBCUTV8kqMDJXLDHOYAr7S/C4HpZFS9N19GFdCg==", 279 + "dependencies": { 280 + "@atcute/bluesky": "^4.0.2", 281 + "@atproto-ui/core": "^0.13.0" 282 + }, 283 + "peerDependencies": { 284 + "solid-js": "^1.8.0" 285 + } 286 + }, 246 287 "node_modules/@babel/code-frame": { 247 288 "version": "7.29.0", 248 289 "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", ··· 274 315 "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", 275 316 "dev": true, 276 317 "license": "MIT", 318 + "peer": true, 277 319 "dependencies": { 278 320 "@babel/code-frame": "^7.29.0", 279 321 "@babel/generator": "^7.29.0", ··· 509 551 "node": ">=6.9.0" 510 552 } 511 553 }, 512 - "node_modules/@emnapi/core": { 513 - "version": "1.10.0", 514 - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", 515 - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", 516 - "dev": true, 517 - "license": "MIT", 518 - "optional": true, 519 - "dependencies": { 520 - "@emnapi/wasi-threads": "1.2.1", 521 - "tslib": "^2.4.0" 522 - } 523 - }, 524 - "node_modules/@emnapi/runtime": { 525 - "version": "1.10.0", 526 - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", 527 - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", 528 - "dev": true, 529 - "license": "MIT", 530 - "optional": true, 531 - "dependencies": { 532 - "tslib": "^2.4.0" 533 - } 534 - }, 535 554 "node_modules/@emnapi/wasi-threads": { 536 - "version": "1.2.1", 537 - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", 538 - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", 555 + "version": "1.2.2", 556 + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", 557 + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", 539 558 "dev": true, 540 559 "license": "MIT", 541 560 "optional": true, ··· 845 864 "node": "^20.19.0 || >=22.12.0" 846 865 } 847 866 }, 867 + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { 868 + "version": "1.10.0", 869 + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", 870 + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", 871 + "dev": true, 872 + "license": "MIT", 873 + "optional": true, 874 + "dependencies": { 875 + "@emnapi/wasi-threads": "1.2.1", 876 + "tslib": "^2.4.0" 877 + } 878 + }, 879 + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { 880 + "version": "1.10.0", 881 + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", 882 + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", 883 + "dev": true, 884 + "license": "MIT", 885 + "optional": true, 886 + "dependencies": { 887 + "tslib": "^2.4.0" 888 + } 889 + }, 890 + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { 891 + "version": "1.2.1", 892 + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", 893 + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", 894 + "dev": true, 895 + "license": "MIT", 896 + "optional": true, 897 + "dependencies": { 898 + "tslib": "^2.4.0" 899 + } 900 + }, 848 901 "node_modules/@rolldown/binding-win32-arm64-msvc": { 849 902 "version": "1.0.1", 850 903 "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz", ··· 901 954 "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", 902 955 "license": "MIT" 903 956 }, 904 - "node_modules/@tanstack/query-core": { 905 - "version": "5.100.10", 906 - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.10.tgz", 907 - "integrity": "sha512-8UR0yJR+GiQ40m3lPhUr0xbfAupe6GSQiksSBSa9SM2NjezFyxXCIA69/lz8cSoNKZLrw1/PktIyQBJcVeMi3w==", 908 - "license": "MIT", 909 - "funding": { 910 - "type": "github", 911 - "url": "https://github.com/sponsors/tannerlinsley" 912 - } 913 - }, 914 - "node_modules/@tanstack/solid-query": { 915 - "version": "5.100.10", 916 - "resolved": "https://registry.npmjs.org/@tanstack/solid-query/-/solid-query-5.100.10.tgz", 917 - "integrity": "sha512-4uUqc+l9f7wNfL9/fxU9P+/RyA0wXh67jrl8EeNYsXHo5Xcy31uiSBq5wZ80NHBCbyw+eLtrkZZMW2ZXNe1ViQ==", 918 - "license": "MIT", 919 - "dependencies": { 920 - "@tanstack/query-core": "5.100.10" 921 - }, 922 - "funding": { 923 - "type": "github", 924 - "url": "https://github.com/sponsors/tannerlinsley" 925 - }, 926 - "peerDependencies": { 927 - "solid-js": "^1.6.0" 928 - } 929 - }, 930 957 "node_modules/@tybys/wasm-util": { 931 958 "version": "0.10.2", 932 959 "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", ··· 989 1016 "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", 990 1017 "dev": true, 991 1018 "license": "MIT", 1019 + "peer": true, 992 1020 "dependencies": { 993 1021 "undici-types": "~7.16.0" 994 1022 } ··· 1082 1110 } 1083 1111 ], 1084 1112 "license": "MIT", 1113 + "peer": true, 1085 1114 "dependencies": { 1086 1115 "baseline-browser-mapping": "^2.10.12", 1087 1116 "caniuse-lite": "^1.0.30001782", ··· 1690 1719 "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", 1691 1720 "dev": true, 1692 1721 "license": "MIT", 1722 + "peer": true, 1693 1723 "engines": { 1694 1724 "node": ">=12" 1695 1725 }, ··· 1775 1805 "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.4.tgz", 1776 1806 "integrity": "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==", 1777 1807 "license": "MIT", 1808 + "peer": true, 1778 1809 "engines": { 1779 1810 "node": ">=10" 1780 1811 } ··· 1796 1827 "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.12.tgz", 1797 1828 "integrity": "sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw==", 1798 1829 "license": "MIT", 1830 + "peer": true, 1799 1831 "dependencies": { 1800 1832 "csstype": "^3.1.0", 1801 1833 "seroval": "~1.5.0", ··· 1858 1890 "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", 1859 1891 "devOptional": true, 1860 1892 "license": "Apache-2.0", 1893 + "peer": true, 1861 1894 "bin": { 1862 1895 "tsc": "bin/tsc", 1863 1896 "tsserver": "bin/tsserver" ··· 1930 1963 "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==", 1931 1964 "dev": true, 1932 1965 "license": "MIT", 1966 + "peer": true, 1933 1967 "dependencies": { 1934 1968 "lightningcss": "^1.32.0", 1935 1969 "picomatch": "^4.0.4",
+2 -1
package.json
··· 16 16 "@atcute/microcosm": "^2.0.0", 17 17 "@atcute/oauth-browser-client": "^4.0.0", 18 18 "@atcute/tangled": "^2.0.1", 19 + "@atproto-ui/core": "^0.13.0", 20 + "@atproto-ui/solid": "^0.13.1", 19 21 "@solidjs/router": "^0.16.1", 20 - "@tanstack/solid-query": "^5.100.10", 21 22 "clsx": "^2.1.1", 22 23 "dompurify": "^3.4.3", 23 24 "fflate": "^0.8.2",
+13 -10
src/App.tsx
··· 1 - import { QueryClient, QueryClientProvider } from '@tanstack/solid-query'; 1 + import { QueryClient, QueryClientProvider } from './lib/atproto-query'; 2 2 import type { Component } from 'solid-js'; 3 3 4 + import { AtProtoProvider } from '@atproto-ui/solid'; 4 5 import { AuthProvider } from './lib/auth'; 5 6 import { LiveEventsProvider } from './lib/live-events'; 6 7 import { AppSettingsProvider } from './lib/settings'; ··· 16 17 }); 17 18 18 19 const App: Component = () => ( 19 - <QueryClientProvider client={queryClient}> 20 - <AppSettingsProvider> 21 - <AuthProvider> 22 - <LiveEventsProvider> 23 - <AppRoutes /> 24 - </LiveEventsProvider> 25 - </AuthProvider> 26 - </AppSettingsProvider> 27 - </QueryClientProvider> 20 + <AtProtoProvider> 21 + <QueryClientProvider client={queryClient}> 22 + <AppSettingsProvider> 23 + <AuthProvider> 24 + <LiveEventsProvider> 25 + <AppRoutes /> 26 + </LiveEventsProvider> 27 + </AuthProvider> 28 + </AppSettingsProvider> 29 + </QueryClientProvider> 30 + </AtProtoProvider> 28 31 ); 29 32 30 33 export default App;
+1 -1
src/components/common.tsx
··· 12 12 } from 'lucide-solid'; 13 13 import { A } from '@solidjs/router'; 14 14 import { Match, Show, Switch, createEffect, createMemo, createSignal, type Component } from 'solid-js'; 15 - import { createQuery, useQueryClient } from '@tanstack/solid-query'; 15 + import { createQuery, useQueryClient } from '../lib/atproto-query'; 16 16 export { useQueryClient }; 17 17 import { resolveAvatarUrl } from '../lib/api/identity'; 18 18
+1 -1
src/components/repo.tsx
··· 27 27 import { For, Show, createMemo, createSignal, createEffect, onCleanup, onMount, type Component, type JSX } from 'solid-js'; 28 28 import type { RepoContext, TreeEntry } from '../lib/api/repos'; 29 29 import type { Did } from '@atcute/lexicons/syntax'; 30 - import { createQuery } from '@tanstack/solid-query'; 30 + import { createQuery } from '../lib/atproto-query'; 31 31 import { getRepo, getRepoLanguages, getRepoForkCount, getRepoByDid } from '../lib/api/repos'; 32 32 import { getRepoStarCount } from '../lib/api/stars'; 33 33 import { getRepoIssueCount } from '../lib/api/issues';
+1 -1
src/layout.tsx
··· 1 1 import clsx from 'clsx'; 2 2 import { Bell, BookMarked, LogOut, Search, Settings, UserRound, Plus, LineSquiggle, BookPlus } from 'lucide-solid'; 3 3 import { A, useLocation, useNavigate } from '@solidjs/router'; 4 - import { createQuery } from '@tanstack/solid-query'; 4 + import { createQuery } from './lib/atproto-query'; 5 5 import { For, Show, createEffect, createSignal, onCleanup, onMount, type Component, type JSX } from 'solid-js'; 6 6 7 7 import { Avatar, buttonStyles, inputStyles } from './components/common';
+50 -2
src/lib/api/repos.ts
··· 306 306 })); 307 307 }; 308 308 309 - export const listRepoRecords = async (owner: string | ResolvedActor): Promise<RepoRecord[]> => 310 - appviewFallback( 309 + export const listRepoRecords = async (owner: string | ResolvedActor): Promise<RepoRecord[]> => { 310 + const records = await appviewFallback( 311 311 () => listRepoRecordsFromAppview(owner), 312 312 () => listRepoRecordsFromPds(owner), 313 313 ); 314 + if (records.length > 0) { 315 + return records; 316 + } 317 + 318 + // Bobbin/Hydrant can return a successful empty list while the user's PDS has 319 + // repo records. Treat an empty appview list as inconclusive and ask the PDS 320 + // before showing an empty profile. 321 + try { 322 + const pdsRecords = await listRepoRecordsFromPds(owner); 323 + return pdsRecords.length > 0 ? pdsRecords : records; 324 + } catch { 325 + return records; 326 + } 327 + }; 314 328 315 329 export const getRepo = async (ownerIdentifier: string, repoSlug: string): Promise<RepoContext> => { 316 330 const owner = await resolveActor(ownerIdentifier); ··· 341 355 slug, 342 356 rkey: match.rkey, 343 357 knot: normalizeServiceUrl(match.value.knot), 358 + }; 359 + }; 360 + 361 + export const getRepoByUri = async (uri: ResourceUri): Promise<RepoContext> => { 362 + const parsed = parseAtUri(uri); 363 + const owner = await resolveActor(parsed.did); 364 + const response = await ok( 365 + getRpc(owner.pds).get('com.atproto.repo.getRecord', { 366 + params: { 367 + repo: owner.did, 368 + collection: parsed.collection, 369 + rkey: parsed.rkey, 370 + }, 371 + }), 372 + ); 373 + const value = response.value as ShTangledRepo.Main; 374 + const slug = value.name?.trim() || parsed.rkey; 375 + 376 + if (!value.repoDid) { 377 + throw new Error(`Repository is missing repoDid metadata`); 378 + } 379 + 380 + return { 381 + owner, 382 + record: { 383 + uri: response.uri, 384 + cid: response.cid ?? '', 385 + rkey: parsed.rkey, 386 + value, 387 + }, 388 + repoDid: value.repoDid, 389 + slug, 390 + rkey: parsed.rkey, 391 + knot: normalizeServiceUrl(value.knot), 344 392 }; 345 393 }; 346 394
+67 -1
src/lib/api/stars.ts
··· 1 1 import { ok } from '@atcute/client'; 2 - import type { Did } from '@atcute/lexicons/syntax'; 2 + import type { Did, ResourceUri } from '@atcute/lexicons/syntax'; 3 3 import type { OAuthUserAgent } from '@atcute/oauth-browser-client'; 4 4 import { ShTangledFeedStar } from '@atcute/tangled'; 5 5 ··· 14 14 getBacklinkDids, 15 15 } from './backlinks'; 16 16 import { STAR_COLLECTION } from './constants'; 17 + import { getRpc } from './client'; 17 18 import { createAuthRpc } from './identity'; 18 19 import { 19 20 parseAtUri, ··· 27 28 currentUserStarRkey?: string; 28 29 } 29 30 31 + export interface StarRecord { 32 + uri: ResourceUri; 33 + cid: string; 34 + rkey: string; 35 + value: ShTangledFeedStar.Main; 36 + } 37 + 30 38 const STAR_BACKLINK_SOURCES = [ 31 39 'sh.tangled.feed.star:subject.did', 32 40 'sh.tangled.feed.star:subjectDid', ··· 65 73 66 74 const getRepoStarCountFromAppview = async (repo: RepoContext): Promise<number> => 67 75 getDistinctAppviewRecordCount('sh.tangled.feed.countStars', repo.repoDid); 76 + 77 + const listStarRecordsFromAppview = async (actor: { did: Did }): Promise<StarRecord[]> => { 78 + const stars = await listAllAppviewRecords<ShTangledFeedStar.Main>('sh.tangled.feed.listStarsBy', actor.did); 79 + return stars.map((star) => ({ 80 + uri: star.uri, 81 + cid: star.cid ?? '', 82 + rkey: parseAtUri(star.uri).rkey, 83 + value: star.value, 84 + })); 85 + }; 86 + 87 + const listStarRecordsFromPds = async (actor: { did: Did; pds: string }): Promise<StarRecord[]> => { 88 + const rpc = getRpc(actor.pds); 89 + const records: StarRecord[] = []; 90 + let cursor: string | undefined; 91 + 92 + do { 93 + const page = await ok( 94 + rpc.get('com.atproto.repo.listRecords', { 95 + params: { 96 + repo: actor.did, 97 + collection: STAR_COLLECTION, 98 + limit: 100, 99 + cursor, 100 + reverse: true, 101 + }, 102 + }), 103 + ); 104 + records.push(...page.records.map((record) => ({ 105 + uri: record.uri, 106 + cid: record.cid ?? '', 107 + rkey: parseAtUri(record.uri).rkey, 108 + value: record.value as ShTangledFeedStar.Main, 109 + }))); 110 + cursor = page.cursor; 111 + } while (cursor); 112 + 113 + return records; 114 + }; 115 + 116 + export const listStarRecordsByActor = async (actor: { did: Did; pds: string }): Promise<StarRecord[]> => { 117 + const records = await appviewFallback( 118 + () => listStarRecordsFromAppview(actor), 119 + () => listStarRecordsFromPds(actor), 120 + ); 121 + if (records.length > 0) { 122 + return records; 123 + } 124 + 125 + // Bobbin/Hydrant can return a successful empty list while the actor's PDS has 126 + // stars. Use PDS as the authoritative tie-breaker for profile data. 127 + try { 128 + const pdsRecords = await listStarRecordsFromPds(actor); 129 + return pdsRecords.length > 0 ? pdsRecords : records; 130 + } catch { 131 + return records; 132 + } 133 + }; 68 134 69 135 const findCurrentUserStarFromAppview = async ( 70 136 repo: RepoContext,
+16 -2
src/lib/api/strings.ts
··· 95 95 })); 96 96 }; 97 97 98 - export const listStringRecords = async (owner: string | ResolvedActor): Promise<StringRecord[]> => 99 - appviewFallback( 98 + export const listStringRecords = async (owner: string | ResolvedActor): Promise<StringRecord[]> => { 99 + const records = await appviewFallback( 100 100 () => listStringRecordsFromAppview(owner), 101 101 () => listStringRecordsFromPds(owner), 102 102 ); 103 + if (records.length > 0) { 104 + return records; 105 + } 106 + 107 + // Bobbin/Hydrant can return a successful empty list while the user's PDS has 108 + // string records. Treat an empty appview list as inconclusive and ask the PDS 109 + // before showing an empty profile. 110 + try { 111 + const pdsRecords = await listStringRecordsFromPds(owner); 112 + return pdsRecords.length > 0 ? pdsRecords : records; 113 + } catch { 114 + return records; 115 + } 116 + }; 103 117 104 118 export const getString = async ( 105 119 ownerIdentifier: string,
+295
src/lib/atproto-query.tsx
··· 1 + import { createContext, createEffect, createMemo, createSignal, onCleanup, useContext, type Accessor, type JSX } from 'solid-js'; 2 + import { getSharedCaches, clearSharedCaches } from '@atproto-ui/core'; 3 + 4 + type QueryKey = readonly unknown[]; 5 + type QueryKeyInput = QueryKey | unknown[]; 6 + type QueryFn<T> = () => Promise<T> | T; 7 + type QueryUpdater<T> = T | ((old: T | undefined) => T | undefined); 8 + 9 + interface QueryOptions<T> { 10 + queryKey: QueryKeyInput; 11 + queryFn: QueryFn<T>; 12 + enabled?: boolean; 13 + staleTime?: number; 14 + placeholderData?: unknown; 15 + refetchOnWindowFocus?: boolean; 16 + } 17 + 18 + interface QueryEntry<T = unknown> { 19 + key: QueryKey; 20 + keyHash: string; 21 + data?: T; 22 + error?: Error; 23 + updatedAt: number; 24 + promise?: Promise<T>; 25 + listeners: Set<() => void>; 26 + queryFn?: QueryFn<T>; 27 + staleTime: number; 28 + invalidated: boolean; 29 + } 30 + 31 + export interface QueryResult<T = unknown> { 32 + readonly data: T | undefined; 33 + readonly error: Error | undefined; 34 + readonly isLoading: boolean; 35 + readonly isFetching: boolean; 36 + readonly isPending: boolean; 37 + readonly isError: boolean; 38 + readonly isSuccess: boolean; 39 + readonly status: 'pending' | 'error' | 'success'; 40 + refetch: () => Promise<T | undefined>; 41 + } 42 + 43 + const UNTANGLED_QUERY_DID = 'did:web:untangled.local'; 44 + const UNTANGLED_QUERY_COLLECTION = 'app.untangled.query'; 45 + 46 + const queryKeyHash = (key: QueryKeyInput): string => JSON.stringify(key); 47 + 48 + const atprotoQueryCache = () => getSharedCaches().recordCache; 49 + 50 + const queryKeyStartsWith = (candidate: QueryKey, prefix?: QueryKeyInput): boolean => { 51 + if (!prefix) return true; 52 + if (prefix.length > candidate.length) return false; 53 + return prefix.every((part, index) => Object.is(part, candidate[index])); 54 + }; 55 + 56 + const toError = (error: unknown): Error => error instanceof Error ? error : new Error(String(error)); 57 + 58 + export class QueryClient { 59 + private entries = new Map<string, QueryEntry>(); 60 + private defaultStaleTime: number; 61 + 62 + constructor(options?: { defaultOptions?: { queries?: { staleTime?: number; retry?: number | boolean } } }) { 63 + this.defaultStaleTime = options?.defaultOptions?.queries?.staleTime ?? 30_000; 64 + } 65 + 66 + getOrCreateEntry<T>(key: QueryKeyInput, staleTime?: number): QueryEntry<T> { 67 + const normalizedKey = [...key]; 68 + const hash = queryKeyHash(normalizedKey); 69 + let entry = this.entries.get(hash) as QueryEntry<T> | undefined; 70 + if (!entry) { 71 + const cached = atprotoQueryCache().get<{ 72 + data: T; 73 + updatedAt: number; 74 + }>(UNTANGLED_QUERY_DID, UNTANGLED_QUERY_COLLECTION, hash); 75 + entry = { 76 + key: normalizedKey, 77 + keyHash: hash, 78 + data: cached?.data, 79 + updatedAt: cached?.updatedAt ?? 0, 80 + listeners: new Set(), 81 + staleTime: staleTime ?? this.defaultStaleTime, 82 + invalidated: false, 83 + }; 84 + this.entries.set(hash, entry); 85 + } else if (staleTime !== undefined) { 86 + entry.staleTime = staleTime; 87 + } 88 + return entry; 89 + } 90 + 91 + subscribe(entry: QueryEntry, listener: () => void): () => void { 92 + entry.listeners.add(listener); 93 + return () => entry.listeners.delete(listener); 94 + } 95 + 96 + private notify(entry: QueryEntry) { 97 + for (const listener of entry.listeners) listener(); 98 + } 99 + 100 + private isFresh(entry: QueryEntry): boolean { 101 + return !entry.invalidated && entry.data !== undefined && Date.now() - entry.updatedAt < entry.staleTime; 102 + } 103 + 104 + async fetchQuery<T>(options: { queryKey: QueryKeyInput; queryFn: QueryFn<T>; staleTime?: number }): Promise<T> { 105 + const entry = this.getOrCreateEntry<T>(options.queryKey, options.staleTime); 106 + entry.queryFn = options.queryFn; 107 + if (this.isFresh(entry)) return entry.data as T; 108 + return this.fetchEntry(entry, options.queryFn); 109 + } 110 + 111 + async ensureQueryData<T>(options: { queryKey: QueryKeyInput; queryFn: QueryFn<T>; staleTime?: number }): Promise<T> { 112 + const entry = this.getOrCreateEntry<T>(options.queryKey, options.staleTime); 113 + entry.queryFn = options.queryFn; 114 + if (entry.data !== undefined && !entry.invalidated) return entry.data as T; 115 + return this.fetchEntry(entry, options.queryFn); 116 + } 117 + 118 + prefetchQuery<T>(options: { queryKey: QueryKeyInput; queryFn: QueryFn<T>; staleTime?: number }): Promise<void> { 119 + return this.fetchQuery(options).then(() => undefined, () => undefined); 120 + } 121 + 122 + async refetchQueries(filter: { queryKey?: QueryKeyInput } = {}): Promise<void> { 123 + await Promise.all(this.matchingEntries(filter.queryKey).map((entry) => { 124 + if (!entry.queryFn) return undefined; 125 + return this.fetchEntry(entry, entry.queryFn).then(() => undefined, () => undefined); 126 + })); 127 + } 128 + 129 + async invalidateQueries(filter: { queryKey?: QueryKeyInput; refetchType?: 'none' | string } = {}): Promise<void> { 130 + const entries = this.matchingEntries(filter.queryKey); 131 + for (const entry of entries) { 132 + entry.invalidated = true; 133 + this.notify(entry); 134 + } 135 + if (filter.refetchType === 'none') return; 136 + await this.refetchQueries(filter); 137 + } 138 + 139 + setQueryData<T>(queryKey: QueryKeyInput, updater: QueryUpdater<T>): T | undefined { 140 + const entry = this.getOrCreateEntry<T>(queryKey); 141 + const next = typeof updater === 'function' 142 + ? (updater as (old: T | undefined) => T | undefined)(entry.data as T | undefined) 143 + : updater; 144 + entry.data = next; 145 + entry.error = undefined; 146 + entry.updatedAt = Date.now(); 147 + entry.invalidated = false; 148 + if (next !== undefined) { 149 + atprotoQueryCache().set(UNTANGLED_QUERY_DID, UNTANGLED_QUERY_COLLECTION, entry.keyHash, { 150 + data: next, 151 + updatedAt: entry.updatedAt, 152 + }); 153 + } 154 + this.notify(entry); 155 + return next; 156 + } 157 + 158 + setQueriesData<T>(filter: { queryKey?: QueryKeyInput }, updater: QueryUpdater<T>): Array<[QueryKey, T | undefined]> { 159 + return this.matchingEntries(filter.queryKey).map((entry) => [ 160 + entry.key, 161 + this.setQueryData<T>(entry.key, updater), 162 + ]); 163 + } 164 + 165 + getQueryData<T>(queryKey: QueryKeyInput): T | undefined { 166 + return this.entries.get(queryKeyHash(queryKey))?.data as T | undefined; 167 + } 168 + 169 + clear(): void { 170 + this.entries.clear(); 171 + clearSharedCaches(); 172 + } 173 + 174 + private matchingEntries(prefix?: QueryKeyInput): QueryEntry[] { 175 + return [...this.entries.values()].filter((entry) => queryKeyStartsWith(entry.key, prefix)); 176 + } 177 + 178 + private fetchEntry<T>(entry: QueryEntry<T>, queryFn: QueryFn<T>): Promise<T> { 179 + if (entry.promise) return entry.promise; 180 + entry.queryFn = queryFn; 181 + const promise = Promise.resolve() 182 + .then(queryFn) 183 + .then((data) => { 184 + entry.data = data; 185 + entry.error = undefined; 186 + entry.updatedAt = Date.now(); 187 + entry.invalidated = false; 188 + atprotoQueryCache().set(UNTANGLED_QUERY_DID, UNTANGLED_QUERY_COLLECTION, entry.keyHash, { 189 + data, 190 + updatedAt: entry.updatedAt, 191 + }); 192 + return data; 193 + }) 194 + .catch((error) => { 195 + entry.error = toError(error); 196 + throw entry.error; 197 + }) 198 + .finally(() => { 199 + entry.promise = undefined; 200 + this.notify(entry); 201 + }); 202 + entry.promise = promise; 203 + this.notify(entry); 204 + return promise; 205 + } 206 + } 207 + 208 + const AtprotoQueryContext = createContext<QueryClient>(); 209 + export const defaultQueryClient = new QueryClient(); 210 + 211 + export function QueryClientProvider(props: { client?: QueryClient; children: JSX.Element }) { 212 + return ( 213 + <AtprotoQueryContext.Provider value={props.client ?? defaultQueryClient}> 214 + {props.children} 215 + </AtprotoQueryContext.Provider> 216 + ); 217 + } 218 + 219 + export function useQueryClient(): QueryClient { 220 + return useContext(AtprotoQueryContext) ?? defaultQueryClient; 221 + } 222 + 223 + const accessOptions = <T,>(options: QueryOptions<T> | Accessor<QueryOptions<T>>): QueryOptions<T> => 224 + typeof options === 'function' ? (options as Accessor<QueryOptions<T>>)() : options; 225 + 226 + export function createQuery<T = unknown>(options: QueryOptions<T> | Accessor<QueryOptions<T>>): QueryResult<T> { 227 + const client = useQueryClient(); 228 + const getOptions = createMemo(() => accessOptions(options)); 229 + const [version, setVersion] = createSignal(0); 230 + const [fetching, setFetching] = createSignal(false); 231 + let activeEntry: QueryEntry<T> | undefined; 232 + let unsubscribe: (() => void) | undefined; 233 + let token = 0; 234 + 235 + const bump = () => setVersion((value) => value + 1); 236 + 237 + createEffect(() => { 238 + const opts = getOptions(); 239 + const enabled = opts.enabled ?? true; 240 + const key = opts.queryKey; 241 + const entry = client.getOrCreateEntry<T>(key, opts.staleTime); 242 + entry.queryFn = opts.queryFn; 243 + activeEntry = entry; 244 + unsubscribe?.(); 245 + unsubscribe = client.subscribe(entry, bump); 246 + bump(); 247 + 248 + const runToken = ++token; 249 + if (!enabled) { 250 + setFetching(false); 251 + return; 252 + } 253 + if (entry.data !== undefined && !entry.invalidated && Date.now() - entry.updatedAt < entry.staleTime) { 254 + setFetching(false); 255 + return; 256 + } 257 + setFetching(true); 258 + client.fetchQuery({ queryKey: key, queryFn: opts.queryFn, staleTime: opts.staleTime }) 259 + .catch(() => undefined) 260 + .finally(() => { 261 + if (runToken === token) setFetching(false); 262 + }); 263 + }); 264 + 265 + onCleanup(() => unsubscribe?.()); 266 + 267 + const entry = () => { 268 + version(); 269 + return activeEntry; 270 + }; 271 + 272 + return { 273 + get data() { return entry()?.data as T | undefined; }, 274 + get error() { return entry()?.error; }, 275 + get isFetching() { return fetching() || !!entry()?.promise; }, 276 + get isLoading() { return (fetching() || !!entry()?.promise) && entry()?.data === undefined; }, 277 + get isPending() { return this.isLoading; }, 278 + get isError() { return !!entry()?.error; }, 279 + get isSuccess() { return entry()?.data !== undefined && !entry()?.error; }, 280 + get status() { return this.isError ? 'error' : this.isSuccess ? 'success' : 'pending'; }, 281 + refetch: async () => { 282 + const opts = getOptions(); 283 + setFetching(true); 284 + try { 285 + return await client.fetchQuery({ ...opts, staleTime: -1 }); 286 + } finally { 287 + setFetching(false); 288 + } 289 + }, 290 + }; 291 + } 292 + 293 + export const keepPreviousData = Symbol('keepPreviousData'); 294 + 295 + export { getSharedCaches };
+1 -1
src/lib/preloading.tsx
··· 1 - import { useQueryClient } from '@tanstack/solid-query'; 1 + import { useQueryClient } from '../lib/atproto-query'; 2 2 import { useAuth } from './auth'; 3 3 import { 4 4 getRepo,
+1 -1
src/pages/home.tsx
··· 14 14 } from 'lucide-solid'; 15 15 import clsx from 'clsx'; 16 16 import { A } from '@solidjs/router'; 17 - import { createQuery } from '@tanstack/solid-query'; 17 + import { createQuery } from '../lib/atproto-query'; 18 18 import { For, Show, createEffect, createMemo, createSignal, onCleanup, type Component, type JSX } from 'solid-js'; 19 19 20 20 import { Avatar, ErrorState, LoadingState, StateBadge } from '../components/common';
+15 -12
src/pages/profile.tsx
··· 20 20 LineSquiggle, 21 21 } from 'lucide-solid'; 22 22 import { A, useParams, useSearchParams, useNavigate } from '@solidjs/router'; 23 - import { createQuery, useQueryClient } from '@tanstack/solid-query'; 23 + import { createQuery, useQueryClient } from '../lib/atproto-query'; 24 24 import { For, Show, Switch, Match, createMemo, createSignal, createEffect, type Component } from 'solid-js'; 25 - import type { Did } from '@atcute/lexicons/syntax'; 25 + import type { Did, ResourceUri } from '@atcute/lexicons/syntax'; 26 26 import { ErrorState, LoadingState, PlaceholderAvatar, SkeletonBlock, textareaStyles, inputStyles } from '../components/common'; 27 27 import { RepoCard, RepoCardSkeleton, StringCardSkeleton } from '../components/repo'; 28 28 import { useStringPreloader } from '../lib/preloading'; ··· 40 40 type VouchRecord, 41 41 } from '../lib/api/graph'; 42 42 import { resolveActor, getActorProfile, resolveAvatarUrl, putActorProfile } from '../lib/api/identity'; 43 - import { listRepoRecords, getRepoByDid, type RepoContext } from '../lib/api/repos'; 43 + import { listRepoRecords, getRepoByDid, getRepoByUri, type RepoContext } from '../lib/api/repos'; 44 44 import { listStringRecords } from '../lib/api/strings'; 45 + import { listStarRecordsByActor } from '../lib/api/stars'; 46 + import { listAllAppviewRecords } from '../lib/api/appview'; 45 47 import { useAuth } from '../lib/auth'; 46 - import { listAllAppviewRecords } from '../lib/api/appview'; 47 48 import { formatRelativeTime, getErrorMessage } from '../lib/repo-utils'; 48 - import { ShTangledFeedStar, ShTangledActorProfile } from '@atcute/tangled'; 49 + import { ShTangledActorProfile } from '@atcute/tangled'; 49 50 50 51 const ProfileTabs: Component<{ 51 52 activeTab: string; ··· 1059 1060 queryKey: ['profile-stars', actorQuery.data?.did], 1060 1061 enabled: !!actorQuery.data, 1061 1062 queryFn: async () => { 1062 - const did = actorQuery.data!.did; 1063 - const starRecords = await listAllAppviewRecords<ShTangledFeedStar.Main>('sh.tangled.feed.listStarsBy', did); 1063 + const actor = actorQuery.data!; 1064 + const starRecords = await listStarRecordsByActor(actor); 1064 1065 const repos = await Promise.all( 1065 1066 starRecords.map(async (star) => { 1066 - const subject = star.value.subject as { did?: string; subjectDid?: string }; 1067 - const repoDid = (subject.did ?? subject.subjectDid) as Did; 1067 + const subject = star.value.subject as string | { did?: string; subjectDid?: string }; 1068 + const subjectUri = typeof subject === 'string' && subject.startsWith('at://') ? subject as ResourceUri : undefined; 1069 + const repoDid = typeof subject === 'object' ? (subject.did ?? subject.subjectDid) as Did | undefined : undefined; 1068 1070 try { 1069 - return await getRepoByDid(repoDid); 1071 + if (repoDid) return await getRepoByDid(repoDid); 1072 + if (subjectUri) return await getRepoByUri(subjectUri); 1070 1073 } catch (e) { 1071 - console.error('failed to resolve starred repo', repoDid, e); 1072 - return null; 1074 + console.error('failed to resolve starred repo', repoDid ?? subjectUri, e); 1073 1075 } 1076 + return null; 1074 1077 }) 1075 1078 ); 1076 1079 return repos.filter(Boolean) as RepoContext[];
+1 -1
src/pages/repo/code.tsx
··· 1 1 import clsx from 'clsx'; 2 2 import { File, Folder, GitBranch, GitCommitHorizontal, List } from 'lucide-solid'; 3 3 import { A, useNavigate, useParams } from '@solidjs/router'; 4 - import { createQuery, keepPreviousData, useQueryClient } from '@tanstack/solid-query'; 4 + import { createQuery, keepPreviousData, useQueryClient } from '../../lib/atproto-query'; 5 5 import { For, Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup, type Accessor, type Component } from 'solid-js'; 6 6 import { buildBlobDataUrl, decodeBlobBytes, decodeBlobText, getRepoBlob, getRepoBranches, getRepoDefaultBranch, getRepoLanguages, getRepoLog, getRepoTags, getRepoTree, type CommitHeadline, type RepoContext, type BranchEntry } from '../../lib/api/repos'; 7 7 import { Avatar, ErrorState, PlaceholderAvatar, buttonStyles, cardStyles } from '../../components/common';
+1 -1
src/pages/repo/commits.tsx
··· 7 7 FolderCode, 8 8 } from 'lucide-solid'; 9 9 import { A, useNavigate, useParams, useSearchParams } from '@solidjs/router'; 10 - import { createQuery } from '@tanstack/solid-query'; 10 + import { createQuery } from '../../lib/atproto-query'; 11 11 import { For, Match, Show, Switch, createEffect, createMemo, createSignal, type Component } from 'solid-js'; 12 12 import { 13 13 getRepoBranches,
+1 -1
src/pages/repo/fork.tsx
··· 1 1 import { A, useNavigate } from '@solidjs/router'; 2 - import { createQuery, useQueryClient } from '@tanstack/solid-query'; 2 + import { createQuery, useQueryClient } from '../../lib/atproto-query'; 3 3 import { GitFork, Info, LoaderCircle } from 'lucide-solid'; 4 4 import { For, Match, Show, Switch, createEffect, createMemo, createSignal, type Component } from 'solid-js'; 5 5
+1 -1
src/pages/repo/issues.tsx
··· 1 1 import clsx from 'clsx'; 2 2 import { Ban, CirclePlus, LoaderCircle, MessageSquarePlus, Pencil, RefreshCcwDot, Reply, Search, Smile, X } from 'lucide-solid'; 3 3 import { A, useNavigate, useParams, useSearchParams, useLocation } from '@solidjs/router'; 4 - import { createQuery, useQueryClient } from '@tanstack/solid-query'; 4 + import { createQuery, useQueryClient } from '../../lib/atproto-query'; 5 5 import type { ResourceUri } from '@atcute/lexicons/syntax'; 6 6 import { For, Match, Show, Switch, createEffect, createMemo, createSignal, type Component } from 'solid-js'; 7 7 import { createIssue, createIssueComment, getIssue, listIssuesPage, setIssueState, type IssueComment } from '../../lib/api/issues';
+1 -1
src/pages/repo/new.tsx
··· 1 1 import { A, useNavigate } from '@solidjs/router'; 2 - import { createQuery, useQueryClient } from '@tanstack/solid-query'; 2 + import { createQuery, useQueryClient } from '../../lib/atproto-query'; 3 3 import { BookPlus, LoaderCircle } from 'lucide-solid'; 4 4 import { For, Show, createEffect, createSignal, type Component } from 'solid-js'; 5 5
+1 -1
src/pages/repo/pulls.tsx
··· 22 22 } from 'lucide-solid'; 23 23 import type { Did } from '@atcute/lexicons/syntax'; 24 24 import { A, useNavigate, useParams, useSearchParams, useLocation } from '@solidjs/router'; 25 - import { createQuery, useQueryClient } from '@tanstack/solid-query'; 25 + import { createQuery, useQueryClient } from '../../lib/atproto-query'; 26 26 import { For, Match, Show, Switch, createEffect, createMemo, createSignal, type Component, type JSX } from 'solid-js'; 27 27 import { resolveActor } from '../../lib/api/identity'; 28 28 import { createPull, createPullComment, fetchPullRoundPatch, getPull, listPullsPage, mergePull, setPullStatus } from '../../lib/api/pulls';
+1 -1
src/pages/repo/shared.tsx
··· 1 1 import { CircleDot, GitFork, GitPullRequest, Globe, LoaderCircle, Rss, SquareChartGantt, Star } from 'lucide-solid'; 2 2 import type { Did } from '@atcute/lexicons/syntax'; 3 3 import { A, type RouteSectionProps, useParams } from '@solidjs/router'; 4 - import { createQuery } from '@tanstack/solid-query'; 4 + import { createQuery } from '../../lib/atproto-query'; 5 5 import { For, Match, Show, Switch, createContext, createEffect, createMemo, createSignal, onCleanup, useContext, type Component, type JSX } from 'solid-js'; 6 6 import { getRepoIssueCount } from '../../lib/api/issues'; 7 7 import { getRepoPullCount } from '../../lib/api/pulls';
+1 -1
src/pages/search.tsx
··· 13 13 LineSquiggle, 14 14 } from 'lucide-solid'; 15 15 import { A, useNavigate, useSearchParams } from '@solidjs/router'; 16 - import { createQuery } from '@tanstack/solid-query'; 16 + import { createQuery } from '../lib/atproto-query'; 17 17 import type { Did, ResourceUri } from '@atcute/lexicons/syntax'; 18 18 import { For, Match, Show, Switch, createEffect, createMemo, createSignal, type Component, type JSX } from 'solid-js'; 19 19
+1 -1
src/pages/settings.tsx
··· 10 10 } from 'solid-js'; 11 11 import { Portal } from 'solid-js/web'; 12 12 import { A, useNavigate, useParams, useLocation, type RouteSectionProps } from '@solidjs/router'; 13 - import { createQuery, useQueryClient } from '@tanstack/solid-query'; 13 + import { createQuery, useQueryClient } from '../lib/atproto-query'; 14 14 import { 15 15 Settings, 16 16 User,
+1 -1
src/pages/string.tsx
··· 1 1 import { A, useParams, useNavigate } from '@solidjs/router'; 2 - import { createQuery, useQueryClient } from '@tanstack/solid-query'; 2 + import { createQuery, useQueryClient } from '../lib/atproto-query'; 3 3 import { Show, Switch, Match, type Component, createSignal, createMemo, createEffect, onCleanup } from 'solid-js'; 4 4 import type { ResourceUri } from '@atcute/lexicons/syntax'; 5 5 import { Avatar, ErrorState, inputStyles, textareaStyles } from '../components/common';