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.

+1 -1
README.md
··· 8 ## running dev and build 9 in the `vite.config.ts` file you should change these values 10 ```ts 11 - const PROD_URL = "https://reddwarf.whey.party" 12 const DEV_URL = "https://local3768forumtest.whey.party" 13 ``` 14 the PROD_URL is what will compile your oauth client metadata so it is very important to change that. same for DEV_URL if you are using a tunnel for dev work
··· 8 ## running dev and build 9 in the `vite.config.ts` file you should change these values 10 ```ts 11 + const PROD_URL = "https://reddwarf.app" 12 const DEV_URL = "https://local3768forumtest.whey.party" 13 ``` 14 the PROD_URL is what will compile your oauth client metadata so it is very important to change that. same for DEV_URL if you are using a tunnel for dev work
+1685
package-lock.json
··· 8 "dependencies": { 9 "@atproto/api": "^0.16.6", 10 "@atproto/oauth-client-browser": "^0.3.33", 11 "@tailwindcss/vite": "^4.0.6", 12 "@tanstack/query-sync-storage-persister": "^5.85.6", 13 "@tanstack/react-devtools": "^0.2.2", ··· 21 "idb-keyval": "^6.2.2", 22 "jotai": "^2.13.1", 23 "npm": "^11.6.2", 24 "react": "^19.0.0", 25 "react-dom": "^19.0.0", 26 "react-player": "^3.3.2", ··· 1592 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1593 } 1594 }, 1595 "node_modules/@humanfs/core": { 1596 "version": "0.19.1", 1597 "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", ··· 1895 "node": ">= 8" 1896 } 1897 }, 1898 "node_modules/@rolldown/pluginutils": { 1899 "version": "1.0.0-beta.27", 1900 "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", ··· 3806 "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 3807 "dev": true, 3808 "license": "Python-2.0" 3809 }, 3810 "node_modules/aria-query": { 3811 "version": "5.3.0", ··· 4716 "node": ">=8" 4717 } 4718 }, 4719 "node_modules/diff": { 4720 "version": "8.0.2", 4721 "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", ··· 5735 }, 5736 "funding": { 5737 "url": "https://github.com/sponsors/ljharb" 5738 } 5739 }, 5740 "node_modules/get-proto": { ··· 10338 ], 10339 "license": "MIT" 10340 }, 10341 "node_modules/react": { 10342 "version": "19.1.1", 10343 "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", ··· 10399 "node": ">=0.10.0" 10400 } 10401 }, 10402 "node_modules/readdirp": { 10403 "version": "3.6.0", 10404 "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", ··· 11892 "peer": true, 11893 "dependencies": { 11894 "punycode": "^2.1.0" 11895 } 11896 }, 11897 "node_modules/use-sync-external-store": {
··· 8 "dependencies": { 9 "@atproto/api": "^0.16.6", 10 "@atproto/oauth-client-browser": "^0.3.33", 11 + "@radix-ui/react-dialog": "^1.1.15", 12 + "@radix-ui/react-dropdown-menu": "^2.1.16", 13 + "@radix-ui/react-hover-card": "^1.1.15", 14 + "@radix-ui/react-slider": "^1.3.6", 15 "@tailwindcss/vite": "^4.0.6", 16 "@tanstack/query-sync-storage-persister": "^5.85.6", 17 "@tanstack/react-devtools": "^0.2.2", ··· 25 "idb-keyval": "^6.2.2", 26 "jotai": "^2.13.1", 27 "npm": "^11.6.2", 28 + "radix-ui": "^1.4.3", 29 "react": "^19.0.0", 30 "react-dom": "^19.0.0", 31 "react-player": "^3.3.2", ··· 1597 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1598 } 1599 }, 1600 + "node_modules/@floating-ui/core": { 1601 + "version": "1.7.3", 1602 + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", 1603 + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", 1604 + "dependencies": { 1605 + "@floating-ui/utils": "^0.2.10" 1606 + } 1607 + }, 1608 + "node_modules/@floating-ui/dom": { 1609 + "version": "1.7.4", 1610 + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", 1611 + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", 1612 + "dependencies": { 1613 + "@floating-ui/core": "^1.7.3", 1614 + "@floating-ui/utils": "^0.2.10" 1615 + } 1616 + }, 1617 + "node_modules/@floating-ui/react-dom": { 1618 + "version": "2.1.6", 1619 + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", 1620 + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", 1621 + "dependencies": { 1622 + "@floating-ui/dom": "^1.7.4" 1623 + }, 1624 + "peerDependencies": { 1625 + "react": ">=16.8.0", 1626 + "react-dom": ">=16.8.0" 1627 + } 1628 + }, 1629 + "node_modules/@floating-ui/utils": { 1630 + "version": "0.2.10", 1631 + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", 1632 + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==" 1633 + }, 1634 "node_modules/@humanfs/core": { 1635 "version": "0.19.1", 1636 "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", ··· 1934 "node": ">= 8" 1935 } 1936 }, 1937 + "node_modules/@radix-ui/number": { 1938 + "version": "1.1.1", 1939 + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", 1940 + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==" 1941 + }, 1942 + "node_modules/@radix-ui/primitive": { 1943 + "version": "1.1.3", 1944 + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", 1945 + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==" 1946 + }, 1947 + "node_modules/@radix-ui/react-accessible-icon": { 1948 + "version": "1.1.7", 1949 + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", 1950 + "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", 1951 + "dependencies": { 1952 + "@radix-ui/react-visually-hidden": "1.2.3" 1953 + }, 1954 + "peerDependencies": { 1955 + "@types/react": "*", 1956 + "@types/react-dom": "*", 1957 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 1958 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 1959 + }, 1960 + "peerDependenciesMeta": { 1961 + "@types/react": { 1962 + "optional": true 1963 + }, 1964 + "@types/react-dom": { 1965 + "optional": true 1966 + } 1967 + } 1968 + }, 1969 + "node_modules/@radix-ui/react-accordion": { 1970 + "version": "1.2.12", 1971 + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", 1972 + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", 1973 + "dependencies": { 1974 + "@radix-ui/primitive": "1.1.3", 1975 + "@radix-ui/react-collapsible": "1.1.12", 1976 + "@radix-ui/react-collection": "1.1.7", 1977 + "@radix-ui/react-compose-refs": "1.1.2", 1978 + "@radix-ui/react-context": "1.1.2", 1979 + "@radix-ui/react-direction": "1.1.1", 1980 + "@radix-ui/react-id": "1.1.1", 1981 + "@radix-ui/react-primitive": "2.1.3", 1982 + "@radix-ui/react-use-controllable-state": "1.2.2" 1983 + }, 1984 + "peerDependencies": { 1985 + "@types/react": "*", 1986 + "@types/react-dom": "*", 1987 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 1988 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 1989 + }, 1990 + "peerDependenciesMeta": { 1991 + "@types/react": { 1992 + "optional": true 1993 + }, 1994 + "@types/react-dom": { 1995 + "optional": true 1996 + } 1997 + } 1998 + }, 1999 + "node_modules/@radix-ui/react-alert-dialog": { 2000 + "version": "1.1.15", 2001 + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", 2002 + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", 2003 + "dependencies": { 2004 + "@radix-ui/primitive": "1.1.3", 2005 + "@radix-ui/react-compose-refs": "1.1.2", 2006 + "@radix-ui/react-context": "1.1.2", 2007 + "@radix-ui/react-dialog": "1.1.15", 2008 + "@radix-ui/react-primitive": "2.1.3", 2009 + "@radix-ui/react-slot": "1.2.3" 2010 + }, 2011 + "peerDependencies": { 2012 + "@types/react": "*", 2013 + "@types/react-dom": "*", 2014 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2015 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2016 + }, 2017 + "peerDependenciesMeta": { 2018 + "@types/react": { 2019 + "optional": true 2020 + }, 2021 + "@types/react-dom": { 2022 + "optional": true 2023 + } 2024 + } 2025 + }, 2026 + "node_modules/@radix-ui/react-arrow": { 2027 + "version": "1.1.7", 2028 + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", 2029 + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", 2030 + "dependencies": { 2031 + "@radix-ui/react-primitive": "2.1.3" 2032 + }, 2033 + "peerDependencies": { 2034 + "@types/react": "*", 2035 + "@types/react-dom": "*", 2036 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2037 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2038 + }, 2039 + "peerDependenciesMeta": { 2040 + "@types/react": { 2041 + "optional": true 2042 + }, 2043 + "@types/react-dom": { 2044 + "optional": true 2045 + } 2046 + } 2047 + }, 2048 + "node_modules/@radix-ui/react-aspect-ratio": { 2049 + "version": "1.1.7", 2050 + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", 2051 + "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", 2052 + "dependencies": { 2053 + "@radix-ui/react-primitive": "2.1.3" 2054 + }, 2055 + "peerDependencies": { 2056 + "@types/react": "*", 2057 + "@types/react-dom": "*", 2058 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2059 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2060 + }, 2061 + "peerDependenciesMeta": { 2062 + "@types/react": { 2063 + "optional": true 2064 + }, 2065 + "@types/react-dom": { 2066 + "optional": true 2067 + } 2068 + } 2069 + }, 2070 + "node_modules/@radix-ui/react-avatar": { 2071 + "version": "1.1.10", 2072 + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", 2073 + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", 2074 + "dependencies": { 2075 + "@radix-ui/react-context": "1.1.2", 2076 + "@radix-ui/react-primitive": "2.1.3", 2077 + "@radix-ui/react-use-callback-ref": "1.1.1", 2078 + "@radix-ui/react-use-is-hydrated": "0.1.0", 2079 + "@radix-ui/react-use-layout-effect": "1.1.1" 2080 + }, 2081 + "peerDependencies": { 2082 + "@types/react": "*", 2083 + "@types/react-dom": "*", 2084 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2085 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2086 + }, 2087 + "peerDependenciesMeta": { 2088 + "@types/react": { 2089 + "optional": true 2090 + }, 2091 + "@types/react-dom": { 2092 + "optional": true 2093 + } 2094 + } 2095 + }, 2096 + "node_modules/@radix-ui/react-checkbox": { 2097 + "version": "1.3.3", 2098 + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", 2099 + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", 2100 + "dependencies": { 2101 + "@radix-ui/primitive": "1.1.3", 2102 + "@radix-ui/react-compose-refs": "1.1.2", 2103 + "@radix-ui/react-context": "1.1.2", 2104 + "@radix-ui/react-presence": "1.1.5", 2105 + "@radix-ui/react-primitive": "2.1.3", 2106 + "@radix-ui/react-use-controllable-state": "1.2.2", 2107 + "@radix-ui/react-use-previous": "1.1.1", 2108 + "@radix-ui/react-use-size": "1.1.1" 2109 + }, 2110 + "peerDependencies": { 2111 + "@types/react": "*", 2112 + "@types/react-dom": "*", 2113 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2114 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2115 + }, 2116 + "peerDependenciesMeta": { 2117 + "@types/react": { 2118 + "optional": true 2119 + }, 2120 + "@types/react-dom": { 2121 + "optional": true 2122 + } 2123 + } 2124 + }, 2125 + "node_modules/@radix-ui/react-collapsible": { 2126 + "version": "1.1.12", 2127 + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", 2128 + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", 2129 + "dependencies": { 2130 + "@radix-ui/primitive": "1.1.3", 2131 + "@radix-ui/react-compose-refs": "1.1.2", 2132 + "@radix-ui/react-context": "1.1.2", 2133 + "@radix-ui/react-id": "1.1.1", 2134 + "@radix-ui/react-presence": "1.1.5", 2135 + "@radix-ui/react-primitive": "2.1.3", 2136 + "@radix-ui/react-use-controllable-state": "1.2.2", 2137 + "@radix-ui/react-use-layout-effect": "1.1.1" 2138 + }, 2139 + "peerDependencies": { 2140 + "@types/react": "*", 2141 + "@types/react-dom": "*", 2142 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2143 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2144 + }, 2145 + "peerDependenciesMeta": { 2146 + "@types/react": { 2147 + "optional": true 2148 + }, 2149 + "@types/react-dom": { 2150 + "optional": true 2151 + } 2152 + } 2153 + }, 2154 + "node_modules/@radix-ui/react-collection": { 2155 + "version": "1.1.7", 2156 + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", 2157 + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", 2158 + "dependencies": { 2159 + "@radix-ui/react-compose-refs": "1.1.2", 2160 + "@radix-ui/react-context": "1.1.2", 2161 + "@radix-ui/react-primitive": "2.1.3", 2162 + "@radix-ui/react-slot": "1.2.3" 2163 + }, 2164 + "peerDependencies": { 2165 + "@types/react": "*", 2166 + "@types/react-dom": "*", 2167 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2168 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2169 + }, 2170 + "peerDependenciesMeta": { 2171 + "@types/react": { 2172 + "optional": true 2173 + }, 2174 + "@types/react-dom": { 2175 + "optional": true 2176 + } 2177 + } 2178 + }, 2179 + "node_modules/@radix-ui/react-compose-refs": { 2180 + "version": "1.1.2", 2181 + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", 2182 + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", 2183 + "peerDependencies": { 2184 + "@types/react": "*", 2185 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2186 + }, 2187 + "peerDependenciesMeta": { 2188 + "@types/react": { 2189 + "optional": true 2190 + } 2191 + } 2192 + }, 2193 + "node_modules/@radix-ui/react-context": { 2194 + "version": "1.1.2", 2195 + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", 2196 + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", 2197 + "peerDependencies": { 2198 + "@types/react": "*", 2199 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2200 + }, 2201 + "peerDependenciesMeta": { 2202 + "@types/react": { 2203 + "optional": true 2204 + } 2205 + } 2206 + }, 2207 + "node_modules/@radix-ui/react-context-menu": { 2208 + "version": "2.2.16", 2209 + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", 2210 + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", 2211 + "dependencies": { 2212 + "@radix-ui/primitive": "1.1.3", 2213 + "@radix-ui/react-context": "1.1.2", 2214 + "@radix-ui/react-menu": "2.1.16", 2215 + "@radix-ui/react-primitive": "2.1.3", 2216 + "@radix-ui/react-use-callback-ref": "1.1.1", 2217 + "@radix-ui/react-use-controllable-state": "1.2.2" 2218 + }, 2219 + "peerDependencies": { 2220 + "@types/react": "*", 2221 + "@types/react-dom": "*", 2222 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2223 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2224 + }, 2225 + "peerDependenciesMeta": { 2226 + "@types/react": { 2227 + "optional": true 2228 + }, 2229 + "@types/react-dom": { 2230 + "optional": true 2231 + } 2232 + } 2233 + }, 2234 + "node_modules/@radix-ui/react-dialog": { 2235 + "version": "1.1.15", 2236 + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", 2237 + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", 2238 + "dependencies": { 2239 + "@radix-ui/primitive": "1.1.3", 2240 + "@radix-ui/react-compose-refs": "1.1.2", 2241 + "@radix-ui/react-context": "1.1.2", 2242 + "@radix-ui/react-dismissable-layer": "1.1.11", 2243 + "@radix-ui/react-focus-guards": "1.1.3", 2244 + "@radix-ui/react-focus-scope": "1.1.7", 2245 + "@radix-ui/react-id": "1.1.1", 2246 + "@radix-ui/react-portal": "1.1.9", 2247 + "@radix-ui/react-presence": "1.1.5", 2248 + "@radix-ui/react-primitive": "2.1.3", 2249 + "@radix-ui/react-slot": "1.2.3", 2250 + "@radix-ui/react-use-controllable-state": "1.2.2", 2251 + "aria-hidden": "^1.2.4", 2252 + "react-remove-scroll": "^2.6.3" 2253 + }, 2254 + "peerDependencies": { 2255 + "@types/react": "*", 2256 + "@types/react-dom": "*", 2257 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2258 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2259 + }, 2260 + "peerDependenciesMeta": { 2261 + "@types/react": { 2262 + "optional": true 2263 + }, 2264 + "@types/react-dom": { 2265 + "optional": true 2266 + } 2267 + } 2268 + }, 2269 + "node_modules/@radix-ui/react-direction": { 2270 + "version": "1.1.1", 2271 + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", 2272 + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", 2273 + "peerDependencies": { 2274 + "@types/react": "*", 2275 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2276 + }, 2277 + "peerDependenciesMeta": { 2278 + "@types/react": { 2279 + "optional": true 2280 + } 2281 + } 2282 + }, 2283 + "node_modules/@radix-ui/react-dismissable-layer": { 2284 + "version": "1.1.11", 2285 + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", 2286 + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", 2287 + "dependencies": { 2288 + "@radix-ui/primitive": "1.1.3", 2289 + "@radix-ui/react-compose-refs": "1.1.2", 2290 + "@radix-ui/react-primitive": "2.1.3", 2291 + "@radix-ui/react-use-callback-ref": "1.1.1", 2292 + "@radix-ui/react-use-escape-keydown": "1.1.1" 2293 + }, 2294 + "peerDependencies": { 2295 + "@types/react": "*", 2296 + "@types/react-dom": "*", 2297 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2298 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2299 + }, 2300 + "peerDependenciesMeta": { 2301 + "@types/react": { 2302 + "optional": true 2303 + }, 2304 + "@types/react-dom": { 2305 + "optional": true 2306 + } 2307 + } 2308 + }, 2309 + "node_modules/@radix-ui/react-dropdown-menu": { 2310 + "version": "2.1.16", 2311 + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", 2312 + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", 2313 + "dependencies": { 2314 + "@radix-ui/primitive": "1.1.3", 2315 + "@radix-ui/react-compose-refs": "1.1.2", 2316 + "@radix-ui/react-context": "1.1.2", 2317 + "@radix-ui/react-id": "1.1.1", 2318 + "@radix-ui/react-menu": "2.1.16", 2319 + "@radix-ui/react-primitive": "2.1.3", 2320 + "@radix-ui/react-use-controllable-state": "1.2.2" 2321 + }, 2322 + "peerDependencies": { 2323 + "@types/react": "*", 2324 + "@types/react-dom": "*", 2325 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2326 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2327 + }, 2328 + "peerDependenciesMeta": { 2329 + "@types/react": { 2330 + "optional": true 2331 + }, 2332 + "@types/react-dom": { 2333 + "optional": true 2334 + } 2335 + } 2336 + }, 2337 + "node_modules/@radix-ui/react-focus-guards": { 2338 + "version": "1.1.3", 2339 + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", 2340 + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", 2341 + "peerDependencies": { 2342 + "@types/react": "*", 2343 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2344 + }, 2345 + "peerDependenciesMeta": { 2346 + "@types/react": { 2347 + "optional": true 2348 + } 2349 + } 2350 + }, 2351 + "node_modules/@radix-ui/react-focus-scope": { 2352 + "version": "1.1.7", 2353 + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", 2354 + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", 2355 + "dependencies": { 2356 + "@radix-ui/react-compose-refs": "1.1.2", 2357 + "@radix-ui/react-primitive": "2.1.3", 2358 + "@radix-ui/react-use-callback-ref": "1.1.1" 2359 + }, 2360 + "peerDependencies": { 2361 + "@types/react": "*", 2362 + "@types/react-dom": "*", 2363 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2364 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2365 + }, 2366 + "peerDependenciesMeta": { 2367 + "@types/react": { 2368 + "optional": true 2369 + }, 2370 + "@types/react-dom": { 2371 + "optional": true 2372 + } 2373 + } 2374 + }, 2375 + "node_modules/@radix-ui/react-form": { 2376 + "version": "0.1.8", 2377 + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", 2378 + "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", 2379 + "dependencies": { 2380 + "@radix-ui/primitive": "1.1.3", 2381 + "@radix-ui/react-compose-refs": "1.1.2", 2382 + "@radix-ui/react-context": "1.1.2", 2383 + "@radix-ui/react-id": "1.1.1", 2384 + "@radix-ui/react-label": "2.1.7", 2385 + "@radix-ui/react-primitive": "2.1.3" 2386 + }, 2387 + "peerDependencies": { 2388 + "@types/react": "*", 2389 + "@types/react-dom": "*", 2390 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2391 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2392 + }, 2393 + "peerDependenciesMeta": { 2394 + "@types/react": { 2395 + "optional": true 2396 + }, 2397 + "@types/react-dom": { 2398 + "optional": true 2399 + } 2400 + } 2401 + }, 2402 + "node_modules/@radix-ui/react-hover-card": { 2403 + "version": "1.1.15", 2404 + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", 2405 + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", 2406 + "license": "MIT", 2407 + "dependencies": { 2408 + "@radix-ui/primitive": "1.1.3", 2409 + "@radix-ui/react-compose-refs": "1.1.2", 2410 + "@radix-ui/react-context": "1.1.2", 2411 + "@radix-ui/react-dismissable-layer": "1.1.11", 2412 + "@radix-ui/react-popper": "1.2.8", 2413 + "@radix-ui/react-portal": "1.1.9", 2414 + "@radix-ui/react-presence": "1.1.5", 2415 + "@radix-ui/react-primitive": "2.1.3", 2416 + "@radix-ui/react-use-controllable-state": "1.2.2" 2417 + }, 2418 + "peerDependencies": { 2419 + "@types/react": "*", 2420 + "@types/react-dom": "*", 2421 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2422 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2423 + }, 2424 + "peerDependenciesMeta": { 2425 + "@types/react": { 2426 + "optional": true 2427 + }, 2428 + "@types/react-dom": { 2429 + "optional": true 2430 + } 2431 + } 2432 + }, 2433 + "node_modules/@radix-ui/react-id": { 2434 + "version": "1.1.1", 2435 + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", 2436 + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", 2437 + "dependencies": { 2438 + "@radix-ui/react-use-layout-effect": "1.1.1" 2439 + }, 2440 + "peerDependencies": { 2441 + "@types/react": "*", 2442 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2443 + }, 2444 + "peerDependenciesMeta": { 2445 + "@types/react": { 2446 + "optional": true 2447 + } 2448 + } 2449 + }, 2450 + "node_modules/@radix-ui/react-label": { 2451 + "version": "2.1.7", 2452 + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", 2453 + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", 2454 + "dependencies": { 2455 + "@radix-ui/react-primitive": "2.1.3" 2456 + }, 2457 + "peerDependencies": { 2458 + "@types/react": "*", 2459 + "@types/react-dom": "*", 2460 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2461 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2462 + }, 2463 + "peerDependenciesMeta": { 2464 + "@types/react": { 2465 + "optional": true 2466 + }, 2467 + "@types/react-dom": { 2468 + "optional": true 2469 + } 2470 + } 2471 + }, 2472 + "node_modules/@radix-ui/react-menu": { 2473 + "version": "2.1.16", 2474 + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", 2475 + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", 2476 + "dependencies": { 2477 + "@radix-ui/primitive": "1.1.3", 2478 + "@radix-ui/react-collection": "1.1.7", 2479 + "@radix-ui/react-compose-refs": "1.1.2", 2480 + "@radix-ui/react-context": "1.1.2", 2481 + "@radix-ui/react-direction": "1.1.1", 2482 + "@radix-ui/react-dismissable-layer": "1.1.11", 2483 + "@radix-ui/react-focus-guards": "1.1.3", 2484 + "@radix-ui/react-focus-scope": "1.1.7", 2485 + "@radix-ui/react-id": "1.1.1", 2486 + "@radix-ui/react-popper": "1.2.8", 2487 + "@radix-ui/react-portal": "1.1.9", 2488 + "@radix-ui/react-presence": "1.1.5", 2489 + "@radix-ui/react-primitive": "2.1.3", 2490 + "@radix-ui/react-roving-focus": "1.1.11", 2491 + "@radix-ui/react-slot": "1.2.3", 2492 + "@radix-ui/react-use-callback-ref": "1.1.1", 2493 + "aria-hidden": "^1.2.4", 2494 + "react-remove-scroll": "^2.6.3" 2495 + }, 2496 + "peerDependencies": { 2497 + "@types/react": "*", 2498 + "@types/react-dom": "*", 2499 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2500 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2501 + }, 2502 + "peerDependenciesMeta": { 2503 + "@types/react": { 2504 + "optional": true 2505 + }, 2506 + "@types/react-dom": { 2507 + "optional": true 2508 + } 2509 + } 2510 + }, 2511 + "node_modules/@radix-ui/react-menubar": { 2512 + "version": "1.1.16", 2513 + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", 2514 + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", 2515 + "dependencies": { 2516 + "@radix-ui/primitive": "1.1.3", 2517 + "@radix-ui/react-collection": "1.1.7", 2518 + "@radix-ui/react-compose-refs": "1.1.2", 2519 + "@radix-ui/react-context": "1.1.2", 2520 + "@radix-ui/react-direction": "1.1.1", 2521 + "@radix-ui/react-id": "1.1.1", 2522 + "@radix-ui/react-menu": "2.1.16", 2523 + "@radix-ui/react-primitive": "2.1.3", 2524 + "@radix-ui/react-roving-focus": "1.1.11", 2525 + "@radix-ui/react-use-controllable-state": "1.2.2" 2526 + }, 2527 + "peerDependencies": { 2528 + "@types/react": "*", 2529 + "@types/react-dom": "*", 2530 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2531 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2532 + }, 2533 + "peerDependenciesMeta": { 2534 + "@types/react": { 2535 + "optional": true 2536 + }, 2537 + "@types/react-dom": { 2538 + "optional": true 2539 + } 2540 + } 2541 + }, 2542 + "node_modules/@radix-ui/react-navigation-menu": { 2543 + "version": "1.2.14", 2544 + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", 2545 + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", 2546 + "dependencies": { 2547 + "@radix-ui/primitive": "1.1.3", 2548 + "@radix-ui/react-collection": "1.1.7", 2549 + "@radix-ui/react-compose-refs": "1.1.2", 2550 + "@radix-ui/react-context": "1.1.2", 2551 + "@radix-ui/react-direction": "1.1.1", 2552 + "@radix-ui/react-dismissable-layer": "1.1.11", 2553 + "@radix-ui/react-id": "1.1.1", 2554 + "@radix-ui/react-presence": "1.1.5", 2555 + "@radix-ui/react-primitive": "2.1.3", 2556 + "@radix-ui/react-use-callback-ref": "1.1.1", 2557 + "@radix-ui/react-use-controllable-state": "1.2.2", 2558 + "@radix-ui/react-use-layout-effect": "1.1.1", 2559 + "@radix-ui/react-use-previous": "1.1.1", 2560 + "@radix-ui/react-visually-hidden": "1.2.3" 2561 + }, 2562 + "peerDependencies": { 2563 + "@types/react": "*", 2564 + "@types/react-dom": "*", 2565 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2566 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2567 + }, 2568 + "peerDependenciesMeta": { 2569 + "@types/react": { 2570 + "optional": true 2571 + }, 2572 + "@types/react-dom": { 2573 + "optional": true 2574 + } 2575 + } 2576 + }, 2577 + "node_modules/@radix-ui/react-one-time-password-field": { 2578 + "version": "0.1.8", 2579 + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", 2580 + "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", 2581 + "dependencies": { 2582 + "@radix-ui/number": "1.1.1", 2583 + "@radix-ui/primitive": "1.1.3", 2584 + "@radix-ui/react-collection": "1.1.7", 2585 + "@radix-ui/react-compose-refs": "1.1.2", 2586 + "@radix-ui/react-context": "1.1.2", 2587 + "@radix-ui/react-direction": "1.1.1", 2588 + "@radix-ui/react-primitive": "2.1.3", 2589 + "@radix-ui/react-roving-focus": "1.1.11", 2590 + "@radix-ui/react-use-controllable-state": "1.2.2", 2591 + "@radix-ui/react-use-effect-event": "0.0.2", 2592 + "@radix-ui/react-use-is-hydrated": "0.1.0", 2593 + "@radix-ui/react-use-layout-effect": "1.1.1" 2594 + }, 2595 + "peerDependencies": { 2596 + "@types/react": "*", 2597 + "@types/react-dom": "*", 2598 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2599 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2600 + }, 2601 + "peerDependenciesMeta": { 2602 + "@types/react": { 2603 + "optional": true 2604 + }, 2605 + "@types/react-dom": { 2606 + "optional": true 2607 + } 2608 + } 2609 + }, 2610 + "node_modules/@radix-ui/react-password-toggle-field": { 2611 + "version": "0.1.3", 2612 + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", 2613 + "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", 2614 + "dependencies": { 2615 + "@radix-ui/primitive": "1.1.3", 2616 + "@radix-ui/react-compose-refs": "1.1.2", 2617 + "@radix-ui/react-context": "1.1.2", 2618 + "@radix-ui/react-id": "1.1.1", 2619 + "@radix-ui/react-primitive": "2.1.3", 2620 + "@radix-ui/react-use-controllable-state": "1.2.2", 2621 + "@radix-ui/react-use-effect-event": "0.0.2", 2622 + "@radix-ui/react-use-is-hydrated": "0.1.0" 2623 + }, 2624 + "peerDependencies": { 2625 + "@types/react": "*", 2626 + "@types/react-dom": "*", 2627 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2628 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2629 + }, 2630 + "peerDependenciesMeta": { 2631 + "@types/react": { 2632 + "optional": true 2633 + }, 2634 + "@types/react-dom": { 2635 + "optional": true 2636 + } 2637 + } 2638 + }, 2639 + "node_modules/@radix-ui/react-popover": { 2640 + "version": "1.1.15", 2641 + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", 2642 + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", 2643 + "dependencies": { 2644 + "@radix-ui/primitive": "1.1.3", 2645 + "@radix-ui/react-compose-refs": "1.1.2", 2646 + "@radix-ui/react-context": "1.1.2", 2647 + "@radix-ui/react-dismissable-layer": "1.1.11", 2648 + "@radix-ui/react-focus-guards": "1.1.3", 2649 + "@radix-ui/react-focus-scope": "1.1.7", 2650 + "@radix-ui/react-id": "1.1.1", 2651 + "@radix-ui/react-popper": "1.2.8", 2652 + "@radix-ui/react-portal": "1.1.9", 2653 + "@radix-ui/react-presence": "1.1.5", 2654 + "@radix-ui/react-primitive": "2.1.3", 2655 + "@radix-ui/react-slot": "1.2.3", 2656 + "@radix-ui/react-use-controllable-state": "1.2.2", 2657 + "aria-hidden": "^1.2.4", 2658 + "react-remove-scroll": "^2.6.3" 2659 + }, 2660 + "peerDependencies": { 2661 + "@types/react": "*", 2662 + "@types/react-dom": "*", 2663 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2664 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2665 + }, 2666 + "peerDependenciesMeta": { 2667 + "@types/react": { 2668 + "optional": true 2669 + }, 2670 + "@types/react-dom": { 2671 + "optional": true 2672 + } 2673 + } 2674 + }, 2675 + "node_modules/@radix-ui/react-popper": { 2676 + "version": "1.2.8", 2677 + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", 2678 + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", 2679 + "dependencies": { 2680 + "@floating-ui/react-dom": "^2.0.0", 2681 + "@radix-ui/react-arrow": "1.1.7", 2682 + "@radix-ui/react-compose-refs": "1.1.2", 2683 + "@radix-ui/react-context": "1.1.2", 2684 + "@radix-ui/react-primitive": "2.1.3", 2685 + "@radix-ui/react-use-callback-ref": "1.1.1", 2686 + "@radix-ui/react-use-layout-effect": "1.1.1", 2687 + "@radix-ui/react-use-rect": "1.1.1", 2688 + "@radix-ui/react-use-size": "1.1.1", 2689 + "@radix-ui/rect": "1.1.1" 2690 + }, 2691 + "peerDependencies": { 2692 + "@types/react": "*", 2693 + "@types/react-dom": "*", 2694 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2695 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2696 + }, 2697 + "peerDependenciesMeta": { 2698 + "@types/react": { 2699 + "optional": true 2700 + }, 2701 + "@types/react-dom": { 2702 + "optional": true 2703 + } 2704 + } 2705 + }, 2706 + "node_modules/@radix-ui/react-portal": { 2707 + "version": "1.1.9", 2708 + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", 2709 + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", 2710 + "dependencies": { 2711 + "@radix-ui/react-primitive": "2.1.3", 2712 + "@radix-ui/react-use-layout-effect": "1.1.1" 2713 + }, 2714 + "peerDependencies": { 2715 + "@types/react": "*", 2716 + "@types/react-dom": "*", 2717 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2718 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2719 + }, 2720 + "peerDependenciesMeta": { 2721 + "@types/react": { 2722 + "optional": true 2723 + }, 2724 + "@types/react-dom": { 2725 + "optional": true 2726 + } 2727 + } 2728 + }, 2729 + "node_modules/@radix-ui/react-presence": { 2730 + "version": "1.1.5", 2731 + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", 2732 + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", 2733 + "dependencies": { 2734 + "@radix-ui/react-compose-refs": "1.1.2", 2735 + "@radix-ui/react-use-layout-effect": "1.1.1" 2736 + }, 2737 + "peerDependencies": { 2738 + "@types/react": "*", 2739 + "@types/react-dom": "*", 2740 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2741 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2742 + }, 2743 + "peerDependenciesMeta": { 2744 + "@types/react": { 2745 + "optional": true 2746 + }, 2747 + "@types/react-dom": { 2748 + "optional": true 2749 + } 2750 + } 2751 + }, 2752 + "node_modules/@radix-ui/react-primitive": { 2753 + "version": "2.1.3", 2754 + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", 2755 + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", 2756 + "dependencies": { 2757 + "@radix-ui/react-slot": "1.2.3" 2758 + }, 2759 + "peerDependencies": { 2760 + "@types/react": "*", 2761 + "@types/react-dom": "*", 2762 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2763 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2764 + }, 2765 + "peerDependenciesMeta": { 2766 + "@types/react": { 2767 + "optional": true 2768 + }, 2769 + "@types/react-dom": { 2770 + "optional": true 2771 + } 2772 + } 2773 + }, 2774 + "node_modules/@radix-ui/react-progress": { 2775 + "version": "1.1.7", 2776 + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", 2777 + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", 2778 + "dependencies": { 2779 + "@radix-ui/react-context": "1.1.2", 2780 + "@radix-ui/react-primitive": "2.1.3" 2781 + }, 2782 + "peerDependencies": { 2783 + "@types/react": "*", 2784 + "@types/react-dom": "*", 2785 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2786 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2787 + }, 2788 + "peerDependenciesMeta": { 2789 + "@types/react": { 2790 + "optional": true 2791 + }, 2792 + "@types/react-dom": { 2793 + "optional": true 2794 + } 2795 + } 2796 + }, 2797 + "node_modules/@radix-ui/react-radio-group": { 2798 + "version": "1.3.8", 2799 + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", 2800 + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", 2801 + "dependencies": { 2802 + "@radix-ui/primitive": "1.1.3", 2803 + "@radix-ui/react-compose-refs": "1.1.2", 2804 + "@radix-ui/react-context": "1.1.2", 2805 + "@radix-ui/react-direction": "1.1.1", 2806 + "@radix-ui/react-presence": "1.1.5", 2807 + "@radix-ui/react-primitive": "2.1.3", 2808 + "@radix-ui/react-roving-focus": "1.1.11", 2809 + "@radix-ui/react-use-controllable-state": "1.2.2", 2810 + "@radix-ui/react-use-previous": "1.1.1", 2811 + "@radix-ui/react-use-size": "1.1.1" 2812 + }, 2813 + "peerDependencies": { 2814 + "@types/react": "*", 2815 + "@types/react-dom": "*", 2816 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2817 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2818 + }, 2819 + "peerDependenciesMeta": { 2820 + "@types/react": { 2821 + "optional": true 2822 + }, 2823 + "@types/react-dom": { 2824 + "optional": true 2825 + } 2826 + } 2827 + }, 2828 + "node_modules/@radix-ui/react-roving-focus": { 2829 + "version": "1.1.11", 2830 + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", 2831 + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", 2832 + "dependencies": { 2833 + "@radix-ui/primitive": "1.1.3", 2834 + "@radix-ui/react-collection": "1.1.7", 2835 + "@radix-ui/react-compose-refs": "1.1.2", 2836 + "@radix-ui/react-context": "1.1.2", 2837 + "@radix-ui/react-direction": "1.1.1", 2838 + "@radix-ui/react-id": "1.1.1", 2839 + "@radix-ui/react-primitive": "2.1.3", 2840 + "@radix-ui/react-use-callback-ref": "1.1.1", 2841 + "@radix-ui/react-use-controllable-state": "1.2.2" 2842 + }, 2843 + "peerDependencies": { 2844 + "@types/react": "*", 2845 + "@types/react-dom": "*", 2846 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2847 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2848 + }, 2849 + "peerDependenciesMeta": { 2850 + "@types/react": { 2851 + "optional": true 2852 + }, 2853 + "@types/react-dom": { 2854 + "optional": true 2855 + } 2856 + } 2857 + }, 2858 + "node_modules/@radix-ui/react-scroll-area": { 2859 + "version": "1.2.10", 2860 + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", 2861 + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", 2862 + "dependencies": { 2863 + "@radix-ui/number": "1.1.1", 2864 + "@radix-ui/primitive": "1.1.3", 2865 + "@radix-ui/react-compose-refs": "1.1.2", 2866 + "@radix-ui/react-context": "1.1.2", 2867 + "@radix-ui/react-direction": "1.1.1", 2868 + "@radix-ui/react-presence": "1.1.5", 2869 + "@radix-ui/react-primitive": "2.1.3", 2870 + "@radix-ui/react-use-callback-ref": "1.1.1", 2871 + "@radix-ui/react-use-layout-effect": "1.1.1" 2872 + }, 2873 + "peerDependencies": { 2874 + "@types/react": "*", 2875 + "@types/react-dom": "*", 2876 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2877 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2878 + }, 2879 + "peerDependenciesMeta": { 2880 + "@types/react": { 2881 + "optional": true 2882 + }, 2883 + "@types/react-dom": { 2884 + "optional": true 2885 + } 2886 + } 2887 + }, 2888 + "node_modules/@radix-ui/react-select": { 2889 + "version": "2.2.6", 2890 + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", 2891 + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", 2892 + "dependencies": { 2893 + "@radix-ui/number": "1.1.1", 2894 + "@radix-ui/primitive": "1.1.3", 2895 + "@radix-ui/react-collection": "1.1.7", 2896 + "@radix-ui/react-compose-refs": "1.1.2", 2897 + "@radix-ui/react-context": "1.1.2", 2898 + "@radix-ui/react-direction": "1.1.1", 2899 + "@radix-ui/react-dismissable-layer": "1.1.11", 2900 + "@radix-ui/react-focus-guards": "1.1.3", 2901 + "@radix-ui/react-focus-scope": "1.1.7", 2902 + "@radix-ui/react-id": "1.1.1", 2903 + "@radix-ui/react-popper": "1.2.8", 2904 + "@radix-ui/react-portal": "1.1.9", 2905 + "@radix-ui/react-primitive": "2.1.3", 2906 + "@radix-ui/react-slot": "1.2.3", 2907 + "@radix-ui/react-use-callback-ref": "1.1.1", 2908 + "@radix-ui/react-use-controllable-state": "1.2.2", 2909 + "@radix-ui/react-use-layout-effect": "1.1.1", 2910 + "@radix-ui/react-use-previous": "1.1.1", 2911 + "@radix-ui/react-visually-hidden": "1.2.3", 2912 + "aria-hidden": "^1.2.4", 2913 + "react-remove-scroll": "^2.6.3" 2914 + }, 2915 + "peerDependencies": { 2916 + "@types/react": "*", 2917 + "@types/react-dom": "*", 2918 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2919 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2920 + }, 2921 + "peerDependenciesMeta": { 2922 + "@types/react": { 2923 + "optional": true 2924 + }, 2925 + "@types/react-dom": { 2926 + "optional": true 2927 + } 2928 + } 2929 + }, 2930 + "node_modules/@radix-ui/react-separator": { 2931 + "version": "1.1.7", 2932 + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", 2933 + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", 2934 + "dependencies": { 2935 + "@radix-ui/react-primitive": "2.1.3" 2936 + }, 2937 + "peerDependencies": { 2938 + "@types/react": "*", 2939 + "@types/react-dom": "*", 2940 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2941 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2942 + }, 2943 + "peerDependenciesMeta": { 2944 + "@types/react": { 2945 + "optional": true 2946 + }, 2947 + "@types/react-dom": { 2948 + "optional": true 2949 + } 2950 + } 2951 + }, 2952 + "node_modules/@radix-ui/react-slider": { 2953 + "version": "1.3.6", 2954 + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", 2955 + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", 2956 + "dependencies": { 2957 + "@radix-ui/number": "1.1.1", 2958 + "@radix-ui/primitive": "1.1.3", 2959 + "@radix-ui/react-collection": "1.1.7", 2960 + "@radix-ui/react-compose-refs": "1.1.2", 2961 + "@radix-ui/react-context": "1.1.2", 2962 + "@radix-ui/react-direction": "1.1.1", 2963 + "@radix-ui/react-primitive": "2.1.3", 2964 + "@radix-ui/react-use-controllable-state": "1.2.2", 2965 + "@radix-ui/react-use-layout-effect": "1.1.1", 2966 + "@radix-ui/react-use-previous": "1.1.1", 2967 + "@radix-ui/react-use-size": "1.1.1" 2968 + }, 2969 + "peerDependencies": { 2970 + "@types/react": "*", 2971 + "@types/react-dom": "*", 2972 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 2973 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2974 + }, 2975 + "peerDependenciesMeta": { 2976 + "@types/react": { 2977 + "optional": true 2978 + }, 2979 + "@types/react-dom": { 2980 + "optional": true 2981 + } 2982 + } 2983 + }, 2984 + "node_modules/@radix-ui/react-slot": { 2985 + "version": "1.2.3", 2986 + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", 2987 + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", 2988 + "dependencies": { 2989 + "@radix-ui/react-compose-refs": "1.1.2" 2990 + }, 2991 + "peerDependencies": { 2992 + "@types/react": "*", 2993 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 2994 + }, 2995 + "peerDependenciesMeta": { 2996 + "@types/react": { 2997 + "optional": true 2998 + } 2999 + } 3000 + }, 3001 + "node_modules/@radix-ui/react-switch": { 3002 + "version": "1.2.6", 3003 + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", 3004 + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", 3005 + "dependencies": { 3006 + "@radix-ui/primitive": "1.1.3", 3007 + "@radix-ui/react-compose-refs": "1.1.2", 3008 + "@radix-ui/react-context": "1.1.2", 3009 + "@radix-ui/react-primitive": "2.1.3", 3010 + "@radix-ui/react-use-controllable-state": "1.2.2", 3011 + "@radix-ui/react-use-previous": "1.1.1", 3012 + "@radix-ui/react-use-size": "1.1.1" 3013 + }, 3014 + "peerDependencies": { 3015 + "@types/react": "*", 3016 + "@types/react-dom": "*", 3017 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 3018 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3019 + }, 3020 + "peerDependenciesMeta": { 3021 + "@types/react": { 3022 + "optional": true 3023 + }, 3024 + "@types/react-dom": { 3025 + "optional": true 3026 + } 3027 + } 3028 + }, 3029 + "node_modules/@radix-ui/react-tabs": { 3030 + "version": "1.1.13", 3031 + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", 3032 + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", 3033 + "dependencies": { 3034 + "@radix-ui/primitive": "1.1.3", 3035 + "@radix-ui/react-context": "1.1.2", 3036 + "@radix-ui/react-direction": "1.1.1", 3037 + "@radix-ui/react-id": "1.1.1", 3038 + "@radix-ui/react-presence": "1.1.5", 3039 + "@radix-ui/react-primitive": "2.1.3", 3040 + "@radix-ui/react-roving-focus": "1.1.11", 3041 + "@radix-ui/react-use-controllable-state": "1.2.2" 3042 + }, 3043 + "peerDependencies": { 3044 + "@types/react": "*", 3045 + "@types/react-dom": "*", 3046 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 3047 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3048 + }, 3049 + "peerDependenciesMeta": { 3050 + "@types/react": { 3051 + "optional": true 3052 + }, 3053 + "@types/react-dom": { 3054 + "optional": true 3055 + } 3056 + } 3057 + }, 3058 + "node_modules/@radix-ui/react-toast": { 3059 + "version": "1.2.15", 3060 + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", 3061 + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", 3062 + "dependencies": { 3063 + "@radix-ui/primitive": "1.1.3", 3064 + "@radix-ui/react-collection": "1.1.7", 3065 + "@radix-ui/react-compose-refs": "1.1.2", 3066 + "@radix-ui/react-context": "1.1.2", 3067 + "@radix-ui/react-dismissable-layer": "1.1.11", 3068 + "@radix-ui/react-portal": "1.1.9", 3069 + "@radix-ui/react-presence": "1.1.5", 3070 + "@radix-ui/react-primitive": "2.1.3", 3071 + "@radix-ui/react-use-callback-ref": "1.1.1", 3072 + "@radix-ui/react-use-controllable-state": "1.2.2", 3073 + "@radix-ui/react-use-layout-effect": "1.1.1", 3074 + "@radix-ui/react-visually-hidden": "1.2.3" 3075 + }, 3076 + "peerDependencies": { 3077 + "@types/react": "*", 3078 + "@types/react-dom": "*", 3079 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 3080 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3081 + }, 3082 + "peerDependenciesMeta": { 3083 + "@types/react": { 3084 + "optional": true 3085 + }, 3086 + "@types/react-dom": { 3087 + "optional": true 3088 + } 3089 + } 3090 + }, 3091 + "node_modules/@radix-ui/react-toggle": { 3092 + "version": "1.1.10", 3093 + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", 3094 + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", 3095 + "dependencies": { 3096 + "@radix-ui/primitive": "1.1.3", 3097 + "@radix-ui/react-primitive": "2.1.3", 3098 + "@radix-ui/react-use-controllable-state": "1.2.2" 3099 + }, 3100 + "peerDependencies": { 3101 + "@types/react": "*", 3102 + "@types/react-dom": "*", 3103 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 3104 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3105 + }, 3106 + "peerDependenciesMeta": { 3107 + "@types/react": { 3108 + "optional": true 3109 + }, 3110 + "@types/react-dom": { 3111 + "optional": true 3112 + } 3113 + } 3114 + }, 3115 + "node_modules/@radix-ui/react-toggle-group": { 3116 + "version": "1.1.11", 3117 + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", 3118 + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", 3119 + "dependencies": { 3120 + "@radix-ui/primitive": "1.1.3", 3121 + "@radix-ui/react-context": "1.1.2", 3122 + "@radix-ui/react-direction": "1.1.1", 3123 + "@radix-ui/react-primitive": "2.1.3", 3124 + "@radix-ui/react-roving-focus": "1.1.11", 3125 + "@radix-ui/react-toggle": "1.1.10", 3126 + "@radix-ui/react-use-controllable-state": "1.2.2" 3127 + }, 3128 + "peerDependencies": { 3129 + "@types/react": "*", 3130 + "@types/react-dom": "*", 3131 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 3132 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3133 + }, 3134 + "peerDependenciesMeta": { 3135 + "@types/react": { 3136 + "optional": true 3137 + }, 3138 + "@types/react-dom": { 3139 + "optional": true 3140 + } 3141 + } 3142 + }, 3143 + "node_modules/@radix-ui/react-toolbar": { 3144 + "version": "1.1.11", 3145 + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", 3146 + "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", 3147 + "dependencies": { 3148 + "@radix-ui/primitive": "1.1.3", 3149 + "@radix-ui/react-context": "1.1.2", 3150 + "@radix-ui/react-direction": "1.1.1", 3151 + "@radix-ui/react-primitive": "2.1.3", 3152 + "@radix-ui/react-roving-focus": "1.1.11", 3153 + "@radix-ui/react-separator": "1.1.7", 3154 + "@radix-ui/react-toggle-group": "1.1.11" 3155 + }, 3156 + "peerDependencies": { 3157 + "@types/react": "*", 3158 + "@types/react-dom": "*", 3159 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 3160 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3161 + }, 3162 + "peerDependenciesMeta": { 3163 + "@types/react": { 3164 + "optional": true 3165 + }, 3166 + "@types/react-dom": { 3167 + "optional": true 3168 + } 3169 + } 3170 + }, 3171 + "node_modules/@radix-ui/react-tooltip": { 3172 + "version": "1.2.8", 3173 + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", 3174 + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", 3175 + "dependencies": { 3176 + "@radix-ui/primitive": "1.1.3", 3177 + "@radix-ui/react-compose-refs": "1.1.2", 3178 + "@radix-ui/react-context": "1.1.2", 3179 + "@radix-ui/react-dismissable-layer": "1.1.11", 3180 + "@radix-ui/react-id": "1.1.1", 3181 + "@radix-ui/react-popper": "1.2.8", 3182 + "@radix-ui/react-portal": "1.1.9", 3183 + "@radix-ui/react-presence": "1.1.5", 3184 + "@radix-ui/react-primitive": "2.1.3", 3185 + "@radix-ui/react-slot": "1.2.3", 3186 + "@radix-ui/react-use-controllable-state": "1.2.2", 3187 + "@radix-ui/react-visually-hidden": "1.2.3" 3188 + }, 3189 + "peerDependencies": { 3190 + "@types/react": "*", 3191 + "@types/react-dom": "*", 3192 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 3193 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3194 + }, 3195 + "peerDependenciesMeta": { 3196 + "@types/react": { 3197 + "optional": true 3198 + }, 3199 + "@types/react-dom": { 3200 + "optional": true 3201 + } 3202 + } 3203 + }, 3204 + "node_modules/@radix-ui/react-use-callback-ref": { 3205 + "version": "1.1.1", 3206 + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", 3207 + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", 3208 + "peerDependencies": { 3209 + "@types/react": "*", 3210 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3211 + }, 3212 + "peerDependenciesMeta": { 3213 + "@types/react": { 3214 + "optional": true 3215 + } 3216 + } 3217 + }, 3218 + "node_modules/@radix-ui/react-use-controllable-state": { 3219 + "version": "1.2.2", 3220 + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", 3221 + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", 3222 + "dependencies": { 3223 + "@radix-ui/react-use-effect-event": "0.0.2", 3224 + "@radix-ui/react-use-layout-effect": "1.1.1" 3225 + }, 3226 + "peerDependencies": { 3227 + "@types/react": "*", 3228 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3229 + }, 3230 + "peerDependenciesMeta": { 3231 + "@types/react": { 3232 + "optional": true 3233 + } 3234 + } 3235 + }, 3236 + "node_modules/@radix-ui/react-use-effect-event": { 3237 + "version": "0.0.2", 3238 + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", 3239 + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", 3240 + "dependencies": { 3241 + "@radix-ui/react-use-layout-effect": "1.1.1" 3242 + }, 3243 + "peerDependencies": { 3244 + "@types/react": "*", 3245 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3246 + }, 3247 + "peerDependenciesMeta": { 3248 + "@types/react": { 3249 + "optional": true 3250 + } 3251 + } 3252 + }, 3253 + "node_modules/@radix-ui/react-use-escape-keydown": { 3254 + "version": "1.1.1", 3255 + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", 3256 + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", 3257 + "dependencies": { 3258 + "@radix-ui/react-use-callback-ref": "1.1.1" 3259 + }, 3260 + "peerDependencies": { 3261 + "@types/react": "*", 3262 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3263 + }, 3264 + "peerDependenciesMeta": { 3265 + "@types/react": { 3266 + "optional": true 3267 + } 3268 + } 3269 + }, 3270 + "node_modules/@radix-ui/react-use-is-hydrated": { 3271 + "version": "0.1.0", 3272 + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", 3273 + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", 3274 + "dependencies": { 3275 + "use-sync-external-store": "^1.5.0" 3276 + }, 3277 + "peerDependencies": { 3278 + "@types/react": "*", 3279 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3280 + }, 3281 + "peerDependenciesMeta": { 3282 + "@types/react": { 3283 + "optional": true 3284 + } 3285 + } 3286 + }, 3287 + "node_modules/@radix-ui/react-use-layout-effect": { 3288 + "version": "1.1.1", 3289 + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", 3290 + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", 3291 + "peerDependencies": { 3292 + "@types/react": "*", 3293 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3294 + }, 3295 + "peerDependenciesMeta": { 3296 + "@types/react": { 3297 + "optional": true 3298 + } 3299 + } 3300 + }, 3301 + "node_modules/@radix-ui/react-use-previous": { 3302 + "version": "1.1.1", 3303 + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", 3304 + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", 3305 + "peerDependencies": { 3306 + "@types/react": "*", 3307 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3308 + }, 3309 + "peerDependenciesMeta": { 3310 + "@types/react": { 3311 + "optional": true 3312 + } 3313 + } 3314 + }, 3315 + "node_modules/@radix-ui/react-use-rect": { 3316 + "version": "1.1.1", 3317 + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", 3318 + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", 3319 + "dependencies": { 3320 + "@radix-ui/rect": "1.1.1" 3321 + }, 3322 + "peerDependencies": { 3323 + "@types/react": "*", 3324 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3325 + }, 3326 + "peerDependenciesMeta": { 3327 + "@types/react": { 3328 + "optional": true 3329 + } 3330 + } 3331 + }, 3332 + "node_modules/@radix-ui/react-use-size": { 3333 + "version": "1.1.1", 3334 + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", 3335 + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", 3336 + "dependencies": { 3337 + "@radix-ui/react-use-layout-effect": "1.1.1" 3338 + }, 3339 + "peerDependencies": { 3340 + "@types/react": "*", 3341 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3342 + }, 3343 + "peerDependenciesMeta": { 3344 + "@types/react": { 3345 + "optional": true 3346 + } 3347 + } 3348 + }, 3349 + "node_modules/@radix-ui/react-visually-hidden": { 3350 + "version": "1.2.3", 3351 + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", 3352 + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", 3353 + "dependencies": { 3354 + "@radix-ui/react-primitive": "2.1.3" 3355 + }, 3356 + "peerDependencies": { 3357 + "@types/react": "*", 3358 + "@types/react-dom": "*", 3359 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 3360 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3361 + }, 3362 + "peerDependenciesMeta": { 3363 + "@types/react": { 3364 + "optional": true 3365 + }, 3366 + "@types/react-dom": { 3367 + "optional": true 3368 + } 3369 + } 3370 + }, 3371 + "node_modules/@radix-ui/rect": { 3372 + "version": "1.1.1", 3373 + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", 3374 + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==" 3375 + }, 3376 "node_modules/@rolldown/pluginutils": { 3377 "version": "1.0.0-beta.27", 3378 "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", ··· 5284 "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 5285 "dev": true, 5286 "license": "Python-2.0" 5287 + }, 5288 + "node_modules/aria-hidden": { 5289 + "version": "1.2.6", 5290 + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", 5291 + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", 5292 + "dependencies": { 5293 + "tslib": "^2.0.0" 5294 + }, 5295 + "engines": { 5296 + "node": ">=10" 5297 + } 5298 }, 5299 "node_modules/aria-query": { 5300 "version": "5.3.0", ··· 6205 "node": ">=8" 6206 } 6207 }, 6208 + "node_modules/detect-node-es": { 6209 + "version": "1.1.0", 6210 + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", 6211 + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" 6212 + }, 6213 "node_modules/diff": { 6214 "version": "8.0.2", 6215 "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", ··· 7229 }, 7230 "funding": { 7231 "url": "https://github.com/sponsors/ljharb" 7232 + } 7233 + }, 7234 + "node_modules/get-nonce": { 7235 + "version": "1.0.1", 7236 + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", 7237 + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", 7238 + "engines": { 7239 + "node": ">=6" 7240 } 7241 }, 7242 "node_modules/get-proto": { ··· 11840 ], 11841 "license": "MIT" 11842 }, 11843 + "node_modules/radix-ui": { 11844 + "version": "1.4.3", 11845 + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", 11846 + "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", 11847 + "dependencies": { 11848 + "@radix-ui/primitive": "1.1.3", 11849 + "@radix-ui/react-accessible-icon": "1.1.7", 11850 + "@radix-ui/react-accordion": "1.2.12", 11851 + "@radix-ui/react-alert-dialog": "1.1.15", 11852 + "@radix-ui/react-arrow": "1.1.7", 11853 + "@radix-ui/react-aspect-ratio": "1.1.7", 11854 + "@radix-ui/react-avatar": "1.1.10", 11855 + "@radix-ui/react-checkbox": "1.3.3", 11856 + "@radix-ui/react-collapsible": "1.1.12", 11857 + "@radix-ui/react-collection": "1.1.7", 11858 + "@radix-ui/react-compose-refs": "1.1.2", 11859 + "@radix-ui/react-context": "1.1.2", 11860 + "@radix-ui/react-context-menu": "2.2.16", 11861 + "@radix-ui/react-dialog": "1.1.15", 11862 + "@radix-ui/react-direction": "1.1.1", 11863 + "@radix-ui/react-dismissable-layer": "1.1.11", 11864 + "@radix-ui/react-dropdown-menu": "2.1.16", 11865 + "@radix-ui/react-focus-guards": "1.1.3", 11866 + "@radix-ui/react-focus-scope": "1.1.7", 11867 + "@radix-ui/react-form": "0.1.8", 11868 + "@radix-ui/react-hover-card": "1.1.15", 11869 + "@radix-ui/react-label": "2.1.7", 11870 + "@radix-ui/react-menu": "2.1.16", 11871 + "@radix-ui/react-menubar": "1.1.16", 11872 + "@radix-ui/react-navigation-menu": "1.2.14", 11873 + "@radix-ui/react-one-time-password-field": "0.1.8", 11874 + "@radix-ui/react-password-toggle-field": "0.1.3", 11875 + "@radix-ui/react-popover": "1.1.15", 11876 + "@radix-ui/react-popper": "1.2.8", 11877 + "@radix-ui/react-portal": "1.1.9", 11878 + "@radix-ui/react-presence": "1.1.5", 11879 + "@radix-ui/react-primitive": "2.1.3", 11880 + "@radix-ui/react-progress": "1.1.7", 11881 + "@radix-ui/react-radio-group": "1.3.8", 11882 + "@radix-ui/react-roving-focus": "1.1.11", 11883 + "@radix-ui/react-scroll-area": "1.2.10", 11884 + "@radix-ui/react-select": "2.2.6", 11885 + "@radix-ui/react-separator": "1.1.7", 11886 + "@radix-ui/react-slider": "1.3.6", 11887 + "@radix-ui/react-slot": "1.2.3", 11888 + "@radix-ui/react-switch": "1.2.6", 11889 + "@radix-ui/react-tabs": "1.1.13", 11890 + "@radix-ui/react-toast": "1.2.15", 11891 + "@radix-ui/react-toggle": "1.1.10", 11892 + "@radix-ui/react-toggle-group": "1.1.11", 11893 + "@radix-ui/react-toolbar": "1.1.11", 11894 + "@radix-ui/react-tooltip": "1.2.8", 11895 + "@radix-ui/react-use-callback-ref": "1.1.1", 11896 + "@radix-ui/react-use-controllable-state": "1.2.2", 11897 + "@radix-ui/react-use-effect-event": "0.0.2", 11898 + "@radix-ui/react-use-escape-keydown": "1.1.1", 11899 + "@radix-ui/react-use-is-hydrated": "0.1.0", 11900 + "@radix-ui/react-use-layout-effect": "1.1.1", 11901 + "@radix-ui/react-use-size": "1.1.1", 11902 + "@radix-ui/react-visually-hidden": "1.2.3" 11903 + }, 11904 + "peerDependencies": { 11905 + "@types/react": "*", 11906 + "@types/react-dom": "*", 11907 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 11908 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 11909 + }, 11910 + "peerDependenciesMeta": { 11911 + "@types/react": { 11912 + "optional": true 11913 + }, 11914 + "@types/react-dom": { 11915 + "optional": true 11916 + } 11917 + } 11918 + }, 11919 "node_modules/react": { 11920 "version": "19.1.1", 11921 "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", ··· 11977 "node": ">=0.10.0" 11978 } 11979 }, 11980 + "node_modules/react-remove-scroll": { 11981 + "version": "2.7.1", 11982 + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", 11983 + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", 11984 + "dependencies": { 11985 + "react-remove-scroll-bar": "^2.3.7", 11986 + "react-style-singleton": "^2.2.3", 11987 + "tslib": "^2.1.0", 11988 + "use-callback-ref": "^1.3.3", 11989 + "use-sidecar": "^1.1.3" 11990 + }, 11991 + "engines": { 11992 + "node": ">=10" 11993 + }, 11994 + "peerDependencies": { 11995 + "@types/react": "*", 11996 + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" 11997 + }, 11998 + "peerDependenciesMeta": { 11999 + "@types/react": { 12000 + "optional": true 12001 + } 12002 + } 12003 + }, 12004 + "node_modules/react-remove-scroll-bar": { 12005 + "version": "2.3.8", 12006 + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", 12007 + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", 12008 + "dependencies": { 12009 + "react-style-singleton": "^2.2.2", 12010 + "tslib": "^2.0.0" 12011 + }, 12012 + "engines": { 12013 + "node": ">=10" 12014 + }, 12015 + "peerDependencies": { 12016 + "@types/react": "*", 12017 + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" 12018 + }, 12019 + "peerDependenciesMeta": { 12020 + "@types/react": { 12021 + "optional": true 12022 + } 12023 + } 12024 + }, 12025 + "node_modules/react-style-singleton": { 12026 + "version": "2.2.3", 12027 + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", 12028 + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", 12029 + "dependencies": { 12030 + "get-nonce": "^1.0.0", 12031 + "tslib": "^2.0.0" 12032 + }, 12033 + "engines": { 12034 + "node": ">=10" 12035 + }, 12036 + "peerDependencies": { 12037 + "@types/react": "*", 12038 + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" 12039 + }, 12040 + "peerDependenciesMeta": { 12041 + "@types/react": { 12042 + "optional": true 12043 + } 12044 + } 12045 + }, 12046 "node_modules/readdirp": { 12047 "version": "3.6.0", 12048 "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", ··· 13536 "peer": true, 13537 "dependencies": { 13538 "punycode": "^2.1.0" 13539 + } 13540 + }, 13541 + "node_modules/use-callback-ref": { 13542 + "version": "1.3.3", 13543 + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", 13544 + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", 13545 + "dependencies": { 13546 + "tslib": "^2.0.0" 13547 + }, 13548 + "engines": { 13549 + "node": ">=10" 13550 + }, 13551 + "peerDependencies": { 13552 + "@types/react": "*", 13553 + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" 13554 + }, 13555 + "peerDependenciesMeta": { 13556 + "@types/react": { 13557 + "optional": true 13558 + } 13559 + } 13560 + }, 13561 + "node_modules/use-sidecar": { 13562 + "version": "1.1.3", 13563 + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", 13564 + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", 13565 + "dependencies": { 13566 + "detect-node-es": "^1.1.0", 13567 + "tslib": "^2.0.0" 13568 + }, 13569 + "engines": { 13570 + "node": ">=10" 13571 + }, 13572 + "peerDependencies": { 13573 + "@types/react": "*", 13574 + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" 13575 + }, 13576 + "peerDependenciesMeta": { 13577 + "@types/react": { 13578 + "optional": true 13579 + } 13580 } 13581 }, 13582 "node_modules/use-sync-external-store": {
+5
package.json
··· 12 "dependencies": { 13 "@atproto/api": "^0.16.6", 14 "@atproto/oauth-client-browser": "^0.3.33", 15 "@tailwindcss/vite": "^4.0.6", 16 "@tanstack/query-sync-storage-persister": "^5.85.6", 17 "@tanstack/react-devtools": "^0.2.2", ··· 25 "idb-keyval": "^6.2.2", 26 "jotai": "^2.13.1", 27 "npm": "^11.6.2", 28 "react": "^19.0.0", 29 "react-dom": "^19.0.0", 30 "react-player": "^3.3.2",
··· 12 "dependencies": { 13 "@atproto/api": "^0.16.6", 14 "@atproto/oauth-client-browser": "^0.3.33", 15 + "@radix-ui/react-dialog": "^1.1.15", 16 + "@radix-ui/react-dropdown-menu": "^2.1.16", 17 + "@radix-ui/react-hover-card": "^1.1.15", 18 + "@radix-ui/react-slider": "^1.3.6", 19 "@tailwindcss/vite": "^4.0.6", 20 "@tanstack/query-sync-storage-persister": "^5.85.6", 21 "@tanstack/react-devtools": "^0.2.2", ··· 29 "idb-keyval": "^6.2.2", 30 "jotai": "^2.13.1", 31 "npm": "^11.6.2", 32 + "radix-ui": "^1.4.3", 33 "react": "^19.0.0", 34 "react-dom": "^19.0.0", 35 "react-player": "^3.3.2",
+339
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 { Button } from "./radix-m3-rd/Button"; 12 + import { UniversalPostRendererATURILoader } from "./UniversalPostRenderer"; 13 + 14 + const MAX_POST_LENGTH = 300; 15 + 16 + export function Composer() { 17 + const [composerState, setComposerState] = useAtom(composerAtom); 18 + const [closeConfirmState, setCloseConfirmState] = useState<boolean>(false); 19 + const { agent } = useAuth(); 20 + 21 + const [postText, setPostText] = useState(""); 22 + const [posting, setPosting] = useState(false); 23 + const [postSuccess, setPostSuccess] = useState(false); 24 + const [postError, setPostError] = useState<string | null>(null); 25 + 26 + useEffect(() => { 27 + setPostText(""); 28 + setPosting(false); 29 + setPostSuccess(false); 30 + setPostError(null); 31 + }, [composerState.kind]); 32 + 33 + const parentUri = 34 + composerState.kind === "reply" 35 + ? composerState.parent 36 + : composerState.kind === "quote" 37 + ? composerState.subject 38 + : undefined; 39 + 40 + const { data: parentPost, isLoading: isParentLoading } = 41 + useQueryPost(parentUri); 42 + 43 + async function handlePost() { 44 + if (!agent || !postText.trim() || postText.length > MAX_POST_LENGTH) return; 45 + 46 + setPosting(true); 47 + setPostError(null); 48 + 49 + try { 50 + const rt = new RichText({ text: postText }); 51 + await rt.detectFacets(agent); 52 + 53 + if (rt.facets?.length) { 54 + rt.facets = rt.facets.filter((item) => { 55 + if (item.$type !== "app.bsky.richtext.facet") return true; 56 + if (!item.features?.length) return true; 57 + 58 + item.features = item.features.filter((feature) => { 59 + if (feature.$type !== "app.bsky.richtext.facet#mention") return true; 60 + const did = feature.$type === "app.bsky.richtext.facet#mention" ? (feature as AppBskyRichtextFacet.Mention)?.did : undefined; 61 + return typeof did === "string" && did.startsWith("did:"); 62 + }); 63 + 64 + return item.features.length > 0; 65 + }); 66 + } 67 + 68 + const record: Record<string, unknown> = { 69 + $type: "app.bsky.feed.post", 70 + text: rt.text, 71 + facets: rt.facets, 72 + createdAt: new Date().toISOString(), 73 + }; 74 + 75 + if (composerState.kind === "reply" && parentPost) { 76 + record.reply = { 77 + root: parentPost.value?.reply?.root ?? { 78 + uri: parentPost.uri, 79 + cid: parentPost.cid, 80 + }, 81 + parent: { 82 + uri: parentPost.uri, 83 + cid: parentPost.cid, 84 + }, 85 + }; 86 + } 87 + 88 + if (composerState.kind === "quote" && parentPost) { 89 + record.embed = { 90 + $type: "app.bsky.embed.record", 91 + record: { 92 + uri: parentPost.uri, 93 + cid: parentPost.cid, 94 + }, 95 + }; 96 + } 97 + 98 + await agent.com.atproto.repo.createRecord({ 99 + collection: "app.bsky.feed.post", 100 + repo: agent.assertDid, 101 + record, 102 + }); 103 + 104 + setPostSuccess(true); 105 + setPostText(""); 106 + 107 + setTimeout(() => { 108 + setPostSuccess(false); 109 + setComposerState({ kind: "closed" }); 110 + }, 1500); 111 + } catch (e: any) { 112 + setPostError(e?.message || "Failed to post"); 113 + } finally { 114 + setPosting(false); 115 + } 116 + } 117 + 118 + const getPlaceholder = () => { 119 + switch (composerState.kind) { 120 + case "reply": 121 + return "Post your reply"; 122 + case "quote": 123 + return "Add a comment..."; 124 + case "root": 125 + default: 126 + return "What's happening?!"; 127 + } 128 + }; 129 + 130 + const charsLeft = MAX_POST_LENGTH - postText.length; 131 + const isPostButtonDisabled = 132 + posting || !postText.trim() || isParentLoading || charsLeft < 0; 133 + 134 + function handleAttemptClose() { 135 + if (postText.trim() && !posting) { 136 + setCloseConfirmState(true); 137 + } else { 138 + setComposerState({ kind: "closed" }); 139 + } 140 + } 141 + 142 + function handleConfirmClose() { 143 + setComposerState({ kind: "closed" }); 144 + setCloseConfirmState(false); 145 + setPostText(""); 146 + } 147 + 148 + return ( 149 + <> 150 + <Dialog.Root 151 + open={composerState.kind !== "closed"} 152 + onOpenChange={(open) => { 153 + if (!open) handleAttemptClose(); 154 + }} 155 + > 156 + <Dialog.Portal> 157 + <Dialog.Overlay className="disablegutter fixed inset-0 z-50 bg-black/40 dark:bg-black/50 data-[state=open]:animate-fadeIn" /> 158 + 159 + <Dialog.Content className="fixed overflow-y-auto gutter inset-0 z-50 flex items-start justify-center pt-10 sm:pt-20 pb-[50dvh] sm:pb-[50dvh]"> 160 + <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"> 161 + <div className="flex flex-row justify-between p-2"> 162 + <Dialog.Close asChild> 163 + <button 164 + 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" 165 + disabled={posting} 166 + aria-label="Close" 167 + onClick={handleAttemptClose} 168 + > 169 + <svg 170 + xmlns="http://www.w3.org/2000/svg" 171 + width="20" 172 + height="20" 173 + viewBox="0 0 24 24" 174 + fill="none" 175 + stroke="currentColor" 176 + strokeWidth="2.5" 177 + strokeLinecap="round" 178 + strokeLinejoin="round" 179 + > 180 + <line x1="18" y1="6" x2="6" y2="18"></line> 181 + <line x1="6" y1="6" x2="18" y2="18"></line> 182 + </svg> 183 + </button> 184 + </Dialog.Close> 185 + 186 + <div className="flex-1" /> 187 + <div className="flex items-center gap-4"> 188 + <span 189 + className={`text-sm ${charsLeft < 0 ? "text-red-500" : "text-gray-500"}`} 190 + > 191 + {charsLeft} 192 + </span> 193 + <Button 194 + onClick={handlePost} 195 + disabled={isPostButtonDisabled} 196 + > 197 + {posting ? "Posting..." : "Post"} 198 + </Button> 199 + </div> 200 + </div> 201 + 202 + {postSuccess ? ( 203 + <div className="flex flex-col items-center justify-center py-16"> 204 + <span className="text-gray-500 text-6xl mb-4">โœ“</span> 205 + <span className="text-xl font-bold text-black dark:text-white"> 206 + Posted! 207 + </span> 208 + </div> 209 + ) : ( 210 + <div className="px-4"> 211 + {composerState.kind === "reply" && ( 212 + <div className="mb-1 -mx-4"> 213 + {isParentLoading ? ( 214 + <div className="text-sm text-gray-500 animate-pulse"> 215 + Loading parent post... 216 + </div> 217 + ) : parentUri ? ( 218 + <UniversalPostRendererATURILoader 219 + atUri={parentUri} 220 + bottomReplyLine 221 + bottomBorder={false} 222 + /> 223 + ) : ( 224 + <div className="text-sm text-red-500 rounded-lg border border-red-500/50 p-3"> 225 + Could not load parent post. 226 + </div> 227 + )} 228 + </div> 229 + )} 230 + 231 + <div className="flex w-full gap-1 flex-col"> 232 + <ProfileThing agent={agent} large /> 233 + <div className="flex pl-[50px]"> 234 + <AutoGrowTextarea 235 + className="w-full text-lg bg-transparent focus:outline-none resize-none placeholder:text-gray-500 text-black dark:text-white pb-2" 236 + rows={5} 237 + placeholder={getPlaceholder()} 238 + value={postText} 239 + onChange={(e) => setPostText(e.target.value)} 240 + disabled={posting} 241 + autoFocus 242 + /> 243 + </div> 244 + </div> 245 + 246 + {composerState.kind === "quote" && ( 247 + <div className="mb-4 ml-[50px] rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden"> 248 + {isParentLoading ? ( 249 + <div className="text-sm text-gray-500 animate-pulse"> 250 + Loading parent post... 251 + </div> 252 + ) : parentUri ? ( 253 + <UniversalPostRendererATURILoader 254 + atUri={parentUri} 255 + isQuote 256 + /> 257 + ) : ( 258 + <div className="text-sm text-red-500 rounded-lg border border-red-500/50 p-3"> 259 + Could not load parent post. 260 + </div> 261 + )} 262 + </div> 263 + )} 264 + 265 + {postError && ( 266 + <div className="text-red-500 text-sm my-2 text-center"> 267 + {postError} 268 + </div> 269 + )} 270 + </div> 271 + )} 272 + </div> 273 + </Dialog.Content> 274 + </Dialog.Portal> 275 + </Dialog.Root> 276 + 277 + {/* Close confirmation dialog */} 278 + <Dialog.Root open={closeConfirmState} onOpenChange={setCloseConfirmState}> 279 + <Dialog.Portal> 280 + 281 + <Dialog.Overlay className="disablegutter fixed inset-0 z-50 bg-black/40 dark:bg-black/50 data-[state=open]:animate-fadeIn" /> 282 + 283 + <Dialog.Content className="fixed gutter inset-0 z-50 flex items-start justify-center pt-30 sm:pt-40"> 284 + <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-md relative mx-4 py-6"> 285 + <div className="text-xl mb-4 text-center"> 286 + Discard your post? 287 + </div> 288 + <div className="text-md mb-4 text-center"> 289 + You will lose your draft 290 + </div> 291 + <div className="flex justify-end gap-2 px-6"> 292 + <Button 293 + onClick={handleConfirmClose} 294 + > 295 + Discard 296 + </Button> 297 + <Button 298 + variant={"outlined"} 299 + onClick={() => setCloseConfirmState(false)} 300 + > 301 + Cancel 302 + </Button> 303 + </div> 304 + </div> 305 + </Dialog.Content> 306 + </Dialog.Portal> 307 + </Dialog.Root> 308 + </> 309 + ); 310 + } 311 + 312 + function AutoGrowTextarea({ 313 + value, 314 + className, 315 + onChange, 316 + ...props 317 + }: React.DetailedHTMLProps< 318 + React.TextareaHTMLAttributes<HTMLTextAreaElement>, 319 + HTMLTextAreaElement 320 + >) { 321 + const ref = useRef<HTMLTextAreaElement>(null); 322 + 323 + useEffect(() => { 324 + const el = ref.current; 325 + if (!el) return; 326 + el.style.height = "auto"; 327 + el.style.height = el.scrollHeight + "px"; 328 + }, [value]); 329 + 330 + return ( 331 + <textarea 332 + ref={ref} 333 + className={className} 334 + value={value} 335 + onChange={onChange} 336 + {...props} 337 + /> 338 + ); 339 + }
+150
src/components/Import.tsx
···
··· 1 + import { AtUri } from "@atproto/api"; 2 + import { useNavigate, type UseNavigateResult } from "@tanstack/react-router"; 3 + import { useState } from "react"; 4 + 5 + /** 6 + * Basically the best equivalent to Search that i can do 7 + */ 8 + export function Import() { 9 + const [textInput, setTextInput] = useState<string | undefined>(); 10 + const navigate = useNavigate(); 11 + 12 + const handleEnter = () => { 13 + if (!textInput) return; 14 + handleImport({ 15 + text: textInput, 16 + navigate, 17 + }); 18 + }; 19 + 20 + return ( 21 + <div className="w-full relative"> 22 + <IconMaterialSymbolsSearch className="w-5 h-5 absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 dark:text-gray-500" /> 23 + 24 + <input 25 + type="text" 26 + placeholder="Import..." 27 + value={textInput} 28 + onChange={(e) => setTextInput(e.target.value)} 29 + onKeyDown={(e) => { 30 + if (e.key === "Enter") handleEnter(); 31 + }} 32 + 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" 33 + /> 34 + </div> 35 + ); 36 + } 37 + 38 + function handleImport({ 39 + text, 40 + navigate, 41 + }: { 42 + text: string; 43 + navigate: UseNavigateResult<string>; 44 + }) { 45 + const trimmed = text.trim(); 46 + // parse text 47 + /** 48 + * text might be 49 + * 1. bsky dot app url (reddwarf link segments might be uri encoded,) 50 + * 2. aturi 51 + * 3. plain handle 52 + * 4. plain did 53 + */ 54 + 55 + // 1. Check if itโ€™s a URL 56 + try { 57 + const url = new URL(text); 58 + const knownHosts = [ 59 + "bsky.app", 60 + "social.daniela.lol", 61 + "deer.social", 62 + "reddwarf.whey.party", 63 + "reddwarf.app", 64 + "main.bsky.dev", 65 + "catsky.social", 66 + "blacksky.community", 67 + "red-dwarf-social-app.whey.party", 68 + "zeppelin.social", 69 + ]; 70 + if (knownHosts.includes(url.hostname)) { 71 + // parse path to get URI or handle 72 + const path = decodeURIComponent(url.pathname.slice(1)); // remove leading / 73 + console.log("BSky URL path:", path); 74 + navigate({ 75 + to: `/${path}`, 76 + }); 77 + return; 78 + } 79 + } catch { 80 + // not a URL, continue 81 + } 82 + 83 + // 2. Check if text looks like an at-uri 84 + try { 85 + if (text.startsWith("at://")) { 86 + console.log("AT URI detected:", text); 87 + const aturi = new AtUri(text); 88 + switch (aturi.collection) { 89 + case "app.bsky.feed.post": { 90 + navigate({ 91 + to: "/profile/$did/post/$rkey", 92 + params: { 93 + did: aturi.host, 94 + rkey: aturi.rkey, 95 + }, 96 + }); 97 + return; 98 + } 99 + case "app.bsky.actor.profile": { 100 + navigate({ 101 + to: "/profile/$did", 102 + params: { 103 + did: aturi.host, 104 + }, 105 + }); 106 + return; 107 + } 108 + // todo add more handlers as more routes are added. like feeds, lists, etc etc thanks! 109 + default: { 110 + // continue 111 + } 112 + } 113 + } 114 + } catch { 115 + // continue 116 + } 117 + 118 + // 3. Plain handle (starts with @) 119 + try { 120 + if (text.startsWith("@")) { 121 + const handle = text.slice(1); 122 + console.log("Handle detected:", handle); 123 + navigate({ to: "/profile/$did", params: { did: handle } }); 124 + return; 125 + } 126 + } catch { 127 + // continue 128 + } 129 + 130 + // 4. Plain DID (starts with did:) 131 + try { 132 + if (text.startsWith("did:")) { 133 + console.log("did detected:", text); 134 + navigate({ to: "/profile/$did", params: { did: text } }); 135 + return; 136 + } 137 + } catch { 138 + // continue 139 + } 140 + 141 + // if all else fails 142 + 143 + // try { 144 + // // probably a user? 145 + // navigate({ to: "/profile/$did", params: { did: text } }); 146 + // return; 147 + // } catch { 148 + // // continue 149 + // } 150 + }
+32 -6
src/components/InfiniteCustomFeed.tsx
··· 1 import * as React from "react"; 2 3 //import { useInView } from "react-intersection-observer"; ··· 37 isFetchingNextPage, 38 refetch, 39 isRefetching, 40 } = useInfiniteQueryFeedSkeleton({ 41 feedUri: feedUri, 42 agent: agent ?? undefined, ··· 44 pdsUrl: pdsUrl, 45 feedServiceDid: feedServiceDid, 46 }); 47 48 const handleRefresh = () => { 49 refetch(); 50 }; 51 52 //const { ref, inView } = useInView(); 53 54 // React.useEffect(() => { ··· 67 ); 68 } 69 70 - const allPosts = 71 - data?.pages.flatMap((page) => { 72 - if (page) return page.feed; 73 - }) ?? []; 74 75 if (!allPosts || typeof allPosts !== "object" || allPosts.length === 0) { 76 return ( ··· 116 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" 117 aria-label="Refresh feed" 118 > 119 - <RefreshIcon className={`h-6 w-6 text-gray-600 dark:text-gray-400 ${isRefetching && "animate-spin"}`} /> 120 </button> 121 </> 122 ); ··· 139 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" 140 ></path> 141 </svg> 142 - );
··· 1 + import { useQueryClient } from "@tanstack/react-query"; 2 import * as React from "react"; 3 4 //import { useInView } from "react-intersection-observer"; ··· 38 isFetchingNextPage, 39 refetch, 40 isRefetching, 41 + queryKey, 42 } = useInfiniteQueryFeedSkeleton({ 43 feedUri: feedUri, 44 agent: agent ?? undefined, ··· 46 pdsUrl: pdsUrl, 47 feedServiceDid: feedServiceDid, 48 }); 49 + const queryClient = useQueryClient(); 50 + 51 52 const handleRefresh = () => { 53 + queryClient.removeQueries({queryKey: queryKey}); 54 + //queryClient.invalidateQueries(["infinite-feed", feedUri] as const); 55 refetch(); 56 }; 57 58 + const allPosts = React.useMemo(() => { 59 + const flattenedPosts = data?.pages.flatMap((page) => page?.feed) ?? []; 60 + 61 + const seenUris = new Set<string>(); 62 + 63 + return flattenedPosts.filter((item) => { 64 + if (!item?.post) return false; 65 + 66 + if (seenUris.has(item.post)) { 67 + return false; 68 + } 69 + 70 + seenUris.add(item.post); 71 + 72 + return true; 73 + }); 74 + }, [data]); 75 + 76 //const { ref, inView } = useInView(); 77 78 // React.useEffect(() => { ··· 91 ); 92 } 93 94 + // const allPosts = 95 + // data?.pages.flatMap((page) => { 96 + // if (page) return page.feed; 97 + // }) ?? []; 98 99 if (!allPosts || typeof allPosts !== "object" || allPosts.length === 0) { 100 return ( ··· 140 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" 141 aria-label="Refresh feed" 142 > 143 + <RefreshIcon 144 + className={`h-6 w-6 text-gray-600 dark:text-gray-400 ${isRefetching && "animate-spin"}`} 145 + /> 146 </button> 147 </> 148 ); ··· 165 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" 166 ></path> 167 </svg> 168 + );
+83 -37
src/components/Login.tsx
··· 1 // src/components/Login.tsx 2 - import { Agent } from "@atproto/api"; 3 import React, { useEffect, useRef, useState } from "react"; 4 5 import { useAuth } from "~/providers/UnifiedAuthProvider"; 6 7 // --- 1. The Main Component (Orchestrator with `compact` prop) --- 8 export default function Login({ ··· 21 className={ 22 compact 23 ? "flex items-center justify-center p-1" 24 - : "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]" 25 } 26 > 27 <span ··· 40 // Large view 41 if (!compact) { 42 return ( 43 - <div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800 mt-6 mx-4"> 44 <div className="flex flex-col items-center justify-center text-center"> 45 <p className="text-lg font-semibold mb-4 text-gray-800 dark:text-gray-100"> 46 You are logged in! 47 </p> 48 <ProfileThing agent={agent} large /> 49 - <button 50 onClick={logout} 51 - className="bg-gray-600 mt-4 hover:bg-gray-700 text-white rounded-full px-6 py-2 font-semibold text-base transition-colors" 52 > 53 Log out 54 - </button> 55 </div> 56 </div> 57 ); ··· 74 if (!compact) { 75 // Large view renders the form directly in the card 76 return ( 77 - <div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800 mt-6 mx-4"> 78 <UnifiedLoginForm /> 79 </div> 80 ); ··· 110 // --- 3. Helper components for layouts, forms, and UI --- 111 112 // A new component to contain the logic for the compact dropdown 113 - const CompactLoginButton = ({popup}:{popup?: boolean}) => { 114 const [showForm, setShowForm] = useState(false); 115 const formRef = useRef<HTMLDivElement>(null); 116 ··· 137 Log in 138 </button> 139 {showForm && ( 140 - <div 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`}> 141 <UnifiedLoginForm /> 142 </div> 143 )} ··· 158 onClick={onClick} 159 className={`px-4 py-2 text-sm font-medium transition-colors rounded-full flex-1 ${ 160 active 161 - ? "text-gray-950 dark:text-gray-200 border-gray-500 bg-gray-400 dark:bg-gray-500" 162 : "text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200" 163 }`} 164 > ··· 187 <p className="text-xs text-gray-500 dark:text-gray-400"> 188 Sign in with AT. Your password is never shared. 189 </p> 190 - <input 191 type="text" 192 placeholder="handle.bsky.social" 193 value={handle} 194 onChange={(e) => setHandle(e.target.value)} 195 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" 196 - /> 197 - <button 198 - type="submit" 199 - className="bg-gray-600 hover:bg-gray-700 text-white rounded-full px-4 py-2 font-medium text-sm transition-colors" 200 - > 201 - Log in 202 - </button> 203 </form> 204 ); 205 }; ··· 232 <p className="text-xs text-red-500 dark:text-red-400"> 233 Warning: Less secure. Use an App Password. 234 </p> 235 - <input 236 type="text" 237 placeholder="handle.bsky.social" 238 value={user} ··· 254 value={serviceURL} 255 onChange={(e) => setServiceURL(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 - /> 258 {error && <p className="text-xs text-red-500">{error}</p>} 259 <button 260 type="submit" ··· 274 agent: Agent | null; 275 large?: boolean; 276 }) => { 277 - const [profile, setProfile] = useState<any>(null); 278 279 - useEffect(() => { 280 - const fetchUser = async () => { 281 - const did = (agent as any)?.session?.did ?? (agent as any)?.assertDid; 282 - if (!did) return; 283 - try { 284 - const res = await agent!.getProfile({ actor: did }); 285 - setProfile(res.data); 286 - } catch (e) { 287 - console.error("Failed to fetch profile", e); 288 - } 289 - }; 290 - if (agent) fetchUser(); 291 - }, [agent]); 292 293 - if (!profile) { 294 return ( 295 // Skeleton loader 296 <div ··· 316 className={`flex flex-row items-center gap-2.5 ${large ? "mb-1" : ""}`} 317 > 318 <img 319 - src={profile?.avatar} 320 alt="avatar" 321 className={`object-cover rounded-full ${large ? "w-10 h-10" : "w-[30px] h-[30px]"}`} 322 /> ··· 329 <div 330 className={` ${large ? "text-gray-500 dark:text-gray-400 text-sm" : "text-gray-500 dark:text-gray-400 text-xs"}`} 331 > 332 - @{profile?.handle} 333 </div> 334 </div> 335 </div>
··· 1 // src/components/Login.tsx 2 + import AtpAgent, { Agent } from "@atproto/api"; 3 + import { useAtom } from "jotai"; 4 import React, { useEffect, useRef, useState } from "react"; 5 6 import { useAuth } from "~/providers/UnifiedAuthProvider"; 7 + import { imgCDNAtom } from "~/utils/atoms"; 8 + import { useQueryIdentity, useQueryProfile } from "~/utils/useQuery"; 9 + 10 + import { Button } from "./radix-m3-rd/Button"; 11 12 // --- 1. The Main Component (Orchestrator with `compact` prop) --- 13 export default function Login({ ··· 26 className={ 27 compact 28 ? "flex items-center justify-center p-1" 29 + : "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]" 30 } 31 > 32 <span ··· 45 // Large view 46 if (!compact) { 47 return ( 48 + <div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800 mt-4 mx-4"> 49 <div className="flex flex-col items-center justify-center text-center"> 50 <p className="text-lg font-semibold mb-4 text-gray-800 dark:text-gray-100"> 51 You are logged in! 52 </p> 53 <ProfileThing agent={agent} large /> 54 + <Button 55 onClick={logout} 56 + className="mt-4" 57 > 58 Log out 59 + </Button> 60 </div> 61 </div> 62 ); ··· 79 if (!compact) { 80 // Large view renders the form directly in the card 81 return ( 82 + <div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800 mt-4 mx-4"> 83 <UnifiedLoginForm /> 84 </div> 85 ); ··· 115 // --- 3. Helper components for layouts, forms, and UI --- 116 117 // A new component to contain the logic for the compact dropdown 118 + const CompactLoginButton = ({ popup }: { popup?: boolean }) => { 119 const [showForm, setShowForm] = useState(false); 120 const formRef = useRef<HTMLDivElement>(null); 121 ··· 142 Log in 143 </button> 144 {showForm && ( 145 + <div 146 + 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`} 147 + > 148 <UnifiedLoginForm /> 149 </div> 150 )} ··· 165 onClick={onClick} 166 className={`px-4 py-2 text-sm font-medium transition-colors rounded-full flex-1 ${ 167 active 168 + ? "text-gray-50 dark:text-gray-200 border-gray-500 bg-gray-400 dark:bg-gray-500" 169 : "text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200" 170 }`} 171 > ··· 194 <p className="text-xs text-gray-500 dark:text-gray-400"> 195 Sign in with AT. Your password is never shared. 196 </p> 197 + {/* <input 198 type="text" 199 placeholder="handle.bsky.social" 200 value={handle} 201 onChange={(e) => setHandle(e.target.value)} 202 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" 203 + /> */} 204 + <div className="flex flex-col gap-3"> 205 + <div className="m3input-field m3input-label m3input-border size-md flex-1"> 206 + <input 207 + type="text" 208 + placeholder=" " 209 + value={handle} 210 + onChange={(e) => setHandle(e.target.value)} 211 + /> 212 + <label>AT Handle</label> 213 + </div> 214 + <button 215 + type="submit" 216 + className="bg-gray-600 hover:bg-gray-700 text-white rounded-full px-4 py-2 font-medium text-sm transition-colors" 217 + > 218 + Log in 219 + </button> 220 + </div> 221 </form> 222 ); 223 }; ··· 250 <p className="text-xs text-red-500 dark:text-red-400"> 251 Warning: Less secure. Use an App Password. 252 </p> 253 + {/* <input 254 type="text" 255 placeholder="handle.bsky.social" 256 value={user} ··· 272 value={serviceURL} 273 onChange={(e) => setServiceURL(e.target.value)} 274 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" 275 + /> */} 276 + <div className="m3input-field m3input-label m3input-border size-md flex-1"> 277 + <input 278 + type="text" 279 + placeholder=" " 280 + value={user} 281 + onChange={(e) => setUser(e.target.value)} 282 + /> 283 + <label>AT Handle</label> 284 + </div> 285 + <div className="m3input-field m3input-label m3input-border size-md flex-1"> 286 + <input 287 + type="text" 288 + placeholder=" " 289 + value={password} 290 + onChange={(e) => setPassword(e.target.value)} 291 + /> 292 + <label>App Password</label> 293 + </div> 294 + <div className="m3input-field m3input-label m3input-border size-md flex-1"> 295 + <input 296 + type="text" 297 + placeholder=" " 298 + value={serviceURL} 299 + onChange={(e) => setServiceURL(e.target.value)} 300 + /> 301 + <label>PDS</label> 302 + </div> 303 {error && <p className="text-xs text-red-500">{error}</p>} 304 <button 305 type="submit" ··· 319 agent: Agent | null; 320 large?: boolean; 321 }) => { 322 + const did = ((agent as AtpAgent)?.session?.did ?? 323 + (agent as AtpAgent)?.assertDid ?? 324 + agent?.did) as string | undefined; 325 + const { data: identity } = useQueryIdentity(did); 326 + const { data: profiledata } = useQueryProfile( 327 + `at://${did}/app.bsky.actor.profile/self` 328 + ); 329 + const profile = profiledata?.value; 330 331 + const [imgcdn] = useAtom(imgCDNAtom) 332 + 333 + function getAvatarUrl(p: typeof profile) { 334 + const link = p?.avatar?.ref?.["$link"]; 335 + if (!link || !did) return null; 336 + return `https://${imgcdn}/img/avatar/plain/${did}/${link}@jpeg`; 337 + } 338 339 + if (!profiledata) { 340 return ( 341 // Skeleton loader 342 <div ··· 362 className={`flex flex-row items-center gap-2.5 ${large ? "mb-1" : ""}`} 363 > 364 <img 365 + src={getAvatarUrl(profile) ?? undefined} 366 alt="avatar" 367 className={`object-cover rounded-full ${large ? "w-10 h-10" : "w-[30px] h-[30px]"}`} 368 /> ··· 375 <div 376 className={` ${large ? "text-gray-500 dark:text-gray-400 text-sm" : "text-gray-500 dark:text-gray-400 text-xs"}`} 377 > 378 + @{identity?.handle} 379 </div> 380 </div> 381 </div>
+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 + }
+445 -212
src/components/UniversalPostRenderer.tsx
··· 1 import { useNavigate } from "@tanstack/react-router"; 2 import DOMPurify from "dompurify"; 3 import { useAtom } from "jotai"; 4 import * as React from "react"; 5 import { type SVGProps } from "react"; 6 - import { createPortal } from "react-dom"; 7 8 - import { ProfilePostComponent } from "~/routes/profile.$did/post.$rkey"; 9 - import { likedPostsAtom } from "~/utils/atoms"; 10 import { useHydratedEmbed } from "~/utils/useHydrated"; 11 import { 12 useQueryConstellation, 13 useQueryIdentity, 14 useQueryPost, 15 useQueryProfile, 16 } from "~/utils/useQuery"; 17 18 function asTyped<T extends { $type: string }>(obj: T): $Typed<T> { ··· 35 ref?: React.Ref<HTMLDivElement>; 36 dataIndexPropPass?: number; 37 nopics?: boolean; 38 } 39 40 // export async function cachedGetRecord({ ··· 143 ref, 144 dataIndexPropPass, 145 nopics, 146 }: UniversalPostRendererATURILoaderProps) { 147 // /*mass comment*/ console.log("atUri", atUri); 148 //const { get, set } = usePersistentStore(); 149 //const [record, setRecord] = React.useState<any>(null); ··· 388 ); 389 }, [links]); 390 391 // const navigateToProfile = (e: React.MouseEvent) => { 392 // e.stopPropagation(); 393 // if (resolved?.did) { ··· 403 } 404 405 return ( 406 - <UniversalPostRendererRawRecordShim 407 - detailed={detailed} 408 - postRecord={postQuery} 409 - profileRecord={opProfile} 410 - aturi={atUri} 411 - resolved={resolved} 412 - likesCount={likes} 413 - repostsCount={reposts} 414 - repliesCount={replies} 415 - bottomReplyLine={bottomReplyLine} 416 - topReplyLine={topReplyLine} 417 - bottomBorder={bottomBorder} 418 - feedviewpost={feedviewpost} 419 - repostedby={repostedby} 420 - style={style} 421 - ref={ref} 422 - dataIndexPropPass={dataIndexPropPass} 423 - nopics={nopics} 424 - /> 425 ); 426 } 427 428 - function getAvatarUrl(opProfile: any, did: string) { 429 const link = opProfile?.value?.avatar?.ref?.["$link"]; 430 if (!link) return null; 431 - return `https://cdn.bsky.app/img/avatar/plain/${did}/${link}@jpeg`; 432 } 433 434 export function UniversalPostRendererRawRecordShim({ ··· 449 ref, 450 dataIndexPropPass, 451 nopics, 452 }: { 453 postRecord: any; 454 profileRecord: any; ··· 467 ref?: React.Ref<HTMLDivElement>; 468 dataIndexPropPass?: number; 469 nopics?: boolean; 470 }) { 471 // /*mass comment*/ console.log(`received aturi: ${aturi} of post content: ${postRecord}`); 472 const navigate = useNavigate(); ··· 543 error: embedError, 544 } = useHydratedEmbed(postRecord?.value?.embed, resolved?.did); 545 546 const parsedaturi = new AtUri(aturi); //parseAtUri(aturi); 547 548 const fakepost = React.useMemo<AppBskyFeedDefs.PostView>( 549 () => ({ 550 $type: "app.bsky.feed.defs#postView", 551 uri: aturi, 552 cid: postRecord?.cid || "", 553 - author: { 554 - did: resolved?.did || "", 555 - handle: resolved?.handle || "", 556 - displayName: profileRecord?.value?.displayName || "", 557 - avatar: getAvatarUrl(profileRecord, resolved?.did) || "", 558 - viewer: undefined, 559 - labels: profileRecord?.labels || undefined, 560 - verification: undefined, 561 - }, 562 record: postRecord?.value || {}, 563 embed: hydratedEmbed ?? undefined, 564 replyCount: repliesCount ?? 0, ··· 575 postRecord?.cid, 576 postRecord?.value, 577 postRecord?.labels, 578 - resolved?.did, 579 - resolved?.handle, 580 - profileRecord, 581 hydratedEmbed, 582 repliesCount, 583 repostsCount, ··· 654 } 655 }} 656 post={fakepost} 657 salt={aturi} 658 bottomReplyLine={bottomReplyLine} 659 topReplyLine={topReplyLine} ··· 665 ref={ref} 666 dataIndexPropPass={dataIndexPropPass} 667 nopics={nopics} 668 /> 669 </> 670 ); ··· 703 {...props} 704 > 705 <path 706 - fill="oklch(0.704 0.05 28)" 707 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" 708 ></path> 709 </svg> ··· 720 {...props} 721 > 722 <path 723 - fill="oklch(0.704 0.05 28)" 724 d="M17 17H7v-3l-4 4l4 4v-3h12v-6h-2M7 7h10v3l4-4l-4-4v3H5v6h2z" 725 ></path> 726 </svg> ··· 771 {...props} 772 > 773 <path 774 - fill="oklch(0.704 0.05 28)" 775 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" 776 ></path> 777 </svg> ··· 788 {...props} 789 > 790 <path 791 - fill="oklch(0.704 0.05 28)" 792 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" 793 ></path> 794 </svg> ··· 805 {...props} 806 > 807 <path 808 - fill="oklch(0.704 0.05 28)" 809 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" 810 ></path> 811 </svg> ··· 822 {...props} 823 > 824 <path 825 - fill="oklch(0.704 0.05 28)" 826 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" 827 ></path> 828 </svg> ··· 856 {...props} 857 > 858 <path 859 - fill="oklch(0.704 0.05 28)" 860 d="M10 9V5l-7 7l7 7v-4.1c5 0 8.5 1.6 11 5.1c-1-5-4-10-11-11" 861 ></path> 862 </svg> ··· 910 {...props} 911 > 912 <path 913 - fill="oklch(0.704 0.05 28)" 914 d="M17 17H7v-3l-4 4l4 4v-3h12v-6h-2M7 7h10v3l4-4l-4-4v3H5v6h2z" 915 ></path> 916 </svg> ··· 927 {...props} 928 > 929 <path 930 - fill="oklch(0.704 0.05 28)" 931 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" 932 ></path> 933 </svg> ··· 955 //import Masonry from "@mui/lab/Masonry"; 956 import { 957 type $Typed, 958 AppBskyEmbedDefs, 959 AppBskyEmbedExternal, 960 AppBskyEmbedImages, ··· 978 PostView, 979 //ThreadViewPost, 980 } from "@atproto/api/dist/client/types/app/bsky/feed/defs"; 981 import { useEffect, useRef, useState } from "react"; 982 import ReactPlayer from "react-player"; 983 984 import defaultpfp from "~/../public/favicon.png"; 985 import { useAuth } from "~/providers/UnifiedAuthProvider"; 986 // import type { OutputSchema } from "@atproto/api/dist/client/types/app/bsky/feed/getFeed"; 987 // import type { 988 // ViewRecord, ··· 1090 1091 function UniversalPostRenderer({ 1092 post, 1093 //setMainItem, 1094 //isMainItem, 1095 onPostClick, ··· 1110 ref, 1111 dataIndexPropPass, 1112 nopics, 1113 }: { 1114 post: PostView; 1115 // optional for now because i havent ported every use to this yet 1116 // setMainItem?: React.Dispatch< 1117 // React.SetStateAction<AppBskyFeedDefs.FeedViewPost> ··· 1133 ref?: React.Ref<HTMLDivElement>; 1134 dataIndexPropPass?: number; 1135 nopics?: boolean; 1136 }) { 1137 const parsed = new AtUri(post.uri); 1138 const navigate = useNavigate(); ··· 1143 const [hasLiked, setHasLiked] = useState<boolean>( 1144 post.uri in likedPosts || post.viewer?.like ? true : false 1145 ); 1146 const { agent } = useAuth(); 1147 const [likeUri, setLikeUri] = useState<string | undefined>(post.viewer?.like); 1148 const [retweetUri, setRetweetUri] = useState<string | undefined>( ··· 1239 paddingLeft: isQuote ? 12 : 16, 1240 paddingRight: isQuote ? 12 : 16, 1241 //paddingTop: 16, 1242 - paddingTop: isRepost ? 10 : isQuote ? 12 : 16, 1243 //paddingBottom: bottomReplyLine ? 0 : 16, 1244 paddingBottom: 0, 1245 fontFamily: "system-ui, sans-serif", ··· 1280 //left: 16 + (42 / 2), 1281 width: 2, 1282 //height: "100%", 1283 - height: isRepost ? "calc(16px + 1rem - 6px)" : 16 - 6, 1284 // background: theme.textSecondary, 1285 //opacity: 0.5, 1286 // no flex here ··· 1288 className="bg-gray-500 dark:bg-gray-400" 1289 /> 1290 )} 1291 - <div 1292 - style={{ 1293 - position: "absolute", 1294 - //top: isRepost ? "calc(16px + 1rem)" : 16, 1295 - //left: 16, 1296 - zIndex: 1, 1297 - top: isRepost ? "calc(16px + 1rem)" : isQuote ? 12 : 16, 1298 - left: isQuote ? 12 : 16, 1299 - }} 1300 - onClick={onProfileClick} 1301 - > 1302 - <img 1303 - src={post.author.avatar || defaultpfp} 1304 - alt="avatar" 1305 - // transition={{ 1306 - // type: "spring", 1307 - // stiffness: 260, 1308 - // damping: 20, 1309 - // }} 1310 - style={{ 1311 - borderRadius: "50%", 1312 - marginRight: 12, 1313 - objectFit: "cover", 1314 - //background: theme.border, 1315 - //border: `1px solid ${theme.border}`, 1316 - width: isQuote ? 16 : 42, 1317 - height: isQuote ? 16 : 42, 1318 - }} 1319 - className="border border-gray-300 dark:border-gray-800 bg-gray-300 dark:bg-gray-600" 1320 - /> 1321 - </div> 1322 <div style={{ display: "flex", alignItems: "flex-start", zIndex: 2 }}> 1323 <div 1324 style={{ ··· 1332 }} 1333 > 1334 {/* dummy for later use */} 1335 - <div style={{ width: 42, height: 42 + 8, minHeight: 42 + 8 }} /> 1336 {/* reply line !!!! bottomReplyLine */} 1337 {bottomReplyLine && ( 1338 <div ··· 1489 > 1490 {fedi ? ( 1491 <> 1492 - <span className="dangerousFediContent" 1493 dangerouslySetInnerHTML={{ 1494 __html: DOMPurify.sanitize(fedi), 1495 }} ··· 1514 navigate={navigate} 1515 postid={{ did: post.author.did, rkey: parsed.rkey }} 1516 nopics={nopics} 1517 /> 1518 ) : null} 1519 {post.embed && depth > 0 && ( ··· 1563 }} 1564 className="text-gray-500 dark:text-gray-400" 1565 > 1566 - <span style={btnstyle}> 1567 - <MdiCommentOutline /> 1568 - {post.replyCount} 1569 - </span> 1570 <HitSlopButton 1571 onClick={() => { 1572 - repostOrUnrepostPost(); 1573 }} 1574 style={{ 1575 ...btnstyle, 1576 - ...(hasRetweeted ? { color: "#5CEFAA" } : {}), 1577 }} 1578 > 1579 - {hasRetweeted ? <MdiRepeatGreen /> : <MdiRepeat />} 1580 - {(post.repostCount || 0) + (hasRetweeted ? 1 : 0)} 1581 </HitSlopButton> 1582 <HitSlopButton 1583 onClick={() => { 1584 likeOrUnlikePost(); ··· 1720 navigate, 1721 postid, 1722 nopics, 1723 }: { 1724 embed?: Embed; 1725 moderation?: ModerationDecision; ··· 1730 navigate: (_: any) => void; 1731 postid?: { did: string; rkey: string }; 1732 nopics?: boolean; 1733 }) { 1734 - const [lightboxIndex, setLightboxIndex] = useState<number | null>(null); 1735 if ( 1736 AppBskyEmbedRecordWithMedia.isView(embed) && 1737 AppBskyEmbedRecord.isViewRecord(embed.record.record) && ··· 1767 navigate={navigate} 1768 postid={postid} 1769 nopics={nopics} 1770 /> 1771 {/* padding empty div of 8px height */} 1772 <div style={{ height: 12 }} /> ··· 1934 1935 // image embed 1936 // = 1937 - if (AppBskyEmbedImages.isView(embed) && !nopics) { 1938 const { images } = embed; 1939 1940 const lightboxImages = images.map((img) => ({ 1941 src: img.fullsize, 1942 alt: img.alt, 1943 })); 1944 1945 if (images.length > 0) { 1946 // const items = embed.images.map(img => ({ ··· 1972 }} 1973 className="border border-gray-200 dark:border-gray-800 was7 bg-gray-200 dark:bg-gray-900" 1974 > 1975 - {lightboxIndex !== null && ( 1976 <Lightbox 1977 images={lightboxImages} 1978 index={lightboxIndex} ··· 1980 onNavigate={(newIndex) => setLightboxIndex(newIndex)} 1981 post={postid} 1982 /> 1983 - )} 1984 <img 1985 src={image.fullsize} 1986 alt={image.alt} ··· 2013 }} 2014 className="border border-gray-200 dark:border-gray-800 was7" 2015 > 2016 - {lightboxIndex !== null && ( 2017 <Lightbox 2018 images={lightboxImages} 2019 index={lightboxIndex} ··· 2021 onNavigate={(newIndex) => setLightboxIndex(newIndex)} 2022 post={postid} 2023 /> 2024 - )} 2025 {images.map((img, i) => ( 2026 <div 2027 key={i} ··· 2063 }} 2064 className="border border-gray-200 dark:border-gray-800 was7" 2065 > 2066 - {lightboxIndex !== null && ( 2067 <Lightbox 2068 images={lightboxImages} 2069 index={lightboxIndex} ··· 2071 onNavigate={(newIndex) => setLightboxIndex(newIndex)} 2072 post={postid} 2073 /> 2074 - )} 2075 {/* Left: 1:1 */} 2076 <div 2077 style={{ flex: 1, aspectRatio: "1 / 1", position: "relative" }} ··· 2148 }} 2149 className="border border-gray-200 dark:border-gray-800 was7" 2150 > 2151 - {lightboxIndex !== null && ( 2152 <Lightbox 2153 images={lightboxImages} 2154 index={lightboxIndex} ··· 2156 onNavigate={(newIndex) => setLightboxIndex(newIndex)} 2157 post={postid} 2158 /> 2159 - )} 2160 {images.map((img, i) => ( 2161 <div 2162 key={i} ··· 2247 return <div />; 2248 } 2249 2250 - type LightboxProps = { 2251 - images: { src: string; alt?: string }[]; 2252 - index: number; 2253 - onClose: () => void; 2254 - onNavigate?: (newIndex: number) => void; 2255 - post?: { did: string; rkey: string }; 2256 - }; 2257 - export function Lightbox({ 2258 - images, 2259 - index, 2260 - onClose, 2261 - onNavigate, 2262 - post, 2263 - }: LightboxProps) { 2264 - const image = images[index]; 2265 - 2266 - useEffect(() => { 2267 - function handleKey(e: KeyboardEvent) { 2268 - if (e.key === "Escape") onClose(); 2269 - if (e.key === "ArrowRight" && onNavigate) 2270 - onNavigate((index + 1) % images.length); 2271 - if (e.key === "ArrowLeft" && onNavigate) 2272 - onNavigate((index - 1 + images.length) % images.length); 2273 - } 2274 - window.addEventListener("keydown", handleKey); 2275 - return () => window.removeEventListener("keydown", handleKey); 2276 - }, [index, images.length, onClose, onNavigate]); 2277 - 2278 - return createPortal( 2279 - <> 2280 - {post && ( 2281 - <div 2282 - onClick={(e) => { 2283 - e.stopPropagation(); 2284 - e.nativeEvent.stopImmediatePropagation(); 2285 - }} 2286 - className="lightbox-sidebar overscroll-none disablegutter border-l dark:border-gray-800 was7 border-gray-300 fixed z-50 flex top-0 right-0 flex-col max-w-[350px] min-w-[350px] max-h-screen overflow-y-scroll dark:bg-gray-950 bg-white" 2287 - > 2288 - <ProfilePostComponent 2289 - did={post.did} 2290 - rkey={post.rkey} 2291 - nopics={onClose} 2292 - /> 2293 - </div> 2294 - )} 2295 - <div 2296 - 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)]" 2297 - onClick={(e) => { 2298 - e.stopPropagation(); 2299 - onClose(); 2300 - }} 2301 - > 2302 - <img 2303 - src={image.src} 2304 - alt={image.alt} 2305 - className="max-h-[90%] max-w-[90%] object-contain rounded-lg shadow-lg" 2306 - onClick={(e) => e.stopPropagation()} 2307 - /> 2308 - 2309 - {images.length > 1 && ( 2310 - <> 2311 - <button 2312 - onClick={(e) => { 2313 - e.stopPropagation(); 2314 - onNavigate?.((index - 1 + images.length) % images.length); 2315 - }} 2316 - 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" 2317 - > 2318 - <svg 2319 - xmlns="http://www.w3.org/2000/svg" 2320 - width={28} 2321 - height={28} 2322 - viewBox="0 0 24 24" 2323 - > 2324 - <g fill="none" fillRule="evenodd"> 2325 - <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> 2326 - <path 2327 - fill="currentColor" 2328 - 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" 2329 - ></path> 2330 - </g> 2331 - </svg> 2332 - </button> 2333 - <button 2334 - onClick={(e) => { 2335 - e.stopPropagation(); 2336 - onNavigate?.((index + 1) % images.length); 2337 - }} 2338 - 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" 2339 - > 2340 - <svg 2341 - xmlns="http://www.w3.org/2000/svg" 2342 - width={28} 2343 - height={28} 2344 - viewBox="0 0 24 24" 2345 - > 2346 - <g fill="none" fillRule="evenodd"> 2347 - <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> 2348 - <path 2349 - fill="currentColor" 2350 - 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" 2351 - ></path> 2352 - </g> 2353 - </svg> 2354 - </button> 2355 - </> 2356 - )} 2357 - </div> 2358 - </>, 2359 - document.body 2360 - ); 2361 - } 2362 - 2363 function getDomain(url: string) { 2364 try { 2365 const { hostname } = new URL(url); ··· 2424 return { start, end, feature: f.features[0] }; 2425 }); 2426 } 2427 - function renderTextWithFacets({ 2428 text, 2429 facets, 2430 navigate,
··· 1 import { useNavigate } from "@tanstack/react-router"; 2 import DOMPurify from "dompurify"; 3 import { useAtom } from "jotai"; 4 + import { DropdownMenu } from "radix-ui"; 5 + import { HoverCard } from "radix-ui"; 6 import * as React from "react"; 7 import { type SVGProps } from "react"; 8 9 + import { 10 + composerAtom, 11 + constellationURLAtom, 12 + imgCDNAtom, 13 + likedPostsAtom, 14 + } from "~/utils/atoms"; 15 import { useHydratedEmbed } from "~/utils/useHydrated"; 16 import { 17 useQueryConstellation, 18 useQueryIdentity, 19 useQueryPost, 20 useQueryProfile, 21 + yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks, 22 } from "~/utils/useQuery"; 23 24 function asTyped<T extends { $type: string }>(obj: T): $Typed<T> { ··· 41 ref?: React.Ref<HTMLDivElement>; 42 dataIndexPropPass?: number; 43 nopics?: boolean; 44 + lightboxCallback?: (d: LightboxProps) => void; 45 + maxReplies?: number; 46 + isQuote?: boolean; 47 } 48 49 // export async function cachedGetRecord({ ··· 152 ref, 153 dataIndexPropPass, 154 nopics, 155 + lightboxCallback, 156 + maxReplies, 157 + isQuote, 158 }: UniversalPostRendererATURILoaderProps) { 159 + // todo remove this once tree rendering is implemented, use a prop like isTree 160 + const TEMPLINEAR = true; 161 // /*mass comment*/ console.log("atUri", atUri); 162 //const { get, set } = usePersistentStore(); 163 //const [record, setRecord] = React.useState<any>(null); ··· 402 ); 403 }, [links]); 404 405 + // const { data: repliesData } = useQueryConstellation({ 406 + // method: "/links", 407 + // target: atUri, 408 + // collection: "app.bsky.feed.post", 409 + // path: ".reply.parent.uri", 410 + // }); 411 + 412 + const [constellationurl] = useAtom(constellationURLAtom); 413 + 414 + const infinitequeryresults = useInfiniteQuery({ 415 + ...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks( 416 + { 417 + constellation: constellationurl, 418 + method: "/links", 419 + target: atUri, 420 + collection: "app.bsky.feed.post", 421 + path: ".reply.parent.uri", 422 + } 423 + ), 424 + enabled: !!atUri && !!maxReplies && !isQuote, 425 + }); 426 + 427 + const { 428 + data: repliesData, 429 + // fetchNextPage, 430 + // hasNextPage, 431 + // isFetchingNextPage, 432 + } = infinitequeryresults; 433 + 434 + // auto-fetch all pages 435 + useEffect(() => { 436 + if (!maxReplies || isQuote || TEMPLINEAR) return; 437 + if ( 438 + infinitequeryresults.hasNextPage && 439 + !infinitequeryresults.isFetchingNextPage 440 + ) { 441 + console.log("Fetching the next page..."); 442 + infinitequeryresults.fetchNextPage(); 443 + } 444 + }, [TEMPLINEAR, infinitequeryresults, isQuote, maxReplies]); 445 + 446 + const replyAturis = repliesData 447 + ? repliesData.pages.flatMap((page) => 448 + page 449 + ? page.linking_records.map((record) => { 450 + const aturi = `at://${record.did}/${record.collection}/${record.rkey}`; 451 + return aturi; 452 + }) 453 + : [] 454 + ) 455 + : []; 456 + 457 + //const [oldestOpsReply, setOldestOpsReply] = useState<string | undefined>(undefined); 458 + 459 + const { oldestOpsReply, oldestOpsReplyElseNewestNonOpsReply } = (() => { 460 + if (isQuote || !replyAturis || replyAturis.length === 0 || !maxReplies) 461 + return { 462 + oldestOpsReply: undefined, 463 + oldestOpsReplyElseNewestNonOpsReply: undefined, 464 + }; 465 + 466 + const opdid = new AtUri( 467 + //postQuery?.value.reply?.root.uri ?? postQuery?.uri ?? atUri 468 + atUri 469 + ).host; 470 + 471 + const opReplies = replyAturis.filter( 472 + (aturi) => new AtUri(aturi).host === opdid 473 + ); 474 + 475 + if (opReplies.length > 0) { 476 + const opreply = opReplies[opReplies.length - 1]; 477 + //setOldestOpsReply(opreply); 478 + return { 479 + oldestOpsReply: opreply, 480 + oldestOpsReplyElseNewestNonOpsReply: opreply, 481 + }; 482 + } else { 483 + return { 484 + oldestOpsReply: undefined, 485 + oldestOpsReplyElseNewestNonOpsReply: replyAturis[0], 486 + }; 487 + } 488 + })(); 489 + 490 // const navigateToProfile = (e: React.MouseEvent) => { 491 // e.stopPropagation(); 492 // if (resolved?.did) { ··· 502 } 503 504 return ( 505 + <> 506 + {/* <span>uprrs {maxReplies} {!!maxReplies&&!!oldestOpsReplyElseNewestNonOpsReply ? "true" : "false"}</span> */} 507 + <UniversalPostRendererRawRecordShim 508 + detailed={detailed} 509 + postRecord={postQuery} 510 + profileRecord={opProfile} 511 + aturi={atUri} 512 + resolved={resolved} 513 + likesCount={likes} 514 + repostsCount={reposts} 515 + repliesCount={replies} 516 + bottomReplyLine={ 517 + maxReplies && oldestOpsReplyElseNewestNonOpsReply 518 + ? true 519 + : maxReplies && !oldestOpsReplyElseNewestNonOpsReply 520 + ? false 521 + : (maxReplies === 0 && (!replies || (!!replies && replies === 0))) ? false : bottomReplyLine 522 + } 523 + topReplyLine={topReplyLine} 524 + //bottomBorder={maxReplies&&oldestOpsReplyElseNewestNonOpsReply ? false : bottomBorder} 525 + bottomBorder={ 526 + maxReplies && oldestOpsReplyElseNewestNonOpsReply 527 + ? false 528 + : maxReplies === 0 529 + ? false 530 + : bottomBorder 531 + } 532 + feedviewpost={feedviewpost} 533 + repostedby={repostedby} 534 + //style={{...style, background: oldestOpsReply === atUri ? "Red" : undefined}} 535 + style={style} 536 + ref={ref} 537 + dataIndexPropPass={dataIndexPropPass} 538 + nopics={nopics} 539 + lightboxCallback={lightboxCallback} 540 + maxReplies={maxReplies} 541 + isQuote={isQuote} 542 + /> 543 + <> 544 + {(maxReplies && maxReplies === 0 && replies && replies > 0) ? ( 545 + <> 546 + {/* <div>hello</div> */} 547 + <MoreReplies atUri={atUri} /> 548 + </> 549 + ) : (<></>)} 550 + </> 551 + {!isQuote && oldestOpsReplyElseNewestNonOpsReply && ( 552 + <> 553 + {/* <span>hello {maxReplies}</span> */} 554 + <UniversalPostRendererATURILoader 555 + //detailed={detailed} 556 + atUri={oldestOpsReplyElseNewestNonOpsReply} 557 + bottomReplyLine={(maxReplies ?? 0) > 0} 558 + topReplyLine={ 559 + (!!(maxReplies && maxReplies - 1 === 0) && 560 + !!(replies && replies > 0)) || 561 + !!((maxReplies ?? 0) > 1) 562 + } 563 + bottomBorder={bottomBorder} 564 + feedviewpost={feedviewpost} 565 + repostedby={repostedby} 566 + style={style} 567 + ref={ref} 568 + dataIndexPropPass={dataIndexPropPass} 569 + nopics={nopics} 570 + lightboxCallback={lightboxCallback} 571 + maxReplies={ 572 + maxReplies && maxReplies > 0 ? maxReplies - 1 : undefined 573 + } 574 + /> 575 + </> 576 + )} 577 + </> 578 ); 579 } 580 581 + function MoreReplies({ atUri }: { atUri: string }) { 582 + const navigate = useNavigate(); 583 + const aturio = new AtUri(atUri); 584 + return ( 585 + <div 586 + onClick={() => 587 + navigate({ 588 + to: "/profile/$did/post/$rkey", 589 + params: { did: aturio.host, rkey: aturio.rkey }, 590 + }) 591 + } 592 + 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" 593 + > 594 + <div className="w-[42px] h-12 flex flex-col items-center justify-center"> 595 + <div 596 + style={{ 597 + width: 2, 598 + height: "100%", 599 + backgroundImage: 600 + "repeating-linear-gradient(to bottom, var(--color-gray-500) 0, var(--color-gray-500) 4px, transparent 4px, transparent 8px)", 601 + opacity: 0.5, 602 + }} 603 + className="dark:bg-[repeating-linear-gradient(to_bottom,var(--color-gray-500)_0,var(--color-gray-400)_4px,transparent_4px,transparent_8px)]" 604 + //className="border-gray-400 dark:border-gray-500" 605 + /> 606 + </div> 607 + 608 + <div className="flex items-center pl-3 text-sm text-gray-500 dark:text-gray-400 select-none"> 609 + More Replies 610 + </div> 611 + </div> 612 + ); 613 + } 614 + 615 + function getAvatarUrl(opProfile: any, did: string, cdn: string) { 616 const link = opProfile?.value?.avatar?.ref?.["$link"]; 617 if (!link) return null; 618 + return `https://${cdn}/img/avatar/plain/${did}/${link}@jpeg`; 619 } 620 621 export function UniversalPostRendererRawRecordShim({ ··· 636 ref, 637 dataIndexPropPass, 638 nopics, 639 + lightboxCallback, 640 + maxReplies, 641 + isQuote, 642 }: { 643 postRecord: any; 644 profileRecord: any; ··· 657 ref?: React.Ref<HTMLDivElement>; 658 dataIndexPropPass?: number; 659 nopics?: boolean; 660 + lightboxCallback?: (d: LightboxProps) => void; 661 + maxReplies?: number; 662 + isQuote?: boolean; 663 }) { 664 // /*mass comment*/ console.log(`received aturi: ${aturi} of post content: ${postRecord}`); 665 const navigate = useNavigate(); ··· 736 error: embedError, 737 } = useHydratedEmbed(postRecord?.value?.embed, resolved?.did); 738 739 + const [imgcdn] = useAtom(imgCDNAtom); 740 + 741 const parsedaturi = new AtUri(aturi); //parseAtUri(aturi); 742 743 + const fakeprofileviewbasic = React.useMemo<AppBskyActorDefs.ProfileViewBasic>( 744 + () => ({ 745 + did: resolved?.did || "", 746 + handle: resolved?.handle || "", 747 + displayName: profileRecord?.value?.displayName || "", 748 + avatar: getAvatarUrl(profileRecord, resolved?.did, imgcdn) || "", 749 + viewer: undefined, 750 + labels: profileRecord?.labels || undefined, 751 + verification: undefined, 752 + }), 753 + [imgcdn, profileRecord, resolved?.did, resolved?.handle] 754 + ); 755 + 756 + const fakeprofileviewdetailed = 757 + React.useMemo<AppBskyActorDefs.ProfileViewDetailed>( 758 + () => ({ 759 + ...fakeprofileviewbasic, 760 + $type: "app.bsky.actor.defs#profileViewDetailed", 761 + description: profileRecord?.value?.description || undefined, 762 + }), 763 + [fakeprofileviewbasic, profileRecord?.value?.description] 764 + ); 765 + 766 const fakepost = React.useMemo<AppBskyFeedDefs.PostView>( 767 () => ({ 768 $type: "app.bsky.feed.defs#postView", 769 uri: aturi, 770 cid: postRecord?.cid || "", 771 + author: fakeprofileviewbasic, 772 record: postRecord?.value || {}, 773 embed: hydratedEmbed ?? undefined, 774 replyCount: repliesCount ?? 0, ··· 785 postRecord?.cid, 786 postRecord?.value, 787 postRecord?.labels, 788 + fakeprofileviewbasic, 789 hydratedEmbed, 790 repliesCount, 791 repostsCount, ··· 862 } 863 }} 864 post={fakepost} 865 + uprrrsauthor={fakeprofileviewdetailed} 866 salt={aturi} 867 bottomReplyLine={bottomReplyLine} 868 topReplyLine={topReplyLine} ··· 874 ref={ref} 875 dataIndexPropPass={dataIndexPropPass} 876 nopics={nopics} 877 + lightboxCallback={lightboxCallback} 878 + maxReplies={maxReplies} 879 + isQuote={isQuote} 880 /> 881 </> 882 ); ··· 915 {...props} 916 > 917 <path 918 + fill="var(--color-gray-400)" 919 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" 920 ></path> 921 </svg> ··· 932 {...props} 933 > 934 <path 935 + fill="var(--color-gray-400)" 936 d="M17 17H7v-3l-4 4l4 4v-3h12v-6h-2M7 7h10v3l4-4l-4-4v3H5v6h2z" 937 ></path> 938 </svg> ··· 983 {...props} 984 > 985 <path 986 + fill="var(--color-gray-400)" 987 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" 988 ></path> 989 </svg> ··· 1000 {...props} 1001 > 1002 <path 1003 + fill="var(--color-gray-400)" 1004 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" 1005 ></path> 1006 </svg> ··· 1017 {...props} 1018 > 1019 <path 1020 + fill="var(--color-gray-400)" 1021 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" 1022 ></path> 1023 </svg> ··· 1034 {...props} 1035 > 1036 <path 1037 + fill="var(--color-gray-400)" 1038 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" 1039 ></path> 1040 </svg> ··· 1068 {...props} 1069 > 1070 <path 1071 + fill="var(--color-gray-400)" 1072 d="M10 9V5l-7 7l7 7v-4.1c5 0 8.5 1.6 11 5.1c-1-5-4-10-11-11" 1073 ></path> 1074 </svg> ··· 1122 {...props} 1123 > 1124 <path 1125 + fill="var(--color-gray-400)" 1126 d="M17 17H7v-3l-4 4l4 4v-3h12v-6h-2M7 7h10v3l4-4l-4-4v3H5v6h2z" 1127 ></path> 1128 </svg> ··· 1139 {...props} 1140 > 1141 <path 1142 + fill="var(--color-gray-400)" 1143 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" 1144 ></path> 1145 </svg> ··· 1167 //import Masonry from "@mui/lab/Masonry"; 1168 import { 1169 type $Typed, 1170 + AppBskyActorDefs, 1171 AppBskyEmbedDefs, 1172 AppBskyEmbedExternal, 1173 AppBskyEmbedImages, ··· 1191 PostView, 1192 //ThreadViewPost, 1193 } from "@atproto/api/dist/client/types/app/bsky/feed/defs"; 1194 + import { useInfiniteQuery } from "@tanstack/react-query"; 1195 import { useEffect, useRef, useState } from "react"; 1196 import ReactPlayer from "react-player"; 1197 1198 import defaultpfp from "~/../public/favicon.png"; 1199 import { useAuth } from "~/providers/UnifiedAuthProvider"; 1200 + import { FollowButton, Mutual } from "~/routes/profile.$did"; 1201 + import type { LightboxProps } from "~/routes/profile.$did/post.$rkey.image.$i"; 1202 // import type { OutputSchema } from "@atproto/api/dist/client/types/app/bsky/feed/getFeed"; 1203 // import type { 1204 // ViewRecord, ··· 1306 1307 function UniversalPostRenderer({ 1308 post, 1309 + uprrrsauthor, 1310 //setMainItem, 1311 //isMainItem, 1312 onPostClick, ··· 1327 ref, 1328 dataIndexPropPass, 1329 nopics, 1330 + lightboxCallback, 1331 + maxReplies, 1332 }: { 1333 post: PostView; 1334 + uprrrsauthor?: AppBskyActorDefs.ProfileViewDetailed; 1335 // optional for now because i havent ported every use to this yet 1336 // setMainItem?: React.Dispatch< 1337 // React.SetStateAction<AppBskyFeedDefs.FeedViewPost> ··· 1353 ref?: React.Ref<HTMLDivElement>; 1354 dataIndexPropPass?: number; 1355 nopics?: boolean; 1356 + lightboxCallback?: (d: LightboxProps) => void; 1357 + maxReplies?: number; 1358 }) { 1359 const parsed = new AtUri(post.uri); 1360 const navigate = useNavigate(); ··· 1365 const [hasLiked, setHasLiked] = useState<boolean>( 1366 post.uri in likedPosts || post.viewer?.like ? true : false 1367 ); 1368 + const [, setComposerPost] = useAtom(composerAtom); 1369 const { agent } = useAuth(); 1370 const [likeUri, setLikeUri] = useState<string | undefined>(post.viewer?.like); 1371 const [retweetUri, setRetweetUri] = useState<string | undefined>( ··· 1462 paddingLeft: isQuote ? 12 : 16, 1463 paddingRight: isQuote ? 12 : 16, 1464 //paddingTop: 16, 1465 + paddingTop: isRepost ? 10 : isQuote ? 12 : topReplyLine ? 8 : 16, 1466 //paddingBottom: bottomReplyLine ? 0 : 16, 1467 paddingBottom: 0, 1468 fontFamily: "system-ui, sans-serif", ··· 1503 //left: 16 + (42 / 2), 1504 width: 2, 1505 //height: "100%", 1506 + height: isRepost 1507 + ? "calc(16px + 1rem - 6px)" 1508 + : topReplyLine 1509 + ? 8 - 6 1510 + : 16 - 6, 1511 // background: theme.textSecondary, 1512 //opacity: 0.5, 1513 // no flex here ··· 1515 className="bg-gray-500 dark:bg-gray-400" 1516 /> 1517 )} 1518 + <HoverCard.Root> 1519 + <HoverCard.Trigger asChild> 1520 + <div 1521 + className={`absolute`} 1522 + style={{ 1523 + top: isRepost 1524 + ? "calc(16px + 1rem)" 1525 + : isQuote 1526 + ? 12 1527 + : topReplyLine 1528 + ? 8 1529 + : 16, 1530 + left: isQuote ? 12 : 16, 1531 + }} 1532 + onClick={onProfileClick} 1533 + > 1534 + <img 1535 + src={post.author.avatar || defaultpfp} 1536 + alt="avatar" 1537 + className={`rounded-full object-cover border border-gray-300 dark:border-gray-800 bg-gray-300 dark:bg-gray-600`} 1538 + style={{ 1539 + width: isQuote ? 16 : 42, 1540 + height: isQuote ? 16 : 42, 1541 + }} 1542 + /> 1543 + </div> 1544 + </HoverCard.Trigger> 1545 + <HoverCard.Portal> 1546 + <HoverCard.Content 1547 + 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" 1548 + side={"bottom"} 1549 + sideOffset={5} 1550 + onClick={onProfileClick} 1551 + > 1552 + <div className="flex flex-col gap-2"> 1553 + <div className="flex flex-row"> 1554 + <img 1555 + src={post.author.avatar || defaultpfp} 1556 + alt="avatar" 1557 + className="rounded-full w-[58px] h-[58px] object-cover border border-gray-300 dark:border-gray-800 bg-gray-300 dark:bg-gray-600" 1558 + /> 1559 + <div className=" flex-1 flex flex-row align-middle justify-end"> 1560 + <div className=" flex flex-col justify-start"> 1561 + <FollowButton targetdidorhandle={post.author.did} /> 1562 + </div> 1563 + </div> 1564 + </div> 1565 + <div className="flex flex-col gap-3"> 1566 + <div> 1567 + <div className="text-gray-900 dark:text-gray-100 font-medium text-md"> 1568 + {post.author.displayName || post.author.handle}{" "} 1569 + </div> 1570 + <div className="text-gray-500 dark:text-gray-400 text-md flex flex-row gap-1"> 1571 + <Mutual targetdidorhandle={post.author.did} />@{post.author.handle}{" "} 1572 + </div> 1573 + </div> 1574 + {uprrrsauthor?.description && ( 1575 + <div className="text-gray-700 dark:text-gray-300 text-sm text-left break-words line-clamp-3"> 1576 + {uprrrsauthor.description} 1577 + </div> 1578 + )} 1579 + {/* <div className="flex gap-4"> 1580 + <div className="flex gap-1"> 1581 + <div className="font-medium text-gray-900 dark:text-gray-100"> 1582 + 0 1583 + </div> 1584 + <div className="text-gray-500 dark:text-gray-400"> 1585 + Following 1586 + </div> 1587 + </div> 1588 + <div className="flex gap-1"> 1589 + <div className="font-medium text-gray-900 dark:text-gray-100"> 1590 + 2,900 1591 + </div> 1592 + <div className="text-gray-500 dark:text-gray-400"> 1593 + Followers 1594 + </div> 1595 + </div> 1596 + </div> */} 1597 + </div> 1598 + </div> 1599 + 1600 + {/* <HoverCard.Arrow className="fill-gray-50 dark:fill-gray-900" /> */} 1601 + </HoverCard.Content> 1602 + </HoverCard.Portal> 1603 + </HoverCard.Root> 1604 + 1605 <div style={{ display: "flex", alignItems: "flex-start", zIndex: 2 }}> 1606 <div 1607 style={{ ··· 1615 }} 1616 > 1617 {/* dummy for later use */} 1618 + <div style={{ width: 42, height: 42 + 6, minHeight: 42 + 6 }} /> 1619 {/* reply line !!!! bottomReplyLine */} 1620 {bottomReplyLine && ( 1621 <div ··· 1772 > 1773 {fedi ? ( 1774 <> 1775 + <span 1776 + className="dangerousFediContent" 1777 dangerouslySetInnerHTML={{ 1778 __html: DOMPurify.sanitize(fedi), 1779 }} ··· 1798 navigate={navigate} 1799 postid={{ did: post.author.did, rkey: parsed.rkey }} 1800 nopics={nopics} 1801 + lightboxCallback={lightboxCallback} 1802 /> 1803 ) : null} 1804 {post.embed && depth > 0 && ( ··· 1848 }} 1849 className="text-gray-500 dark:text-gray-400" 1850 > 1851 <HitSlopButton 1852 onClick={() => { 1853 + setComposerPost({ kind: "reply", parent: post.uri }); 1854 }} 1855 style={{ 1856 ...btnstyle, 1857 }} 1858 > 1859 + <MdiCommentOutline /> 1860 + {post.replyCount} 1861 </HitSlopButton> 1862 + <DropdownMenu.Root modal={false}> 1863 + <DropdownMenu.Trigger asChild> 1864 + <div 1865 + style={{ 1866 + ...btnstyle, 1867 + ...(hasRetweeted ? { color: "#5CEFAA" } : {}), 1868 + }} 1869 + aria-label="Repost or quote post" 1870 + > 1871 + {hasRetweeted ? <MdiRepeatGreen /> : <MdiRepeat />} 1872 + {post.repostCount ?? 0} 1873 + </div> 1874 + </DropdownMenu.Trigger> 1875 + 1876 + <DropdownMenu.Portal> 1877 + <DropdownMenu.Content 1878 + align="start" 1879 + sideOffset={5} 1880 + 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" 1881 + > 1882 + <DropdownMenu.Item 1883 + onSelect={repostOrUnrepostPost} 1884 + 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" 1885 + > 1886 + <MdiRepeat 1887 + className={hasRetweeted ? "text-green-400" : ""} 1888 + /> 1889 + <span>{hasRetweeted ? "Undo Repost" : "Repost"}</span> 1890 + </DropdownMenu.Item> 1891 + 1892 + <DropdownMenu.Item 1893 + onSelect={() => { 1894 + setComposerPost({ 1895 + kind: "quote", 1896 + subject: post.uri, 1897 + }); 1898 + }} 1899 + 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" 1900 + > 1901 + {/* You might want a specific quote icon here */} 1902 + <MdiCommentOutline /> 1903 + <span>Quote</span> 1904 + </DropdownMenu.Item> 1905 + </DropdownMenu.Content> 1906 + </DropdownMenu.Portal> 1907 + </DropdownMenu.Root> 1908 <HitSlopButton 1909 onClick={() => { 1910 likeOrUnlikePost(); ··· 2046 navigate, 2047 postid, 2048 nopics, 2049 + lightboxCallback, 2050 }: { 2051 embed?: Embed; 2052 moderation?: ModerationDecision; ··· 2057 navigate: (_: any) => void; 2058 postid?: { did: string; rkey: string }; 2059 nopics?: boolean; 2060 + lightboxCallback?: (d: LightboxProps) => void; 2061 }) { 2062 + //const [lightboxIndex, setLightboxIndex] = useState<number | null>(null); 2063 + function setLightboxIndex(number: number) { 2064 + navigate({ 2065 + to: "/profile/$did/post/$rkey/image/$i", 2066 + params: { 2067 + did: postid?.did, 2068 + rkey: postid?.rkey, 2069 + i: number.toString(), 2070 + }, 2071 + }); 2072 + } 2073 if ( 2074 AppBskyEmbedRecordWithMedia.isView(embed) && 2075 AppBskyEmbedRecord.isViewRecord(embed.record.record) && ··· 2105 navigate={navigate} 2106 postid={postid} 2107 nopics={nopics} 2108 + lightboxCallback={lightboxCallback} 2109 /> 2110 {/* padding empty div of 8px height */} 2111 <div style={{ height: 12 }} /> ··· 2273 2274 // image embed 2275 // = 2276 + if (AppBskyEmbedImages.isView(embed)) { 2277 const { images } = embed; 2278 2279 const lightboxImages = images.map((img) => ({ 2280 src: img.fullsize, 2281 alt: img.alt, 2282 })); 2283 + console.log("rendering images"); 2284 + if (lightboxCallback) { 2285 + lightboxCallback({ images: lightboxImages }); 2286 + console.log("rendering images"); 2287 + } 2288 + 2289 + if (nopics) return; 2290 2291 if (images.length > 0) { 2292 // const items = embed.images.map(img => ({ ··· 2318 }} 2319 className="border border-gray-200 dark:border-gray-800 was7 bg-gray-200 dark:bg-gray-900" 2320 > 2321 + {/* {lightboxIndex !== null && ( 2322 <Lightbox 2323 images={lightboxImages} 2324 index={lightboxIndex} ··· 2326 onNavigate={(newIndex) => setLightboxIndex(newIndex)} 2327 post={postid} 2328 /> 2329 + )} */} 2330 <img 2331 src={image.fullsize} 2332 alt={image.alt} ··· 2359 }} 2360 className="border border-gray-200 dark:border-gray-800 was7" 2361 > 2362 + {/* {lightboxIndex !== null && ( 2363 <Lightbox 2364 images={lightboxImages} 2365 index={lightboxIndex} ··· 2367 onNavigate={(newIndex) => setLightboxIndex(newIndex)} 2368 post={postid} 2369 /> 2370 + )} */} 2371 {images.map((img, i) => ( 2372 <div 2373 key={i} ··· 2409 }} 2410 className="border border-gray-200 dark:border-gray-800 was7" 2411 > 2412 + {/* {lightboxIndex !== null && ( 2413 <Lightbox 2414 images={lightboxImages} 2415 index={lightboxIndex} ··· 2417 onNavigate={(newIndex) => setLightboxIndex(newIndex)} 2418 post={postid} 2419 /> 2420 + )} */} 2421 {/* Left: 1:1 */} 2422 <div 2423 style={{ flex: 1, aspectRatio: "1 / 1", position: "relative" }} ··· 2494 }} 2495 className="border border-gray-200 dark:border-gray-800 was7" 2496 > 2497 + {/* {lightboxIndex !== null && ( 2498 <Lightbox 2499 images={lightboxImages} 2500 index={lightboxIndex} ··· 2502 onNavigate={(newIndex) => setLightboxIndex(newIndex)} 2503 post={postid} 2504 /> 2505 + )} */} 2506 {images.map((img, i) => ( 2507 <div 2508 key={i} ··· 2593 return <div />; 2594 } 2595 2596 function getDomain(url: string) { 2597 try { 2598 const { hostname } = new URL(url); ··· 2657 return { start, end, feature: f.features[0] }; 2658 }); 2659 } 2660 + export function renderTextWithFacets({ 2661 text, 2662 facets, 2663 navigate,
+59
src/components/radix-m3-rd/Button.tsx
···
··· 1 + import { Slot } from "@radix-ui/react-slot"; 2 + import clsx from "clsx"; 3 + import * as React from "react"; 4 + 5 + export type ButtonVariant = "filled" | "outlined" | "text" | "secondary"; 6 + export type ButtonSize = "sm" | "md" | "lg"; 7 + 8 + const variantClasses: Record<ButtonVariant, string> = { 9 + filled: 10 + "bg-gray-300 text-gray-900 hover:bg-gray-400 dark:bg-gray-600 dark:text-white dark:hover:bg-gray-500", 11 + secondary: 12 + "bg-gray-300 text-gray-900 hover:bg-gray-400 dark:bg-gray-600 dark:text-white dark:hover:bg-gray-500", 13 + outlined: 14 + "border border-gray-800 text-gray-800 hover:bg-gray-100 dark:border-gray-200 dark:text-gray-200 dark:hover:bg-gray-800/10", 15 + text: "bg-transparent text-gray-800 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-800/10", 16 + }; 17 + 18 + const sizeClasses: Record<ButtonSize, string> = { 19 + sm: "px-3 py-1.5 text-sm", 20 + md: "px-4 py-2 text-base", 21 + lg: "px-6 py-3 text-lg", 22 + }; 23 + 24 + export function Button({ 25 + variant = "filled", 26 + size = "md", 27 + asChild = false, 28 + ref, 29 + className, 30 + children, 31 + ...props 32 + }: { 33 + variant?: ButtonVariant; 34 + size?: ButtonSize; 35 + asChild?: boolean; 36 + className?: string; 37 + children?: React.ReactNode; 38 + ref?: React.Ref<HTMLButtonElement>; 39 + } & React.ComponentPropsWithoutRef<"button">) { 40 + const Comp = asChild ? Slot : "button"; 41 + 42 + return ( 43 + <Comp 44 + ref={ref} 45 + className={clsx( 46 + //focus:outline-none focus:ring-1 focus:ring-offset-1 focus:ring-gray-500 dark:focus:ring-gray-300 47 + "inline-flex items-center justify-center rounded-full transition-colors disabled:opacity-50 disabled:cursor-not-allowed", 48 + variantClasses[variant], 49 + sizeClasses[size], 50 + className 51 + )} 52 + {...props} 53 + > 54 + {children} 55 + </Comp> 56 + ); 57 + } 58 + 59 + Button.displayName = "Button";
+2
src/main.tsx
··· 14 import { routeTree } from "./routeTree.gen"; 15 import { isAtTopAtom } from "./utils/atoms.ts"; 16 17 const queryClient = new QueryClient({ 18 defaultOptions: { 19 queries: {
··· 14 import { routeTree } from "./routeTree.gen"; 15 import { isAtTopAtom } from "./utils/atoms.ts"; 16 17 + //initAtomToCssVar(hueAtom, "--tw-gray-hue") 18 + 19 const queryClient = new QueryClient({ 20 defaultOptions: { 21 queries: {
+26 -23
src/providers/UnifiedAuthProvider.tsx
··· 1 - // src/providers/UnifiedAuthProvider.tsx 2 - // Import both Agent and the (soon to be deprecated) AtpAgent 3 import { Agent, AtpAgent, type AtpSessionData } from "@atproto/api"; 4 import { 5 type OAuthSession, ··· 7 TokenRefreshError, 8 TokenRevokedError, 9 } from "@atproto/oauth-client-browser"; 10 import React, { 11 createContext, 12 use, ··· 15 useState, 16 } from "react"; 17 18 - import { oauthClient } from "../utils/oauthClient"; // Adjust path if needed 19 20 - // Define the unified status and authentication method 21 type AuthStatus = "loading" | "signedIn" | "signedOut"; 22 type AuthMethod = "password" | "oauth" | null; 23 24 interface AuthContextValue { 25 - agent: Agent | null; // The agent is typed as the base class `Agent` 26 status: AuthStatus; 27 authMethod: AuthMethod; 28 loginWithPassword: ( ··· 41 }: { 42 children: React.ReactNode; 43 }) => { 44 - // The state is typed as the base class `Agent`, which accepts both `Agent` and `AtpAgent` instances. 45 const [agent, setAgent] = useState<Agent | null>(null); 46 const [status, setStatus] = useState<AuthStatus>("loading"); 47 const [authMethod, setAuthMethod] = useState<AuthMethod>(null); 48 const [oauthSession, setOauthSession] = useState<OAuthSession | null>(null); 49 50 - // Unified Initialization Logic 51 const initialize = useCallback(async () => { 52 - // --- 1. Try OAuth initialization first --- 53 try { 54 const oauthResult = await oauthClient.init(); 55 if (oauthResult) { 56 // /*mass comment*/ console.log("OAuth session restored."); 57 - const apiAgent = new Agent(oauthResult.session); // Standard Agent 58 setAgent(apiAgent); 59 setOauthSession(oauthResult.session); 60 setAuthMethod("oauth"); 61 setStatus("signedIn"); 62 - return; // Success 63 } 64 } catch (e) { 65 console.error("OAuth init failed, checking password session.", e); 66 } 67 68 - // --- 2. If no OAuth, try password-based session using AtpAgent --- 69 try { 70 const service = localStorage.getItem("service"); 71 const sessionString = localStorage.getItem("sess"); 72 73 if (service && sessionString) { 74 // /*mass comment*/ console.log("Resuming password-based session using AtpAgent..."); 75 - // Use the original, working AtpAgent logic 76 const apiAgent = new AtpAgent({ service }); 77 const session: AtpSessionData = JSON.parse(sessionString); 78 await apiAgent.resumeSession(session); 79 80 // /*mass comment*/ console.log("Password-based session resumed successfully."); 81 - setAgent(apiAgent); // This works because AtpAgent is a subclass of Agent 82 setAuthMethod("password"); 83 setStatus("signedIn"); 84 - return; // Success 85 } 86 } catch (e) { 87 console.error("Failed to resume password-based session.", e); ··· 89 localStorage.removeItem("service"); 90 } 91 92 - // --- 3. If neither worked, user is signed out --- 93 // /*mass comment*/ console.log("No active session found."); 94 setStatus("signedOut"); 95 setAgent(null); 96 setAuthMethod(null); 97 - }, []); 98 99 useEffect(() => { 100 const handleOAuthSessionDeleted = ( ··· 105 setOauthSession(null); 106 setAuthMethod(null); 107 setStatus("signedOut"); 108 }; 109 110 oauthClient.addEventListener("deleted", handleOAuthSessionDeleted as EventListener); ··· 113 return () => { 114 oauthClient.removeEventListener("deleted", handleOAuthSessionDeleted as EventListener); 115 }; 116 - }, [initialize]); 117 118 - // --- Login Methods --- 119 const loginWithPassword = async ( 120 user: string, 121 password: string, ··· 125 setStatus("loading"); 126 try { 127 let sessionData: AtpSessionData | undefined; 128 - // Use the AtpAgent for its simple login and session persistence 129 const apiAgent = new AtpAgent({ 130 service, 131 persistSession: (_evt, sess) => { ··· 137 if (sessionData) { 138 localStorage.setItem("service", service); 139 localStorage.setItem("sess", JSON.stringify(sessionData)); 140 - setAgent(apiAgent); // Store the AtpAgent instance in our state 141 setAuthMethod("password"); 142 setStatus("signedIn"); 143 // /*mass comment*/ console.log("Successfully logged in with password."); 144 } else { 145 throw new Error("Session data not persisted after login."); ··· 147 } catch (e) { 148 console.error("Password login failed:", e); 149 setStatus("signedOut"); 150 throw e; 151 } 152 }; ··· 161 } 162 }, [status]); 163 164 - // --- Unified Logout --- 165 const logout = useCallback(async () => { 166 if (status !== "signedIn" || !agent) return; 167 setStatus("loading"); ··· 173 } else if (authMethod === "password") { 174 localStorage.removeItem("service"); 175 localStorage.removeItem("sess"); 176 - // AtpAgent has its own logout methods 177 await (agent as AtpAgent).com.atproto.server.deleteSession(); 178 // /*mass comment*/ console.log("Password-based session deleted."); 179 } ··· 184 setAuthMethod(null); 185 setOauthSession(null); 186 setStatus("signedOut"); 187 } 188 - }, [status, authMethod, agent, oauthSession]); 189 190 return ( 191 <AuthContext
··· 1 import { Agent, AtpAgent, type AtpSessionData } from "@atproto/api"; 2 import { 3 type OAuthSession, ··· 5 TokenRefreshError, 6 TokenRevokedError, 7 } from "@atproto/oauth-client-browser"; 8 + import { useAtom } from "jotai"; 9 import React, { 10 createContext, 11 use, ··· 14 useState, 15 } from "react"; 16 17 + import { quickAuthAtom } from "~/utils/atoms"; 18 + 19 + import { oauthClient } from "../utils/oauthClient"; 20 21 type AuthStatus = "loading" | "signedIn" | "signedOut"; 22 type AuthMethod = "password" | "oauth" | null; 23 24 interface AuthContextValue { 25 + agent: Agent | null; 26 status: AuthStatus; 27 authMethod: AuthMethod; 28 loginWithPassword: ( ··· 41 }: { 42 children: React.ReactNode; 43 }) => { 44 const [agent, setAgent] = useState<Agent | null>(null); 45 const [status, setStatus] = useState<AuthStatus>("loading"); 46 const [authMethod, setAuthMethod] = useState<AuthMethod>(null); 47 const [oauthSession, setOauthSession] = useState<OAuthSession | null>(null); 48 + const [quickAuth, setQuickAuth] = useAtom(quickAuthAtom); 49 50 const initialize = useCallback(async () => { 51 try { 52 const oauthResult = await oauthClient.init(); 53 if (oauthResult) { 54 // /*mass comment*/ console.log("OAuth session restored."); 55 + const apiAgent = new Agent(oauthResult.session); 56 setAgent(apiAgent); 57 setOauthSession(oauthResult.session); 58 setAuthMethod("oauth"); 59 setStatus("signedIn"); 60 + setQuickAuth(apiAgent?.did || null); 61 + return; 62 } 63 } catch (e) { 64 console.error("OAuth init failed, checking password session.", e); 65 + if (!quickAuth) { 66 + // quickAuth restoration. if last used method is oauth we immediately call for oauth redo 67 + // (and set a persistent atom somewhere to not retry again if it failed) 68 + } 69 } 70 71 try { 72 const service = localStorage.getItem("service"); 73 const sessionString = localStorage.getItem("sess"); 74 75 if (service && sessionString) { 76 // /*mass comment*/ console.log("Resuming password-based session using AtpAgent..."); 77 const apiAgent = new AtpAgent({ service }); 78 const session: AtpSessionData = JSON.parse(sessionString); 79 await apiAgent.resumeSession(session); 80 81 // /*mass comment*/ console.log("Password-based session resumed successfully."); 82 + setAgent(apiAgent); 83 setAuthMethod("password"); 84 setStatus("signedIn"); 85 + setQuickAuth(apiAgent?.did || null); 86 + return; 87 } 88 } catch (e) { 89 console.error("Failed to resume password-based session.", e); ··· 91 localStorage.removeItem("service"); 92 } 93 94 // /*mass comment*/ console.log("No active session found."); 95 setStatus("signedOut"); 96 setAgent(null); 97 setAuthMethod(null); 98 + // do we want to null it here? 99 + setQuickAuth(null); 100 + }, [quickAuth, setQuickAuth]); 101 102 useEffect(() => { 103 const handleOAuthSessionDeleted = ( ··· 108 setOauthSession(null); 109 setAuthMethod(null); 110 setStatus("signedOut"); 111 + setQuickAuth(null); 112 }; 113 114 oauthClient.addEventListener("deleted", handleOAuthSessionDeleted as EventListener); ··· 117 return () => { 118 oauthClient.removeEventListener("deleted", handleOAuthSessionDeleted as EventListener); 119 }; 120 + }, [initialize, setQuickAuth]); 121 122 const loginWithPassword = async ( 123 user: string, 124 password: string, ··· 128 setStatus("loading"); 129 try { 130 let sessionData: AtpSessionData | undefined; 131 const apiAgent = new AtpAgent({ 132 service, 133 persistSession: (_evt, sess) => { ··· 139 if (sessionData) { 140 localStorage.setItem("service", service); 141 localStorage.setItem("sess", JSON.stringify(sessionData)); 142 + setAgent(apiAgent); 143 setAuthMethod("password"); 144 setStatus("signedIn"); 145 + setQuickAuth(apiAgent?.did || null); 146 // /*mass comment*/ console.log("Successfully logged in with password."); 147 } else { 148 throw new Error("Session data not persisted after login."); ··· 150 } catch (e) { 151 console.error("Password login failed:", e); 152 setStatus("signedOut"); 153 + setQuickAuth(null); 154 throw e; 155 } 156 }; ··· 165 } 166 }, [status]); 167 168 const logout = useCallback(async () => { 169 if (status !== "signedIn" || !agent) return; 170 setStatus("loading"); ··· 176 } else if (authMethod === "password") { 177 localStorage.removeItem("service"); 178 localStorage.removeItem("sess"); 179 await (agent as AtpAgent).com.atproto.server.deleteSession(); 180 // /*mass comment*/ console.log("Password-based session deleted."); 181 } ··· 186 setAuthMethod(null); 187 setOauthSession(null); 188 setStatus("signedOut"); 189 + setQuickAuth(null); 190 } 191 + }, [status, agent, authMethod, oauthSession, setQuickAuth]); 192 193 return ( 194 <AuthContext
+36 -5
src/routeTree.gen.ts
··· 21 import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b' 22 import { Route as PathlessLayoutNestedLayoutRouteARouteImport } from './routes/_pathlessLayout/_nested-layout/route-a' 23 import { Route as ProfileDidPostRkeyRouteImport } from './routes/profile.$did/post.$rkey' 24 25 const SettingsRoute = SettingsRouteImport.update({ 26 id: '/settings', ··· 83 path: '/profile/$did/post/$rkey', 84 getParentRoute: () => rootRouteImport, 85 } as any) 86 87 export interface FileRoutesByFullPath { 88 '/': typeof IndexRoute ··· 94 '/route-a': typeof PathlessLayoutNestedLayoutRouteARoute 95 '/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute 96 '/profile/$did': typeof ProfileDidIndexRoute 97 - '/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRoute 98 } 99 export interface FileRoutesByTo { 100 '/': typeof IndexRoute ··· 106 '/route-a': typeof PathlessLayoutNestedLayoutRouteARoute 107 '/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute 108 '/profile/$did': typeof ProfileDidIndexRoute 109 - '/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRoute 110 } 111 export interface FileRoutesById { 112 __root__: typeof rootRouteImport ··· 121 '/_pathlessLayout/_nested-layout/route-a': typeof PathlessLayoutNestedLayoutRouteARoute 122 '/_pathlessLayout/_nested-layout/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute 123 '/profile/$did/': typeof ProfileDidIndexRoute 124 - '/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRoute 125 } 126 export interface FileRouteTypes { 127 fileRoutesByFullPath: FileRoutesByFullPath ··· 136 | '/route-b' 137 | '/profile/$did' 138 | '/profile/$did/post/$rkey' 139 fileRoutesByTo: FileRoutesByTo 140 to: 141 | '/' ··· 148 | '/route-b' 149 | '/profile/$did' 150 | '/profile/$did/post/$rkey' 151 id: 152 | '__root__' 153 | '/' ··· 162 | '/_pathlessLayout/_nested-layout/route-b' 163 | '/profile/$did/' 164 | '/profile/$did/post/$rkey' 165 fileRoutesById: FileRoutesById 166 } 167 export interface RootRouteChildren { ··· 173 SettingsRoute: typeof SettingsRoute 174 CallbackIndexRoute: typeof CallbackIndexRoute 175 ProfileDidIndexRoute: typeof ProfileDidIndexRoute 176 - ProfileDidPostRkeyRoute: typeof ProfileDidPostRkeyRoute 177 } 178 179 declare module '@tanstack/react-router' { ··· 262 preLoaderRoute: typeof ProfileDidPostRkeyRouteImport 263 parentRoute: typeof rootRouteImport 264 } 265 } 266 } 267 ··· 295 PathlessLayoutRouteChildren, 296 ) 297 298 const rootRouteChildren: RootRouteChildren = { 299 IndexRoute: IndexRoute, 300 PathlessLayoutRoute: PathlessLayoutRouteWithChildren, ··· 304 SettingsRoute: SettingsRoute, 305 CallbackIndexRoute: CallbackIndexRoute, 306 ProfileDidIndexRoute: ProfileDidIndexRoute, 307 - ProfileDidPostRkeyRoute: ProfileDidPostRkeyRoute, 308 } 309 export const routeTree = rootRouteImport 310 ._addFileChildren(rootRouteChildren)
··· 21 import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b' 22 import { Route as PathlessLayoutNestedLayoutRouteARouteImport } from './routes/_pathlessLayout/_nested-layout/route-a' 23 import { Route as ProfileDidPostRkeyRouteImport } from './routes/profile.$did/post.$rkey' 24 + import { Route as ProfileDidPostRkeyImageIRouteImport } from './routes/profile.$did/post.$rkey.image.$i' 25 26 const SettingsRoute = SettingsRouteImport.update({ 27 id: '/settings', ··· 84 path: '/profile/$did/post/$rkey', 85 getParentRoute: () => rootRouteImport, 86 } as any) 87 + const ProfileDidPostRkeyImageIRoute = 88 + ProfileDidPostRkeyImageIRouteImport.update({ 89 + id: '/image/$i', 90 + path: '/image/$i', 91 + getParentRoute: () => ProfileDidPostRkeyRoute, 92 + } as any) 93 94 export interface FileRoutesByFullPath { 95 '/': typeof IndexRoute ··· 101 '/route-a': typeof PathlessLayoutNestedLayoutRouteARoute 102 '/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute 103 '/profile/$did': typeof ProfileDidIndexRoute 104 + '/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren 105 + '/profile/$did/post/$rkey/image/$i': typeof ProfileDidPostRkeyImageIRoute 106 } 107 export interface FileRoutesByTo { 108 '/': typeof IndexRoute ··· 114 '/route-a': typeof PathlessLayoutNestedLayoutRouteARoute 115 '/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute 116 '/profile/$did': typeof ProfileDidIndexRoute 117 + '/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren 118 + '/profile/$did/post/$rkey/image/$i': typeof ProfileDidPostRkeyImageIRoute 119 } 120 export interface FileRoutesById { 121 __root__: typeof rootRouteImport ··· 130 '/_pathlessLayout/_nested-layout/route-a': typeof PathlessLayoutNestedLayoutRouteARoute 131 '/_pathlessLayout/_nested-layout/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute 132 '/profile/$did/': typeof ProfileDidIndexRoute 133 + '/profile/$did/post/$rkey': typeof ProfileDidPostRkeyRouteWithChildren 134 + '/profile/$did/post/$rkey/image/$i': typeof ProfileDidPostRkeyImageIRoute 135 } 136 export interface FileRouteTypes { 137 fileRoutesByFullPath: FileRoutesByFullPath ··· 146 | '/route-b' 147 | '/profile/$did' 148 | '/profile/$did/post/$rkey' 149 + | '/profile/$did/post/$rkey/image/$i' 150 fileRoutesByTo: FileRoutesByTo 151 to: 152 | '/' ··· 159 | '/route-b' 160 | '/profile/$did' 161 | '/profile/$did/post/$rkey' 162 + | '/profile/$did/post/$rkey/image/$i' 163 id: 164 | '__root__' 165 | '/' ··· 174 | '/_pathlessLayout/_nested-layout/route-b' 175 | '/profile/$did/' 176 | '/profile/$did/post/$rkey' 177 + | '/profile/$did/post/$rkey/image/$i' 178 fileRoutesById: FileRoutesById 179 } 180 export interface RootRouteChildren { ··· 186 SettingsRoute: typeof SettingsRoute 187 CallbackIndexRoute: typeof CallbackIndexRoute 188 ProfileDidIndexRoute: typeof ProfileDidIndexRoute 189 + ProfileDidPostRkeyRoute: typeof ProfileDidPostRkeyRouteWithChildren 190 } 191 192 declare module '@tanstack/react-router' { ··· 275 preLoaderRoute: typeof ProfileDidPostRkeyRouteImport 276 parentRoute: typeof rootRouteImport 277 } 278 + '/profile/$did/post/$rkey/image/$i': { 279 + id: '/profile/$did/post/$rkey/image/$i' 280 + path: '/image/$i' 281 + fullPath: '/profile/$did/post/$rkey/image/$i' 282 + preLoaderRoute: typeof ProfileDidPostRkeyImageIRouteImport 283 + parentRoute: typeof ProfileDidPostRkeyRoute 284 + } 285 } 286 } 287 ··· 315 PathlessLayoutRouteChildren, 316 ) 317 318 + interface ProfileDidPostRkeyRouteChildren { 319 + ProfileDidPostRkeyImageIRoute: typeof ProfileDidPostRkeyImageIRoute 320 + } 321 + 322 + const ProfileDidPostRkeyRouteChildren: ProfileDidPostRkeyRouteChildren = { 323 + ProfileDidPostRkeyImageIRoute: ProfileDidPostRkeyImageIRoute, 324 + } 325 + 326 + const ProfileDidPostRkeyRouteWithChildren = 327 + ProfileDidPostRkeyRoute._addFileChildren(ProfileDidPostRkeyRouteChildren) 328 + 329 const rootRouteChildren: RootRouteChildren = { 330 IndexRoute: IndexRoute, 331 PathlessLayoutRoute: PathlessLayoutRouteWithChildren, ··· 335 SettingsRoute: SettingsRoute, 336 CallbackIndexRoute: CallbackIndexRoute, 337 ProfileDidIndexRoute: ProfileDidIndexRoute, 338 + ProfileDidPostRkeyRoute: ProfileDidPostRkeyRouteWithChildren, 339 } 340 export const routeTree = rootRouteImport 341 ._addFileChildren(rootRouteChildren)
+181 -117
src/routes/__root.tsx
··· 12 useNavigate, 13 } from "@tanstack/react-router"; 14 import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; 15 - import { useState } from "react"; 16 import * as React from "react"; 17 import { KeepAliveOutlet, KeepAliveProvider } from "tanstack-router-keepalive"; 18 19 import { DefaultCatchBoundary } from "~/components/DefaultCatchBoundary"; 20 import Login from "~/components/Login"; 21 import { NotFound } from "~/components/NotFound"; 22 import { UnifiedAuthProvider, useAuth } from "~/providers/UnifiedAuthProvider"; 23 import { seo } from "~/utils/seo"; 24 25 export const Route = createRootRouteWithContext<{ ··· 85 } 86 87 function RootDocument({ children }: { children: React.ReactNode }) { 88 const location = useLocation(); 89 const navigate = useNavigate(); 90 const { agent } = useAuth(); ··· 95 agent && 96 (location.pathname === `/profile/${agent?.did}` || 97 location.pathname === `/profile/${encodeURIComponent(agent?.did ?? "")}`); 98 99 - const [postOpen, setPostOpen] = useState(false); 100 - const [postText, setPostText] = useState(""); 101 - const [posting, setPosting] = useState(false); 102 - const [postSuccess, setPostSuccess] = useState(false); 103 - const [postError, setPostError] = useState<string | null>(null); 104 105 - async function handlePost() { 106 - if (!agent) return; 107 - setPosting(true); 108 - setPostError(null); 109 - try { 110 - await agent.com.atproto.repo.createRecord({ 111 - collection: "app.bsky.feed.post", 112 - repo: agent.assertDid, 113 - record: { 114 - $type: "app.bsky.feed.post", 115 - text: postText, 116 - createdAt: new Date().toISOString(), 117 - }, 118 - }); 119 - setPostSuccess(true); 120 - setPostText(""); 121 - setTimeout(() => { 122 - setPostSuccess(false); 123 - setPostOpen(false); 124 - }, 1500); 125 - } catch (e: any) { 126 - setPostError(e?.message || "Failed to post"); 127 - } finally { 128 - setPosting(false); 129 - } 130 - } 131 132 return ( 133 <> 134 - {postOpen && ( 135 - <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"> 136 - <div className="bg-white dark:bg-gray-900 rounded-lg shadow-lg p-6 w-full max-w-md relative"> 137 - <button 138 - className="absolute top-2 right-2 text-gray-400 hover:text-gray-700 dark:hover:text-gray-200" 139 - onClick={() => !posting && setPostOpen(false)} 140 - disabled={posting} 141 - aria-label="Close" 142 - > 143 - ร— 144 - </button> 145 - <h2 className="text-lg font-bold mb-2">Create Post</h2> 146 - {postSuccess ? ( 147 - <div className="flex flex-col items-center justify-center py-8"> 148 - <span className="text-green-500 text-4xl mb-2">โœ“</span> 149 - <span className="text-green-600">Posted!</span> 150 - </div> 151 - ) : ( 152 - <> 153 - <textarea 154 - className="w-full border rounded p-2 mb-2 dark:bg-gray-800 dark:border-gray-700" 155 - rows={4} 156 - placeholder="What's on your mind?" 157 - value={postText} 158 - onChange={(e) => setPostText(e.target.value)} 159 - disabled={posting} 160 - autoFocus 161 - /> 162 - {postError && ( 163 - <div className="text-red-500 text-sm mb-2">{postError}</div> 164 - )} 165 - <button 166 - className="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded disabled:opacity-50" 167 - onClick={handlePost} 168 - disabled={posting || !postText.trim()} 169 - > 170 - {posting ? "Posting..." : "Post"} 171 - </button> 172 - </> 173 - )} 174 - </div> 175 - </div> 176 - )} 177 178 <div className="min-h-screen flex justify-center bg-gray-50 dark:bg-gray-950"> 179 <nav className="hidden lg:flex h-screen w-[250px] flex-col gap-0 p-4 dark:border-gray-800 sticky top-0 self-start"> 180 <div className="flex items-center gap-3 mb-4"> 181 - <img src="/redstar.png" alt="Red Dwarf Logo" className="w-8 h-8" /> 182 <span className="font-extrabold text-2xl tracking-tight text-gray-900 dark:text-gray-100"> 183 Red Dwarf{" "} 184 {/* <span className="text-gray-500 dark:text-gray-400 text-sm"> ··· 191 <IconMaterialSymbolsHomeOutline className="w-6 h-6" /> 192 } 193 ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />} 194 - active={isHome} 195 onClickCallbback={() => 196 navigate({ 197 to: "/", ··· 202 /> 203 204 <MaterialNavItem 205 InactiveIcon={ 206 <IconMaterialSymbolsNotificationsOutline className="w-6 h-6" /> 207 } 208 ActiveIcon={ 209 <IconMaterialSymbolsNotifications className="w-6 h-6" /> 210 } 211 - active={isNotifications} 212 onClickCallbback={() => 213 navigate({ 214 to: "/notifications", ··· 220 <MaterialNavItem 221 InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 222 ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 223 - active={location.pathname.startsWith("/feeds")} 224 onClickCallbback={() => 225 navigate({ 226 to: "/feeds", ··· 230 text="Feeds" 231 /> 232 <MaterialNavItem 233 - InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 234 - ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 235 - active={location.pathname.startsWith("/search")} 236 - onClickCallbback={() => 237 - navigate({ 238 - to: "/search", 239 - //params: { did: agent.assertDid }, 240 - }) 241 - } 242 - text="Search" 243 - /> 244 - <MaterialNavItem 245 InactiveIcon={ 246 <IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" /> 247 } 248 ActiveIcon={ 249 <IconMaterialSymbolsAccountCircle className="w-6 h-6" /> 250 } 251 - active={isProfile ?? false} 252 onClickCallbback={() => { 253 if (authed && agent && agent.assertDid) { 254 //window.location.href = `/profile/${agent.assertDid}`; ··· 265 <IconMaterialSymbolsSettingsOutline className="w-6 h-6" /> 266 } 267 ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />} 268 - active={location.pathname.startsWith("/settings")} 269 onClickCallbback={() => 270 navigate({ 271 to: "/settings", ··· 279 InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 280 ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 281 //active={true} 282 - onClickCallbback={() => setPostOpen(true)} 283 text="Post" 284 /> 285 </div> ··· 415 </div> 416 </nav> 417 418 {agent?.did && ( 419 <button 420 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" 421 style={{ boxShadow: "0 4px 24px 0 rgba(0,0,0,0.12)" }} 422 - onClick={() => setPostOpen(true)} 423 type="button" 424 aria-label="Create Post" 425 > ··· 431 </button> 432 )} 433 434 - <main className="w-full max-w-[600px] lg:border-x border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 pb-16 lg:pb-0 overflow-x-clip"> 435 {children} 436 </main> 437 438 <aside className="hidden lg:flex h-screen w-[250px] sticky top-0 self-start flex-col"> 439 <Login /> 440 441 <div className="flex-1"></div> 442 <p className="text-xs text-gray-400 dark:text-gray-500 text-justify mx-4 mb-4"> 443 - Red Dwarf is a bluesky client that uses Constellation and direct PDS 444 - queries. Skylite would be a self-hosted bluesky "instance". Stay 445 - tuned for the release of Skylite. 446 </p> 447 </aside> 448 </div> 449 450 {agent?.did ? ( 451 - <nav className="lg: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"> 452 <div className="flex justify-around items-center p-2"> 453 <MaterialNavItem 454 small ··· 456 <IconMaterialSymbolsHomeOutline className="w-6 h-6" /> 457 } 458 ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />} 459 - active={isHome} 460 onClickCallbback={() => 461 navigate({ 462 to: "/", ··· 484 small 485 InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 486 ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 487 - active={location.pathname.startsWith("/search")} 488 onClickCallbback={() => 489 navigate({ 490 to: "/search", 491 //params: { did: agent.assertDid }, 492 }) 493 } 494 - text="Search" 495 /> 496 {/* <Link 497 to="/search" ··· 516 ActiveIcon={ 517 <IconMaterialSymbolsNotifications className="w-6 h-6" /> 518 } 519 - active={isNotifications} 520 onClickCallbback={() => 521 navigate({ 522 to: "/notifications", ··· 551 ActiveIcon={ 552 <IconMaterialSymbolsAccountCircle className="w-6 h-6" /> 553 } 554 - active={isProfile ?? false} 555 onClickCallbback={() => { 556 if (authed && agent && agent.assertDid) { 557 //window.location.href = `/profile/${agent.assertDid}`; ··· 589 <IconMaterialSymbolsSettingsOutline className="w-6 h-6" /> 590 } 591 ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />} 592 - active={location.pathname.startsWith("/settings")} 593 onClickCallbback={() => 594 navigate({ 595 to: "/settings", ··· 618 ) : ( 619 <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"> 620 <div className="flex items-center gap-2"> 621 - <img src="/redstar.png" alt="Red Dwarf Logo" className="w-6 h-6" /> 622 <span className="font-bold text-lg text-gray-900 dark:text-gray-100"> 623 Red Dwarf{" "} 624 {/* <span className="text-gray-500 dark:text-gray-400 text-sm"> ··· 651 text: string; 652 active: boolean; 653 onClickCallbback: () => void; 654 - small?: boolean; 655 }) { 656 if (small) 657 return ( 658 <button 659 - className={`flex flex-col items-center rounded-lg transition-colors flex-1 gap-1 ${ 660 active 661 ? "text-gray-900 dark:text-gray-100" 662 : "text-gray-600 dark:text-gray-400" ··· 714 text: string; 715 //active: boolean; 716 onClickCallbback: () => void; 717 - small?: boolean; 718 }) { 719 const active = false; 720 return ( 721 <button 722 - className={`flex border border-gray-400 dark:border-gray-400 flex-row h-12 min-h-12 max-h-12 px-4 py-0.5 items-center rounded-full transition-colors gap-1 ${ 723 active 724 ? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-700 bg-gray-200 hover:dark:bg-gray-600" 725 : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800" ··· 728 onClickCallbback(); 729 }} 730 > 731 - <div className={`mr-2 ${active ? " " : " "}`}> 732 {active ? ActiveIcon : InactiveIcon} 733 </div> 734 - <span 735 - className={`text-[17px] text-roboto ${active ? "font-medium" : ""}`} 736 - > 737 - {text} 738 - </span> 739 </button> 740 ); 741 }
··· 12 useNavigate, 13 } from "@tanstack/react-router"; 14 import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; 15 + import { useAtom } from "jotai"; 16 import * as React from "react"; 17 import { KeepAliveOutlet, KeepAliveProvider } from "tanstack-router-keepalive"; 18 19 + import { Composer } from "~/components/Composer"; 20 import { DefaultCatchBoundary } from "~/components/DefaultCatchBoundary"; 21 + import { Import } from "~/components/Import"; 22 import Login from "~/components/Login"; 23 import { NotFound } from "~/components/NotFound"; 24 + import { FluentEmojiHighContrastGlowingStar } from "~/components/Star"; 25 import { UnifiedAuthProvider, useAuth } from "~/providers/UnifiedAuthProvider"; 26 + import { composerAtom, hueAtom, useAtomCssVar } from "~/utils/atoms"; 27 import { seo } from "~/utils/seo"; 28 29 export const Route = createRootRouteWithContext<{ ··· 89 } 90 91 function RootDocument({ children }: { children: React.ReactNode }) { 92 + useAtomCssVar(hueAtom, "--tw-gray-hue"); 93 const location = useLocation(); 94 const navigate = useNavigate(); 95 const { agent } = useAuth(); ··· 100 agent && 101 (location.pathname === `/profile/${agent?.did}` || 102 location.pathname === `/profile/${encodeURIComponent(agent?.did ?? "")}`); 103 + const isSettings = location.pathname.startsWith("/settings"); 104 + const isSearch = location.pathname.startsWith("/search"); 105 + const isFeeds = location.pathname.startsWith("/feeds"); 106 107 + const locationEnum: 108 + | "feeds" 109 + | "search" 110 + | "settings" 111 + | "notifications" 112 + | "profile" 113 + | "home" = isFeeds 114 + ? "feeds" 115 + : isSearch 116 + ? "search" 117 + : isSettings 118 + ? "settings" 119 + : isNotifications 120 + ? "notifications" 121 + : isProfile 122 + ? "profile" 123 + : "home"; 124 125 + const [, setComposerPost] = useAtom(composerAtom); 126 127 return ( 128 <> 129 + <Composer /> 130 131 <div className="min-h-screen flex justify-center bg-gray-50 dark:bg-gray-950"> 132 <nav className="hidden lg:flex h-screen w-[250px] flex-col gap-0 p-4 dark:border-gray-800 sticky top-0 self-start"> 133 <div className="flex items-center gap-3 mb-4"> 134 + <FluentEmojiHighContrastGlowingStar className="h-8 w-8" style={{color: "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))"}} /> 135 <span className="font-extrabold text-2xl tracking-tight text-gray-900 dark:text-gray-100"> 136 Red Dwarf{" "} 137 {/* <span className="text-gray-500 dark:text-gray-400 text-sm"> ··· 144 <IconMaterialSymbolsHomeOutline className="w-6 h-6" /> 145 } 146 ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />} 147 + active={locationEnum === "home"} 148 onClickCallbback={() => 149 navigate({ 150 to: "/", ··· 155 /> 156 157 <MaterialNavItem 158 + InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 159 + ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 160 + active={locationEnum === "search"} 161 + onClickCallbback={() => 162 + navigate({ 163 + to: "/search", 164 + //params: { did: agent.assertDid }, 165 + }) 166 + } 167 + text="Explore" 168 + /> 169 + <MaterialNavItem 170 InactiveIcon={ 171 <IconMaterialSymbolsNotificationsOutline className="w-6 h-6" /> 172 } 173 ActiveIcon={ 174 <IconMaterialSymbolsNotifications className="w-6 h-6" /> 175 } 176 + active={locationEnum === "notifications"} 177 onClickCallbback={() => 178 navigate({ 179 to: "/notifications", ··· 185 <MaterialNavItem 186 InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 187 ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 188 + active={locationEnum === "feeds"} 189 onClickCallbback={() => 190 navigate({ 191 to: "/feeds", ··· 195 text="Feeds" 196 /> 197 <MaterialNavItem 198 InactiveIcon={ 199 <IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" /> 200 } 201 ActiveIcon={ 202 <IconMaterialSymbolsAccountCircle className="w-6 h-6" /> 203 } 204 + active={locationEnum === "profile"} 205 onClickCallbback={() => { 206 if (authed && agent && agent.assertDid) { 207 //window.location.href = `/profile/${agent.assertDid}`; ··· 218 <IconMaterialSymbolsSettingsOutline className="w-6 h-6" /> 219 } 220 ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />} 221 + active={locationEnum === "settings"} 222 onClickCallbback={() => 223 navigate({ 224 to: "/settings", ··· 232 InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 233 ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 234 //active={true} 235 + onClickCallbback={() => setComposerPost({ kind: 'root' })} 236 text="Post" 237 /> 238 </div> ··· 368 </div> 369 </nav> 370 371 + <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"> 372 + <div className="flex items-center gap-3 mb-4"> 373 + <FluentEmojiHighContrastGlowingStar className="h-8 w-8" style={{color: "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))"}} /> 374 + </div> 375 + <MaterialNavItem 376 + small 377 + InactiveIcon={ 378 + <IconMaterialSymbolsHomeOutline className="w-6 h-6" /> 379 + } 380 + ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />} 381 + active={locationEnum === "home"} 382 + onClickCallbback={() => 383 + navigate({ 384 + to: "/", 385 + //params: { did: agent.assertDid }, 386 + }) 387 + } 388 + text="Home" 389 + /> 390 + 391 + <MaterialNavItem 392 + small 393 + InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 394 + ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 395 + active={locationEnum === "search"} 396 + onClickCallbback={() => 397 + navigate({ 398 + to: "/search", 399 + //params: { did: agent.assertDid }, 400 + }) 401 + } 402 + text="Explore" 403 + /> 404 + <MaterialNavItem 405 + small 406 + InactiveIcon={ 407 + <IconMaterialSymbolsNotificationsOutline className="w-6 h-6" /> 408 + } 409 + ActiveIcon={ 410 + <IconMaterialSymbolsNotifications className="w-6 h-6" /> 411 + } 412 + active={locationEnum === "notifications"} 413 + onClickCallbback={() => 414 + navigate({ 415 + to: "/notifications", 416 + //params: { did: agent.assertDid }, 417 + }) 418 + } 419 + text="Notifications" 420 + /> 421 + <MaterialNavItem 422 + small 423 + InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 424 + ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 425 + active={locationEnum === "feeds"} 426 + onClickCallbback={() => 427 + navigate({ 428 + to: "/feeds", 429 + //params: { did: agent.assertDid }, 430 + }) 431 + } 432 + text="Feeds" 433 + /> 434 + <MaterialNavItem 435 + small 436 + InactiveIcon={ 437 + <IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" /> 438 + } 439 + ActiveIcon={ 440 + <IconMaterialSymbolsAccountCircle className="w-6 h-6" /> 441 + } 442 + active={locationEnum === "profile"} 443 + onClickCallbback={() => { 444 + if (authed && agent && agent.assertDid) { 445 + //window.location.href = `/profile/${agent.assertDid}`; 446 + navigate({ 447 + to: "/profile/$did", 448 + params: { did: agent.assertDid }, 449 + }); 450 + } 451 + }} 452 + text="Profile" 453 + /> 454 + <MaterialNavItem 455 + small 456 + InactiveIcon={ 457 + <IconMaterialSymbolsSettingsOutline className="w-6 h-6" /> 458 + } 459 + ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />} 460 + active={locationEnum === "settings"} 461 + onClickCallbback={() => 462 + navigate({ 463 + to: "/settings", 464 + //params: { did: agent.assertDid }, 465 + }) 466 + } 467 + text="Settings" 468 + /> 469 + <div className="flex flex-row items-center justify-center mt-3"> 470 + <MaterialPillButton 471 + small 472 + InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 473 + ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 474 + //active={true} 475 + onClickCallbback={() => setComposerPost({ kind: 'root' })} 476 + text="Post" 477 + /> 478 + </div> 479 + </nav> 480 + 481 {agent?.did && ( 482 <button 483 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" 484 style={{ boxShadow: "0 4px 24px 0 rgba(0,0,0,0.12)" }} 485 + onClick={() => setComposerPost({ kind: 'root' })} 486 type="button" 487 aria-label="Create Post" 488 > ··· 494 </button> 495 )} 496 497 + <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"> 498 {children} 499 </main> 500 501 <aside className="hidden lg:flex h-screen w-[250px] sticky top-0 self-start flex-col"> 502 + <div className="px-4 pt-4"><Import /></div> 503 <Login /> 504 505 <div className="flex-1"></div> 506 <p className="text-xs text-gray-400 dark:text-gray-500 text-justify mx-4 mb-4"> 507 + Red Dwarf is a Bluesky client that does not rely on any Bluesky API App Servers. Instead, it uses Microcosm to fetch records directly from each users' PDS (via Slingshot) and connect them using backlinks (via Constellation) 508 </p> 509 </aside> 510 </div> 511 512 {agent?.did ? ( 513 + <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"> 514 <div className="flex justify-around items-center p-2"> 515 <MaterialNavItem 516 small ··· 518 <IconMaterialSymbolsHomeOutline className="w-6 h-6" /> 519 } 520 ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />} 521 + active={locationEnum === "home"} 522 onClickCallbback={() => 523 navigate({ 524 to: "/", ··· 546 small 547 InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 548 ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 549 + active={locationEnum === "search"} 550 onClickCallbback={() => 551 navigate({ 552 to: "/search", 553 //params: { did: agent.assertDid }, 554 }) 555 } 556 + text="Explore" 557 /> 558 {/* <Link 559 to="/search" ··· 578 ActiveIcon={ 579 <IconMaterialSymbolsNotifications className="w-6 h-6" /> 580 } 581 + active={locationEnum === "notifications"} 582 onClickCallbback={() => 583 navigate({ 584 to: "/notifications", ··· 613 ActiveIcon={ 614 <IconMaterialSymbolsAccountCircle className="w-6 h-6" /> 615 } 616 + active={locationEnum === "profile"} 617 onClickCallbback={() => { 618 if (authed && agent && agent.assertDid) { 619 //window.location.href = `/profile/${agent.assertDid}`; ··· 651 <IconMaterialSymbolsSettingsOutline className="w-6 h-6" /> 652 } 653 ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />} 654 + active={locationEnum === "settings"} 655 onClickCallbback={() => 656 navigate({ 657 to: "/settings", ··· 680 ) : ( 681 <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"> 682 <div className="flex items-center gap-2"> 683 + <FluentEmojiHighContrastGlowingStar className="h-6 w-6" style={{color: "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))"}} /> 684 <span className="font-bold text-lg text-gray-900 dark:text-gray-100"> 685 Red Dwarf{" "} 686 {/* <span className="text-gray-500 dark:text-gray-400 text-sm"> ··· 713 text: string; 714 active: boolean; 715 onClickCallbback: () => void; 716 + small?: boolean | string; 717 }) { 718 if (small) 719 return ( 720 <button 721 + className={`flex flex-col items-center rounded-lg transition-colors ${small} gap-1 ${ 722 active 723 ? "text-gray-900 dark:text-gray-100" 724 : "text-gray-600 dark:text-gray-400" ··· 776 text: string; 777 //active: boolean; 778 onClickCallbback: () => void; 779 + small?: boolean | string; 780 }) { 781 const active = false; 782 return ( 783 <button 784 + 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 ${ 785 active 786 ? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-700 bg-gray-200 hover:dark:bg-gray-600" 787 : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800" ··· 790 onClickCallbback(); 791 }} 792 > 793 + <div className={`${!small && "mr-2"} ${active ? " " : " "}`}> 794 {active ? ActiveIcon : InactiveIcon} 795 </div> 796 + {!small && ( 797 + <span 798 + className={`text-[17px] text-roboto ${active ? "font-medium" : ""}`} 799 + > 800 + {text} 801 + </span> 802 + )} 803 </button> 804 ); 805 }
+40 -40
src/routes/index.tsx
··· 1 import { createFileRoute } from "@tanstack/react-router"; 2 import { useAtom } from "jotai"; 3 import * as React from "react"; 4 - import { useEffect, useLayoutEffect } from "react"; 5 6 import { Header } from "~/components/Header"; 7 import { InfiniteCustomFeed } from "~/components/InfiniteCustomFeed"; 8 import { useAuth } from "~/providers/UnifiedAuthProvider"; 9 import { 10 - agentAtom, 11 - authedAtom, 12 feedScrollPositionsAtom, 13 isAtTopAtom, 14 selectedFeedUriAtom, 15 - store, 16 } from "~/utils/atoms"; 17 //import { usePersistentStore } from "~/providers/PersistentStoreProvider"; 18 import { ··· 107 } = useAuth(); 108 const authed = !!agent?.did; 109 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 - // eslint-disable-next-line @typescript-eslint/ban-ts-comment 120 - // @ts-ignore is it just me or is the type really weird here it should be Agent not AtpAgent 121 - store.set(agentAtom, agent); 122 - } else { 123 - store.set(agentAtom, null); 124 - } 125 - }, [status, agent, authed]); 126 127 //const { get, set } = usePersistentStore(); 128 // const [feed, setFeed] = React.useState<any[]>([]); ··· 162 163 // const savedFeeds = savedFeedsPref?.items || []; 164 165 - const identityresultmaybe = useQueryIdentity(agent?.did); 166 const identity = identityresultmaybe?.data; 167 168 const prefsresultmaybe = useQueryPreferences({ 169 - agent: agent ?? undefined, 170 - pdsUrl: identity?.pds, 171 }); 172 const prefs = prefsresultmaybe?.data; 173 ··· 178 return savedFeedsPref?.items || []; 179 }, [prefs]); 180 181 - const [persistentSelectedFeed, setPersistentSelectedFeed] = 182 - useAtom(selectedFeedUriAtom); // React.useState<string | null>(null); 183 - const [unauthedSelectedFeed, setUnauthedSelectedFeed] = React.useState( 184 - persistentSelectedFeed 185 - ); // React.useState<string | null>(null); 186 const selectedFeed = agent?.did 187 ? persistentSelectedFeed 188 : unauthedSelectedFeed; ··· 306 }, [scrollPositions]); 307 308 useLayoutEffect(() => { 309 const savedPosition = scrollPositions[selectedFeed ?? "null"] ?? 0; 310 311 window.scrollTo({ top: savedPosition, behavior: "instant" }); 312 // eslint-disable-next-line react-hooks/exhaustive-deps 313 - }, [selectedFeed]); 314 315 useLayoutEffect(() => { 316 - if (!selectedFeed) return; 317 318 const handleScroll = () => { 319 scrollPositionsRef.current = { ··· 328 329 setScrollPositions(scrollPositionsRef.current); 330 }; 331 - }, [selectedFeed, setScrollPositions]); 332 333 - const feedGengetrecordquery = useQueryArbitrary(selectedFeed ?? undefined); 334 - const feedServiceDid = (feedGengetrecordquery?.data?.value as any)?.did; 335 336 // const { 337 // data: feedData, ··· 347 348 // const feed = feedData?.feed || []; 349 350 - const isReadyForAuthedFeed = 351 - authed && agent && identity?.pds && feedServiceDid; 352 - const isReadyForUnauthedFeed = !authed && selectedFeed; 353 354 355 const [isAtTop] = useAtom(isAtTopAtom); ··· 358 <div 359 className={`relative flex flex-col divide-y divide-gray-200 dark:divide-gray-800 ${hidden && "hidden"}`} 360 > 361 - {savedFeeds.length > 0 ? ( 362 <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`}> 363 {savedFeeds.map((item: any, idx: number) => { 364 const label = item.value.split("/").pop() || item.value; ··· 410 /> 411 ))} */} 412 413 - {authed && (!identity?.pds || !feedServiceDid) && ( 414 <div className="p-4 text-center text-gray-500"> 415 Preparing your feed... 416 </div> 417 )} 418 419 - {isReadyForAuthedFeed || isReadyForUnauthedFeed ? ( 420 <InfiniteCustomFeed 421 feedUri={selectedFeed!} 422 pdsUrl={identity?.pds} 423 feedServiceDid={feedServiceDid} 424 /> 425 ) : ( 426 <div className="p-4 text-center text-gray-500"> 427 - Select a feed to get started. 428 </div> 429 )} 430 {/* {false && restoringScrollPosition && (
··· 1 import { createFileRoute } from "@tanstack/react-router"; 2 import { useAtom } from "jotai"; 3 import * as React from "react"; 4 + import { useLayoutEffect, useState } from "react"; 5 6 import { Header } from "~/components/Header"; 7 import { InfiniteCustomFeed } from "~/components/InfiniteCustomFeed"; 8 import { useAuth } from "~/providers/UnifiedAuthProvider"; 9 import { 10 feedScrollPositionsAtom, 11 isAtTopAtom, 12 + quickAuthAtom, 13 selectedFeedUriAtom, 14 } from "~/utils/atoms"; 15 //import { usePersistentStore } from "~/providers/PersistentStoreProvider"; 16 import { ··· 105 } = useAuth(); 106 const authed = !!agent?.did; 107 108 + // i dont remember why this is even here 109 + // useEffect(() => { 110 + // if (agent?.did) { 111 + // store.set(authedAtom, true); 112 + // } else { 113 + // store.set(authedAtom, false); 114 + // } 115 + // }, [status, agent, authed]); 116 + // useEffect(() => { 117 + // if (agent) { 118 + // // eslint-disable-next-line @typescript-eslint/ban-ts-comment 119 + // // @ts-ignore is it just me or is the type really weird here it should be Agent not AtpAgent 120 + // store.set(agentAtom, agent); 121 + // } else { 122 + // store.set(agentAtom, null); 123 + // } 124 + // }, [status, agent, authed]); 125 126 //const { get, set } = usePersistentStore(); 127 // const [feed, setFeed] = React.useState<any[]>([]); ··· 161 162 // const savedFeeds = savedFeedsPref?.items || []; 163 164 + const [quickAuth, setQuickAuth] = useAtom(quickAuthAtom); 165 + const isAuthRestoring = quickAuth ? status === "loading" : false; 166 + 167 + const identityresultmaybe = useQueryIdentity(!isAuthRestoring ? agent?.did : undefined); 168 const identity = identityresultmaybe?.data; 169 170 const prefsresultmaybe = useQueryPreferences({ 171 + agent: !isAuthRestoring ? (agent ?? undefined) : undefined, 172 + pdsUrl: !isAuthRestoring ? (identity?.pds) : undefined, 173 }); 174 const prefs = prefsresultmaybe?.data; 175 ··· 180 return savedFeedsPref?.items || []; 181 }, [prefs]); 182 183 + const [persistentSelectedFeed, setPersistentSelectedFeed] = useAtom(selectedFeedUriAtom); 184 + const [unauthedSelectedFeed, setUnauthedSelectedFeed] = useState(persistentSelectedFeed); 185 const selectedFeed = agent?.did 186 ? persistentSelectedFeed 187 : unauthedSelectedFeed; ··· 305 }, [scrollPositions]); 306 307 useLayoutEffect(() => { 308 + if (isAuthRestoring) return; 309 const savedPosition = scrollPositions[selectedFeed ?? "null"] ?? 0; 310 311 window.scrollTo({ top: savedPosition, behavior: "instant" }); 312 // eslint-disable-next-line react-hooks/exhaustive-deps 313 + }, [selectedFeed, isAuthRestoring]); 314 315 useLayoutEffect(() => { 316 + if (!selectedFeed || isAuthRestoring) return; 317 318 const handleScroll = () => { 319 scrollPositionsRef.current = { ··· 328 329 setScrollPositions(scrollPositionsRef.current); 330 }; 331 + }, [isAuthRestoring, selectedFeed, setScrollPositions]); 332 333 + const feedGengetrecordquery = useQueryArbitrary(!isAuthRestoring ? selectedFeed ?? undefined : undefined); 334 + const feedServiceDid = !isAuthRestoring ? (feedGengetrecordquery?.data?.value as any)?.did as string | undefined : undefined; 335 336 // const { 337 // data: feedData, ··· 347 348 // const feed = feedData?.feed || []; 349 350 + const isReadyForAuthedFeed = !isAuthRestoring && authed && agent && identity?.pds && feedServiceDid; 351 + const isReadyForUnauthedFeed = !isAuthRestoring && !authed && selectedFeed; 352 353 354 const [isAtTop] = useAtom(isAtTopAtom); ··· 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) => { 363 const label = item.value.split("/").pop() || item.value; ··· 409 /> 410 ))} */} 411 412 + {isAuthRestoring || authed && (!identity?.pds || !feedServiceDid) && ( 413 <div className="p-4 text-center text-gray-500"> 414 Preparing your feed... 415 </div> 416 )} 417 418 + {!isAuthRestoring && (isReadyForAuthedFeed || isReadyForUnauthedFeed) ? ( 419 <InfiniteCustomFeed 420 + key={selectedFeed!} 421 feedUri={selectedFeed!} 422 pdsUrl={identity?.pds} 423 feedServiceDid={feedServiceDid} 424 /> 425 ) : ( 426 <div className="p-4 text-center text-gray-500"> 427 + Loading....... 428 </div> 429 )} 430 {/* {false && restoringScrollPosition && (
+7 -3
src/routes/notifications.tsx
··· 1 import { createFileRoute } from "@tanstack/react-router"; 2 import React, { useEffect, useRef,useState } from "react"; 3 4 import { useAuth } from "~/providers/UnifiedAuthProvider"; 5 6 const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour 7 ··· 70 } 71 } 72 73 useEffect(() => { 74 if (!did) return; 75 setLoading(true); 76 setError(null); 77 const urls = [ 78 - `https://constellation.microcosm.blue/links?target=${encodeURIComponent(did)}&collection=app.bsky.feed.post&path=.facets[app.bsky.richtext.facet].features[app.bsky.richtext.facet%23mention].did`, 79 - `https://constellation.microcosm.blue/links?target=${encodeURIComponent(did)}&collection=app.bsky.feed.post&path=.facets[].features[app.bsky.richtext.facet%23mention].did`, 80 - `https://constellation.microcosm.blue/links?target=${encodeURIComponent(did)}&collection=app.bsky.graph.follow&path=.subject`, 81 ]; 82 let ignore = false; 83 Promise.all(
··· 1 import { createFileRoute } from "@tanstack/react-router"; 2 + import { useAtom } from "jotai"; 3 import React, { useEffect, useRef,useState } from "react"; 4 5 import { useAuth } from "~/providers/UnifiedAuthProvider"; 6 + import { constellationURLAtom } from "~/utils/atoms"; 7 8 const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour 9 ··· 72 } 73 } 74 75 + const [constellationURL] = useAtom(constellationURLAtom) 76 + 77 useEffect(() => { 78 if (!did) return; 79 setLoading(true); 80 setError(null); 81 const urls = [ 82 + `https://${constellationURL}/links?target=${encodeURIComponent(did)}&collection=app.bsky.feed.post&path=.facets[app.bsky.richtext.facet].features[app.bsky.richtext.facet%23mention].did`, 83 + `https://${constellationURL}/links?target=${encodeURIComponent(did)}&collection=app.bsky.feed.post&path=.facets[].features[app.bsky.richtext.facet%23mention].did`, 84 + `https://${constellationURL}/links?target=${encodeURIComponent(did)}&collection=app.bsky.graph.follow&path=.subject`, 85 ]; 86 let ignore = false; 87 Promise.all(
+181 -53
src/routes/profile.$did/index.tsx
··· 1 import { useQueryClient } from "@tanstack/react-query"; 2 - import { createFileRoute } from "@tanstack/react-router"; 3 - import React from "react"; 4 5 import { Header } from "~/components/Header"; 6 - import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer"; 7 import { useAuth } from "~/providers/UnifiedAuthProvider"; 8 - import { toggleFollow, useGetFollowState } from "~/utils/followState"; 9 import { 10 useInfiniteQueryAuthorFeed, 11 useQueryIdentity, ··· 19 function ProfileComponent() { 20 // booo bad this is not always the did it might be a handle, use identity.did instead 21 const { did } = Route.useParams(); 22 const queryClient = useQueryClient(); 23 - const { agent } = useAuth(); 24 const { 25 data: identity, 26 isLoading: isIdentityLoading, 27 error: identityError, 28 } = useQueryIdentity(did); 29 - 30 - const followRecords = useGetFollowState({ 31 - target: identity?.did || did, 32 - user: agent?.did, 33 - }); 34 35 const resolvedDid = did.startsWith("did:") ? did : identity?.did; 36 const resolvedHandle = did.startsWith("did:") ? identity?.handle : did; ··· 67 [postsData] 68 ); 69 70 function getAvatarUrl(p: typeof profile) { 71 const link = p?.avatar?.ref?.["$link"]; 72 if (!link || !resolvedDid) return null; 73 - return `https://cdn.bsky.app/img/avatar/plain/${resolvedDid}/${link}@jpeg`; 74 } 75 function getBannerUrl(p: typeof profile) { 76 const link = p?.banner?.ref?.["$link"]; 77 if (!link || !resolvedDid) return null; 78 - return `https://cdn.bsky.app/img/banner/plain/${resolvedDid}/${link}@jpeg`; 79 } 80 81 const displayName = ··· 162 also delay the backfill to be on demand because it would be pretty intense 163 also save it persistently 164 */} 165 - {identity?.did !== agent?.did ? ( 166 - <> 167 - {!(followRecords?.length && followRecords?.length > 0) ? ( 168 - <button 169 - onClick={() => 170 - toggleFollow({ 171 - agent: agent || undefined, 172 - targetDid: identity?.did, 173 - followRecords: followRecords, 174 - queryClient: queryClient, 175 - }) 176 - } 177 - className="rounded-full dark:bg-gray-600 bg-gray-300 px-3 py-2 text-[14px]" 178 - > 179 - Follow 180 - </button> 181 - ) : ( 182 - <button 183 - onClick={() => 184 - toggleFollow({ 185 - agent: agent || undefined, 186 - targetDid: identity?.did, 187 - followRecords: followRecords, 188 - queryClient: queryClient, 189 - }) 190 - } 191 - className="rounded-full dark:bg-gray-600 bg-gray-300 px-3 py-2 text-[14px]" 192 - > 193 - Unfollow 194 - </button> 195 - )} 196 - </> 197 - ) : ( 198 - <button className="rounded-full dark:bg-gray-600 bg-gray-300 px-3 py-2 text-[14px]"> 199 - Edit Profile 200 - </button> 201 - )} 202 - <button className="rounded-full dark:bg-gray-600 bg-gray-300 px-3 py-2 text-[14px]"> 203 ... {/* todo: icon */} 204 - </button> 205 </div> 206 207 {/* Info Card */} 208 <div className="mt-16 pb-2 px-4 text-gray-900 dark:text-gray-100"> 209 <div className="font-bold text-2xl">{displayName}</div> 210 - <div className="text-gray-500 dark:text-gray-400 text-base mb-3"> 211 {handle} 212 </div> 213 {description && ( 214 <div className="text-base leading-relaxed text-gray-800 dark:text-gray-300 mb-5 whitespace-pre-wrap break-words text-[15px]"> 215 - {description} 216 </div> 217 )} 218 </div> ··· 255 </> 256 ); 257 }
··· 1 + import { RichText } from "@atproto/api"; 2 import { useQueryClient } from "@tanstack/react-query"; 3 + import { createFileRoute, useNavigate } from "@tanstack/react-router"; 4 + import { useAtom } from "jotai"; 5 + import React, { type ReactNode, useEffect, useState } from "react"; 6 7 import { Header } from "~/components/Header"; 8 + import { Button } from "~/components/radix-m3-rd/Button"; 9 + import { 10 + renderTextWithFacets, 11 + UniversalPostRendererATURILoader, 12 + } from "~/components/UniversalPostRenderer"; 13 import { useAuth } from "~/providers/UnifiedAuthProvider"; 14 + import { imgCDNAtom } from "~/utils/atoms"; 15 + import { 16 + toggleFollow, 17 + useGetFollowState, 18 + useGetOneToOneState, 19 + } from "~/utils/followState"; 20 import { 21 useInfiniteQueryAuthorFeed, 22 useQueryIdentity, ··· 30 function ProfileComponent() { 31 // booo bad this is not always the did it might be a handle, use identity.did instead 32 const { did } = Route.useParams(); 33 + const navigate = useNavigate(); 34 const queryClient = useQueryClient(); 35 const { 36 data: identity, 37 isLoading: isIdentityLoading, 38 error: identityError, 39 } = useQueryIdentity(did); 40 41 const resolvedDid = did.startsWith("did:") ? did : identity?.did; 42 const resolvedHandle = did.startsWith("did:") ? identity?.handle : did; ··· 73 [postsData] 74 ); 75 76 + const [imgcdn] = useAtom(imgCDNAtom); 77 + 78 function getAvatarUrl(p: typeof profile) { 79 const link = p?.avatar?.ref?.["$link"]; 80 if (!link || !resolvedDid) return null; 81 + return `https://${imgcdn}/img/avatar/plain/${resolvedDid}/${link}@jpeg`; 82 } 83 function getBannerUrl(p: typeof profile) { 84 const link = p?.banner?.ref?.["$link"]; 85 if (!link || !resolvedDid) return null; 86 + return `https://${imgcdn}/img/banner/plain/${resolvedDid}/${link}@jpeg`; 87 } 88 89 const displayName = ··· 170 also delay the backfill to be on demand because it would be pretty intense 171 also save it persistently 172 */} 173 + <FollowButton targetdidorhandle={did} /> 174 + <Button className="rounded-full" variant={"secondary"}> 175 ... {/* todo: icon */} 176 + </Button> 177 </div> 178 179 {/* Info Card */} 180 <div className="mt-16 pb-2 px-4 text-gray-900 dark:text-gray-100"> 181 <div className="font-bold text-2xl">{displayName}</div> 182 + <div className="text-gray-500 dark:text-gray-400 text-base mb-3 flex flex-row gap-1"> 183 + <Mutual targetdidorhandle={did} /> 184 {handle} 185 </div> 186 {description && ( 187 <div className="text-base leading-relaxed text-gray-800 dark:text-gray-300 mb-5 whitespace-pre-wrap break-words text-[15px]"> 188 + {/* {description} */} 189 + <RichTextRenderer key={did} description={description} /> 190 </div> 191 )} 192 </div> ··· 229 </> 230 ); 231 } 232 + 233 + export function FollowButton({ 234 + targetdidorhandle, 235 + }: { 236 + targetdidorhandle: string; 237 + }) { 238 + const { agent } = useAuth(); 239 + const { data: identity } = useQueryIdentity(targetdidorhandle); 240 + const queryClient = useQueryClient(); 241 + 242 + const followRecords = useGetFollowState({ 243 + target: identity?.did ?? targetdidorhandle, 244 + user: agent?.did, 245 + }); 246 + 247 + return ( 248 + <> 249 + {identity?.did !== agent?.did ? ( 250 + <> 251 + {!(followRecords?.length && followRecords?.length > 0) ? ( 252 + <Button 253 + onClick={(e) => { 254 + e.stopPropagation(); 255 + toggleFollow({ 256 + agent: agent || undefined, 257 + targetDid: identity?.did, 258 + followRecords: followRecords, 259 + queryClient: queryClient, 260 + }); 261 + }} 262 + > 263 + Follow 264 + </Button> 265 + ) : ( 266 + <Button 267 + onClick={(e) => { 268 + e.stopPropagation(); 269 + toggleFollow({ 270 + agent: agent || undefined, 271 + targetDid: identity?.did, 272 + followRecords: followRecords, 273 + queryClient: queryClient, 274 + }); 275 + }} 276 + > 277 + Unfollow 278 + </Button> 279 + )} 280 + </> 281 + ) : ( 282 + <Button variant={"secondary"}>Edit Profile</Button> 283 + )} 284 + </> 285 + ); 286 + } 287 + 288 + export function Mutual({ targetdidorhandle }: { targetdidorhandle: string }) { 289 + const { agent } = useAuth(); 290 + const { data: identity } = useQueryIdentity(targetdidorhandle); 291 + 292 + const theyFollowYouRes = useGetOneToOneState( 293 + agent?.did 294 + ? { 295 + target: agent?.did, 296 + user: identity?.did ?? targetdidorhandle, 297 + collection: "app.bsky.graph.follow", 298 + path: ".subject", 299 + } 300 + : undefined 301 + ); 302 + 303 + const youFollowThemRes = useGetFollowState({ 304 + target: identity?.did ?? targetdidorhandle, 305 + user: agent?.did, 306 + }); 307 + 308 + const theyFollowYou: boolean = 309 + !!theyFollowYouRes?.length && theyFollowYouRes.length > 0; 310 + const youFollowThem: boolean = 311 + !!youFollowThemRes?.length && youFollowThemRes.length > 0; 312 + 313 + return ( 314 + <> 315 + {/* if not self */} 316 + {identity?.did !== agent?.did ? ( 317 + <> 318 + {theyFollowYou ? ( 319 + <> 320 + {youFollowThem ? ( 321 + <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"> 322 + mutuals 323 + </div> 324 + ) : ( 325 + <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"> 326 + follows you 327 + </div> 328 + )} 329 + </> 330 + ) : ( 331 + <></> 332 + )} 333 + </> 334 + ) : ( 335 + // lmao can someone be mutuals with themselves ?? 336 + <></> 337 + )} 338 + </> 339 + ); 340 + } 341 + 342 + export function RichTextRenderer({ description }: { description: string }) { 343 + const [richDescription, setRichDescription] = useState<string | ReactNode[]>( 344 + description 345 + ); 346 + const { agent } = useAuth(); 347 + const navigate = useNavigate(); 348 + 349 + useEffect(() => { 350 + let mounted = true; 351 + 352 + // setRichDescription(description); 353 + 354 + async function processRichText() { 355 + try { 356 + if (!agent?.did) return; 357 + const rt = new RichText({ text: description }); 358 + await rt.detectFacets(agent); 359 + 360 + if (!mounted) return; 361 + 362 + if (rt.facets) { 363 + setRichDescription( 364 + renderTextWithFacets({ text: rt.text, facets: rt.facets, navigate }) 365 + ); 366 + } else { 367 + setRichDescription(rt.text); 368 + } 369 + } catch (error) { 370 + console.error("Failed to detect facets:", error); 371 + if (mounted) { 372 + setRichDescription(description); 373 + } 374 + } 375 + } 376 + 377 + processRichText(); 378 + 379 + return () => { 380 + mounted = false; 381 + }; 382 + }, [description, agent, navigate]); 383 + 384 + return <>{richDescription}</>; 385 + }
+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 + }
+225 -59
src/routes/profile.$did/post.$rkey.tsx
··· 1 - import { useQueryClient } from "@tanstack/react-query"; 2 - import { createFileRoute } from "@tanstack/react-router"; 3 import React, { useLayoutEffect } from "react"; 4 5 import { Header } from "~/components/Header"; 6 import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer"; 7 //import { usePersistentStore } from '~/providers/PersistentStoreProvider'; 8 import { 9 constructPostQuery, 10 useQueryConstellation, 11 useQueryIdentity, 12 useQueryPost, 13 } from "~/utils/useQuery"; 14 15 //const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour 16 ··· 37 did, 38 rkey, 39 nopics, 40 }: { 41 did: string; 42 rkey: string; 43 - nopics?: () => void; 44 }) { 45 //const { get, set } = usePersistentStore(); 46 const queryClient = useQueryClient(); ··· 188 () => 189 resolvedDid 190 ? `at://${decodeURIComponent(resolvedDid)}/app.bsky.feed.post/${rkey}` 191 - : "", 192 [resolvedDid, rkey] 193 ); 194 195 const { data: mainPost } = useQueryPost(atUri); 196 197 - const { data: repliesData } = useQueryConstellation({ 198 - method: "/links", 199 target: atUri, 200 - collection: "app.bsky.feed.post", 201 - path: ".reply.parent.uri", 202 }); 203 - const replies = repliesData?.linking_records.slice(0, 50) ?? []; 204 205 const [parents, setParents] = React.useState<any[]>([]); 206 const [parentsLoading, setParentsLoading] = React.useState(false); 207 208 const mainPostRef = React.useRef<HTMLDivElement>(null); 209 - const userHasScrolled = React.useRef(false); 210 211 - const scrollAnchor = React.useRef<{ top: number } | null>(null); 212 213 - React.useEffect(() => { 214 - const onScroll = () => { 215 - if (window.scrollY > 50) { 216 - userHasScrolled.current = true; 217 218 - window.removeEventListener("scroll", onScroll); 219 } 220 - }; 221 - 222 - if (!userHasScrolled.current) { 223 - window.addEventListener("scroll", onScroll, { passive: true }); 224 } 225 - return () => window.removeEventListener("scroll", onScroll); 226 - }, []); 227 228 - useLayoutEffect(() => { 229 - if (parentsLoading && mainPostRef.current && !userHasScrolled.current) { 230 - scrollAnchor.current = { 231 - top: mainPostRef.current.getBoundingClientRect().top, 232 - }; 233 } 234 - }, [parentsLoading]); 235 236 - useLayoutEffect(() => { 237 - if ( 238 - scrollAnchor.current && 239 - mainPostRef.current && 240 - !userHasScrolled.current 241 - ) { 242 - const newTop = mainPostRef.current.getBoundingClientRect().top; 243 - const topDiff = newTop - scrollAnchor.current.top; 244 - if (topDiff > 0) { 245 - window.scrollBy(0, topDiff); 246 - } 247 - scrollAnchor.current = null; 248 } 249 - }, [parents]); 250 251 React.useEffect(() => { 252 if (!mainPost?.value?.reply?.parent?.uri) { ··· 265 while (currentParentUri && safetyCounter < MAX_PARENTS) { 266 try { 267 const parentPost = await queryClient.fetchQuery( 268 - constructPostQuery(currentParentUri) 269 ); 270 if (!parentPost) break; 271 parentChain.push(parentPost); ··· 297 298 return ( 299 <> 300 <Header 301 title={`Post`} 302 - backButtonCallback={ 303 - nopics 304 - ? nopics 305 - : () => { 306 - if (window.history.length > 1) { 307 - window.history.back(); 308 - } else { 309 - window.location.assign("/"); 310 - } 311 - } 312 - } 313 /> 314 315 {parentsLoading && ( ··· 342 detailed={true} 343 topReplyLine={parentsLoading || parents.length > 0} 344 nopics={!!nopics} 345 /> 346 </div> 347 <div ··· 349 maxWidth: 600, 350 //margin: "0px auto 0", 351 padding: 0, 352 - minHeight: "100dvh", 353 }} 354 > 355 <div ··· 363 Replies 364 </div> 365 <div style={{ display: "flex", flexDirection: "column", gap: 0 }}> 366 - {replies.length > 0 && 367 - replies.map((reply) => { 368 - const replyAtUri = `at://${reply.did}/app.bsky.feed.post/${reply.rkey}`; 369 return ( 370 <UniversalPostRendererATURILoader 371 - key={replyAtUri} 372 - atUri={replyAtUri} 373 /> 374 ); 375 })} 376 </div> 377 </div> 378 </>
··· 1 + import { AtUri } from "@atproto/api"; 2 + import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; 3 + import { createFileRoute, Outlet } from "@tanstack/react-router"; 4 + import { useAtom } from "jotai"; 5 import React, { useLayoutEffect } from "react"; 6 7 import { Header } from "~/components/Header"; 8 import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer"; 9 + import { constellationURLAtom, slingshotURLAtom } from "~/utils/atoms"; 10 //import { usePersistentStore } from '~/providers/PersistentStoreProvider'; 11 import { 12 constructPostQuery, 13 + type linksAllResponse, 14 + type linksRecordsResponse, 15 useQueryConstellation, 16 useQueryIdentity, 17 useQueryPost, 18 + yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks, 19 } from "~/utils/useQuery"; 20 + 21 + import type { LightboxProps } from "./post.$rkey.image.$i"; 22 23 //const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour 24 ··· 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 { get, set } = usePersistentStore(); 56 const queryClient = useQueryClient(); ··· 198 () => 199 resolvedDid 200 ? `at://${decodeURIComponent(resolvedDid)}/app.bsky.feed.post/${rkey}` 201 + : undefined, 202 [resolvedDid, rkey] 203 ); 204 205 const { data: mainPost } = useQueryPost(atUri); 206 207 + console.log("atUri",atUri) 208 + 209 + const opdid = React.useMemo( 210 + () => 211 + atUri 212 + ? new AtUri(atUri).host 213 + : undefined, 214 + [atUri] 215 + ); 216 + 217 + // @ts-expect-error i hate overloads 218 + const { data: links } = useQueryConstellation(atUri?{ 219 + method: "/links/all", 220 target: atUri, 221 + } : { 222 + method: "undefined", 223 + target: "" 224 + })as { data: linksAllResponse | undefined }; 225 + 226 + //const [likes, setLikes] = React.useState<number | null>(null); 227 + //const [reposts, setReposts] = React.useState<number | null>(null); 228 + const [replyCount, setReplyCount] = React.useState<number | null>(null); 229 + 230 + React.useEffect(() => { 231 + // /*mass comment*/ console.log(JSON.stringify(links, null, 2)); 232 + // setLikes( 233 + // links 234 + // ? links?.links?.["app.bsky.feed.like"]?.[".subject.uri"]?.records || 0 235 + // : null 236 + // ); 237 + // setReposts( 238 + // links 239 + // ? links?.links?.["app.bsky.feed.repost"]?.[".subject.uri"]?.records || 0 240 + // : null 241 + // ); 242 + setReplyCount( 243 + links 244 + ? links?.links?.["app.bsky.feed.post"]?.[".reply.parent.uri"] 245 + ?.records || 0 246 + : null 247 + ); 248 + }, [links]); 249 + 250 + const { data: opreplies } = useQueryConstellation( 251 + !!opdid && replyCount && replyCount >= 25 252 + ? { 253 + method: "/links", 254 + target: atUri, 255 + // @ts-expect-error overloading sucks so much 256 + collection: "app.bsky.feed.post", 257 + path: ".reply.parent.uri", 258 + //cursor?: string; 259 + dids: [opdid], 260 + } 261 + : { 262 + method: "undefined", 263 + target: "", 264 + } 265 + ) as { data: linksRecordsResponse | undefined }; 266 + 267 + const opReplyAturis = 268 + opreplies?.linking_records.map( 269 + (r) => `at://${r.did}/${r.collection}/${r.rkey}`, 270 + ) ?? []; 271 + 272 + 273 + // const { data: repliesData } = useQueryConstellation({ 274 + // method: "/links", 275 + // target: atUri, 276 + // collection: "app.bsky.feed.post", 277 + // path: ".reply.parent.uri", 278 + // }); 279 + // const replies = repliesData?.linking_records.slice(0, 50) ?? []; 280 + const [constellationurl] = useAtom(constellationURLAtom) 281 + 282 + const infinitequeryresults = useInfiniteQuery({ 283 + ...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks( 284 + { 285 + constellation: constellationurl, 286 + method: "/links", 287 + target: atUri, 288 + collection: "app.bsky.feed.post", 289 + path: ".reply.parent.uri", 290 + } 291 + ), 292 + enabled: !!atUri, 293 }); 294 + 295 + const { 296 + data: infiniteRepliesData, 297 + fetchNextPage, 298 + hasNextPage, 299 + isFetchingNextPage, 300 + } = infinitequeryresults; 301 + 302 + // // auto-fetch all pages 303 + // useEffect(() => { 304 + // if ( 305 + // infinitequeryresults.hasNextPage && 306 + // !infinitequeryresults.isFetchingNextPage 307 + // ) { 308 + // console.log("Fetching the next page..."); 309 + // infinitequeryresults.fetchNextPage(); 310 + // } 311 + // }, [infinitequeryresults]); 312 + 313 + // const replyAturis = repliesData 314 + // ? repliesData.pages.flatMap((page) => 315 + // page 316 + // ? page.linking_records.map((record) => { 317 + // const aturi = `at://${record.did}/${record.collection}/${record.rkey}`; 318 + // return aturi; 319 + // }) 320 + // : [] 321 + // ) 322 + // : []; 323 + 324 + const replyAturis = React.useMemo(() => { 325 + // Get all replies from the standard infinite query 326 + const allReplies = 327 + infiniteRepliesData?.pages.flatMap( 328 + (page) => 329 + page?.linking_records.map( 330 + (r) => `at://${r.did}/${r.collection}/${r.rkey}`, 331 + ) ?? [], 332 + ) ?? []; 333 + 334 + if (replyCount && (replyCount < 25)) { 335 + // If count is low, just use the standard list and find the oldest OP reply to move to the top 336 + const opdidFromUri = atUri ? new AtUri(atUri).host : undefined; 337 + const oldestOpsIndex = allReplies.findIndex( 338 + (aturi) => new AtUri(aturi).host === opdidFromUri, 339 + ); 340 + if (oldestOpsIndex > 0) { 341 + const [oldestOpsReply] = allReplies.splice(oldestOpsIndex, 1); 342 + allReplies.unshift(oldestOpsReply); 343 + } 344 + return allReplies; 345 + } else { 346 + // If count is high, prioritize OP replies from the special query 347 + // and filter them out from the main list to avoid duplication. 348 + const opReplySet = new Set(opReplyAturis); 349 + const otherReplies = allReplies.filter((uri) => !opReplySet.has(uri)); 350 + return [...opReplyAturis, ...otherReplies]; 351 + } 352 + }, [infiniteRepliesData, opReplyAturis, replyCount, atUri]); 353 + 354 + // Find oldest OP reply 355 + const oldestOpsIndex = replyAturis.findIndex( 356 + (aturi) => new AtUri(aturi).host === opdid 357 + ); 358 + 359 + // Reorder: move oldest OP reply to the front 360 + if (oldestOpsIndex > 0) { 361 + const [oldestOpsReply] = replyAturis.splice(oldestOpsIndex, 1); 362 + replyAturis.unshift(oldestOpsReply); 363 + } 364 365 const [parents, setParents] = React.useState<any[]>([]); 366 const [parentsLoading, setParentsLoading] = React.useState(false); 367 368 const mainPostRef = React.useRef<HTMLDivElement>(null); 369 + const hasPerformedInitialLayout = React.useRef(false); 370 371 + const [layoutReady, setLayoutReady] = React.useState(false); 372 373 + useLayoutEffect(() => { 374 + if (parents.length > 0 && !layoutReady && mainPostRef.current) { 375 + const mainPostElement = mainPostRef.current; 376 + 377 + if (window.scrollY === 0 && !hasPerformedInitialLayout.current) { 378 + const elementTop = mainPostElement.getBoundingClientRect().top; 379 + const headerOffset = 70; 380 + 381 + const targetScrollY = elementTop - headerOffset; 382 + 383 + window.scrollBy(0, targetScrollY); 384 385 + hasPerformedInitialLayout.current = true; 386 } 387 + 388 + // todo idk what to do with this 389 + // eslint-disable-next-line react-hooks/set-state-in-effect 390 + setLayoutReady(true); 391 } 392 + }, [parents, layoutReady]); 393 394 + 395 + const [slingshoturl] = useAtom(slingshotURLAtom) 396 + 397 + React.useEffect(() => { 398 + if (parentsLoading) { 399 + setLayoutReady(false); 400 } 401 402 + if (!mainPost?.value?.reply?.parent?.uri && !parentsLoading) { 403 + setLayoutReady(true); 404 + hasPerformedInitialLayout.current = true; 405 } 406 + }, [parentsLoading, mainPost]); 407 408 React.useEffect(() => { 409 if (!mainPost?.value?.reply?.parent?.uri) { ··· 422 while (currentParentUri && safetyCounter < MAX_PARENTS) { 423 try { 424 const parentPost = await queryClient.fetchQuery( 425 + constructPostQuery(currentParentUri, slingshoturl) 426 ); 427 if (!parentPost) break; 428 parentChain.push(parentPost); ··· 454 455 return ( 456 <> 457 + <Outlet /> 458 <Header 459 title={`Post`} 460 + backButtonCallback={() => { 461 + if (window.history.length > 1) { 462 + window.history.back(); 463 + } else { 464 + window.location.assign("/"); 465 + } 466 + }} 467 /> 468 469 {parentsLoading && ( ··· 496 detailed={true} 497 topReplyLine={parentsLoading || parents.length > 0} 498 nopics={!!nopics} 499 + lightboxCallback={lightboxCallback} 500 /> 501 </div> 502 <div ··· 504 maxWidth: 600, 505 //margin: "0px auto 0", 506 padding: 0, 507 + minHeight: "80dvh", 508 + paddingBottom: "20dvh", 509 }} 510 > 511 <div ··· 519 Replies 520 </div> 521 <div style={{ display: "flex", flexDirection: "column", gap: 0 }}> 522 + {replyAturis.length > 0 && 523 + replyAturis.map((reply) => { 524 + //const replyAtUri = `at://${reply.did}/app.bsky.feed.post/${reply.rkey}`; 525 return ( 526 <UniversalPostRendererATURILoader 527 + key={reply} 528 + atUri={reply} 529 + maxReplies={4} 530 /> 531 ); 532 })} 533 + {hasNextPage && ( 534 + <button 535 + onClick={() => fetchNextPage()} 536 + disabled={isFetchingNextPage} 537 + 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" 538 + > 539 + {isFetchingNextPage ? "Loading..." : "Load More"} 540 + </button> 541 + )} 542 </div> 543 </div> 544 </>
+50 -1
src/routes/search.tsx
··· 1 import { createFileRoute } from "@tanstack/react-router"; 2 3 export const Route = createFileRoute("/search")({ 4 component: Search, 5 }); 6 7 export function Search() { 8 - return <div className="p-6">Search page (coming soon)</div>; 9 }
··· 1 import { createFileRoute } from "@tanstack/react-router"; 2 3 + import { Header } from "~/components/Header"; 4 + import { Import } from "~/components/Import"; 5 + 6 export const Route = createFileRoute("/search")({ 7 component: Search, 8 }); 9 10 export function Search() { 11 + return ( 12 + <> 13 + <Header 14 + title="Explore" 15 + backButtonCallback={() => { 16 + if (window.history.length > 1) { 17 + window.history.back(); 18 + } else { 19 + window.location.assign("/"); 20 + } 21 + }} 22 + /> 23 + <div className=" flex flex-col items-center mt-4 mx-4 gap-4"> 24 + <Import /> 25 + <div className="flex flex-col"> 26 + <p className="text-gray-600 dark:text-gray-400"> 27 + Sorry we dont have search. But instead, you can load some of these 28 + types of content into Red Dwarf: 29 + </p> 30 + <ul className="list-disc list-inside mt-2 text-gray-600 dark:text-gray-400"> 31 + <li> 32 + Bluesky URLs from supported clients (like{" "} 33 + <code className="text-sm">bsky.app</code> or{" "} 34 + <code className="text-sm">deer.social</code>). 35 + </li> 36 + <li> 37 + AT-URIs (e.g.,{" "} 38 + <code className="text-sm">at://did:example/collection/item</code> 39 + ). 40 + </li> 41 + <li> 42 + Plain handles (like{" "} 43 + <code className="text-sm">@username.bsky.social</code>). 44 + </li> 45 + <li> 46 + Direct DIDs (Decentralized Identifiers, starting with{" "} 47 + <code className="text-sm">did:</code>). 48 + </li> 49 + </ul> 50 + <p className="mt-2 text-gray-600 dark:text-gray-400"> 51 + Simply paste one of these into the import field above and press 52 + Enter to load the content. 53 + </p> 54 + </div> 55 + </div> 56 + </> 57 + ); 58 }
+164 -1
src/routes/settings.tsx
··· 1 import { createFileRoute } from "@tanstack/react-router"; 2 3 import { Header } from "~/components/Header"; 4 import Login from "~/components/Login"; 5 6 export const Route = createFileRoute("/settings")({ 7 component: Settings, ··· 20 } 21 }} 22 /> 23 - <Login /> 24 </> 25 ); 26 }
··· 1 import { createFileRoute } from "@tanstack/react-router"; 2 + import { useAtom } from "jotai"; 3 + import { Slider } from "radix-ui"; 4 5 import { Header } from "~/components/Header"; 6 import Login from "~/components/Login"; 7 + import { 8 + constellationURLAtom, 9 + defaultconstellationURL, 10 + defaulthue, 11 + defaultImgCDN, 12 + defaultslingshotURL, 13 + defaultVideoCDN, 14 + hueAtom, 15 + imgCDNAtom, 16 + slingshotURLAtom, 17 + videoCDNAtom, 18 + } from "~/utils/atoms"; 19 20 export const Route = createFileRoute("/settings")({ 21 component: Settings, ··· 34 } 35 }} 36 /> 37 + <div className="lg:hidden"> 38 + <Login /> 39 + </div> 40 + <div className="h-4" /> 41 + <TextInputSetting 42 + atom={constellationURLAtom} 43 + title={"Constellation"} 44 + description={ 45 + "Customize the Constellation instance to be used by Red Dwarf" 46 + } 47 + init={defaultconstellationURL} 48 + /> 49 + <TextInputSetting 50 + atom={slingshotURLAtom} 51 + title={"Slingshot"} 52 + description={"Customize the Slingshot instance to be used by Red Dwarf"} 53 + init={defaultslingshotURL} 54 + /> 55 + <TextInputSetting 56 + atom={imgCDNAtom} 57 + title={"Image CDN"} 58 + description={ 59 + "Customize the Constellation instance to be used by Red Dwarf" 60 + } 61 + init={defaultImgCDN} 62 + /> 63 + <TextInputSetting 64 + atom={videoCDNAtom} 65 + title={"Video CDN"} 66 + description={"Customize the Slingshot instance to be used by Red Dwarf"} 67 + init={defaultVideoCDN} 68 + /> 69 + 70 + <Hue /> 71 + <p className="text-gray-500 dark:text-gray-400 py-4 px-6 text-sm"> 72 + please restart/refresh the app if changes arent applying correctly 73 + </p> 74 </> 75 ); 76 } 77 + function Hue() { 78 + const [hue, setHue] = useAtom(hueAtom); 79 + return ( 80 + <div className="flex flex-col px-4 mt-4 "> 81 + <span className="z-10">Hue</span> 82 + <div className="flex flex-row items-center gap-4"> 83 + <SliderComponent 84 + atom={hueAtom} 85 + max={360} 86 + /> 87 + <button 88 + onClick={() => setHue(defaulthue ?? 28)} 89 + className="px-6 py-2 h-12 rounded-full bg-gray-100 dark:bg-gray-800 90 + text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition" 91 + > 92 + Reset 93 + </button> 94 + </div> 95 + </div> 96 + ); 97 + } 98 + 99 + export function TextInputSetting({ 100 + atom, 101 + title, 102 + description, 103 + init, 104 + }: { 105 + atom: typeof constellationURLAtom; 106 + title?: string; 107 + description?: string; 108 + init?: string; 109 + }) { 110 + const [value, setValue] = useAtom(atom); 111 + return ( 112 + <div className="flex flex-col gap-2 px-4 py-2"> 113 + {/* <div> 114 + {title && ( 115 + <h3 className="text-sm font-medium text-gray-900 dark:text-gray-100"> 116 + {title} 117 + </h3> 118 + )} 119 + {description && ( 120 + <p className="text-sm text-gray-500 dark:text-gray-400"> 121 + {description} 122 + </p> 123 + )} 124 + </div> */} 125 + 126 + <div className="flex flex-row gap-2 items-center"> 127 + <div className="m3input-field m3input-label m3input-border size-md flex-1"> 128 + <input 129 + type="text" 130 + placeholder=" " 131 + value={value} 132 + onChange={(e) => setValue(e.target.value)} 133 + /> 134 + <label>{title}</label> 135 + </div> 136 + {/* <input 137 + type="text" 138 + value={value} 139 + onChange={(e) => setValue(e.target.value)} 140 + className="flex-1 px-3 py-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 141 + text-gray-900 dark:text-gray-100 placeholder:text-gray-500 dark:placeholder:text-gray-400 142 + focus:outline-none focus:ring-2 focus:ring-gray-400 dark:focus:ring-gray-600" 143 + placeholder="Enter value..." 144 + /> */} 145 + <button 146 + onClick={() => setValue(init ?? "")} 147 + className="px-6 py-2 h-12 rounded-full bg-gray-100 dark:bg-gray-800 148 + text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition" 149 + > 150 + Reset 151 + </button> 152 + </div> 153 + </div> 154 + ); 155 + } 156 + 157 + 158 + interface SliderProps { 159 + atom: typeof hueAtom; 160 + min?: number; 161 + max?: number; 162 + step?: number; 163 + } 164 + 165 + export const SliderComponent: React.FC<SliderProps> = ({ 166 + atom, 167 + min = 0, 168 + max = 100, 169 + step = 1, 170 + }) => { 171 + 172 + const [value, setValue] = useAtom(atom) 173 + 174 + return ( 175 + <Slider.Root 176 + className="relative flex items-center w-full h-4" 177 + value={[value]} 178 + min={min} 179 + max={max} 180 + step={step} 181 + onValueChange={(v: number[]) => setValue(v[0])} 182 + > 183 + <Slider.Track className="relative flex-grow h-4 bg-gray-300 dark:bg-gray-700 rounded-full"> 184 + <Slider.Range className="absolute h-full bg-gray-500 dark:bg-gray-400 rounded-l-full rounded-r-none" /> 185 + </Slider.Track> 186 + <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" /> 187 + </Slider.Root> 188 + ); 189 + };
+139 -13
src/styles/app.css
··· 15 --color-gray-950: oklch(0.129 0.050 222.000); 16 } */ 17 18 @theme { 19 - --color-gray-50: oklch(0.984 0.012 28); 20 - --color-gray-100: oklch(0.968 0.017 28); 21 - --color-gray-200: oklch(0.929 0.025 28); 22 - --color-gray-300: oklch(0.869 0.035 28); 23 - --color-gray-400: oklch(0.704 0.05 28); 24 - --color-gray-500: oklch(0.554 0.06 28); 25 - --color-gray-600: oklch(0.446 0.058 28); 26 - --color-gray-700: oklch(0.372 0.058 28); 27 - --color-gray-800: oklch(0.279 0.055 28); 28 - --color-gray-900: oklch(0.208 0.055 28); 29 - --color-gray-950: oklch(0.129 0.055 28); 30 } 31 32 @layer base { ··· 48 } 49 } 50 51 @media (width >= 64rem /* 1024px */) { 52 html:not(:has(.disablegutter)), 53 body:not(:has(.disablegutter)) { 54 scrollbar-gutter: stable both-edges !important; 55 } 56 - html:has(.disablegutter), 57 - body:has(.disablegutter) { 58 scrollbar-width: none; 59 overflow-y: hidden; 60 } ··· 105 :root { 106 --shadow-opacity: calc(1 - var(--is-top)); 107 --tw-shadow-header: 0 2px 8px hsl(0 0% 0% / calc(var(--shadow-opacity) * 0.15)); 108 }
··· 15 --color-gray-950: oklch(0.129 0.050 222.000); 16 } */ 17 18 + :root { 19 + --safe-hue: var(--tw-gray-hue, 28) 20 + } 21 + 22 @theme { 23 + --color-gray-50: oklch(0.984 0.012 var(--safe-hue)); 24 + --color-gray-100: oklch(0.968 0.017 var(--safe-hue)); 25 + --color-gray-200: oklch(0.929 0.025 var(--safe-hue)); 26 + --color-gray-300: oklch(0.869 0.035 var(--safe-hue)); 27 + --color-gray-400: oklch(0.704 0.05 var(--safe-hue)); 28 + --color-gray-500: oklch(0.554 0.06 var(--safe-hue)); 29 + --color-gray-600: oklch(0.446 0.058 var(--safe-hue)); 30 + --color-gray-700: oklch(0.372 0.058 var(--safe-hue)); 31 + --color-gray-800: oklch(0.279 0.055 var(--safe-hue)); 32 + --color-gray-900: oklch(0.208 0.055 var(--safe-hue)); 33 + --color-gray-950: oklch(0.129 0.055 var(--safe-hue)); 34 } 35 36 @layer base { ··· 52 } 53 } 54 55 + .gutter{ 56 + scrollbar-gutter: stable both-edges; 57 + } 58 + 59 @media (width >= 64rem /* 1024px */) { 60 html:not(:has(.disablegutter)), 61 body:not(:has(.disablegutter)) { 62 scrollbar-gutter: stable both-edges !important; 63 } 64 + html:has(.disablescroll), 65 + body:has(.disablescroll) { 66 scrollbar-width: none; 67 overflow-y: hidden; 68 } ··· 113 :root { 114 --shadow-opacity: calc(1 - var(--is-top)); 115 --tw-shadow-header: 0 2px 8px hsl(0 0% 0% / calc(var(--shadow-opacity) * 0.15)); 116 + } 117 + 118 + 119 + /* m3 input */ 120 + :root { 121 + --m3input-radius: 6px; 122 + --m3input-border-width: .0625rem; 123 + --m3input-font-size: 16px; 124 + --m3input-transition: 150ms cubic-bezier(.2, .8, .2, 1); 125 + /* light theme */ 126 + --m3input-bg: var(--color-gray-50); 127 + --m3input-border-color: var(--color-gray-400); 128 + --m3input-label-color: var(--color-gray-500); 129 + --m3input-text-color: var(--color-gray-900); 130 + --m3input-focus-color: var(--color-gray-600); 131 + } 132 + 133 + @media (prefers-color-scheme: dark) { 134 + :root { 135 + --m3input-bg: var(--color-gray-950); 136 + --m3input-border-color: var(--color-gray-700); 137 + --m3input-label-color: var(--color-gray-400); 138 + --m3input-text-color: var(--color-gray-50); 139 + --m3input-focus-color: var(--color-gray-400); 140 + } 141 + } 142 + 143 + /* reset page *//* 144 + html, 145 + body { 146 + background: var(--m3input-bg); 147 + margin: 0; 148 + padding: 1rem; 149 + color: var(--m3input-text-color); 150 + font-family: system-ui, sans-serif; 151 + font-size: var(--m3input-font-size); 152 + }*/ 153 + 154 + /* base wrapper */ 155 + .m3input-field.m3input-label.m3input-border { 156 + position: relative; 157 + display: inline-block; 158 + width: 100%; 159 + /*max-width: 400px;*/ 160 + } 161 + 162 + /* size variants */ 163 + .m3input-field.size-sm { 164 + --m3input-h: 40px; 165 + } 166 + 167 + .m3input-field.size-md { 168 + --m3input-h: 48px; 169 + } 170 + 171 + .m3input-field.size-lg { 172 + --m3input-h: 56px; 173 + } 174 + 175 + .m3input-field.size-xl { 176 + --m3input-h: 64px; 177 + } 178 + 179 + .m3input-field.m3input-label.m3input-border:not(.size-sm):not(.size-md):not(.size-lg):not(.size-xl) { 180 + --m3input-h: 48px; 181 + } 182 + 183 + /* outlined input */ 184 + .m3input-field.m3input-label.m3input-border input { 185 + width: 100%; 186 + height: var(--m3input-h); 187 + border: var(--m3input-border-width) solid var(--m3input-border-color); 188 + border-radius: var(--m3input-radius); 189 + background: var(--m3input-bg); 190 + color: var(--m3input-text-color); 191 + font-size: var(--m3input-font-size); 192 + padding: 0 12px; 193 + box-sizing: border-box; 194 + outline: none; 195 + transition: border-color var(--m3input-transition), box-shadow var(--m3input-transition); 196 + } 197 + 198 + /* focus ring */ 199 + .m3input-field.m3input-label.m3input-border input:focus { 200 + border-color: var(--m3input-focus-color); 201 + /*box-shadow: 0 0 0 2px color-mix(in srgb, var(--focus-color) 20%, transparent);*/ 202 + } 203 + 204 + /* label */ 205 + .m3input-field.m3input-label.m3input-border label { 206 + position: absolute; 207 + left: 12px; 208 + top: 50%; 209 + transform: translateY(-50%); 210 + background: var(--m3input-bg); 211 + padding: 0 .25em; 212 + color: var(--m3input-label-color); 213 + pointer-events: none; 214 + transition: all var(--m3input-transition); 215 + } 216 + 217 + /* float on focus or when filled */ 218 + .m3input-field.m3input-label.m3input-border input:focus+label, 219 + .m3input-field.m3input-label.m3input-border input:not(:placeholder-shown)+label { 220 + top: 0; 221 + transform: translateY(-50%) scale(.78); 222 + left: 0; 223 + color: var(--m3input-focus-color); 224 + } 225 + 226 + /* placeholder trick */ 227 + .m3input-field.m3input-label.m3input-border input::placeholder { 228 + color: transparent; 229 + } 230 + 231 + /* radix i love you but like cmon man */ 232 + body[data-scroll-locked]{ 233 + margin-left: var(--removed-body-scroll-bar-size) !important; 234 }
+63 -8
src/utils/atoms.ts
··· 1 - import type Agent from "@atproto/api"; 2 - import { atom, createStore } from "jotai"; 3 - import { atomWithStorage } from 'jotai/utils'; 4 5 export const store = createStore(); 6 7 export const selectedFeedUriAtom = atomWithStorage<string | null>( 8 - 'selectedFeedUri', 9 null 10 ); 11 12 //export const feedScrollPositionsAtom = atom<Record<string, number>>({}); 13 14 export const feedScrollPositionsAtom = atomWithStorage<Record<string, number>>( 15 - 'feedscrollpositions', 16 {} 17 ); 18 19 export const likedPostsAtom = atomWithStorage<Record<string, string>>( 20 - 'likedPosts', 21 {} 22 ); 23 24 export const isAtTopAtom = atom<boolean>(true); 25 26 - export const agentAtom = atom<Agent|null>(null); 27 - export const authedAtom = atom<boolean>(false);
··· 1 + import { atom, createStore, useAtomValue } from "jotai"; 2 + import { atomWithStorage } from "jotai/utils"; 3 + import { useEffect } from "react"; 4 5 export const store = createStore(); 6 7 + export const quickAuthAtom = atomWithStorage<string | null>( 8 + "quickAuth", 9 + null 10 + ); 11 + 12 export const selectedFeedUriAtom = atomWithStorage<string | null>( 13 + "selectedFeedUri", 14 null 15 ); 16 17 //export const feedScrollPositionsAtom = atom<Record<string, number>>({}); 18 19 export const feedScrollPositionsAtom = atomWithStorage<Record<string, number>>( 20 + "feedscrollpositions", 21 {} 22 ); 23 24 export const likedPostsAtom = atomWithStorage<Record<string, string>>( 25 + "likedPosts", 26 {} 27 ); 28 29 + export const defaultconstellationURL = "constellation.microcosm.blue"; 30 + export const constellationURLAtom = atomWithStorage<string>( 31 + "constellationURL", 32 + defaultconstellationURL 33 + ); 34 + export const defaultslingshotURL = "slingshot.microcosm.blue"; 35 + export const slingshotURLAtom = atomWithStorage<string>( 36 + "slingshotURL", 37 + defaultslingshotURL 38 + ); 39 + export const defaultImgCDN = "cdn.bsky.app"; 40 + export const imgCDNAtom = atomWithStorage<string>("imgcdnurl", defaultImgCDN); 41 + export const defaultVideoCDN = "video.bsky.app"; 42 + export const videoCDNAtom = atomWithStorage<string>( 43 + "videocdnurl", 44 + defaultVideoCDN 45 + ); 46 + 47 + export const defaulthue = 28; 48 + export const hueAtom = atomWithStorage<number>("hue", defaulthue); 49 + 50 export const isAtTopAtom = atom<boolean>(true); 51 52 + type ComposerState = 53 + | { kind: "closed" } 54 + | { kind: "root" } 55 + | { kind: "reply"; parent: string } 56 + | { kind: "quote"; subject: string }; 57 + export const composerAtom = atom<ComposerState>({ kind: "closed" }); 58 + 59 + //export const agentAtom = atom<Agent | null>(null); 60 + //export const authedAtom = atom<boolean>(false); 61 + 62 + export function useAtomCssVar(atom: typeof hueAtom, cssVar: string) { 63 + const value = useAtomValue(atom); 64 + 65 + useEffect(() => { 66 + document.documentElement.style.setProperty(cssVar, value.toString()); 67 + }, [value, cssVar]); 68 + 69 + useEffect(() => { 70 + document.documentElement.style.setProperty(cssVar, value.toString()); 71 + }, []); 72 + } 73 + 74 + hueAtom.onMount = (setAtom) => { 75 + const stored = localStorage.getItem("hue"); 76 + if (stored != null) setAtom(Number(stored)); 77 + }; 78 + // export function initAtomToCssVar(atom: typeof hueAtom, cssVar: string) { 79 + // const initial = store.get(atom); 80 + // console.log("atom get ", initial); 81 + // document.documentElement.style.setProperty(cssVar, initial.toString()); 82 + // }
+37 -3
src/utils/followState.ts
··· 1 - import { AtUri, type Agent } from "@atproto/api"; 2 - import { useQueryConstellation, type linksRecordsResponse } from "./useQuery"; 3 import type { QueryClient } from "@tanstack/react-query"; 4 - import { TID } from "@atproto/common-web"; 5 6 export function useGetFollowState({ 7 target, ··· 127 }; 128 }); 129 }
··· 1 + import { type Agent,AtUri } from "@atproto/api"; 2 + import { TID } from "@atproto/common-web"; 3 import type { QueryClient } from "@tanstack/react-query"; 4 + 5 + import { type linksRecordsResponse,useQueryConstellation } from "./useQuery"; 6 7 export function useGetFollowState({ 8 target, ··· 128 }; 129 }); 130 } 131 + 132 + 133 + 134 + export function useGetOneToOneState(params?: { 135 + target: string; 136 + user: string; 137 + collection: string; 138 + path: string; 139 + }): string[] | undefined { 140 + const { data: arbitrarydata } = useQueryConstellation( 141 + params && params.user 142 + ? { 143 + method: "/links", 144 + target: params.target, 145 + // @ts-expect-error overloading sucks so much 146 + collection: params.collection, 147 + path: params.path, 148 + dids: [params.user], 149 + } 150 + : { method: "undefined", target: "whatever" } 151 + // overloading sucks so much 152 + ) as { data: linksRecordsResponse | undefined }; 153 + if (!params || !params.user) return undefined; 154 + const data = arbitrarydata?.linking_records.slice(0, 50) ?? []; 155 + 156 + if (data.length > 0) { 157 + return data.map((linksRecord) => { 158 + return `at://${linksRecord.did}/${linksRecord.collection}/${linksRecord.rkey}`; 159 + }); 160 + } 161 + 162 + return undefined; 163 + }
+53 -23
src/utils/useHydrated.ts
··· 9 AppBskyFeedPost, 10 AtUri, 11 } from "@atproto/api"; 12 import { useMemo } from "react"; 13 14 - import { useQueryIdentity,useQueryPost, useQueryProfile } from "./useQuery"; 15 16 - type QueryResultData<T extends (...args: any) => any> = ReturnType<T> extends 17 - | { data: infer D } 18 - | undefined 19 - ? D 20 - : never; 21 22 function asTyped<T extends { $type: string }>(obj: T): $Typed<T> { 23 return obj as $Typed<T>; ··· 26 export function hydrateEmbedImages( 27 embed: AppBskyEmbedImages.Main, 28 did: string, 29 ): $Typed<AppBskyEmbedImages.View> { 30 return asTyped({ 31 $type: "app.bsky.embed.images#view" as const, ··· 34 const link = img.image.ref?.["$link"]; 35 if (!link) return null; 36 return { 37 - thumb: `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${link}@jpeg`, 38 - fullsize: `https://cdn.bsky.app/img/feed_fullsize/plain/${did}/${link}@jpeg`, 39 alt: img.alt || "", 40 aspectRatio: img.aspectRatio, 41 }; ··· 47 export function hydrateEmbedExternal( 48 embed: AppBskyEmbedExternal.Main, 49 did: string, 50 ): $Typed<AppBskyEmbedExternal.View> { 51 return asTyped({ 52 $type: "app.bsky.embed.external#view" as const, ··· 55 title: embed.external.title, 56 description: embed.external.description, 57 thumb: embed.external.thumb?.ref?.$link 58 - ? `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${embed.external.thumb.ref.$link}@jpeg` 59 : undefined, 60 }, 61 }); ··· 64 export function hydrateEmbedVideo( 65 embed: AppBskyEmbedVideo.Main, 66 did: string, 67 ): $Typed<AppBskyEmbedVideo.View> { 68 const videoLink = embed.video.ref.$link; 69 return asTyped({ 70 $type: "app.bsky.embed.video#view" as const, 71 - playlist: `https://video.bsky.app/watch/${did}/${videoLink}/playlist.m3u8`, 72 - thumbnail: `https://video.bsky.app/watch/${did}/${videoLink}/thumbnail.jpg`, 73 aspectRatio: embed.aspectRatio, 74 cid: videoLink, 75 }); ··· 80 quotedPost: QueryResultData<typeof useQueryPost>, 81 quotedProfile: QueryResultData<typeof useQueryProfile>, 82 quotedIdentity: QueryResultData<typeof useQueryIdentity>, 83 ): $Typed<AppBskyEmbedRecord.View> | undefined { 84 if (!quotedPost || !quotedProfile || !quotedIdentity) { 85 return undefined; ··· 91 handle: quotedIdentity.handle, 92 displayName: quotedProfile.value.displayName ?? quotedIdentity.handle, 93 avatar: quotedProfile.value.avatar?.ref?.$link 94 - ? `https://cdn.bsky.app/img/avatar/plain/${quotedIdentity.did}/${quotedProfile.value.avatar.ref.$link}@jpeg` 95 : undefined, 96 viewer: {}, 97 labels: [], ··· 122 quotedPost: QueryResultData<typeof useQueryPost>, 123 quotedProfile: QueryResultData<typeof useQueryProfile>, 124 quotedIdentity: QueryResultData<typeof useQueryIdentity>, 125 ): $Typed<AppBskyEmbedRecordWithMedia.View> | undefined { 126 const hydratedRecord = hydrateEmbedRecord( 127 embed.record, 128 quotedPost, 129 quotedProfile, 130 quotedIdentity, 131 ); 132 133 if (!hydratedRecord) return undefined; ··· 148 149 export function useHydratedEmbed( 150 embed: AppBskyFeedPost.Record["embed"], 151 - postAuthorDid: string | undefined, 152 ) { 153 const recordInfo = useMemo(() => { 154 if (AppBskyEmbedRecordWithMedia.isMain(embed)) { ··· 181 error: profileError, 182 } = useQueryProfile(profileUri); 183 184 const queryidentityresult = useQueryIdentity(quotedAuthorDid); 185 186 const hydratedEmbed: HydratedEmbedView | undefined = (() => { 187 if (!embed || !postAuthorDid) return undefined; 188 189 - if (isRecordType && (!usequerypostresults?.data || !quotedProfile || !queryidentityresult?.data)) { 190 return undefined; 191 } 192 193 try { 194 if (AppBskyEmbedImages.isMain(embed)) { 195 - return hydrateEmbedImages(embed, postAuthorDid); 196 } else if (AppBskyEmbedExternal.isMain(embed)) { 197 - return hydrateEmbedExternal(embed, postAuthorDid); 198 } else if (AppBskyEmbedVideo.isMain(embed)) { 199 - return hydrateEmbedVideo(embed, postAuthorDid); 200 } else if (AppBskyEmbedRecord.isMain(embed)) { 201 return hydrateEmbedRecord( 202 embed, 203 usequerypostresults?.data, 204 quotedProfile, 205 queryidentityresult?.data, 206 ); 207 } else if (AppBskyEmbedRecordWithMedia.isMain(embed)) { 208 let hydratedMedia: ··· 212 | undefined; 213 214 if (AppBskyEmbedImages.isMain(embed.media)) { 215 - hydratedMedia = hydrateEmbedImages(embed.media, postAuthorDid); 216 } else if (AppBskyEmbedExternal.isMain(embed.media)) { 217 - hydratedMedia = hydrateEmbedExternal(embed.media, postAuthorDid); 218 } else if (AppBskyEmbedVideo.isMain(embed.media)) { 219 - hydratedMedia = hydrateEmbedVideo(embed.media, postAuthorDid); 220 } 221 222 if (hydratedMedia) { ··· 226 usequerypostresults?.data, 227 quotedProfile, 228 queryidentityresult?.data, 229 ); 230 } 231 } ··· 236 })(); 237 238 const isLoading = isRecordType 239 - ? usequerypostresults?.isLoading || isLoadingProfile || queryidentityresult?.isLoading 240 : false; 241 242 - const error = usequerypostresults?.error || profileError || queryidentityresult?.error; 243 244 return { data: hydratedEmbed, isLoading, error }; 245 - }
··· 9 AppBskyFeedPost, 10 AtUri, 11 } from "@atproto/api"; 12 + import { useAtom } from "jotai"; 13 import { useMemo } from "react"; 14 15 + import { imgCDNAtom, videoCDNAtom } from "./atoms"; 16 + import { useQueryIdentity, useQueryPost, useQueryProfile } from "./useQuery"; 17 18 + type QueryResultData<T extends (...args: any) => any> = 19 + ReturnType<T> extends { data: infer D } | undefined ? D : never; 20 21 function asTyped<T extends { $type: string }>(obj: T): $Typed<T> { 22 return obj as $Typed<T>; ··· 25 export function hydrateEmbedImages( 26 embed: AppBskyEmbedImages.Main, 27 did: string, 28 + cdn: string 29 ): $Typed<AppBskyEmbedImages.View> { 30 return asTyped({ 31 $type: "app.bsky.embed.images#view" as const, ··· 34 const link = img.image.ref?.["$link"]; 35 if (!link) return null; 36 return { 37 + thumb: `https://${cdn}/img/feed_thumbnail/plain/${did}/${link}@jpeg`, 38 + fullsize: `https://${cdn}/img/feed_fullsize/plain/${did}/${link}@jpeg`, 39 alt: img.alt || "", 40 aspectRatio: img.aspectRatio, 41 }; ··· 47 export function hydrateEmbedExternal( 48 embed: AppBskyEmbedExternal.Main, 49 did: string, 50 + cdn: string 51 ): $Typed<AppBskyEmbedExternal.View> { 52 return asTyped({ 53 $type: "app.bsky.embed.external#view" as const, ··· 56 title: embed.external.title, 57 description: embed.external.description, 58 thumb: embed.external.thumb?.ref?.$link 59 + ? `https://${cdn}/img/feed_thumbnail/plain/${did}/${embed.external.thumb.ref.$link}@jpeg` 60 : undefined, 61 }, 62 }); ··· 65 export function hydrateEmbedVideo( 66 embed: AppBskyEmbedVideo.Main, 67 did: string, 68 + videocdn: string 69 ): $Typed<AppBskyEmbedVideo.View> { 70 const videoLink = embed.video.ref.$link; 71 return asTyped({ 72 $type: "app.bsky.embed.video#view" as const, 73 + playlist: `https://${videocdn}/watch/${did}/${videoLink}/playlist.m3u8`, 74 + thumbnail: `https://${videocdn}/watch/${did}/${videoLink}/thumbnail.jpg`, 75 aspectRatio: embed.aspectRatio, 76 cid: videoLink, 77 }); ··· 82 quotedPost: QueryResultData<typeof useQueryPost>, 83 quotedProfile: QueryResultData<typeof useQueryProfile>, 84 quotedIdentity: QueryResultData<typeof useQueryIdentity>, 85 + cdn: string 86 ): $Typed<AppBskyEmbedRecord.View> | undefined { 87 if (!quotedPost || !quotedProfile || !quotedIdentity) { 88 return undefined; ··· 94 handle: quotedIdentity.handle, 95 displayName: quotedProfile.value.displayName ?? quotedIdentity.handle, 96 avatar: quotedProfile.value.avatar?.ref?.$link 97 + ? `https://${cdn}/img/avatar/plain/${quotedIdentity.did}/${quotedProfile.value.avatar.ref.$link}@jpeg` 98 : undefined, 99 viewer: {}, 100 labels: [], ··· 125 quotedPost: QueryResultData<typeof useQueryPost>, 126 quotedProfile: QueryResultData<typeof useQueryProfile>, 127 quotedIdentity: QueryResultData<typeof useQueryIdentity>, 128 + cdn: string 129 ): $Typed<AppBskyEmbedRecordWithMedia.View> | undefined { 130 const hydratedRecord = hydrateEmbedRecord( 131 embed.record, 132 quotedPost, 133 quotedProfile, 134 quotedIdentity, 135 + cdn 136 ); 137 138 if (!hydratedRecord) return undefined; ··· 153 154 export function useHydratedEmbed( 155 embed: AppBskyFeedPost.Record["embed"], 156 + postAuthorDid: string | undefined 157 ) { 158 const recordInfo = useMemo(() => { 159 if (AppBskyEmbedRecordWithMedia.isMain(embed)) { ··· 186 error: profileError, 187 } = useQueryProfile(profileUri); 188 189 + const [imgcdn] = useAtom(imgCDNAtom); 190 + const [videocdn] = useAtom(videoCDNAtom); 191 + 192 const queryidentityresult = useQueryIdentity(quotedAuthorDid); 193 194 const hydratedEmbed: HydratedEmbedView | undefined = (() => { 195 if (!embed || !postAuthorDid) return undefined; 196 197 + if ( 198 + isRecordType && 199 + (!usequerypostresults?.data || 200 + !quotedProfile || 201 + !queryidentityresult?.data) 202 + ) { 203 return undefined; 204 } 205 206 try { 207 if (AppBskyEmbedImages.isMain(embed)) { 208 + return hydrateEmbedImages(embed, postAuthorDid, imgcdn); 209 } else if (AppBskyEmbedExternal.isMain(embed)) { 210 + return hydrateEmbedExternal(embed, postAuthorDid, imgcdn); 211 } else if (AppBskyEmbedVideo.isMain(embed)) { 212 + return hydrateEmbedVideo(embed, postAuthorDid, videocdn); 213 } else if (AppBskyEmbedRecord.isMain(embed)) { 214 return hydrateEmbedRecord( 215 embed, 216 usequerypostresults?.data, 217 quotedProfile, 218 queryidentityresult?.data, 219 + imgcdn 220 ); 221 } else if (AppBskyEmbedRecordWithMedia.isMain(embed)) { 222 let hydratedMedia: ··· 226 | undefined; 227 228 if (AppBskyEmbedImages.isMain(embed.media)) { 229 + hydratedMedia = hydrateEmbedImages( 230 + embed.media, 231 + postAuthorDid, 232 + imgcdn 233 + ); 234 } else if (AppBskyEmbedExternal.isMain(embed.media)) { 235 + hydratedMedia = hydrateEmbedExternal( 236 + embed.media, 237 + postAuthorDid, 238 + imgcdn 239 + ); 240 } else if (AppBskyEmbedVideo.isMain(embed.media)) { 241 + hydratedMedia = hydrateEmbedVideo( 242 + embed.media, 243 + postAuthorDid, 244 + videocdn 245 + ); 246 } 247 248 if (hydratedMedia) { ··· 252 usequerypostresults?.data, 253 quotedProfile, 254 queryidentityresult?.data, 255 + imgcdn 256 ); 257 } 258 } ··· 263 })(); 264 265 const isLoading = isRecordType 266 + ? usequerypostresults?.isLoading || 267 + isLoadingProfile || 268 + queryidentityresult?.isLoading 269 : false; 270 271 + const error = 272 + usequerypostresults?.error || profileError || queryidentityresult?.error; 273 274 return { data: hydratedEmbed, isLoading, error }; 275 + }
+81 -17
src/utils/useQuery.ts
··· 1 import * as ATPAPI from "@atproto/api"; 2 import { 3 type QueryFunctionContext, 4 queryOptions, 5 useInfiniteQuery, 6 useQuery, 7 type UseQueryResult} from "@tanstack/react-query"; 8 9 - export function constructIdentityQuery(didorhandle?: string) { 10 return queryOptions({ 11 queryKey: ["identity", didorhandle], 12 queryFn: async () => { 13 if (!didorhandle) return undefined as undefined 14 const res = await fetch( 15 - `https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(didorhandle)}` 16 ); 17 if (!res.ok) throw new Error("Failed to fetch post"); 18 try { ··· 54 Error 55 > 56 export function useQueryIdentity(didorhandle?: string) { 57 - return useQuery(constructIdentityQuery(didorhandle)); 58 } 59 60 - export function constructPostQuery(uri?: string) { 61 return queryOptions({ 62 queryKey: ["post", uri], 63 queryFn: async () => { 64 if (!uri) return undefined as undefined 65 const res = await fetch( 66 - `https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 67 ); 68 let data: any; 69 try { ··· 117 Error 118 > 119 export function useQueryPost(uri?: string) { 120 - return useQuery(constructPostQuery(uri)); 121 } 122 123 - export function constructProfileQuery(uri?: string) { 124 return queryOptions({ 125 queryKey: ["profile", uri], 126 queryFn: async () => { 127 if (!uri) return undefined as undefined 128 const res = await fetch( 129 - `https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 130 ); 131 let data: any; 132 try { ··· 180 Error 181 > 182 export function useQueryProfile(uri?: string) { 183 - return useQuery(constructProfileQuery(uri)); 184 } 185 186 // export function constructConstellationQuery( ··· 216 // target: string 217 // ): QueryOptions<linksAllResponse, Error>; 218 export function constructConstellationQuery(query?:{ 219 method: 220 | "/links" 221 | "/links/distinct-dids" ··· 249 const cursor = query.cursor 250 const dids = query?.dids 251 const res = await fetch( 252 - `https://constellation.microcosm.blue${method}?target=${encodeURIComponent(target)}${collection ? `&collection=${encodeURIComponent(collection)}` : ""}${path ? `&path=${encodeURIComponent(path)}` : ""}${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}${dids ? dids.map((did) => `&did=${encodeURIComponent(did)}`).join("") : ""}` 253 ); 254 if (!res.ok) throw new Error("Failed to fetch post"); 255 try { ··· 338 > 339 | undefined { 340 //if (!query) return; 341 return useQuery( 342 - constructConstellationQuery(query) 343 ); 344 } 345 ··· 361 type linksCountResponse = { 362 total: string; 363 }; 364 - type linksAllResponse = { 365 links: Record< 366 string, 367 Record< ··· 444 445 446 447 - export function constructArbitraryQuery(uri?: string) { 448 return queryOptions({ 449 queryKey: ["arbitrary", uri], 450 queryFn: async () => { 451 if (!uri) return undefined as undefined 452 const res = await fetch( 453 - `https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 454 ); 455 let data: any; 456 try { ··· 503 Error 504 >; 505 export function useQueryArbitrary(uri?: string) { 506 - return useQuery(constructArbitraryQuery(uri)); 507 } 508 509 export function constructFallbackNothingQuery(){ ··· 605 }) { 606 const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery(options); 607 608 - return useInfiniteQuery({ 609 queryKey, 610 queryFn, 611 initialPageParam: undefined as never, ··· 613 staleTime: Infinity, 614 refetchOnWindowFocus: false, 615 enabled: !!options.feedUri && (options.isAuthed ? !!options.agent && !!options.pdsUrl && !!options.feedServiceDid : true), 616 - }); 617 }
··· 1 import * as ATPAPI from "@atproto/api"; 2 import { 3 + infiniteQueryOptions, 4 type QueryFunctionContext, 5 queryOptions, 6 useInfiniteQuery, 7 useQuery, 8 type UseQueryResult} from "@tanstack/react-query"; 9 + import { useAtom } from "jotai"; 10 11 + import { constellationURLAtom, slingshotURLAtom } from "./atoms"; 12 + 13 + export function constructIdentityQuery(didorhandle?: string, slingshoturl?: string) { 14 return queryOptions({ 15 queryKey: ["identity", didorhandle], 16 queryFn: async () => { 17 if (!didorhandle) return undefined as undefined 18 const res = await fetch( 19 + `https://${slingshoturl}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(didorhandle)}` 20 ); 21 if (!res.ok) throw new Error("Failed to fetch post"); 22 try { ··· 58 Error 59 > 60 export function useQueryIdentity(didorhandle?: string) { 61 + const [slingshoturl] = useAtom(slingshotURLAtom) 62 + return useQuery(constructIdentityQuery(didorhandle, slingshoturl)); 63 } 64 65 + export function constructPostQuery(uri?: string, slingshoturl?: string) { 66 return queryOptions({ 67 queryKey: ["post", uri], 68 queryFn: async () => { 69 if (!uri) return undefined as undefined 70 const res = await fetch( 71 + `https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 72 ); 73 let data: any; 74 try { ··· 122 Error 123 > 124 export function useQueryPost(uri?: string) { 125 + const [slingshoturl] = useAtom(slingshotURLAtom) 126 + return useQuery(constructPostQuery(uri, slingshoturl)); 127 } 128 129 + export function constructProfileQuery(uri?: string, slingshoturl?: string) { 130 return queryOptions({ 131 queryKey: ["profile", uri], 132 queryFn: async () => { 133 if (!uri) return undefined as undefined 134 const res = await fetch( 135 + `https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 136 ); 137 let data: any; 138 try { ··· 186 Error 187 > 188 export function useQueryProfile(uri?: string) { 189 + const [slingshoturl] = useAtom(slingshotURLAtom) 190 + return useQuery(constructProfileQuery(uri, slingshoturl)); 191 } 192 193 // export function constructConstellationQuery( ··· 223 // target: string 224 // ): QueryOptions<linksAllResponse, Error>; 225 export function constructConstellationQuery(query?:{ 226 + constellation: string, 227 method: 228 | "/links" 229 | "/links/distinct-dids" ··· 257 const cursor = query.cursor 258 const dids = query?.dids 259 const res = await fetch( 260 + `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("") : ""}` 261 ); 262 if (!res.ok) throw new Error("Failed to fetch post"); 263 try { ··· 346 > 347 | undefined { 348 //if (!query) return; 349 + const [constellationurl] = useAtom(constellationURLAtom) 350 return useQuery( 351 + constructConstellationQuery(query && {constellation: constellationurl, ...query}) 352 ); 353 } 354 ··· 370 type linksCountResponse = { 371 total: string; 372 }; 373 + export type linksAllResponse = { 374 links: Record< 375 string, 376 Record< ··· 453 454 455 456 + export function constructArbitraryQuery(uri?: string, slingshoturl?: string) { 457 return queryOptions({ 458 queryKey: ["arbitrary", uri], 459 queryFn: async () => { 460 if (!uri) return undefined as undefined 461 const res = await fetch( 462 + `https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 463 ); 464 let data: any; 465 try { ··· 512 Error 513 >; 514 export function useQueryArbitrary(uri?: string) { 515 + const [slingshoturl] = useAtom(slingshotURLAtom) 516 + return useQuery(constructArbitraryQuery(uri, slingshoturl)); 517 } 518 519 export function constructFallbackNothingQuery(){ ··· 615 }) { 616 const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery(options); 617 618 + return {...useInfiniteQuery({ 619 queryKey, 620 queryFn, 621 initialPageParam: undefined as never, ··· 623 staleTime: Infinity, 624 refetchOnWindowFocus: false, 625 enabled: !!options.feedUri && (options.isAuthed ? !!options.agent && !!options.pdsUrl && !!options.feedServiceDid : true), 626 + }), queryKey: queryKey}; 627 + } 628 + 629 + 630 + export function yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(query?: { 631 + constellation: string, 632 + method: '/links' 633 + target?: string 634 + collection: string 635 + path: string 636 + }) { 637 + console.log( 638 + 'yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks', 639 + query, 640 + ) 641 + 642 + return infiniteQueryOptions({ 643 + enabled: !!query?.target, 644 + queryKey: [ 645 + 'reddwarf_constellation', 646 + query?.method, 647 + query?.target, 648 + query?.collection, 649 + query?.path, 650 + ] as const, 651 + 652 + queryFn: async ({pageParam}: {pageParam?: string}) => { 653 + if (!query || !query?.target) return undefined 654 + 655 + const method = query.method 656 + const target = query.target 657 + const collection = query.collection 658 + const path = query.path 659 + const cursor = pageParam 660 + 661 + const res = await fetch( 662 + `https://${query.constellation}${method}?target=${encodeURIComponent(target)}${ 663 + collection ? `&collection=${encodeURIComponent(collection)}` : '' 664 + }${path ? `&path=${encodeURIComponent(path)}` : ''}${ 665 + cursor ? `&cursor=${encodeURIComponent(cursor)}` : '' 666 + }`, 667 + ) 668 + 669 + if (!res.ok) throw new Error('Failed to fetch') 670 + 671 + return (await res.json()) as linksRecordsResponse 672 + }, 673 + 674 + getNextPageParam: lastPage => { 675 + return (lastPage as any)?.cursor ?? undefined 676 + }, 677 + initialPageParam: undefined, 678 + staleTime: 5 * 60 * 1000, 679 + gcTime: 5 * 60 * 1000, 680 + }) 681 }
+1 -1
vite.config.ts
··· 10 11 import { generateMetadataPlugin } from "./oauthdev.mts"; 12 13 - const PROD_URL = "https://reddwarf.whey.party" 14 const DEV_URL = "https://local3768forumtest.whey.party" 15 16 function shp(url: string): string {
··· 10 11 import { generateMetadataPlugin } from "./oauthdev.mts"; 12 13 + const PROD_URL = "https://reddwarf.app" 14 const DEV_URL = "https://local3768forumtest.whey.party" 15 16 function shp(url: string): string {