an independent Bluesky client using Constellation, PDS Queries, and other services reddwarf.app
frontend spa bluesky reddwarf microcosm client app
99
fork

Configure Feed

Select the types of activity you want to include in your feed.

more material 3 tweaks

rimar1337 91e90cba fb3fbe80

+109 -37
+5 -1
src/components/Header.tsx
··· 1 1 import { Link, useRouter } from "@tanstack/react-router"; 2 + import { useAtom } from "jotai"; 3 + 4 + import { isAtTopAtom } from "~/utils/atoms"; 2 5 3 6 export function Header({ 4 7 backButtonCallback, ··· 8 11 title?: string; 9 12 }) { 10 13 const router = useRouter(); 14 + const [isAtTop] = useAtom(isAtTopAtom); 11 15 //const what = router.history. 12 16 return ( 13 - <div className="flex items-center gap-4 px-4 py-3 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700"> 17 + <div className={`flex items-center gap-3 px-3 py-3 h-[52px] sticky top-0 bg-[var(--header-bg-light)] dark:bg-[var(--header-bg-dark)] z-10 border-0 ${!isAtTop && "shadow"} border-gray-200 dark:border-gray-700`}> 14 18 {backButtonCallback ? (<Link 15 19 to=".." 16 20 //className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg"
+2 -2
src/components/InfiniteCustomFeed.tsx
··· 113 113 <button 114 114 onClick={handleRefresh} 115 115 disabled={isRefetching} 116 - className="sticky lg:bottom-4 bottom-22 ml-4 w-[42px] h-[42px] z-10 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-gray-50 p-[9px] rounded-full shadow-lg transition-transform duration-200 ease-in-out hover:scale-110 disabled:bg-gray-400 disabled:cursor-not-allowed" 116 + className="sticky lg:bottom-4 bottom-22 ml-4 w-[42px] h-[42px] z-10 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-gray-50 p-[9px] rounded-full shadow-lg transition-transform duration-200 ease-in-out hover:scale-110 disabled:dark:bg-gray-900 disabled:bg-gray-100 disabled:cursor-not-allowed" 117 117 aria-label="Refresh feed" 118 118 > 119 - {isRefetching ? <RefreshIcon className="h-6 w-6 text-gray-600 dark:text-gray-400 animate-spin" /> : <RefreshIcon className="h-6 w-6 text-gray-600 dark:text-gray-400" />} 119 + <RefreshIcon className={`h-6 w-6 text-gray-600 dark:text-gray-400 ${isRefetching && "animate-spin"}`} /> 120 120 </button> 121 121 </> 122 122 );
+15 -15
src/components/UniversalPostRenderer.tsx
··· 1248 1248 // dont cursor: "pointer", 1249 1249 borderBottomWidth: bottomBorder ? (isQuote ? 0 : 1) : 0, 1250 1250 }} 1251 - className="border-gray-300 dark:border-gray-600" 1251 + className="border-gray-300 dark:border-gray-800" 1252 1252 > 1253 1253 {isRepost && ( 1254 1254 <div ··· 1316 1316 width: isQuote ? 16 : 42, 1317 1317 height: isQuote ? 16 : 42, 1318 1318 }} 1319 - className="border border-gray-300 dark:border-gray-600 bg-gray-300 dark:bg-gray-600" 1319 + className="border border-gray-300 dark:border-gray-800 bg-gray-300 dark:bg-gray-600" 1320 1320 /> 1321 1321 </div> 1322 1322 <div style={{ display: "flex", alignItems: "flex-start", zIndex: 2 }}> ··· 1521 1521 hydrate embeds this deep but the connection here is implicit 1522 1522 todo: idk make this a real part of the embed shim so its not implicit */ 1523 1523 <> 1524 - <div className="border-gray-300 dark:border-gray-600 p-3 rounded-xl border italic text-gray-400 text-[14px]"> 1524 + <div className="border-gray-300 dark:border-gray-800 p-3 rounded-xl border italic text-gray-400 text-[14px]"> 1525 1525 (there is an embed here thats too deep to render) 1526 1526 </div> 1527 1527 </> ··· 1544 1544 borderBottomWidth: 1, 1545 1545 marginBottom: 8, 1546 1546 }} // important for height animation 1547 - className="text-gray-500 dark:text-gray-400 border-gray-200 dark:border-gray-700" 1547 + className="text-gray-500 dark:text-gray-400 border-gray-200 dark:border-gray-800 was7" 1548 1548 > 1549 1549 {fullDateTimeFormat(post.indexedAt)} 1550 1550 </div> ··· 1780 1780 //boxShadow: theme.cardShadow, 1781 1781 overflow: "hidden", 1782 1782 }} 1783 - className="shadow border border-gray-200 dark:border-gray-700" 1783 + className="shadow border border-gray-200 dark:border-gray-800 was7" 1784 1784 > 1785 1785 <UniversalPostRenderer 1786 1786 post={post} ··· 1897 1897 //boxShadow: theme.cardShadow, 1898 1898 overflow: "hidden", 1899 1899 }} 1900 - className="shadow border border-gray-200 dark:border-gray-700" 1900 + className="shadow border border-gray-200 dark:border-gray-800 was7" 1901 1901 > 1902 1902 <UniversalPostRenderer 1903 1903 post={post} ··· 1970 1970 //border: `1px solid ${theme.border}`, 1971 1971 overflow: "hidden", 1972 1972 }} 1973 - className="border border-gray-200 dark:border-gray-700 bg-gray-200 dark:bg-gray-900" 1973 + className="border border-gray-200 dark:border-gray-800 was7 bg-gray-200 dark:bg-gray-900" 1974 1974 > 1975 1975 {lightboxIndex !== null && ( 1976 1976 <Lightbox ··· 2011 2011 overflow: "hidden", 2012 2012 //border: `1px solid ${theme.border}`, 2013 2013 }} 2014 - className="border border-gray-200 dark:border-gray-700" 2014 + className="border border-gray-200 dark:border-gray-800 was7" 2015 2015 > 2016 2016 {lightboxIndex !== null && ( 2017 2017 <Lightbox ··· 2061 2061 //border: `1px solid ${theme.border}`, 2062 2062 // height: 240, // fixed height for cropping 2063 2063 }} 2064 - className="border border-gray-200 dark:border-gray-700" 2064 + className="border border-gray-200 dark:border-gray-800 was7" 2065 2065 > 2066 2066 {lightboxIndex !== null && ( 2067 2067 <Lightbox ··· 2146 2146 //border: `1px solid ${theme.border}`, 2147 2147 //aspectRatio: "3 / 2", // overall grid aspect 2148 2148 }} 2149 - className="border border-gray-200 dark:border-gray-700" 2149 + className="border border-gray-200 dark:border-gray-800 was7" 2150 2150 > 2151 2151 {lightboxIndex !== null && ( 2152 2152 <Lightbox ··· 2283 2283 e.stopPropagation(); 2284 2284 e.nativeEvent.stopImmediatePropagation(); 2285 2285 }} 2286 - className="lightbox-sidebar overscroll-none disablegutter border-l dark:border-gray-700 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" 2286 + className="lightbox-sidebar overscroll-none disablegutter border-l dark:border-gray-800 was7 border-gray-300 fixed z-50 flex top-0 right-0 flex-col max-w-[350px] min-w-[350px] max-h-screen overflow-y-scroll dark:bg-gray-950 bg-white" 2287 2287 > 2288 2288 <ProfilePostComponent 2289 2289 did={post.did} ··· 2587 2587 > 2588 2588 <div 2589 2589 style={containerStyle as React.CSSProperties} 2590 - className="border border-gray-200 dark:border-gray-700" 2590 + className="border border-gray-200 dark:border-gray-800 was7" 2591 2591 > 2592 2592 {thumb && ( 2593 2593 <div ··· 2601 2601 marginBottom: 8, 2602 2602 //borderBottom: `1px solid ${theme.border}`, 2603 2603 }} 2604 - className="border-b border-gray-200 dark:border-gray-700" 2604 + className="border-b border-gray-200 dark:border-gray-800 was7" 2605 2605 > 2606 2606 <img 2607 2607 src={thumb} ··· 2727 2727 borderRadius: 12, 2728 2728 //border: `1px solid ${theme.border}`, 2729 2729 }} 2730 - className="border border-gray-200 dark:border-gray-700" 2730 + className="border border-gray-200 dark:border-gray-800 was7" 2731 2731 onClick={async (e) => { 2732 2732 e.stopPropagation(); 2733 2733 setPlaying(true); ··· 2768 2768 100 / (aspect ? aspect.width / aspect.height : 16 / 9) 2769 2769 }%`, // 16:9 = 56.25%, 4:3 = 75% 2770 2770 }} 2771 - className="border border-gray-200 dark:border-gray-700" 2771 + className="border border-gray-200 dark:border-gray-800 was7" 2772 2772 > 2773 2773 <ReactPlayer 2774 2774 src={url}
+53 -10
src/main.tsx
··· 1 1 import "~/styles/app.css"; 2 2 3 3 import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister"; 4 - import { QueryClient, QueryClientProvider, } from "@tanstack/react-query"; 5 - import { 6 - persistQueryClient, 7 - } from "@tanstack/react-query-persist-client"; 8 - import { createRouter,RouterProvider } from "@tanstack/react-router"; 4 + import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 5 + import { persistQueryClient } from "@tanstack/react-query-persist-client"; 6 + import { createRouter, RouterProvider } from "@tanstack/react-router"; 7 + import { useSetAtom } from "jotai"; 8 + import { useEffect } from "react"; 9 9 //import { StrictMode } from "react"; 10 10 import ReactDOM from "react-dom/client"; 11 11 12 12 import reportWebVitals from "./reportWebVitals.ts"; 13 13 // Import the generated route tree 14 14 import { routeTree } from "./routeTree.gen"; 15 - 15 + import { isAtTopAtom } from "./utils/atoms.ts"; 16 16 17 17 const queryClient = new QueryClient({ 18 18 defaultOptions: { ··· 28 28 persistQueryClient({ 29 29 queryClient, 30 30 persister: localStoragePersister, 31 - }) 31 + }); 32 32 33 33 // Create a new router instance 34 34 const router = createRouter({ ··· 54 54 root.render( 55 55 // double queries annoys me 56 56 // <StrictMode> 57 - <QueryClientProvider client={queryClient}> 58 - <RouterProvider router={router} /> 59 - </QueryClientProvider> 57 + <QueryClientProvider client={queryClient}> 58 + <ScrollTopWatcher /> 59 + <RouterProvider router={router} /> 60 + </QueryClientProvider> 60 61 // </StrictMode> 61 62 ); 62 63 } ··· 65 66 // to log results (for example: reportWebVitals(// /*mass comment*/ console.log)) 66 67 // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 67 68 reportWebVitals(); 69 + 70 + export default function ScrollTopWatcher() { 71 + const setIsAtTop = useSetAtom(isAtTopAtom); 72 + useEffect(() => { 73 + const meta = document.querySelector('meta[name="theme-color"]'); 74 + let lastAtTop = window.scrollY === 0; 75 + let timeoutId: number | undefined; 76 + 77 + const setVars = (atTop: boolean) => { 78 + const root = document.documentElement; 79 + root.style.setProperty("--is-top", atTop ? "1" : "0"); 80 + 81 + const bg = getComputedStyle(root).getPropertyValue("--header-bg").trim(); 82 + if (meta && bg) meta.setAttribute("content", bg); 83 + setIsAtTop(atTop); 84 + }; 85 + 86 + const check = () => { 87 + const atTop = window.scrollY === 0; 88 + if (atTop !== lastAtTop) { 89 + lastAtTop = atTop; 90 + setVars(atTop); 91 + } 92 + }; 93 + 94 + const handleScroll = () => { 95 + if (timeoutId) clearTimeout(timeoutId); 96 + timeoutId = window.setTimeout(check, 2); 97 + }; 98 + 99 + // initialize 100 + setVars(lastAtTop); 101 + window.addEventListener("scroll", handleScroll, { passive: true }); 102 + 103 + return () => { 104 + window.removeEventListener("scroll", handleScroll); 105 + if (timeoutId) clearTimeout(timeoutId); 106 + }; 107 + }, []); 108 + 109 + return null; 110 + }
+7 -7
src/routes/__root.tsx
··· 431 431 </button> 432 432 )} 433 433 434 - <main className="w-full max-w-[600px] lg:border-x border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-950 pb-16 lg:pb-0"> 434 + <main className="w-full max-w-[600px] lg:border-x border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 pb-16 lg:pb-0 overflow-x-clip"> 435 435 {children} 436 436 </main> 437 437 ··· 448 448 </div> 449 449 450 450 {agent?.did ? ( 451 - <nav className="lg:hidden fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-950 border-t border-gray-200 dark:border-gray-700 z-40"> 451 + <nav className="lg:hidden fixed bottom-0 left-0 right-0 bg-gray-50 dark:bg-gray-900 border-0 shadow border-gray-200 dark:border-gray-700 z-40"> 452 452 <div className="flex justify-around items-center p-2"> 453 453 <MaterialNavItem 454 454 small ··· 616 616 </div> 617 617 </nav> 618 618 ) : ( 619 - <div className="lg:hidden flex items-center fixed bottom-0 left-0 right-0 justify-between px-4 py-3 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-950 z-10"> 619 + <div className="lg:hidden flex items-center fixed bottom-0 left-0 right-0 justify-between px-4 py-3 border-0 shadow border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 z-10"> 620 620 <div className="flex items-center gap-2"> 621 621 <img src="/redstar.png" alt="Red Dwarf Logo" className="w-6 h-6" /> 622 622 <span className="font-bold text-lg text-gray-900 dark:text-gray-100"> ··· 682 682 <button 683 683 className={`flex flex-row h-12 min-h-12 max-h-12 px-4 py-0.5 w-full items-center rounded-full transition-colors flex-1 gap-1 ${ 684 684 active 685 - ? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-700 bg-gray-200 hover:dark:bg-gray-600" 686 - : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800" 685 + ? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-800 bg-gray-200 hover:dark:bg-gray-700" 686 + : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-900" 687 687 }`} 688 688 onClick={() => { 689 689 onClickCallbback(); ··· 693 693 {active ? ActiveIcon : InactiveIcon} 694 694 </div> 695 695 <span 696 - className={`text-[16px] text-roboto ${active ? "font-medium" : ""}`} 696 + className={`text-[17px] text-roboto ${active ? "font-medium" : ""}`} 697 697 > 698 698 {text} 699 699 </span> ··· 732 732 {active ? ActiveIcon : InactiveIcon} 733 733 </div> 734 734 <span 735 - className={`text-[16px] text-roboto ${active ? "font-medium" : ""}`} 735 + className={`text-[17px] text-roboto ${active ? "font-medium" : ""}`} 736 736 > 737 737 {text} 738 738 </span>
+6 -2
src/routes/index.tsx
··· 10 10 agentAtom, 11 11 authedAtom, 12 12 feedScrollPositionsAtom, 13 + isAtTopAtom, 13 14 selectedFeedUriAtom, 14 15 store, 15 16 } from "~/utils/atoms"; ··· 350 351 authed && agent && identity?.pds && feedServiceDid; 351 352 const isReadyForUnauthedFeed = !authed && selectedFeed; 352 353 354 + 355 + const [isAtTop] = useAtom(isAtTopAtom); 356 + 353 357 return ( 354 358 <div 355 - className={`relative flex flex-col divide-y divide-gray-200 dark:divide-gray-800 ${hidden && "hidden"}`} 359 + className={`relative flex flex-col divide-y divide-gray-200 dark:divide-gray-800 ${hidden && "hidden"} ${!isAtTop && "shadow"}`} 356 360 > 357 361 {savedFeeds.length > 0 ? ( 358 - <div className="flex items-center px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700 overflow-x-auto overflow-y-hidden scroll-thin"> 362 + <div className="flex items-center px-4 py-2 h-[52px] sticky top-0 bg-[var(--header-bg-light)] dark:bg-[var(--header-bg-dark)] z-10 border-0 border-gray-200 dark:border-gray-700 overflow-x-auto overflow-y-hidden scroll-thin"> 359 363 {savedFeeds.map((item: any, idx: number) => { 360 364 const label = item.value.split("/").pop() || item.value; 361 365 const isActive = selectedFeed === item.value;
+19
src/styles/app.css
··· 86 86 } 87 87 .font-roboto { 88 88 font-family: "Roboto", sans-serif; 89 + } 90 + 91 + :root { 92 + --header-bg-light: color-mix(in srgb, var(--color-white) calc(var(--is-top) * 100%), var(--color-gray-50)); 93 + --header-bg-dark: color-mix(in srgb, var(--color-gray-950) calc(var(--is-top) * 100%), var(--color-gray-900)); 94 + } 95 + 96 + :root { 97 + --header-bg: var(--header-bg-light); 98 + } 99 + @media (prefers-color-scheme: dark) { 100 + :root { 101 + --header-bg: var(--header-bg-dark); 102 + } 103 + } 104 + 105 + :root { 106 + --shadow-opacity: calc(1 - var(--is-top)); 107 + --tw-shadow-header: 0 2px 8px hsl(0 0% 0% / calc(var(--shadow-opacity) * 0.15)); 89 108 }
+2
src/utils/atoms.ts
··· 21 21 {} 22 22 ); 23 23 24 + export const isAtTopAtom = atom<boolean>(true); 25 + 24 26 export const agentAtom = atom<Agent|null>(null); 25 27 export const authedAtom = atom<boolean>(false);