+2
-1
.gitignore
+2
-1
.gitignore
+31
-4
README.md
+31
-4
README.md
···
1
1
# Red Dwarf
2
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
3
4
-

4
+

5
5
6
6
huge thanks to [Microcosm](https://microcosm.blue/) for making this possible
7
7
8
8
## running dev and build
9
9
in the `vite.config.ts` file you should change these values
10
10
```ts
11
-
const PROD_URL = "https://reddwarf.whey.party"
11
+
const PROD_URL = "https://reddwarf.app"
12
12
const DEV_URL = "https://local3768forumtest.whey.party"
13
13
```
14
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
15
16
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
17
26
18
27
## useQuery
19
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!
···
52
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
53
62
54
63
## Tanstack Router
55
-
it does the job, nothing very specific was used here
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
+
```
56
81
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
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';
1
+
import fs from "fs";
2
+
import path from "path";
3
3
//import { generateClientMetadata } from './src/helpers/oauthClient'
4
4
export const generateClientMetadata = (appOrigin: string) => {
5
-
const callbackPath = '/callback';
5
+
const callbackPath = "/callback";
6
6
7
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
-
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
+
};
24
26
25
-
export function generateMetadataPlugin({prod, dev}:{prod: string, dev: string}) {
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
+
}) {
26
38
return {
27
-
name: 'vite-plugin-generate-metadata',
39
+
name: "vite-plugin-generate-metadata",
28
40
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.');
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
+
);
34
50
}
35
51
} else {
36
52
appOrigin = dev;
53
+
resolver = devResolver;
37
54
}
38
-
39
-
55
+
40
56
const metadata = generateClientMetadata(appOrigin);
41
-
const outputPath = path.resolve(process.cwd(), 'public', 'client-metadata.json');
57
+
const outputPath = path.resolve(
58
+
process.cwd(),
59
+
"public",
60
+
"client-metadata.json"
61
+
);
42
62
43
63
fs.writeFileSync(outputPath, JSON.stringify(metadata, null, 2));
44
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
+
45
77
// /*mass comment*/ console.log(`โ
Generated client-metadata.json for ${appOrigin}`);
46
78
},
47
79
};
48
-
}
80
+
}
+4990
-25
package-lock.json
+4990
-25
package-lock.json
···
8
8
"dependencies": {
9
9
"@atproto/api": "^0.16.6",
10
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",
11
15
"@tailwindcss/vite": "^4.0.6",
12
16
"@tanstack/query-sync-storage-persister": "^5.85.6",
13
17
"@tanstack/react-devtools": "^0.2.2",
···
16
20
"@tanstack/react-router": "^1.130.2",
17
21
"@tanstack/react-router-devtools": "^1.131.5",
18
22
"@tanstack/router-plugin": "^1.121.2",
23
+
"dompurify": "^3.3.0",
24
+
"i": "^0.3.7",
19
25
"idb-keyval": "^6.2.2",
20
26
"jotai": "^2.13.1",
27
+
"npm": "^11.6.2",
28
+
"radix-ui": "^1.4.3",
21
29
"react": "^19.0.0",
22
30
"react-dom": "^19.0.0",
23
31
"react-player": "^3.3.2",
24
-
"tailwindcss": "^4.0.6"
32
+
"sonner": "^2.0.7",
33
+
"tailwindcss": "^4.0.6",
34
+
"tanstack-router-keepalive": "^1.0.0"
25
35
},
26
36
"devDependencies": {
27
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",
28
44
"@testing-library/dom": "^10.4.0",
29
45
"@testing-library/react": "^16.2.0",
30
46
"@types/node": "^24.3.0",
···
42
58
"prettier": "^3.6.2",
43
59
"typescript": "^5.7.2",
44
60
"typescript-eslint": "^8.46.1",
61
+
"unplugin-auto-import": "^20.2.0",
62
+
"unplugin-icons": "^22.4.2",
45
63
"vite": "^6.3.5",
46
64
"vitest": "^3.0.5",
47
65
"web-vitals": "^4.2.4"
···
58
76
},
59
77
"engines": {
60
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"
61
110
}
62
111
},
63
112
"node_modules/@asamuzakjp/css-color": {
···
1549
1598
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1550
1599
}
1551
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
+
},
1552
1635
"node_modules/@humanfs/core": {
1553
1636
"version": "0.19.1",
1554
1637
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
···
1605
1688
"url": "https://github.com/sponsors/nzakas"
1606
1689
}
1607
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
+
},
1608
1775
"node_modules/@isaacs/fs-minipass": {
1609
1776
"version": "4.0.1",
1610
1777
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
···
1768
1935
"node": ">= 8"
1769
1936
}
1770
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
+
},
1771
3377
"node_modules/@rolldown/pluginutils": {
1772
3378
"version": "1.0.0-beta.27",
1773
3379
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
···
2082
3688
"solid-js": "^1.6.12"
2083
3689
}
2084
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
+
},
2085
3929
"node_modules/@svta/common-media-library": {
2086
3930
"version": "0.12.4",
2087
3931
"resolved": "https://registry.npmjs.org/@svta/common-media-library/-/common-media-library-0.12.4.tgz",
···
2909
4753
"@types/react": "^19.0.0"
2910
4754
}
2911
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
+
},
2912
4763
"node_modules/@typescript-eslint/eslint-plugin": {
2913
4764
"version": "8.46.1",
2914
4765
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz",
···
3433
5284
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
3434
5285
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
3435
5286
"dev": true,
3436
-
"license": "Python-2.0",
3437
-
"peer": 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
+
}
3438
5299
},
3439
5300
"node_modules/aria-query": {
3440
5301
"version": "5.3.0",
···
3846
5707
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
3847
5708
"dev": true,
3848
5709
"license": "MIT",
3849
-
"peer": true,
3850
5710
"engines": {
3851
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"
3852
5725
}
3853
5726
},
3854
5727
"node_modules/caniuse-lite": {
···
4041
5914
"dev": true,
4042
5915
"license": "MIT"
4043
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
+
},
4044
5924
"node_modules/convert-source-map": {
4045
5925
"version": "2.0.0",
4046
5926
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
···
4062
5942
"funding": {
4063
5943
"type": "opencollective",
4064
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
+
}
4065
5972
}
4066
5973
},
4067
5974
"node_modules/cross-spawn": {
···
4203
6110
}
4204
6111
},
4205
6112
"node_modules/debug": {
4206
-
"version": "4.4.1",
4207
-
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
4208
-
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
6113
+
"version": "4.4.3",
6114
+
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
6115
+
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
4209
6116
"license": "MIT",
4210
6117
"dependencies": {
4211
6118
"ms": "^2.1.3"
···
4299
6206
"node": ">=8"
4300
6207
}
4301
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
+
},
4302
6214
"node_modules/diff": {
4303
6215
"version": "8.0.2",
4304
6216
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz",
···
4328
6240
"dev": true,
4329
6241
"license": "MIT"
4330
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
+
},
4331
6263
"node_modules/dunder-proto": {
4332
6264
"version": "1.0.1",
4333
6265
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
···
4373
6305
},
4374
6306
"funding": {
4375
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"
4376
6318
}
4377
6319
},
4378
6320
"node_modules/es-abstract": {
···
5036
6978
"node": ">=0.10.0"
5037
6979
}
5038
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
+
},
5039
6987
"node_modules/expect-type": {
5040
6988
"version": "1.2.2",
5041
6989
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
···
5045
6993
"engines": {
5046
6994
"node": ">=12.0.0"
5047
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"
5048
7003
},
5049
7004
"node_modules/fast-deep-equal": {
5050
7005
"version": "3.1.3",
···
5275
7230
},
5276
7231
"funding": {
5277
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"
5278
7241
}
5279
7242
},
5280
7243
"node_modules/get-proto": {
···
5584
7547
"node": ">= 14"
5585
7548
}
5586
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
+
},
5587
7571
"node_modules/iconv-lite": {
5588
7572
"version": "0.6.3",
5589
7573
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
···
5626
7610
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
5627
7611
"dev": true,
5628
7612
"license": "MIT",
5629
-
"peer": true,
5630
7613
"dependencies": {
5631
7614
"parent-module": "^1.0.0",
5632
7615
"resolve-from": "^4.0.0"
···
5715
7698
"url": "https://github.com/sponsors/ljharb"
5716
7699
}
5717
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
+
},
5718
7708
"node_modules/is-async-function": {
5719
7709
"version": "2.1.1",
5720
7710
"resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
···
6238
8228
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
6239
8229
"dev": true,
6240
8230
"license": "MIT",
6241
-
"peer": true,
6242
8231
"dependencies": {
6243
8232
"argparse": "^2.0.1"
6244
8233
},
···
6305
8294
"dev": true,
6306
8295
"license": "MIT",
6307
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"
6308
8304
},
6309
8305
"node_modules/json-schema-traverse": {
6310
8306
"version": "0.4.1",
···
6360
8356
"dependencies": {
6361
8357
"json-buffer": "3.0.1"
6362
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"
6363
8366
},
6364
8367
"node_modules/levn": {
6365
8368
"version": "0.4.1",
···
6613
8616
"url": "https://opencollective.com/parcel"
6614
8617
}
6615
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
+
},
6616
8644
"node_modules/localforage": {
6617
8645
"version": "1.10.0",
6618
8646
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
···
6639
8667
"url": "https://github.com/sponsors/sindresorhus"
6640
8668
}
6641
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
+
},
6642
8676
"node_modules/lodash.merge": {
6643
8677
"version": "4.6.2",
6644
8678
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
···
6666
8700
"dev": true,
6667
8701
"license": "MIT"
6668
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
+
},
6669
8713
"node_modules/lru-cache": {
6670
8714
"version": "5.1.1",
6671
8715
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
···
6686
8730
}
6687
8731
},
6688
8732
"node_modules/magic-string": {
6689
-
"version": "0.30.18",
6690
-
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz",
6691
-
"integrity": "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==",
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==",
6692
8736
"license": "MIT",
6693
8737
"dependencies": {
6694
8738
"@jridgewell/sourcemap-codec": "^1.5.5"
···
6793
8837
"url": "https://github.com/sponsors/isaacs"
6794
8838
}
6795
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
+
},
6796
8872
"node_modules/ms": {
6797
8873
"version": "2.1.3",
6798
8874
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
···
6842
8918
"dev": true,
6843
8919
"license": "MIT"
6844
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
+
},
6845
8932
"node_modules/node-releases": {
6846
8933
"version": "2.0.19",
6847
8934
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
···
6857
8944
"node": ">=0.10.0"
6858
8945
}
6859
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
+
},
6860
11355
"node_modules/nwsapi": {
6861
11356
"version": "2.2.21",
6862
11357
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz",
···
7042
11537
"url": "https://github.com/sponsors/sindresorhus"
7043
11538
}
7044
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
+
},
7045
11547
"node_modules/parent-module": {
7046
11548
"version": "1.0.1",
7047
11549
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
7048
11550
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
7049
11551
"dev": true,
7050
11552
"license": "MIT",
7051
-
"peer": true,
7052
11553
"dependencies": {
7053
11554
"callsites": "^3.0.0"
7054
11555
},
7055
11556
"engines": {
7056
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"
7057
11577
}
7058
11578
},
7059
11579
"node_modules/parse5": {
···
7104
11624
"dev": true,
7105
11625
"license": "MIT"
7106
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
+
},
7107
11637
"node_modules/pathe": {
7108
11638
"version": "2.0.3",
7109
11639
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
···
7137
11667
},
7138
11668
"funding": {
7139
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"
7140
11682
}
7141
11683
},
7142
11684
"node_modules/player.style": {
···
7260
11802
"engines": {
7261
11803
"node": ">=6"
7262
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"
7263
11822
},
7264
11823
"node_modules/queue-microtask": {
7265
11824
"version": "1.2.3",
···
7282
11841
],
7283
11842
"license": "MIT"
7284
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
+
},
7285
11920
"node_modules/react": {
7286
11921
"version": "19.1.1",
7287
11922
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
···
7343
11978
"node": ">=0.10.0"
7344
11979
}
7345
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
+
},
7346
12047
"node_modules/readdirp": {
7347
12048
"version": "3.6.0",
7348
12049
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
···
7448
12149
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
7449
12150
"dev": true,
7450
12151
"license": "MIT",
7451
-
"peer": true,
7452
12152
"engines": {
7453
12153
"node": ">=4"
7454
12154
}
···
7630
12330
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
7631
12331
"license": "MIT"
7632
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
+
},
7633
12340
"node_modules/semver": {
7634
12341
"version": "6.3.1",
7635
12342
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
···
7817
12524
"dev": true,
7818
12525
"license": "ISC"
7819
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
+
},
7820
12538
"node_modules/solid-js": {
7821
12539
"version": "1.9.9",
7822
12540
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.9.tgz",
···
7826
12544
"csstype": "^3.1.0",
7827
12545
"seroval": "~1.3.0",
7828
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"
7829
12556
}
7830
12557
},
7831
12558
"node_modules/source-map": {
···
8000
12727
}
8001
12728
},
8002
12729
"node_modules/strip-literal": {
8003
-
"version": "3.0.0",
8004
-
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz",
8005
-
"integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==",
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==",
8006
12733
"dev": true,
8007
12734
"license": "MIT",
8008
12735
"dependencies": {
···
8052
12779
"url": "https://github.com/sponsors/ljharb"
8053
12780
}
8054
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
+
},
8055
12789
"node_modules/symbol-tree": {
8056
12790
"version": "3.2.4",
8057
12791
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
···
8064
12798
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz",
8065
12799
"integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==",
8066
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
+
}
8067
12811
},
8068
12812
"node_modules/tapable": {
8069
12813
"version": "2.2.3",
···
8137
12881
"license": "MIT"
8138
12882
},
8139
12883
"node_modules/tinyglobby": {
8140
-
"version": "0.2.14",
8141
-
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
8142
-
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
12884
+
"version": "0.2.15",
12885
+
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
12886
+
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
8143
12887
"license": "MIT",
8144
12888
"dependencies": {
8145
-
"fdir": "^6.4.4",
8146
-
"picomatch": "^4.0.2"
12889
+
"fdir": "^6.5.0",
12890
+
"picomatch": "^4.0.3"
8147
12891
},
8148
12892
"engines": {
8149
12893
"node": ">=12.0.0"
···
8521
13265
"node": "*"
8522
13266
}
8523
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
+
},
8524
13275
"node_modules/uint8arrays": {
8525
13276
"version": "3.0.0",
8526
13277
"resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz",
···
8556
13307
"devOptional": true,
8557
13308
"license": "MIT"
8558
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
+
},
8559
13362
"node_modules/unplugin": {
8560
-
"version": "2.3.9",
8561
-
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.9.tgz",
8562
-
"integrity": "sha512-2dcbZq6aprwXTkzptq3k5qm5B8cvpjG9ynPd5fyM2wDJuuF7PeUK64Sxf0d+X1ZyDOeGydbNzMqBSIVlH8GIfA==",
13363
+
"version": "2.3.10",
13364
+
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.10.tgz",
13365
+
"integrity": "sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==",
8563
13366
"license": "MIT",
8564
13367
"dependencies": {
8565
13368
"@jridgewell/remapping": "^2.3.5",
···
8571
13374
"node": ">=18.12.0"
8572
13375
}
8573
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
+
},
8574
13498
"node_modules/unplugin/node_modules/picomatch": {
8575
13499
"version": "4.0.3",
8576
13500
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
···
8622
13546
"peer": true,
8623
13547
"dependencies": {
8624
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
+
}
8625
13590
}
8626
13591
},
8627
13592
"node_modules/use-sync-external-store": {
+19
-1
package.json
+19
-1
package.json
···
12
12
"dependencies": {
13
13
"@atproto/api": "^0.16.6",
14
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",
15
19
"@tailwindcss/vite": "^4.0.6",
16
20
"@tanstack/query-sync-storage-persister": "^5.85.6",
17
21
"@tanstack/react-devtools": "^0.2.2",
···
20
24
"@tanstack/react-router": "^1.130.2",
21
25
"@tanstack/react-router-devtools": "^1.131.5",
22
26
"@tanstack/router-plugin": "^1.121.2",
27
+
"dompurify": "^3.3.0",
28
+
"i": "^0.3.7",
23
29
"idb-keyval": "^6.2.2",
24
30
"jotai": "^2.13.1",
31
+
"npm": "^11.6.2",
32
+
"radix-ui": "^1.4.3",
25
33
"react": "^19.0.0",
26
34
"react-dom": "^19.0.0",
27
35
"react-player": "^3.3.2",
28
-
"tailwindcss": "^4.0.6"
36
+
"sonner": "^2.0.7",
37
+
"tailwindcss": "^4.0.6",
38
+
"tanstack-router-keepalive": "^1.0.0"
29
39
},
30
40
"devDependencies": {
31
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",
32
48
"@testing-library/dom": "^10.4.0",
33
49
"@testing-library/react": "^16.2.0",
34
50
"@types/node": "^24.3.0",
···
46
62
"prettier": "^3.6.2",
47
63
"typescript": "^5.7.2",
48
64
"typescript-eslint": "^8.46.1",
65
+
"unplugin-auto-import": "^20.2.0",
66
+
"unplugin-icons": "^22.4.2",
49
67
"vite": "^6.3.5",
50
68
"vitest": "^3.0.5",
51
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
+
}
+42
-10
src/components/InfiniteCustomFeed.tsx
+42
-10
src/components/InfiniteCustomFeed.tsx
···
1
+
import { useQueryClient } from "@tanstack/react-query";
1
2
import * as React from "react";
3
+
2
4
//import { useInView } from "react-intersection-observer";
3
5
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
4
6
import { useAuth } from "~/providers/UnifiedAuthProvider";
5
7
import {
6
-
useQueryArbitrary,
7
-
useQueryIdentity,
8
8
useInfiniteQueryFeedSkeleton,
9
+
// useQueryArbitrary,
10
+
// useQueryIdentity,
9
11
} from "~/utils/useQuery";
10
12
11
13
interface InfiniteCustomFeedProps {
12
14
feedUri: string;
13
15
pdsUrl?: string;
14
16
feedServiceDid?: string;
17
+
authedOverride?: boolean;
18
+
unauthedfeedurl?: string;
15
19
}
16
20
17
21
export function InfiniteCustomFeed({
18
22
feedUri,
19
23
pdsUrl,
20
24
feedServiceDid,
25
+
authedOverride,
26
+
unauthedfeedurl,
21
27
}: InfiniteCustomFeedProps) {
22
28
const { agent } = useAuth();
23
-
const authed = !!agent?.did;
29
+
const authed = authedOverride || !!agent?.did;
24
30
25
31
// const identityresultmaybe = useQueryIdentity(agent?.did);
26
32
// const identity = identityresultmaybe?.data;
···
36
42
isFetchingNextPage,
37
43
refetch,
38
44
isRefetching,
45
+
queryKey,
39
46
} = useInfiniteQueryFeedSkeleton({
40
47
feedUri: feedUri,
41
48
agent: agent ?? undefined,
42
49
isAuthed: authed ?? false,
43
50
pdsUrl: pdsUrl,
44
51
feedServiceDid: feedServiceDid,
52
+
unauthedfeedurl: unauthedfeedurl,
45
53
});
54
+
const queryClient = useQueryClient();
55
+
46
56
47
57
const handleRefresh = () => {
58
+
queryClient.removeQueries({queryKey: queryKey});
59
+
//queryClient.invalidateQueries(["infinite-feed", feedUri] as const);
48
60
refetch();
49
61
};
50
62
63
+
const allPosts = React.useMemo(() => {
64
+
const flattenedPosts = data?.pages.flatMap((page) => page?.feed) ?? [];
65
+
66
+
const seenUris = new Set<string>();
67
+
68
+
return flattenedPosts.filter((item) => {
69
+
if (!item?.post) return false;
70
+
71
+
if (seenUris.has(item.post)) {
72
+
return false;
73
+
}
74
+
75
+
seenUris.add(item.post);
76
+
77
+
return true;
78
+
});
79
+
}, [data]);
80
+
51
81
//const { ref, inView } = useInView();
52
82
53
83
// React.useEffect(() => {
···
66
96
);
67
97
}
68
98
69
-
const allPosts =
70
-
data?.pages.flatMap((page) => {
71
-
if (page) return page.feed;
72
-
}) ?? [];
99
+
// const allPosts =
100
+
// data?.pages.flatMap((page) => {
101
+
// if (page) return page.feed;
102
+
// }) ?? [];
73
103
74
104
if (!allPosts || typeof allPosts !== "object" || allPosts.length === 0) {
75
105
return (
···
112
142
<button
113
143
onClick={handleRefresh}
114
144
disabled={isRefetching}
115
-
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"
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"
116
146
aria-label="Refresh feed"
117
147
>
118
-
{isRefetching ? <RefreshIcon className="h-6 w-6 animate-spin" /> : <RefreshIcon className="h-6 w-6" />}
148
+
<RefreshIcon
149
+
className={`h-6 w-6 text-gray-600 dark:text-gray-400 ${isRefetching && "animate-spin"}`}
150
+
/>
119
151
</button>
120
152
</>
121
153
);
···
138
170
d="M20 11A8.1 8.1 0 0 0 4.5 9M4 5v4h4m-4 4a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"
139
171
></path>
140
172
</svg>
141
-
);
173
+
);
+188
-58
src/components/Login.tsx
+188
-58
src/components/Login.tsx
···
1
1
// src/components/Login.tsx
2
-
import React, { useEffect, useState, useRef } from "react";
2
+
import AtpAgent, { Agent } from "@atproto/api";
3
+
import { useAtom } from "jotai";
4
+
import React, { useEffect, useRef, useState } from "react";
5
+
3
6
import { useAuth } from "~/providers/UnifiedAuthProvider";
4
-
import { Agent } from "@atproto/api";
7
+
import { imgCDNAtom } from "~/utils/atoms";
8
+
import { useQueryIdentity, useQueryProfile } from "~/utils/useQuery";
5
9
6
10
// --- 1. The Main Component (Orchestrator with `compact` prop) ---
7
-
export default function Login({ compact = false }: { compact?: boolean }) {
11
+
export default function Login({
12
+
compact = false,
13
+
popup = false,
14
+
}: {
15
+
compact?: boolean;
16
+
popup?: boolean;
17
+
}) {
8
18
const { status, agent, logout } = useAuth();
9
19
10
20
// Loading state can be styled differently based on the prop
···
14
24
className={
15
25
compact
16
26
? "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]"
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]"
18
28
}
19
29
>
20
30
<span
···
33
43
// Large view
34
44
if (!compact) {
35
45
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">
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">
37
47
<div className="flex flex-col items-center justify-center text-center">
38
48
<p className="text-lg font-semibold mb-4 text-gray-800 dark:text-gray-100">
39
49
You are logged in!
···
41
51
<ProfileThing agent={agent} large />
42
52
<button
43
53
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"
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"
45
55
>
46
56
Log out
47
57
</button>
···
67
77
if (!compact) {
68
78
// Large view renders the form directly in the card
69
79
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">
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">
71
81
<UnifiedLoginForm />
72
82
</div>
73
83
);
74
84
}
75
85
76
86
// Compact view renders a button that toggles the form in a dropdown
77
-
return <CompactLoginButton />;
87
+
return <CompactLoginButton popup={popup} />;
78
88
}
79
89
80
90
// --- 2. The Reusable, Self-Contained Login Form Component ---
···
83
93
84
94
return (
85
95
<div>
86
-
<div className="flex border-b border-gray-200 dark:border-gray-700 mb-4">
96
+
<div className="flex bg-gray-300 rounded-full dark:bg-gray-700 mb-4">
87
97
<TabButton
88
98
label="OAuth"
89
99
active={mode === "oauth"}
···
103
113
// --- 3. Helper components for layouts, forms, and UI ---
104
114
105
115
// A new component to contain the logic for the compact dropdown
106
-
const CompactLoginButton = () => {
116
+
const CompactLoginButton = ({ popup }: { popup?: boolean }) => {
107
117
const [showForm, setShowForm] = useState(false);
108
118
const formRef = useRef<HTMLDivElement>(null);
109
119
···
125
135
<div className="relative" ref={formRef}>
126
136
<button
127
137
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"
138
+
className="text-sm bg-gray-600 hover:bg-gray-700 text-white rounded-full px-3 py-1 font-medium transition-colors"
129
139
>
130
140
Log in
131
141
</button>
132
142
{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">
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
+
>
134
146
<UnifiedLoginForm />
135
147
</div>
136
148
)}
···
138
150
);
139
151
};
140
152
141
-
const TabButton = ({ label, active, onClick }: { label: string; active: boolean; onClick: () => void; }) => (
153
+
const TabButton = ({
154
+
label,
155
+
active,
156
+
onClick,
157
+
}: {
158
+
label: string;
159
+
active: boolean;
160
+
onClick: () => void;
161
+
}) => (
142
162
<button
143
163
onClick={onClick}
144
-
className={`px-4 py-2 text-sm font-medium transition-colors ${
164
+
className={`px-4 py-2 text-sm font-medium transition-colors rounded-full flex-1 ${
145
165
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"
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"
148
168
}`}
149
169
>
150
170
{label}
···
169
189
};
170
190
return (
171
191
<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>
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>
175
219
</form>
176
220
);
177
221
};
···
201
245
202
246
return (
203
247
<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" />
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>
208
301
{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>
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>
210
308
</form>
211
309
);
212
310
};
213
311
214
312
// --- 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);
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;
217
328
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]);
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
+
}
229
357
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>
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}
249
377
</div>
250
-
);
251
-
};
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
+
}
+1037
-566
src/components/UniversalPostRenderer.tsx
+1037
-566
src/components/UniversalPostRenderer.tsx
···
1
+
import * as ATPAPI from "@atproto/api";
1
2
import { useNavigate } from "@tanstack/react-router";
2
-
import { useAtom } from 'jotai';
3
+
import DOMPurify from "dompurify";
4
+
import { useAtom } from "jotai";
5
+
import { DropdownMenu } from "radix-ui";
6
+
import { HoverCard } from "radix-ui";
3
7
import * as React from "react";
4
8
import { type SVGProps } from "react";
5
9
6
-
import { likedPostsAtom } from "~/utils/atoms";
10
+
import {
11
+
composerAtom,
12
+
constellationURLAtom,
13
+
enableBridgyTextAtom,
14
+
enableWafrnTextAtom,
15
+
imgCDNAtom,
16
+
} from "~/utils/atoms";
7
17
import { useHydratedEmbed } from "~/utils/useHydrated";
8
18
import {
9
19
useQueryConstellation,
10
20
useQueryIdentity,
11
21
useQueryPost,
12
22
useQueryProfile,
23
+
yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks,
13
24
} from "~/utils/useQuery";
14
25
15
26
function asTyped<T extends { $type: string }>(obj: T): $Typed<T> {
···
28
39
bottomBorder?: boolean;
29
40
feedviewpost?: boolean;
30
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;
31
53
}
32
54
33
55
// export async function cachedGetRecord({
···
132
154
bottomBorder = true,
133
155
feedviewpost = false,
134
156
repostedby,
157
+
style,
158
+
ref,
159
+
dataIndexPropPass,
160
+
nopics,
161
+
concise,
162
+
lightboxCallback,
163
+
maxReplies,
164
+
isQuote,
165
+
filterNoReplies,
166
+
filterMustHaveMedia,
167
+
filterMustBeReply,
135
168
}: UniversalPostRendererATURILoaderProps) {
169
+
// todo remove this once tree rendering is implemented, use a prop like isTree
170
+
const TEMPLINEAR = true;
136
171
// /*mass comment*/ console.log("atUri", atUri);
137
172
//const { get, set } = usePersistentStore();
138
173
//const [record, setRecord] = React.useState<any>(null);
···
146
181
// >(null);
147
182
//const router = useRouter();
148
183
149
-
const parsed = React.useMemo(() => parseAtUri(atUri), [atUri]);
150
-
const did = parsed?.did;
184
+
//const parsed = React.useMemo(() => parseAtUri(atUri), [atUri]);
185
+
const parsed = new AtUri(atUri);
186
+
const did = parsed?.host;
151
187
const rkey = parsed?.rkey;
152
188
// /*mass comment*/ console.log("did", did);
153
189
// /*mass comment*/ console.log("rkey", rkey);
···
376
412
);
377
413
}, [links]);
378
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
+
379
500
// const navigateToProfile = (e: React.MouseEvent) => {
380
501
// e.stopPropagation();
381
502
// if (resolved?.did) {
···
387
508
// };
388
509
if (!postQuery?.value) {
389
510
// deleted post more often than a non-resolvable post
390
-
return (<></>)
511
+
return <></>;
391
512
}
392
513
393
514
return (
394
-
<UniversalPostRendererRawRecordShim
395
-
detailed={detailed}
396
-
postRecord={postQuery}
397
-
profileRecord={opProfile}
398
-
aturi={atUri}
399
-
resolved={resolved}
400
-
likesCount={likes}
401
-
repostsCount={reposts}
402
-
repliesCount={replies}
403
-
bottomReplyLine={bottomReplyLine}
404
-
topReplyLine={topReplyLine}
405
-
bottomBorder={bottomBorder}
406
-
feedviewpost={feedviewpost}
407
-
repostedby={repostedby}
408
-
/>
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
+
</>
409
597
);
410
598
}
411
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
+
412
640
export function UniversalPostRendererRawRecordShim({
413
641
postRecord,
414
642
profileRecord,
···
423
651
bottomBorder = true,
424
652
feedviewpost = false,
425
653
repostedby,
654
+
style,
655
+
ref,
656
+
dataIndexPropPass,
657
+
nopics,
658
+
concise,
659
+
lightboxCallback,
660
+
maxReplies,
661
+
isQuote,
662
+
filterNoReplies,
663
+
filterMustHaveMedia,
664
+
filterMustBeReply,
426
665
}: {
427
666
postRecord: any;
428
667
profileRecord: any;
···
437
676
bottomBorder?: boolean;
438
677
feedviewpost?: boolean;
439
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;
440
690
}) {
441
691
// /*mass comment*/ console.log(`received aturi: ${aturi} of post content: ${postRecord}`);
442
692
const navigate = useNavigate();
443
693
444
694
//const { get, set } = usePersistentStore();
445
-
function getAvatarUrl(opProfile: any) {
446
-
const link = opProfile?.value?.avatar?.ref?.["$link"];
447
-
if (!link) return null;
448
-
return `https://cdn.bsky.app/img/avatar/plain/${resolved?.did}/${link}@jpeg`;
449
-
}
450
-
451
695
// const [hydratedEmbed, setHydratedEmbed] = useState<any>(undefined);
452
696
453
697
// useEffect(() => {
···
513
757
// run();
514
758
// }, [postRecord, resolved?.did]);
515
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
+
516
777
const {
517
778
data: hydratedEmbed,
518
779
isLoading: isEmbedLoading,
519
780
error: embedError,
520
781
} = useHydratedEmbed(postRecord?.value?.embed, resolved?.did);
521
782
522
-
const parsedaturi = parseAtUri(aturi);
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
+
);
523
809
524
810
const fakepost = React.useMemo<AppBskyFeedDefs.PostView>(
525
811
() => ({
526
812
$type: "app.bsky.feed.defs#postView",
527
813
uri: aturi,
528
814
cid: postRecord?.cid || "",
529
-
author: {
530
-
did: resolved?.did || "",
531
-
handle: resolved?.handle || "",
532
-
displayName: profileRecord?.value?.displayName || "",
533
-
avatar: getAvatarUrl(profileRecord) || "",
534
-
viewer: undefined,
535
-
labels: profileRecord?.labels || undefined,
536
-
verification: undefined,
537
-
},
815
+
author: fakeprofileviewbasic,
538
816
record: postRecord?.value || {},
539
817
embed: hydratedEmbed ?? undefined,
540
818
replyCount: repliesCount ?? 0,
···
548
826
}),
549
827
[
550
828
aturi,
551
-
postRecord,
552
-
profileRecord,
829
+
postRecord?.cid,
830
+
postRecord?.value,
831
+
postRecord?.labels,
832
+
fakeprofileviewbasic,
553
833
hydratedEmbed,
554
834
repliesCount,
555
835
repostsCount,
556
836
likesCount,
557
-
resolved,
558
837
]
559
838
);
560
839
···
589
868
// }, [fakepost, get, set]);
590
869
const thereply = (fakepost?.record as AppBskyFeedPost.Record)?.reply?.parent
591
870
?.uri;
592
-
const feedviewpostreplydid = thereply ? new AtUri(thereply).host : undefined;
871
+
const feedviewpostreplydid =
872
+
thereply && !filterNoReplies ? new AtUri(thereply).host : undefined;
593
873
const replyhookvalue = useQueryIdentity(
594
874
feedviewpost ? feedviewpostreplydid : undefined
595
875
);
596
876
const feedviewpostreplyhandle = replyhookvalue?.data?.handle;
597
877
598
-
599
-
const aturirepostbydid = repostedby ? new AtUri(repostedby).host : undefined
878
+
const aturirepostbydid = repostedby ? new AtUri(repostedby).host : undefined;
600
879
const repostedbyhookvalue = useQueryIdentity(
601
880
repostedby ? aturirepostbydid : undefined
602
881
);
603
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
+
604
890
return (
605
891
<>
606
892
{/* <p>
607
893
{postRecord?.value?.embed.$type + " " + JSON.stringify(hydratedEmbed)}
608
894
</p> */}
895
+
{/* <span>filtermustbereply is {filterMustBeReply ? "true" : "false"}</span>
896
+
<span>thereply is {thereply ? "true" : "false"}</span> */}
609
897
<UniversalPostRenderer
610
898
expanded={detailed}
611
899
onPostClick={() =>
612
900
parsedaturi &&
613
901
navigate({
614
902
to: "/profile/$did/post/$rkey",
615
-
params: { did: parsedaturi.did, rkey: parsedaturi.rkey },
903
+
params: { did: parsedaturi.host, rkey: parsedaturi.rkey },
616
904
})
617
905
}
618
906
// onProfileClick={() => parsedaturi && navigate({to: "/profile/$did",
···
623
911
if (parsedaturi) {
624
912
navigate({
625
913
to: "/profile/$did",
626
-
params: { did: parsedaturi.did },
914
+
params: { did: parsedaturi.host },
627
915
});
628
916
}
629
917
}}
630
918
post={fakepost}
919
+
uprrrsauthor={fakeprofileviewdetailed}
631
920
salt={aturi}
632
921
bottomReplyLine={bottomReplyLine}
633
922
topReplyLine={topReplyLine}
···
635
924
//extraOptionalItemInfo={{reply: postRecord?.value?.reply as AppBskyFeedDefs.ReplyRef, post: fakepost}}
636
925
feedviewpostreplyhandle={feedviewpostreplyhandle}
637
926
repostedby={feedviewpostrepostedbyhandle}
927
+
style={style}
928
+
ref={ref}
929
+
dataIndexPropPass={dataIndexPropPass}
930
+
nopics={nopics}
931
+
concise={concise}
932
+
lightboxCallback={lightboxCallback}
933
+
maxReplies={maxReplies}
934
+
isQuote={isQuote}
638
935
/>
639
936
</>
640
937
);
641
938
}
642
939
643
-
export function parseAtUri(
644
-
atUri: string
645
-
): { did: string; collection: string; rkey: string } | null {
646
-
const PREFIX = "at://";
647
-
if (!atUri.startsWith(PREFIX)) {
648
-
return null;
649
-
}
940
+
// export function parseAtUri(
941
+
// atUri: string
942
+
// ): { did: string; collection: string; rkey: string } | null {
943
+
// const PREFIX = "at://";
944
+
// if (!atUri.startsWith(PREFIX)) {
945
+
// return null;
946
+
// }
650
947
651
-
const parts = atUri.slice(PREFIX.length).split("/");
948
+
// const parts = atUri.slice(PREFIX.length).split("/");
652
949
653
-
if (parts.length !== 3) {
654
-
return null;
655
-
}
950
+
// if (parts.length !== 3) {
951
+
// return null;
952
+
// }
656
953
657
-
const [did, collection, rkey] = parts;
954
+
// const [did, collection, rkey] = parts;
658
955
659
-
if (!did || !collection || !rkey) {
660
-
return null;
661
-
}
956
+
// if (!did || !collection || !rkey) {
957
+
// return null;
958
+
// }
662
959
663
-
return { did, collection, rkey };
664
-
}
960
+
// return { did, collection, rkey };
961
+
// }
665
962
666
963
export function MdiCommentOutline(props: SVGProps<SVGSVGElement>) {
667
964
return (
···
673
970
{...props}
674
971
>
675
972
<path
676
-
fill="oklch(0.704 0.05 28)"
973
+
fill="var(--color-gray-400)"
677
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"
678
975
></path>
679
976
</svg>
···
690
987
{...props}
691
988
>
692
989
<path
693
-
fill="oklch(0.704 0.05 28)"
990
+
fill="var(--color-gray-400)"
694
991
d="M17 17H7v-3l-4 4l4 4v-3h12v-6h-2M7 7h10v3l4-4l-4-4v3H5v6h2z"
695
992
></path>
696
993
</svg>
···
741
1038
{...props}
742
1039
>
743
1040
<path
744
-
fill="oklch(0.704 0.05 28)"
1041
+
fill="var(--color-gray-400)"
745
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"
746
1043
></path>
747
1044
</svg>
···
758
1055
{...props}
759
1056
>
760
1057
<path
761
-
fill="oklch(0.704 0.05 28)"
1058
+
fill="var(--color-gray-400)"
762
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"
763
1060
></path>
764
1061
</svg>
···
775
1072
{...props}
776
1073
>
777
1074
<path
778
-
fill="oklch(0.704 0.05 28)"
1075
+
fill="var(--color-gray-400)"
779
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"
780
1077
></path>
781
1078
</svg>
···
792
1089
{...props}
793
1090
>
794
1091
<path
795
-
fill="oklch(0.704 0.05 28)"
1092
+
fill="var(--color-gray-400)"
796
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"
797
1094
></path>
798
1095
</svg>
···
826
1123
{...props}
827
1124
>
828
1125
<path
829
-
fill="oklch(0.704 0.05 28)"
1126
+
fill="var(--color-gray-400)"
830
1127
d="M10 9V5l-7 7l7 7v-4.1c5 0 8.5 1.6 11 5.1c-1-5-4-10-11-11"
831
1128
></path>
832
1129
</svg>
···
880
1177
{...props}
881
1178
>
882
1179
<path
883
-
fill="oklch(0.704 0.05 28)"
1180
+
fill="var(--color-gray-400)"
884
1181
d="M17 17H7v-3l-4 4l4 4v-3h12v-6h-2M7 7h10v3l4-4l-4-4v3H5v6h2z"
885
1182
></path>
886
1183
</svg>
···
897
1194
{...props}
898
1195
>
899
1196
<path
900
-
fill="oklch(0.704 0.05 28)"
1197
+
fill="var(--color-gray-400)"
901
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"
902
1199
></path>
903
1200
</svg>
···
925
1222
//import Masonry from "@mui/lab/Masonry";
926
1223
import {
927
1224
type $Typed,
1225
+
AppBskyActorDefs,
928
1226
AppBskyEmbedDefs,
929
1227
AppBskyEmbedExternal,
930
1228
AppBskyEmbedImages,
···
948
1246
PostView,
949
1247
//ThreadViewPost,
950
1248
} from "@atproto/api/dist/client/types/app/bsky/feed/defs";
1249
+
import { useInfiniteQuery } from "@tanstack/react-query";
951
1250
import { useEffect, useRef, useState } from "react";
952
1251
import ReactPlayer from "react-player";
953
1252
954
1253
import defaultpfp from "~/../public/favicon.png";
955
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";
956
1263
// import type { OutputSchema } from "@atproto/api/dist/client/types/app/bsky/feed/getFeed";
957
1264
// import type {
958
1265
// ViewRecord,
···
1060
1367
1061
1368
function UniversalPostRenderer({
1062
1369
post,
1370
+
uprrrsauthor,
1063
1371
//setMainItem,
1064
1372
//isMainItem,
1065
1373
onPostClick,
···
1076
1384
feedviewpostreplyhandle,
1077
1385
depth = 0,
1078
1386
repostedby,
1387
+
style,
1388
+
ref,
1389
+
dataIndexPropPass,
1390
+
nopics,
1391
+
concise,
1392
+
lightboxCallback,
1393
+
maxReplies,
1079
1394
}: {
1080
1395
post: PostView;
1396
+
uprrrsauthor?: AppBskyActorDefs.ProfileViewDetailed;
1081
1397
// optional for now because i havent ported every use to this yet
1082
1398
// setMainItem?: React.Dispatch<
1083
1399
// React.SetStateAction<AppBskyFeedDefs.FeedViewPost>
···
1095
1411
feedviewpostreplyhandle?: string;
1096
1412
depth?: number;
1097
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;
1098
1421
}) {
1422
+
const parsed = new AtUri(post.uri);
1099
1423
const navigate = useNavigate();
1100
-
const [likedPosts, setLikedPosts] = useAtom(likedPostsAtom);
1101
1424
const [hasRetweeted, setHasRetweeted] = useState<boolean>(
1102
1425
post.viewer?.repost ? true : false
1103
1426
);
1104
-
const [hasLiked, setHasLiked] = useState<boolean>(
1105
-
(post.uri in likedPosts) || post.viewer?.like ? true : false
1106
-
);
1427
+
const [, setComposerPost] = useAtom(composerAtom);
1107
1428
const { agent } = useAuth();
1108
-
const [likeUri, setLikeUri] = useState<string | undefined>(post.viewer?.like);
1109
1429
const [retweetUri, setRetweetUri] = useState<string | undefined>(
1110
1430
post.viewer?.repost
1111
1431
);
1112
-
1113
-
const likeOrUnlikePost = async () => {
1114
-
const newLikedPosts = { ...likedPosts };
1115
-
if (!agent) {
1116
-
console.error("Agent is null or undefined");
1117
-
return;
1118
-
}
1119
-
if (hasLiked) {
1120
-
if (post.uri in likedPosts) {
1121
-
const likeUri = likedPosts[post.uri];
1122
-
setLikeUri(likeUri);
1123
-
}
1124
-
if (likeUri) {
1125
-
await agent.deleteLike(likeUri);
1126
-
setHasLiked(false);
1127
-
delete newLikedPosts[post.uri];
1128
-
}
1129
-
} else {
1130
-
const { uri } = await agent.like(post.uri, post.cid);
1131
-
setLikeUri(uri);
1132
-
setHasLiked(true);
1133
-
newLikedPosts[post.uri] = uri;
1134
-
}
1135
-
setLikedPosts(newLikedPosts)
1136
-
};
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])
1137
1439
1138
1440
const repostOrUnrepostPost = async () => {
1139
1441
if (!agent) {
···
1152
1454
}
1153
1455
};
1154
1456
1155
-
const isRepost = repostedby ? repostedby : extraOptionalItemInfo
1156
-
? AppBskyFeedDefs.isReasonRepost(extraOptionalItemInfo.reason)
1157
-
? extraOptionalItemInfo.reason?.by.displayName
1158
-
: undefined
1159
-
: undefined;
1457
+
const isRepost = repostedby
1458
+
? repostedby
1459
+
: extraOptionalItemInfo
1460
+
? AppBskyFeedDefs.isReasonRepost(extraOptionalItemInfo.reason)
1461
+
? extraOptionalItemInfo.reason?.by.displayName
1462
+
: undefined
1463
+
: undefined;
1160
1464
const isReply = extraOptionalItemInfo
1161
1465
? extraOptionalItemInfo.reply
1162
1466
: undefined;
1163
1467
1164
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);
1165
1507
1166
1508
/* fuck you */
1167
1509
const isMainItem = false;
1168
1510
const setMainItem = (any: any) => {};
1511
+
// eslint-disable-next-line react-hooks/refs
1512
+
//console.log("Received ref in UniversalPostRenderer:", usedref);
1169
1513
return (
1170
-
<div
1171
-
key={salt + "-" + (post.uri || emergencySalt)}
1172
-
onClick={
1173
-
isMainItem
1174
-
? onPostClick
1175
-
: setMainItem
1514
+
<div ref={ref} style={style} data-index={dataIndexPropPass}>
1515
+
<div
1516
+
//ref={ref}
1517
+
key={salt + "-" + (post.uri || emergencySalt)}
1518
+
onClick={
1519
+
isMainItem
1176
1520
? onPostClick
1177
-
? (e) => {
1178
-
setMainItem({ post: post });
1179
-
onPostClick(e);
1180
-
}
1181
-
: () => {
1182
-
setMainItem({ post: post });
1183
-
}
1184
-
: undefined
1185
-
}
1186
-
style={{
1187
-
//border: "1px solid #e1e8ed",
1188
-
//borderRadius: 12,
1189
-
opacity: "1 !important",
1190
-
background: "transparent",
1191
-
paddingLeft: isQuote ? 12 : 16,
1192
-
paddingRight: isQuote ? 12 : 16,
1193
-
//paddingTop: 16,
1194
-
paddingTop: isRepost ? 10 : isQuote ? 12 : 16,
1195
-
//paddingBottom: bottomReplyLine ? 0 : 16,
1196
-
paddingBottom: 0,
1197
-
fontFamily: "system-ui, sans-serif",
1198
-
//boxShadow: "0 2px 8px rgba(0,0,0,0.04)",
1199
-
position: "relative",
1200
-
// dont cursor: "pointer",
1201
-
borderBottomWidth: bottomBorder ? (isQuote ? 0 : 1) : 0,
1202
-
}}
1203
-
className="border-gray-300 dark:border-gray-600"
1204
-
>
1205
-
{isRepost && (
1206
-
<div
1207
-
style={{
1208
-
marginLeft: 36,
1209
-
display: "flex",
1210
-
borderRadius: 12,
1211
-
paddingBottom: "calc(22px - 1rem)",
1212
-
fontSize: 14,
1213
-
maxHeight: "1rem",
1214
-
justifyContent: "flex-start",
1215
-
//color: theme.textSecondary,
1216
-
gap: 4,
1217
-
alignItems: "center",
1218
-
}}
1219
-
className="text-gray-500 dark:text-gray-400"
1220
-
>
1221
-
<MdiRepost /> Reposted by @{isRepost}{" "}
1222
-
</div>
1223
-
)}
1224
-
{!isQuote && (
1225
-
<div
1226
-
style={{
1227
-
opacity: topReplyLine || (isReply /*&& (true || expanded)*/) ? 0.5 : 0,
1228
-
position: "absolute",
1229
-
top: 0,
1230
-
left: 36, // why 36 ???
1231
-
//left: 16 + (42 / 2),
1232
-
width: 2,
1233
-
//height: "100%",
1234
-
height: isRepost ? "calc(16px + 1rem - 6px)" : 16 - 6,
1235
-
// background: theme.textSecondary,
1236
-
//opacity: 0.5,
1237
-
// no flex here
1238
-
}}
1239
-
className="bg-gray-500 dark:bg-gray-400"
1240
-
/>
1241
-
)}
1242
-
<div
1521
+
: setMainItem
1522
+
? onPostClick
1523
+
? (e) => {
1524
+
setMainItem({ post: post });
1525
+
onPostClick(e);
1526
+
}
1527
+
: () => {
1528
+
setMainItem({ post: post });
1529
+
}
1530
+
: undefined
1531
+
}
1243
1532
style={{
1244
-
position: "absolute",
1245
-
//top: isRepost ? "calc(16px + 1rem)" : 16,
1246
-
//left: 16,
1247
-
zIndex: 1,
1248
-
top: isRepost ? "calc(16px + 1rem)" : isQuote ? 12 : 16,
1249
-
left: isQuote ? 12 : 16,
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,
1250
1549
}}
1251
-
onClick={onProfileClick}
1550
+
className="border-gray-300 dark:border-gray-800"
1252
1551
>
1253
-
<img
1254
-
src={post.author.avatar || defaultpfp}
1255
-
alt="avatar"
1256
-
// transition={{
1257
-
// type: "spring",
1258
-
// stiffness: 260,
1259
-
// damping: 20,
1260
-
// }}
1261
-
style={{
1262
-
borderRadius: "50%",
1263
-
marginRight: 12,
1264
-
objectFit: "cover",
1265
-
//background: theme.border,
1266
-
//border: `1px solid ${theme.border}`,
1267
-
width: isQuote ? 16 : 42,
1268
-
height: isQuote ? 16 : 42,
1269
-
}}
1270
-
className="border border-gray-300 dark:border-gray-600 bg-gray-300 dark:bg-gray-600"
1271
-
/>
1272
-
</div>
1273
-
<div style={{ display: "flex", alignItems: "flex-start", zIndex: 2 }}>
1274
-
<div
1275
-
style={{
1276
-
display: "flex",
1277
-
flexDirection: "column",
1278
-
alignSelf: "stretch",
1279
-
alignItems: "center",
1280
-
overflow: "hidden",
1281
-
width: expanded || isQuote ? 0 : "auto",
1282
-
marginRight: expanded || isQuote ? 0 : 12,
1283
-
}}
1284
-
>
1285
-
{/* dummy for later use */}
1286
-
<div style={{ width: 42, height: 42 + 8, minHeight: 42 + 8 }} />
1287
-
{/* reply line !!!! bottomReplyLine */}
1288
-
{bottomReplyLine && (
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>
1289
1596
<div
1597
+
className={`absolute`}
1290
1598
style={{
1291
-
width: 2,
1292
-
height: "100%",
1293
-
//background: theme.textSecondary,
1294
-
opacity: 0.5,
1295
-
// no flex here
1296
-
//color: "Red",
1297
-
//zIndex: 99
1599
+
top: isRepost
1600
+
? "calc(16px + 1rem)"
1601
+
: isQuote
1602
+
? 12
1603
+
: topReplyLine
1604
+
? 8
1605
+
: 16,
1606
+
left: isQuote ? 12 : 16,
1298
1607
}}
1299
-
className="bg-gray-500 dark:bg-gray-400"
1300
-
/>
1301
-
)}
1302
-
{/* <div
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
1303
1710
layout
1304
1711
transition={{ duration: 0.2 }}
1305
1712
animate={{ height: expanded ? 0 : '100%' }}
···
1309
1716
// no flex here
1310
1717
}}
1311
1718
/> */}
1312
-
</div>
1313
-
<div style={{ flex: 1, maxWidth: "100%" }}>
1314
-
<div
1315
-
style={{
1316
-
display: "flex",
1317
-
flexDirection: "row",
1318
-
alignItems: "center",
1319
-
flexWrap: "nowrap",
1320
-
maxWidth: `calc(100% - ${!expanded ? (isQuote ? 26 : 0) : 54}px)`,
1321
-
width: `calc(100% - ${!expanded ? (isQuote ? 26 : 0) : 54}px)`,
1322
-
marginLeft: !expanded ? (isQuote ? 26 : 0) : 54,
1323
-
marginBottom: !expanded ? 4 : 6,
1324
-
}}
1325
-
>
1719
+
</div>
1720
+
<div style={{ flex: 1, maxWidth: "100%" }}>
1326
1721
<div
1327
1722
style={{
1328
1723
display: "flex",
1329
-
//overflow: "hidden", // hey why is overflow hidden unapplied
1330
-
overflow: "hidden",
1331
-
textOverflow: "ellipsis",
1332
-
flexShrink: 1,
1333
-
flexGrow: 1,
1334
-
flexBasis: 0,
1335
-
width: 0,
1336
-
gap: expanded ? 0 : 6,
1337
-
alignItems: expanded ? "flex-start" : "center",
1338
-
flexDirection: expanded ? "column" : "row",
1339
-
height: expanded ? 42 : "1rem",
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,
1340
1731
}}
1341
1732
>
1342
-
<span
1733
+
<div
1343
1734
style={{
1344
1735
display: "flex",
1345
-
fontWeight: 700,
1346
-
fontSize: 16,
1736
+
//overflow: "hidden", // hey why is overflow hidden unapplied
1347
1737
overflow: "hidden",
1348
1738
textOverflow: "ellipsis",
1349
-
whiteSpace: "nowrap",
1350
1739
flexShrink: 1,
1351
-
minWidth: 0,
1352
-
gap: 4,
1353
-
alignItems: "center",
1354
-
//color: theme.text,
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",
1355
1747
}}
1356
-
className="text-gray-900 dark:text-gray-100"
1357
1748
>
1358
-
{/* verified checkmark */}
1359
-
{post.author.displayName || post.author.handle}{" "}
1360
-
{post.author.verification?.verifiedStatus == "valid" && (
1361
-
<MdiVerified />
1362
-
)}
1363
-
</span>
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>
1364
1771
1365
-
<span
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
1366
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",
1367
1820
//color: theme.textSecondary,
1368
-
fontSize: 16,
1369
-
overflowX: "hidden",
1370
-
textOverflow: "ellipsis",
1371
-
whiteSpace: "nowrap",
1372
-
flexShrink: 1,
1373
-
flexGrow: 0,
1374
-
minWidth: 0,
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,
1375
1830
}}
1376
1831
className="text-gray-500 dark:text-gray-400"
1377
1832
>
1378
-
@{post.author.handle}
1379
-
</span>
1380
-
</div>
1833
+
<MdiReply /> Reply to @{feedviewpostreplyhandle}
1834
+
</div>
1835
+
)}
1381
1836
<div
1382
1837
style={{
1383
-
display: "flex",
1384
-
alignItems: "center",
1385
-
height: "1rem",
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
+
}),
1386
1850
}}
1851
+
className="text-gray-900 dark:text-gray-100"
1387
1852
>
1388
-
<span
1389
-
style={{
1390
-
//color: theme.textSecondary,
1391
-
fontSize: 16,
1392
-
marginLeft: 8,
1393
-
whiteSpace: "nowrap",
1394
-
flexShrink: 0,
1395
-
maxWidth: "100%",
1396
-
}}
1397
-
className="text-gray-500 dark:text-gray-400"
1398
-
>
1399
-
ยท {/* time placeholder */}
1400
-
{shortTimeAgo(post.indexedAt)}
1401
-
</span>
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
+
)}
1402
1871
</div>
1403
-
</div>
1404
-
{/* reply indicator */}
1405
-
{!!feedviewpostreplyhandle && (
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
+
)}
1406
1894
<div
1407
1895
style={{
1408
-
display: "flex",
1409
-
borderRadius: 12,
1410
-
paddingBottom: 2,
1411
-
fontSize: 14,
1412
-
justifyContent: "flex-start",
1413
-
//color: theme.textSecondary,
1414
-
gap: 4,
1415
-
alignItems: "center",
1416
-
//marginLeft: 36,
1417
-
height:
1418
-
!(expanded || isQuote) && !!feedviewpostreplyhandle
1419
-
? "1rem"
1420
-
: 0,
1421
-
opacity:
1422
-
!(expanded || isQuote) && !!feedviewpostreplyhandle ? 1 : 0,
1896
+
paddingTop: post.embed && !concise && depth < 1 ? 4 : 0,
1423
1897
}}
1424
-
className="text-gray-500 dark:text-gray-400"
1425
1898
>
1426
-
<MdiReply /> Reply to @{feedviewpostreplyhandle}
1427
-
</div>
1428
-
)}
1429
-
<div
1430
-
style={{
1431
-
fontSize: 16,
1432
-
marginBottom: !post.embed /*|| depth > 0*/ ? 0 : 8,
1433
-
whiteSpace: "pre-wrap",
1434
-
textAlign: "left",
1435
-
overflowWrap: "anywhere",
1436
-
wordBreak: "break-word",
1437
-
//color: theme.text,
1438
-
}}
1439
-
className="text-gray-900 dark:text-gray-100"
1440
-
>
1441
-
{renderTextWithFacets({
1442
-
text: (post.record as { text?: string }).text ?? "",
1443
-
facets: (post.record.facets as Facet[]) ?? [],
1444
-
navigate: navigate
1445
-
})}
1446
-
{}
1447
-
</div>
1448
-
{post.embed && depth < 1 ? (
1449
-
<PostEmbeds
1450
-
embed={post.embed}
1451
-
//moderation={moderation}
1452
-
viewContext={PostEmbedViewContext.Feed}
1453
-
salt={salt}
1454
-
navigate={navigate}
1455
-
/>
1456
-
) : null}
1457
-
{post.embed && depth > 0 && (
1458
-
<>
1459
-
<div className="border-gray-300 dark:border-gray-600 p-3 rounded-xl border italic text-gray-400 text-[14px]">
1460
-
(there is an embed here thats too deep to render)
1461
-
</div>
1462
-
</>
1463
-
)}
1464
-
<div style={{ paddingTop: post.embed && depth < 1 ? 4 : 0 }}>
1465
-
<>
1466
-
{expanded && (
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 && (
1467
1923
<div
1468
1924
style={{
1469
-
overflow: "hidden",
1470
-
//color: theme.textSecondary,
1471
-
fontSize: 14,
1472
1925
display: "flex",
1473
-
borderBottomStyle: "solid",
1474
-
//borderBottomColor: theme.border,
1475
-
//background: "#f00",
1476
-
// height: "1rem",
1477
-
paddingTop: 4,
1478
-
paddingBottom: 8,
1479
-
borderBottomWidth: 1,
1480
-
marginBottom: 8,
1481
-
}} // important for height animation
1482
-
className="text-gray-500 dark:text-gray-400 border-gray-200 dark:border-gray-700"
1483
-
>
1484
-
{fullDateTimeFormat(post.indexedAt)}
1485
-
</div>
1486
-
)}
1487
-
</>
1488
-
{!isQuote && (
1489
-
<div
1490
-
style={{
1491
-
display: "flex",
1492
-
gap: 32,
1493
-
paddingTop: 8,
1494
-
//color: theme.textSecondary,
1495
-
fontSize: 15,
1496
-
justifyContent: "space-between",
1497
-
//background: "#0f0",
1498
-
}}
1499
-
className="text-gray-500 dark:text-gray-400"
1500
-
>
1501
-
<span style={btnstyle}>
1502
-
<MdiCommentOutline />
1503
-
{post.replyCount}
1504
-
</span>
1505
-
<HitSlopButton
1506
-
onClick={() => {
1507
-
repostOrUnrepostPost();
1926
+
gap: 32,
1927
+
paddingTop: 8,
1928
+
//color: theme.textSecondary,
1929
+
fontSize: 15,
1930
+
justifyContent: "space-between",
1931
+
//background: "#0f0",
1508
1932
}}
1509
-
style={{
1510
-
...btnstyle,
1511
-
...(hasRetweeted ? { color: "#5CEFAA" } : {}),
1512
-
}}
1513
-
>
1514
-
{hasRetweeted ? <MdiRepeatGreen /> : <MdiRepeat />}
1515
-
{(post.repostCount || 0) + (hasRetweeted ? 1 : 0)}
1516
-
</HitSlopButton>
1517
-
<HitSlopButton
1518
-
onClick={() => {
1519
-
likeOrUnlikePost();
1520
-
}}
1521
-
style={{
1522
-
...btnstyle,
1523
-
...(hasLiked ? { color: "#EC4899" } : {}),
1524
-
}}
1933
+
className="text-gray-500 dark:text-gray-400"
1525
1934
>
1526
-
{hasLiked ? <MdiCardsHeart /> : <MdiCardsHeartOutline />}
1527
-
{(post.likeCount || 0) + (hasLiked ? 1 : 0)}
1528
-
</HitSlopButton>
1529
-
<div style={{ display: "flex", gap: 8 }}>
1530
1935
<HitSlopButton
1531
-
onClick={async (e) => {
1532
-
e.stopPropagation();
1533
-
try {
1534
-
await navigator.clipboard.writeText(
1535
-
"https://bsky.app" +
1536
-
"/profile/" +
1537
-
post.author.handle +
1538
-
"/post/" +
1539
-
post.uri.split("/").pop()
1540
-
);
1541
-
} catch (_e) {
1542
-
// idk
1543
-
}
1936
+
onClick={() => {
1937
+
setComposerPost({ kind: "reply", parent: post.uri });
1544
1938
}}
1545
1939
style={{
1546
1940
...btnstyle,
1547
1941
}}
1548
1942
>
1549
-
<MdiShareVariant />
1943
+
<MdiCommentOutline />
1944
+
{post.replyCount}
1550
1945
</HitSlopButton>
1551
-
<span style={btnstyle}>
1552
-
<MdiMoreHoriz />
1553
-
</span>
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>
1554
2044
</div>
1555
-
</div>
1556
-
)}
2045
+
)}
2046
+
</div>
2047
+
<div
2048
+
style={{
2049
+
//height: bottomReplyLine ? 16 : 0
2050
+
height: isQuote ? 12 : 16,
2051
+
}}
2052
+
/>
1557
2053
</div>
1558
-
<div
1559
-
style={{
1560
-
//height: bottomReplyLine ? 16 : 0
1561
-
height: isQuote ? 12 : 16,
1562
-
}}
1563
-
/>
1564
2054
</div>
1565
2055
</div>
1566
2056
</div>
···
1652
2142
viewContext,
1653
2143
salt,
1654
2144
navigate,
2145
+
postid,
2146
+
nopics,
2147
+
lightboxCallback,
1655
2148
}: {
1656
2149
embed?: Embed;
1657
2150
moderation?: ModerationDecision;
···
1660
2153
viewContext?: PostEmbedViewContext;
1661
2154
salt: string;
1662
2155
navigate: (_: any) => void;
2156
+
postid?: { did: string; rkey: string };
2157
+
nopics?: boolean;
2158
+
lightboxCallback?: (d: LightboxProps) => void;
1663
2159
}) {
1664
-
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
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
+
}
1665
2171
if (
1666
2172
AppBskyEmbedRecordWithMedia.isView(embed) &&
1667
2173
AppBskyEmbedRecord.isViewRecord(embed.record.record) &&
···
1695
2201
viewContext={viewContext}
1696
2202
salt={salt}
1697
2203
navigate={navigate}
2204
+
postid={postid}
2205
+
nopics={nopics}
2206
+
lightboxCallback={lightboxCallback}
1698
2207
/>
1699
2208
{/* padding empty div of 8px height */}
1700
2209
<div style={{ height: 12 }} />
···
1708
2217
//boxShadow: theme.cardShadow,
1709
2218
overflow: "hidden",
1710
2219
}}
1711
-
className="shadow border border-gray-200 dark:border-gray-700"
2220
+
className="shadow border border-gray-200 dark:border-gray-800 was7"
1712
2221
>
1713
2222
<UniversalPostRenderer
1714
2223
post={post}
···
1716
2225
salt={salt}
1717
2226
onPostClick={(e) => {
1718
2227
e.stopPropagation();
1719
-
const parsed = parseAtUri(post.uri);
2228
+
const parsed = new AtUri(post.uri); //parseAtUri(post.uri);
1720
2229
if (parsed) {
1721
2230
navigate({
1722
2231
to: "/profile/$did/post/$rkey",
1723
-
params: { did: parsed.did, rkey: parsed.rkey },
2232
+
params: { did: parsed.host, rkey: parsed.rkey },
1724
2233
});
1725
2234
}
1726
2235
}}
···
1756
2265
}
1757
2266
1758
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
+
1759
2272
// custom feed embed (i.e. generator view)
1760
2273
if (AppBskyFeedDefs.isGeneratorView(embed.record)) {
1761
2274
// stopgap sorry
···
1765
2278
// <MaybeFeedCard view={embed.record} />
1766
2279
// </div>
1767
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
+
);
1768
2291
}
1769
2292
1770
2293
// list embed
···
1776
2299
// <MaybeListCard view={embed.record} />
1777
2300
// </div>
1778
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
+
);
1779
2317
}
1780
2318
1781
2319
// starter pack embed
···
1787
2325
// <StarterPackCard starterPack={embed.record} />
1788
2326
// </div>
1789
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
+
);
1790
2343
}
1791
2344
1792
2345
// quote post
···
1825
2378
//boxShadow: theme.cardShadow,
1826
2379
overflow: "hidden",
1827
2380
}}
1828
-
className="shadow border border-gray-200 dark:border-gray-700"
2381
+
className="shadow border border-gray-200 dark:border-gray-800 was7"
1829
2382
>
1830
2383
<UniversalPostRenderer
1831
2384
post={post}
···
1833
2386
salt={salt}
1834
2387
onPostClick={(e) => {
1835
2388
e.stopPropagation();
1836
-
const parsed = parseAtUri(post.uri);
2389
+
const parsed = new AtUri(post.uri); //parseAtUri(post.uri);
1837
2390
if (parsed) {
1838
2391
navigate({
1839
2392
to: "/profile/$did/post/$rkey",
1840
-
params: { did: parsed.did, rkey: parsed.rkey },
2393
+
params: { did: parsed.host, rkey: parsed.rkey },
1841
2394
});
1842
2395
}
1843
2396
}}
···
1846
2399
</div>
1847
2400
);
1848
2401
} else {
2402
+
console.log("what the hell is a ", embed);
1849
2403
return <>sorry</>;
1850
2404
}
1851
2405
//return <QuotePostRenderer record={embed.record} moderation={moderation} />;
···
1869
2423
src: img.fullsize,
1870
2424
alt: img.alt,
1871
2425
}));
2426
+
console.log("rendering images");
2427
+
if (lightboxCallback) {
2428
+
lightboxCallback({ images: lightboxImages });
2429
+
console.log("rendering images");
2430
+
}
2431
+
2432
+
if (nopics) return;
1872
2433
1873
2434
if (images.length > 0) {
1874
2435
// const items = embed.images.map(img => ({
···
1898
2459
//border: `1px solid ${theme.border}`,
1899
2460
overflow: "hidden",
1900
2461
}}
1901
-
className="border border-gray-200 dark:border-gray-700 bg-gray-200 dark:bg-gray-900"
2462
+
className="border border-gray-200 dark:border-gray-800 was7 bg-gray-200 dark:bg-gray-900"
1902
2463
>
1903
-
{lightboxIndex !== null && (
2464
+
{/* {lightboxIndex !== null && (
1904
2465
<Lightbox
1905
2466
images={lightboxImages}
1906
2467
index={lightboxIndex}
1907
2468
onClose={() => setLightboxIndex(null)}
1908
2469
onNavigate={(newIndex) => setLightboxIndex(newIndex)}
2470
+
post={postid}
1909
2471
/>
1910
-
)}
2472
+
)} */}
1911
2473
<img
1912
2474
src={image.fullsize}
1913
2475
alt={image.alt}
···
1938
2500
overflow: "hidden",
1939
2501
//border: `1px solid ${theme.border}`,
1940
2502
}}
1941
-
className="border border-gray-200 dark:border-gray-700"
2503
+
className="border border-gray-200 dark:border-gray-800 was7"
1942
2504
>
1943
-
{lightboxIndex !== null && (
2505
+
{/* {lightboxIndex !== null && (
1944
2506
<Lightbox
1945
2507
images={lightboxImages}
1946
2508
index={lightboxIndex}
1947
2509
onClose={() => setLightboxIndex(null)}
1948
2510
onNavigate={(newIndex) => setLightboxIndex(newIndex)}
2511
+
post={postid}
1949
2512
/>
1950
-
)}
2513
+
)} */}
1951
2514
{images.map((img, i) => (
1952
2515
<div
1953
2516
key={i}
···
1987
2550
//border: `1px solid ${theme.border}`,
1988
2551
// height: 240, // fixed height for cropping
1989
2552
}}
1990
-
className="border border-gray-200 dark:border-gray-700"
2553
+
className="border border-gray-200 dark:border-gray-800 was7"
1991
2554
>
1992
-
{lightboxIndex !== null && (
2555
+
{/* {lightboxIndex !== null && (
1993
2556
<Lightbox
1994
2557
images={lightboxImages}
1995
2558
index={lightboxIndex}
1996
2559
onClose={() => setLightboxIndex(null)}
1997
2560
onNavigate={(newIndex) => setLightboxIndex(newIndex)}
2561
+
post={postid}
1998
2562
/>
1999
-
)}
2563
+
)} */}
2000
2564
{/* Left: 1:1 */}
2001
2565
<div
2002
2566
style={{ flex: 1, aspectRatio: "1 / 1", position: "relative" }}
···
2071
2635
//border: `1px solid ${theme.border}`,
2072
2636
//aspectRatio: "3 / 2", // overall grid aspect
2073
2637
}}
2074
-
className="border border-gray-200 dark:border-gray-700"
2638
+
className="border border-gray-200 dark:border-gray-800 was7"
2075
2639
>
2076
-
{lightboxIndex !== null && (
2640
+
{/* {lightboxIndex !== null && (
2077
2641
<Lightbox
2078
2642
images={lightboxImages}
2079
2643
index={lightboxIndex}
2080
2644
onClose={() => setLightboxIndex(null)}
2081
2645
onNavigate={(newIndex) => setLightboxIndex(newIndex)}
2646
+
post={postid}
2082
2647
/>
2083
-
)}
2648
+
)} */}
2084
2649
{images.map((img, i) => (
2085
2650
<div
2086
2651
key={i}
···
2144
2709
// =
2145
2710
if (AppBskyEmbedVideo.isView(embed)) {
2146
2711
// hls playlist
2712
+
if (nopics) return;
2147
2713
const playlist = embed.playlist;
2148
2714
return (
2149
2715
<SmartHLSPlayer
···
2171
2737
return <div />;
2172
2738
}
2173
2739
2174
-
import { createPortal } from "react-dom";
2175
-
type LightboxProps = {
2176
-
images: { src: string; alt?: string }[];
2177
-
index: number;
2178
-
onClose: () => void;
2179
-
onNavigate?: (newIndex: number) => void;
2180
-
};
2181
-
export function Lightbox({
2182
-
images,
2183
-
index,
2184
-
onClose,
2185
-
onNavigate,
2186
-
}: LightboxProps) {
2187
-
const image = images[index];
2188
-
2189
-
useEffect(() => {
2190
-
function handleKey(e: KeyboardEvent) {
2191
-
if (e.key === "Escape") onClose();
2192
-
if (e.key === "ArrowRight" && onNavigate)
2193
-
onNavigate((index + 1) % images.length);
2194
-
if (e.key === "ArrowLeft" && onNavigate)
2195
-
onNavigate((index - 1 + images.length) % images.length);
2196
-
}
2197
-
window.addEventListener("keydown", handleKey);
2198
-
return () => window.removeEventListener("keydown", handleKey);
2199
-
}, [index, images.length, onClose, onNavigate]);
2200
-
2201
-
return createPortal(
2202
-
<div
2203
-
className="fixed inset-0 z-50 flex items-center justify-center bg-black/80"
2204
-
onClick={(e) => {
2205
-
e.stopPropagation();
2206
-
onClose();
2207
-
}}
2208
-
>
2209
-
<img
2210
-
src={image.src}
2211
-
alt={image.alt}
2212
-
className="max-h-[90vh] max-w-[90vw] object-contain rounded-lg shadow-lg"
2213
-
onClick={(e) => e.stopPropagation()}
2214
-
/>
2215
-
2216
-
{images.length > 1 && (
2217
-
<>
2218
-
<button
2219
-
onClick={(e) => {
2220
-
e.stopPropagation();
2221
-
onNavigate?.((index - 1 + images.length) % images.length);
2222
-
}}
2223
-
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"
2224
-
>
2225
-
<svg
2226
-
xmlns="http://www.w3.org/2000/svg"
2227
-
width={28}
2228
-
height={28}
2229
-
viewBox="0 0 24 24"
2230
-
>
2231
-
<g fill="none" fillRule="evenodd">
2232
-
<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>
2233
-
<path
2234
-
fill="currentColor"
2235
-
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"
2236
-
></path>
2237
-
</g>
2238
-
</svg>
2239
-
</button>
2240
-
<button
2241
-
onClick={(e) => {
2242
-
e.stopPropagation();
2243
-
onNavigate?.((index + 1) % images.length);
2244
-
}}
2245
-
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"
2246
-
>
2247
-
<svg
2248
-
xmlns="http://www.w3.org/2000/svg"
2249
-
width={28}
2250
-
height={28}
2251
-
viewBox="0 0 24 24"
2252
-
>
2253
-
<g fill="none" fillRule="evenodd">
2254
-
<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>
2255
-
<path
2256
-
fill="currentColor"
2257
-
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"
2258
-
></path>
2259
-
</g>
2260
-
</svg>
2261
-
</button>
2262
-
</>
2263
-
)}
2264
-
</div>,
2265
-
document.body
2266
-
);
2267
-
}
2268
-
2269
2740
function getDomain(url: string) {
2270
2741
try {
2271
2742
const { hostname } = new URL(url);
···
2296
2767
for (let i = 0; i < bytes.length; i++) {
2297
2768
map[byteIndex++] = charIndex;
2298
2769
}
2299
-
charIndex+=char.length;
2770
+
charIndex += char.length;
2300
2771
}
2301
2772
2302
2773
return map;
···
2330
2801
return { start, end, feature: f.features[0] };
2331
2802
});
2332
2803
}
2333
-
function renderTextWithFacets({
2804
+
export function renderTextWithFacets({
2334
2805
text,
2335
2806
facets,
2336
2807
navigate,
···
2362
2833
className="link"
2363
2834
style={{
2364
2835
textDecoration: "none",
2365
-
color: "rgb(29, 122, 242)",
2836
+
color: "var(--link-text-color)",
2366
2837
wordBreak: "break-all",
2367
2838
}}
2368
2839
target="_blank"
···
2382
2853
result.push(
2383
2854
<span
2384
2855
key={start}
2385
-
style={{ color: "rgb(29, 122, 242)" }}
2856
+
style={{ color: "var(--link-text-color)" }}
2386
2857
className=" cursor-pointer"
2387
2858
onClick={(e) => {
2388
2859
e.stopPropagation();
2389
2860
navigate({
2390
2861
to: "/profile/$did",
2391
2862
// @ts-expect-error i didnt bother with the correct types here sorry. bsky api types are cursed
2392
-
params: { did: feature.did},
2863
+
params: { did: feature.did },
2393
2864
});
2394
2865
}}
2395
2866
>
···
2400
2871
result.push(
2401
2872
<span
2402
2873
key={start}
2403
-
style={{ color: "rgb(29, 122, 242)" }}
2874
+
style={{ color: "var(--link-text-color)" }}
2404
2875
onClick={(e) => {
2405
2876
e.stopPropagation();
2406
2877
}}
···
2493
2964
>
2494
2965
<div
2495
2966
style={containerStyle as React.CSSProperties}
2496
-
className="border border-gray-200 dark:border-gray-700"
2967
+
className="border border-gray-200 dark:border-gray-800 was7"
2497
2968
>
2498
2969
{thumb && (
2499
2970
<div
···
2507
2978
marginBottom: 8,
2508
2979
//borderBottom: `1px solid ${theme.border}`,
2509
2980
}}
2510
-
className="border-b border-gray-200 dark:border-gray-700"
2981
+
className="border-b border-gray-200 dark:border-gray-800 was7"
2511
2982
>
2512
2983
<img
2513
2984
src={thumb}
···
2633
3104
borderRadius: 12,
2634
3105
//border: `1px solid ${theme.border}`,
2635
3106
}}
2636
-
className="border border-gray-200 dark:border-gray-700"
3107
+
className="border border-gray-200 dark:border-gray-800 was7"
2637
3108
onClick={async (e) => {
2638
3109
e.stopPropagation();
2639
3110
setPlaying(true);
···
2674
3145
100 / (aspect ? aspect.width / aspect.height : 16 / 9)
2675
3146
}%`, // 16:9 = 56.25%, 4:3 = 75%
2676
3147
}}
2677
-
className="border border-gray-200 dark:border-gray-700"
3148
+
className="border border-gray-200 dark:border-gray-800 was7"
2678
3149
>
2679
3150
<ReactPlayer
2680
3151
src={url}
+59
-14
src/main.tsx
+59
-14
src/main.tsx
···
1
-
import { StrictMode } from "react";
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";
2
10
import ReactDOM from "react-dom/client";
3
-
import { RouterProvider, createRouter } from "@tanstack/react-router";
4
11
12
+
import reportWebVitals from "./reportWebVitals.ts";
5
13
// Import the generated route tree
6
14
import { routeTree } from "./routeTree.gen";
15
+
import { isAtTopAtom } from "./utils/atoms.ts";
7
16
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
-
17
+
//initAtomToCssVar(hueAtom, "--tw-gray-hue")
16
18
17
19
const queryClient = new QueryClient({
18
20
defaultOptions: {
···
28
30
persistQueryClient({
29
31
queryClient,
30
32
persister: localStoragePersister,
31
-
})
33
+
});
32
34
33
35
// Create a new router instance
34
36
const router = createRouter({
···
54
56
root.render(
55
57
// double queries annoys me
56
58
// <StrictMode>
57
-
<QueryClientProvider client={queryClient}>
58
-
<RouterProvider router={router} />
59
-
</QueryClientProvider>
59
+
<QueryClientProvider client={queryClient}>
60
+
<ScrollTopWatcher />
61
+
<RouterProvider router={router} />
62
+
</QueryClientProvider>
60
63
// </StrictMode>
61
64
);
62
65
}
···
65
68
// to log results (for example: reportWebVitals(// /*mass comment*/ console.log))
66
69
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
67
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
+
}
-149
src/providers/PassAuthProvider.tsx
-149
src/providers/PassAuthProvider.tsx
···
1
-
import React, { createContext, useState, useEffect, useContext } from "react";
2
-
import { AtpAgent, type AtpSessionData } from "@atproto/api";
3
-
4
-
interface AuthContextValue {
5
-
agent: AtpAgent | null;
6
-
loginStatus: boolean;
7
-
login: (user: string, password: string, service?: string) => Promise<void>;
8
-
logout: () => Promise<void>;
9
-
loading: boolean;
10
-
authed: boolean | undefined;
11
-
}
12
-
13
-
const AuthContext = createContext<AuthContextValue>({} as AuthContextValue);
14
-
15
-
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
16
-
const [agent, setAgent] = useState<AtpAgent | null>(null);
17
-
const [loginStatus, setLoginStatus] = useState(false);
18
-
const [loading, setLoading] = useState(true);
19
-
const [increment, setIncrement] = useState(0);
20
-
const [authed, setAuthed] = useState<boolean | undefined>(undefined);
21
-
22
-
useEffect(() => {
23
-
const initialize = async () => {
24
-
try {
25
-
const service = localStorage.getItem("service");
26
-
// const user = await AsyncStorage.getItem('user');
27
-
// const password = await AsyncStorage.getItem('password');
28
-
const session = localStorage.getItem("sess");
29
-
30
-
if (service && session) {
31
-
// /*mass comment*/ console.log("Auto-login service is:", service);
32
-
const apiAgent = new AtpAgent({ service });
33
-
try {
34
-
if (!apiAgent) {
35
-
// /*mass comment*/ console.log("Agent is null or undefined");
36
-
return;
37
-
}
38
-
let sess: AtpSessionData = JSON.parse(session);
39
-
// /*mass comment*/ console.log("resuming session is:", sess);
40
-
const { data } = await apiAgent.resumeSession(sess);
41
-
// /*mass comment*/ console.log("!!!8!!! agent resume session");
42
-
setAgent(apiAgent);
43
-
setLoginStatus(true);
44
-
setLoading(false);
45
-
setAuthed(true);
46
-
} catch (e) {
47
-
// /*mass comment*/ console.log("Failed to resume session" + e);
48
-
setLoginStatus(true);
49
-
localStorage.removeItem("sess");
50
-
localStorage.removeItem("service");
51
-
const apiAgent = new AtpAgent({ service: "https://api.bsky.app" });
52
-
setAgent(apiAgent);
53
-
setLoginStatus(true);
54
-
setLoading(false);
55
-
setAuthed(false);
56
-
return;
57
-
}
58
-
} else {
59
-
const apiAgent = new AtpAgent({ service: "https://api.bsky.app" });
60
-
setAgent(apiAgent);
61
-
setLoginStatus(true);
62
-
setLoading(false);
63
-
setAuthed(false);
64
-
}
65
-
} catch (e) {
66
-
// /*mass comment*/ console.log("Failed to auto-login:", e);
67
-
} finally {
68
-
setLoading(false);
69
-
}
70
-
};
71
-
72
-
initialize();
73
-
}, [increment]);
74
-
75
-
const login = async (
76
-
user: string,
77
-
password: string,
78
-
service: string = "https://bsky.social",
79
-
) => {
80
-
try {
81
-
let sessionthing;
82
-
const apiAgent = new AtpAgent({
83
-
service: service,
84
-
persistSession: (evt, sess) => {
85
-
sessionthing = sess;
86
-
},
87
-
});
88
-
await apiAgent.login({ identifier: user, password });
89
-
// /*mass comment*/ console.log("!!!8!!! agent logged on");
90
-
91
-
localStorage.setItem("service", service);
92
-
// await AsyncStorage.setItem('user', user);
93
-
// await AsyncStorage.setItem('password', password);
94
-
if (sessionthing) {
95
-
localStorage.setItem("sess", JSON.stringify(sessionthing));
96
-
} else {
97
-
localStorage.setItem("sess", "{}");
98
-
}
99
-
100
-
setAgent(apiAgent);
101
-
setLoginStatus(true);
102
-
setAuthed(true);
103
-
} catch (e) {
104
-
console.error("Login failed:", e);
105
-
}
106
-
};
107
-
108
-
const logout = async () => {
109
-
if (!agent) {
110
-
console.error("Agent is null or undefined");
111
-
return;
112
-
}
113
-
setLoading(true);
114
-
try {
115
-
// check if its even in async storage before removing
116
-
if (localStorage.getItem("service") && localStorage.getItem("sess")) {
117
-
localStorage.removeItem("service");
118
-
localStorage.removeItem("sess");
119
-
}
120
-
await agent.logout();
121
-
// /*mass comment*/ console.log("!!!8!!! agent logout");
122
-
setLoginStatus(false);
123
-
setAuthed(undefined);
124
-
await agent.com.atproto.server.deleteSession();
125
-
// /*mass comment*/ console.log("!!!8!!! agent deltesession");
126
-
//setAgent(null);
127
-
setIncrement(increment + 1);
128
-
} catch (e) {
129
-
console.error("Logout failed:", e);
130
-
} finally {
131
-
setLoading(false);
132
-
}
133
-
};
134
-
135
-
// why the hell are we doing this
136
-
/*if (loading) {
137
-
return <div><span>Laoding...ae</span></div>;
138
-
}*/
139
-
140
-
return (
141
-
<AuthContext.Provider
142
-
value={{ agent, loginStatus, login, logout, loading, authed }}
143
-
>
144
-
{children}
145
-
</AuthContext.Provider>
146
-
);
147
-
};
148
-
149
-
export const useAuth = () => useContext(AuthContext);
-61
src/providers/PersistentStoreProvider.tsx
-61
src/providers/PersistentStoreProvider.tsx
···
1
-
import React, { createContext, useContext, useCallback } from "react";
2
-
import { get as idbGet, set as idbSet, del as idbDel } from "idb-keyval";
3
-
4
-
type PersistentValue = {
5
-
value: string;
6
-
time: number;
7
-
};
8
-
9
-
type PersistentStoreContextType = {
10
-
get: (key: string) => Promise<PersistentValue | null>;
11
-
set: (key: string, value: string) => Promise<void>;
12
-
remove: (key: string) => Promise<void>;
13
-
};
14
-
15
-
const PersistentStoreContext = createContext<PersistentStoreContextType | null>(
16
-
null,
17
-
);
18
-
19
-
export const PersistentStoreProvider: React.FC<{
20
-
children: React.ReactNode;
21
-
}> = ({ children }) => {
22
-
const get = useCallback(
23
-
async (key: string): Promise<PersistentValue | null> => {
24
-
if (typeof window === "undefined") return null;
25
-
const raw = await idbGet(key);
26
-
if (!raw) return null;
27
-
try {
28
-
return JSON.parse(raw) as PersistentValue;
29
-
} catch {
30
-
return null;
31
-
}
32
-
},
33
-
[],
34
-
);
35
-
36
-
const set = useCallback(async (key: string, value: string) => {
37
-
if (typeof window === "undefined") return;
38
-
const entry: PersistentValue = { value, time: Date.now() };
39
-
await idbSet(key, JSON.stringify(entry));
40
-
}, []);
41
-
42
-
const remove = useCallback(async (key: string) => {
43
-
if (typeof window === "undefined") return;
44
-
await idbDel(key);
45
-
}, []);
46
-
47
-
return (
48
-
<PersistentStoreContext.Provider value={{ get, set, remove }}>
49
-
{children}
50
-
</PersistentStoreContext.Provider>
51
-
);
52
-
};
53
-
54
-
export const usePersistentStore = (): PersistentStoreContextType => {
55
-
const context = useContext(PersistentStoreContext);
56
-
if (!context)
57
-
throw new Error(
58
-
"usePersistentStore must be used within a PersistentStoreProvider",
59
-
);
60
-
return context;
61
-
};
+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
1
import { Agent, AtpAgent, type AtpSessionData } from "@atproto/api";
4
2
import {
5
3
type OAuthSession,
···
7
5
TokenRefreshError,
8
6
TokenRevokedError,
9
7
} from "@atproto/oauth-client-browser";
8
+
import { useAtom } from "jotai";
10
9
import React, {
11
10
createContext,
12
11
use,
···
15
14
useState,
16
15
} from "react";
17
16
18
-
import { oauthClient } from "../utils/oauthClient"; // Adjust path if needed
17
+
import { quickAuthAtom } from "~/utils/atoms";
18
+
19
+
import { oauthClient } from "../utils/oauthClient";
19
20
20
-
// Define the unified status and authentication method
21
21
type AuthStatus = "loading" | "signedIn" | "signedOut";
22
22
type AuthMethod = "password" | "oauth" | null;
23
23
24
24
interface AuthContextValue {
25
-
agent: Agent | null; // The agent is typed as the base class `Agent`
25
+
agent: Agent | null;
26
26
status: AuthStatus;
27
27
authMethod: AuthMethod;
28
28
loginWithPassword: (
···
41
41
}: {
42
42
children: React.ReactNode;
43
43
}) => {
44
-
// The state is typed as the base class `Agent`, which accepts both `Agent` and `AtpAgent` instances.
45
44
const [agent, setAgent] = useState<Agent | null>(null);
46
45
const [status, setStatus] = useState<AuthStatus>("loading");
47
46
const [authMethod, setAuthMethod] = useState<AuthMethod>(null);
48
47
const [oauthSession, setOauthSession] = useState<OAuthSession | null>(null);
48
+
const [quickAuth, setQuickAuth] = useAtom(quickAuthAtom);
49
49
50
-
// Unified Initialization Logic
51
50
const initialize = useCallback(async () => {
52
-
// --- 1. Try OAuth initialization first ---
53
51
try {
54
52
const oauthResult = await oauthClient.init();
55
53
if (oauthResult) {
56
54
// /*mass comment*/ console.log("OAuth session restored.");
57
-
const apiAgent = new Agent(oauthResult.session); // Standard Agent
55
+
const apiAgent = new Agent(oauthResult.session);
58
56
setAgent(apiAgent);
59
57
setOauthSession(oauthResult.session);
60
58
setAuthMethod("oauth");
61
59
setStatus("signedIn");
62
-
return; // Success
60
+
setQuickAuth(apiAgent?.did || null);
61
+
return;
63
62
}
64
63
} catch (e) {
65
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
+
}
66
69
}
67
70
68
-
// --- 2. If no OAuth, try password-based session using AtpAgent ---
69
71
try {
70
72
const service = localStorage.getItem("service");
71
73
const sessionString = localStorage.getItem("sess");
72
74
73
75
if (service && sessionString) {
74
76
// /*mass comment*/ console.log("Resuming password-based session using AtpAgent...");
75
-
// Use the original, working AtpAgent logic
76
77
const apiAgent = new AtpAgent({ service });
77
78
const session: AtpSessionData = JSON.parse(sessionString);
78
79
await apiAgent.resumeSession(session);
79
80
80
81
// /*mass comment*/ console.log("Password-based session resumed successfully.");
81
-
setAgent(apiAgent); // This works because AtpAgent is a subclass of Agent
82
+
setAgent(apiAgent);
82
83
setAuthMethod("password");
83
84
setStatus("signedIn");
84
-
return; // Success
85
+
setQuickAuth(apiAgent?.did || null);
86
+
return;
85
87
}
86
88
} catch (e) {
87
89
console.error("Failed to resume password-based session.", e);
···
89
91
localStorage.removeItem("service");
90
92
}
91
93
92
-
// --- 3. If neither worked, user is signed out ---
93
94
// /*mass comment*/ console.log("No active session found.");
94
95
setStatus("signedOut");
95
96
setAgent(null);
96
97
setAuthMethod(null);
97
-
}, []);
98
+
// do we want to null it here?
99
+
setQuickAuth(null);
100
+
}, [quickAuth, setQuickAuth]);
98
101
99
102
useEffect(() => {
100
103
const handleOAuthSessionDeleted = (
···
105
108
setOauthSession(null);
106
109
setAuthMethod(null);
107
110
setStatus("signedOut");
111
+
setQuickAuth(null);
108
112
};
109
113
110
114
oauthClient.addEventListener("deleted", handleOAuthSessionDeleted as EventListener);
···
113
117
return () => {
114
118
oauthClient.removeEventListener("deleted", handleOAuthSessionDeleted as EventListener);
115
119
};
116
-
}, [initialize]);
120
+
}, [initialize, setQuickAuth]);
117
121
118
-
// --- Login Methods ---
119
122
const loginWithPassword = async (
120
123
user: string,
121
124
password: string,
···
125
128
setStatus("loading");
126
129
try {
127
130
let sessionData: AtpSessionData | undefined;
128
-
// Use the AtpAgent for its simple login and session persistence
129
131
const apiAgent = new AtpAgent({
130
132
service,
131
133
persistSession: (_evt, sess) => {
···
137
139
if (sessionData) {
138
140
localStorage.setItem("service", service);
139
141
localStorage.setItem("sess", JSON.stringify(sessionData));
140
-
setAgent(apiAgent); // Store the AtpAgent instance in our state
142
+
setAgent(apiAgent);
141
143
setAuthMethod("password");
142
144
setStatus("signedIn");
145
+
setQuickAuth(apiAgent?.did || null);
143
146
// /*mass comment*/ console.log("Successfully logged in with password.");
144
147
} else {
145
148
throw new Error("Session data not persisted after login.");
···
147
150
} catch (e) {
148
151
console.error("Password login failed:", e);
149
152
setStatus("signedOut");
153
+
setQuickAuth(null);
150
154
throw e;
151
155
}
152
156
};
···
161
165
}
162
166
}, [status]);
163
167
164
-
// --- Unified Logout ---
165
168
const logout = useCallback(async () => {
166
169
if (status !== "signedIn" || !agent) return;
167
170
setStatus("loading");
···
173
176
} else if (authMethod === "password") {
174
177
localStorage.removeItem("service");
175
178
localStorage.removeItem("sess");
176
-
// AtpAgent has its own logout methods
177
179
await (agent as AtpAgent).com.atproto.server.deleteSession();
178
180
// /*mass comment*/ console.log("Password-based session deleted.");
179
181
}
···
184
186
setAuthMethod(null);
185
187
setOauthSession(null);
186
188
setStatus("signedOut");
189
+
setQuickAuth(null);
187
190
}
188
-
}, [status, authMethod, agent, oauthSession]);
191
+
}, [status, agent, authMethod, oauthSession, setQuickAuth]);
189
192
190
193
return (
191
194
<AuthContext
+186
-5
src/routeTree.gen.ts
+186
-5
src/routeTree.gen.ts
···
12
12
import { Route as SettingsRouteImport } from './routes/settings'
13
13
import { Route as SearchRouteImport } from './routes/search'
14
14
import { Route as NotificationsRouteImport } from './routes/notifications'
15
+
import { Route as ModerationRouteImport } from './routes/moderation'
15
16
import { Route as FeedsRouteImport } from './routes/feeds'
16
17
import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout'
17
18
import { Route as IndexRouteImport } from './routes/index'
18
19
import { Route as CallbackIndexRouteImport } from './routes/callback/index'
19
20
import { Route as PathlessLayoutNestedLayoutRouteImport } from './routes/_pathlessLayout/_nested-layout'
20
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'
21
24
import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
22
25
import { Route as PathlessLayoutNestedLayoutRouteARouteImport } from './routes/_pathlessLayout/_nested-layout/route-a'
23
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'
24
32
25
33
const SettingsRoute = SettingsRouteImport.update({
26
34
id: '/settings',
···
37
45
path: '/notifications',
38
46
getParentRoute: () => rootRouteImport,
39
47
} as any)
48
+
const ModerationRoute = ModerationRouteImport.update({
49
+
id: '/moderation',
50
+
path: '/moderation',
51
+
getParentRoute: () => rootRouteImport,
52
+
} as any)
40
53
const FeedsRoute = FeedsRouteImport.update({
41
54
id: '/feeds',
42
55
path: '/feeds',
···
66
79
path: '/profile/$did/',
67
80
getParentRoute: () => rootRouteImport,
68
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)
69
92
const PathlessLayoutNestedLayoutRouteBRoute =
70
93
PathlessLayoutNestedLayoutRouteBRouteImport.update({
71
94
id: '/route-b',
···
83
106
path: '/profile/$did/post/$rkey',
84
107
getParentRoute: () => rootRouteImport,
85
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)
86
138
87
139
export interface FileRoutesByFullPath {
88
140
'/': typeof IndexRoute
89
141
'/feeds': typeof FeedsRoute
142
+
'/moderation': typeof ModerationRoute
90
143
'/notifications': typeof NotificationsRoute
91
144
'/search': typeof SearchRoute
92
145
'/settings': typeof SettingsRoute
93
146
'/callback': typeof CallbackIndexRoute
94
147
'/route-a': typeof PathlessLayoutNestedLayoutRouteARoute
95
148
'/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
149
+
'/profile/$did/followers': typeof ProfileDidFollowersRoute
150
+
'/profile/$did/follows': typeof ProfileDidFollowsRoute
96
151
'/profile/$did': typeof ProfileDidIndexRoute
97
-
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRoute
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
98
158
}
99
159
export interface FileRoutesByTo {
100
160
'/': typeof IndexRoute
101
161
'/feeds': typeof FeedsRoute
162
+
'/moderation': typeof ModerationRoute
102
163
'/notifications': typeof NotificationsRoute
103
164
'/search': typeof SearchRoute
104
165
'/settings': typeof SettingsRoute
105
166
'/callback': typeof CallbackIndexRoute
106
167
'/route-a': typeof PathlessLayoutNestedLayoutRouteARoute
107
168
'/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
169
+
'/profile/$did/followers': typeof ProfileDidFollowersRoute
170
+
'/profile/$did/follows': typeof ProfileDidFollowsRoute
108
171
'/profile/$did': typeof ProfileDidIndexRoute
109
-
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRoute
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
110
178
}
111
179
export interface FileRoutesById {
112
180
__root__: typeof rootRouteImport
113
181
'/': typeof IndexRoute
114
182
'/_pathlessLayout': typeof PathlessLayoutRouteWithChildren
115
183
'/feeds': typeof FeedsRoute
184
+
'/moderation': typeof ModerationRoute
116
185
'/notifications': typeof NotificationsRoute
117
186
'/search': typeof SearchRoute
118
187
'/settings': typeof SettingsRoute
···
120
189
'/callback/': typeof CallbackIndexRoute
121
190
'/_pathlessLayout/_nested-layout/route-a': typeof PathlessLayoutNestedLayoutRouteARoute
122
191
'/_pathlessLayout/_nested-layout/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
192
+
'/profile/$did/followers': typeof ProfileDidFollowersRoute
193
+
'/profile/$did/follows': typeof ProfileDidFollowsRoute
123
194
'/profile/$did/': typeof ProfileDidIndexRoute
124
-
'/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRoute
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
125
201
}
126
202
export interface FileRouteTypes {
127
203
fileRoutesByFullPath: FileRoutesByFullPath
128
204
fullPaths:
129
205
| '/'
130
206
| '/feeds'
207
+
| '/moderation'
131
208
| '/notifications'
132
209
| '/search'
133
210
| '/settings'
134
211
| '/callback'
135
212
| '/route-a'
136
213
| '/route-b'
214
+
| '/profile/$did/followers'
215
+
| '/profile/$did/follows'
137
216
| '/profile/$did'
217
+
| '/profile/$did/feed/$rkey'
138
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'
139
223
fileRoutesByTo: FileRoutesByTo
140
224
to:
141
225
| '/'
142
226
| '/feeds'
227
+
| '/moderation'
143
228
| '/notifications'
144
229
| '/search'
145
230
| '/settings'
146
231
| '/callback'
147
232
| '/route-a'
148
233
| '/route-b'
234
+
| '/profile/$did/followers'
235
+
| '/profile/$did/follows'
149
236
| '/profile/$did'
237
+
| '/profile/$did/feed/$rkey'
150
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'
151
243
id:
152
244
| '__root__'
153
245
| '/'
154
246
| '/_pathlessLayout'
155
247
| '/feeds'
248
+
| '/moderation'
156
249
| '/notifications'
157
250
| '/search'
158
251
| '/settings'
···
160
253
| '/callback/'
161
254
| '/_pathlessLayout/_nested-layout/route-a'
162
255
| '/_pathlessLayout/_nested-layout/route-b'
256
+
| '/profile/$did/followers'
257
+
| '/profile/$did/follows'
163
258
| '/profile/$did/'
259
+
| '/profile/$did/feed/$rkey'
164
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'
165
265
fileRoutesById: FileRoutesById
166
266
}
167
267
export interface RootRouteChildren {
168
268
IndexRoute: typeof IndexRoute
169
269
PathlessLayoutRoute: typeof PathlessLayoutRouteWithChildren
170
270
FeedsRoute: typeof FeedsRoute
271
+
ModerationRoute: typeof ModerationRoute
171
272
NotificationsRoute: typeof NotificationsRoute
172
273
SearchRoute: typeof SearchRoute
173
274
SettingsRoute: typeof SettingsRoute
174
275
CallbackIndexRoute: typeof CallbackIndexRoute
276
+
ProfileDidFollowersRoute: typeof ProfileDidFollowersRoute
277
+
ProfileDidFollowsRoute: typeof ProfileDidFollowsRoute
175
278
ProfileDidIndexRoute: typeof ProfileDidIndexRoute
176
-
ProfileDidPostRkeyRoute: typeof ProfileDidPostRkeyRoute
279
+
ProfileDidFeedRkeyRoute: typeof ProfileDidFeedRkeyRoute
280
+
ProfileDidPostRkeyRoute: typeof ProfileDidPostRkeyRouteWithChildren
177
281
}
178
282
179
283
declare module '@tanstack/react-router' {
···
197
301
path: '/notifications'
198
302
fullPath: '/notifications'
199
303
preLoaderRoute: typeof NotificationsRouteImport
304
+
parentRoute: typeof rootRouteImport
305
+
}
306
+
'/moderation': {
307
+
id: '/moderation'
308
+
path: '/moderation'
309
+
fullPath: '/moderation'
310
+
preLoaderRoute: typeof ModerationRouteImport
200
311
parentRoute: typeof rootRouteImport
201
312
}
202
313
'/feeds': {
···
241
352
preLoaderRoute: typeof ProfileDidIndexRouteImport
242
353
parentRoute: typeof rootRouteImport
243
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
+
}
244
369
'/_pathlessLayout/_nested-layout/route-b': {
245
370
id: '/_pathlessLayout/_nested-layout/route-b'
246
371
path: '/route-b'
···
262
387
preLoaderRoute: typeof ProfileDidPostRkeyRouteImport
263
388
parentRoute: typeof rootRouteImport
264
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
+
}
265
425
}
266
426
}
267
427
···
295
455
PathlessLayoutRouteChildren,
296
456
)
297
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
+
298
475
const rootRouteChildren: RootRouteChildren = {
299
476
IndexRoute: IndexRoute,
300
477
PathlessLayoutRoute: PathlessLayoutRouteWithChildren,
301
478
FeedsRoute: FeedsRoute,
479
+
ModerationRoute: ModerationRoute,
302
480
NotificationsRoute: NotificationsRoute,
303
481
SearchRoute: SearchRoute,
304
482
SettingsRoute: SettingsRoute,
305
483
CallbackIndexRoute: CallbackIndexRoute,
484
+
ProfileDidFollowersRoute: ProfileDidFollowersRoute,
485
+
ProfileDidFollowsRoute: ProfileDidFollowsRoute,
306
486
ProfileDidIndexRoute: ProfileDidIndexRoute,
307
-
ProfileDidPostRkeyRoute: ProfileDidPostRkeyRoute,
487
+
ProfileDidFeedRkeyRoute: ProfileDidFeedRkeyRoute,
488
+
ProfileDidPostRkeyRoute: ProfileDidPostRkeyRouteWithChildren,
308
489
}
309
490
export const routeTree = rootRouteImport
310
491
._addFileChildren(rootRouteChildren)
+724
-463
src/routes/__root.tsx
+724
-463
src/routes/__root.tsx
···
2
2
3
3
// dont forget to run this
4
4
// npx @tanstack/router-cli generate
5
-
6
-
import { useState, type SVGProps } from "react";
5
+
import type { QueryClient } from "@tanstack/react-query";
7
6
import {
8
-
HeadContent,
9
-
Link,
10
-
Outlet,
11
-
Scripts,
12
-
createRootRoute,
13
7
createRootRouteWithContext,
8
+
// Link,
9
+
// Outlet,
10
+
Scripts,
14
11
useLocation,
15
12
useNavigate,
16
13
} from "@tanstack/react-router";
17
14
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
15
+
import { useAtom } from "jotai";
18
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";
19
22
import { DefaultCatchBoundary } from "~/components/DefaultCatchBoundary";
23
+
import { Import } from "~/components/Import";
20
24
import Login from "~/components/Login";
21
25
import { NotFound } from "~/components/NotFound";
22
-
import appCss from "~/styles/app.css?url";
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";
23
30
import { seo } from "~/utils/seo";
24
-
import { UnifiedAuthProvider, useAuth } from "~/providers/UnifiedAuthProvider";
25
-
import { PersistentStoreProvider } from "~/providers/PersistentStoreProvider";
26
-
import type Agent from "@atproto/api";
27
-
import type { QueryClient } from "@tanstack/react-query";
28
31
29
32
export const Route = createRootRouteWithContext<{
30
33
queryClient: QueryClient;
···
79
82
function RootComponent() {
80
83
return (
81
84
<UnifiedAuthProvider>
82
-
<PersistentStoreProvider>
85
+
<LikeMutationQueueProvider>
83
86
<RootDocument>
84
-
<Outlet />
87
+
<KeepAliveProvider>
88
+
<AppToaster />
89
+
<KeepAliveOutlet />
90
+
</KeepAliveProvider>
85
91
</RootDocument>
86
-
</PersistentStoreProvider>
92
+
</LikeMutationQueueProvider>
87
93
</UnifiedAuthProvider>
88
94
);
89
95
}
90
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
+
91
201
function RootDocument({ children }: { children: React.ReactNode }) {
202
+
useAtomCssVar(hueAtom, "--tw-gray-hue");
92
203
const location = useLocation();
93
204
const navigate = useNavigate();
94
205
const { agent } = useAuth();
95
206
const authed = !!agent?.did;
96
207
const isHome = location.pathname === "/";
97
208
const isNotifications = location.pathname.startsWith("/notifications");
98
-
const isProfile = agent && ((location.pathname === (`/profile/${agent?.did}`)) || (location.pathname === (`/profile/${encodeURIComponent(agent?.did??"")}`)));
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");
99
217
100
-
const [postOpen, setPostOpen] = useState(false);
101
-
const [postText, setPostText] = useState("");
102
-
const [posting, setPosting] = useState(false);
103
-
const [postSuccess, setPostSuccess] = useState(false);
104
-
const [postError, setPostError] = useState<string | null>(null);
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";
105
238
106
-
async function handlePost() {
107
-
if (!agent) return;
108
-
setPosting(true);
109
-
setPostError(null);
110
-
try {
111
-
await agent.com.atproto.repo.createRecord({
112
-
collection: "app.bsky.feed.post",
113
-
repo: agent.assertDid,
114
-
record: {
115
-
$type: "app.bsky.feed.post",
116
-
text: postText,
117
-
createdAt: new Date().toISOString(),
118
-
},
119
-
});
120
-
setPostSuccess(true);
121
-
setPostText("");
122
-
setTimeout(() => {
123
-
setPostSuccess(false);
124
-
setPostOpen(false);
125
-
}, 1500);
126
-
} catch (e: any) {
127
-
setPostError(e?.message || "Failed to post");
128
-
} finally {
129
-
setPosting(false);
130
-
}
131
-
}
239
+
const [, setComposerPost] = useAtom(composerAtom);
132
240
133
241
return (
134
242
<>
135
-
{postOpen && (
136
-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
137
-
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-lg p-6 w-full max-w-md relative">
138
-
<button
139
-
className="absolute top-2 right-2 text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
140
-
onClick={() => !posting && setPostOpen(false)}
141
-
disabled={posting}
142
-
aria-label="Close"
143
-
>
144
-
ร
145
-
</button>
146
-
<h2 className="text-lg font-bold mb-2">Create Post</h2>
147
-
{postSuccess ? (
148
-
<div className="flex flex-col items-center justify-center py-8">
149
-
<span className="text-green-500 text-4xl mb-2">โ</span>
150
-
<span className="text-green-600">Posted!</span>
151
-
</div>
152
-
) : (
153
-
<>
154
-
<textarea
155
-
className="w-full border rounded p-2 mb-2 dark:bg-gray-800 dark:border-gray-700"
156
-
rows={4}
157
-
placeholder="What's on your mind?"
158
-
value={postText}
159
-
onChange={(e) => setPostText(e.target.value)}
160
-
disabled={posting}
161
-
autoFocus
162
-
/>
163
-
{postError && (
164
-
<div className="text-red-500 text-sm mb-2">{postError}</div>
165
-
)}
166
-
<button
167
-
className="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded disabled:opacity-50"
168
-
onClick={handlePost}
169
-
disabled={posting || !postText.trim()}
170
-
>
171
-
{posting ? "Posting..." : "Post"}
172
-
</button>
173
-
</>
174
-
)}
175
-
</div>
176
-
</div>
177
-
)}
243
+
<Composer />
178
244
179
245
<div className="min-h-screen flex justify-center bg-gray-50 dark:bg-gray-950">
180
-
<nav className="hidden lg:flex h-screen w-[250px] flex-col gap-2 p-4 dark:border-gray-800 sticky top-0 self-start">
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">
181
247
<div className="flex items-center gap-3 mb-4">
182
-
<img src="/redstar.png" alt="Red Dwarf Logo" className="w-8 h-8" />
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
+
/>
183
255
<span className="font-extrabold text-2xl tracking-tight text-gray-900 dark:text-gray-100">
184
256
Red Dwarf{" "}
185
257
{/* <span className="text-gray-500 dark:text-gray-400 text-sm">
···
187
259
</span> */}
188
260
</span>
189
261
</div>
190
-
<Link
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
191
372
to="/"
192
373
className={
193
374
`py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-900 text-xl flex items-center gap-3 ` +
194
375
(isHome ? "font-bold" : "")
195
376
}
196
377
>
197
-
{isHome ? (
198
-
<TablerHomeFilled width={28} height={28} />
378
+
{!isHome ? (
379
+
<IconMaterialSymbolsHomeOutline width={28} height={28} />
199
380
) : (
200
-
<TablerHome width={28} height={28} />
381
+
<IconMaterialSymbolsHome width={28} height={28} />
201
382
)}
202
383
<span>Home</span>
203
384
</Link>
···
208
389
(isNotifications ? "font-bold" : "")
209
390
}
210
391
>
211
-
{isNotifications ? (
212
-
<TablerBellFilled width={28} height={28} />
392
+
{!isNotifications ? (
393
+
<IconMaterialSymbolsNotificationsOutline width={28} height={28} />
213
394
) : (
214
-
<TablerBell width={28} height={28} />
395
+
<IconMaterialSymbolsNotifications width={28} height={28} />
215
396
)}
216
397
<span>Notifications</span>
217
398
</Link>
···
222
403
}`}
223
404
>
224
405
{location.pathname.startsWith("/feeds") ? (
225
-
<TablerHashtagFilled width={28} height={28} />
406
+
<IconMaterialSymbolsTag width={28} height={28} />
226
407
) : (
227
-
<TablerHashtag width={28} height={28} />
408
+
<IconMaterialSymbolsTag width={28} height={28} />
228
409
)}
229
410
<span>Feeds</span>
230
411
</Link>
···
236
417
}`}
237
418
>
238
419
{location.pathname.startsWith("/search") ? (
239
-
<TablerSearchFilled width={28} height={28} />
420
+
<IconMaterialSymbolsSearch width={28} height={28} />
240
421
) : (
241
-
<TablerSearch width={28} height={28} />
422
+
<IconMaterialSymbolsSearch width={28} height={28} />
242
423
)}
243
424
<span>Search</span>
244
425
</Link>
···
252
433
navigate({
253
434
to: "/profile/$did",
254
435
params: { did: agent.assertDid },
255
-
})
436
+
});
256
437
}
257
438
}}
258
439
type="button"
259
440
>
260
-
<TablerUserCircle width={28} height={28} />
441
+
{!isProfile ? (
442
+
<IconMaterialSymbolsAccountCircleOutline width={28} height={28} />
443
+
) : (
444
+
<IconMaterialSymbolsAccountCircle width={28} height={28} />
445
+
)}
261
446
<span>Profile</span>
262
447
</button>
263
448
<Link
···
266
451
location.pathname.startsWith("/settings") ? "font-bold" : ""
267
452
}`}
268
453
>
269
-
{location.pathname.startsWith("/settings") ? (
270
-
<IonSettingsSharp width={28} height={28} />
454
+
{!location.pathname.startsWith("/settings") ? (
455
+
<IconMaterialSymbolsSettingsOutline width={28} height={28} />
271
456
) : (
272
-
<IonSettings width={28} height={28} />
457
+
<IconMaterialSymbolsSettings width={28} height={28} />
273
458
)}
274
459
<span>Settings</span>
275
-
</Link>
276
-
<button
460
+
</Link> */}
461
+
{/* <button
277
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"
278
463
onClick={() => setPostOpen(true)}
279
464
type="button"
280
465
>
281
-
<TablerEdit
466
+
<IconMdiPencilOutline
282
467
width={24}
283
468
height={24}
284
469
className="text-gray-600 dark:text-gray-400"
285
470
/>
286
471
<span>Post</span>
287
-
</button>
472
+
</button> */}
288
473
<div className="flex-1"></div>
289
474
<a
290
475
href="https://tangled.sh/@whey.party/red-dwarf"
···
315
500
</div>
316
501
</nav>
317
502
318
-
<button
319
-
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"
320
-
style={{ boxShadow: "0 4px 24px 0 rgba(0,0,0,0.12)" }}
321
-
onClick={() => setPostOpen(true)}
322
-
type="button"
323
-
aria-label="Create Post"
324
-
>
325
-
<TablerEdit
326
-
width={24}
327
-
height={24}
328
-
className="text-gray-600 dark:text-gray-400"
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"
329
527
/>
330
-
</button>
331
528
332
-
<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">
333
-
<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">
334
-
<div className="flex items-center gap-2">
335
-
<img
336
-
src="/redstar.png"
337
-
alt="Red Dwarf Logo"
338
-
className="w-6 h-6"
339
-
/>
340
-
<span className="font-bold text-lg text-gray-900 dark:text-gray-100">
341
-
Red Dwarf{" "}
342
-
{/* <span className="text-gray-500 dark:text-gray-400 text-sm">
343
-
lite
344
-
</span> */}
345
-
</span>
346
-
</div>
347
-
<div className="flex items-center gap-2">
348
-
<Login compact={true} />
349
-
</div>
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
+
/>
350
629
</div>
630
+
</nav>
351
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">
352
649
{children}
353
650
</main>
354
651
355
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>
356
656
<Login />
357
657
358
658
<div className="flex-1"></div>
359
659
<p className="text-xs text-gray-400 dark:text-gray-500 text-justify mx-4 mb-4">
360
-
Red Dwarf is a bluesky client that uses Constellation and direct PDS
361
-
queries. Skylite would be a self-hosted bluesky "instance". Stay
362
-
tuned for the release of Skylite.
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)
363
664
</p>
364
665
</aside>
365
666
</div>
366
667
367
-
<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">
368
-
<div className="flex justify-around items-center py-2">
369
-
<Link
370
-
to="/"
371
-
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
372
-
isHome
373
-
? "text-gray-900 dark:text-gray-100"
374
-
: "text-gray-600 dark:text-gray-400"
375
-
}`}
376
-
>
377
-
{isHome ? (
378
-
<TablerHomeFilled width={24} height={24} />
379
-
) : (
380
-
<TablerHome width={24} height={24} />
381
-
)}
382
-
<span className="text-xs mt-1">Home</span>
383
-
</Link>
384
-
<Link
385
-
to="/search"
386
-
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
387
-
location.pathname.startsWith("/search")
388
-
? "text-gray-900 dark:text-gray-100"
389
-
: "text-gray-600 dark:text-gray-400"
390
-
}`}
391
-
>
392
-
{location.pathname.startsWith("/search") ? (
393
-
<TablerSearchFilled width={24} height={24} />
394
-
) : (
395
-
<TablerSearch width={24} height={24} />
396
-
)}
397
-
<span className="text-xs mt-1">Search</span>
398
-
</Link>
399
-
<Link
400
-
to="/notifications"
401
-
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
402
-
isNotifications
403
-
? "text-gray-900 dark:text-gray-100"
404
-
: "text-gray-600 dark:text-gray-400"
405
-
}`}
406
-
>
407
-
{isNotifications ? (
408
-
<TablerBellFilled width={24} height={24} />
409
-
) : (
410
-
<TablerBell width={24} height={24} />
411
-
)}
412
-
<span className="text-xs mt-1">Notifications</span>
413
-
</Link>
414
-
<button
415
-
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
416
-
isProfile
417
-
? "text-gray-900 dark:text-gray-100"
418
-
: "text-gray-600 dark:text-gray-400"
419
-
}`}
420
-
onClick={() => {
421
-
if (authed && agent && agent.assertDid) {
422
-
//window.location.href = `/profile/${agent.assertDid}`;
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={() =>
423
812
navigate({
424
-
to: "/profile/$did",
425
-
params: { did: agent.assertDid },
813
+
to: "/settings",
814
+
//params: { did: agent.assertDid },
426
815
})
427
816
}
428
-
}}
429
-
type="button"
430
-
>
431
-
<TablerUserCircle width={24} height={24} />
432
-
<span className="text-xs mt-1">Profile</span>
433
-
</button>
434
-
<Link
435
-
to="/settings"
436
-
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
437
-
location.pathname.startsWith("/settings")
438
-
? "text-gray-900 dark:text-gray-100"
439
-
: "text-gray-600 dark:text-gray-400"
440
-
}`}
441
-
>
442
-
{location.pathname.startsWith("/settings") ? (
443
-
<IonSettingsSharp width={24} height={24} />
444
-
) : (
445
-
<IonSettings width={24} height={24} />
446
-
)}
447
-
<span className="text-xs mt-1">Settings</span>
448
-
</Link>
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>
449
856
</div>
450
-
</nav>
857
+
)}
451
858
452
-
<TanStackRouterDevtools position="bottom-right" />
859
+
<TanStackRouterDevtools position="bottom-left" />
453
860
<Scripts />
454
861
</>
455
862
);
456
863
}
457
-
export function TablerHashtag(props: SVGProps<SVGSVGElement>) {
458
-
return (
459
-
<svg
460
-
xmlns="http://www.w3.org/2000/svg"
461
-
width={24}
462
-
height={24}
463
-
viewBox="0 0 24 24"
464
-
{...props}
465
-
>
466
-
<path
467
-
fill="none"
468
-
stroke="currentColor"
469
-
strokeLinecap="round"
470
-
strokeLinejoin="round"
471
-
strokeWidth={2}
472
-
d="M5 9h14M5 15h14M11 4L7 20M17 4l-4 16"
473
-
></path>
474
-
</svg>
475
-
);
476
-
}
477
864
478
-
export function TablerHashtagFilled(props: SVGProps<SVGSVGElement>) {
479
-
return (
480
-
<svg
481
-
xmlns="http://www.w3.org/2000/svg"
482
-
width={24}
483
-
height={24}
484
-
viewBox="0 0 24 24"
485
-
{...props}
486
-
>
487
-
<path
488
-
fill="none"
489
-
stroke="currentColor"
490
-
strokeLinecap="round"
491
-
strokeLinejoin="round"
492
-
strokeWidth={3}
493
-
d="M5 9h14M5 15h14M11 4L7 20M17 4l-4 16"
494
-
></path>
495
-
</svg>
496
-
);
497
-
}
498
-
export function TablerEdit(props: SVGProps<SVGSVGElement>) {
499
-
return (
500
-
<svg
501
-
xmlns="http://www.w3.org/2000/svg"
502
-
width={24}
503
-
height={24}
504
-
viewBox="0 0 24 24"
505
-
className="text-white"
506
-
{...props}
507
-
>
508
-
<g
509
-
fill="none"
510
-
stroke="currentColor"
511
-
strokeLinecap="round"
512
-
strokeLinejoin="round"
513
-
strokeWidth={2}
514
-
>
515
-
<path d="M16.475 5.408a2.36 2.36 0 1 1 3.34 3.34L7.5 21H3v-4.5z"></path>
516
-
</g>
517
-
</svg>
518
-
);
519
-
}
520
-
export function TablerHome(props: SVGProps<SVGSVGElement>) {
521
-
return (
522
-
<svg
523
-
xmlns="http://www.w3.org/2000/svg"
524
-
width={24}
525
-
height={24}
526
-
viewBox="0 0 24 24"
527
-
className="text-gray-900 dark:text-gray-100 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
528
-
{...props}
529
-
>
530
-
<g
531
-
stroke="currentColor"
532
-
strokeLinecap="round"
533
-
strokeLinejoin="round"
534
-
strokeWidth={2}
535
-
fill="none"
536
-
>
537
-
<path d="M5 12H3l9-9l9 9h-2M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-7"></path>
538
-
<path d="M9 21v-6a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v6"></path>
539
-
</g>
540
-
</svg>
541
-
);
542
-
}
543
-
export function TablerHomeFilled(props: SVGProps<SVGSVGElement>) {
544
-
return (
545
-
<svg
546
-
xmlns="http://www.w3.org/2000/svg"
547
-
width={24}
548
-
height={24}
549
-
viewBox="0 0 24 24"
550
-
className="text-gray-900 dark:text-gray-100 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
551
-
{...props}
552
-
>
553
-
<path
554
-
fill="currentColor"
555
-
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"
556
-
></path>
557
-
</svg>
558
-
);
559
-
}
560
-
561
-
export function TablerBell(props: SVGProps<SVGSVGElement>) {
562
-
return (
563
-
<svg
564
-
xmlns="http://www.w3.org/2000/svg"
565
-
width={24}
566
-
height={24}
567
-
viewBox="0 0 24 24"
568
-
{...props}
569
-
>
570
-
<path
571
-
className="text-gray-900 dark:text-gray-100 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
572
-
stroke="currentColor"
573
-
strokeLinecap="round"
574
-
strokeLinejoin="round"
575
-
strokeWidth={2}
576
-
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"
577
-
></path>
578
-
</svg>
579
-
);
580
-
}
581
-
export function TablerBellFilled(props: SVGProps<SVGSVGElement>) {
582
-
return (
583
-
<svg
584
-
xmlns="http://www.w3.org/2000/svg"
585
-
width={24}
586
-
height={24}
587
-
viewBox="0 0 24 24"
588
-
className="text-gray-900 dark:text-gray-100 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
589
-
{...props}
590
-
>
591
-
<path
592
-
fill="currentColor"
593
-
stroke="currentColor"
594
-
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"
595
-
></path>
596
-
</svg>
597
-
);
598
-
}
599
-
600
-
export function TablerUserCircle(props: SVGProps<SVGSVGElement>) {
601
-
return (
602
-
<svg
603
-
xmlns="http://www.w3.org/2000/svg"
604
-
width={24}
605
-
height={24}
606
-
viewBox="0 0 24 24"
607
-
className="text-gray-900 dark:text-gray-100 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
608
-
{...props}
609
-
>
610
-
<g
611
-
fill="none"
612
-
stroke="currentColor"
613
-
strokeLinecap="round"
614
-
strokeLinejoin="round"
615
-
strokeWidth={2}
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
+
}}
616
891
>
617
-
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0-18 0"></path>
618
-
<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>
619
-
</g>
620
-
</svg>
621
-
);
622
-
}
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
+
);
623
904
624
-
export function TablerSearch(props: SVGProps<SVGSVGElement>) {
625
905
return (
626
-
<svg
627
-
xmlns="http://www.w3.org/2000/svg"
628
-
width={24}
629
-
height={24}
630
-
viewBox="0 0 24 24"
631
-
//className="text-gray-400 dark:text-gray-500"
632
-
{...props}
633
-
>
634
-
<g
635
-
fill="none"
636
-
stroke="currentColor"
637
-
strokeLinecap="round"
638
-
strokeLinejoin="round"
639
-
strokeWidth={2}
640
-
>
641
-
<path d="M3 10a7 7 0 1 0 14 0a7 7 0 1 0-14 0"></path>
642
-
<path d="m21 21l-6-6"></path>
643
-
</g>
644
-
</svg>
645
-
);
646
-
}
647
-
export function TablerSearchFilled(props: SVGProps<SVGSVGElement>) {
648
-
return (
649
-
<svg
650
-
xmlns="http://www.w3.org/2000/svg"
651
-
width={24}
652
-
height={24}
653
-
viewBox="0 0 24 24"
654
-
//className="text-gray-400 dark:text-gray-500"
655
-
{...props}
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
+
}}
656
915
>
657
-
<g
658
-
fill="none"
659
-
stroke="currentColor"
660
-
strokeLinecap="round"
661
-
strokeLinejoin="round"
662
-
strokeWidth={3}
916
+
<div className={`mr-4 ${active ? " " : " "}`}>
917
+
{active ? ActiveIcon : InactiveIcon}
918
+
</div>
919
+
<span
920
+
className={`text-[17px] text-roboto ${active ? "font-medium" : ""}`}
663
921
>
664
-
<path d="M3 10a7 7 0 1 0 14 0a7 7 0 1 0-14 0"></path>
665
-
<path d="m21 21l-6-6"></path>
666
-
</g>
667
-
</svg>
922
+
{text}
923
+
</span>
924
+
</button>
668
925
);
669
926
}
670
927
671
-
export function IonSettings(props: SVGProps<SVGSVGElement>) {
672
-
return (
673
-
<svg
674
-
xmlns="http://www.w3.org/2000/svg"
675
-
width={24}
676
-
height={24}
677
-
viewBox="0 0 512 512"
678
-
{...props}
679
-
>
680
-
<path
681
-
fill="none"
682
-
stroke="currentColor"
683
-
strokeLinecap="round"
684
-
strokeLinejoin="round"
685
-
strokeWidth={32}
686
-
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"
687
-
></path>
688
-
</svg>
689
-
);
690
-
}
691
-
export function IonSettingsSharp(props: SVGProps<SVGSVGElement>) {
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;
692
944
return (
693
-
<svg
694
-
xmlns="http://www.w3.org/2000/svg"
695
-
width={24}
696
-
height={24}
697
-
viewBox="0 0 512 512"
698
-
{...props}
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
+
}}
699
954
>
700
-
<path
701
-
fill="currentColor"
702
-
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"
703
-
></path>
704
-
</svg>
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>
705
966
);
706
967
}
+18
-1
src/routes/feeds.tsx
+18
-1
src/routes/feeds.tsx
···
1
1
import { createFileRoute } from "@tanstack/react-router";
2
2
3
+
import { Header } from "~/components/Header";
4
+
3
5
export const Route = createFileRoute("/feeds")({
4
6
component: Feeds,
5
7
});
6
8
7
9
export function Feeds() {
8
-
return <div className="p-6">Feeds page (coming soon)</div>;
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
+
);
9
26
}
+231
-220
src/routes/index.tsx
+231
-220
src/routes/index.tsx
···
1
1
import { createFileRoute } from "@tanstack/react-router";
2
-
import {
3
-
CACHE_TIMEOUT,
4
-
//cachedGetRecord,
5
-
//cachedResolveIdentity,
6
-
UniversalPostRendererATURILoader,
7
-
} from "~/components/UniversalPostRenderer";
2
+
import { useAtom } from "jotai";
8
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";
9
8
import { useAuth } from "~/providers/UnifiedAuthProvider";
9
+
import {
10
+
feedScrollPositionsAtom,
11
+
isAtTopAtom,
12
+
quickAuthAtom,
13
+
selectedFeedUriAtom,
14
+
} from "~/utils/atoms";
10
15
//import { usePersistentStore } from "~/providers/PersistentStoreProvider";
11
16
import {
17
+
//constructArbitraryQuery,
18
+
//constructIdentityQuery,
19
+
//constructInfiniteFeedSkeletonQuery,
20
+
//constructPostQuery,
21
+
useQueryArbitrary,
12
22
useQueryIdentity,
13
-
useQueryPost,
14
-
useQueryFeedSkeleton,
15
23
useQueryPreferences,
16
-
useQueryArbitrary,
17
-
constructInfiniteFeedSkeletonQuery,
18
-
constructArbitraryQuery,
19
-
constructIdentityQuery,
20
-
constructPostQuery,
21
24
} from "~/utils/useQuery";
22
-
import { InfiniteCustomFeed } from "~/components/InfiniteCustomFeed";
23
-
import { useAtom, useSetAtom } from "jotai";
24
-
import {
25
-
selectedFeedUriAtom,
26
-
store,
27
-
agentAtom,
28
-
authedAtom,
29
-
feedScrollPositionsAtom,
30
-
} from "~/utils/atoms";
31
-
import { useEffect, useLayoutEffect } from "react";
32
25
33
26
export const Route = createFileRoute("/")({
34
-
loader: async ({ context }) => {
35
-
const { queryClient } = context;
36
-
const atomauth = store.get(authedAtom);
37
-
const atomagent = store.get(agentAtom);
27
+
// loader: async ({ context }) => {
28
+
// const { queryClient } = context;
29
+
// const atomauth = store.get(authedAtom);
30
+
// const atomagent = store.get(agentAtom);
38
31
39
-
let identitypds: string | undefined;
40
-
const initialselectedfeed = store.get(selectedFeedUriAtom);
41
-
if (atomagent && atomauth && atomagent?.did) {
42
-
const identityopts = constructIdentityQuery(atomagent.did);
43
-
const identityresultmaybe =
44
-
await queryClient.ensureQueryData(identityopts);
45
-
identitypds = identityresultmaybe?.pds;
46
-
}
32
+
// let identitypds: string | undefined;
33
+
// const initialselectedfeed = store.get(selectedFeedUriAtom);
34
+
// if (atomagent && atomauth && atomagent?.did) {
35
+
// const identityopts = constructIdentityQuery(atomagent.did);
36
+
// const identityresultmaybe =
37
+
// await queryClient.ensureQueryData(identityopts);
38
+
// identitypds = identityresultmaybe?.pds;
39
+
// }
47
40
48
-
const arbitraryopts = constructArbitraryQuery(
49
-
initialselectedfeed ??
50
-
"at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot"
51
-
);
52
-
const feedGengetrecordquery =
53
-
await queryClient.ensureQueryData(arbitraryopts);
54
-
const feedServiceDid = (feedGengetrecordquery?.value as any)?.did;
55
-
//queryClient.ensureInfiniteQueryData()
41
+
// const arbitraryopts = constructArbitraryQuery(
42
+
// initialselectedfeed ??
43
+
// "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot"
44
+
// );
45
+
// const feedGengetrecordquery =
46
+
// await queryClient.ensureQueryData(arbitraryopts);
47
+
// const feedServiceDid = (feedGengetrecordquery?.value as any)?.did;
48
+
// //queryClient.ensureInfiniteQueryData()
56
49
57
-
const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery({
58
-
feedUri:
59
-
initialselectedfeed ??
60
-
"at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot",
61
-
agent: atomagent ?? undefined,
62
-
isAuthed: atomauth ?? false,
63
-
pdsUrl: identitypds,
64
-
feedServiceDid: feedServiceDid,
65
-
});
50
+
// const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery({
51
+
// feedUri:
52
+
// initialselectedfeed ??
53
+
// "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot",
54
+
// agent: atomagent ?? undefined,
55
+
// isAuthed: atomauth ?? false,
56
+
// pdsUrl: identitypds,
57
+
// feedServiceDid: feedServiceDid,
58
+
// });
66
59
67
-
const res = await queryClient.ensureInfiniteQueryData({
68
-
queryKey,
69
-
queryFn,
70
-
initialPageParam: undefined as never,
71
-
getNextPageParam: (lastPage: any) => lastPage.cursor as null | undefined,
72
-
staleTime: Infinity,
73
-
//refetchOnWindowFocus: false,
74
-
//enabled: true,
75
-
});
76
-
await Promise.all(
77
-
res.pages.map(async (page) => {
78
-
await Promise.all(
79
-
page.feed.map(async (feedviewpost) => {
80
-
if (!feedviewpost.post) return;
81
-
// /*mass comment*/ console.log("preloading: ", feedviewpost.post);
82
-
const opts = constructPostQuery(feedviewpost.post);
83
-
try {
84
-
await queryClient.ensureQueryData(opts);
85
-
} catch (e) {
86
-
// /*mass comment*/ console.log(" failed:", e);
87
-
}
88
-
})
89
-
);
90
-
})
91
-
);
92
-
},
60
+
// const res = await queryClient.ensureInfiniteQueryData({
61
+
// queryKey,
62
+
// queryFn,
63
+
// initialPageParam: undefined as never,
64
+
// getNextPageParam: (lastPage: any) => lastPage.cursor as null | undefined,
65
+
// staleTime: Infinity,
66
+
// //refetchOnWindowFocus: false,
67
+
// //enabled: true,
68
+
// });
69
+
// await Promise.all(
70
+
// res.pages.map(async (page) => {
71
+
// await Promise.all(
72
+
// page.feed.map(async (feedviewpost) => {
73
+
// if (!feedviewpost.post) return;
74
+
// // /*mass comment*/ console.log("preloading: ", feedviewpost.post);
75
+
// const opts = constructPostQuery(feedviewpost.post);
76
+
// try {
77
+
// await queryClient.ensureQueryData(opts);
78
+
// } catch (e) {
79
+
// // /*mass comment*/ console.log(" failed:", e);
80
+
// }
81
+
// })
82
+
// );
83
+
// })
84
+
// );
85
+
// },
93
86
component: Home,
94
-
pendingComponent: PendingHome,
87
+
pendingComponent: PendingHome, // PendingHome,
88
+
staticData: { keepAlive: true },
95
89
});
96
90
function PendingHome() {
97
91
return <div>loading... (prefetching your timeline)</div>;
98
92
}
99
-
function Home() {
93
+
94
+
//function Homer() {
95
+
// return <div></div>
96
+
//}
97
+
export function Home({ hidden = false }: { hidden?: boolean }) {
100
98
const {
101
99
agent,
102
100
status,
···
107
105
} = useAuth();
108
106
const authed = !!agent?.did;
109
107
110
-
useEffect(() => {
111
-
if (agent?.did) {
112
-
store.set(authedAtom, true);
113
-
} else {
114
-
store.set(authedAtom, false);
115
-
}
116
-
}, [status, agent, authed]);
117
-
useEffect(() => {
118
-
if (agent) {
119
-
// 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]);
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
125
126
126
//const { get, set } = usePersistentStore();
127
127
// const [feed, setFeed] = React.useState<any[]>([]);
···
161
161
162
162
// const savedFeeds = savedFeedsPref?.items || [];
163
163
164
-
const identityresultmaybe = useQueryIdentity(agent?.did);
164
+
const [quickAuth, setQuickAuth] = useAtom(quickAuthAtom);
165
+
const isAuthRestoring = quickAuth ? status === "loading" : false;
166
+
167
+
const identityresultmaybe = useQueryIdentity(!isAuthRestoring ? agent?.did : undefined);
165
168
const identity = identityresultmaybe?.data;
166
169
167
170
const prefsresultmaybe = useQueryPreferences({
168
-
agent: agent ?? undefined,
169
-
pdsUrl: identity?.pds,
171
+
agent: !isAuthRestoring ? (agent ?? undefined) : undefined,
172
+
pdsUrl: !isAuthRestoring ? (identity?.pds) : undefined,
170
173
});
171
174
const prefs = prefsresultmaybe?.data;
172
175
···
177
180
return savedFeedsPref?.items || [];
178
181
}, [prefs]);
179
182
180
-
const [persistentSelectedFeed, setPersistentSelectedFeed] =
181
-
useAtom(selectedFeedUriAtom); // React.useState<string | null>(null);
182
-
const [unauthedSelectedFeed, setUnauthedSelectedFeed] = React.useState(
183
-
persistentSelectedFeed
184
-
); // React.useState<string | null>(null);
183
+
const [persistentSelectedFeed, setPersistentSelectedFeed] = useAtom(selectedFeedUriAtom);
184
+
const [unauthedSelectedFeed, setUnauthedSelectedFeed] = useState(persistentSelectedFeed);
185
185
const selectedFeed = agent?.did
186
186
? persistentSelectedFeed
187
187
: unauthedSelectedFeed;
···
298
298
feedScrollPositionsAtom
299
299
);
300
300
301
-
const scrollRef = React.useRef<Record<string, number>>({});
301
+
const scrollPositionsRef = React.useRef(scrollPositions);
302
302
303
-
useEffect(() => {
304
-
const onScroll = () => {
305
-
//if (!selectedFeed) return;
306
-
scrollRef.current[selectedFeed ?? "null"] = window.scrollY;
307
-
};
308
-
window.addEventListener("scroll", onScroll, { passive: true });
309
-
return () => window.removeEventListener("scroll", onScroll);
310
-
}, [selectedFeed]);
311
-
const [donerestored, setdonerestored] = React.useState(false);
303
+
React.useEffect(() => {
304
+
scrollPositionsRef.current = scrollPositions;
305
+
}, [scrollPositions]);
312
306
313
-
useEffect(() => {
314
-
return () => {
315
-
if (!donerestored) return;
316
-
// /*mass comment*/ console.log("FEEDSCROLLSHIT saving at uhhh: ", scrollRef.current);
317
-
//if (!selectedFeed) return;
318
-
setScrollPositions((prev) => ({
319
-
...prev,
320
-
[selectedFeed ?? "null"]:
321
-
scrollRef.current[selectedFeed ?? "null"] ?? 0,
322
-
}));
323
-
};
324
-
}, [selectedFeed, setScrollPositions, donerestored]);
307
+
useLayoutEffect(() => {
308
+
if (isAuthRestoring) return;
309
+
const savedPosition = scrollPositions[selectedFeed ?? "null"] ?? 0;
325
310
326
-
const [restoringScrollPosition, setRestoringScrollPosition] =
327
-
React.useState(false);
311
+
window.scrollTo({ top: savedPosition, behavior: "instant" });
312
+
// eslint-disable-next-line react-hooks/exhaustive-deps
313
+
}, [selectedFeed, isAuthRestoring]);
328
314
329
315
useLayoutEffect(() => {
330
-
setRestoringScrollPosition(true);
331
-
const savedPosition = scrollPositions[selectedFeed ?? "null"] ?? 0;
316
+
if (!selectedFeed || isAuthRestoring) return;
332
317
333
-
let raf = requestAnimationFrame(() => {
334
-
// setRestoringScrollPosition(true);
335
-
// raf = requestAnimationFrame(() => {
336
-
// window.scrollTo({ top: savedPosition, behavior: "instant" });
337
-
// setRestoringScrollPosition(false);
338
-
// setdonerestored(true);
339
-
// });
340
-
window.scrollTo({ top: savedPosition, behavior: "instant" });
341
-
setRestoringScrollPosition(false);
342
-
setdonerestored(true);
343
-
});
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);
344
328
345
-
return () => cancelAnimationFrame(raf);
346
-
}, [selectedFeed, scrollPositions]);
329
+
setScrollPositions(scrollPositionsRef.current);
330
+
};
331
+
}, [isAuthRestoring, selectedFeed, setScrollPositions]);
347
332
348
-
const feedGengetrecordquery = useQueryArbitrary(selectedFeed ?? undefined);
349
-
const feedServiceDid = (feedGengetrecordquery?.data?.value as any)?.did;
333
+
const feedGengetrecordquery = useQueryArbitrary(!isAuthRestoring ? selectedFeed ?? undefined : undefined);
334
+
const feedServiceDid = !isAuthRestoring ? (feedGengetrecordquery?.data?.value as any)?.did as string | undefined : undefined;
350
335
351
336
// const {
352
337
// data: feedData,
···
362
347
363
348
// const feed = feedData?.feed || [];
364
349
365
-
const isReadyForAuthedFeed =
366
-
authed && agent && identity?.pds && feedServiceDid;
367
-
const isReadyForUnauthedFeed = !authed && selectedFeed;
350
+
const isReadyForAuthedFeed = !isAuthRestoring && authed && agent && identity?.pds && feedServiceDid;
351
+
const isReadyForUnauthedFeed = !isAuthRestoring && !authed && selectedFeed;
352
+
353
+
354
+
const [isAtTop] = useAtom(isAtTopAtom);
368
355
369
356
return (
370
-
<div className="relative flex flex-col divide-y divide-gray-200 dark:divide-gray-800">
371
-
<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">
372
-
{savedFeeds.length > 0 ? (
373
-
savedFeeds.map((item: any, idx: number) => {
374
-
const label = item.value.split("/").pop() || item.value;
375
-
const isActive = selectedFeed === item.value;
376
-
return (
377
-
<button
378
-
key={item.value || idx}
379
-
className={`px-3 py-1 rounded-full whitespace-nowrap font-medium transition-colors ${
380
-
isActive
381
-
? "bg-gray-500 text-white"
382
-
: item.pinned
383
-
? "bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-200"
384
-
: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-200"
385
-
}`}
386
-
onClick={() => setSelectedFeed(item.value)}
387
-
title={item.value}
388
-
>
389
-
{label}
390
-
{item.pinned && (
391
-
<span className="ml-1 text-xs text-gray-700 dark:text-gray-200">
392
-
โ
393
-
</span>
394
-
)}
395
-
</button>
396
-
);
397
-
})
398
-
) : (
399
-
<span className="text-xl font-bold ml-2">Home</span>
400
-
)}
401
-
</div>
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
+
)}
402
368
{/* {isFeedLoading && <div className="p-4 text-gray-500">Loading...</div>}
403
369
{feedError && <div className="p-4 text-red-500">{feedError.message}</div>}
404
370
{!isFeedLoading && !feedError && feed.length === 0 && (
···
411
377
/>
412
378
))} */}
413
379
414
-
{authed && (!identity?.pds || !feedServiceDid) && (
380
+
{isAuthRestoring || authed && (!identity?.pds || !feedServiceDid) && (
415
381
<div className="p-4 text-center text-gray-500">
416
382
Preparing your feed...
417
383
</div>
418
384
)}
419
385
420
-
{isReadyForAuthedFeed || isReadyForUnauthedFeed ? (
386
+
{!isAuthRestoring && (isReadyForAuthedFeed || isReadyForUnauthedFeed) ? (
421
387
<InfiniteCustomFeed
388
+
key={selectedFeed!}
422
389
feedUri={selectedFeed!}
423
390
pdsUrl={identity?.pds}
424
391
feedServiceDid={feedServiceDid}
425
392
/>
426
393
) : (
427
394
<div className="p-4 text-center text-gray-500">
428
-
Select a feed to get started.
395
+
Loading.......
429
396
</div>
430
397
)}
431
-
{false && restoringScrollPosition && (
398
+
{/* {false && restoringScrollPosition && (
432
399
<div className="fixed top-1/2 left-1/2 right-1/2">
433
400
restoringScrollPosition
434
401
</div>
435
-
)}
402
+
)} */}
436
403
</div>
437
404
);
438
405
}
439
406
440
-
export async function cachedResolveDIDWEBDOC({
441
-
didweb,
442
-
cacheTimeout = CACHE_TIMEOUT,
443
-
get,
444
-
set,
445
-
}: {
446
-
didweb: string;
447
-
cacheTimeout?: number;
448
-
get: (key: string) => any;
449
-
set: (key: string, value: string) => void;
450
-
}): Promise<any> {
451
-
const isDidInput = didweb.startsWith("did:web:");
452
-
const cacheKey = `didwebdoc:${didweb}`;
453
-
const now = Date.now();
454
-
const cached = get(cacheKey);
455
-
if (
456
-
cached &&
457
-
cached.value &&
458
-
cached.time &&
459
-
now - cached.time < cacheTimeout
460
-
) {
461
-
try {
462
-
return JSON.parse(cached.value);
463
-
} catch {}
464
-
}
465
-
const url = `https://free-fly-24.deno.dev/resolve-did-web?did=${encodeURIComponent(
466
-
didweb
467
-
)}`;
468
-
const res = await fetch(url);
469
-
if (!res.ok) throw new Error("Failed to resolve didwebdoc");
470
-
const data = await res.json();
471
-
set(cacheKey, JSON.stringify(data));
472
-
if (!isDidInput && data.did) {
473
-
set(`didwebdoc:${data.did}`, JSON.stringify(data));
474
-
}
475
-
return data;
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
+
);
476
447
}
448
+
449
+
// not even used lmaooo
450
+
451
+
// export async function cachedResolveDIDWEBDOC({
452
+
// didweb,
453
+
// cacheTimeout = CACHE_TIMEOUT,
454
+
// get,
455
+
// set,
456
+
// }: {
457
+
// didweb: string;
458
+
// cacheTimeout?: number;
459
+
// get: (key: string) => any;
460
+
// set: (key: string, value: string) => void;
461
+
// }): Promise<any> {
462
+
// const isDidInput = didweb.startsWith("did:web:");
463
+
// const cacheKey = `didwebdoc:${didweb}`;
464
+
// const now = Date.now();
465
+
// const cached = get(cacheKey);
466
+
// if (
467
+
// cached &&
468
+
// cached.value &&
469
+
// cached.time &&
470
+
// now - cached.time < cacheTimeout
471
+
// ) {
472
+
// try {
473
+
// return JSON.parse(cached.value);
474
+
// } catch (_e) {/* whatever*/ }
475
+
// }
476
+
// const url = `https://free-fly-24.deno.dev/resolve-did-web?did=${encodeURIComponent(
477
+
// didweb
478
+
// )}`;
479
+
// const res = await fetch(url);
480
+
// if (!res.ok) throw new Error("Failed to resolve didwebdoc");
481
+
// const data = await res.json();
482
+
// set(cacheKey, JSON.stringify(data));
483
+
// if (!isDidInput && data.did) {
484
+
// set(`didwebdoc:${data.did}`, JSON.stringify(data));
485
+
// }
486
+
// return data;
487
+
// }
477
488
478
489
// export async function cachedGetPrefs({
479
490
// did,
+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
+
}
+646
-153
src/routes/notifications.tsx
+646
-153
src/routes/notifications.tsx
···
1
-
import { createFileRoute } from "@tanstack/react-router";
2
-
import React, { useEffect, useState, useRef } from "react";
3
-
import { useAuth } from "~/providers/PassAuthProvider";
4
-
import { usePersistentStore } from "~/providers/PersistentStoreProvider";
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";
5
6
6
-
const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour
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
+
}
7
54
8
55
export const Route = createFileRoute("/notifications")({
9
56
component: NotificationsComponent,
10
57
});
11
58
12
-
function NotificationsComponent() {
13
-
// /*mass comment*/ console.log("NotificationsComponent render");
14
-
const { agent, authed, loading: authLoading } = useAuth();
15
-
const { get, set } = usePersistentStore();
16
-
const [did, setDid] = useState<string | null>(null);
17
-
const [resolving, setResolving] = useState(false);
18
-
const [error, setError] = useState<string | null>(null);
19
-
const [responses, setResponses] = useState<any[]>([null, null, null]);
20
-
const [loading, setLoading] = useState(false);
21
-
const inputRef = useRef<HTMLInputElement>(null);
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
+
}
22
75
23
-
useEffect(() => {
24
-
if (authLoading) return;
25
-
if (authed && agent && agent.assertDid) {
26
-
setDid(agent.assertDid);
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
+
});
27
299
}
28
-
}, [authed, agent, authLoading]);
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
+
}
29
418
30
-
async function handleSubmit() {
31
-
// /*mass comment*/ console.log("handleSubmit called");
32
-
setError(null);
33
-
setResponses([null, null, null]);
34
-
const value = inputRef.current?.value?.trim() || "";
35
-
if (!value) return;
36
-
if (value.startsWith("did:")) {
37
-
setDid(value);
38
-
setError(null);
39
-
return;
40
-
}
41
-
setResolving(true);
42
-
const cacheKey = `handleDid:${value}`;
43
-
const now = Date.now();
44
-
const cached = await get(cacheKey);
45
-
if (
46
-
cached &&
47
-
cached.value &&
48
-
cached.time &&
49
-
now - cached.time < HANDLE_DID_CACHE_TIMEOUT
50
-
) {
51
-
try {
52
-
const data = JSON.parse(cached.value);
53
-
setDid(data.did);
54
-
setResolving(false);
55
-
return;
56
-
} catch {}
57
-
}
58
-
try {
59
-
const url = `https://free-fly-24.deno.dev/?handle=${encodeURIComponent(value)}`;
60
-
const res = await fetch(url);
61
-
if (!res.ok) throw new Error("Failed to resolve handle");
62
-
const data = await res.json();
63
-
set(cacheKey, JSON.stringify(data));
64
-
setDid(data.did);
65
-
} catch (e: any) {
66
-
setError("Failed to resolve handle: " + (e?.message || e));
67
-
} finally {
68
-
setResolving(false);
69
-
}
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`;
70
593
}
71
594
72
-
useEffect(() => {
73
-
if (!did) return;
74
-
setLoading(true);
75
-
setError(null);
76
-
const urls = [
77
-
`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`,
78
-
`https://constellation.microcosm.blue/links?target=${encodeURIComponent(did)}&collection=app.bsky.feed.post&path=.facets[].features[app.bsky.richtext.facet%23mention].did`,
79
-
`https://constellation.microcosm.blue/links?target=${encodeURIComponent(did)}&collection=app.bsky.graph.follow&path=.subject`,
80
-
];
81
-
let ignore = false;
82
-
Promise.all(
83
-
urls.map(async (url) => {
84
-
try {
85
-
const r = await fetch(url);
86
-
if (!r.ok) throw new Error("Failed to fetch");
87
-
const text = await r.text();
88
-
if (!text) return null;
89
-
try {
90
-
return JSON.parse(text);
91
-
} catch {
92
-
return null;
93
-
}
94
-
} catch (e: any) {
95
-
return { error: e?.message || String(e) };
96
-
}
97
-
}),
98
-
)
99
-
.then((results) => {
100
-
if (!ignore) setResponses(results);
101
-
})
102
-
.catch((e) => {
103
-
if (!ignore)
104
-
setError("Failed to fetch notifications: " + (e?.message || e));
105
-
})
106
-
.finally(() => {
107
-
if (!ignore) setLoading(false);
108
-
});
109
-
return () => {
110
-
ignore = true;
111
-
};
112
-
}, [did]);
595
+
const avatar = getAvatarUrl(profile);
113
596
114
597
return (
115
-
<div className="flex flex-col divide-y divide-gray-200 dark:divide-gray-800">
116
-
<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">
117
-
<span className="text-xl font-bold ml-2">Notifications</span>
118
-
{!authed && (
119
-
<div className="flex items-center gap-2">
120
-
<input
121
-
type="text"
122
-
placeholder="Enter handle or DID"
123
-
ref={inputRef}
124
-
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"
125
-
style={{ minWidth: 220 }}
126
-
disabled={resolving}
127
-
/>
128
-
<button
129
-
type="button"
130
-
className="px-3 py-1 rounded bg-blue-600 text-white font-semibold disabled:opacity-50"
131
-
disabled={resolving}
132
-
onClick={handleSubmit}
133
-
>
134
-
{resolving ? "Resolving..." : "Submit"}
135
-
</button>
136
-
</div>
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
+
<></>
137
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>
138
641
</div>
139
-
{error && <div className="p-4 text-red-500">{error}</div>}
140
-
{loading && (
141
-
<div className="p-4 text-gray-500">Loading notifications...</div>
142
-
)}
143
-
{!loading &&
144
-
!error &&
145
-
responses.map((resp, i) => (
146
-
<div key={i} className="p-4">
147
-
<div className="font-bold mb-2">Query {i + 1}</div>
148
-
{!resp ||
149
-
(typeof resp === "object" && Object.keys(resp).length === 0) ||
150
-
(Array.isArray(resp) && resp.length === 0) ? (
151
-
<div className="text-gray-500">No notifications found.</div>
152
-
) : (
153
-
<pre
154
-
style={{
155
-
background: "#222",
156
-
color: "#eee",
157
-
borderRadius: 8,
158
-
padding: 12,
159
-
fontSize: 13,
160
-
overflowX: "auto",
161
-
}}
162
-
>
163
-
{JSON.stringify(resp, null, 2)}
164
-
</pre>
165
-
)}
166
-
</div>
167
-
))}
168
-
{/* <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> */}
642
+
<div className="flex-1" />
643
+
{identity?.did && <FollowButton targetdidorhandle={identity?.did} />}
169
644
</div>
170
645
);
171
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 { Agent, RichText } from "@atproto/api";
2
+
import * as ATPAPI from "@atproto/api";
3
+
import { TID } from "@atproto/common-web";
1
4
import { useQueryClient } from "@tanstack/react-query";
2
-
import { createFileRoute, Link } from "@tanstack/react-router";
3
-
import React from "react";
5
+
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
6
+
import { useAtom } from "jotai";
7
+
import React, { type ReactNode, useEffect, useState } from "react";
4
8
5
-
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
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";
6
19
import { useAuth } from "~/providers/UnifiedAuthProvider";
7
-
import { toggleFollow, useGetFollowState } from "~/utils/followState";
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";
8
27
import {
9
28
useInfiniteQueryAuthorFeed,
29
+
useQueryArbitrary,
30
+
useQueryConstellation,
31
+
useQueryConstellationLinksCountDistinctDids,
10
32
useQueryIdentity,
11
33
useQueryProfile,
12
34
} from "~/utils/useQuery";
35
+
import IconMdiShieldOutline from "~icons/mdi/shield-outline.jsx";
36
+
37
+
import { renderSnack } from "../__root";
38
+
import { Chip } from "../notifications";
13
39
14
40
export const Route = createFileRoute("/profile/$did/")({
15
41
component: ProfileComponent,
···
18
44
function ProfileComponent() {
19
45
// booo bad this is not always the did it might be a handle, use identity.did instead
20
46
const { did } = Route.useParams();
21
-
const queryClient = useQueryClient();
22
47
const { agent } = useAuth();
48
+
const navigate = useNavigate();
49
+
const queryClient = useQueryClient();
23
50
const {
24
51
data: identity,
25
52
isLoading: isIdentityLoading,
26
53
error: identityError,
27
54
} = useQueryIdentity(did);
28
55
29
-
const followRecords = useGetFollowState({
30
-
target: identity?.did || did,
31
-
user: agent?.did,
32
-
});
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;
33
69
34
70
const resolvedDid = did.startsWith("did:") ? did : identity?.did;
35
71
const resolvedHandle = did.startsWith("did:") ? identity?.handle : did;
···
41
77
const { data: profileRecord } = useQueryProfile(profileUri);
42
78
const profile = profileRecord?.value;
43
79
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
-
);
80
+
const [imgcdn] = useAtom(imgCDNAtom);
68
81
69
82
function getAvatarUrl(p: typeof profile) {
70
83
const link = p?.avatar?.ref?.["$link"];
71
84
if (!link || !resolvedDid) return null;
72
-
return `https://cdn.bsky.app/img/avatar/plain/${resolvedDid}/${link}@jpeg`;
85
+
return `https://${imgcdn}/img/avatar/plain/${resolvedDid}/${link}@jpeg`;
73
86
}
74
87
function getBannerUrl(p: typeof profile) {
75
88
const link = p?.banner?.ref?.["$link"];
76
89
if (!link || !resolvedDid) return null;
77
-
return `https://cdn.bsky.app/img/banner/plain/${resolvedDid}/${link}@jpeg`;
90
+
return `https://${imgcdn}/img/banner/plain/${resolvedDid}/${link}@jpeg`;
78
91
}
79
92
80
93
const displayName =
···
82
95
const handle = resolvedHandle ? `@${resolvedHandle}` : resolvedDid || did;
83
96
const description = profile?.description || "";
84
97
85
-
if (isIdentityLoading) {
86
-
return (
87
-
<div className="p-4 text-center text-gray-500">Resolving profile...</div>
88
-
);
89
-
}
98
+
const isReady = !!resolvedDid && !isIdentityLoading && !!profileRecord;
90
99
91
-
if (identityError) {
92
-
return (
93
-
<div className="p-4 text-center text-red-500">
94
-
Error: {identityError.message}
95
-
</div>
96
-
);
97
-
}
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
+
);
98
110
99
-
if (!resolvedDid) {
100
-
return (
101
-
<div className="p-4 text-center text-gray-500">Profile not found.</div>
102
-
);
103
-
}
111
+
const followercount = resultwhateversure?.data?.total;
104
112
105
113
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">
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">
108
127
<Link
109
128
to=".."
110
129
className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg"
···
121
140
โ
122
141
</Link>
123
142
<span className="text-xl font-bold ml-2">Profile</span>
124
-
</div>
143
+
</div> */}
125
144
126
145
{/* Profile Header */}
127
146
<div className="w-full max-w-2xl mx-auto overflow-hidden relative bg-gray-100 dark:bg-gray-900">
···
137
156
138
157
{/* Avatar (PFP) */}
139
158
<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
-
/>
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
+
)}
145
172
</div>
146
173
147
174
<div className="absolute right-[16px] top-[170px] flex flex-row gap-2.5">
175
+
<BiteButton targetdidorhandle={did} />
148
176
{/*
149
177
todo: full follow and unfollow backfill (along with partial likes backfill,
150
178
just enough for it to be useful)
151
179
also delay the backfill to be on demand because it would be pretty intense
152
180
also save it persistently
153
181
*/}
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]">
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
+
>
192
193
... {/* todo: icon */}
193
194
</button>
194
195
</div>
···
196
197
{/* Info Card */}
197
198
<div className="mt-16 pb-2 px-4 text-gray-900 dark:text-gray-100">
198
199
<div className="font-bold text-2xl">{displayName}</div>
199
-
<div className="text-gray-500 dark:text-gray-400 text-base mb-3">
200
+
<div className="text-gray-500 dark:text-gray-400 text-base mb-3 flex flex-row gap-1">
201
+
<Mutual targetdidorhandle={did} />
200
202
{handle}
201
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>
202
218
{description && (
203
219
<div className="text-base leading-relaxed text-gray-800 dark:text-gray-300 mb-5 whitespace-pre-wrap break-words text-[15px]">
204
-
{description}
220
+
{/* {description} */}
221
+
<RichTextRenderer key={did} description={description} />
205
222
</div>
206
223
)}
207
224
</div>
208
225
</div>
209
226
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
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...
214
261
</div>
215
-
<div>
216
-
{posts.map((post) => (
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 (
217
471
<UniversalPostRendererATURILoader
218
-
key={post.uri}
219
-
atUri={post.uri}
472
+
key={repostRecord.subject.uri}
473
+
atUri={repostRecord.subject.uri}
220
474
feedviewpost={true}
475
+
repostedby={repost.uri}
221
476
/>
222
-
))}
223
-
</div>
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);
224
580
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"
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"
236
610
>
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
-
)}
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
+
))}
243
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
+
)} */}
244
642
</>
245
643
);
246
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";
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";
3
5
import React, { useLayoutEffect } from "react";
4
6
7
+
import { Header } from "~/components/Header";
5
8
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
9
+
import { constellationURLAtom, slingshotURLAtom } from "~/utils/atoms";
6
10
//import { usePersistentStore } from '~/providers/PersistentStoreProvider';
7
11
import {
8
12
constructPostQuery,
13
+
type linksAllResponse,
14
+
type linksRecordsResponse,
9
15
useQueryConstellation,
10
16
useQueryIdentity,
11
17
useQueryPost,
18
+
yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks,
12
19
} from "~/utils/useQuery";
20
+
21
+
import type { LightboxProps } from "./post.$rkey.image.$i";
13
22
14
23
//const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour
15
24
···
32
41
);
33
42
}
34
43
35
-
function ProfilePostComponent({ did, rkey }: { did: string; rkey: string }) {
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
+
36
58
//const { get, set } = usePersistentStore();
37
59
const queryClient = useQueryClient();
38
60
// const [resolvedDid, setResolvedDid] = React.useState<string | null>(null);
···
171
193
data: identity,
172
194
isLoading: isIdentityLoading,
173
195
error: identityError,
174
-
} = useQueryIdentity(did);
196
+
} = useQueryIdentity(showMainPostRoute ? did : undefined);
175
197
176
198
const resolvedDid = did.startsWith("did:") ? did : identity?.did;
177
199
178
200
const atUri = React.useMemo(
179
201
() =>
180
-
resolvedDid
202
+
resolvedDid && showMainPostRoute
181
203
? `at://${decodeURIComponent(resolvedDid)}/app.bsky.feed.post/${rkey}`
182
-
: "",
183
-
[resolvedDid, rkey]
204
+
: undefined,
205
+
[resolvedDid, rkey, showMainPostRoute]
184
206
);
185
207
186
-
const { data: mainPost } = useQueryPost(atUri);
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
+
);
187
219
188
-
const { data: repliesData } = useQueryConstellation({
189
-
method: "/links",
220
+
// @ts-expect-error i hate overloads
221
+
const { data: links } = useQueryConstellation(atUri&&showMainPostRoute?{
222
+
method: "/links/all",
190
223
target: atUri,
191
-
collection: "app.bsky.feed.post",
192
-
path: ".reply.parent.uri",
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,
193
296
});
194
-
const replies = repliesData?.linking_records.slice(0, 50) ?? [];
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
+
}
195
367
196
368
const [parents, setParents] = React.useState<any[]>([]);
197
369
const [parentsLoading, setParentsLoading] = React.useState(false);
198
370
199
371
const mainPostRef = React.useRef<HTMLDivElement>(null);
200
-
const userHasScrolled = React.useRef(false);
372
+
const hasPerformedInitialLayout = React.useRef(false);
201
373
202
-
const scrollAnchor = React.useRef<{ top: number } | null>(null);
374
+
const [layoutReady, setLayoutReady] = React.useState(false);
203
375
376
+
useLayoutEffect(() => {
377
+
if (!showMainPostRoute) return
378
+
if (parents.length > 0 && !layoutReady && mainPostRef.current) {
379
+
const mainPostElement = mainPostRef.current;
204
380
205
-
React.useEffect(() => {
206
-
const onScroll = () => {
381
+
if (window.scrollY === 0 && !hasPerformedInitialLayout.current) {
382
+
const elementTop = mainPostElement.getBoundingClientRect().top;
383
+
const headerOffset = 70;
207
384
208
-
if (window.scrollY > 50) {
209
-
userHasScrolled.current = true;
385
+
const targetScrollY = elementTop - headerOffset;
210
386
211
-
window.removeEventListener("scroll", onScroll);
212
-
}
213
-
};
387
+
window.scrollBy(0, targetScrollY);
214
388
215
-
if (!userHasScrolled.current) {
216
-
window.addEventListener("scroll", onScroll, { passive: true });
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);
217
395
}
218
-
return () => window.removeEventListener("scroll", onScroll);
219
-
}, []);
396
+
}, [parents, layoutReady, showMainPostRoute]);
397
+
220
398
221
-
useLayoutEffect(() => {
222
-
if (parentsLoading && mainPostRef.current && !userHasScrolled.current) {
223
-
scrollAnchor.current = {
224
-
top: mainPostRef.current.getBoundingClientRect().top,
225
-
};
399
+
const [slingshoturl] = useAtom(slingshotURLAtom)
400
+
401
+
React.useEffect(() => {
402
+
if (parentsLoading || !showMainPostRoute) {
403
+
setLayoutReady(false);
226
404
}
227
-
}, [parentsLoading]);
228
405
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;
406
+
if (!mainPost?.value?.reply?.parent?.uri && !parentsLoading) {
407
+
setLayoutReady(true);
408
+
hasPerformedInitialLayout.current = true;
241
409
}
242
-
}, [parents]);
410
+
}, [parentsLoading, mainPost, showMainPostRoute]);
243
411
244
412
React.useEffect(() => {
245
413
if (!mainPost?.value?.reply?.parent?.uri) {
···
258
426
while (currentParentUri && safetyCounter < MAX_PARENTS) {
259
427
try {
260
428
const parentPost = await queryClient.fetchQuery(
261
-
constructPostQuery(currentParentUri)
429
+
constructPostQuery(currentParentUri, slingshoturl)
262
430
);
263
431
if (!parentPost) break;
264
432
parentChain.push(parentPost);
···
280
448
return () => {
281
449
ignore = true;
282
450
};
283
-
}, [mainPost, queryClient]);
451
+
}, [mainPost, queryClient, slingshoturl]);
284
452
285
-
if (!did || !rkey) return <div>Invalid post URI</div>;
286
-
if (isIdentityLoading) return <div>Resolving handle...</div>;
287
-
if (identityError)
453
+
if ((!did || !rkey) && showMainPostRoute) return <div>Invalid post URI</div>;
454
+
if (isIdentityLoading && showMainPostRoute) return <div>Resolving handle...</div>;
455
+
if (identityError && showMainPostRoute)
288
456
return <div style={{ color: "red" }}>{identityError.message}</div>;
289
-
if (!atUri) return <div>Could not construct post URI.</div>;
457
+
if (!atUri && showMainPostRoute) return <div>Could not construct post URI.</div>;
290
458
291
459
return (
292
460
<>
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();
461
+
<Outlet />
462
+
{showMainPostRoute && (<>
463
+
<Header
464
+
title={`Post`}
465
+
backButtonCallback={() => {
299
466
if (window.history.length > 1) {
300
467
window.history.back();
301
468
} else {
302
469
window.location.assign("/");
303
470
}
304
471
}}
305
-
aria-label="Go back"
306
-
>
307
-
โ
308
-
</Link>
309
-
<span className="text-xl font-bold ml-2">Post</span>
310
-
</div>
472
+
/>
311
473
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>
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...
319
483
</div>
320
-
Loading conversation...
321
-
</div>
322
-
)}
484
+
)}
323
485
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) => (
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}>
327
499
<UniversalPostRendererATURILoader
328
-
key={parent.uri}
329
-
atUri={parent.uri}
330
-
topReplyLine={index > 0}
331
-
bottomReplyLine={true}
332
-
bottomBorder={false}
500
+
atUri={atUri!}
501
+
detailed={true}
502
+
topReplyLine={parentsLoading || parents.length > 0}
503
+
nopics={!!nopics}
504
+
lightboxCallback={lightboxCallback}
333
505
/>
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
-
>
506
+
</div>
351
507
<div
352
-
className="text-gray-500 dark:text-gray-400 text-sm font-bold"
353
508
style={{
354
-
fontSize: 18,
355
-
margin: "12px 16px 12px 16px",
356
-
fontWeight: 600,
509
+
maxWidth: 600,
510
+
//margin: "0px auto 0",
511
+
padding: 0,
512
+
minHeight: "80dvh",
513
+
paddingBottom: "20dvh",
357
514
}}
358
515
>
359
-
Replies
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>
360
548
</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>
549
+
</>)}
374
550
</>
375
551
);
376
552
}
+259
-2
src/routes/search.tsx
+259
-2
src/routes/search.tsx
···
1
-
import { createFileRoute } from "@tanstack/react-router";
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";
2
25
3
26
export const Route = createFileRoute("/search")({
4
27
component: Search,
5
28
});
6
29
7
30
export function Search() {
8
-
return <div className="p-6">Search page (coming soon)</div>;
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 <></>;
9
266
}
+353
-2
src/routes/settings.tsx
+353
-2
src/routes/settings.tsx
···
1
-
import { createFileRoute } from "@tanstack/react-router";
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";
2
27
3
28
export const Route = createFileRoute("/settings")({
4
29
component: Settings,
5
30
});
6
31
7
32
export function Settings() {
8
-
return <div className="p-6">Settings page (coming soon)</div>;
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
+
);
9
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 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');
1
2
@import "tailwindcss";
2
3
3
4
/* @theme {
···
14
15
--color-gray-950: oklch(0.129 0.050 222.000);
15
16
} */
16
17
18
+
:root {
19
+
--safe-hue: var(--tw-gray-hue, 28)
20
+
}
21
+
17
22
@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);
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));*/
29
40
}
30
41
31
42
@layer base {
···
47
58
}
48
59
}
49
60
61
+
.gutter{
62
+
scrollbar-gutter: stable both-edges;
63
+
}
64
+
50
65
@media (width >= 64rem /* 1024px */) {
51
-
html,
52
-
body {
66
+
html:not(:has(.disablegutter)),
67
+
body:not(:has(.disablegutter)) {
53
68
scrollbar-gutter: stable both-edges !important;
54
69
}
70
+
html:has(.disablescroll),
71
+
body:has(.disablescroll) {
72
+
scrollbar-width: none;
73
+
overflow-y: hidden;
74
+
}
55
75
}
76
+
77
+
.lightbox:has(+.lightbox-sidebar){
78
+
opacity: 0;
79
+
}
80
+
56
81
.scroll-thin {
57
82
scrollbar-width: thin;
58
83
/*scrollbar-gutter: stable both-edges !important;*/
···
61
86
.scroll-none {
62
87
scrollbar-width: none;
63
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
+
}
+138
-8
src/utils/atoms.ts
+138
-8
src/utils/atoms.ts
···
1
-
import type Agent from "@atproto/api";
2
-
import { atom, createStore } from "jotai";
3
-
import { atomWithStorage } from 'jotai/utils';
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";
4
6
5
7
export const store = createStore();
6
8
9
+
export const quickAuthAtom = atomWithStorage<string | null>(
10
+
"quickAuth",
11
+
null
12
+
);
13
+
7
14
export const selectedFeedUriAtom = atomWithStorage<string | null>(
8
-
'selectedFeedUri',
15
+
"selectedFeedUri",
9
16
null
10
17
);
11
18
12
19
//export const feedScrollPositionsAtom = atom<Record<string, number>>({});
13
20
14
21
export const feedScrollPositionsAtom = atomWithStorage<Record<string, number>>(
15
-
'feedscrollpositions',
22
+
"feedscrollpositions",
16
23
{}
17
24
);
18
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
+
19
59
export const likedPostsAtom = atomWithStorage<Record<string, string>>(
20
-
'likedPosts',
60
+
"likedPosts",
21
61
{}
22
62
);
23
63
24
-
export const agentAtom = atom<Agent|null>(null);
25
-
export const authedAtom = atom<boolean>(false);
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";
1
+
import { type Agent,AtUri } from "@atproto/api";
2
+
import { TID } from "@atproto/common-web";
3
3
import type { QueryClient } from "@tanstack/react-query";
4
-
import { TID } from "@atproto/common-web";
4
+
5
+
import { type linksRecordsResponse,useQueryConstellation } from "./useQuery";
5
6
6
7
export function useGetFollowState({
7
8
target,
···
127
128
};
128
129
});
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
+
}
+4
-6
src/utils/oauthClient.ts
+4
-6
src/utils/oauthClient.ts
···
1
-
// src/helpers/oauthClient.ts
2
1
import { BrowserOAuthClient, type ClientMetadata } from '@atproto/oauth-client-browser';
3
2
4
-
// This is your app's PDS for resolving handles if not provided.
5
-
// You might need to host your own or use a public one.
6
-
const handleResolverPDS = 'https://bsky.social';
3
+
import resolvers from '../../public/resolvers.json' with { type: 'json' };
4
+
const handleResolverPDS = resolvers.resolver || 'https://bsky.social';
7
5
8
-
// This assumes your client-metadata.json is in the /public folder
9
-
// and will be served at the root of your domain.
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
10
8
import clientMetadata from '../../public/client-metadata.json' with { type: 'json' };
11
9
12
10
export const oauthClient = new BrowserOAuthClient({
+68
-68
src/utils/useHydrated.ts
+68
-68
src/utils/useHydrated.ts
···
9
9
AppBskyFeedPost,
10
10
AtUri,
11
11
} from "@atproto/api";
12
-
import { useEffect, useMemo,useState } from "react";
12
+
import { useAtom } from "jotai";
13
+
import { useMemo } from "react";
13
14
14
-
import { useQueryIdentity,useQueryPost, useQueryProfile } from "./useQuery";
15
+
import { imgCDNAtom, videoCDNAtom } from "./atoms";
16
+
import { useQueryIdentity, useQueryPost, useQueryProfile } from "./useQuery";
15
17
16
-
type QueryResultData<T extends (...args: any) => any> = ReturnType<T> extends
17
-
| { data: infer D }
18
-
| undefined
19
-
? D
20
-
: never;
18
+
type QueryResultData<T extends (...args: any) => any> =
19
+
ReturnType<T> extends { data: infer D } | undefined ? D : never;
21
20
22
21
function asTyped<T extends { $type: string }>(obj: T): $Typed<T> {
23
22
return obj as $Typed<T>;
···
26
25
export function hydrateEmbedImages(
27
26
embed: AppBskyEmbedImages.Main,
28
27
did: string,
28
+
cdn: string
29
29
): $Typed<AppBskyEmbedImages.View> {
30
30
return asTyped({
31
31
$type: "app.bsky.embed.images#view" as const,
···
34
34
const link = img.image.ref?.["$link"];
35
35
if (!link) return null;
36
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`,
37
+
thumb: `https://${cdn}/img/feed_thumbnail/plain/${did}/${link}@jpeg`,
38
+
fullsize: `https://${cdn}/img/feed_fullsize/plain/${did}/${link}@jpeg`,
39
39
alt: img.alt || "",
40
40
aspectRatio: img.aspectRatio,
41
41
};
···
47
47
export function hydrateEmbedExternal(
48
48
embed: AppBskyEmbedExternal.Main,
49
49
did: string,
50
+
cdn: string
50
51
): $Typed<AppBskyEmbedExternal.View> {
51
52
return asTyped({
52
53
$type: "app.bsky.embed.external#view" as const,
···
55
56
title: embed.external.title,
56
57
description: embed.external.description,
57
58
thumb: embed.external.thumb?.ref?.$link
58
-
? `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${embed.external.thumb.ref.$link}@jpeg`
59
+
? `https://${cdn}/img/feed_thumbnail/plain/${did}/${embed.external.thumb.ref.$link}@jpeg`
59
60
: undefined,
60
61
},
61
62
});
···
64
65
export function hydrateEmbedVideo(
65
66
embed: AppBskyEmbedVideo.Main,
66
67
did: string,
68
+
videocdn: string
67
69
): $Typed<AppBskyEmbedVideo.View> {
68
70
const videoLink = embed.video.ref.$link;
69
71
return asTyped({
70
72
$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
+
playlist: `https://${videocdn}/watch/${did}/${videoLink}/playlist.m3u8`,
74
+
thumbnail: `https://${videocdn}/watch/${did}/${videoLink}/thumbnail.jpg`,
73
75
aspectRatio: embed.aspectRatio,
74
76
cid: videoLink,
75
77
});
···
80
82
quotedPost: QueryResultData<typeof useQueryPost>,
81
83
quotedProfile: QueryResultData<typeof useQueryProfile>,
82
84
quotedIdentity: QueryResultData<typeof useQueryIdentity>,
85
+
cdn: string
83
86
): $Typed<AppBskyEmbedRecord.View> | undefined {
84
87
if (!quotedPost || !quotedProfile || !quotedIdentity) {
85
88
return undefined;
···
91
94
handle: quotedIdentity.handle,
92
95
displayName: quotedProfile.value.displayName ?? quotedIdentity.handle,
93
96
avatar: quotedProfile.value.avatar?.ref?.$link
94
-
? `https://cdn.bsky.app/img/avatar/plain/${quotedIdentity.did}/${quotedProfile.value.avatar.ref.$link}@jpeg`
97
+
? `https://${cdn}/img/avatar/plain/${quotedIdentity.did}/${quotedProfile.value.avatar.ref.$link}@jpeg`
95
98
: undefined,
96
99
viewer: {},
97
100
labels: [],
···
122
125
quotedPost: QueryResultData<typeof useQueryPost>,
123
126
quotedProfile: QueryResultData<typeof useQueryProfile>,
124
127
quotedIdentity: QueryResultData<typeof useQueryIdentity>,
128
+
cdn: string
125
129
): $Typed<AppBskyEmbedRecordWithMedia.View> | undefined {
126
130
const hydratedRecord = hydrateEmbedRecord(
127
131
embed.record,
128
132
quotedPost,
129
133
quotedProfile,
130
134
quotedIdentity,
135
+
cdn
131
136
);
132
137
133
138
if (!hydratedRecord) return undefined;
···
148
153
149
154
export function useHydratedEmbed(
150
155
embed: AppBskyFeedPost.Record["embed"],
151
-
postAuthorDid: string | undefined,
156
+
postAuthorDid: string | undefined
152
157
) {
153
158
const recordInfo = useMemo(() => {
154
-
if (
155
-
AppBskyEmbedRecordWithMedia.isMain(embed)
156
-
) {
159
+
if (AppBskyEmbedRecordWithMedia.isMain(embed)) {
157
160
const recordUri = embed.record.record.uri;
158
161
const quotedAuthorDid = new AtUri(recordUri).hostname;
159
162
return { recordUri, quotedAuthorDid, isRecordType: true };
160
-
} else
161
-
if (
162
-
AppBskyEmbedRecord.isMain(embed)
163
-
) {
163
+
} else if (AppBskyEmbedRecord.isMain(embed)) {
164
164
const recordUri = embed.record.uri;
165
165
const quotedAuthorDid = new AtUri(recordUri).hostname;
166
166
return { recordUri, quotedAuthorDid, isRecordType: true };
···
171
171
isRecordType: false,
172
172
};
173
173
}, [embed]);
174
+
174
175
const { isRecordType, recordUri, quotedAuthorDid } = recordInfo;
175
176
176
-
177
177
const usequerypostresults = useQueryPost(recordUri);
178
-
// const {
179
-
// data: quotedPost,
180
-
// isLoading: isLoadingPost,
181
-
// error: postError,
182
-
// } = usequerypostresults
183
178
184
-
const profileUri = quotedAuthorDid ? `at://${quotedAuthorDid}/app.bsky.actor.profile/self` : undefined;
179
+
const profileUri = quotedAuthorDid
180
+
? `at://${quotedAuthorDid}/app.bsky.actor.profile/self`
181
+
: undefined;
185
182
186
183
const {
187
184
data: quotedProfile,
···
189
186
error: profileError,
190
187
} = useQueryProfile(profileUri);
191
188
189
+
const [imgcdn] = useAtom(imgCDNAtom);
190
+
const [videocdn] = useAtom(videoCDNAtom);
191
+
192
192
const queryidentityresult = useQueryIdentity(quotedAuthorDid);
193
-
// const {
194
-
// data: quotedIdentity,
195
-
// isLoading: isLoadingIdentity,
196
-
// error: identityError,
197
-
// } = queryidentityresult
198
193
199
-
const [hydratedEmbed, setHydratedEmbed] = useState<
200
-
HydratedEmbedView | undefined
201
-
>(undefined);
202
-
203
-
useEffect(() => {
204
-
if (!embed || !postAuthorDid) {
205
-
setHydratedEmbed(undefined);
206
-
return;
207
-
}
194
+
const hydratedEmbed: HydratedEmbedView | undefined = (() => {
195
+
if (!embed || !postAuthorDid) return undefined;
208
196
209
-
if (isRecordType && (!usequerypostresults?.data || !quotedProfile || !queryidentityresult?.data)) {
210
-
setHydratedEmbed(undefined);
211
-
return;
197
+
if (
198
+
isRecordType &&
199
+
(!usequerypostresults?.data ||
200
+
!quotedProfile ||
201
+
!queryidentityresult?.data)
202
+
) {
203
+
return undefined;
212
204
}
213
205
214
206
try {
215
-
let result: HydratedEmbedView | undefined;
216
-
217
207
if (AppBskyEmbedImages.isMain(embed)) {
218
-
result = hydrateEmbedImages(embed, postAuthorDid);
208
+
return hydrateEmbedImages(embed, postAuthorDid, imgcdn);
219
209
} else if (AppBskyEmbedExternal.isMain(embed)) {
220
-
result = hydrateEmbedExternal(embed, postAuthorDid);
210
+
return hydrateEmbedExternal(embed, postAuthorDid, imgcdn);
221
211
} else if (AppBskyEmbedVideo.isMain(embed)) {
222
-
result = hydrateEmbedVideo(embed, postAuthorDid);
212
+
return hydrateEmbedVideo(embed, postAuthorDid, videocdn);
223
213
} else if (AppBskyEmbedRecord.isMain(embed)) {
224
-
result = hydrateEmbedRecord(
214
+
return hydrateEmbedRecord(
225
215
embed,
226
216
usequerypostresults?.data,
227
217
quotedProfile,
228
218
queryidentityresult?.data,
219
+
imgcdn
229
220
);
230
221
} else if (AppBskyEmbedRecordWithMedia.isMain(embed)) {
231
222
let hydratedMedia:
···
235
226
| undefined;
236
227
237
228
if (AppBskyEmbedImages.isMain(embed.media)) {
238
-
hydratedMedia = hydrateEmbedImages(embed.media, postAuthorDid);
229
+
hydratedMedia = hydrateEmbedImages(
230
+
embed.media,
231
+
postAuthorDid,
232
+
imgcdn
233
+
);
239
234
} else if (AppBskyEmbedExternal.isMain(embed.media)) {
240
-
hydratedMedia = hydrateEmbedExternal(embed.media, postAuthorDid);
235
+
hydratedMedia = hydrateEmbedExternal(
236
+
embed.media,
237
+
postAuthorDid,
238
+
imgcdn
239
+
);
241
240
} else if (AppBskyEmbedVideo.isMain(embed.media)) {
242
-
hydratedMedia = hydrateEmbedVideo(embed.media, postAuthorDid);
241
+
hydratedMedia = hydrateEmbedVideo(
242
+
embed.media,
243
+
postAuthorDid,
244
+
videocdn
245
+
);
243
246
}
244
247
245
248
if (hydratedMedia) {
246
-
result = hydrateEmbedRecordWithMedia(
249
+
return hydrateEmbedRecordWithMedia(
247
250
embed,
248
251
hydratedMedia,
249
252
usequerypostresults?.data,
250
253
quotedProfile,
251
254
queryidentityresult?.data,
255
+
imgcdn
252
256
);
253
257
}
254
258
}
255
-
setHydratedEmbed(result);
256
259
} catch (e) {
257
260
console.error("Error hydrating embed", e);
258
-
setHydratedEmbed(undefined);
261
+
return undefined;
259
262
}
260
-
}, [
261
-
embed,
262
-
postAuthorDid,
263
-
isRecordType,
264
-
usequerypostresults?.data,
265
-
quotedProfile,
266
-
queryidentityresult?.data,
267
-
]);
263
+
})();
268
264
269
265
const isLoading = isRecordType
270
-
? usequerypostresults?.isLoading || isLoadingProfile || queryidentityresult?.isLoading
266
+
? usequerypostresults?.isLoading ||
267
+
isLoadingProfile ||
268
+
queryidentityresult?.isLoading
271
269
: false;
272
-
const error = usequerypostresults?.error || profileError || queryidentityresult?.error;
270
+
271
+
const error =
272
+
usequerypostresults?.error || profileError || queryidentityresult?.error;
273
273
274
274
return { data: hydratedEmbed, isLoading, error };
275
-
}
275
+
}
+452
-137
src/utils/useQuery.ts
+452
-137
src/utils/useQuery.ts
···
1
1
import * as ATPAPI from "@atproto/api";
2
2
import {
3
+
infiniteQueryOptions,
3
4
type QueryFunctionContext,
4
5
queryOptions,
5
6
useInfiniteQuery,
6
7
useQuery,
7
-
type UseQueryResult} from "@tanstack/react-query";
8
+
type UseQueryResult,
9
+
} from "@tanstack/react-query";
10
+
import { useAtom } from "jotai";
8
11
9
-
export function constructIdentityQuery(didorhandle?: string) {
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
+
) {
10
20
return queryOptions({
11
21
queryKey: ["identity", didorhandle],
12
22
queryFn: async () => {
13
-
if (!didorhandle) return undefined as undefined
23
+
if (!didorhandle) return undefined as undefined;
14
24
const res = await fetch(
15
-
`https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(didorhandle)}`
25
+
`https://${slingshoturl}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(didorhandle)}`
16
26
);
17
27
if (!res.ok) throw new Error("Failed to fetch post");
18
28
try {
···
27
37
}
28
38
},
29
39
staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes
30
-
gcTime: /*0//*/5 * 60 * 1000,
40
+
gcTime: /*0//*/ 5 * 60 * 1000,
31
41
});
32
42
}
33
43
export function useQueryIdentity(didorhandle: string): UseQueryResult<
···
39
49
},
40
50
Error
41
51
>;
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
-
>
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
+
>;
56
63
export function useQueryIdentity(didorhandle?: string) {
57
-
return useQuery(constructIdentityQuery(didorhandle));
64
+
const [slingshoturl] = useAtom(slingshotURLAtom);
65
+
return useQuery(constructIdentityQuery(didorhandle, slingshoturl));
58
66
}
59
67
60
-
export function constructPostQuery(uri?: string) {
68
+
export function constructPostQuery(uri?: string, slingshoturl?: string) {
61
69
return queryOptions({
62
70
queryKey: ["post", uri],
63
71
queryFn: async () => {
64
-
if (!uri) return undefined as undefined
72
+
if (!uri) return undefined as undefined;
65
73
const res = await fetch(
66
-
`https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
74
+
`https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
67
75
);
68
76
let data: any;
69
77
try {
···
72
80
return undefined;
73
81
}
74
82
if (res.status === 400) return undefined;
75
-
if (data?.error === "InvalidRequest" && data.message?.includes("Could not find repo")) {
83
+
if (
84
+
data?.error === "InvalidRequest" &&
85
+
data.message?.includes("Could not find repo")
86
+
) {
76
87
return undefined; // cache โnot foundโ
77
88
}
78
89
try {
79
90
if (!res.ok) throw new Error("Failed to fetch post");
80
-
return (data) as {
91
+
return data as {
81
92
uri: string;
82
93
cid: string;
83
94
value: any;
···
92
103
return failureCount < 2;
93
104
},
94
105
staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes
95
-
gcTime: /*0//*/5 * 60 * 1000,
106
+
gcTime: /*0//*/ 5 * 60 * 1000,
96
107
});
97
108
}
98
109
export function useQueryPost(uri: string): UseQueryResult<
···
103
114
},
104
115
Error
105
116
>;
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
-
>
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
+
>;
119
127
export function useQueryPost(uri?: string) {
120
-
return useQuery(constructPostQuery(uri));
128
+
const [slingshoturl] = useAtom(slingshotURLAtom);
129
+
return useQuery(constructPostQuery(uri, slingshoturl));
121
130
}
122
131
123
-
export function constructProfileQuery(uri?: string) {
132
+
export function constructProfileQuery(uri?: string, slingshoturl?: string) {
124
133
return queryOptions({
125
134
queryKey: ["profile", uri],
126
135
queryFn: async () => {
127
-
if (!uri) return undefined as undefined
136
+
if (!uri) return undefined as undefined;
128
137
const res = await fetch(
129
-
`https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
138
+
`https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
130
139
);
131
140
let data: any;
132
141
try {
···
135
144
return undefined;
136
145
}
137
146
if (res.status === 400) return undefined;
138
-
if (data?.error === "InvalidRequest" && data.message?.includes("Could not find repo")) {
147
+
if (
148
+
data?.error === "InvalidRequest" &&
149
+
data.message?.includes("Could not find repo")
150
+
) {
139
151
return undefined; // cache โnot foundโ
140
152
}
141
153
try {
142
154
if (!res.ok) throw new Error("Failed to fetch post");
143
-
return (data) as {
155
+
return data as {
144
156
uri: string;
145
157
cid: string;
146
158
value: any;
···
155
167
return failureCount < 2;
156
168
},
157
169
staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes
158
-
gcTime: /*0//*/5 * 60 * 1000,
170
+
gcTime: /*0//*/ 5 * 60 * 1000,
159
171
});
160
172
}
161
173
export function useQueryProfile(uri: string): UseQueryResult<
···
166
178
},
167
179
Error
168
180
>;
169
-
export function useQueryProfile(): UseQueryResult<
170
-
undefined,
171
-
Error
172
-
>;
173
-
export function useQueryProfile(uri?: string):
174
-
UseQueryResult<
175
-
{
181
+
export function useQueryProfile(): UseQueryResult<undefined, Error>;
182
+
export function useQueryProfile(uri?: string): UseQueryResult<
183
+
| {
176
184
uri: string;
177
185
cid: string;
178
186
value: ATPAPI.AppBskyActorProfile.Record;
179
-
} | undefined,
180
-
Error
181
-
>
187
+
}
188
+
| undefined,
189
+
Error
190
+
>;
182
191
export function useQueryProfile(uri?: string) {
183
-
return useQuery(constructProfileQuery(uri));
192
+
const [slingshoturl] = useAtom(slingshotURLAtom);
193
+
return useQuery(constructProfileQuery(uri, slingshoturl));
184
194
}
185
195
186
196
// export function constructConstellationQuery(
···
215
225
// method: "/links/all",
216
226
// target: string
217
227
// ): QueryOptions<linksAllResponse, Error>;
218
-
export function constructConstellationQuery(query?:{
228
+
export function constructConstellationQuery(query?: {
229
+
constellation: string;
219
230
method:
220
231
| "/links"
221
232
| "/links/distinct-dids"
222
233
| "/links/count"
223
234
| "/links/count/distinct-dids"
224
235
| "/links/all"
225
-
| "undefined",
226
-
target: string,
227
-
collection?: string,
228
-
path?: string,
229
-
cursor?: string,
230
-
dids?: string[]
231
-
}
232
-
) {
236
+
| "undefined";
237
+
target: string;
238
+
collection?: string;
239
+
path?: string;
240
+
cursor?: string;
241
+
dids?: string[];
242
+
}) {
233
243
// : QueryOptions<
234
244
// | linksRecordsResponse
235
245
// | linksDidsResponse
···
239
249
// Error
240
250
// >
241
251
return queryOptions({
242
-
queryKey: ["constellation", query?.method, query?.target, query?.collection, query?.path, query?.cursor, query?.dids] as const,
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,
243
261
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
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;
251
269
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("") : ""}`
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("") : ""}`
253
271
);
254
272
if (!res.ok) throw new Error("Failed to fetch post");
255
273
try {
···
273
291
},
274
292
// enforce short lifespan
275
293
staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes
276
-
gcTime: /*0//*/5 * 60 * 1000,
294
+
gcTime: /*0//*/ 5 * 60 * 1000,
277
295
});
278
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
+
279
318
export function useQueryConstellation(query: {
280
319
method: "/links";
281
320
target: string;
···
338
377
>
339
378
| undefined {
340
379
//if (!query) return;
380
+
const [constellationurl] = useAtom(constellationURLAtom);
341
381
return useQuery(
342
-
constructConstellationQuery(query)
382
+
constructConstellationQuery(
383
+
query && { constellation: constellationurl, ...query }
384
+
)
343
385
);
344
386
}
345
387
346
-
type linksRecord = {
388
+
export type linksRecord = {
347
389
did: string;
348
390
collection: string;
349
391
rkey: string;
···
361
403
type linksCountResponse = {
362
404
total: string;
363
405
};
364
-
type linksAllResponse = {
406
+
export type linksAllResponse = {
365
407
links: Record<
366
408
string,
367
409
Record<
···
383
425
}) {
384
426
return queryOptions({
385
427
// 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 }],
428
+
queryKey: [
429
+
"feedSkeleton",
430
+
options?.feedUri,
431
+
{ isAuthed: options?.isAuthed, did: options?.agent?.did },
432
+
],
387
433
queryFn: async () => {
388
-
if (!options) return undefined as undefined
434
+
if (!options) return undefined as undefined;
389
435
const { feedUri, agent, isAuthed, pdsUrl, feedServiceDid } = options;
390
436
if (isAuthed) {
391
437
// Authenticated flow
392
438
if (!agent || !pdsUrl || !feedServiceDid) {
393
-
throw new Error("Missing required info for authenticated feed fetch.");
439
+
throw new Error(
440
+
"Missing required info for authenticated feed fetch."
441
+
);
394
442
}
395
443
const url = `${pdsUrl}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}`;
396
444
const res = await agent.fetchHandler(url, {
···
400
448
"Content-Type": "application/json",
401
449
},
402
450
});
403
-
if (!res.ok) throw new Error(`Authenticated feed fetch failed: ${res.statusText}`);
451
+
if (!res.ok)
452
+
throw new Error(`Authenticated feed fetch failed: ${res.statusText}`);
404
453
return (await res.json()) as ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema;
405
454
} else {
406
455
// Unauthenticated flow (using a public PDS/AppView)
407
456
const url = `https://discover.bsky.app/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}`;
408
457
const res = await fetch(url);
409
-
if (!res.ok) throw new Error(`Public feed fetch failed: ${res.statusText}`);
458
+
if (!res.ok)
459
+
throw new Error(`Public feed fetch failed: ${res.statusText}`);
410
460
return (await res.json()) as ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema;
411
461
}
412
462
},
···
424
474
return useQuery(constructFeedSkeletonQuery(options));
425
475
}
426
476
427
-
export function constructPreferencesQuery(agent?: ATPAPI.Agent | undefined, pdsUrl?: string | undefined) {
477
+
export function constructPreferencesQuery(
478
+
agent?: ATPAPI.Agent | undefined,
479
+
pdsUrl?: string | undefined
480
+
) {
428
481
return queryOptions({
429
-
queryKey: ['preferences', agent?.did],
482
+
queryKey: ["preferences", agent?.did],
430
483
queryFn: async () => {
431
484
if (!agent || !pdsUrl) throw new Error("Agent or PDS URL not available");
432
485
const url = `${pdsUrl}/xrpc/app.bsky.actor.getPreferences`;
···
437
490
});
438
491
}
439
492
export function useQueryPreferences(options: {
440
-
agent?: ATPAPI.Agent | undefined, pdsUrl?: string | undefined
493
+
agent?: ATPAPI.Agent | undefined;
494
+
pdsUrl?: string | undefined;
441
495
}) {
442
496
return useQuery(constructPreferencesQuery(options.agent, options.pdsUrl));
443
497
}
444
498
445
-
446
-
447
-
export function constructArbitraryQuery(uri?: string) {
499
+
export function constructArbitraryQuery(uri?: string, slingshoturl?: string) {
448
500
return queryOptions({
449
501
queryKey: ["arbitrary", uri],
450
502
queryFn: async () => {
451
-
if (!uri) return undefined as undefined
503
+
if (!uri) return undefined as undefined;
452
504
const res = await fetch(
453
-
`https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
505
+
`https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
454
506
);
455
507
let data: any;
456
508
try {
···
459
511
return undefined;
460
512
}
461
513
if (res.status === 400) return undefined;
462
-
if (data?.error === "InvalidRequest" && data.message?.includes("Could not find repo")) {
514
+
if (
515
+
data?.error === "InvalidRequest" &&
516
+
data.message?.includes("Could not find repo")
517
+
) {
463
518
return undefined; // cache โnot foundโ
464
519
}
465
520
try {
466
521
if (!res.ok) throw new Error("Failed to fetch post");
467
-
return (data) as {
522
+
return data as {
468
523
uri: string;
469
524
cid: string;
470
525
value: any;
···
479
534
return failureCount < 2;
480
535
},
481
536
staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes
482
-
gcTime: /*0//*/5 * 60 * 1000,
537
+
gcTime: /*0//*/ 5 * 60 * 1000,
483
538
});
484
539
}
485
540
export function useQueryArbitrary(uri: string): UseQueryResult<
···
490
545
},
491
546
Error
492
547
>;
493
-
export function useQueryArbitrary(): UseQueryResult<
494
-
undefined,
495
-
Error
496
-
>;
548
+
export function useQueryArbitrary(): UseQueryResult<undefined, Error>;
497
549
export function useQueryArbitrary(uri?: string): UseQueryResult<
498
-
{
499
-
uri: string;
500
-
cid: string;
501
-
value: any;
502
-
} | undefined,
550
+
| {
551
+
uri: string;
552
+
cid: string;
553
+
value: any;
554
+
}
555
+
| undefined,
503
556
Error
504
557
>;
505
558
export function useQueryArbitrary(uri?: string) {
506
-
return useQuery(constructArbitraryQuery(uri));
559
+
const [slingshoturl] = useAtom(slingshotURLAtom);
560
+
return useQuery(constructArbitraryQuery(uri, slingshoturl));
507
561
}
508
562
509
-
export function constructFallbackNothingQuery(){
563
+
export function constructFallbackNothingQuery() {
510
564
return queryOptions({
511
565
queryKey: ["nothing"],
512
566
queryFn: async () => {
513
-
return undefined
567
+
return undefined;
514
568
},
515
569
});
516
570
}
···
524
578
}[];
525
579
};
526
580
527
-
export function constructAuthorFeedQuery(did: string, pdsUrl: string) {
581
+
export function constructAuthorFeedQuery(
582
+
did: string,
583
+
pdsUrl: string,
584
+
collection: string = "app.bsky.feed.post"
585
+
) {
528
586
return queryOptions({
529
-
queryKey: ['authorFeed', did],
587
+
queryKey: ["authorFeed", did, collection],
530
588
queryFn: async ({ pageParam }: QueryFunctionContext) => {
531
589
const limit = 25;
532
-
590
+
533
591
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
-
592
+
const cursorParam = cursor ? `&cursor=${cursor}` : "";
593
+
594
+
const url = `${pdsUrl}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=${collection}&limit=${limit}${cursorParam}`;
595
+
538
596
const res = await fetch(url);
539
597
if (!res.ok) throw new Error("Failed to fetch author's posts");
540
-
598
+
541
599
return res.json() as Promise<ListRecordsResponse>;
542
600
},
543
601
});
544
602
}
545
603
546
-
export function useInfiniteQueryAuthorFeed(did: string | undefined, pdsUrl: string | undefined) {
547
-
const { queryKey, queryFn } = constructAuthorFeedQuery(did!, pdsUrl!);
548
-
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
+
549
615
return useInfiniteQuery({
550
616
queryKey,
551
617
queryFn,
···
563
629
isAuthed: boolean;
564
630
pdsUrl?: string;
565
631
feedServiceDid?: string;
632
+
// todo the hell is a unauthedfeedurl
633
+
unauthedfeedurl?: string;
566
634
}) {
567
-
const { feedUri, agent, isAuthed, pdsUrl, feedServiceDid } = options;
568
-
635
+
const { feedUri, agent, isAuthed, pdsUrl, feedServiceDid, unauthedfeedurl } =
636
+
options;
637
+
569
638
return queryOptions({
570
639
queryKey: ["feedSkeleton", feedUri, { isAuthed, did: agent?.did }],
571
-
572
-
queryFn: async ({ pageParam }: QueryFunctionContext): Promise<FeedSkeletonPage> => {
640
+
641
+
queryFn: async ({
642
+
pageParam,
643
+
}: QueryFunctionContext): Promise<FeedSkeletonPage> => {
573
644
const cursorParam = pageParam ? `&cursor=${pageParam}` : "";
574
-
575
-
if (isAuthed) {
645
+
646
+
if (isAuthed && !unauthedfeedurl) {
576
647
if (!agent || !pdsUrl || !feedServiceDid) {
577
-
throw new Error("Missing required info for authenticated feed fetch.");
648
+
throw new Error(
649
+
"Missing required info for authenticated feed fetch."
650
+
);
578
651
}
579
652
const url = `${pdsUrl}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}${cursorParam}`;
580
653
const res = await agent.fetchHandler(url, {
···
584
657
"Content-Type": "application/json",
585
658
},
586
659
});
587
-
if (!res.ok) throw new Error(`Authenticated feed fetch failed: ${res.statusText}`);
660
+
if (!res.ok)
661
+
throw new Error(`Authenticated feed fetch failed: ${res.statusText}`);
588
662
return (await res.json()) as FeedSkeletonPage;
589
663
} else {
590
-
const url = `https://discover.bsky.app/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}${cursorParam}`;
664
+
const url = `https://${unauthedfeedurl ? unauthedfeedurl : "discover.bsky.app"}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}${cursorParam}`;
591
665
const res = await fetch(url);
592
-
if (!res.ok) throw new Error(`Public feed fetch failed: ${res.statusText}`);
666
+
if (!res.ok)
667
+
throw new Error(`Public feed fetch failed: ${res.statusText}`);
593
668
return (await res.json()) as FeedSkeletonPage;
594
669
}
595
670
},
···
602
677
isAuthed: boolean;
603
678
pdsUrl?: string;
604
679
feedServiceDid?: string;
680
+
unauthedfeedurl?: string;
605
681
}) {
606
682
const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery(options);
607
-
608
-
return useInfiniteQuery({
609
-
queryKey,
610
-
queryFn,
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
+
},
611
929
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),
930
+
getNextPageParam: (lastPage) => lastPage?.cursor as null | undefined,
616
931
});
617
-
}
932
+
}
+1
-1
tsconfig.json
+1
-1
tsconfig.json
+27
-1
vite.config.ts
+27
-1
vite.config.ts
···
3
3
import tailwindcss from "@tailwindcss/vite";
4
4
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
5
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'
6
9
import { defineConfig } from "vite";
7
10
8
11
import { generateMetadataPlugin } from "./oauthdev.mts";
9
12
10
-
const PROD_URL = "https://reddwarf.whey.party"
13
+
const PROD_URL = "https://reddwarf.app"
11
14
const DEV_URL = "https://local3768forumtest.whey.party"
12
15
16
+
const PROD_HANDLE_RESOLVER_PDS = "https://pds-nd.whey.party"
17
+
const DEV_HANDLE_RESOLVER_PDS = "https://bsky.social"
18
+
13
19
function shp(url: string): string {
14
20
return url.replace(/^https?:\/\//, '');
15
21
}
···
20
26
generateMetadataPlugin({
21
27
prod: PROD_URL,
22
28
dev: DEV_URL,
29
+
prodResolver: PROD_HANDLE_RESOLVER_PDS,
30
+
devResolver: DEV_HANDLE_RESOLVER_PDS,
23
31
}),
24
32
TanStackRouterVite({ autoCodeSplitting: true }),
25
33
viteReact({
···
28
36
},
29
37
}),
30
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
+
}),
31
57
],
32
58
// test: {
33
59
// globals: true,