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 8 ## running dev and build 9 9 in the `vite.config.ts` file you should change these values 10 10 ```ts 11 - const PROD_URL = "https://reddwarf.whey.party" 11 + const PROD_URL = "https://reddwarf.app" 12 12 const DEV_URL = "https://local3768forumtest.whey.party" 13 13 ``` 14 14 the PROD_URL is what will compile your oauth client metadata so it is very important to change that. same for DEV_URL if you are using a tunnel for dev work
+1685
package-lock.json
··· 8 8 "dependencies": { 9 9 "@atproto/api": "^0.16.6", 10 10 "@atproto/oauth-client-browser": "^0.3.33", 11 + "@radix-ui/react-dialog": "^1.1.15", 12 + "@radix-ui/react-dropdown-menu": "^2.1.16", 13 + "@radix-ui/react-hover-card": "^1.1.15", 14 + "@radix-ui/react-slider": "^1.3.6", 11 15 "@tailwindcss/vite": "^4.0.6", 12 16 "@tanstack/query-sync-storage-persister": "^5.85.6", 13 17 "@tanstack/react-devtools": "^0.2.2", ··· 21 25 "idb-keyval": "^6.2.2", 22 26 "jotai": "^2.13.1", 23 27 "npm": "^11.6.2", 28 + "radix-ui": "^1.4.3", 24 29 "react": "^19.0.0", 25 30 "react-dom": "^19.0.0", 26 31 "react-player": "^3.3.2", ··· 1592 1597 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1593 1598 } 1594 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 + }, 1595 1634 "node_modules/@humanfs/core": { 1596 1635 "version": "0.19.1", 1597 1636 "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", ··· 1895 1934 "node": ">= 8" 1896 1935 } 1897 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 + }, 1898 3376 "node_modules/@rolldown/pluginutils": { 1899 3377 "version": "1.0.0-beta.27", 1900 3378 "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", ··· 3806 5284 "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 3807 5285 "dev": true, 3808 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 + } 3809 5298 }, 3810 5299 "node_modules/aria-query": { 3811 5300 "version": "5.3.0", ··· 4716 6205 "node": ">=8" 4717 6206 } 4718 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 + }, 4719 6213 "node_modules/diff": { 4720 6214 "version": "8.0.2", 4721 6215 "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", ··· 5735 7229 }, 5736 7230 "funding": { 5737 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" 5738 7240 } 5739 7241 }, 5740 7242 "node_modules/get-proto": { ··· 10338 11840 ], 10339 11841 "license": "MIT" 10340 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 + }, 10341 11919 "node_modules/react": { 10342 11920 "version": "19.1.1", 10343 11921 "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", ··· 10399 11977 "node": ">=0.10.0" 10400 11978 } 10401 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 + }, 10402 12046 "node_modules/readdirp": { 10403 12047 "version": "3.6.0", 10404 12048 "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", ··· 11892 13536 "peer": true, 11893 13537 "dependencies": { 11894 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 + } 11895 13580 } 11896 13581 }, 11897 13582 "node_modules/use-sync-external-store": {
+5
package.json
··· 12 12 "dependencies": { 13 13 "@atproto/api": "^0.16.6", 14 14 "@atproto/oauth-client-browser": "^0.3.33", 15 + "@radix-ui/react-dialog": "^1.1.15", 16 + "@radix-ui/react-dropdown-menu": "^2.1.16", 17 + "@radix-ui/react-hover-card": "^1.1.15", 18 + "@radix-ui/react-slider": "^1.3.6", 15 19 "@tailwindcss/vite": "^4.0.6", 16 20 "@tanstack/query-sync-storage-persister": "^5.85.6", 17 21 "@tanstack/react-devtools": "^0.2.2", ··· 25 29 "idb-keyval": "^6.2.2", 26 30 "jotai": "^2.13.1", 27 31 "npm": "^11.6.2", 32 + "radix-ui": "^1.4.3", 28 33 "react": "^19.0.0", 29 34 "react-dom": "^19.0.0", 30 35 "react-player": "^3.3.2",
+292
src/components/Composer.tsx
··· 1 + import { AppBskyRichtextFacet, RichText } from "@atproto/api"; 2 + import { useAtom } from "jotai"; 3 + import { Dialog } from "radix-ui"; 4 + import { useEffect, useRef, useState } from "react"; 5 + 6 + import { useAuth } from "~/providers/UnifiedAuthProvider"; 7 + import { composerAtom } from "~/utils/atoms"; 8 + import { useQueryPost } from "~/utils/useQuery"; 9 + 10 + import { ProfileThing } from "./Login"; 11 + import { UniversalPostRendererATURILoader } from "./UniversalPostRenderer"; 12 + 13 + const MAX_POST_LENGTH = 300; 14 + 15 + export function Composer() { 16 + const [composerState, setComposerState] = useAtom(composerAtom); 17 + const { agent } = useAuth(); 18 + 19 + const [postText, setPostText] = useState(""); 20 + const [posting, setPosting] = useState(false); 21 + const [postSuccess, setPostSuccess] = useState(false); 22 + const [postError, setPostError] = useState<string | null>(null); 23 + 24 + useEffect(() => { 25 + setPostText(""); 26 + setPosting(false); 27 + setPostSuccess(false); 28 + setPostError(null); 29 + }, [composerState.kind]); 30 + 31 + const parentUri = 32 + composerState.kind === "reply" 33 + ? composerState.parent 34 + : composerState.kind === "quote" 35 + ? composerState.subject 36 + : undefined; 37 + 38 + const { data: parentPost, isLoading: isParentLoading } = 39 + useQueryPost(parentUri); 40 + 41 + async function handlePost() { 42 + if (!agent || !postText.trim() || postText.length > MAX_POST_LENGTH) return; 43 + 44 + setPosting(true); 45 + setPostError(null); 46 + 47 + try { 48 + const rt = new RichText({ text: postText }); 49 + await rt.detectFacets(agent); 50 + 51 + if (rt.facets?.length) { 52 + rt.facets = rt.facets.filter((item) => { 53 + if (item.$type !== "app.bsky.richtext.facet") return true; 54 + if (!item.features?.length) return true; 55 + 56 + item.features = item.features.filter((feature) => { 57 + if (feature.$type !== "app.bsky.richtext.facet#mention") return true; 58 + const did = feature.$type === "app.bsky.richtext.facet#mention" ? (feature as AppBskyRichtextFacet.Mention)?.did : undefined; 59 + return typeof did === "string" && did.startsWith("did:"); 60 + }); 61 + 62 + return item.features.length > 0; 63 + }); 64 + } 65 + 66 + const record: Record<string, unknown> = { 67 + $type: "app.bsky.feed.post", 68 + text: rt.text, 69 + facets: rt.facets, 70 + createdAt: new Date().toISOString(), 71 + }; 72 + 73 + if (composerState.kind === "reply" && parentPost) { 74 + record.reply = { 75 + root: parentPost.value?.reply?.root ?? { 76 + uri: parentPost.uri, 77 + cid: parentPost.cid, 78 + }, 79 + parent: { 80 + uri: parentPost.uri, 81 + cid: parentPost.cid, 82 + }, 83 + }; 84 + } 85 + 86 + if (composerState.kind === "quote" && parentPost) { 87 + record.embed = { 88 + $type: "app.bsky.embed.record", 89 + record: { 90 + uri: parentPost.uri, 91 + cid: parentPost.cid, 92 + }, 93 + }; 94 + } 95 + 96 + await agent.com.atproto.repo.createRecord({ 97 + collection: "app.bsky.feed.post", 98 + repo: agent.assertDid, 99 + record, 100 + }); 101 + 102 + setPostSuccess(true); 103 + setPostText(""); 104 + 105 + setTimeout(() => { 106 + setPostSuccess(false); 107 + setComposerState({ kind: "closed" }); 108 + }, 1500); 109 + } catch (e: any) { 110 + setPostError(e?.message || "Failed to post"); 111 + } finally { 112 + setPosting(false); 113 + } 114 + } 115 + // if (composerState.kind === "closed") { 116 + // return null; 117 + // } 118 + 119 + const getPlaceholder = () => { 120 + switch (composerState.kind) { 121 + case "reply": 122 + return "Post your reply"; 123 + case "quote": 124 + return "Add a comment..."; 125 + case "root": 126 + default: 127 + return "What's happening?!"; 128 + } 129 + }; 130 + 131 + const charsLeft = MAX_POST_LENGTH - postText.length; 132 + const isPostButtonDisabled = 133 + posting || !postText.trim() || isParentLoading || charsLeft < 0; 134 + 135 + return ( 136 + <Dialog.Root 137 + open={composerState.kind !== "closed"} 138 + onOpenChange={(open) => { 139 + if (!open) setComposerState({ kind: "closed" }); 140 + }} 141 + > 142 + <Dialog.Portal> 143 + <Dialog.Overlay className="fixed inset-0 z-50 bg-black/40 dark:bg-black/50 data-[state=open]:animate-fadeIn" /> 144 + 145 + <Dialog.Content className="fixed overflow-y-scroll inset-0 z-50 flex items-start justify-center py-10 sm:py-20"> 146 + <div className="bg-gray-50 dark:bg-gray-950 border border-gray-200 dark:border-gray-700 rounded-2xl shadow-xl w-full max-w-xl relative mx-4"> 147 + <div className="flex flex-row justify-between p-2"> 148 + <Dialog.Close asChild> 149 + <button 150 + className="h-8 w-8 flex items-center justify-center rounded-full text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800" 151 + disabled={posting} 152 + aria-label="Close" 153 + > 154 + <svg 155 + xmlns="http://www.w3.org/2000/svg" 156 + width="20" 157 + height="20" 158 + viewBox="0 0 24 24" 159 + fill="none" 160 + stroke="currentColor" 161 + strokeWidth="2.5" 162 + strokeLinecap="round" 163 + strokeLinejoin="round" 164 + > 165 + <line x1="18" y1="6" x2="6" y2="18"></line> 166 + <line x1="6" y1="6" x2="18" y2="18"></line> 167 + </svg> 168 + </button> 169 + </Dialog.Close> 170 + 171 + <div className="flex-1" /> 172 + <div className="flex items-center gap-4"> 173 + <span 174 + className={`text-sm ${charsLeft < 0 ? "text-red-500" : "text-gray-500"}`} 175 + > 176 + {charsLeft} 177 + </span> 178 + <button 179 + className="bg-gray-600 hover:bg-gray-700 text-white font-bold py-1 px-4 rounded-full disabled:opacity-50 disabled:cursor-not-allowed transition-colors" 180 + onClick={handlePost} 181 + disabled={isPostButtonDisabled} 182 + > 183 + {posting ? "Posting..." : "Post"} 184 + </button> 185 + </div> 186 + </div> 187 + 188 + {postSuccess ? ( 189 + <div className="flex flex-col items-center justify-center py-16"> 190 + <span className="text-gray-500 text-6xl mb-4">โœ“</span> 191 + <span className="text-xl font-bold text-black dark:text-white"> 192 + Posted! 193 + </span> 194 + </div> 195 + ) : ( 196 + <div className="px-4"> 197 + {composerState.kind === "reply" && ( 198 + <div className="mb-1 -mx-4"> 199 + {isParentLoading ? ( 200 + <div className="text-sm text-gray-500 animate-pulse"> 201 + Loading parent post... 202 + </div> 203 + ) : parentUri ? ( 204 + <UniversalPostRendererATURILoader 205 + atUri={parentUri} 206 + bottomReplyLine 207 + bottomBorder={false} 208 + /> 209 + ) : ( 210 + <div className="text-sm text-red-500 rounded-lg border border-red-500/50 p-3"> 211 + Could not load parent post. 212 + </div> 213 + )} 214 + </div> 215 + )} 216 + 217 + <div className="flex w-full gap-1 flex-col"> 218 + <ProfileThing agent={agent} large /> 219 + <div className="flex pl-[50px]"> 220 + <AutoGrowTextarea 221 + className="w-full text-lg bg-transparent focus:outline-none resize-none placeholder:text-gray-500 text-black dark:text-white pb-2" 222 + rows={5} 223 + placeholder={getPlaceholder()} 224 + value={postText} 225 + onChange={(e) => setPostText(e.target.value)} 226 + disabled={posting} 227 + autoFocus 228 + /> 229 + </div> 230 + </div> 231 + 232 + {composerState.kind === "quote" && ( 233 + <div className="mb-4 ml-[50px] rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden"> 234 + {isParentLoading ? ( 235 + <div className="text-sm text-gray-500 animate-pulse"> 236 + Loading parent post... 237 + </div> 238 + ) : parentUri ? ( 239 + <UniversalPostRendererATURILoader 240 + atUri={parentUri} 241 + isQuote 242 + /> 243 + ) : ( 244 + <div className="text-sm text-red-500 rounded-lg border border-red-500/50 p-3"> 245 + Could not load parent post. 246 + </div> 247 + )} 248 + </div> 249 + )} 250 + 251 + {postError && ( 252 + <div className="text-red-500 text-sm my-2 text-center"> 253 + {postError} 254 + </div> 255 + )} 256 + </div> 257 + )} 258 + </div> 259 + </Dialog.Content> 260 + </Dialog.Portal> 261 + </Dialog.Root> 262 + ); 263 + } 264 + 265 + function AutoGrowTextarea({ 266 + value, 267 + className, 268 + onChange, 269 + ...props 270 + }: React.DetailedHTMLProps< 271 + React.TextareaHTMLAttributes<HTMLTextAreaElement>, 272 + HTMLTextAreaElement 273 + >) { 274 + const ref = useRef<HTMLTextAreaElement>(null); 275 + 276 + useEffect(() => { 277 + const el = ref.current; 278 + if (!el) return; 279 + el.style.height = "auto"; 280 + el.style.height = el.scrollHeight + "px"; 281 + }, [value]); 282 + 283 + return ( 284 + <textarea 285 + ref={ref} 286 + className={className} 287 + value={value} 288 + onChange={onChange} 289 + {...props} 290 + /> 291 + ); 292 + }
+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 { useQueryClient } from "@tanstack/react-query"; 1 2 import * as React from "react"; 2 3 3 4 //import { useInView } from "react-intersection-observer"; ··· 37 38 isFetchingNextPage, 38 39 refetch, 39 40 isRefetching, 41 + queryKey, 40 42 } = useInfiniteQueryFeedSkeleton({ 41 43 feedUri: feedUri, 42 44 agent: agent ?? undefined, ··· 44 46 pdsUrl: pdsUrl, 45 47 feedServiceDid: feedServiceDid, 46 48 }); 49 + const queryClient = useQueryClient(); 50 + 47 51 48 52 const handleRefresh = () => { 53 + queryClient.removeQueries({queryKey: queryKey}); 54 + //queryClient.invalidateQueries(["infinite-feed", feedUri] as const); 49 55 refetch(); 50 56 }; 51 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 + 52 76 //const { ref, inView } = useInView(); 53 77 54 78 // React.useEffect(() => { ··· 67 91 ); 68 92 } 69 93 70 - const allPosts = 71 - data?.pages.flatMap((page) => { 72 - if (page) return page.feed; 73 - }) ?? []; 94 + // const allPosts = 95 + // data?.pages.flatMap((page) => { 96 + // if (page) return page.feed; 97 + // }) ?? []; 74 98 75 99 if (!allPosts || typeof allPosts !== "object" || allPosts.length === 0) { 76 100 return ( ··· 116 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" 117 141 aria-label="Refresh feed" 118 142 > 119 - <RefreshIcon className={`h-6 w-6 text-gray-600 dark:text-gray-400 ${isRefetching && "animate-spin"}`} /> 143 + <RefreshIcon 144 + className={`h-6 w-6 text-gray-600 dark:text-gray-400 ${isRefetching && "animate-spin"}`} 145 + /> 120 146 </button> 121 147 </> 122 148 ); ··· 139 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" 140 166 ></path> 141 167 </svg> 142 - ); 168 + );
+78 -34
src/components/Login.tsx
··· 1 1 // src/components/Login.tsx 2 - import { Agent } from "@atproto/api"; 2 + import AtpAgent, { Agent } from "@atproto/api"; 3 + import { useAtom } from "jotai"; 3 4 import React, { useEffect, useRef, useState } from "react"; 4 5 5 6 import { useAuth } from "~/providers/UnifiedAuthProvider"; 7 + import { imgCDNAtom } from "~/utils/atoms"; 8 + import { useQueryIdentity, useQueryProfile } from "~/utils/useQuery"; 6 9 7 10 // --- 1. The Main Component (Orchestrator with `compact` prop) --- 8 11 export default function Login({ ··· 21 24 className={ 22 25 compact 23 26 ? "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]" 27 + : "p-6 bg-gray-100 dark:bg-gray-900 rounded-xl shadow border border-gray-200 dark:border-gray-800 mt-4 mx-4 flex justify-center items-center h-[280px]" 25 28 } 26 29 > 27 30 <span ··· 40 43 // Large view 41 44 if (!compact) { 42 45 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"> 46 + <div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800 mt-4 mx-4"> 44 47 <div className="flex flex-col items-center justify-center text-center"> 45 48 <p className="text-lg font-semibold mb-4 text-gray-800 dark:text-gray-100"> 46 49 You are logged in! ··· 74 77 if (!compact) { 75 78 // Large view renders the form directly in the card 76 79 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"> 80 + <div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800 mt-4 mx-4"> 78 81 <UnifiedLoginForm /> 79 82 </div> 80 83 ); ··· 110 113 // --- 3. Helper components for layouts, forms, and UI --- 111 114 112 115 // A new component to contain the logic for the compact dropdown 113 - const CompactLoginButton = ({popup}:{popup?: boolean}) => { 116 + const CompactLoginButton = ({ popup }: { popup?: boolean }) => { 114 117 const [showForm, setShowForm] = useState(false); 115 118 const formRef = useRef<HTMLDivElement>(null); 116 119 ··· 137 140 Log in 138 141 </button> 139 142 {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`}> 143 + <div 144 + className={`absolute ${popup ? `bottom-[calc(100%)]` : `top-full`} right-0 mt-2 w-80 bg-white dark:bg-gray-900 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-4 z-50`} 145 + > 141 146 <UnifiedLoginForm /> 142 147 </div> 143 148 )} ··· 158 163 onClick={onClick} 159 164 className={`px-4 py-2 text-sm font-medium transition-colors rounded-full flex-1 ${ 160 165 active 161 - ? "text-gray-950 dark:text-gray-200 border-gray-500 bg-gray-400 dark:bg-gray-500" 166 + ? "text-gray-50 dark:text-gray-200 border-gray-500 bg-gray-400 dark:bg-gray-500" 162 167 : "text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200" 163 168 }`} 164 169 > ··· 187 192 <p className="text-xs text-gray-500 dark:text-gray-400"> 188 193 Sign in with AT. Your password is never shared. 189 194 </p> 190 - <input 195 + {/* <input 191 196 type="text" 192 197 placeholder="handle.bsky.social" 193 198 value={handle} 194 199 onChange={(e) => setHandle(e.target.value)} 195 200 className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" 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> 201 + /> */} 202 + <div className="flex flex-col gap-3"> 203 + <div className="m3input-field m3input-label m3input-border size-md flex-1"> 204 + <input 205 + type="text" 206 + placeholder=" " 207 + value={handle} 208 + onChange={(e) => setHandle(e.target.value)} 209 + /> 210 + <label>AT Handle</label> 211 + </div> 212 + <button 213 + type="submit" 214 + className="bg-gray-600 hover:bg-gray-700 text-white rounded-full px-4 py-2 font-medium text-sm transition-colors" 215 + > 216 + Log in 217 + </button> 218 + </div> 203 219 </form> 204 220 ); 205 221 }; ··· 232 248 <p className="text-xs text-red-500 dark:text-red-400"> 233 249 Warning: Less secure. Use an App Password. 234 250 </p> 235 - <input 251 + {/* <input 236 252 type="text" 237 253 placeholder="handle.bsky.social" 238 254 value={user} ··· 254 270 value={serviceURL} 255 271 onChange={(e) => setServiceURL(e.target.value)} 256 272 className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" 257 - /> 273 + /> */} 274 + <div className="m3input-field m3input-label m3input-border size-md flex-1"> 275 + <input 276 + type="text" 277 + placeholder=" " 278 + value={user} 279 + onChange={(e) => setUser(e.target.value)} 280 + /> 281 + <label>AT Handle</label> 282 + </div> 283 + <div className="m3input-field m3input-label m3input-border size-md flex-1"> 284 + <input 285 + type="text" 286 + placeholder=" " 287 + value={password} 288 + onChange={(e) => setPassword(e.target.value)} 289 + /> 290 + <label>App Password</label> 291 + </div> 292 + <div className="m3input-field m3input-label m3input-border size-md flex-1"> 293 + <input 294 + type="text" 295 + placeholder=" " 296 + value={serviceURL} 297 + onChange={(e) => setServiceURL(e.target.value)} 298 + /> 299 + <label>PDS</label> 300 + </div> 258 301 {error && <p className="text-xs text-red-500">{error}</p>} 259 302 <button 260 303 type="submit" ··· 274 317 agent: Agent | null; 275 318 large?: boolean; 276 319 }) => { 277 - const [profile, setProfile] = useState<any>(null); 320 + const did = ((agent as AtpAgent)?.session?.did ?? 321 + (agent as AtpAgent)?.assertDid ?? 322 + agent?.did) as string | undefined; 323 + const { data: identity } = useQueryIdentity(did); 324 + const { data: profiledata } = useQueryProfile( 325 + `at://${did}/app.bsky.actor.profile/self` 326 + ); 327 + const profile = profiledata?.value; 278 328 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]); 329 + const [imgcdn] = useAtom(imgCDNAtom) 292 330 293 - if (!profile) { 331 + function getAvatarUrl(p: typeof profile) { 332 + const link = p?.avatar?.ref?.["$link"]; 333 + if (!link || !did) return null; 334 + return `https://${imgcdn}/img/avatar/plain/${did}/${link}@jpeg`; 335 + } 336 + 337 + if (!profiledata) { 294 338 return ( 295 339 // Skeleton loader 296 340 <div ··· 316 360 className={`flex flex-row items-center gap-2.5 ${large ? "mb-1" : ""}`} 317 361 > 318 362 <img 319 - src={profile?.avatar} 363 + src={getAvatarUrl(profile) ?? undefined} 320 364 alt="avatar" 321 365 className={`object-cover rounded-full ${large ? "w-10 h-10" : "w-[30px] h-[30px]"}`} 322 366 /> ··· 329 373 <div 330 374 className={` ${large ? "text-gray-500 dark:text-gray-400 text-sm" : "text-gray-500 dark:text-gray-400 text-xs"}`} 331 375 > 332 - @{profile?.handle} 376 + @{identity?.handle} 333 377 </div> 334 378 </div> 335 379 </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 + }
+422 -106
src/components/UniversalPostRenderer.tsx
··· 1 1 import { useNavigate } from "@tanstack/react-router"; 2 2 import DOMPurify from "dompurify"; 3 3 import { useAtom } from "jotai"; 4 + import { DropdownMenu } from "radix-ui"; 5 + import { HoverCard } from "radix-ui"; 4 6 import * as React from "react"; 5 7 import { type SVGProps } from "react"; 6 8 7 - import { likedPostsAtom } from "~/utils/atoms"; 9 + import { 10 + composerAtom, 11 + constellationURLAtom, 12 + imgCDNAtom, 13 + likedPostsAtom, 14 + } from "~/utils/atoms"; 8 15 import { useHydratedEmbed } from "~/utils/useHydrated"; 9 16 import { 10 17 useQueryConstellation, 11 18 useQueryIdentity, 12 19 useQueryPost, 13 20 useQueryProfile, 21 + yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks, 14 22 } from "~/utils/useQuery"; 15 23 16 24 function asTyped<T extends { $type: string }>(obj: T): $Typed<T> { ··· 33 41 ref?: React.Ref<HTMLDivElement>; 34 42 dataIndexPropPass?: number; 35 43 nopics?: boolean; 36 - lightboxCallback?: (d:LightboxProps) => void; 44 + lightboxCallback?: (d: LightboxProps) => void; 45 + maxReplies?: number; 46 + isQuote?: boolean; 37 47 } 38 48 39 49 // export async function cachedGetRecord({ ··· 143 153 dataIndexPropPass, 144 154 nopics, 145 155 lightboxCallback, 156 + maxReplies, 157 + isQuote, 146 158 }: UniversalPostRendererATURILoaderProps) { 159 + // todo remove this once tree rendering is implemented, use a prop like isTree 160 + const TEMPLINEAR = true; 147 161 // /*mass comment*/ console.log("atUri", atUri); 148 162 //const { get, set } = usePersistentStore(); 149 163 //const [record, setRecord] = React.useState<any>(null); ··· 388 402 ); 389 403 }, [links]); 390 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 + 391 490 // const navigateToProfile = (e: React.MouseEvent) => { 392 491 // e.stopPropagation(); 393 492 // if (resolved?.did) { ··· 403 502 } 404 503 405 504 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 - lightboxCallback={lightboxCallback} 425 - /> 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 + </> 426 578 ); 427 579 } 428 580 429 - function getAvatarUrl(opProfile: any, did: string) { 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) { 430 616 const link = opProfile?.value?.avatar?.ref?.["$link"]; 431 617 if (!link) return null; 432 - return `https://cdn.bsky.app/img/avatar/plain/${did}/${link}@jpeg`; 618 + return `https://${cdn}/img/avatar/plain/${did}/${link}@jpeg`; 433 619 } 434 620 435 621 export function UniversalPostRendererRawRecordShim({ ··· 451 637 dataIndexPropPass, 452 638 nopics, 453 639 lightboxCallback, 640 + maxReplies, 641 + isQuote, 454 642 }: { 455 643 postRecord: any; 456 644 profileRecord: any; ··· 469 657 ref?: React.Ref<HTMLDivElement>; 470 658 dataIndexPropPass?: number; 471 659 nopics?: boolean; 472 - lightboxCallback?: (d:LightboxProps) => void; 660 + lightboxCallback?: (d: LightboxProps) => void; 661 + maxReplies?: number; 662 + isQuote?: boolean; 473 663 }) { 474 664 // /*mass comment*/ console.log(`received aturi: ${aturi} of post content: ${postRecord}`); 475 665 const navigate = useNavigate(); ··· 546 736 error: embedError, 547 737 } = useHydratedEmbed(postRecord?.value?.embed, resolved?.did); 548 738 739 + const [imgcdn] = useAtom(imgCDNAtom); 740 + 549 741 const parsedaturi = new AtUri(aturi); //parseAtUri(aturi); 550 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 + 551 766 const fakepost = React.useMemo<AppBskyFeedDefs.PostView>( 552 767 () => ({ 553 768 $type: "app.bsky.feed.defs#postView", 554 769 uri: aturi, 555 770 cid: postRecord?.cid || "", 556 - author: { 557 - did: resolved?.did || "", 558 - handle: resolved?.handle || "", 559 - displayName: profileRecord?.value?.displayName || "", 560 - avatar: getAvatarUrl(profileRecord, resolved?.did) || "", 561 - viewer: undefined, 562 - labels: profileRecord?.labels || undefined, 563 - verification: undefined, 564 - }, 771 + author: fakeprofileviewbasic, 565 772 record: postRecord?.value || {}, 566 773 embed: hydratedEmbed ?? undefined, 567 774 replyCount: repliesCount ?? 0, ··· 578 785 postRecord?.cid, 579 786 postRecord?.value, 580 787 postRecord?.labels, 581 - resolved?.did, 582 - resolved?.handle, 583 - profileRecord, 788 + fakeprofileviewbasic, 584 789 hydratedEmbed, 585 790 repliesCount, 586 791 repostsCount, ··· 657 862 } 658 863 }} 659 864 post={fakepost} 865 + uprrrsauthor={fakeprofileviewdetailed} 660 866 salt={aturi} 661 867 bottomReplyLine={bottomReplyLine} 662 868 topReplyLine={topReplyLine} ··· 669 875 dataIndexPropPass={dataIndexPropPass} 670 876 nopics={nopics} 671 877 lightboxCallback={lightboxCallback} 878 + maxReplies={maxReplies} 879 + isQuote={isQuote} 672 880 /> 673 881 </> 674 882 ); ··· 707 915 {...props} 708 916 > 709 917 <path 710 - fill="oklch(0.704 0.05 28)" 918 + fill="var(--color-gray-400)" 711 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" 712 920 ></path> 713 921 </svg> ··· 724 932 {...props} 725 933 > 726 934 <path 727 - fill="oklch(0.704 0.05 28)" 935 + fill="var(--color-gray-400)" 728 936 d="M17 17H7v-3l-4 4l4 4v-3h12v-6h-2M7 7h10v3l4-4l-4-4v3H5v6h2z" 729 937 ></path> 730 938 </svg> ··· 775 983 {...props} 776 984 > 777 985 <path 778 - fill="oklch(0.704 0.05 28)" 986 + fill="var(--color-gray-400)" 779 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" 780 988 ></path> 781 989 </svg> ··· 792 1000 {...props} 793 1001 > 794 1002 <path 795 - fill="oklch(0.704 0.05 28)" 1003 + fill="var(--color-gray-400)" 796 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" 797 1005 ></path> 798 1006 </svg> ··· 809 1017 {...props} 810 1018 > 811 1019 <path 812 - fill="oklch(0.704 0.05 28)" 1020 + fill="var(--color-gray-400)" 813 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" 814 1022 ></path> 815 1023 </svg> ··· 826 1034 {...props} 827 1035 > 828 1036 <path 829 - fill="oklch(0.704 0.05 28)" 1037 + fill="var(--color-gray-400)" 830 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" 831 1039 ></path> 832 1040 </svg> ··· 860 1068 {...props} 861 1069 > 862 1070 <path 863 - fill="oklch(0.704 0.05 28)" 1071 + fill="var(--color-gray-400)" 864 1072 d="M10 9V5l-7 7l7 7v-4.1c5 0 8.5 1.6 11 5.1c-1-5-4-10-11-11" 865 1073 ></path> 866 1074 </svg> ··· 914 1122 {...props} 915 1123 > 916 1124 <path 917 - fill="oklch(0.704 0.05 28)" 1125 + fill="var(--color-gray-400)" 918 1126 d="M17 17H7v-3l-4 4l4 4v-3h12v-6h-2M7 7h10v3l4-4l-4-4v3H5v6h2z" 919 1127 ></path> 920 1128 </svg> ··· 931 1139 {...props} 932 1140 > 933 1141 <path 934 - fill="oklch(0.704 0.05 28)" 1142 + fill="var(--color-gray-400)" 935 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" 936 1144 ></path> 937 1145 </svg> ··· 959 1167 //import Masonry from "@mui/lab/Masonry"; 960 1168 import { 961 1169 type $Typed, 1170 + AppBskyActorDefs, 962 1171 AppBskyEmbedDefs, 963 1172 AppBskyEmbedExternal, 964 1173 AppBskyEmbedImages, ··· 982 1191 PostView, 983 1192 //ThreadViewPost, 984 1193 } from "@atproto/api/dist/client/types/app/bsky/feed/defs"; 1194 + import { useInfiniteQuery } from "@tanstack/react-query"; 985 1195 import { useEffect, useRef, useState } from "react"; 986 1196 import ReactPlayer from "react-player"; 987 1197 988 1198 import defaultpfp from "~/../public/favicon.png"; 989 1199 import { useAuth } from "~/providers/UnifiedAuthProvider"; 1200 + import { FollowButton, Mutual } from "~/routes/profile.$did"; 990 1201 import type { LightboxProps } from "~/routes/profile.$did/post.$rkey.image.$i"; 991 1202 // import type { OutputSchema } from "@atproto/api/dist/client/types/app/bsky/feed/getFeed"; 992 1203 // import type { ··· 1095 1306 1096 1307 function UniversalPostRenderer({ 1097 1308 post, 1309 + uprrrsauthor, 1098 1310 //setMainItem, 1099 1311 //isMainItem, 1100 1312 onPostClick, ··· 1115 1327 ref, 1116 1328 dataIndexPropPass, 1117 1329 nopics, 1118 - lightboxCallback 1330 + lightboxCallback, 1331 + maxReplies, 1119 1332 }: { 1120 1333 post: PostView; 1334 + uprrrsauthor?: AppBskyActorDefs.ProfileViewDetailed; 1121 1335 // optional for now because i havent ported every use to this yet 1122 1336 // setMainItem?: React.Dispatch< 1123 1337 // React.SetStateAction<AppBskyFeedDefs.FeedViewPost> ··· 1139 1353 ref?: React.Ref<HTMLDivElement>; 1140 1354 dataIndexPropPass?: number; 1141 1355 nopics?: boolean; 1142 - lightboxCallback?: (d:LightboxProps) => void; 1356 + lightboxCallback?: (d: LightboxProps) => void; 1357 + maxReplies?: number; 1143 1358 }) { 1144 1359 const parsed = new AtUri(post.uri); 1145 1360 const navigate = useNavigate(); ··· 1150 1365 const [hasLiked, setHasLiked] = useState<boolean>( 1151 1366 post.uri in likedPosts || post.viewer?.like ? true : false 1152 1367 ); 1368 + const [, setComposerPost] = useAtom(composerAtom); 1153 1369 const { agent } = useAuth(); 1154 1370 const [likeUri, setLikeUri] = useState<string | undefined>(post.viewer?.like); 1155 1371 const [retweetUri, setRetweetUri] = useState<string | undefined>( ··· 1246 1462 paddingLeft: isQuote ? 12 : 16, 1247 1463 paddingRight: isQuote ? 12 : 16, 1248 1464 //paddingTop: 16, 1249 - paddingTop: isRepost ? 10 : isQuote ? 12 : 16, 1465 + paddingTop: isRepost ? 10 : isQuote ? 12 : topReplyLine ? 8 : 16, 1250 1466 //paddingBottom: bottomReplyLine ? 0 : 16, 1251 1467 paddingBottom: 0, 1252 1468 fontFamily: "system-ui, sans-serif", ··· 1287 1503 //left: 16 + (42 / 2), 1288 1504 width: 2, 1289 1505 //height: "100%", 1290 - height: isRepost ? "calc(16px + 1rem - 6px)" : 16 - 6, 1506 + height: isRepost 1507 + ? "calc(16px + 1rem - 6px)" 1508 + : topReplyLine 1509 + ? 8 - 6 1510 + : 16 - 6, 1291 1511 // background: theme.textSecondary, 1292 1512 //opacity: 0.5, 1293 1513 // no flex here ··· 1295 1515 className="bg-gray-500 dark:bg-gray-400" 1296 1516 /> 1297 1517 )} 1298 - <div 1299 - style={{ 1300 - position: "absolute", 1301 - //top: isRepost ? "calc(16px + 1rem)" : 16, 1302 - //left: 16, 1303 - zIndex: 1, 1304 - top: isRepost ? "calc(16px + 1rem)" : isQuote ? 12 : 16, 1305 - left: isQuote ? 12 : 16, 1306 - }} 1307 - onClick={onProfileClick} 1308 - > 1309 - <img 1310 - src={post.author.avatar || defaultpfp} 1311 - alt="avatar" 1312 - // transition={{ 1313 - // type: "spring", 1314 - // stiffness: 260, 1315 - // damping: 20, 1316 - // }} 1317 - style={{ 1318 - borderRadius: "50%", 1319 - marginRight: 12, 1320 - objectFit: "cover", 1321 - //background: theme.border, 1322 - //border: `1px solid ${theme.border}`, 1323 - width: isQuote ? 16 : 42, 1324 - height: isQuote ? 16 : 42, 1325 - }} 1326 - className="border border-gray-300 dark:border-gray-800 bg-gray-300 dark:bg-gray-600" 1327 - /> 1328 - </div> 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 + <FollowButton targetdidorhandle={post.author.did} /> 1561 + </div> 1562 + </div> 1563 + <div className="flex flex-col gap-3"> 1564 + <div> 1565 + <div className="text-gray-900 dark:text-gray-100 font-medium text-md"> 1566 + {post.author.displayName || post.author.handle}{" "} 1567 + </div> 1568 + <div className="text-gray-500 dark:text-gray-400 text-md flex flex-row gap-1"> 1569 + <Mutual targetdidorhandle={post.author.did} />@{post.author.handle}{" "} 1570 + </div> 1571 + </div> 1572 + {uprrrsauthor?.description && ( 1573 + <div className="text-gray-700 dark:text-gray-300 text-sm text-left break-words line-clamp-3"> 1574 + {uprrrsauthor.description} 1575 + </div> 1576 + )} 1577 + {/* <div className="flex gap-4"> 1578 + <div className="flex gap-1"> 1579 + <div className="font-medium text-gray-900 dark:text-gray-100"> 1580 + 0 1581 + </div> 1582 + <div className="text-gray-500 dark:text-gray-400"> 1583 + Following 1584 + </div> 1585 + </div> 1586 + <div className="flex gap-1"> 1587 + <div className="font-medium text-gray-900 dark:text-gray-100"> 1588 + 2,900 1589 + </div> 1590 + <div className="text-gray-500 dark:text-gray-400"> 1591 + Followers 1592 + </div> 1593 + </div> 1594 + </div> */} 1595 + </div> 1596 + </div> 1597 + 1598 + {/* <HoverCard.Arrow className="fill-gray-50 dark:fill-gray-900" /> */} 1599 + </HoverCard.Content> 1600 + </HoverCard.Portal> 1601 + </HoverCard.Root> 1602 + 1329 1603 <div style={{ display: "flex", alignItems: "flex-start", zIndex: 2 }}> 1330 1604 <div 1331 1605 style={{ ··· 1339 1613 }} 1340 1614 > 1341 1615 {/* dummy for later use */} 1342 - <div style={{ width: 42, height: 42 + 8, minHeight: 42 + 8 }} /> 1616 + <div style={{ width: 42, height: 42 + 6, minHeight: 42 + 6 }} /> 1343 1617 {/* reply line !!!! bottomReplyLine */} 1344 1618 {bottomReplyLine && ( 1345 1619 <div ··· 1496 1770 > 1497 1771 {fedi ? ( 1498 1772 <> 1499 - <span className="dangerousFediContent" 1773 + <span 1774 + className="dangerousFediContent" 1500 1775 dangerouslySetInnerHTML={{ 1501 1776 __html: DOMPurify.sanitize(fedi), 1502 1777 }} ··· 1571 1846 }} 1572 1847 className="text-gray-500 dark:text-gray-400" 1573 1848 > 1574 - <span style={btnstyle}> 1575 - <MdiCommentOutline /> 1576 - {post.replyCount} 1577 - </span> 1578 1849 <HitSlopButton 1579 1850 onClick={() => { 1580 - repostOrUnrepostPost(); 1851 + setComposerPost({ kind: "reply", parent: post.uri }); 1581 1852 }} 1582 1853 style={{ 1583 1854 ...btnstyle, 1584 - ...(hasRetweeted ? { color: "#5CEFAA" } : {}), 1585 1855 }} 1586 1856 > 1587 - {hasRetweeted ? <MdiRepeatGreen /> : <MdiRepeat />} 1588 - {(post.repostCount || 0) + (hasRetweeted ? 1 : 0)} 1857 + <MdiCommentOutline /> 1858 + {post.replyCount} 1589 1859 </HitSlopButton> 1860 + <DropdownMenu.Root modal={false}> 1861 + <DropdownMenu.Trigger asChild> 1862 + <div 1863 + style={{ 1864 + ...btnstyle, 1865 + ...(hasRetweeted ? { color: "#5CEFAA" } : {}), 1866 + }} 1867 + aria-label="Repost or quote post" 1868 + > 1869 + {hasRetweeted ? <MdiRepeatGreen /> : <MdiRepeat />} 1870 + {post.repostCount ?? 0} 1871 + </div> 1872 + </DropdownMenu.Trigger> 1873 + 1874 + <DropdownMenu.Portal> 1875 + <DropdownMenu.Content 1876 + align="start" 1877 + sideOffset={5} 1878 + 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" 1879 + > 1880 + <DropdownMenu.Item 1881 + onSelect={repostOrUnrepostPost} 1882 + 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" 1883 + > 1884 + <MdiRepeat 1885 + className={hasRetweeted ? "text-green-400" : ""} 1886 + /> 1887 + <span>{hasRetweeted ? "Undo Repost" : "Repost"}</span> 1888 + </DropdownMenu.Item> 1889 + 1890 + <DropdownMenu.Item 1891 + onSelect={() => { 1892 + setComposerPost({ 1893 + kind: "quote", 1894 + subject: post.uri, 1895 + }); 1896 + }} 1897 + 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" 1898 + > 1899 + {/* You might want a specific quote icon here */} 1900 + <MdiCommentOutline /> 1901 + <span>Quote</span> 1902 + </DropdownMenu.Item> 1903 + </DropdownMenu.Content> 1904 + </DropdownMenu.Portal> 1905 + </DropdownMenu.Root> 1590 1906 <HitSlopButton 1591 1907 onClick={() => { 1592 1908 likeOrUnlikePost(); ··· 1728 2044 navigate, 1729 2045 postid, 1730 2046 nopics, 1731 - lightboxCallback 2047 + lightboxCallback, 1732 2048 }: { 1733 2049 embed?: Embed; 1734 2050 moderation?: ModerationDecision; ··· 1739 2055 navigate: (_: any) => void; 1740 2056 postid?: { did: string; rkey: string }; 1741 2057 nopics?: boolean; 1742 - lightboxCallback?: (d:LightboxProps) => void; 2058 + lightboxCallback?: (d: LightboxProps) => void; 1743 2059 }) { 1744 2060 //const [lightboxIndex, setLightboxIndex] = useState<number | null>(null); 1745 - function setLightboxIndex(number:number) { 2061 + function setLightboxIndex(number: number) { 1746 2062 navigate({ 1747 - to: "/profile/$did/post/$rkey/image/$i", 1748 - params: { 1749 - did: postid?.did, 1750 - rkey: postid?.rkey, 1751 - i: number.toString(), 1752 - }, 1753 - }); 2063 + to: "/profile/$did/post/$rkey/image/$i", 2064 + params: { 2065 + did: postid?.did, 2066 + rkey: postid?.rkey, 2067 + i: number.toString(), 2068 + }, 2069 + }); 1754 2070 } 1755 2071 if ( 1756 2072 AppBskyEmbedRecordWithMedia.isView(embed) && ··· 1962 2278 src: img.fullsize, 1963 2279 alt: img.alt, 1964 2280 })); 1965 - console.log("rendering images") 2281 + console.log("rendering images"); 1966 2282 if (lightboxCallback) { 1967 - lightboxCallback({images: lightboxImages}) 1968 - console.log("rendering images") 1969 - }; 2283 + lightboxCallback({ images: lightboxImages }); 2284 + console.log("rendering images"); 2285 + } 1970 2286 1971 2287 if (nopics) return; 1972 2288 ··· 2339 2655 return { start, end, feature: f.features[0] }; 2340 2656 }); 2341 2657 } 2342 - function renderTextWithFacets({ 2658 + export function renderTextWithFacets({ 2343 2659 text, 2344 2660 facets, 2345 2661 navigate,
+2
src/main.tsx
··· 14 14 import { routeTree } from "./routeTree.gen"; 15 15 import { isAtTopAtom } from "./utils/atoms.ts"; 16 16 17 + //initAtomToCssVar(hueAtom, "--tw-gray-hue") 18 + 17 19 const queryClient = new QueryClient({ 18 20 defaultOptions: { 19 21 queries: {
+84 -130
src/routes/__root.tsx
··· 12 12 useNavigate, 13 13 } from "@tanstack/react-router"; 14 14 import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; 15 - import { useState } from "react"; 15 + import { useAtom } from "jotai"; 16 16 import * as React from "react"; 17 17 import { KeepAliveOutlet, KeepAliveProvider } from "tanstack-router-keepalive"; 18 18 19 + import { Composer } from "~/components/Composer"; 19 20 import { DefaultCatchBoundary } from "~/components/DefaultCatchBoundary"; 21 + import { Import } from "~/components/Import"; 20 22 import Login from "~/components/Login"; 21 23 import { NotFound } from "~/components/NotFound"; 24 + import { FluentEmojiHighContrastGlowingStar } from "~/components/Star"; 22 25 import { UnifiedAuthProvider, useAuth } from "~/providers/UnifiedAuthProvider"; 26 + import { composerAtom, hueAtom, useAtomCssVar } from "~/utils/atoms"; 23 27 import { seo } from "~/utils/seo"; 24 28 25 29 export const Route = createRootRouteWithContext<{ ··· 85 89 } 86 90 87 91 function RootDocument({ children }: { children: React.ReactNode }) { 92 + useAtomCssVar(hueAtom, "--tw-gray-hue"); 88 93 const location = useLocation(); 89 94 const navigate = useNavigate(); 90 95 const { agent } = useAuth(); ··· 95 100 agent && 96 101 (location.pathname === `/profile/${agent?.did}` || 97 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"); 98 106 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); 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"; 104 124 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 - } 125 + const [, setComposerPost] = useAtom(composerAtom); 131 126 132 127 return ( 133 128 <> 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 - )} 129 + <Composer /> 177 130 178 131 <div className="min-h-screen flex justify-center bg-gray-50 dark:bg-gray-950"> 179 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"> 180 133 <div className="flex items-center gap-3 mb-4"> 181 - <img src="/redstar.png" alt="Red Dwarf Logo" className="w-8 h-8" /> 134 + <FluentEmojiHighContrastGlowingStar className="h-8 w-8" style={{color: "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))"}} /> 182 135 <span className="font-extrabold text-2xl tracking-tight text-gray-900 dark:text-gray-100"> 183 136 Red Dwarf{" "} 184 137 {/* <span className="text-gray-500 dark:text-gray-400 text-sm"> ··· 191 144 <IconMaterialSymbolsHomeOutline className="w-6 h-6" /> 192 145 } 193 146 ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />} 194 - active={isHome} 147 + active={locationEnum === "home"} 195 148 onClickCallbback={() => 196 149 navigate({ 197 150 to: "/", ··· 202 155 /> 203 156 204 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 205 170 InactiveIcon={ 206 171 <IconMaterialSymbolsNotificationsOutline className="w-6 h-6" /> 207 172 } 208 173 ActiveIcon={ 209 174 <IconMaterialSymbolsNotifications className="w-6 h-6" /> 210 175 } 211 - active={isNotifications} 176 + active={locationEnum === "notifications"} 212 177 onClickCallbback={() => 213 178 navigate({ 214 179 to: "/notifications", ··· 220 185 <MaterialNavItem 221 186 InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 222 187 ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 223 - active={location.pathname.startsWith("/feeds")} 188 + active={locationEnum === "feeds"} 224 189 onClickCallbback={() => 225 190 navigate({ 226 191 to: "/feeds", ··· 230 195 text="Feeds" 231 196 /> 232 197 <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 198 InactiveIcon={ 246 199 <IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" /> 247 200 } 248 201 ActiveIcon={ 249 202 <IconMaterialSymbolsAccountCircle className="w-6 h-6" /> 250 203 } 251 - active={isProfile ?? false} 204 + active={locationEnum === "profile"} 252 205 onClickCallbback={() => { 253 206 if (authed && agent && agent.assertDid) { 254 207 //window.location.href = `/profile/${agent.assertDid}`; ··· 265 218 <IconMaterialSymbolsSettingsOutline className="w-6 h-6" /> 266 219 } 267 220 ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />} 268 - active={location.pathname.startsWith("/settings")} 221 + active={locationEnum === "settings"} 269 222 onClickCallbback={() => 270 223 navigate({ 271 224 to: "/settings", ··· 279 232 InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 280 233 ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 281 234 //active={true} 282 - onClickCallbback={() => setPostOpen(true)} 235 + onClickCallbback={() => setComposerPost({ kind: 'root' })} 283 236 text="Post" 284 237 /> 285 238 </div> ··· 417 370 418 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"> 419 372 <div className="flex items-center gap-3 mb-4"> 420 - <img src="/redstar.png" alt="Red Dwarf Logo" className="w-8 h-8" /> 373 + <FluentEmojiHighContrastGlowingStar className="h-8 w-8" style={{color: "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))"}} /> 421 374 </div> 422 375 <MaterialNavItem 423 376 small ··· 425 378 <IconMaterialSymbolsHomeOutline className="w-6 h-6" /> 426 379 } 427 380 ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />} 428 - active={isHome} 381 + active={locationEnum === "home"} 429 382 onClickCallbback={() => 430 383 navigate({ 431 384 to: "/", ··· 437 390 438 391 <MaterialNavItem 439 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 440 406 InactiveIcon={ 441 407 <IconMaterialSymbolsNotificationsOutline className="w-6 h-6" /> 442 408 } 443 409 ActiveIcon={ 444 410 <IconMaterialSymbolsNotifications className="w-6 h-6" /> 445 411 } 446 - active={isNotifications} 412 + active={locationEnum === "notifications"} 447 413 onClickCallbback={() => 448 414 navigate({ 449 415 to: "/notifications", ··· 456 422 small 457 423 InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 458 424 ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 459 - active={location.pathname.startsWith("/feeds")} 425 + active={locationEnum === "feeds"} 460 426 onClickCallbback={() => 461 427 navigate({ 462 428 to: "/feeds", ··· 464 430 }) 465 431 } 466 432 text="Feeds" 467 - /> 468 - <MaterialNavItem 469 - small 470 - InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 471 - ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 472 - active={location.pathname.startsWith("/search")} 473 - onClickCallbback={() => 474 - navigate({ 475 - to: "/search", 476 - //params: { did: agent.assertDid }, 477 - }) 478 - } 479 - text="Search" 480 433 /> 481 434 <MaterialNavItem 482 435 small ··· 486 439 ActiveIcon={ 487 440 <IconMaterialSymbolsAccountCircle className="w-6 h-6" /> 488 441 } 489 - active={isProfile ?? false} 442 + active={locationEnum === "profile"} 490 443 onClickCallbback={() => { 491 444 if (authed && agent && agent.assertDid) { 492 445 //window.location.href = `/profile/${agent.assertDid}`; ··· 504 457 <IconMaterialSymbolsSettingsOutline className="w-6 h-6" /> 505 458 } 506 459 ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />} 507 - active={location.pathname.startsWith("/settings")} 460 + active={locationEnum === "settings"} 508 461 onClickCallbback={() => 509 462 navigate({ 510 463 to: "/settings", ··· 519 472 InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 520 473 ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 521 474 //active={true} 522 - onClickCallbback={() => setPostOpen(true)} 475 + onClickCallbback={() => setComposerPost({ kind: 'root' })} 523 476 text="Post" 524 477 /> 525 478 </div> ··· 529 482 <button 530 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" 531 484 style={{ boxShadow: "0 4px 24px 0 rgba(0,0,0,0.12)" }} 532 - onClick={() => setPostOpen(true)} 485 + onClick={() => setComposerPost({ kind: 'root' })} 533 486 type="button" 534 487 aria-label="Create Post" 535 488 > ··· 546 499 </main> 547 500 548 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> 549 503 <Login /> 550 504 551 505 <div className="flex-1"></div> 552 506 <p className="text-xs text-gray-400 dark:text-gray-500 text-justify mx-4 mb-4"> 553 - Red Dwarf is a bluesky client that uses Constellation and direct PDS 554 - queries. Skylite would be a self-hosted bluesky "instance". Stay 555 - tuned for the release of Skylite. 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) 556 508 </p> 557 509 </aside> 558 510 </div> ··· 566 518 <IconMaterialSymbolsHomeOutline className="w-6 h-6" /> 567 519 } 568 520 ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />} 569 - active={isHome} 521 + active={locationEnum === "home"} 570 522 onClickCallbback={() => 571 523 navigate({ 572 524 to: "/", ··· 594 546 small 595 547 InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 596 548 ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 597 - active={location.pathname.startsWith("/search")} 549 + active={locationEnum === "search"} 598 550 onClickCallbback={() => 599 551 navigate({ 600 552 to: "/search", 601 553 //params: { did: agent.assertDid }, 602 554 }) 603 555 } 604 - text="Search" 556 + text="Explore" 605 557 /> 606 558 {/* <Link 607 559 to="/search" ··· 626 578 ActiveIcon={ 627 579 <IconMaterialSymbolsNotifications className="w-6 h-6" /> 628 580 } 629 - active={isNotifications} 581 + active={locationEnum === "notifications"} 630 582 onClickCallbback={() => 631 583 navigate({ 632 584 to: "/notifications", ··· 661 613 ActiveIcon={ 662 614 <IconMaterialSymbolsAccountCircle className="w-6 h-6" /> 663 615 } 664 - active={isProfile ?? false} 616 + active={locationEnum === "profile"} 665 617 onClickCallbback={() => { 666 618 if (authed && agent && agent.assertDid) { 667 619 //window.location.href = `/profile/${agent.assertDid}`; ··· 699 651 <IconMaterialSymbolsSettingsOutline className="w-6 h-6" /> 700 652 } 701 653 ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />} 702 - active={location.pathname.startsWith("/settings")} 654 + active={locationEnum === "settings"} 703 655 onClickCallbback={() => 704 656 navigate({ 705 657 to: "/settings", ··· 728 680 ) : ( 729 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"> 730 682 <div className="flex items-center gap-2"> 731 - <img src="/redstar.png" alt="Red Dwarf Logo" className="w-6 h-6" /> 683 + <FluentEmojiHighContrastGlowingStar className="h-6 w-6" style={{color: "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))"}} /> 732 684 <span className="font-bold text-lg text-gray-900 dark:text-gray-100"> 733 685 Red Dwarf{" "} 734 686 {/* <span className="text-gray-500 dark:text-gray-400 text-sm"> ··· 841 793 <div className={`${!small && "mr-2"} ${active ? " " : " "}`}> 842 794 {active ? ActiveIcon : InactiveIcon} 843 795 </div> 844 - {!small && (<span 845 - className={`text-[17px] text-roboto ${active ? "font-medium" : ""}`} 846 - > 847 - {text} 848 - </span>)} 796 + {!small && ( 797 + <span 798 + className={`text-[17px] text-roboto ${active ? "font-medium" : ""}`} 799 + > 800 + {text} 801 + </span> 802 + )} 849 803 </button> 850 804 ); 851 805 }
+1
src/routes/index.tsx
··· 418 418 419 419 {isReadyForAuthedFeed || isReadyForUnauthedFeed ? ( 420 420 <InfiniteCustomFeed 421 + key={selectedFeed!} 421 422 feedUri={selectedFeed!} 422 423 pdsUrl={identity?.pds} 423 424 feedServiceDid={feedServiceDid}
+7 -3
src/routes/notifications.tsx
··· 1 1 import { createFileRoute } from "@tanstack/react-router"; 2 + import { useAtom } from "jotai"; 2 3 import React, { useEffect, useRef,useState } from "react"; 3 4 4 5 import { useAuth } from "~/providers/UnifiedAuthProvider"; 6 + import { constellationURLAtom } from "~/utils/atoms"; 5 7 6 8 const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour 7 9 ··· 70 72 } 71 73 } 72 74 75 + const [constellationURL] = useAtom(constellationURLAtom) 76 + 73 77 useEffect(() => { 74 78 if (!did) return; 75 79 setLoading(true); 76 80 setError(null); 77 81 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`, 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`, 81 85 ]; 82 86 let ignore = false; 83 87 Promise.all(
+194 -56
src/routes/profile.$did/index.tsx
··· 1 + import { RichText } from "@atproto/api"; 1 2 import { useQueryClient } from "@tanstack/react-query"; 2 - import { createFileRoute } from "@tanstack/react-router"; 3 - import React from "react"; 3 + import { createFileRoute, useNavigate } from "@tanstack/react-router"; 4 + import { useAtom } from "jotai"; 5 + import React, { type ReactNode, useEffect, useState } from "react"; 4 6 5 7 import { Header } from "~/components/Header"; 6 - import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer"; 8 + import { 9 + renderTextWithFacets, 10 + UniversalPostRendererATURILoader, 11 + } from "~/components/UniversalPostRenderer"; 7 12 import { useAuth } from "~/providers/UnifiedAuthProvider"; 8 - import { toggleFollow, useGetFollowState } from "~/utils/followState"; 13 + import { aturiListServiceAtom, imgCDNAtom } from "~/utils/atoms"; 9 14 import { 10 - useInfiniteQueryAuthorFeed, 15 + toggleFollow, 16 + useGetFollowState, 17 + useGetOneToOneState, 18 + } from "~/utils/followState"; 19 + import { 20 + useInfiniteQueryAturiList, 11 21 useQueryIdentity, 12 22 useQueryProfile, 13 23 } from "~/utils/useQuery"; ··· 19 29 function ProfileComponent() { 20 30 // booo bad this is not always the did it might be a handle, use identity.did instead 21 31 const { did } = Route.useParams(); 32 + //const navigate = useNavigate(); 22 33 const queryClient = useQueryClient(); 23 - const { agent } = useAuth(); 24 34 const { 25 35 data: identity, 26 36 isLoading: isIdentityLoading, 27 37 error: identityError, 28 38 } = useQueryIdentity(did); 29 39 30 - const followRecords = useGetFollowState({ 31 - target: identity?.did || did, 32 - user: agent?.did, 33 - }); 34 - 35 40 const resolvedDid = did.startsWith("did:") ? did : identity?.did; 36 41 const resolvedHandle = did.startsWith("did:") ? identity?.handle : did; 37 - const pdsUrl = identity?.pds; 42 + //const pdsUrl = identity?.pds; 38 43 39 44 const profileUri = resolvedDid 40 45 ? `at://${resolvedDid}/app.bsky.actor.profile/self` ··· 42 47 const { data: profileRecord } = useQueryProfile(profileUri); 43 48 const profile = profileRecord?.value; 44 49 50 + const [aturilistservice] = useAtom(aturiListServiceAtom); 51 + 45 52 const { 46 53 data: postsData, 47 54 fetchNextPage, 48 55 hasNextPage, 49 56 isFetchingNextPage, 50 57 isLoading: arePostsLoading, 51 - } = useInfiniteQueryAuthorFeed(resolvedDid, pdsUrl); 58 + } = useInfiniteQueryAturiList({ 59 + aturilistservice: aturilistservice, 60 + did: resolvedDid, 61 + collection: "app.bsky.feed.post", 62 + reverse: true 63 + }); 52 64 53 65 React.useEffect(() => { 54 66 if (postsData) { 55 67 postsData.pages.forEach((page) => { 56 - page.records.forEach((record) => { 68 + page.forEach((record) => { 57 69 if (!queryClient.getQueryData(["post", record.uri])) { 58 70 queryClient.setQueryData(["post", record.uri], record); 59 71 } ··· 63 75 }, [postsData, queryClient]); 64 76 65 77 const posts = React.useMemo( 66 - () => postsData?.pages.flatMap((page) => page.records) ?? [], 78 + () => postsData?.pages.flatMap((page) => page) ?? [], 67 79 [postsData] 68 80 ); 81 + 82 + const [imgcdn] = useAtom(imgCDNAtom); 69 83 70 84 function getAvatarUrl(p: typeof profile) { 71 85 const link = p?.avatar?.ref?.["$link"]; 72 86 if (!link || !resolvedDid) return null; 73 - return `https://cdn.bsky.app/img/avatar/plain/${resolvedDid}/${link}@jpeg`; 87 + return `https://${imgcdn}/img/avatar/plain/${resolvedDid}/${link}@jpeg`; 74 88 } 75 89 function getBannerUrl(p: typeof profile) { 76 90 const link = p?.banner?.ref?.["$link"]; 77 91 if (!link || !resolvedDid) return null; 78 - return `https://cdn.bsky.app/img/banner/plain/${resolvedDid}/${link}@jpeg`; 92 + return `https://${imgcdn}/img/banner/plain/${resolvedDid}/${link}@jpeg`; 79 93 } 80 94 81 95 const displayName = ··· 162 176 also delay the backfill to be on demand because it would be pretty intense 163 177 also save it persistently 164 178 */} 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 - )} 179 + <FollowButton targetdidorhandle={did} /> 202 180 <button className="rounded-full dark:bg-gray-600 bg-gray-300 px-3 py-2 text-[14px]"> 203 181 ... {/* todo: icon */} 204 182 </button> ··· 207 185 {/* Info Card */} 208 186 <div className="mt-16 pb-2 px-4 text-gray-900 dark:text-gray-100"> 209 187 <div className="font-bold text-2xl">{displayName}</div> 210 - <div className="text-gray-500 dark:text-gray-400 text-base mb-3"> 188 + <div className="text-gray-500 dark:text-gray-400 text-base mb-3 flex flex-row gap-1"> 189 + <Mutual targetdidorhandle={did} /> 211 190 {handle} 212 191 </div> 213 192 {description && ( 214 193 <div className="text-base leading-relaxed text-gray-800 dark:text-gray-300 mb-5 whitespace-pre-wrap break-words text-[15px]"> 215 - {description} 194 + {/* {description} */} 195 + <RichTextRenderer key={did} description={description} /> 216 196 </div> 217 197 )} 218 198 </div> ··· 255 235 </> 256 236 ); 257 237 } 238 + 239 + export function FollowButton({ 240 + targetdidorhandle, 241 + }: { 242 + targetdidorhandle: string; 243 + }) { 244 + const { agent } = useAuth(); 245 + const { data: identity } = useQueryIdentity(targetdidorhandle); 246 + const queryClient = useQueryClient(); 247 + 248 + const followRecords = useGetFollowState({ 249 + target: identity?.did ?? targetdidorhandle, 250 + user: agent?.did, 251 + }); 252 + 253 + return ( 254 + <> 255 + {identity?.did !== agent?.did ? ( 256 + <> 257 + {!(followRecords?.length && followRecords?.length > 0) ? ( 258 + <button 259 + onClick={(e) => { 260 + e.stopPropagation(); 261 + toggleFollow({ 262 + agent: agent || undefined, 263 + targetDid: identity?.did, 264 + followRecords: followRecords, 265 + queryClient: queryClient, 266 + }); 267 + }} 268 + className="rounded-full h-10 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors px-4 py-2 text-[14px]" 269 + > 270 + Follow 271 + </button> 272 + ) : ( 273 + <button 274 + onClick={(e) => { 275 + e.stopPropagation(); 276 + toggleFollow({ 277 + agent: agent || undefined, 278 + targetDid: identity?.did, 279 + followRecords: followRecords, 280 + queryClient: queryClient, 281 + }); 282 + }} 283 + className="rounded-full h-10 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors px-4 py-2 text-[14px]" 284 + > 285 + Unfollow 286 + </button> 287 + )} 288 + </> 289 + ) : ( 290 + <button className="rounded-full h-10 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors px-4 py-2 text-[14px]"> 291 + Edit Profile 292 + </button> 293 + )} 294 + </> 295 + ); 296 + } 297 + 298 + export function Mutual({ targetdidorhandle }: { targetdidorhandle: string }) { 299 + const { agent } = useAuth(); 300 + const { data: identity } = useQueryIdentity(targetdidorhandle); 301 + 302 + const theyFollowYouRes = useGetOneToOneState( 303 + agent?.did 304 + ? { 305 + target: agent?.did, 306 + user: identity?.did ?? targetdidorhandle, 307 + collection: "app.bsky.graph.follow", 308 + path: ".subject", 309 + } 310 + : undefined 311 + ); 312 + 313 + const youFollowThemRes = useGetFollowState({ 314 + target: identity?.did ?? targetdidorhandle, 315 + user: agent?.did, 316 + }); 317 + 318 + const theyFollowYou: boolean = 319 + !!theyFollowYouRes?.length && theyFollowYouRes.length > 0; 320 + const youFollowThem: boolean = 321 + !!youFollowThemRes?.length && youFollowThemRes.length > 0; 322 + 323 + return ( 324 + <> 325 + {/* if not self */} 326 + {identity?.did !== agent?.did ? ( 327 + <> 328 + {theyFollowYou ? ( 329 + <> 330 + {youFollowThem ? ( 331 + <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"> 332 + mutuals 333 + </div> 334 + ) : ( 335 + <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"> 336 + follows you 337 + </div> 338 + )} 339 + </> 340 + ) : ( 341 + <></> 342 + )} 343 + </> 344 + ) : ( 345 + // lmao can someone be mutuals with themselves ?? 346 + <></> 347 + )} 348 + </> 349 + ); 350 + } 351 + 352 + export function RichTextRenderer({ description }: { description: string }) { 353 + const [richDescription, setRichDescription] = useState<string | ReactNode[]>( 354 + description 355 + ); 356 + const { agent } = useAuth(); 357 + const navigate = useNavigate(); 358 + 359 + useEffect(() => { 360 + let mounted = true; 361 + 362 + // setRichDescription(description); 363 + 364 + async function processRichText() { 365 + try { 366 + if (!agent?.did) return; 367 + const rt = new RichText({ text: description }); 368 + await rt.detectFacets(agent); 369 + 370 + if (!mounted) return; 371 + 372 + if (rt.facets) { 373 + setRichDescription( 374 + renderTextWithFacets({ text: rt.text, facets: rt.facets, navigate }) 375 + ); 376 + } else { 377 + setRichDescription(rt.text); 378 + } 379 + } catch (error) { 380 + console.error("Failed to detect facets:", error); 381 + if (mounted) { 382 + setRichDescription(description); 383 + } 384 + } 385 + } 386 + 387 + processRichText(); 388 + 389 + return () => { 390 + mounted = false; 391 + }; 392 + }, [description, agent, navigate]); 393 + 394 + return <>{richDescription}</>; 395 + }
+1 -1
src/routes/profile.$did/post.$rkey.image.$i.tsx
··· 85 85 e.stopPropagation(); 86 86 e.nativeEvent.stopImmediatePropagation(); 87 87 }} 88 - className="lightbox-sidebar hidden lg:flex 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" 88 + className="lightbox-sidebar hidden lg:flex overscroll-none disablegutter 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 89 > 90 90 <ProfilePostComponent 91 91 key={`/profile/${did}/post/${rkey}`}
+212 -48
src/routes/profile.$did/post.$rkey.tsx
··· 1 - import { useQueryClient } from "@tanstack/react-query"; 1 + import { AtUri } from "@atproto/api"; 2 + import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; 2 3 import { createFileRoute, Outlet } from "@tanstack/react-router"; 4 + import { useAtom } from "jotai"; 3 5 import React, { useLayoutEffect } from "react"; 4 6 5 7 import { Header } from "~/components/Header"; 6 8 import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer"; 9 + import { constellationURLAtom, slingshotURLAtom } from "~/utils/atoms"; 7 10 //import { usePersistentStore } from '~/providers/PersistentStoreProvider'; 8 11 import { 9 12 constructPostQuery, 13 + type linksAllResponse, 14 + type linksRecordsResponse, 10 15 useQueryConstellation, 11 16 useQueryIdentity, 12 17 useQueryPost, 18 + yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks, 13 19 } from "~/utils/useQuery"; 14 20 15 21 import type { LightboxProps } from "./post.$rkey.image.$i"; ··· 39 45 did, 40 46 rkey, 41 47 nopics, 42 - lightboxCallback 48 + lightboxCallback, 43 49 }: { 44 50 did: string; 45 51 rkey: string; 46 52 nopics?: boolean; 47 - lightboxCallback?: (d:LightboxProps) => void; 53 + lightboxCallback?: (d: LightboxProps) => void; 48 54 }) { 49 55 //const { get, set } = usePersistentStore(); 50 56 const queryClient = useQueryClient(); ··· 192 198 () => 193 199 resolvedDid 194 200 ? `at://${decodeURIComponent(resolvedDid)}/app.bsky.feed.post/${rkey}` 195 - : "", 201 + : undefined, 196 202 [resolvedDid, rkey] 197 203 ); 198 204 199 205 const { data: mainPost } = useQueryPost(atUri); 200 206 201 - const { data: repliesData } = useQueryConstellation({ 202 - method: "/links", 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", 203 220 target: atUri, 204 - collection: "app.bsky.feed.post", 205 - path: ".reply.parent.uri", 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, 206 293 }); 207 - const replies = repliesData?.linking_records.slice(0, 50) ?? []; 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 + } 208 364 209 365 const [parents, setParents] = React.useState<any[]>([]); 210 366 const [parentsLoading, setParentsLoading] = React.useState(false); 211 367 212 368 const mainPostRef = React.useRef<HTMLDivElement>(null); 213 - const userHasScrolled = React.useRef(false); 369 + const hasPerformedInitialLayout = React.useRef(false); 214 370 215 - const scrollAnchor = React.useRef<{ top: number } | null>(null); 371 + const [layoutReady, setLayoutReady] = React.useState(false); 216 372 217 - React.useEffect(() => { 218 - const onScroll = () => { 219 - if (window.scrollY > 50) { 220 - userHasScrolled.current = true; 373 + useLayoutEffect(() => { 374 + if (parents.length > 0 && !layoutReady && mainPostRef.current) { 375 + const mainPostElement = mainPostRef.current; 221 376 222 - window.removeEventListener("scroll", onScroll); 223 - } 224 - }; 377 + if (window.scrollY === 0 && !hasPerformedInitialLayout.current) { 378 + const elementTop = mainPostElement.getBoundingClientRect().top; 379 + const headerOffset = 70; 225 380 226 - if (!userHasScrolled.current) { 227 - window.addEventListener("scroll", onScroll, { passive: true }); 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); 228 391 } 229 - return () => window.removeEventListener("scroll", onScroll); 230 - }, []); 392 + }, [parents, layoutReady]); 393 + 231 394 232 - useLayoutEffect(() => { 233 - if (parentsLoading && mainPostRef.current && !userHasScrolled.current) { 234 - scrollAnchor.current = { 235 - top: mainPostRef.current.getBoundingClientRect().top, 236 - }; 395 + const [slingshoturl] = useAtom(slingshotURLAtom) 396 + 397 + React.useEffect(() => { 398 + if (parentsLoading) { 399 + setLayoutReady(false); 237 400 } 238 - }, [parentsLoading]); 239 401 240 - useLayoutEffect(() => { 241 - if ( 242 - scrollAnchor.current && 243 - mainPostRef.current && 244 - !userHasScrolled.current 245 - ) { 246 - const newTop = mainPostRef.current.getBoundingClientRect().top; 247 - const topDiff = newTop - scrollAnchor.current.top; 248 - if (topDiff > 0) { 249 - window.scrollBy(0, topDiff); 250 - } 251 - scrollAnchor.current = null; 402 + if (!mainPost?.value?.reply?.parent?.uri && !parentsLoading) { 403 + setLayoutReady(true); 404 + hasPerformedInitialLayout.current = true; 252 405 } 253 - }, [parents]); 406 + }, [parentsLoading, mainPost]); 254 407 255 408 React.useEffect(() => { 256 409 if (!mainPost?.value?.reply?.parent?.uri) { ··· 269 422 while (currentParentUri && safetyCounter < MAX_PARENTS) { 270 423 try { 271 424 const parentPost = await queryClient.fetchQuery( 272 - constructPostQuery(currentParentUri) 425 + constructPostQuery(currentParentUri, slingshoturl) 273 426 ); 274 427 if (!parentPost) break; 275 428 parentChain.push(parentPost); ··· 351 504 maxWidth: 600, 352 505 //margin: "0px auto 0", 353 506 padding: 0, 354 - minHeight: "100dvh", 507 + minHeight: "80dvh", 508 + paddingBottom: "20dvh", 355 509 }} 356 510 > 357 511 <div ··· 365 519 Replies 366 520 </div> 367 521 <div style={{ display: "flex", flexDirection: "column", gap: 0 }}> 368 - {replies.length > 0 && 369 - replies.map((reply) => { 370 - const replyAtUri = `at://${reply.did}/app.bsky.feed.post/${reply.rkey}`; 522 + {replyAturis.length > 0 && 523 + replyAturis.map((reply) => { 524 + //const replyAtUri = `at://${reply.did}/app.bsky.feed.post/${reply.rkey}`; 371 525 return ( 372 526 <UniversalPostRendererATURILoader 373 - key={replyAtUri} 374 - atUri={replyAtUri} 527 + key={reply} 528 + atUri={reply} 529 + maxReplies={4} 375 530 /> 376 531 ); 377 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 + )} 378 542 </div> 379 543 </div> 380 544 </>
+50 -1
src/routes/search.tsx
··· 1 1 import { createFileRoute } from "@tanstack/react-router"; 2 2 3 + import { Header } from "~/components/Header"; 4 + import { Import } from "~/components/Import"; 5 + 3 6 export const Route = createFileRoute("/search")({ 4 7 component: Search, 5 8 }); 6 9 7 10 export function Search() { 8 - return <div className="p-6">Search page (coming soon)</div>; 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 + ); 9 58 }
+172 -1
src/routes/settings.tsx
··· 1 1 import { createFileRoute } from "@tanstack/react-router"; 2 + import { useAtom } from "jotai"; 3 + import { Slider } from "radix-ui"; 2 4 3 5 import { Header } from "~/components/Header"; 4 6 import Login from "~/components/Login"; 7 + import { 8 + aturiListServiceAtom, 9 + constellationURLAtom, 10 + defaultaturilistservice, 11 + defaultconstellationURL, 12 + defaulthue, 13 + defaultImgCDN, 14 + defaultslingshotURL, 15 + defaultVideoCDN, 16 + hueAtom, 17 + imgCDNAtom, 18 + slingshotURLAtom, 19 + videoCDNAtom, 20 + } from "~/utils/atoms"; 5 21 6 22 export const Route = createFileRoute("/settings")({ 7 23 component: Settings, ··· 20 36 } 21 37 }} 22 38 /> 23 - <Login /> 39 + <div className="lg:hidden"> 40 + <Login /> 41 + </div> 42 + <div className="h-4" /> 43 + <TextInputSetting 44 + atom={constellationURLAtom} 45 + title={"Constellation"} 46 + description={ 47 + "Customize the Constellation instance to be used by Red Dwarf" 48 + } 49 + init={defaultconstellationURL} 50 + /> 51 + <TextInputSetting 52 + atom={slingshotURLAtom} 53 + title={"Slingshot"} 54 + description={"Customize the Slingshot instance to be used by Red Dwarf"} 55 + init={defaultslingshotURL} 56 + /> 57 + <TextInputSetting 58 + atom={aturiListServiceAtom} 59 + title={"AtUriListService"} 60 + description={"Customize the AtUriListService instance to be used by Red Dwarf"} 61 + init={defaultaturilistservice} 62 + /> 63 + <TextInputSetting 64 + atom={imgCDNAtom} 65 + title={"Image CDN"} 66 + description={ 67 + "Customize the Constellation instance to be used by Red Dwarf" 68 + } 69 + init={defaultImgCDN} 70 + /> 71 + <TextInputSetting 72 + atom={videoCDNAtom} 73 + title={"Video CDN"} 74 + description={"Customize the Slingshot instance to be used by Red Dwarf"} 75 + init={defaultVideoCDN} 76 + /> 77 + 78 + <Hue /> 79 + <p className="text-gray-500 dark:text-gray-400 py-4 px-6 text-sm"> 80 + please restart/refresh the app if changes arent applying correctly 81 + </p> 24 82 </> 25 83 ); 26 84 } 85 + function Hue() { 86 + const [hue, setHue] = useAtom(hueAtom); 87 + return ( 88 + <div className="flex flex-col px-4 mt-4 "> 89 + <span className="z-10">Hue</span> 90 + <div className="flex flex-row items-center gap-4"> 91 + <SliderComponent 92 + atom={hueAtom} 93 + max={360} 94 + /> 95 + <button 96 + onClick={() => setHue(defaulthue ?? 28)} 97 + className="px-6 py-2 h-12 rounded-full bg-gray-100 dark:bg-gray-800 98 + text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition" 99 + > 100 + Reset 101 + </button> 102 + </div> 103 + </div> 104 + ); 105 + } 106 + 107 + export function TextInputSetting({ 108 + atom, 109 + title, 110 + description, 111 + init, 112 + }: { 113 + atom: typeof constellationURLAtom; 114 + title?: string; 115 + description?: string; 116 + init?: string; 117 + }) { 118 + const [value, setValue] = useAtom(atom); 119 + return ( 120 + <div className="flex flex-col gap-2 px-4 py-2"> 121 + {/* <div> 122 + {title && ( 123 + <h3 className="text-sm font-medium text-gray-900 dark:text-gray-100"> 124 + {title} 125 + </h3> 126 + )} 127 + {description && ( 128 + <p className="text-sm text-gray-500 dark:text-gray-400"> 129 + {description} 130 + </p> 131 + )} 132 + </div> */} 133 + 134 + <div className="flex flex-row gap-2 items-center"> 135 + <div className="m3input-field m3input-label m3input-border size-md flex-1"> 136 + <input 137 + type="text" 138 + placeholder=" " 139 + value={value} 140 + onChange={(e) => setValue(e.target.value)} 141 + /> 142 + <label>{title}</label> 143 + </div> 144 + {/* <input 145 + type="text" 146 + value={value} 147 + onChange={(e) => setValue(e.target.value)} 148 + className="flex-1 px-3 py-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 149 + text-gray-900 dark:text-gray-100 placeholder:text-gray-500 dark:placeholder:text-gray-400 150 + focus:outline-none focus:ring-2 focus:ring-gray-400 dark:focus:ring-gray-600" 151 + placeholder="Enter value..." 152 + /> */} 153 + <button 154 + onClick={() => setValue(init ?? "")} 155 + className="px-6 py-2 h-12 rounded-full bg-gray-100 dark:bg-gray-800 156 + text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition" 157 + > 158 + Reset 159 + </button> 160 + </div> 161 + </div> 162 + ); 163 + } 164 + 165 + 166 + interface SliderProps { 167 + atom: typeof hueAtom; 168 + min?: number; 169 + max?: number; 170 + step?: number; 171 + } 172 + 173 + export const SliderComponent: React.FC<SliderProps> = ({ 174 + atom, 175 + min = 0, 176 + max = 100, 177 + step = 1, 178 + }) => { 179 + 180 + const [value, setValue] = useAtom(atom) 181 + 182 + return ( 183 + <Slider.Root 184 + className="relative flex items-center w-full h-4" 185 + value={[value]} 186 + min={min} 187 + max={max} 188 + step={step} 189 + onValueChange={(v: number[]) => setValue(v[0])} 190 + > 191 + <Slider.Track className="relative flex-grow h-4 bg-gray-300 dark:bg-gray-700 rounded-full"> 192 + <Slider.Range className="absolute h-full bg-gray-500 dark:bg-gray-400 rounded-l-full rounded-r-none" /> 193 + </Slider.Track> 194 + <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" /> 195 + </Slider.Root> 196 + ); 197 + };
+133 -11
src/styles/app.css
··· 15 15 --color-gray-950: oklch(0.129 0.050 222.000); 16 16 } */ 17 17 18 + :root { 19 + --safe-hue: var(--tw-gray-hue, 28) 20 + } 21 + 18 22 @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); 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)); 30 34 } 31 35 32 36 @layer base { ··· 105 109 :root { 106 110 --shadow-opacity: calc(1 - var(--is-top)); 107 111 --tw-shadow-header: 0 2px 8px hsl(0 0% 0% / calc(var(--shadow-opacity) * 0.15)); 112 + } 113 + 114 + 115 + /* m3 input */ 116 + :root { 117 + --m3input-radius: 6px; 118 + --m3input-border-width: .0625rem; 119 + --m3input-font-size: 16px; 120 + --m3input-transition: 150ms cubic-bezier(.2, .8, .2, 1); 121 + /* light theme */ 122 + --m3input-bg: var(--color-gray-50); 123 + --m3input-border-color: var(--color-gray-400); 124 + --m3input-label-color: var(--color-gray-500); 125 + --m3input-text-color: var(--color-gray-900); 126 + --m3input-focus-color: var(--color-gray-600); 127 + } 128 + 129 + @media (prefers-color-scheme: dark) { 130 + :root { 131 + --m3input-bg: var(--color-gray-950); 132 + --m3input-border-color: var(--color-gray-700); 133 + --m3input-label-color: var(--color-gray-400); 134 + --m3input-text-color: var(--color-gray-50); 135 + --m3input-focus-color: var(--color-gray-400); 136 + } 137 + } 138 + 139 + /* reset page *//* 140 + html, 141 + body { 142 + background: var(--m3input-bg); 143 + margin: 0; 144 + padding: 1rem; 145 + color: var(--m3input-text-color); 146 + font-family: system-ui, sans-serif; 147 + font-size: var(--m3input-font-size); 148 + }*/ 149 + 150 + /* base wrapper */ 151 + .m3input-field.m3input-label.m3input-border { 152 + position: relative; 153 + display: inline-block; 154 + width: 100%; 155 + /*max-width: 400px;*/ 156 + } 157 + 158 + /* size variants */ 159 + .m3input-field.size-sm { 160 + --m3input-h: 40px; 161 + } 162 + 163 + .m3input-field.size-md { 164 + --m3input-h: 48px; 165 + } 166 + 167 + .m3input-field.size-lg { 168 + --m3input-h: 56px; 169 + } 170 + 171 + .m3input-field.size-xl { 172 + --m3input-h: 64px; 173 + } 174 + 175 + .m3input-field.m3input-label.m3input-border:not(.size-sm):not(.size-md):not(.size-lg):not(.size-xl) { 176 + --m3input-h: 48px; 177 + } 178 + 179 + /* outlined input */ 180 + .m3input-field.m3input-label.m3input-border input { 181 + width: 100%; 182 + height: var(--m3input-h); 183 + border: var(--m3input-border-width) solid var(--m3input-border-color); 184 + border-radius: var(--m3input-radius); 185 + background: var(--m3input-bg); 186 + color: var(--m3input-text-color); 187 + font-size: var(--m3input-font-size); 188 + padding: 0 12px; 189 + box-sizing: border-box; 190 + outline: none; 191 + transition: border-color var(--m3input-transition), box-shadow var(--m3input-transition); 192 + } 193 + 194 + /* focus ring */ 195 + .m3input-field.m3input-label.m3input-border input:focus { 196 + border-color: var(--m3input-focus-color); 197 + /*box-shadow: 0 0 0 2px color-mix(in srgb, var(--focus-color) 20%, transparent);*/ 198 + } 199 + 200 + /* label */ 201 + .m3input-field.m3input-label.m3input-border label { 202 + position: absolute; 203 + left: 12px; 204 + top: 50%; 205 + transform: translateY(-50%); 206 + background: var(--m3input-bg); 207 + padding: 0 .25em; 208 + color: var(--m3input-label-color); 209 + pointer-events: none; 210 + transition: all var(--m3input-transition); 211 + } 212 + 213 + /* float on focus or when filled */ 214 + .m3input-field.m3input-label.m3input-border input:focus+label, 215 + .m3input-field.m3input-label.m3input-border input:not(:placeholder-shown)+label { 216 + top: 0; 217 + transform: translateY(-50%) scale(.78); 218 + left: 0; 219 + color: var(--m3input-focus-color); 220 + } 221 + 222 + /* placeholder trick */ 223 + .m3input-field.m3input-label.m3input-border input::placeholder { 224 + color: transparent; 225 + } 226 + 227 + /* radix i love you but like cmon man */ 228 + body[data-scroll-locked]{ 229 + margin-left: var(--removed-body-scroll-bar-size) !important; 108 230 }
+62 -6
src/utils/atoms.ts
··· 1 1 import type Agent from "@atproto/api"; 2 - import { atom, createStore } from "jotai"; 3 - import { atomWithStorage } from 'jotai/utils'; 2 + import { atom, createStore, useAtomValue } from "jotai"; 3 + import { atomWithStorage } from "jotai/utils"; 4 + import { useEffect } from "react"; 4 5 5 6 export const store = createStore(); 6 7 7 8 export const selectedFeedUriAtom = atomWithStorage<string | null>( 8 - 'selectedFeedUri', 9 + "selectedFeedUri", 9 10 null 10 11 ); 11 12 12 13 //export const feedScrollPositionsAtom = atom<Record<string, number>>({}); 13 14 14 15 export const feedScrollPositionsAtom = atomWithStorage<Record<string, number>>( 15 - 'feedscrollpositions', 16 + "feedscrollpositions", 16 17 {} 17 18 ); 18 19 19 20 export const likedPostsAtom = atomWithStorage<Record<string, string>>( 20 - 'likedPosts', 21 + "likedPosts", 21 22 {} 22 23 ); 23 24 25 + export const defaultconstellationURL = "constellation.microcosm.blue"; 26 + export const constellationURLAtom = atomWithStorage<string>( 27 + "constellationURL", 28 + defaultconstellationURL 29 + ); 30 + export const defaultslingshotURL = "slingshot.microcosm.blue"; 31 + export const slingshotURLAtom = atomWithStorage<string>( 32 + "slingshotURL", 33 + defaultslingshotURL 34 + ); 35 + export const defaultaturilistservice = "aturilistservice.reddwarf.app"; 36 + export const aturiListServiceAtom = atomWithStorage<string>( 37 + "aturilistservice", 38 + defaultaturilistservice 39 + ); 40 + export const defaultImgCDN = "cdn.bsky.app"; 41 + export const imgCDNAtom = atomWithStorage<string>("imgcdnurl", defaultImgCDN); 42 + export const defaultVideoCDN = "video.bsky.app"; 43 + export const videoCDNAtom = atomWithStorage<string>( 44 + "videocdnurl", 45 + defaultVideoCDN 46 + ); 47 + 48 + export const defaulthue = 28; 49 + export const hueAtom = atomWithStorage<number>("hue", defaulthue); 50 + 24 51 export const isAtTopAtom = atom<boolean>(true); 25 52 26 - export const agentAtom = atom<Agent|null>(null); 53 + type ComposerState = 54 + | { kind: "closed" } 55 + | { kind: "root" } 56 + | { kind: "reply"; parent: string } 57 + | { kind: "quote"; subject: string }; 58 + export const composerAtom = atom<ComposerState>({ kind: "closed" }); 59 + 60 + export const agentAtom = atom<Agent | null>(null); 27 61 export const authedAtom = atom<boolean>(false); 62 + 63 + export function useAtomCssVar(atom: typeof hueAtom, cssVar: string) { 64 + const value = useAtomValue(atom); 65 + 66 + useEffect(() => { 67 + document.documentElement.style.setProperty(cssVar, value.toString()); 68 + }, [value, cssVar]); 69 + 70 + useEffect(() => { 71 + document.documentElement.style.setProperty(cssVar, value.toString()); 72 + }, []); 73 + } 74 + 75 + hueAtom.onMount = (setAtom) => { 76 + const stored = localStorage.getItem("hue"); 77 + if (stored != null) setAtom(Number(stored)); 78 + }; 79 + // export function initAtomToCssVar(atom: typeof hueAtom, cssVar: string) { 80 + // const initial = store.get(atom); 81 + // console.log("atom get ", initial); 82 + // document.documentElement.style.setProperty(cssVar, initial.toString()); 83 + // }
+37 -3
src/utils/followState.ts
··· 1 - import { AtUri, type Agent } from "@atproto/api"; 2 - import { useQueryConstellation, type linksRecordsResponse } from "./useQuery"; 1 + import { type Agent,AtUri } from "@atproto/api"; 2 + import { TID } from "@atproto/common-web"; 3 3 import type { QueryClient } from "@tanstack/react-query"; 4 - import { TID } from "@atproto/common-web"; 4 + 5 + import { type linksRecordsResponse,useQueryConstellation } from "./useQuery"; 5 6 6 7 export function useGetFollowState({ 7 8 target, ··· 127 128 }; 128 129 }); 129 130 } 131 + 132 + 133 + 134 + export function useGetOneToOneState(params?: { 135 + target: string; 136 + user: string; 137 + collection: string; 138 + path: string; 139 + }): string[] | undefined { 140 + const { data: arbitrarydata } = useQueryConstellation( 141 + params && params.user 142 + ? { 143 + method: "/links", 144 + target: params.target, 145 + // @ts-expect-error overloading sucks so much 146 + collection: params.collection, 147 + path: params.path, 148 + dids: [params.user], 149 + } 150 + : { method: "undefined", target: "whatever" } 151 + // overloading sucks so much 152 + ) as { data: linksRecordsResponse | undefined }; 153 + if (!params || !params.user) return undefined; 154 + const data = arbitrarydata?.linking_records.slice(0, 50) ?? []; 155 + 156 + if (data.length > 0) { 157 + return data.map((linksRecord) => { 158 + return `at://${linksRecord.did}/${linksRecord.collection}/${linksRecord.rkey}`; 159 + }); 160 + } 161 + 162 + return undefined; 163 + }
+53 -23
src/utils/useHydrated.ts
··· 9 9 AppBskyFeedPost, 10 10 AtUri, 11 11 } from "@atproto/api"; 12 + import { useAtom } from "jotai"; 12 13 import { useMemo } from "react"; 13 14 14 - import { useQueryIdentity,useQueryPost, useQueryProfile } from "./useQuery"; 15 + import { imgCDNAtom, videoCDNAtom } from "./atoms"; 16 + import { useQueryIdentity, useQueryPost, useQueryProfile } from "./useQuery"; 15 17 16 - type QueryResultData<T extends (...args: any) => any> = ReturnType<T> extends 17 - | { data: infer D } 18 - | undefined 19 - ? D 20 - : never; 18 + type QueryResultData<T extends (...args: any) => any> = 19 + ReturnType<T> extends { data: infer D } | undefined ? D : never; 21 20 22 21 function asTyped<T extends { $type: string }>(obj: T): $Typed<T> { 23 22 return obj as $Typed<T>; ··· 26 25 export function hydrateEmbedImages( 27 26 embed: AppBskyEmbedImages.Main, 28 27 did: string, 28 + cdn: string 29 29 ): $Typed<AppBskyEmbedImages.View> { 30 30 return asTyped({ 31 31 $type: "app.bsky.embed.images#view" as const, ··· 34 34 const link = img.image.ref?.["$link"]; 35 35 if (!link) return null; 36 36 return { 37 - thumb: `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${link}@jpeg`, 38 - fullsize: `https://cdn.bsky.app/img/feed_fullsize/plain/${did}/${link}@jpeg`, 37 + thumb: `https://${cdn}/img/feed_thumbnail/plain/${did}/${link}@jpeg`, 38 + fullsize: `https://${cdn}/img/feed_fullsize/plain/${did}/${link}@jpeg`, 39 39 alt: img.alt || "", 40 40 aspectRatio: img.aspectRatio, 41 41 }; ··· 47 47 export function hydrateEmbedExternal( 48 48 embed: AppBskyEmbedExternal.Main, 49 49 did: string, 50 + cdn: string 50 51 ): $Typed<AppBskyEmbedExternal.View> { 51 52 return asTyped({ 52 53 $type: "app.bsky.embed.external#view" as const, ··· 55 56 title: embed.external.title, 56 57 description: embed.external.description, 57 58 thumb: embed.external.thumb?.ref?.$link 58 - ? `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${embed.external.thumb.ref.$link}@jpeg` 59 + ? `https://${cdn}/img/feed_thumbnail/plain/${did}/${embed.external.thumb.ref.$link}@jpeg` 59 60 : undefined, 60 61 }, 61 62 }); ··· 64 65 export function hydrateEmbedVideo( 65 66 embed: AppBskyEmbedVideo.Main, 66 67 did: string, 68 + videocdn: string 67 69 ): $Typed<AppBskyEmbedVideo.View> { 68 70 const videoLink = embed.video.ref.$link; 69 71 return asTyped({ 70 72 $type: "app.bsky.embed.video#view" as const, 71 - playlist: `https://video.bsky.app/watch/${did}/${videoLink}/playlist.m3u8`, 72 - thumbnail: `https://video.bsky.app/watch/${did}/${videoLink}/thumbnail.jpg`, 73 + playlist: `https://${videocdn}/watch/${did}/${videoLink}/playlist.m3u8`, 74 + thumbnail: `https://${videocdn}/watch/${did}/${videoLink}/thumbnail.jpg`, 73 75 aspectRatio: embed.aspectRatio, 74 76 cid: videoLink, 75 77 }); ··· 80 82 quotedPost: QueryResultData<typeof useQueryPost>, 81 83 quotedProfile: QueryResultData<typeof useQueryProfile>, 82 84 quotedIdentity: QueryResultData<typeof useQueryIdentity>, 85 + cdn: string 83 86 ): $Typed<AppBskyEmbedRecord.View> | undefined { 84 87 if (!quotedPost || !quotedProfile || !quotedIdentity) { 85 88 return undefined; ··· 91 94 handle: quotedIdentity.handle, 92 95 displayName: quotedProfile.value.displayName ?? quotedIdentity.handle, 93 96 avatar: quotedProfile.value.avatar?.ref?.$link 94 - ? `https://cdn.bsky.app/img/avatar/plain/${quotedIdentity.did}/${quotedProfile.value.avatar.ref.$link}@jpeg` 97 + ? `https://${cdn}/img/avatar/plain/${quotedIdentity.did}/${quotedProfile.value.avatar.ref.$link}@jpeg` 95 98 : undefined, 96 99 viewer: {}, 97 100 labels: [], ··· 122 125 quotedPost: QueryResultData<typeof useQueryPost>, 123 126 quotedProfile: QueryResultData<typeof useQueryProfile>, 124 127 quotedIdentity: QueryResultData<typeof useQueryIdentity>, 128 + cdn: string 125 129 ): $Typed<AppBskyEmbedRecordWithMedia.View> | undefined { 126 130 const hydratedRecord = hydrateEmbedRecord( 127 131 embed.record, 128 132 quotedPost, 129 133 quotedProfile, 130 134 quotedIdentity, 135 + cdn 131 136 ); 132 137 133 138 if (!hydratedRecord) return undefined; ··· 148 153 149 154 export function useHydratedEmbed( 150 155 embed: AppBskyFeedPost.Record["embed"], 151 - postAuthorDid: string | undefined, 156 + postAuthorDid: string | undefined 152 157 ) { 153 158 const recordInfo = useMemo(() => { 154 159 if (AppBskyEmbedRecordWithMedia.isMain(embed)) { ··· 181 186 error: profileError, 182 187 } = useQueryProfile(profileUri); 183 188 189 + const [imgcdn] = useAtom(imgCDNAtom); 190 + const [videocdn] = useAtom(videoCDNAtom); 191 + 184 192 const queryidentityresult = useQueryIdentity(quotedAuthorDid); 185 193 186 194 const hydratedEmbed: HydratedEmbedView | undefined = (() => { 187 195 if (!embed || !postAuthorDid) return undefined; 188 196 189 - if (isRecordType && (!usequerypostresults?.data || !quotedProfile || !queryidentityresult?.data)) { 197 + if ( 198 + isRecordType && 199 + (!usequerypostresults?.data || 200 + !quotedProfile || 201 + !queryidentityresult?.data) 202 + ) { 190 203 return undefined; 191 204 } 192 205 193 206 try { 194 207 if (AppBskyEmbedImages.isMain(embed)) { 195 - return hydrateEmbedImages(embed, postAuthorDid); 208 + return hydrateEmbedImages(embed, postAuthorDid, imgcdn); 196 209 } else if (AppBskyEmbedExternal.isMain(embed)) { 197 - return hydrateEmbedExternal(embed, postAuthorDid); 210 + return hydrateEmbedExternal(embed, postAuthorDid, imgcdn); 198 211 } else if (AppBskyEmbedVideo.isMain(embed)) { 199 - return hydrateEmbedVideo(embed, postAuthorDid); 212 + return hydrateEmbedVideo(embed, postAuthorDid, videocdn); 200 213 } else if (AppBskyEmbedRecord.isMain(embed)) { 201 214 return hydrateEmbedRecord( 202 215 embed, 203 216 usequerypostresults?.data, 204 217 quotedProfile, 205 218 queryidentityresult?.data, 219 + imgcdn 206 220 ); 207 221 } else if (AppBskyEmbedRecordWithMedia.isMain(embed)) { 208 222 let hydratedMedia: ··· 212 226 | undefined; 213 227 214 228 if (AppBskyEmbedImages.isMain(embed.media)) { 215 - hydratedMedia = hydrateEmbedImages(embed.media, postAuthorDid); 229 + hydratedMedia = hydrateEmbedImages( 230 + embed.media, 231 + postAuthorDid, 232 + imgcdn 233 + ); 216 234 } else if (AppBskyEmbedExternal.isMain(embed.media)) { 217 - hydratedMedia = hydrateEmbedExternal(embed.media, postAuthorDid); 235 + hydratedMedia = hydrateEmbedExternal( 236 + embed.media, 237 + postAuthorDid, 238 + imgcdn 239 + ); 218 240 } else if (AppBskyEmbedVideo.isMain(embed.media)) { 219 - hydratedMedia = hydrateEmbedVideo(embed.media, postAuthorDid); 241 + hydratedMedia = hydrateEmbedVideo( 242 + embed.media, 243 + postAuthorDid, 244 + videocdn 245 + ); 220 246 } 221 247 222 248 if (hydratedMedia) { ··· 226 252 usequerypostresults?.data, 227 253 quotedProfile, 228 254 queryidentityresult?.data, 255 + imgcdn 229 256 ); 230 257 } 231 258 } ··· 236 263 })(); 237 264 238 265 const isLoading = isRecordType 239 - ? usequerypostresults?.isLoading || isLoadingProfile || queryidentityresult?.isLoading 266 + ? usequerypostresults?.isLoading || 267 + isLoadingProfile || 268 + queryidentityresult?.isLoading 240 269 : false; 241 270 242 - const error = usequerypostresults?.error || profileError || queryidentityresult?.error; 271 + const error = 272 + usequerypostresults?.error || profileError || queryidentityresult?.error; 243 273 244 274 return { data: hydratedEmbed, isLoading, error }; 245 - } 275 + }
+161 -17
src/utils/useQuery.ts
··· 1 1 import * as ATPAPI from "@atproto/api"; 2 2 import { 3 + infiniteQueryOptions, 3 4 type QueryFunctionContext, 4 5 queryOptions, 5 6 useInfiniteQuery, 6 7 useQuery, 7 8 type UseQueryResult} from "@tanstack/react-query"; 9 + import { useAtom } from "jotai"; 8 10 9 - export function constructIdentityQuery(didorhandle?: string) { 11 + import { constellationURLAtom, slingshotURLAtom } from "./atoms"; 12 + 13 + export function constructIdentityQuery(didorhandle?: string, slingshoturl?: string) { 10 14 return queryOptions({ 11 15 queryKey: ["identity", didorhandle], 12 16 queryFn: async () => { 13 17 if (!didorhandle) return undefined as undefined 14 18 const res = await fetch( 15 - `https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(didorhandle)}` 19 + `https://${slingshoturl}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(didorhandle)}` 16 20 ); 17 21 if (!res.ok) throw new Error("Failed to fetch post"); 18 22 try { ··· 54 58 Error 55 59 > 56 60 export function useQueryIdentity(didorhandle?: string) { 57 - return useQuery(constructIdentityQuery(didorhandle)); 61 + const [slingshoturl] = useAtom(slingshotURLAtom) 62 + return useQuery(constructIdentityQuery(didorhandle, slingshoturl)); 58 63 } 59 64 60 - export function constructPostQuery(uri?: string) { 65 + export function constructPostQuery(uri?: string, slingshoturl?: string) { 61 66 return queryOptions({ 62 67 queryKey: ["post", uri], 63 68 queryFn: async () => { 64 69 if (!uri) return undefined as undefined 65 70 const res = await fetch( 66 - `https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 71 + `https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 67 72 ); 68 73 let data: any; 69 74 try { ··· 117 122 Error 118 123 > 119 124 export function useQueryPost(uri?: string) { 120 - return useQuery(constructPostQuery(uri)); 125 + const [slingshoturl] = useAtom(slingshotURLAtom) 126 + return useQuery(constructPostQuery(uri, slingshoturl)); 121 127 } 122 128 123 - export function constructProfileQuery(uri?: string) { 129 + export function constructProfileQuery(uri?: string, slingshoturl?: string) { 124 130 return queryOptions({ 125 131 queryKey: ["profile", uri], 126 132 queryFn: async () => { 127 133 if (!uri) return undefined as undefined 128 134 const res = await fetch( 129 - `https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 135 + `https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 130 136 ); 131 137 let data: any; 132 138 try { ··· 180 186 Error 181 187 > 182 188 export function useQueryProfile(uri?: string) { 183 - return useQuery(constructProfileQuery(uri)); 189 + const [slingshoturl] = useAtom(slingshotURLAtom) 190 + return useQuery(constructProfileQuery(uri, slingshoturl)); 184 191 } 185 192 186 193 // export function constructConstellationQuery( ··· 216 223 // target: string 217 224 // ): QueryOptions<linksAllResponse, Error>; 218 225 export function constructConstellationQuery(query?:{ 226 + constellation: string, 219 227 method: 220 228 | "/links" 221 229 | "/links/distinct-dids" ··· 249 257 const cursor = query.cursor 250 258 const dids = query?.dids 251 259 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("") : ""}` 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("") : ""}` 253 261 ); 254 262 if (!res.ok) throw new Error("Failed to fetch post"); 255 263 try { ··· 338 346 > 339 347 | undefined { 340 348 //if (!query) return; 349 + const [constellationurl] = useAtom(constellationURLAtom) 341 350 return useQuery( 342 - constructConstellationQuery(query) 351 + constructConstellationQuery(query && {constellation: constellationurl, ...query}) 343 352 ); 344 353 } 345 354 ··· 361 370 type linksCountResponse = { 362 371 total: string; 363 372 }; 364 - type linksAllResponse = { 373 + export type linksAllResponse = { 365 374 links: Record< 366 375 string, 367 376 Record< ··· 444 453 445 454 446 455 447 - export function constructArbitraryQuery(uri?: string) { 456 + export function constructArbitraryQuery(uri?: string, slingshoturl?: string) { 448 457 return queryOptions({ 449 458 queryKey: ["arbitrary", uri], 450 459 queryFn: async () => { 451 460 if (!uri) return undefined as undefined 452 461 const res = await fetch( 453 - `https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 462 + `https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 454 463 ); 455 464 let data: any; 456 465 try { ··· 503 512 Error 504 513 >; 505 514 export function useQueryArbitrary(uri?: string) { 506 - return useQuery(constructArbitraryQuery(uri)); 515 + const [slingshoturl] = useAtom(slingshotURLAtom) 516 + return useQuery(constructArbitraryQuery(uri, slingshoturl)); 507 517 } 508 518 509 519 export function constructFallbackNothingQuery(){ ··· 555 565 }); 556 566 } 557 567 568 + export const ATURI_PAGE_LIMIT = 100; 569 + 570 + export interface AturiDirectoryAturisItem { 571 + uri: string; 572 + cid: string; 573 + rkey: string; 574 + } 575 + 576 + export type AturiDirectoryAturis = AturiDirectoryAturisItem[]; 577 + 578 + export function constructAturiListQuery(aturilistservice: string, did: string, collection: string, reverse?: boolean) { 579 + return queryOptions({ 580 + // A unique key for this query, including all parameters that affect the data. 581 + queryKey: ["aturiList", did, collection, { reverse }], 582 + 583 + // The function that fetches the data. 584 + queryFn: async ({ pageParam }: QueryFunctionContext) => { 585 + const cursor = pageParam as string | undefined; 586 + 587 + // Use URLSearchParams for safe and clean URL construction. 588 + const params = new URLSearchParams({ 589 + did, 590 + collection, 591 + }); 592 + 593 + if (cursor) { 594 + params.set("cursor", cursor); 595 + } 596 + 597 + // Add the reverse parameter if it's true 598 + if (reverse) { 599 + params.set("reverse", "true"); 600 + } 601 + 602 + const url = `https://${aturilistservice}/aturis?${params.toString()}`; 603 + 604 + const res = await fetch(url); 605 + if (!res.ok) { 606 + // You can add more specific error handling here 607 + throw new Error(`Failed to fetch AT-URI list for ${did}`); 608 + } 609 + 610 + return res.json() as Promise<AturiDirectoryAturis>; 611 + }, 612 + }); 613 + } 614 + 615 + export function useInfiniteQueryAturiList({aturilistservice, did, collection, reverse}:{aturilistservice: string, did: string | undefined, collection: string | undefined, reverse?: boolean}) { 616 + // We only enable the query if both `did` and `collection` are provided. 617 + const isEnabled = !!did && !!collection; 618 + 619 + const { queryKey, queryFn } = constructAturiListQuery(aturilistservice, did!, collection!, reverse); 620 + 621 + return useInfiniteQuery({ 622 + queryKey, 623 + queryFn, 624 + initialPageParam: undefined as never, // ???? what is this shit 625 + 626 + // @ts-expect-error i wouldve used as null | undefined, anyways 627 + getNextPageParam: (lastPage: AturiDirectoryAturis) => { 628 + // If the last page returned no records, we're at the end. 629 + if (!lastPage || lastPage.length === 0) { 630 + return undefined; 631 + } 632 + 633 + // If the number of records is less than our page limit, it must be the last page. 634 + if (lastPage.length < ATURI_PAGE_LIMIT) { 635 + return undefined; 636 + } 637 + 638 + // The cursor for the next page is the `rkey` of the last item we received. 639 + const lastItem = lastPage[lastPage.length - 1]; 640 + return lastItem.rkey; 641 + }, 642 + 643 + enabled: isEnabled, 644 + }); 645 + } 646 + 647 + 558 648 type FeedSkeletonPage = ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema; 559 649 560 650 export function constructInfiniteFeedSkeletonQuery(options: { ··· 605 695 }) { 606 696 const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery(options); 607 697 608 - return useInfiniteQuery({ 698 + return {...useInfiniteQuery({ 609 699 queryKey, 610 700 queryFn, 611 701 initialPageParam: undefined as never, ··· 613 703 staleTime: Infinity, 614 704 refetchOnWindowFocus: false, 615 705 enabled: !!options.feedUri && (options.isAuthed ? !!options.agent && !!options.pdsUrl && !!options.feedServiceDid : true), 616 - }); 706 + }), queryKey: queryKey}; 707 + } 708 + 709 + 710 + export function yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(query?: { 711 + constellation: string, 712 + method: '/links' 713 + target?: string 714 + collection: string 715 + path: string 716 + }) { 717 + console.log( 718 + 'yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks', 719 + query, 720 + ) 721 + 722 + return infiniteQueryOptions({ 723 + enabled: !!query?.target, 724 + queryKey: [ 725 + 'reddwarf_constellation', 726 + query?.method, 727 + query?.target, 728 + query?.collection, 729 + query?.path, 730 + ] as const, 731 + 732 + queryFn: async ({pageParam}: {pageParam?: string}) => { 733 + if (!query || !query?.target) return undefined 734 + 735 + const method = query.method 736 + const target = query.target 737 + const collection = query.collection 738 + const path = query.path 739 + const cursor = pageParam 740 + 741 + const res = await fetch( 742 + `https://${query.constellation}${method}?target=${encodeURIComponent(target)}${ 743 + collection ? `&collection=${encodeURIComponent(collection)}` : '' 744 + }${path ? `&path=${encodeURIComponent(path)}` : ''}${ 745 + cursor ? `&cursor=${encodeURIComponent(cursor)}` : '' 746 + }`, 747 + ) 748 + 749 + if (!res.ok) throw new Error('Failed to fetch') 750 + 751 + return (await res.json()) as linksRecordsResponse 752 + }, 753 + 754 + getNextPageParam: lastPage => { 755 + return (lastPage as any)?.cursor ?? undefined 756 + }, 757 + initialPageParam: undefined, 758 + staleTime: 5 * 60 * 1000, 759 + gcTime: 5 * 60 * 1000, 760 + }) 617 761 }
+1 -1
vite.config.ts
··· 10 10 11 11 import { generateMetadataPlugin } from "./oauthdev.mts"; 12 12 13 - const PROD_URL = "https://reddwarf.whey.party" 13 + const PROD_URL = "https://reddwarf.app" 14 14 const DEV_URL = "https://local3768forumtest.whey.party" 15 15 16 16 function shp(url: string): string {