+2
-1
.gitignore
+2
-1
.gitignore
+31
-4
README.md
+31
-4
README.md
···
1
# Red Dwarf
2
Red Dwarf is a Bluesky client that does not use any AppView servers, instead it gathers the data from [Constellation](https://constellation.microcosm.blue/) and each users' PDS.
3
4
-

5
6
huge thanks to [Microcosm](https://microcosm.blue/) for making this possible
7
8
## running dev and build
9
in the `vite.config.ts` file you should change these values
10
```ts
11
-
const PROD_URL = "https://reddwarf.whey.party"
12
const DEV_URL = "https://local3768forumtest.whey.party"
13
```
14
the PROD_URL is what will compile your oauth client metadata so it is very important to change that. same for DEV_URL if you are using a tunnel for dev work
15
16
run dev with `npm run dev` (port 3768) and build with `npm run build` (the output is the `dist` folder)
17
18
## useQuery
19
Red Dwarf has been upgraded from its original bespoke caching system to Tanstack Query (react query). this migration was done to achieve a more robust and maintainable approach to data fetching and caching and state synchronization. ive seen serious performance gains from this switch!
···
52
and for list feeds, you can just use something like graze or skyfeed to input a list of users and output a custom feed
53
54
## Tanstack Router
55
-
it does the job, nothing very specific was used here
56
57
-
im planning to use the loader system on select pages to prevent loss of scroll positon and state though its really complex so i havent done it yet but the migration to tanstack query is a huge first step towards this goal
···
1
# Red Dwarf
2
Red Dwarf is a Bluesky client that does not use any AppView servers, instead it gathers the data from [Constellation](https://constellation.microcosm.blue/) and each users' PDS.
3
4
+

5
6
huge thanks to [Microcosm](https://microcosm.blue/) for making this possible
7
8
## running dev and build
9
in the `vite.config.ts` file you should change these values
10
```ts
11
+
const PROD_URL = "https://reddwarf.app"
12
const DEV_URL = "https://local3768forumtest.whey.party"
13
```
14
the PROD_URL is what will compile your oauth client metadata so it is very important to change that. same for DEV_URL if you are using a tunnel for dev work
15
16
run dev with `npm run dev` (port 3768) and build with `npm run build` (the output is the `dist` folder)
17
+
18
+
19
+
20
+
you probably dont need to change these
21
+
```ts
22
+
const PROD_HANDLE_RESOLVER_PDS = "https://pds-nd.whey.party"
23
+
const DEV_HANDLE_RESOLVER_PDS = "https://bsky.social"
24
+
```
25
+
if you do want to change these, i recommend changing both of these to your own PDS url. i separate the prod and dev urls so that you can change it as needed. here i separated it because if the prod resolver and prod url shares the same domain itll error and prevent logins
26
27
## useQuery
28
Red Dwarf has been upgraded from its original bespoke caching system to Tanstack Query (react query). this migration was done to achieve a more robust and maintainable approach to data fetching and caching and state synchronization. ive seen serious performance gains from this switch!
···
61
and for list feeds, you can just use something like graze or skyfeed to input a list of users and output a custom feed
62
63
## Tanstack Router
64
+
something specific was used here
65
+
66
+
so tanstack router is used as the base, but the home route is using tanstack-router-keepalive to preserve the route for better responsiveness, and it also saves scroll position of feeds into jotai (persistent)
67
+
68
+
i previously used a tanstack router loader to ensure the tanstack query cache is ready to prevent scroll jumps but it is way too slow so i replaced it with tanstack-router-keepalive
69
+
70
+
## Icons
71
+
this project uses Material icons. do not the light variant. sometimes i use `Mdi` if the icon needed doesnt exist in `MaterialSymbols`
72
+
73
+
the project uses unplugin icon auto import, so you can just use the component and itll just work!
74
+
75
+
the format is:
76
+
```tsx
77
+
<IconMaterialSymbols{icon name here} />
78
+
// or
79
+
<IconMdi{icon name here} />
80
+
```
81
82
+
you can get the full list of icon names from iconify ([Material Symbols](https://icon-sets.iconify.design/material-symbols/) or [MDI](https://icon-sets.iconify.design/mdi/))
83
+
84
+
while it is nice to keep everything consistent by using material icons, if the icon you need is not provided by either material symbols nor mdi, you are allowed to just grab any icon from any pack (please do prioritize icons that fit in)
+62
-30
oauthdev.mts
+62
-30
oauthdev.mts
···
1
-
import fs from 'fs';
2
-
import path from 'path';
3
//import { generateClientMetadata } from './src/helpers/oauthClient'
4
export const generateClientMetadata = (appOrigin: string) => {
5
-
const callbackPath = '/callback';
6
7
return {
8
-
"client_id": `${appOrigin}/client-metadata.json`,
9
-
"client_name": "ForumTest",
10
-
"client_uri": appOrigin,
11
-
"logo_uri": `${appOrigin}/logo192.png`,
12
-
"tos_uri": `${appOrigin}/terms-of-service`,
13
-
"policy_uri": `${appOrigin}/privacy-policy`,
14
-
"redirect_uris": [`${appOrigin}${callbackPath}`] as [string, ...string[]],
15
-
"scope": "atproto transition:generic",
16
-
"grant_types": ["authorization_code", "refresh_token"] as ["authorization_code", "refresh_token"],
17
-
"response_types": ["code"] as ["code"],
18
-
"token_endpoint_auth_method": "none" as "none",
19
-
"application_type": "web" as "web",
20
-
"dpop_bound_access_tokens": true
21
-
};
22
-
}
23
-
24
25
-
export function generateMetadataPlugin({prod, dev}:{prod: string, dev: string}) {
26
return {
27
-
name: 'vite-plugin-generate-metadata',
28
config(_config: any, { mode }: any) {
29
-
let appOrigin;
30
-
if (mode === 'production') {
31
-
appOrigin = prod
32
-
if (!appOrigin || !appOrigin.startsWith('https://')) {
33
-
throw new Error('VITE_APP_ORIGIN environment variable must be set to a valid HTTPS URL for production build.');
34
}
35
} else {
36
appOrigin = dev;
37
}
38
-
39
-
40
const metadata = generateClientMetadata(appOrigin);
41
-
const outputPath = path.resolve(process.cwd(), 'public', 'client-metadata.json');
42
43
fs.writeFileSync(outputPath, JSON.stringify(metadata, null, 2));
44
45
// /*mass comment*/ console.log(`โ
Generated client-metadata.json for ${appOrigin}`);
46
},
47
};
48
-
}
···
1
+
import fs from "fs";
2
+
import path from "path";
3
//import { generateClientMetadata } from './src/helpers/oauthClient'
4
export const generateClientMetadata = (appOrigin: string) => {
5
+
const callbackPath = "/callback";
6
7
return {
8
+
client_id: `${appOrigin}/client-metadata.json`,
9
+
client_name: "ForumTest",
10
+
client_uri: appOrigin,
11
+
logo_uri: `${appOrigin}/logo192.png`,
12
+
tos_uri: `${appOrigin}/terms-of-service`,
13
+
policy_uri: `${appOrigin}/privacy-policy`,
14
+
redirect_uris: [`${appOrigin}${callbackPath}`] as [string, ...string[]],
15
+
scope: "atproto transition:generic",
16
+
grant_types: ["authorization_code", "refresh_token"] as [
17
+
"authorization_code",
18
+
"refresh_token",
19
+
],
20
+
response_types: ["code"] as ["code"],
21
+
token_endpoint_auth_method: "none" as "none",
22
+
application_type: "web" as "web",
23
+
dpop_bound_access_tokens: true,
24
+
};
25
+
};
26
27
+
export function generateMetadataPlugin({
28
+
prod,
29
+
dev,
30
+
prodResolver = "https://bsky.social",
31
+
devResolver = prodResolver,
32
+
}: {
33
+
prod: string;
34
+
dev: string;
35
+
prodResolver?: string;
36
+
devResolver?: string;
37
+
}) {
38
return {
39
+
name: "vite-plugin-generate-metadata",
40
config(_config: any, { mode }: any) {
41
+
console.log('๐ก vite mode =', mode)
42
+
let appOrigin, resolver;
43
+
if (mode === "production") {
44
+
appOrigin = prod;
45
+
resolver = prodResolver;
46
+
if (!appOrigin || !appOrigin.startsWith("https://")) {
47
+
throw new Error(
48
+
"VITE_APP_ORIGIN environment variable must be set to a valid HTTPS URL for production build."
49
+
);
50
}
51
} else {
52
appOrigin = dev;
53
+
resolver = devResolver;
54
}
55
+
56
const metadata = generateClientMetadata(appOrigin);
57
+
const outputPath = path.resolve(
58
+
process.cwd(),
59
+
"public",
60
+
"client-metadata.json"
61
+
);
62
63
fs.writeFileSync(outputPath, JSON.stringify(metadata, null, 2));
64
65
+
const resolvers = {
66
+
resolver: resolver,
67
+
};
68
+
const resolverOutPath = path.resolve(
69
+
process.cwd(),
70
+
"public",
71
+
"resolvers.json"
72
+
);
73
+
74
+
fs.writeFileSync(resolverOutPath, JSON.stringify(resolvers, null, 2));
75
+
76
+
77
// /*mass comment*/ console.log(`โ
Generated client-metadata.json for ${appOrigin}`);
78
},
79
};
80
+
}
+4990
-53
package-lock.json
+4990
-53
package-lock.json
···
8
"dependencies": {
9
"@atproto/api": "^0.16.6",
10
"@atproto/oauth-client-browser": "^0.3.33",
11
"@tailwindcss/vite": "^4.0.6",
12
"@tanstack/query-sync-storage-persister": "^5.85.6",
13
"@tanstack/react-devtools": "^0.2.2",
···
15
"@tanstack/react-query-persist-client": "^5.85.6",
16
"@tanstack/react-router": "^1.130.2",
17
"@tanstack/react-router-devtools": "^1.131.5",
18
-
"@tanstack/react-virtual": "^3.13.12",
19
"@tanstack/router-plugin": "^1.121.2",
20
"idb-keyval": "^6.2.2",
21
"jotai": "^2.13.1",
22
"react": "^19.0.0",
23
"react-dom": "^19.0.0",
24
"react-player": "^3.3.2",
25
-
"tailwindcss": "^4.0.6"
26
},
27
"devDependencies": {
28
"@eslint-react/eslint-plugin": "^2.2.1",
29
"@testing-library/dom": "^10.4.0",
30
"@testing-library/react": "^16.2.0",
31
"@types/node": "^24.3.0",
···
43
"prettier": "^3.6.2",
44
"typescript": "^5.7.2",
45
"typescript-eslint": "^8.46.1",
46
"vite": "^6.3.5",
47
"vitest": "^3.0.5",
48
"web-vitals": "^4.2.4"
···
59
},
60
"engines": {
61
"node": ">=6.0.0"
62
}
63
},
64
"node_modules/@asamuzakjp/css-color": {
···
1550
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1551
}
1552
},
1553
"node_modules/@humanfs/core": {
1554
"version": "0.19.1",
1555
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
···
1606
"url": "https://github.com/sponsors/nzakas"
1607
}
1608
},
1609
"node_modules/@isaacs/fs-minipass": {
1610
"version": "4.0.1",
1611
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
···
1769
"node": ">= 8"
1770
}
1771
},
1772
"node_modules/@rolldown/pluginutils": {
1773
"version": "1.0.0-beta.27",
1774
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
···
2083
"solid-js": "^1.6.12"
2084
}
2085
},
2086
"node_modules/@svta/common-media-library": {
2087
"version": "0.12.4",
2088
"resolved": "https://registry.npmjs.org/@svta/common-media-library/-/common-media-library-0.12.4.tgz",
···
2580
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
2581
}
2582
},
2583
-
"node_modules/@tanstack/react-virtual": {
2584
-
"version": "3.13.12",
2585
-
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
2586
-
"integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==",
2587
-
"license": "MIT",
2588
-
"dependencies": {
2589
-
"@tanstack/virtual-core": "3.13.12"
2590
-
},
2591
-
"funding": {
2592
-
"type": "github",
2593
-
"url": "https://github.com/sponsors/tannerlinsley"
2594
-
},
2595
-
"peerDependencies": {
2596
-
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
2597
-
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
2598
-
}
2599
-
},
2600
"node_modules/@tanstack/router-core": {
2601
"version": "1.131.28",
2602
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.131.28.tgz",
···
2749
"version": "0.7.4",
2750
"resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.7.4.tgz",
2751
"integrity": "sha512-F1XqZQici1Aq6WigEfcxJSml92nW+85Om8ElBMokPNg5glCYVOmPkZGIQeieYFxcPiKTfwo0MTOQpUyJtwncrg==",
2752
-
"license": "MIT",
2753
-
"funding": {
2754
-
"type": "github",
2755
-
"url": "https://github.com/sponsors/tannerlinsley"
2756
-
}
2757
-
},
2758
-
"node_modules/@tanstack/virtual-core": {
2759
-
"version": "3.13.12",
2760
-
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
2761
-
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
2762
"license": "MIT",
2763
"funding": {
2764
"type": "github",
···
2936
"peerDependencies": {
2937
"@types/react": "^19.0.0"
2938
}
2939
},
2940
"node_modules/@typescript-eslint/eslint-plugin": {
2941
"version": "8.46.1",
···
3461
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
3462
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
3463
"dev": true,
3464
-
"license": "Python-2.0",
3465
-
"peer": true
3466
},
3467
"node_modules/aria-query": {
3468
"version": "5.3.0",
···
3874
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
3875
"dev": true,
3876
"license": "MIT",
3877
-
"peer": true,
3878
"engines": {
3879
"node": ">=6"
3880
}
3881
},
3882
"node_modules/caniuse-lite": {
···
4069
"dev": true,
4070
"license": "MIT"
4071
},
4072
"node_modules/convert-source-map": {
4073
"version": "2.0.0",
4074
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
···
4092
"url": "https://opencollective.com/core-js"
4093
}
4094
},
4095
"node_modules/cross-spawn": {
4096
"version": "7.0.6",
4097
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
···
4231
}
4232
},
4233
"node_modules/debug": {
4234
-
"version": "4.4.1",
4235
-
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
4236
-
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
4237
"license": "MIT",
4238
"dependencies": {
4239
"ms": "^2.1.3"
···
4327
"node": ">=8"
4328
}
4329
},
4330
"node_modules/diff": {
4331
"version": "8.0.2",
4332
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz",
···
4356
"dev": true,
4357
"license": "MIT"
4358
},
4359
"node_modules/dunder-proto": {
4360
"version": "1.0.1",
4361
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
···
4401
},
4402
"funding": {
4403
"url": "https://github.com/fb55/entities?sponsor=1"
4404
}
4405
},
4406
"node_modules/es-abstract": {
···
5064
"node": ">=0.10.0"
5065
}
5066
},
5067
"node_modules/expect-type": {
5068
"version": "1.2.2",
5069
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
···
5073
"engines": {
5074
"node": ">=12.0.0"
5075
}
5076
},
5077
"node_modules/fast-deep-equal": {
5078
"version": "3.1.3",
···
5303
},
5304
"funding": {
5305
"url": "https://github.com/sponsors/ljharb"
5306
}
5307
},
5308
"node_modules/get-proto": {
···
5612
"node": ">= 14"
5613
}
5614
},
5615
"node_modules/iconv-lite": {
5616
"version": "0.6.3",
5617
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
···
5654
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
5655
"dev": true,
5656
"license": "MIT",
5657
-
"peer": true,
5658
"dependencies": {
5659
"parent-module": "^1.0.0",
5660
"resolve-from": "^4.0.0"
···
5743
"url": "https://github.com/sponsors/ljharb"
5744
}
5745
},
5746
"node_modules/is-async-function": {
5747
"version": "2.1.1",
5748
"resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
···
6266
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
6267
"dev": true,
6268
"license": "MIT",
6269
-
"peer": true,
6270
"dependencies": {
6271
"argparse": "^2.0.1"
6272
},
···
6333
"dev": true,
6334
"license": "MIT",
6335
"peer": true
6336
},
6337
"node_modules/json-schema-traverse": {
6338
"version": "0.4.1",
···
6388
"dependencies": {
6389
"json-buffer": "3.0.1"
6390
}
6391
},
6392
"node_modules/levn": {
6393
"version": "0.4.1",
···
6641
"url": "https://opencollective.com/parcel"
6642
}
6643
},
6644
"node_modules/localforage": {
6645
"version": "1.10.0",
6646
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
···
6666
"funding": {
6667
"url": "https://github.com/sponsors/sindresorhus"
6668
}
6669
},
6670
"node_modules/lodash.merge": {
6671
"version": "4.6.2",
···
6693
"integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
6694
"dev": true,
6695
"license": "MIT"
6696
},
6697
"node_modules/lru-cache": {
6698
"version": "5.1.1",
···
6714
}
6715
},
6716
"node_modules/magic-string": {
6717
-
"version": "0.30.18",
6718
-
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz",
6719
-
"integrity": "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==",
6720
"license": "MIT",
6721
"dependencies": {
6722
"@jridgewell/sourcemap-codec": "^1.5.5"
···
6821
"url": "https://github.com/sponsors/isaacs"
6822
}
6823
},
6824
"node_modules/ms": {
6825
"version": "2.1.3",
6826
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
···
6870
"dev": true,
6871
"license": "MIT"
6872
},
6873
"node_modules/node-releases": {
6874
"version": "2.0.19",
6875
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
···
6885
"node": ">=0.10.0"
6886
}
6887
},
6888
"node_modules/nwsapi": {
6889
"version": "2.2.21",
6890
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz",
···
7070
"url": "https://github.com/sponsors/sindresorhus"
7071
}
7072
},
7073
"node_modules/parent-module": {
7074
"version": "1.0.1",
7075
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
7076
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
7077
"dev": true,
7078
"license": "MIT",
7079
-
"peer": true,
7080
"dependencies": {
7081
"callsites": "^3.0.0"
7082
},
···
7084
"node": ">=6"
7085
}
7086
},
7087
"node_modules/parse5": {
7088
"version": "7.3.0",
7089
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
···
7131
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
7132
"dev": true,
7133
"license": "MIT"
7134
},
7135
"node_modules/pathe": {
7136
"version": "2.0.3",
···
7165
},
7166
"funding": {
7167
"url": "https://github.com/sponsors/jonschlinkert"
7168
}
7169
},
7170
"node_modules/player.style": {
···
7289
"node": ">=6"
7290
}
7291
},
7292
"node_modules/queue-microtask": {
7293
"version": "1.2.3",
7294
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
···
7310
],
7311
"license": "MIT"
7312
},
7313
"node_modules/react": {
7314
"version": "19.1.1",
7315
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
···
7371
"node": ">=0.10.0"
7372
}
7373
},
7374
"node_modules/readdirp": {
7375
"version": "3.6.0",
7376
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
···
7476
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
7477
"dev": true,
7478
"license": "MIT",
7479
-
"peer": true,
7480
"engines": {
7481
"node": ">=4"
7482
}
···
7658
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
7659
"license": "MIT"
7660
},
7661
"node_modules/semver": {
7662
"version": "6.3.1",
7663
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
···
7845
"dev": true,
7846
"license": "ISC"
7847
},
7848
"node_modules/solid-js": {
7849
"version": "1.9.9",
7850
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.9.tgz",
···
7854
"csstype": "^3.1.0",
7855
"seroval": "~1.3.0",
7856
"seroval-plugins": "~1.3.0"
7857
}
7858
},
7859
"node_modules/source-map": {
···
8028
}
8029
},
8030
"node_modules/strip-literal": {
8031
-
"version": "3.0.0",
8032
-
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz",
8033
-
"integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==",
8034
"dev": true,
8035
"license": "MIT",
8036
"dependencies": {
···
8080
"url": "https://github.com/sponsors/ljharb"
8081
}
8082
},
8083
"node_modules/symbol-tree": {
8084
"version": "3.2.4",
8085
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
···
8093
"integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==",
8094
"license": "MIT"
8095
},
8096
"node_modules/tapable": {
8097
"version": "2.2.3",
8098
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz",
···
8165
"license": "MIT"
8166
},
8167
"node_modules/tinyglobby": {
8168
-
"version": "0.2.14",
8169
-
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
8170
-
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
8171
"license": "MIT",
8172
"dependencies": {
8173
-
"fdir": "^6.4.4",
8174
-
"picomatch": "^4.0.2"
8175
},
8176
"engines": {
8177
"node": ">=12.0.0"
···
8549
"node": "*"
8550
}
8551
},
8552
"node_modules/uint8arrays": {
8553
"version": "3.0.0",
8554
"resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz",
···
8584
"devOptional": true,
8585
"license": "MIT"
8586
},
8587
"node_modules/unplugin": {
8588
-
"version": "2.3.9",
8589
-
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.9.tgz",
8590
-
"integrity": "sha512-2dcbZq6aprwXTkzptq3k5qm5B8cvpjG9ynPd5fyM2wDJuuF7PeUK64Sxf0d+X1ZyDOeGydbNzMqBSIVlH8GIfA==",
8591
"license": "MIT",
8592
"dependencies": {
8593
"@jridgewell/remapping": "^2.3.5",
···
8599
"node": ">=18.12.0"
8600
}
8601
},
8602
"node_modules/unplugin/node_modules/picomatch": {
8603
"version": "4.0.3",
8604
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
···
8650
"peer": true,
8651
"dependencies": {
8652
"punycode": "^2.1.0"
8653
}
8654
},
8655
"node_modules/use-sync-external-store": {
···
8
"dependencies": {
9
"@atproto/api": "^0.16.6",
10
"@atproto/oauth-client-browser": "^0.3.33",
11
+
"@radix-ui/react-dialog": "^1.1.15",
12
+
"@radix-ui/react-dropdown-menu": "^2.1.16",
13
+
"@radix-ui/react-hover-card": "^1.1.15",
14
+
"@radix-ui/react-slider": "^1.3.6",
15
"@tailwindcss/vite": "^4.0.6",
16
"@tanstack/query-sync-storage-persister": "^5.85.6",
17
"@tanstack/react-devtools": "^0.2.2",
···
19
"@tanstack/react-query-persist-client": "^5.85.6",
20
"@tanstack/react-router": "^1.130.2",
21
"@tanstack/react-router-devtools": "^1.131.5",
22
"@tanstack/router-plugin": "^1.121.2",
23
+
"dompurify": "^3.3.0",
24
+
"i": "^0.3.7",
25
"idb-keyval": "^6.2.2",
26
"jotai": "^2.13.1",
27
+
"npm": "^11.6.2",
28
+
"radix-ui": "^1.4.3",
29
"react": "^19.0.0",
30
"react-dom": "^19.0.0",
31
"react-player": "^3.3.2",
32
+
"sonner": "^2.0.7",
33
+
"tailwindcss": "^4.0.6",
34
+
"tanstack-router-keepalive": "^1.0.0"
35
},
36
"devDependencies": {
37
"@eslint-react/eslint-plugin": "^2.2.1",
38
+
"@iconify-icon/react": "^3.0.1",
39
+
"@iconify-json/material-symbols": "^1.2.42",
40
+
"@iconify-json/mdi": "^1.2.3",
41
+
"@iconify/json": "^2.2.396",
42
+
"@svgr/core": "^8.1.0",
43
+
"@svgr/plugin-jsx": "^8.1.0",
44
"@testing-library/dom": "^10.4.0",
45
"@testing-library/react": "^16.2.0",
46
"@types/node": "^24.3.0",
···
58
"prettier": "^3.6.2",
59
"typescript": "^5.7.2",
60
"typescript-eslint": "^8.46.1",
61
+
"unplugin-auto-import": "^20.2.0",
62
+
"unplugin-icons": "^22.4.2",
63
"vite": "^6.3.5",
64
"vitest": "^3.0.5",
65
"web-vitals": "^4.2.4"
···
76
},
77
"engines": {
78
"node": ">=6.0.0"
79
+
}
80
+
},
81
+
"node_modules/@antfu/install-pkg": {
82
+
"version": "1.1.0",
83
+
"resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz",
84
+
"integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==",
85
+
"dev": true,
86
+
"license": "MIT",
87
+
"dependencies": {
88
+
"package-manager-detector": "^1.3.0",
89
+
"tinyexec": "^1.0.1"
90
+
},
91
+
"funding": {
92
+
"url": "https://github.com/sponsors/antfu"
93
+
}
94
+
},
95
+
"node_modules/@antfu/install-pkg/node_modules/tinyexec": {
96
+
"version": "1.0.1",
97
+
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz",
98
+
"integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==",
99
+
"dev": true,
100
+
"license": "MIT"
101
+
},
102
+
"node_modules/@antfu/utils": {
103
+
"version": "9.3.0",
104
+
"resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-9.3.0.tgz",
105
+
"integrity": "sha512-9hFT4RauhcUzqOE4f1+frMKLZrgNog5b06I7VmZQV1BkvwvqrbC8EBZf3L1eEL2AKb6rNKjER0sEvJiSP1FXEA==",
106
+
"dev": true,
107
+
"license": "MIT",
108
+
"funding": {
109
+
"url": "https://github.com/sponsors/antfu"
110
}
111
},
112
"node_modules/@asamuzakjp/css-color": {
···
1598
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1599
}
1600
},
1601
+
"node_modules/@floating-ui/core": {
1602
+
"version": "1.7.3",
1603
+
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
1604
+
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
1605
+
"dependencies": {
1606
+
"@floating-ui/utils": "^0.2.10"
1607
+
}
1608
+
},
1609
+
"node_modules/@floating-ui/dom": {
1610
+
"version": "1.7.4",
1611
+
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
1612
+
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
1613
+
"dependencies": {
1614
+
"@floating-ui/core": "^1.7.3",
1615
+
"@floating-ui/utils": "^0.2.10"
1616
+
}
1617
+
},
1618
+
"node_modules/@floating-ui/react-dom": {
1619
+
"version": "2.1.6",
1620
+
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
1621
+
"integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
1622
+
"dependencies": {
1623
+
"@floating-ui/dom": "^1.7.4"
1624
+
},
1625
+
"peerDependencies": {
1626
+
"react": ">=16.8.0",
1627
+
"react-dom": ">=16.8.0"
1628
+
}
1629
+
},
1630
+
"node_modules/@floating-ui/utils": {
1631
+
"version": "0.2.10",
1632
+
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
1633
+
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="
1634
+
},
1635
"node_modules/@humanfs/core": {
1636
"version": "0.19.1",
1637
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
···
1688
"url": "https://github.com/sponsors/nzakas"
1689
}
1690
},
1691
+
"node_modules/@iconify-icon/react": {
1692
+
"version": "3.0.1",
1693
+
"resolved": "https://registry.npmjs.org/@iconify-icon/react/-/react-3.0.1.tgz",
1694
+
"integrity": "sha512-/4CAVpk8HDyKS78r1G0rZhML7hI6jLxb8kAmjEXsCtuVUDwdGqicGCRg0T14mqeHNImrQPR49MhbuSSS++JlUA==",
1695
+
"dev": true,
1696
+
"license": "MIT",
1697
+
"dependencies": {
1698
+
"iconify-icon": "^3.0.1"
1699
+
},
1700
+
"funding": {
1701
+
"url": "https://github.com/sponsors/cyberalien"
1702
+
},
1703
+
"peerDependencies": {
1704
+
"react": ">=16"
1705
+
}
1706
+
},
1707
+
"node_modules/@iconify-json/material-symbols": {
1708
+
"version": "1.2.42",
1709
+
"resolved": "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.2.42.tgz",
1710
+
"integrity": "sha512-FDRfnQqy8iXaq/swVPFWaHftqP9tk3qDCRhC30s3UZL2j4mvGZk5gVECRXCkZv5jnsAiTpZxGQM8HrMiwE7GtA==",
1711
+
"dev": true,
1712
+
"license": "Apache-2.0",
1713
+
"dependencies": {
1714
+
"@iconify/types": "*"
1715
+
}
1716
+
},
1717
+
"node_modules/@iconify-json/mdi": {
1718
+
"version": "1.2.3",
1719
+
"resolved": "https://registry.npmjs.org/@iconify-json/mdi/-/mdi-1.2.3.tgz",
1720
+
"integrity": "sha512-O3cLwbDOK7NNDf2ihaQOH5F9JglnulNDFV7WprU2dSoZu3h3cWH//h74uQAB87brHmvFVxIOkuBX2sZSzYhScg==",
1721
+
"dev": true,
1722
+
"license": "Apache-2.0",
1723
+
"dependencies": {
1724
+
"@iconify/types": "*"
1725
+
}
1726
+
},
1727
+
"node_modules/@iconify/json": {
1728
+
"version": "2.2.396",
1729
+
"resolved": "https://registry.npmjs.org/@iconify/json/-/json-2.2.396.tgz",
1730
+
"integrity": "sha512-tijg77JFuYIt32S9N8p7La8C0zp9zKZsX6UP8ip5GVB1F6Mp3pZA5Vc5eAquTY50NoDJX58U6z4Qn3d6Wyossg==",
1731
+
"dev": true,
1732
+
"license": "MIT",
1733
+
"dependencies": {
1734
+
"@iconify/types": "*",
1735
+
"pathe": "^2.0.0"
1736
+
}
1737
+
},
1738
+
"node_modules/@iconify/types": {
1739
+
"version": "2.0.0",
1740
+
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
1741
+
"integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
1742
+
"dev": true,
1743
+
"license": "MIT"
1744
+
},
1745
+
"node_modules/@iconify/utils": {
1746
+
"version": "3.0.2",
1747
+
"resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.0.2.tgz",
1748
+
"integrity": "sha512-EfJS0rLfVuRuJRn4psJHtK2A9TqVnkxPpHY6lYHiB9+8eSuudsxbwMiavocG45ujOo6FJ+CIRlRnlOGinzkaGQ==",
1749
+
"dev": true,
1750
+
"license": "MIT",
1751
+
"dependencies": {
1752
+
"@antfu/install-pkg": "^1.1.0",
1753
+
"@antfu/utils": "^9.2.0",
1754
+
"@iconify/types": "^2.0.0",
1755
+
"debug": "^4.4.1",
1756
+
"globals": "^15.15.0",
1757
+
"kolorist": "^1.8.0",
1758
+
"local-pkg": "^1.1.1",
1759
+
"mlly": "^1.7.4"
1760
+
}
1761
+
},
1762
+
"node_modules/@iconify/utils/node_modules/globals": {
1763
+
"version": "15.15.0",
1764
+
"resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
1765
+
"integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==",
1766
+
"dev": true,
1767
+
"license": "MIT",
1768
+
"engines": {
1769
+
"node": ">=18"
1770
+
},
1771
+
"funding": {
1772
+
"url": "https://github.com/sponsors/sindresorhus"
1773
+
}
1774
+
},
1775
"node_modules/@isaacs/fs-minipass": {
1776
"version": "4.0.1",
1777
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
···
1935
"node": ">= 8"
1936
}
1937
},
1938
+
"node_modules/@radix-ui/number": {
1939
+
"version": "1.1.1",
1940
+
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
1941
+
"integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="
1942
+
},
1943
+
"node_modules/@radix-ui/primitive": {
1944
+
"version": "1.1.3",
1945
+
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
1946
+
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="
1947
+
},
1948
+
"node_modules/@radix-ui/react-accessible-icon": {
1949
+
"version": "1.1.7",
1950
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz",
1951
+
"integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==",
1952
+
"dependencies": {
1953
+
"@radix-ui/react-visually-hidden": "1.2.3"
1954
+
},
1955
+
"peerDependencies": {
1956
+
"@types/react": "*",
1957
+
"@types/react-dom": "*",
1958
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1959
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1960
+
},
1961
+
"peerDependenciesMeta": {
1962
+
"@types/react": {
1963
+
"optional": true
1964
+
},
1965
+
"@types/react-dom": {
1966
+
"optional": true
1967
+
}
1968
+
}
1969
+
},
1970
+
"node_modules/@radix-ui/react-accordion": {
1971
+
"version": "1.2.12",
1972
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz",
1973
+
"integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==",
1974
+
"dependencies": {
1975
+
"@radix-ui/primitive": "1.1.3",
1976
+
"@radix-ui/react-collapsible": "1.1.12",
1977
+
"@radix-ui/react-collection": "1.1.7",
1978
+
"@radix-ui/react-compose-refs": "1.1.2",
1979
+
"@radix-ui/react-context": "1.1.2",
1980
+
"@radix-ui/react-direction": "1.1.1",
1981
+
"@radix-ui/react-id": "1.1.1",
1982
+
"@radix-ui/react-primitive": "2.1.3",
1983
+
"@radix-ui/react-use-controllable-state": "1.2.2"
1984
+
},
1985
+
"peerDependencies": {
1986
+
"@types/react": "*",
1987
+
"@types/react-dom": "*",
1988
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1989
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1990
+
},
1991
+
"peerDependenciesMeta": {
1992
+
"@types/react": {
1993
+
"optional": true
1994
+
},
1995
+
"@types/react-dom": {
1996
+
"optional": true
1997
+
}
1998
+
}
1999
+
},
2000
+
"node_modules/@radix-ui/react-alert-dialog": {
2001
+
"version": "1.1.15",
2002
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz",
2003
+
"integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==",
2004
+
"dependencies": {
2005
+
"@radix-ui/primitive": "1.1.3",
2006
+
"@radix-ui/react-compose-refs": "1.1.2",
2007
+
"@radix-ui/react-context": "1.1.2",
2008
+
"@radix-ui/react-dialog": "1.1.15",
2009
+
"@radix-ui/react-primitive": "2.1.3",
2010
+
"@radix-ui/react-slot": "1.2.3"
2011
+
},
2012
+
"peerDependencies": {
2013
+
"@types/react": "*",
2014
+
"@types/react-dom": "*",
2015
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2016
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2017
+
},
2018
+
"peerDependenciesMeta": {
2019
+
"@types/react": {
2020
+
"optional": true
2021
+
},
2022
+
"@types/react-dom": {
2023
+
"optional": true
2024
+
}
2025
+
}
2026
+
},
2027
+
"node_modules/@radix-ui/react-arrow": {
2028
+
"version": "1.1.7",
2029
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
2030
+
"integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
2031
+
"dependencies": {
2032
+
"@radix-ui/react-primitive": "2.1.3"
2033
+
},
2034
+
"peerDependencies": {
2035
+
"@types/react": "*",
2036
+
"@types/react-dom": "*",
2037
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2038
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2039
+
},
2040
+
"peerDependenciesMeta": {
2041
+
"@types/react": {
2042
+
"optional": true
2043
+
},
2044
+
"@types/react-dom": {
2045
+
"optional": true
2046
+
}
2047
+
}
2048
+
},
2049
+
"node_modules/@radix-ui/react-aspect-ratio": {
2050
+
"version": "1.1.7",
2051
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz",
2052
+
"integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==",
2053
+
"dependencies": {
2054
+
"@radix-ui/react-primitive": "2.1.3"
2055
+
},
2056
+
"peerDependencies": {
2057
+
"@types/react": "*",
2058
+
"@types/react-dom": "*",
2059
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2060
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2061
+
},
2062
+
"peerDependenciesMeta": {
2063
+
"@types/react": {
2064
+
"optional": true
2065
+
},
2066
+
"@types/react-dom": {
2067
+
"optional": true
2068
+
}
2069
+
}
2070
+
},
2071
+
"node_modules/@radix-ui/react-avatar": {
2072
+
"version": "1.1.10",
2073
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz",
2074
+
"integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==",
2075
+
"dependencies": {
2076
+
"@radix-ui/react-context": "1.1.2",
2077
+
"@radix-ui/react-primitive": "2.1.3",
2078
+
"@radix-ui/react-use-callback-ref": "1.1.1",
2079
+
"@radix-ui/react-use-is-hydrated": "0.1.0",
2080
+
"@radix-ui/react-use-layout-effect": "1.1.1"
2081
+
},
2082
+
"peerDependencies": {
2083
+
"@types/react": "*",
2084
+
"@types/react-dom": "*",
2085
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2086
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2087
+
},
2088
+
"peerDependenciesMeta": {
2089
+
"@types/react": {
2090
+
"optional": true
2091
+
},
2092
+
"@types/react-dom": {
2093
+
"optional": true
2094
+
}
2095
+
}
2096
+
},
2097
+
"node_modules/@radix-ui/react-checkbox": {
2098
+
"version": "1.3.3",
2099
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz",
2100
+
"integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==",
2101
+
"dependencies": {
2102
+
"@radix-ui/primitive": "1.1.3",
2103
+
"@radix-ui/react-compose-refs": "1.1.2",
2104
+
"@radix-ui/react-context": "1.1.2",
2105
+
"@radix-ui/react-presence": "1.1.5",
2106
+
"@radix-ui/react-primitive": "2.1.3",
2107
+
"@radix-ui/react-use-controllable-state": "1.2.2",
2108
+
"@radix-ui/react-use-previous": "1.1.1",
2109
+
"@radix-ui/react-use-size": "1.1.1"
2110
+
},
2111
+
"peerDependencies": {
2112
+
"@types/react": "*",
2113
+
"@types/react-dom": "*",
2114
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2115
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2116
+
},
2117
+
"peerDependenciesMeta": {
2118
+
"@types/react": {
2119
+
"optional": true
2120
+
},
2121
+
"@types/react-dom": {
2122
+
"optional": true
2123
+
}
2124
+
}
2125
+
},
2126
+
"node_modules/@radix-ui/react-collapsible": {
2127
+
"version": "1.1.12",
2128
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz",
2129
+
"integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==",
2130
+
"dependencies": {
2131
+
"@radix-ui/primitive": "1.1.3",
2132
+
"@radix-ui/react-compose-refs": "1.1.2",
2133
+
"@radix-ui/react-context": "1.1.2",
2134
+
"@radix-ui/react-id": "1.1.1",
2135
+
"@radix-ui/react-presence": "1.1.5",
2136
+
"@radix-ui/react-primitive": "2.1.3",
2137
+
"@radix-ui/react-use-controllable-state": "1.2.2",
2138
+
"@radix-ui/react-use-layout-effect": "1.1.1"
2139
+
},
2140
+
"peerDependencies": {
2141
+
"@types/react": "*",
2142
+
"@types/react-dom": "*",
2143
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2144
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2145
+
},
2146
+
"peerDependenciesMeta": {
2147
+
"@types/react": {
2148
+
"optional": true
2149
+
},
2150
+
"@types/react-dom": {
2151
+
"optional": true
2152
+
}
2153
+
}
2154
+
},
2155
+
"node_modules/@radix-ui/react-collection": {
2156
+
"version": "1.1.7",
2157
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
2158
+
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
2159
+
"dependencies": {
2160
+
"@radix-ui/react-compose-refs": "1.1.2",
2161
+
"@radix-ui/react-context": "1.1.2",
2162
+
"@radix-ui/react-primitive": "2.1.3",
2163
+
"@radix-ui/react-slot": "1.2.3"
2164
+
},
2165
+
"peerDependencies": {
2166
+
"@types/react": "*",
2167
+
"@types/react-dom": "*",
2168
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2169
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2170
+
},
2171
+
"peerDependenciesMeta": {
2172
+
"@types/react": {
2173
+
"optional": true
2174
+
},
2175
+
"@types/react-dom": {
2176
+
"optional": true
2177
+
}
2178
+
}
2179
+
},
2180
+
"node_modules/@radix-ui/react-compose-refs": {
2181
+
"version": "1.1.2",
2182
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
2183
+
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
2184
+
"peerDependencies": {
2185
+
"@types/react": "*",
2186
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2187
+
},
2188
+
"peerDependenciesMeta": {
2189
+
"@types/react": {
2190
+
"optional": true
2191
+
}
2192
+
}
2193
+
},
2194
+
"node_modules/@radix-ui/react-context": {
2195
+
"version": "1.1.2",
2196
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
2197
+
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
2198
+
"peerDependencies": {
2199
+
"@types/react": "*",
2200
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2201
+
},
2202
+
"peerDependenciesMeta": {
2203
+
"@types/react": {
2204
+
"optional": true
2205
+
}
2206
+
}
2207
+
},
2208
+
"node_modules/@radix-ui/react-context-menu": {
2209
+
"version": "2.2.16",
2210
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz",
2211
+
"integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==",
2212
+
"dependencies": {
2213
+
"@radix-ui/primitive": "1.1.3",
2214
+
"@radix-ui/react-context": "1.1.2",
2215
+
"@radix-ui/react-menu": "2.1.16",
2216
+
"@radix-ui/react-primitive": "2.1.3",
2217
+
"@radix-ui/react-use-callback-ref": "1.1.1",
2218
+
"@radix-ui/react-use-controllable-state": "1.2.2"
2219
+
},
2220
+
"peerDependencies": {
2221
+
"@types/react": "*",
2222
+
"@types/react-dom": "*",
2223
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2224
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2225
+
},
2226
+
"peerDependenciesMeta": {
2227
+
"@types/react": {
2228
+
"optional": true
2229
+
},
2230
+
"@types/react-dom": {
2231
+
"optional": true
2232
+
}
2233
+
}
2234
+
},
2235
+
"node_modules/@radix-ui/react-dialog": {
2236
+
"version": "1.1.15",
2237
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
2238
+
"integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
2239
+
"dependencies": {
2240
+
"@radix-ui/primitive": "1.1.3",
2241
+
"@radix-ui/react-compose-refs": "1.1.2",
2242
+
"@radix-ui/react-context": "1.1.2",
2243
+
"@radix-ui/react-dismissable-layer": "1.1.11",
2244
+
"@radix-ui/react-focus-guards": "1.1.3",
2245
+
"@radix-ui/react-focus-scope": "1.1.7",
2246
+
"@radix-ui/react-id": "1.1.1",
2247
+
"@radix-ui/react-portal": "1.1.9",
2248
+
"@radix-ui/react-presence": "1.1.5",
2249
+
"@radix-ui/react-primitive": "2.1.3",
2250
+
"@radix-ui/react-slot": "1.2.3",
2251
+
"@radix-ui/react-use-controllable-state": "1.2.2",
2252
+
"aria-hidden": "^1.2.4",
2253
+
"react-remove-scroll": "^2.6.3"
2254
+
},
2255
+
"peerDependencies": {
2256
+
"@types/react": "*",
2257
+
"@types/react-dom": "*",
2258
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2259
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2260
+
},
2261
+
"peerDependenciesMeta": {
2262
+
"@types/react": {
2263
+
"optional": true
2264
+
},
2265
+
"@types/react-dom": {
2266
+
"optional": true
2267
+
}
2268
+
}
2269
+
},
2270
+
"node_modules/@radix-ui/react-direction": {
2271
+
"version": "1.1.1",
2272
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
2273
+
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
2274
+
"peerDependencies": {
2275
+
"@types/react": "*",
2276
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2277
+
},
2278
+
"peerDependenciesMeta": {
2279
+
"@types/react": {
2280
+
"optional": true
2281
+
}
2282
+
}
2283
+
},
2284
+
"node_modules/@radix-ui/react-dismissable-layer": {
2285
+
"version": "1.1.11",
2286
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
2287
+
"integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
2288
+
"dependencies": {
2289
+
"@radix-ui/primitive": "1.1.3",
2290
+
"@radix-ui/react-compose-refs": "1.1.2",
2291
+
"@radix-ui/react-primitive": "2.1.3",
2292
+
"@radix-ui/react-use-callback-ref": "1.1.1",
2293
+
"@radix-ui/react-use-escape-keydown": "1.1.1"
2294
+
},
2295
+
"peerDependencies": {
2296
+
"@types/react": "*",
2297
+
"@types/react-dom": "*",
2298
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2299
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2300
+
},
2301
+
"peerDependenciesMeta": {
2302
+
"@types/react": {
2303
+
"optional": true
2304
+
},
2305
+
"@types/react-dom": {
2306
+
"optional": true
2307
+
}
2308
+
}
2309
+
},
2310
+
"node_modules/@radix-ui/react-dropdown-menu": {
2311
+
"version": "2.1.16",
2312
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz",
2313
+
"integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==",
2314
+
"dependencies": {
2315
+
"@radix-ui/primitive": "1.1.3",
2316
+
"@radix-ui/react-compose-refs": "1.1.2",
2317
+
"@radix-ui/react-context": "1.1.2",
2318
+
"@radix-ui/react-id": "1.1.1",
2319
+
"@radix-ui/react-menu": "2.1.16",
2320
+
"@radix-ui/react-primitive": "2.1.3",
2321
+
"@radix-ui/react-use-controllable-state": "1.2.2"
2322
+
},
2323
+
"peerDependencies": {
2324
+
"@types/react": "*",
2325
+
"@types/react-dom": "*",
2326
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2327
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2328
+
},
2329
+
"peerDependenciesMeta": {
2330
+
"@types/react": {
2331
+
"optional": true
2332
+
},
2333
+
"@types/react-dom": {
2334
+
"optional": true
2335
+
}
2336
+
}
2337
+
},
2338
+
"node_modules/@radix-ui/react-focus-guards": {
2339
+
"version": "1.1.3",
2340
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
2341
+
"integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
2342
+
"peerDependencies": {
2343
+
"@types/react": "*",
2344
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2345
+
},
2346
+
"peerDependenciesMeta": {
2347
+
"@types/react": {
2348
+
"optional": true
2349
+
}
2350
+
}
2351
+
},
2352
+
"node_modules/@radix-ui/react-focus-scope": {
2353
+
"version": "1.1.7",
2354
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
2355
+
"integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
2356
+
"dependencies": {
2357
+
"@radix-ui/react-compose-refs": "1.1.2",
2358
+
"@radix-ui/react-primitive": "2.1.3",
2359
+
"@radix-ui/react-use-callback-ref": "1.1.1"
2360
+
},
2361
+
"peerDependencies": {
2362
+
"@types/react": "*",
2363
+
"@types/react-dom": "*",
2364
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2365
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2366
+
},
2367
+
"peerDependenciesMeta": {
2368
+
"@types/react": {
2369
+
"optional": true
2370
+
},
2371
+
"@types/react-dom": {
2372
+
"optional": true
2373
+
}
2374
+
}
2375
+
},
2376
+
"node_modules/@radix-ui/react-form": {
2377
+
"version": "0.1.8",
2378
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz",
2379
+
"integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==",
2380
+
"dependencies": {
2381
+
"@radix-ui/primitive": "1.1.3",
2382
+
"@radix-ui/react-compose-refs": "1.1.2",
2383
+
"@radix-ui/react-context": "1.1.2",
2384
+
"@radix-ui/react-id": "1.1.1",
2385
+
"@radix-ui/react-label": "2.1.7",
2386
+
"@radix-ui/react-primitive": "2.1.3"
2387
+
},
2388
+
"peerDependencies": {
2389
+
"@types/react": "*",
2390
+
"@types/react-dom": "*",
2391
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2392
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2393
+
},
2394
+
"peerDependenciesMeta": {
2395
+
"@types/react": {
2396
+
"optional": true
2397
+
},
2398
+
"@types/react-dom": {
2399
+
"optional": true
2400
+
}
2401
+
}
2402
+
},
2403
+
"node_modules/@radix-ui/react-hover-card": {
2404
+
"version": "1.1.15",
2405
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz",
2406
+
"integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==",
2407
+
"license": "MIT",
2408
+
"dependencies": {
2409
+
"@radix-ui/primitive": "1.1.3",
2410
+
"@radix-ui/react-compose-refs": "1.1.2",
2411
+
"@radix-ui/react-context": "1.1.2",
2412
+
"@radix-ui/react-dismissable-layer": "1.1.11",
2413
+
"@radix-ui/react-popper": "1.2.8",
2414
+
"@radix-ui/react-portal": "1.1.9",
2415
+
"@radix-ui/react-presence": "1.1.5",
2416
+
"@radix-ui/react-primitive": "2.1.3",
2417
+
"@radix-ui/react-use-controllable-state": "1.2.2"
2418
+
},
2419
+
"peerDependencies": {
2420
+
"@types/react": "*",
2421
+
"@types/react-dom": "*",
2422
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2423
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2424
+
},
2425
+
"peerDependenciesMeta": {
2426
+
"@types/react": {
2427
+
"optional": true
2428
+
},
2429
+
"@types/react-dom": {
2430
+
"optional": true
2431
+
}
2432
+
}
2433
+
},
2434
+
"node_modules/@radix-ui/react-id": {
2435
+
"version": "1.1.1",
2436
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
2437
+
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
2438
+
"dependencies": {
2439
+
"@radix-ui/react-use-layout-effect": "1.1.1"
2440
+
},
2441
+
"peerDependencies": {
2442
+
"@types/react": "*",
2443
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2444
+
},
2445
+
"peerDependenciesMeta": {
2446
+
"@types/react": {
2447
+
"optional": true
2448
+
}
2449
+
}
2450
+
},
2451
+
"node_modules/@radix-ui/react-label": {
2452
+
"version": "2.1.7",
2453
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz",
2454
+
"integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==",
2455
+
"dependencies": {
2456
+
"@radix-ui/react-primitive": "2.1.3"
2457
+
},
2458
+
"peerDependencies": {
2459
+
"@types/react": "*",
2460
+
"@types/react-dom": "*",
2461
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2462
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2463
+
},
2464
+
"peerDependenciesMeta": {
2465
+
"@types/react": {
2466
+
"optional": true
2467
+
},
2468
+
"@types/react-dom": {
2469
+
"optional": true
2470
+
}
2471
+
}
2472
+
},
2473
+
"node_modules/@radix-ui/react-menu": {
2474
+
"version": "2.1.16",
2475
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz",
2476
+
"integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==",
2477
+
"dependencies": {
2478
+
"@radix-ui/primitive": "1.1.3",
2479
+
"@radix-ui/react-collection": "1.1.7",
2480
+
"@radix-ui/react-compose-refs": "1.1.2",
2481
+
"@radix-ui/react-context": "1.1.2",
2482
+
"@radix-ui/react-direction": "1.1.1",
2483
+
"@radix-ui/react-dismissable-layer": "1.1.11",
2484
+
"@radix-ui/react-focus-guards": "1.1.3",
2485
+
"@radix-ui/react-focus-scope": "1.1.7",
2486
+
"@radix-ui/react-id": "1.1.1",
2487
+
"@radix-ui/react-popper": "1.2.8",
2488
+
"@radix-ui/react-portal": "1.1.9",
2489
+
"@radix-ui/react-presence": "1.1.5",
2490
+
"@radix-ui/react-primitive": "2.1.3",
2491
+
"@radix-ui/react-roving-focus": "1.1.11",
2492
+
"@radix-ui/react-slot": "1.2.3",
2493
+
"@radix-ui/react-use-callback-ref": "1.1.1",
2494
+
"aria-hidden": "^1.2.4",
2495
+
"react-remove-scroll": "^2.6.3"
2496
+
},
2497
+
"peerDependencies": {
2498
+
"@types/react": "*",
2499
+
"@types/react-dom": "*",
2500
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2501
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2502
+
},
2503
+
"peerDependenciesMeta": {
2504
+
"@types/react": {
2505
+
"optional": true
2506
+
},
2507
+
"@types/react-dom": {
2508
+
"optional": true
2509
+
}
2510
+
}
2511
+
},
2512
+
"node_modules/@radix-ui/react-menubar": {
2513
+
"version": "1.1.16",
2514
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz",
2515
+
"integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==",
2516
+
"dependencies": {
2517
+
"@radix-ui/primitive": "1.1.3",
2518
+
"@radix-ui/react-collection": "1.1.7",
2519
+
"@radix-ui/react-compose-refs": "1.1.2",
2520
+
"@radix-ui/react-context": "1.1.2",
2521
+
"@radix-ui/react-direction": "1.1.1",
2522
+
"@radix-ui/react-id": "1.1.1",
2523
+
"@radix-ui/react-menu": "2.1.16",
2524
+
"@radix-ui/react-primitive": "2.1.3",
2525
+
"@radix-ui/react-roving-focus": "1.1.11",
2526
+
"@radix-ui/react-use-controllable-state": "1.2.2"
2527
+
},
2528
+
"peerDependencies": {
2529
+
"@types/react": "*",
2530
+
"@types/react-dom": "*",
2531
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2532
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2533
+
},
2534
+
"peerDependenciesMeta": {
2535
+
"@types/react": {
2536
+
"optional": true
2537
+
},
2538
+
"@types/react-dom": {
2539
+
"optional": true
2540
+
}
2541
+
}
2542
+
},
2543
+
"node_modules/@radix-ui/react-navigation-menu": {
2544
+
"version": "1.2.14",
2545
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz",
2546
+
"integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==",
2547
+
"dependencies": {
2548
+
"@radix-ui/primitive": "1.1.3",
2549
+
"@radix-ui/react-collection": "1.1.7",
2550
+
"@radix-ui/react-compose-refs": "1.1.2",
2551
+
"@radix-ui/react-context": "1.1.2",
2552
+
"@radix-ui/react-direction": "1.1.1",
2553
+
"@radix-ui/react-dismissable-layer": "1.1.11",
2554
+
"@radix-ui/react-id": "1.1.1",
2555
+
"@radix-ui/react-presence": "1.1.5",
2556
+
"@radix-ui/react-primitive": "2.1.3",
2557
+
"@radix-ui/react-use-callback-ref": "1.1.1",
2558
+
"@radix-ui/react-use-controllable-state": "1.2.2",
2559
+
"@radix-ui/react-use-layout-effect": "1.1.1",
2560
+
"@radix-ui/react-use-previous": "1.1.1",
2561
+
"@radix-ui/react-visually-hidden": "1.2.3"
2562
+
},
2563
+
"peerDependencies": {
2564
+
"@types/react": "*",
2565
+
"@types/react-dom": "*",
2566
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2567
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2568
+
},
2569
+
"peerDependenciesMeta": {
2570
+
"@types/react": {
2571
+
"optional": true
2572
+
},
2573
+
"@types/react-dom": {
2574
+
"optional": true
2575
+
}
2576
+
}
2577
+
},
2578
+
"node_modules/@radix-ui/react-one-time-password-field": {
2579
+
"version": "0.1.8",
2580
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz",
2581
+
"integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==",
2582
+
"dependencies": {
2583
+
"@radix-ui/number": "1.1.1",
2584
+
"@radix-ui/primitive": "1.1.3",
2585
+
"@radix-ui/react-collection": "1.1.7",
2586
+
"@radix-ui/react-compose-refs": "1.1.2",
2587
+
"@radix-ui/react-context": "1.1.2",
2588
+
"@radix-ui/react-direction": "1.1.1",
2589
+
"@radix-ui/react-primitive": "2.1.3",
2590
+
"@radix-ui/react-roving-focus": "1.1.11",
2591
+
"@radix-ui/react-use-controllable-state": "1.2.2",
2592
+
"@radix-ui/react-use-effect-event": "0.0.2",
2593
+
"@radix-ui/react-use-is-hydrated": "0.1.0",
2594
+
"@radix-ui/react-use-layout-effect": "1.1.1"
2595
+
},
2596
+
"peerDependencies": {
2597
+
"@types/react": "*",
2598
+
"@types/react-dom": "*",
2599
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2600
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2601
+
},
2602
+
"peerDependenciesMeta": {
2603
+
"@types/react": {
2604
+
"optional": true
2605
+
},
2606
+
"@types/react-dom": {
2607
+
"optional": true
2608
+
}
2609
+
}
2610
+
},
2611
+
"node_modules/@radix-ui/react-password-toggle-field": {
2612
+
"version": "0.1.3",
2613
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz",
2614
+
"integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==",
2615
+
"dependencies": {
2616
+
"@radix-ui/primitive": "1.1.3",
2617
+
"@radix-ui/react-compose-refs": "1.1.2",
2618
+
"@radix-ui/react-context": "1.1.2",
2619
+
"@radix-ui/react-id": "1.1.1",
2620
+
"@radix-ui/react-primitive": "2.1.3",
2621
+
"@radix-ui/react-use-controllable-state": "1.2.2",
2622
+
"@radix-ui/react-use-effect-event": "0.0.2",
2623
+
"@radix-ui/react-use-is-hydrated": "0.1.0"
2624
+
},
2625
+
"peerDependencies": {
2626
+
"@types/react": "*",
2627
+
"@types/react-dom": "*",
2628
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2629
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2630
+
},
2631
+
"peerDependenciesMeta": {
2632
+
"@types/react": {
2633
+
"optional": true
2634
+
},
2635
+
"@types/react-dom": {
2636
+
"optional": true
2637
+
}
2638
+
}
2639
+
},
2640
+
"node_modules/@radix-ui/react-popover": {
2641
+
"version": "1.1.15",
2642
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz",
2643
+
"integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==",
2644
+
"dependencies": {
2645
+
"@radix-ui/primitive": "1.1.3",
2646
+
"@radix-ui/react-compose-refs": "1.1.2",
2647
+
"@radix-ui/react-context": "1.1.2",
2648
+
"@radix-ui/react-dismissable-layer": "1.1.11",
2649
+
"@radix-ui/react-focus-guards": "1.1.3",
2650
+
"@radix-ui/react-focus-scope": "1.1.7",
2651
+
"@radix-ui/react-id": "1.1.1",
2652
+
"@radix-ui/react-popper": "1.2.8",
2653
+
"@radix-ui/react-portal": "1.1.9",
2654
+
"@radix-ui/react-presence": "1.1.5",
2655
+
"@radix-ui/react-primitive": "2.1.3",
2656
+
"@radix-ui/react-slot": "1.2.3",
2657
+
"@radix-ui/react-use-controllable-state": "1.2.2",
2658
+
"aria-hidden": "^1.2.4",
2659
+
"react-remove-scroll": "^2.6.3"
2660
+
},
2661
+
"peerDependencies": {
2662
+
"@types/react": "*",
2663
+
"@types/react-dom": "*",
2664
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2665
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2666
+
},
2667
+
"peerDependenciesMeta": {
2668
+
"@types/react": {
2669
+
"optional": true
2670
+
},
2671
+
"@types/react-dom": {
2672
+
"optional": true
2673
+
}
2674
+
}
2675
+
},
2676
+
"node_modules/@radix-ui/react-popper": {
2677
+
"version": "1.2.8",
2678
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
2679
+
"integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
2680
+
"dependencies": {
2681
+
"@floating-ui/react-dom": "^2.0.0",
2682
+
"@radix-ui/react-arrow": "1.1.7",
2683
+
"@radix-ui/react-compose-refs": "1.1.2",
2684
+
"@radix-ui/react-context": "1.1.2",
2685
+
"@radix-ui/react-primitive": "2.1.3",
2686
+
"@radix-ui/react-use-callback-ref": "1.1.1",
2687
+
"@radix-ui/react-use-layout-effect": "1.1.1",
2688
+
"@radix-ui/react-use-rect": "1.1.1",
2689
+
"@radix-ui/react-use-size": "1.1.1",
2690
+
"@radix-ui/rect": "1.1.1"
2691
+
},
2692
+
"peerDependencies": {
2693
+
"@types/react": "*",
2694
+
"@types/react-dom": "*",
2695
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2696
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2697
+
},
2698
+
"peerDependenciesMeta": {
2699
+
"@types/react": {
2700
+
"optional": true
2701
+
},
2702
+
"@types/react-dom": {
2703
+
"optional": true
2704
+
}
2705
+
}
2706
+
},
2707
+
"node_modules/@radix-ui/react-portal": {
2708
+
"version": "1.1.9",
2709
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
2710
+
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
2711
+
"dependencies": {
2712
+
"@radix-ui/react-primitive": "2.1.3",
2713
+
"@radix-ui/react-use-layout-effect": "1.1.1"
2714
+
},
2715
+
"peerDependencies": {
2716
+
"@types/react": "*",
2717
+
"@types/react-dom": "*",
2718
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2719
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2720
+
},
2721
+
"peerDependenciesMeta": {
2722
+
"@types/react": {
2723
+
"optional": true
2724
+
},
2725
+
"@types/react-dom": {
2726
+
"optional": true
2727
+
}
2728
+
}
2729
+
},
2730
+
"node_modules/@radix-ui/react-presence": {
2731
+
"version": "1.1.5",
2732
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
2733
+
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
2734
+
"dependencies": {
2735
+
"@radix-ui/react-compose-refs": "1.1.2",
2736
+
"@radix-ui/react-use-layout-effect": "1.1.1"
2737
+
},
2738
+
"peerDependencies": {
2739
+
"@types/react": "*",
2740
+
"@types/react-dom": "*",
2741
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2742
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2743
+
},
2744
+
"peerDependenciesMeta": {
2745
+
"@types/react": {
2746
+
"optional": true
2747
+
},
2748
+
"@types/react-dom": {
2749
+
"optional": true
2750
+
}
2751
+
}
2752
+
},
2753
+
"node_modules/@radix-ui/react-primitive": {
2754
+
"version": "2.1.3",
2755
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
2756
+
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
2757
+
"dependencies": {
2758
+
"@radix-ui/react-slot": "1.2.3"
2759
+
},
2760
+
"peerDependencies": {
2761
+
"@types/react": "*",
2762
+
"@types/react-dom": "*",
2763
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2764
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2765
+
},
2766
+
"peerDependenciesMeta": {
2767
+
"@types/react": {
2768
+
"optional": true
2769
+
},
2770
+
"@types/react-dom": {
2771
+
"optional": true
2772
+
}
2773
+
}
2774
+
},
2775
+
"node_modules/@radix-ui/react-progress": {
2776
+
"version": "1.1.7",
2777
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz",
2778
+
"integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==",
2779
+
"dependencies": {
2780
+
"@radix-ui/react-context": "1.1.2",
2781
+
"@radix-ui/react-primitive": "2.1.3"
2782
+
},
2783
+
"peerDependencies": {
2784
+
"@types/react": "*",
2785
+
"@types/react-dom": "*",
2786
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2787
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2788
+
},
2789
+
"peerDependenciesMeta": {
2790
+
"@types/react": {
2791
+
"optional": true
2792
+
},
2793
+
"@types/react-dom": {
2794
+
"optional": true
2795
+
}
2796
+
}
2797
+
},
2798
+
"node_modules/@radix-ui/react-radio-group": {
2799
+
"version": "1.3.8",
2800
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz",
2801
+
"integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==",
2802
+
"dependencies": {
2803
+
"@radix-ui/primitive": "1.1.3",
2804
+
"@radix-ui/react-compose-refs": "1.1.2",
2805
+
"@radix-ui/react-context": "1.1.2",
2806
+
"@radix-ui/react-direction": "1.1.1",
2807
+
"@radix-ui/react-presence": "1.1.5",
2808
+
"@radix-ui/react-primitive": "2.1.3",
2809
+
"@radix-ui/react-roving-focus": "1.1.11",
2810
+
"@radix-ui/react-use-controllable-state": "1.2.2",
2811
+
"@radix-ui/react-use-previous": "1.1.1",
2812
+
"@radix-ui/react-use-size": "1.1.1"
2813
+
},
2814
+
"peerDependencies": {
2815
+
"@types/react": "*",
2816
+
"@types/react-dom": "*",
2817
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2818
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2819
+
},
2820
+
"peerDependenciesMeta": {
2821
+
"@types/react": {
2822
+
"optional": true
2823
+
},
2824
+
"@types/react-dom": {
2825
+
"optional": true
2826
+
}
2827
+
}
2828
+
},
2829
+
"node_modules/@radix-ui/react-roving-focus": {
2830
+
"version": "1.1.11",
2831
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
2832
+
"integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
2833
+
"dependencies": {
2834
+
"@radix-ui/primitive": "1.1.3",
2835
+
"@radix-ui/react-collection": "1.1.7",
2836
+
"@radix-ui/react-compose-refs": "1.1.2",
2837
+
"@radix-ui/react-context": "1.1.2",
2838
+
"@radix-ui/react-direction": "1.1.1",
2839
+
"@radix-ui/react-id": "1.1.1",
2840
+
"@radix-ui/react-primitive": "2.1.3",
2841
+
"@radix-ui/react-use-callback-ref": "1.1.1",
2842
+
"@radix-ui/react-use-controllable-state": "1.2.2"
2843
+
},
2844
+
"peerDependencies": {
2845
+
"@types/react": "*",
2846
+
"@types/react-dom": "*",
2847
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2848
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2849
+
},
2850
+
"peerDependenciesMeta": {
2851
+
"@types/react": {
2852
+
"optional": true
2853
+
},
2854
+
"@types/react-dom": {
2855
+
"optional": true
2856
+
}
2857
+
}
2858
+
},
2859
+
"node_modules/@radix-ui/react-scroll-area": {
2860
+
"version": "1.2.10",
2861
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz",
2862
+
"integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==",
2863
+
"dependencies": {
2864
+
"@radix-ui/number": "1.1.1",
2865
+
"@radix-ui/primitive": "1.1.3",
2866
+
"@radix-ui/react-compose-refs": "1.1.2",
2867
+
"@radix-ui/react-context": "1.1.2",
2868
+
"@radix-ui/react-direction": "1.1.1",
2869
+
"@radix-ui/react-presence": "1.1.5",
2870
+
"@radix-ui/react-primitive": "2.1.3",
2871
+
"@radix-ui/react-use-callback-ref": "1.1.1",
2872
+
"@radix-ui/react-use-layout-effect": "1.1.1"
2873
+
},
2874
+
"peerDependencies": {
2875
+
"@types/react": "*",
2876
+
"@types/react-dom": "*",
2877
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2878
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2879
+
},
2880
+
"peerDependenciesMeta": {
2881
+
"@types/react": {
2882
+
"optional": true
2883
+
},
2884
+
"@types/react-dom": {
2885
+
"optional": true
2886
+
}
2887
+
}
2888
+
},
2889
+
"node_modules/@radix-ui/react-select": {
2890
+
"version": "2.2.6",
2891
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
2892
+
"integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==",
2893
+
"dependencies": {
2894
+
"@radix-ui/number": "1.1.1",
2895
+
"@radix-ui/primitive": "1.1.3",
2896
+
"@radix-ui/react-collection": "1.1.7",
2897
+
"@radix-ui/react-compose-refs": "1.1.2",
2898
+
"@radix-ui/react-context": "1.1.2",
2899
+
"@radix-ui/react-direction": "1.1.1",
2900
+
"@radix-ui/react-dismissable-layer": "1.1.11",
2901
+
"@radix-ui/react-focus-guards": "1.1.3",
2902
+
"@radix-ui/react-focus-scope": "1.1.7",
2903
+
"@radix-ui/react-id": "1.1.1",
2904
+
"@radix-ui/react-popper": "1.2.8",
2905
+
"@radix-ui/react-portal": "1.1.9",
2906
+
"@radix-ui/react-primitive": "2.1.3",
2907
+
"@radix-ui/react-slot": "1.2.3",
2908
+
"@radix-ui/react-use-callback-ref": "1.1.1",
2909
+
"@radix-ui/react-use-controllable-state": "1.2.2",
2910
+
"@radix-ui/react-use-layout-effect": "1.1.1",
2911
+
"@radix-ui/react-use-previous": "1.1.1",
2912
+
"@radix-ui/react-visually-hidden": "1.2.3",
2913
+
"aria-hidden": "^1.2.4",
2914
+
"react-remove-scroll": "^2.6.3"
2915
+
},
2916
+
"peerDependencies": {
2917
+
"@types/react": "*",
2918
+
"@types/react-dom": "*",
2919
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2920
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2921
+
},
2922
+
"peerDependenciesMeta": {
2923
+
"@types/react": {
2924
+
"optional": true
2925
+
},
2926
+
"@types/react-dom": {
2927
+
"optional": true
2928
+
}
2929
+
}
2930
+
},
2931
+
"node_modules/@radix-ui/react-separator": {
2932
+
"version": "1.1.7",
2933
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
2934
+
"integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==",
2935
+
"dependencies": {
2936
+
"@radix-ui/react-primitive": "2.1.3"
2937
+
},
2938
+
"peerDependencies": {
2939
+
"@types/react": "*",
2940
+
"@types/react-dom": "*",
2941
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2942
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2943
+
},
2944
+
"peerDependenciesMeta": {
2945
+
"@types/react": {
2946
+
"optional": true
2947
+
},
2948
+
"@types/react-dom": {
2949
+
"optional": true
2950
+
}
2951
+
}
2952
+
},
2953
+
"node_modules/@radix-ui/react-slider": {
2954
+
"version": "1.3.6",
2955
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz",
2956
+
"integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==",
2957
+
"dependencies": {
2958
+
"@radix-ui/number": "1.1.1",
2959
+
"@radix-ui/primitive": "1.1.3",
2960
+
"@radix-ui/react-collection": "1.1.7",
2961
+
"@radix-ui/react-compose-refs": "1.1.2",
2962
+
"@radix-ui/react-context": "1.1.2",
2963
+
"@radix-ui/react-direction": "1.1.1",
2964
+
"@radix-ui/react-primitive": "2.1.3",
2965
+
"@radix-ui/react-use-controllable-state": "1.2.2",
2966
+
"@radix-ui/react-use-layout-effect": "1.1.1",
2967
+
"@radix-ui/react-use-previous": "1.1.1",
2968
+
"@radix-ui/react-use-size": "1.1.1"
2969
+
},
2970
+
"peerDependencies": {
2971
+
"@types/react": "*",
2972
+
"@types/react-dom": "*",
2973
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
2974
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2975
+
},
2976
+
"peerDependenciesMeta": {
2977
+
"@types/react": {
2978
+
"optional": true
2979
+
},
2980
+
"@types/react-dom": {
2981
+
"optional": true
2982
+
}
2983
+
}
2984
+
},
2985
+
"node_modules/@radix-ui/react-slot": {
2986
+
"version": "1.2.3",
2987
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
2988
+
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
2989
+
"dependencies": {
2990
+
"@radix-ui/react-compose-refs": "1.1.2"
2991
+
},
2992
+
"peerDependencies": {
2993
+
"@types/react": "*",
2994
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
2995
+
},
2996
+
"peerDependenciesMeta": {
2997
+
"@types/react": {
2998
+
"optional": true
2999
+
}
3000
+
}
3001
+
},
3002
+
"node_modules/@radix-ui/react-switch": {
3003
+
"version": "1.2.6",
3004
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz",
3005
+
"integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==",
3006
+
"dependencies": {
3007
+
"@radix-ui/primitive": "1.1.3",
3008
+
"@radix-ui/react-compose-refs": "1.1.2",
3009
+
"@radix-ui/react-context": "1.1.2",
3010
+
"@radix-ui/react-primitive": "2.1.3",
3011
+
"@radix-ui/react-use-controllable-state": "1.2.2",
3012
+
"@radix-ui/react-use-previous": "1.1.1",
3013
+
"@radix-ui/react-use-size": "1.1.1"
3014
+
},
3015
+
"peerDependencies": {
3016
+
"@types/react": "*",
3017
+
"@types/react-dom": "*",
3018
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
3019
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3020
+
},
3021
+
"peerDependenciesMeta": {
3022
+
"@types/react": {
3023
+
"optional": true
3024
+
},
3025
+
"@types/react-dom": {
3026
+
"optional": true
3027
+
}
3028
+
}
3029
+
},
3030
+
"node_modules/@radix-ui/react-tabs": {
3031
+
"version": "1.1.13",
3032
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
3033
+
"integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
3034
+
"dependencies": {
3035
+
"@radix-ui/primitive": "1.1.3",
3036
+
"@radix-ui/react-context": "1.1.2",
3037
+
"@radix-ui/react-direction": "1.1.1",
3038
+
"@radix-ui/react-id": "1.1.1",
3039
+
"@radix-ui/react-presence": "1.1.5",
3040
+
"@radix-ui/react-primitive": "2.1.3",
3041
+
"@radix-ui/react-roving-focus": "1.1.11",
3042
+
"@radix-ui/react-use-controllable-state": "1.2.2"
3043
+
},
3044
+
"peerDependencies": {
3045
+
"@types/react": "*",
3046
+
"@types/react-dom": "*",
3047
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
3048
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3049
+
},
3050
+
"peerDependenciesMeta": {
3051
+
"@types/react": {
3052
+
"optional": true
3053
+
},
3054
+
"@types/react-dom": {
3055
+
"optional": true
3056
+
}
3057
+
}
3058
+
},
3059
+
"node_modules/@radix-ui/react-toast": {
3060
+
"version": "1.2.15",
3061
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz",
3062
+
"integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==",
3063
+
"dependencies": {
3064
+
"@radix-ui/primitive": "1.1.3",
3065
+
"@radix-ui/react-collection": "1.1.7",
3066
+
"@radix-ui/react-compose-refs": "1.1.2",
3067
+
"@radix-ui/react-context": "1.1.2",
3068
+
"@radix-ui/react-dismissable-layer": "1.1.11",
3069
+
"@radix-ui/react-portal": "1.1.9",
3070
+
"@radix-ui/react-presence": "1.1.5",
3071
+
"@radix-ui/react-primitive": "2.1.3",
3072
+
"@radix-ui/react-use-callback-ref": "1.1.1",
3073
+
"@radix-ui/react-use-controllable-state": "1.2.2",
3074
+
"@radix-ui/react-use-layout-effect": "1.1.1",
3075
+
"@radix-ui/react-visually-hidden": "1.2.3"
3076
+
},
3077
+
"peerDependencies": {
3078
+
"@types/react": "*",
3079
+
"@types/react-dom": "*",
3080
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
3081
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3082
+
},
3083
+
"peerDependenciesMeta": {
3084
+
"@types/react": {
3085
+
"optional": true
3086
+
},
3087
+
"@types/react-dom": {
3088
+
"optional": true
3089
+
}
3090
+
}
3091
+
},
3092
+
"node_modules/@radix-ui/react-toggle": {
3093
+
"version": "1.1.10",
3094
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz",
3095
+
"integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==",
3096
+
"dependencies": {
3097
+
"@radix-ui/primitive": "1.1.3",
3098
+
"@radix-ui/react-primitive": "2.1.3",
3099
+
"@radix-ui/react-use-controllable-state": "1.2.2"
3100
+
},
3101
+
"peerDependencies": {
3102
+
"@types/react": "*",
3103
+
"@types/react-dom": "*",
3104
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
3105
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3106
+
},
3107
+
"peerDependenciesMeta": {
3108
+
"@types/react": {
3109
+
"optional": true
3110
+
},
3111
+
"@types/react-dom": {
3112
+
"optional": true
3113
+
}
3114
+
}
3115
+
},
3116
+
"node_modules/@radix-ui/react-toggle-group": {
3117
+
"version": "1.1.11",
3118
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz",
3119
+
"integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==",
3120
+
"dependencies": {
3121
+
"@radix-ui/primitive": "1.1.3",
3122
+
"@radix-ui/react-context": "1.1.2",
3123
+
"@radix-ui/react-direction": "1.1.1",
3124
+
"@radix-ui/react-primitive": "2.1.3",
3125
+
"@radix-ui/react-roving-focus": "1.1.11",
3126
+
"@radix-ui/react-toggle": "1.1.10",
3127
+
"@radix-ui/react-use-controllable-state": "1.2.2"
3128
+
},
3129
+
"peerDependencies": {
3130
+
"@types/react": "*",
3131
+
"@types/react-dom": "*",
3132
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
3133
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3134
+
},
3135
+
"peerDependenciesMeta": {
3136
+
"@types/react": {
3137
+
"optional": true
3138
+
},
3139
+
"@types/react-dom": {
3140
+
"optional": true
3141
+
}
3142
+
}
3143
+
},
3144
+
"node_modules/@radix-ui/react-toolbar": {
3145
+
"version": "1.1.11",
3146
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz",
3147
+
"integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==",
3148
+
"dependencies": {
3149
+
"@radix-ui/primitive": "1.1.3",
3150
+
"@radix-ui/react-context": "1.1.2",
3151
+
"@radix-ui/react-direction": "1.1.1",
3152
+
"@radix-ui/react-primitive": "2.1.3",
3153
+
"@radix-ui/react-roving-focus": "1.1.11",
3154
+
"@radix-ui/react-separator": "1.1.7",
3155
+
"@radix-ui/react-toggle-group": "1.1.11"
3156
+
},
3157
+
"peerDependencies": {
3158
+
"@types/react": "*",
3159
+
"@types/react-dom": "*",
3160
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
3161
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3162
+
},
3163
+
"peerDependenciesMeta": {
3164
+
"@types/react": {
3165
+
"optional": true
3166
+
},
3167
+
"@types/react-dom": {
3168
+
"optional": true
3169
+
}
3170
+
}
3171
+
},
3172
+
"node_modules/@radix-ui/react-tooltip": {
3173
+
"version": "1.2.8",
3174
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
3175
+
"integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==",
3176
+
"dependencies": {
3177
+
"@radix-ui/primitive": "1.1.3",
3178
+
"@radix-ui/react-compose-refs": "1.1.2",
3179
+
"@radix-ui/react-context": "1.1.2",
3180
+
"@radix-ui/react-dismissable-layer": "1.1.11",
3181
+
"@radix-ui/react-id": "1.1.1",
3182
+
"@radix-ui/react-popper": "1.2.8",
3183
+
"@radix-ui/react-portal": "1.1.9",
3184
+
"@radix-ui/react-presence": "1.1.5",
3185
+
"@radix-ui/react-primitive": "2.1.3",
3186
+
"@radix-ui/react-slot": "1.2.3",
3187
+
"@radix-ui/react-use-controllable-state": "1.2.2",
3188
+
"@radix-ui/react-visually-hidden": "1.2.3"
3189
+
},
3190
+
"peerDependencies": {
3191
+
"@types/react": "*",
3192
+
"@types/react-dom": "*",
3193
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
3194
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3195
+
},
3196
+
"peerDependenciesMeta": {
3197
+
"@types/react": {
3198
+
"optional": true
3199
+
},
3200
+
"@types/react-dom": {
3201
+
"optional": true
3202
+
}
3203
+
}
3204
+
},
3205
+
"node_modules/@radix-ui/react-use-callback-ref": {
3206
+
"version": "1.1.1",
3207
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
3208
+
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
3209
+
"peerDependencies": {
3210
+
"@types/react": "*",
3211
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3212
+
},
3213
+
"peerDependenciesMeta": {
3214
+
"@types/react": {
3215
+
"optional": true
3216
+
}
3217
+
}
3218
+
},
3219
+
"node_modules/@radix-ui/react-use-controllable-state": {
3220
+
"version": "1.2.2",
3221
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
3222
+
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
3223
+
"dependencies": {
3224
+
"@radix-ui/react-use-effect-event": "0.0.2",
3225
+
"@radix-ui/react-use-layout-effect": "1.1.1"
3226
+
},
3227
+
"peerDependencies": {
3228
+
"@types/react": "*",
3229
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3230
+
},
3231
+
"peerDependenciesMeta": {
3232
+
"@types/react": {
3233
+
"optional": true
3234
+
}
3235
+
}
3236
+
},
3237
+
"node_modules/@radix-ui/react-use-effect-event": {
3238
+
"version": "0.0.2",
3239
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
3240
+
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
3241
+
"dependencies": {
3242
+
"@radix-ui/react-use-layout-effect": "1.1.1"
3243
+
},
3244
+
"peerDependencies": {
3245
+
"@types/react": "*",
3246
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3247
+
},
3248
+
"peerDependenciesMeta": {
3249
+
"@types/react": {
3250
+
"optional": true
3251
+
}
3252
+
}
3253
+
},
3254
+
"node_modules/@radix-ui/react-use-escape-keydown": {
3255
+
"version": "1.1.1",
3256
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
3257
+
"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
3258
+
"dependencies": {
3259
+
"@radix-ui/react-use-callback-ref": "1.1.1"
3260
+
},
3261
+
"peerDependencies": {
3262
+
"@types/react": "*",
3263
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3264
+
},
3265
+
"peerDependenciesMeta": {
3266
+
"@types/react": {
3267
+
"optional": true
3268
+
}
3269
+
}
3270
+
},
3271
+
"node_modules/@radix-ui/react-use-is-hydrated": {
3272
+
"version": "0.1.0",
3273
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz",
3274
+
"integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==",
3275
+
"dependencies": {
3276
+
"use-sync-external-store": "^1.5.0"
3277
+
},
3278
+
"peerDependencies": {
3279
+
"@types/react": "*",
3280
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3281
+
},
3282
+
"peerDependenciesMeta": {
3283
+
"@types/react": {
3284
+
"optional": true
3285
+
}
3286
+
}
3287
+
},
3288
+
"node_modules/@radix-ui/react-use-layout-effect": {
3289
+
"version": "1.1.1",
3290
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
3291
+
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
3292
+
"peerDependencies": {
3293
+
"@types/react": "*",
3294
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3295
+
},
3296
+
"peerDependenciesMeta": {
3297
+
"@types/react": {
3298
+
"optional": true
3299
+
}
3300
+
}
3301
+
},
3302
+
"node_modules/@radix-ui/react-use-previous": {
3303
+
"version": "1.1.1",
3304
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
3305
+
"integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
3306
+
"peerDependencies": {
3307
+
"@types/react": "*",
3308
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3309
+
},
3310
+
"peerDependenciesMeta": {
3311
+
"@types/react": {
3312
+
"optional": true
3313
+
}
3314
+
}
3315
+
},
3316
+
"node_modules/@radix-ui/react-use-rect": {
3317
+
"version": "1.1.1",
3318
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
3319
+
"integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
3320
+
"dependencies": {
3321
+
"@radix-ui/rect": "1.1.1"
3322
+
},
3323
+
"peerDependencies": {
3324
+
"@types/react": "*",
3325
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3326
+
},
3327
+
"peerDependenciesMeta": {
3328
+
"@types/react": {
3329
+
"optional": true
3330
+
}
3331
+
}
3332
+
},
3333
+
"node_modules/@radix-ui/react-use-size": {
3334
+
"version": "1.1.1",
3335
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
3336
+
"integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
3337
+
"dependencies": {
3338
+
"@radix-ui/react-use-layout-effect": "1.1.1"
3339
+
},
3340
+
"peerDependencies": {
3341
+
"@types/react": "*",
3342
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3343
+
},
3344
+
"peerDependenciesMeta": {
3345
+
"@types/react": {
3346
+
"optional": true
3347
+
}
3348
+
}
3349
+
},
3350
+
"node_modules/@radix-ui/react-visually-hidden": {
3351
+
"version": "1.2.3",
3352
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
3353
+
"integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
3354
+
"dependencies": {
3355
+
"@radix-ui/react-primitive": "2.1.3"
3356
+
},
3357
+
"peerDependencies": {
3358
+
"@types/react": "*",
3359
+
"@types/react-dom": "*",
3360
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
3361
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
3362
+
},
3363
+
"peerDependenciesMeta": {
3364
+
"@types/react": {
3365
+
"optional": true
3366
+
},
3367
+
"@types/react-dom": {
3368
+
"optional": true
3369
+
}
3370
+
}
3371
+
},
3372
+
"node_modules/@radix-ui/rect": {
3373
+
"version": "1.1.1",
3374
+
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
3375
+
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="
3376
+
},
3377
"node_modules/@rolldown/pluginutils": {
3378
"version": "1.0.0-beta.27",
3379
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
···
3688
"solid-js": "^1.6.12"
3689
}
3690
},
3691
+
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
3692
+
"version": "8.0.0",
3693
+
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz",
3694
+
"integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==",
3695
+
"dev": true,
3696
+
"license": "MIT",
3697
+
"engines": {
3698
+
"node": ">=14"
3699
+
},
3700
+
"funding": {
3701
+
"type": "github",
3702
+
"url": "https://github.com/sponsors/gregberge"
3703
+
},
3704
+
"peerDependencies": {
3705
+
"@babel/core": "^7.0.0-0"
3706
+
}
3707
+
},
3708
+
"node_modules/@svgr/babel-plugin-remove-jsx-attribute": {
3709
+
"version": "8.0.0",
3710
+
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz",
3711
+
"integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==",
3712
+
"dev": true,
3713
+
"license": "MIT",
3714
+
"engines": {
3715
+
"node": ">=14"
3716
+
},
3717
+
"funding": {
3718
+
"type": "github",
3719
+
"url": "https://github.com/sponsors/gregberge"
3720
+
},
3721
+
"peerDependencies": {
3722
+
"@babel/core": "^7.0.0-0"
3723
+
}
3724
+
},
3725
+
"node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": {
3726
+
"version": "8.0.0",
3727
+
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz",
3728
+
"integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==",
3729
+
"dev": true,
3730
+
"license": "MIT",
3731
+
"engines": {
3732
+
"node": ">=14"
3733
+
},
3734
+
"funding": {
3735
+
"type": "github",
3736
+
"url": "https://github.com/sponsors/gregberge"
3737
+
},
3738
+
"peerDependencies": {
3739
+
"@babel/core": "^7.0.0-0"
3740
+
}
3741
+
},
3742
+
"node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": {
3743
+
"version": "8.0.0",
3744
+
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz",
3745
+
"integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==",
3746
+
"dev": true,
3747
+
"license": "MIT",
3748
+
"engines": {
3749
+
"node": ">=14"
3750
+
},
3751
+
"funding": {
3752
+
"type": "github",
3753
+
"url": "https://github.com/sponsors/gregberge"
3754
+
},
3755
+
"peerDependencies": {
3756
+
"@babel/core": "^7.0.0-0"
3757
+
}
3758
+
},
3759
+
"node_modules/@svgr/babel-plugin-svg-dynamic-title": {
3760
+
"version": "8.0.0",
3761
+
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz",
3762
+
"integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==",
3763
+
"dev": true,
3764
+
"license": "MIT",
3765
+
"engines": {
3766
+
"node": ">=14"
3767
+
},
3768
+
"funding": {
3769
+
"type": "github",
3770
+
"url": "https://github.com/sponsors/gregberge"
3771
+
},
3772
+
"peerDependencies": {
3773
+
"@babel/core": "^7.0.0-0"
3774
+
}
3775
+
},
3776
+
"node_modules/@svgr/babel-plugin-svg-em-dimensions": {
3777
+
"version": "8.0.0",
3778
+
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz",
3779
+
"integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==",
3780
+
"dev": true,
3781
+
"license": "MIT",
3782
+
"engines": {
3783
+
"node": ">=14"
3784
+
},
3785
+
"funding": {
3786
+
"type": "github",
3787
+
"url": "https://github.com/sponsors/gregberge"
3788
+
},
3789
+
"peerDependencies": {
3790
+
"@babel/core": "^7.0.0-0"
3791
+
}
3792
+
},
3793
+
"node_modules/@svgr/babel-plugin-transform-react-native-svg": {
3794
+
"version": "8.1.0",
3795
+
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz",
3796
+
"integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==",
3797
+
"dev": true,
3798
+
"license": "MIT",
3799
+
"engines": {
3800
+
"node": ">=14"
3801
+
},
3802
+
"funding": {
3803
+
"type": "github",
3804
+
"url": "https://github.com/sponsors/gregberge"
3805
+
},
3806
+
"peerDependencies": {
3807
+
"@babel/core": "^7.0.0-0"
3808
+
}
3809
+
},
3810
+
"node_modules/@svgr/babel-plugin-transform-svg-component": {
3811
+
"version": "8.0.0",
3812
+
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz",
3813
+
"integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==",
3814
+
"dev": true,
3815
+
"license": "MIT",
3816
+
"engines": {
3817
+
"node": ">=12"
3818
+
},
3819
+
"funding": {
3820
+
"type": "github",
3821
+
"url": "https://github.com/sponsors/gregberge"
3822
+
},
3823
+
"peerDependencies": {
3824
+
"@babel/core": "^7.0.0-0"
3825
+
}
3826
+
},
3827
+
"node_modules/@svgr/babel-preset": {
3828
+
"version": "8.1.0",
3829
+
"resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz",
3830
+
"integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==",
3831
+
"dev": true,
3832
+
"license": "MIT",
3833
+
"dependencies": {
3834
+
"@svgr/babel-plugin-add-jsx-attribute": "8.0.0",
3835
+
"@svgr/babel-plugin-remove-jsx-attribute": "8.0.0",
3836
+
"@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0",
3837
+
"@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0",
3838
+
"@svgr/babel-plugin-svg-dynamic-title": "8.0.0",
3839
+
"@svgr/babel-plugin-svg-em-dimensions": "8.0.0",
3840
+
"@svgr/babel-plugin-transform-react-native-svg": "8.1.0",
3841
+
"@svgr/babel-plugin-transform-svg-component": "8.0.0"
3842
+
},
3843
+
"engines": {
3844
+
"node": ">=14"
3845
+
},
3846
+
"funding": {
3847
+
"type": "github",
3848
+
"url": "https://github.com/sponsors/gregberge"
3849
+
},
3850
+
"peerDependencies": {
3851
+
"@babel/core": "^7.0.0-0"
3852
+
}
3853
+
},
3854
+
"node_modules/@svgr/core": {
3855
+
"version": "8.1.0",
3856
+
"resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz",
3857
+
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
3858
+
"dev": true,
3859
+
"license": "MIT",
3860
+
"dependencies": {
3861
+
"@babel/core": "^7.21.3",
3862
+
"@svgr/babel-preset": "8.1.0",
3863
+
"camelcase": "^6.2.0",
3864
+
"cosmiconfig": "^8.1.3",
3865
+
"snake-case": "^3.0.4"
3866
+
},
3867
+
"engines": {
3868
+
"node": ">=14"
3869
+
},
3870
+
"funding": {
3871
+
"type": "github",
3872
+
"url": "https://github.com/sponsors/gregberge"
3873
+
}
3874
+
},
3875
+
"node_modules/@svgr/hast-util-to-babel-ast": {
3876
+
"version": "8.0.0",
3877
+
"resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz",
3878
+
"integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==",
3879
+
"dev": true,
3880
+
"license": "MIT",
3881
+
"dependencies": {
3882
+
"@babel/types": "^7.21.3",
3883
+
"entities": "^4.4.0"
3884
+
},
3885
+
"engines": {
3886
+
"node": ">=14"
3887
+
},
3888
+
"funding": {
3889
+
"type": "github",
3890
+
"url": "https://github.com/sponsors/gregberge"
3891
+
}
3892
+
},
3893
+
"node_modules/@svgr/hast-util-to-babel-ast/node_modules/entities": {
3894
+
"version": "4.5.0",
3895
+
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
3896
+
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
3897
+
"dev": true,
3898
+
"license": "BSD-2-Clause",
3899
+
"engines": {
3900
+
"node": ">=0.12"
3901
+
},
3902
+
"funding": {
3903
+
"url": "https://github.com/fb55/entities?sponsor=1"
3904
+
}
3905
+
},
3906
+
"node_modules/@svgr/plugin-jsx": {
3907
+
"version": "8.1.0",
3908
+
"resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz",
3909
+
"integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==",
3910
+
"dev": true,
3911
+
"license": "MIT",
3912
+
"dependencies": {
3913
+
"@babel/core": "^7.21.3",
3914
+
"@svgr/babel-preset": "8.1.0",
3915
+
"@svgr/hast-util-to-babel-ast": "8.0.0",
3916
+
"svg-parser": "^2.0.4"
3917
+
},
3918
+
"engines": {
3919
+
"node": ">=14"
3920
+
},
3921
+
"funding": {
3922
+
"type": "github",
3923
+
"url": "https://github.com/sponsors/gregberge"
3924
+
},
3925
+
"peerDependencies": {
3926
+
"@svgr/core": "*"
3927
+
}
3928
+
},
3929
"node_modules/@svta/common-media-library": {
3930
"version": "0.12.4",
3931
"resolved": "https://registry.npmjs.org/@svta/common-media-library/-/common-media-library-0.12.4.tgz",
···
4423
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
4424
}
4425
},
4426
"node_modules/@tanstack/router-core": {
4427
"version": "1.131.28",
4428
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.131.28.tgz",
···
4575
"version": "0.7.4",
4576
"resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.7.4.tgz",
4577
"integrity": "sha512-F1XqZQici1Aq6WigEfcxJSml92nW+85Om8ElBMokPNg5glCYVOmPkZGIQeieYFxcPiKTfwo0MTOQpUyJtwncrg==",
4578
"license": "MIT",
4579
"funding": {
4580
"type": "github",
···
4752
"peerDependencies": {
4753
"@types/react": "^19.0.0"
4754
}
4755
+
},
4756
+
"node_modules/@types/trusted-types": {
4757
+
"version": "2.0.7",
4758
+
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
4759
+
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
4760
+
"license": "MIT",
4761
+
"optional": true
4762
},
4763
"node_modules/@typescript-eslint/eslint-plugin": {
4764
"version": "8.46.1",
···
5284
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
5285
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
5286
"dev": true,
5287
+
"license": "Python-2.0"
5288
+
},
5289
+
"node_modules/aria-hidden": {
5290
+
"version": "1.2.6",
5291
+
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
5292
+
"integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
5293
+
"dependencies": {
5294
+
"tslib": "^2.0.0"
5295
+
},
5296
+
"engines": {
5297
+
"node": ">=10"
5298
+
}
5299
},
5300
"node_modules/aria-query": {
5301
"version": "5.3.0",
···
5707
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
5708
"dev": true,
5709
"license": "MIT",
5710
"engines": {
5711
"node": ">=6"
5712
+
}
5713
+
},
5714
+
"node_modules/camelcase": {
5715
+
"version": "6.3.0",
5716
+
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
5717
+
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
5718
+
"dev": true,
5719
+
"license": "MIT",
5720
+
"engines": {
5721
+
"node": ">=10"
5722
+
},
5723
+
"funding": {
5724
+
"url": "https://github.com/sponsors/sindresorhus"
5725
}
5726
},
5727
"node_modules/caniuse-lite": {
···
5914
"dev": true,
5915
"license": "MIT"
5916
},
5917
+
"node_modules/confbox": {
5918
+
"version": "0.2.2",
5919
+
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
5920
+
"integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
5921
+
"dev": true,
5922
+
"license": "MIT"
5923
+
},
5924
"node_modules/convert-source-map": {
5925
"version": "2.0.0",
5926
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
···
5944
"url": "https://opencollective.com/core-js"
5945
}
5946
},
5947
+
"node_modules/cosmiconfig": {
5948
+
"version": "8.3.6",
5949
+
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
5950
+
"integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==",
5951
+
"dev": true,
5952
+
"license": "MIT",
5953
+
"dependencies": {
5954
+
"import-fresh": "^3.3.0",
5955
+
"js-yaml": "^4.1.0",
5956
+
"parse-json": "^5.2.0",
5957
+
"path-type": "^4.0.0"
5958
+
},
5959
+
"engines": {
5960
+
"node": ">=14"
5961
+
},
5962
+
"funding": {
5963
+
"url": "https://github.com/sponsors/d-fischer"
5964
+
},
5965
+
"peerDependencies": {
5966
+
"typescript": ">=4.9.5"
5967
+
},
5968
+
"peerDependenciesMeta": {
5969
+
"typescript": {
5970
+
"optional": true
5971
+
}
5972
+
}
5973
+
},
5974
"node_modules/cross-spawn": {
5975
"version": "7.0.6",
5976
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
···
6110
}
6111
},
6112
"node_modules/debug": {
6113
+
"version": "4.4.3",
6114
+
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
6115
+
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
6116
"license": "MIT",
6117
"dependencies": {
6118
"ms": "^2.1.3"
···
6206
"node": ">=8"
6207
}
6208
},
6209
+
"node_modules/detect-node-es": {
6210
+
"version": "1.1.0",
6211
+
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
6212
+
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
6213
+
},
6214
"node_modules/diff": {
6215
"version": "8.0.2",
6216
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz",
···
6240
"dev": true,
6241
"license": "MIT"
6242
},
6243
+
"node_modules/dompurify": {
6244
+
"version": "3.3.0",
6245
+
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz",
6246
+
"integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==",
6247
+
"license": "(MPL-2.0 OR Apache-2.0)",
6248
+
"optionalDependencies": {
6249
+
"@types/trusted-types": "^2.0.7"
6250
+
}
6251
+
},
6252
+
"node_modules/dot-case": {
6253
+
"version": "3.0.4",
6254
+
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
6255
+
"integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
6256
+
"dev": true,
6257
+
"license": "MIT",
6258
+
"dependencies": {
6259
+
"no-case": "^3.0.4",
6260
+
"tslib": "^2.0.3"
6261
+
}
6262
+
},
6263
"node_modules/dunder-proto": {
6264
"version": "1.0.1",
6265
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
···
6305
},
6306
"funding": {
6307
"url": "https://github.com/fb55/entities?sponsor=1"
6308
+
}
6309
+
},
6310
+
"node_modules/error-ex": {
6311
+
"version": "1.3.4",
6312
+
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
6313
+
"integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
6314
+
"dev": true,
6315
+
"license": "MIT",
6316
+
"dependencies": {
6317
+
"is-arrayish": "^0.2.1"
6318
}
6319
},
6320
"node_modules/es-abstract": {
···
6978
"node": ">=0.10.0"
6979
}
6980
},
6981
+
"node_modules/eventemitter3": {
6982
+
"version": "5.0.1",
6983
+
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
6984
+
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
6985
+
"license": "MIT"
6986
+
},
6987
"node_modules/expect-type": {
6988
"version": "1.2.2",
6989
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
···
6993
"engines": {
6994
"node": ">=12.0.0"
6995
}
6996
+
},
6997
+
"node_modules/exsolve": {
6998
+
"version": "1.0.7",
6999
+
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz",
7000
+
"integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==",
7001
+
"dev": true,
7002
+
"license": "MIT"
7003
},
7004
"node_modules/fast-deep-equal": {
7005
"version": "3.1.3",
···
7230
},
7231
"funding": {
7232
"url": "https://github.com/sponsors/ljharb"
7233
+
}
7234
+
},
7235
+
"node_modules/get-nonce": {
7236
+
"version": "1.0.1",
7237
+
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
7238
+
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
7239
+
"engines": {
7240
+
"node": ">=6"
7241
}
7242
},
7243
"node_modules/get-proto": {
···
7547
"node": ">= 14"
7548
}
7549
},
7550
+
"node_modules/i": {
7551
+
"version": "0.3.7",
7552
+
"resolved": "https://registry.npmjs.org/i/-/i-0.3.7.tgz",
7553
+
"integrity": "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q==",
7554
+
"engines": {
7555
+
"node": ">=0.4"
7556
+
}
7557
+
},
7558
+
"node_modules/iconify-icon": {
7559
+
"version": "3.0.1",
7560
+
"resolved": "https://registry.npmjs.org/iconify-icon/-/iconify-icon-3.0.1.tgz",
7561
+
"integrity": "sha512-M3/kH3C+e/ufhmQuOSYSb1Ri1ImJ+ZEQYcVRMKnlSc8Nrdoy+iY9YvFnplX8t/3aCRuo5wN4RVPtCSHGnbt8dg==",
7562
+
"dev": true,
7563
+
"license": "MIT",
7564
+
"dependencies": {
7565
+
"@iconify/types": "^2.0.0"
7566
+
},
7567
+
"funding": {
7568
+
"url": "https://github.com/sponsors/cyberalien"
7569
+
}
7570
+
},
7571
"node_modules/iconv-lite": {
7572
"version": "0.6.3",
7573
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
···
7610
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
7611
"dev": true,
7612
"license": "MIT",
7613
"dependencies": {
7614
"parent-module": "^1.0.0",
7615
"resolve-from": "^4.0.0"
···
7698
"url": "https://github.com/sponsors/ljharb"
7699
}
7700
},
7701
+
"node_modules/is-arrayish": {
7702
+
"version": "0.2.1",
7703
+
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
7704
+
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
7705
+
"dev": true,
7706
+
"license": "MIT"
7707
+
},
7708
"node_modules/is-async-function": {
7709
"version": "2.1.1",
7710
"resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
···
8228
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
8229
"dev": true,
8230
"license": "MIT",
8231
"dependencies": {
8232
"argparse": "^2.0.1"
8233
},
···
8294
"dev": true,
8295
"license": "MIT",
8296
"peer": true
8297
+
},
8298
+
"node_modules/json-parse-even-better-errors": {
8299
+
"version": "2.3.1",
8300
+
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
8301
+
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
8302
+
"dev": true,
8303
+
"license": "MIT"
8304
},
8305
"node_modules/json-schema-traverse": {
8306
"version": "0.4.1",
···
8356
"dependencies": {
8357
"json-buffer": "3.0.1"
8358
}
8359
+
},
8360
+
"node_modules/kolorist": {
8361
+
"version": "1.8.0",
8362
+
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
8363
+
"integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==",
8364
+
"dev": true,
8365
+
"license": "MIT"
8366
},
8367
"node_modules/levn": {
8368
"version": "0.4.1",
···
8616
"url": "https://opencollective.com/parcel"
8617
}
8618
},
8619
+
"node_modules/lines-and-columns": {
8620
+
"version": "1.2.4",
8621
+
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
8622
+
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
8623
+
"dev": true,
8624
+
"license": "MIT"
8625
+
},
8626
+
"node_modules/local-pkg": {
8627
+
"version": "1.1.2",
8628
+
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
8629
+
"integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==",
8630
+
"dev": true,
8631
+
"license": "MIT",
8632
+
"dependencies": {
8633
+
"mlly": "^1.7.4",
8634
+
"pkg-types": "^2.3.0",
8635
+
"quansync": "^0.2.11"
8636
+
},
8637
+
"engines": {
8638
+
"node": ">=14"
8639
+
},
8640
+
"funding": {
8641
+
"url": "https://github.com/sponsors/antfu"
8642
+
}
8643
+
},
8644
"node_modules/localforage": {
8645
"version": "1.10.0",
8646
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
···
8666
"funding": {
8667
"url": "https://github.com/sponsors/sindresorhus"
8668
}
8669
+
},
8670
+
"node_modules/lodash.clonedeep": {
8671
+
"version": "4.5.0",
8672
+
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
8673
+
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
8674
+
"license": "MIT"
8675
},
8676
"node_modules/lodash.merge": {
8677
"version": "4.6.2",
···
8699
"integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
8700
"dev": true,
8701
"license": "MIT"
8702
+
},
8703
+
"node_modules/lower-case": {
8704
+
"version": "2.0.2",
8705
+
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
8706
+
"integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
8707
+
"dev": true,
8708
+
"license": "MIT",
8709
+
"dependencies": {
8710
+
"tslib": "^2.0.3"
8711
+
}
8712
},
8713
"node_modules/lru-cache": {
8714
"version": "5.1.1",
···
8730
}
8731
},
8732
"node_modules/magic-string": {
8733
+
"version": "0.30.19",
8734
+
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
8735
+
"integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
8736
"license": "MIT",
8737
"dependencies": {
8738
"@jridgewell/sourcemap-codec": "^1.5.5"
···
8837
"url": "https://github.com/sponsors/isaacs"
8838
}
8839
},
8840
+
"node_modules/mlly": {
8841
+
"version": "1.8.0",
8842
+
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz",
8843
+
"integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==",
8844
+
"dev": true,
8845
+
"license": "MIT",
8846
+
"dependencies": {
8847
+
"acorn": "^8.15.0",
8848
+
"pathe": "^2.0.3",
8849
+
"pkg-types": "^1.3.1",
8850
+
"ufo": "^1.6.1"
8851
+
}
8852
+
},
8853
+
"node_modules/mlly/node_modules/confbox": {
8854
+
"version": "0.1.8",
8855
+
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
8856
+
"integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
8857
+
"dev": true,
8858
+
"license": "MIT"
8859
+
},
8860
+
"node_modules/mlly/node_modules/pkg-types": {
8861
+
"version": "1.3.1",
8862
+
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
8863
+
"integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
8864
+
"dev": true,
8865
+
"license": "MIT",
8866
+
"dependencies": {
8867
+
"confbox": "^0.1.8",
8868
+
"mlly": "^1.7.4",
8869
+
"pathe": "^2.0.1"
8870
+
}
8871
+
},
8872
"node_modules/ms": {
8873
"version": "2.1.3",
8874
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
···
8918
"dev": true,
8919
"license": "MIT"
8920
},
8921
+
"node_modules/no-case": {
8922
+
"version": "3.0.4",
8923
+
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
8924
+
"integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
8925
+
"dev": true,
8926
+
"license": "MIT",
8927
+
"dependencies": {
8928
+
"lower-case": "^2.0.2",
8929
+
"tslib": "^2.0.3"
8930
+
}
8931
+
},
8932
"node_modules/node-releases": {
8933
"version": "2.0.19",
8934
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
···
8944
"node": ">=0.10.0"
8945
}
8946
},
8947
+
"node_modules/npm": {
8948
+
"version": "11.6.2",
8949
+
"resolved": "https://registry.npmjs.org/npm/-/npm-11.6.2.tgz",
8950
+
"integrity": "sha512-7iKzNfy8lWYs3zq4oFPa8EXZz5xt9gQNKJZau3B1ErLBb6bF7sBJ00x09485DOvRT2l5Gerbl3VlZNT57MxJVA==",
8951
+
"bundleDependencies": [
8952
+
"@isaacs/string-locale-compare",
8953
+
"@npmcli/arborist",
8954
+
"@npmcli/config",
8955
+
"@npmcli/fs",
8956
+
"@npmcli/map-workspaces",
8957
+
"@npmcli/package-json",
8958
+
"@npmcli/promise-spawn",
8959
+
"@npmcli/redact",
8960
+
"@npmcli/run-script",
8961
+
"@sigstore/tuf",
8962
+
"abbrev",
8963
+
"archy",
8964
+
"cacache",
8965
+
"chalk",
8966
+
"ci-info",
8967
+
"cli-columns",
8968
+
"fastest-levenshtein",
8969
+
"fs-minipass",
8970
+
"glob",
8971
+
"graceful-fs",
8972
+
"hosted-git-info",
8973
+
"ini",
8974
+
"init-package-json",
8975
+
"is-cidr",
8976
+
"json-parse-even-better-errors",
8977
+
"libnpmaccess",
8978
+
"libnpmdiff",
8979
+
"libnpmexec",
8980
+
"libnpmfund",
8981
+
"libnpmorg",
8982
+
"libnpmpack",
8983
+
"libnpmpublish",
8984
+
"libnpmsearch",
8985
+
"libnpmteam",
8986
+
"libnpmversion",
8987
+
"make-fetch-happen",
8988
+
"minimatch",
8989
+
"minipass",
8990
+
"minipass-pipeline",
8991
+
"ms",
8992
+
"node-gyp",
8993
+
"nopt",
8994
+
"npm-audit-report",
8995
+
"npm-install-checks",
8996
+
"npm-package-arg",
8997
+
"npm-pick-manifest",
8998
+
"npm-profile",
8999
+
"npm-registry-fetch",
9000
+
"npm-user-validate",
9001
+
"p-map",
9002
+
"pacote",
9003
+
"parse-conflict-json",
9004
+
"proc-log",
9005
+
"qrcode-terminal",
9006
+
"read",
9007
+
"semver",
9008
+
"spdx-expression-parse",
9009
+
"ssri",
9010
+
"supports-color",
9011
+
"tar",
9012
+
"text-table",
9013
+
"tiny-relative-date",
9014
+
"treeverse",
9015
+
"validate-npm-package-name",
9016
+
"which"
9017
+
],
9018
+
"license": "Artistic-2.0",
9019
+
"workspaces": [
9020
+
"docs",
9021
+
"smoke-tests",
9022
+
"mock-globals",
9023
+
"mock-registry",
9024
+
"workspaces/*"
9025
+
],
9026
+
"dependencies": {
9027
+
"@isaacs/string-locale-compare": "^1.1.0",
9028
+
"@npmcli/arborist": "^9.1.6",
9029
+
"@npmcli/config": "^10.4.2",
9030
+
"@npmcli/fs": "^4.0.0",
9031
+
"@npmcli/map-workspaces": "^5.0.0",
9032
+
"@npmcli/package-json": "^7.0.1",
9033
+
"@npmcli/promise-spawn": "^8.0.3",
9034
+
"@npmcli/redact": "^3.2.2",
9035
+
"@npmcli/run-script": "^10.0.0",
9036
+
"@sigstore/tuf": "^4.0.0",
9037
+
"abbrev": "^3.0.1",
9038
+
"archy": "~1.0.0",
9039
+
"cacache": "^20.0.1",
9040
+
"chalk": "^5.6.2",
9041
+
"ci-info": "^4.3.1",
9042
+
"cli-columns": "^4.0.0",
9043
+
"fastest-levenshtein": "^1.0.16",
9044
+
"fs-minipass": "^3.0.3",
9045
+
"glob": "^11.0.3",
9046
+
"graceful-fs": "^4.2.11",
9047
+
"hosted-git-info": "^9.0.2",
9048
+
"ini": "^5.0.0",
9049
+
"init-package-json": "^8.2.2",
9050
+
"is-cidr": "^6.0.1",
9051
+
"json-parse-even-better-errors": "^4.0.0",
9052
+
"libnpmaccess": "^10.0.3",
9053
+
"libnpmdiff": "^8.0.9",
9054
+
"libnpmexec": "^10.1.8",
9055
+
"libnpmfund": "^7.0.9",
9056
+
"libnpmorg": "^8.0.1",
9057
+
"libnpmpack": "^9.0.9",
9058
+
"libnpmpublish": "^11.1.2",
9059
+
"libnpmsearch": "^9.0.1",
9060
+
"libnpmteam": "^8.0.2",
9061
+
"libnpmversion": "^8.0.2",
9062
+
"make-fetch-happen": "^15.0.2",
9063
+
"minimatch": "^10.0.3",
9064
+
"minipass": "^7.1.1",
9065
+
"minipass-pipeline": "^1.2.4",
9066
+
"ms": "^2.1.2",
9067
+
"node-gyp": "^11.4.2",
9068
+
"nopt": "^8.1.0",
9069
+
"npm-audit-report": "^6.0.0",
9070
+
"npm-install-checks": "^7.1.2",
9071
+
"npm-package-arg": "^13.0.1",
9072
+
"npm-pick-manifest": "^11.0.1",
9073
+
"npm-profile": "^12.0.0",
9074
+
"npm-registry-fetch": "^19.0.0",
9075
+
"npm-user-validate": "^3.0.0",
9076
+
"p-map": "^7.0.3",
9077
+
"pacote": "^21.0.3",
9078
+
"parse-conflict-json": "^4.0.0",
9079
+
"proc-log": "^5.0.0",
9080
+
"qrcode-terminal": "^0.12.0",
9081
+
"read": "^4.1.0",
9082
+
"semver": "^7.7.3",
9083
+
"spdx-expression-parse": "^4.0.0",
9084
+
"ssri": "^12.0.0",
9085
+
"supports-color": "^10.2.2",
9086
+
"tar": "^7.5.1",
9087
+
"text-table": "~0.2.0",
9088
+
"tiny-relative-date": "^2.0.2",
9089
+
"treeverse": "^3.0.0",
9090
+
"validate-npm-package-name": "^6.0.2",
9091
+
"which": "^5.0.0"
9092
+
},
9093
+
"bin": {
9094
+
"npm": "bin/npm-cli.js",
9095
+
"npx": "bin/npx-cli.js"
9096
+
},
9097
+
"engines": {
9098
+
"node": "^20.17.0 || >=22.9.0"
9099
+
}
9100
+
},
9101
+
"node_modules/npm/node_modules/@isaacs/balanced-match": {
9102
+
"version": "4.0.1",
9103
+
"inBundle": true,
9104
+
"license": "MIT",
9105
+
"engines": {
9106
+
"node": "20 || >=22"
9107
+
}
9108
+
},
9109
+
"node_modules/npm/node_modules/@isaacs/brace-expansion": {
9110
+
"version": "5.0.0",
9111
+
"inBundle": true,
9112
+
"license": "MIT",
9113
+
"dependencies": {
9114
+
"@isaacs/balanced-match": "^4.0.1"
9115
+
},
9116
+
"engines": {
9117
+
"node": "20 || >=22"
9118
+
}
9119
+
},
9120
+
"node_modules/npm/node_modules/@isaacs/cliui": {
9121
+
"version": "8.0.2",
9122
+
"inBundle": true,
9123
+
"license": "ISC",
9124
+
"dependencies": {
9125
+
"string-width": "^5.1.2",
9126
+
"string-width-cjs": "npm:string-width@^4.2.0",
9127
+
"strip-ansi": "^7.0.1",
9128
+
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
9129
+
"wrap-ansi": "^8.1.0",
9130
+
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
9131
+
},
9132
+
"engines": {
9133
+
"node": ">=12"
9134
+
}
9135
+
},
9136
+
"node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": {
9137
+
"version": "6.2.2",
9138
+
"inBundle": true,
9139
+
"license": "MIT",
9140
+
"engines": {
9141
+
"node": ">=12"
9142
+
},
9143
+
"funding": {
9144
+
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
9145
+
}
9146
+
},
9147
+
"node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": {
9148
+
"version": "9.2.2",
9149
+
"inBundle": true,
9150
+
"license": "MIT"
9151
+
},
9152
+
"node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": {
9153
+
"version": "5.1.2",
9154
+
"inBundle": true,
9155
+
"license": "MIT",
9156
+
"dependencies": {
9157
+
"eastasianwidth": "^0.2.0",
9158
+
"emoji-regex": "^9.2.2",
9159
+
"strip-ansi": "^7.0.1"
9160
+
},
9161
+
"engines": {
9162
+
"node": ">=12"
9163
+
},
9164
+
"funding": {
9165
+
"url": "https://github.com/sponsors/sindresorhus"
9166
+
}
9167
+
},
9168
+
"node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": {
9169
+
"version": "7.1.2",
9170
+
"inBundle": true,
9171
+
"license": "MIT",
9172
+
"dependencies": {
9173
+
"ansi-regex": "^6.0.1"
9174
+
},
9175
+
"engines": {
9176
+
"node": ">=12"
9177
+
},
9178
+
"funding": {
9179
+
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
9180
+
}
9181
+
},
9182
+
"node_modules/npm/node_modules/@isaacs/fs-minipass": {
9183
+
"version": "4.0.1",
9184
+
"inBundle": true,
9185
+
"license": "ISC",
9186
+
"dependencies": {
9187
+
"minipass": "^7.0.4"
9188
+
},
9189
+
"engines": {
9190
+
"node": ">=18.0.0"
9191
+
}
9192
+
},
9193
+
"node_modules/npm/node_modules/@isaacs/string-locale-compare": {
9194
+
"version": "1.1.0",
9195
+
"inBundle": true,
9196
+
"license": "ISC"
9197
+
},
9198
+
"node_modules/npm/node_modules/@npmcli/agent": {
9199
+
"version": "4.0.0",
9200
+
"inBundle": true,
9201
+
"license": "ISC",
9202
+
"dependencies": {
9203
+
"agent-base": "^7.1.0",
9204
+
"http-proxy-agent": "^7.0.0",
9205
+
"https-proxy-agent": "^7.0.1",
9206
+
"lru-cache": "^11.2.1",
9207
+
"socks-proxy-agent": "^8.0.3"
9208
+
},
9209
+
"engines": {
9210
+
"node": "^20.17.0 || >=22.9.0"
9211
+
}
9212
+
},
9213
+
"node_modules/npm/node_modules/@npmcli/arborist": {
9214
+
"version": "9.1.6",
9215
+
"inBundle": true,
9216
+
"license": "ISC",
9217
+
"dependencies": {
9218
+
"@isaacs/string-locale-compare": "^1.1.0",
9219
+
"@npmcli/fs": "^4.0.0",
9220
+
"@npmcli/installed-package-contents": "^3.0.0",
9221
+
"@npmcli/map-workspaces": "^5.0.0",
9222
+
"@npmcli/metavuln-calculator": "^9.0.2",
9223
+
"@npmcli/name-from-folder": "^3.0.0",
9224
+
"@npmcli/node-gyp": "^4.0.0",
9225
+
"@npmcli/package-json": "^7.0.0",
9226
+
"@npmcli/query": "^4.0.0",
9227
+
"@npmcli/redact": "^3.0.0",
9228
+
"@npmcli/run-script": "^10.0.0",
9229
+
"bin-links": "^5.0.0",
9230
+
"cacache": "^20.0.1",
9231
+
"common-ancestor-path": "^1.0.1",
9232
+
"hosted-git-info": "^9.0.0",
9233
+
"json-stringify-nice": "^1.1.4",
9234
+
"lru-cache": "^11.2.1",
9235
+
"minimatch": "^10.0.3",
9236
+
"nopt": "^8.0.0",
9237
+
"npm-install-checks": "^7.1.0",
9238
+
"npm-package-arg": "^13.0.0",
9239
+
"npm-pick-manifest": "^11.0.1",
9240
+
"npm-registry-fetch": "^19.0.0",
9241
+
"pacote": "^21.0.2",
9242
+
"parse-conflict-json": "^4.0.0",
9243
+
"proc-log": "^5.0.0",
9244
+
"proggy": "^3.0.0",
9245
+
"promise-all-reject-late": "^1.0.0",
9246
+
"promise-call-limit": "^3.0.1",
9247
+
"semver": "^7.3.7",
9248
+
"ssri": "^12.0.0",
9249
+
"treeverse": "^3.0.0",
9250
+
"walk-up-path": "^4.0.0"
9251
+
},
9252
+
"bin": {
9253
+
"arborist": "bin/index.js"
9254
+
},
9255
+
"engines": {
9256
+
"node": "^20.17.0 || >=22.9.0"
9257
+
}
9258
+
},
9259
+
"node_modules/npm/node_modules/@npmcli/config": {
9260
+
"version": "10.4.2",
9261
+
"inBundle": true,
9262
+
"license": "ISC",
9263
+
"dependencies": {
9264
+
"@npmcli/map-workspaces": "^5.0.0",
9265
+
"@npmcli/package-json": "^7.0.0",
9266
+
"ci-info": "^4.0.0",
9267
+
"ini": "^5.0.0",
9268
+
"nopt": "^8.1.0",
9269
+
"proc-log": "^5.0.0",
9270
+
"semver": "^7.3.5",
9271
+
"walk-up-path": "^4.0.0"
9272
+
},
9273
+
"engines": {
9274
+
"node": "^20.17.0 || >=22.9.0"
9275
+
}
9276
+
},
9277
+
"node_modules/npm/node_modules/@npmcli/fs": {
9278
+
"version": "4.0.0",
9279
+
"inBundle": true,
9280
+
"license": "ISC",
9281
+
"dependencies": {
9282
+
"semver": "^7.3.5"
9283
+
},
9284
+
"engines": {
9285
+
"node": "^18.17.0 || >=20.5.0"
9286
+
}
9287
+
},
9288
+
"node_modules/npm/node_modules/@npmcli/git": {
9289
+
"version": "7.0.0",
9290
+
"inBundle": true,
9291
+
"license": "ISC",
9292
+
"dependencies": {
9293
+
"@npmcli/promise-spawn": "^8.0.0",
9294
+
"ini": "^5.0.0",
9295
+
"lru-cache": "^11.2.1",
9296
+
"npm-pick-manifest": "^11.0.1",
9297
+
"proc-log": "^5.0.0",
9298
+
"promise-retry": "^2.0.1",
9299
+
"semver": "^7.3.5",
9300
+
"which": "^5.0.0"
9301
+
},
9302
+
"engines": {
9303
+
"node": "^20.17.0 || >=22.9.0"
9304
+
}
9305
+
},
9306
+
"node_modules/npm/node_modules/@npmcli/installed-package-contents": {
9307
+
"version": "3.0.0",
9308
+
"inBundle": true,
9309
+
"license": "ISC",
9310
+
"dependencies": {
9311
+
"npm-bundled": "^4.0.0",
9312
+
"npm-normalize-package-bin": "^4.0.0"
9313
+
},
9314
+
"bin": {
9315
+
"installed-package-contents": "bin/index.js"
9316
+
},
9317
+
"engines": {
9318
+
"node": "^18.17.0 || >=20.5.0"
9319
+
}
9320
+
},
9321
+
"node_modules/npm/node_modules/@npmcli/map-workspaces": {
9322
+
"version": "5.0.0",
9323
+
"inBundle": true,
9324
+
"license": "ISC",
9325
+
"dependencies": {
9326
+
"@npmcli/name-from-folder": "^3.0.0",
9327
+
"@npmcli/package-json": "^7.0.0",
9328
+
"glob": "^11.0.3",
9329
+
"minimatch": "^10.0.3"
9330
+
},
9331
+
"engines": {
9332
+
"node": "^20.17.0 || >=22.9.0"
9333
+
}
9334
+
},
9335
+
"node_modules/npm/node_modules/@npmcli/metavuln-calculator": {
9336
+
"version": "9.0.2",
9337
+
"inBundle": true,
9338
+
"license": "ISC",
9339
+
"dependencies": {
9340
+
"cacache": "^20.0.0",
9341
+
"json-parse-even-better-errors": "^4.0.0",
9342
+
"pacote": "^21.0.0",
9343
+
"proc-log": "^5.0.0",
9344
+
"semver": "^7.3.5"
9345
+
},
9346
+
"engines": {
9347
+
"node": "^20.17.0 || >=22.9.0"
9348
+
}
9349
+
},
9350
+
"node_modules/npm/node_modules/@npmcli/name-from-folder": {
9351
+
"version": "3.0.0",
9352
+
"inBundle": true,
9353
+
"license": "ISC",
9354
+
"engines": {
9355
+
"node": "^18.17.0 || >=20.5.0"
9356
+
}
9357
+
},
9358
+
"node_modules/npm/node_modules/@npmcli/node-gyp": {
9359
+
"version": "4.0.0",
9360
+
"inBundle": true,
9361
+
"license": "ISC",
9362
+
"engines": {
9363
+
"node": "^18.17.0 || >=20.5.0"
9364
+
}
9365
+
},
9366
+
"node_modules/npm/node_modules/@npmcli/package-json": {
9367
+
"version": "7.0.1",
9368
+
"inBundle": true,
9369
+
"license": "ISC",
9370
+
"dependencies": {
9371
+
"@npmcli/git": "^7.0.0",
9372
+
"glob": "^11.0.3",
9373
+
"hosted-git-info": "^9.0.0",
9374
+
"json-parse-even-better-errors": "^4.0.0",
9375
+
"proc-log": "^5.0.0",
9376
+
"semver": "^7.5.3",
9377
+
"validate-npm-package-license": "^3.0.4"
9378
+
},
9379
+
"engines": {
9380
+
"node": "^20.17.0 || >=22.9.0"
9381
+
}
9382
+
},
9383
+
"node_modules/npm/node_modules/@npmcli/promise-spawn": {
9384
+
"version": "8.0.3",
9385
+
"inBundle": true,
9386
+
"license": "ISC",
9387
+
"dependencies": {
9388
+
"which": "^5.0.0"
9389
+
},
9390
+
"engines": {
9391
+
"node": "^18.17.0 || >=20.5.0"
9392
+
}
9393
+
},
9394
+
"node_modules/npm/node_modules/@npmcli/query": {
9395
+
"version": "4.0.1",
9396
+
"inBundle": true,
9397
+
"license": "ISC",
9398
+
"dependencies": {
9399
+
"postcss-selector-parser": "^7.0.0"
9400
+
},
9401
+
"engines": {
9402
+
"node": "^18.17.0 || >=20.5.0"
9403
+
}
9404
+
},
9405
+
"node_modules/npm/node_modules/@npmcli/redact": {
9406
+
"version": "3.2.2",
9407
+
"inBundle": true,
9408
+
"license": "ISC",
9409
+
"engines": {
9410
+
"node": "^18.17.0 || >=20.5.0"
9411
+
}
9412
+
},
9413
+
"node_modules/npm/node_modules/@npmcli/run-script": {
9414
+
"version": "10.0.0",
9415
+
"inBundle": true,
9416
+
"license": "ISC",
9417
+
"dependencies": {
9418
+
"@npmcli/node-gyp": "^4.0.0",
9419
+
"@npmcli/package-json": "^7.0.0",
9420
+
"@npmcli/promise-spawn": "^8.0.0",
9421
+
"node-gyp": "^11.0.0",
9422
+
"proc-log": "^5.0.0",
9423
+
"which": "^5.0.0"
9424
+
},
9425
+
"engines": {
9426
+
"node": "^20.17.0 || >=22.9.0"
9427
+
}
9428
+
},
9429
+
"node_modules/npm/node_modules/@pkgjs/parseargs": {
9430
+
"version": "0.11.0",
9431
+
"inBundle": true,
9432
+
"license": "MIT",
9433
+
"optional": true,
9434
+
"engines": {
9435
+
"node": ">=14"
9436
+
}
9437
+
},
9438
+
"node_modules/npm/node_modules/@sigstore/bundle": {
9439
+
"version": "4.0.0",
9440
+
"inBundle": true,
9441
+
"license": "Apache-2.0",
9442
+
"dependencies": {
9443
+
"@sigstore/protobuf-specs": "^0.5.0"
9444
+
},
9445
+
"engines": {
9446
+
"node": "^20.17.0 || >=22.9.0"
9447
+
}
9448
+
},
9449
+
"node_modules/npm/node_modules/@sigstore/core": {
9450
+
"version": "3.0.0",
9451
+
"inBundle": true,
9452
+
"license": "Apache-2.0",
9453
+
"engines": {
9454
+
"node": "^20.17.0 || >=22.9.0"
9455
+
}
9456
+
},
9457
+
"node_modules/npm/node_modules/@sigstore/protobuf-specs": {
9458
+
"version": "0.5.0",
9459
+
"inBundle": true,
9460
+
"license": "Apache-2.0",
9461
+
"engines": {
9462
+
"node": "^18.17.0 || >=20.5.0"
9463
+
}
9464
+
},
9465
+
"node_modules/npm/node_modules/@sigstore/sign": {
9466
+
"version": "4.0.1",
9467
+
"inBundle": true,
9468
+
"license": "Apache-2.0",
9469
+
"dependencies": {
9470
+
"@sigstore/bundle": "^4.0.0",
9471
+
"@sigstore/core": "^3.0.0",
9472
+
"@sigstore/protobuf-specs": "^0.5.0",
9473
+
"make-fetch-happen": "^15.0.2",
9474
+
"proc-log": "^5.0.0",
9475
+
"promise-retry": "^2.0.1"
9476
+
},
9477
+
"engines": {
9478
+
"node": "^20.17.0 || >=22.9.0"
9479
+
}
9480
+
},
9481
+
"node_modules/npm/node_modules/@sigstore/tuf": {
9482
+
"version": "4.0.0",
9483
+
"inBundle": true,
9484
+
"license": "Apache-2.0",
9485
+
"dependencies": {
9486
+
"@sigstore/protobuf-specs": "^0.5.0",
9487
+
"tuf-js": "^4.0.0"
9488
+
},
9489
+
"engines": {
9490
+
"node": "^20.17.0 || >=22.9.0"
9491
+
}
9492
+
},
9493
+
"node_modules/npm/node_modules/@sigstore/verify": {
9494
+
"version": "3.0.0",
9495
+
"inBundle": true,
9496
+
"license": "Apache-2.0",
9497
+
"dependencies": {
9498
+
"@sigstore/bundle": "^4.0.0",
9499
+
"@sigstore/core": "^3.0.0",
9500
+
"@sigstore/protobuf-specs": "^0.5.0"
9501
+
},
9502
+
"engines": {
9503
+
"node": "^20.17.0 || >=22.9.0"
9504
+
}
9505
+
},
9506
+
"node_modules/npm/node_modules/@tufjs/canonical-json": {
9507
+
"version": "2.0.0",
9508
+
"inBundle": true,
9509
+
"license": "MIT",
9510
+
"engines": {
9511
+
"node": "^16.14.0 || >=18.0.0"
9512
+
}
9513
+
},
9514
+
"node_modules/npm/node_modules/@tufjs/models": {
9515
+
"version": "4.0.0",
9516
+
"inBundle": true,
9517
+
"license": "MIT",
9518
+
"dependencies": {
9519
+
"@tufjs/canonical-json": "2.0.0",
9520
+
"minimatch": "^9.0.5"
9521
+
},
9522
+
"engines": {
9523
+
"node": "^20.17.0 || >=22.9.0"
9524
+
}
9525
+
},
9526
+
"node_modules/npm/node_modules/@tufjs/models/node_modules/minimatch": {
9527
+
"version": "9.0.5",
9528
+
"inBundle": true,
9529
+
"license": "ISC",
9530
+
"dependencies": {
9531
+
"brace-expansion": "^2.0.1"
9532
+
},
9533
+
"engines": {
9534
+
"node": ">=16 || 14 >=14.17"
9535
+
},
9536
+
"funding": {
9537
+
"url": "https://github.com/sponsors/isaacs"
9538
+
}
9539
+
},
9540
+
"node_modules/npm/node_modules/abbrev": {
9541
+
"version": "3.0.1",
9542
+
"inBundle": true,
9543
+
"license": "ISC",
9544
+
"engines": {
9545
+
"node": "^18.17.0 || >=20.5.0"
9546
+
}
9547
+
},
9548
+
"node_modules/npm/node_modules/agent-base": {
9549
+
"version": "7.1.4",
9550
+
"inBundle": true,
9551
+
"license": "MIT",
9552
+
"engines": {
9553
+
"node": ">= 14"
9554
+
}
9555
+
},
9556
+
"node_modules/npm/node_modules/ansi-regex": {
9557
+
"version": "5.0.1",
9558
+
"inBundle": true,
9559
+
"license": "MIT",
9560
+
"engines": {
9561
+
"node": ">=8"
9562
+
}
9563
+
},
9564
+
"node_modules/npm/node_modules/ansi-styles": {
9565
+
"version": "6.2.3",
9566
+
"inBundle": true,
9567
+
"license": "MIT",
9568
+
"engines": {
9569
+
"node": ">=12"
9570
+
},
9571
+
"funding": {
9572
+
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
9573
+
}
9574
+
},
9575
+
"node_modules/npm/node_modules/aproba": {
9576
+
"version": "2.1.0",
9577
+
"inBundle": true,
9578
+
"license": "ISC"
9579
+
},
9580
+
"node_modules/npm/node_modules/archy": {
9581
+
"version": "1.0.0",
9582
+
"inBundle": true,
9583
+
"license": "MIT"
9584
+
},
9585
+
"node_modules/npm/node_modules/balanced-match": {
9586
+
"version": "1.0.2",
9587
+
"inBundle": true,
9588
+
"license": "MIT"
9589
+
},
9590
+
"node_modules/npm/node_modules/bin-links": {
9591
+
"version": "5.0.0",
9592
+
"inBundle": true,
9593
+
"license": "ISC",
9594
+
"dependencies": {
9595
+
"cmd-shim": "^7.0.0",
9596
+
"npm-normalize-package-bin": "^4.0.0",
9597
+
"proc-log": "^5.0.0",
9598
+
"read-cmd-shim": "^5.0.0",
9599
+
"write-file-atomic": "^6.0.0"
9600
+
},
9601
+
"engines": {
9602
+
"node": "^18.17.0 || >=20.5.0"
9603
+
}
9604
+
},
9605
+
"node_modules/npm/node_modules/binary-extensions": {
9606
+
"version": "3.1.0",
9607
+
"inBundle": true,
9608
+
"license": "MIT",
9609
+
"engines": {
9610
+
"node": ">=18.20"
9611
+
},
9612
+
"funding": {
9613
+
"url": "https://github.com/sponsors/sindresorhus"
9614
+
}
9615
+
},
9616
+
"node_modules/npm/node_modules/brace-expansion": {
9617
+
"version": "2.0.2",
9618
+
"inBundle": true,
9619
+
"license": "MIT",
9620
+
"dependencies": {
9621
+
"balanced-match": "^1.0.0"
9622
+
}
9623
+
},
9624
+
"node_modules/npm/node_modules/cacache": {
9625
+
"version": "20.0.1",
9626
+
"inBundle": true,
9627
+
"license": "ISC",
9628
+
"dependencies": {
9629
+
"@npmcli/fs": "^4.0.0",
9630
+
"fs-minipass": "^3.0.0",
9631
+
"glob": "^11.0.3",
9632
+
"lru-cache": "^11.1.0",
9633
+
"minipass": "^7.0.3",
9634
+
"minipass-collect": "^2.0.1",
9635
+
"minipass-flush": "^1.0.5",
9636
+
"minipass-pipeline": "^1.2.4",
9637
+
"p-map": "^7.0.2",
9638
+
"ssri": "^12.0.0",
9639
+
"unique-filename": "^4.0.0"
9640
+
},
9641
+
"engines": {
9642
+
"node": "^20.17.0 || >=22.9.0"
9643
+
}
9644
+
},
9645
+
"node_modules/npm/node_modules/chalk": {
9646
+
"version": "5.6.2",
9647
+
"inBundle": true,
9648
+
"license": "MIT",
9649
+
"engines": {
9650
+
"node": "^12.17.0 || ^14.13 || >=16.0.0"
9651
+
},
9652
+
"funding": {
9653
+
"url": "https://github.com/chalk/chalk?sponsor=1"
9654
+
}
9655
+
},
9656
+
"node_modules/npm/node_modules/chownr": {
9657
+
"version": "3.0.0",
9658
+
"inBundle": true,
9659
+
"license": "BlueOak-1.0.0",
9660
+
"engines": {
9661
+
"node": ">=18"
9662
+
}
9663
+
},
9664
+
"node_modules/npm/node_modules/ci-info": {
9665
+
"version": "4.3.1",
9666
+
"funding": [
9667
+
{
9668
+
"type": "github",
9669
+
"url": "https://github.com/sponsors/sibiraj-s"
9670
+
}
9671
+
],
9672
+
"inBundle": true,
9673
+
"license": "MIT",
9674
+
"engines": {
9675
+
"node": ">=8"
9676
+
}
9677
+
},
9678
+
"node_modules/npm/node_modules/cidr-regex": {
9679
+
"version": "5.0.1",
9680
+
"inBundle": true,
9681
+
"license": "BSD-2-Clause",
9682
+
"dependencies": {
9683
+
"ip-regex": "5.0.0"
9684
+
},
9685
+
"engines": {
9686
+
"node": ">=20"
9687
+
}
9688
+
},
9689
+
"node_modules/npm/node_modules/cli-columns": {
9690
+
"version": "4.0.0",
9691
+
"inBundle": true,
9692
+
"license": "MIT",
9693
+
"dependencies": {
9694
+
"string-width": "^4.2.3",
9695
+
"strip-ansi": "^6.0.1"
9696
+
},
9697
+
"engines": {
9698
+
"node": ">= 10"
9699
+
}
9700
+
},
9701
+
"node_modules/npm/node_modules/cmd-shim": {
9702
+
"version": "7.0.0",
9703
+
"inBundle": true,
9704
+
"license": "ISC",
9705
+
"engines": {
9706
+
"node": "^18.17.0 || >=20.5.0"
9707
+
}
9708
+
},
9709
+
"node_modules/npm/node_modules/color-convert": {
9710
+
"version": "2.0.1",
9711
+
"inBundle": true,
9712
+
"license": "MIT",
9713
+
"dependencies": {
9714
+
"color-name": "~1.1.4"
9715
+
},
9716
+
"engines": {
9717
+
"node": ">=7.0.0"
9718
+
}
9719
+
},
9720
+
"node_modules/npm/node_modules/color-name": {
9721
+
"version": "1.1.4",
9722
+
"inBundle": true,
9723
+
"license": "MIT"
9724
+
},
9725
+
"node_modules/npm/node_modules/common-ancestor-path": {
9726
+
"version": "1.0.1",
9727
+
"inBundle": true,
9728
+
"license": "ISC"
9729
+
},
9730
+
"node_modules/npm/node_modules/cross-spawn": {
9731
+
"version": "7.0.6",
9732
+
"inBundle": true,
9733
+
"license": "MIT",
9734
+
"dependencies": {
9735
+
"path-key": "^3.1.0",
9736
+
"shebang-command": "^2.0.0",
9737
+
"which": "^2.0.1"
9738
+
},
9739
+
"engines": {
9740
+
"node": ">= 8"
9741
+
}
9742
+
},
9743
+
"node_modules/npm/node_modules/cross-spawn/node_modules/isexe": {
9744
+
"version": "2.0.0",
9745
+
"inBundle": true,
9746
+
"license": "ISC"
9747
+
},
9748
+
"node_modules/npm/node_modules/cross-spawn/node_modules/which": {
9749
+
"version": "2.0.2",
9750
+
"inBundle": true,
9751
+
"license": "ISC",
9752
+
"dependencies": {
9753
+
"isexe": "^2.0.0"
9754
+
},
9755
+
"bin": {
9756
+
"node-which": "bin/node-which"
9757
+
},
9758
+
"engines": {
9759
+
"node": ">= 8"
9760
+
}
9761
+
},
9762
+
"node_modules/npm/node_modules/cssesc": {
9763
+
"version": "3.0.0",
9764
+
"inBundle": true,
9765
+
"license": "MIT",
9766
+
"bin": {
9767
+
"cssesc": "bin/cssesc"
9768
+
},
9769
+
"engines": {
9770
+
"node": ">=4"
9771
+
}
9772
+
},
9773
+
"node_modules/npm/node_modules/debug": {
9774
+
"version": "4.4.3",
9775
+
"inBundle": true,
9776
+
"license": "MIT",
9777
+
"dependencies": {
9778
+
"ms": "^2.1.3"
9779
+
},
9780
+
"engines": {
9781
+
"node": ">=6.0"
9782
+
},
9783
+
"peerDependenciesMeta": {
9784
+
"supports-color": {
9785
+
"optional": true
9786
+
}
9787
+
}
9788
+
},
9789
+
"node_modules/npm/node_modules/diff": {
9790
+
"version": "8.0.2",
9791
+
"inBundle": true,
9792
+
"license": "BSD-3-Clause",
9793
+
"engines": {
9794
+
"node": ">=0.3.1"
9795
+
}
9796
+
},
9797
+
"node_modules/npm/node_modules/eastasianwidth": {
9798
+
"version": "0.2.0",
9799
+
"inBundle": true,
9800
+
"license": "MIT"
9801
+
},
9802
+
"node_modules/npm/node_modules/emoji-regex": {
9803
+
"version": "8.0.0",
9804
+
"inBundle": true,
9805
+
"license": "MIT"
9806
+
},
9807
+
"node_modules/npm/node_modules/encoding": {
9808
+
"version": "0.1.13",
9809
+
"inBundle": true,
9810
+
"license": "MIT",
9811
+
"optional": true,
9812
+
"dependencies": {
9813
+
"iconv-lite": "^0.6.2"
9814
+
}
9815
+
},
9816
+
"node_modules/npm/node_modules/env-paths": {
9817
+
"version": "2.2.1",
9818
+
"inBundle": true,
9819
+
"license": "MIT",
9820
+
"engines": {
9821
+
"node": ">=6"
9822
+
}
9823
+
},
9824
+
"node_modules/npm/node_modules/err-code": {
9825
+
"version": "2.0.3",
9826
+
"inBundle": true,
9827
+
"license": "MIT"
9828
+
},
9829
+
"node_modules/npm/node_modules/exponential-backoff": {
9830
+
"version": "3.1.2",
9831
+
"inBundle": true,
9832
+
"license": "Apache-2.0"
9833
+
},
9834
+
"node_modules/npm/node_modules/fastest-levenshtein": {
9835
+
"version": "1.0.16",
9836
+
"inBundle": true,
9837
+
"license": "MIT",
9838
+
"engines": {
9839
+
"node": ">= 4.9.1"
9840
+
}
9841
+
},
9842
+
"node_modules/npm/node_modules/foreground-child": {
9843
+
"version": "3.3.1",
9844
+
"inBundle": true,
9845
+
"license": "ISC",
9846
+
"dependencies": {
9847
+
"cross-spawn": "^7.0.6",
9848
+
"signal-exit": "^4.0.1"
9849
+
},
9850
+
"engines": {
9851
+
"node": ">=14"
9852
+
},
9853
+
"funding": {
9854
+
"url": "https://github.com/sponsors/isaacs"
9855
+
}
9856
+
},
9857
+
"node_modules/npm/node_modules/fs-minipass": {
9858
+
"version": "3.0.3",
9859
+
"inBundle": true,
9860
+
"license": "ISC",
9861
+
"dependencies": {
9862
+
"minipass": "^7.0.3"
9863
+
},
9864
+
"engines": {
9865
+
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
9866
+
}
9867
+
},
9868
+
"node_modules/npm/node_modules/glob": {
9869
+
"version": "11.0.3",
9870
+
"inBundle": true,
9871
+
"license": "ISC",
9872
+
"dependencies": {
9873
+
"foreground-child": "^3.3.1",
9874
+
"jackspeak": "^4.1.1",
9875
+
"minimatch": "^10.0.3",
9876
+
"minipass": "^7.1.2",
9877
+
"package-json-from-dist": "^1.0.0",
9878
+
"path-scurry": "^2.0.0"
9879
+
},
9880
+
"bin": {
9881
+
"glob": "dist/esm/bin.mjs"
9882
+
},
9883
+
"engines": {
9884
+
"node": "20 || >=22"
9885
+
},
9886
+
"funding": {
9887
+
"url": "https://github.com/sponsors/isaacs"
9888
+
}
9889
+
},
9890
+
"node_modules/npm/node_modules/graceful-fs": {
9891
+
"version": "4.2.11",
9892
+
"inBundle": true,
9893
+
"license": "ISC"
9894
+
},
9895
+
"node_modules/npm/node_modules/hosted-git-info": {
9896
+
"version": "9.0.2",
9897
+
"inBundle": true,
9898
+
"license": "ISC",
9899
+
"dependencies": {
9900
+
"lru-cache": "^11.1.0"
9901
+
},
9902
+
"engines": {
9903
+
"node": "^20.17.0 || >=22.9.0"
9904
+
}
9905
+
},
9906
+
"node_modules/npm/node_modules/http-cache-semantics": {
9907
+
"version": "4.2.0",
9908
+
"inBundle": true,
9909
+
"license": "BSD-2-Clause"
9910
+
},
9911
+
"node_modules/npm/node_modules/http-proxy-agent": {
9912
+
"version": "7.0.2",
9913
+
"inBundle": true,
9914
+
"license": "MIT",
9915
+
"dependencies": {
9916
+
"agent-base": "^7.1.0",
9917
+
"debug": "^4.3.4"
9918
+
},
9919
+
"engines": {
9920
+
"node": ">= 14"
9921
+
}
9922
+
},
9923
+
"node_modules/npm/node_modules/https-proxy-agent": {
9924
+
"version": "7.0.6",
9925
+
"inBundle": true,
9926
+
"license": "MIT",
9927
+
"dependencies": {
9928
+
"agent-base": "^7.1.2",
9929
+
"debug": "4"
9930
+
},
9931
+
"engines": {
9932
+
"node": ">= 14"
9933
+
}
9934
+
},
9935
+
"node_modules/npm/node_modules/iconv-lite": {
9936
+
"version": "0.6.3",
9937
+
"inBundle": true,
9938
+
"license": "MIT",
9939
+
"optional": true,
9940
+
"dependencies": {
9941
+
"safer-buffer": ">= 2.1.2 < 3.0.0"
9942
+
},
9943
+
"engines": {
9944
+
"node": ">=0.10.0"
9945
+
}
9946
+
},
9947
+
"node_modules/npm/node_modules/ignore-walk": {
9948
+
"version": "8.0.0",
9949
+
"inBundle": true,
9950
+
"license": "ISC",
9951
+
"dependencies": {
9952
+
"minimatch": "^10.0.3"
9953
+
},
9954
+
"engines": {
9955
+
"node": "^20.17.0 || >=22.9.0"
9956
+
}
9957
+
},
9958
+
"node_modules/npm/node_modules/imurmurhash": {
9959
+
"version": "0.1.4",
9960
+
"inBundle": true,
9961
+
"license": "MIT",
9962
+
"engines": {
9963
+
"node": ">=0.8.19"
9964
+
}
9965
+
},
9966
+
"node_modules/npm/node_modules/ini": {
9967
+
"version": "5.0.0",
9968
+
"inBundle": true,
9969
+
"license": "ISC",
9970
+
"engines": {
9971
+
"node": "^18.17.0 || >=20.5.0"
9972
+
}
9973
+
},
9974
+
"node_modules/npm/node_modules/init-package-json": {
9975
+
"version": "8.2.2",
9976
+
"inBundle": true,
9977
+
"license": "ISC",
9978
+
"dependencies": {
9979
+
"@npmcli/package-json": "^7.0.0",
9980
+
"npm-package-arg": "^13.0.0",
9981
+
"promzard": "^2.0.0",
9982
+
"read": "^4.0.0",
9983
+
"semver": "^7.7.2",
9984
+
"validate-npm-package-license": "^3.0.4",
9985
+
"validate-npm-package-name": "^6.0.2"
9986
+
},
9987
+
"engines": {
9988
+
"node": "^20.17.0 || >=22.9.0"
9989
+
}
9990
+
},
9991
+
"node_modules/npm/node_modules/ip-address": {
9992
+
"version": "10.0.1",
9993
+
"inBundle": true,
9994
+
"license": "MIT",
9995
+
"engines": {
9996
+
"node": ">= 12"
9997
+
}
9998
+
},
9999
+
"node_modules/npm/node_modules/ip-regex": {
10000
+
"version": "5.0.0",
10001
+
"inBundle": true,
10002
+
"license": "MIT",
10003
+
"engines": {
10004
+
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
10005
+
},
10006
+
"funding": {
10007
+
"url": "https://github.com/sponsors/sindresorhus"
10008
+
}
10009
+
},
10010
+
"node_modules/npm/node_modules/is-cidr": {
10011
+
"version": "6.0.1",
10012
+
"inBundle": true,
10013
+
"license": "BSD-2-Clause",
10014
+
"dependencies": {
10015
+
"cidr-regex": "5.0.1"
10016
+
},
10017
+
"engines": {
10018
+
"node": ">=20"
10019
+
}
10020
+
},
10021
+
"node_modules/npm/node_modules/is-fullwidth-code-point": {
10022
+
"version": "3.0.0",
10023
+
"inBundle": true,
10024
+
"license": "MIT",
10025
+
"engines": {
10026
+
"node": ">=8"
10027
+
}
10028
+
},
10029
+
"node_modules/npm/node_modules/isexe": {
10030
+
"version": "3.1.1",
10031
+
"inBundle": true,
10032
+
"license": "ISC",
10033
+
"engines": {
10034
+
"node": ">=16"
10035
+
}
10036
+
},
10037
+
"node_modules/npm/node_modules/jackspeak": {
10038
+
"version": "4.1.1",
10039
+
"inBundle": true,
10040
+
"license": "BlueOak-1.0.0",
10041
+
"dependencies": {
10042
+
"@isaacs/cliui": "^8.0.2"
10043
+
},
10044
+
"engines": {
10045
+
"node": "20 || >=22"
10046
+
},
10047
+
"funding": {
10048
+
"url": "https://github.com/sponsors/isaacs"
10049
+
}
10050
+
},
10051
+
"node_modules/npm/node_modules/json-parse-even-better-errors": {
10052
+
"version": "4.0.0",
10053
+
"inBundle": true,
10054
+
"license": "MIT",
10055
+
"engines": {
10056
+
"node": "^18.17.0 || >=20.5.0"
10057
+
}
10058
+
},
10059
+
"node_modules/npm/node_modules/json-stringify-nice": {
10060
+
"version": "1.1.4",
10061
+
"inBundle": true,
10062
+
"license": "ISC",
10063
+
"funding": {
10064
+
"url": "https://github.com/sponsors/isaacs"
10065
+
}
10066
+
},
10067
+
"node_modules/npm/node_modules/jsonparse": {
10068
+
"version": "1.3.1",
10069
+
"engines": [
10070
+
"node >= 0.2.0"
10071
+
],
10072
+
"inBundle": true,
10073
+
"license": "MIT"
10074
+
},
10075
+
"node_modules/npm/node_modules/just-diff": {
10076
+
"version": "6.0.2",
10077
+
"inBundle": true,
10078
+
"license": "MIT"
10079
+
},
10080
+
"node_modules/npm/node_modules/just-diff-apply": {
10081
+
"version": "5.5.0",
10082
+
"inBundle": true,
10083
+
"license": "MIT"
10084
+
},
10085
+
"node_modules/npm/node_modules/libnpmaccess": {
10086
+
"version": "10.0.3",
10087
+
"inBundle": true,
10088
+
"license": "ISC",
10089
+
"dependencies": {
10090
+
"npm-package-arg": "^13.0.0",
10091
+
"npm-registry-fetch": "^19.0.0"
10092
+
},
10093
+
"engines": {
10094
+
"node": "^20.17.0 || >=22.9.0"
10095
+
}
10096
+
},
10097
+
"node_modules/npm/node_modules/libnpmdiff": {
10098
+
"version": "8.0.9",
10099
+
"inBundle": true,
10100
+
"license": "ISC",
10101
+
"dependencies": {
10102
+
"@npmcli/arborist": "^9.1.6",
10103
+
"@npmcli/installed-package-contents": "^3.0.0",
10104
+
"binary-extensions": "^3.0.0",
10105
+
"diff": "^8.0.2",
10106
+
"minimatch": "^10.0.3",
10107
+
"npm-package-arg": "^13.0.0",
10108
+
"pacote": "^21.0.2",
10109
+
"tar": "^7.5.1"
10110
+
},
10111
+
"engines": {
10112
+
"node": "^20.17.0 || >=22.9.0"
10113
+
}
10114
+
},
10115
+
"node_modules/npm/node_modules/libnpmexec": {
10116
+
"version": "10.1.8",
10117
+
"inBundle": true,
10118
+
"license": "ISC",
10119
+
"dependencies": {
10120
+
"@npmcli/arborist": "^9.1.6",
10121
+
"@npmcli/package-json": "^7.0.0",
10122
+
"@npmcli/run-script": "^10.0.0",
10123
+
"ci-info": "^4.0.0",
10124
+
"npm-package-arg": "^13.0.0",
10125
+
"pacote": "^21.0.2",
10126
+
"proc-log": "^5.0.0",
10127
+
"promise-retry": "^2.0.1",
10128
+
"read": "^4.0.0",
10129
+
"semver": "^7.3.7",
10130
+
"signal-exit": "^4.1.0",
10131
+
"walk-up-path": "^4.0.0"
10132
+
},
10133
+
"engines": {
10134
+
"node": "^20.17.0 || >=22.9.0"
10135
+
}
10136
+
},
10137
+
"node_modules/npm/node_modules/libnpmfund": {
10138
+
"version": "7.0.9",
10139
+
"inBundle": true,
10140
+
"license": "ISC",
10141
+
"dependencies": {
10142
+
"@npmcli/arborist": "^9.1.6"
10143
+
},
10144
+
"engines": {
10145
+
"node": "^20.17.0 || >=22.9.0"
10146
+
}
10147
+
},
10148
+
"node_modules/npm/node_modules/libnpmorg": {
10149
+
"version": "8.0.1",
10150
+
"inBundle": true,
10151
+
"license": "ISC",
10152
+
"dependencies": {
10153
+
"aproba": "^2.0.0",
10154
+
"npm-registry-fetch": "^19.0.0"
10155
+
},
10156
+
"engines": {
10157
+
"node": "^20.17.0 || >=22.9.0"
10158
+
}
10159
+
},
10160
+
"node_modules/npm/node_modules/libnpmpack": {
10161
+
"version": "9.0.9",
10162
+
"inBundle": true,
10163
+
"license": "ISC",
10164
+
"dependencies": {
10165
+
"@npmcli/arborist": "^9.1.6",
10166
+
"@npmcli/run-script": "^10.0.0",
10167
+
"npm-package-arg": "^13.0.0",
10168
+
"pacote": "^21.0.2"
10169
+
},
10170
+
"engines": {
10171
+
"node": "^20.17.0 || >=22.9.0"
10172
+
}
10173
+
},
10174
+
"node_modules/npm/node_modules/libnpmpublish": {
10175
+
"version": "11.1.2",
10176
+
"inBundle": true,
10177
+
"license": "ISC",
10178
+
"dependencies": {
10179
+
"@npmcli/package-json": "^7.0.0",
10180
+
"ci-info": "^4.0.0",
10181
+
"npm-package-arg": "^13.0.0",
10182
+
"npm-registry-fetch": "^19.0.0",
10183
+
"proc-log": "^5.0.0",
10184
+
"semver": "^7.3.7",
10185
+
"sigstore": "^4.0.0",
10186
+
"ssri": "^12.0.0"
10187
+
},
10188
+
"engines": {
10189
+
"node": "^20.17.0 || >=22.9.0"
10190
+
}
10191
+
},
10192
+
"node_modules/npm/node_modules/libnpmsearch": {
10193
+
"version": "9.0.1",
10194
+
"inBundle": true,
10195
+
"license": "ISC",
10196
+
"dependencies": {
10197
+
"npm-registry-fetch": "^19.0.0"
10198
+
},
10199
+
"engines": {
10200
+
"node": "^20.17.0 || >=22.9.0"
10201
+
}
10202
+
},
10203
+
"node_modules/npm/node_modules/libnpmteam": {
10204
+
"version": "8.0.2",
10205
+
"inBundle": true,
10206
+
"license": "ISC",
10207
+
"dependencies": {
10208
+
"aproba": "^2.0.0",
10209
+
"npm-registry-fetch": "^19.0.0"
10210
+
},
10211
+
"engines": {
10212
+
"node": "^20.17.0 || >=22.9.0"
10213
+
}
10214
+
},
10215
+
"node_modules/npm/node_modules/libnpmversion": {
10216
+
"version": "8.0.2",
10217
+
"inBundle": true,
10218
+
"license": "ISC",
10219
+
"dependencies": {
10220
+
"@npmcli/git": "^7.0.0",
10221
+
"@npmcli/run-script": "^10.0.0",
10222
+
"json-parse-even-better-errors": "^4.0.0",
10223
+
"proc-log": "^5.0.0",
10224
+
"semver": "^7.3.7"
10225
+
},
10226
+
"engines": {
10227
+
"node": "^20.17.0 || >=22.9.0"
10228
+
}
10229
+
},
10230
+
"node_modules/npm/node_modules/lru-cache": {
10231
+
"version": "11.2.2",
10232
+
"inBundle": true,
10233
+
"license": "ISC",
10234
+
"engines": {
10235
+
"node": "20 || >=22"
10236
+
}
10237
+
},
10238
+
"node_modules/npm/node_modules/make-fetch-happen": {
10239
+
"version": "15.0.2",
10240
+
"inBundle": true,
10241
+
"license": "ISC",
10242
+
"dependencies": {
10243
+
"@npmcli/agent": "^4.0.0",
10244
+
"cacache": "^20.0.1",
10245
+
"http-cache-semantics": "^4.1.1",
10246
+
"minipass": "^7.0.2",
10247
+
"minipass-fetch": "^4.0.0",
10248
+
"minipass-flush": "^1.0.5",
10249
+
"minipass-pipeline": "^1.2.4",
10250
+
"negotiator": "^1.0.0",
10251
+
"proc-log": "^5.0.0",
10252
+
"promise-retry": "^2.0.1",
10253
+
"ssri": "^12.0.0"
10254
+
},
10255
+
"engines": {
10256
+
"node": "^20.17.0 || >=22.9.0"
10257
+
}
10258
+
},
10259
+
"node_modules/npm/node_modules/minimatch": {
10260
+
"version": "10.0.3",
10261
+
"inBundle": true,
10262
+
"license": "ISC",
10263
+
"dependencies": {
10264
+
"@isaacs/brace-expansion": "^5.0.0"
10265
+
},
10266
+
"engines": {
10267
+
"node": "20 || >=22"
10268
+
},
10269
+
"funding": {
10270
+
"url": "https://github.com/sponsors/isaacs"
10271
+
}
10272
+
},
10273
+
"node_modules/npm/node_modules/minipass": {
10274
+
"version": "7.1.2",
10275
+
"inBundle": true,
10276
+
"license": "ISC",
10277
+
"engines": {
10278
+
"node": ">=16 || 14 >=14.17"
10279
+
}
10280
+
},
10281
+
"node_modules/npm/node_modules/minipass-collect": {
10282
+
"version": "2.0.1",
10283
+
"inBundle": true,
10284
+
"license": "ISC",
10285
+
"dependencies": {
10286
+
"minipass": "^7.0.3"
10287
+
},
10288
+
"engines": {
10289
+
"node": ">=16 || 14 >=14.17"
10290
+
}
10291
+
},
10292
+
"node_modules/npm/node_modules/minipass-fetch": {
10293
+
"version": "4.0.1",
10294
+
"inBundle": true,
10295
+
"license": "MIT",
10296
+
"dependencies": {
10297
+
"minipass": "^7.0.3",
10298
+
"minipass-sized": "^1.0.3",
10299
+
"minizlib": "^3.0.1"
10300
+
},
10301
+
"engines": {
10302
+
"node": "^18.17.0 || >=20.5.0"
10303
+
},
10304
+
"optionalDependencies": {
10305
+
"encoding": "^0.1.13"
10306
+
}
10307
+
},
10308
+
"node_modules/npm/node_modules/minipass-flush": {
10309
+
"version": "1.0.5",
10310
+
"inBundle": true,
10311
+
"license": "ISC",
10312
+
"dependencies": {
10313
+
"minipass": "^3.0.0"
10314
+
},
10315
+
"engines": {
10316
+
"node": ">= 8"
10317
+
}
10318
+
},
10319
+
"node_modules/npm/node_modules/minipass-flush/node_modules/minipass": {
10320
+
"version": "3.3.6",
10321
+
"inBundle": true,
10322
+
"license": "ISC",
10323
+
"dependencies": {
10324
+
"yallist": "^4.0.0"
10325
+
},
10326
+
"engines": {
10327
+
"node": ">=8"
10328
+
}
10329
+
},
10330
+
"node_modules/npm/node_modules/minipass-pipeline": {
10331
+
"version": "1.2.4",
10332
+
"inBundle": true,
10333
+
"license": "ISC",
10334
+
"dependencies": {
10335
+
"minipass": "^3.0.0"
10336
+
},
10337
+
"engines": {
10338
+
"node": ">=8"
10339
+
}
10340
+
},
10341
+
"node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": {
10342
+
"version": "3.3.6",
10343
+
"inBundle": true,
10344
+
"license": "ISC",
10345
+
"dependencies": {
10346
+
"yallist": "^4.0.0"
10347
+
},
10348
+
"engines": {
10349
+
"node": ">=8"
10350
+
}
10351
+
},
10352
+
"node_modules/npm/node_modules/minipass-sized": {
10353
+
"version": "1.0.3",
10354
+
"inBundle": true,
10355
+
"license": "ISC",
10356
+
"dependencies": {
10357
+
"minipass": "^3.0.0"
10358
+
},
10359
+
"engines": {
10360
+
"node": ">=8"
10361
+
}
10362
+
},
10363
+
"node_modules/npm/node_modules/minipass-sized/node_modules/minipass": {
10364
+
"version": "3.3.6",
10365
+
"inBundle": true,
10366
+
"license": "ISC",
10367
+
"dependencies": {
10368
+
"yallist": "^4.0.0"
10369
+
},
10370
+
"engines": {
10371
+
"node": ">=8"
10372
+
}
10373
+
},
10374
+
"node_modules/npm/node_modules/minizlib": {
10375
+
"version": "3.1.0",
10376
+
"inBundle": true,
10377
+
"license": "MIT",
10378
+
"dependencies": {
10379
+
"minipass": "^7.1.2"
10380
+
},
10381
+
"engines": {
10382
+
"node": ">= 18"
10383
+
}
10384
+
},
10385
+
"node_modules/npm/node_modules/ms": {
10386
+
"version": "2.1.3",
10387
+
"inBundle": true,
10388
+
"license": "MIT"
10389
+
},
10390
+
"node_modules/npm/node_modules/mute-stream": {
10391
+
"version": "2.0.0",
10392
+
"inBundle": true,
10393
+
"license": "ISC",
10394
+
"engines": {
10395
+
"node": "^18.17.0 || >=20.5.0"
10396
+
}
10397
+
},
10398
+
"node_modules/npm/node_modules/negotiator": {
10399
+
"version": "1.0.0",
10400
+
"inBundle": true,
10401
+
"license": "MIT",
10402
+
"engines": {
10403
+
"node": ">= 0.6"
10404
+
}
10405
+
},
10406
+
"node_modules/npm/node_modules/node-gyp": {
10407
+
"version": "11.4.2",
10408
+
"inBundle": true,
10409
+
"license": "MIT",
10410
+
"dependencies": {
10411
+
"env-paths": "^2.2.0",
10412
+
"exponential-backoff": "^3.1.1",
10413
+
"graceful-fs": "^4.2.6",
10414
+
"make-fetch-happen": "^14.0.3",
10415
+
"nopt": "^8.0.0",
10416
+
"proc-log": "^5.0.0",
10417
+
"semver": "^7.3.5",
10418
+
"tar": "^7.4.3",
10419
+
"tinyglobby": "^0.2.12",
10420
+
"which": "^5.0.0"
10421
+
},
10422
+
"bin": {
10423
+
"node-gyp": "bin/node-gyp.js"
10424
+
},
10425
+
"engines": {
10426
+
"node": "^18.17.0 || >=20.5.0"
10427
+
}
10428
+
},
10429
+
"node_modules/npm/node_modules/node-gyp/node_modules/@npmcli/agent": {
10430
+
"version": "3.0.0",
10431
+
"inBundle": true,
10432
+
"license": "ISC",
10433
+
"dependencies": {
10434
+
"agent-base": "^7.1.0",
10435
+
"http-proxy-agent": "^7.0.0",
10436
+
"https-proxy-agent": "^7.0.1",
10437
+
"lru-cache": "^10.0.1",
10438
+
"socks-proxy-agent": "^8.0.3"
10439
+
},
10440
+
"engines": {
10441
+
"node": "^18.17.0 || >=20.5.0"
10442
+
}
10443
+
},
10444
+
"node_modules/npm/node_modules/node-gyp/node_modules/cacache": {
10445
+
"version": "19.0.1",
10446
+
"inBundle": true,
10447
+
"license": "ISC",
10448
+
"dependencies": {
10449
+
"@npmcli/fs": "^4.0.0",
10450
+
"fs-minipass": "^3.0.0",
10451
+
"glob": "^10.2.2",
10452
+
"lru-cache": "^10.0.1",
10453
+
"minipass": "^7.0.3",
10454
+
"minipass-collect": "^2.0.1",
10455
+
"minipass-flush": "^1.0.5",
10456
+
"minipass-pipeline": "^1.2.4",
10457
+
"p-map": "^7.0.2",
10458
+
"ssri": "^12.0.0",
10459
+
"tar": "^7.4.3",
10460
+
"unique-filename": "^4.0.0"
10461
+
},
10462
+
"engines": {
10463
+
"node": "^18.17.0 || >=20.5.0"
10464
+
}
10465
+
},
10466
+
"node_modules/npm/node_modules/node-gyp/node_modules/glob": {
10467
+
"version": "10.4.5",
10468
+
"inBundle": true,
10469
+
"license": "ISC",
10470
+
"dependencies": {
10471
+
"foreground-child": "^3.1.0",
10472
+
"jackspeak": "^3.1.2",
10473
+
"minimatch": "^9.0.4",
10474
+
"minipass": "^7.1.2",
10475
+
"package-json-from-dist": "^1.0.0",
10476
+
"path-scurry": "^1.11.1"
10477
+
},
10478
+
"bin": {
10479
+
"glob": "dist/esm/bin.mjs"
10480
+
},
10481
+
"funding": {
10482
+
"url": "https://github.com/sponsors/isaacs"
10483
+
}
10484
+
},
10485
+
"node_modules/npm/node_modules/node-gyp/node_modules/jackspeak": {
10486
+
"version": "3.4.3",
10487
+
"inBundle": true,
10488
+
"license": "BlueOak-1.0.0",
10489
+
"dependencies": {
10490
+
"@isaacs/cliui": "^8.0.2"
10491
+
},
10492
+
"funding": {
10493
+
"url": "https://github.com/sponsors/isaacs"
10494
+
},
10495
+
"optionalDependencies": {
10496
+
"@pkgjs/parseargs": "^0.11.0"
10497
+
}
10498
+
},
10499
+
"node_modules/npm/node_modules/node-gyp/node_modules/lru-cache": {
10500
+
"version": "10.4.3",
10501
+
"inBundle": true,
10502
+
"license": "ISC"
10503
+
},
10504
+
"node_modules/npm/node_modules/node-gyp/node_modules/make-fetch-happen": {
10505
+
"version": "14.0.3",
10506
+
"inBundle": true,
10507
+
"license": "ISC",
10508
+
"dependencies": {
10509
+
"@npmcli/agent": "^3.0.0",
10510
+
"cacache": "^19.0.1",
10511
+
"http-cache-semantics": "^4.1.1",
10512
+
"minipass": "^7.0.2",
10513
+
"minipass-fetch": "^4.0.0",
10514
+
"minipass-flush": "^1.0.5",
10515
+
"minipass-pipeline": "^1.2.4",
10516
+
"negotiator": "^1.0.0",
10517
+
"proc-log": "^5.0.0",
10518
+
"promise-retry": "^2.0.1",
10519
+
"ssri": "^12.0.0"
10520
+
},
10521
+
"engines": {
10522
+
"node": "^18.17.0 || >=20.5.0"
10523
+
}
10524
+
},
10525
+
"node_modules/npm/node_modules/node-gyp/node_modules/minimatch": {
10526
+
"version": "9.0.5",
10527
+
"inBundle": true,
10528
+
"license": "ISC",
10529
+
"dependencies": {
10530
+
"brace-expansion": "^2.0.1"
10531
+
},
10532
+
"engines": {
10533
+
"node": ">=16 || 14 >=14.17"
10534
+
},
10535
+
"funding": {
10536
+
"url": "https://github.com/sponsors/isaacs"
10537
+
}
10538
+
},
10539
+
"node_modules/npm/node_modules/node-gyp/node_modules/path-scurry": {
10540
+
"version": "1.11.1",
10541
+
"inBundle": true,
10542
+
"license": "BlueOak-1.0.0",
10543
+
"dependencies": {
10544
+
"lru-cache": "^10.2.0",
10545
+
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
10546
+
},
10547
+
"engines": {
10548
+
"node": ">=16 || 14 >=14.18"
10549
+
},
10550
+
"funding": {
10551
+
"url": "https://github.com/sponsors/isaacs"
10552
+
}
10553
+
},
10554
+
"node_modules/npm/node_modules/nopt": {
10555
+
"version": "8.1.0",
10556
+
"inBundle": true,
10557
+
"license": "ISC",
10558
+
"dependencies": {
10559
+
"abbrev": "^3.0.0"
10560
+
},
10561
+
"bin": {
10562
+
"nopt": "bin/nopt.js"
10563
+
},
10564
+
"engines": {
10565
+
"node": "^18.17.0 || >=20.5.0"
10566
+
}
10567
+
},
10568
+
"node_modules/npm/node_modules/npm-audit-report": {
10569
+
"version": "6.0.0",
10570
+
"inBundle": true,
10571
+
"license": "ISC",
10572
+
"engines": {
10573
+
"node": "^18.17.0 || >=20.5.0"
10574
+
}
10575
+
},
10576
+
"node_modules/npm/node_modules/npm-bundled": {
10577
+
"version": "4.0.0",
10578
+
"inBundle": true,
10579
+
"license": "ISC",
10580
+
"dependencies": {
10581
+
"npm-normalize-package-bin": "^4.0.0"
10582
+
},
10583
+
"engines": {
10584
+
"node": "^18.17.0 || >=20.5.0"
10585
+
}
10586
+
},
10587
+
"node_modules/npm/node_modules/npm-install-checks": {
10588
+
"version": "7.1.2",
10589
+
"inBundle": true,
10590
+
"license": "BSD-2-Clause",
10591
+
"dependencies": {
10592
+
"semver": "^7.1.1"
10593
+
},
10594
+
"engines": {
10595
+
"node": "^18.17.0 || >=20.5.0"
10596
+
}
10597
+
},
10598
+
"node_modules/npm/node_modules/npm-normalize-package-bin": {
10599
+
"version": "4.0.0",
10600
+
"inBundle": true,
10601
+
"license": "ISC",
10602
+
"engines": {
10603
+
"node": "^18.17.0 || >=20.5.0"
10604
+
}
10605
+
},
10606
+
"node_modules/npm/node_modules/npm-package-arg": {
10607
+
"version": "13.0.1",
10608
+
"inBundle": true,
10609
+
"license": "ISC",
10610
+
"dependencies": {
10611
+
"hosted-git-info": "^9.0.0",
10612
+
"proc-log": "^5.0.0",
10613
+
"semver": "^7.3.5",
10614
+
"validate-npm-package-name": "^6.0.0"
10615
+
},
10616
+
"engines": {
10617
+
"node": "^20.17.0 || >=22.9.0"
10618
+
}
10619
+
},
10620
+
"node_modules/npm/node_modules/npm-packlist": {
10621
+
"version": "10.0.2",
10622
+
"inBundle": true,
10623
+
"license": "ISC",
10624
+
"dependencies": {
10625
+
"ignore-walk": "^8.0.0",
10626
+
"proc-log": "^5.0.0"
10627
+
},
10628
+
"engines": {
10629
+
"node": "^20.17.0 || >=22.9.0"
10630
+
}
10631
+
},
10632
+
"node_modules/npm/node_modules/npm-pick-manifest": {
10633
+
"version": "11.0.1",
10634
+
"inBundle": true,
10635
+
"license": "ISC",
10636
+
"dependencies": {
10637
+
"npm-install-checks": "^7.1.0",
10638
+
"npm-normalize-package-bin": "^4.0.0",
10639
+
"npm-package-arg": "^13.0.0",
10640
+
"semver": "^7.3.5"
10641
+
},
10642
+
"engines": {
10643
+
"node": "^20.17.0 || >=22.9.0"
10644
+
}
10645
+
},
10646
+
"node_modules/npm/node_modules/npm-profile": {
10647
+
"version": "12.0.0",
10648
+
"inBundle": true,
10649
+
"license": "ISC",
10650
+
"dependencies": {
10651
+
"npm-registry-fetch": "^19.0.0",
10652
+
"proc-log": "^5.0.0"
10653
+
},
10654
+
"engines": {
10655
+
"node": "^20.17.0 || >=22.9.0"
10656
+
}
10657
+
},
10658
+
"node_modules/npm/node_modules/npm-registry-fetch": {
10659
+
"version": "19.0.0",
10660
+
"inBundle": true,
10661
+
"license": "ISC",
10662
+
"dependencies": {
10663
+
"@npmcli/redact": "^3.0.0",
10664
+
"jsonparse": "^1.3.1",
10665
+
"make-fetch-happen": "^15.0.0",
10666
+
"minipass": "^7.0.2",
10667
+
"minipass-fetch": "^4.0.0",
10668
+
"minizlib": "^3.0.1",
10669
+
"npm-package-arg": "^13.0.0",
10670
+
"proc-log": "^5.0.0"
10671
+
},
10672
+
"engines": {
10673
+
"node": "^20.17.0 || >=22.9.0"
10674
+
}
10675
+
},
10676
+
"node_modules/npm/node_modules/npm-user-validate": {
10677
+
"version": "3.0.0",
10678
+
"inBundle": true,
10679
+
"license": "BSD-2-Clause",
10680
+
"engines": {
10681
+
"node": "^18.17.0 || >=20.5.0"
10682
+
}
10683
+
},
10684
+
"node_modules/npm/node_modules/p-map": {
10685
+
"version": "7.0.3",
10686
+
"inBundle": true,
10687
+
"license": "MIT",
10688
+
"engines": {
10689
+
"node": ">=18"
10690
+
},
10691
+
"funding": {
10692
+
"url": "https://github.com/sponsors/sindresorhus"
10693
+
}
10694
+
},
10695
+
"node_modules/npm/node_modules/package-json-from-dist": {
10696
+
"version": "1.0.1",
10697
+
"inBundle": true,
10698
+
"license": "BlueOak-1.0.0"
10699
+
},
10700
+
"node_modules/npm/node_modules/pacote": {
10701
+
"version": "21.0.3",
10702
+
"inBundle": true,
10703
+
"license": "ISC",
10704
+
"dependencies": {
10705
+
"@npmcli/git": "^7.0.0",
10706
+
"@npmcli/installed-package-contents": "^3.0.0",
10707
+
"@npmcli/package-json": "^7.0.0",
10708
+
"@npmcli/promise-spawn": "^8.0.0",
10709
+
"@npmcli/run-script": "^10.0.0",
10710
+
"cacache": "^20.0.0",
10711
+
"fs-minipass": "^3.0.0",
10712
+
"minipass": "^7.0.2",
10713
+
"npm-package-arg": "^13.0.0",
10714
+
"npm-packlist": "^10.0.1",
10715
+
"npm-pick-manifest": "^11.0.1",
10716
+
"npm-registry-fetch": "^19.0.0",
10717
+
"proc-log": "^5.0.0",
10718
+
"promise-retry": "^2.0.1",
10719
+
"sigstore": "^4.0.0",
10720
+
"ssri": "^12.0.0",
10721
+
"tar": "^7.4.3"
10722
+
},
10723
+
"bin": {
10724
+
"pacote": "bin/index.js"
10725
+
},
10726
+
"engines": {
10727
+
"node": "^20.17.0 || >=22.9.0"
10728
+
}
10729
+
},
10730
+
"node_modules/npm/node_modules/parse-conflict-json": {
10731
+
"version": "4.0.0",
10732
+
"inBundle": true,
10733
+
"license": "ISC",
10734
+
"dependencies": {
10735
+
"json-parse-even-better-errors": "^4.0.0",
10736
+
"just-diff": "^6.0.0",
10737
+
"just-diff-apply": "^5.2.0"
10738
+
},
10739
+
"engines": {
10740
+
"node": "^18.17.0 || >=20.5.0"
10741
+
}
10742
+
},
10743
+
"node_modules/npm/node_modules/path-key": {
10744
+
"version": "3.1.1",
10745
+
"inBundle": true,
10746
+
"license": "MIT",
10747
+
"engines": {
10748
+
"node": ">=8"
10749
+
}
10750
+
},
10751
+
"node_modules/npm/node_modules/path-scurry": {
10752
+
"version": "2.0.0",
10753
+
"inBundle": true,
10754
+
"license": "BlueOak-1.0.0",
10755
+
"dependencies": {
10756
+
"lru-cache": "^11.0.0",
10757
+
"minipass": "^7.1.2"
10758
+
},
10759
+
"engines": {
10760
+
"node": "20 || >=22"
10761
+
},
10762
+
"funding": {
10763
+
"url": "https://github.com/sponsors/isaacs"
10764
+
}
10765
+
},
10766
+
"node_modules/npm/node_modules/postcss-selector-parser": {
10767
+
"version": "7.1.0",
10768
+
"inBundle": true,
10769
+
"license": "MIT",
10770
+
"dependencies": {
10771
+
"cssesc": "^3.0.0",
10772
+
"util-deprecate": "^1.0.2"
10773
+
},
10774
+
"engines": {
10775
+
"node": ">=4"
10776
+
}
10777
+
},
10778
+
"node_modules/npm/node_modules/proc-log": {
10779
+
"version": "5.0.0",
10780
+
"inBundle": true,
10781
+
"license": "ISC",
10782
+
"engines": {
10783
+
"node": "^18.17.0 || >=20.5.0"
10784
+
}
10785
+
},
10786
+
"node_modules/npm/node_modules/proggy": {
10787
+
"version": "3.0.0",
10788
+
"inBundle": true,
10789
+
"license": "ISC",
10790
+
"engines": {
10791
+
"node": "^18.17.0 || >=20.5.0"
10792
+
}
10793
+
},
10794
+
"node_modules/npm/node_modules/promise-all-reject-late": {
10795
+
"version": "1.0.1",
10796
+
"inBundle": true,
10797
+
"license": "ISC",
10798
+
"funding": {
10799
+
"url": "https://github.com/sponsors/isaacs"
10800
+
}
10801
+
},
10802
+
"node_modules/npm/node_modules/promise-call-limit": {
10803
+
"version": "3.0.2",
10804
+
"inBundle": true,
10805
+
"license": "ISC",
10806
+
"funding": {
10807
+
"url": "https://github.com/sponsors/isaacs"
10808
+
}
10809
+
},
10810
+
"node_modules/npm/node_modules/promise-retry": {
10811
+
"version": "2.0.1",
10812
+
"inBundle": true,
10813
+
"license": "MIT",
10814
+
"dependencies": {
10815
+
"err-code": "^2.0.2",
10816
+
"retry": "^0.12.0"
10817
+
},
10818
+
"engines": {
10819
+
"node": ">=10"
10820
+
}
10821
+
},
10822
+
"node_modules/npm/node_modules/promzard": {
10823
+
"version": "2.0.0",
10824
+
"inBundle": true,
10825
+
"license": "ISC",
10826
+
"dependencies": {
10827
+
"read": "^4.0.0"
10828
+
},
10829
+
"engines": {
10830
+
"node": "^18.17.0 || >=20.5.0"
10831
+
}
10832
+
},
10833
+
"node_modules/npm/node_modules/qrcode-terminal": {
10834
+
"version": "0.12.0",
10835
+
"inBundle": true,
10836
+
"bin": {
10837
+
"qrcode-terminal": "bin/qrcode-terminal.js"
10838
+
}
10839
+
},
10840
+
"node_modules/npm/node_modules/read": {
10841
+
"version": "4.1.0",
10842
+
"inBundle": true,
10843
+
"license": "ISC",
10844
+
"dependencies": {
10845
+
"mute-stream": "^2.0.0"
10846
+
},
10847
+
"engines": {
10848
+
"node": "^18.17.0 || >=20.5.0"
10849
+
}
10850
+
},
10851
+
"node_modules/npm/node_modules/read-cmd-shim": {
10852
+
"version": "5.0.0",
10853
+
"inBundle": true,
10854
+
"license": "ISC",
10855
+
"engines": {
10856
+
"node": "^18.17.0 || >=20.5.0"
10857
+
}
10858
+
},
10859
+
"node_modules/npm/node_modules/retry": {
10860
+
"version": "0.12.0",
10861
+
"inBundle": true,
10862
+
"license": "MIT",
10863
+
"engines": {
10864
+
"node": ">= 4"
10865
+
}
10866
+
},
10867
+
"node_modules/npm/node_modules/safer-buffer": {
10868
+
"version": "2.1.2",
10869
+
"inBundle": true,
10870
+
"license": "MIT",
10871
+
"optional": true
10872
+
},
10873
+
"node_modules/npm/node_modules/semver": {
10874
+
"version": "7.7.3",
10875
+
"inBundle": true,
10876
+
"license": "ISC",
10877
+
"bin": {
10878
+
"semver": "bin/semver.js"
10879
+
},
10880
+
"engines": {
10881
+
"node": ">=10"
10882
+
}
10883
+
},
10884
+
"node_modules/npm/node_modules/shebang-command": {
10885
+
"version": "2.0.0",
10886
+
"inBundle": true,
10887
+
"license": "MIT",
10888
+
"dependencies": {
10889
+
"shebang-regex": "^3.0.0"
10890
+
},
10891
+
"engines": {
10892
+
"node": ">=8"
10893
+
}
10894
+
},
10895
+
"node_modules/npm/node_modules/shebang-regex": {
10896
+
"version": "3.0.0",
10897
+
"inBundle": true,
10898
+
"license": "MIT",
10899
+
"engines": {
10900
+
"node": ">=8"
10901
+
}
10902
+
},
10903
+
"node_modules/npm/node_modules/signal-exit": {
10904
+
"version": "4.1.0",
10905
+
"inBundle": true,
10906
+
"license": "ISC",
10907
+
"engines": {
10908
+
"node": ">=14"
10909
+
},
10910
+
"funding": {
10911
+
"url": "https://github.com/sponsors/isaacs"
10912
+
}
10913
+
},
10914
+
"node_modules/npm/node_modules/sigstore": {
10915
+
"version": "4.0.0",
10916
+
"inBundle": true,
10917
+
"license": "Apache-2.0",
10918
+
"dependencies": {
10919
+
"@sigstore/bundle": "^4.0.0",
10920
+
"@sigstore/core": "^3.0.0",
10921
+
"@sigstore/protobuf-specs": "^0.5.0",
10922
+
"@sigstore/sign": "^4.0.0",
10923
+
"@sigstore/tuf": "^4.0.0",
10924
+
"@sigstore/verify": "^3.0.0"
10925
+
},
10926
+
"engines": {
10927
+
"node": "^20.17.0 || >=22.9.0"
10928
+
}
10929
+
},
10930
+
"node_modules/npm/node_modules/smart-buffer": {
10931
+
"version": "4.2.0",
10932
+
"inBundle": true,
10933
+
"license": "MIT",
10934
+
"engines": {
10935
+
"node": ">= 6.0.0",
10936
+
"npm": ">= 3.0.0"
10937
+
}
10938
+
},
10939
+
"node_modules/npm/node_modules/socks": {
10940
+
"version": "2.8.7",
10941
+
"inBundle": true,
10942
+
"license": "MIT",
10943
+
"dependencies": {
10944
+
"ip-address": "^10.0.1",
10945
+
"smart-buffer": "^4.2.0"
10946
+
},
10947
+
"engines": {
10948
+
"node": ">= 10.0.0",
10949
+
"npm": ">= 3.0.0"
10950
+
}
10951
+
},
10952
+
"node_modules/npm/node_modules/socks-proxy-agent": {
10953
+
"version": "8.0.5",
10954
+
"inBundle": true,
10955
+
"license": "MIT",
10956
+
"dependencies": {
10957
+
"agent-base": "^7.1.2",
10958
+
"debug": "^4.3.4",
10959
+
"socks": "^2.8.3"
10960
+
},
10961
+
"engines": {
10962
+
"node": ">= 14"
10963
+
}
10964
+
},
10965
+
"node_modules/npm/node_modules/spdx-correct": {
10966
+
"version": "3.2.0",
10967
+
"inBundle": true,
10968
+
"license": "Apache-2.0",
10969
+
"dependencies": {
10970
+
"spdx-expression-parse": "^3.0.0",
10971
+
"spdx-license-ids": "^3.0.0"
10972
+
}
10973
+
},
10974
+
"node_modules/npm/node_modules/spdx-correct/node_modules/spdx-expression-parse": {
10975
+
"version": "3.0.1",
10976
+
"inBundle": true,
10977
+
"license": "MIT",
10978
+
"dependencies": {
10979
+
"spdx-exceptions": "^2.1.0",
10980
+
"spdx-license-ids": "^3.0.0"
10981
+
}
10982
+
},
10983
+
"node_modules/npm/node_modules/spdx-exceptions": {
10984
+
"version": "2.5.0",
10985
+
"inBundle": true,
10986
+
"license": "CC-BY-3.0"
10987
+
},
10988
+
"node_modules/npm/node_modules/spdx-expression-parse": {
10989
+
"version": "4.0.0",
10990
+
"inBundle": true,
10991
+
"license": "MIT",
10992
+
"dependencies": {
10993
+
"spdx-exceptions": "^2.1.0",
10994
+
"spdx-license-ids": "^3.0.0"
10995
+
}
10996
+
},
10997
+
"node_modules/npm/node_modules/spdx-license-ids": {
10998
+
"version": "3.0.22",
10999
+
"inBundle": true,
11000
+
"license": "CC0-1.0"
11001
+
},
11002
+
"node_modules/npm/node_modules/ssri": {
11003
+
"version": "12.0.0",
11004
+
"inBundle": true,
11005
+
"license": "ISC",
11006
+
"dependencies": {
11007
+
"minipass": "^7.0.3"
11008
+
},
11009
+
"engines": {
11010
+
"node": "^18.17.0 || >=20.5.0"
11011
+
}
11012
+
},
11013
+
"node_modules/npm/node_modules/string-width": {
11014
+
"version": "4.2.3",
11015
+
"inBundle": true,
11016
+
"license": "MIT",
11017
+
"dependencies": {
11018
+
"emoji-regex": "^8.0.0",
11019
+
"is-fullwidth-code-point": "^3.0.0",
11020
+
"strip-ansi": "^6.0.1"
11021
+
},
11022
+
"engines": {
11023
+
"node": ">=8"
11024
+
}
11025
+
},
11026
+
"node_modules/npm/node_modules/string-width-cjs": {
11027
+
"name": "string-width",
11028
+
"version": "4.2.3",
11029
+
"inBundle": true,
11030
+
"license": "MIT",
11031
+
"dependencies": {
11032
+
"emoji-regex": "^8.0.0",
11033
+
"is-fullwidth-code-point": "^3.0.0",
11034
+
"strip-ansi": "^6.0.1"
11035
+
},
11036
+
"engines": {
11037
+
"node": ">=8"
11038
+
}
11039
+
},
11040
+
"node_modules/npm/node_modules/strip-ansi": {
11041
+
"version": "6.0.1",
11042
+
"inBundle": true,
11043
+
"license": "MIT",
11044
+
"dependencies": {
11045
+
"ansi-regex": "^5.0.1"
11046
+
},
11047
+
"engines": {
11048
+
"node": ">=8"
11049
+
}
11050
+
},
11051
+
"node_modules/npm/node_modules/strip-ansi-cjs": {
11052
+
"name": "strip-ansi",
11053
+
"version": "6.0.1",
11054
+
"inBundle": true,
11055
+
"license": "MIT",
11056
+
"dependencies": {
11057
+
"ansi-regex": "^5.0.1"
11058
+
},
11059
+
"engines": {
11060
+
"node": ">=8"
11061
+
}
11062
+
},
11063
+
"node_modules/npm/node_modules/supports-color": {
11064
+
"version": "10.2.2",
11065
+
"inBundle": true,
11066
+
"license": "MIT",
11067
+
"engines": {
11068
+
"node": ">=18"
11069
+
},
11070
+
"funding": {
11071
+
"url": "https://github.com/chalk/supports-color?sponsor=1"
11072
+
}
11073
+
},
11074
+
"node_modules/npm/node_modules/tar": {
11075
+
"version": "7.5.1",
11076
+
"inBundle": true,
11077
+
"license": "ISC",
11078
+
"dependencies": {
11079
+
"@isaacs/fs-minipass": "^4.0.0",
11080
+
"chownr": "^3.0.0",
11081
+
"minipass": "^7.1.2",
11082
+
"minizlib": "^3.1.0",
11083
+
"yallist": "^5.0.0"
11084
+
},
11085
+
"engines": {
11086
+
"node": ">=18"
11087
+
}
11088
+
},
11089
+
"node_modules/npm/node_modules/tar/node_modules/yallist": {
11090
+
"version": "5.0.0",
11091
+
"inBundle": true,
11092
+
"license": "BlueOak-1.0.0",
11093
+
"engines": {
11094
+
"node": ">=18"
11095
+
}
11096
+
},
11097
+
"node_modules/npm/node_modules/text-table": {
11098
+
"version": "0.2.0",
11099
+
"inBundle": true,
11100
+
"license": "MIT"
11101
+
},
11102
+
"node_modules/npm/node_modules/tiny-relative-date": {
11103
+
"version": "2.0.2",
11104
+
"inBundle": true,
11105
+
"license": "MIT"
11106
+
},
11107
+
"node_modules/npm/node_modules/tinyglobby": {
11108
+
"version": "0.2.15",
11109
+
"inBundle": true,
11110
+
"license": "MIT",
11111
+
"dependencies": {
11112
+
"fdir": "^6.5.0",
11113
+
"picomatch": "^4.0.3"
11114
+
},
11115
+
"engines": {
11116
+
"node": ">=12.0.0"
11117
+
},
11118
+
"funding": {
11119
+
"url": "https://github.com/sponsors/SuperchupuDev"
11120
+
}
11121
+
},
11122
+
"node_modules/npm/node_modules/tinyglobby/node_modules/fdir": {
11123
+
"version": "6.5.0",
11124
+
"inBundle": true,
11125
+
"license": "MIT",
11126
+
"engines": {
11127
+
"node": ">=12.0.0"
11128
+
},
11129
+
"peerDependencies": {
11130
+
"picomatch": "^3 || ^4"
11131
+
},
11132
+
"peerDependenciesMeta": {
11133
+
"picomatch": {
11134
+
"optional": true
11135
+
}
11136
+
}
11137
+
},
11138
+
"node_modules/npm/node_modules/tinyglobby/node_modules/picomatch": {
11139
+
"version": "4.0.3",
11140
+
"inBundle": true,
11141
+
"license": "MIT",
11142
+
"engines": {
11143
+
"node": ">=12"
11144
+
},
11145
+
"funding": {
11146
+
"url": "https://github.com/sponsors/jonschlinkert"
11147
+
}
11148
+
},
11149
+
"node_modules/npm/node_modules/treeverse": {
11150
+
"version": "3.0.0",
11151
+
"inBundle": true,
11152
+
"license": "ISC",
11153
+
"engines": {
11154
+
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
11155
+
}
11156
+
},
11157
+
"node_modules/npm/node_modules/tuf-js": {
11158
+
"version": "4.0.0",
11159
+
"inBundle": true,
11160
+
"license": "MIT",
11161
+
"dependencies": {
11162
+
"@tufjs/models": "4.0.0",
11163
+
"debug": "^4.4.1",
11164
+
"make-fetch-happen": "^15.0.0"
11165
+
},
11166
+
"engines": {
11167
+
"node": "^20.17.0 || >=22.9.0"
11168
+
}
11169
+
},
11170
+
"node_modules/npm/node_modules/unique-filename": {
11171
+
"version": "4.0.0",
11172
+
"inBundle": true,
11173
+
"license": "ISC",
11174
+
"dependencies": {
11175
+
"unique-slug": "^5.0.0"
11176
+
},
11177
+
"engines": {
11178
+
"node": "^18.17.0 || >=20.5.0"
11179
+
}
11180
+
},
11181
+
"node_modules/npm/node_modules/unique-slug": {
11182
+
"version": "5.0.0",
11183
+
"inBundle": true,
11184
+
"license": "ISC",
11185
+
"dependencies": {
11186
+
"imurmurhash": "^0.1.4"
11187
+
},
11188
+
"engines": {
11189
+
"node": "^18.17.0 || >=20.5.0"
11190
+
}
11191
+
},
11192
+
"node_modules/npm/node_modules/util-deprecate": {
11193
+
"version": "1.0.2",
11194
+
"inBundle": true,
11195
+
"license": "MIT"
11196
+
},
11197
+
"node_modules/npm/node_modules/validate-npm-package-license": {
11198
+
"version": "3.0.4",
11199
+
"inBundle": true,
11200
+
"license": "Apache-2.0",
11201
+
"dependencies": {
11202
+
"spdx-correct": "^3.0.0",
11203
+
"spdx-expression-parse": "^3.0.0"
11204
+
}
11205
+
},
11206
+
"node_modules/npm/node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": {
11207
+
"version": "3.0.1",
11208
+
"inBundle": true,
11209
+
"license": "MIT",
11210
+
"dependencies": {
11211
+
"spdx-exceptions": "^2.1.0",
11212
+
"spdx-license-ids": "^3.0.0"
11213
+
}
11214
+
},
11215
+
"node_modules/npm/node_modules/validate-npm-package-name": {
11216
+
"version": "6.0.2",
11217
+
"inBundle": true,
11218
+
"license": "ISC",
11219
+
"engines": {
11220
+
"node": "^18.17.0 || >=20.5.0"
11221
+
}
11222
+
},
11223
+
"node_modules/npm/node_modules/walk-up-path": {
11224
+
"version": "4.0.0",
11225
+
"inBundle": true,
11226
+
"license": "ISC",
11227
+
"engines": {
11228
+
"node": "20 || >=22"
11229
+
}
11230
+
},
11231
+
"node_modules/npm/node_modules/which": {
11232
+
"version": "5.0.0",
11233
+
"inBundle": true,
11234
+
"license": "ISC",
11235
+
"dependencies": {
11236
+
"isexe": "^3.1.1"
11237
+
},
11238
+
"bin": {
11239
+
"node-which": "bin/which.js"
11240
+
},
11241
+
"engines": {
11242
+
"node": "^18.17.0 || >=20.5.0"
11243
+
}
11244
+
},
11245
+
"node_modules/npm/node_modules/wrap-ansi": {
11246
+
"version": "8.1.0",
11247
+
"inBundle": true,
11248
+
"license": "MIT",
11249
+
"dependencies": {
11250
+
"ansi-styles": "^6.1.0",
11251
+
"string-width": "^5.0.1",
11252
+
"strip-ansi": "^7.0.1"
11253
+
},
11254
+
"engines": {
11255
+
"node": ">=12"
11256
+
},
11257
+
"funding": {
11258
+
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
11259
+
}
11260
+
},
11261
+
"node_modules/npm/node_modules/wrap-ansi-cjs": {
11262
+
"name": "wrap-ansi",
11263
+
"version": "7.0.0",
11264
+
"inBundle": true,
11265
+
"license": "MIT",
11266
+
"dependencies": {
11267
+
"ansi-styles": "^4.0.0",
11268
+
"string-width": "^4.1.0",
11269
+
"strip-ansi": "^6.0.0"
11270
+
},
11271
+
"engines": {
11272
+
"node": ">=10"
11273
+
},
11274
+
"funding": {
11275
+
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
11276
+
}
11277
+
},
11278
+
"node_modules/npm/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
11279
+
"version": "4.3.0",
11280
+
"inBundle": true,
11281
+
"license": "MIT",
11282
+
"dependencies": {
11283
+
"color-convert": "^2.0.1"
11284
+
},
11285
+
"engines": {
11286
+
"node": ">=8"
11287
+
},
11288
+
"funding": {
11289
+
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
11290
+
}
11291
+
},
11292
+
"node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": {
11293
+
"version": "6.2.2",
11294
+
"inBundle": true,
11295
+
"license": "MIT",
11296
+
"engines": {
11297
+
"node": ">=12"
11298
+
},
11299
+
"funding": {
11300
+
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
11301
+
}
11302
+
},
11303
+
"node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": {
11304
+
"version": "9.2.2",
11305
+
"inBundle": true,
11306
+
"license": "MIT"
11307
+
},
11308
+
"node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": {
11309
+
"version": "5.1.2",
11310
+
"inBundle": true,
11311
+
"license": "MIT",
11312
+
"dependencies": {
11313
+
"eastasianwidth": "^0.2.0",
11314
+
"emoji-regex": "^9.2.2",
11315
+
"strip-ansi": "^7.0.1"
11316
+
},
11317
+
"engines": {
11318
+
"node": ">=12"
11319
+
},
11320
+
"funding": {
11321
+
"url": "https://github.com/sponsors/sindresorhus"
11322
+
}
11323
+
},
11324
+
"node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": {
11325
+
"version": "7.1.2",
11326
+
"inBundle": true,
11327
+
"license": "MIT",
11328
+
"dependencies": {
11329
+
"ansi-regex": "^6.0.1"
11330
+
},
11331
+
"engines": {
11332
+
"node": ">=12"
11333
+
},
11334
+
"funding": {
11335
+
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
11336
+
}
11337
+
},
11338
+
"node_modules/npm/node_modules/write-file-atomic": {
11339
+
"version": "6.0.0",
11340
+
"inBundle": true,
11341
+
"license": "ISC",
11342
+
"dependencies": {
11343
+
"imurmurhash": "^0.1.4",
11344
+
"signal-exit": "^4.0.1"
11345
+
},
11346
+
"engines": {
11347
+
"node": "^18.17.0 || >=20.5.0"
11348
+
}
11349
+
},
11350
+
"node_modules/npm/node_modules/yallist": {
11351
+
"version": "4.0.0",
11352
+
"inBundle": true,
11353
+
"license": "ISC"
11354
+
},
11355
"node_modules/nwsapi": {
11356
"version": "2.2.21",
11357
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz",
···
11537
"url": "https://github.com/sponsors/sindresorhus"
11538
}
11539
},
11540
+
"node_modules/package-manager-detector": {
11541
+
"version": "1.4.1",
11542
+
"resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.4.1.tgz",
11543
+
"integrity": "sha512-dSMiVLBEA4XaNJ0PRb4N5cV/SEP4BWrWZKBmfF+OUm2pQTiZ6DDkKeWaltwu3JRhLoy59ayIkJ00cx9K9CaYTg==",
11544
+
"dev": true,
11545
+
"license": "MIT"
11546
+
},
11547
"node_modules/parent-module": {
11548
"version": "1.0.1",
11549
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
11550
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
11551
"dev": true,
11552
"license": "MIT",
11553
"dependencies": {
11554
"callsites": "^3.0.0"
11555
},
···
11557
"node": ">=6"
11558
}
11559
},
11560
+
"node_modules/parse-json": {
11561
+
"version": "5.2.0",
11562
+
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
11563
+
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
11564
+
"dev": true,
11565
+
"license": "MIT",
11566
+
"dependencies": {
11567
+
"@babel/code-frame": "^7.0.0",
11568
+
"error-ex": "^1.3.1",
11569
+
"json-parse-even-better-errors": "^2.3.0",
11570
+
"lines-and-columns": "^1.1.6"
11571
+
},
11572
+
"engines": {
11573
+
"node": ">=8"
11574
+
},
11575
+
"funding": {
11576
+
"url": "https://github.com/sponsors/sindresorhus"
11577
+
}
11578
+
},
11579
"node_modules/parse5": {
11580
"version": "7.3.0",
11581
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
···
11623
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
11624
"dev": true,
11625
"license": "MIT"
11626
+
},
11627
+
"node_modules/path-type": {
11628
+
"version": "4.0.0",
11629
+
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
11630
+
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
11631
+
"dev": true,
11632
+
"license": "MIT",
11633
+
"engines": {
11634
+
"node": ">=8"
11635
+
}
11636
},
11637
"node_modules/pathe": {
11638
"version": "2.0.3",
···
11667
},
11668
"funding": {
11669
"url": "https://github.com/sponsors/jonschlinkert"
11670
+
}
11671
+
},
11672
+
"node_modules/pkg-types": {
11673
+
"version": "2.3.0",
11674
+
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
11675
+
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
11676
+
"dev": true,
11677
+
"license": "MIT",
11678
+
"dependencies": {
11679
+
"confbox": "^0.2.2",
11680
+
"exsolve": "^1.0.7",
11681
+
"pathe": "^2.0.3"
11682
}
11683
},
11684
"node_modules/player.style": {
···
11803
"node": ">=6"
11804
}
11805
},
11806
+
"node_modules/quansync": {
11807
+
"version": "0.2.11",
11808
+
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
11809
+
"integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==",
11810
+
"dev": true,
11811
+
"funding": [
11812
+
{
11813
+
"type": "individual",
11814
+
"url": "https://github.com/sponsors/antfu"
11815
+
},
11816
+
{
11817
+
"type": "individual",
11818
+
"url": "https://github.com/sponsors/sxzz"
11819
+
}
11820
+
],
11821
+
"license": "MIT"
11822
+
},
11823
"node_modules/queue-microtask": {
11824
"version": "1.2.3",
11825
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
···
11841
],
11842
"license": "MIT"
11843
},
11844
+
"node_modules/radix-ui": {
11845
+
"version": "1.4.3",
11846
+
"resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz",
11847
+
"integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==",
11848
+
"dependencies": {
11849
+
"@radix-ui/primitive": "1.1.3",
11850
+
"@radix-ui/react-accessible-icon": "1.1.7",
11851
+
"@radix-ui/react-accordion": "1.2.12",
11852
+
"@radix-ui/react-alert-dialog": "1.1.15",
11853
+
"@radix-ui/react-arrow": "1.1.7",
11854
+
"@radix-ui/react-aspect-ratio": "1.1.7",
11855
+
"@radix-ui/react-avatar": "1.1.10",
11856
+
"@radix-ui/react-checkbox": "1.3.3",
11857
+
"@radix-ui/react-collapsible": "1.1.12",
11858
+
"@radix-ui/react-collection": "1.1.7",
11859
+
"@radix-ui/react-compose-refs": "1.1.2",
11860
+
"@radix-ui/react-context": "1.1.2",
11861
+
"@radix-ui/react-context-menu": "2.2.16",
11862
+
"@radix-ui/react-dialog": "1.1.15",
11863
+
"@radix-ui/react-direction": "1.1.1",
11864
+
"@radix-ui/react-dismissable-layer": "1.1.11",
11865
+
"@radix-ui/react-dropdown-menu": "2.1.16",
11866
+
"@radix-ui/react-focus-guards": "1.1.3",
11867
+
"@radix-ui/react-focus-scope": "1.1.7",
11868
+
"@radix-ui/react-form": "0.1.8",
11869
+
"@radix-ui/react-hover-card": "1.1.15",
11870
+
"@radix-ui/react-label": "2.1.7",
11871
+
"@radix-ui/react-menu": "2.1.16",
11872
+
"@radix-ui/react-menubar": "1.1.16",
11873
+
"@radix-ui/react-navigation-menu": "1.2.14",
11874
+
"@radix-ui/react-one-time-password-field": "0.1.8",
11875
+
"@radix-ui/react-password-toggle-field": "0.1.3",
11876
+
"@radix-ui/react-popover": "1.1.15",
11877
+
"@radix-ui/react-popper": "1.2.8",
11878
+
"@radix-ui/react-portal": "1.1.9",
11879
+
"@radix-ui/react-presence": "1.1.5",
11880
+
"@radix-ui/react-primitive": "2.1.3",
11881
+
"@radix-ui/react-progress": "1.1.7",
11882
+
"@radix-ui/react-radio-group": "1.3.8",
11883
+
"@radix-ui/react-roving-focus": "1.1.11",
11884
+
"@radix-ui/react-scroll-area": "1.2.10",
11885
+
"@radix-ui/react-select": "2.2.6",
11886
+
"@radix-ui/react-separator": "1.1.7",
11887
+
"@radix-ui/react-slider": "1.3.6",
11888
+
"@radix-ui/react-slot": "1.2.3",
11889
+
"@radix-ui/react-switch": "1.2.6",
11890
+
"@radix-ui/react-tabs": "1.1.13",
11891
+
"@radix-ui/react-toast": "1.2.15",
11892
+
"@radix-ui/react-toggle": "1.1.10",
11893
+
"@radix-ui/react-toggle-group": "1.1.11",
11894
+
"@radix-ui/react-toolbar": "1.1.11",
11895
+
"@radix-ui/react-tooltip": "1.2.8",
11896
+
"@radix-ui/react-use-callback-ref": "1.1.1",
11897
+
"@radix-ui/react-use-controllable-state": "1.2.2",
11898
+
"@radix-ui/react-use-effect-event": "0.0.2",
11899
+
"@radix-ui/react-use-escape-keydown": "1.1.1",
11900
+
"@radix-ui/react-use-is-hydrated": "0.1.0",
11901
+
"@radix-ui/react-use-layout-effect": "1.1.1",
11902
+
"@radix-ui/react-use-size": "1.1.1",
11903
+
"@radix-ui/react-visually-hidden": "1.2.3"
11904
+
},
11905
+
"peerDependencies": {
11906
+
"@types/react": "*",
11907
+
"@types/react-dom": "*",
11908
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
11909
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
11910
+
},
11911
+
"peerDependenciesMeta": {
11912
+
"@types/react": {
11913
+
"optional": true
11914
+
},
11915
+
"@types/react-dom": {
11916
+
"optional": true
11917
+
}
11918
+
}
11919
+
},
11920
"node_modules/react": {
11921
"version": "19.1.1",
11922
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
···
11978
"node": ">=0.10.0"
11979
}
11980
},
11981
+
"node_modules/react-remove-scroll": {
11982
+
"version": "2.7.1",
11983
+
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
11984
+
"integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==",
11985
+
"dependencies": {
11986
+
"react-remove-scroll-bar": "^2.3.7",
11987
+
"react-style-singleton": "^2.2.3",
11988
+
"tslib": "^2.1.0",
11989
+
"use-callback-ref": "^1.3.3",
11990
+
"use-sidecar": "^1.1.3"
11991
+
},
11992
+
"engines": {
11993
+
"node": ">=10"
11994
+
},
11995
+
"peerDependencies": {
11996
+
"@types/react": "*",
11997
+
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
11998
+
},
11999
+
"peerDependenciesMeta": {
12000
+
"@types/react": {
12001
+
"optional": true
12002
+
}
12003
+
}
12004
+
},
12005
+
"node_modules/react-remove-scroll-bar": {
12006
+
"version": "2.3.8",
12007
+
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
12008
+
"integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
12009
+
"dependencies": {
12010
+
"react-style-singleton": "^2.2.2",
12011
+
"tslib": "^2.0.0"
12012
+
},
12013
+
"engines": {
12014
+
"node": ">=10"
12015
+
},
12016
+
"peerDependencies": {
12017
+
"@types/react": "*",
12018
+
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
12019
+
},
12020
+
"peerDependenciesMeta": {
12021
+
"@types/react": {
12022
+
"optional": true
12023
+
}
12024
+
}
12025
+
},
12026
+
"node_modules/react-style-singleton": {
12027
+
"version": "2.2.3",
12028
+
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
12029
+
"integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
12030
+
"dependencies": {
12031
+
"get-nonce": "^1.0.0",
12032
+
"tslib": "^2.0.0"
12033
+
},
12034
+
"engines": {
12035
+
"node": ">=10"
12036
+
},
12037
+
"peerDependencies": {
12038
+
"@types/react": "*",
12039
+
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
12040
+
},
12041
+
"peerDependenciesMeta": {
12042
+
"@types/react": {
12043
+
"optional": true
12044
+
}
12045
+
}
12046
+
},
12047
"node_modules/readdirp": {
12048
"version": "3.6.0",
12049
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
···
12149
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
12150
"dev": true,
12151
"license": "MIT",
12152
"engines": {
12153
"node": ">=4"
12154
}
···
12330
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
12331
"license": "MIT"
12332
},
12333
+
"node_modules/scule": {
12334
+
"version": "1.3.0",
12335
+
"resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz",
12336
+
"integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
12337
+
"dev": true,
12338
+
"license": "MIT"
12339
+
},
12340
"node_modules/semver": {
12341
"version": "6.3.1",
12342
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
···
12524
"dev": true,
12525
"license": "ISC"
12526
},
12527
+
"node_modules/snake-case": {
12528
+
"version": "3.0.4",
12529
+
"resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz",
12530
+
"integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==",
12531
+
"dev": true,
12532
+
"license": "MIT",
12533
+
"dependencies": {
12534
+
"dot-case": "^3.0.4",
12535
+
"tslib": "^2.0.3"
12536
+
}
12537
+
},
12538
"node_modules/solid-js": {
12539
"version": "1.9.9",
12540
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.9.tgz",
···
12544
"csstype": "^3.1.0",
12545
"seroval": "~1.3.0",
12546
"seroval-plugins": "~1.3.0"
12547
+
}
12548
+
},
12549
+
"node_modules/sonner": {
12550
+
"version": "2.0.7",
12551
+
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
12552
+
"integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==",
12553
+
"peerDependencies": {
12554
+
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
12555
+
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
12556
}
12557
},
12558
"node_modules/source-map": {
···
12727
}
12728
},
12729
"node_modules/strip-literal": {
12730
+
"version": "3.1.0",
12731
+
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
12732
+
"integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
12733
"dev": true,
12734
"license": "MIT",
12735
"dependencies": {
···
12779
"url": "https://github.com/sponsors/ljharb"
12780
}
12781
},
12782
+
"node_modules/svg-parser": {
12783
+
"version": "2.0.4",
12784
+
"resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz",
12785
+
"integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==",
12786
+
"dev": true,
12787
+
"license": "MIT"
12788
+
},
12789
"node_modules/symbol-tree": {
12790
"version": "3.2.4",
12791
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
···
12799
"integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==",
12800
"license": "MIT"
12801
},
12802
+
"node_modules/tanstack-router-keepalive": {
12803
+
"version": "1.0.0",
12804
+
"resolved": "https://registry.npmjs.org/tanstack-router-keepalive/-/tanstack-router-keepalive-1.0.0.tgz",
12805
+
"integrity": "sha512-SxMl9sgIZGjB4OZvGXufTz14ygmZi+eAbrhz3sjmXYZzhSQOekx5LYi9TvKcMXVu8fQR6W/itF5hdglqD6WB/w==",
12806
+
"license": "MIT",
12807
+
"dependencies": {
12808
+
"eventemitter3": "^5.0.1",
12809
+
"lodash.clonedeep": "^4.5.0"
12810
+
}
12811
+
},
12812
"node_modules/tapable": {
12813
"version": "2.2.3",
12814
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz",
···
12881
"license": "MIT"
12882
},
12883
"node_modules/tinyglobby": {
12884
+
"version": "0.2.15",
12885
+
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
12886
+
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
12887
"license": "MIT",
12888
"dependencies": {
12889
+
"fdir": "^6.5.0",
12890
+
"picomatch": "^4.0.3"
12891
},
12892
"engines": {
12893
"node": ">=12.0.0"
···
13265
"node": "*"
13266
}
13267
},
13268
+
"node_modules/ufo": {
13269
+
"version": "1.6.1",
13270
+
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",
13271
+
"integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
13272
+
"dev": true,
13273
+
"license": "MIT"
13274
+
},
13275
"node_modules/uint8arrays": {
13276
"version": "3.0.0",
13277
"resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz",
···
13307
"devOptional": true,
13308
"license": "MIT"
13309
},
13310
+
"node_modules/unimport": {
13311
+
"version": "5.5.0",
13312
+
"resolved": "https://registry.npmjs.org/unimport/-/unimport-5.5.0.tgz",
13313
+
"integrity": "sha512-/JpWMG9s1nBSlXJAQ8EREFTFy3oy6USFd8T6AoBaw1q2GGcF4R9yp3ofg32UODZlYEO5VD0EWE1RpI9XDWyPYg==",
13314
+
"dev": true,
13315
+
"license": "MIT",
13316
+
"dependencies": {
13317
+
"acorn": "^8.15.0",
13318
+
"escape-string-regexp": "^5.0.0",
13319
+
"estree-walker": "^3.0.3",
13320
+
"local-pkg": "^1.1.2",
13321
+
"magic-string": "^0.30.19",
13322
+
"mlly": "^1.8.0",
13323
+
"pathe": "^2.0.3",
13324
+
"picomatch": "^4.0.3",
13325
+
"pkg-types": "^2.3.0",
13326
+
"scule": "^1.3.0",
13327
+
"strip-literal": "^3.1.0",
13328
+
"tinyglobby": "^0.2.15",
13329
+
"unplugin": "^2.3.10",
13330
+
"unplugin-utils": "^0.3.0"
13331
+
},
13332
+
"engines": {
13333
+
"node": ">=18.12.0"
13334
+
}
13335
+
},
13336
+
"node_modules/unimport/node_modules/escape-string-regexp": {
13337
+
"version": "5.0.0",
13338
+
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
13339
+
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
13340
+
"dev": true,
13341
+
"license": "MIT",
13342
+
"engines": {
13343
+
"node": ">=12"
13344
+
},
13345
+
"funding": {
13346
+
"url": "https://github.com/sponsors/sindresorhus"
13347
+
}
13348
+
},
13349
+
"node_modules/unimport/node_modules/picomatch": {
13350
+
"version": "4.0.3",
13351
+
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
13352
+
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
13353
+
"dev": true,
13354
+
"license": "MIT",
13355
+
"engines": {
13356
+
"node": ">=12"
13357
+
},
13358
+
"funding": {
13359
+
"url": "https://github.com/sponsors/jonschlinkert"
13360
+
}
13361
+
},
13362
"node_modules/unplugin": {
13363
+
"version": "2.3.10",
13364
+
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.10.tgz",
13365
+
"integrity": "sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==",
13366
"license": "MIT",
13367
"dependencies": {
13368
"@jridgewell/remapping": "^2.3.5",
···
13374
"node": ">=18.12.0"
13375
}
13376
},
13377
+
"node_modules/unplugin-auto-import": {
13378
+
"version": "20.2.0",
13379
+
"resolved": "https://registry.npmjs.org/unplugin-auto-import/-/unplugin-auto-import-20.2.0.tgz",
13380
+
"integrity": "sha512-vfBI/SvD9hJqYNinipVOAj5n8dS8DJXFlCKFR5iLDp2SaQwsfdnfLXgZ+34Kd3YY3YEY9omk8XQg0bwos3Q8ug==",
13381
+
"dev": true,
13382
+
"license": "MIT",
13383
+
"dependencies": {
13384
+
"local-pkg": "^1.1.2",
13385
+
"magic-string": "^0.30.19",
13386
+
"picomatch": "^4.0.3",
13387
+
"unimport": "^5.4.0",
13388
+
"unplugin": "^2.3.10",
13389
+
"unplugin-utils": "^0.3.0"
13390
+
},
13391
+
"engines": {
13392
+
"node": ">=14"
13393
+
},
13394
+
"funding": {
13395
+
"url": "https://github.com/sponsors/antfu"
13396
+
},
13397
+
"peerDependencies": {
13398
+
"@nuxt/kit": "^4.0.0",
13399
+
"@vueuse/core": "*"
13400
+
},
13401
+
"peerDependenciesMeta": {
13402
+
"@nuxt/kit": {
13403
+
"optional": true
13404
+
},
13405
+
"@vueuse/core": {
13406
+
"optional": true
13407
+
}
13408
+
}
13409
+
},
13410
+
"node_modules/unplugin-auto-import/node_modules/picomatch": {
13411
+
"version": "4.0.3",
13412
+
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
13413
+
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
13414
+
"dev": true,
13415
+
"license": "MIT",
13416
+
"engines": {
13417
+
"node": ">=12"
13418
+
},
13419
+
"funding": {
13420
+
"url": "https://github.com/sponsors/jonschlinkert"
13421
+
}
13422
+
},
13423
+
"node_modules/unplugin-icons": {
13424
+
"version": "22.4.2",
13425
+
"resolved": "https://registry.npmjs.org/unplugin-icons/-/unplugin-icons-22.4.2.tgz",
13426
+
"integrity": "sha512-Yv15405unO67Chme0Slk0JRA/H2AiAZLK5t7ebt8/ZpTDlBfM4d4En2qD3MX2rzOSkIteQ0syIm3q8MSofeoBA==",
13427
+
"dev": true,
13428
+
"license": "MIT",
13429
+
"dependencies": {
13430
+
"@antfu/install-pkg": "^1.1.0",
13431
+
"@iconify/utils": "^3.0.2",
13432
+
"debug": "^4.4.3",
13433
+
"local-pkg": "^1.1.2",
13434
+
"unplugin": "^2.3.10"
13435
+
},
13436
+
"funding": {
13437
+
"url": "https://github.com/sponsors/antfu"
13438
+
},
13439
+
"peerDependencies": {
13440
+
"@svgr/core": ">=7.0.0",
13441
+
"@svgx/core": "^1.0.1",
13442
+
"@vue/compiler-sfc": "^3.0.2 || ^2.7.0",
13443
+
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0",
13444
+
"vue-template-compiler": "^2.6.12",
13445
+
"vue-template-es2015-compiler": "^1.9.0"
13446
+
},
13447
+
"peerDependenciesMeta": {
13448
+
"@svgr/core": {
13449
+
"optional": true
13450
+
},
13451
+
"@svgx/core": {
13452
+
"optional": true
13453
+
},
13454
+
"@vue/compiler-sfc": {
13455
+
"optional": true
13456
+
},
13457
+
"svelte": {
13458
+
"optional": true
13459
+
},
13460
+
"vue-template-compiler": {
13461
+
"optional": true
13462
+
},
13463
+
"vue-template-es2015-compiler": {
13464
+
"optional": true
13465
+
}
13466
+
}
13467
+
},
13468
+
"node_modules/unplugin-utils": {
13469
+
"version": "0.3.1",
13470
+
"resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz",
13471
+
"integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==",
13472
+
"dev": true,
13473
+
"license": "MIT",
13474
+
"dependencies": {
13475
+
"pathe": "^2.0.3",
13476
+
"picomatch": "^4.0.3"
13477
+
},
13478
+
"engines": {
13479
+
"node": ">=20.19.0"
13480
+
},
13481
+
"funding": {
13482
+
"url": "https://github.com/sponsors/sxzz"
13483
+
}
13484
+
},
13485
+
"node_modules/unplugin-utils/node_modules/picomatch": {
13486
+
"version": "4.0.3",
13487
+
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
13488
+
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
13489
+
"dev": true,
13490
+
"license": "MIT",
13491
+
"engines": {
13492
+
"node": ">=12"
13493
+
},
13494
+
"funding": {
13495
+
"url": "https://github.com/sponsors/jonschlinkert"
13496
+
}
13497
+
},
13498
"node_modules/unplugin/node_modules/picomatch": {
13499
"version": "4.0.3",
13500
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
···
13546
"peer": true,
13547
"dependencies": {
13548
"punycode": "^2.1.0"
13549
+
}
13550
+
},
13551
+
"node_modules/use-callback-ref": {
13552
+
"version": "1.3.3",
13553
+
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
13554
+
"integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
13555
+
"dependencies": {
13556
+
"tslib": "^2.0.0"
13557
+
},
13558
+
"engines": {
13559
+
"node": ">=10"
13560
+
},
13561
+
"peerDependencies": {
13562
+
"@types/react": "*",
13563
+
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
13564
+
},
13565
+
"peerDependenciesMeta": {
13566
+
"@types/react": {
13567
+
"optional": true
13568
+
}
13569
+
}
13570
+
},
13571
+
"node_modules/use-sidecar": {
13572
+
"version": "1.1.3",
13573
+
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
13574
+
"integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
13575
+
"dependencies": {
13576
+
"detect-node-es": "^1.1.0",
13577
+
"tslib": "^2.0.0"
13578
+
},
13579
+
"engines": {
13580
+
"node": ">=10"
13581
+
},
13582
+
"peerDependencies": {
13583
+
"@types/react": "*",
13584
+
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
13585
+
},
13586
+
"peerDependenciesMeta": {
13587
+
"@types/react": {
13588
+
"optional": true
13589
+
}
13590
}
13591
},
13592
"node_modules/use-sync-external-store": {
+19
-2
package.json
+19
-2
package.json
···
12
"dependencies": {
13
"@atproto/api": "^0.16.6",
14
"@atproto/oauth-client-browser": "^0.3.33",
15
"@tailwindcss/vite": "^4.0.6",
16
"@tanstack/query-sync-storage-persister": "^5.85.6",
17
"@tanstack/react-devtools": "^0.2.2",
···
19
"@tanstack/react-query-persist-client": "^5.85.6",
20
"@tanstack/react-router": "^1.130.2",
21
"@tanstack/react-router-devtools": "^1.131.5",
22
-
"@tanstack/react-virtual": "^3.13.12",
23
"@tanstack/router-plugin": "^1.121.2",
24
"idb-keyval": "^6.2.2",
25
"jotai": "^2.13.1",
26
"react": "^19.0.0",
27
"react-dom": "^19.0.0",
28
"react-player": "^3.3.2",
29
-
"tailwindcss": "^4.0.6"
30
},
31
"devDependencies": {
32
"@eslint-react/eslint-plugin": "^2.2.1",
33
"@testing-library/dom": "^10.4.0",
34
"@testing-library/react": "^16.2.0",
35
"@types/node": "^24.3.0",
···
47
"prettier": "^3.6.2",
48
"typescript": "^5.7.2",
49
"typescript-eslint": "^8.46.1",
50
"vite": "^6.3.5",
51
"vitest": "^3.0.5",
52
"web-vitals": "^4.2.4"
···
12
"dependencies": {
13
"@atproto/api": "^0.16.6",
14
"@atproto/oauth-client-browser": "^0.3.33",
15
+
"@radix-ui/react-dialog": "^1.1.15",
16
+
"@radix-ui/react-dropdown-menu": "^2.1.16",
17
+
"@radix-ui/react-hover-card": "^1.1.15",
18
+
"@radix-ui/react-slider": "^1.3.6",
19
"@tailwindcss/vite": "^4.0.6",
20
"@tanstack/query-sync-storage-persister": "^5.85.6",
21
"@tanstack/react-devtools": "^0.2.2",
···
23
"@tanstack/react-query-persist-client": "^5.85.6",
24
"@tanstack/react-router": "^1.130.2",
25
"@tanstack/react-router-devtools": "^1.131.5",
26
"@tanstack/router-plugin": "^1.121.2",
27
+
"dompurify": "^3.3.0",
28
+
"i": "^0.3.7",
29
"idb-keyval": "^6.2.2",
30
"jotai": "^2.13.1",
31
+
"npm": "^11.6.2",
32
+
"radix-ui": "^1.4.3",
33
"react": "^19.0.0",
34
"react-dom": "^19.0.0",
35
"react-player": "^3.3.2",
36
+
"sonner": "^2.0.7",
37
+
"tailwindcss": "^4.0.6",
38
+
"tanstack-router-keepalive": "^1.0.0"
39
},
40
"devDependencies": {
41
"@eslint-react/eslint-plugin": "^2.2.1",
42
+
"@iconify-icon/react": "^3.0.1",
43
+
"@iconify-json/material-symbols": "^1.2.42",
44
+
"@iconify-json/mdi": "^1.2.3",
45
+
"@iconify/json": "^2.2.396",
46
+
"@svgr/core": "^8.1.0",
47
+
"@svgr/plugin-jsx": "^8.1.0",
48
"@testing-library/dom": "^10.4.0",
49
"@testing-library/react": "^16.2.0",
50
"@types/node": "^24.3.0",
···
62
"prettier": "^3.6.2",
63
"typescript": "^5.7.2",
64
"typescript-eslint": "^8.46.1",
65
+
"unplugin-auto-import": "^20.2.0",
66
+
"unplugin-icons": "^22.4.2",
67
"vite": "^6.3.5",
68
"vitest": "^3.0.5",
69
"web-vitals": "^4.2.4"
public/screenshot.jpg
public/screenshot.jpg
This is a binary file and will not be displayed.
public/screenshot.png
public/screenshot.png
This is a binary file and will not be displayed.
+28
src/auto-imports.d.ts
+28
src/auto-imports.d.ts
···
···
1
+
/* eslint-disable */
2
+
/* prettier-ignore */
3
+
// @ts-nocheck
4
+
// noinspection JSUnusedGlobalSymbols
5
+
// Generated by unplugin-auto-import
6
+
// biome-ignore lint: disable
7
+
export {}
8
+
declare global {
9
+
const IconMaterialSymbolsAccountCircle: typeof import('~icons/material-symbols/account-circle.jsx').default
10
+
const IconMaterialSymbolsAccountCircleOutline: typeof import('~icons/material-symbols/account-circle-outline.jsx').default
11
+
const IconMaterialSymbolsArrowBack: typeof import('~icons/material-symbols/arrow-back.jsx').default
12
+
const IconMaterialSymbolsHome: typeof import('~icons/material-symbols/home.jsx').default
13
+
const IconMaterialSymbolsHomeOutline: typeof import('~icons/material-symbols/home-outline.jsx').default
14
+
const IconMaterialSymbolsNotifications: typeof import('~icons/material-symbols/notifications.jsx').default
15
+
const IconMaterialSymbolsNotificationsOutline: typeof import('~icons/material-symbols/notifications-outline.jsx').default
16
+
const IconMaterialSymbolsSearch: typeof import('~icons/material-symbols/search.jsx').default
17
+
const IconMaterialSymbolsSettings: typeof import('~icons/material-symbols/settings.jsx').default
18
+
const IconMaterialSymbolsSettingsOutline: typeof import('~icons/material-symbols/settings-outline.jsx').default
19
+
const IconMaterialSymbolsTag: typeof import('~icons/material-symbols/tag.jsx').default
20
+
const IconMdiAccountCircle: typeof import('~icons/mdi/account-circle.jsx').default
21
+
const IconMdiAccountPlus: typeof import('~icons/mdi/account-plus.jsx').default
22
+
const IconMdiCheck: typeof import('~icons/mdi/check.jsx').default
23
+
const IconMdiClose: typeof import('~icons/mdi/close.jsx').default
24
+
const IconMdiMessageReplyTextOutline: typeof import('~icons/mdi/message-reply-text-outline.jsx').default
25
+
const IconMdiPencilOutline: typeof import('~icons/mdi/pencil-outline.jsx').default
26
+
const IconMdiShield: typeof import('~icons/mdi/shield.jsx').default
27
+
const IconMdiShieldOutline: typeof import('~icons/mdi/shield-outline.jsx').default
28
+
}
+292
src/components/Composer.tsx
+292
src/components/Composer.tsx
···
···
1
+
import { AppBskyRichtextFacet, RichText } from "@atproto/api";
2
+
import { useAtom } from "jotai";
3
+
import { Dialog } from "radix-ui";
4
+
import { useEffect, useRef, useState } from "react";
5
+
6
+
import { useAuth } from "~/providers/UnifiedAuthProvider";
7
+
import { composerAtom } from "~/utils/atoms";
8
+
import { useQueryPost } from "~/utils/useQuery";
9
+
10
+
import { ProfileThing } from "./Login";
11
+
import { UniversalPostRendererATURILoader } from "./UniversalPostRenderer";
12
+
13
+
const MAX_POST_LENGTH = 300;
14
+
15
+
export function Composer() {
16
+
const [composerState, setComposerState] = useAtom(composerAtom);
17
+
const { agent } = useAuth();
18
+
19
+
const [postText, setPostText] = useState("");
20
+
const [posting, setPosting] = useState(false);
21
+
const [postSuccess, setPostSuccess] = useState(false);
22
+
const [postError, setPostError] = useState<string | null>(null);
23
+
24
+
useEffect(() => {
25
+
setPostText("");
26
+
setPosting(false);
27
+
setPostSuccess(false);
28
+
setPostError(null);
29
+
}, [composerState.kind]);
30
+
31
+
const parentUri =
32
+
composerState.kind === "reply"
33
+
? composerState.parent
34
+
: composerState.kind === "quote"
35
+
? composerState.subject
36
+
: undefined;
37
+
38
+
const { data: parentPost, isLoading: isParentLoading } =
39
+
useQueryPost(parentUri);
40
+
41
+
async function handlePost() {
42
+
if (!agent || !postText.trim() || postText.length > MAX_POST_LENGTH) return;
43
+
44
+
setPosting(true);
45
+
setPostError(null);
46
+
47
+
try {
48
+
const rt = new RichText({ text: postText });
49
+
await rt.detectFacets(agent);
50
+
51
+
if (rt.facets?.length) {
52
+
rt.facets = rt.facets.filter((item) => {
53
+
if (item.$type !== "app.bsky.richtext.facet") return true;
54
+
if (!item.features?.length) return true;
55
+
56
+
item.features = item.features.filter((feature) => {
57
+
if (feature.$type !== "app.bsky.richtext.facet#mention") return true;
58
+
const did = feature.$type === "app.bsky.richtext.facet#mention" ? (feature as AppBskyRichtextFacet.Mention)?.did : undefined;
59
+
return typeof did === "string" && did.startsWith("did:");
60
+
});
61
+
62
+
return item.features.length > 0;
63
+
});
64
+
}
65
+
66
+
const record: Record<string, unknown> = {
67
+
$type: "app.bsky.feed.post",
68
+
text: rt.text,
69
+
facets: rt.facets,
70
+
createdAt: new Date().toISOString(),
71
+
};
72
+
73
+
if (composerState.kind === "reply" && parentPost) {
74
+
record.reply = {
75
+
root: parentPost.value?.reply?.root ?? {
76
+
uri: parentPost.uri,
77
+
cid: parentPost.cid,
78
+
},
79
+
parent: {
80
+
uri: parentPost.uri,
81
+
cid: parentPost.cid,
82
+
},
83
+
};
84
+
}
85
+
86
+
if (composerState.kind === "quote" && parentPost) {
87
+
record.embed = {
88
+
$type: "app.bsky.embed.record",
89
+
record: {
90
+
uri: parentPost.uri,
91
+
cid: parentPost.cid,
92
+
},
93
+
};
94
+
}
95
+
96
+
await agent.com.atproto.repo.createRecord({
97
+
collection: "app.bsky.feed.post",
98
+
repo: agent.assertDid,
99
+
record,
100
+
});
101
+
102
+
setPostSuccess(true);
103
+
setPostText("");
104
+
105
+
setTimeout(() => {
106
+
setPostSuccess(false);
107
+
setComposerState({ kind: "closed" });
108
+
}, 1500);
109
+
} catch (e: any) {
110
+
setPostError(e?.message || "Failed to post");
111
+
} finally {
112
+
setPosting(false);
113
+
}
114
+
}
115
+
// if (composerState.kind === "closed") {
116
+
// return null;
117
+
// }
118
+
119
+
const getPlaceholder = () => {
120
+
switch (composerState.kind) {
121
+
case "reply":
122
+
return "Post your reply";
123
+
case "quote":
124
+
return "Add a comment...";
125
+
case "root":
126
+
default:
127
+
return "What's happening?!";
128
+
}
129
+
};
130
+
131
+
const charsLeft = MAX_POST_LENGTH - postText.length;
132
+
const isPostButtonDisabled =
133
+
posting || !postText.trim() || isParentLoading || charsLeft < 0;
134
+
135
+
return (
136
+
<Dialog.Root
137
+
open={composerState.kind !== "closed"}
138
+
onOpenChange={(open) => {
139
+
if (!open) setComposerState({ kind: "closed" });
140
+
}}
141
+
>
142
+
<Dialog.Portal>
143
+
<Dialog.Overlay className="fixed disablegutter inset-0 z-50 bg-black/40 dark:bg-black/50 data-[state=open]:animate-fadeIn" />
144
+
145
+
<Dialog.Content className="fixed gutter overflow-y-scroll inset-0 z-50 flex items-start justify-center pt-10 sm:pt-20 pb-[50dvh] sm:pb-[50dvh]">
146
+
<div className="bg-gray-50 dark:bg-gray-950 border border-gray-200 dark:border-gray-700 rounded-2xl shadow-xl w-full max-w-xl relative mx-4">
147
+
<div className="flex flex-row justify-between p-2">
148
+
<Dialog.Close asChild>
149
+
<button
150
+
className="h-8 w-8 flex items-center justify-center rounded-full text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800"
151
+
disabled={posting}
152
+
aria-label="Close"
153
+
>
154
+
<svg
155
+
xmlns="http://www.w3.org/2000/svg"
156
+
width="20"
157
+
height="20"
158
+
viewBox="0 0 24 24"
159
+
fill="none"
160
+
stroke="currentColor"
161
+
strokeWidth="2.5"
162
+
strokeLinecap="round"
163
+
strokeLinejoin="round"
164
+
>
165
+
<line x1="18" y1="6" x2="6" y2="18"></line>
166
+
<line x1="6" y1="6" x2="18" y2="18"></line>
167
+
</svg>
168
+
</button>
169
+
</Dialog.Close>
170
+
171
+
<div className="flex-1" />
172
+
<div className="flex items-center gap-4">
173
+
<span
174
+
className={`text-sm ${charsLeft < 0 ? "text-red-500" : "text-gray-500"}`}
175
+
>
176
+
{charsLeft}
177
+
</span>
178
+
<button
179
+
className="bg-gray-600 hover:bg-gray-700 text-white font-bold py-1 px-4 rounded-full disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
180
+
onClick={handlePost}
181
+
disabled={isPostButtonDisabled}
182
+
>
183
+
{posting ? "Posting..." : "Post"}
184
+
</button>
185
+
</div>
186
+
</div>
187
+
188
+
{postSuccess ? (
189
+
<div className="flex flex-col items-center justify-center py-16">
190
+
<span className="text-gray-500 text-6xl mb-4">โ</span>
191
+
<span className="text-xl font-bold text-black dark:text-white">
192
+
Posted!
193
+
</span>
194
+
</div>
195
+
) : (
196
+
<div className="px-4">
197
+
{composerState.kind === "reply" && (
198
+
<div className="mb-1 -mx-4">
199
+
{isParentLoading ? (
200
+
<div className="text-sm text-gray-500 animate-pulse">
201
+
Loading parent post...
202
+
</div>
203
+
) : parentUri ? (
204
+
<UniversalPostRendererATURILoader
205
+
atUri={parentUri}
206
+
bottomReplyLine
207
+
bottomBorder={false}
208
+
/>
209
+
) : (
210
+
<div className="text-sm text-red-500 rounded-lg border border-red-500/50 p-3">
211
+
Could not load parent post.
212
+
</div>
213
+
)}
214
+
</div>
215
+
)}
216
+
217
+
<div className="flex w-full gap-1 flex-col">
218
+
<ProfileThing agent={agent} large />
219
+
<div className="flex pl-[50px]">
220
+
<AutoGrowTextarea
221
+
className="w-full text-lg bg-transparent focus:outline-none resize-none placeholder:text-gray-500 text-black dark:text-white pb-2"
222
+
rows={5}
223
+
placeholder={getPlaceholder()}
224
+
value={postText}
225
+
onChange={(e) => setPostText(e.target.value)}
226
+
disabled={posting}
227
+
autoFocus
228
+
/>
229
+
</div>
230
+
</div>
231
+
232
+
{composerState.kind === "quote" && (
233
+
<div className="mb-4 ml-[50px] rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
234
+
{isParentLoading ? (
235
+
<div className="text-sm text-gray-500 animate-pulse">
236
+
Loading parent post...
237
+
</div>
238
+
) : parentUri ? (
239
+
<UniversalPostRendererATURILoader
240
+
atUri={parentUri}
241
+
isQuote
242
+
/>
243
+
) : (
244
+
<div className="text-sm text-red-500 rounded-lg border border-red-500/50 p-3">
245
+
Could not load parent post.
246
+
</div>
247
+
)}
248
+
</div>
249
+
)}
250
+
251
+
{postError && (
252
+
<div className="text-red-500 text-sm my-2 text-center">
253
+
{postError}
254
+
</div>
255
+
)}
256
+
</div>
257
+
)}
258
+
</div>
259
+
</Dialog.Content>
260
+
</Dialog.Portal>
261
+
</Dialog.Root>
262
+
);
263
+
}
264
+
265
+
function AutoGrowTextarea({
266
+
value,
267
+
className,
268
+
onChange,
269
+
...props
270
+
}: React.DetailedHTMLProps<
271
+
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
272
+
HTMLTextAreaElement
273
+
>) {
274
+
const ref = useRef<HTMLTextAreaElement>(null);
275
+
276
+
useEffect(() => {
277
+
const el = ref.current;
278
+
if (!el) return;
279
+
el.style.height = "auto";
280
+
el.style.height = el.scrollHeight + "px";
281
+
}, [value]);
282
+
283
+
return (
284
+
<textarea
285
+
ref={ref}
286
+
className={className}
287
+
value={value}
288
+
onChange={onChange}
289
+
{...props}
290
+
/>
291
+
);
292
+
}
+35
src/components/Header.tsx
+35
src/components/Header.tsx
···
···
1
+
import { Link, useRouter } from "@tanstack/react-router";
2
+
import { useAtom } from "jotai";
3
+
4
+
import { isAtTopAtom } from "~/utils/atoms";
5
+
6
+
export function Header({
7
+
backButtonCallback,
8
+
title,
9
+
bottomBorderDisabled,
10
+
}: {
11
+
backButtonCallback?: () => void;
12
+
title?: string;
13
+
bottomBorderDisabled?: boolean;
14
+
}) {
15
+
const router = useRouter();
16
+
const [isAtTop] = useAtom(isAtTopAtom);
17
+
//const what = router.history.
18
+
return (
19
+
<div className={`flex items-center gap-3 px-3 py-3 h-[52px] sticky top-0 bg-[var(--header-bg-light)] dark:bg-[var(--header-bg-dark)] z-10 border-0 ${!bottomBorderDisabled && "sm:border-b"} ${!isAtTop && !bottomBorderDisabled && "shadow-sm"} sm:shadow-none sm:dark:bg-gray-950 sm:bg-white border-gray-200 dark:border-gray-700`}>
20
+
{backButtonCallback ? (<Link
21
+
to=".."
22
+
//className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg"
23
+
className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg"
24
+
onClick={(e) => {
25
+
e.preventDefault();
26
+
backButtonCallback();
27
+
}}
28
+
aria-label="Go back"
29
+
>
30
+
<IconMaterialSymbolsArrowBack className="w-6 h-6" />
31
+
</Link>) : (<div className="w-[0px]" />)}
32
+
<span className="text-[21px] sm:text-[19px] sm:font-semibold font-roboto">{title}</span>
33
+
</div>
34
+
);
35
+
}
+184
src/components/Import.tsx
+184
src/components/Import.tsx
···
···
1
+
import { AtUri } from "@atproto/api";
2
+
import { useNavigate, type UseNavigateResult } from "@tanstack/react-router";
3
+
import { useAtom } from "jotai";
4
+
import { useState } from "react";
5
+
6
+
import { useAuth } from "~/providers/UnifiedAuthProvider";
7
+
import { lycanURLAtom } from "~/utils/atoms";
8
+
import { useQueryLycanStatus } from "~/utils/useQuery";
9
+
10
+
/**
11
+
* Basically the best equivalent to Search that i can do
12
+
*/
13
+
export function Import({
14
+
optionaltextstring,
15
+
}: {
16
+
optionaltextstring?: string;
17
+
}) {
18
+
const [textInput, setTextInput] = useState<string | undefined>(
19
+
optionaltextstring
20
+
);
21
+
const navigate = useNavigate();
22
+
23
+
const { status } = useAuth();
24
+
const [lycandomain] = useAtom(lycanURLAtom);
25
+
const lycanExists = lycandomain !== "";
26
+
const { data: lycanstatusdata } = useQueryLycanStatus();
27
+
const lycanIndexed = lycanstatusdata?.status === "finished" || false;
28
+
const lycanIndexing = lycanstatusdata?.status === "in_progress" || false;
29
+
const lycanIndexingProgress = lycanIndexing
30
+
? lycanstatusdata?.progress
31
+
: undefined;
32
+
const authed = status === "signedIn";
33
+
34
+
const lycanReady = lycanExists && lycanIndexed && authed;
35
+
36
+
const handleEnter = () => {
37
+
if (!textInput) return;
38
+
handleImport({
39
+
text: textInput,
40
+
navigate,
41
+
lycanReady:
42
+
lycanReady || (!!lycanIndexingProgress && lycanIndexingProgress > 0),
43
+
});
44
+
};
45
+
46
+
const placeholder = lycanReady ? "Search..." : "Import...";
47
+
48
+
return (
49
+
<div className="w-full relative">
50
+
<IconMaterialSymbolsSearch className="w-5 h-5 absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 dark:text-gray-500" />
51
+
52
+
<input
53
+
type="text"
54
+
placeholder={placeholder}
55
+
value={textInput}
56
+
onChange={(e) => setTextInput(e.target.value)}
57
+
onKeyDown={(e) => {
58
+
if (e.key === "Enter") handleEnter();
59
+
}}
60
+
className="w-full h-12 pl-12 pr-4 rounded-full bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-gray-400 dark:focus:ring-gray-500 box-border transition"
61
+
/>
62
+
</div>
63
+
);
64
+
}
65
+
66
+
function handleImport({
67
+
text,
68
+
navigate,
69
+
lycanReady,
70
+
}: {
71
+
text: string;
72
+
navigate: UseNavigateResult<string>;
73
+
lycanReady?: boolean;
74
+
}) {
75
+
const trimmed = text.trim();
76
+
// parse text
77
+
/**
78
+
* text might be
79
+
* 1. bsky dot app url (reddwarf link segments might be uri encoded,)
80
+
* 2. aturi
81
+
* 3. plain handle
82
+
* 4. plain did
83
+
*/
84
+
85
+
// 1. Check if itโs a URL
86
+
try {
87
+
const url = new URL(text);
88
+
const knownHosts = [
89
+
"bsky.app",
90
+
"social.daniela.lol",
91
+
"deer.social",
92
+
"reddwarf.whey.party",
93
+
"reddwarf.app",
94
+
"main.bsky.dev",
95
+
"catsky.social",
96
+
"blacksky.community",
97
+
"red-dwarf-social-app.whey.party",
98
+
"zeppelin.social",
99
+
];
100
+
if (knownHosts.includes(url.hostname)) {
101
+
// parse path to get URI or handle
102
+
const path = decodeURIComponent(url.pathname.slice(1)); // remove leading /
103
+
console.log("BSky URL path:", path);
104
+
navigate({
105
+
to: `/${path}`,
106
+
});
107
+
return;
108
+
}
109
+
} catch {
110
+
// not a URL, continue
111
+
}
112
+
113
+
// 2. Check if text looks like an at-uri
114
+
try {
115
+
if (text.startsWith("at://")) {
116
+
console.log("AT URI detected:", text);
117
+
const aturi = new AtUri(text);
118
+
switch (aturi.collection) {
119
+
case "app.bsky.feed.post": {
120
+
navigate({
121
+
to: "/profile/$did/post/$rkey",
122
+
params: {
123
+
did: aturi.host,
124
+
rkey: aturi.rkey,
125
+
},
126
+
});
127
+
return;
128
+
}
129
+
case "app.bsky.actor.profile": {
130
+
navigate({
131
+
to: "/profile/$did",
132
+
params: {
133
+
did: aturi.host,
134
+
},
135
+
});
136
+
return;
137
+
}
138
+
// todo add more handlers as more routes are added. like feeds, lists, etc etc thanks!
139
+
default: {
140
+
// continue
141
+
}
142
+
}
143
+
}
144
+
} catch {
145
+
// continue
146
+
}
147
+
148
+
// 3. Plain handle (starts with @)
149
+
try {
150
+
if (text.startsWith("@")) {
151
+
const handle = text.slice(1);
152
+
console.log("Handle detected:", handle);
153
+
navigate({ to: "/profile/$did", params: { did: handle } });
154
+
return;
155
+
}
156
+
} catch {
157
+
// continue
158
+
}
159
+
160
+
// 4. Plain DID (starts with did:)
161
+
try {
162
+
if (text.startsWith("did:")) {
163
+
console.log("did detected:", text);
164
+
navigate({ to: "/profile/$did", params: { did: text } });
165
+
return;
166
+
}
167
+
} catch {
168
+
// continue
169
+
}
170
+
171
+
// if all else fails
172
+
173
+
// try {
174
+
// // probably a user?
175
+
// navigate({ to: "/profile/$did", params: { did: text } });
176
+
// return;
177
+
// } catch {
178
+
// // continue
179
+
// }
180
+
181
+
if (lycanReady) {
182
+
navigate({ to: "/search", search: { q: text } });
183
+
}
184
+
}
+43
-201
src/components/InfiniteCustomFeed.tsx
+43
-201
src/components/InfiniteCustomFeed.tsx
···
1
-
/* eslint-disable react-hooks/refs */
2
-
import { useWindowVirtualizer } from "@tanstack/react-virtual";
3
-
import { useAtom } from "jotai";
4
import * as React from "react";
5
-
import { useEffect, useLayoutEffect } from "react";
6
7
//import { useInView } from "react-intersection-observer";
8
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
9
import { useAuth } from "~/providers/UnifiedAuthProvider";
10
-
import { feedHeightsAtom, feedScrollIndexAtom } from "~/utils/atoms";
11
-
import { useInfiniteQueryFeedSkeleton } from "~/utils/useQuery";
12
13
interface InfiniteCustomFeedProps {
14
feedUri: string;
15
pdsUrl?: string;
16
feedServiceDid?: string;
17
-
initialScrollIndex?: number;
18
-
//onVisibleIndexChange?: (index: number) => void;
19
}
20
21
export function InfiniteCustomFeed({
22
feedUri,
23
pdsUrl,
24
feedServiceDid,
25
-
initialScrollIndex,
26
-
//onVisibleIndexChange,
27
}: InfiniteCustomFeedProps) {
28
-
const OVERSCAN_COUNT = 10;
29
-
const ESTIMATE_HEIGHT = 150;
30
-
31
const { agent } = useAuth();
32
-
const authed = !!agent?.did;
33
-
34
-
const listRef = React.useRef<HTMLDivElement | null>(null);
35
-
const [offsetTop, setOffsetTop] = React.useState(0);
36
-
const [scrollIndexes, setScrollIndexes] = useAtom(feedScrollIndexAtom);
37
-
//const initialScrollIndex = scrollIndexes[feedUri];
38
39
// const identityresultmaybe = useQueryIdentity(agent?.did);
40
// const identity = identityresultmaybe?.data;
···
50
isFetchingNextPage,
51
refetch,
52
isRefetching,
53
} = useInfiniteQueryFeedSkeleton({
54
feedUri: feedUri,
55
agent: agent ?? undefined,
56
isAuthed: authed ?? false,
57
pdsUrl: pdsUrl,
58
feedServiceDid: feedServiceDid,
59
});
60
61
const handleRefresh = () => {
62
refetch();
63
};
64
65
-
//const { ref, inView } = useInView();
66
-
67
-
// React.useEffect(() => {
68
-
// if (inView && hasNextPage && !isFetchingNextPage) {
69
-
// fetchNextPage();
70
-
// }
71
-
// }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);
72
-
73
const allPosts = React.useMemo(() => {
74
const flattenedPosts = data?.pages.flatMap((page) => page?.feed) ?? [];
75
···
83
}
84
85
seenUris.add(item.post);
86
return true;
87
});
88
}, [data]);
89
90
-
const [feedHeights, setFeedHeights] = useAtom(feedHeightsAtom);
91
-
const currentFeedCache = feedHeights[feedUri] ?? {};
92
-
93
-
const virtualizerRef = React.useRef<ReturnType<
94
-
typeof useWindowVirtualizer
95
-
> | null>(null);
96
-
97
-
const virtualizer = useWindowVirtualizer({
98
-
count: allPosts.length,
99
-
// +
100
-
// (isFetchingNextPage ? 1 : 0) +
101
-
// (hasNextPage && !isFetchingNextPage ? 1 : 0) +
102
-
// (!hasNextPage ? 1 : 0) +
103
-
// 1,
104
-
estimateSize: (index) => {
105
-
const post = allPosts[index];
106
-
if (!post) return ESTIMATE_HEIGHT;
107
-
108
-
if (currentFeedCache[post.post]) {
109
-
return currentFeedCache[post.post];
110
-
}
111
-
112
-
return ESTIMATE_HEIGHT;
113
-
},
114
-
// measureElement: measureElement,
115
-
overscan: OVERSCAN_COUNT,
116
-
scrollMargin: offsetTop,
117
-
});
118
-
// React.useEffect(() => {
119
-
// virtualizer.measure();
120
-
// }, [data]);
121
-
122
-
const measureElement = React.useCallback(
123
-
(node: HTMLElement | null) => {
124
-
if (!node) return;
125
-
126
-
virtualizer.measureElement(node);
127
-
128
-
const postUri = node.dataset.postUri;
129
-
const newHeight = node.offsetHeight;
130
-
131
-
if (postUri && newHeight > 0 && currentFeedCache[postUri] !== newHeight) {
132
-
setFeedHeights((prev) => ({
133
-
...prev,
134
-
[feedUri]: {
135
-
...prev[feedUri],
136
-
[postUri]: newHeight,
137
-
},
138
-
}));
139
-
}
140
-
},
141
-
[virtualizer, setFeedHeights, feedUri, currentFeedCache]
142
-
);
143
-
144
-
virtualizerRef.current = virtualizer;
145
-
146
-
useLayoutEffect(() => {
147
-
const update = () => {
148
-
if (listRef.current) {
149
-
setOffsetTop(listRef.current.offsetTop);
150
-
}
151
-
//if (virtualizerRef.current) {
152
-
// virtualizerRef.current.measure();
153
-
// }
154
-
};
155
-
156
-
update();
157
-
158
-
let debounceTimeout: NodeJS.Timeout;
159
-
160
-
const debouncedUpdate = () => {
161
-
clearTimeout(debounceTimeout);
162
-
debounceTimeout = setTimeout(update, 100);
163
-
};
164
-
165
-
window.addEventListener("resize", debouncedUpdate);
166
-
167
-
return () => {
168
-
window.removeEventListener("resize", debouncedUpdate);
169
-
clearTimeout(debounceTimeout);
170
-
};
171
-
}, []);
172
-
173
-
const hasRestoredScroll = React.useRef(false);
174
-
useLayoutEffect(() => {
175
-
if (
176
-
hasRestoredScroll.current ||
177
-
!initialScrollIndex ||
178
-
initialScrollIndex === 0
179
-
) {
180
-
return;
181
-
}
182
-
183
-
if (initialScrollIndex < allPosts.length) {
184
-
console.log(`Restoring scroll to index: ${initialScrollIndex}`);
185
-
virtualizer.scrollToIndex(initialScrollIndex, {
186
-
align: "start",
187
-
behavior: "auto",
188
-
});
189
-
hasRestoredScroll.current = true;
190
-
}
191
-
}, [initialScrollIndex, allPosts.length, virtualizer]);
192
193
// React.useEffect(() => {
194
-
// const handleScroll = () => {
195
-
// const topVisibleItem = virtualizer.getVirtualItems()[0];
196
-
// if (topVisibleItem && onVisibleIndexChange) {
197
-
// onVisibleIndexChange(topVisibleItem.index);
198
-
// }
199
-
// };
200
-
201
-
// window.addEventListener('scroll', handleScroll, { passive: true });
202
-
// return () => window.removeEventListener('scroll', handleScroll);
203
-
// }, [virtualizer, onVisibleIndexChange]);
204
-
205
-
useEffect(() => {
206
-
return () => {
207
-
const topVisibleItem = virtualizer.getVirtualItems()[OVERSCAN_COUNT];
208
-
209
-
if (topVisibleItem) {
210
-
console.log(
211
-
`Saving final scroll index ${topVisibleItem.index} for feed ${feedUri}`
212
-
);
213
-
setScrollIndexes((prev) => ({
214
-
...prev,
215
-
[feedUri]: topVisibleItem.index,
216
-
}));
217
-
}
218
-
};
219
-
}, [virtualizer, feedUri, setScrollIndexes]);
220
221
if (isLoading) {
222
return <div className="p-4 text-center text-gray-500">Loading feed...</div>;
···
227
<div className="p-4 text-center text-red-500">Error: {error.message}</div>
228
);
229
}
230
231
if (!allPosts || typeof allPosts !== "object" || allPosts.length === 0) {
232
return (
···
236
);
237
}
238
239
-
//if (offsetTop === 0) {
240
-
// return <div ref={listRef}>Calculating...</div>;
241
-
//}
242
-
243
return (
244
<>
245
-
<div ref={listRef}>
246
-
<div
247
-
style={{
248
-
height: `${virtualizer.getTotalSize()}px`,
249
-
width: "100%",
250
-
position: "relative",
251
-
}}
252
-
>
253
-
{virtualizer.getVirtualItems().map((virtualItem) => {
254
-
const item = allPosts[virtualItem.index];
255
-
const i = virtualItem.index;
256
-
if (item)
257
-
return (
258
-
<UniversalPostRendererATURILoader
259
-
key={item.post || i}
260
-
atUri={item.post}
261
-
dataIndexPropPass={i}
262
-
feedviewpost={true}
263
-
ref={measureElement}
264
-
repostedby={
265
-
!!item.reason?.$type && (item.reason as any)?.repost
266
-
}
267
-
style={{
268
-
position: "absolute",
269
-
top: 0,
270
-
left: 0,
271
-
width: "100%",
272
-
//height: `${item.size}px`,
273
-
transform: `translateY(${virtualItem.start - offsetTop}px)`,
274
-
}}
275
-
/>
276
-
);
277
-
})}
278
-
</div>
279
-
</div>
280
-
281
{/* allPosts?: {allPosts ? "true" : "false"}
282
hasNextPage?: {hasNextPage ? "true" : "false"}
283
isFetchingNextPage?: {isFetchingNextPage ? "true" : "false"} */}
···
298
<button
299
onClick={handleRefresh}
300
disabled={isRefetching}
301
-
className="sticky lg:bottom-6 bottom-24 ml-4 w-[42px] h-[42px] z-10 bg-gray-500 hover:bg-gray-600 text-gray-50 p-[9px] rounded-full shadow-lg transition-transform duration-200 ease-in-out hover:scale-110 disabled:bg-gray-400 disabled:cursor-not-allowed"
302
aria-label="Refresh feed"
303
>
304
-
{isRefetching ? (
305
-
<RefreshIcon className="h-6 w-6 animate-spin" />
306
-
) : (
307
-
<RefreshIcon className="h-6 w-6" />
308
-
)}
309
</button>
310
</>
311
);
···
1
+
import { useQueryClient } from "@tanstack/react-query";
2
import * as React from "react";
3
4
//import { useInView } from "react-intersection-observer";
5
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
6
import { useAuth } from "~/providers/UnifiedAuthProvider";
7
+
import {
8
+
useInfiniteQueryFeedSkeleton,
9
+
// useQueryArbitrary,
10
+
// useQueryIdentity,
11
+
} from "~/utils/useQuery";
12
13
interface InfiniteCustomFeedProps {
14
feedUri: string;
15
pdsUrl?: string;
16
feedServiceDid?: string;
17
+
authedOverride?: boolean;
18
+
unauthedfeedurl?: string;
19
}
20
21
export function InfiniteCustomFeed({
22
feedUri,
23
pdsUrl,
24
feedServiceDid,
25
+
authedOverride,
26
+
unauthedfeedurl,
27
}: InfiniteCustomFeedProps) {
28
const { agent } = useAuth();
29
+
const authed = authedOverride || !!agent?.did;
30
31
// const identityresultmaybe = useQueryIdentity(agent?.did);
32
// const identity = identityresultmaybe?.data;
···
42
isFetchingNextPage,
43
refetch,
44
isRefetching,
45
+
queryKey,
46
} = useInfiniteQueryFeedSkeleton({
47
feedUri: feedUri,
48
agent: agent ?? undefined,
49
isAuthed: authed ?? false,
50
pdsUrl: pdsUrl,
51
feedServiceDid: feedServiceDid,
52
+
unauthedfeedurl: unauthedfeedurl,
53
});
54
+
const queryClient = useQueryClient();
55
+
56
57
const handleRefresh = () => {
58
+
queryClient.removeQueries({queryKey: queryKey});
59
+
//queryClient.invalidateQueries(["infinite-feed", feedUri] as const);
60
refetch();
61
};
62
63
const allPosts = React.useMemo(() => {
64
const flattenedPosts = data?.pages.flatMap((page) => page?.feed) ?? [];
65
···
73
}
74
75
seenUris.add(item.post);
76
+
77
return true;
78
});
79
}, [data]);
80
81
+
//const { ref, inView } = useInView();
82
83
// React.useEffect(() => {
84
+
// if (inView && hasNextPage && !isFetchingNextPage) {
85
+
// fetchNextPage();
86
+
// }
87
+
// }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);
88
89
if (isLoading) {
90
return <div className="p-4 text-center text-gray-500">Loading feed...</div>;
···
95
<div className="p-4 text-center text-red-500">Error: {error.message}</div>
96
);
97
}
98
+
99
+
// const allPosts =
100
+
// data?.pages.flatMap((page) => {
101
+
// if (page) return page.feed;
102
+
// }) ?? [];
103
104
if (!allPosts || typeof allPosts !== "object" || allPosts.length === 0) {
105
return (
···
109
);
110
}
111
112
return (
113
<>
114
+
{allPosts.map((item, i) => {
115
+
if (item)
116
+
return (
117
+
<UniversalPostRendererATURILoader
118
+
key={item.post || i}
119
+
atUri={item.post}
120
+
feedviewpost={true}
121
+
repostedby={!!item.reason?.$type && (item.reason as any)?.repost}
122
+
/>
123
+
);
124
+
})}
125
{/* allPosts?: {allPosts ? "true" : "false"}
126
hasNextPage?: {hasNextPage ? "true" : "false"}
127
isFetchingNextPage?: {isFetchingNextPage ? "true" : "false"} */}
···
142
<button
143
onClick={handleRefresh}
144
disabled={isRefetching}
145
+
className="sticky lg:bottom-4 bottom-22 ml-4 w-[42px] h-[42px] z-10 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-gray-50 p-[9px] rounded-full shadow-lg transition-transform duration-200 ease-in-out hover:scale-110 disabled:dark:bg-gray-900 disabled:bg-gray-100 disabled:cursor-not-allowed"
146
aria-label="Refresh feed"
147
>
148
+
<RefreshIcon
149
+
className={`h-6 w-6 text-gray-600 dark:text-gray-400 ${isRefetching && "animate-spin"}`}
150
+
/>
151
</button>
152
</>
153
);
+188
-58
src/components/Login.tsx
+188
-58
src/components/Login.tsx
···
1
// src/components/Login.tsx
2
-
import React, { useEffect, useState, useRef } from "react";
3
import { useAuth } from "~/providers/UnifiedAuthProvider";
4
-
import { Agent } from "@atproto/api";
5
6
// --- 1. The Main Component (Orchestrator with `compact` prop) ---
7
-
export default function Login({ compact = false }: { compact?: boolean }) {
8
const { status, agent, logout } = useAuth();
9
10
// Loading state can be styled differently based on the prop
···
14
className={
15
compact
16
? "flex items-center justify-center p-1"
17
-
: "p-6 bg-gray-100 dark:bg-gray-900 rounded-xl shadow border border-gray-200 dark:border-gray-800 mt-6 mx-4 flex justify-center items-center h-[280px]"
18
}
19
>
20
<span
···
33
// Large view
34
if (!compact) {
35
return (
36
-
<div className="p-6 bg-gray-100 dark:bg-gray-900 rounded-xl shadow border border-gray-200 dark:border-gray-800 mt-6 mx-4">
37
<div className="flex flex-col items-center justify-center text-center">
38
<p className="text-lg font-semibold mb-4 text-gray-800 dark:text-gray-100">
39
You are logged in!
···
41
<ProfileThing agent={agent} large />
42
<button
43
onClick={logout}
44
-
className="bg-gray-600 mt-4 hover:bg-gray-700 text-white rounded px-6 py-2 font-semibold text-base transition-colors"
45
>
46
Log out
47
</button>
···
67
if (!compact) {
68
// Large view renders the form directly in the card
69
return (
70
-
<div className="p-6 bg-gray-100 dark:bg-gray-900 rounded-xl shadow border border-gray-200 dark:border-gray-800 mt-6 mx-4">
71
<UnifiedLoginForm />
72
</div>
73
);
74
}
75
76
// Compact view renders a button that toggles the form in a dropdown
77
-
return <CompactLoginButton />;
78
}
79
80
// --- 2. The Reusable, Self-Contained Login Form Component ---
···
83
84
return (
85
<div>
86
-
<div className="flex border-b border-gray-200 dark:border-gray-700 mb-4">
87
<TabButton
88
label="OAuth"
89
active={mode === "oauth"}
···
103
// --- 3. Helper components for layouts, forms, and UI ---
104
105
// A new component to contain the logic for the compact dropdown
106
-
const CompactLoginButton = () => {
107
const [showForm, setShowForm] = useState(false);
108
const formRef = useRef<HTMLDivElement>(null);
109
···
125
<div className="relative" ref={formRef}>
126
<button
127
onClick={() => setShowForm(!showForm)}
128
-
className="text-sm bg-gray-600 hover:bg-gray-700 text-white rounded px-3 py-1 font-medium transition-colors"
129
>
130
Log in
131
</button>
132
{showForm && (
133
-
<div className="absolute top-full right-0 mt-2 w-80 bg-white dark:bg-gray-900 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-4 z-50">
134
<UnifiedLoginForm />
135
</div>
136
)}
···
138
);
139
};
140
141
-
const TabButton = ({ label, active, onClick }: { label: string; active: boolean; onClick: () => void; }) => (
142
<button
143
onClick={onClick}
144
-
className={`px-4 py-2 text-sm font-medium transition-colors ${
145
active
146
-
? "text-gray-600 dark:text-gray-200 border-b-2 border-gray-500"
147
-
: "text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
148
}`}
149
>
150
{label}
···
169
};
170
return (
171
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
172
-
<p className="text-xs text-gray-500 dark:text-gray-400">Sign in with AT. Your password is never shared.</p>
173
-
<input type="text" placeholder="handle.bsky.social" value={handle} onChange={(e) => setHandle(e.target.value)} className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" />
174
-
<button type="submit" className="bg-gray-600 hover:bg-gray-700 text-white rounded px-4 py-2 font-medium text-sm transition-colors">Log in</button>
175
</form>
176
);
177
};
···
201
202
return (
203
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
204
-
<p className="text-xs text-red-500 dark:text-red-400">Warning: Less secure. Use an App Password.</p>
205
-
<input type="text" placeholder="handle.bsky.social" value={user} onChange={(e) => setUser(e.target.value)} className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" autoComplete="username" />
206
-
<input type="password" placeholder="App Password" value={password} onChange={(e) => setPassword(e.target.value)} className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" autoComplete="current-password" />
207
-
<input type="text" placeholder="PDS (e.g., bsky.social)" value={serviceURL} onChange={(e) => setServiceURL(e.target.value)} className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" />
208
{error && <p className="text-xs text-red-500">{error}</p>}
209
-
<button type="submit" className="bg-gray-600 hover:bg-gray-700 text-white rounded px-4 py-2 font-medium text-sm transition-colors">Log in</button>
210
</form>
211
);
212
};
213
214
// --- Profile Component (now supports a `large` prop for styling) ---
215
-
export const ProfileThing = ({ agent, large = false }: { agent: Agent | null; large?: boolean }) => {
216
-
const [profile, setProfile] = useState<any>(null);
217
218
-
useEffect(() => {
219
-
const fetchUser = async () => {
220
-
const did = (agent as any)?.session?.did ?? (agent as any)?.assertDid;
221
-
if (!did) return;
222
-
try {
223
-
const res = await agent!.getProfile({ actor: did });
224
-
setProfile(res.data);
225
-
} catch (e) { console.error("Failed to fetch profile", e); }
226
-
};
227
-
if (agent) fetchUser();
228
-
}, [agent]);
229
230
-
if (!profile) {
231
-
return ( // Skeleton loader
232
-
<div className={`flex items-center gap-2.5 animate-pulse ${large ? 'mb-1' : ''}`}>
233
-
<div className={`rounded-full bg-gray-300 dark:bg-gray-700 ${large ? 'w-10 h-10' : 'w-[30px] h-[30px]'}`} />
234
-
<div className="flex flex-col gap-2">
235
-
<div className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? 'h-4 w-28' : 'h-3 w-20'}`} />
236
-
<div className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? 'h-4 w-20' : 'h-3 w-16'}`} />
237
-
</div>
238
-
</div>
239
-
);
240
-
}
241
-
242
-
return (
243
-
<div className={`flex flex-row items-center gap-2.5 ${large ? 'mb-1' : ''}`}>
244
-
<img src={profile?.avatar} alt="avatar" className={`object-cover rounded-full ${large ? 'w-10 h-10' : 'w-[30px] h-[30px]'}`} />
245
-
<div className="flex flex-col items-start text-left">
246
-
<div className={`font-medium ${large ? 'text-gray-800 dark:text-gray-100 text-md' : 'text-gray-800 dark:text-gray-100 text-sm'}`}>{profile?.displayName}</div>
247
-
<div className={` ${large ? 'text-gray-500 dark:text-gray-400 text-sm' : 'text-gray-500 dark:text-gray-400 text-xs'}`}>@{profile?.handle}</div>
248
-
</div>
249
</div>
250
-
);
251
-
};
···
1
// src/components/Login.tsx
2
+
import AtpAgent, { Agent } from "@atproto/api";
3
+
import { useAtom } from "jotai";
4
+
import React, { useEffect, useRef, useState } from "react";
5
+
6
import { useAuth } from "~/providers/UnifiedAuthProvider";
7
+
import { imgCDNAtom } from "~/utils/atoms";
8
+
import { useQueryIdentity, useQueryProfile } from "~/utils/useQuery";
9
10
// --- 1. The Main Component (Orchestrator with `compact` prop) ---
11
+
export default function Login({
12
+
compact = false,
13
+
popup = false,
14
+
}: {
15
+
compact?: boolean;
16
+
popup?: boolean;
17
+
}) {
18
const { status, agent, logout } = useAuth();
19
20
// Loading state can be styled differently based on the prop
···
24
className={
25
compact
26
? "flex items-center justify-center p-1"
27
+
: "p-6 bg-gray-100 dark:bg-gray-900 rounded-xl shadow border border-gray-200 dark:border-gray-800 mt-4 mx-4 flex justify-center items-center h-[280px]"
28
}
29
>
30
<span
···
43
// Large view
44
if (!compact) {
45
return (
46
+
<div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800 mt-4 mx-4">
47
<div className="flex flex-col items-center justify-center text-center">
48
<p className="text-lg font-semibold mb-4 text-gray-800 dark:text-gray-100">
49
You are logged in!
···
51
<ProfileThing agent={agent} large />
52
<button
53
onClick={logout}
54
+
className="bg-gray-600 mt-4 hover:bg-gray-700 text-white rounded-full px-6 py-2 font-semibold text-base transition-colors"
55
>
56
Log out
57
</button>
···
77
if (!compact) {
78
// Large view renders the form directly in the card
79
return (
80
+
<div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800 mt-4 mx-4">
81
<UnifiedLoginForm />
82
</div>
83
);
84
}
85
86
// Compact view renders a button that toggles the form in a dropdown
87
+
return <CompactLoginButton popup={popup} />;
88
}
89
90
// --- 2. The Reusable, Self-Contained Login Form Component ---
···
93
94
return (
95
<div>
96
+
<div className="flex bg-gray-300 rounded-full dark:bg-gray-700 mb-4">
97
<TabButton
98
label="OAuth"
99
active={mode === "oauth"}
···
113
// --- 3. Helper components for layouts, forms, and UI ---
114
115
// A new component to contain the logic for the compact dropdown
116
+
const CompactLoginButton = ({ popup }: { popup?: boolean }) => {
117
const [showForm, setShowForm] = useState(false);
118
const formRef = useRef<HTMLDivElement>(null);
119
···
135
<div className="relative" ref={formRef}>
136
<button
137
onClick={() => setShowForm(!showForm)}
138
+
className="text-sm bg-gray-600 hover:bg-gray-700 text-white rounded-full px-3 py-1 font-medium transition-colors"
139
>
140
Log in
141
</button>
142
{showForm && (
143
+
<div
144
+
className={`absolute ${popup ? `bottom-[calc(100%)]` : `top-full`} right-0 mt-2 w-80 bg-white dark:bg-gray-900 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-4 z-50`}
145
+
>
146
<UnifiedLoginForm />
147
</div>
148
)}
···
150
);
151
};
152
153
+
const TabButton = ({
154
+
label,
155
+
active,
156
+
onClick,
157
+
}: {
158
+
label: string;
159
+
active: boolean;
160
+
onClick: () => void;
161
+
}) => (
162
<button
163
onClick={onClick}
164
+
className={`px-4 py-2 text-sm font-medium transition-colors rounded-full flex-1 ${
165
active
166
+
? "text-gray-50 dark:text-gray-200 border-gray-500 bg-gray-400 dark:bg-gray-500"
167
+
: "text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200"
168
}`}
169
>
170
{label}
···
189
};
190
return (
191
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
192
+
<p className="text-xs text-gray-500 dark:text-gray-400">
193
+
Sign in with AT. Your password is never shared.
194
+
</p>
195
+
{/* <input
196
+
type="text"
197
+
placeholder="handle.bsky.social"
198
+
value={handle}
199
+
onChange={(e) => setHandle(e.target.value)}
200
+
className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500"
201
+
/> */}
202
+
<div className="flex flex-col gap-3">
203
+
<div className="m3input-field m3input-label m3input-border size-md flex-1">
204
+
<input
205
+
type="text"
206
+
placeholder=" "
207
+
value={handle}
208
+
onChange={(e) => setHandle(e.target.value)}
209
+
/>
210
+
<label>AT Handle</label>
211
+
</div>
212
+
<button
213
+
type="submit"
214
+
className="bg-gray-600 hover:bg-gray-700 text-white rounded-full px-4 py-2 font-medium text-sm transition-colors"
215
+
>
216
+
Log in
217
+
</button>
218
+
</div>
219
</form>
220
);
221
};
···
245
246
return (
247
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
248
+
<p className="text-xs text-red-500 dark:text-red-400">
249
+
Warning: Less secure. Use an App Password.
250
+
</p>
251
+
{/* <input
252
+
type="text"
253
+
placeholder="handle.bsky.social"
254
+
value={user}
255
+
onChange={(e) => setUser(e.target.value)}
256
+
className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500"
257
+
autoComplete="username"
258
+
/>
259
+
<input
260
+
type="password"
261
+
placeholder="App Password"
262
+
value={password}
263
+
onChange={(e) => setPassword(e.target.value)}
264
+
className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500"
265
+
autoComplete="current-password"
266
+
/>
267
+
<input
268
+
type="text"
269
+
placeholder="PDS (e.g., bsky.social)"
270
+
value={serviceURL}
271
+
onChange={(e) => setServiceURL(e.target.value)}
272
+
className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500"
273
+
/> */}
274
+
<div className="m3input-field m3input-label m3input-border size-md flex-1">
275
+
<input
276
+
type="text"
277
+
placeholder=" "
278
+
value={user}
279
+
onChange={(e) => setUser(e.target.value)}
280
+
/>
281
+
<label>AT Handle</label>
282
+
</div>
283
+
<div className="m3input-field m3input-label m3input-border size-md flex-1">
284
+
<input
285
+
type="text"
286
+
placeholder=" "
287
+
value={password}
288
+
onChange={(e) => setPassword(e.target.value)}
289
+
/>
290
+
<label>App Password</label>
291
+
</div>
292
+
<div className="m3input-field m3input-label m3input-border size-md flex-1">
293
+
<input
294
+
type="text"
295
+
placeholder=" "
296
+
value={serviceURL}
297
+
onChange={(e) => setServiceURL(e.target.value)}
298
+
/>
299
+
<label>PDS</label>
300
+
</div>
301
{error && <p className="text-xs text-red-500">{error}</p>}
302
+
<button
303
+
type="submit"
304
+
className="bg-gray-600 hover:bg-gray-700 text-white rounded-full px-4 py-2 font-medium text-sm transition-colors"
305
+
>
306
+
Log in
307
+
</button>
308
</form>
309
);
310
};
311
312
// --- Profile Component (now supports a `large` prop for styling) ---
313
+
export const ProfileThing = ({
314
+
agent,
315
+
large = false,
316
+
}: {
317
+
agent: Agent | null;
318
+
large?: boolean;
319
+
}) => {
320
+
const did = ((agent as AtpAgent)?.session?.did ??
321
+
(agent as AtpAgent)?.assertDid ??
322
+
agent?.did) as string | undefined;
323
+
const { data: identity } = useQueryIdentity(did);
324
+
const { data: profiledata } = useQueryProfile(
325
+
`at://${did}/app.bsky.actor.profile/self`
326
+
);
327
+
const profile = profiledata?.value;
328
329
+
const [imgcdn] = useAtom(imgCDNAtom)
330
+
331
+
function getAvatarUrl(p: typeof profile) {
332
+
const link = p?.avatar?.ref?.["$link"];
333
+
if (!link || !did) return null;
334
+
return `https://${imgcdn}/img/avatar/plain/${did}/${link}@jpeg`;
335
+
}
336
+
337
+
if (!profiledata) {
338
+
return (
339
+
// Skeleton loader
340
+
<div
341
+
className={`flex items-center gap-2.5 animate-pulse ${large ? "mb-1" : ""}`}
342
+
>
343
+
<div
344
+
className={`rounded-full bg-gray-300 dark:bg-gray-700 ${large ? "w-10 h-10" : "w-[30px] h-[30px]"}`}
345
+
/>
346
+
<div className="flex flex-col gap-2">
347
+
<div
348
+
className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? "h-4 w-28" : "h-3 w-20"}`}
349
+
/>
350
+
<div
351
+
className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? "h-4 w-20" : "h-3 w-16"}`}
352
+
/>
353
+
</div>
354
+
</div>
355
+
);
356
+
}
357
358
+
return (
359
+
<div
360
+
className={`flex flex-row items-center gap-2.5 ${large ? "mb-1" : ""}`}
361
+
>
362
+
<img
363
+
src={getAvatarUrl(profile) ?? undefined}
364
+
alt="avatar"
365
+
className={`object-cover rounded-full ${large ? "w-10 h-10" : "w-[30px] h-[30px]"}`}
366
+
/>
367
+
<div className="flex flex-col items-start text-left">
368
+
<div
369
+
className={`font-medium ${large ? "text-gray-800 dark:text-gray-100 text-md" : "text-gray-800 dark:text-gray-100 text-sm"}`}
370
+
>
371
+
{profile?.displayName}
372
+
</div>
373
+
<div
374
+
className={` ${large ? "text-gray-500 dark:text-gray-400 text-sm" : "text-gray-500 dark:text-gray-400 text-xs"}`}
375
+
>
376
+
@{identity?.handle}
377
</div>
378
+
</div>
379
+
</div>
380
+
);
381
+
};
+124
src/components/ReusableTabRoute.tsx
+124
src/components/ReusableTabRoute.tsx
···
···
1
+
import * as TabsPrimitive from "@radix-ui/react-tabs";
2
+
import { useAtom } from "jotai";
3
+
import { useEffect, useLayoutEffect } from "react";
4
+
5
+
import { isAtTopAtom, reusableTabRouteScrollAtom } from "~/utils/atoms";
6
+
7
+
/**
8
+
* Please wrap your Route in a div, do not return a top-level fragment,
9
+
* it will break navigation scroll restoration
10
+
*/
11
+
export function ReusableTabRoute({
12
+
route,
13
+
tabs,
14
+
}: {
15
+
route: string;
16
+
tabs: Record<string, React.ReactNode>;
17
+
}) {
18
+
const [reusableTabState, setReusableTabState] = useAtom(
19
+
reusableTabRouteScrollAtom
20
+
);
21
+
const [isAtTop] = useAtom(isAtTopAtom);
22
+
23
+
const routeState = reusableTabState?.[route] ?? {
24
+
activeTab: Object.keys(tabs)[0],
25
+
scrollPositions: {},
26
+
};
27
+
const activeTab = routeState.activeTab;
28
+
29
+
const handleValueChange = (newTab: string) => {
30
+
setReusableTabState((prev) => {
31
+
const current = prev?.[route] ?? routeState;
32
+
return {
33
+
...prev,
34
+
[route]: {
35
+
...current,
36
+
scrollPositions: {
37
+
...current.scrollPositions,
38
+
[current.activeTab]: window.scrollY,
39
+
},
40
+
activeTab: newTab,
41
+
},
42
+
};
43
+
});
44
+
};
45
+
46
+
// // todo, warning experimental, usually this doesnt work,
47
+
// // like at all, and i usually do this for each tab
48
+
// useLayoutEffect(() => {
49
+
// const savedScroll = routeState.scrollPositions[activeTab] ?? 0;
50
+
// window.scrollTo({ top: savedScroll });
51
+
// // eslint-disable-next-line react-hooks/exhaustive-deps
52
+
// }, [activeTab, route]);
53
+
54
+
useLayoutEffect(() => {
55
+
return () => {
56
+
setReusableTabState((prev) => {
57
+
const current = prev?.[route] ?? routeState;
58
+
return {
59
+
...prev,
60
+
[route]: {
61
+
...current,
62
+
scrollPositions: {
63
+
...current.scrollPositions,
64
+
[current.activeTab]: window.scrollY,
65
+
},
66
+
},
67
+
};
68
+
});
69
+
};
70
+
// eslint-disable-next-line react-hooks/exhaustive-deps
71
+
}, []);
72
+
73
+
return (
74
+
<TabsPrimitive.Root
75
+
value={activeTab}
76
+
onValueChange={handleValueChange}
77
+
className={`w-full`}
78
+
>
79
+
<TabsPrimitive.List
80
+
className={`flex sticky top-[52px] bg-[var(--header-bg-light)] dark:bg-[var(--header-bg-dark)] z-[9] border-0 sm:border-b ${!isAtTop && "shadow-sm"} sm:shadow-none sm:dark:bg-gray-950 sm:bg-white border-gray-200 dark:border-gray-700`}
81
+
>
82
+
{Object.entries(tabs).map(([key]) => (
83
+
<TabsPrimitive.Trigger key={key} value={key} className="m3tab">
84
+
{key}
85
+
</TabsPrimitive.Trigger>
86
+
))}
87
+
</TabsPrimitive.List>
88
+
89
+
{Object.entries(tabs).map(([key, node]) => (
90
+
<TabsPrimitive.Content key={key} value={key} className="flex-1 min-h-[80dvh]">
91
+
{activeTab === key && node}
92
+
</TabsPrimitive.Content>
93
+
))}
94
+
</TabsPrimitive.Root>
95
+
);
96
+
}
97
+
98
+
export function useReusableTabScrollRestore(route: string) {
99
+
const [reusableTabState] = useAtom(
100
+
reusableTabRouteScrollAtom
101
+
);
102
+
103
+
const routeState = reusableTabState?.[route];
104
+
const activeTab = routeState?.activeTab;
105
+
106
+
useEffect(() => {
107
+
const savedScroll = activeTab ? routeState?.scrollPositions[activeTab] ?? 0 : 0;
108
+
//window.scrollTo(0, savedScroll);
109
+
window.scrollTo({ top: savedScroll });
110
+
// eslint-disable-next-line react-hooks/exhaustive-deps
111
+
}, []);
112
+
}
113
+
114
+
115
+
/*
116
+
117
+
const [notifState] = useAtom(notificationsScrollAtom);
118
+
const activeTab = notifState.activeTab;
119
+
useEffect(() => {
120
+
const savedY = notifState.scrollPositions[activeTab] ?? 0;
121
+
window.scrollTo(0, savedY);
122
+
}, [activeTab, notifState.scrollPositions]);
123
+
124
+
*/
+6
src/components/Star.tsx
+6
src/components/Star.tsx
···
···
1
+
import type { SVGProps } from 'react';
2
+
import React from 'react';
3
+
4
+
export function FluentEmojiHighContrastGlowingStar(props: SVGProps<SVGSVGElement>) {
5
+
return (<svg xmlns="http://www.w3.org/2000/svg" width={32} height={32} viewBox="0 0 32 32" {...props}><g fill="currentColor"><path d="m28.979 17.003l-3.108.214c-.834.06-1.178 1.079-.542 1.608l2.388 1.955c.521.428 1.314.204 1.523-.428l.709-2.127c.219-.632-.292-1.273-.97-1.222M21.75 2.691l-.72 2.9c-.2.78.66 1.41 1.34.98l2.54-1.58c.55-.34.58-1.14.05-1.52l-1.78-1.29a.912.912 0 0 0-1.43.51M6.43 4.995l2.53 1.58c.68.43 1.54-.19 1.35-.98l-.72-2.9a.92.92 0 0 0-1.43-.52l-1.78 1.29c-.53.4-.5 1.19.05 1.53M4.185 20.713l2.29-1.92c.62-.52.29-1.53-.51-1.58l-2.98-.21a.92.92 0 0 0-.94 1.2l.68 2.09c.2.62.97.84 1.46.42m13.61 7.292l-1.12-2.77c-.3-.75-1.36-.75-1.66 0l-1.12 2.77c-.24.6.2 1.26.85 1.26h2.2a.92.92 0 0 0 .85-1.26"></path><path d="m17.565 3.324l1.726 3.72c.326.694.967 1.18 1.717 1.29l4.056.624c1.835.278 2.575 2.53 1.293 3.859L23.268 16a2.28 2.28 0 0 0-.612 1.964l.71 4.374c.307 1.885-1.687 3.293-3.354 2.37l-3.405-1.894a2.25 2.25 0 0 0-2.21 0l-3.404 1.895c-1.668.922-3.661-.486-3.355-2.37l.71-4.375A2.28 2.28 0 0 0 7.736 16l-3.088-3.184c-1.293-1.34-.543-3.581 1.293-3.859l4.055-.625a2.3 2.3 0 0 0 1.717-1.29l1.727-3.719c.819-1.765 3.306-1.765 4.124 0"></path></g></svg>);
6
+
}
+969
-538
src/components/UniversalPostRenderer.tsx
+969
-538
src/components/UniversalPostRenderer.tsx
···
1
import { useNavigate } from "@tanstack/react-router";
2
import { useAtom } from "jotai";
3
import * as React from "react";
4
import { type SVGProps } from "react";
5
6
-
import { likedPostsAtom } from "~/utils/atoms";
7
import { useHydratedEmbed } from "~/utils/useHydrated";
8
import {
9
useQueryConstellation,
10
useQueryIdentity,
11
useQueryPost,
12
useQueryProfile,
13
} from "~/utils/useQuery";
14
15
function asTyped<T extends { $type: string }>(obj: T): $Typed<T> {
···
29
feedviewpost?: boolean;
30
repostedby?: string;
31
style?: React.CSSProperties;
32
-
ref?: React.Ref<HTMLDivElement>;
33
dataIndexPropPass?: number;
34
}
35
36
// export async function cachedGetRecord({
···
138
style,
139
ref,
140
dataIndexPropPass,
141
}: UniversalPostRendererATURILoaderProps) {
142
// /*mass comment*/ console.log("atUri", atUri);
143
//const { get, set } = usePersistentStore();
144
//const [record, setRecord] = React.useState<any>(null);
···
383
);
384
}, [links]);
385
386
// const navigateToProfile = (e: React.MouseEvent) => {
387
// e.stopPropagation();
388
// if (resolved?.did) {
···
398
}
399
400
return (
401
-
<UniversalPostRendererRawRecordShim
402
-
detailed={detailed}
403
-
postRecord={postQuery}
404
-
profileRecord={opProfile}
405
-
aturi={atUri}
406
-
resolved={resolved}
407
-
likesCount={likes}
408
-
repostsCount={reposts}
409
-
repliesCount={replies}
410
-
bottomReplyLine={bottomReplyLine}
411
-
topReplyLine={topReplyLine}
412
-
bottomBorder={bottomBorder}
413
-
feedviewpost={feedviewpost}
414
-
repostedby={repostedby}
415
-
style={style}
416
-
ref={ref}
417
-
dataIndexPropPass={dataIndexPropPass}
418
-
/>
419
);
420
}
421
422
-
function getAvatarUrl(opProfile: any, did: string) {
423
const link = opProfile?.value?.avatar?.ref?.["$link"];
424
if (!link) return null;
425
-
return `https://cdn.bsky.app/img/avatar/plain/${did}/${link}@jpeg`;
426
}
427
428
export function UniversalPostRendererRawRecordShim({
···
442
style,
443
ref,
444
dataIndexPropPass,
445
}: {
446
postRecord: any;
447
profileRecord: any;
···
457
feedviewpost?: boolean;
458
repostedby?: string;
459
style?: React.CSSProperties;
460
-
ref?: React.Ref<HTMLDivElement>;
461
dataIndexPropPass?: number;
462
}) {
463
// /*mass comment*/ console.log(`received aturi: ${aturi} of post content: ${postRecord}`);
464
const navigate = useNavigate();
···
529
// run();
530
// }, [postRecord, resolved?.did]);
531
532
const {
533
data: hydratedEmbed,
534
isLoading: isEmbedLoading,
535
error: embedError,
536
} = useHydratedEmbed(postRecord?.value?.embed, resolved?.did);
537
538
const parsedaturi = new AtUri(aturi); //parseAtUri(aturi);
539
540
const fakepost = React.useMemo<AppBskyFeedDefs.PostView>(
541
() => ({
542
$type: "app.bsky.feed.defs#postView",
543
uri: aturi,
544
cid: postRecord?.cid || "",
545
-
author: {
546
-
did: resolved?.did || "",
547
-
handle: resolved?.handle || "",
548
-
displayName: profileRecord?.value?.displayName || "",
549
-
avatar: getAvatarUrl(profileRecord, resolved?.did) || "",
550
-
viewer: undefined,
551
-
labels: profileRecord?.labels || undefined,
552
-
verification: undefined,
553
-
},
554
record: postRecord?.value || {},
555
embed: hydratedEmbed ?? undefined,
556
replyCount: repliesCount ?? 0,
···
567
postRecord?.cid,
568
postRecord?.value,
569
postRecord?.labels,
570
-
resolved?.did,
571
-
resolved?.handle,
572
-
profileRecord,
573
hydratedEmbed,
574
repliesCount,
575
repostsCount,
···
608
// }, [fakepost, get, set]);
609
const thereply = (fakepost?.record as AppBskyFeedPost.Record)?.reply?.parent
610
?.uri;
611
-
const feedviewpostreplydid = thereply ? new AtUri(thereply).host : undefined;
612
const replyhookvalue = useQueryIdentity(
613
feedviewpost ? feedviewpostreplydid : undefined
614
);
···
619
repostedby ? aturirepostbydid : undefined
620
);
621
const feedviewpostrepostedbyhandle = repostedbyhookvalue?.data?.handle;
622
return (
623
<>
624
{/* <p>
625
{postRecord?.value?.embed.$type + " " + JSON.stringify(hydratedEmbed)}
626
</p> */}
627
<UniversalPostRenderer
628
expanded={detailed}
629
onPostClick={() =>
···
646
}
647
}}
648
post={fakepost}
649
salt={aturi}
650
bottomReplyLine={bottomReplyLine}
651
topReplyLine={topReplyLine}
···
656
style={style}
657
ref={ref}
658
dataIndexPropPass={dataIndexPropPass}
659
/>
660
</>
661
);
···
694
{...props}
695
>
696
<path
697
-
fill="oklch(0.704 0.05 28)"
698
d="M9 22a1 1 0 0 1-1-1v-3H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-6.1l-3.7 3.71c-.2.19-.45.29-.7.29zm1-6v3.08L13.08 16H20V4H4v12z"
699
></path>
700
</svg>
···
711
{...props}
712
>
713
<path
714
-
fill="oklch(0.704 0.05 28)"
715
d="M17 17H7v-3l-4 4l4 4v-3h12v-6h-2M7 7h10v3l4-4l-4-4v3H5v6h2z"
716
></path>
717
</svg>
···
762
{...props}
763
>
764
<path
765
-
fill="oklch(0.704 0.05 28)"
766
d="m12.1 18.55l-.1.1l-.11-.1C7.14 14.24 4 11.39 4 8.5C4 6.5 5.5 5 7.5 5c1.54 0 3.04 1 3.57 2.36h1.86C13.46 6 14.96 5 16.5 5c2 0 3.5 1.5 3.5 3.5c0 2.89-3.14 5.74-7.9 10.05M16.5 3c-1.74 0-3.41.81-4.5 2.08C10.91 3.81 9.24 3 7.5 3C4.42 3 2 5.41 2 8.5c0 3.77 3.4 6.86 8.55 11.53L12 21.35l1.45-1.32C18.6 15.36 22 12.27 22 8.5C22 5.41 19.58 3 16.5 3"
767
></path>
768
</svg>
···
779
{...props}
780
>
781
<path
782
-
fill="oklch(0.704 0.05 28)"
783
d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81a3 3 0 0 0 3-3a3 3 0 0 0-3-3a3 3 0 0 0-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.15c-.05.21-.08.43-.08.66c0 1.61 1.31 2.91 2.92 2.91s2.92-1.3 2.92-2.91A2.92 2.92 0 0 0 18 16.08"
784
></path>
785
</svg>
···
796
{...props}
797
>
798
<path
799
-
fill="oklch(0.704 0.05 28)"
800
d="M16 12a2 2 0 0 1 2-2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2m-6 0a2 2 0 0 1 2-2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2m-6 0a2 2 0 0 1 2-2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2"
801
></path>
802
</svg>
···
813
{...props}
814
>
815
<path
816
-
fill="oklch(0.704 0.05 28)"
817
d="M17.9 17.39c-.26-.8-1.01-1.39-1.9-1.39h-1v-3a1 1 0 0 0-1-1H8v-2h2a1 1 0 0 0 1-1V7h2a2 2 0 0 0 2-2v-.41a7.984 7.984 0 0 1 2.9 12.8M11 19.93c-3.95-.49-7-3.85-7-7.93c0-.62.08-1.22.21-1.79L9 15v1a2 2 0 0 0 2 2m1-16A10 10 0 0 0 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2"
818
></path>
819
</svg>
···
847
{...props}
848
>
849
<path
850
-
fill="oklch(0.704 0.05 28)"
851
d="M10 9V5l-7 7l7 7v-4.1c5 0 8.5 1.6 11 5.1c-1-5-4-10-11-11"
852
></path>
853
</svg>
···
901
{...props}
902
>
903
<path
904
-
fill="oklch(0.704 0.05 28)"
905
d="M17 17H7v-3l-4 4l4 4v-3h12v-6h-2M7 7h10v3l4-4l-4-4v3H5v6h2z"
906
></path>
907
</svg>
···
918
{...props}
919
>
920
<path
921
-
fill="oklch(0.704 0.05 28)"
922
d="M6 5.75L10.25 10H7v6h6.5l2 2H7a2 2 0 0 1-2-2v-6H1.75zm12 12.5L13.75 14H17V8h-6.5l-2-2H17a2 2 0 0 1 2 2v6h3.25z"
923
></path>
924
</svg>
···
946
//import Masonry from "@mui/lab/Masonry";
947
import {
948
type $Typed,
949
AppBskyEmbedDefs,
950
AppBskyEmbedExternal,
951
AppBskyEmbedImages,
···
969
PostView,
970
//ThreadViewPost,
971
} from "@atproto/api/dist/client/types/app/bsky/feed/defs";
972
import { useEffect, useRef, useState } from "react";
973
import ReactPlayer from "react-player";
974
975
import defaultpfp from "~/../public/favicon.png";
976
import { useAuth } from "~/providers/UnifiedAuthProvider";
977
// import type { OutputSchema } from "@atproto/api/dist/client/types/app/bsky/feed/getFeed";
978
// import type {
979
// ViewRecord,
···
1081
1082
function UniversalPostRenderer({
1083
post,
1084
//setMainItem,
1085
//isMainItem,
1086
onPostClick,
···
1100
style,
1101
ref,
1102
dataIndexPropPass,
1103
}: {
1104
post: PostView;
1105
// optional for now because i havent ported every use to this yet
1106
// setMainItem?: React.Dispatch<
1107
// React.SetStateAction<AppBskyFeedDefs.FeedViewPost>
···
1120
depth?: number;
1121
repostedby?: string;
1122
style?: React.CSSProperties;
1123
-
ref?: React.Ref<HTMLDivElement>;
1124
dataIndexPropPass?: number;
1125
}) {
1126
const navigate = useNavigate();
1127
-
const [likedPosts, setLikedPosts] = useAtom(likedPostsAtom);
1128
const [hasRetweeted, setHasRetweeted] = useState<boolean>(
1129
post.viewer?.repost ? true : false
1130
);
1131
-
const [hasLiked, setHasLiked] = useState<boolean>(
1132
-
post.uri in likedPosts || post.viewer?.like ? true : false
1133
-
);
1134
const { agent } = useAuth();
1135
-
const [likeUri, setLikeUri] = useState<string | undefined>(post.viewer?.like);
1136
const [retweetUri, setRetweetUri] = useState<string | undefined>(
1137
post.viewer?.repost
1138
);
1139
-
1140
-
const likeOrUnlikePost = async () => {
1141
-
const newLikedPosts = { ...likedPosts };
1142
-
if (!agent) {
1143
-
console.error("Agent is null or undefined");
1144
-
return;
1145
-
}
1146
-
if (hasLiked) {
1147
-
if (post.uri in likedPosts) {
1148
-
const likeUri = likedPosts[post.uri];
1149
-
setLikeUri(likeUri);
1150
-
}
1151
-
if (likeUri) {
1152
-
await agent.deleteLike(likeUri);
1153
-
setHasLiked(false);
1154
-
delete newLikedPosts[post.uri];
1155
-
}
1156
-
} else {
1157
-
const { uri } = await agent.like(post.uri, post.cid);
1158
-
setLikeUri(uri);
1159
-
setHasLiked(true);
1160
-
newLikedPosts[post.uri] = uri;
1161
-
}
1162
-
setLikedPosts(newLikedPosts);
1163
-
};
1164
1165
const repostOrUnrepostPost = async () => {
1166
if (!agent) {
···
1192
1193
const emergencySalt = randomString();
1194
1195
/* fuck you */
1196
const isMainItem = false;
1197
const setMainItem = (any: any) => {};
1198
// eslint-disable-next-line react-hooks/refs
1199
-
console.log("Received ref in UniversalPostRenderer:", ref);
1200
return (
1201
<div ref={ref} style={style} data-index={dataIndexPropPass}>
1202
-
<div
1203
-
//ref={ref}
1204
-
key={salt + "-" + (post.uri || emergencySalt)}
1205
-
onClick={
1206
-
isMainItem
1207
-
? onPostClick
1208
-
: setMainItem
1209
? onPostClick
1210
-
? (e) => {
1211
-
setMainItem({ post: post });
1212
-
onPostClick(e);
1213
-
}
1214
-
: () => {
1215
-
setMainItem({ post: post });
1216
-
}
1217
-
: undefined
1218
-
}
1219
-
style={
1220
-
{
1221
-
//...style,
1222
-
//border: "1px solid #e1e8ed",
1223
-
//borderRadius: 12,
1224
-
opacity: "1 !important",
1225
-
background: "transparent",
1226
-
paddingLeft: isQuote ? 12 : 16,
1227
-
paddingRight: isQuote ? 12 : 16,
1228
-
//paddingTop: 16,
1229
-
paddingTop: isRepost ? 10 : isQuote ? 12 : 16,
1230
-
//paddingBottom: bottomReplyLine ? 0 : 16,
1231
-
paddingBottom: 0,
1232
-
fontFamily: "system-ui, sans-serif",
1233
-
//boxShadow: "0 2px 8px rgba(0,0,0,0.04)",
1234
-
position: "relative",
1235
-
// dont cursor: "pointer",
1236
-
borderBottomWidth: bottomBorder ? (isQuote ? 0 : 1) : 0,
1237
-
}}
1238
-
className="border-gray-300 dark:border-gray-600"
1239
-
>
1240
-
{isRepost && (
1241
-
<div
1242
-
style={{
1243
-
marginLeft: 36,
1244
-
display: "flex",
1245
-
borderRadius: 12,
1246
-
paddingBottom: "calc(22px - 1rem)",
1247
-
fontSize: 14,
1248
-
maxHeight: "1rem",
1249
-
justifyContent: "flex-start",
1250
-
//color: theme.textSecondary,
1251
-
gap: 4,
1252
-
alignItems: "center",
1253
-
}}
1254
-
className="text-gray-500 dark:text-gray-400"
1255
-
>
1256
-
<MdiRepost /> Reposted by @{isRepost}{" "}
1257
-
</div>
1258
-
)}
1259
-
{!isQuote && (
1260
-
<div
1261
-
style={{
1262
-
opacity:
1263
-
topReplyLine || isReply /*&& (true || expanded)*/ ? 0.5 : 0,
1264
-
position: "absolute",
1265
-
top: 0,
1266
-
left: 36, // why 36 ???
1267
-
//left: 16 + (42 / 2),
1268
-
width: 2,
1269
-
//height: "100%",
1270
-
height: isRepost ? "calc(16px + 1rem - 6px)" : 16 - 6,
1271
-
// background: theme.textSecondary,
1272
-
//opacity: 0.5,
1273
-
// no flex here
1274
-
}}
1275
-
className="bg-gray-500 dark:bg-gray-400"
1276
-
/>
1277
-
)}
1278
-
<div
1279
style={{
1280
-
position: "absolute",
1281
-
//top: isRepost ? "calc(16px + 1rem)" : 16,
1282
-
//left: 16,
1283
-
zIndex: 1,
1284
-
top: isRepost ? "calc(16px + 1rem)" : isQuote ? 12 : 16,
1285
-
left: isQuote ? 12 : 16,
1286
}}
1287
-
onClick={onProfileClick}
1288
>
1289
-
<img
1290
-
src={post.author.avatar || defaultpfp}
1291
-
alt="avatar"
1292
-
// transition={{
1293
-
// type: "spring",
1294
-
// stiffness: 260,
1295
-
// damping: 20,
1296
-
// }}
1297
-
style={{
1298
-
borderRadius: "50%",
1299
-
marginRight: 12,
1300
-
objectFit: "cover",
1301
-
//background: theme.border,
1302
-
//border: `1px solid ${theme.border}`,
1303
-
width: isQuote ? 16 : 42,
1304
-
height: isQuote ? 16 : 42,
1305
-
}}
1306
-
className="border border-gray-300 dark:border-gray-600 bg-gray-300 dark:bg-gray-600"
1307
-
/>
1308
-
</div>
1309
-
<div style={{ display: "flex", alignItems: "flex-start", zIndex: 2 }}>
1310
-
<div
1311
-
style={{
1312
-
display: "flex",
1313
-
flexDirection: "column",
1314
-
alignSelf: "stretch",
1315
-
alignItems: "center",
1316
-
overflow: "hidden",
1317
-
width: expanded || isQuote ? 0 : "auto",
1318
-
marginRight: expanded || isQuote ? 0 : 12,
1319
-
}}
1320
-
>
1321
-
{/* dummy for later use */}
1322
-
<div style={{ width: 42, height: 42 + 8, minHeight: 42 + 8 }} />
1323
-
{/* reply line !!!! bottomReplyLine */}
1324
-
{bottomReplyLine && (
1325
<div
1326
style={{
1327
-
width: 2,
1328
-
height: "100%",
1329
-
//background: theme.textSecondary,
1330
-
opacity: 0.5,
1331
-
// no flex here
1332
-
//color: "Red",
1333
-
//zIndex: 99
1334
}}
1335
-
className="bg-gray-500 dark:bg-gray-400"
1336
-
/>
1337
-
)}
1338
-
{/* <div
1339
layout
1340
transition={{ duration: 0.2 }}
1341
animate={{ height: expanded ? 0 : '100%' }}
···
1345
// no flex here
1346
}}
1347
/> */}
1348
-
</div>
1349
-
<div style={{ flex: 1, maxWidth: "100%" }}>
1350
-
<div
1351
-
style={{
1352
-
display: "flex",
1353
-
flexDirection: "row",
1354
-
alignItems: "center",
1355
-
flexWrap: "nowrap",
1356
-
maxWidth: `calc(100% - ${!expanded ? (isQuote ? 26 : 0) : 54}px)`,
1357
-
width: `calc(100% - ${!expanded ? (isQuote ? 26 : 0) : 54}px)`,
1358
-
marginLeft: !expanded ? (isQuote ? 26 : 0) : 54,
1359
-
marginBottom: !expanded ? 4 : 6,
1360
-
}}
1361
-
>
1362
<div
1363
style={{
1364
display: "flex",
1365
-
//overflow: "hidden", // hey why is overflow hidden unapplied
1366
-
overflow: "hidden",
1367
-
textOverflow: "ellipsis",
1368
-
flexShrink: 1,
1369
-
flexGrow: 1,
1370
-
flexBasis: 0,
1371
-
width: 0,
1372
-
gap: expanded ? 0 : 6,
1373
-
alignItems: expanded ? "flex-start" : "center",
1374
-
flexDirection: expanded ? "column" : "row",
1375
-
height: expanded ? 42 : "1rem",
1376
}}
1377
>
1378
-
<span
1379
style={{
1380
display: "flex",
1381
-
fontWeight: 700,
1382
-
fontSize: 16,
1383
overflow: "hidden",
1384
textOverflow: "ellipsis",
1385
-
whiteSpace: "nowrap",
1386
flexShrink: 1,
1387
-
minWidth: 0,
1388
-
gap: 4,
1389
-
alignItems: "center",
1390
-
//color: theme.text,
1391
}}
1392
-
className="text-gray-900 dark:text-gray-100"
1393
>
1394
-
{/* verified checkmark */}
1395
-
{post.author.displayName || post.author.handle}{" "}
1396
-
{post.author.verification?.verifiedStatus == "valid" && (
1397
-
<MdiVerified />
1398
-
)}
1399
-
</span>
1400
1401
-
<span
1402
style={{
1403
-
//color: theme.textSecondary,
1404
-
fontSize: 16,
1405
-
overflowX: "hidden",
1406
-
textOverflow: "ellipsis",
1407
-
whiteSpace: "nowrap",
1408
-
flexShrink: 1,
1409
-
flexGrow: 0,
1410
-
minWidth: 0,
1411
}}
1412
-
className="text-gray-500 dark:text-gray-400"
1413
>
1414
-
@{post.author.handle}
1415
-
</span>
1416
</div>
1417
-
<div
1418
-
style={{
1419
-
display: "flex",
1420
-
alignItems: "center",
1421
-
height: "1rem",
1422
-
}}
1423
-
>
1424
-
<span
1425
style={{
1426
//color: theme.textSecondary,
1427
-
fontSize: 16,
1428
-
marginLeft: 8,
1429
-
whiteSpace: "nowrap",
1430
-
flexShrink: 0,
1431
-
maxWidth: "100%",
1432
}}
1433
className="text-gray-500 dark:text-gray-400"
1434
>
1435
-
ยท {/* time placeholder */}
1436
-
{shortTimeAgo(post.indexedAt)}
1437
-
</span>
1438
-
</div>
1439
-
</div>
1440
-
{/* reply indicator */}
1441
-
{!!feedviewpostreplyhandle && (
1442
<div
1443
style={{
1444
-
display: "flex",
1445
-
borderRadius: 12,
1446
-
paddingBottom: 2,
1447
-
fontSize: 14,
1448
-
justifyContent: "flex-start",
1449
-
//color: theme.textSecondary,
1450
-
gap: 4,
1451
-
alignItems: "center",
1452
-
//marginLeft: 36,
1453
-
height:
1454
-
!(expanded || isQuote) && !!feedviewpostreplyhandle
1455
-
? "1rem"
1456
-
: 0,
1457
-
opacity:
1458
-
!(expanded || isQuote) && !!feedviewpostreplyhandle ? 1 : 0,
1459
}}
1460
-
className="text-gray-500 dark:text-gray-400"
1461
>
1462
-
<MdiReply /> Reply to @{feedviewpostreplyhandle}
1463
</div>
1464
-
)}
1465
-
<div
1466
-
style={{
1467
-
fontSize: 16,
1468
-
marginBottom: !post.embed /*|| depth > 0*/ ? 0 : 8,
1469
-
whiteSpace: "pre-wrap",
1470
-
textAlign: "left",
1471
-
overflowWrap: "anywhere",
1472
-
wordBreak: "break-word",
1473
-
//color: theme.text,
1474
-
}}
1475
-
className="text-gray-900 dark:text-gray-100"
1476
-
>
1477
-
{renderTextWithFacets({
1478
-
text: (post.record as { text?: string }).text ?? "",
1479
-
facets: (post.record.facets as Facet[]) ?? [],
1480
-
navigate: navigate,
1481
-
})}
1482
-
{}
1483
-
</div>
1484
-
{post.embed && depth < 1 ? (
1485
-
<PostEmbeds
1486
-
embed={post.embed}
1487
-
//moderation={moderation}
1488
-
viewContext={PostEmbedViewContext.Feed}
1489
-
salt={salt}
1490
-
navigate={navigate}
1491
-
/>
1492
-
) : null}
1493
-
{post.embed && depth > 0 && (
1494
-
/* pretty bad hack imo. its trying to sync up with how the embed shim doesnt
1495
hydrate embeds this deep but the connection here is implicit
1496
todo: idk make this a real part of the embed shim so its not implicit */
1497
-
<>
1498
-
<div className="border-gray-300 dark:border-gray-600 p-3 rounded-xl border italic text-gray-400 text-[14px]">
1499
-
(there is an embed here thats too deep to render)
1500
-
</div>
1501
-
</>
1502
-
)}
1503
-
<div style={{ paddingTop: post.embed && depth < 1 ? 4 : 0 }}>
1504
-
<>
1505
-
{expanded && (
1506
<div
1507
style={{
1508
-
overflow: "hidden",
1509
-
//color: theme.textSecondary,
1510
-
fontSize: 14,
1511
display: "flex",
1512
-
borderBottomStyle: "solid",
1513
-
//borderBottomColor: theme.border,
1514
-
//background: "#f00",
1515
-
// height: "1rem",
1516
-
paddingTop: 4,
1517
-
paddingBottom: 8,
1518
-
borderBottomWidth: 1,
1519
-
marginBottom: 8,
1520
-
}} // important for height animation
1521
-
className="text-gray-500 dark:text-gray-400 border-gray-200 dark:border-gray-700"
1522
-
>
1523
-
{fullDateTimeFormat(post.indexedAt)}
1524
-
</div>
1525
-
)}
1526
-
</>
1527
-
{!isQuote && (
1528
-
<div
1529
-
style={{
1530
-
display: "flex",
1531
-
gap: 32,
1532
-
paddingTop: 8,
1533
-
//color: theme.textSecondary,
1534
-
fontSize: 15,
1535
-
justifyContent: "space-between",
1536
-
//background: "#0f0",
1537
-
}}
1538
-
className="text-gray-500 dark:text-gray-400"
1539
-
>
1540
-
<span style={btnstyle}>
1541
-
<MdiCommentOutline />
1542
-
{post.replyCount}
1543
-
</span>
1544
-
<HitSlopButton
1545
-
onClick={() => {
1546
-
repostOrUnrepostPost();
1547
}}
1548
-
style={{
1549
-
...btnstyle,
1550
-
...(hasRetweeted ? { color: "#5CEFAA" } : {}),
1551
-
}}
1552
-
>
1553
-
{hasRetweeted ? <MdiRepeatGreen /> : <MdiRepeat />}
1554
-
{(post.repostCount || 0) + (hasRetweeted ? 1 : 0)}
1555
-
</HitSlopButton>
1556
-
<HitSlopButton
1557
-
onClick={() => {
1558
-
likeOrUnlikePost();
1559
-
}}
1560
-
style={{
1561
-
...btnstyle,
1562
-
...(hasLiked ? { color: "#EC4899" } : {}),
1563
-
}}
1564
>
1565
-
{hasLiked ? <MdiCardsHeart /> : <MdiCardsHeartOutline />}
1566
-
{(post.likeCount || 0) + (hasLiked ? 1 : 0)}
1567
-
</HitSlopButton>
1568
-
<div style={{ display: "flex", gap: 8 }}>
1569
<HitSlopButton
1570
-
onClick={async (e) => {
1571
-
e.stopPropagation();
1572
-
try {
1573
-
await navigator.clipboard.writeText(
1574
-
"https://bsky.app" +
1575
-
"/profile/" +
1576
-
post.author.handle +
1577
-
"/post/" +
1578
-
post.uri.split("/").pop()
1579
-
);
1580
-
} catch (_e) {
1581
-
// idk
1582
-
}
1583
}}
1584
style={{
1585
...btnstyle,
1586
}}
1587
>
1588
-
<MdiShareVariant />
1589
</HitSlopButton>
1590
-
<span style={btnstyle}>
1591
-
<MdiMoreHoriz />
1592
-
</span>
1593
</div>
1594
-
</div>
1595
-
)}
1596
</div>
1597
-
<div
1598
-
style={{
1599
-
//height: bottomReplyLine ? 16 : 0
1600
-
height: isQuote ? 12 : 16,
1601
-
}}
1602
-
/>
1603
</div>
1604
</div>
1605
-
</div>
1606
</div>
1607
);
1608
}
···
1692
viewContext,
1693
salt,
1694
navigate,
1695
}: {
1696
embed?: Embed;
1697
moderation?: ModerationDecision;
···
1700
viewContext?: PostEmbedViewContext;
1701
salt: string;
1702
navigate: (_: any) => void;
1703
}) {
1704
-
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
1705
if (
1706
AppBskyEmbedRecordWithMedia.isView(embed) &&
1707
AppBskyEmbedRecord.isViewRecord(embed.record.record) &&
···
1735
viewContext={viewContext}
1736
salt={salt}
1737
navigate={navigate}
1738
/>
1739
{/* padding empty div of 8px height */}
1740
<div style={{ height: 12 }} />
···
1748
//boxShadow: theme.cardShadow,
1749
overflow: "hidden",
1750
}}
1751
-
className="shadow border border-gray-200 dark:border-gray-700"
1752
>
1753
<UniversalPostRenderer
1754
post={post}
···
1796
}
1797
1798
if (AppBskyEmbedRecord.isView(embed)) {
1799
// custom feed embed (i.e. generator view)
1800
if (AppBskyFeedDefs.isGeneratorView(embed.record)) {
1801
// stopgap sorry
···
1805
// <MaybeFeedCard view={embed.record} />
1806
// </div>
1807
// )
1808
}
1809
1810
// list embed
···
1816
// <MaybeListCard view={embed.record} />
1817
// </div>
1818
// )
1819
}
1820
1821
// starter pack embed
···
1827
// <StarterPackCard starterPack={embed.record} />
1828
// </div>
1829
// )
1830
}
1831
1832
// quote post
···
1865
//boxShadow: theme.cardShadow,
1866
overflow: "hidden",
1867
}}
1868
-
className="shadow border border-gray-200 dark:border-gray-700"
1869
>
1870
<UniversalPostRenderer
1871
post={post}
···
1886
</div>
1887
);
1888
} else {
1889
return <>sorry</>;
1890
}
1891
//return <QuotePostRenderer record={embed.record} moderation={moderation} />;
···
1909
src: img.fullsize,
1910
alt: img.alt,
1911
}));
1912
1913
if (images.length > 0) {
1914
// const items = embed.images.map(img => ({
···
1938
//border: `1px solid ${theme.border}`,
1939
overflow: "hidden",
1940
}}
1941
-
className="border border-gray-200 dark:border-gray-700 bg-gray-200 dark:bg-gray-900"
1942
>
1943
-
{lightboxIndex !== null && (
1944
<Lightbox
1945
images={lightboxImages}
1946
index={lightboxIndex}
1947
onClose={() => setLightboxIndex(null)}
1948
onNavigate={(newIndex) => setLightboxIndex(newIndex)}
1949
/>
1950
-
)}
1951
<img
1952
src={image.fullsize}
1953
alt={image.alt}
···
1978
overflow: "hidden",
1979
//border: `1px solid ${theme.border}`,
1980
}}
1981
-
className="border border-gray-200 dark:border-gray-700"
1982
>
1983
-
{lightboxIndex !== null && (
1984
<Lightbox
1985
images={lightboxImages}
1986
index={lightboxIndex}
1987
onClose={() => setLightboxIndex(null)}
1988
onNavigate={(newIndex) => setLightboxIndex(newIndex)}
1989
/>
1990
-
)}
1991
{images.map((img, i) => (
1992
<div
1993
key={i}
···
2027
//border: `1px solid ${theme.border}`,
2028
// height: 240, // fixed height for cropping
2029
}}
2030
-
className="border border-gray-200 dark:border-gray-700"
2031
>
2032
-
{lightboxIndex !== null && (
2033
<Lightbox
2034
images={lightboxImages}
2035
index={lightboxIndex}
2036
onClose={() => setLightboxIndex(null)}
2037
onNavigate={(newIndex) => setLightboxIndex(newIndex)}
2038
/>
2039
-
)}
2040
{/* Left: 1:1 */}
2041
<div
2042
style={{ flex: 1, aspectRatio: "1 / 1", position: "relative" }}
···
2111
//border: `1px solid ${theme.border}`,
2112
//aspectRatio: "3 / 2", // overall grid aspect
2113
}}
2114
-
className="border border-gray-200 dark:border-gray-700"
2115
>
2116
-
{lightboxIndex !== null && (
2117
<Lightbox
2118
images={lightboxImages}
2119
index={lightboxIndex}
2120
onClose={() => setLightboxIndex(null)}
2121
onNavigate={(newIndex) => setLightboxIndex(newIndex)}
2122
/>
2123
-
)}
2124
{images.map((img, i) => (
2125
<div
2126
key={i}
···
2184
// =
2185
if (AppBskyEmbedVideo.isView(embed)) {
2186
// hls playlist
2187
const playlist = embed.playlist;
2188
return (
2189
<SmartHLSPlayer
···
2211
return <div />;
2212
}
2213
2214
-
import { createPortal } from "react-dom";
2215
-
type LightboxProps = {
2216
-
images: { src: string; alt?: string }[];
2217
-
index: number;
2218
-
onClose: () => void;
2219
-
onNavigate?: (newIndex: number) => void;
2220
-
};
2221
-
export function Lightbox({
2222
-
images,
2223
-
index,
2224
-
onClose,
2225
-
onNavigate,
2226
-
}: LightboxProps) {
2227
-
const image = images[index];
2228
-
2229
-
useEffect(() => {
2230
-
function handleKey(e: KeyboardEvent) {
2231
-
if (e.key === "Escape") onClose();
2232
-
if (e.key === "ArrowRight" && onNavigate)
2233
-
onNavigate((index + 1) % images.length);
2234
-
if (e.key === "ArrowLeft" && onNavigate)
2235
-
onNavigate((index - 1 + images.length) % images.length);
2236
-
}
2237
-
window.addEventListener("keydown", handleKey);
2238
-
return () => window.removeEventListener("keydown", handleKey);
2239
-
}, [index, images.length, onClose, onNavigate]);
2240
-
2241
-
return createPortal(
2242
-
<div
2243
-
className="fixed inset-0 z-50 flex items-center justify-center bg-black/80"
2244
-
onClick={(e) => {
2245
-
e.stopPropagation();
2246
-
onClose();
2247
-
}}
2248
-
>
2249
-
<img
2250
-
src={image.src}
2251
-
alt={image.alt}
2252
-
className="max-h-[90vh] max-w-[90vw] object-contain rounded-lg shadow-lg"
2253
-
onClick={(e) => e.stopPropagation()}
2254
-
/>
2255
-
2256
-
{images.length > 1 && (
2257
-
<>
2258
-
<button
2259
-
onClick={(e) => {
2260
-
e.stopPropagation();
2261
-
onNavigate?.((index - 1 + images.length) % images.length);
2262
-
}}
2263
-
className="absolute left-4 top-1/2 -translate-y-1/2 text-white text-4xl h-8 w-8 rounded-full bg-gray-900 flex items-center justify-center"
2264
-
>
2265
-
<svg
2266
-
xmlns="http://www.w3.org/2000/svg"
2267
-
width={28}
2268
-
height={28}
2269
-
viewBox="0 0 24 24"
2270
-
>
2271
-
<g fill="none" fillRule="evenodd">
2272
-
<path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"></path>
2273
-
<path
2274
-
fill="currentColor"
2275
-
d="M8.293 12.707a1 1 0 0 1 0-1.414l5.657-5.657a1 1 0 1 1 1.414 1.414L10.414 12l4.95 4.95a1 1 0 0 1-1.414 1.414z"
2276
-
></path>
2277
-
</g>
2278
-
</svg>
2279
-
</button>
2280
-
<button
2281
-
onClick={(e) => {
2282
-
e.stopPropagation();
2283
-
onNavigate?.((index + 1) % images.length);
2284
-
}}
2285
-
className="absolute right-4 top-1/2 -translate-y-1/2 text-white text-4xl h-8 w-8 rounded-full bg-gray-900 flex items-center justify-center"
2286
-
>
2287
-
<svg
2288
-
xmlns="http://www.w3.org/2000/svg"
2289
-
width={28}
2290
-
height={28}
2291
-
viewBox="0 0 24 24"
2292
-
>
2293
-
<g fill="none" fillRule="evenodd">
2294
-
<path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"></path>
2295
-
<path
2296
-
fill="currentColor"
2297
-
d="M15.707 11.293a1 1 0 0 1 0 1.414l-5.657 5.657a1 1 0 1 1-1.414-1.414l4.95-4.95l-4.95-4.95a1 1 0 0 1 1.414-1.414z"
2298
-
></path>
2299
-
</g>
2300
-
</svg>
2301
-
</button>
2302
-
</>
2303
-
)}
2304
-
</div>,
2305
-
document.body
2306
-
);
2307
-
}
2308
-
2309
function getDomain(url: string) {
2310
try {
2311
const { hostname } = new URL(url);
···
2370
return { start, end, feature: f.features[0] };
2371
});
2372
}
2373
-
function renderTextWithFacets({
2374
text,
2375
facets,
2376
navigate,
···
2402
className="link"
2403
style={{
2404
textDecoration: "none",
2405
-
color: "rgb(29, 122, 242)",
2406
wordBreak: "break-all",
2407
}}
2408
target="_blank"
···
2422
result.push(
2423
<span
2424
key={start}
2425
-
style={{ color: "rgb(29, 122, 242)" }}
2426
className=" cursor-pointer"
2427
onClick={(e) => {
2428
e.stopPropagation();
···
2440
result.push(
2441
<span
2442
key={start}
2443
-
style={{ color: "rgb(29, 122, 242)" }}
2444
onClick={(e) => {
2445
e.stopPropagation();
2446
}}
···
2533
>
2534
<div
2535
style={containerStyle as React.CSSProperties}
2536
-
className="border border-gray-200 dark:border-gray-700"
2537
>
2538
{thumb && (
2539
<div
···
2547
marginBottom: 8,
2548
//borderBottom: `1px solid ${theme.border}`,
2549
}}
2550
-
className="border-b border-gray-200 dark:border-gray-700"
2551
>
2552
<img
2553
src={thumb}
···
2673
borderRadius: 12,
2674
//border: `1px solid ${theme.border}`,
2675
}}
2676
-
className="border border-gray-200 dark:border-gray-700"
2677
onClick={async (e) => {
2678
e.stopPropagation();
2679
setPlaying(true);
···
2714
100 / (aspect ? aspect.width / aspect.height : 16 / 9)
2715
}%`, // 16:9 = 56.25%, 4:3 = 75%
2716
}}
2717
-
className="border border-gray-200 dark:border-gray-700"
2718
>
2719
<ReactPlayer
2720
src={url}
···
1
+
import * as ATPAPI from "@atproto/api";
2
import { useNavigate } from "@tanstack/react-router";
3
+
import DOMPurify from "dompurify";
4
import { useAtom } from "jotai";
5
+
import { DropdownMenu } from "radix-ui";
6
+
import { HoverCard } from "radix-ui";
7
import * as React from "react";
8
import { type SVGProps } from "react";
9
10
+
import {
11
+
composerAtom,
12
+
constellationURLAtom,
13
+
enableBridgyTextAtom,
14
+
enableWafrnTextAtom,
15
+
imgCDNAtom,
16
+
} from "~/utils/atoms";
17
import { useHydratedEmbed } from "~/utils/useHydrated";
18
import {
19
useQueryConstellation,
20
useQueryIdentity,
21
useQueryPost,
22
useQueryProfile,
23
+
yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks,
24
} from "~/utils/useQuery";
25
26
function asTyped<T extends { $type: string }>(obj: T): $Typed<T> {
···
40
feedviewpost?: boolean;
41
repostedby?: string;
42
style?: React.CSSProperties;
43
+
ref?: React.RefObject<HTMLDivElement>;
44
dataIndexPropPass?: number;
45
+
nopics?: boolean;
46
+
concise?: boolean;
47
+
lightboxCallback?: (d: LightboxProps) => void;
48
+
maxReplies?: number;
49
+
isQuote?: boolean;
50
+
filterNoReplies?: boolean;
51
+
filterMustHaveMedia?: boolean;
52
+
filterMustBeReply?: boolean;
53
}
54
55
// export async function cachedGetRecord({
···
157
style,
158
ref,
159
dataIndexPropPass,
160
+
nopics,
161
+
concise,
162
+
lightboxCallback,
163
+
maxReplies,
164
+
isQuote,
165
+
filterNoReplies,
166
+
filterMustHaveMedia,
167
+
filterMustBeReply,
168
}: UniversalPostRendererATURILoaderProps) {
169
+
// todo remove this once tree rendering is implemented, use a prop like isTree
170
+
const TEMPLINEAR = true;
171
// /*mass comment*/ console.log("atUri", atUri);
172
//const { get, set } = usePersistentStore();
173
//const [record, setRecord] = React.useState<any>(null);
···
412
);
413
}, [links]);
414
415
+
// const { data: repliesData } = useQueryConstellation({
416
+
// method: "/links",
417
+
// target: atUri,
418
+
// collection: "app.bsky.feed.post",
419
+
// path: ".reply.parent.uri",
420
+
// });
421
+
422
+
const [constellationurl] = useAtom(constellationURLAtom);
423
+
424
+
const infinitequeryresults = useInfiniteQuery({
425
+
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
426
+
{
427
+
constellation: constellationurl,
428
+
method: "/links",
429
+
target: atUri,
430
+
collection: "app.bsky.feed.post",
431
+
path: ".reply.parent.uri",
432
+
}
433
+
),
434
+
enabled: !!atUri && !!maxReplies && !isQuote,
435
+
});
436
+
437
+
const {
438
+
data: repliesData,
439
+
// fetchNextPage,
440
+
// hasNextPage,
441
+
// isFetchingNextPage,
442
+
} = infinitequeryresults;
443
+
444
+
// auto-fetch all pages
445
+
useEffect(() => {
446
+
if (!maxReplies || isQuote || TEMPLINEAR) return;
447
+
if (
448
+
infinitequeryresults.hasNextPage &&
449
+
!infinitequeryresults.isFetchingNextPage
450
+
) {
451
+
console.log("Fetching the next page...");
452
+
infinitequeryresults.fetchNextPage();
453
+
}
454
+
}, [TEMPLINEAR, infinitequeryresults, isQuote, maxReplies]);
455
+
456
+
const replyAturis = repliesData
457
+
? repliesData.pages.flatMap((page) =>
458
+
page
459
+
? page.linking_records.map((record) => {
460
+
const aturi = `at://${record.did}/${record.collection}/${record.rkey}`;
461
+
return aturi;
462
+
})
463
+
: []
464
+
)
465
+
: [];
466
+
467
+
//const [oldestOpsReply, setOldestOpsReply] = useState<string | undefined>(undefined);
468
+
469
+
const { oldestOpsReply, oldestOpsReplyElseNewestNonOpsReply } = (() => {
470
+
if (isQuote || !replyAturis || replyAturis.length === 0 || !maxReplies)
471
+
return {
472
+
oldestOpsReply: undefined,
473
+
oldestOpsReplyElseNewestNonOpsReply: undefined,
474
+
};
475
+
476
+
const opdid = new AtUri(
477
+
//postQuery?.value.reply?.root.uri ?? postQuery?.uri ?? atUri
478
+
atUri
479
+
).host;
480
+
481
+
const opReplies = replyAturis.filter(
482
+
(aturi) => new AtUri(aturi).host === opdid
483
+
);
484
+
485
+
if (opReplies.length > 0) {
486
+
const opreply = opReplies[opReplies.length - 1];
487
+
//setOldestOpsReply(opreply);
488
+
return {
489
+
oldestOpsReply: opreply,
490
+
oldestOpsReplyElseNewestNonOpsReply: opreply,
491
+
};
492
+
} else {
493
+
return {
494
+
oldestOpsReply: undefined,
495
+
oldestOpsReplyElseNewestNonOpsReply: replyAturis[0],
496
+
};
497
+
}
498
+
})();
499
+
500
// const navigateToProfile = (e: React.MouseEvent) => {
501
// e.stopPropagation();
502
// if (resolved?.did) {
···
512
}
513
514
return (
515
+
<>
516
+
{/* <span>uprrs {maxReplies} {!!maxReplies&&!!oldestOpsReplyElseNewestNonOpsReply ? "true" : "false"}</span> */}
517
+
<UniversalPostRendererRawRecordShim
518
+
detailed={detailed}
519
+
postRecord={postQuery}
520
+
profileRecord={opProfile}
521
+
aturi={atUri}
522
+
resolved={resolved}
523
+
likesCount={likes}
524
+
repostsCount={reposts}
525
+
repliesCount={replies}
526
+
bottomReplyLine={
527
+
maxReplies && oldestOpsReplyElseNewestNonOpsReply
528
+
? true
529
+
: maxReplies && !oldestOpsReplyElseNewestNonOpsReply
530
+
? false
531
+
: maxReplies === 0 && (!replies || (!!replies && replies === 0))
532
+
? false
533
+
: bottomReplyLine
534
+
}
535
+
topReplyLine={topReplyLine}
536
+
//bottomBorder={maxReplies&&oldestOpsReplyElseNewestNonOpsReply ? false : bottomBorder}
537
+
bottomBorder={
538
+
maxReplies && oldestOpsReplyElseNewestNonOpsReply
539
+
? false
540
+
: maxReplies === 0
541
+
? false
542
+
: bottomBorder
543
+
}
544
+
feedviewpost={feedviewpost}
545
+
repostedby={repostedby}
546
+
//style={{...style, background: oldestOpsReply === atUri ? "Red" : undefined}}
547
+
style={style}
548
+
ref={ref}
549
+
dataIndexPropPass={dataIndexPropPass}
550
+
nopics={nopics}
551
+
concise={concise}
552
+
lightboxCallback={lightboxCallback}
553
+
maxReplies={maxReplies}
554
+
isQuote={isQuote}
555
+
filterNoReplies={filterNoReplies}
556
+
filterMustHaveMedia={filterMustHaveMedia}
557
+
filterMustBeReply={filterMustBeReply}
558
+
/>
559
+
<>
560
+
{maxReplies && maxReplies === 0 && replies && replies > 0 ? (
561
+
<>
562
+
{/* <div>hello</div> */}
563
+
<MoreReplies atUri={atUri} />
564
+
</>
565
+
) : (
566
+
<></>
567
+
)}
568
+
</>
569
+
{!isQuote && oldestOpsReplyElseNewestNonOpsReply && (
570
+
<>
571
+
{/* <span>hello {maxReplies}</span> */}
572
+
<UniversalPostRendererATURILoader
573
+
//detailed={detailed}
574
+
atUri={oldestOpsReplyElseNewestNonOpsReply}
575
+
bottomReplyLine={(maxReplies ?? 0) > 0}
576
+
topReplyLine={
577
+
(!!(maxReplies && maxReplies - 1 === 0) &&
578
+
!!(replies && replies > 0)) ||
579
+
!!((maxReplies ?? 0) > 1)
580
+
}
581
+
bottomBorder={bottomBorder}
582
+
feedviewpost={feedviewpost}
583
+
repostedby={repostedby}
584
+
style={style}
585
+
ref={ref}
586
+
dataIndexPropPass={dataIndexPropPass}
587
+
nopics={nopics}
588
+
concise={concise}
589
+
lightboxCallback={lightboxCallback}
590
+
maxReplies={
591
+
maxReplies && maxReplies > 0 ? maxReplies - 1 : undefined
592
+
}
593
+
/>
594
+
</>
595
+
)}
596
+
</>
597
);
598
}
599
600
+
function MoreReplies({ atUri }: { atUri: string }) {
601
+
const navigate = useNavigate();
602
+
const aturio = new AtUri(atUri);
603
+
return (
604
+
<div
605
+
onClick={() =>
606
+
navigate({
607
+
to: "/profile/$did/post/$rkey",
608
+
params: { did: aturio.host, rkey: aturio.rkey },
609
+
})
610
+
}
611
+
className="border-b border-gray-300 dark:border-gray-800 flex flex-row px-4 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-900 transition-colors"
612
+
>
613
+
<div className="w-[42px] h-12 flex flex-col items-center justify-center">
614
+
<div
615
+
style={{
616
+
width: 2,
617
+
height: "100%",
618
+
backgroundImage:
619
+
"repeating-linear-gradient(to bottom, var(--color-gray-500) 0, var(--color-gray-500) 4px, transparent 4px, transparent 8px)",
620
+
opacity: 0.5,
621
+
}}
622
+
className="dark:bg-[repeating-linear-gradient(to_bottom,var(--color-gray-500)_0,var(--color-gray-400)_4px,transparent_4px,transparent_8px)]"
623
+
//className="border-gray-400 dark:border-gray-500"
624
+
/>
625
+
</div>
626
+
627
+
<div className="flex items-center pl-3 text-sm text-gray-500 dark:text-gray-400 select-none">
628
+
More Replies
629
+
</div>
630
+
</div>
631
+
);
632
+
}
633
+
634
+
function getAvatarUrl(opProfile: any, did: string, cdn: string) {
635
const link = opProfile?.value?.avatar?.ref?.["$link"];
636
if (!link) return null;
637
+
return `https://${cdn}/img/avatar/plain/${did}/${link}@jpeg`;
638
}
639
640
export function UniversalPostRendererRawRecordShim({
···
654
style,
655
ref,
656
dataIndexPropPass,
657
+
nopics,
658
+
concise,
659
+
lightboxCallback,
660
+
maxReplies,
661
+
isQuote,
662
+
filterNoReplies,
663
+
filterMustHaveMedia,
664
+
filterMustBeReply,
665
}: {
666
postRecord: any;
667
profileRecord: any;
···
677
feedviewpost?: boolean;
678
repostedby?: string;
679
style?: React.CSSProperties;
680
+
ref?: React.RefObject<HTMLDivElement>;
681
dataIndexPropPass?: number;
682
+
nopics?: boolean;
683
+
concise?: boolean;
684
+
lightboxCallback?: (d: LightboxProps) => void;
685
+
maxReplies?: number;
686
+
isQuote?: boolean;
687
+
filterNoReplies?: boolean;
688
+
filterMustHaveMedia?: boolean;
689
+
filterMustBeReply?: boolean;
690
}) {
691
// /*mass comment*/ console.log(`received aturi: ${aturi} of post content: ${postRecord}`);
692
const navigate = useNavigate();
···
757
// run();
758
// }, [postRecord, resolved?.did]);
759
760
+
const hasEmbed = (postRecord?.value as ATPAPI.AppBskyFeedPost.Record)?.embed;
761
+
const hasImages = hasEmbed?.$type === "app.bsky.embed.images";
762
+
const hasVideo = hasEmbed?.$type === "app.bsky.embed.video";
763
+
const isquotewithmedia = hasEmbed?.$type === "app.bsky.embed.recordWithMedia";
764
+
const isQuotewithImages =
765
+
isquotewithmedia &&
766
+
(hasEmbed as ATPAPI.AppBskyEmbedRecordWithMedia.Main)?.media?.$type ===
767
+
"app.bsky.embed.images";
768
+
const isQuotewithVideo =
769
+
isquotewithmedia &&
770
+
(hasEmbed as ATPAPI.AppBskyEmbedRecordWithMedia.Main)?.media?.$type ===
771
+
"app.bsky.embed.video";
772
+
773
+
const hasMedia =
774
+
hasEmbed &&
775
+
(hasImages || hasVideo || isQuotewithImages || isQuotewithVideo);
776
+
777
const {
778
data: hydratedEmbed,
779
isLoading: isEmbedLoading,
780
error: embedError,
781
} = useHydratedEmbed(postRecord?.value?.embed, resolved?.did);
782
783
+
const [imgcdn] = useAtom(imgCDNAtom);
784
+
785
const parsedaturi = new AtUri(aturi); //parseAtUri(aturi);
786
787
+
const fakeprofileviewbasic = React.useMemo<AppBskyActorDefs.ProfileViewBasic>(
788
+
() => ({
789
+
did: resolved?.did || "",
790
+
handle: resolved?.handle || "",
791
+
displayName: profileRecord?.value?.displayName || "",
792
+
avatar: getAvatarUrl(profileRecord, resolved?.did, imgcdn) || "",
793
+
viewer: undefined,
794
+
labels: profileRecord?.labels || undefined,
795
+
verification: undefined,
796
+
}),
797
+
[imgcdn, profileRecord, resolved?.did, resolved?.handle]
798
+
);
799
+
800
+
const fakeprofileviewdetailed =
801
+
React.useMemo<AppBskyActorDefs.ProfileViewDetailed>(
802
+
() => ({
803
+
...fakeprofileviewbasic,
804
+
$type: "app.bsky.actor.defs#profileViewDetailed",
805
+
description: profileRecord?.value?.description || undefined,
806
+
}),
807
+
[fakeprofileviewbasic, profileRecord?.value?.description]
808
+
);
809
+
810
const fakepost = React.useMemo<AppBskyFeedDefs.PostView>(
811
() => ({
812
$type: "app.bsky.feed.defs#postView",
813
uri: aturi,
814
cid: postRecord?.cid || "",
815
+
author: fakeprofileviewbasic,
816
record: postRecord?.value || {},
817
embed: hydratedEmbed ?? undefined,
818
replyCount: repliesCount ?? 0,
···
829
postRecord?.cid,
830
postRecord?.value,
831
postRecord?.labels,
832
+
fakeprofileviewbasic,
833
hydratedEmbed,
834
repliesCount,
835
repostsCount,
···
868
// }, [fakepost, get, set]);
869
const thereply = (fakepost?.record as AppBskyFeedPost.Record)?.reply?.parent
870
?.uri;
871
+
const feedviewpostreplydid =
872
+
thereply && !filterNoReplies ? new AtUri(thereply).host : undefined;
873
const replyhookvalue = useQueryIdentity(
874
feedviewpost ? feedviewpostreplydid : undefined
875
);
···
880
repostedby ? aturirepostbydid : undefined
881
);
882
const feedviewpostrepostedbyhandle = repostedbyhookvalue?.data?.handle;
883
+
884
+
if (filterNoReplies && thereply) return null;
885
+
886
+
if (filterMustHaveMedia && !hasMedia) return null;
887
+
888
+
if (filterMustBeReply && !thereply) return null;
889
+
890
return (
891
<>
892
{/* <p>
893
{postRecord?.value?.embed.$type + " " + JSON.stringify(hydratedEmbed)}
894
</p> */}
895
+
{/* <span>filtermustbereply is {filterMustBeReply ? "true" : "false"}</span>
896
+
<span>thereply is {thereply ? "true" : "false"}</span> */}
897
<UniversalPostRenderer
898
expanded={detailed}
899
onPostClick={() =>
···
916
}
917
}}
918
post={fakepost}
919
+
uprrrsauthor={fakeprofileviewdetailed}
920
salt={aturi}
921
bottomReplyLine={bottomReplyLine}
922
topReplyLine={topReplyLine}
···
927
style={style}
928
ref={ref}
929
dataIndexPropPass={dataIndexPropPass}
930
+
nopics={nopics}
931
+
concise={concise}
932
+
lightboxCallback={lightboxCallback}
933
+
maxReplies={maxReplies}
934
+
isQuote={isQuote}
935
/>
936
</>
937
);
···
970
{...props}
971
>
972
<path
973
+
fill="var(--color-gray-400)"
974
d="M9 22a1 1 0 0 1-1-1v-3H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-6.1l-3.7 3.71c-.2.19-.45.29-.7.29zm1-6v3.08L13.08 16H20V4H4v12z"
975
></path>
976
</svg>
···
987
{...props}
988
>
989
<path
990
+
fill="var(--color-gray-400)"
991
d="M17 17H7v-3l-4 4l4 4v-3h12v-6h-2M7 7h10v3l4-4l-4-4v3H5v6h2z"
992
></path>
993
</svg>
···
1038
{...props}
1039
>
1040
<path
1041
+
fill="var(--color-gray-400)"
1042
d="m12.1 18.55l-.1.1l-.11-.1C7.14 14.24 4 11.39 4 8.5C4 6.5 5.5 5 7.5 5c1.54 0 3.04 1 3.57 2.36h1.86C13.46 6 14.96 5 16.5 5c2 0 3.5 1.5 3.5 3.5c0 2.89-3.14 5.74-7.9 10.05M16.5 3c-1.74 0-3.41.81-4.5 2.08C10.91 3.81 9.24 3 7.5 3C4.42 3 2 5.41 2 8.5c0 3.77 3.4 6.86 8.55 11.53L12 21.35l1.45-1.32C18.6 15.36 22 12.27 22 8.5C22 5.41 19.58 3 16.5 3"
1043
></path>
1044
</svg>
···
1055
{...props}
1056
>
1057
<path
1058
+
fill="var(--color-gray-400)"
1059
d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81a3 3 0 0 0 3-3a3 3 0 0 0-3-3a3 3 0 0 0-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.15c-.05.21-.08.43-.08.66c0 1.61 1.31 2.91 2.92 2.91s2.92-1.3 2.92-2.91A2.92 2.92 0 0 0 18 16.08"
1060
></path>
1061
</svg>
···
1072
{...props}
1073
>
1074
<path
1075
+
fill="var(--color-gray-400)"
1076
d="M16 12a2 2 0 0 1 2-2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2m-6 0a2 2 0 0 1 2-2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2m-6 0a2 2 0 0 1 2-2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2"
1077
></path>
1078
</svg>
···
1089
{...props}
1090
>
1091
<path
1092
+
fill="var(--color-gray-400)"
1093
d="M17.9 17.39c-.26-.8-1.01-1.39-1.9-1.39h-1v-3a1 1 0 0 0-1-1H8v-2h2a1 1 0 0 0 1-1V7h2a2 2 0 0 0 2-2v-.41a7.984 7.984 0 0 1 2.9 12.8M11 19.93c-3.95-.49-7-3.85-7-7.93c0-.62.08-1.22.21-1.79L9 15v1a2 2 0 0 0 2 2m1-16A10 10 0 0 0 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2"
1094
></path>
1095
</svg>
···
1123
{...props}
1124
>
1125
<path
1126
+
fill="var(--color-gray-400)"
1127
d="M10 9V5l-7 7l7 7v-4.1c5 0 8.5 1.6 11 5.1c-1-5-4-10-11-11"
1128
></path>
1129
</svg>
···
1177
{...props}
1178
>
1179
<path
1180
+
fill="var(--color-gray-400)"
1181
d="M17 17H7v-3l-4 4l4 4v-3h12v-6h-2M7 7h10v3l4-4l-4-4v3H5v6h2z"
1182
></path>
1183
</svg>
···
1194
{...props}
1195
>
1196
<path
1197
+
fill="var(--color-gray-400)"
1198
d="M6 5.75L10.25 10H7v6h6.5l2 2H7a2 2 0 0 1-2-2v-6H1.75zm12 12.5L13.75 14H17V8h-6.5l-2-2H17a2 2 0 0 1 2 2v6h3.25z"
1199
></path>
1200
</svg>
···
1222
//import Masonry from "@mui/lab/Masonry";
1223
import {
1224
type $Typed,
1225
+
AppBskyActorDefs,
1226
AppBskyEmbedDefs,
1227
AppBskyEmbedExternal,
1228
AppBskyEmbedImages,
···
1246
PostView,
1247
//ThreadViewPost,
1248
} from "@atproto/api/dist/client/types/app/bsky/feed/defs";
1249
+
import { useInfiniteQuery } from "@tanstack/react-query";
1250
import { useEffect, useRef, useState } from "react";
1251
import ReactPlayer from "react-player";
1252
1253
import defaultpfp from "~/../public/favicon.png";
1254
import { useAuth } from "~/providers/UnifiedAuthProvider";
1255
+
import { renderSnack } from "~/routes/__root";
1256
+
import {
1257
+
FeedItemRenderAturiLoader,
1258
+
FollowButton,
1259
+
Mutual,
1260
+
} from "~/routes/profile.$did";
1261
+
import type { LightboxProps } from "~/routes/profile.$did/post.$rkey.image.$i";
1262
+
import { useFastLike } from "~/utils/likeMutationQueue";
1263
// import type { OutputSchema } from "@atproto/api/dist/client/types/app/bsky/feed/getFeed";
1264
// import type {
1265
// ViewRecord,
···
1367
1368
function UniversalPostRenderer({
1369
post,
1370
+
uprrrsauthor,
1371
//setMainItem,
1372
//isMainItem,
1373
onPostClick,
···
1387
style,
1388
ref,
1389
dataIndexPropPass,
1390
+
nopics,
1391
+
concise,
1392
+
lightboxCallback,
1393
+
maxReplies,
1394
}: {
1395
post: PostView;
1396
+
uprrrsauthor?: AppBskyActorDefs.ProfileViewDetailed;
1397
// optional for now because i havent ported every use to this yet
1398
// setMainItem?: React.Dispatch<
1399
// React.SetStateAction<AppBskyFeedDefs.FeedViewPost>
···
1412
depth?: number;
1413
repostedby?: string;
1414
style?: React.CSSProperties;
1415
+
ref?: React.RefObject<HTMLDivElement>;
1416
dataIndexPropPass?: number;
1417
+
nopics?: boolean;
1418
+
concise?: boolean;
1419
+
lightboxCallback?: (d: LightboxProps) => void;
1420
+
maxReplies?: number;
1421
}) {
1422
+
const parsed = new AtUri(post.uri);
1423
const navigate = useNavigate();
1424
const [hasRetweeted, setHasRetweeted] = useState<boolean>(
1425
post.viewer?.repost ? true : false
1426
);
1427
+
const [, setComposerPost] = useAtom(composerAtom);
1428
const { agent } = useAuth();
1429
const [retweetUri, setRetweetUri] = useState<string | undefined>(
1430
post.viewer?.repost
1431
);
1432
+
const { liked, toggle, backfill } = useFastLike(post.uri, post.cid);
1433
+
// const bovref = useBackfillOnView(post.uri, post.cid);
1434
+
// React.useLayoutEffect(()=>{
1435
+
// if (expanded && !isQuote) {
1436
+
// backfill();
1437
+
// }
1438
+
// },[backfill, expanded, isQuote])
1439
1440
const repostOrUnrepostPost = async () => {
1441
if (!agent) {
···
1467
1468
const emergencySalt = randomString();
1469
1470
+
const [showBridgyText] = useAtom(enableBridgyTextAtom);
1471
+
const [showWafrnText] = useAtom(enableWafrnTextAtom);
1472
+
1473
+
const unfedibridgy = (post.record as { bridgyOriginalText?: string })
1474
+
.bridgyOriginalText;
1475
+
const unfediwafrnPartial = (post.record as { fullText?: string }).fullText;
1476
+
const unfediwafrnTags = (post.record as { fullTags?: string }).fullTags;
1477
+
const unfediwafrnUnHost = (post.record as { fediverseId?: string })
1478
+
.fediverseId;
1479
+
1480
+
const undfediwafrnHost = unfediwafrnUnHost
1481
+
? new URL(unfediwafrnUnHost).hostname
1482
+
: undefined;
1483
+
1484
+
const tags = unfediwafrnTags
1485
+
? unfediwafrnTags
1486
+
.split("\n")
1487
+
.map((t) => t.trim())
1488
+
.filter(Boolean)
1489
+
: undefined;
1490
+
1491
+
const links = tags
1492
+
? tags
1493
+
.map((tag) => {
1494
+
const encoded = encodeURIComponent(tag);
1495
+
return `<a href="https://${undfediwafrnHost}/search/${encoded}" target="_blank">#${tag.replaceAll(" ", "-")}</a>`;
1496
+
})
1497
+
.join("<br>")
1498
+
: "";
1499
+
1500
+
const unfediwafrn = unfediwafrnPartial
1501
+
? unfediwafrnPartial + (links ? `<br>${links}` : "")
1502
+
: undefined;
1503
+
1504
+
const fedi =
1505
+
(showBridgyText ? unfedibridgy : undefined) ??
1506
+
(showWafrnText ? unfediwafrn : undefined);
1507
+
1508
/* fuck you */
1509
const isMainItem = false;
1510
const setMainItem = (any: any) => {};
1511
// eslint-disable-next-line react-hooks/refs
1512
+
//console.log("Received ref in UniversalPostRenderer:", usedref);
1513
return (
1514
<div ref={ref} style={style} data-index={dataIndexPropPass}>
1515
+
<div
1516
+
//ref={ref}
1517
+
key={salt + "-" + (post.uri || emergencySalt)}
1518
+
onClick={
1519
+
isMainItem
1520
? onPostClick
1521
+
: setMainItem
1522
+
? onPostClick
1523
+
? (e) => {
1524
+
setMainItem({ post: post });
1525
+
onPostClick(e);
1526
+
}
1527
+
: () => {
1528
+
setMainItem({ post: post });
1529
+
}
1530
+
: undefined
1531
+
}
1532
style={{
1533
+
//...style,
1534
+
//border: "1px solid #e1e8ed",
1535
+
//borderRadius: 12,
1536
+
opacity: "1 !important",
1537
+
background: "transparent",
1538
+
paddingLeft: isQuote ? 12 : 16,
1539
+
paddingRight: isQuote ? 12 : 16,
1540
+
//paddingTop: 16,
1541
+
paddingTop: isRepost ? 10 : isQuote ? 12 : topReplyLine ? 8 : 16,
1542
+
//paddingBottom: bottomReplyLine ? 0 : 16,
1543
+
paddingBottom: 0,
1544
+
fontFamily: "system-ui, sans-serif",
1545
+
//boxShadow: "0 2px 8px rgba(0,0,0,0.04)",
1546
+
position: "relative",
1547
+
// dont cursor: "pointer",
1548
+
borderBottomWidth: bottomBorder ? (isQuote ? 0 : 1) : 0,
1549
}}
1550
+
className="border-gray-300 dark:border-gray-800"
1551
>
1552
+
{isRepost && (
1553
+
<div
1554
+
style={{
1555
+
marginLeft: 36,
1556
+
display: "flex",
1557
+
borderRadius: 12,
1558
+
paddingBottom: "calc(22px - 1rem)",
1559
+
fontSize: 14,
1560
+
maxHeight: "1rem",
1561
+
justifyContent: "flex-start",
1562
+
//color: theme.textSecondary,
1563
+
gap: 4,
1564
+
alignItems: "center",
1565
+
}}
1566
+
className="text-gray-500 dark:text-gray-400"
1567
+
>
1568
+
<MdiRepost /> Reposted by @{isRepost}{" "}
1569
+
</div>
1570
+
)}
1571
+
{!isQuote && (
1572
+
<div
1573
+
style={{
1574
+
opacity:
1575
+
topReplyLine || isReply /*&& (true || expanded)*/ ? 0.5 : 0,
1576
+
position: "absolute",
1577
+
top: 0,
1578
+
left: 36, // why 36 ???
1579
+
//left: 16 + (42 / 2),
1580
+
width: 2,
1581
+
//height: "100%",
1582
+
height: isRepost
1583
+
? "calc(16px + 1rem - 6px)"
1584
+
: topReplyLine
1585
+
? 8 - 6
1586
+
: 16 - 6,
1587
+
// background: theme.textSecondary,
1588
+
//opacity: 0.5,
1589
+
// no flex here
1590
+
}}
1591
+
className="bg-gray-500 dark:bg-gray-400"
1592
+
/>
1593
+
)}
1594
+
<HoverCard.Root>
1595
+
<HoverCard.Trigger asChild>
1596
<div
1597
+
className={`absolute`}
1598
style={{
1599
+
top: isRepost
1600
+
? "calc(16px + 1rem)"
1601
+
: isQuote
1602
+
? 12
1603
+
: topReplyLine
1604
+
? 8
1605
+
: 16,
1606
+
left: isQuote ? 12 : 16,
1607
}}
1608
+
onClick={onProfileClick}
1609
+
>
1610
+
<img
1611
+
src={post.author.avatar || defaultpfp}
1612
+
alt="avatar"
1613
+
className={`rounded-full object-cover border border-gray-300 dark:border-gray-800 bg-gray-300 dark:bg-gray-600`}
1614
+
style={{
1615
+
width: isQuote ? 16 : 42,
1616
+
height: isQuote ? 16 : 42,
1617
+
}}
1618
+
/>
1619
+
</div>
1620
+
</HoverCard.Trigger>
1621
+
<HoverCard.Portal>
1622
+
<HoverCard.Content
1623
+
className="rounded-md p-4 w-72 bg-gray-50 dark:bg-gray-900 shadow-lg border border-gray-300 dark:border-gray-800 animate-slide-fade z-50"
1624
+
side={"bottom"}
1625
+
sideOffset={5}
1626
+
onClick={onProfileClick}
1627
+
>
1628
+
<div className="flex flex-col gap-2">
1629
+
<div className="flex flex-row">
1630
+
<img
1631
+
src={post.author.avatar || defaultpfp}
1632
+
alt="avatar"
1633
+
className="rounded-full w-[58px] h-[58px] object-cover border border-gray-300 dark:border-gray-800 bg-gray-300 dark:bg-gray-600"
1634
+
/>
1635
+
<div className=" flex-1 flex flex-row align-middle justify-end">
1636
+
<FollowButton targetdidorhandle={post.author.did} />
1637
+
</div>
1638
+
</div>
1639
+
<div className="flex flex-col gap-3">
1640
+
<div>
1641
+
<div className="text-gray-900 dark:text-gray-100 font-medium text-md">
1642
+
{post.author.displayName || post.author.handle}{" "}
1643
+
</div>
1644
+
<div className="text-gray-500 dark:text-gray-400 text-md flex flex-row gap-1">
1645
+
<Mutual targetdidorhandle={post.author.did} />@
1646
+
{post.author.handle}{" "}
1647
+
</div>
1648
+
</div>
1649
+
{uprrrsauthor?.description && (
1650
+
<div className="text-gray-700 dark:text-gray-300 text-sm text-left break-words line-clamp-3">
1651
+
{uprrrsauthor.description}
1652
+
</div>
1653
+
)}
1654
+
{/* <div className="flex gap-4">
1655
+
<div className="flex gap-1">
1656
+
<div className="font-medium text-gray-900 dark:text-gray-100">
1657
+
0
1658
+
</div>
1659
+
<div className="text-gray-500 dark:text-gray-400">
1660
+
Following
1661
+
</div>
1662
+
</div>
1663
+
<div className="flex gap-1">
1664
+
<div className="font-medium text-gray-900 dark:text-gray-100">
1665
+
2,900
1666
+
</div>
1667
+
<div className="text-gray-500 dark:text-gray-400">
1668
+
Followers
1669
+
</div>
1670
+
</div>
1671
+
</div> */}
1672
+
</div>
1673
+
</div>
1674
+
1675
+
{/* <HoverCard.Arrow className="fill-gray-50 dark:fill-gray-900" /> */}
1676
+
</HoverCard.Content>
1677
+
</HoverCard.Portal>
1678
+
</HoverCard.Root>
1679
+
1680
+
<div style={{ display: "flex", alignItems: "flex-start", zIndex: 2 }}>
1681
+
<div
1682
+
style={{
1683
+
display: "flex",
1684
+
flexDirection: "column",
1685
+
alignSelf: "stretch",
1686
+
alignItems: "center",
1687
+
overflow: "hidden",
1688
+
width: expanded || isQuote ? 0 : "auto",
1689
+
marginRight: expanded || isQuote ? 0 : 12,
1690
+
}}
1691
+
>
1692
+
{/* dummy for later use */}
1693
+
<div style={{ width: 42, height: 42 + 6, minHeight: 42 + 6 }} />
1694
+
{/* reply line !!!! bottomReplyLine */}
1695
+
{bottomReplyLine && (
1696
+
<div
1697
+
style={{
1698
+
width: 2,
1699
+
height: "100%",
1700
+
//background: theme.textSecondary,
1701
+
opacity: 0.5,
1702
+
// no flex here
1703
+
//color: "Red",
1704
+
//zIndex: 99
1705
+
}}
1706
+
className="bg-gray-500 dark:bg-gray-400"
1707
+
/>
1708
+
)}
1709
+
{/* <div
1710
layout
1711
transition={{ duration: 0.2 }}
1712
animate={{ height: expanded ? 0 : '100%' }}
···
1716
// no flex here
1717
}}
1718
/> */}
1719
+
</div>
1720
+
<div style={{ flex: 1, maxWidth: "100%" }}>
1721
<div
1722
style={{
1723
display: "flex",
1724
+
flexDirection: "row",
1725
+
alignItems: "center",
1726
+
flexWrap: "nowrap",
1727
+
maxWidth: `calc(100% - ${!expanded ? (isQuote ? 26 : 0) : 54}px)`,
1728
+
width: `calc(100% - ${!expanded ? (isQuote ? 26 : 0) : 54}px)`,
1729
+
marginLeft: !expanded ? (isQuote ? 26 : 0) : 54,
1730
+
marginBottom: !expanded ? 4 : 6,
1731
}}
1732
>
1733
+
<div
1734
style={{
1735
display: "flex",
1736
+
//overflow: "hidden", // hey why is overflow hidden unapplied
1737
overflow: "hidden",
1738
textOverflow: "ellipsis",
1739
flexShrink: 1,
1740
+
flexGrow: 1,
1741
+
flexBasis: 0,
1742
+
width: 0,
1743
+
gap: expanded ? 0 : 6,
1744
+
alignItems: expanded ? "flex-start" : "center",
1745
+
flexDirection: expanded ? "column" : "row",
1746
+
height: expanded ? 42 : "1rem",
1747
}}
1748
>
1749
+
<span
1750
+
style={{
1751
+
display: "flex",
1752
+
fontWeight: 700,
1753
+
fontSize: 16,
1754
+
overflow: "hidden",
1755
+
textOverflow: "ellipsis",
1756
+
whiteSpace: "nowrap",
1757
+
flexShrink: 1,
1758
+
minWidth: 0,
1759
+
gap: 4,
1760
+
alignItems: "center",
1761
+
//color: theme.text,
1762
+
}}
1763
+
className="text-gray-900 dark:text-gray-100"
1764
+
>
1765
+
{/* verified checkmark */}
1766
+
{post.author.displayName || post.author.handle}{" "}
1767
+
{post.author.verification?.verifiedStatus == "valid" && (
1768
+
<MdiVerified />
1769
+
)}
1770
+
</span>
1771
1772
+
<span
1773
+
style={{
1774
+
//color: theme.textSecondary,
1775
+
fontSize: 16,
1776
+
overflowX: "hidden",
1777
+
textOverflow: "ellipsis",
1778
+
whiteSpace: "nowrap",
1779
+
flexShrink: 1,
1780
+
flexGrow: 0,
1781
+
minWidth: 0,
1782
+
}}
1783
+
className="text-gray-500 dark:text-gray-400"
1784
+
>
1785
+
@{post.author.handle}
1786
+
</span>
1787
+
</div>
1788
+
<div
1789
style={{
1790
+
display: "flex",
1791
+
alignItems: "center",
1792
+
height: "1rem",
1793
}}
1794
>
1795
+
<span
1796
+
style={{
1797
+
//color: theme.textSecondary,
1798
+
fontSize: 16,
1799
+
marginLeft: 8,
1800
+
whiteSpace: "nowrap",
1801
+
flexShrink: 0,
1802
+
maxWidth: "100%",
1803
+
}}
1804
+
className="text-gray-500 dark:text-gray-400"
1805
+
>
1806
+
ยท {/* time placeholder */}
1807
+
{shortTimeAgo(post.indexedAt)}
1808
+
</span>
1809
+
</div>
1810
</div>
1811
+
{/* reply indicator */}
1812
+
{!!feedviewpostreplyhandle && (
1813
+
<div
1814
style={{
1815
+
display: "flex",
1816
+
borderRadius: 12,
1817
+
paddingBottom: 2,
1818
+
fontSize: 14,
1819
+
justifyContent: "flex-start",
1820
//color: theme.textSecondary,
1821
+
gap: 4,
1822
+
alignItems: "center",
1823
+
//marginLeft: 36,
1824
+
height:
1825
+
!(expanded || isQuote) && !!feedviewpostreplyhandle
1826
+
? "1rem"
1827
+
: 0,
1828
+
opacity:
1829
+
!(expanded || isQuote) && !!feedviewpostreplyhandle ? 1 : 0,
1830
}}
1831
className="text-gray-500 dark:text-gray-400"
1832
>
1833
+
<MdiReply /> Reply to @{feedviewpostreplyhandle}
1834
+
</div>
1835
+
)}
1836
<div
1837
style={{
1838
+
fontSize: 16,
1839
+
marginBottom: !post.embed || concise ? 0 : 8,
1840
+
whiteSpace: "pre-wrap",
1841
+
textAlign: "left",
1842
+
overflowWrap: "anywhere",
1843
+
wordBreak: "break-word",
1844
+
...(concise && {
1845
+
display: "-webkit-box",
1846
+
WebkitBoxOrient: "vertical",
1847
+
WebkitLineClamp: 2,
1848
+
overflow: "hidden",
1849
+
}),
1850
}}
1851
+
className="text-gray-900 dark:text-gray-100"
1852
>
1853
+
{fedi ? (
1854
+
<>
1855
+
<span
1856
+
className="dangerousFediContent"
1857
+
dangerouslySetInnerHTML={{
1858
+
__html: DOMPurify.sanitize(fedi),
1859
+
}}
1860
+
/>
1861
+
</>
1862
+
) : (
1863
+
<>
1864
+
{renderTextWithFacets({
1865
+
text: (post.record as { text?: string }).text ?? "",
1866
+
facets: (post.record.facets as Facet[]) ?? [],
1867
+
navigate: navigate,
1868
+
})}
1869
+
</>
1870
+
)}
1871
</div>
1872
+
{post.embed && depth < 1 && !concise ? (
1873
+
<PostEmbeds
1874
+
embed={post.embed}
1875
+
//moderation={moderation}
1876
+
viewContext={PostEmbedViewContext.Feed}
1877
+
salt={salt}
1878
+
navigate={navigate}
1879
+
postid={{ did: post.author.did, rkey: parsed.rkey }}
1880
+
nopics={nopics}
1881
+
lightboxCallback={lightboxCallback}
1882
+
/>
1883
+
) : null}
1884
+
{post.embed && depth > 0 && (
1885
+
/* pretty bad hack imo. its trying to sync up with how the embed shim doesnt
1886
hydrate embeds this deep but the connection here is implicit
1887
todo: idk make this a real part of the embed shim so its not implicit */
1888
+
<>
1889
+
<div className="border-gray-300 dark:border-gray-800 p-3 rounded-xl border italic text-gray-400 text-[14px]">
1890
+
(there is an embed here thats too deep to render)
1891
+
</div>
1892
+
</>
1893
+
)}
1894
+
<div
1895
+
style={{
1896
+
paddingTop: post.embed && !concise && depth < 1 ? 4 : 0,
1897
+
}}
1898
+
>
1899
+
<>
1900
+
{expanded && (
1901
+
<div
1902
+
style={{
1903
+
overflow: "hidden",
1904
+
//color: theme.textSecondary,
1905
+
fontSize: 14,
1906
+
display: "flex",
1907
+
borderBottomStyle: "solid",
1908
+
//borderBottomColor: theme.border,
1909
+
//background: "#f00",
1910
+
// height: "1rem",
1911
+
paddingTop: 4,
1912
+
paddingBottom: 8,
1913
+
borderBottomWidth: 1,
1914
+
marginBottom: 8,
1915
+
}} // important for height animation
1916
+
className="text-gray-500 dark:text-gray-400 border-gray-200 dark:border-gray-800 was7"
1917
+
>
1918
+
{fullDateTimeFormat(post.indexedAt)}
1919
+
</div>
1920
+
)}
1921
+
</>
1922
+
{!isQuote && (
1923
<div
1924
style={{
1925
display: "flex",
1926
+
gap: 32,
1927
+
paddingTop: 8,
1928
+
//color: theme.textSecondary,
1929
+
fontSize: 15,
1930
+
justifyContent: "space-between",
1931
+
//background: "#0f0",
1932
}}
1933
+
className="text-gray-500 dark:text-gray-400"
1934
>
1935
<HitSlopButton
1936
+
onClick={() => {
1937
+
setComposerPost({ kind: "reply", parent: post.uri });
1938
}}
1939
style={{
1940
...btnstyle,
1941
}}
1942
>
1943
+
<MdiCommentOutline />
1944
+
{post.replyCount}
1945
</HitSlopButton>
1946
+
<DropdownMenu.Root modal={false}>
1947
+
<DropdownMenu.Trigger asChild>
1948
+
<div
1949
+
style={{
1950
+
...btnstyle,
1951
+
...(hasRetweeted ? { color: "#5CEFAA" } : {}),
1952
+
}}
1953
+
aria-label="Repost or quote post"
1954
+
>
1955
+
{hasRetweeted ? <MdiRepeatGreen /> : <MdiRepeat />}
1956
+
{post.repostCount ?? 0}
1957
+
</div>
1958
+
</DropdownMenu.Trigger>
1959
+
1960
+
<DropdownMenu.Portal>
1961
+
<DropdownMenu.Content
1962
+
align="start"
1963
+
sideOffset={5}
1964
+
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 w-32 z-50 overflow-hidden"
1965
+
>
1966
+
<DropdownMenu.Item
1967
+
onSelect={repostOrUnrepostPost}
1968
+
className="px-3 py-2 text-sm flex items-center gap-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-700"
1969
+
>
1970
+
<MdiRepeat
1971
+
className={hasRetweeted ? "text-green-400" : ""}
1972
+
/>
1973
+
<span>{hasRetweeted ? "Undo Repost" : "Repost"}</span>
1974
+
</DropdownMenu.Item>
1975
+
1976
+
<DropdownMenu.Item
1977
+
onSelect={() => {
1978
+
setComposerPost({
1979
+
kind: "quote",
1980
+
subject: post.uri,
1981
+
});
1982
+
}}
1983
+
className="px-3 py-2 text-sm flex items-center gap-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-700"
1984
+
>
1985
+
{/* You might want a specific quote icon here */}
1986
+
<MdiCommentOutline />
1987
+
<span>Quote</span>
1988
+
</DropdownMenu.Item>
1989
+
</DropdownMenu.Content>
1990
+
</DropdownMenu.Portal>
1991
+
</DropdownMenu.Root>
1992
+
<HitSlopButton
1993
+
onClick={() => {
1994
+
toggle();
1995
+
}}
1996
+
style={{
1997
+
...btnstyle,
1998
+
...(liked ? { color: "#EC4899" } : {}),
1999
+
}}
2000
+
>
2001
+
{liked ? <MdiCardsHeart /> : <MdiCardsHeartOutline />}
2002
+
{(post.likeCount || 0) + (liked ? 1 : 0)}
2003
+
</HitSlopButton>
2004
+
<div style={{ display: "flex", gap: 8 }}>
2005
+
<HitSlopButton
2006
+
onClick={async (e) => {
2007
+
e.stopPropagation();
2008
+
try {
2009
+
await navigator.clipboard.writeText(
2010
+
"https://bsky.app" +
2011
+
"/profile/" +
2012
+
post.author.handle +
2013
+
"/post/" +
2014
+
post.uri.split("/").pop()
2015
+
);
2016
+
renderSnack({
2017
+
title: "Copied to clipboard!",
2018
+
});
2019
+
} catch (_e) {
2020
+
// idk
2021
+
renderSnack({
2022
+
title: "Failed to copy link",
2023
+
});
2024
+
}
2025
+
}}
2026
+
style={{
2027
+
...btnstyle,
2028
+
}}
2029
+
>
2030
+
<MdiShareVariant />
2031
+
</HitSlopButton>
2032
+
<HitSlopButton
2033
+
onClick={() => {
2034
+
renderSnack({
2035
+
title: "Not implemented yet...",
2036
+
});
2037
+
}}
2038
+
>
2039
+
<span style={btnstyle}>
2040
+
<MdiMoreHoriz />
2041
+
</span>
2042
+
</HitSlopButton>
2043
+
</div>
2044
</div>
2045
+
)}
2046
+
</div>
2047
+
<div
2048
+
style={{
2049
+
//height: bottomReplyLine ? 16 : 0
2050
+
height: isQuote ? 12 : 16,
2051
+
}}
2052
+
/>
2053
</div>
2054
</div>
2055
</div>
2056
</div>
2057
);
2058
}
···
2142
viewContext,
2143
salt,
2144
navigate,
2145
+
postid,
2146
+
nopics,
2147
+
lightboxCallback,
2148
}: {
2149
embed?: Embed;
2150
moderation?: ModerationDecision;
···
2153
viewContext?: PostEmbedViewContext;
2154
salt: string;
2155
navigate: (_: any) => void;
2156
+
postid?: { did: string; rkey: string };
2157
+
nopics?: boolean;
2158
+
lightboxCallback?: (d: LightboxProps) => void;
2159
}) {
2160
+
//const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
2161
+
function setLightboxIndex(number: number) {
2162
+
navigate({
2163
+
to: "/profile/$did/post/$rkey/image/$i",
2164
+
params: {
2165
+
did: postid?.did,
2166
+
rkey: postid?.rkey,
2167
+
i: number.toString(),
2168
+
},
2169
+
});
2170
+
}
2171
if (
2172
AppBskyEmbedRecordWithMedia.isView(embed) &&
2173
AppBskyEmbedRecord.isViewRecord(embed.record.record) &&
···
2201
viewContext={viewContext}
2202
salt={salt}
2203
navigate={navigate}
2204
+
postid={postid}
2205
+
nopics={nopics}
2206
+
lightboxCallback={lightboxCallback}
2207
/>
2208
{/* padding empty div of 8px height */}
2209
<div style={{ height: 12 }} />
···
2217
//boxShadow: theme.cardShadow,
2218
overflow: "hidden",
2219
}}
2220
+
className="shadow border border-gray-200 dark:border-gray-800 was7"
2221
>
2222
<UniversalPostRenderer
2223
post={post}
···
2265
}
2266
2267
if (AppBskyEmbedRecord.isView(embed)) {
2268
+
// hey im really lazy and im gonna do it the bad way
2269
+
const reallybaduri = (embed?.record as any)?.uri as string | undefined;
2270
+
const reallybadaturi = reallybaduri ? new AtUri(reallybaduri) : undefined;
2271
+
2272
// custom feed embed (i.e. generator view)
2273
if (AppBskyFeedDefs.isGeneratorView(embed.record)) {
2274
// stopgap sorry
···
2278
// <MaybeFeedCard view={embed.record} />
2279
// </div>
2280
// )
2281
+
} else if (
2282
+
!!reallybaduri &&
2283
+
!!reallybadaturi &&
2284
+
reallybadaturi.collection === "app.bsky.feed.generator"
2285
+
) {
2286
+
return (
2287
+
<div className="rounded-xl border">
2288
+
<FeedItemRenderAturiLoader aturi={reallybaduri} disableBottomBorder />
2289
+
</div>
2290
+
);
2291
}
2292
2293
// list embed
···
2299
// <MaybeListCard view={embed.record} />
2300
// </div>
2301
// )
2302
+
} else if (
2303
+
!!reallybaduri &&
2304
+
!!reallybadaturi &&
2305
+
reallybadaturi.collection === "app.bsky.graph.list"
2306
+
) {
2307
+
return (
2308
+
<div className="rounded-xl border">
2309
+
<FeedItemRenderAturiLoader
2310
+
aturi={reallybaduri}
2311
+
disableBottomBorder
2312
+
listmode
2313
+
disablePropagation
2314
+
/>
2315
+
</div>
2316
+
);
2317
}
2318
2319
// starter pack embed
···
2325
// <StarterPackCard starterPack={embed.record} />
2326
// </div>
2327
// )
2328
+
} else if (
2329
+
!!reallybaduri &&
2330
+
!!reallybadaturi &&
2331
+
reallybadaturi.collection === "app.bsky.graph.starterpack"
2332
+
) {
2333
+
return (
2334
+
<div className="rounded-xl border">
2335
+
<FeedItemRenderAturiLoader
2336
+
aturi={reallybaduri}
2337
+
disableBottomBorder
2338
+
listmode
2339
+
disablePropagation
2340
+
/>
2341
+
</div>
2342
+
);
2343
}
2344
2345
// quote post
···
2378
//boxShadow: theme.cardShadow,
2379
overflow: "hidden",
2380
}}
2381
+
className="shadow border border-gray-200 dark:border-gray-800 was7"
2382
>
2383
<UniversalPostRenderer
2384
post={post}
···
2399
</div>
2400
);
2401
} else {
2402
+
console.log("what the hell is a ", embed);
2403
return <>sorry</>;
2404
}
2405
//return <QuotePostRenderer record={embed.record} moderation={moderation} />;
···
2423
src: img.fullsize,
2424
alt: img.alt,
2425
}));
2426
+
console.log("rendering images");
2427
+
if (lightboxCallback) {
2428
+
lightboxCallback({ images: lightboxImages });
2429
+
console.log("rendering images");
2430
+
}
2431
+
2432
+
if (nopics) return;
2433
2434
if (images.length > 0) {
2435
// const items = embed.images.map(img => ({
···
2459
//border: `1px solid ${theme.border}`,
2460
overflow: "hidden",
2461
}}
2462
+
className="border border-gray-200 dark:border-gray-800 was7 bg-gray-200 dark:bg-gray-900"
2463
>
2464
+
{/* {lightboxIndex !== null && (
2465
<Lightbox
2466
images={lightboxImages}
2467
index={lightboxIndex}
2468
onClose={() => setLightboxIndex(null)}
2469
onNavigate={(newIndex) => setLightboxIndex(newIndex)}
2470
+
post={postid}
2471
/>
2472
+
)} */}
2473
<img
2474
src={image.fullsize}
2475
alt={image.alt}
···
2500
overflow: "hidden",
2501
//border: `1px solid ${theme.border}`,
2502
}}
2503
+
className="border border-gray-200 dark:border-gray-800 was7"
2504
>
2505
+
{/* {lightboxIndex !== null && (
2506
<Lightbox
2507
images={lightboxImages}
2508
index={lightboxIndex}
2509
onClose={() => setLightboxIndex(null)}
2510
onNavigate={(newIndex) => setLightboxIndex(newIndex)}
2511
+
post={postid}
2512
/>
2513
+
)} */}
2514
{images.map((img, i) => (
2515
<div
2516
key={i}
···
2550
//border: `1px solid ${theme.border}`,
2551
// height: 240, // fixed height for cropping
2552
}}
2553
+
className="border border-gray-200 dark:border-gray-800 was7"
2554
>
2555
+
{/* {lightboxIndex !== null && (
2556
<Lightbox
2557
images={lightboxImages}
2558
index={lightboxIndex}
2559
onClose={() => setLightboxIndex(null)}
2560
onNavigate={(newIndex) => setLightboxIndex(newIndex)}
2561
+
post={postid}
2562
/>
2563
+
)} */}
2564
{/* Left: 1:1 */}
2565
<div
2566
style={{ flex: 1, aspectRatio: "1 / 1", position: "relative" }}
···
2635
//border: `1px solid ${theme.border}`,
2636
//aspectRatio: "3 / 2", // overall grid aspect
2637
}}
2638
+
className="border border-gray-200 dark:border-gray-800 was7"
2639
>
2640
+
{/* {lightboxIndex !== null && (
2641
<Lightbox
2642
images={lightboxImages}
2643
index={lightboxIndex}
2644
onClose={() => setLightboxIndex(null)}
2645
onNavigate={(newIndex) => setLightboxIndex(newIndex)}
2646
+
post={postid}
2647
/>
2648
+
)} */}
2649
{images.map((img, i) => (
2650
<div
2651
key={i}
···
2709
// =
2710
if (AppBskyEmbedVideo.isView(embed)) {
2711
// hls playlist
2712
+
if (nopics) return;
2713
const playlist = embed.playlist;
2714
return (
2715
<SmartHLSPlayer
···
2737
return <div />;
2738
}
2739
2740
function getDomain(url: string) {
2741
try {
2742
const { hostname } = new URL(url);
···
2801
return { start, end, feature: f.features[0] };
2802
});
2803
}
2804
+
export function renderTextWithFacets({
2805
text,
2806
facets,
2807
navigate,
···
2833
className="link"
2834
style={{
2835
textDecoration: "none",
2836
+
color: "var(--link-text-color)",
2837
wordBreak: "break-all",
2838
}}
2839
target="_blank"
···
2853
result.push(
2854
<span
2855
key={start}
2856
+
style={{ color: "var(--link-text-color)" }}
2857
className=" cursor-pointer"
2858
onClick={(e) => {
2859
e.stopPropagation();
···
2871
result.push(
2872
<span
2873
key={start}
2874
+
style={{ color: "var(--link-text-color)" }}
2875
onClick={(e) => {
2876
e.stopPropagation();
2877
}}
···
2964
>
2965
<div
2966
style={containerStyle as React.CSSProperties}
2967
+
className="border border-gray-200 dark:border-gray-800 was7"
2968
>
2969
{thumb && (
2970
<div
···
2978
marginBottom: 8,
2979
//borderBottom: `1px solid ${theme.border}`,
2980
}}
2981
+
className="border-b border-gray-200 dark:border-gray-800 was7"
2982
>
2983
<img
2984
src={thumb}
···
3104
borderRadius: 12,
3105
//border: `1px solid ${theme.border}`,
3106
}}
3107
+
className="border border-gray-200 dark:border-gray-800 was7"
3108
onClick={async (e) => {
3109
e.stopPropagation();
3110
setPlaying(true);
···
3145
100 / (aspect ? aspect.width / aspect.height : 16 / 9)
3146
}%`, // 16:9 = 56.25%, 4:3 = 75%
3147
}}
3148
+
className="border border-gray-200 dark:border-gray-800 was7"
3149
>
3150
<ReactPlayer
3151
src={url}
+59
-14
src/main.tsx
+59
-14
src/main.tsx
···
1
-
import { StrictMode } from "react";
2
import ReactDOM from "react-dom/client";
3
-
import { RouterProvider, createRouter } from "@tanstack/react-router";
4
5
// Import the generated route tree
6
import { routeTree } from "./routeTree.gen";
7
8
-
import "~/styles/app.css";
9
-
import reportWebVitals from "./reportWebVitals.ts";
10
-
import { QueryClient, QueryClientProvider, } from "@tanstack/react-query";
11
-
import {
12
-
persistQueryClient,
13
-
} from "@tanstack/react-query-persist-client";
14
-
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
15
-
16
17
const queryClient = new QueryClient({
18
defaultOptions: {
···
28
persistQueryClient({
29
queryClient,
30
persister: localStoragePersister,
31
-
})
32
33
// Create a new router instance
34
const router = createRouter({
···
54
root.render(
55
// double queries annoys me
56
// <StrictMode>
57
-
<QueryClientProvider client={queryClient}>
58
-
<RouterProvider router={router} />
59
-
</QueryClientProvider>
60
// </StrictMode>
61
);
62
}
···
65
// to log results (for example: reportWebVitals(// /*mass comment*/ console.log))
66
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
67
reportWebVitals();
···
1
+
import "~/styles/app.css";
2
+
3
+
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
4
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5
+
import { persistQueryClient } from "@tanstack/react-query-persist-client";
6
+
import { createRouter, RouterProvider } from "@tanstack/react-router";
7
+
import { useSetAtom } from "jotai";
8
+
import { useEffect } from "react";
9
+
//import { StrictMode } from "react";
10
import ReactDOM from "react-dom/client";
11
12
+
import reportWebVitals from "./reportWebVitals.ts";
13
// Import the generated route tree
14
import { routeTree } from "./routeTree.gen";
15
+
import { isAtTopAtom } from "./utils/atoms.ts";
16
17
+
//initAtomToCssVar(hueAtom, "--tw-gray-hue")
18
19
const queryClient = new QueryClient({
20
defaultOptions: {
···
30
persistQueryClient({
31
queryClient,
32
persister: localStoragePersister,
33
+
});
34
35
// Create a new router instance
36
const router = createRouter({
···
56
root.render(
57
// double queries annoys me
58
// <StrictMode>
59
+
<QueryClientProvider client={queryClient}>
60
+
<ScrollTopWatcher />
61
+
<RouterProvider router={router} />
62
+
</QueryClientProvider>
63
// </StrictMode>
64
);
65
}
···
68
// to log results (for example: reportWebVitals(// /*mass comment*/ console.log))
69
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
70
reportWebVitals();
71
+
72
+
export default function ScrollTopWatcher() {
73
+
const setIsAtTop = useSetAtom(isAtTopAtom);
74
+
useEffect(() => {
75
+
const meta = document.querySelector('meta[name="theme-color"]');
76
+
let lastAtTop = window.scrollY === 0;
77
+
let timeoutId: number | undefined;
78
+
79
+
const setVars = (atTop: boolean) => {
80
+
const root = document.documentElement;
81
+
root.style.setProperty("--is-top", atTop ? "1" : "0");
82
+
83
+
const bg = getComputedStyle(root).getPropertyValue("--header-bg").trim();
84
+
if (meta && bg) meta.setAttribute("content", bg);
85
+
setIsAtTop(atTop);
86
+
};
87
+
88
+
const check = () => {
89
+
const atTop = window.scrollY === 0;
90
+
if (atTop !== lastAtTop) {
91
+
lastAtTop = atTop;
92
+
setVars(atTop);
93
+
}
94
+
};
95
+
96
+
const handleScroll = () => {
97
+
if (timeoutId) clearTimeout(timeoutId);
98
+
timeoutId = window.setTimeout(check, 2);
99
+
};
100
+
101
+
// initialize
102
+
setVars(lastAtTop);
103
+
window.addEventListener("scroll", handleScroll, { passive: true });
104
+
105
+
return () => {
106
+
window.removeEventListener("scroll", handleScroll);
107
+
if (timeoutId) clearTimeout(timeoutId);
108
+
};
109
+
}, []);
110
+
111
+
return null;
112
+
}
+163
src/providers/LikeMutationQueueProvider.tsx
+163
src/providers/LikeMutationQueueProvider.tsx
···
···
1
+
import { AtUri } from "@atproto/api";
2
+
import { TID } from "@atproto/common-web";
3
+
import { useQueryClient } from "@tanstack/react-query";
4
+
import { useAtom } from "jotai";
5
+
import React, { createContext, use, useCallback, useEffect, useRef } from "react";
6
+
7
+
import { useAuth } from "~/providers/UnifiedAuthProvider";
8
+
import { renderSnack } from "~/routes/__root";
9
+
import { constellationURLAtom, internalLikedPostsAtom } from "~/utils/atoms";
10
+
import { constructArbitraryQuery, constructConstellationQuery, type linksRecordsResponse } from "~/utils/useQuery";
11
+
12
+
export type LikeRecord = { uri: string; target: string; cid: string };
13
+
export type LikeMutation = { type: 'like'; target: string; cid: string };
14
+
export type UnlikeMutation = { type: 'unlike'; likeRecordUri: string; target: string, originalRecord: LikeRecord };
15
+
export type Mutation = LikeMutation | UnlikeMutation;
16
+
17
+
interface LikeMutationQueueContextType {
18
+
fastState: (target: string) => LikeRecord | null | undefined;
19
+
fastToggle: (target:string, cid:string) => void;
20
+
backfillState: (target: string, user: string) => Promise<void>;
21
+
}
22
+
23
+
const LikeMutationQueueContext = createContext<LikeMutationQueueContextType | undefined>(undefined);
24
+
25
+
export function LikeMutationQueueProvider({ children }: { children: React.ReactNode }) {
26
+
const { agent } = useAuth();
27
+
const queryClient = useQueryClient();
28
+
const [likedPosts, setLikedPosts] = useAtom(internalLikedPostsAtom);
29
+
const [constellationurl] = useAtom(constellationURLAtom);
30
+
31
+
const likedPostsRef = useRef(likedPosts);
32
+
useEffect(() => {
33
+
likedPostsRef.current = likedPosts;
34
+
}, [likedPosts]);
35
+
36
+
const queueRef = useRef<Mutation[]>([]);
37
+
const runningRef = useRef(false);
38
+
39
+
const fastState = (target: string) => likedPosts[target];
40
+
41
+
const setFastState = useCallback(
42
+
(target: string, record: LikeRecord | null) =>
43
+
setLikedPosts((prev) => ({ ...prev, [target]: record })),
44
+
[setLikedPosts]
45
+
);
46
+
47
+
const enqueue = (mutation: Mutation) => queueRef.current.push(mutation);
48
+
49
+
const fastToggle = useCallback((target: string, cid: string) => {
50
+
const likedRecord = likedPostsRef.current[target];
51
+
52
+
if (likedRecord) {
53
+
setFastState(target, null);
54
+
if (likedRecord.uri !== 'pending') {
55
+
enqueue({ type: "unlike", likeRecordUri: likedRecord.uri, target, originalRecord: likedRecord });
56
+
}
57
+
} else {
58
+
setFastState(target, { uri: "pending", target, cid });
59
+
enqueue({ type: "like", target, cid });
60
+
}
61
+
}, [setFastState]);
62
+
63
+
/**
64
+
*
65
+
* @deprecated dont use it yet, will cause infinite rerenders
66
+
*/
67
+
const backfillState = async (target: string, user: string) => {
68
+
const query = constructConstellationQuery({
69
+
constellation: constellationurl,
70
+
method: "/links",
71
+
target,
72
+
collection: "app.bsky.feed.like",
73
+
path: ".subject.uri",
74
+
dids: [user],
75
+
});
76
+
const data = await queryClient.fetchQuery(query);
77
+
const likes = (data as linksRecordsResponse)?.linking_records?.slice(0, 50) ?? [];
78
+
const found = likes.find((r) => r.did === user);
79
+
if (found) {
80
+
const uri = `at://${found.did}/${found.collection}/${found.rkey}`;
81
+
const ciddata = await queryClient.fetchQuery(
82
+
constructArbitraryQuery(uri)
83
+
);
84
+
if (ciddata?.cid)
85
+
setFastState(target, { uri, target, cid: ciddata?.cid });
86
+
} else {
87
+
setFastState(target, null);
88
+
}
89
+
};
90
+
91
+
92
+
useEffect(() => {
93
+
if (!agent?.did) return;
94
+
95
+
const processQueue = async () => {
96
+
if (runningRef.current || queueRef.current.length === 0) return;
97
+
runningRef.current = true;
98
+
99
+
while (queueRef.current.length > 0) {
100
+
const mutation = queueRef.current.shift()!;
101
+
try {
102
+
if (mutation.type === "like") {
103
+
const newRecord = {
104
+
repo: agent.did!,
105
+
collection: "app.bsky.feed.like",
106
+
rkey: TID.next().toString(),
107
+
record: {
108
+
$type: "app.bsky.feed.like",
109
+
subject: { uri: mutation.target, cid: mutation.cid },
110
+
createdAt: new Date().toISOString(),
111
+
},
112
+
};
113
+
const response = await agent.com.atproto.repo.createRecord(newRecord);
114
+
if (!response.success) throw new Error("createRecord failed");
115
+
116
+
const uri = `at://${agent.did}/${newRecord.collection}/${newRecord.rkey}`;
117
+
setFastState(mutation.target, {
118
+
uri,
119
+
target: mutation.target,
120
+
cid: mutation.cid,
121
+
});
122
+
} else if (mutation.type === "unlike") {
123
+
const aturi = new AtUri(mutation.likeRecordUri);
124
+
await agent.com.atproto.repo.deleteRecord({ repo: agent.did!, collection: aturi.collection, rkey: aturi.rkey });
125
+
setFastState(mutation.target, null);
126
+
}
127
+
} catch (err) {
128
+
console.error("Like mutation failed, reverting:", err);
129
+
renderSnack({
130
+
title: 'Like Mutation Failed',
131
+
description: 'Please try again.',
132
+
//button: { label: 'Try Again', onClick: () => console.log('whatever') },
133
+
})
134
+
if (mutation.type === 'like') {
135
+
setFastState(mutation.target, null);
136
+
} else if (mutation.type === 'unlike') {
137
+
setFastState(mutation.target, mutation.originalRecord);
138
+
}
139
+
}
140
+
}
141
+
runningRef.current = false;
142
+
};
143
+
144
+
const interval = setInterval(processQueue, 1000);
145
+
return () => clearInterval(interval);
146
+
}, [agent, setFastState]);
147
+
148
+
const value = { fastState, fastToggle, backfillState };
149
+
150
+
return (
151
+
<LikeMutationQueueContext value={value}>
152
+
{children}
153
+
</LikeMutationQueueContext>
154
+
);
155
+
}
156
+
157
+
export function useLikeMutationQueue() {
158
+
const context = use(LikeMutationQueueContext);
159
+
if (context === undefined) {
160
+
throw new Error('useLikeMutationQueue must be used within a LikeMutationQueueProvider');
161
+
}
162
+
return context;
163
+
}
+26
-23
src/providers/UnifiedAuthProvider.tsx
+26
-23
src/providers/UnifiedAuthProvider.tsx
···
1
-
// src/providers/UnifiedAuthProvider.tsx
2
-
// Import both Agent and the (soon to be deprecated) AtpAgent
3
import { Agent, AtpAgent, type AtpSessionData } from "@atproto/api";
4
import {
5
type OAuthSession,
···
7
TokenRefreshError,
8
TokenRevokedError,
9
} from "@atproto/oauth-client-browser";
10
import React, {
11
createContext,
12
use,
···
15
useState,
16
} from "react";
17
18
-
import { oauthClient } from "../utils/oauthClient"; // Adjust path if needed
19
20
-
// Define the unified status and authentication method
21
type AuthStatus = "loading" | "signedIn" | "signedOut";
22
type AuthMethod = "password" | "oauth" | null;
23
24
interface AuthContextValue {
25
-
agent: Agent | null; // The agent is typed as the base class `Agent`
26
status: AuthStatus;
27
authMethod: AuthMethod;
28
loginWithPassword: (
···
41
}: {
42
children: React.ReactNode;
43
}) => {
44
-
// The state is typed as the base class `Agent`, which accepts both `Agent` and `AtpAgent` instances.
45
const [agent, setAgent] = useState<Agent | null>(null);
46
const [status, setStatus] = useState<AuthStatus>("loading");
47
const [authMethod, setAuthMethod] = useState<AuthMethod>(null);
48
const [oauthSession, setOauthSession] = useState<OAuthSession | null>(null);
49
50
-
// Unified Initialization Logic
51
const initialize = useCallback(async () => {
52
-
// --- 1. Try OAuth initialization first ---
53
try {
54
const oauthResult = await oauthClient.init();
55
if (oauthResult) {
56
// /*mass comment*/ console.log("OAuth session restored.");
57
-
const apiAgent = new Agent(oauthResult.session); // Standard Agent
58
setAgent(apiAgent);
59
setOauthSession(oauthResult.session);
60
setAuthMethod("oauth");
61
setStatus("signedIn");
62
-
return; // Success
63
}
64
} catch (e) {
65
console.error("OAuth init failed, checking password session.", e);
66
}
67
68
-
// --- 2. If no OAuth, try password-based session using AtpAgent ---
69
try {
70
const service = localStorage.getItem("service");
71
const sessionString = localStorage.getItem("sess");
72
73
if (service && sessionString) {
74
// /*mass comment*/ console.log("Resuming password-based session using AtpAgent...");
75
-
// Use the original, working AtpAgent logic
76
const apiAgent = new AtpAgent({ service });
77
const session: AtpSessionData = JSON.parse(sessionString);
78
await apiAgent.resumeSession(session);
79
80
// /*mass comment*/ console.log("Password-based session resumed successfully.");
81
-
setAgent(apiAgent); // This works because AtpAgent is a subclass of Agent
82
setAuthMethod("password");
83
setStatus("signedIn");
84
-
return; // Success
85
}
86
} catch (e) {
87
console.error("Failed to resume password-based session.", e);
···
89
localStorage.removeItem("service");
90
}
91
92
-
// --- 3. If neither worked, user is signed out ---
93
// /*mass comment*/ console.log("No active session found.");
94
setStatus("signedOut");
95
setAgent(null);
96
setAuthMethod(null);
97
-
}, []);
98
99
useEffect(() => {
100
const handleOAuthSessionDeleted = (
···
105
setOauthSession(null);
106
setAuthMethod(null);
107
setStatus("signedOut");
108
};
109
110
oauthClient.addEventListener("deleted", handleOAuthSessionDeleted as EventListener);
···
113
return () => {
114
oauthClient.removeEventListener("deleted", handleOAuthSessionDeleted as EventListener);
115
};
116
-
}, [initialize]);
117
118
-
// --- Login Methods ---
119
const loginWithPassword = async (
120
user: string,
121
password: string,
···
125
setStatus("loading");
126
try {
127
let sessionData: AtpSessionData | undefined;
128
-
// Use the AtpAgent for its simple login and session persistence
129
const apiAgent = new AtpAgent({
130
service,
131
persistSession: (_evt, sess) => {
···
137
if (sessionData) {
138
localStorage.setItem("service", service);
139
localStorage.setItem("sess", JSON.stringify(sessionData));
140
-
setAgent(apiAgent); // Store the AtpAgent instance in our state
141
setAuthMethod("password");
142
setStatus("signedIn");
143
// /*mass comment*/ console.log("Successfully logged in with password.");
144
} else {
145
throw new Error("Session data not persisted after login.");
···
147
} catch (e) {
148
console.error("Password login failed:", e);
149
setStatus("signedOut");
150
throw e;
151
}
152
};
···
161
}
162
}, [status]);
163
164
-
// --- Unified Logout ---
165
const logout = useCallback(async () => {
166
if (status !== "signedIn" || !agent) return;
167
setStatus("loading");
···
173
} else if (authMethod === "password") {
174
localStorage.removeItem("service");
175
localStorage.removeItem("sess");
176
-
// AtpAgent has its own logout methods
177
await (agent as AtpAgent).com.atproto.server.deleteSession();
178
// /*mass comment*/ console.log("Password-based session deleted.");
179
}
···
184
setAuthMethod(null);
185
setOauthSession(null);
186
setStatus("signedOut");
187
}
188
-
}, [status, authMethod, agent, oauthSession]);
189
190
return (
191
<AuthContext
···
1
import { Agent, AtpAgent, type AtpSessionData } from "@atproto/api";
2
import {
3
type OAuthSession,
···
5
TokenRefreshError,
6
TokenRevokedError,
7
} from "@atproto/oauth-client-browser";
8
+
import { useAtom } from "jotai";
9
import React, {
10
createContext,
11
use,
···
14
useState,
15
} from "react";
16
17
+
import { quickAuthAtom } from "~/utils/atoms";
18
+
19
+
import { oauthClient } from "../utils/oauthClient";
20
21
type AuthStatus = "loading" | "signedIn" | "signedOut";
22
type AuthMethod = "password" | "oauth" | null;
23
24
interface AuthContextValue {
25
+
agent: Agent | null;
26
status: AuthStatus;
27
authMethod: AuthMethod;
28
loginWithPassword: (
···
41
}: {
42
children: React.ReactNode;
43
}) => {
44
const [agent, setAgent] = useState<Agent | null>(null);
45
const [status, setStatus] = useState<AuthStatus>("loading");
46
const [authMethod, setAuthMethod] = useState<AuthMethod>(null);
47
const [oauthSession, setOauthSession] = useState<OAuthSession | null>(null);
48
+
const [quickAuth, setQuickAuth] = useAtom(quickAuthAtom);
49
50
const initialize = useCallback(async () => {
51
try {
52
const oauthResult = await oauthClient.init();
53
if (oauthResult) {
54
// /*mass comment*/ console.log("OAuth session restored.");
55
+
const apiAgent = new Agent(oauthResult.session);
56
setAgent(apiAgent);
57
setOauthSession(oauthResult.session);
58
setAuthMethod("oauth");
59
setStatus("signedIn");
60
+
setQuickAuth(apiAgent?.did || null);
61
+
return;
62
}
63
} catch (e) {
64
console.error("OAuth init failed, checking password session.", e);
65
+
if (!quickAuth) {
66
+
// quickAuth restoration. if last used method is oauth we immediately call for oauth redo
67
+
// (and set a persistent atom somewhere to not retry again if it failed)
68
+
}
69
}
70
71
try {
72
const service = localStorage.getItem("service");
73
const sessionString = localStorage.getItem("sess");
74
75
if (service && sessionString) {
76
// /*mass comment*/ console.log("Resuming password-based session using AtpAgent...");
77
const apiAgent = new AtpAgent({ service });
78
const session: AtpSessionData = JSON.parse(sessionString);
79
await apiAgent.resumeSession(session);
80
81
// /*mass comment*/ console.log("Password-based session resumed successfully.");
82
+
setAgent(apiAgent);
83
setAuthMethod("password");
84
setStatus("signedIn");
85
+
setQuickAuth(apiAgent?.did || null);
86
+
return;
87
}
88
} catch (e) {
89
console.error("Failed to resume password-based session.", e);
···
91
localStorage.removeItem("service");
92
}
93
94
// /*mass comment*/ console.log("No active session found.");
95
setStatus("signedOut");
96
setAgent(null);
97
setAuthMethod(null);
98
+
// do we want to null it here?
99
+
setQuickAuth(null);
100
+
}, [quickAuth, setQuickAuth]);
101
102
useEffect(() => {
103
const handleOAuthSessionDeleted = (
···
108
setOauthSession(null);
109
setAuthMethod(null);
110
setStatus("signedOut");
111
+
setQuickAuth(null);
112
};
113
114
oauthClient.addEventListener("deleted", handleOAuthSessionDeleted as EventListener);
···
117
return () => {
118
oauthClient.removeEventListener("deleted", handleOAuthSessionDeleted as EventListener);
119
};
120
+
}, [initialize, setQuickAuth]);
121
122
const loginWithPassword = async (
123
user: string,
124
password: string,
···
128
setStatus("loading");
129
try {
130
let sessionData: AtpSessionData | undefined;
131
const apiAgent = new AtpAgent({
132
service,
133
persistSession: (_evt, sess) => {
···
139
if (sessionData) {
140
localStorage.setItem("service", service);
141
localStorage.setItem("sess", JSON.stringify(sessionData));
142
+
setAgent(apiAgent);
143
setAuthMethod("password");
144
setStatus("signedIn");
145
+
setQuickAuth(apiAgent?.did || null);
146
// /*mass comment*/ console.log("Successfully logged in with password.");
147
} else {
148
throw new Error("Session data not persisted after login.");
···
150
} catch (e) {
151
console.error("Password login failed:", e);
152
setStatus("signedOut");
153
+
setQuickAuth(null);
154
throw e;
155
}
156
};
···
165
}
166
}, [status]);
167
168
const logout = useCallback(async () => {
169
if (status !== "signedIn" || !agent) return;
170
setStatus("loading");
···
176
} else if (authMethod === "password") {
177
localStorage.removeItem("service");
178
localStorage.removeItem("sess");
179
await (agent as AtpAgent).com.atproto.server.deleteSession();
180
// /*mass comment*/ console.log("Password-based session deleted.");
181
}
···
186
setAuthMethod(null);
187
setOauthSession(null);
188
setStatus("signedOut");
189
+
setQuickAuth(null);
190
}
191
+
}, [status, agent, authMethod, oauthSession, setQuickAuth]);
192
193
return (
194
<AuthContext
+186
-5
src/routeTree.gen.ts
+186
-5
src/routeTree.gen.ts
···
12
import { Route as SettingsRouteImport } from './routes/settings'
13
import { Route as SearchRouteImport } from './routes/search'
14
import { Route as NotificationsRouteImport } from './routes/notifications'
15
import { Route as FeedsRouteImport } from './routes/feeds'
16
import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout'
17
import { Route as IndexRouteImport } from './routes/index'
18
import { Route as CallbackIndexRouteImport } from './routes/callback/index'
19
import { Route as PathlessLayoutNestedLayoutRouteImport } from './routes/_pathlessLayout/_nested-layout'
20
import { Route as ProfileDidIndexRouteImport } from './routes/profile.$did/index'
21
import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
22
import { Route as PathlessLayoutNestedLayoutRouteARouteImport } from './routes/_pathlessLayout/_nested-layout/route-a'
23
import { Route as ProfileDidPostRkeyRouteImport } from './routes/profile.$did/post.$rkey'
24
25
const SettingsRoute = SettingsRouteImport.update({
26
id: '/settings',
···
37
path: '/notifications',
38
getParentRoute: () => rootRouteImport,
39
} as any)
40
const FeedsRoute = FeedsRouteImport.update({
41
id: '/feeds',
42
path: '/feeds',
···
66
path: '/profile/$did/',
67
getParentRoute: () => rootRouteImport,
68
} as any)
69
const PathlessLayoutNestedLayoutRouteBRoute =
70
PathlessLayoutNestedLayoutRouteBRouteImport.update({
71
id: '/route-b',
···
83
path: '/profile/$did/post/$rkey',
84
getParentRoute: () => rootRouteImport,
85
} as any)
86
87
export interface FileRoutesByFullPath {
88
'/': typeof IndexRoute
89
'/feeds': typeof FeedsRoute
90
'/notifications': typeof NotificationsRoute
91
'/search': typeof SearchRoute
92
'/settings': typeof SettingsRoute
93
'/callback': typeof CallbackIndexRoute
94
'/route-a': typeof PathlessLayoutNestedLayoutRouteARoute
95
'/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
96
'/profile/$did': typeof ProfileDidIndexRoute
97
-
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRoute
98
}
99
export interface FileRoutesByTo {
100
'/': typeof IndexRoute
101
'/feeds': typeof FeedsRoute
102
'/notifications': typeof NotificationsRoute
103
'/search': typeof SearchRoute
104
'/settings': typeof SettingsRoute
105
'/callback': typeof CallbackIndexRoute
106
'/route-a': typeof PathlessLayoutNestedLayoutRouteARoute
107
'/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
108
'/profile/$did': typeof ProfileDidIndexRoute
109
-
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRoute
110
}
111
export interface FileRoutesById {
112
__root__: typeof rootRouteImport
113
'/': typeof IndexRoute
114
'/_pathlessLayout': typeof PathlessLayoutRouteWithChildren
115
'/feeds': typeof FeedsRoute
116
'/notifications': typeof NotificationsRoute
117
'/search': typeof SearchRoute
118
'/settings': typeof SettingsRoute
···
120
'/callback/': typeof CallbackIndexRoute
121
'/_pathlessLayout/_nested-layout/route-a': typeof PathlessLayoutNestedLayoutRouteARoute
122
'/_pathlessLayout/_nested-layout/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
123
'/profile/$did/': typeof ProfileDidIndexRoute
124
-
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRoute
125
}
126
export interface FileRouteTypes {
127
fileRoutesByFullPath: FileRoutesByFullPath
128
fullPaths:
129
| '/'
130
| '/feeds'
131
| '/notifications'
132
| '/search'
133
| '/settings'
134
| '/callback'
135
| '/route-a'
136
| '/route-b'
137
| '/profile/$did'
138
| '/profile/$did/post/$rkey'
139
fileRoutesByTo: FileRoutesByTo
140
to:
141
| '/'
142
| '/feeds'
143
| '/notifications'
144
| '/search'
145
| '/settings'
146
| '/callback'
147
| '/route-a'
148
| '/route-b'
149
| '/profile/$did'
150
| '/profile/$did/post/$rkey'
151
id:
152
| '__root__'
153
| '/'
154
| '/_pathlessLayout'
155
| '/feeds'
156
| '/notifications'
157
| '/search'
158
| '/settings'
···
160
| '/callback/'
161
| '/_pathlessLayout/_nested-layout/route-a'
162
| '/_pathlessLayout/_nested-layout/route-b'
163
| '/profile/$did/'
164
| '/profile/$did/post/$rkey'
165
fileRoutesById: FileRoutesById
166
}
167
export interface RootRouteChildren {
168
IndexRoute: typeof IndexRoute
169
PathlessLayoutRoute: typeof PathlessLayoutRouteWithChildren
170
FeedsRoute: typeof FeedsRoute
171
NotificationsRoute: typeof NotificationsRoute
172
SearchRoute: typeof SearchRoute
173
SettingsRoute: typeof SettingsRoute
174
CallbackIndexRoute: typeof CallbackIndexRoute
175
ProfileDidIndexRoute: typeof ProfileDidIndexRoute
176
-
ProfileDidPostRkeyRoute: typeof ProfileDidPostRkeyRoute
177
}
178
179
declare module '@tanstack/react-router' {
···
197
path: '/notifications'
198
fullPath: '/notifications'
199
preLoaderRoute: typeof NotificationsRouteImport
200
parentRoute: typeof rootRouteImport
201
}
202
'/feeds': {
···
241
preLoaderRoute: typeof ProfileDidIndexRouteImport
242
parentRoute: typeof rootRouteImport
243
}
244
'/_pathlessLayout/_nested-layout/route-b': {
245
id: '/_pathlessLayout/_nested-layout/route-b'
246
path: '/route-b'
···
262
preLoaderRoute: typeof ProfileDidPostRkeyRouteImport
263
parentRoute: typeof rootRouteImport
264
}
265
}
266
}
267
···
295
PathlessLayoutRouteChildren,
296
)
297
298
const rootRouteChildren: RootRouteChildren = {
299
IndexRoute: IndexRoute,
300
PathlessLayoutRoute: PathlessLayoutRouteWithChildren,
301
FeedsRoute: FeedsRoute,
302
NotificationsRoute: NotificationsRoute,
303
SearchRoute: SearchRoute,
304
SettingsRoute: SettingsRoute,
305
CallbackIndexRoute: CallbackIndexRoute,
306
ProfileDidIndexRoute: ProfileDidIndexRoute,
307
-
ProfileDidPostRkeyRoute: ProfileDidPostRkeyRoute,
308
}
309
export const routeTree = rootRouteImport
310
._addFileChildren(rootRouteChildren)
···
12
import { Route as SettingsRouteImport } from './routes/settings'
13
import { Route as SearchRouteImport } from './routes/search'
14
import { Route as NotificationsRouteImport } from './routes/notifications'
15
+
import { Route as ModerationRouteImport } from './routes/moderation'
16
import { Route as FeedsRouteImport } from './routes/feeds'
17
import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout'
18
import { Route as IndexRouteImport } from './routes/index'
19
import { Route as CallbackIndexRouteImport } from './routes/callback/index'
20
import { Route as PathlessLayoutNestedLayoutRouteImport } from './routes/_pathlessLayout/_nested-layout'
21
import { Route as ProfileDidIndexRouteImport } from './routes/profile.$did/index'
22
+
import { Route as ProfileDidFollowsRouteImport } from './routes/profile.$did/follows'
23
+
import { Route as ProfileDidFollowersRouteImport } from './routes/profile.$did/followers'
24
import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
25
import { Route as PathlessLayoutNestedLayoutRouteARouteImport } from './routes/_pathlessLayout/_nested-layout/route-a'
26
import { Route as ProfileDidPostRkeyRouteImport } from './routes/profile.$did/post.$rkey'
27
+
import { Route as ProfileDidFeedRkeyRouteImport } from './routes/profile.$did/feed.$rkey'
28
+
import { Route as ProfileDidPostRkeyRepostedByRouteImport } from './routes/profile.$did/post.$rkey.reposted-by'
29
+
import { Route as ProfileDidPostRkeyQuotesRouteImport } from './routes/profile.$did/post.$rkey.quotes'
30
+
import { Route as ProfileDidPostRkeyLikedByRouteImport } from './routes/profile.$did/post.$rkey.liked-by'
31
+
import { Route as ProfileDidPostRkeyImageIRouteImport } from './routes/profile.$did/post.$rkey.image.$i'
32
33
const SettingsRoute = SettingsRouteImport.update({
34
id: '/settings',
···
45
path: '/notifications',
46
getParentRoute: () => rootRouteImport,
47
} as any)
48
+
const ModerationRoute = ModerationRouteImport.update({
49
+
id: '/moderation',
50
+
path: '/moderation',
51
+
getParentRoute: () => rootRouteImport,
52
+
} as any)
53
const FeedsRoute = FeedsRouteImport.update({
54
id: '/feeds',
55
path: '/feeds',
···
79
path: '/profile/$did/',
80
getParentRoute: () => rootRouteImport,
81
} as any)
82
+
const ProfileDidFollowsRoute = ProfileDidFollowsRouteImport.update({
83
+
id: '/profile/$did/follows',
84
+
path: '/profile/$did/follows',
85
+
getParentRoute: () => rootRouteImport,
86
+
} as any)
87
+
const ProfileDidFollowersRoute = ProfileDidFollowersRouteImport.update({
88
+
id: '/profile/$did/followers',
89
+
path: '/profile/$did/followers',
90
+
getParentRoute: () => rootRouteImport,
91
+
} as any)
92
const PathlessLayoutNestedLayoutRouteBRoute =
93
PathlessLayoutNestedLayoutRouteBRouteImport.update({
94
id: '/route-b',
···
106
path: '/profile/$did/post/$rkey',
107
getParentRoute: () => rootRouteImport,
108
} as any)
109
+
const ProfileDidFeedRkeyRoute = ProfileDidFeedRkeyRouteImport.update({
110
+
id: '/profile/$did/feed/$rkey',
111
+
path: '/profile/$did/feed/$rkey',
112
+
getParentRoute: () => rootRouteImport,
113
+
} as any)
114
+
const ProfileDidPostRkeyRepostedByRoute =
115
+
ProfileDidPostRkeyRepostedByRouteImport.update({
116
+
id: '/reposted-by',
117
+
path: '/reposted-by',
118
+
getParentRoute: () => ProfileDidPostRkeyRoute,
119
+
} as any)
120
+
const ProfileDidPostRkeyQuotesRoute =
121
+
ProfileDidPostRkeyQuotesRouteImport.update({
122
+
id: '/quotes',
123
+
path: '/quotes',
124
+
getParentRoute: () => ProfileDidPostRkeyRoute,
125
+
} as any)
126
+
const ProfileDidPostRkeyLikedByRoute =
127
+
ProfileDidPostRkeyLikedByRouteImport.update({
128
+
id: '/liked-by',
129
+
path: '/liked-by',
130
+
getParentRoute: () => ProfileDidPostRkeyRoute,
131
+
} as any)
132
+
const ProfileDidPostRkeyImageIRoute =
133
+
ProfileDidPostRkeyImageIRouteImport.update({
134
+
id: '/image/$i',
135
+
path: '/image/$i',
136
+
getParentRoute: () => ProfileDidPostRkeyRoute,
137
+
} as any)
138
139
export interface FileRoutesByFullPath {
140
'/': typeof IndexRoute
141
'/feeds': typeof FeedsRoute
142
+
'/moderation': typeof ModerationRoute
143
'/notifications': typeof NotificationsRoute
144
'/search': typeof SearchRoute
145
'/settings': typeof SettingsRoute
146
'/callback': typeof CallbackIndexRoute
147
'/route-a': typeof PathlessLayoutNestedLayoutRouteARoute
148
'/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
149
+
'/profile/$did/followers': typeof ProfileDidFollowersRoute
150
+
'/profile/$did/follows': typeof ProfileDidFollowsRoute
151
'/profile/$did': typeof ProfileDidIndexRoute
152
+
'/profile/$did/feed/$rkey': typeof ProfileDidFeedRkeyRoute
153
+
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren
154
+
'/profile/$did/post/$rkey/liked-by': typeof ProfileDidPostRkeyLikedByRoute
155
+
'/profile/$did/post/$rkey/quotes': typeof ProfileDidPostRkeyQuotesRoute
156
+
'/profile/$did/post/$rkey/reposted-by': typeof ProfileDidPostRkeyRepostedByRoute
157
+
'/profile/$did/post/$rkey/image/$i': typeof ProfileDidPostRkeyImageIRoute
158
}
159
export interface FileRoutesByTo {
160
'/': typeof IndexRoute
161
'/feeds': typeof FeedsRoute
162
+
'/moderation': typeof ModerationRoute
163
'/notifications': typeof NotificationsRoute
164
'/search': typeof SearchRoute
165
'/settings': typeof SettingsRoute
166
'/callback': typeof CallbackIndexRoute
167
'/route-a': typeof PathlessLayoutNestedLayoutRouteARoute
168
'/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
169
+
'/profile/$did/followers': typeof ProfileDidFollowersRoute
170
+
'/profile/$did/follows': typeof ProfileDidFollowsRoute
171
'/profile/$did': typeof ProfileDidIndexRoute
172
+
'/profile/$did/feed/$rkey': typeof ProfileDidFeedRkeyRoute
173
+
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren
174
+
'/profile/$did/post/$rkey/liked-by': typeof ProfileDidPostRkeyLikedByRoute
175
+
'/profile/$did/post/$rkey/quotes': typeof ProfileDidPostRkeyQuotesRoute
176
+
'/profile/$did/post/$rkey/reposted-by': typeof ProfileDidPostRkeyRepostedByRoute
177
+
'/profile/$did/post/$rkey/image/$i': typeof ProfileDidPostRkeyImageIRoute
178
}
179
export interface FileRoutesById {
180
__root__: typeof rootRouteImport
181
'/': typeof IndexRoute
182
'/_pathlessLayout': typeof PathlessLayoutRouteWithChildren
183
'/feeds': typeof FeedsRoute
184
+
'/moderation': typeof ModerationRoute
185
'/notifications': typeof NotificationsRoute
186
'/search': typeof SearchRoute
187
'/settings': typeof SettingsRoute
···
189
'/callback/': typeof CallbackIndexRoute
190
'/_pathlessLayout/_nested-layout/route-a': typeof PathlessLayoutNestedLayoutRouteARoute
191
'/_pathlessLayout/_nested-layout/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
192
+
'/profile/$did/followers': typeof ProfileDidFollowersRoute
193
+
'/profile/$did/follows': typeof ProfileDidFollowsRoute
194
'/profile/$did/': typeof ProfileDidIndexRoute
195
+
'/profile/$did/feed/$rkey': typeof ProfileDidFeedRkeyRoute
196
+
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren
197
+
'/profile/$did/post/$rkey/liked-by': typeof ProfileDidPostRkeyLikedByRoute
198
+
'/profile/$did/post/$rkey/quotes': typeof ProfileDidPostRkeyQuotesRoute
199
+
'/profile/$did/post/$rkey/reposted-by': typeof ProfileDidPostRkeyRepostedByRoute
200
+
'/profile/$did/post/$rkey/image/$i': typeof ProfileDidPostRkeyImageIRoute
201
}
202
export interface FileRouteTypes {
203
fileRoutesByFullPath: FileRoutesByFullPath
204
fullPaths:
205
| '/'
206
| '/feeds'
207
+
| '/moderation'
208
| '/notifications'
209
| '/search'
210
| '/settings'
211
| '/callback'
212
| '/route-a'
213
| '/route-b'
214
+
| '/profile/$did/followers'
215
+
| '/profile/$did/follows'
216
| '/profile/$did'
217
+
| '/profile/$did/feed/$rkey'
218
| '/profile/$did/post/$rkey'
219
+
| '/profile/$did/post/$rkey/liked-by'
220
+
| '/profile/$did/post/$rkey/quotes'
221
+
| '/profile/$did/post/$rkey/reposted-by'
222
+
| '/profile/$did/post/$rkey/image/$i'
223
fileRoutesByTo: FileRoutesByTo
224
to:
225
| '/'
226
| '/feeds'
227
+
| '/moderation'
228
| '/notifications'
229
| '/search'
230
| '/settings'
231
| '/callback'
232
| '/route-a'
233
| '/route-b'
234
+
| '/profile/$did/followers'
235
+
| '/profile/$did/follows'
236
| '/profile/$did'
237
+
| '/profile/$did/feed/$rkey'
238
| '/profile/$did/post/$rkey'
239
+
| '/profile/$did/post/$rkey/liked-by'
240
+
| '/profile/$did/post/$rkey/quotes'
241
+
| '/profile/$did/post/$rkey/reposted-by'
242
+
| '/profile/$did/post/$rkey/image/$i'
243
id:
244
| '__root__'
245
| '/'
246
| '/_pathlessLayout'
247
| '/feeds'
248
+
| '/moderation'
249
| '/notifications'
250
| '/search'
251
| '/settings'
···
253
| '/callback/'
254
| '/_pathlessLayout/_nested-layout/route-a'
255
| '/_pathlessLayout/_nested-layout/route-b'
256
+
| '/profile/$did/followers'
257
+
| '/profile/$did/follows'
258
| '/profile/$did/'
259
+
| '/profile/$did/feed/$rkey'
260
| '/profile/$did/post/$rkey'
261
+
| '/profile/$did/post/$rkey/liked-by'
262
+
| '/profile/$did/post/$rkey/quotes'
263
+
| '/profile/$did/post/$rkey/reposted-by'
264
+
| '/profile/$did/post/$rkey/image/$i'
265
fileRoutesById: FileRoutesById
266
}
267
export interface RootRouteChildren {
268
IndexRoute: typeof IndexRoute
269
PathlessLayoutRoute: typeof PathlessLayoutRouteWithChildren
270
FeedsRoute: typeof FeedsRoute
271
+
ModerationRoute: typeof ModerationRoute
272
NotificationsRoute: typeof NotificationsRoute
273
SearchRoute: typeof SearchRoute
274
SettingsRoute: typeof SettingsRoute
275
CallbackIndexRoute: typeof CallbackIndexRoute
276
+
ProfileDidFollowersRoute: typeof ProfileDidFollowersRoute
277
+
ProfileDidFollowsRoute: typeof ProfileDidFollowsRoute
278
ProfileDidIndexRoute: typeof ProfileDidIndexRoute
279
+
ProfileDidFeedRkeyRoute: typeof ProfileDidFeedRkeyRoute
280
+
ProfileDidPostRkeyRoute: typeof ProfileDidPostRkeyRouteWithChildren
281
}
282
283
declare module '@tanstack/react-router' {
···
301
path: '/notifications'
302
fullPath: '/notifications'
303
preLoaderRoute: typeof NotificationsRouteImport
304
+
parentRoute: typeof rootRouteImport
305
+
}
306
+
'/moderation': {
307
+
id: '/moderation'
308
+
path: '/moderation'
309
+
fullPath: '/moderation'
310
+
preLoaderRoute: typeof ModerationRouteImport
311
parentRoute: typeof rootRouteImport
312
}
313
'/feeds': {
···
352
preLoaderRoute: typeof ProfileDidIndexRouteImport
353
parentRoute: typeof rootRouteImport
354
}
355
+
'/profile/$did/follows': {
356
+
id: '/profile/$did/follows'
357
+
path: '/profile/$did/follows'
358
+
fullPath: '/profile/$did/follows'
359
+
preLoaderRoute: typeof ProfileDidFollowsRouteImport
360
+
parentRoute: typeof rootRouteImport
361
+
}
362
+
'/profile/$did/followers': {
363
+
id: '/profile/$did/followers'
364
+
path: '/profile/$did/followers'
365
+
fullPath: '/profile/$did/followers'
366
+
preLoaderRoute: typeof ProfileDidFollowersRouteImport
367
+
parentRoute: typeof rootRouteImport
368
+
}
369
'/_pathlessLayout/_nested-layout/route-b': {
370
id: '/_pathlessLayout/_nested-layout/route-b'
371
path: '/route-b'
···
387
preLoaderRoute: typeof ProfileDidPostRkeyRouteImport
388
parentRoute: typeof rootRouteImport
389
}
390
+
'/profile/$did/feed/$rkey': {
391
+
id: '/profile/$did/feed/$rkey'
392
+
path: '/profile/$did/feed/$rkey'
393
+
fullPath: '/profile/$did/feed/$rkey'
394
+
preLoaderRoute: typeof ProfileDidFeedRkeyRouteImport
395
+
parentRoute: typeof rootRouteImport
396
+
}
397
+
'/profile/$did/post/$rkey/reposted-by': {
398
+
id: '/profile/$did/post/$rkey/reposted-by'
399
+
path: '/reposted-by'
400
+
fullPath: '/profile/$did/post/$rkey/reposted-by'
401
+
preLoaderRoute: typeof ProfileDidPostRkeyRepostedByRouteImport
402
+
parentRoute: typeof ProfileDidPostRkeyRoute
403
+
}
404
+
'/profile/$did/post/$rkey/quotes': {
405
+
id: '/profile/$did/post/$rkey/quotes'
406
+
path: '/quotes'
407
+
fullPath: '/profile/$did/post/$rkey/quotes'
408
+
preLoaderRoute: typeof ProfileDidPostRkeyQuotesRouteImport
409
+
parentRoute: typeof ProfileDidPostRkeyRoute
410
+
}
411
+
'/profile/$did/post/$rkey/liked-by': {
412
+
id: '/profile/$did/post/$rkey/liked-by'
413
+
path: '/liked-by'
414
+
fullPath: '/profile/$did/post/$rkey/liked-by'
415
+
preLoaderRoute: typeof ProfileDidPostRkeyLikedByRouteImport
416
+
parentRoute: typeof ProfileDidPostRkeyRoute
417
+
}
418
+
'/profile/$did/post/$rkey/image/$i': {
419
+
id: '/profile/$did/post/$rkey/image/$i'
420
+
path: '/image/$i'
421
+
fullPath: '/profile/$did/post/$rkey/image/$i'
422
+
preLoaderRoute: typeof ProfileDidPostRkeyImageIRouteImport
423
+
parentRoute: typeof ProfileDidPostRkeyRoute
424
+
}
425
}
426
}
427
···
455
PathlessLayoutRouteChildren,
456
)
457
458
+
interface ProfileDidPostRkeyRouteChildren {
459
+
ProfileDidPostRkeyLikedByRoute: typeof ProfileDidPostRkeyLikedByRoute
460
+
ProfileDidPostRkeyQuotesRoute: typeof ProfileDidPostRkeyQuotesRoute
461
+
ProfileDidPostRkeyRepostedByRoute: typeof ProfileDidPostRkeyRepostedByRoute
462
+
ProfileDidPostRkeyImageIRoute: typeof ProfileDidPostRkeyImageIRoute
463
+
}
464
+
465
+
const ProfileDidPostRkeyRouteChildren: ProfileDidPostRkeyRouteChildren = {
466
+
ProfileDidPostRkeyLikedByRoute: ProfileDidPostRkeyLikedByRoute,
467
+
ProfileDidPostRkeyQuotesRoute: ProfileDidPostRkeyQuotesRoute,
468
+
ProfileDidPostRkeyRepostedByRoute: ProfileDidPostRkeyRepostedByRoute,
469
+
ProfileDidPostRkeyImageIRoute: ProfileDidPostRkeyImageIRoute,
470
+
}
471
+
472
+
const ProfileDidPostRkeyRouteWithChildren =
473
+
ProfileDidPostRkeyRoute._addFileChildren(ProfileDidPostRkeyRouteChildren)
474
+
475
const rootRouteChildren: RootRouteChildren = {
476
IndexRoute: IndexRoute,
477
PathlessLayoutRoute: PathlessLayoutRouteWithChildren,
478
FeedsRoute: FeedsRoute,
479
+
ModerationRoute: ModerationRoute,
480
NotificationsRoute: NotificationsRoute,
481
SearchRoute: SearchRoute,
482
SettingsRoute: SettingsRoute,
483
CallbackIndexRoute: CallbackIndexRoute,
484
+
ProfileDidFollowersRoute: ProfileDidFollowersRoute,
485
+
ProfileDidFollowsRoute: ProfileDidFollowsRoute,
486
ProfileDidIndexRoute: ProfileDidIndexRoute,
487
+
ProfileDidFeedRkeyRoute: ProfileDidFeedRkeyRoute,
488
+
ProfileDidPostRkeyRoute: ProfileDidPostRkeyRouteWithChildren,
489
}
490
export const routeTree = rootRouteImport
491
._addFileChildren(rootRouteChildren)
+722
-455
src/routes/__root.tsx
+722
-455
src/routes/__root.tsx
···
2
3
// dont forget to run this
4
// npx @tanstack/router-cli generate
5
-
6
import type { QueryClient } from "@tanstack/react-query";
7
import {
8
createRootRouteWithContext,
9
-
Link,
10
-
Outlet,
11
Scripts,
12
useLocation,
13
useNavigate,
14
} from "@tanstack/react-router";
15
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
16
-
import { type SVGProps,useState } from "react";
17
import * as React from "react";
18
19
import { DefaultCatchBoundary } from "~/components/DefaultCatchBoundary";
20
import Login from "~/components/Login";
21
import { NotFound } from "~/components/NotFound";
22
import { UnifiedAuthProvider, useAuth } from "~/providers/UnifiedAuthProvider";
23
import { seo } from "~/utils/seo";
24
25
export const Route = createRootRouteWithContext<{
···
75
function RootComponent() {
76
return (
77
<UnifiedAuthProvider>
78
-
<RootDocument>
79
-
<Outlet />
80
-
</RootDocument>
81
</UnifiedAuthProvider>
82
);
83
}
84
85
function RootDocument({ children }: { children: React.ReactNode }) {
86
const location = useLocation();
87
const navigate = useNavigate();
88
const { agent } = useAuth();
89
const authed = !!agent?.did;
90
const isHome = location.pathname === "/";
91
const isNotifications = location.pathname.startsWith("/notifications");
92
-
const isProfile = agent && ((location.pathname === (`/profile/${agent?.did}`)) || (location.pathname === (`/profile/${encodeURIComponent(agent?.did??"")}`)));
93
94
-
const [postOpen, setPostOpen] = useState(false);
95
-
const [postText, setPostText] = useState("");
96
-
const [posting, setPosting] = useState(false);
97
-
const [postSuccess, setPostSuccess] = useState(false);
98
-
const [postError, setPostError] = useState<string | null>(null);
99
100
-
async function handlePost() {
101
-
if (!agent) return;
102
-
setPosting(true);
103
-
setPostError(null);
104
-
try {
105
-
await agent.com.atproto.repo.createRecord({
106
-
collection: "app.bsky.feed.post",
107
-
repo: agent.assertDid,
108
-
record: {
109
-
$type: "app.bsky.feed.post",
110
-
text: postText,
111
-
createdAt: new Date().toISOString(),
112
-
},
113
-
});
114
-
setPostSuccess(true);
115
-
setPostText("");
116
-
setTimeout(() => {
117
-
setPostSuccess(false);
118
-
setPostOpen(false);
119
-
}, 1500);
120
-
} catch (e: any) {
121
-
setPostError(e?.message || "Failed to post");
122
-
} finally {
123
-
setPosting(false);
124
-
}
125
-
}
126
127
return (
128
<>
129
-
{postOpen && (
130
-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
131
-
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-lg p-6 w-full max-w-md relative">
132
-
<button
133
-
className="absolute top-2 right-2 text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
134
-
onClick={() => !posting && setPostOpen(false)}
135
-
disabled={posting}
136
-
aria-label="Close"
137
-
>
138
-
ร
139
-
</button>
140
-
<h2 className="text-lg font-bold mb-2">Create Post</h2>
141
-
{postSuccess ? (
142
-
<div className="flex flex-col items-center justify-center py-8">
143
-
<span className="text-green-500 text-4xl mb-2">โ</span>
144
-
<span className="text-green-600">Posted!</span>
145
-
</div>
146
-
) : (
147
-
<>
148
-
<textarea
149
-
className="w-full border rounded p-2 mb-2 dark:bg-gray-800 dark:border-gray-700"
150
-
rows={4}
151
-
placeholder="What's on your mind?"
152
-
value={postText}
153
-
onChange={(e) => setPostText(e.target.value)}
154
-
disabled={posting}
155
-
autoFocus
156
-
/>
157
-
{postError && (
158
-
<div className="text-red-500 text-sm mb-2">{postError}</div>
159
-
)}
160
-
<button
161
-
className="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded disabled:opacity-50"
162
-
onClick={handlePost}
163
-
disabled={posting || !postText.trim()}
164
-
>
165
-
{posting ? "Posting..." : "Post"}
166
-
</button>
167
-
</>
168
-
)}
169
-
</div>
170
-
</div>
171
-
)}
172
173
<div className="min-h-screen flex justify-center bg-gray-50 dark:bg-gray-950">
174
-
<nav className="hidden lg:flex h-screen w-[250px] flex-col gap-2 p-4 dark:border-gray-800 sticky top-0 self-start">
175
<div className="flex items-center gap-3 mb-4">
176
-
<img src="/redstar.png" alt="Red Dwarf Logo" className="w-8 h-8" />
177
<span className="font-extrabold text-2xl tracking-tight text-gray-900 dark:text-gray-100">
178
Red Dwarf{" "}
179
{/* <span className="text-gray-500 dark:text-gray-400 text-sm">
···
181
</span> */}
182
</span>
183
</div>
184
-
<Link
185
to="/"
186
className={
187
`py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-900 text-xl flex items-center gap-3 ` +
188
(isHome ? "font-bold" : "")
189
}
190
>
191
-
{isHome ? (
192
-
<TablerHomeFilled width={28} height={28} />
193
) : (
194
-
<TablerHome width={28} height={28} />
195
)}
196
<span>Home</span>
197
</Link>
···
202
(isNotifications ? "font-bold" : "")
203
}
204
>
205
-
{isNotifications ? (
206
-
<TablerBellFilled width={28} height={28} />
207
) : (
208
-
<TablerBell width={28} height={28} />
209
)}
210
<span>Notifications</span>
211
</Link>
···
216
}`}
217
>
218
{location.pathname.startsWith("/feeds") ? (
219
-
<TablerHashtagFilled width={28} height={28} />
220
) : (
221
-
<TablerHashtag width={28} height={28} />
222
)}
223
<span>Feeds</span>
224
</Link>
···
230
}`}
231
>
232
{location.pathname.startsWith("/search") ? (
233
-
<TablerSearchFilled width={28} height={28} />
234
) : (
235
-
<TablerSearch width={28} height={28} />
236
)}
237
<span>Search</span>
238
</Link>
···
246
navigate({
247
to: "/profile/$did",
248
params: { did: agent.assertDid },
249
-
})
250
}
251
}}
252
type="button"
253
>
254
-
<TablerUserCircle width={28} height={28} />
255
<span>Profile</span>
256
</button>
257
<Link
···
260
location.pathname.startsWith("/settings") ? "font-bold" : ""
261
}`}
262
>
263
-
{location.pathname.startsWith("/settings") ? (
264
-
<IonSettingsSharp width={28} height={28} />
265
) : (
266
-
<IonSettings width={28} height={28} />
267
)}
268
<span>Settings</span>
269
-
</Link>
270
-
<button
271
className="mt-4 w-full flex items-center justify-center gap-3 py-3 px-0 mb-3 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100 text-xl font-bold rounded-full transition-colors shadow"
272
onClick={() => setPostOpen(true)}
273
type="button"
274
>
275
-
<TablerEdit
276
width={24}
277
height={24}
278
className="text-gray-600 dark:text-gray-400"
279
/>
280
<span>Post</span>
281
-
</button>
282
<div className="flex-1"></div>
283
<a
284
href="https://tangled.sh/@whey.party/red-dwarf"
···
309
</div>
310
</nav>
311
312
-
<button
313
-
className="lg:hidden fixed bottom-20 right-6 z-50 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-blue-600 dark:text-blue-400 rounded-full shadow-lg w-16 h-16 flex items-center justify-center border-4 border-white dark:border-gray-950 transition-all"
314
-
style={{ boxShadow: "0 4px 24px 0 rgba(0,0,0,0.12)" }}
315
-
onClick={() => setPostOpen(true)}
316
-
type="button"
317
-
aria-label="Create Post"
318
-
>
319
-
<TablerEdit
320
-
width={24}
321
-
height={24}
322
-
className="text-gray-600 dark:text-gray-400"
323
/>
324
-
</button>
325
326
-
<main className="w-full max-w-[600px] lg:border-x border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-950 pb-16 lg:pb-0">
327
-
<div className="lg:hidden flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-950">
328
-
<div className="flex items-center gap-2">
329
-
<img
330
-
src="/redstar.png"
331
-
alt="Red Dwarf Logo"
332
-
className="w-6 h-6"
333
-
/>
334
-
<span className="font-bold text-lg text-gray-900 dark:text-gray-100">
335
-
Red Dwarf{" "}
336
-
{/* <span className="text-gray-500 dark:text-gray-400 text-sm">
337
-
lite
338
-
</span> */}
339
-
</span>
340
-
</div>
341
-
<div className="flex items-center gap-2">
342
-
<Login compact={true} />
343
-
</div>
344
</div>
345
346
{children}
347
</main>
348
349
<aside className="hidden lg:flex h-screen w-[250px] sticky top-0 self-start flex-col">
350
<Login />
351
352
<div className="flex-1"></div>
353
<p className="text-xs text-gray-400 dark:text-gray-500 text-justify mx-4 mb-4">
354
-
Red Dwarf is a bluesky client that uses Constellation and direct PDS
355
-
queries. Skylite would be a self-hosted bluesky "instance". Stay
356
-
tuned for the release of Skylite.
357
</p>
358
</aside>
359
</div>
360
361
-
<nav className="lg:hidden fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-950 border-t border-gray-200 dark:border-gray-700 z-40">
362
-
<div className="flex justify-around items-center py-2">
363
-
<Link
364
-
to="/"
365
-
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
366
-
isHome
367
-
? "text-gray-900 dark:text-gray-100"
368
-
: "text-gray-600 dark:text-gray-400"
369
-
}`}
370
-
>
371
-
{isHome ? (
372
-
<TablerHomeFilled width={24} height={24} />
373
-
) : (
374
-
<TablerHome width={24} height={24} />
375
-
)}
376
-
<span className="text-xs mt-1">Home</span>
377
-
</Link>
378
-
<Link
379
-
to="/search"
380
-
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
381
-
location.pathname.startsWith("/search")
382
-
? "text-gray-900 dark:text-gray-100"
383
-
: "text-gray-600 dark:text-gray-400"
384
-
}`}
385
-
>
386
-
{location.pathname.startsWith("/search") ? (
387
-
<TablerSearchFilled width={24} height={24} />
388
-
) : (
389
-
<TablerSearch width={24} height={24} />
390
-
)}
391
-
<span className="text-xs mt-1">Search</span>
392
-
</Link>
393
-
<Link
394
-
to="/notifications"
395
-
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
396
-
isNotifications
397
-
? "text-gray-900 dark:text-gray-100"
398
-
: "text-gray-600 dark:text-gray-400"
399
-
}`}
400
-
>
401
-
{isNotifications ? (
402
-
<TablerBellFilled width={24} height={24} />
403
-
) : (
404
-
<TablerBell width={24} height={24} />
405
-
)}
406
-
<span className="text-xs mt-1">Notifications</span>
407
-
</Link>
408
-
<button
409
-
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
410
-
isProfile
411
-
? "text-gray-900 dark:text-gray-100"
412
-
: "text-gray-600 dark:text-gray-400"
413
-
}`}
414
-
onClick={() => {
415
-
if (authed && agent && agent.assertDid) {
416
-
//window.location.href = `/profile/${agent.assertDid}`;
417
navigate({
418
-
to: "/profile/$did",
419
-
params: { did: agent.assertDid },
420
})
421
}
422
-
}}
423
-
type="button"
424
-
>
425
-
<TablerUserCircle width={24} height={24} />
426
-
<span className="text-xs mt-1">Profile</span>
427
-
</button>
428
-
<Link
429
-
to="/settings"
430
-
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
431
-
location.pathname.startsWith("/settings")
432
-
? "text-gray-900 dark:text-gray-100"
433
-
: "text-gray-600 dark:text-gray-400"
434
-
}`}
435
-
>
436
-
{location.pathname.startsWith("/settings") ? (
437
-
<IonSettingsSharp width={24} height={24} />
438
-
) : (
439
-
<IonSettings width={24} height={24} />
440
-
)}
441
-
<span className="text-xs mt-1">Settings</span>
442
-
</Link>
443
</div>
444
-
</nav>
445
446
-
<TanStackRouterDevtools position="bottom-right" />
447
<Scripts />
448
</>
449
);
450
}
451
-
export function TablerHashtag(props: SVGProps<SVGSVGElement>) {
452
-
return (
453
-
<svg
454
-
xmlns="http://www.w3.org/2000/svg"
455
-
width={24}
456
-
height={24}
457
-
viewBox="0 0 24 24"
458
-
{...props}
459
-
>
460
-
<path
461
-
fill="none"
462
-
stroke="currentColor"
463
-
strokeLinecap="round"
464
-
strokeLinejoin="round"
465
-
strokeWidth={2}
466
-
d="M5 9h14M5 15h14M11 4L7 20M17 4l-4 16"
467
-
></path>
468
-
</svg>
469
-
);
470
-
}
471
472
-
export function TablerHashtagFilled(props: SVGProps<SVGSVGElement>) {
473
-
return (
474
-
<svg
475
-
xmlns="http://www.w3.org/2000/svg"
476
-
width={24}
477
-
height={24}
478
-
viewBox="0 0 24 24"
479
-
{...props}
480
-
>
481
-
<path
482
-
fill="none"
483
-
stroke="currentColor"
484
-
strokeLinecap="round"
485
-
strokeLinejoin="round"
486
-
strokeWidth={3}
487
-
d="M5 9h14M5 15h14M11 4L7 20M17 4l-4 16"
488
-
></path>
489
-
</svg>
490
-
);
491
-
}
492
-
export function TablerEdit(props: SVGProps<SVGSVGElement>) {
493
-
return (
494
-
<svg
495
-
xmlns="http://www.w3.org/2000/svg"
496
-
width={24}
497
-
height={24}
498
-
viewBox="0 0 24 24"
499
-
className="text-white"
500
-
{...props}
501
-
>
502
-
<g
503
-
fill="none"
504
-
stroke="currentColor"
505
-
strokeLinecap="round"
506
-
strokeLinejoin="round"
507
-
strokeWidth={2}
508
-
>
509
-
<path d="M16.475 5.408a2.36 2.36 0 1 1 3.34 3.34L7.5 21H3v-4.5z"></path>
510
-
</g>
511
-
</svg>
512
-
);
513
-
}
514
-
export function TablerHome(props: SVGProps<SVGSVGElement>) {
515
-
return (
516
-
<svg
517
-
xmlns="http://www.w3.org/2000/svg"
518
-
width={24}
519
-
height={24}
520
-
viewBox="0 0 24 24"
521
-
className="text-gray-900 dark:text-gray-100 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
522
-
{...props}
523
-
>
524
-
<g
525
-
stroke="currentColor"
526
-
strokeLinecap="round"
527
-
strokeLinejoin="round"
528
-
strokeWidth={2}
529
-
fill="none"
530
-
>
531
-
<path d="M5 12H3l9-9l9 9h-2M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-7"></path>
532
-
<path d="M9 21v-6a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v6"></path>
533
-
</g>
534
-
</svg>
535
-
);
536
-
}
537
-
export function TablerHomeFilled(props: SVGProps<SVGSVGElement>) {
538
-
return (
539
-
<svg
540
-
xmlns="http://www.w3.org/2000/svg"
541
-
width={24}
542
-
height={24}
543
-
viewBox="0 0 24 24"
544
-
className="text-gray-900 dark:text-gray-100 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
545
-
{...props}
546
-
>
547
-
<path
548
-
fill="currentColor"
549
-
d="m12.707 2.293l9 9c.63.63.184 1.707-.707 1.707h-1v6a3 3 0 0 1-3 3h-1v-7a3 3 0 0 0-2.824-2.995L13 12h-2a3 3 0 0 0-3 3v7H7a3 3 0 0 1-3-3v-6H3c-.89 0-1.337-1.077-.707-1.707l9-9a1 1 0 0 1 1.414 0M13 14a1 1 0 0 1 1 1v7h-4v-7a1 1 0 0 1 .883-.993L11 14z"
550
-
></path>
551
-
</svg>
552
-
);
553
-
}
554
-
555
-
export function TablerBell(props: SVGProps<SVGSVGElement>) {
556
-
return (
557
-
<svg
558
-
xmlns="http://www.w3.org/2000/svg"
559
-
width={24}
560
-
height={24}
561
-
viewBox="0 0 24 24"
562
-
{...props}
563
-
>
564
-
<path
565
-
className="text-gray-900 dark:text-gray-100 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
566
-
stroke="currentColor"
567
-
strokeLinecap="round"
568
-
strokeLinejoin="round"
569
-
strokeWidth={2}
570
-
d="M10 5a2 2 0 1 1 4 0a7 7 0 0 1 4 6v3a4 4 0 0 0 2 3H4a4 4 0 0 0 2-3v-3a7 7 0 0 1 4-6M9 17v1a3 3 0 0 0 6 0v-1"
571
-
></path>
572
-
</svg>
573
-
);
574
-
}
575
-
export function TablerBellFilled(props: SVGProps<SVGSVGElement>) {
576
-
return (
577
-
<svg
578
-
xmlns="http://www.w3.org/2000/svg"
579
-
width={24}
580
-
height={24}
581
-
viewBox="0 0 24 24"
582
-
className="text-gray-900 dark:text-gray-100 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
583
-
{...props}
584
-
>
585
-
<path
586
-
fill="currentColor"
587
-
stroke="currentColor"
588
-
d="M14.235 19c.865 0 1.322 1.024.745 1.668A4 4 0 0 1 12 22a4 4 0 0 1-2.98-1.332c-.552-.616-.158-1.579.634-1.661l.11-.006zM12 2c1.358 0 2.506.903 2.875 2.141l.046.171l.008.043a8.01 8.01 0 0 1 4.024 6.069l.028.287L19 11v2.931l.021.136a3 3 0 0 0 1.143 1.847l.167.117l.162.099c.86.487.56 1.766-.377 1.864L20 18H4c-1.028 0-1.387-1.364-.493-1.87a3 3 0 0 0 1.472-2.063L5 13.924l.001-2.97A8 8 0 0 1 8.822 4.5l.248-.146l.01-.043a3 3 0 0 1 2.562-2.29l.182-.017z"
589
-
></path>
590
-
</svg>
591
-
);
592
-
}
593
-
594
-
export function TablerUserCircle(props: SVGProps<SVGSVGElement>) {
595
-
return (
596
-
<svg
597
-
xmlns="http://www.w3.org/2000/svg"
598
-
width={24}
599
-
height={24}
600
-
viewBox="0 0 24 24"
601
-
className="text-gray-900 dark:text-gray-100 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
602
-
{...props}
603
-
>
604
-
<g
605
-
fill="none"
606
-
stroke="currentColor"
607
-
strokeLinecap="round"
608
-
strokeLinejoin="round"
609
-
strokeWidth={2}
610
>
611
-
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0-18 0"></path>
612
-
<path d="M9 10a3 3 0 1 0 6 0a3 3 0 1 0-6 0m-2.832 8.849A4 4 0 0 1 10 16h4a4 4 0 0 1 3.834 2.855"></path>
613
-
</g>
614
-
</svg>
615
-
);
616
-
}
617
618
-
export function TablerSearch(props: SVGProps<SVGSVGElement>) {
619
return (
620
-
<svg
621
-
xmlns="http://www.w3.org/2000/svg"
622
-
width={24}
623
-
height={24}
624
-
viewBox="0 0 24 24"
625
-
//className="text-gray-400 dark:text-gray-500"
626
-
{...props}
627
>
628
-
<g
629
-
fill="none"
630
-
stroke="currentColor"
631
-
strokeLinecap="round"
632
-
strokeLinejoin="round"
633
-
strokeWidth={2}
634
>
635
-
<path d="M3 10a7 7 0 1 0 14 0a7 7 0 1 0-14 0"></path>
636
-
<path d="m21 21l-6-6"></path>
637
-
</g>
638
-
</svg>
639
-
);
640
-
}
641
-
export function TablerSearchFilled(props: SVGProps<SVGSVGElement>) {
642
-
return (
643
-
<svg
644
-
xmlns="http://www.w3.org/2000/svg"
645
-
width={24}
646
-
height={24}
647
-
viewBox="0 0 24 24"
648
-
//className="text-gray-400 dark:text-gray-500"
649
-
{...props}
650
-
>
651
-
<g
652
-
fill="none"
653
-
stroke="currentColor"
654
-
strokeLinecap="round"
655
-
strokeLinejoin="round"
656
-
strokeWidth={3}
657
-
>
658
-
<path d="M3 10a7 7 0 1 0 14 0a7 7 0 1 0-14 0"></path>
659
-
<path d="m21 21l-6-6"></path>
660
-
</g>
661
-
</svg>
662
);
663
}
664
665
-
export function IonSettings(props: SVGProps<SVGSVGElement>) {
666
return (
667
-
<svg
668
-
xmlns="http://www.w3.org/2000/svg"
669
-
width={24}
670
-
height={24}
671
-
viewBox="0 0 512 512"
672
-
{...props}
673
>
674
-
<path
675
-
fill="none"
676
-
stroke="currentColor"
677
-
strokeLinecap="round"
678
-
strokeLinejoin="round"
679
-
strokeWidth={32}
680
-
d="M262.29 192.31a64 64 0 1 0 57.4 57.4a64.13 64.13 0 0 0-57.4-57.4M416.39 256a154 154 0 0 1-1.53 20.79l45.21 35.46a10.81 10.81 0 0 1 2.45 13.75l-42.77 74a10.81 10.81 0 0 1-13.14 4.59l-44.9-18.08a16.11 16.11 0 0 0-15.17 1.75A164.5 164.5 0 0 1 325 400.8a15.94 15.94 0 0 0-8.82 12.14l-6.73 47.89a11.08 11.08 0 0 1-10.68 9.17h-85.54a11.11 11.11 0 0 1-10.69-8.87l-6.72-47.82a16.07 16.07 0 0 0-9-12.22a155 155 0 0 1-21.46-12.57a16 16 0 0 0-15.11-1.71l-44.89 18.07a10.81 10.81 0 0 1-13.14-4.58l-42.77-74a10.8 10.8 0 0 1 2.45-13.75l38.21-30a16.05 16.05 0 0 0 6-14.08c-.36-4.17-.58-8.33-.58-12.5s.21-8.27.58-12.35a16 16 0 0 0-6.07-13.94l-38.19-30A10.81 10.81 0 0 1 49.48 186l42.77-74a10.81 10.81 0 0 1 13.14-4.59l44.9 18.08a16.11 16.11 0 0 0 15.17-1.75A164.5 164.5 0 0 1 187 111.2a15.94 15.94 0 0 0 8.82-12.14l6.73-47.89A11.08 11.08 0 0 1 213.23 42h85.54a11.11 11.11 0 0 1 10.69 8.87l6.72 47.82a16.07 16.07 0 0 0 9 12.22a155 155 0 0 1 21.46 12.57a16 16 0 0 0 15.11 1.71l44.89-18.07a10.81 10.81 0 0 1 13.14 4.58l42.77 74a10.8 10.8 0 0 1-2.45 13.75l-38.21 30a16.05 16.05 0 0 0-6.05 14.08c.33 4.14.55 8.3.55 12.47"
681
-
></path>
682
-
</svg>
683
-
);
684
-
}
685
-
export function IonSettingsSharp(props: SVGProps<SVGSVGElement>) {
686
-
return (
687
-
<svg
688
-
xmlns="http://www.w3.org/2000/svg"
689
-
width={24}
690
-
height={24}
691
-
viewBox="0 0 512 512"
692
-
{...props}
693
-
>
694
-
<path
695
-
fill="currentColor"
696
-
d="M256 176a80 80 0 1 0 80 80a80.24 80.24 0 0 0-80-80m172.72 80a165.5 165.5 0 0 1-1.64 22.34l48.69 38.12a11.59 11.59 0 0 1 2.63 14.78l-46.06 79.52a11.64 11.64 0 0 1-14.14 4.93l-57.25-23a176.6 176.6 0 0 1-38.82 22.67l-8.56 60.78a11.93 11.93 0 0 1-11.51 9.86h-92.12a12 12 0 0 1-11.51-9.53l-8.56-60.78A169.3 169.3 0 0 1 151.05 393L93.8 416a11.64 11.64 0 0 1-14.14-4.92L33.6 331.57a11.59 11.59 0 0 1 2.63-14.78l48.69-38.12A175 175 0 0 1 83.28 256a165.5 165.5 0 0 1 1.64-22.34l-48.69-38.12a11.59 11.59 0 0 1-2.63-14.78l46.06-79.52a11.64 11.64 0 0 1 14.14-4.93l57.25 23a176.6 176.6 0 0 1 38.82-22.67l8.56-60.78A11.93 11.93 0 0 1 209.94 26h92.12a12 12 0 0 1 11.51 9.53l8.56 60.78A169.3 169.3 0 0 1 361 119l57.2-23a11.64 11.64 0 0 1 14.14 4.92l46.06 79.52a11.59 11.59 0 0 1-2.63 14.78l-48.69 38.12a175 175 0 0 1 1.64 22.66"
697
-
></path>
698
-
</svg>
699
);
700
}
···
2
3
// dont forget to run this
4
// npx @tanstack/router-cli generate
5
import type { QueryClient } from "@tanstack/react-query";
6
import {
7
createRootRouteWithContext,
8
+
// Link,
9
+
// Outlet,
10
Scripts,
11
useLocation,
12
useNavigate,
13
} from "@tanstack/react-router";
14
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
15
+
import { useAtom } from "jotai";
16
import * as React from "react";
17
+
import { toast as sonnerToast } from "sonner";
18
+
import { Toaster } from "sonner";
19
+
import { KeepAliveOutlet, KeepAliveProvider } from "tanstack-router-keepalive";
20
21
+
import { Composer } from "~/components/Composer";
22
import { DefaultCatchBoundary } from "~/components/DefaultCatchBoundary";
23
+
import { Import } from "~/components/Import";
24
import Login from "~/components/Login";
25
import { NotFound } from "~/components/NotFound";
26
+
import { FluentEmojiHighContrastGlowingStar } from "~/components/Star";
27
+
import { LikeMutationQueueProvider } from "~/providers/LikeMutationQueueProvider";
28
import { UnifiedAuthProvider, useAuth } from "~/providers/UnifiedAuthProvider";
29
+
import { composerAtom, hueAtom, useAtomCssVar } from "~/utils/atoms";
30
import { seo } from "~/utils/seo";
31
32
export const Route = createRootRouteWithContext<{
···
82
function RootComponent() {
83
return (
84
<UnifiedAuthProvider>
85
+
<LikeMutationQueueProvider>
86
+
<RootDocument>
87
+
<KeepAliveProvider>
88
+
<AppToaster />
89
+
<KeepAliveOutlet />
90
+
</KeepAliveProvider>
91
+
</RootDocument>
92
+
</LikeMutationQueueProvider>
93
</UnifiedAuthProvider>
94
);
95
}
96
97
+
export function AppToaster() {
98
+
return (
99
+
<Toaster
100
+
position="bottom-center"
101
+
toastOptions={{
102
+
duration: 4000,
103
+
}}
104
+
/>
105
+
);
106
+
}
107
+
108
+
export function renderSnack({
109
+
title,
110
+
description,
111
+
button,
112
+
}: Omit<ToastProps, "id">) {
113
+
return sonnerToast.custom((id) => (
114
+
<Snack
115
+
id={id}
116
+
title={title}
117
+
description={description}
118
+
button={
119
+
button?.label
120
+
? {
121
+
label: button?.label,
122
+
onClick: () => {
123
+
button?.onClick?.();
124
+
},
125
+
}
126
+
: undefined
127
+
}
128
+
/>
129
+
));
130
+
}
131
+
132
+
function Snack(props: ToastProps) {
133
+
const { title, description, button, id } = props;
134
+
135
+
return (
136
+
<div
137
+
role="status"
138
+
aria-live="polite"
139
+
className="
140
+
w-full md:max-w-[520px]
141
+
flex items-center justify-between
142
+
rounded-md
143
+
px-4 py-3
144
+
shadow-sm
145
+
dark:bg-gray-300 dark:text-gray-900
146
+
bg-gray-700 text-gray-100
147
+
ring-1 dark:ring-gray-200 ring-gray-800
148
+
"
149
+
>
150
+
<div className="flex-1 min-w-0">
151
+
<p className="text-sm font-medium truncate">{title}</p>
152
+
{description ? (
153
+
<p className="mt-1 text-sm dark:text-gray-600 text-gray-300 truncate">
154
+
{description}
155
+
</p>
156
+
) : null}
157
+
</div>
158
+
159
+
{button ? (
160
+
<div className="ml-4 flex-shrink-0">
161
+
<button
162
+
className="
163
+
text-sm font-medium
164
+
px-3 py-1 rounded-md
165
+
bg-gray-200 text-gray-900
166
+
hover:bg-gray-300
167
+
dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700
168
+
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-300 dark:focus:ring-gray-700
169
+
"
170
+
onClick={() => {
171
+
button.onClick();
172
+
sonnerToast.dismiss(id);
173
+
}}
174
+
>
175
+
{button.label}
176
+
</button>
177
+
</div>
178
+
) : null}
179
+
<button className=" ml-4"
180
+
onClick={() => {
181
+
sonnerToast.dismiss(id);
182
+
}}
183
+
>
184
+
<IconMdiClose />
185
+
</button>
186
+
</div>
187
+
);
188
+
}
189
+
190
+
/* Types */
191
+
interface ToastProps {
192
+
id: string | number;
193
+
title: string;
194
+
description?: string;
195
+
button?: {
196
+
label: string;
197
+
onClick: () => void;
198
+
};
199
+
}
200
+
201
function RootDocument({ children }: { children: React.ReactNode }) {
202
+
useAtomCssVar(hueAtom, "--tw-gray-hue");
203
const location = useLocation();
204
const navigate = useNavigate();
205
const { agent } = useAuth();
206
const authed = !!agent?.did;
207
const isHome = location.pathname === "/";
208
const isNotifications = location.pathname.startsWith("/notifications");
209
+
const isProfile =
210
+
agent &&
211
+
(location.pathname === `/profile/${agent?.did}` ||
212
+
location.pathname === `/profile/${encodeURIComponent(agent?.did ?? "")}`);
213
+
const isSettings = location.pathname.startsWith("/settings");
214
+
const isSearch = location.pathname.startsWith("/search");
215
+
const isFeeds = location.pathname.startsWith("/feeds");
216
+
const isModeration = location.pathname.startsWith("/moderation");
217
218
+
const locationEnum:
219
+
| "feeds"
220
+
| "search"
221
+
| "settings"
222
+
| "notifications"
223
+
| "profile"
224
+
| "moderation"
225
+
| "home" = isFeeds
226
+
? "feeds"
227
+
: isSearch
228
+
? "search"
229
+
: isSettings
230
+
? "settings"
231
+
: isNotifications
232
+
? "notifications"
233
+
: isProfile
234
+
? "profile"
235
+
: isModeration
236
+
? "moderation"
237
+
: "home";
238
239
+
const [, setComposerPost] = useAtom(composerAtom);
240
241
return (
242
<>
243
+
<Composer />
244
245
<div className="min-h-screen flex justify-center bg-gray-50 dark:bg-gray-950">
246
+
<nav className="hidden lg:flex h-screen w-[250px] flex-col gap-0 p-4 dark:border-gray-800 sticky top-0 self-start">
247
<div className="flex items-center gap-3 mb-4">
248
+
<FluentEmojiHighContrastGlowingStar
249
+
className="h-8 w-8"
250
+
style={{
251
+
color:
252
+
"oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))",
253
+
}}
254
+
/>
255
<span className="font-extrabold text-2xl tracking-tight text-gray-900 dark:text-gray-100">
256
Red Dwarf{" "}
257
{/* <span className="text-gray-500 dark:text-gray-400 text-sm">
···
259
</span> */}
260
</span>
261
</div>
262
+
<MaterialNavItem
263
+
InactiveIcon={
264
+
<IconMaterialSymbolsHomeOutline className="w-6 h-6" />
265
+
}
266
+
ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />}
267
+
active={locationEnum === "home"}
268
+
onClickCallbback={() =>
269
+
navigate({
270
+
to: "/",
271
+
//params: { did: agent.assertDid },
272
+
})
273
+
}
274
+
text="Home"
275
+
/>
276
+
277
+
<MaterialNavItem
278
+
InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
279
+
ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
280
+
active={locationEnum === "search"}
281
+
onClickCallbback={() =>
282
+
navigate({
283
+
to: "/search",
284
+
//params: { did: agent.assertDid },
285
+
})
286
+
}
287
+
text="Explore"
288
+
/>
289
+
<MaterialNavItem
290
+
InactiveIcon={
291
+
<IconMaterialSymbolsNotificationsOutline className="w-6 h-6" />
292
+
}
293
+
ActiveIcon={
294
+
<IconMaterialSymbolsNotifications className="w-6 h-6" />
295
+
}
296
+
active={locationEnum === "notifications"}
297
+
onClickCallbback={() =>
298
+
navigate({
299
+
to: "/notifications",
300
+
//params: { did: agent.assertDid },
301
+
})
302
+
}
303
+
text="Notifications"
304
+
/>
305
+
<MaterialNavItem
306
+
InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
307
+
ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
308
+
active={locationEnum === "feeds"}
309
+
onClickCallbback={() =>
310
+
navigate({
311
+
to: "/feeds",
312
+
//params: { did: agent.assertDid },
313
+
})
314
+
}
315
+
text="Feeds"
316
+
/>
317
+
<MaterialNavItem
318
+
InactiveIcon={<IconMdiShieldOutline className="w-6 h-6" />}
319
+
ActiveIcon={<IconMdiShield className="w-6 h-6" />}
320
+
active={locationEnum === "moderation"}
321
+
onClickCallbback={() =>
322
+
navigate({
323
+
to: "/moderation",
324
+
//params: { did: agent.assertDid },
325
+
})
326
+
}
327
+
text="Moderation"
328
+
/>
329
+
<MaterialNavItem
330
+
InactiveIcon={
331
+
<IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" />
332
+
}
333
+
ActiveIcon={
334
+
<IconMaterialSymbolsAccountCircle className="w-6 h-6" />
335
+
}
336
+
active={locationEnum === "profile"}
337
+
onClickCallbback={() => {
338
+
if (authed && agent && agent.assertDid) {
339
+
//window.location.href = `/profile/${agent.assertDid}`;
340
+
navigate({
341
+
to: "/profile/$did",
342
+
params: { did: agent.assertDid },
343
+
});
344
+
}
345
+
}}
346
+
text="Profile"
347
+
/>
348
+
<MaterialNavItem
349
+
InactiveIcon={
350
+
<IconMaterialSymbolsSettingsOutline className="w-6 h-6" />
351
+
}
352
+
ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />}
353
+
active={locationEnum === "settings"}
354
+
onClickCallbback={() =>
355
+
navigate({
356
+
to: "/settings",
357
+
//params: { did: agent.assertDid },
358
+
})
359
+
}
360
+
text="Settings"
361
+
/>
362
+
<div className="flex flex-row items-center justify-center mt-3">
363
+
<MaterialPillButton
364
+
InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
365
+
ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
366
+
//active={true}
367
+
onClickCallbback={() => setComposerPost({ kind: "root" })}
368
+
text="Post"
369
+
/>
370
+
</div>
371
+
{/* <Link
372
to="/"
373
className={
374
`py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-900 text-xl flex items-center gap-3 ` +
375
(isHome ? "font-bold" : "")
376
}
377
>
378
+
{!isHome ? (
379
+
<IconMaterialSymbolsHomeOutline width={28} height={28} />
380
) : (
381
+
<IconMaterialSymbolsHome width={28} height={28} />
382
)}
383
<span>Home</span>
384
</Link>
···
389
(isNotifications ? "font-bold" : "")
390
}
391
>
392
+
{!isNotifications ? (
393
+
<IconMaterialSymbolsNotificationsOutline width={28} height={28} />
394
) : (
395
+
<IconMaterialSymbolsNotifications width={28} height={28} />
396
)}
397
<span>Notifications</span>
398
</Link>
···
403
}`}
404
>
405
{location.pathname.startsWith("/feeds") ? (
406
+
<IconMaterialSymbolsTag width={28} height={28} />
407
) : (
408
+
<IconMaterialSymbolsTag width={28} height={28} />
409
)}
410
<span>Feeds</span>
411
</Link>
···
417
}`}
418
>
419
{location.pathname.startsWith("/search") ? (
420
+
<IconMaterialSymbolsSearch width={28} height={28} />
421
) : (
422
+
<IconMaterialSymbolsSearch width={28} height={28} />
423
)}
424
<span>Search</span>
425
</Link>
···
433
navigate({
434
to: "/profile/$did",
435
params: { did: agent.assertDid },
436
+
});
437
}
438
}}
439
type="button"
440
>
441
+
{!isProfile ? (
442
+
<IconMaterialSymbolsAccountCircleOutline width={28} height={28} />
443
+
) : (
444
+
<IconMaterialSymbolsAccountCircle width={28} height={28} />
445
+
)}
446
<span>Profile</span>
447
</button>
448
<Link
···
451
location.pathname.startsWith("/settings") ? "font-bold" : ""
452
}`}
453
>
454
+
{!location.pathname.startsWith("/settings") ? (
455
+
<IconMaterialSymbolsSettingsOutline width={28} height={28} />
456
) : (
457
+
<IconMaterialSymbolsSettings width={28} height={28} />
458
)}
459
<span>Settings</span>
460
+
</Link> */}
461
+
{/* <button
462
className="mt-4 w-full flex items-center justify-center gap-3 py-3 px-0 mb-3 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100 text-xl font-bold rounded-full transition-colors shadow"
463
onClick={() => setPostOpen(true)}
464
type="button"
465
>
466
+
<IconMdiPencilOutline
467
width={24}
468
height={24}
469
className="text-gray-600 dark:text-gray-400"
470
/>
471
<span>Post</span>
472
+
</button> */}
473
<div className="flex-1"></div>
474
<a
475
href="https://tangled.sh/@whey.party/red-dwarf"
···
500
</div>
501
</nav>
502
503
+
<nav className="hidden sm:flex items-center lg:hidden h-screen flex-col gap-2 p-4 dark:border-gray-800 sticky top-0 self-start">
504
+
<div className="flex items-center gap-3 mb-4">
505
+
<FluentEmojiHighContrastGlowingStar
506
+
className="h-8 w-8"
507
+
style={{
508
+
color:
509
+
"oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))",
510
+
}}
511
+
/>
512
+
</div>
513
+
<MaterialNavItem
514
+
small
515
+
InactiveIcon={
516
+
<IconMaterialSymbolsHomeOutline className="w-6 h-6" />
517
+
}
518
+
ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />}
519
+
active={locationEnum === "home"}
520
+
onClickCallbback={() =>
521
+
navigate({
522
+
to: "/",
523
+
//params: { did: agent.assertDid },
524
+
})
525
+
}
526
+
text="Home"
527
/>
528
529
+
<MaterialNavItem
530
+
small
531
+
InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
532
+
ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
533
+
active={locationEnum === "search"}
534
+
onClickCallbback={() =>
535
+
navigate({
536
+
to: "/search",
537
+
//params: { did: agent.assertDid },
538
+
})
539
+
}
540
+
text="Explore"
541
+
/>
542
+
<MaterialNavItem
543
+
small
544
+
InactiveIcon={
545
+
<IconMaterialSymbolsNotificationsOutline className="w-6 h-6" />
546
+
}
547
+
ActiveIcon={
548
+
<IconMaterialSymbolsNotifications className="w-6 h-6" />
549
+
}
550
+
active={locationEnum === "notifications"}
551
+
onClickCallbback={() =>
552
+
navigate({
553
+
to: "/notifications",
554
+
//params: { did: agent.assertDid },
555
+
})
556
+
}
557
+
text="Notifications"
558
+
/>
559
+
<MaterialNavItem
560
+
small
561
+
InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
562
+
ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
563
+
active={locationEnum === "feeds"}
564
+
onClickCallbback={() =>
565
+
navigate({
566
+
to: "/feeds",
567
+
//params: { did: agent.assertDid },
568
+
})
569
+
}
570
+
text="Feeds"
571
+
/>
572
+
<MaterialNavItem
573
+
small
574
+
InactiveIcon={<IconMdiShieldOutline className="w-6 h-6" />}
575
+
ActiveIcon={<IconMdiShield className="w-6 h-6" />}
576
+
active={locationEnum === "moderation"}
577
+
onClickCallbback={() =>
578
+
navigate({
579
+
to: "/moderation",
580
+
//params: { did: agent.assertDid },
581
+
})
582
+
}
583
+
text="Moderation"
584
+
/>
585
+
<MaterialNavItem
586
+
small
587
+
InactiveIcon={
588
+
<IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" />
589
+
}
590
+
ActiveIcon={
591
+
<IconMaterialSymbolsAccountCircle className="w-6 h-6" />
592
+
}
593
+
active={locationEnum === "profile"}
594
+
onClickCallbback={() => {
595
+
if (authed && agent && agent.assertDid) {
596
+
//window.location.href = `/profile/${agent.assertDid}`;
597
+
navigate({
598
+
to: "/profile/$did",
599
+
params: { did: agent.assertDid },
600
+
});
601
+
}
602
+
}}
603
+
text="Profile"
604
+
/>
605
+
<MaterialNavItem
606
+
small
607
+
InactiveIcon={
608
+
<IconMaterialSymbolsSettingsOutline className="w-6 h-6" />
609
+
}
610
+
ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />}
611
+
active={locationEnum === "settings"}
612
+
onClickCallbback={() =>
613
+
navigate({
614
+
to: "/settings",
615
+
//params: { did: agent.assertDid },
616
+
})
617
+
}
618
+
text="Settings"
619
+
/>
620
+
<div className="flex flex-row items-center justify-center mt-3">
621
+
<MaterialPillButton
622
+
small
623
+
InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
624
+
ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
625
+
//active={true}
626
+
onClickCallbback={() => setComposerPost({ kind: "root" })}
627
+
text="Post"
628
+
/>
629
</div>
630
+
</nav>
631
632
+
{agent?.did && (
633
+
<button
634
+
className="lg:hidden fixed bottom-22 right-4 z-50 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 rounded-2xl w-14 h-14 flex items-center justify-center transition-all"
635
+
style={{ boxShadow: "0 4px 24px 0 rgba(0,0,0,0.12)" }}
636
+
onClick={() => setComposerPost({ kind: "root" })}
637
+
type="button"
638
+
aria-label="Create Post"
639
+
>
640
+
<IconMdiPencilOutline
641
+
width={24}
642
+
height={24}
643
+
className="text-gray-600 dark:text-gray-400"
644
+
/>
645
+
</button>
646
+
)}
647
+
648
+
<main className="w-full max-w-[600px] sm:border-x border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 pb-16 lg:pb-0 overflow-x-clip">
649
{children}
650
</main>
651
652
<aside className="hidden lg:flex h-screen w-[250px] sticky top-0 self-start flex-col">
653
+
<div className="px-4 pt-4">
654
+
<Import />
655
+
</div>
656
<Login />
657
658
<div className="flex-1"></div>
659
<p className="text-xs text-gray-400 dark:text-gray-500 text-justify mx-4 mb-4">
660
+
Red Dwarf is a Bluesky client that does not rely on any Bluesky API
661
+
App Servers. Instead, it uses Microcosm to fetch records directly
662
+
from each users' PDS (via Slingshot) and connect them using
663
+
backlinks (via Constellation)
664
</p>
665
</aside>
666
</div>
667
668
+
{agent?.did ? (
669
+
<nav className="sm:hidden fixed bottom-0 left-0 right-0 bg-gray-50 dark:bg-gray-900 border-0 shadow border-gray-200 dark:border-gray-700 z-40">
670
+
<div className="flex justify-around items-center p-2">
671
+
<MaterialNavItem
672
+
small
673
+
InactiveIcon={
674
+
<IconMaterialSymbolsHomeOutline className="w-6 h-6" />
675
+
}
676
+
ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />}
677
+
active={locationEnum === "home"}
678
+
onClickCallbback={() =>
679
+
navigate({
680
+
to: "/",
681
+
//params: { did: agent.assertDid },
682
+
})
683
+
}
684
+
text="Home"
685
+
/>
686
+
{/* <Link
687
+
to="/"
688
+
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
689
+
isHome
690
+
? "text-gray-900 dark:text-gray-100"
691
+
: "text-gray-600 dark:text-gray-400"
692
+
}`}
693
+
>
694
+
{!isHome ? (
695
+
<IconMaterialSymbolsHomeOutline width={24} height={24} />
696
+
) : (
697
+
<IconMaterialSymbolsHome width={24} height={24} />
698
+
)}
699
+
<span className="text-xs mt-1">Home</span>
700
+
</Link> */}
701
+
<MaterialNavItem
702
+
small
703
+
InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
704
+
ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
705
+
active={locationEnum === "search"}
706
+
onClickCallbback={() =>
707
+
navigate({
708
+
to: "/search",
709
+
//params: { did: agent.assertDid },
710
+
})
711
+
}
712
+
text="Explore"
713
+
/>
714
+
{/* <Link
715
+
to="/search"
716
+
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
717
+
location.pathname.startsWith("/search")
718
+
? "text-gray-900 dark:text-gray-100"
719
+
: "text-gray-600 dark:text-gray-400"
720
+
}`}
721
+
>
722
+
{!location.pathname.startsWith("/search") ? (
723
+
<IconMaterialSymbolsSearch width={24} height={24} />
724
+
) : (
725
+
<IconMaterialSymbolsSearch width={24} height={24} />
726
+
)}
727
+
<span className="text-xs mt-1">Search</span>
728
+
</Link> */}
729
+
<MaterialNavItem
730
+
small
731
+
InactiveIcon={
732
+
<IconMaterialSymbolsNotificationsOutline className="w-6 h-6" />
733
+
}
734
+
ActiveIcon={
735
+
<IconMaterialSymbolsNotifications className="w-6 h-6" />
736
+
}
737
+
active={locationEnum === "notifications"}
738
+
onClickCallbback={() =>
739
navigate({
740
+
to: "/notifications",
741
+
//params: { did: agent.assertDid },
742
})
743
}
744
+
text="Notifications"
745
+
/>
746
+
{/* <Link
747
+
to="/notifications"
748
+
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
749
+
isNotifications
750
+
? "text-gray-900 dark:text-gray-100"
751
+
: "text-gray-600 dark:text-gray-400"
752
+
}`}
753
+
>
754
+
{!isNotifications ? (
755
+
<IconMaterialSymbolsNotificationsOutline
756
+
width={24}
757
+
height={24}
758
+
/>
759
+
) : (
760
+
<IconMaterialSymbolsNotifications width={24} height={24} />
761
+
)}
762
+
<span className="text-xs mt-1">Notifications</span>
763
+
</Link> */}
764
+
<MaterialNavItem
765
+
small
766
+
InactiveIcon={
767
+
<IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" />
768
+
}
769
+
ActiveIcon={
770
+
<IconMaterialSymbolsAccountCircle className="w-6 h-6" />
771
+
}
772
+
active={locationEnum === "profile"}
773
+
onClickCallbback={() => {
774
+
if (authed && agent && agent.assertDid) {
775
+
//window.location.href = `/profile/${agent.assertDid}`;
776
+
navigate({
777
+
to: "/profile/$did",
778
+
params: { did: agent.assertDid },
779
+
});
780
+
}
781
+
}}
782
+
text="Profile"
783
+
/>
784
+
{/* <button
785
+
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
786
+
isProfile
787
+
? "text-gray-900 dark:text-gray-100"
788
+
: "text-gray-600 dark:text-gray-400"
789
+
}`}
790
+
onClick={() => {
791
+
if (authed && agent && agent.assertDid) {
792
+
//window.location.href = `/profile/${agent.assertDid}`;
793
+
navigate({
794
+
to: "/profile/$did",
795
+
params: { did: agent.assertDid },
796
+
});
797
+
}
798
+
}}
799
+
type="button"
800
+
>
801
+
<IconMaterialSymbolsAccountCircleOutline width={24} height={24} />
802
+
<span className="text-xs mt-1">Profile</span>
803
+
</button> */}
804
+
<MaterialNavItem
805
+
small
806
+
InactiveIcon={
807
+
<IconMaterialSymbolsSettingsOutline className="w-6 h-6" />
808
+
}
809
+
ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />}
810
+
active={locationEnum === "settings" || locationEnum === "feeds" || locationEnum === "moderation"}
811
+
onClickCallbback={() =>
812
+
navigate({
813
+
to: "/settings",
814
+
//params: { did: agent.assertDid },
815
+
})
816
+
}
817
+
text="Settings"
818
+
/>
819
+
{/* <Link
820
+
to="/settings"
821
+
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
822
+
location.pathname.startsWith("/settings")
823
+
? "text-gray-900 dark:text-gray-100"
824
+
: "text-gray-600 dark:text-gray-400"
825
+
}`}
826
+
>
827
+
{!location.pathname.startsWith("/settings") ? (
828
+
<IconMaterialSymbolsSettingsOutline width={24} height={24} />
829
+
) : (
830
+
<IconMaterialSymbolsSettings width={24} height={24} />
831
+
)}
832
+
<span className="text-xs mt-1">Settings</span>
833
+
</Link> */}
834
+
</div>
835
+
</nav>
836
+
) : (
837
+
<div className="lg:hidden flex items-center fixed bottom-0 left-0 right-0 justify-between px-4 py-3 border-0 shadow border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 z-10">
838
+
<div className="flex items-center gap-2">
839
+
<FluentEmojiHighContrastGlowingStar
840
+
className="h-6 w-6"
841
+
style={{
842
+
color:
843
+
"oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))",
844
+
}}
845
+
/>
846
+
<span className="font-bold text-lg text-gray-900 dark:text-gray-100">
847
+
Red Dwarf{" "}
848
+
{/* <span className="text-gray-500 dark:text-gray-400 text-sm">
849
+
lite
850
+
</span> */}
851
+
</span>
852
+
</div>
853
+
<div className="flex items-center gap-2">
854
+
<Login compact={true} popup={true} />
855
+
</div>
856
</div>
857
+
)}
858
859
+
<TanStackRouterDevtools position="bottom-left" />
860
<Scripts />
861
</>
862
);
863
}
864
865
+
export function MaterialNavItem({
866
+
InactiveIcon,
867
+
ActiveIcon,
868
+
text,
869
+
active,
870
+
onClickCallbback,
871
+
small,
872
+
}: {
873
+
InactiveIcon: React.ReactElement;
874
+
ActiveIcon: React.ReactElement;
875
+
text: string;
876
+
active: boolean;
877
+
onClickCallbback: () => void;
878
+
small?: boolean | string;
879
+
}) {
880
+
if (small)
881
+
return (
882
+
<button
883
+
className={`flex flex-col items-center rounded-lg transition-colors ${small} gap-1 ${
884
+
active
885
+
? "text-gray-900 dark:text-gray-100"
886
+
: "text-gray-600 dark:text-gray-400"
887
+
}`}
888
+
onClick={() => {
889
+
onClickCallbback();
890
+
}}
891
>
892
+
<div
893
+
className={`px-4 py-1 rounded-full flex items-center justify-center ${active ? " bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 hover:dark:bg-gray-700" : "hover:bg-gray-50 hover:dark:bg-gray-900"}`}
894
+
>
895
+
{active ? ActiveIcon : InactiveIcon}
896
+
</div>
897
+
<span
898
+
className={`text-[12.8px] text-roboto ${active ? "font-medium" : ""}`}
899
+
>
900
+
{text}
901
+
</span>
902
+
</button>
903
+
);
904
905
return (
906
+
<button
907
+
className={`flex flex-row h-12 min-h-12 max-h-12 px-4 py-0.5 w-full items-center rounded-full transition-colors flex-1 gap-1 ${
908
+
active
909
+
? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-800 bg-gray-200 hover:dark:bg-gray-700"
910
+
: "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-900"
911
+
}`}
912
+
onClick={() => {
913
+
onClickCallbback();
914
+
}}
915
>
916
+
<div className={`mr-4 ${active ? " " : " "}`}>
917
+
{active ? ActiveIcon : InactiveIcon}
918
+
</div>
919
+
<span
920
+
className={`text-[17px] text-roboto ${active ? "font-medium" : ""}`}
921
>
922
+
{text}
923
+
</span>
924
+
</button>
925
);
926
}
927
928
+
function MaterialPillButton({
929
+
InactiveIcon,
930
+
ActiveIcon,
931
+
text,
932
+
//active,
933
+
onClickCallbback,
934
+
small,
935
+
}: {
936
+
InactiveIcon: React.ReactElement;
937
+
ActiveIcon: React.ReactElement;
938
+
text: string;
939
+
//active: boolean;
940
+
onClickCallbback: () => void;
941
+
small?: boolean | string;
942
+
}) {
943
+
const active = false;
944
return (
945
+
<button
946
+
className={`flex border border-gray-400 dark:border-gray-400 flex-row h-12 min-h-12 max-h-12 ${small ? "p-3 w-12" : "px-4 py-0.5"} items-center rounded-full transition-colors gap-1 ${
947
+
active
948
+
? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-700 bg-gray-200 hover:dark:bg-gray-600"
949
+
: "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800"
950
+
}`}
951
+
onClick={() => {
952
+
onClickCallbback();
953
+
}}
954
>
955
+
<div className={`${!small && "mr-2"} ${active ? " " : " "}`}>
956
+
{active ? ActiveIcon : InactiveIcon}
957
+
</div>
958
+
{!small && (
959
+
<span
960
+
className={`text-[17px] text-roboto ${active ? "font-medium" : ""}`}
961
+
>
962
+
{text}
963
+
</span>
964
+
)}
965
+
</button>
966
);
967
}
+18
-1
src/routes/feeds.tsx
+18
-1
src/routes/feeds.tsx
···
1
import { createFileRoute } from "@tanstack/react-router";
2
3
+
import { Header } from "~/components/Header";
4
+
5
export const Route = createFileRoute("/feeds")({
6
component: Feeds,
7
});
8
9
export function Feeds() {
10
+
return (
11
+
<div className="">
12
+
<Header
13
+
title={`Feeds`}
14
+
backButtonCallback={() => {
15
+
if (window.history.length > 1) {
16
+
window.history.back();
17
+
} else {
18
+
window.location.assign("/");
19
+
}
20
+
}}
21
+
bottomBorderDisabled={true}
22
+
/>
23
+
Feeds page (coming soon)
24
+
</div>
25
+
);
26
}
+128
-135
src/routes/index.tsx
+128
-135
src/routes/index.tsx
···
1
import { createFileRoute } from "@tanstack/react-router";
2
import { useAtom } from "jotai";
3
import * as React from "react";
4
-
import { useEffect } from "react";
5
6
import { InfiniteCustomFeed } from "~/components/InfiniteCustomFeed";
7
import { useAuth } from "~/providers/UnifiedAuthProvider";
8
import {
9
-
agentAtom,
10
-
authedAtom,
11
-
feedScrollIndexAtom,
12
selectedFeedUriAtom,
13
-
store,
14
} from "~/utils/atoms";
15
//import { usePersistentStore } from "~/providers/PersistentStoreProvider";
16
import {
···
84
// );
85
// },
86
component: Home,
87
-
pendingComponent: PendingHome,
88
});
89
function PendingHome() {
90
return <div>loading... (prefetching your timeline)</div>;
91
}
92
-
function Home() {
93
const {
94
agent,
95
status,
···
100
} = useAuth();
101
const authed = !!agent?.did;
102
103
-
useEffect(() => {
104
-
if (agent?.did) {
105
-
store.set(authedAtom, true);
106
-
} else {
107
-
store.set(authedAtom, false);
108
-
}
109
-
}, [status, agent, authed]);
110
-
useEffect(() => {
111
-
if (agent) {
112
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
113
-
// @ts-ignore is it just me or is the type really weird here it should be Agent not AtpAgent
114
-
store.set(agentAtom, agent);
115
-
} else {
116
-
store.set(agentAtom, null);
117
-
}
118
-
}, [status, agent, authed]);
119
120
//const { get, set } = usePersistentStore();
121
// const [feed, setFeed] = React.useState<any[]>([]);
···
155
156
// const savedFeeds = savedFeedsPref?.items || [];
157
158
-
const identityresultmaybe = useQueryIdentity(agent?.did);
159
const identity = identityresultmaybe?.data;
160
161
const prefsresultmaybe = useQueryPreferences({
162
-
agent: agent ?? undefined,
163
-
pdsUrl: identity?.pds,
164
});
165
const prefs = prefsresultmaybe?.data;
166
···
171
return savedFeedsPref?.items || [];
172
}, [prefs]);
173
174
-
const [persistentSelectedFeed, setPersistentSelectedFeed] =
175
-
useAtom(selectedFeedUriAtom); // React.useState<string | null>(null);
176
-
const [unauthedSelectedFeed, setUnauthedSelectedFeed] = React.useState(
177
-
persistentSelectedFeed
178
-
); // React.useState<string | null>(null);
179
const selectedFeed = agent?.did
180
? persistentSelectedFeed
181
: unauthedSelectedFeed;
···
288
// };
289
// }, [authed, agent, loadering, selectedFeed, get, set]);
290
291
-
// const [scrollPositions, setScrollPositions] = useAtom(
292
-
// feedScrollPositionsAtom
293
-
// );
294
295
-
const [scrollIndexes] = useAtom(feedScrollIndexAtom);
296
297
-
//const latestVisibleIndexRef = React.useRef(0);
298
299
-
// const handleVisibleIndexChange = React.useCallback((index: number) => {
300
-
// latestVisibleIndexRef.current = index;
301
-
// }, []);
302
303
-
// React.useEffect(() => {
304
-
// // This return function is the cleanup effect.
305
-
// return () => {
306
-
// if (selectedFeed) {
307
-
// console.log(`Saving scroll index ${latestVisibleIndexRef.current} for feed ${selectedFeed}`);
308
-
// setScrollIndexes((prev) => ({
309
-
// ...prev,
310
-
// [selectedFeed]: latestVisibleIndexRef.current,
311
-
// }));
312
-
// }
313
-
// };
314
-
// }, [selectedFeed, setScrollIndexes]);
315
316
-
// useEffect(() => {
317
-
// const onScroll = () => {
318
-
// //if (!selectedFeed) return;
319
-
// scrollRef.current[selectedFeed ?? "null"] = window.scrollY;
320
-
// };
321
-
// window.addEventListener("scroll", onScroll, { passive: true });
322
-
// return () => window.removeEventListener("scroll", onScroll);
323
-
// }, [selectedFeed]);
324
-
// const [donerestored, setdonerestored] = React.useState(false);
325
326
-
// useEffect(() => {
327
-
// return () => {
328
-
// if (!donerestored) return;
329
-
// // /*mass comment*/ console.log("FEEDSCROLLSHIT saving at uhhh: ", scrollRef.current);
330
-
// //if (!selectedFeed) return;
331
-
// setScrollPositions((prev) => ({
332
-
// ...prev,
333
-
// [selectedFeed ?? "null"]:
334
-
// scrollRef.current[selectedFeed ?? "null"] ?? 0,
335
-
// }));
336
-
// };
337
-
// }, [selectedFeed, setScrollPositions, donerestored]);
338
339
-
// const [restoringScrollPosition, setRestoringScrollPosition] =
340
-
// React.useState(false);
341
342
-
// useLayoutEffect(() => {
343
-
// setRestoringScrollPosition(true);
344
-
// const savedPosition = scrollPositions[selectedFeed ?? "null"] ?? 0;
345
346
-
// const raf = requestAnimationFrame(() => {
347
-
// // setRestoringScrollPosition(true);
348
-
// // raf = requestAnimationFrame(() => {
349
-
// // window.scrollTo({ top: savedPosition, behavior: "instant" });
350
-
// // setRestoringScrollPosition(false);
351
-
// // setdonerestored(true);
352
-
// // });
353
-
// window.scrollTo({ top: savedPosition, behavior: "instant" });
354
-
// setRestoringScrollPosition(false);
355
-
// setdonerestored(true);
356
-
// });
357
-
358
-
// return () => cancelAnimationFrame(raf);
359
-
// }, [selectedFeed, scrollPositions]);
360
-
361
-
const feedGengetrecordquery = useQueryArbitrary(selectedFeed ?? undefined);
362
-
const feedServiceDid = (feedGengetrecordquery?.data?.value as any)?.did;
363
364
// const {
365
// data: feedData,
···
375
376
// const feed = feedData?.feed || [];
377
378
-
const isReadyForAuthedFeed =
379
-
authed && agent && identity?.pds && feedServiceDid;
380
-
const isReadyForUnauthedFeed = !authed && selectedFeed;
381
382
-
const savedIndex = selectedFeed ? scrollIndexes[selectedFeed] : 0;
383
384
return (
385
-
<div className="relative flex flex-col divide-y divide-gray-200 dark:divide-gray-800">
386
-
<div className="flex items-center gap-2 px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700 overflow-x-auto overflow-y-hidden scroll-thin">
387
-
{savedFeeds.length > 0 ? (
388
-
savedFeeds.map((item: any, idx: number) => {
389
-
const label = item.value.split("/").pop() || item.value;
390
-
const isActive = selectedFeed === item.value;
391
-
return (
392
-
<button
393
-
key={item.value || idx}
394
-
className={`px-3 py-1 rounded-full whitespace-nowrap font-medium transition-colors ${
395
-
isActive
396
-
? "bg-gray-500 text-white"
397
-
: item.pinned
398
-
? "bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-200"
399
-
: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-200"
400
-
}`}
401
-
onClick={() => setSelectedFeed(item.value)}
402
-
title={item.value}
403
-
>
404
-
{label}
405
-
{item.pinned && (
406
-
<span className="ml-1 text-xs text-gray-700 dark:text-gray-200">
407
-
โ
408
-
</span>
409
-
)}
410
-
</button>
411
-
);
412
-
})
413
-
) : (
414
-
<span className="text-xl font-bold ml-2">Home</span>
415
-
)}
416
-
</div>
417
{/* {isFeedLoading && <div className="p-4 text-gray-500">Loading...</div>}
418
{feedError && <div className="p-4 text-red-500">{feedError.message}</div>}
419
{!isFeedLoading && !feedError && feed.length === 0 && (
···
426
/>
427
))} */}
428
429
-
{authed && (!identity?.pds || !feedServiceDid) && (
430
<div className="p-4 text-center text-gray-500">
431
Preparing your feed...
432
</div>
433
)}
434
435
-
{isReadyForAuthedFeed || isReadyForUnauthedFeed ? (
436
<InfiniteCustomFeed
437
feedUri={selectedFeed!}
438
pdsUrl={identity?.pds}
439
feedServiceDid={feedServiceDid}
440
-
initialScrollIndex={savedIndex}
441
-
//onVisibleIndexChange={handleVisibleIndexChange}
442
/>
443
) : (
444
<div className="p-4 text-center text-gray-500">
445
-
Select a feed to get started.
446
</div>
447
)}
448
{/* {false && restoringScrollPosition && (
···
453
</div>
454
);
455
}
456
// not even used lmaooo
457
458
// export async function cachedResolveDIDWEBDOC({
···
1
import { createFileRoute } from "@tanstack/react-router";
2
import { useAtom } from "jotai";
3
import * as React from "react";
4
+
import { useLayoutEffect, useState } from "react";
5
6
+
import { Header } from "~/components/Header";
7
import { InfiniteCustomFeed } from "~/components/InfiniteCustomFeed";
8
import { useAuth } from "~/providers/UnifiedAuthProvider";
9
import {
10
+
feedScrollPositionsAtom,
11
+
isAtTopAtom,
12
+
quickAuthAtom,
13
selectedFeedUriAtom,
14
} from "~/utils/atoms";
15
//import { usePersistentStore } from "~/providers/PersistentStoreProvider";
16
import {
···
84
// );
85
// },
86
component: Home,
87
+
pendingComponent: PendingHome, // PendingHome,
88
+
staticData: { keepAlive: true },
89
});
90
function PendingHome() {
91
return <div>loading... (prefetching your timeline)</div>;
92
}
93
+
94
+
//function Homer() {
95
+
// return <div></div>
96
+
//}
97
+
export function Home({ hidden = false }: { hidden?: boolean }) {
98
const {
99
agent,
100
status,
···
105
} = useAuth();
106
const authed = !!agent?.did;
107
108
+
// i dont remember why this is even here
109
+
// useEffect(() => {
110
+
// if (agent?.did) {
111
+
// store.set(authedAtom, true);
112
+
// } else {
113
+
// store.set(authedAtom, false);
114
+
// }
115
+
// }, [status, agent, authed]);
116
+
// useEffect(() => {
117
+
// if (agent) {
118
+
// // eslint-disable-next-line @typescript-eslint/ban-ts-comment
119
+
// // @ts-ignore is it just me or is the type really weird here it should be Agent not AtpAgent
120
+
// store.set(agentAtom, agent);
121
+
// } else {
122
+
// store.set(agentAtom, null);
123
+
// }
124
+
// }, [status, agent, authed]);
125
126
//const { get, set } = usePersistentStore();
127
// const [feed, setFeed] = React.useState<any[]>([]);
···
161
162
// const savedFeeds = savedFeedsPref?.items || [];
163
164
+
const [quickAuth, setQuickAuth] = useAtom(quickAuthAtom);
165
+
const isAuthRestoring = quickAuth ? status === "loading" : false;
166
+
167
+
const identityresultmaybe = useQueryIdentity(!isAuthRestoring ? agent?.did : undefined);
168
const identity = identityresultmaybe?.data;
169
170
const prefsresultmaybe = useQueryPreferences({
171
+
agent: !isAuthRestoring ? (agent ?? undefined) : undefined,
172
+
pdsUrl: !isAuthRestoring ? (identity?.pds) : undefined,
173
});
174
const prefs = prefsresultmaybe?.data;
175
···
180
return savedFeedsPref?.items || [];
181
}, [prefs]);
182
183
+
const [persistentSelectedFeed, setPersistentSelectedFeed] = useAtom(selectedFeedUriAtom);
184
+
const [unauthedSelectedFeed, setUnauthedSelectedFeed] = useState(persistentSelectedFeed);
185
const selectedFeed = agent?.did
186
? persistentSelectedFeed
187
: unauthedSelectedFeed;
···
294
// };
295
// }, [authed, agent, loadering, selectedFeed, get, set]);
296
297
+
const [scrollPositions, setScrollPositions] = useAtom(
298
+
feedScrollPositionsAtom
299
+
);
300
301
+
const scrollPositionsRef = React.useRef(scrollPositions);
302
303
+
React.useEffect(() => {
304
+
scrollPositionsRef.current = scrollPositions;
305
+
}, [scrollPositions]);
306
307
+
useLayoutEffect(() => {
308
+
if (isAuthRestoring) return;
309
+
const savedPosition = scrollPositions[selectedFeed ?? "null"] ?? 0;
310
311
+
window.scrollTo({ top: savedPosition, behavior: "instant" });
312
+
// eslint-disable-next-line react-hooks/exhaustive-deps
313
+
}, [selectedFeed, isAuthRestoring]);
314
315
+
useLayoutEffect(() => {
316
+
if (!selectedFeed || isAuthRestoring) return;
317
318
+
const handleScroll = () => {
319
+
scrollPositionsRef.current = {
320
+
...scrollPositionsRef.current,
321
+
[selectedFeed]: window.scrollY,
322
+
};
323
+
};
324
325
+
window.addEventListener("scroll", handleScroll, { passive: true });
326
+
return () => {
327
+
window.removeEventListener("scroll", handleScroll);
328
329
+
setScrollPositions(scrollPositionsRef.current);
330
+
};
331
+
}, [isAuthRestoring, selectedFeed, setScrollPositions]);
332
333
+
const feedGengetrecordquery = useQueryArbitrary(!isAuthRestoring ? selectedFeed ?? undefined : undefined);
334
+
const feedServiceDid = !isAuthRestoring ? (feedGengetrecordquery?.data?.value as any)?.did as string | undefined : undefined;
335
336
// const {
337
// data: feedData,
···
347
348
// const feed = feedData?.feed || [];
349
350
+
const isReadyForAuthedFeed = !isAuthRestoring && authed && agent && identity?.pds && feedServiceDid;
351
+
const isReadyForUnauthedFeed = !isAuthRestoring && !authed && selectedFeed;
352
353
+
354
+
const [isAtTop] = useAtom(isAtTopAtom);
355
356
return (
357
+
<div
358
+
className={`relative flex flex-col divide-y divide-gray-200 dark:divide-gray-800 ${hidden && "hidden"}`}
359
+
>
360
+
{!isAuthRestoring && savedFeeds.length > 0 ? (
361
+
<div className={`flex items-center px-4 py-2 h-[52px] sticky top-0 bg-[var(--header-bg-light)] dark:bg-[var(--header-bg-dark)] ${!isAtTop && "shadow-sm"} sm:shadow-none sm:bg-white sm:dark:bg-gray-950 z-10 border-0 sm:border-b border-gray-200 dark:border-gray-700 overflow-x-auto overflow-y-hidden scroll-thin`}>
362
+
{savedFeeds.map((item: any, idx: number) => {return <FeedTabOnTop key={item} item={item} idx={idx} />})}
363
+
</div>
364
+
) : (
365
+
// <span className="text-xl font-bold ml-2">Home</span>
366
+
<Header title="Home" />
367
+
)}
368
{/* {isFeedLoading && <div className="p-4 text-gray-500">Loading...</div>}
369
{feedError && <div className="p-4 text-red-500">{feedError.message}</div>}
370
{!isFeedLoading && !feedError && feed.length === 0 && (
···
377
/>
378
))} */}
379
380
+
{isAuthRestoring || authed && (!identity?.pds || !feedServiceDid) && (
381
<div className="p-4 text-center text-gray-500">
382
Preparing your feed...
383
</div>
384
)}
385
386
+
{!isAuthRestoring && (isReadyForAuthedFeed || isReadyForUnauthedFeed) ? (
387
<InfiniteCustomFeed
388
+
key={selectedFeed!}
389
feedUri={selectedFeed!}
390
pdsUrl={identity?.pds}
391
feedServiceDid={feedServiceDid}
392
/>
393
) : (
394
<div className="p-4 text-center text-gray-500">
395
+
Loading.......
396
</div>
397
)}
398
{/* {false && restoringScrollPosition && (
···
403
</div>
404
);
405
}
406
+
407
+
408
+
// todo please use types this is dangerous very dangerous.
409
+
// todo fix this whenever proper preferences is handled
410
+
function FeedTabOnTop({item, idx}:{item: any, idx: number}) {
411
+
const [persistentSelectedFeed, setPersistentSelectedFeed] = useAtom(selectedFeedUriAtom);
412
+
const selectedFeed = persistentSelectedFeed
413
+
const setSelectedFeed = setPersistentSelectedFeed
414
+
const rkey = item.value.split("/").pop() || item.value;
415
+
const isActive = selectedFeed === item.value;
416
+
const { data: feedrecord } = useQueryArbitrary(item.value)
417
+
const label = feedrecord?.value?.displayName || rkey
418
+
return (
419
+
<button
420
+
key={item.value || idx}
421
+
className={`px-3 py-1 rounded-full whitespace-nowrap font-medium transition-colors ${
422
+
isActive
423
+
? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-700 bg-gray-200 hover:dark:bg-gray-600"
424
+
: "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800"
425
+
// ? "bg-gray-500 text-white"
426
+
// : item.pinned
427
+
// ? "bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-200"
428
+
// : "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-200"
429
+
}`}
430
+
onClick={() => setSelectedFeed(item.value)}
431
+
title={item.value}
432
+
>
433
+
{label}
434
+
{item.pinned && (
435
+
<span
436
+
className={`ml-1 text-xs ${
437
+
isActive
438
+
? "text-gray-900 dark:text-gray-100"
439
+
: "text-gray-600 dark:text-gray-400"
440
+
}`}
441
+
>
442
+
โ
443
+
</span>
444
+
)}
445
+
</button>
446
+
);
447
+
}
448
+
449
// not even used lmaooo
450
451
// export async function cachedResolveDIDWEBDOC({
+269
src/routes/moderation.tsx
+269
src/routes/moderation.tsx
···
···
1
+
import * as ATPAPI from "@atproto/api";
2
+
import {
3
+
isAdultContentPref,
4
+
isBskyAppStatePref,
5
+
isContentLabelPref,
6
+
isFeedViewPref,
7
+
isLabelersPref,
8
+
isMutedWordsPref,
9
+
isSavedFeedsPref,
10
+
} from "@atproto/api/dist/client/types/app/bsky/actor/defs";
11
+
import { createFileRoute } from "@tanstack/react-router";
12
+
import { useAtom } from "jotai";
13
+
import { Switch } from "radix-ui";
14
+
15
+
import { Header } from "~/components/Header";
16
+
import { useAuth } from "~/providers/UnifiedAuthProvider";
17
+
import { quickAuthAtom } from "~/utils/atoms";
18
+
import { useQueryIdentity, useQueryPreferences } from "~/utils/useQuery";
19
+
20
+
import { renderSnack } from "./__root";
21
+
import { NotificationItem } from "./notifications";
22
+
import { SettingHeading } from "./settings";
23
+
24
+
export const Route = createFileRoute("/moderation")({
25
+
component: RouteComponent,
26
+
});
27
+
28
+
function RouteComponent() {
29
+
const { agent } = useAuth();
30
+
31
+
const [quickAuth, setQuickAuth] = useAtom(quickAuthAtom);
32
+
const isAuthRestoring = quickAuth ? status === "loading" : false;
33
+
34
+
const identityresultmaybe = useQueryIdentity(
35
+
!isAuthRestoring ? agent?.did : undefined
36
+
);
37
+
const identity = identityresultmaybe?.data;
38
+
39
+
const prefsresultmaybe = useQueryPreferences({
40
+
agent: !isAuthRestoring ? (agent ?? undefined) : undefined,
41
+
pdsUrl: !isAuthRestoring ? identity?.pds : undefined,
42
+
});
43
+
const rawprefs = prefsresultmaybe?.data?.preferences as
44
+
| ATPAPI.AppBskyActorGetPreferences.OutputSchema["preferences"]
45
+
| undefined;
46
+
47
+
//console.log(JSON.stringify(prefs, null, 2))
48
+
49
+
const parsedPref = parsePreferences(rawprefs);
50
+
51
+
return (
52
+
<div>
53
+
<Header
54
+
title={`Moderation`}
55
+
backButtonCallback={() => {
56
+
if (window.history.length > 1) {
57
+
window.history.back();
58
+
} else {
59
+
window.location.assign("/");
60
+
}
61
+
}}
62
+
bottomBorderDisabled={true}
63
+
/>
64
+
{/* <SettingHeading title="Moderation Tools" />
65
+
<p>
66
+
todo: add all these:
67
+
<br />
68
+
- Interaction settings
69
+
<br />
70
+
- Muted words & tags
71
+
<br />
72
+
- Moderation lists
73
+
<br />
74
+
- Muted accounts
75
+
<br />
76
+
- Blocked accounts
77
+
<br />
78
+
- Verification settings
79
+
<br />
80
+
</p> */}
81
+
<SettingHeading title="Content Filters" />
82
+
<div>
83
+
<div className="flex items-center gap-4 px-4 py-2 border-b">
84
+
<label
85
+
htmlFor={`switch-${"hardcoded"}`}
86
+
className="flex flex-row flex-1"
87
+
>
88
+
<div className="flex flex-col">
89
+
<span className="text-md">{"Adult Content"}</span>
90
+
<span className="text-sm text-gray-500 dark:text-gray-400">
91
+
{"Enable adult content"}
92
+
</span>
93
+
</div>
94
+
</label>
95
+
96
+
<Switch.Root
97
+
id={`switch-${"hardcoded"}`}
98
+
checked={parsedPref?.adultContentEnabled}
99
+
onCheckedChange={(v) => {
100
+
renderSnack({
101
+
title: "Sorry... Modifying preferences is not implemented yet",
102
+
description: "You can use another app to change preferences",
103
+
//button: { label: 'Try Again', onClick: () => console.log('whatever') },
104
+
});
105
+
}}
106
+
className="m3switch root"
107
+
>
108
+
<Switch.Thumb className="m3switch thumb " />
109
+
</Switch.Root>
110
+
</div>
111
+
<div className="">
112
+
{Object.entries(parsedPref?.contentLabelPrefs ?? {}).map(
113
+
([label, visibility]) => (
114
+
<div
115
+
key={label}
116
+
className="flex justify-between border-b py-2 px-4"
117
+
>
118
+
<label
119
+
htmlFor={`switch-${"hardcoded"}`}
120
+
className="flex flex-row flex-1"
121
+
>
122
+
<div className="flex flex-col">
123
+
<span className="text-md">{label}</span>
124
+
<span className="text-sm text-gray-500 dark:text-gray-400">
125
+
{"uknown labeler"}
126
+
</span>
127
+
</div>
128
+
</label>
129
+
{/* <span className="text-md text-gray-500 dark:text-gray-400">
130
+
{visibility}
131
+
</span> */}
132
+
<TripleToggle
133
+
value={visibility as "ignore" | "warn" | "hide"}
134
+
/>
135
+
</div>
136
+
)
137
+
)}
138
+
</div>
139
+
</div>
140
+
<SettingHeading title="Advanced" />
141
+
{parsedPref?.labelers.map((labeler) => {
142
+
return (
143
+
<NotificationItem
144
+
key={labeler}
145
+
notification={labeler}
146
+
labeler={true}
147
+
/>
148
+
);
149
+
})}
150
+
</div>
151
+
);
152
+
}
153
+
154
+
export function TripleToggle({
155
+
value,
156
+
onChange,
157
+
}: {
158
+
value: "ignore" | "warn" | "hide";
159
+
onChange?: (newValue: "ignore" | "warn" | "hide") => void;
160
+
}) {
161
+
const options: Array<"ignore" | "warn" | "hide"> = ["ignore", "warn", "hide"];
162
+
return (
163
+
<div className="flex rounded-full bg-gray-200 dark:bg-gray-800 p-1 text-sm">
164
+
{options.map((opt) => {
165
+
const isActive = opt === value;
166
+
return (
167
+
<button
168
+
key={opt}
169
+
onClick={() => {
170
+
renderSnack({
171
+
title: "Sorry... Modifying preferences is not implemented yet",
172
+
description: "You can use another app to change preferences",
173
+
//button: { label: 'Try Again', onClick: () => console.log('whatever') },
174
+
});
175
+
onChange?.(opt);
176
+
}}
177
+
className={`flex-1 px-3 py-1.5 rounded-full transition-colors ${
178
+
isActive
179
+
? "bg-gray-400 dark:bg-gray-600 text-white"
180
+
: "text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-700"
181
+
}`}
182
+
>
183
+
{" "}
184
+
{opt.charAt(0).toUpperCase() + opt.slice(1)}
185
+
</button>
186
+
);
187
+
})}
188
+
</div>
189
+
);
190
+
}
191
+
192
+
type PrefItem =
193
+
ATPAPI.AppBskyActorGetPreferences.OutputSchema["preferences"][number];
194
+
195
+
export interface NormalizedPreferences {
196
+
contentLabelPrefs: Record<string, string>;
197
+
mutedWords: string[];
198
+
feedViewPrefs: Record<string, any>;
199
+
labelers: string[];
200
+
adultContentEnabled: boolean;
201
+
savedFeeds: {
202
+
pinned: string[];
203
+
saved: string[];
204
+
};
205
+
nuxs: string[];
206
+
}
207
+
208
+
export function parsePreferences(
209
+
prefs?: PrefItem[]
210
+
): NormalizedPreferences | undefined {
211
+
if (!prefs) return undefined;
212
+
const normalized: NormalizedPreferences = {
213
+
contentLabelPrefs: {},
214
+
mutedWords: [],
215
+
feedViewPrefs: {},
216
+
labelers: [],
217
+
adultContentEnabled: false,
218
+
savedFeeds: { pinned: [], saved: [] },
219
+
nuxs: [],
220
+
};
221
+
222
+
for (const pref of prefs) {
223
+
switch (pref.$type) {
224
+
case "app.bsky.actor.defs#contentLabelPref":
225
+
if (!isContentLabelPref(pref)) break;
226
+
normalized.contentLabelPrefs[pref.label] = pref.visibility;
227
+
break;
228
+
229
+
case "app.bsky.actor.defs#mutedWordsPref":
230
+
if (!isMutedWordsPref(pref)) break;
231
+
for (const item of pref.items ?? []) {
232
+
normalized.mutedWords.push(item.value);
233
+
}
234
+
break;
235
+
236
+
case "app.bsky.actor.defs#feedViewPref":
237
+
if (!isFeedViewPref(pref)) break;
238
+
normalized.feedViewPrefs[pref.feed] = pref;
239
+
break;
240
+
241
+
case "app.bsky.actor.defs#labelersPref":
242
+
if (!isLabelersPref(pref)) break;
243
+
normalized.labelers.push(...(pref.labelers?.map((l) => l.did) ?? []));
244
+
break;
245
+
246
+
case "app.bsky.actor.defs#adultContentPref":
247
+
if (!isAdultContentPref(pref)) break;
248
+
normalized.adultContentEnabled = !!pref.enabled;
249
+
break;
250
+
251
+
case "app.bsky.actor.defs#savedFeedsPref":
252
+
if (!isSavedFeedsPref(pref)) break;
253
+
normalized.savedFeeds.pinned.push(...(pref.pinned ?? []));
254
+
normalized.savedFeeds.saved.push(...(pref.saved ?? []));
255
+
break;
256
+
257
+
case "app.bsky.actor.defs#bskyAppStatePref":
258
+
if (!isBskyAppStatePref(pref)) break;
259
+
normalized.nuxs.push(...(pref.nuxs?.map((n) => n.id) ?? []));
260
+
break;
261
+
262
+
default:
263
+
// unknown pref type โ just ignore for now
264
+
break;
265
+
}
266
+
}
267
+
268
+
return normalized;
269
+
}
+644
-152
src/routes/notifications.tsx
+644
-152
src/routes/notifications.tsx
···
1
-
import { createFileRoute } from "@tanstack/react-router";
2
-
import React, { useEffect, useRef,useState } from "react";
3
4
import { useAuth } from "~/providers/UnifiedAuthProvider";
5
6
-
const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour
7
8
export const Route = createFileRoute("/notifications")({
9
component: NotificationsComponent,
10
});
11
12
-
function NotificationsComponent() {
13
-
// /*mass comment*/ console.log("NotificationsComponent render");
14
-
const { agent, status } = useAuth();
15
-
const authed = !!agent?.did;
16
-
const authLoading = status === "loading";
17
-
const [did, setDid] = useState<string | null>(null);
18
-
const [resolving, setResolving] = useState(false);
19
-
const [error, setError] = useState<string | null>(null);
20
-
const [responses, setResponses] = useState<any[]>([null, null, null]);
21
-
const [loading, setLoading] = useState(false);
22
-
const inputRef = useRef<HTMLInputElement>(null);
23
24
-
useEffect(() => {
25
-
if (authLoading) return;
26
-
if (authed && agent && agent.assertDid) {
27
-
setDid(agent.assertDid);
28
-
}
29
-
}, [authed, agent, authLoading]);
30
31
-
async function handleSubmit() {
32
-
// /*mass comment*/ console.log("handleSubmit called");
33
-
setError(null);
34
-
setResponses([null, null, null]);
35
-
const value = inputRef.current?.value?.trim() || "";
36
-
if (!value) return;
37
-
if (value.startsWith("did:")) {
38
-
setDid(value);
39
-
setError(null);
40
-
return;
41
-
}
42
-
setResolving(true);
43
-
const cacheKey = `handleDid:${value}`;
44
-
const now = Date.now();
45
-
const cached = undefined // await get(cacheKey);
46
-
// if (
47
-
// cached &&
48
-
// cached.value &&
49
-
// cached.time &&
50
-
// now - cached.time < HANDLE_DID_CACHE_TIMEOUT
51
-
// ) {
52
-
// try {
53
-
// const data = JSON.parse(cached.value);
54
-
// setDid(data.did);
55
-
// setResolving(false);
56
-
// return;
57
-
// } catch {}
58
-
// }
59
-
try {
60
-
const url = `https://free-fly-24.deno.dev/?handle=${encodeURIComponent(value)}`;
61
-
const res = await fetch(url);
62
-
if (!res.ok) throw new Error("Failed to resolve handle");
63
-
const data = await res.json();
64
-
//set(cacheKey, JSON.stringify(data));
65
-
setDid(data.did);
66
-
} catch (e: any) {
67
-
setError("Failed to resolve handle: " + (e?.message || e));
68
-
} finally {
69
-
setResolving(false);
70
-
}
71
-
}
72
73
-
useEffect(() => {
74
-
if (!did) return;
75
-
setLoading(true);
76
-
setError(null);
77
-
const urls = [
78
-
`https://constellation.microcosm.blue/links?target=${encodeURIComponent(did)}&collection=app.bsky.feed.post&path=.facets[app.bsky.richtext.facet].features[app.bsky.richtext.facet%23mention].did`,
79
-
`https://constellation.microcosm.blue/links?target=${encodeURIComponent(did)}&collection=app.bsky.feed.post&path=.facets[].features[app.bsky.richtext.facet%23mention].did`,
80
-
`https://constellation.microcosm.blue/links?target=${encodeURIComponent(did)}&collection=app.bsky.graph.follow&path=.subject`,
81
-
];
82
-
let ignore = false;
83
-
Promise.all(
84
-
urls.map(async (url) => {
85
-
try {
86
-
const r = await fetch(url);
87
-
if (!r.ok) throw new Error("Failed to fetch");
88
-
const text = await r.text();
89
-
if (!text) return null;
90
-
try {
91
-
return JSON.parse(text);
92
-
} catch {
93
-
return null;
94
}
95
-
} catch (e: any) {
96
-
return { error: e?.message || String(e) };
97
-
}
98
-
})
99
-
)
100
-
.then((results) => {
101
-
if (!ignore) setResponses(results);
102
-
})
103
-
.catch((e) => {
104
-
if (!ignore)
105
-
setError("Failed to fetch notifications: " + (e?.message || e));
106
-
})
107
-
.finally(() => {
108
-
if (!ignore) setLoading(false);
109
});
110
-
return () => {
111
-
ignore = true;
112
-
};
113
-
}, [did]);
114
115
return (
116
-
<div className="flex flex-col divide-y divide-gray-200 dark:divide-gray-800">
117
-
<div className="flex items-center gap-2 px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-800">
118
-
<span className="text-xl font-bold ml-2">Notifications</span>
119
-
{!authed && (
120
-
<div className="flex items-center gap-2">
121
-
<input
122
-
type="text"
123
-
placeholder="Enter handle or DID"
124
-
ref={inputRef}
125
-
className="ml-4 px-2 py-1 rounded border border-gray-300 dark:border-gray-700 bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100"
126
-
style={{ minWidth: 220 }}
127
-
disabled={resolving}
128
-
/>
129
-
<button
130
-
type="button"
131
-
className="px-3 py-1 rounded bg-blue-600 text-white font-semibold disabled:opacity-50"
132
-
disabled={resolving}
133
-
onClick={handleSubmit}
134
-
>
135
-
{resolving ? "Resolving..." : "Submit"}
136
-
</button>
137
-
</div>
138
-
)}
139
</div>
140
-
{error && <div className="p-4 text-red-500">{error}</div>}
141
-
{loading && (
142
-
<div className="p-4 text-gray-500">Loading notifications...</div>
143
)}
144
-
{!loading &&
145
-
!error &&
146
-
responses.map((resp, i) => (
147
-
<div key={i} className="p-4">
148
-
<div className="font-bold mb-2">Query {i + 1}</div>
149
-
{!resp ||
150
-
(typeof resp === "object" && Object.keys(resp).length === 0) ||
151
-
(Array.isArray(resp) && resp.length === 0) ? (
152
-
<div className="text-gray-500">No notifications found.</div>
153
-
) : (
154
-
<pre
155
-
style={{
156
-
background: "#222",
157
-
color: "#eee",
158
-
borderRadius: 8,
159
-
padding: 12,
160
-
fontSize: 13,
161
-
overflowX: "auto",
162
-
}}
163
-
>
164
-
{JSON.stringify(resp, null, 2)}
165
-
</pre>
166
-
)}
167
-
</div>
168
-
))}
169
-
{/* <div className="p-4"> yo this project sucks, ill remake it some other time, like cmon inputting anything into the textbox makes it break. ive warned you</div> */}
170
</div>
171
);
172
}
···
1
+
import { AtUri } from "@atproto/api";
2
+
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
3
+
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
4
+
import { useAtom } from "jotai";
5
+
import * as React from "react";
6
7
+
import defaultpfp from "~/../public/favicon.png";
8
+
import { Header } from "~/components/Header";
9
+
import {
10
+
ReusableTabRoute,
11
+
useReusableTabScrollRestore,
12
+
} from "~/components/ReusableTabRoute";
13
+
import {
14
+
MdiCardsHeartOutline,
15
+
MdiCommentOutline,
16
+
MdiRepeat,
17
+
UniversalPostRendererATURILoader,
18
+
} from "~/components/UniversalPostRenderer";
19
import { useAuth } from "~/providers/UnifiedAuthProvider";
20
+
import {
21
+
constellationURLAtom,
22
+
enableBitesAtom,
23
+
imgCDNAtom,
24
+
postInteractionsFiltersAtom,
25
+
} from "~/utils/atoms";
26
+
import {
27
+
useInfiniteQueryAuthorFeed,
28
+
useQueryConstellation,
29
+
useQueryIdentity,
30
+
useQueryProfile,
31
+
yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks,
32
+
} from "~/utils/useQuery";
33
34
+
import { FollowButton, Mutual } from "./profile.$did";
35
+
36
+
export function NotificationsComponent() {
37
+
return (
38
+
<div className="">
39
+
<Header
40
+
title={`Notifications`}
41
+
backButtonCallback={() => {
42
+
if (window.history.length > 1) {
43
+
window.history.back();
44
+
} else {
45
+
window.location.assign("/");
46
+
}
47
+
}}
48
+
bottomBorderDisabled={true}
49
+
/>
50
+
<NotificationsTabs />
51
+
</div>
52
+
);
53
+
}
54
55
export const Route = createFileRoute("/notifications")({
56
component: NotificationsComponent,
57
});
58
59
+
export default function NotificationsTabs() {
60
+
const [bitesEnabled] = useAtom(enableBitesAtom);
61
+
return (
62
+
<ReusableTabRoute
63
+
route={`Notifications`}
64
+
tabs={{
65
+
Mentions: <MentionsTab />,
66
+
Follows: <FollowsTab />,
67
+
"Post Interactions": <PostInteractionsTab />,
68
+
...bitesEnabled ? {
69
+
Bites: <BitesTab />,
70
+
} : {}
71
+
}}
72
+
/>
73
+
);
74
+
}
75
+
76
+
function MentionsTab() {
77
+
const { agent } = useAuth();
78
+
const [constellationurl] = useAtom(constellationURLAtom);
79
+
const infinitequeryresults = useInfiniteQuery({
80
+
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
81
+
{
82
+
constellation: constellationurl,
83
+
method: "/links",
84
+
target: agent?.did,
85
+
collection: "app.bsky.feed.post",
86
+
path: ".facets[app.bsky.richtext.facet].features[app.bsky.richtext.facet#mention].did",
87
+
}
88
+
),
89
+
enabled: !!agent?.did,
90
+
});
91
+
92
+
const {
93
+
data: infiniteMentionsData,
94
+
fetchNextPage,
95
+
hasNextPage,
96
+
isFetchingNextPage,
97
+
isLoading,
98
+
isError,
99
+
error,
100
+
} = infinitequeryresults;
101
+
102
+
const mentionsAturis = React.useMemo(() => {
103
+
// Get all replies from the standard infinite query
104
+
return (
105
+
infiniteMentionsData?.pages.flatMap(
106
+
(page) =>
107
+
page?.linking_records.map(
108
+
(r) => `at://${r.did}/${r.collection}/${r.rkey}`
109
+
) ?? []
110
+
) ?? []
111
+
);
112
+
}, [infiniteMentionsData]);
113
114
+
useReusableTabScrollRestore("Notifications");
115
116
+
if (isLoading) return <LoadingState text="Loading mentions..." />;
117
+
if (isError) return <ErrorState error={error} />;
118
+
119
+
if (!mentionsAturis?.length) return <EmptyState text="No mentions yet." />;
120
+
121
+
return (
122
+
<>
123
+
{mentionsAturis.map((m) => (
124
+
<UniversalPostRendererATURILoader key={m} atUri={m} />
125
+
))}
126
127
+
{hasNextPage && (
128
+
<button
129
+
onClick={() => fetchNextPage()}
130
+
disabled={isFetchingNextPage}
131
+
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold disabled:opacity-50"
132
+
>
133
+
{isFetchingNextPage ? "Loading..." : "Load More"}
134
+
</button>
135
+
)}
136
+
</>
137
+
);
138
+
}
139
+
140
+
export function FollowsTab({did}:{did?:string}) {
141
+
const { agent } = useAuth();
142
+
const userdidunsafe = did ?? agent?.did;
143
+
const { data: identity} = useQueryIdentity(userdidunsafe);
144
+
const userdid = identity?.did;
145
+
146
+
const [constellationurl] = useAtom(constellationURLAtom);
147
+
const infinitequeryresults = useInfiniteQuery({
148
+
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
149
+
{
150
+
constellation: constellationurl,
151
+
method: "/links",
152
+
target: userdid,
153
+
collection: "app.bsky.graph.follow",
154
+
path: ".subject",
155
+
}
156
+
),
157
+
enabled: !!userdid,
158
+
});
159
+
160
+
const {
161
+
data: infiniteFollowsData,
162
+
fetchNextPage,
163
+
hasNextPage,
164
+
isFetchingNextPage,
165
+
isLoading,
166
+
isError,
167
+
error,
168
+
} = infinitequeryresults;
169
+
170
+
const followsAturis = React.useMemo(() => {
171
+
// Get all replies from the standard infinite query
172
+
return (
173
+
infiniteFollowsData?.pages.flatMap(
174
+
(page) =>
175
+
page?.linking_records.map(
176
+
(r) => `at://${r.did}/${r.collection}/${r.rkey}`
177
+
) ?? []
178
+
) ?? []
179
+
);
180
+
}, [infiniteFollowsData]);
181
+
182
+
useReusableTabScrollRestore("Notifications");
183
+
184
+
if (isLoading) return <LoadingState text="Loading follows..." />;
185
+
if (isError) return <ErrorState error={error} />;
186
+
187
+
if (!followsAturis?.length) return <EmptyState text="No follows yet." />;
188
+
189
+
return (
190
+
<>
191
+
{followsAturis.map((m) => (
192
+
<NotificationItem key={m} notification={m} />
193
+
))}
194
+
195
+
{hasNextPage && (
196
+
<button
197
+
onClick={() => fetchNextPage()}
198
+
disabled={isFetchingNextPage}
199
+
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold disabled:opacity-50"
200
+
>
201
+
{isFetchingNextPage ? "Loading..." : "Load More"}
202
+
</button>
203
+
)}
204
+
</>
205
+
);
206
+
}
207
+
208
+
209
+
export function BitesTab({did}:{did?:string}) {
210
+
const { agent } = useAuth();
211
+
const userdidunsafe = did ?? agent?.did;
212
+
const { data: identity} = useQueryIdentity(userdidunsafe);
213
+
const userdid = identity?.did;
214
+
215
+
const [constellationurl] = useAtom(constellationURLAtom);
216
+
const infinitequeryresults = useInfiniteQuery({
217
+
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
218
+
{
219
+
constellation: constellationurl,
220
+
method: "/links",
221
+
target: "at://"+userdid,
222
+
collection: "net.wafrn.feed.bite",
223
+
path: ".subject",
224
+
staleMult: 0 // safe fun
225
+
}
226
+
),
227
+
enabled: !!userdid,
228
+
});
229
+
230
+
const {
231
+
data: infiniteFollowsData,
232
+
fetchNextPage,
233
+
hasNextPage,
234
+
isFetchingNextPage,
235
+
isLoading,
236
+
isError,
237
+
error,
238
+
} = infinitequeryresults;
239
+
240
+
const followsAturis = React.useMemo(() => {
241
+
// Get all replies from the standard infinite query
242
+
return (
243
+
infiniteFollowsData?.pages.flatMap(
244
+
(page) =>
245
+
page?.linking_records.map(
246
+
(r) => `at://${r.did}/${r.collection}/${r.rkey}`
247
+
) ?? []
248
+
) ?? []
249
+
);
250
+
}, [infiniteFollowsData]);
251
+
252
+
useReusableTabScrollRestore("Notifications");
253
+
254
+
if (isLoading) return <LoadingState text="Loading bites..." />;
255
+
if (isError) return <ErrorState error={error} />;
256
+
257
+
if (!followsAturis?.length) return <EmptyState text="No bites yet." />;
258
+
259
+
return (
260
+
<>
261
+
{followsAturis.map((m) => (
262
+
<NotificationItem key={m} notification={m} />
263
+
))}
264
+
265
+
{hasNextPage && (
266
+
<button
267
+
onClick={() => fetchNextPage()}
268
+
disabled={isFetchingNextPage}
269
+
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold disabled:opacity-50"
270
+
>
271
+
{isFetchingNextPage ? "Loading..." : "Load More"}
272
+
</button>
273
+
)}
274
+
</>
275
+
);
276
+
}
277
+
278
+
function PostInteractionsTab() {
279
+
const { agent } = useAuth();
280
+
const { data: identity } = useQueryIdentity(agent?.did);
281
+
const queryClient = useQueryClient();
282
+
const {
283
+
data: postsData,
284
+
fetchNextPage,
285
+
hasNextPage,
286
+
isFetchingNextPage,
287
+
isLoading: arePostsLoading,
288
+
} = useInfiniteQueryAuthorFeed(agent?.did, identity?.pds);
289
+
290
+
React.useEffect(() => {
291
+
if (postsData) {
292
+
postsData.pages.forEach((page) => {
293
+
page.records.forEach((record) => {
294
+
if (!queryClient.getQueryData(["post", record.uri])) {
295
+
queryClient.setQueryData(["post", record.uri], record);
296
}
297
+
});
298
});
299
+
}
300
+
}, [postsData, queryClient]);
301
+
302
+
const posts = React.useMemo(
303
+
() => postsData?.pages.flatMap((page) => page.records) ?? [],
304
+
[postsData]
305
+
);
306
+
307
+
useReusableTabScrollRestore("Notifications");
308
+
309
+
const [filters] = useAtom(postInteractionsFiltersAtom);
310
+
const empty = (!filters.likes && !filters.quotes && !filters.replies && !filters.reposts);
311
312
return (
313
+
<>
314
+
<PostInteractionsFilterChipBar />
315
+
{!empty && posts.map((m) => (
316
+
<PostInteractionsItem key={m.uri} uri={m.uri} />
317
+
))}
318
+
319
+
{hasNextPage && (
320
+
<button
321
+
onClick={() => fetchNextPage()}
322
+
disabled={isFetchingNextPage}
323
+
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold disabled:opacity-50"
324
+
>
325
+
{isFetchingNextPage ? "Loading..." : "Load More"}
326
+
</button>
327
+
)}
328
+
</>
329
+
);
330
+
}
331
+
332
+
function PostInteractionsFilterChipBar() {
333
+
const [filters, setFilters] = useAtom(postInteractionsFiltersAtom);
334
+
// const empty = (!filters.likes && !filters.quotes && !filters.replies && !filters.reposts);
335
+
336
+
// useEffect(() => {
337
+
// if (empty) {
338
+
// setFilters((prev) => ({
339
+
// ...prev,
340
+
// likes: true,
341
+
// }));
342
+
// }
343
+
// }, [
344
+
// empty,
345
+
// setFilters,
346
+
// ]);
347
+
348
+
const toggle = (key: keyof typeof filters) => {
349
+
setFilters((prev) => ({
350
+
...prev,
351
+
[key]: !prev[key],
352
+
}));
353
+
};
354
+
355
+
return (
356
+
<div className="flex flex-row flex-wrap gap-2 px-4 pt-4">
357
+
<Chip
358
+
state={filters.likes}
359
+
text="Likes"
360
+
onClick={() => toggle("likes")}
361
+
/>
362
+
<Chip
363
+
state={filters.reposts}
364
+
text="Reposts"
365
+
onClick={() => toggle("reposts")}
366
+
/>
367
+
<Chip
368
+
state={filters.replies}
369
+
text="Replies"
370
+
onClick={() => toggle("replies")}
371
+
/>
372
+
<Chip
373
+
state={filters.quotes}
374
+
text="Quotes"
375
+
onClick={() => toggle("quotes")}
376
+
/>
377
+
<Chip
378
+
state={filters.showAll}
379
+
text="Show All Metrics"
380
+
onClick={() => toggle("showAll")}
381
+
/>
382
+
</div>
383
+
);
384
+
}
385
+
386
+
export function Chip({
387
+
state,
388
+
text,
389
+
onClick,
390
+
}: {
391
+
state: boolean;
392
+
text: string;
393
+
onClick: React.MouseEventHandler<HTMLButtonElement>;
394
+
}) {
395
+
return (
396
+
<button
397
+
onClick={onClick}
398
+
className={`relative inline-flex items-center px-3 py-1.5 rounded-lg text-sm font-medium transition-all
399
+
${
400
+
state
401
+
? "bg-primary/20 text-primary bg-gray-200 dark:bg-gray-800 border border-transparent"
402
+
: "bg-surface-container-low text-on-surface-variant border border-outline"
403
+
}
404
+
hover:bg-primary/30 active:scale-[0.97]
405
+
dark:border-outline-variant
406
+
`}
407
+
>
408
+
{state && (
409
+
<IconMdiCheck
410
+
className="mr-1.5 inline-block w-4 h-4 rounded-full bg-primary"
411
+
aria-hidden
412
+
/>
413
+
)}
414
+
{text}
415
+
</button>
416
+
);
417
+
}
418
+
419
+
function PostInteractionsItem({ uri }: { uri: string }) {
420
+
const [filters] = useAtom(postInteractionsFiltersAtom);
421
+
const { data: links } = useQueryConstellation({
422
+
method: "/links/all",
423
+
target: uri,
424
+
});
425
+
426
+
const likes =
427
+
links?.links?.["app.bsky.feed.like"]?.[".subject.uri"]?.records || 0;
428
+
const replies =
429
+
links?.links?.["app.bsky.feed.post"]?.[".reply.parent.uri"]?.records || 0;
430
+
const reposts =
431
+
links?.links?.["app.bsky.feed.repost"]?.[".subject.uri"]?.records || 0;
432
+
const quotes1 =
433
+
links?.links?.["app.bsky.feed.post"]?.[".embed.record.uri"]?.records || 0;
434
+
const quotes2 =
435
+
links?.links?.["app.bsky.feed.post"]?.[".embed.record.record.uri"]
436
+
?.records || 0;
437
+
const quotes = quotes1 + quotes2;
438
+
439
+
const all = likes + replies + reposts + quotes;
440
+
441
+
//const failLikes = filters.likes && likes < 1;
442
+
//const failReposts = filters.reposts && reposts < 1;
443
+
//const failReplies = filters.replies && replies < 1;
444
+
//const failQuotes = filters.quotes && quotes < 1;
445
+
446
+
const showLikes = filters.showAll || filters.likes
447
+
const showReposts = filters.showAll || filters.reposts
448
+
const showReplies = filters.showAll || filters.replies
449
+
const showQuotes = filters.showAll || filters.quotes
450
+
451
+
//const showNone = !showLikes && !showReposts && !showReplies && !showQuotes;
452
+
453
+
//const fail = failLikes || failReposts || failReplies || failQuotes || showNone;
454
+
455
+
const matchesLikes = filters.likes && likes > 0;
456
+
const matchesReposts = filters.reposts && reposts > 0;
457
+
const matchesReplies = filters.replies && replies > 0;
458
+
const matchesQuotes = filters.quotes && quotes > 0;
459
+
460
+
const matchesAnything =
461
+
// filters.showAll ||
462
+
matchesLikes ||
463
+
matchesReposts ||
464
+
matchesReplies ||
465
+
matchesQuotes;
466
+
467
+
if (!matchesAnything) return null;
468
+
469
+
//if (fail) return;
470
+
471
+
return (
472
+
<div className="flex flex-col">
473
+
{/* <span>fail likes {failLikes ? "true" : "false"}</span>
474
+
<span>fail repost {failReposts ? "true" : "false"}</span>
475
+
<span>fail reply {failReplies ? "true" : "false"}</span>
476
+
<span>fail qupte {failQuotes ? "true" : "false"}</span> */}
477
+
<div className="border rounded-xl mx-4 mt-4 overflow-hidden">
478
+
<UniversalPostRendererATURILoader
479
+
isQuote
480
+
key={uri}
481
+
atUri={uri}
482
+
nopics={true}
483
+
concise={true}
484
+
/>
485
+
<div className="flex flex-col divide-x">
486
+
{showLikes &&(<InteractionsButton
487
+
type={"like"}
488
+
uri={uri}
489
+
count={likes}
490
+
/>)}
491
+
{showReposts && (<InteractionsButton
492
+
type={"repost"}
493
+
uri={uri}
494
+
count={reposts}
495
+
/>)}
496
+
{showReplies && (<InteractionsButton
497
+
type={"reply"}
498
+
uri={uri}
499
+
count={replies}
500
+
/>)}
501
+
{showQuotes && (<InteractionsButton
502
+
type={"quote"}
503
+
uri={uri}
504
+
count={quotes}
505
+
/>)}
506
+
{!all && (
507
+
<div className="text-center text-gray-500 dark:text-gray-400 pb-3 pt-2 border-t">
508
+
No interactions yet.
509
+
</div>
510
+
)}
511
+
</div>
512
</div>
513
+
</div>
514
+
);
515
+
}
516
+
517
+
function InteractionsButton({
518
+
type,
519
+
uri,
520
+
count,
521
+
}: {
522
+
type: "reply" | "repost" | "like" | "quote";
523
+
uri: string;
524
+
count: number;
525
+
}) {
526
+
if (!count) return <></>;
527
+
const aturi = new AtUri(uri);
528
+
return (
529
+
<Link
530
+
to={
531
+
`/profile/$did/post/$rkey` +
532
+
(type === "like"
533
+
? "/liked-by"
534
+
: type === "repost"
535
+
? "/reposted-by"
536
+
: type === "quote"
537
+
? "/quotes"
538
+
: "")
539
+
}
540
+
params={{
541
+
did: aturi.host,
542
+
rkey: aturi.rkey,
543
+
}}
544
+
className="flex-1 border-t py-2 px-4 flex flex-row items-center gap-2 transition-colors hover:bg-gray-100 hover:dark:bg-gray-800"
545
+
>
546
+
{type === "like" ? (
547
+
<MdiCardsHeartOutline height={22} width={22} />
548
+
) : type === "repost" ? (
549
+
<MdiRepeat height={22} width={22} />
550
+
) : type === "reply" ? (
551
+
<MdiCommentOutline height={22} width={22} />
552
+
) : type === "quote" ? (
553
+
<IconMdiMessageReplyTextOutline
554
+
height={22}
555
+
width={22}
556
+
className=" text-gray-400"
557
+
/>
558
+
) : (
559
+
<></>
560
+
)}
561
+
{type === "like"
562
+
? "likes"
563
+
: type === "reply"
564
+
? "replies"
565
+
: type === "quote"
566
+
? "quotes"
567
+
: type === "repost"
568
+
? "reposts"
569
+
: ""}
570
+
<div className="flex-1" /> {count}
571
+
</Link>
572
+
);
573
+
}
574
+
575
+
export function NotificationItem({ notification, labeler }: { notification: string, labeler?: boolean }) {
576
+
const aturi = new AtUri(notification);
577
+
const bite = aturi.collection === "net.wafrn.feed.bite";
578
+
const navigate = useNavigate();
579
+
const { data: identity } = useQueryIdentity(aturi.host);
580
+
const resolvedDid = identity?.did;
581
+
const profileUri = resolvedDid
582
+
? `at://${resolvedDid}/app.bsky.actor.profile/self`
583
+
: undefined;
584
+
const { data: profileRecord } = useQueryProfile(profileUri);
585
+
const profile = profileRecord?.value;
586
+
587
+
const [imgcdn] = useAtom(imgCDNAtom);
588
+
589
+
function getAvatarUrl(p: typeof profile) {
590
+
const link = p?.avatar?.ref?.["$link"];
591
+
if (!link || !resolvedDid) return null;
592
+
return `https://${imgcdn}/img/avatar/plain/${resolvedDid}/${link}@jpeg`;
593
+
}
594
+
595
+
const avatar = getAvatarUrl(profile);
596
+
597
+
return (
598
+
<div
599
+
className="flex items-center p-4 cursor-pointer gap-3 justify-around border-b flex-row"
600
+
onClick={() =>
601
+
aturi &&
602
+
navigate({
603
+
to: "/profile/$did",
604
+
params: { did: aturi.host },
605
+
})
606
+
}
607
+
>
608
+
{/* <div>
609
+
{aturi.collection === "app.bsky.graph.follow" ? (
610
+
<IconMdiAccountPlus />
611
+
) : aturi.collection === "app.bsky.feed.like" ? (
612
+
<MdiCardsHeart />
613
+
) : (
614
+
<></>
615
+
)}
616
+
</div> */}
617
+
{profile ? (
618
+
<img
619
+
src={avatar || defaultpfp}
620
+
alt={identity?.handle}
621
+
className={`w-10 h-10 ${labeler ? "rounded-md" : "rounded-full"}`}
622
+
/>
623
+
) : (
624
+
<div className="w-10 h-10 rounded-full bg-gray-300 dark:bg-gray-700" />
625
)}
626
+
<div className="flex flex-col min-w-0">
627
+
<div className="flex flex-row gap-2 overflow-hidden text-ellipsis whitespace-nowrap min-w-0">
628
+
<span className="font-medium text-gray-900 dark:text-gray-100 truncate">
629
+
{profile?.displayName || identity?.handle || "Someone"}
630
+
</span>
631
+
<span className="text-gray-700 dark:text-gray-400 truncate">
632
+
@{identity?.handle}
633
+
</span>
634
+
</div>
635
+
<div className="flex flex-row gap-2">
636
+
{identity?.did && <Mutual targetdidorhandle={identity?.did} />}
637
+
{/* <span className="text-sm text-gray-600 dark:text-gray-400">
638
+
followed you
639
+
</span> */}
640
+
</div>
641
+
</div>
642
+
<div className="flex-1" />
643
+
{identity?.did && <FollowButton targetdidorhandle={identity?.did} />}
644
</div>
645
);
646
}
647
+
648
+
export const EmptyState = ({ text }: { text: string }) => (
649
+
<div className="py-10 text-center text-gray-500 dark:text-gray-400">
650
+
{text}
651
+
</div>
652
+
);
653
+
654
+
export const LoadingState = ({ text }: { text: string }) => (
655
+
<div className="py-10 text-center text-gray-500 dark:text-gray-400 italic">
656
+
{text}
657
+
</div>
658
+
);
659
+
660
+
export const ErrorState = ({ error }: { error: unknown }) => (
661
+
<div className="py-10 text-center text-red-600 dark:text-red-400">
662
+
Error: {(error as Error)?.message || "Something went wrong."}
663
+
</div>
664
+
);
+91
src/routes/profile.$did/feed.$rkey.tsx
+91
src/routes/profile.$did/feed.$rkey.tsx
···
···
1
+
import * as ATPAPI from "@atproto/api";
2
+
import { AtUri } from "@atproto/api";
3
+
import { createFileRoute } from "@tanstack/react-router";
4
+
import { useAtom } from "jotai";
5
+
6
+
import { Header } from "~/components/Header";
7
+
import { InfiniteCustomFeed } from "~/components/InfiniteCustomFeed";
8
+
import { useAuth } from "~/providers/UnifiedAuthProvider";
9
+
import { quickAuthAtom } from "~/utils/atoms";
10
+
import { useQueryArbitrary, useQueryIdentity } from "~/utils/useQuery";
11
+
12
+
export const Route = createFileRoute("/profile/$did/feed/$rkey")({
13
+
component: FeedRoute,
14
+
});
15
+
16
+
// todo: scroll restoration
17
+
function FeedRoute() {
18
+
const { did, rkey } = Route.useParams();
19
+
const { agent, status } = useAuth();
20
+
const { data: identitydata } = useQueryIdentity(did);
21
+
const { data: identity } = useQueryIdentity(agent?.did);
22
+
const uri = `at://${identitydata?.did || did}/app.bsky.feed.generator/${rkey}`;
23
+
const aturi = new AtUri(uri);
24
+
const { data: feeddata } = useQueryArbitrary(uri);
25
+
26
+
const [quickAuth, setQuickAuth] = useAtom(quickAuthAtom);
27
+
const isAuthRestoring = quickAuth ? status === "loading" : false;
28
+
29
+
const authed = status === "signedIn";
30
+
31
+
const feedServiceDid = !isAuthRestoring
32
+
? ((feeddata?.value as any)?.did as string | undefined)
33
+
: undefined;
34
+
35
+
// const {
36
+
// data: feedData,
37
+
// isLoading: isFeedLoading,
38
+
// error: feedError,
39
+
// } = useQueryFeedSkeleton({
40
+
// feedUri: selectedFeed!,
41
+
// agent: agent ?? undefined,
42
+
// isAuthed: authed ?? false,
43
+
// pdsUrl: identity?.pds,
44
+
// feedServiceDid: feedServiceDid,
45
+
// });
46
+
47
+
// const feed = feedData?.feed || [];
48
+
49
+
const isReadyForAuthedFeed =
50
+
!isAuthRestoring && authed && agent && identity?.pds && feedServiceDid;
51
+
const isReadyForUnauthedFeed = !isAuthRestoring && !authed;
52
+
53
+
const feed: ATPAPI.AppBskyFeedGenerator.Record | undefined = feeddata?.value;
54
+
55
+
const web = feedServiceDid?.replace(/^did:web:/, "") || "";
56
+
57
+
return (
58
+
<>
59
+
<Header
60
+
title={feed?.displayName || aturi.rkey}
61
+
backButtonCallback={() => {
62
+
if (window.history.length > 1) {
63
+
window.history.back();
64
+
} else {
65
+
window.location.assign("/");
66
+
}
67
+
}}
68
+
/>
69
+
70
+
{isAuthRestoring ||
71
+
(authed && (!identity?.pds || !feedServiceDid) && (
72
+
<div className="p-4 text-center text-gray-500">
73
+
Preparing your feed...
74
+
</div>
75
+
))}
76
+
77
+
{!isAuthRestoring && (isReadyForAuthedFeed || isReadyForUnauthedFeed) ? (
78
+
<InfiniteCustomFeed
79
+
key={uri}
80
+
feedUri={uri}
81
+
pdsUrl={identity?.pds}
82
+
feedServiceDid={feedServiceDid}
83
+
authedOverride={!authed && true || undefined}
84
+
unauthedfeedurl={!authed && web || undefined}
85
+
/>
86
+
) : (
87
+
<div className="p-4 text-center text-gray-500">Loading.......</div>
88
+
)}
89
+
</>
90
+
);
91
+
}
+30
src/routes/profile.$did/followers.tsx
+30
src/routes/profile.$did/followers.tsx
···
···
1
+
import { createFileRoute } from "@tanstack/react-router";
2
+
3
+
import { Header } from "~/components/Header";
4
+
5
+
import { FollowsTab } from "../notifications";
6
+
7
+
export const Route = createFileRoute("/profile/$did/followers")({
8
+
component: RouteComponent,
9
+
});
10
+
11
+
// todo: scroll restoration
12
+
function RouteComponent() {
13
+
const params = Route.useParams();
14
+
15
+
return (
16
+
<div>
17
+
<Header
18
+
title={"Followers"}
19
+
backButtonCallback={() => {
20
+
if (window.history.length > 1) {
21
+
window.history.back();
22
+
} else {
23
+
window.location.assign("/");
24
+
}
25
+
}}
26
+
/>
27
+
<FollowsTab did={params.did} />
28
+
</div>
29
+
);
30
+
}
+79
src/routes/profile.$did/follows.tsx
+79
src/routes/profile.$did/follows.tsx
···
···
1
+
import * as ATPAPI from "@atproto/api"
2
+
import { createFileRoute } from '@tanstack/react-router'
3
+
import React from 'react';
4
+
5
+
import { Header } from '~/components/Header';
6
+
import { useReusableTabScrollRestore } from '~/components/ReusableTabRoute';
7
+
import { useInfiniteQueryAuthorFeed, useQueryIdentity } from '~/utils/useQuery';
8
+
9
+
import { EmptyState, ErrorState, LoadingState, NotificationItem } from '../notifications';
10
+
11
+
export const Route = createFileRoute('/profile/$did/follows')({
12
+
component: RouteComponent,
13
+
})
14
+
15
+
// todo: scroll restoration
16
+
function RouteComponent() {
17
+
const params = Route.useParams();
18
+
return (
19
+
<div>
20
+
<Header
21
+
title={"Follows"}
22
+
backButtonCallback={() => {
23
+
if (window.history.length > 1) {
24
+
window.history.back();
25
+
} else {
26
+
window.location.assign("/");
27
+
}
28
+
}}
29
+
/>
30
+
<Follows did={params.did}/>
31
+
</div>
32
+
);
33
+
}
34
+
35
+
function Follows({did}:{did:string}) {
36
+
const {data: identity} = useQueryIdentity(did);
37
+
const infinitequeryresults = useInfiniteQueryAuthorFeed(identity?.did, identity?.pds, "app.bsky.graph.follow");
38
+
39
+
const {
40
+
data: infiniteFollowsData,
41
+
fetchNextPage,
42
+
hasNextPage,
43
+
isFetchingNextPage,
44
+
isLoading,
45
+
isError,
46
+
error,
47
+
} = infinitequeryresults;
48
+
49
+
const followsAturis = React.useMemo(
50
+
() => infiniteFollowsData?.pages.flatMap((page) => page.records) ?? [],
51
+
[infiniteFollowsData]
52
+
);
53
+
54
+
useReusableTabScrollRestore("Notifications");
55
+
56
+
if (isLoading) return <LoadingState text="Loading follows..." />;
57
+
if (isError) return <ErrorState error={error} />;
58
+
59
+
if (!followsAturis?.length) return <EmptyState text="No follows yet." />;
60
+
61
+
return (
62
+
<>
63
+
{followsAturis.map((m) => {
64
+
const record = m.value as unknown as ATPAPI.AppBskyGraphFollow.Record;
65
+
return <NotificationItem key={record.subject} notification={record.subject} />
66
+
})}
67
+
68
+
{hasNextPage && (
69
+
<button
70
+
onClick={() => fetchNextPage()}
71
+
disabled={isFetchingNextPage}
72
+
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold disabled:opacity-50"
73
+
>
74
+
{isFetchingNextPage ? "Loading..." : "Load More"}
75
+
</button>
76
+
)}
77
+
</>
78
+
);
79
+
}
+1018
-127
src/routes/profile.$did/index.tsx
+1018
-127
src/routes/profile.$did/index.tsx
···
1
import { useQueryClient } from "@tanstack/react-query";
2
-
import { createFileRoute, Link } from "@tanstack/react-router";
3
-
import React from "react";
4
5
-
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
6
import { useAuth } from "~/providers/UnifiedAuthProvider";
7
-
import { toggleFollow, useGetFollowState } from "~/utils/followState";
8
import {
9
useInfiniteQueryAuthorFeed,
10
useQueryIdentity,
11
useQueryProfile,
12
} from "~/utils/useQuery";
13
14
export const Route = createFileRoute("/profile/$did/")({
15
component: ProfileComponent,
···
18
function ProfileComponent() {
19
// booo bad this is not always the did it might be a handle, use identity.did instead
20
const { did } = Route.useParams();
21
-
const queryClient = useQueryClient();
22
const { agent } = useAuth();
23
const {
24
data: identity,
25
isLoading: isIdentityLoading,
26
error: identityError,
27
} = useQueryIdentity(did);
28
29
-
const followRecords = useGetFollowState({
30
-
target: identity?.did || did,
31
-
user: agent?.did,
32
-
});
33
34
const resolvedDid = did.startsWith("did:") ? did : identity?.did;
35
const resolvedHandle = did.startsWith("did:") ? identity?.handle : did;
···
41
const { data: profileRecord } = useQueryProfile(profileUri);
42
const profile = profileRecord?.value;
43
44
-
const {
45
-
data: postsData,
46
-
fetchNextPage,
47
-
hasNextPage,
48
-
isFetchingNextPage,
49
-
isLoading: arePostsLoading,
50
-
} = useInfiniteQueryAuthorFeed(resolvedDid, pdsUrl);
51
-
52
-
React.useEffect(() => {
53
-
if (postsData) {
54
-
postsData.pages.forEach((page) => {
55
-
page.records.forEach((record) => {
56
-
if (!queryClient.getQueryData(["post", record.uri])) {
57
-
queryClient.setQueryData(["post", record.uri], record);
58
-
}
59
-
});
60
-
});
61
-
}
62
-
}, [postsData, queryClient]);
63
-
64
-
const posts = React.useMemo(
65
-
() => postsData?.pages.flatMap((page) => page.records) ?? [],
66
-
[postsData]
67
-
);
68
69
function getAvatarUrl(p: typeof profile) {
70
const link = p?.avatar?.ref?.["$link"];
71
if (!link || !resolvedDid) return null;
72
-
return `https://cdn.bsky.app/img/avatar/plain/${resolvedDid}/${link}@jpeg`;
73
}
74
function getBannerUrl(p: typeof profile) {
75
const link = p?.banner?.ref?.["$link"];
76
if (!link || !resolvedDid) return null;
77
-
return `https://cdn.bsky.app/img/banner/plain/${resolvedDid}/${link}@jpeg`;
78
}
79
80
const displayName =
···
82
const handle = resolvedHandle ? `@${resolvedHandle}` : resolvedDid || did;
83
const description = profile?.description || "";
84
85
-
if (isIdentityLoading) {
86
-
return (
87
-
<div className="p-4 text-center text-gray-500">Resolving profile...</div>
88
-
);
89
-
}
90
91
-
if (identityError) {
92
-
return (
93
-
<div className="p-4 text-center text-red-500">
94
-
Error: {identityError.message}
95
-
</div>
96
-
);
97
-
}
98
99
-
if (!resolvedDid) {
100
-
return (
101
-
<div className="p-4 text-center text-gray-500">Profile not found.</div>
102
-
);
103
-
}
104
105
return (
106
-
<>
107
-
<div className="flex gap-2 px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700">
108
<Link
109
to=".."
110
className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg"
···
121
โ
122
</Link>
123
<span className="text-xl font-bold ml-2">Profile</span>
124
-
</div>
125
126
{/* Profile Header */}
127
<div className="w-full max-w-2xl mx-auto overflow-hidden relative bg-gray-100 dark:bg-gray-900">
···
137
138
{/* Avatar (PFP) */}
139
<div className="absolute left-[16px] top-[100px] ">
140
-
<img
141
-
src={getAvatarUrl(profile) || "/favicon.png"}
142
-
alt="avatar"
143
-
className="w-28 h-28 rounded-full object-cover border-4 border-white dark:border-gray-950 bg-gray-300 dark:bg-gray-700"
144
-
/>
145
</div>
146
147
<div className="absolute right-[16px] top-[170px] flex flex-row gap-2.5">
148
{/*
149
todo: full follow and unfollow backfill (along with partial likes backfill,
150
just enough for it to be useful)
151
also delay the backfill to be on demand because it would be pretty intense
152
also save it persistently
153
*/}
154
-
{identity?.did !== agent?.did ? (
155
-
<>
156
-
{!(followRecords?.length && followRecords?.length > 0) ? (
157
-
<button
158
-
onClick={() =>
159
-
toggleFollow({
160
-
agent: agent || undefined,
161
-
targetDid: identity?.did,
162
-
followRecords: followRecords,
163
-
queryClient: queryClient,
164
-
})
165
-
}
166
-
className="rounded-full dark:bg-gray-600 bg-gray-300 px-3 py-2 text-[14px]"
167
-
>
168
-
Follow
169
-
</button>
170
-
) : (
171
-
<button
172
-
onClick={() =>
173
-
toggleFollow({
174
-
agent: agent || undefined,
175
-
targetDid: identity?.did,
176
-
followRecords: followRecords,
177
-
queryClient: queryClient,
178
-
})
179
-
}
180
-
className="rounded-full dark:bg-gray-600 bg-gray-300 px-3 py-2 text-[14px]"
181
-
>
182
-
Unfollow
183
-
</button>
184
-
)}
185
-
</>
186
-
) : (
187
-
<button className="rounded-full dark:bg-gray-600 bg-gray-300 px-3 py-2 text-[14px]">
188
-
Edit Profile
189
-
</button>
190
-
)}
191
-
<button className="rounded-full dark:bg-gray-600 bg-gray-300 px-3 py-2 text-[14px]">
192
... {/* todo: icon */}
193
</button>
194
</div>
···
196
{/* Info Card */}
197
<div className="mt-16 pb-2 px-4 text-gray-900 dark:text-gray-100">
198
<div className="font-bold text-2xl">{displayName}</div>
199
-
<div className="text-gray-500 dark:text-gray-400 text-base mb-3">
200
{handle}
201
</div>
202
{description && (
203
<div className="text-base leading-relaxed text-gray-800 dark:text-gray-300 mb-5 whitespace-pre-wrap break-words text-[15px]">
204
-
{description}
205
</div>
206
)}
207
</div>
208
</div>
209
210
-
{/* Posts Section */}
211
-
<div className="max-w-2xl mx-auto">
212
-
<div className="text-gray-500 dark:text-gray-400 text-lg font-semibold my-3 mx-4">
213
-
Posts
214
</div>
215
-
<div>
216
-
{posts.map((post) => (
217
<UniversalPostRendererATURILoader
218
-
key={post.uri}
219
-
atUri={post.uri}
220
feedviewpost={true}
221
/>
222
-
))}
223
-
</div>
224
225
-
{/* Loading and "Load More" states */}
226
-
{arePostsLoading && posts.length === 0 && (
227
-
<div className="p-4 text-center text-gray-500">Loading posts...</div>
228
-
)}
229
-
{isFetchingNextPage && (
230
-
<div className="p-4 text-center text-gray-500">Loading more...</div>
231
-
)}
232
-
{hasNextPage && !isFetchingNextPage && (
233
-
<button
234
-
onClick={() => fetchNextPage()}
235
-
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold"
236
>
237
-
Load More Posts
238
-
</button>
239
-
)}
240
-
{posts.length === 0 && !arePostsLoading && (
241
-
<div className="p-4 text-center text-gray-500">No posts found.</div>
242
-
)}
243
</div>
244
</>
245
);
246
}
···
1
+
import { Agent, RichText } from "@atproto/api";
2
+
import * as ATPAPI from "@atproto/api";
3
+
import { TID } from "@atproto/common-web";
4
import { useQueryClient } from "@tanstack/react-query";
5
+
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
6
+
import { useAtom } from "jotai";
7
+
import React, { type ReactNode, useEffect, useState } from "react";
8
9
+
import defaultpfp from "~/../public/favicon.png";
10
+
import { Header } from "~/components/Header";
11
+
import {
12
+
ReusableTabRoute,
13
+
useReusableTabScrollRestore,
14
+
} from "~/components/ReusableTabRoute";
15
+
import {
16
+
renderTextWithFacets,
17
+
UniversalPostRendererATURILoader,
18
+
} from "~/components/UniversalPostRenderer";
19
import { useAuth } from "~/providers/UnifiedAuthProvider";
20
+
import { enableBitesAtom, imgCDNAtom, profileChipsAtom } from "~/utils/atoms";
21
+
import {
22
+
toggleFollow,
23
+
useGetFollowState,
24
+
useGetOneToOneState,
25
+
} from "~/utils/followState";
26
+
import { useFastSetLikesFromFeed } from "~/utils/likeMutationQueue";
27
import {
28
useInfiniteQueryAuthorFeed,
29
+
useQueryArbitrary,
30
+
useQueryConstellation,
31
+
useQueryConstellationLinksCountDistinctDids,
32
useQueryIdentity,
33
useQueryProfile,
34
} from "~/utils/useQuery";
35
+
import IconMdiShieldOutline from "~icons/mdi/shield-outline.jsx";
36
+
37
+
import { renderSnack } from "../__root";
38
+
import { Chip } from "../notifications";
39
40
export const Route = createFileRoute("/profile/$did/")({
41
component: ProfileComponent,
···
44
function ProfileComponent() {
45
// booo bad this is not always the did it might be a handle, use identity.did instead
46
const { did } = Route.useParams();
47
const { agent } = useAuth();
48
+
const navigate = useNavigate();
49
+
const queryClient = useQueryClient();
50
const {
51
data: identity,
52
isLoading: isIdentityLoading,
53
error: identityError,
54
} = useQueryIdentity(did);
55
56
+
// i was gonna check the did doc but useQueryIdentity doesnt return that info (slingshot minidoc)
57
+
// so instead we should query the labeler profile
58
+
59
+
const { data: labelerProfile } = useQueryArbitrary(
60
+
identity?.did
61
+
? `at://${identity?.did}/app.bsky.labeler.service/self`
62
+
: undefined
63
+
);
64
+
65
+
const isLabeler = !!labelerProfile?.cid;
66
+
const labelerRecord = isLabeler
67
+
? (labelerProfile?.value as ATPAPI.AppBskyLabelerService.Record)
68
+
: undefined;
69
70
const resolvedDid = did.startsWith("did:") ? did : identity?.did;
71
const resolvedHandle = did.startsWith("did:") ? identity?.handle : did;
···
77
const { data: profileRecord } = useQueryProfile(profileUri);
78
const profile = profileRecord?.value;
79
80
+
const [imgcdn] = useAtom(imgCDNAtom);
81
82
function getAvatarUrl(p: typeof profile) {
83
const link = p?.avatar?.ref?.["$link"];
84
if (!link || !resolvedDid) return null;
85
+
return `https://${imgcdn}/img/avatar/plain/${resolvedDid}/${link}@jpeg`;
86
}
87
function getBannerUrl(p: typeof profile) {
88
const link = p?.banner?.ref?.["$link"];
89
if (!link || !resolvedDid) return null;
90
+
return `https://${imgcdn}/img/banner/plain/${resolvedDid}/${link}@jpeg`;
91
}
92
93
const displayName =
···
95
const handle = resolvedHandle ? `@${resolvedHandle}` : resolvedDid || did;
96
const description = profile?.description || "";
97
98
+
const isReady = !!resolvedDid && !isIdentityLoading && !!profileRecord;
99
100
+
const resultwhateversure = useQueryConstellationLinksCountDistinctDids(
101
+
resolvedDid
102
+
? {
103
+
method: "/links/count/distinct-dids",
104
+
collection: "app.bsky.graph.follow",
105
+
target: resolvedDid,
106
+
path: ".subject",
107
+
}
108
+
: undefined
109
+
);
110
111
+
const followercount = resultwhateversure?.data?.total;
112
113
return (
114
+
<div className="">
115
+
<Header
116
+
title={`Profile`}
117
+
backButtonCallback={() => {
118
+
if (window.history.length > 1) {
119
+
window.history.back();
120
+
} else {
121
+
window.location.assign("/");
122
+
}
123
+
}}
124
+
bottomBorderDisabled={true}
125
+
/>
126
+
{/* <div className="flex gap-2 px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700">
127
<Link
128
to=".."
129
className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg"
···
140
โ
141
</Link>
142
<span className="text-xl font-bold ml-2">Profile</span>
143
+
</div> */}
144
145
{/* Profile Header */}
146
<div className="w-full max-w-2xl mx-auto overflow-hidden relative bg-gray-100 dark:bg-gray-900">
···
156
157
{/* Avatar (PFP) */}
158
<div className="absolute left-[16px] top-[100px] ">
159
+
{!getAvatarUrl(profile) && isLabeler ? (
160
+
<div
161
+
className={`w-28 h-28 ${isLabeler ? "rounded-md" : "rounded-full"} items-center justify-center flex object-cover border-4 border-white dark:border-gray-950 bg-gray-300 dark:bg-gray-700`}
162
+
>
163
+
<IconMdiShieldOutline className="w-20 h-20" />
164
+
</div>
165
+
) : (
166
+
<img
167
+
src={getAvatarUrl(profile) || "/favicon.png"}
168
+
alt="avatar"
169
+
className={`w-28 h-28 ${isLabeler ? "rounded-md" : "rounded-full"} object-cover border-4 border-white dark:border-gray-950 bg-gray-300 dark:bg-gray-700`}
170
+
/>
171
+
)}
172
</div>
173
174
<div className="absolute right-[16px] top-[170px] flex flex-row gap-2.5">
175
+
<BiteButton targetdidorhandle={did} />
176
{/*
177
todo: full follow and unfollow backfill (along with partial likes backfill,
178
just enough for it to be useful)
179
also delay the backfill to be on demand because it would be pretty intense
180
also save it persistently
181
*/}
182
+
<FollowButton targetdidorhandle={did} />
183
+
<button
184
+
className="rounded-full dark:bg-gray-600 bg-gray-300 px-3 py-2 text-[14px]"
185
+
onClick={(e) => {
186
+
renderSnack({
187
+
title: "Not Implemented Yet",
188
+
description: "Sorry...",
189
+
//button: { label: 'Try Again', onClick: () => console.log('whatever') },
190
+
});
191
+
}}
192
+
>
193
... {/* todo: icon */}
194
</button>
195
</div>
···
197
{/* Info Card */}
198
<div className="mt-16 pb-2 px-4 text-gray-900 dark:text-gray-100">
199
<div className="font-bold text-2xl">{displayName}</div>
200
+
<div className="text-gray-500 dark:text-gray-400 text-base mb-3 flex flex-row gap-1">
201
+
<Mutual targetdidorhandle={did} />
202
{handle}
203
</div>
204
+
<div className="flex flex-row gap-2 text-md text-gray-500 dark:text-gray-400 mb-2">
205
+
<Link to="/profile/$did/followers" params={{ did: did }}>
206
+
{followercount && (
207
+
<span className="mr-1 text-gray-900 dark:text-gray-200 font-medium">
208
+
{followercount}
209
+
</span>
210
+
)}
211
+
Followers
212
+
</Link>
213
+
-
214
+
<Link to="/profile/$did/follows" params={{ did: did }}>
215
+
Follows
216
+
</Link>
217
+
</div>
218
{description && (
219
<div className="text-base leading-relaxed text-gray-800 dark:text-gray-300 mb-5 whitespace-pre-wrap break-words text-[15px]">
220
+
{/* {description} */}
221
+
<RichTextRenderer key={did} description={description} />
222
</div>
223
)}
224
</div>
225
</div>
226
227
+
{/* this should not be rendered until its ready (the top profile layout is stable) */}
228
+
{isReady ? (
229
+
<ReusableTabRoute
230
+
route={`Profile` + did}
231
+
tabs={{
232
+
...(isLabeler
233
+
? {
234
+
Labels: <LabelsTab did={did} labelerRecord={labelerRecord} />,
235
+
}
236
+
: {}),
237
+
...{
238
+
Posts: <PostsTab did={did} />,
239
+
Reposts: <RepostsTab did={did} />,
240
+
Feeds: <FeedsTab did={did} />,
241
+
Lists: <ListsTab did={did} />,
242
+
},
243
+
...(identity?.did === agent?.did
244
+
? { Likes: <SelfLikesTab did={did} /> }
245
+
: {}),
246
+
}}
247
+
/>
248
+
) : isIdentityLoading ? (
249
+
<div className="p-4 text-center text-gray-500">
250
+
Resolving profile...
251
+
</div>
252
+
) : identityError ? (
253
+
<div className="p-4 text-center text-red-500">
254
+
Error: {identityError.message}
255
+
</div>
256
+
) : !resolvedDid ? (
257
+
<div className="p-4 text-center text-gray-500">Profile not found.</div>
258
+
) : (
259
+
<div className="p-4 text-center text-gray-500">
260
+
Loading profile content...
261
</div>
262
+
)}
263
+
</div>
264
+
);
265
+
}
266
+
267
+
export type ProfilePostsFilter = {
268
+
posts: boolean;
269
+
replies: boolean;
270
+
mediaOnly: boolean;
271
+
};
272
+
export const defaultProfilePostsFilter: ProfilePostsFilter = {
273
+
posts: true,
274
+
replies: true,
275
+
mediaOnly: false,
276
+
};
277
+
278
+
function ProfilePostsFilterChipBar({
279
+
filters,
280
+
toggle,
281
+
}: {
282
+
filters: ProfilePostsFilter | null;
283
+
toggle: (key: keyof ProfilePostsFilter) => void;
284
+
}) {
285
+
const empty = !filters?.replies && !filters?.posts;
286
+
const almostEmpty = !filters?.replies && filters?.posts;
287
+
288
+
useEffect(() => {
289
+
if (empty) {
290
+
toggle("posts");
291
+
}
292
+
}, [empty, toggle]);
293
+
294
+
return (
295
+
<div className="flex flex-row flex-wrap gap-2 px-4 pt-4">
296
+
<Chip
297
+
state={filters?.posts ?? true}
298
+
text="Posts"
299
+
onClick={() => (almostEmpty ? null : toggle("posts"))}
300
+
/>
301
+
<Chip
302
+
state={filters?.replies ?? true}
303
+
text="Replies"
304
+
onClick={() => toggle("replies")}
305
+
/>
306
+
<Chip
307
+
state={filters?.mediaOnly ?? false}
308
+
text="Media Only"
309
+
onClick={() => toggle("mediaOnly")}
310
+
/>
311
+
</div>
312
+
);
313
+
}
314
+
315
+
function PostsTab({ did }: { did: string }) {
316
+
// todo: this needs to be a (non-persisted is fine) atom to survive navigation
317
+
const [filterses, setFilterses] = useAtom(profileChipsAtom);
318
+
const filters = filterses?.[did];
319
+
const setFilters = (obj: ProfilePostsFilter) => {
320
+
setFilterses((prev) => {
321
+
return {
322
+
...prev,
323
+
[did]: obj,
324
+
};
325
+
});
326
+
};
327
+
useEffect(() => {
328
+
if (!filters) {
329
+
setFilters(defaultProfilePostsFilter);
330
+
}
331
+
});
332
+
useReusableTabScrollRestore(`Profile` + did);
333
+
const queryClient = useQueryClient();
334
+
const {
335
+
data: identity,
336
+
isLoading: isIdentityLoading,
337
+
error: identityError,
338
+
} = useQueryIdentity(did);
339
+
340
+
const resolvedDid = did.startsWith("did:") ? did : identity?.did;
341
+
342
+
const {
343
+
data: postsData,
344
+
fetchNextPage,
345
+
hasNextPage,
346
+
isFetchingNextPage,
347
+
isLoading: arePostsLoading,
348
+
} = useInfiniteQueryAuthorFeed(resolvedDid, identity?.pds);
349
+
350
+
React.useEffect(() => {
351
+
if (postsData) {
352
+
postsData.pages.forEach((page) => {
353
+
page.records.forEach((record) => {
354
+
if (!queryClient.getQueryData(["post", record.uri])) {
355
+
queryClient.setQueryData(["post", record.uri], record);
356
+
}
357
+
});
358
+
});
359
+
}
360
+
}, [postsData, queryClient]);
361
+
362
+
const posts = React.useMemo(
363
+
() => postsData?.pages.flatMap((page) => page.records) ?? [],
364
+
[postsData]
365
+
);
366
+
367
+
const toggle = (key: keyof ProfilePostsFilter) => {
368
+
setFilterses((prev) => {
369
+
const existing = prev[did] ?? {
370
+
posts: false,
371
+
replies: false,
372
+
mediaOnly: false,
373
+
}; // default
374
+
375
+
return {
376
+
...prev,
377
+
[did]: {
378
+
...existing,
379
+
[key]: !existing[key], // safely negate
380
+
},
381
+
};
382
+
});
383
+
};
384
+
385
+
return (
386
+
<>
387
+
{/* <div className="text-gray-500 dark:text-gray-400 text-lg font-semibold my-3 mx-4">
388
+
Posts
389
+
</div> */}
390
+
<ProfilePostsFilterChipBar filters={filters} toggle={toggle} />
391
+
<div>
392
+
{posts.map((post) => (
393
+
<UniversalPostRendererATURILoader
394
+
key={post.uri}
395
+
atUri={post.uri}
396
+
feedviewpost={true}
397
+
filterNoReplies={!filters?.replies}
398
+
filterMustHaveMedia={filters?.mediaOnly}
399
+
filterMustBeReply={!filters?.posts}
400
+
/>
401
+
))}
402
+
</div>
403
+
404
+
{/* Loading and "Load More" states */}
405
+
{arePostsLoading && posts.length === 0 && (
406
+
<div className="p-4 text-center text-gray-500">Loading posts...</div>
407
+
)}
408
+
{isFetchingNextPage && (
409
+
<div className="p-4 text-center text-gray-500">Loading more...</div>
410
+
)}
411
+
{hasNextPage && !isFetchingNextPage && (
412
+
<button
413
+
onClick={() => fetchNextPage()}
414
+
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold"
415
+
>
416
+
Load More Posts
417
+
</button>
418
+
)}
419
+
{posts.length === 0 && !arePostsLoading && (
420
+
<div className="p-4 text-center text-gray-500">No posts found.</div>
421
+
)}
422
+
</>
423
+
);
424
+
}
425
+
426
+
function RepostsTab({ did }: { did: string }) {
427
+
useReusableTabScrollRestore(`Profile` + did);
428
+
const {
429
+
data: identity,
430
+
isLoading: isIdentityLoading,
431
+
error: identityError,
432
+
} = useQueryIdentity(did);
433
+
434
+
const resolvedDid = did.startsWith("did:") ? did : identity?.did;
435
+
436
+
const {
437
+
data: repostsData,
438
+
fetchNextPage,
439
+
hasNextPage,
440
+
isFetchingNextPage,
441
+
isLoading: arePostsLoading,
442
+
} = useInfiniteQueryAuthorFeed(
443
+
resolvedDid,
444
+
identity?.pds,
445
+
"app.bsky.feed.repost"
446
+
);
447
+
448
+
const reposts = React.useMemo(
449
+
() => repostsData?.pages.flatMap((page) => page.records) ?? [],
450
+
[repostsData]
451
+
);
452
+
453
+
return (
454
+
<>
455
+
<div className="text-gray-500 dark:text-gray-400 text-lg font-semibold my-3 mx-4">
456
+
Reposts
457
+
</div>
458
+
<div>
459
+
{reposts.map((repost) => {
460
+
if (
461
+
!repost ||
462
+
!repost?.value ||
463
+
!repost?.value?.subject ||
464
+
// @ts-expect-error blehhhhh
465
+
!repost?.value?.subject?.uri
466
+
)
467
+
return;
468
+
const repostRecord =
469
+
repost.value as unknown as ATPAPI.AppBskyFeedRepost.Record;
470
+
return (
471
<UniversalPostRendererATURILoader
472
+
key={repostRecord.subject.uri}
473
+
atUri={repostRecord.subject.uri}
474
feedviewpost={true}
475
+
repostedby={repost.uri}
476
/>
477
+
);
478
+
})}
479
+
</div>
480
+
481
+
{/* Loading and "Load More" states */}
482
+
{arePostsLoading && reposts.length === 0 && (
483
+
<div className="p-4 text-center text-gray-500">Loading posts...</div>
484
+
)}
485
+
{isFetchingNextPage && (
486
+
<div className="p-4 text-center text-gray-500">Loading more...</div>
487
+
)}
488
+
{hasNextPage && !isFetchingNextPage && (
489
+
<button
490
+
onClick={() => fetchNextPage()}
491
+
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold"
492
+
>
493
+
Load More Posts
494
+
</button>
495
+
)}
496
+
{reposts.length === 0 && !arePostsLoading && (
497
+
<div className="p-4 text-center text-gray-500">No posts found.</div>
498
+
)}
499
+
</>
500
+
);
501
+
}
502
+
503
+
function FeedsTab({ did }: { did: string }) {
504
+
useReusableTabScrollRestore(`Profile` + did);
505
+
const {
506
+
data: identity,
507
+
isLoading: isIdentityLoading,
508
+
error: identityError,
509
+
} = useQueryIdentity(did);
510
+
511
+
const resolvedDid = did.startsWith("did:") ? did : identity?.did;
512
+
513
+
const {
514
+
data: feedsData,
515
+
fetchNextPage,
516
+
hasNextPage,
517
+
isFetchingNextPage,
518
+
isLoading: arePostsLoading,
519
+
} = useInfiniteQueryAuthorFeed(
520
+
resolvedDid,
521
+
identity?.pds,
522
+
"app.bsky.feed.generator"
523
+
);
524
+
525
+
const feeds = React.useMemo(
526
+
() => feedsData?.pages.flatMap((page) => page.records) ?? [],
527
+
[feedsData]
528
+
);
529
+
530
+
return (
531
+
<>
532
+
<div className="text-gray-500 dark:text-gray-400 text-lg font-semibold my-3 mx-4">
533
+
Feeds
534
+
</div>
535
+
<div>
536
+
{feeds.map((feed) => {
537
+
if (!feed || !feed?.value) return;
538
+
const feedGenRecord =
539
+
feed.value as unknown as ATPAPI.AppBskyFeedGenerator.Record;
540
+
return <FeedItemRender feed={feed as any} key={feed.uri} />;
541
+
})}
542
+
</div>
543
+
544
+
{/* Loading and "Load More" states */}
545
+
{arePostsLoading && feeds.length === 0 && (
546
+
<div className="p-4 text-center text-gray-500">Loading feeds...</div>
547
+
)}
548
+
{isFetchingNextPage && (
549
+
<div className="p-4 text-center text-gray-500">Loading more...</div>
550
+
)}
551
+
{hasNextPage && !isFetchingNextPage && (
552
+
<button
553
+
onClick={() => fetchNextPage()}
554
+
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold"
555
+
>
556
+
Load More Feeds
557
+
</button>
558
+
)}
559
+
{feeds.length === 0 && !arePostsLoading && (
560
+
<div className="p-4 text-center text-gray-500">No feeds found.</div>
561
+
)}
562
+
</>
563
+
);
564
+
}
565
+
566
+
function LabelsTab({
567
+
did,
568
+
labelerRecord,
569
+
}: {
570
+
did: string;
571
+
labelerRecord?: ATPAPI.AppBskyLabelerService.Record;
572
+
}) {
573
+
useReusableTabScrollRestore(`Profile` + did);
574
+
const { agent } = useAuth();
575
+
// const {
576
+
// data: identity,
577
+
// isLoading: isIdentityLoading,
578
+
// error: identityError,
579
+
// } = useQueryIdentity(did);
580
581
+
// const resolvedDid = did.startsWith("did:") ? did : identity?.did;
582
+
583
+
const labelMap = new Map(
584
+
labelerRecord?.policies?.labelValueDefinitions?.map((def) => {
585
+
const locale = def.locales.find((l) => l.lang === "en") ?? def.locales[0];
586
+
return [
587
+
def.identifier,
588
+
{
589
+
name: locale?.name,
590
+
description: locale?.description,
591
+
blur: def.blurs,
592
+
severity: def.severity,
593
+
adultOnly: def.adultOnly,
594
+
defaultSetting: def.defaultSetting,
595
+
},
596
+
];
597
+
})
598
+
);
599
+
600
+
return (
601
+
<>
602
+
<div className="text-gray-500 dark:text-gray-400 text-lg font-semibold my-3 mx-4">
603
+
Labels
604
+
</div>
605
+
<div>
606
+
{[...labelMap.entries()].map(([key, item]) => (
607
+
<div
608
+
key={key}
609
+
className="border-gray-300 dark:border-gray-700 border-b px-4 py-4"
610
>
611
+
<div className="font-semibold text-lg">{item.name}</div>
612
+
<div className="text-sm text-gray-500 dark:text-gray-400">
613
+
{item.description}
614
+
</div>
615
+
<div className="mt-1 text-xs text-gray-400">
616
+
{item.blur && <span>Blur: {item.blur} </span>}
617
+
{item.severity && <span>โข Severity: {item.severity} </span>}
618
+
{item.adultOnly && <span>โข 18+ only</span>}
619
+
</div>
620
+
</div>
621
+
))}
622
</div>
623
+
624
+
{/* Loading and "Load More" states */}
625
+
{!labelerRecord && (
626
+
<div className="p-4 text-center text-gray-500">Loading labels...</div>
627
+
)}
628
+
{/* {!labelerRecord && (
629
+
<div className="p-4 text-center text-gray-500">Loading more...</div>
630
+
)} */}
631
+
{/* {hasNextPage && !isFetchingNextPage && (
632
+
<button
633
+
onClick={() => fetchNextPage()}
634
+
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold"
635
+
>
636
+
Load More Feeds
637
+
</button>
638
+
)}
639
+
{feeds.length === 0 && !arePostsLoading && (
640
+
<div className="p-4 text-center text-gray-500">No feeds found.</div>
641
+
)} */}
642
</>
643
);
644
}
645
+
646
+
export function FeedItemRenderAturiLoader({
647
+
aturi,
648
+
listmode,
649
+
disableBottomBorder,
650
+
disablePropagation,
651
+
}: {
652
+
aturi: string;
653
+
listmode?: boolean;
654
+
disableBottomBorder?: boolean;
655
+
disablePropagation?: boolean;
656
+
}) {
657
+
const { data: record } = useQueryArbitrary(aturi);
658
+
659
+
if (!record) return;
660
+
return (
661
+
<FeedItemRender
662
+
listmode={listmode}
663
+
feed={record}
664
+
disableBottomBorder={disableBottomBorder}
665
+
disablePropagation={disablePropagation}
666
+
/>
667
+
);
668
+
}
669
+
670
+
export function FeedItemRender({
671
+
feed,
672
+
listmode,
673
+
disableBottomBorder,
674
+
disablePropagation,
675
+
}: {
676
+
feed: { uri: string; cid: string; value: any };
677
+
listmode?: boolean;
678
+
disableBottomBorder?: boolean;
679
+
disablePropagation?: boolean;
680
+
}) {
681
+
const name = listmode
682
+
? (feed.value?.name as string)
683
+
: (feed.value?.displayName as string);
684
+
const aturi = new ATPAPI.AtUri(feed.uri);
685
+
const { data: identity } = useQueryIdentity(aturi.host);
686
+
const resolvedDid = identity?.did;
687
+
const [imgcdn] = useAtom(imgCDNAtom);
688
+
689
+
function getAvatarThumbnailUrl(f: typeof feed) {
690
+
const link = f?.value.avatar?.ref?.["$link"];
691
+
if (!link || !resolvedDid) return null;
692
+
return `https://${imgcdn}/img/avatar/plain/${resolvedDid}/${link}@jpeg`;
693
+
}
694
+
695
+
const { data: likes } = useQueryConstellation(
696
+
// @ts-expect-error overloads sucks
697
+
!listmode
698
+
? {
699
+
target: feed.uri,
700
+
method: "/links/count",
701
+
collection: "app.bsky.feed.like",
702
+
path: ".subject.uri",
703
+
}
704
+
: undefined
705
+
);
706
+
707
+
return (
708
+
<Link
709
+
className={`px-4 py-4 ${!disableBottomBorder && "border-b"} flex flex-col gap-1`}
710
+
to="/profile/$did/feed/$rkey"
711
+
params={{ did: aturi.host, rkey: aturi.rkey }}
712
+
onClick={(e) => {
713
+
e.stopPropagation();
714
+
}}
715
+
>
716
+
<div className="flex flex-row gap-3">
717
+
<div className="min-w-10 min-h-10">
718
+
<img
719
+
src={getAvatarThumbnailUrl(feed) || defaultpfp}
720
+
className="h-10 w-10 rounded border"
721
+
/>
722
+
</div>
723
+
<div className="flex flex-col">
724
+
<span className="">{name}</span>
725
+
<span className=" text-sm px-1.5 py-0.5 text-gray-500 bg-gray-200 dark:text-gray-400 dark:bg-gray-800 rounded-lg flex flex-row items-center justify-center">
726
+
{feed.value.did || aturi.rkey}
727
+
</span>
728
+
</div>
729
+
<div className="flex-1" />
730
+
{/* <div className="button bg-red-500 rounded-full min-w-[60px]" /> */}
731
+
</div>
732
+
<span className=" text-sm">{feed.value?.description}</span>
733
+
{!listmode && (
734
+
<span className=" text-sm dark:text-gray-400 text-gray-500">
735
+
Liked by {((likes as unknown as any)?.total as number) || 0} users
736
+
</span>
737
+
)}
738
+
</Link>
739
+
);
740
+
}
741
+
742
+
function ListsTab({ did }: { did: string }) {
743
+
useReusableTabScrollRestore(`Profile` + did);
744
+
const {
745
+
data: identity,
746
+
isLoading: isIdentityLoading,
747
+
error: identityError,
748
+
} = useQueryIdentity(did);
749
+
750
+
const resolvedDid = did.startsWith("did:") ? did : identity?.did;
751
+
752
+
const {
753
+
data: feedsData,
754
+
fetchNextPage,
755
+
hasNextPage,
756
+
isFetchingNextPage,
757
+
isLoading: arePostsLoading,
758
+
} = useInfiniteQueryAuthorFeed(
759
+
resolvedDid,
760
+
identity?.pds,
761
+
"app.bsky.graph.list"
762
+
);
763
+
764
+
const feeds = React.useMemo(
765
+
() => feedsData?.pages.flatMap((page) => page.records) ?? [],
766
+
[feedsData]
767
+
);
768
+
769
+
return (
770
+
<>
771
+
<div className="text-gray-500 dark:text-gray-400 text-lg font-semibold my-3 mx-4">
772
+
Feeds
773
+
</div>
774
+
<div>
775
+
{feeds.map((feed) => {
776
+
if (!feed || !feed?.value) return;
777
+
const feedGenRecord =
778
+
feed.value as unknown as ATPAPI.AppBskyFeedGenerator.Record;
779
+
return (
780
+
<FeedItemRender listmode={true} feed={feed as any} key={feed.uri} />
781
+
);
782
+
})}
783
+
</div>
784
+
785
+
{/* Loading and "Load More" states */}
786
+
{arePostsLoading && feeds.length === 0 && (
787
+
<div className="p-4 text-center text-gray-500">Loading lists...</div>
788
+
)}
789
+
{isFetchingNextPage && (
790
+
<div className="p-4 text-center text-gray-500">Loading more...</div>
791
+
)}
792
+
{hasNextPage && !isFetchingNextPage && (
793
+
<button
794
+
onClick={() => fetchNextPage()}
795
+
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold"
796
+
>
797
+
Load More Lists
798
+
</button>
799
+
)}
800
+
{feeds.length === 0 && !arePostsLoading && (
801
+
<div className="p-4 text-center text-gray-500">No lists found.</div>
802
+
)}
803
+
</>
804
+
);
805
+
}
806
+
807
+
function SelfLikesTab({ did }: { did: string }) {
808
+
useReusableTabScrollRestore(`Profile` + did);
809
+
const {
810
+
data: identity,
811
+
isLoading: isIdentityLoading,
812
+
error: identityError,
813
+
} = useQueryIdentity(did);
814
+
815
+
const resolvedDid = did.startsWith("did:") ? did : identity?.did;
816
+
817
+
const {
818
+
data: likesData,
819
+
fetchNextPage,
820
+
hasNextPage,
821
+
isFetchingNextPage,
822
+
isLoading: arePostsLoading,
823
+
} = useInfiniteQueryAuthorFeed(
824
+
resolvedDid,
825
+
identity?.pds,
826
+
"app.bsky.feed.like"
827
+
);
828
+
829
+
const likes = React.useMemo(
830
+
() => likesData?.pages.flatMap((page) => page.records) ?? [],
831
+
[likesData]
832
+
);
833
+
834
+
const { setFastState } = useFastSetLikesFromFeed();
835
+
const seededRef = React.useRef(new Set<string>());
836
+
837
+
useEffect(() => {
838
+
for (const like of likes) {
839
+
if (!seededRef.current.has(like.uri)) {
840
+
seededRef.current.add(like.uri);
841
+
const record = like.value as unknown as ATPAPI.AppBskyFeedLike.Record;
842
+
setFastState(record.subject.uri, {
843
+
target: record.subject.uri,
844
+
uri: like.uri,
845
+
cid: like.cid,
846
+
});
847
+
}
848
+
}
849
+
}, [likes, setFastState]);
850
+
851
+
return (
852
+
<>
853
+
<div className="text-gray-500 dark:text-gray-400 text-lg font-semibold my-3 mx-4">
854
+
Likes
855
+
</div>
856
+
<div>
857
+
{likes.map((like) => {
858
+
if (
859
+
!like ||
860
+
!like?.value ||
861
+
!like?.value?.subject ||
862
+
// @ts-expect-error blehhhhh
863
+
!like?.value?.subject?.uri
864
+
)
865
+
return;
866
+
const likeRecord =
867
+
like.value as unknown as ATPAPI.AppBskyFeedLike.Record;
868
+
return (
869
+
<UniversalPostRendererATURILoader
870
+
key={likeRecord.subject.uri}
871
+
atUri={likeRecord.subject.uri}
872
+
feedviewpost={true}
873
+
/>
874
+
);
875
+
})}
876
+
</div>
877
+
878
+
{/* Loading and "Load More" states */}
879
+
{arePostsLoading && likes.length === 0 && (
880
+
<div className="p-4 text-center text-gray-500">Loading likes...</div>
881
+
)}
882
+
{isFetchingNextPage && (
883
+
<div className="p-4 text-center text-gray-500">Loading more...</div>
884
+
)}
885
+
{hasNextPage && !isFetchingNextPage && (
886
+
<button
887
+
onClick={() => fetchNextPage()}
888
+
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold"
889
+
>
890
+
Load More Likes
891
+
</button>
892
+
)}
893
+
{likes.length === 0 && !arePostsLoading && (
894
+
<div className="p-4 text-center text-gray-500">No likes found.</div>
895
+
)}
896
+
</>
897
+
);
898
+
}
899
+
900
+
export function FollowButton({
901
+
targetdidorhandle,
902
+
}: {
903
+
targetdidorhandle: string;
904
+
}) {
905
+
const { agent } = useAuth();
906
+
const { data: identity } = useQueryIdentity(targetdidorhandle);
907
+
const queryClient = useQueryClient();
908
+
909
+
const followRecords = useGetFollowState({
910
+
target: identity?.did ?? targetdidorhandle,
911
+
user: agent?.did,
912
+
});
913
+
914
+
return (
915
+
<>
916
+
{identity?.did !== agent?.did ? (
917
+
<>
918
+
{!(followRecords?.length && followRecords?.length > 0) ? (
919
+
<button
920
+
onClick={(e) => {
921
+
e.stopPropagation();
922
+
toggleFollow({
923
+
agent: agent || undefined,
924
+
targetDid: identity?.did,
925
+
followRecords: followRecords,
926
+
queryClient: queryClient,
927
+
});
928
+
}}
929
+
className="rounded-full h-10 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors px-4 py-2 text-[14px]"
930
+
>
931
+
Follow
932
+
</button>
933
+
) : (
934
+
<button
935
+
onClick={(e) => {
936
+
e.stopPropagation();
937
+
toggleFollow({
938
+
agent: agent || undefined,
939
+
targetDid: identity?.did,
940
+
followRecords: followRecords,
941
+
queryClient: queryClient,
942
+
});
943
+
}}
944
+
className="rounded-full h-10 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors px-4 py-2 text-[14px]"
945
+
>
946
+
Unfollow
947
+
</button>
948
+
)}
949
+
</>
950
+
) : (
951
+
<button
952
+
className="rounded-full h-10 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors px-4 py-2 text-[14px]"
953
+
onClick={(e) => {
954
+
renderSnack({
955
+
title: "Not Implemented Yet",
956
+
description: "Sorry...",
957
+
//button: { label: 'Try Again', onClick: () => console.log('whatever') },
958
+
});
959
+
}}
960
+
>
961
+
Edit Profile
962
+
</button>
963
+
)}
964
+
</>
965
+
);
966
+
}
967
+
968
+
export function BiteButton({
969
+
targetdidorhandle,
970
+
}: {
971
+
targetdidorhandle: string;
972
+
}) {
973
+
const { agent } = useAuth();
974
+
const { data: identity } = useQueryIdentity(targetdidorhandle);
975
+
const [show] = useAtom(enableBitesAtom);
976
+
977
+
if (!show) return;
978
+
979
+
return (
980
+
<>
981
+
<button
982
+
onClick={async (e) => {
983
+
e.stopPropagation();
984
+
await sendBite({
985
+
agent: agent || undefined,
986
+
targetDid: identity?.did,
987
+
});
988
+
}}
989
+
className="rounded-full h-10 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors px-4 py-2 text-[14px]"
990
+
>
991
+
Bite
992
+
</button>
993
+
</>
994
+
);
995
+
}
996
+
997
+
async function sendBite({
998
+
agent,
999
+
targetDid,
1000
+
}: {
1001
+
agent?: Agent;
1002
+
targetDid?: string;
1003
+
}) {
1004
+
if (!agent?.did || !targetDid) {
1005
+
renderSnack({
1006
+
title: "Bite Failed",
1007
+
description: "You must be logged-in to bite someone.",
1008
+
//button: { label: 'Try Again', onClick: () => console.log('whatever') },
1009
+
});
1010
+
return;
1011
+
}
1012
+
const newRecord = {
1013
+
repo: agent.did,
1014
+
collection: "net.wafrn.feed.bite",
1015
+
rkey: TID.next().toString(),
1016
+
record: {
1017
+
$type: "net.wafrn.feed.bite",
1018
+
subject: "at://" + targetDid,
1019
+
createdAt: new Date().toISOString(),
1020
+
},
1021
+
};
1022
+
1023
+
try {
1024
+
await agent.com.atproto.repo.createRecord(newRecord);
1025
+
renderSnack({
1026
+
title: "Bite Sent",
1027
+
description: "Your bite was delivered.",
1028
+
//button: { label: 'Undo', onClick: () => console.log('Undo clicked') },
1029
+
});
1030
+
} catch (err) {
1031
+
console.error("Bite failed:", err);
1032
+
renderSnack({
1033
+
title: "Bite Failed",
1034
+
description: "Your bite failed to be delivered.",
1035
+
//button: { label: 'Try Again', onClick: () => console.log('whatever') },
1036
+
});
1037
+
}
1038
+
}
1039
+
1040
+
export function Mutual({ targetdidorhandle }: { targetdidorhandle: string }) {
1041
+
const { agent } = useAuth();
1042
+
const { data: identity } = useQueryIdentity(targetdidorhandle);
1043
+
1044
+
const theyFollowYouRes = useGetOneToOneState(
1045
+
agent?.did
1046
+
? {
1047
+
target: agent?.did,
1048
+
user: identity?.did ?? targetdidorhandle,
1049
+
collection: "app.bsky.graph.follow",
1050
+
path: ".subject",
1051
+
}
1052
+
: undefined
1053
+
);
1054
+
1055
+
const youFollowThemRes = useGetFollowState({
1056
+
target: identity?.did ?? targetdidorhandle,
1057
+
user: agent?.did,
1058
+
});
1059
+
1060
+
const theyFollowYou: boolean =
1061
+
!!theyFollowYouRes?.length && theyFollowYouRes.length > 0;
1062
+
const youFollowThem: boolean =
1063
+
!!youFollowThemRes?.length && youFollowThemRes.length > 0;
1064
+
1065
+
return (
1066
+
<>
1067
+
{/* if not self */}
1068
+
{identity?.did !== agent?.did ? (
1069
+
<>
1070
+
{theyFollowYou ? (
1071
+
<>
1072
+
{youFollowThem ? (
1073
+
<div className=" text-sm px-1.5 py-0.5 text-gray-500 bg-gray-200 dark:text-gray-400 dark:bg-gray-800 rounded-lg flex flex-row items-center justify-center">
1074
+
mutuals
1075
+
</div>
1076
+
) : (
1077
+
<div className=" text-sm px-1.5 py-0.5 text-gray-500 bg-gray-200 dark:text-gray-400 dark:bg-gray-800 rounded-lg flex flex-row items-center justify-center">
1078
+
follows you
1079
+
</div>
1080
+
)}
1081
+
</>
1082
+
) : (
1083
+
<></>
1084
+
)}
1085
+
</>
1086
+
) : (
1087
+
// lmao can someone be mutuals with themselves ??
1088
+
<></>
1089
+
)}
1090
+
</>
1091
+
);
1092
+
}
1093
+
1094
+
export function RichTextRenderer({ description }: { description: string }) {
1095
+
const [richDescription, setRichDescription] = useState<string | ReactNode[]>(
1096
+
description
1097
+
);
1098
+
const { agent } = useAuth();
1099
+
const navigate = useNavigate();
1100
+
1101
+
useEffect(() => {
1102
+
let mounted = true;
1103
+
1104
+
// setRichDescription(description);
1105
+
1106
+
async function processRichText() {
1107
+
try {
1108
+
if (!agent?.did) return;
1109
+
const rt = new RichText({ text: description });
1110
+
await rt.detectFacets(agent);
1111
+
1112
+
if (!mounted) return;
1113
+
1114
+
if (rt.facets) {
1115
+
setRichDescription(
1116
+
renderTextWithFacets({ text: rt.text, facets: rt.facets, navigate })
1117
+
);
1118
+
} else {
1119
+
setRichDescription(rt.text);
1120
+
}
1121
+
} catch (error) {
1122
+
console.error("Failed to detect facets:", error);
1123
+
if (mounted) {
1124
+
setRichDescription(description);
1125
+
}
1126
+
}
1127
+
}
1128
+
1129
+
processRichText();
1130
+
1131
+
return () => {
1132
+
mounted = false;
1133
+
};
1134
+
}, [description, agent, navigate]);
1135
+
1136
+
return <>{richDescription}</>;
1137
+
}
+165
src/routes/profile.$did/post.$rkey.image.$i.tsx
+165
src/routes/profile.$did/post.$rkey.image.$i.tsx
···
···
1
+
import {
2
+
createFileRoute,
3
+
useNavigate,
4
+
type UseNavigateResult,
5
+
} from "@tanstack/react-router";
6
+
import { useEffect, useState } from "react";
7
+
import { createPortal } from "react-dom";
8
+
9
+
import { ProfilePostComponent } from "./post.$rkey";
10
+
11
+
export const Route = createFileRoute("/profile/$did/post/$rkey/image/$i")({
12
+
component: Lightbox,
13
+
});
14
+
15
+
export type LightboxProps = {
16
+
images: { src: string; alt?: string }[];
17
+
};
18
+
19
+
function nextprev({
20
+
index,
21
+
images,
22
+
navigate,
23
+
did,
24
+
rkey,
25
+
prev,
26
+
}: {
27
+
index?: number;
28
+
images?: LightboxProps["images"];
29
+
navigate: UseNavigateResult<string>;
30
+
did: string;
31
+
rkey: string;
32
+
prev?: boolean;
33
+
}) {
34
+
const len = images?.length ?? 0;
35
+
if (len === 0) return;
36
+
37
+
const nextIndex = ((index ?? 0) + (prev ? -1 : 1) + len) % len;
38
+
39
+
navigate({
40
+
to: "/profile/$did/post/$rkey/image/$i",
41
+
params: {
42
+
did,
43
+
rkey,
44
+
i: nextIndex.toString(),
45
+
},
46
+
replace: true,
47
+
});
48
+
}
49
+
50
+
export function Lightbox() {
51
+
console.log("hey the $i route is loaded w!!!");
52
+
const { did, rkey, i } = Route.useParams();
53
+
const [images, setImages] = useState<LightboxProps["images"] | undefined>(
54
+
undefined
55
+
);
56
+
const index = Number(i);
57
+
const navigate = useNavigate();
58
+
const post = true;
59
+
const image = images?.[index] ?? undefined;
60
+
61
+
function lightboxCallback(d: LightboxProps) {
62
+
console.log("callback actually called!");
63
+
setImages(d.images);
64
+
}
65
+
66
+
useEffect(() => {
67
+
function handleKey(e: KeyboardEvent) {
68
+
if (e.key === "Escape") window.history.back();
69
+
if (e.key === "ArrowRight")
70
+
nextprev({ index, images, navigate, did, rkey });
71
+
//onNavigate((index + 1) % images.length);
72
+
if (e.key === "ArrowLeft")
73
+
nextprev({ index, images, navigate, did, rkey, prev: true });
74
+
//onNavigate((index - 1 + images.length) % images.length);
75
+
}
76
+
window.addEventListener("keydown", handleKey);
77
+
return () => window.removeEventListener("keydown", handleKey);
78
+
}, [index, navigate, did, rkey, images]);
79
+
80
+
return createPortal(
81
+
<>
82
+
{post && (
83
+
<div
84
+
onClick={(e) => {
85
+
e.stopPropagation();
86
+
e.nativeEvent.stopImmediatePropagation();
87
+
}}
88
+
className="lightbox-sidebar hidden lg:flex overscroll-none disablegutter disablescroll border-l dark:border-gray-800 was7 border-gray-300 fixed z-50 top-0 right-0 flex-col max-w-[350px] min-w-[350px] max-h-screen overflow-y-scroll dark:bg-gray-950 bg-white"
89
+
>
90
+
<ProfilePostComponent
91
+
key={`/profile/${did}/post/${rkey}`}
92
+
did={did}
93
+
rkey={rkey}
94
+
nopics
95
+
lightboxCallback={lightboxCallback}
96
+
/>
97
+
</div>
98
+
)}
99
+
<div
100
+
className="lightbox fixed inset-0 z-50 flex items-center justify-center bg-black/80 w-screen lg:w-[calc(100vw-350px)] lg:max-w-[calc(100vw-350px)]"
101
+
onClick={(e) => {
102
+
e.stopPropagation();
103
+
window.history.back();
104
+
}}
105
+
>
106
+
<img
107
+
src={image?.src}
108
+
alt={image?.alt}
109
+
className="max-h-[90%] max-w-[90%] object-contain rounded-lg shadow-lg"
110
+
onClick={(e) => e.stopPropagation()}
111
+
/>
112
+
113
+
{(images?.length ?? 0) > 1 && (
114
+
<>
115
+
<button
116
+
onClick={(e) => {
117
+
e.stopPropagation();
118
+
nextprev({ index, images, navigate, did, rkey, prev: true });
119
+
}}
120
+
className="absolute left-4 top-1/2 -translate-y-1/2 text-white text-4xl h-8 w-8 rounded-full bg-gray-900 flex items-center justify-center"
121
+
>
122
+
<svg
123
+
xmlns="http://www.w3.org/2000/svg"
124
+
width={28}
125
+
height={28}
126
+
viewBox="0 0 24 24"
127
+
>
128
+
<g fill="none" fillRule="evenodd">
129
+
<path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"></path>
130
+
<path
131
+
fill="currentColor"
132
+
d="M8.293 12.707a1 1 0 0 1 0-1.414l5.657-5.657a1 1 0 1 1 1.414 1.414L10.414 12l4.95 4.95a1 1 0 0 1-1.414 1.414z"
133
+
></path>
134
+
</g>
135
+
</svg>
136
+
</button>
137
+
<button
138
+
onClick={(e) => {
139
+
e.stopPropagation();
140
+
nextprev({ index, images, navigate, did, rkey });
141
+
}}
142
+
className="absolute right-4 top-1/2 -translate-y-1/2 text-white text-4xl h-8 w-8 rounded-full bg-gray-900 flex items-center justify-center"
143
+
>
144
+
<svg
145
+
xmlns="http://www.w3.org/2000/svg"
146
+
width={28}
147
+
height={28}
148
+
viewBox="0 0 24 24"
149
+
>
150
+
<g fill="none" fillRule="evenodd">
151
+
<path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"></path>
152
+
<path
153
+
fill="currentColor"
154
+
d="M15.707 11.293a1 1 0 0 1 0 1.414l-5.657 5.657a1 1 0 1 1-1.414-1.414l4.95-4.95l-4.95-4.95a1 1 0 0 1 1.414-1.414z"
155
+
></path>
156
+
</g>
157
+
</svg>
158
+
</button>
159
+
</>
160
+
)}
161
+
</div>
162
+
</>,
163
+
document.body
164
+
);
165
+
}
+100
src/routes/profile.$did/post.$rkey.liked-by.tsx
+100
src/routes/profile.$did/post.$rkey.liked-by.tsx
···
···
1
+
import { useInfiniteQuery } from "@tanstack/react-query";
2
+
import { createFileRoute } from "@tanstack/react-router";
3
+
import { useAtom } from "jotai";
4
+
import React from "react";
5
+
6
+
import { Header } from "~/components/Header";
7
+
import { constellationURLAtom } from "~/utils/atoms";
8
+
import { useQueryIdentity, yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks } from "~/utils/useQuery";
9
+
10
+
import {
11
+
EmptyState,
12
+
ErrorState,
13
+
LoadingState,
14
+
NotificationItem,
15
+
} from "../notifications";
16
+
17
+
export const Route = createFileRoute("/profile/$did/post/$rkey/liked-by")({
18
+
component: RouteComponent,
19
+
});
20
+
21
+
function RouteComponent() {
22
+
const { did, rkey } = Route.useParams();
23
+
const { data: identity } = useQueryIdentity(did);
24
+
const atUri = identity?.did && rkey ? `at://${decodeURIComponent(identity.did)}/app.bsky.feed.post/${rkey}` : '';
25
+
26
+
const [constellationurl] = useAtom(constellationURLAtom);
27
+
const infinitequeryresults = useInfiniteQuery({
28
+
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
29
+
{
30
+
constellation: constellationurl,
31
+
method: "/links",
32
+
target: atUri,
33
+
collection: "app.bsky.feed.like",
34
+
path: ".subject.uri",
35
+
}
36
+
),
37
+
enabled: !!atUri,
38
+
});
39
+
40
+
const {
41
+
data: infiniteLikesData,
42
+
fetchNextPage,
43
+
hasNextPage,
44
+
isFetchingNextPage,
45
+
isLoading,
46
+
isError,
47
+
error,
48
+
} = infinitequeryresults;
49
+
50
+
const likesAturis = React.useMemo(() => {
51
+
// Get all replies from the standard infinite query
52
+
return (
53
+
infiniteLikesData?.pages.flatMap(
54
+
(page) =>
55
+
page?.linking_records.map(
56
+
(r) => `at://${r.did}/${r.collection}/${r.rkey}`
57
+
) ?? []
58
+
) ?? []
59
+
);
60
+
}, [infiniteLikesData]);
61
+
62
+
return (
63
+
<>
64
+
<Header
65
+
title={`Liked By`}
66
+
backButtonCallback={() => {
67
+
if (window.history.length > 1) {
68
+
window.history.back();
69
+
} else {
70
+
window.location.assign("/");
71
+
}
72
+
}}
73
+
/>
74
+
75
+
<>
76
+
{(() => {
77
+
if (isLoading) return <LoadingState text="Loading likes..." />;
78
+
if (isError) return <ErrorState error={error} />;
79
+
80
+
if (!likesAturis?.length)
81
+
return <EmptyState text="No likes yet." />;
82
+
})()}
83
+
</>
84
+
85
+
{likesAturis.map((m) => (
86
+
<NotificationItem key={m} notification={m} />
87
+
))}
88
+
89
+
{hasNextPage && (
90
+
<button
91
+
onClick={() => fetchNextPage()}
92
+
disabled={isFetchingNextPage}
93
+
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold disabled:opacity-50"
94
+
>
95
+
{isFetchingNextPage ? "Loading..." : "Load More"}
96
+
</button>
97
+
)}
98
+
</>
99
+
);
100
+
}
+141
src/routes/profile.$did/post.$rkey.quotes.tsx
+141
src/routes/profile.$did/post.$rkey.quotes.tsx
···
···
1
+
import { useInfiniteQuery } from "@tanstack/react-query";
2
+
import { createFileRoute } from "@tanstack/react-router";
3
+
import { useAtom } from "jotai";
4
+
import React from "react";
5
+
6
+
import { Header } from "~/components/Header";
7
+
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
8
+
import { constellationURLAtom } from "~/utils/atoms";
9
+
import { type linksRecord,useQueryIdentity, yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks } from "~/utils/useQuery";
10
+
11
+
import {
12
+
EmptyState,
13
+
ErrorState,
14
+
LoadingState,
15
+
} from "../notifications";
16
+
17
+
export const Route = createFileRoute("/profile/$did/post/$rkey/quotes")({
18
+
component: RouteComponent,
19
+
});
20
+
21
+
function RouteComponent() {
22
+
const { did, rkey } = Route.useParams();
23
+
const { data: identity } = useQueryIdentity(did);
24
+
const atUri = identity?.did && rkey ? `at://${decodeURIComponent(identity.did)}/app.bsky.feed.post/${rkey}` : '';
25
+
26
+
const [constellationurl] = useAtom(constellationURLAtom);
27
+
const infinitequeryresultsWithoutMedia = useInfiniteQuery({
28
+
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
29
+
{
30
+
constellation: constellationurl,
31
+
method: "/links",
32
+
target: atUri,
33
+
collection: "app.bsky.feed.post",
34
+
path: ".embed.record.uri", // embed.record.record.uri and embed.record.uri
35
+
}
36
+
),
37
+
enabled: !!atUri,
38
+
});
39
+
const infinitequeryresultsWithMedia = useInfiniteQuery({
40
+
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
41
+
{
42
+
constellation: constellationurl,
43
+
method: "/links",
44
+
target: atUri,
45
+
collection: "app.bsky.feed.post",
46
+
path: ".embed.record.record.uri", // embed.record.record.uri and embed.record.uri
47
+
}
48
+
),
49
+
enabled: !!atUri,
50
+
});
51
+
52
+
const {
53
+
data: infiniteQuotesDataWithoutMedia,
54
+
fetchNextPage: fetchNextPageWithoutMedia,
55
+
hasNextPage: hasNextPageWithoutMedia,
56
+
isFetchingNextPage: isFetchingNextPageWithoutMedia,
57
+
isLoading: isLoadingWithoutMedia,
58
+
isError: isErrorWithoutMedia,
59
+
error: errorWithoutMedia,
60
+
} = infinitequeryresultsWithoutMedia;
61
+
const {
62
+
data: infiniteQuotesDataWithMedia,
63
+
fetchNextPage: fetchNextPageWithMedia,
64
+
hasNextPage: hasNextPageWithMedia,
65
+
isFetchingNextPage: isFetchingNextPageWithMedia,
66
+
isLoading: isLoadingWithMedia,
67
+
isError: isErrorWithMedia,
68
+
error: errorWithMedia,
69
+
} = infinitequeryresultsWithMedia;
70
+
71
+
const fetchNextPage = async () => {
72
+
await Promise.all([
73
+
hasNextPageWithMedia && fetchNextPageWithMedia(),
74
+
hasNextPageWithoutMedia && fetchNextPageWithoutMedia(),
75
+
]);
76
+
};
77
+
78
+
const hasNextPage = hasNextPageWithMedia || hasNextPageWithoutMedia;
79
+
const isFetchingNextPage = isFetchingNextPageWithMedia || isFetchingNextPageWithoutMedia;
80
+
const isLoading = isLoadingWithMedia || isLoadingWithoutMedia;
81
+
82
+
const allQuotes = React.useMemo(() => {
83
+
const withPages = infiniteQuotesDataWithMedia?.pages ?? [];
84
+
const withoutPages = infiniteQuotesDataWithoutMedia?.pages ?? [];
85
+
const maxLen = Math.max(withPages.length, withoutPages.length);
86
+
const merged: linksRecord[] = [];
87
+
88
+
for (let i = 0; i < maxLen; i++) {
89
+
const a = withPages[i]?.linking_records ?? [];
90
+
const b = withoutPages[i]?.linking_records ?? [];
91
+
const mergedPage = [...a, ...b].sort((b, a) => a.rkey.localeCompare(b.rkey));
92
+
merged.push(...mergedPage);
93
+
}
94
+
95
+
return merged;
96
+
}, [infiniteQuotesDataWithMedia?.pages, infiniteQuotesDataWithoutMedia?.pages]);
97
+
98
+
const quotesAturis = React.useMemo(() => {
99
+
return allQuotes.flatMap((r) => `at://${r.did}/${r.collection}/${r.rkey}`);
100
+
}, [allQuotes]);
101
+
102
+
return (
103
+
<>
104
+
<Header
105
+
title={`Quotes`}
106
+
backButtonCallback={() => {
107
+
if (window.history.length > 1) {
108
+
window.history.back();
109
+
} else {
110
+
window.location.assign("/");
111
+
}
112
+
}}
113
+
/>
114
+
115
+
<>
116
+
{(() => {
117
+
if (isLoading) return <LoadingState text="Loading quotes..." />;
118
+
if (isErrorWithMedia) return <ErrorState error={errorWithMedia} />;
119
+
if (isErrorWithoutMedia) return <ErrorState error={errorWithoutMedia} />;
120
+
121
+
if (!quotesAturis?.length)
122
+
return <EmptyState text="No quotes yet." />;
123
+
})()}
124
+
</>
125
+
126
+
{quotesAturis.map((m) => (
127
+
<UniversalPostRendererATURILoader key={m} atUri={m} />
128
+
))}
129
+
130
+
{hasNextPage && (
131
+
<button
132
+
onClick={() => fetchNextPage()}
133
+
disabled={isFetchingNextPage}
134
+
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold disabled:opacity-50"
135
+
>
136
+
{isFetchingNextPage ? "Loading..." : "Load More"}
137
+
</button>
138
+
)}
139
+
</>
140
+
);
141
+
}
+100
src/routes/profile.$did/post.$rkey.reposted-by.tsx
+100
src/routes/profile.$did/post.$rkey.reposted-by.tsx
···
···
1
+
import { useInfiniteQuery } from "@tanstack/react-query";
2
+
import { createFileRoute } from "@tanstack/react-router";
3
+
import { useAtom } from "jotai";
4
+
import React from "react";
5
+
6
+
import { Header } from "~/components/Header";
7
+
import { constellationURLAtom } from "~/utils/atoms";
8
+
import { useQueryIdentity, yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks } from "~/utils/useQuery";
9
+
10
+
import {
11
+
EmptyState,
12
+
ErrorState,
13
+
LoadingState,
14
+
NotificationItem,
15
+
} from "../notifications";
16
+
17
+
export const Route = createFileRoute("/profile/$did/post/$rkey/reposted-by")({
18
+
component: RouteComponent,
19
+
});
20
+
21
+
function RouteComponent() {
22
+
const { did, rkey } = Route.useParams();
23
+
const { data: identity } = useQueryIdentity(did);
24
+
const atUri = identity?.did && rkey ? `at://${decodeURIComponent(identity.did)}/app.bsky.feed.post/${rkey}` : '';
25
+
26
+
const [constellationurl] = useAtom(constellationURLAtom);
27
+
const infinitequeryresults = useInfiniteQuery({
28
+
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
29
+
{
30
+
constellation: constellationurl,
31
+
method: "/links",
32
+
target: atUri,
33
+
collection: "app.bsky.feed.repost",
34
+
path: ".subject.uri",
35
+
}
36
+
),
37
+
enabled: !!atUri,
38
+
});
39
+
40
+
const {
41
+
data: infiniteRepostsData,
42
+
fetchNextPage,
43
+
hasNextPage,
44
+
isFetchingNextPage,
45
+
isLoading,
46
+
isError,
47
+
error,
48
+
} = infinitequeryresults;
49
+
50
+
const repostsAturis = React.useMemo(() => {
51
+
// Get all replies from the standard infinite query
52
+
return (
53
+
infiniteRepostsData?.pages.flatMap(
54
+
(page) =>
55
+
page?.linking_records.map(
56
+
(r) => `at://${r.did}/${r.collection}/${r.rkey}`
57
+
) ?? []
58
+
) ?? []
59
+
);
60
+
}, [infiniteRepostsData]);
61
+
62
+
return (
63
+
<>
64
+
<Header
65
+
title={`Reposted By`}
66
+
backButtonCallback={() => {
67
+
if (window.history.length > 1) {
68
+
window.history.back();
69
+
} else {
70
+
window.location.assign("/");
71
+
}
72
+
}}
73
+
/>
74
+
75
+
<>
76
+
{(() => {
77
+
if (isLoading) return <LoadingState text="Loading reposts..." />;
78
+
if (isError) return <ErrorState error={error} />;
79
+
80
+
if (!repostsAturis?.length)
81
+
return <EmptyState text="No reposts yet." />;
82
+
})()}
83
+
</>
84
+
85
+
{repostsAturis.map((m) => (
86
+
<NotificationItem key={m} notification={m} />
87
+
))}
88
+
89
+
{hasNextPage && (
90
+
<button
91
+
onClick={() => fetchNextPage()}
92
+
disabled={isFetchingNextPage}
93
+
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold disabled:opacity-50"
94
+
>
95
+
{isFetchingNextPage ? "Loading..." : "Load More"}
96
+
</button>
97
+
)}
98
+
</>
99
+
);
100
+
}
+292
-116
src/routes/profile.$did/post.$rkey.tsx
+292
-116
src/routes/profile.$did/post.$rkey.tsx
···
1
-
import { useQueryClient } from "@tanstack/react-query";
2
-
import { createFileRoute, Link } from "@tanstack/react-router";
3
import React, { useLayoutEffect } from "react";
4
5
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
6
//import { usePersistentStore } from '~/providers/PersistentStoreProvider';
7
import {
8
constructPostQuery,
9
useQueryConstellation,
10
useQueryIdentity,
11
useQueryPost,
12
} from "~/utils/useQuery";
13
14
//const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour
15
···
32
);
33
}
34
35
-
function ProfilePostComponent({ did, rkey }: { did: string; rkey: string }) {
36
//const { get, set } = usePersistentStore();
37
const queryClient = useQueryClient();
38
// const [resolvedDid, setResolvedDid] = React.useState<string | null>(null);
···
171
data: identity,
172
isLoading: isIdentityLoading,
173
error: identityError,
174
-
} = useQueryIdentity(did);
175
176
const resolvedDid = did.startsWith("did:") ? did : identity?.did;
177
178
const atUri = React.useMemo(
179
() =>
180
-
resolvedDid
181
? `at://${decodeURIComponent(resolvedDid)}/app.bsky.feed.post/${rkey}`
182
-
: "",
183
-
[resolvedDid, rkey]
184
);
185
186
-
const { data: mainPost } = useQueryPost(atUri);
187
188
-
const { data: repliesData } = useQueryConstellation({
189
-
method: "/links",
190
target: atUri,
191
-
collection: "app.bsky.feed.post",
192
-
path: ".reply.parent.uri",
193
});
194
-
const replies = repliesData?.linking_records.slice(0, 50) ?? [];
195
196
const [parents, setParents] = React.useState<any[]>([]);
197
const [parentsLoading, setParentsLoading] = React.useState(false);
198
199
const mainPostRef = React.useRef<HTMLDivElement>(null);
200
-
const userHasScrolled = React.useRef(false);
201
202
-
const scrollAnchor = React.useRef<{ top: number } | null>(null);
203
204
205
-
React.useEffect(() => {
206
-
const onScroll = () => {
207
208
-
if (window.scrollY > 50) {
209
-
userHasScrolled.current = true;
210
211
-
window.removeEventListener("scroll", onScroll);
212
-
}
213
-
};
214
215
-
if (!userHasScrolled.current) {
216
-
window.addEventListener("scroll", onScroll, { passive: true });
217
}
218
-
return () => window.removeEventListener("scroll", onScroll);
219
-
}, []);
220
221
-
useLayoutEffect(() => {
222
-
if (parentsLoading && mainPostRef.current && !userHasScrolled.current) {
223
-
scrollAnchor.current = {
224
-
top: mainPostRef.current.getBoundingClientRect().top,
225
-
};
226
}
227
-
}, [parentsLoading]);
228
229
-
useLayoutEffect(() => {
230
-
if (
231
-
scrollAnchor.current &&
232
-
mainPostRef.current &&
233
-
!userHasScrolled.current
234
-
) {
235
-
const newTop = mainPostRef.current.getBoundingClientRect().top;
236
-
const topDiff = newTop - scrollAnchor.current.top;
237
-
if (topDiff > 0) {
238
-
window.scrollBy(0, topDiff);
239
-
}
240
-
scrollAnchor.current = null;
241
}
242
-
}, [parents]);
243
244
React.useEffect(() => {
245
if (!mainPost?.value?.reply?.parent?.uri) {
···
258
while (currentParentUri && safetyCounter < MAX_PARENTS) {
259
try {
260
const parentPost = await queryClient.fetchQuery(
261
-
constructPostQuery(currentParentUri)
262
);
263
if (!parentPost) break;
264
parentChain.push(parentPost);
···
280
return () => {
281
ignore = true;
282
};
283
-
}, [mainPost, queryClient]);
284
285
-
if (!did || !rkey) return <div>Invalid post URI</div>;
286
-
if (isIdentityLoading) return <div>Resolving handle...</div>;
287
-
if (identityError)
288
return <div style={{ color: "red" }}>{identityError.message}</div>;
289
-
if (!atUri) return <div>Could not construct post URI.</div>;
290
291
return (
292
<>
293
-
<div className="flex items-center gap-2 px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700">
294
-
<Link
295
-
to=".."
296
-
className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg"
297
-
onClick={(e) => {
298
-
e.preventDefault();
299
if (window.history.length > 1) {
300
window.history.back();
301
} else {
302
window.location.assign("/");
303
}
304
}}
305
-
aria-label="Go back"
306
-
>
307
-
โ
308
-
</Link>
309
-
<span className="text-xl font-bold ml-2">Post</span>
310
-
</div>
311
312
-
{parentsLoading && (
313
-
<div className="text-center text-gray-500 dark:text-gray-400 flex flex-row">
314
-
<div className="ml-4 w-[42px] flex justify-center">
315
-
<div
316
-
style={{ width: 2, height: "100%", opacity: 0.5 }}
317
-
className="bg-gray-500 dark:bg-gray-400"
318
-
></div>
319
</div>
320
-
Loading conversation...
321
-
</div>
322
-
)}
323
324
-
{/* we should use the reply lines here thats provided by UPR*/}
325
-
<div style={{ maxWidth: 600, margin: "0px auto 0", padding: 0 }}>
326
-
{parents.map((parent, index) => (
327
<UniversalPostRendererATURILoader
328
-
key={parent.uri}
329
-
atUri={parent.uri}
330
-
topReplyLine={index > 0}
331
-
bottomReplyLine={true}
332
-
bottomBorder={false}
333
/>
334
-
))}
335
-
</div>
336
-
<div ref={mainPostRef}>
337
-
<UniversalPostRendererATURILoader
338
-
atUri={atUri}
339
-
detailed={true}
340
-
topReplyLine={parentsLoading || parents.length > 0}
341
-
/>
342
-
</div>
343
-
<div
344
-
style={{
345
-
maxWidth: 600,
346
-
margin: "0px auto 0",
347
-
padding: 0,
348
-
minHeight: "100dvh",
349
-
}}
350
-
>
351
<div
352
-
className="text-gray-500 dark:text-gray-400 text-sm font-bold"
353
style={{
354
-
fontSize: 18,
355
-
margin: "12px 16px 12px 16px",
356
-
fontWeight: 600,
357
}}
358
>
359
-
Replies
360
</div>
361
-
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
362
-
{replies.length > 0 &&
363
-
replies.map((reply) => {
364
-
const replyAtUri = `at://${reply.did}/app.bsky.feed.post/${reply.rkey}`;
365
-
return (
366
-
<UniversalPostRendererATURILoader
367
-
key={replyAtUri}
368
-
atUri={replyAtUri}
369
-
/>
370
-
);
371
-
})}
372
-
</div>
373
-
</div>
374
</>
375
);
376
}
···
1
+
import { AtUri } from "@atproto/api";
2
+
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
3
+
import { createFileRoute, Outlet, useMatchRoute } from "@tanstack/react-router";
4
+
import { useAtom } from "jotai";
5
import React, { useLayoutEffect } from "react";
6
7
+
import { Header } from "~/components/Header";
8
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
9
+
import { constellationURLAtom, slingshotURLAtom } from "~/utils/atoms";
10
//import { usePersistentStore } from '~/providers/PersistentStoreProvider';
11
import {
12
constructPostQuery,
13
+
type linksAllResponse,
14
+
type linksRecordsResponse,
15
useQueryConstellation,
16
useQueryIdentity,
17
useQueryPost,
18
+
yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks,
19
} from "~/utils/useQuery";
20
+
21
+
import type { LightboxProps } from "./post.$rkey.image.$i";
22
23
//const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour
24
···
41
);
42
}
43
44
+
export function ProfilePostComponent({
45
+
did,
46
+
rkey,
47
+
nopics,
48
+
lightboxCallback,
49
+
}: {
50
+
did: string;
51
+
rkey: string;
52
+
nopics?: boolean;
53
+
lightboxCallback?: (d: LightboxProps) => void;
54
+
}) {
55
+
const matchRoute = useMatchRoute()
56
+
const showMainPostRoute = !!matchRoute({ to: '/profile/$did/post/$rkey' }) || !!matchRoute({ to: '/profile/$did/post/$rkey/image/$i' })
57
+
58
//const { get, set } = usePersistentStore();
59
const queryClient = useQueryClient();
60
// const [resolvedDid, setResolvedDid] = React.useState<string | null>(null);
···
193
data: identity,
194
isLoading: isIdentityLoading,
195
error: identityError,
196
+
} = useQueryIdentity(showMainPostRoute ? did : undefined);
197
198
const resolvedDid = did.startsWith("did:") ? did : identity?.did;
199
200
const atUri = React.useMemo(
201
() =>
202
+
resolvedDid && showMainPostRoute
203
? `at://${decodeURIComponent(resolvedDid)}/app.bsky.feed.post/${rkey}`
204
+
: undefined,
205
+
[resolvedDid, rkey, showMainPostRoute]
206
);
207
208
+
const { data: mainPost } = useQueryPost(showMainPostRoute ? atUri : undefined);
209
+
210
+
console.log("atUri",atUri)
211
+
212
+
const opdid = React.useMemo(
213
+
() =>
214
+
atUri
215
+
? new AtUri(atUri).host
216
+
: undefined,
217
+
[atUri]
218
+
);
219
220
+
// @ts-expect-error i hate overloads
221
+
const { data: links } = useQueryConstellation(atUri&&showMainPostRoute?{
222
+
method: "/links/all",
223
target: atUri,
224
+
} : {
225
+
method: "undefined",
226
+
target: ""
227
+
})as { data: linksAllResponse | undefined };
228
+
229
+
//const [likes, setLikes] = React.useState<number | null>(null);
230
+
//const [reposts, setReposts] = React.useState<number | null>(null);
231
+
const [replyCount, setReplyCount] = React.useState<number | null>(null);
232
+
233
+
React.useEffect(() => {
234
+
// /*mass comment*/ console.log(JSON.stringify(links, null, 2));
235
+
// setLikes(
236
+
// links
237
+
// ? links?.links?.["app.bsky.feed.like"]?.[".subject.uri"]?.records || 0
238
+
// : null
239
+
// );
240
+
// setReposts(
241
+
// links
242
+
// ? links?.links?.["app.bsky.feed.repost"]?.[".subject.uri"]?.records || 0
243
+
// : null
244
+
// );
245
+
setReplyCount(
246
+
links
247
+
? links?.links?.["app.bsky.feed.post"]?.[".reply.parent.uri"]
248
+
?.records || 0
249
+
: null
250
+
);
251
+
}, [links]);
252
+
253
+
const { data: opreplies } = useQueryConstellation(
254
+
showMainPostRoute && !!opdid && replyCount && replyCount >= 25
255
+
? {
256
+
method: "/links",
257
+
target: atUri,
258
+
// @ts-expect-error overloading sucks so much
259
+
collection: "app.bsky.feed.post",
260
+
path: ".reply.parent.uri",
261
+
//cursor?: string;
262
+
dids: [opdid],
263
+
}
264
+
: {
265
+
method: "undefined",
266
+
target: "",
267
+
}
268
+
) as { data: linksRecordsResponse | undefined };
269
+
270
+
const opReplyAturis =
271
+
opreplies?.linking_records.map(
272
+
(r) => `at://${r.did}/${r.collection}/${r.rkey}`,
273
+
) ?? [];
274
+
275
+
276
+
// const { data: repliesData } = useQueryConstellation({
277
+
// method: "/links",
278
+
// target: atUri,
279
+
// collection: "app.bsky.feed.post",
280
+
// path: ".reply.parent.uri",
281
+
// });
282
+
// const replies = repliesData?.linking_records.slice(0, 50) ?? [];
283
+
const [constellationurl] = useAtom(constellationURLAtom)
284
+
285
+
const infinitequeryresults = useInfiniteQuery({
286
+
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
287
+
{
288
+
constellation: constellationurl,
289
+
method: "/links",
290
+
target: atUri,
291
+
collection: "app.bsky.feed.post",
292
+
path: ".reply.parent.uri",
293
+
}
294
+
),
295
+
enabled: !!atUri && showMainPostRoute,
296
});
297
+
298
+
const {
299
+
data: infiniteRepliesData,
300
+
fetchNextPage,
301
+
hasNextPage,
302
+
isFetchingNextPage,
303
+
} = infinitequeryresults;
304
+
305
+
// // auto-fetch all pages
306
+
// useEffect(() => {
307
+
// if (
308
+
// infinitequeryresults.hasNextPage &&
309
+
// !infinitequeryresults.isFetchingNextPage
310
+
// ) {
311
+
// console.log("Fetching the next page...");
312
+
// infinitequeryresults.fetchNextPage();
313
+
// }
314
+
// }, [infinitequeryresults]);
315
+
316
+
// const replyAturis = repliesData
317
+
// ? repliesData.pages.flatMap((page) =>
318
+
// page
319
+
// ? page.linking_records.map((record) => {
320
+
// const aturi = `at://${record.did}/${record.collection}/${record.rkey}`;
321
+
// return aturi;
322
+
// })
323
+
// : []
324
+
// )
325
+
// : [];
326
+
327
+
const replyAturis = React.useMemo(() => {
328
+
// Get all replies from the standard infinite query
329
+
const allReplies =
330
+
infiniteRepliesData?.pages.flatMap(
331
+
(page) =>
332
+
page?.linking_records.map(
333
+
(r) => `at://${r.did}/${r.collection}/${r.rkey}`,
334
+
) ?? [],
335
+
) ?? [];
336
+
337
+
if (replyCount && (replyCount < 25)) {
338
+
// If count is low, just use the standard list and find the oldest OP reply to move to the top
339
+
const opdidFromUri = atUri ? new AtUri(atUri).host : undefined;
340
+
const oldestOpsIndex = allReplies.findIndex(
341
+
(aturi) => new AtUri(aturi).host === opdidFromUri,
342
+
);
343
+
if (oldestOpsIndex > 0) {
344
+
const [oldestOpsReply] = allReplies.splice(oldestOpsIndex, 1);
345
+
allReplies.unshift(oldestOpsReply);
346
+
}
347
+
return allReplies;
348
+
} else {
349
+
// If count is high, prioritize OP replies from the special query
350
+
// and filter them out from the main list to avoid duplication.
351
+
const opReplySet = new Set(opReplyAturis);
352
+
const otherReplies = allReplies.filter((uri) => !opReplySet.has(uri));
353
+
return [...opReplyAturis, ...otherReplies];
354
+
}
355
+
}, [infiniteRepliesData, opReplyAturis, replyCount, atUri]);
356
+
357
+
// Find oldest OP reply
358
+
const oldestOpsIndex = replyAturis.findIndex(
359
+
(aturi) => new AtUri(aturi).host === opdid
360
+
);
361
+
362
+
// Reorder: move oldest OP reply to the front
363
+
if (oldestOpsIndex > 0) {
364
+
const [oldestOpsReply] = replyAturis.splice(oldestOpsIndex, 1);
365
+
replyAturis.unshift(oldestOpsReply);
366
+
}
367
368
const [parents, setParents] = React.useState<any[]>([]);
369
const [parentsLoading, setParentsLoading] = React.useState(false);
370
371
const mainPostRef = React.useRef<HTMLDivElement>(null);
372
+
const hasPerformedInitialLayout = React.useRef(false);
373
374
+
const [layoutReady, setLayoutReady] = React.useState(false);
375
376
+
useLayoutEffect(() => {
377
+
if (!showMainPostRoute) return
378
+
if (parents.length > 0 && !layoutReady && mainPostRef.current) {
379
+
const mainPostElement = mainPostRef.current;
380
381
+
if (window.scrollY === 0 && !hasPerformedInitialLayout.current) {
382
+
const elementTop = mainPostElement.getBoundingClientRect().top;
383
+
const headerOffset = 70;
384
385
+
const targetScrollY = elementTop - headerOffset;
386
387
+
window.scrollBy(0, targetScrollY);
388
389
+
hasPerformedInitialLayout.current = true;
390
+
}
391
+
392
+
// todo idk what to do with this
393
+
// eslint-disable-next-line react-hooks/set-state-in-effect
394
+
setLayoutReady(true);
395
}
396
+
}, [parents, layoutReady, showMainPostRoute]);
397
+
398
399
+
const [slingshoturl] = useAtom(slingshotURLAtom)
400
+
401
+
React.useEffect(() => {
402
+
if (parentsLoading || !showMainPostRoute) {
403
+
setLayoutReady(false);
404
}
405
406
+
if (!mainPost?.value?.reply?.parent?.uri && !parentsLoading) {
407
+
setLayoutReady(true);
408
+
hasPerformedInitialLayout.current = true;
409
}
410
+
}, [parentsLoading, mainPost, showMainPostRoute]);
411
412
React.useEffect(() => {
413
if (!mainPost?.value?.reply?.parent?.uri) {
···
426
while (currentParentUri && safetyCounter < MAX_PARENTS) {
427
try {
428
const parentPost = await queryClient.fetchQuery(
429
+
constructPostQuery(currentParentUri, slingshoturl)
430
);
431
if (!parentPost) break;
432
parentChain.push(parentPost);
···
448
return () => {
449
ignore = true;
450
};
451
+
}, [mainPost, queryClient, slingshoturl]);
452
453
+
if ((!did || !rkey) && showMainPostRoute) return <div>Invalid post URI</div>;
454
+
if (isIdentityLoading && showMainPostRoute) return <div>Resolving handle...</div>;
455
+
if (identityError && showMainPostRoute)
456
return <div style={{ color: "red" }}>{identityError.message}</div>;
457
+
if (!atUri && showMainPostRoute) return <div>Could not construct post URI.</div>;
458
459
return (
460
<>
461
+
<Outlet />
462
+
{showMainPostRoute && (<>
463
+
<Header
464
+
title={`Post`}
465
+
backButtonCallback={() => {
466
if (window.history.length > 1) {
467
window.history.back();
468
} else {
469
window.location.assign("/");
470
}
471
}}
472
+
/>
473
474
+
{parentsLoading && (
475
+
<div className="text-center text-gray-500 dark:text-gray-400 flex flex-row">
476
+
<div className="ml-4 w-[42px] flex justify-center">
477
+
<div
478
+
style={{ width: 2, height: "100%", opacity: 0.5 }}
479
+
className="bg-gray-500 dark:bg-gray-400"
480
+
></div>
481
+
</div>
482
+
Loading conversation...
483
</div>
484
+
)}
485
486
+
{/* we should use the reply lines here thats provided by UPR*/}
487
+
<div style={{ maxWidth: 600, padding: 0 }}>
488
+
{parents.map((parent, index) => (
489
+
<UniversalPostRendererATURILoader
490
+
key={parent.uri}
491
+
atUri={parent.uri}
492
+
topReplyLine={index > 0}
493
+
bottomReplyLine={true}
494
+
bottomBorder={false}
495
+
/>
496
+
))}
497
+
</div>
498
+
<div ref={mainPostRef}>
499
<UniversalPostRendererATURILoader
500
+
atUri={atUri!}
501
+
detailed={true}
502
+
topReplyLine={parentsLoading || parents.length > 0}
503
+
nopics={!!nopics}
504
+
lightboxCallback={lightboxCallback}
505
/>
506
+
</div>
507
<div
508
style={{
509
+
maxWidth: 600,
510
+
//margin: "0px auto 0",
511
+
padding: 0,
512
+
minHeight: "80dvh",
513
+
paddingBottom: "20dvh",
514
}}
515
>
516
+
<div
517
+
className="text-gray-500 dark:text-gray-400 text-sm font-bold"
518
+
style={{
519
+
fontSize: 18,
520
+
margin: "12px 16px 12px 16px",
521
+
fontWeight: 600,
522
+
}}
523
+
>
524
+
Replies
525
+
</div>
526
+
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
527
+
{replyAturis.length > 0 &&
528
+
replyAturis.map((reply) => {
529
+
//const replyAtUri = `at://${reply.did}/app.bsky.feed.post/${reply.rkey}`;
530
+
return (
531
+
<UniversalPostRendererATURILoader
532
+
key={reply}
533
+
atUri={reply}
534
+
maxReplies={4}
535
+
/>
536
+
);
537
+
})}
538
+
{hasNextPage && (
539
+
<button
540
+
onClick={() => fetchNextPage()}
541
+
disabled={isFetchingNextPage}
542
+
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold disabled:opacity-50"
543
+
>
544
+
{isFetchingNextPage ? "Loading..." : "Load More"}
545
+
</button>
546
+
)}
547
+
</div>
548
</div>
549
+
</>)}
550
</>
551
);
552
}
+259
-2
src/routes/search.tsx
+259
-2
src/routes/search.tsx
···
1
+
import type { Agent } from "@atproto/api";
2
+
import { useQueryClient } from "@tanstack/react-query";
3
+
import { createFileRoute, useSearch } from "@tanstack/react-router";
4
+
import { useAtom } from "jotai";
5
+
import { useEffect,useMemo } from "react";
6
+
7
+
import { Header } from "~/components/Header";
8
+
import { Import } from "~/components/Import";
9
+
import {
10
+
ReusableTabRoute,
11
+
useReusableTabScrollRestore,
12
+
} from "~/components/ReusableTabRoute";
13
+
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
14
+
import { useAuth } from "~/providers/UnifiedAuthProvider";
15
+
import { lycanURLAtom } from "~/utils/atoms";
16
+
import {
17
+
constructLycanRequestIndexQuery,
18
+
useInfiniteQueryLycanSearch,
19
+
useQueryIdentity,
20
+
useQueryLycanStatus,
21
+
} from "~/utils/useQuery";
22
+
23
+
import { renderSnack } from "./__root";
24
+
import { SliderPrimitive } from "./settings";
25
26
export const Route = createFileRoute("/search")({
27
component: Search,
28
});
29
30
export function Search() {
31
+
const queryClient = useQueryClient();
32
+
const { agent, status } = useAuth();
33
+
const { data: identity } = useQueryIdentity(agent?.did);
34
+
const [lycandomain] = useAtom(lycanURLAtom);
35
+
const lycanExists = lycandomain !== "";
36
+
const { data: lycanstatusdata, refetch } = useQueryLycanStatus();
37
+
const lycanIndexed = lycanstatusdata?.status === "finished" || false;
38
+
const lycanIndexing = lycanstatusdata?.status === "in_progress" || false;
39
+
const lycanIndexingProgress = lycanIndexing
40
+
? lycanstatusdata?.progress
41
+
: undefined;
42
+
43
+
const authed = status === "signedIn";
44
+
45
+
const lycanReady = lycanExists && lycanIndexed && authed;
46
+
47
+
const { q }: { q: string } = useSearch({ from: "/search" });
48
+
49
+
// auto-refetch Lycan status until ready
50
+
useEffect(() => {
51
+
if (!lycanExists || !authed) return;
52
+
if (lycanReady) return;
53
+
54
+
const interval = setInterval(() => {
55
+
refetch();
56
+
}, 3000);
57
+
58
+
return () => clearInterval(interval);
59
+
}, [lycanExists, authed, lycanReady, refetch]);
60
+
61
+
const maintext = !lycanExists
62
+
? "Sorry we dont have search. But instead, you can load some of these types of content into Red Dwarf:"
63
+
: authed
64
+
? lycanReady
65
+
? "Lycan Search is enabled and ready! Type to search posts you've interacted with in the past. You can also load some of these types of content into Red Dwarf:"
66
+
: "Sorry, while Lycan Search is enabled, you are not indexed. Index below please. You can load some of these types of content into Red Dwarf:"
67
+
: "Sorry, while Lycan Search is enabled, you are unauthed. Please log in to use Lycan. You can load some of these types of content into Red Dwarf:";
68
+
69
+
async function index(opts: {
70
+
agent?: Agent;
71
+
isAuthed: boolean;
72
+
pdsUrl?: string;
73
+
feedServiceDid?: string;
74
+
}) {
75
+
renderSnack({
76
+
title: "Registering account...",
77
+
});
78
+
try {
79
+
const response = await queryClient.fetchQuery(
80
+
constructLycanRequestIndexQuery(opts)
81
+
);
82
+
if (
83
+
response?.message !== "Import has already started" &&
84
+
response?.message !== "Import has been scheduled"
85
+
) {
86
+
renderSnack({
87
+
title: "Registration failed!",
88
+
description: "Unknown server error (2)",
89
+
});
90
+
} else {
91
+
renderSnack({
92
+
title: "Succesfully sent registration request!",
93
+
description: "Please wait for the server to index your account",
94
+
});
95
+
refetch();
96
+
}
97
+
} catch {
98
+
renderSnack({
99
+
title: "Registration failed!",
100
+
description: "Unknown server error (1)",
101
+
});
102
+
}
103
+
}
104
+
105
+
return (
106
+
<>
107
+
<Header
108
+
title="Explore"
109
+
backButtonCallback={() => {
110
+
if (window.history.length > 1) {
111
+
window.history.back();
112
+
} else {
113
+
window.location.assign("/");
114
+
}
115
+
}}
116
+
/>
117
+
<div className=" flex flex-col items-center mt-4 mx-4 gap-4">
118
+
<Import optionaltextstring={q} />
119
+
<div className="flex flex-col">
120
+
<p className="text-gray-600 dark:text-gray-400">{maintext}</p>
121
+
<ul className="list-disc list-inside mt-2 text-gray-600 dark:text-gray-400">
122
+
<li>
123
+
Bluesky URLs (from supported clients) (like{" "}
124
+
<code className="text-sm">bsky.app</code> or{" "}
125
+
<code className="text-sm">deer.social</code>).
126
+
</li>
127
+
<li>
128
+
AT-URIs (e.g.,{" "}
129
+
<code className="text-sm">at://did:example/collection/item</code>
130
+
).
131
+
</li>
132
+
<li>
133
+
User Handles (like{" "}
134
+
<code className="text-sm">@username.bsky.social</code>).
135
+
</li>
136
+
<li>
137
+
DIDs (Decentralized Identifiers, starting with{" "}
138
+
<code className="text-sm">did:</code>).
139
+
</li>
140
+
</ul>
141
+
<p className="mt-2 text-gray-600 dark:text-gray-400">
142
+
Simply paste one of these into the import field above and press
143
+
Enter to load the content.
144
+
</p>
145
+
146
+
{lycanExists && authed && !lycanReady ? (
147
+
!lycanIndexing ? (
148
+
<div className="mt-4 mx-auto">
149
+
<button
150
+
onClick={() =>
151
+
index({
152
+
agent: agent || undefined,
153
+
isAuthed: status === "signedIn",
154
+
pdsUrl: identity?.pds,
155
+
feedServiceDid: "did:web:" + lycandomain,
156
+
})
157
+
}
158
+
className="px-6 py-2 h-12 rounded-full bg-gray-100 dark:bg-gray-800
159
+
text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition"
160
+
>
161
+
Index my Account
162
+
</button>
163
+
</div>
164
+
) : (
165
+
<div className="mt-4 gap-2 flex flex-col">
166
+
<span>indexing...</span>
167
+
<SliderPrimitive
168
+
value={lycanIndexingProgress || 0}
169
+
min={0}
170
+
max={1}
171
+
/>
172
+
</div>
173
+
)
174
+
) : (
175
+
<></>
176
+
)}
177
+
</div>
178
+
</div>
179
+
{q ? <SearchTabs query={q} /> : <></>}
180
+
</>
181
+
);
182
+
}
183
+
184
+
function SearchTabs({ query }: { query: string }) {
185
+
return (
186
+
<div>
187
+
<ReusableTabRoute
188
+
route={`search` + query}
189
+
tabs={{
190
+
Likes: <LycanTab query={query} type={"likes"} key={"likes"} />,
191
+
Reposts: <LycanTab query={query} type={"reposts"} key={"reposts"} />,
192
+
Quotes: <LycanTab query={query} type={"quotes"} key={"quotes"} />,
193
+
Pins: <LycanTab query={query} type={"pins"} key={"pins"} />,
194
+
}}
195
+
/>
196
+
</div>
197
+
);
198
+
}
199
+
200
+
function LycanTab({
201
+
query,
202
+
type,
203
+
}: {
204
+
query: string;
205
+
type: "likes" | "pins" | "reposts" | "quotes";
206
+
}) {
207
+
useReusableTabScrollRestore("search" + query);
208
+
209
+
const {
210
+
data: postsData,
211
+
fetchNextPage,
212
+
hasNextPage,
213
+
isFetchingNextPage,
214
+
isLoading: arePostsLoading,
215
+
} = useInfiniteQueryLycanSearch({ query: query, type: type });
216
+
217
+
const posts = useMemo(
218
+
() =>
219
+
postsData?.pages.flatMap((page) => {
220
+
if (page) {
221
+
return page.posts;
222
+
} else {
223
+
return [];
224
+
}
225
+
}) ?? [],
226
+
[postsData]
227
+
);
228
+
229
+
return (
230
+
<>
231
+
{/* <div className="text-gray-500 dark:text-gray-400 text-lg font-semibold my-3 mx-4">
232
+
Posts
233
+
</div> */}
234
+
<div>
235
+
{posts.map((post) => (
236
+
<UniversalPostRendererATURILoader
237
+
key={post}
238
+
atUri={post}
239
+
feedviewpost={true}
240
+
/>
241
+
))}
242
+
</div>
243
+
244
+
{/* Loading and "Load More" states */}
245
+
{arePostsLoading && posts.length === 0 && (
246
+
<div className="p-4 text-center text-gray-500">Loading posts...</div>
247
+
)}
248
+
{isFetchingNextPage && (
249
+
<div className="p-4 text-center text-gray-500">Loading more...</div>
250
+
)}
251
+
{hasNextPage && !isFetchingNextPage && (
252
+
<button
253
+
onClick={() => fetchNextPage()}
254
+
className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold"
255
+
>
256
+
Load More Posts
257
+
</button>
258
+
)}
259
+
{posts.length === 0 && !arePostsLoading && (
260
+
<div className="p-4 text-center text-gray-500">No posts found.</div>
261
+
)}
262
+
</>
263
+
);
264
+
265
+
return <></>;
266
}
+353
-2
src/routes/settings.tsx
+353
-2
src/routes/settings.tsx
···
1
+
import { createFileRoute, useNavigate } from "@tanstack/react-router";
2
+
import { useAtom, useAtomValue, useSetAtom } from "jotai";
3
+
import { Slider, Switch } from "radix-ui";
4
+
import { useEffect, useState } from "react";
5
+
6
+
import { Header } from "~/components/Header";
7
+
import Login from "~/components/Login";
8
+
import {
9
+
constellationURLAtom,
10
+
defaultconstellationURL,
11
+
defaulthue,
12
+
defaultImgCDN,
13
+
defaultLycanURL,
14
+
defaultslingshotURL,
15
+
defaultVideoCDN,
16
+
enableBitesAtom,
17
+
enableBridgyTextAtom,
18
+
enableWafrnTextAtom,
19
+
hueAtom,
20
+
imgCDNAtom,
21
+
lycanURLAtom,
22
+
slingshotURLAtom,
23
+
videoCDNAtom,
24
+
} from "~/utils/atoms";
25
+
26
+
import { MaterialNavItem } from "./__root";
27
28
export const Route = createFileRoute("/settings")({
29
component: Settings,
30
});
31
32
export function Settings() {
33
+
const navigate = useNavigate();
34
+
return (
35
+
<>
36
+
<Header
37
+
title="Settings"
38
+
backButtonCallback={() => {
39
+
if (window.history.length > 1) {
40
+
window.history.back();
41
+
} else {
42
+
window.location.assign("/");
43
+
}
44
+
}}
45
+
/>
46
+
<div className="lg:hidden">
47
+
<Login />
48
+
</div>
49
+
<div className="sm:hidden flex flex-col justify-around mt-4">
50
+
<SettingHeading title="Other Pages" top />
51
+
<MaterialNavItem
52
+
InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
53
+
ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
54
+
active={false}
55
+
onClickCallbback={() =>
56
+
navigate({
57
+
to: "/feeds",
58
+
//params: { did: agent.assertDid },
59
+
})
60
+
}
61
+
text="Feeds"
62
+
/>
63
+
<MaterialNavItem
64
+
InactiveIcon={<IconMdiShieldOutline className="w-6 h-6" />}
65
+
ActiveIcon={<IconMdiShield className="w-6 h-6" />}
66
+
active={false}
67
+
onClickCallbback={() =>
68
+
navigate({
69
+
to: "/moderation",
70
+
//params: { did: agent.assertDid },
71
+
})
72
+
}
73
+
text="Moderation"
74
+
/>
75
+
</div>
76
+
<div className="h-4" />
77
+
78
+
<SettingHeading title="Personalization" top />
79
+
<Hue />
80
+
81
+
<SettingHeading title="Network Configuration" />
82
+
<div className="flex flex-col px-4 pb-2">
83
+
<span className="text-md">Service Endpoints</span>
84
+
<span className="text-sm text-gray-500 dark:text-gray-400">
85
+
Customize the servers to be used by the app
86
+
</span>
87
+
</div>
88
+
<TextInputSetting
89
+
atom={constellationURLAtom}
90
+
title={"Constellation"}
91
+
description={
92
+
"Customize the Constellation instance to be used by Red Dwarf"
93
+
}
94
+
init={defaultconstellationURL}
95
+
/>
96
+
<TextInputSetting
97
+
atom={slingshotURLAtom}
98
+
title={"Slingshot"}
99
+
description={"Customize the Slingshot instance to be used by Red Dwarf"}
100
+
init={defaultslingshotURL}
101
+
/>
102
+
<TextInputSetting
103
+
atom={imgCDNAtom}
104
+
title={"Image CDN"}
105
+
description={
106
+
"Customize the Constellation instance to be used by Red Dwarf"
107
+
}
108
+
init={defaultImgCDN}
109
+
/>
110
+
<TextInputSetting
111
+
atom={videoCDNAtom}
112
+
title={"Video CDN"}
113
+
description={"Customize the Slingshot instance to be used by Red Dwarf"}
114
+
init={defaultVideoCDN}
115
+
/>
116
+
<TextInputSetting
117
+
atom={lycanURLAtom}
118
+
title={"Lycan Search"}
119
+
description={"Enable text search across posts you've interacted with"}
120
+
init={defaultLycanURL}
121
+
/>
122
+
123
+
<SettingHeading title="Experimental" />
124
+
<SwitchSetting
125
+
atom={enableBitesAtom}
126
+
title={"Bites"}
127
+
description={"Enable Wafrn Bites to bite and be bitten by other people"}
128
+
//init={false}
129
+
/>
130
+
<div className="h-4" />
131
+
<SwitchSetting
132
+
atom={enableBridgyTextAtom}
133
+
title={"Bridgy Text"}
134
+
description={
135
+
"Show the original text of posts bridged from the Fediverse"
136
+
}
137
+
//init={false}
138
+
/>
139
+
<div className="h-4" />
140
+
<SwitchSetting
141
+
atom={enableWafrnTextAtom}
142
+
title={"Wafrn Text"}
143
+
description={"Show the original text of posts from Wafrn instances"}
144
+
//init={false}
145
+
/>
146
+
<p className="text-gray-500 dark:text-gray-400 py-4 px-4 text-sm border rounded-xl mx-4 mt-8 mb-4">
147
+
Notice: Please restart/refresh the app if changes arent applying
148
+
correctly
149
+
</p>
150
+
</>
151
+
);
152
+
}
153
+
154
+
export function SettingHeading({
155
+
title,
156
+
top,
157
+
}: {
158
+
title: string;
159
+
top?: boolean;
160
+
}) {
161
+
return (
162
+
<div
163
+
className="px-4"
164
+
style={{ marginTop: top ? 0 : 18, paddingBottom: 12 }}
165
+
>
166
+
<span className=" text-sm font-medium text-gray-500 dark:text-gray-400">
167
+
{title}
168
+
</span>
169
+
</div>
170
+
);
171
+
}
172
+
173
+
export function SwitchSetting({
174
+
atom,
175
+
title,
176
+
description,
177
+
}: {
178
+
atom: typeof enableBitesAtom;
179
+
title?: string;
180
+
description?: string;
181
+
}) {
182
+
const value = useAtomValue(atom);
183
+
const setValue = useSetAtom(atom);
184
+
185
+
const [hydrated, setHydrated] = useState(false);
186
+
// eslint-disable-next-line react-hooks/set-state-in-effect
187
+
useEffect(() => setHydrated(true), []);
188
+
189
+
if (!hydrated) {
190
+
// Avoid rendering Switch until we know storage is loaded
191
+
return null;
192
+
}
193
+
194
+
return (
195
+
<div className="flex items-center gap-4 px-4 ">
196
+
<label htmlFor={`switch-${title}`} className="flex flex-row flex-1">
197
+
<div className="flex flex-col">
198
+
<span className="text-md">{title}</span>
199
+
<span className="text-sm text-gray-500 dark:text-gray-400">
200
+
{description}
201
+
</span>
202
+
</div>
203
+
</label>
204
+
205
+
<Switch.Root
206
+
id={`switch-${title}`}
207
+
checked={value}
208
+
onCheckedChange={(v) => setValue(v)}
209
+
className="m3switch root"
210
+
>
211
+
<Switch.Thumb className="m3switch thumb " />
212
+
</Switch.Root>
213
+
</div>
214
+
);
215
+
}
216
+
217
+
function Hue() {
218
+
const [hue, setHue] = useAtom(hueAtom);
219
+
return (
220
+
<div className="flex flex-col px-4">
221
+
<span className="z-[2] text-md">Hue</span>
222
+
<span className="z-[2] text-sm text-gray-500 dark:text-gray-400">
223
+
Change the colors of the app
224
+
</span>
225
+
<div className="z-[1] flex flex-row items-center gap-4">
226
+
<SliderComponent atom={hueAtom} max={360} />
227
+
<button
228
+
onClick={() => setHue(defaulthue ?? 28)}
229
+
className="px-6 py-2 h-12 rounded-full bg-gray-100 dark:bg-gray-800
230
+
text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition"
231
+
>
232
+
Reset
233
+
</button>
234
+
</div>
235
+
</div>
236
+
);
237
+
}
238
+
239
+
export function TextInputSetting({
240
+
atom,
241
+
title,
242
+
description,
243
+
init,
244
+
}: {
245
+
atom: typeof constellationURLAtom;
246
+
title?: string;
247
+
description?: string;
248
+
init?: string;
249
+
}) {
250
+
const [value, setValue] = useAtom(atom);
251
+
return (
252
+
<div className="flex flex-col gap-2 px-4 py-2">
253
+
{/* <div>
254
+
{title && (
255
+
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
256
+
{title}
257
+
</h3>
258
+
)}
259
+
{description && (
260
+
<p className="text-sm text-gray-500 dark:text-gray-400">
261
+
{description}
262
+
</p>
263
+
)}
264
+
</div> */}
265
+
266
+
<div className="flex flex-row gap-2 items-center">
267
+
<div className="m3input-field m3input-label m3input-border size-md flex-1">
268
+
<input
269
+
type="text"
270
+
placeholder=" "
271
+
value={value}
272
+
onChange={(e) => setValue(e.target.value)}
273
+
/>
274
+
<label>{title}</label>
275
+
</div>
276
+
{/* <input
277
+
type="text"
278
+
value={value}
279
+
onChange={(e) => setValue(e.target.value)}
280
+
className="flex-1 px-3 py-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700
281
+
text-gray-900 dark:text-gray-100 placeholder:text-gray-500 dark:placeholder:text-gray-400
282
+
focus:outline-none focus:ring-2 focus:ring-gray-400 dark:focus:ring-gray-600"
283
+
placeholder="Enter value..."
284
+
/> */}
285
+
<button
286
+
onClick={() => setValue(init ?? "")}
287
+
className="px-6 py-2 h-12 rounded-full bg-gray-100 dark:bg-gray-800
288
+
text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition"
289
+
>
290
+
Reset
291
+
</button>
292
+
</div>
293
+
</div>
294
+
);
295
}
296
+
297
+
interface SliderProps {
298
+
atom: typeof hueAtom;
299
+
min?: number;
300
+
max?: number;
301
+
step?: number;
302
+
}
303
+
304
+
export const SliderComponent: React.FC<SliderProps> = ({
305
+
atom,
306
+
min = 0,
307
+
max = 100,
308
+
step = 1,
309
+
}) => {
310
+
const [value, setValue] = useAtom(atom);
311
+
312
+
return (
313
+
<Slider.Root
314
+
className="relative flex items-center w-full h-4"
315
+
value={[value]}
316
+
min={min}
317
+
max={max}
318
+
step={step}
319
+
onValueChange={(v: number[]) => setValue(v[0])}
320
+
>
321
+
<Slider.Track className="relative flex-grow h-4 bg-gray-300 dark:bg-gray-700 rounded-full">
322
+
<Slider.Range className="absolute h-full bg-gray-500 dark:bg-gray-400 rounded-l-full rounded-r-none" />
323
+
</Slider.Track>
324
+
<Slider.Thumb className="shadow-[0_0_0_8px_var(--color-white)] dark:shadow-[0_0_0_8px_var(--color-gray-950)] block w-[3px] h-12 bg-gray-500 dark:bg-gray-400 rounded-md focus:outline-none" />
325
+
</Slider.Root>
326
+
);
327
+
};
328
+
329
+
330
+
interface SliderPProps {
331
+
value: number;
332
+
min?: number;
333
+
max?: number;
334
+
step?: number;
335
+
}
336
+
337
+
338
+
export const SliderPrimitive: React.FC<SliderPProps> = ({
339
+
value,
340
+
min = 0,
341
+
max = 100,
342
+
step = 1,
343
+
}) => {
344
+
345
+
return (
346
+
<Slider.Root
347
+
className="relative flex items-center w-full h-4"
348
+
value={[value]}
349
+
min={min}
350
+
max={max}
351
+
step={step}
352
+
onValueChange={(v: number[]) => {}}
353
+
>
354
+
<Slider.Track className="relative flex-grow h-4 bg-gray-300 dark:bg-gray-700 rounded-full">
355
+
<Slider.Range className="absolute h-full bg-gray-500 dark:bg-gray-400 rounded-l-full rounded-r-none" />
356
+
</Slider.Track>
357
+
<Slider.Thumb className=" hidden shadow-[0_0_0_8px_var(--color-white)] dark:shadow-[0_0_0_8px_var(--color-gray-950)] block w-[3px] h-12 bg-gray-500 dark:bg-gray-400 rounded-md focus:outline-none" />
358
+
</Slider.Root>
359
+
);
360
+
};
+327
-13
src/styles/app.css
+327
-13
src/styles/app.css
···
1
@import "tailwindcss";
2
3
/* @theme {
···
14
--color-gray-950: oklch(0.129 0.050 222.000);
15
} */
16
17
@theme {
18
-
--color-gray-50: oklch(0.984 0.012 28);
19
-
--color-gray-100: oklch(0.968 0.017 28);
20
-
--color-gray-200: oklch(0.929 0.025 28);
21
-
--color-gray-300: oklch(0.869 0.035 28);
22
-
--color-gray-400: oklch(0.704 0.05 28);
23
-
--color-gray-500: oklch(0.554 0.06 28);
24
-
--color-gray-600: oklch(0.446 0.058 28);
25
-
--color-gray-700: oklch(0.372 0.058 28);
26
-
--color-gray-800: oklch(0.279 0.055 28);
27
-
--color-gray-900: oklch(0.208 0.055 28);
28
-
--color-gray-950: oklch(0.129 0.055 28);
29
}
30
31
@layer base {
···
47
}
48
}
49
50
@media (width >= 64rem /* 1024px */) {
51
-
html,
52
-
body {
53
scrollbar-gutter: stable both-edges !important;
54
}
55
}
56
.scroll-thin {
57
scrollbar-width: thin;
58
/*scrollbar-gutter: stable both-edges !important;*/
···
61
.scroll-none {
62
scrollbar-width: none;
63
}
···
1
+
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Roboto:ital,wght@0,100..900;1,100..900&family=Spectral+SC:wght@500&display=swap');
2
@import "tailwindcss";
3
4
/* @theme {
···
15
--color-gray-950: oklch(0.129 0.050 222.000);
16
} */
17
18
+
:root {
19
+
--safe-hue: var(--tw-gray-hue, 28)
20
+
}
21
+
22
@theme {
23
+
--color-gray-50: oklch(0.984 0.012 var(--safe-hue));
24
+
--color-gray-100: oklch(0.968 0.017 var(--safe-hue));
25
+
--color-gray-200: oklch(0.929 0.025 var(--safe-hue));
26
+
--color-gray-300: oklch(0.869 0.035 var(--safe-hue));
27
+
--color-gray-400: oklch(0.704 0.05 var(--safe-hue));
28
+
--color-gray-500: oklch(0.554 0.06 var(--safe-hue));
29
+
--color-gray-600: oklch(0.446 0.058 var(--safe-hue));
30
+
--color-gray-700: oklch(0.372 0.058 var(--safe-hue));
31
+
--color-gray-800: oklch(0.279 0.055 var(--safe-hue));
32
+
--color-gray-900: oklch(0.208 0.055 var(--safe-hue));
33
+
--color-gray-950: oklch(0.129 0.055 var(--safe-hue));
34
+
}
35
+
36
+
:root {
37
+
--link-text-color: oklch(0.5962 0.1987 var(--safe-hue));
38
+
/* max chroma!!! use fallback*/
39
+
/*--link-text-color: oklch(0.6 0.37 var(--safe-hue));*/
40
}
41
42
@layer base {
···
58
}
59
}
60
61
+
.gutter{
62
+
scrollbar-gutter: stable both-edges;
63
+
}
64
+
65
@media (width >= 64rem /* 1024px */) {
66
+
html:not(:has(.disablegutter)),
67
+
body:not(:has(.disablegutter)) {
68
scrollbar-gutter: stable both-edges !important;
69
}
70
+
html:has(.disablescroll),
71
+
body:has(.disablescroll) {
72
+
scrollbar-width: none;
73
+
overflow-y: hidden;
74
+
}
75
}
76
+
77
+
.lightbox:has(+.lightbox-sidebar){
78
+
opacity: 0;
79
+
}
80
+
81
.scroll-thin {
82
scrollbar-width: thin;
83
/*scrollbar-gutter: stable both-edges !important;*/
···
86
.scroll-none {
87
scrollbar-width: none;
88
}
89
+
90
+
.dangerousFediContent {
91
+
& a[href]{
92
+
text-decoration: none;
93
+
color: var(--link-text-color);
94
+
word-break: break-all;
95
+
}
96
+
}
97
+
98
+
.font-inter {
99
+
font-family: "Inter", sans-serif;
100
+
}
101
+
.font-roboto {
102
+
font-family: "Roboto", sans-serif;
103
+
}
104
+
105
+
:root {
106
+
--header-bg-light: color-mix(in srgb, var(--color-white) calc(var(--is-top) * 100%), var(--color-gray-50));
107
+
--header-bg-dark: color-mix(in srgb, var(--color-gray-950) calc(var(--is-top) * 100%), var(--color-gray-900));
108
+
}
109
+
110
+
:root {
111
+
--header-bg: var(--header-bg-light);
112
+
}
113
+
@media (prefers-color-scheme: dark) {
114
+
:root {
115
+
--header-bg: var(--header-bg-dark);
116
+
}
117
+
}
118
+
119
+
:root {
120
+
--shadow-opacity: calc(1 - var(--is-top));
121
+
--tw-shadow-header: 0 2px 8px hsl(0 0% 0% / calc(var(--shadow-opacity) * 0.15));
122
+
}
123
+
124
+
125
+
/* m3 input */
126
+
:root {
127
+
--m3input-radius: 6px;
128
+
--m3input-border-width: .0625rem;
129
+
--m3input-font-size: 16px;
130
+
--m3input-transition: 150ms cubic-bezier(.2, .8, .2, 1);
131
+
/* light theme */
132
+
--m3input-bg: var(--color-gray-50);
133
+
--m3input-border-color: var(--color-gray-400);
134
+
--m3input-label-color: var(--color-gray-500);
135
+
--m3input-text-color: var(--color-gray-900);
136
+
--m3input-focus-color: var(--color-gray-600);
137
+
}
138
+
139
+
@media (prefers-color-scheme: dark) {
140
+
:root {
141
+
--m3input-bg: var(--color-gray-950);
142
+
--m3input-border-color: var(--color-gray-700);
143
+
--m3input-label-color: var(--color-gray-400);
144
+
--m3input-text-color: var(--color-gray-50);
145
+
--m3input-focus-color: var(--color-gray-400);
146
+
}
147
+
}
148
+
149
+
/* reset page *//*
150
+
html,
151
+
body {
152
+
background: var(--m3input-bg);
153
+
margin: 0;
154
+
padding: 1rem;
155
+
color: var(--m3input-text-color);
156
+
font-family: system-ui, sans-serif;
157
+
font-size: var(--m3input-font-size);
158
+
}*/
159
+
160
+
/* base wrapper */
161
+
.m3input-field.m3input-label.m3input-border {
162
+
position: relative;
163
+
display: inline-block;
164
+
width: 100%;
165
+
/*max-width: 400px;*/
166
+
}
167
+
168
+
/* size variants */
169
+
.m3input-field.size-sm {
170
+
--m3input-h: 40px;
171
+
}
172
+
173
+
.m3input-field.size-md {
174
+
--m3input-h: 48px;
175
+
}
176
+
177
+
.m3input-field.size-lg {
178
+
--m3input-h: 56px;
179
+
}
180
+
181
+
.m3input-field.size-xl {
182
+
--m3input-h: 64px;
183
+
}
184
+
185
+
.m3input-field.m3input-label.m3input-border:not(.size-sm):not(.size-md):not(.size-lg):not(.size-xl) {
186
+
--m3input-h: 48px;
187
+
}
188
+
189
+
/* outlined input */
190
+
.m3input-field.m3input-label.m3input-border input {
191
+
width: 100%;
192
+
height: var(--m3input-h);
193
+
border: var(--m3input-border-width) solid var(--m3input-border-color);
194
+
border-radius: var(--m3input-radius);
195
+
background: var(--m3input-bg);
196
+
color: var(--m3input-text-color);
197
+
font-size: var(--m3input-font-size);
198
+
padding: 0 12px;
199
+
box-sizing: border-box;
200
+
outline: none;
201
+
transition: border-color var(--m3input-transition), box-shadow var(--m3input-transition);
202
+
}
203
+
204
+
/* focus ring */
205
+
.m3input-field.m3input-label.m3input-border input:focus {
206
+
/*border-color: var(--m3input-focus-color);*/
207
+
border-color: var(--m3input-focus-color);
208
+
box-shadow: 0 0 0 1px var(--m3input-focus-color);
209
+
/*box-shadow: 0 0 0 2px color-mix(in srgb, var(--focus-color) 20%, transparent);*/
210
+
}
211
+
212
+
/* label */
213
+
.m3input-field.m3input-label.m3input-border label {
214
+
position: absolute;
215
+
left: 12px;
216
+
top: 50%;
217
+
transform: translateY(-50%);
218
+
background: var(--m3input-bg);
219
+
padding: 0 .25em;
220
+
color: var(--m3input-label-color);
221
+
pointer-events: none;
222
+
transition: all var(--m3input-transition);
223
+
}
224
+
225
+
/* float on focus or when filled */
226
+
.m3input-field.m3input-label.m3input-border input:focus+label,
227
+
.m3input-field.m3input-label.m3input-border input:not(:placeholder-shown)+label {
228
+
top: 0;
229
+
transform: translateY(-50%) scale(.78);
230
+
left: 0;
231
+
color: var(--m3input-focus-color);
232
+
}
233
+
234
+
/* placeholder trick */
235
+
.m3input-field.m3input-label.m3input-border input::placeholder {
236
+
color: transparent;
237
+
}
238
+
239
+
/* radix i love you but like cmon man */
240
+
body[data-scroll-locked]{
241
+
margin-left: var(--removed-body-scroll-bar-size) !important;
242
+
}
243
+
244
+
/* radix tabs */
245
+
246
+
.m3tab[data-radix-collection-item] {
247
+
flex: 1;
248
+
display: flex;
249
+
padding: 12px 8px;
250
+
align-items: center;
251
+
justify-content: center;
252
+
color: var(--color-gray-500);
253
+
font-weight: 500;
254
+
&:hover {
255
+
background-color: var(--color-gray-100);
256
+
cursor: pointer;
257
+
}
258
+
&[aria-selected="true"] {
259
+
color: var(--color-gray-950);
260
+
&::before{
261
+
content: "";
262
+
position: absolute;
263
+
width: min(80px, 80%);
264
+
border-radius: 99px 99px 0px 0px ;
265
+
height: 3px;
266
+
bottom: 0;
267
+
background-color: var(--color-gray-400);
268
+
}
269
+
}
270
+
}
271
+
272
+
@media (prefers-color-scheme: dark) {
273
+
.m3tab[data-radix-collection-item] {
274
+
color: var(--color-gray-400);
275
+
&:hover {
276
+
background-color: var(--color-gray-900);
277
+
cursor: pointer;
278
+
}
279
+
&[aria-selected="true"] {
280
+
color: var(--color-gray-50);
281
+
&::before{
282
+
background-color: var(--color-gray-500);
283
+
}
284
+
}
285
+
}
286
+
}
287
+
288
+
:root{
289
+
--thumb-size: 2rem;
290
+
--root-size: 3.25rem;
291
+
292
+
--switch-off-border: var(--color-gray-400);
293
+
--switch-off-bg: var(--color-gray-200);
294
+
--switch-off-thumb: var(--color-gray-400);
295
+
296
+
297
+
--switch-on-bg: var(--color-gray-500);
298
+
--switch-on-thumb: var(--color-gray-50);
299
+
300
+
}
301
+
@media (prefers-color-scheme: dark) {
302
+
:root {
303
+
--switch-off-border: var(--color-gray-500);
304
+
--switch-off-bg: var(--color-gray-800);
305
+
--switch-off-thumb: var(--color-gray-500);
306
+
307
+
308
+
--switch-on-bg: var(--color-gray-400);
309
+
--switch-on-thumb: var(--color-gray-700);
310
+
}
311
+
}
312
+
313
+
.m3switch.root{
314
+
/*w-10 h-6 bg-gray-300 rounded-full relative data-[state=checked]:bg-gray-500 transition-colors*/
315
+
/*width: 40px;
316
+
height: 24px;*/
317
+
318
+
inline-size: var(--root-size);
319
+
block-size: 2rem;
320
+
border-radius: 99999px;
321
+
322
+
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
323
+
transition-timing-function: var(--default-transition-timing-function); /* cubic-bezier(0.4, 0, 0.2, 1) */
324
+
transition-duration: var(--default-transition-duration); /* 150ms */
325
+
326
+
.m3switch.thumb{
327
+
/*block w-5 h-5 bg-white rounded-full shadow-sm transition-transform translate-x-[2px] data-[state=checked]:translate-x-[20px]*/
328
+
329
+
height: var(--thumb-size);
330
+
width: var(--thumb-size);
331
+
display: inline-block;
332
+
border-radius: 9999px;
333
+
334
+
transform-origin: center;
335
+
336
+
transition-property: transform, translate, scale, rotate;
337
+
transition-timing-function: var(--default-transition-timing-function); /* cubic-bezier(0.4, 0, 0.2, 1) */
338
+
transition-duration: var(--default-transition-duration); /* 150ms */
339
+
340
+
}
341
+
342
+
&[aria-checked="true"] {
343
+
box-shadow: inset 0px 0px 0px 1.8px transparent;
344
+
background-color: var(--switch-on-bg);
345
+
346
+
.m3switch.thumb{
347
+
/*block w-5 h-5 bg-white rounded-full shadow-sm transition-transform translate-x-[2px] data-[state=checked]:translate-x-[20px]*/
348
+
349
+
background-color: var(--switch-on-thumb);
350
+
transform: translate(calc((var(--root-size) / 2) - 50%),0) scale(0.72);
351
+
&:active {
352
+
transform: translate(calc((var(--root-size) / 2) - 50%),0) scale(0.88);
353
+
}
354
+
355
+
}
356
+
&:active .m3switch.thumb{
357
+
transform: translate(calc((var(--root-size) / 2) - 50%),0) scale(0.88);
358
+
}
359
+
}
360
+
361
+
&[aria-checked="false"] {
362
+
box-shadow: inset 0px 0px 0px 1.8px var(--switch-off-border);
363
+
background-color: var(--switch-off-bg);
364
+
.m3switch.thumb{
365
+
/*block w-5 h-5 bg-white rounded-full shadow-sm transition-transform translate-x-[2px] data-[state=checked]:translate-x-[20px]*/
366
+
367
+
background-color: var(--switch-off-thumb);
368
+
transform: translate(calc(-1 * (var(--root-size) / 2) + 50%),0) scale(0.5);
369
+
&:active {
370
+
transform: translate(calc(-1 * (var(--root-size) / 2) + 50%),0) scale(0.88);
371
+
}
372
+
}
373
+
&:active .m3switch.thumb{
374
+
transform: translate(calc(-1 * (var(--root-size) / 2) + 50%),0) scale(0.88);
375
+
}
376
+
}
377
+
}
+135
-15
src/utils/atoms.ts
+135
-15
src/utils/atoms.ts
···
1
-
import type Agent from "@atproto/api";
2
-
import { atom, createStore } from "jotai";
3
-
import { atomWithStorage } from 'jotai/utils';
4
5
export const store = createStore();
6
7
export const selectedFeedUriAtom = atomWithStorage<string | null>(
8
-
'selectedFeedUri',
9
null
10
);
11
12
//export const feedScrollPositionsAtom = atom<Record<string, number>>({});
13
14
/**
15
-
* @deprecated use the Tanstack Virtual index thanks
16
*/
17
-
export const feedScrollPositionsAtom = atomWithStorage<Record<string, number>>(
18
-
'feedscrollpositions',
19
-
{}
20
);
21
22
-
export const feedScrollIndexAtom = atomWithStorage<Record<string, number>>('feedScrollIndexes',{});
23
24
-
export const feedHeightsAtom = atomWithStorage<Record<string, Record<string, number>>>(
25
-
'feedPostHeights',
26
{}
27
);
28
29
-
export const likedPostsAtom = atomWithStorage<Record<string, string>>(
30
-
'likedPosts',
31
{}
32
);
33
34
-
export const agentAtom = atom<Agent|null>(null);
35
-
export const authedAtom = atom<boolean>(false);
···
1
+
import { atom, createStore, useAtomValue } from "jotai";
2
+
import { atomWithStorage } from "jotai/utils";
3
+
import { useEffect } from "react";
4
+
5
+
import { type ProfilePostsFilter } from "~/routes/profile.$did";
6
7
export const store = createStore();
8
9
+
export const quickAuthAtom = atomWithStorage<string | null>(
10
+
"quickAuth",
11
+
null
12
+
);
13
+
14
export const selectedFeedUriAtom = atomWithStorage<string | null>(
15
+
"selectedFeedUri",
16
null
17
);
18
19
//export const feedScrollPositionsAtom = atom<Record<string, number>>({});
20
21
+
export const feedScrollPositionsAtom = atomWithStorage<Record<string, number>>(
22
+
"feedscrollpositions",
23
+
{}
24
+
);
25
+
26
+
type TabRouteScrollState = {
27
+
activeTab: string;
28
+
scrollPositions: Record<string, number>;
29
+
};
30
/**
31
+
* @deprecated should be safe to remove i think
32
*/
33
+
export const notificationsScrollAtom = atom<TabRouteScrollState>({
34
+
activeTab: "mentions",
35
+
scrollPositions: {},
36
+
});
37
+
38
+
export type InteractionFilter = {
39
+
likes: boolean;
40
+
reposts: boolean;
41
+
quotes: boolean;
42
+
replies: boolean;
43
+
showAll: boolean;
44
+
};
45
+
const defaultFilters: InteractionFilter = {
46
+
likes: true,
47
+
reposts: true,
48
+
quotes: true,
49
+
replies: true,
50
+
showAll: false,
51
+
};
52
+
export const postInteractionsFiltersAtom = atomWithStorage<InteractionFilter>(
53
+
"postInteractionsFilters",
54
+
defaultFilters
55
);
56
57
+
export const reusableTabRouteScrollAtom = atom<Record<string, TabRouteScrollState | undefined> | undefined>({});
58
59
+
export const likedPostsAtom = atomWithStorage<Record<string, string>>(
60
+
"likedPosts",
61
{}
62
);
63
64
+
export type LikeRecord = {
65
+
uri: string; // at://did/collection/rkey
66
+
target: string;
67
+
cid: string;
68
+
};
69
+
70
+
export const internalLikedPostsAtom = atomWithStorage<Record<string, LikeRecord | null>>(
71
+
"internal-liked-posts",
72
{}
73
);
74
75
+
export const profileChipsAtom = atom<Record<string, ProfilePostsFilter | null>>({})
76
+
77
+
export const defaultconstellationURL = "constellation.microcosm.blue";
78
+
export const constellationURLAtom = atomWithStorage<string>(
79
+
"constellationURL",
80
+
defaultconstellationURL
81
+
);
82
+
export const defaultslingshotURL = "slingshot.microcosm.blue";
83
+
export const slingshotURLAtom = atomWithStorage<string>(
84
+
"slingshotURL",
85
+
defaultslingshotURL
86
+
);
87
+
export const defaultImgCDN = "cdn.bsky.app";
88
+
export const imgCDNAtom = atomWithStorage<string>("imgcdnurl", defaultImgCDN);
89
+
export const defaultVideoCDN = "video.bsky.app";
90
+
export const videoCDNAtom = atomWithStorage<string>(
91
+
"videocdnurl",
92
+
defaultVideoCDN
93
+
);
94
+
95
+
export const defaultLycanURL = "";
96
+
export const lycanURLAtom = atomWithStorage<string>(
97
+
"lycanURL",
98
+
defaultLycanURL
99
+
);
100
+
101
+
export const defaulthue = 28;
102
+
export const hueAtom = atomWithStorage<number>("hue", defaulthue);
103
+
104
+
export const isAtTopAtom = atom<boolean>(true);
105
+
106
+
type ComposerState =
107
+
| { kind: "closed" }
108
+
| { kind: "root" }
109
+
| { kind: "reply"; parent: string }
110
+
| { kind: "quote"; subject: string };
111
+
export const composerAtom = atom<ComposerState>({ kind: "closed" });
112
+
113
+
//export const agentAtom = atom<Agent | null>(null);
114
+
//export const authedAtom = atom<boolean>(false);
115
+
116
+
export function useAtomCssVar(atom: typeof hueAtom, cssVar: string) {
117
+
const value = useAtomValue(atom);
118
+
119
+
useEffect(() => {
120
+
document.documentElement.style.setProperty(cssVar, value.toString());
121
+
}, [value, cssVar]);
122
+
123
+
useEffect(() => {
124
+
document.documentElement.style.setProperty(cssVar, value.toString());
125
+
}, []);
126
+
}
127
+
128
+
hueAtom.onMount = (setAtom) => {
129
+
const stored = localStorage.getItem("hue");
130
+
if (stored != null) setAtom(Number(stored));
131
+
};
132
+
// export function initAtomToCssVar(atom: typeof hueAtom, cssVar: string) {
133
+
// const initial = store.get(atom);
134
+
// console.log("atom get ", initial);
135
+
// document.documentElement.style.setProperty(cssVar, initial.toString());
136
+
// }
137
+
138
+
139
+
140
+
// fun stuff
141
+
142
+
export const enableBitesAtom = atomWithStorage<boolean>(
143
+
"enableBitesAtom",
144
+
false
145
+
);
146
+
147
+
export const enableBridgyTextAtom = atomWithStorage<boolean>(
148
+
"enableBridgyTextAtom",
149
+
false
150
+
);
151
+
152
+
export const enableWafrnTextAtom = atomWithStorage<boolean>(
153
+
"enableWafrnTextAtom",
154
+
false
155
+
);
+37
-3
src/utils/followState.ts
+37
-3
src/utils/followState.ts
···
1
-
import { AtUri, type Agent } from "@atproto/api";
2
-
import { useQueryConstellation, type linksRecordsResponse } from "./useQuery";
3
import type { QueryClient } from "@tanstack/react-query";
4
-
import { TID } from "@atproto/common-web";
5
6
export function useGetFollowState({
7
target,
···
127
};
128
});
129
}
···
1
+
import { type Agent,AtUri } from "@atproto/api";
2
+
import { TID } from "@atproto/common-web";
3
import type { QueryClient } from "@tanstack/react-query";
4
+
5
+
import { type linksRecordsResponse,useQueryConstellation } from "./useQuery";
6
7
export function useGetFollowState({
8
target,
···
128
};
129
});
130
}
131
+
132
+
133
+
134
+
export function useGetOneToOneState(params?: {
135
+
target: string;
136
+
user: string;
137
+
collection: string;
138
+
path: string;
139
+
}): string[] | undefined {
140
+
const { data: arbitrarydata } = useQueryConstellation(
141
+
params && params.user
142
+
? {
143
+
method: "/links",
144
+
target: params.target,
145
+
// @ts-expect-error overloading sucks so much
146
+
collection: params.collection,
147
+
path: params.path,
148
+
dids: [params.user],
149
+
}
150
+
: { method: "undefined", target: "whatever" }
151
+
// overloading sucks so much
152
+
) as { data: linksRecordsResponse | undefined };
153
+
if (!params || !params.user) return undefined;
154
+
const data = arbitrarydata?.linking_records.slice(0, 50) ?? [];
155
+
156
+
if (data.length > 0) {
157
+
return data.map((linksRecord) => {
158
+
return `at://${linksRecord.did}/${linksRecord.collection}/${linksRecord.rkey}`;
159
+
});
160
+
}
161
+
162
+
return undefined;
163
+
}
+34
src/utils/likeMutationQueue.ts
+34
src/utils/likeMutationQueue.ts
···
···
1
+
import { useAtom } from "jotai";
2
+
import { useCallback } from "react";
3
+
4
+
import { type LikeRecord,useLikeMutationQueue as useLikeMutationQueueFromProvider } from "~/providers/LikeMutationQueueProvider";
5
+
import { useAuth } from "~/providers/UnifiedAuthProvider";
6
+
7
+
import { internalLikedPostsAtom } from "./atoms";
8
+
9
+
export function useFastLike(target: string, cid: string) {
10
+
const { agent } = useAuth();
11
+
const { fastState, fastToggle, backfillState } = useLikeMutationQueueFromProvider();
12
+
13
+
const liked = fastState(target);
14
+
const toggle = () => fastToggle(target, cid);
15
+
/**
16
+
*
17
+
* @deprecated dont use it yet, will cause infinite rerenders
18
+
*/
19
+
const backfill = () => agent?.did && backfillState(target, agent.did);
20
+
21
+
return { liked, toggle, backfill };
22
+
}
23
+
24
+
export function useFastSetLikesFromFeed() {
25
+
const [_, setLikedPosts] = useAtom(internalLikedPostsAtom);
26
+
27
+
const setFastState = useCallback(
28
+
(target: string, record: LikeRecord | null) =>
29
+
setLikedPosts((prev) => ({ ...prev, [target]: record })),
30
+
[setLikedPosts]
31
+
);
32
+
33
+
return { setFastState };
34
+
}
+2
-2
src/utils/oauthClient.ts
+2
-2
src/utils/oauthClient.ts
···
1
import { BrowserOAuthClient, type ClientMetadata } from '@atproto/oauth-client-browser';
2
3
-
// i tried making this https://pds-nd.whey.party but cors is annoying as fuck
4
-
const handleResolverPDS = 'https://bsky.social';
5
6
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
7
// @ts-ignore this should be fine ? the vite plugin should generate this before errors
···
1
import { BrowserOAuthClient, type ClientMetadata } from '@atproto/oauth-client-browser';
2
3
+
import resolvers from '../../public/resolvers.json' with { type: 'json' };
4
+
const handleResolverPDS = resolvers.resolver || 'https://bsky.social';
5
6
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
7
// @ts-ignore this should be fine ? the vite plugin should generate this before errors
+53
-23
src/utils/useHydrated.ts
+53
-23
src/utils/useHydrated.ts
···
9
AppBskyFeedPost,
10
AtUri,
11
} from "@atproto/api";
12
import { useMemo } from "react";
13
14
-
import { useQueryIdentity,useQueryPost, useQueryProfile } from "./useQuery";
15
16
-
type QueryResultData<T extends (...args: any) => any> = ReturnType<T> extends
17
-
| { data: infer D }
18
-
| undefined
19
-
? D
20
-
: never;
21
22
function asTyped<T extends { $type: string }>(obj: T): $Typed<T> {
23
return obj as $Typed<T>;
···
26
export function hydrateEmbedImages(
27
embed: AppBskyEmbedImages.Main,
28
did: string,
29
): $Typed<AppBskyEmbedImages.View> {
30
return asTyped({
31
$type: "app.bsky.embed.images#view" as const,
···
34
const link = img.image.ref?.["$link"];
35
if (!link) return null;
36
return {
37
-
thumb: `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${link}@jpeg`,
38
-
fullsize: `https://cdn.bsky.app/img/feed_fullsize/plain/${did}/${link}@jpeg`,
39
alt: img.alt || "",
40
aspectRatio: img.aspectRatio,
41
};
···
47
export function hydrateEmbedExternal(
48
embed: AppBskyEmbedExternal.Main,
49
did: string,
50
): $Typed<AppBskyEmbedExternal.View> {
51
return asTyped({
52
$type: "app.bsky.embed.external#view" as const,
···
55
title: embed.external.title,
56
description: embed.external.description,
57
thumb: embed.external.thumb?.ref?.$link
58
-
? `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${embed.external.thumb.ref.$link}@jpeg`
59
: undefined,
60
},
61
});
···
64
export function hydrateEmbedVideo(
65
embed: AppBskyEmbedVideo.Main,
66
did: string,
67
): $Typed<AppBskyEmbedVideo.View> {
68
const videoLink = embed.video.ref.$link;
69
return asTyped({
70
$type: "app.bsky.embed.video#view" as const,
71
-
playlist: `https://video.bsky.app/watch/${did}/${videoLink}/playlist.m3u8`,
72
-
thumbnail: `https://video.bsky.app/watch/${did}/${videoLink}/thumbnail.jpg`,
73
aspectRatio: embed.aspectRatio,
74
cid: videoLink,
75
});
···
80
quotedPost: QueryResultData<typeof useQueryPost>,
81
quotedProfile: QueryResultData<typeof useQueryProfile>,
82
quotedIdentity: QueryResultData<typeof useQueryIdentity>,
83
): $Typed<AppBskyEmbedRecord.View> | undefined {
84
if (!quotedPost || !quotedProfile || !quotedIdentity) {
85
return undefined;
···
91
handle: quotedIdentity.handle,
92
displayName: quotedProfile.value.displayName ?? quotedIdentity.handle,
93
avatar: quotedProfile.value.avatar?.ref?.$link
94
-
? `https://cdn.bsky.app/img/avatar/plain/${quotedIdentity.did}/${quotedProfile.value.avatar.ref.$link}@jpeg`
95
: undefined,
96
viewer: {},
97
labels: [],
···
122
quotedPost: QueryResultData<typeof useQueryPost>,
123
quotedProfile: QueryResultData<typeof useQueryProfile>,
124
quotedIdentity: QueryResultData<typeof useQueryIdentity>,
125
): $Typed<AppBskyEmbedRecordWithMedia.View> | undefined {
126
const hydratedRecord = hydrateEmbedRecord(
127
embed.record,
128
quotedPost,
129
quotedProfile,
130
quotedIdentity,
131
);
132
133
if (!hydratedRecord) return undefined;
···
148
149
export function useHydratedEmbed(
150
embed: AppBskyFeedPost.Record["embed"],
151
-
postAuthorDid: string | undefined,
152
) {
153
const recordInfo = useMemo(() => {
154
if (AppBskyEmbedRecordWithMedia.isMain(embed)) {
···
181
error: profileError,
182
} = useQueryProfile(profileUri);
183
184
const queryidentityresult = useQueryIdentity(quotedAuthorDid);
185
186
const hydratedEmbed: HydratedEmbedView | undefined = (() => {
187
if (!embed || !postAuthorDid) return undefined;
188
189
-
if (isRecordType && (!usequerypostresults?.data || !quotedProfile || !queryidentityresult?.data)) {
190
return undefined;
191
}
192
193
try {
194
if (AppBskyEmbedImages.isMain(embed)) {
195
-
return hydrateEmbedImages(embed, postAuthorDid);
196
} else if (AppBskyEmbedExternal.isMain(embed)) {
197
-
return hydrateEmbedExternal(embed, postAuthorDid);
198
} else if (AppBskyEmbedVideo.isMain(embed)) {
199
-
return hydrateEmbedVideo(embed, postAuthorDid);
200
} else if (AppBskyEmbedRecord.isMain(embed)) {
201
return hydrateEmbedRecord(
202
embed,
203
usequerypostresults?.data,
204
quotedProfile,
205
queryidentityresult?.data,
206
);
207
} else if (AppBskyEmbedRecordWithMedia.isMain(embed)) {
208
let hydratedMedia:
···
212
| undefined;
213
214
if (AppBskyEmbedImages.isMain(embed.media)) {
215
-
hydratedMedia = hydrateEmbedImages(embed.media, postAuthorDid);
216
} else if (AppBskyEmbedExternal.isMain(embed.media)) {
217
-
hydratedMedia = hydrateEmbedExternal(embed.media, postAuthorDid);
218
} else if (AppBskyEmbedVideo.isMain(embed.media)) {
219
-
hydratedMedia = hydrateEmbedVideo(embed.media, postAuthorDid);
220
}
221
222
if (hydratedMedia) {
···
226
usequerypostresults?.data,
227
quotedProfile,
228
queryidentityresult?.data,
229
);
230
}
231
}
···
236
})();
237
238
const isLoading = isRecordType
239
-
? usequerypostresults?.isLoading || isLoadingProfile || queryidentityresult?.isLoading
240
: false;
241
242
-
const error = usequerypostresults?.error || profileError || queryidentityresult?.error;
243
244
return { data: hydratedEmbed, isLoading, error };
245
-
}
···
9
AppBskyFeedPost,
10
AtUri,
11
} from "@atproto/api";
12
+
import { useAtom } from "jotai";
13
import { useMemo } from "react";
14
15
+
import { imgCDNAtom, videoCDNAtom } from "./atoms";
16
+
import { useQueryIdentity, useQueryPost, useQueryProfile } from "./useQuery";
17
18
+
type QueryResultData<T extends (...args: any) => any> =
19
+
ReturnType<T> extends { data: infer D } | undefined ? D : never;
20
21
function asTyped<T extends { $type: string }>(obj: T): $Typed<T> {
22
return obj as $Typed<T>;
···
25
export function hydrateEmbedImages(
26
embed: AppBskyEmbedImages.Main,
27
did: string,
28
+
cdn: string
29
): $Typed<AppBskyEmbedImages.View> {
30
return asTyped({
31
$type: "app.bsky.embed.images#view" as const,
···
34
const link = img.image.ref?.["$link"];
35
if (!link) return null;
36
return {
37
+
thumb: `https://${cdn}/img/feed_thumbnail/plain/${did}/${link}@jpeg`,
38
+
fullsize: `https://${cdn}/img/feed_fullsize/plain/${did}/${link}@jpeg`,
39
alt: img.alt || "",
40
aspectRatio: img.aspectRatio,
41
};
···
47
export function hydrateEmbedExternal(
48
embed: AppBskyEmbedExternal.Main,
49
did: string,
50
+
cdn: string
51
): $Typed<AppBskyEmbedExternal.View> {
52
return asTyped({
53
$type: "app.bsky.embed.external#view" as const,
···
56
title: embed.external.title,
57
description: embed.external.description,
58
thumb: embed.external.thumb?.ref?.$link
59
+
? `https://${cdn}/img/feed_thumbnail/plain/${did}/${embed.external.thumb.ref.$link}@jpeg`
60
: undefined,
61
},
62
});
···
65
export function hydrateEmbedVideo(
66
embed: AppBskyEmbedVideo.Main,
67
did: string,
68
+
videocdn: string
69
): $Typed<AppBskyEmbedVideo.View> {
70
const videoLink = embed.video.ref.$link;
71
return asTyped({
72
$type: "app.bsky.embed.video#view" as const,
73
+
playlist: `https://${videocdn}/watch/${did}/${videoLink}/playlist.m3u8`,
74
+
thumbnail: `https://${videocdn}/watch/${did}/${videoLink}/thumbnail.jpg`,
75
aspectRatio: embed.aspectRatio,
76
cid: videoLink,
77
});
···
82
quotedPost: QueryResultData<typeof useQueryPost>,
83
quotedProfile: QueryResultData<typeof useQueryProfile>,
84
quotedIdentity: QueryResultData<typeof useQueryIdentity>,
85
+
cdn: string
86
): $Typed<AppBskyEmbedRecord.View> | undefined {
87
if (!quotedPost || !quotedProfile || !quotedIdentity) {
88
return undefined;
···
94
handle: quotedIdentity.handle,
95
displayName: quotedProfile.value.displayName ?? quotedIdentity.handle,
96
avatar: quotedProfile.value.avatar?.ref?.$link
97
+
? `https://${cdn}/img/avatar/plain/${quotedIdentity.did}/${quotedProfile.value.avatar.ref.$link}@jpeg`
98
: undefined,
99
viewer: {},
100
labels: [],
···
125
quotedPost: QueryResultData<typeof useQueryPost>,
126
quotedProfile: QueryResultData<typeof useQueryProfile>,
127
quotedIdentity: QueryResultData<typeof useQueryIdentity>,
128
+
cdn: string
129
): $Typed<AppBskyEmbedRecordWithMedia.View> | undefined {
130
const hydratedRecord = hydrateEmbedRecord(
131
embed.record,
132
quotedPost,
133
quotedProfile,
134
quotedIdentity,
135
+
cdn
136
);
137
138
if (!hydratedRecord) return undefined;
···
153
154
export function useHydratedEmbed(
155
embed: AppBskyFeedPost.Record["embed"],
156
+
postAuthorDid: string | undefined
157
) {
158
const recordInfo = useMemo(() => {
159
if (AppBskyEmbedRecordWithMedia.isMain(embed)) {
···
186
error: profileError,
187
} = useQueryProfile(profileUri);
188
189
+
const [imgcdn] = useAtom(imgCDNAtom);
190
+
const [videocdn] = useAtom(videoCDNAtom);
191
+
192
const queryidentityresult = useQueryIdentity(quotedAuthorDid);
193
194
const hydratedEmbed: HydratedEmbedView | undefined = (() => {
195
if (!embed || !postAuthorDid) return undefined;
196
197
+
if (
198
+
isRecordType &&
199
+
(!usequerypostresults?.data ||
200
+
!quotedProfile ||
201
+
!queryidentityresult?.data)
202
+
) {
203
return undefined;
204
}
205
206
try {
207
if (AppBskyEmbedImages.isMain(embed)) {
208
+
return hydrateEmbedImages(embed, postAuthorDid, imgcdn);
209
} else if (AppBskyEmbedExternal.isMain(embed)) {
210
+
return hydrateEmbedExternal(embed, postAuthorDid, imgcdn);
211
} else if (AppBskyEmbedVideo.isMain(embed)) {
212
+
return hydrateEmbedVideo(embed, postAuthorDid, videocdn);
213
} else if (AppBskyEmbedRecord.isMain(embed)) {
214
return hydrateEmbedRecord(
215
embed,
216
usequerypostresults?.data,
217
quotedProfile,
218
queryidentityresult?.data,
219
+
imgcdn
220
);
221
} else if (AppBskyEmbedRecordWithMedia.isMain(embed)) {
222
let hydratedMedia:
···
226
| undefined;
227
228
if (AppBskyEmbedImages.isMain(embed.media)) {
229
+
hydratedMedia = hydrateEmbedImages(
230
+
embed.media,
231
+
postAuthorDid,
232
+
imgcdn
233
+
);
234
} else if (AppBskyEmbedExternal.isMain(embed.media)) {
235
+
hydratedMedia = hydrateEmbedExternal(
236
+
embed.media,
237
+
postAuthorDid,
238
+
imgcdn
239
+
);
240
} else if (AppBskyEmbedVideo.isMain(embed.media)) {
241
+
hydratedMedia = hydrateEmbedVideo(
242
+
embed.media,
243
+
postAuthorDid,
244
+
videocdn
245
+
);
246
}
247
248
if (hydratedMedia) {
···
252
usequerypostresults?.data,
253
quotedProfile,
254
queryidentityresult?.data,
255
+
imgcdn
256
);
257
}
258
}
···
263
})();
264
265
const isLoading = isRecordType
266
+
? usequerypostresults?.isLoading ||
267
+
isLoadingProfile ||
268
+
queryidentityresult?.isLoading
269
: false;
270
271
+
const error =
272
+
usequerypostresults?.error || profileError || queryidentityresult?.error;
273
274
return { data: hydratedEmbed, isLoading, error };
275
+
}
+452
-137
src/utils/useQuery.ts
+452
-137
src/utils/useQuery.ts
···
1
import * as ATPAPI from "@atproto/api";
2
import {
3
type QueryFunctionContext,
4
queryOptions,
5
useInfiniteQuery,
6
useQuery,
7
-
type UseQueryResult} from "@tanstack/react-query";
8
9
-
export function constructIdentityQuery(didorhandle?: string) {
10
return queryOptions({
11
queryKey: ["identity", didorhandle],
12
queryFn: async () => {
13
-
if (!didorhandle) return undefined as undefined
14
const res = await fetch(
15
-
`https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(didorhandle)}`
16
);
17
if (!res.ok) throw new Error("Failed to fetch post");
18
try {
···
27
}
28
},
29
staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes
30
-
gcTime: /*0//*/5 * 60 * 1000,
31
});
32
}
33
export function useQueryIdentity(didorhandle: string): UseQueryResult<
···
39
},
40
Error
41
>;
42
-
export function useQueryIdentity(): UseQueryResult<
43
-
undefined,
44
-
Error
45
-
>
46
-
export function useQueryIdentity(didorhandle?: string):
47
-
UseQueryResult<
48
-
{
49
-
did: string;
50
-
handle: string;
51
-
pds: string;
52
-
signing_key: string;
53
-
} | undefined,
54
-
Error
55
-
>
56
export function useQueryIdentity(didorhandle?: string) {
57
-
return useQuery(constructIdentityQuery(didorhandle));
58
}
59
60
-
export function constructPostQuery(uri?: string) {
61
return queryOptions({
62
queryKey: ["post", uri],
63
queryFn: async () => {
64
-
if (!uri) return undefined as undefined
65
const res = await fetch(
66
-
`https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
67
);
68
let data: any;
69
try {
···
72
return undefined;
73
}
74
if (res.status === 400) return undefined;
75
-
if (data?.error === "InvalidRequest" && data.message?.includes("Could not find repo")) {
76
return undefined; // cache โnot foundโ
77
}
78
try {
79
if (!res.ok) throw new Error("Failed to fetch post");
80
-
return (data) as {
81
uri: string;
82
cid: string;
83
value: any;
···
92
return failureCount < 2;
93
},
94
staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes
95
-
gcTime: /*0//*/5 * 60 * 1000,
96
});
97
}
98
export function useQueryPost(uri: string): UseQueryResult<
···
103
},
104
Error
105
>;
106
-
export function useQueryPost(): UseQueryResult<
107
-
undefined,
108
-
Error
109
-
>
110
-
export function useQueryPost(uri?: string):
111
-
UseQueryResult<
112
-
{
113
-
uri: string;
114
-
cid: string;
115
-
value: ATPAPI.AppBskyFeedPost.Record;
116
-
} | undefined,
117
-
Error
118
-
>
119
export function useQueryPost(uri?: string) {
120
-
return useQuery(constructPostQuery(uri));
121
}
122
123
-
export function constructProfileQuery(uri?: string) {
124
return queryOptions({
125
queryKey: ["profile", uri],
126
queryFn: async () => {
127
-
if (!uri) return undefined as undefined
128
const res = await fetch(
129
-
`https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
130
);
131
let data: any;
132
try {
···
135
return undefined;
136
}
137
if (res.status === 400) return undefined;
138
-
if (data?.error === "InvalidRequest" && data.message?.includes("Could not find repo")) {
139
return undefined; // cache โnot foundโ
140
}
141
try {
142
if (!res.ok) throw new Error("Failed to fetch post");
143
-
return (data) as {
144
uri: string;
145
cid: string;
146
value: any;
···
155
return failureCount < 2;
156
},
157
staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes
158
-
gcTime: /*0//*/5 * 60 * 1000,
159
});
160
}
161
export function useQueryProfile(uri: string): UseQueryResult<
···
166
},
167
Error
168
>;
169
-
export function useQueryProfile(): UseQueryResult<
170
-
undefined,
171
-
Error
172
-
>;
173
-
export function useQueryProfile(uri?: string):
174
-
UseQueryResult<
175
-
{
176
uri: string;
177
cid: string;
178
value: ATPAPI.AppBskyActorProfile.Record;
179
-
} | undefined,
180
-
Error
181
-
>
182
export function useQueryProfile(uri?: string) {
183
-
return useQuery(constructProfileQuery(uri));
184
}
185
186
// export function constructConstellationQuery(
···
215
// method: "/links/all",
216
// target: string
217
// ): QueryOptions<linksAllResponse, Error>;
218
-
export function constructConstellationQuery(query?:{
219
method:
220
| "/links"
221
| "/links/distinct-dids"
222
| "/links/count"
223
| "/links/count/distinct-dids"
224
| "/links/all"
225
-
| "undefined",
226
-
target: string,
227
-
collection?: string,
228
-
path?: string,
229
-
cursor?: string,
230
-
dids?: string[]
231
-
}
232
-
) {
233
// : QueryOptions<
234
// | linksRecordsResponse
235
// | linksDidsResponse
···
239
// Error
240
// >
241
return queryOptions({
242
-
queryKey: ["constellation", query?.method, query?.target, query?.collection, query?.path, query?.cursor, query?.dids] as const,
243
queryFn: async () => {
244
-
if (!query || query.method === "undefined") return undefined as undefined
245
-
const method = query.method
246
-
const target = query.target
247
-
const collection = query?.collection
248
-
const path = query?.path
249
-
const cursor = query.cursor
250
-
const dids = query?.dids
251
const res = await fetch(
252
-
`https://constellation.microcosm.blue${method}?target=${encodeURIComponent(target)}${collection ? `&collection=${encodeURIComponent(collection)}` : ""}${path ? `&path=${encodeURIComponent(path)}` : ""}${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}${dids ? dids.map((did) => `&did=${encodeURIComponent(did)}`).join("") : ""}`
253
);
254
if (!res.ok) throw new Error("Failed to fetch post");
255
try {
···
273
},
274
// enforce short lifespan
275
staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes
276
-
gcTime: /*0//*/5 * 60 * 1000,
277
});
278
}
279
export function useQueryConstellation(query: {
280
method: "/links";
281
target: string;
···
338
>
339
| undefined {
340
//if (!query) return;
341
return useQuery(
342
-
constructConstellationQuery(query)
343
);
344
}
345
346
-
type linksRecord = {
347
did: string;
348
collection: string;
349
rkey: string;
···
361
type linksCountResponse = {
362
total: string;
363
};
364
-
type linksAllResponse = {
365
links: Record<
366
string,
367
Record<
···
383
}) {
384
return queryOptions({
385
// The query key includes all dependencies to ensure it refetches when they change
386
-
queryKey: ["feedSkeleton", options?.feedUri, { isAuthed: options?.isAuthed, did: options?.agent?.did }],
387
queryFn: async () => {
388
-
if (!options) return undefined as undefined
389
const { feedUri, agent, isAuthed, pdsUrl, feedServiceDid } = options;
390
if (isAuthed) {
391
// Authenticated flow
392
if (!agent || !pdsUrl || !feedServiceDid) {
393
-
throw new Error("Missing required info for authenticated feed fetch.");
394
}
395
const url = `${pdsUrl}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}`;
396
const res = await agent.fetchHandler(url, {
···
400
"Content-Type": "application/json",
401
},
402
});
403
-
if (!res.ok) throw new Error(`Authenticated feed fetch failed: ${res.statusText}`);
404
return (await res.json()) as ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema;
405
} else {
406
// Unauthenticated flow (using a public PDS/AppView)
407
const url = `https://discover.bsky.app/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}`;
408
const res = await fetch(url);
409
-
if (!res.ok) throw new Error(`Public feed fetch failed: ${res.statusText}`);
410
return (await res.json()) as ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema;
411
}
412
},
···
424
return useQuery(constructFeedSkeletonQuery(options));
425
}
426
427
-
export function constructPreferencesQuery(agent?: ATPAPI.Agent | undefined, pdsUrl?: string | undefined) {
428
return queryOptions({
429
-
queryKey: ['preferences', agent?.did],
430
queryFn: async () => {
431
if (!agent || !pdsUrl) throw new Error("Agent or PDS URL not available");
432
const url = `${pdsUrl}/xrpc/app.bsky.actor.getPreferences`;
···
437
});
438
}
439
export function useQueryPreferences(options: {
440
-
agent?: ATPAPI.Agent | undefined, pdsUrl?: string | undefined
441
}) {
442
return useQuery(constructPreferencesQuery(options.agent, options.pdsUrl));
443
}
444
445
-
446
-
447
-
export function constructArbitraryQuery(uri?: string) {
448
return queryOptions({
449
queryKey: ["arbitrary", uri],
450
queryFn: async () => {
451
-
if (!uri) return undefined as undefined
452
const res = await fetch(
453
-
`https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
454
);
455
let data: any;
456
try {
···
459
return undefined;
460
}
461
if (res.status === 400) return undefined;
462
-
if (data?.error === "InvalidRequest" && data.message?.includes("Could not find repo")) {
463
return undefined; // cache โnot foundโ
464
}
465
try {
466
if (!res.ok) throw new Error("Failed to fetch post");
467
-
return (data) as {
468
uri: string;
469
cid: string;
470
value: any;
···
479
return failureCount < 2;
480
},
481
staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes
482
-
gcTime: /*0//*/5 * 60 * 1000,
483
});
484
}
485
export function useQueryArbitrary(uri: string): UseQueryResult<
···
490
},
491
Error
492
>;
493
-
export function useQueryArbitrary(): UseQueryResult<
494
-
undefined,
495
-
Error
496
-
>;
497
export function useQueryArbitrary(uri?: string): UseQueryResult<
498
-
{
499
-
uri: string;
500
-
cid: string;
501
-
value: any;
502
-
} | undefined,
503
Error
504
>;
505
export function useQueryArbitrary(uri?: string) {
506
-
return useQuery(constructArbitraryQuery(uri));
507
}
508
509
-
export function constructFallbackNothingQuery(){
510
return queryOptions({
511
queryKey: ["nothing"],
512
queryFn: async () => {
513
-
return undefined
514
},
515
});
516
}
···
524
}[];
525
};
526
527
-
export function constructAuthorFeedQuery(did: string, pdsUrl: string) {
528
return queryOptions({
529
-
queryKey: ['authorFeed', did],
530
queryFn: async ({ pageParam }: QueryFunctionContext) => {
531
const limit = 25;
532
-
533
const cursor = pageParam as string | undefined;
534
-
const cursorParam = cursor ? `&cursor=${cursor}` : '';
535
-
536
-
const url = `${pdsUrl}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=app.bsky.feed.post&limit=${limit}${cursorParam}`;
537
-
538
const res = await fetch(url);
539
if (!res.ok) throw new Error("Failed to fetch author's posts");
540
-
541
return res.json() as Promise<ListRecordsResponse>;
542
},
543
});
544
}
545
546
-
export function useInfiniteQueryAuthorFeed(did: string | undefined, pdsUrl: string | undefined) {
547
-
const { queryKey, queryFn } = constructAuthorFeedQuery(did!, pdsUrl!);
548
-
549
return useInfiniteQuery({
550
queryKey,
551
queryFn,
···
563
isAuthed: boolean;
564
pdsUrl?: string;
565
feedServiceDid?: string;
566
}) {
567
-
const { feedUri, agent, isAuthed, pdsUrl, feedServiceDid } = options;
568
-
569
return queryOptions({
570
queryKey: ["feedSkeleton", feedUri, { isAuthed, did: agent?.did }],
571
-
572
-
queryFn: async ({ pageParam }: QueryFunctionContext): Promise<FeedSkeletonPage> => {
573
const cursorParam = pageParam ? `&cursor=${pageParam}` : "";
574
-
575
-
if (isAuthed) {
576
if (!agent || !pdsUrl || !feedServiceDid) {
577
-
throw new Error("Missing required info for authenticated feed fetch.");
578
}
579
const url = `${pdsUrl}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}${cursorParam}`;
580
const res = await agent.fetchHandler(url, {
···
584
"Content-Type": "application/json",
585
},
586
});
587
-
if (!res.ok) throw new Error(`Authenticated feed fetch failed: ${res.statusText}`);
588
return (await res.json()) as FeedSkeletonPage;
589
} else {
590
-
const url = `https://discover.bsky.app/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}${cursorParam}`;
591
const res = await fetch(url);
592
-
if (!res.ok) throw new Error(`Public feed fetch failed: ${res.statusText}`);
593
return (await res.json()) as FeedSkeletonPage;
594
}
595
},
···
602
isAuthed: boolean;
603
pdsUrl?: string;
604
feedServiceDid?: string;
605
}) {
606
const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery(options);
607
-
608
-
return useInfiniteQuery({
609
-
queryKey,
610
-
queryFn,
611
initialPageParam: undefined as never,
612
-
getNextPageParam: (lastPage) => lastPage.cursor as null | undefined,
613
-
staleTime: Infinity,
614
-
refetchOnWindowFocus: false,
615
-
enabled: !!options.feedUri && (options.isAuthed ? !!options.agent && !!options.pdsUrl && !!options.feedServiceDid : true),
616
});
617
-
}
···
1
import * as ATPAPI from "@atproto/api";
2
import {
3
+
infiniteQueryOptions,
4
type QueryFunctionContext,
5
queryOptions,
6
useInfiniteQuery,
7
useQuery,
8
+
type UseQueryResult,
9
+
} from "@tanstack/react-query";
10
+
import { useAtom } from "jotai";
11
12
+
import { useAuth } from "~/providers/UnifiedAuthProvider";
13
+
14
+
import { constellationURLAtom, lycanURLAtom, slingshotURLAtom } from "./atoms";
15
+
16
+
export function constructIdentityQuery(
17
+
didorhandle?: string,
18
+
slingshoturl?: string
19
+
) {
20
return queryOptions({
21
queryKey: ["identity", didorhandle],
22
queryFn: async () => {
23
+
if (!didorhandle) return undefined as undefined;
24
const res = await fetch(
25
+
`https://${slingshoturl}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(didorhandle)}`
26
);
27
if (!res.ok) throw new Error("Failed to fetch post");
28
try {
···
37
}
38
},
39
staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes
40
+
gcTime: /*0//*/ 5 * 60 * 1000,
41
});
42
}
43
export function useQueryIdentity(didorhandle: string): UseQueryResult<
···
49
},
50
Error
51
>;
52
+
export function useQueryIdentity(): UseQueryResult<undefined, Error>;
53
+
export function useQueryIdentity(didorhandle?: string): UseQueryResult<
54
+
| {
55
+
did: string;
56
+
handle: string;
57
+
pds: string;
58
+
signing_key: string;
59
+
}
60
+
| undefined,
61
+
Error
62
+
>;
63
export function useQueryIdentity(didorhandle?: string) {
64
+
const [slingshoturl] = useAtom(slingshotURLAtom);
65
+
return useQuery(constructIdentityQuery(didorhandle, slingshoturl));
66
}
67
68
+
export function constructPostQuery(uri?: string, slingshoturl?: string) {
69
return queryOptions({
70
queryKey: ["post", uri],
71
queryFn: async () => {
72
+
if (!uri) return undefined as undefined;
73
const res = await fetch(
74
+
`https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
75
);
76
let data: any;
77
try {
···
80
return undefined;
81
}
82
if (res.status === 400) return undefined;
83
+
if (
84
+
data?.error === "InvalidRequest" &&
85
+
data.message?.includes("Could not find repo")
86
+
) {
87
return undefined; // cache โnot foundโ
88
}
89
try {
90
if (!res.ok) throw new Error("Failed to fetch post");
91
+
return data as {
92
uri: string;
93
cid: string;
94
value: any;
···
103
return failureCount < 2;
104
},
105
staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes
106
+
gcTime: /*0//*/ 5 * 60 * 1000,
107
});
108
}
109
export function useQueryPost(uri: string): UseQueryResult<
···
114
},
115
Error
116
>;
117
+
export function useQueryPost(): UseQueryResult<undefined, Error>;
118
+
export function useQueryPost(uri?: string): UseQueryResult<
119
+
| {
120
+
uri: string;
121
+
cid: string;
122
+
value: ATPAPI.AppBskyFeedPost.Record;
123
+
}
124
+
| undefined,
125
+
Error
126
+
>;
127
export function useQueryPost(uri?: string) {
128
+
const [slingshoturl] = useAtom(slingshotURLAtom);
129
+
return useQuery(constructPostQuery(uri, slingshoturl));
130
}
131
132
+
export function constructProfileQuery(uri?: string, slingshoturl?: string) {
133
return queryOptions({
134
queryKey: ["profile", uri],
135
queryFn: async () => {
136
+
if (!uri) return undefined as undefined;
137
const res = await fetch(
138
+
`https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
139
);
140
let data: any;
141
try {
···
144
return undefined;
145
}
146
if (res.status === 400) return undefined;
147
+
if (
148
+
data?.error === "InvalidRequest" &&
149
+
data.message?.includes("Could not find repo")
150
+
) {
151
return undefined; // cache โnot foundโ
152
}
153
try {
154
if (!res.ok) throw new Error("Failed to fetch post");
155
+
return data as {
156
uri: string;
157
cid: string;
158
value: any;
···
167
return failureCount < 2;
168
},
169
staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes
170
+
gcTime: /*0//*/ 5 * 60 * 1000,
171
});
172
}
173
export function useQueryProfile(uri: string): UseQueryResult<
···
178
},
179
Error
180
>;
181
+
export function useQueryProfile(): UseQueryResult<undefined, Error>;
182
+
export function useQueryProfile(uri?: string): UseQueryResult<
183
+
| {
184
uri: string;
185
cid: string;
186
value: ATPAPI.AppBskyActorProfile.Record;
187
+
}
188
+
| undefined,
189
+
Error
190
+
>;
191
export function useQueryProfile(uri?: string) {
192
+
const [slingshoturl] = useAtom(slingshotURLAtom);
193
+
return useQuery(constructProfileQuery(uri, slingshoturl));
194
}
195
196
// export function constructConstellationQuery(
···
225
// method: "/links/all",
226
// target: string
227
// ): QueryOptions<linksAllResponse, Error>;
228
+
export function constructConstellationQuery(query?: {
229
+
constellation: string;
230
method:
231
| "/links"
232
| "/links/distinct-dids"
233
| "/links/count"
234
| "/links/count/distinct-dids"
235
| "/links/all"
236
+
| "undefined";
237
+
target: string;
238
+
collection?: string;
239
+
path?: string;
240
+
cursor?: string;
241
+
dids?: string[];
242
+
}) {
243
// : QueryOptions<
244
// | linksRecordsResponse
245
// | linksDidsResponse
···
249
// Error
250
// >
251
return queryOptions({
252
+
queryKey: [
253
+
"constellation",
254
+
query?.method,
255
+
query?.target,
256
+
query?.collection,
257
+
query?.path,
258
+
query?.cursor,
259
+
query?.dids,
260
+
] as const,
261
queryFn: async () => {
262
+
if (!query || query.method === "undefined") return undefined as undefined;
263
+
const method = query.method;
264
+
const target = query.target;
265
+
const collection = query?.collection;
266
+
const path = query?.path;
267
+
const cursor = query.cursor;
268
+
const dids = query?.dids;
269
const res = await fetch(
270
+
`https://${query.constellation}${method}?target=${encodeURIComponent(target)}${collection ? `&collection=${encodeURIComponent(collection)}` : ""}${path ? `&path=${encodeURIComponent(path)}` : ""}${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}${dids ? dids.map((did) => `&did=${encodeURIComponent(did)}`).join("") : ""}`
271
);
272
if (!res.ok) throw new Error("Failed to fetch post");
273
try {
···
291
},
292
// enforce short lifespan
293
staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes
294
+
gcTime: /*0//*/ 5 * 60 * 1000,
295
});
296
}
297
+
// todo do more of these instead of overloads since overloads sucks so much apparently
298
+
export function useQueryConstellationLinksCountDistinctDids(query?: {
299
+
method: "/links/count/distinct-dids";
300
+
target: string;
301
+
collection: string;
302
+
path: string;
303
+
cursor?: string;
304
+
}): UseQueryResult<linksCountResponse, Error> | undefined {
305
+
//if (!query) return;
306
+
const [constellationurl] = useAtom(constellationURLAtom);
307
+
const queryres = useQuery(
308
+
constructConstellationQuery(
309
+
query && { constellation: constellationurl, ...query }
310
+
)
311
+
) as unknown as UseQueryResult<linksCountResponse, Error>;
312
+
if (!query) {
313
+
return undefined as undefined;
314
+
}
315
+
return queryres as UseQueryResult<linksCountResponse, Error>;
316
+
}
317
+
318
export function useQueryConstellation(query: {
319
method: "/links";
320
target: string;
···
377
>
378
| undefined {
379
//if (!query) return;
380
+
const [constellationurl] = useAtom(constellationURLAtom);
381
return useQuery(
382
+
constructConstellationQuery(
383
+
query && { constellation: constellationurl, ...query }
384
+
)
385
);
386
}
387
388
+
export type linksRecord = {
389
did: string;
390
collection: string;
391
rkey: string;
···
403
type linksCountResponse = {
404
total: string;
405
};
406
+
export type linksAllResponse = {
407
links: Record<
408
string,
409
Record<
···
425
}) {
426
return queryOptions({
427
// The query key includes all dependencies to ensure it refetches when they change
428
+
queryKey: [
429
+
"feedSkeleton",
430
+
options?.feedUri,
431
+
{ isAuthed: options?.isAuthed, did: options?.agent?.did },
432
+
],
433
queryFn: async () => {
434
+
if (!options) return undefined as undefined;
435
const { feedUri, agent, isAuthed, pdsUrl, feedServiceDid } = options;
436
if (isAuthed) {
437
// Authenticated flow
438
if (!agent || !pdsUrl || !feedServiceDid) {
439
+
throw new Error(
440
+
"Missing required info for authenticated feed fetch."
441
+
);
442
}
443
const url = `${pdsUrl}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}`;
444
const res = await agent.fetchHandler(url, {
···
448
"Content-Type": "application/json",
449
},
450
});
451
+
if (!res.ok)
452
+
throw new Error(`Authenticated feed fetch failed: ${res.statusText}`);
453
return (await res.json()) as ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema;
454
} else {
455
// Unauthenticated flow (using a public PDS/AppView)
456
const url = `https://discover.bsky.app/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}`;
457
const res = await fetch(url);
458
+
if (!res.ok)
459
+
throw new Error(`Public feed fetch failed: ${res.statusText}`);
460
return (await res.json()) as ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema;
461
}
462
},
···
474
return useQuery(constructFeedSkeletonQuery(options));
475
}
476
477
+
export function constructPreferencesQuery(
478
+
agent?: ATPAPI.Agent | undefined,
479
+
pdsUrl?: string | undefined
480
+
) {
481
return queryOptions({
482
+
queryKey: ["preferences", agent?.did],
483
queryFn: async () => {
484
if (!agent || !pdsUrl) throw new Error("Agent or PDS URL not available");
485
const url = `${pdsUrl}/xrpc/app.bsky.actor.getPreferences`;
···
490
});
491
}
492
export function useQueryPreferences(options: {
493
+
agent?: ATPAPI.Agent | undefined;
494
+
pdsUrl?: string | undefined;
495
}) {
496
return useQuery(constructPreferencesQuery(options.agent, options.pdsUrl));
497
}
498
499
+
export function constructArbitraryQuery(uri?: string, slingshoturl?: string) {
500
return queryOptions({
501
queryKey: ["arbitrary", uri],
502
queryFn: async () => {
503
+
if (!uri) return undefined as undefined;
504
const res = await fetch(
505
+
`https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
506
);
507
let data: any;
508
try {
···
511
return undefined;
512
}
513
if (res.status === 400) return undefined;
514
+
if (
515
+
data?.error === "InvalidRequest" &&
516
+
data.message?.includes("Could not find repo")
517
+
) {
518
return undefined; // cache โnot foundโ
519
}
520
try {
521
if (!res.ok) throw new Error("Failed to fetch post");
522
+
return data as {
523
uri: string;
524
cid: string;
525
value: any;
···
534
return failureCount < 2;
535
},
536
staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes
537
+
gcTime: /*0//*/ 5 * 60 * 1000,
538
});
539
}
540
export function useQueryArbitrary(uri: string): UseQueryResult<
···
545
},
546
Error
547
>;
548
+
export function useQueryArbitrary(): UseQueryResult<undefined, Error>;
549
export function useQueryArbitrary(uri?: string): UseQueryResult<
550
+
| {
551
+
uri: string;
552
+
cid: string;
553
+
value: any;
554
+
}
555
+
| undefined,
556
Error
557
>;
558
export function useQueryArbitrary(uri?: string) {
559
+
const [slingshoturl] = useAtom(slingshotURLAtom);
560
+
return useQuery(constructArbitraryQuery(uri, slingshoturl));
561
}
562
563
+
export function constructFallbackNothingQuery() {
564
return queryOptions({
565
queryKey: ["nothing"],
566
queryFn: async () => {
567
+
return undefined;
568
},
569
});
570
}
···
578
}[];
579
};
580
581
+
export function constructAuthorFeedQuery(
582
+
did: string,
583
+
pdsUrl: string,
584
+
collection: string = "app.bsky.feed.post"
585
+
) {
586
return queryOptions({
587
+
queryKey: ["authorFeed", did, collection],
588
queryFn: async ({ pageParam }: QueryFunctionContext) => {
589
const limit = 25;
590
+
591
const cursor = pageParam as string | undefined;
592
+
const cursorParam = cursor ? `&cursor=${cursor}` : "";
593
+
594
+
const url = `${pdsUrl}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=${collection}&limit=${limit}${cursorParam}`;
595
+
596
const res = await fetch(url);
597
if (!res.ok) throw new Error("Failed to fetch author's posts");
598
+
599
return res.json() as Promise<ListRecordsResponse>;
600
},
601
});
602
}
603
604
+
export function useInfiniteQueryAuthorFeed(
605
+
did: string | undefined,
606
+
pdsUrl: string | undefined,
607
+
collection?: string
608
+
) {
609
+
const { queryKey, queryFn } = constructAuthorFeedQuery(
610
+
did!,
611
+
pdsUrl!,
612
+
collection
613
+
);
614
+
615
return useInfiniteQuery({
616
queryKey,
617
queryFn,
···
629
isAuthed: boolean;
630
pdsUrl?: string;
631
feedServiceDid?: string;
632
+
// todo the hell is a unauthedfeedurl
633
+
unauthedfeedurl?: string;
634
}) {
635
+
const { feedUri, agent, isAuthed, pdsUrl, feedServiceDid, unauthedfeedurl } =
636
+
options;
637
+
638
return queryOptions({
639
queryKey: ["feedSkeleton", feedUri, { isAuthed, did: agent?.did }],
640
+
641
+
queryFn: async ({
642
+
pageParam,
643
+
}: QueryFunctionContext): Promise<FeedSkeletonPage> => {
644
const cursorParam = pageParam ? `&cursor=${pageParam}` : "";
645
+
646
+
if (isAuthed && !unauthedfeedurl) {
647
if (!agent || !pdsUrl || !feedServiceDid) {
648
+
throw new Error(
649
+
"Missing required info for authenticated feed fetch."
650
+
);
651
}
652
const url = `${pdsUrl}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}${cursorParam}`;
653
const res = await agent.fetchHandler(url, {
···
657
"Content-Type": "application/json",
658
},
659
});
660
+
if (!res.ok)
661
+
throw new Error(`Authenticated feed fetch failed: ${res.statusText}`);
662
return (await res.json()) as FeedSkeletonPage;
663
} else {
664
+
const url = `https://${unauthedfeedurl ? unauthedfeedurl : "discover.bsky.app"}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}${cursorParam}`;
665
const res = await fetch(url);
666
+
if (!res.ok)
667
+
throw new Error(`Public feed fetch failed: ${res.statusText}`);
668
return (await res.json()) as FeedSkeletonPage;
669
}
670
},
···
677
isAuthed: boolean;
678
pdsUrl?: string;
679
feedServiceDid?: string;
680
+
unauthedfeedurl?: string;
681
}) {
682
const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery(options);
683
+
684
+
return {
685
+
...useInfiniteQuery({
686
+
queryKey,
687
+
queryFn,
688
+
initialPageParam: undefined as never,
689
+
getNextPageParam: (lastPage) => lastPage.cursor as null | undefined,
690
+
staleTime: Infinity,
691
+
refetchOnWindowFocus: false,
692
+
enabled:
693
+
!!options.feedUri &&
694
+
(options.isAuthed
695
+
? ((!!options.agent && !!options.pdsUrl) ||
696
+
!!options.unauthedfeedurl) &&
697
+
!!options.feedServiceDid
698
+
: true),
699
+
}),
700
+
queryKey: queryKey,
701
+
};
702
+
}
703
+
704
+
export function yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(query?: {
705
+
constellation: string;
706
+
method: "/links";
707
+
target?: string;
708
+
collection: string;
709
+
path: string;
710
+
staleMult?: number;
711
+
}) {
712
+
const safemult = query?.staleMult ?? 1;
713
+
// console.log(
714
+
// 'yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks',
715
+
// query,
716
+
// )
717
+
718
+
return infiniteQueryOptions({
719
+
enabled: !!query?.target,
720
+
queryKey: [
721
+
"reddwarf_constellation",
722
+
query?.method,
723
+
query?.target,
724
+
query?.collection,
725
+
query?.path,
726
+
] as const,
727
+
728
+
queryFn: async ({ pageParam }: { pageParam?: string }) => {
729
+
if (!query || !query?.target) return undefined;
730
+
731
+
const method = query.method;
732
+
const target = query.target;
733
+
const collection = query.collection;
734
+
const path = query.path;
735
+
const cursor = pageParam;
736
+
737
+
const res = await fetch(
738
+
`https://${query.constellation}${method}?target=${encodeURIComponent(target)}${
739
+
collection ? `&collection=${encodeURIComponent(collection)}` : ""
740
+
}${path ? `&path=${encodeURIComponent(path)}` : ""}${
741
+
cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""
742
+
}`
743
+
);
744
+
745
+
if (!res.ok) throw new Error("Failed to fetch");
746
+
747
+
return (await res.json()) as linksRecordsResponse;
748
+
},
749
+
750
+
getNextPageParam: (lastPage) => {
751
+
return (lastPage as any)?.cursor ?? undefined;
752
+
},
753
+
initialPageParam: undefined,
754
+
staleTime: 5 * 60 * 1000 * safemult,
755
+
gcTime: 5 * 60 * 1000 * safemult,
756
+
});
757
+
}
758
+
759
+
export function useQueryLycanStatus() {
760
+
const [lycanurl] = useAtom(lycanURLAtom);
761
+
const { agent, status } = useAuth();
762
+
const { data: identity } = useQueryIdentity(agent?.did);
763
+
return useQuery(
764
+
constructLycanStatusCheckQuery({
765
+
agent: agent || undefined,
766
+
isAuthed: status === "signedIn",
767
+
pdsUrl: identity?.pds,
768
+
feedServiceDid: "did:web:"+lycanurl,
769
+
})
770
+
);
771
+
}
772
+
773
+
export function constructLycanStatusCheckQuery(options: {
774
+
agent?: ATPAPI.Agent;
775
+
isAuthed: boolean;
776
+
pdsUrl?: string;
777
+
feedServiceDid?: string;
778
+
}) {
779
+
const { agent, isAuthed, pdsUrl, feedServiceDid } = options;
780
+
781
+
return queryOptions({
782
+
queryKey: ["lycanStatus", { isAuthed, did: agent?.did }],
783
+
784
+
queryFn: async () => {
785
+
if (isAuthed && agent && pdsUrl && feedServiceDid) {
786
+
const url = `${pdsUrl}/xrpc/blue.feeds.lycan.getImportStatus`;
787
+
const res = await agent.fetchHandler(url, {
788
+
method: "GET",
789
+
headers: {
790
+
"atproto-proxy": `${feedServiceDid}#lycan`,
791
+
"Content-Type": "application/json",
792
+
},
793
+
});
794
+
if (!res.ok)
795
+
throw new Error(
796
+
`Authenticated lycan status fetch failed: ${res.statusText}`
797
+
);
798
+
return (await res.json()) as statuschek;
799
+
}
800
+
return undefined;
801
+
},
802
+
});
803
+
}
804
+
805
+
type statuschek = {
806
+
[key: string]: unknown;
807
+
error?: "MethodNotImplemented";
808
+
message?: "Method Not Implemented";
809
+
status?: "finished" | "in_progress";
810
+
position?: string,
811
+
progress?: number,
812
+
813
+
};
814
+
815
+
//{"status":"in_progress","position":"2025-08-30T06:53:18Z","progress":0.0878319661441268}
816
+
type importtype = {
817
+
message?: "Import has already started" | "Import has been scheduled"
818
+
}
819
+
820
+
export function constructLycanRequestIndexQuery(options: {
821
+
agent?: ATPAPI.Agent;
822
+
isAuthed: boolean;
823
+
pdsUrl?: string;
824
+
feedServiceDid?: string;
825
+
}) {
826
+
const { agent, isAuthed, pdsUrl, feedServiceDid } = options;
827
+
828
+
return queryOptions({
829
+
queryKey: ["lycanIndex", { isAuthed, did: agent?.did }],
830
+
831
+
queryFn: async () => {
832
+
if (isAuthed && agent && pdsUrl && feedServiceDid) {
833
+
const url = `${pdsUrl}/xrpc/blue.feeds.lycan.startImport`;
834
+
const res = await agent.fetchHandler(url, {
835
+
method: "POST",
836
+
headers: {
837
+
"atproto-proxy": `${feedServiceDid}#lycan`,
838
+
"Content-Type": "application/json",
839
+
},
840
+
});
841
+
if (!res.ok)
842
+
throw new Error(
843
+
`Authenticated lycan status fetch failed: ${res.statusText}`
844
+
);
845
+
return await res.json() as importtype;
846
+
}
847
+
return undefined;
848
+
},
849
+
});
850
+
}
851
+
852
+
type LycanSearchPage = {
853
+
terms: string[];
854
+
posts: string[];
855
+
cursor?: string;
856
+
};
857
+
858
+
859
+
export function useInfiniteQueryLycanSearch(options: { query: string, type: "likes" | "pins" | "reposts" | "quotes"}) {
860
+
861
+
862
+
const [lycanurl] = useAtom(lycanURLAtom);
863
+
const { agent, status } = useAuth();
864
+
const { data: identity } = useQueryIdentity(agent?.did);
865
+
866
+
const { queryKey, queryFn } = constructLycanSearchQuery({
867
+
agent: agent || undefined,
868
+
isAuthed: status === "signedIn",
869
+
pdsUrl: identity?.pds,
870
+
feedServiceDid: "did:web:"+lycanurl,
871
+
query: options.query,
872
+
type: options.type,
873
+
})
874
+
875
+
return {
876
+
...useInfiniteQuery({
877
+
queryKey,
878
+
queryFn,
879
+
initialPageParam: undefined as never,
880
+
getNextPageParam: (lastPage) => lastPage?.cursor as null | undefined,
881
+
//staleTime: Infinity,
882
+
refetchOnWindowFocus: false,
883
+
// enabled:
884
+
// !!options.feedUri &&
885
+
// (options.isAuthed
886
+
// ? ((!!options.agent && !!options.pdsUrl) ||
887
+
// !!options.unauthedfeedurl) &&
888
+
// !!options.feedServiceDid
889
+
// : true),
890
+
}),
891
+
queryKey: queryKey,
892
+
};
893
+
}
894
+
895
+
896
+
export function constructLycanSearchQuery(options: {
897
+
agent?: ATPAPI.Agent;
898
+
isAuthed: boolean;
899
+
pdsUrl?: string;
900
+
feedServiceDid?: string;
901
+
type: "likes" | "pins" | "reposts" | "quotes";
902
+
query: string;
903
+
}) {
904
+
const { agent, isAuthed, pdsUrl, feedServiceDid, type, query } = options;
905
+
906
+
return infiniteQueryOptions({
907
+
queryKey: ["lycanSearch", query, type, { isAuthed, did: agent?.did }],
908
+
909
+
queryFn: async ({
910
+
pageParam,
911
+
}: QueryFunctionContext): Promise<LycanSearchPage | undefined> => {
912
+
if (isAuthed && agent && pdsUrl && feedServiceDid) {
913
+
const url = `${pdsUrl}/xrpc/blue.feeds.lycan.searchPosts?query=${query}&collection=${type}${pageParam ? `&cursor=${pageParam}` : ""}`;
914
+
const res = await agent.fetchHandler(url, {
915
+
method: "GET",
916
+
headers: {
917
+
"atproto-proxy": `${feedServiceDid}#lycan`,
918
+
"Content-Type": "application/json",
919
+
},
920
+
});
921
+
if (!res.ok)
922
+
throw new Error(
923
+
`Authenticated lycan status fetch failed: ${res.statusText}`
924
+
);
925
+
return (await res.json()) as LycanSearchPage;
926
+
}
927
+
return undefined;
928
+
},
929
initialPageParam: undefined as never,
930
+
getNextPageParam: (lastPage) => lastPage?.cursor as null | undefined,
931
});
932
+
}
+1
-1
tsconfig.json
+1
-1
tsconfig.json
+27
-1
vite.config.ts
+27
-1
vite.config.ts
···
3
import tailwindcss from "@tailwindcss/vite";
4
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
5
import viteReact from "@vitejs/plugin-react";
6
import { defineConfig } from "vite";
7
8
import { generateMetadataPlugin } from "./oauthdev.mts";
9
10
-
const PROD_URL = "https://reddwarf.whey.party"
11
const DEV_URL = "https://local3768forumtest.whey.party"
12
13
function shp(url: string): string {
14
return url.replace(/^https?:\/\//, '');
15
}
···
20
generateMetadataPlugin({
21
prod: PROD_URL,
22
dev: DEV_URL,
23
}),
24
TanStackRouterVite({ autoCodeSplitting: true }),
25
viteReact({
···
28
},
29
}),
30
tailwindcss(),
31
],
32
// test: {
33
// globals: true,
···
3
import tailwindcss from "@tailwindcss/vite";
4
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
5
import viteReact from "@vitejs/plugin-react";
6
+
import AutoImport from 'unplugin-auto-import/vite'
7
+
import IconsResolver from 'unplugin-icons/resolver'
8
+
import Icons from 'unplugin-icons/vite'
9
import { defineConfig } from "vite";
10
11
import { generateMetadataPlugin } from "./oauthdev.mts";
12
13
+
const PROD_URL = "https://reddwarf.app"
14
const DEV_URL = "https://local3768forumtest.whey.party"
15
16
+
const PROD_HANDLE_RESOLVER_PDS = "https://pds-nd.whey.party"
17
+
const DEV_HANDLE_RESOLVER_PDS = "https://bsky.social"
18
+
19
function shp(url: string): string {
20
return url.replace(/^https?:\/\//, '');
21
}
···
26
generateMetadataPlugin({
27
prod: PROD_URL,
28
dev: DEV_URL,
29
+
prodResolver: PROD_HANDLE_RESOLVER_PDS,
30
+
devResolver: DEV_HANDLE_RESOLVER_PDS,
31
}),
32
TanStackRouterVite({ autoCodeSplitting: true }),
33
viteReact({
···
36
},
37
}),
38
tailwindcss(),
39
+
AutoImport({
40
+
include: [
41
+
/\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
42
+
],
43
+
resolvers: [
44
+
IconsResolver({
45
+
prefix: 'Icon',
46
+
extension: 'jsx',
47
+
enabledCollections: ['mdi','material-symbols'],
48
+
}),
49
+
],
50
+
dts: 'src/auto-imports.d.ts',
51
+
}),
52
+
Icons({
53
+
//autoInstall: true,
54
+
compiler: 'jsx',
55
+
jsx: 'react'
56
+
}),
57
],
58
// test: {
59
// globals: true,