an appview-less Bluesky client using Constellation and PDS Queries reddwarf.app
frontend spa bluesky reddwarf microcosm

Compare changes

Choose any two refs to compare.

+2 -1
.gitignore
··· 7 7 .env 8 8 .nitro 9 9 .tanstack 10 - public/client-metadata.json 10 + public/client-metadata.json 11 + public/resolvers.json
+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 - ![screenshot of red dwarf](/public/screenshot.png) 4 + ![screenshot of red dwarf](/public/screenshot.jpg) 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
··· 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
··· 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
··· 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

This is a binary file and will not be displayed.

public/screenshot.png

This is a binary file and will not be displayed.

+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 5 5 "jsx": "react-jsx", 6 6 "module": "ESNext", 7 7 "lib": ["ES2022", "DOM", "DOM.Iterable"], 8 - "types": ["vite/client"], 8 + "types": ["vite/client", "unplugin-icons/types/react"], 9 9 10 10 /* Bundler mode */ 11 11 "moduleResolution": "bundler",
+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,