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

Configure Feed

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

sidebar redesign and about page integration

+490 -106
+6 -4
src/components/Login.tsx
··· 24 24 className={ 25 25 compact 26 26 ? "flex items-center justify-center p-1" 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]" 27 + : "p-6 bg-gray-100 dark:bg-gray-900 rounded-xl shadow border border-gray-200 dark:border-gray-800 flex justify-center items-center h-[280px]" 28 28 } 29 29 > 30 30 <span ··· 43 43 // Large view 44 44 if (!compact) { 45 45 return ( 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"> 46 + <div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800"> 47 47 <div className="flex flex-col items-center justify-center text-center"> 48 48 <p className="text-lg font-semibold mb-4 text-gray-800 dark:text-gray-100"> 49 49 You are logged in! ··· 77 77 if (!compact) { 78 78 // Large view renders the form directly in the card 79 79 return ( 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"> 80 + <div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800"> 81 81 <UnifiedLoginForm /> 82 82 </div> 83 83 ); ··· 177 177 178 178 useEffect(() => { 179 179 const lastHandle = localStorage.getItem("lastHandle"); 180 + // eslint-disable-next-line react-hooks/set-state-in-effect 180 181 if (lastHandle) setHandle(lastHandle); 181 182 }, []); 182 183 ··· 229 230 230 231 useEffect(() => { 231 232 const lastHandle = localStorage.getItem("lastHandle"); 233 + // eslint-disable-next-line react-hooks/set-state-in-effect 232 234 if (lastHandle) setUser(lastHandle); 233 235 }, []); 234 236 ··· 246 248 return ( 247 249 <form onSubmit={handleSubmit} className="flex flex-col gap-3"> 248 250 <p className="text-xs text-red-500 dark:text-red-400"> 249 - Warning: Less secure. Use an App Password. 251 + Less secure. Do not use your main password, please use an App Password. 250 252 </p> 251 253 {/* <input 252 254 type="text"
+10 -1
src/components/LogoSvg.tsx
··· 1 1 import type { SVGProps } from 'react'; 2 2 import React from 'react'; 3 3 4 + import { HOST_LOGO_USE_FAVICON } from '~/../policy'; 5 + 6 + export default function LogoSVG(props: SVGProps<SVGSVGElement>) { 7 + if (HOST_LOGO_USE_FAVICON) { 8 + return (<img src={"/favicon.png"} width={32} height={32} {...props as any}/>) 9 + } 10 + return (<FluentEmojiHighContrastGlowingStar {...props} />) 11 + } 12 + 4 13 // FluentEmojiHighContrastGlowingStar 5 - export default function FluentEmojiHighContrastGlowingStar(props: SVGProps<SVGSVGElement>) { 14 + export function FluentEmojiHighContrastGlowingStar(props: SVGProps<SVGSVGElement>) { 6 15 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>); 7 16 } 8 17
+341 -90
src/routes/__root.tsx
··· 5 5 import type { QueryClient } from "@tanstack/react-query"; 6 6 import { 7 7 createRootRouteWithContext, 8 + Link, 8 9 // Link, 9 10 // Outlet, 10 11 Scripts, ··· 18 19 import { Toaster } from "sonner"; 19 20 import { KeepAliveOutlet, KeepAliveProvider } from "tanstack-router-keepalive"; 20 21 21 - import { HOST_TITLE } from "~/../policy"; 22 + import { HOST_ADMIN, HOST_DESCRIPTION, HOST_HERO, HOST_LOGIN_BLURB, HOST_MAIN_TITLE, HOST_SIGNUP_PDS, HOST_SUB_TITLE, HOST_TITLE, HOST_UNAUTHED_DEFAULT_FEEDS } from "~/../policy"; 22 23 import { Composer } from "~/components/Composer"; 23 24 import { DefaultCatchBoundary } from "~/components/DefaultCatchBoundary"; 24 25 import { Import } from "~/components/Import"; 25 - import Login from "~/components/Login"; 26 + //import Login from "~/components/Login"; 26 27 import Logo from "~/components/LogoSvg"; 27 - import { ModerationBatcher } from "~/components/ModerationBatcher"; 28 - import { ModerationInitializer } from "~/components/ModerationInitializer"; 28 + //import { ModerationBatcher } from "~/components/ModerationBatcher"; 29 + //import { ModerationInitializer } from "~/components/ModerationInitializer"; 29 30 import { NotFound } from "~/components/NotFound"; 31 + import { AutoLabelProvider } from "~/providers/AutoLabelProvider"; 30 32 import { LikeMutationQueueProvider } from "~/providers/LikeMutationQueueProvider"; 31 33 import { PollMutationQueueProvider } from "~/providers/PollMutationQueueProvider"; 32 34 import { UnifiedAuthProvider, useAuth } from "~/providers/UnifiedAuthProvider"; 33 - import { composerAtom, hueAtom, useAtomCssVar } from "~/utils/atoms"; 35 + import { FeedTabOnTop } from "~/routes/index"; 36 + import { composerAtom, hueAtom, imgCDNAtom, quickAuthAtom, useAtomCssVar } from "~/utils/atoms"; 34 37 import { seo } from "~/utils/seo"; 38 + import { useQueryIdentity, useQueryPreferences, useQueryProfile } from "~/utils/useQuery"; 35 39 36 40 export const Route = createRootRouteWithContext<{ 37 41 queryClient: QueryClient; ··· 75 79 errorComponent: import.meta.env.DEV 76 80 ? undefined 77 81 : (props) => ( 78 - <RootDocument> 79 - <DefaultCatchBoundary {...props} /> 80 - </RootDocument> 81 - ), 82 + <RootDocument> 83 + <DefaultCatchBoundary {...props} /> 84 + </RootDocument> 85 + ), 82 86 notFoundComponent: () => <NotFound />, 83 87 component: RootComponent, 84 88 }); ··· 86 90 function RootComponent() { 87 91 return ( 88 92 <UnifiedAuthProvider> 89 - <LikeMutationQueueProvider> 90 - <PollMutationQueueProvider> 91 - <ModerationInitializer /> 92 - <ModerationBatcher /> 93 - <RootDocument> 94 - <KeepAliveProvider> 95 - <AppToaster /> 96 - <KeepAliveOutlet /> 97 - </KeepAliveProvider> 98 - </RootDocument> 99 - </PollMutationQueueProvider> 100 - </LikeMutationQueueProvider> 93 + <AutoLabelProvider> 94 + <LikeMutationQueueProvider> 95 + <PollMutationQueueProvider> 96 + {/* <ModerationInitializer /> 97 + <ModerationBatcher /> */} 98 + <RootDocument> 99 + <KeepAliveProvider> 100 + <AppToaster /> 101 + <KeepAliveOutlet /> 102 + </KeepAliveProvider> 103 + </RootDocument> 104 + </PollMutationQueueProvider> 105 + </LikeMutationQueueProvider> 106 + </AutoLabelProvider> 101 107 </UnifiedAuthProvider> 102 108 ); 103 109 } ··· 126 132 button={ 127 133 button?.label 128 134 ? { 129 - label: button?.label, 130 - onClick: () => { 131 - button?.onClick?.(); 132 - }, 133 - } 135 + label: button?.label, 136 + onClick: () => { 137 + button?.onClick?.(); 138 + }, 139 + } 134 140 : undefined 135 141 } 136 142 /> ··· 222 228 const isSearch = location.pathname.startsWith("/search"); 223 229 const isFeeds = location.pathname.startsWith("/feeds"); 224 230 const isModeration = location.pathname.startsWith("/moderation"); 231 + const isAbout = location.pathname.startsWith("/about"); 225 232 226 233 const locationEnum: 227 234 | "feeds" ··· 230 237 | "notifications" 231 238 | "profile" 232 239 | "moderation" 240 + | "about" 233 241 | "home" = isFeeds 234 - ? "feeds" 235 - : isSearch 236 - ? "search" 237 - : isSettings 238 - ? "settings" 239 - : isNotifications 240 - ? "notifications" 241 - : isProfile 242 - ? "profile" 243 - : isModeration 244 - ? "moderation" 245 - : "home"; 242 + ? "feeds" 243 + : isSearch 244 + ? "search" 245 + : isSettings 246 + ? "settings" 247 + : isNotifications 248 + ? "notifications" 249 + : isProfile 250 + ? "profile" 251 + : isModeration 252 + ? "moderation" 253 + : isAbout ? 254 + "about" 255 + : "home"; 246 256 247 257 const [, setComposerPost] = useAtom(composerAtom); 248 258 ··· 251 261 <Composer /> 252 262 253 263 <div className="min-h-screen flex justify-center bg-gray-50 dark:bg-gray-950"> 254 - <nav className="hidden lg:flex h-screen w-[250px] flex-col gap-0 p-4 dark:border-gray-800 sticky top-0 self-start"> 264 + <nav className="hidden lg:flex h-screen w-[250px] xl:ml-[50px] flex-col gap-0 p-4 dark:border-gray-800 sticky top-0 self-start"> 255 265 <div className="flex items-center gap-3 mb-4 pl-3"> 256 266 <Logo 257 267 className="h-8 w-8" ··· 261 271 }} 262 272 /> 263 273 <span className="font-extrabold text-2xl text-gray-900 dark:text-gray-100"> 264 - {HOST_TITLE}{" "} 265 - {/* <span className="text-gray-500 dark:text-gray-400 text-sm"> 266 - lite 267 - </span> */} 274 + {HOST_MAIN_TITLE} 275 + {HOST_SUB_TITLE && (<span className="text-gray-500 dark:text-gray-400 text-sm"> 276 + {HOST_SUB_TITLE} 277 + </span>) } 268 278 </span> 269 279 </div> 270 280 <MaterialNavItem ··· 295 305 text="Explore" 296 306 /> 297 307 <MaterialNavItem 308 + visible={!!agent?.did} 298 309 InactiveIcon={ 299 310 <IconMaterialSymbolsNotificationsOutline className="w-6 h-6" /> 300 311 } ··· 311 322 text="Notifications" 312 323 /> 313 324 <MaterialNavItem 325 + visible={!!agent?.did} 314 326 InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 315 327 ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 316 328 active={locationEnum === "feeds"} ··· 323 335 text="Feeds" 324 336 /> 325 337 <MaterialNavItem 338 + visible={!!agent?.did} 326 339 InactiveIcon={<IconMdiShieldOutline className="w-6 h-6" />} 327 340 ActiveIcon={<IconMdiShield className="w-6 h-6" />} 328 341 active={locationEnum === "moderation"} ··· 335 348 text="Moderation" 336 349 /> 337 350 <MaterialNavItem 351 + visible={!!agent?.did} 338 352 InactiveIcon={ 339 353 <IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" /> 340 354 } ··· 367 381 } 368 382 text="Settings" 369 383 /> 370 - <div className="flex flex-row items-center justify-center mt-3"> 371 - <MaterialPillButton 372 - InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 373 - ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 374 - //active={true} 375 - onClickCallbback={() => setComposerPost({ kind: "root" })} 376 - text="Post" 384 + {!agent?.did && ( 385 + <MaterialNavItem 386 + InactiveIcon={ 387 + <IconMaterialSymbolsInfoOutline className="w-6 h-6" /> 388 + } 389 + ActiveIcon={<IconMaterialSymbolsInfo className="w-6 h-6" />} 390 + active={locationEnum === "about"} 391 + onClickCallbback={() => 392 + navigate({ 393 + to: "/about", 394 + //params: { did: agent.assertDid }, 395 + }) 396 + } 397 + text="About" 377 398 /> 378 - </div> 399 + )} 400 + {agent?.did && ( 401 + <div className="flex flex-row items-center justify-center mt-3"> 402 + <MaterialPillButton 403 + InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 404 + ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 405 + //active={true} 406 + onClickCallbback={() => setComposerPost({ kind: "root" })} 407 + text="Post" 408 + /> 409 + </div> 410 + )} 411 + {!agent?.did && ( 412 + <> 413 + <div className="mt-4 mb-2 w-full h-[1px] bg-gray-200 dark:bg-gray-800" /> 414 + {/* <Login /> */} 415 + <LoginRedirect /> 416 + </> 417 + )} 379 418 {/* <Link 380 419 to="/" 381 420 className={ ··· 479 518 <span>Post</span> 480 519 </button> */} 481 520 <div className="flex-1"></div> 521 + {!!agent?.did && ( 522 + <div className="flex flex-row items-center lg:mb-1"> 523 + <div className="flex p-2 h-12 flex-1 rounded-full hover:dark:bg-gray-800 hover:bg-gray-200"> 524 + <ProfileSmall did={agent.did} /> 525 + </div> 526 + <Link 527 + to="/settings" 528 + className="flex p-3 h-12 w-12 rounded-full hover:dark:bg-gray-800 hover:bg-gray-200 items-center justify-center" 529 + > 530 + <IconMaterialSymbolsMoreVert /> 531 + </Link> 532 + </div> 533 + )} 534 + {/* 482 535 <a 483 536 href="https://tangled.sh/@whey.party/red-dwarf" 484 537 target="_blank" ··· 506 559 microcosm.blue 507 560 </a> 508 561 </div> 562 + */} 509 563 </nav> 510 564 511 565 <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"> ··· 549 603 /> 550 604 <MaterialNavItem 551 605 small 606 + visible={!!agent?.did} 552 607 InactiveIcon={ 553 608 <IconMaterialSymbolsNotificationsOutline className="w-6 h-6" /> 554 609 } ··· 566 621 /> 567 622 <MaterialNavItem 568 623 small 624 + visible={!!agent?.did} 569 625 InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 570 626 ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 571 627 active={locationEnum === "feeds"} ··· 579 635 /> 580 636 <MaterialNavItem 581 637 small 638 + visible={!!agent?.did} 582 639 InactiveIcon={<IconMdiShieldOutline className="w-6 h-6" />} 583 640 ActiveIcon={<IconMdiShield className="w-6 h-6" />} 584 641 active={locationEnum === "moderation"} ··· 592 649 /> 593 650 <MaterialNavItem 594 651 small 652 + visible={!!agent?.did} 595 653 InactiveIcon={ 596 654 <IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" /> 597 655 } ··· 625 683 } 626 684 text="Settings" 627 685 /> 628 - <div className="flex flex-row items-center justify-center mt-3"> 629 - <MaterialPillButton 686 + 687 + {!agent?.did && ( 688 + <MaterialNavItem 630 689 small 631 - InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 632 - ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 633 - //active={true} 634 - onClickCallbback={() => setComposerPost({ kind: "root" })} 635 - text="Post" 690 + InactiveIcon={ 691 + <IconMaterialSymbolsInfoOutline className="w-6 h-6" /> 692 + } 693 + ActiveIcon={<IconMaterialSymbolsInfo className="w-6 h-6" />} 694 + active={locationEnum === "about"} 695 + onClickCallbback={() => 696 + navigate({ 697 + to: "/about", 698 + //params: { did: agent.assertDid }, 699 + }) 700 + } 701 + text="About" 636 702 /> 637 - </div> 703 + )} 704 + {!!agent?.did && ( 705 + <div className="flex flex-row items-center justify-center mt-3"> 706 + <MaterialPillButton 707 + small 708 + InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 709 + ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 710 + //active={true} 711 + onClickCallbback={() => setComposerPost({ kind: "root" })} 712 + text="Post" 713 + /> 714 + </div> 715 + )} 638 716 </nav> 639 717 640 718 {agent?.did && ( ··· 657 735 {children} 658 736 </main> 659 737 660 - <aside className="hidden lg:flex h-screen w-[250px] sticky top-0 self-start flex-col"> 661 - <div className="px-4 pt-4"> 738 + <aside className="hidden lg:flex h-screen xl:w-[300px] w-[250px] sticky top-0 self-start flex-col"> 739 + <div className="px-4 pt-4 gap-4 flex flex-col"> 662 740 <Import /> 663 741 </div> 664 - <Login /> 742 + <div className="px-4 pt-4 gap-4 flex flex-col max-h-[calc(100dvh - 80px)] overflow-y-auto"> 743 + {( 744 + (!agent?.did && HOST_UNAUTHED_DEFAULT_FEEDS.length > 0) 745 + || (!!agent?.did) 746 + ) && ( 747 + <FeedListDesktopSidebar /> 748 + )} 749 + {!agent?.did && ( 750 + <> 751 + <span className=" text-gray-500 dark:text-gray-400 text-sm leading-tight"><span className=" font-bold">{window.location.host}</span> is a hosted Red Dwarf instance that you can use to participate in the Bluesky social network.</span> 752 + <img className="rounded-sm" src={HOST_HERO} /> 753 + <span className=" text-gray-500 dark:text-gray-400 text-sm">{HOST_DESCRIPTION}</span> 754 + <div className="flex flex-col gap-1 "> 755 + <span className="text-gray-500 dark:text-gray-400 text-sm font-bold">ADMINISTERED BY:</span> 756 + <ProfileSmall did={HOST_ADMIN} /> 757 + </div> 758 + </> 759 + )} 760 + </div> 665 761 <div className="flex-1"></div> 666 - <p className="text-xs text-gray-400 dark:text-gray-500 text-justify mx-4 mb-4"> 667 - {HOST_TITLE} is a Bluesky client that does not rely on any Bluesky API 668 - App Servers. Instead, it uses Microcosm to fetch records directly 669 - from each users' PDS (via Slingshot) and connect them using 670 - backlinks (via Constellation) 671 - </p> 762 + {/* todo */} 763 + <span>TODO: add red dwarf the software policy along with instance policy here</span> 672 764 </aside> 673 765 </div> 674 766 675 767 {agent?.did ? ( 676 - <nav className="sm:hidden fixed bottom-0 left-0 right-0 bg-gray-50 dark:bg-gray-900 border-0 shadow border-gray-200 dark:border-gray-700 z-40"> 768 + <nav className="sm:hidden fixed bottom-0 left-0 right-0 bg-gray-50 dark:bg-gray-900 border-0 border-t-1 dark:border-t-0 shadow border-gray-200 dark:border-gray-700 z-40"> 677 769 <div className="flex justify-around items-center p-2"> 678 770 <MaterialNavItem 679 771 small ··· 845 937 </div> 846 938 </nav> 847 939 ) : ( 848 - <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"> 940 + <div className="lg:hidden flex items-center fixed bottom-0 left-0 right-0 justify-between px-4 py-3 border-0 border-t-1 dark:border-t-0 shadow border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 z-10"> 849 941 <div className="flex items-center gap-2"> 850 942 <Logo 851 943 className="h-6 w-6" ··· 855 947 }} 856 948 /> 857 949 <span className="font-bold text-lg text-gray-900 dark:text-gray-100"> 858 - {HOST_TITLE}{" "} 859 - {/* <span className="text-gray-500 dark:text-gray-400 text-sm"> 860 - lite 861 - </span> */} 950 + {HOST_MAIN_TITLE} 951 + {HOST_SUB_TITLE && (<span className="text-gray-500 dark:text-gray-400 text-sm"> 952 + {HOST_SUB_TITLE} 953 + </span>) } 862 954 </span> 863 955 </div> 864 956 <div className="flex items-center gap-2"> 865 - <Login compact={true} popup={true} /> 957 + {/* <Login compact={true} popup={true} /> */} 958 + <Link 959 + to="/settings" 960 + className="rounded-full bg-gray-600 text-gray-100 dark:bg-gray-400 dark:text-gray-900 px-4 py-2 text-sm font-medium text-center" 961 + > 962 + Log in 963 + </Link> 866 964 </div> 867 965 </div> 868 966 )} ··· 874 972 } 875 973 876 974 export function MaterialNavItem({ 975 + visible = true, 877 976 InactiveIcon, 878 977 ActiveIcon, 879 978 text, ··· 881 980 onClickCallbback, 882 981 small, 883 982 }: { 983 + visible?: boolean; 884 984 InactiveIcon: React.ReactElement; 885 985 ActiveIcon: React.ReactElement; 886 986 text: string; ··· 888 988 onClickCallbback: () => void; 889 989 small?: boolean | string; 890 990 }) { 991 + if (!visible) return null 891 992 if (small) 892 993 return ( 893 994 <button 894 - className={`flex flex-col items-center rounded-lg transition-colors ${small} gap-1 ${ 895 - active 896 - ? "text-gray-900 dark:text-gray-100" 897 - : "text-gray-600 dark:text-gray-400" 898 - }`} 995 + className={`flex flex-col items-center rounded-lg transition-colors ${small} gap-1 ${active 996 + ? "text-gray-900 dark:text-gray-100" 997 + : "text-gray-600 dark:text-gray-400" 998 + }`} 899 999 onClick={() => { 900 1000 onClickCallbback(); 901 1001 }} ··· 915 1015 916 1016 return ( 917 1017 <button 918 - 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 ${ 919 - active 920 - ? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-800 bg-gray-200 hover:dark:bg-gray-700" 921 - : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-900" 922 - }`} 1018 + 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 ${active 1019 + ? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-800 bg-gray-200 hover:dark:bg-gray-700" 1020 + : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-900" 1021 + }`} 923 1022 onClick={() => { 924 1023 onClickCallbback(); 925 1024 }} ··· 954 1053 const active = false; 955 1054 return ( 956 1055 <button 957 - className={`flex border border-gray-400 dark:border-gray-400 flex-row h-12 min-h-12 max-h-12 ${small ? "p-3 w-12" : "px-4 py-0.5"} items-center rounded-full transition-colors gap-1 ${ 958 - active 959 - ? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-700 bg-gray-200 hover:dark:bg-gray-600" 960 - : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800" 961 - }`} 1056 + className={`flex border border-gray-400 dark:border-gray-400 flex-row h-12 min-h-12 max-h-12 ${small ? "p-3 w-12" : "px-4 py-0.5"} items-center rounded-full transition-colors gap-1 ${active 1057 + ? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-700 bg-gray-200 hover:dark:bg-gray-600" 1058 + : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800" 1059 + }`} 962 1060 onClick={() => { 963 1061 onClickCallbback(); 964 1062 }} ··· 976 1074 </button> 977 1075 ); 978 1076 } 1077 + 1078 + 1079 + export const ProfileSmall = ({ 1080 + did, 1081 + large = false, 1082 + }: { 1083 + did: string, 1084 + large?: boolean; 1085 + }) => { 1086 + const navigate = useNavigate(); 1087 + const { data: identity } = useQueryIdentity(did); 1088 + const { data: profiledata } = useQueryProfile( 1089 + `at://${did}/app.bsky.actor.profile/self` 1090 + ); 1091 + const profile = profiledata?.value; 1092 + 1093 + const [imgcdn] = useAtom(imgCDNAtom) 1094 + 1095 + function getAvatarUrl(p: typeof profile) { 1096 + const link = p?.avatar?.ref?.["$link"]; 1097 + if (!link || !did) return null; 1098 + return `https://${imgcdn}/img/avatar/plain/${did}/${link}@jpeg`; 1099 + } 1100 + 1101 + const onProfileClick = (e: React.MouseEvent<Element, MouseEvent>) => { 1102 + e.stopPropagation(); 1103 + navigate({ 1104 + to: "/profile/$did", 1105 + params: { did: did }, 1106 + }); 1107 + } 1108 + 1109 + if (!profiledata) { 1110 + return ( 1111 + // Skeleton loader 1112 + <div 1113 + onClick={onProfileClick} 1114 + className={`hover:cursor-pointer flex items-center gap-2.5 animate-pulse ${large ? "mb-1" : ""}`} 1115 + > 1116 + <div 1117 + className={`rounded-full bg-gray-300 dark:bg-gray-700 ${large ? "w-10 h-10" : "w-[30px] h-[30px]"}`} 1118 + /> 1119 + <div className="flex flex-col gap-2"> 1120 + <div 1121 + className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? "h-4 w-28" : "h-3 w-20"}`} 1122 + /> 1123 + <div 1124 + className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? "h-4 w-20" : "h-3 w-16"}`} 1125 + /> 1126 + </div> 1127 + </div> 1128 + ); 1129 + } 1130 + 1131 + return ( 1132 + <div 1133 + onClick={onProfileClick} 1134 + className={`hover:cursor-pointer flex flex-row items-center gap-2.5 ${large ? "mb-1" : ""}`} 1135 + > 1136 + <img 1137 + src={getAvatarUrl(profile) ?? undefined} 1138 + alt="avatar" 1139 + className={`object-cover rounded-full ${large ? "w-10 h-10" : "w-[30px] h-[30px]"}`} 1140 + /> 1141 + <div className="flex flex-col items-start text-left"> 1142 + <div 1143 + className={`font-medium ${large ? "text-gray-800 dark:text-gray-100 text-md" : "text-gray-800 dark:text-gray-100 text-sm"}`} 1144 + > 1145 + {profile?.displayName} 1146 + </div> 1147 + <div 1148 + className={` ${large ? "text-gray-500 dark:text-gray-400 text-sm" : "text-gray-500 dark:text-gray-400 text-xs"}`} 1149 + > 1150 + @{identity?.handle} 1151 + </div> 1152 + </div> 1153 + </div> 1154 + ); 1155 + }; 1156 + 1157 + 1158 + function FeedListDesktopSidebar() { 1159 + const { agent, status } = useAuth(); 1160 + const [quickAuth] = useAtom(quickAuthAtom); 1161 + const isAuthRestoring = quickAuth ? status === "loading" : false; 1162 + 1163 + const identityresultmaybe = useQueryIdentity( 1164 + !isAuthRestoring ? agent?.did : undefined, 1165 + ); 1166 + const identity = identityresultmaybe?.data; 1167 + 1168 + const prefsresultmaybe = useQueryPreferences({ 1169 + agent: !isAuthRestoring ? (agent ?? undefined) : undefined, 1170 + pdsUrl: !isAuthRestoring ? identity?.pds : undefined, 1171 + }); 1172 + const prefs = prefsresultmaybe?.data; 1173 + 1174 + const savedFeeds = React.useMemo(() => { 1175 + const savedFeedsPref = prefs?.preferences?.find( 1176 + (p: any) => p?.$type === "app.bsky.actor.defs#savedFeedsPrefV2", 1177 + ); 1178 + return savedFeedsPref?.items || []; 1179 + }, [prefs]); 1180 + 1181 + const pinnedFeeds = React.useMemo(() => { 1182 + return savedFeeds.filter((feed: any) => feed.pinned); 1183 + }, [savedFeeds]); 1184 + 1185 + const shimmedunautheddefault = HOST_UNAUTHED_DEFAULT_FEEDS.map((aturi: string, idx: number) => { 1186 + return { 1187 + value: aturi, 1188 + pinned: true, 1189 + } 1190 + }) 1191 + 1192 + const feedsmap = agent?.did ? pinnedFeeds : shimmedunautheddefault; 1193 + 1194 + return ( 1195 + <div className="flex flex-col gap-1 items-start "> 1196 + {feedsmap.map((item: any, idx: number) => { return <FeedTabOnTop key={item} item={item} idx={idx} rightDesktopSidebar={true} /> })} 1197 + </div> 1198 + ) 1199 + } 1200 + 1201 + function LoginRedirect() { 1202 + const location = useLocation(); 1203 + const dontShowLoginButton = location.pathname === "/settings" 1204 + return ( 1205 + <div className=""> 1206 + <span className="text-gray-500 dark:text-gray-400 text-sm leading-tight"> 1207 + {HOST_LOGIN_BLURB} 1208 + </span> 1209 + 1210 + <div className="flex flex-col gap-2 my-4"> 1211 + {!dontShowLoginButton && (<Link 1212 + to="/settings" 1213 + className="w-full rounded-full bg-gray-600 text-gray-100 dark:bg-gray-400 dark:text-gray-900 px-4 py-2 text-sm font-medium text-center" 1214 + > 1215 + Log in 1216 + </Link>)} 1217 + 1218 + {HOST_SIGNUP_PDS && ( 1219 + // todo make signup actually work 1220 + <button 1221 + className="w-full rounded-sm border border-gray-300 dark:border-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300" 1222 + > 1223 + Sign up 1224 + </button> 1225 + )} 1226 + </div> 1227 + </div> 1228 + ) 1229 + }
+42
src/routes/feeds.tsx
··· 166 166 </Link> 167 167 ); 168 168 } 169 + 170 + export function FeedIcon({ feedUri, className = "w-10 h-10 rounded-sm object-cover" }: {feedUri: string, className?: string }) { 171 + const { data: feedData } = useQueryArbitrary(feedUri); 172 + const feed = feedData?.value as ATPAPI.AppBskyFeedGenerator.Record; 173 + const [imgcdn] = useAtom(imgCDNAtom); 174 + let aturi: ATPAPI.AtUri | null = null; 175 + try { 176 + aturi = new ATPAPI.AtUri(feedUri); 177 + } catch (err) { 178 + // todo terrible hack lmaoo (hack type: forcing following feed to fallback to rinds fresh feed) 179 + aturi = new ATPAPI.AtUri("at://did:plc:mn45tewwnse5btfftvd3powc/app.bsky.feed.generator/rinds"); 180 + } 181 + 182 + function getAvatarUrl() { 183 + const link = feed?.avatar?.ref?.["$link"]; 184 + if (!link) return null; 185 + return `https://${imgcdn}/img/avatar/plain/${aturi?.host}/${link}@jpeg`; 186 + } 187 + 188 + const avatarUrl = getAvatarUrl(); 189 + if (!avatarUrl) { 190 + return ( 191 + <div 192 + className={className} 193 + > 194 + <IconMaterialSymbolsRssFeed className="text-gray-200 p-0.5 rounded-sm bg-gray-600" /> 195 + </div> 196 + ) 197 + } 198 + return ( 199 + <img 200 + src={avatarUrl} 201 + alt={feed?.displayName || "Feed avatar"} 202 + className={className} 203 + onError={(e) => { 204 + const target = e.target as HTMLImageElement; 205 + target.onerror = null; 206 + target.src = "/defaultpfp.png"; 207 + }} 208 + /> 209 + ) 210 + }
+36 -9
src/routes/index.tsx
··· 1 - import { createFileRoute } from "@tanstack/react-router"; 1 + import { createFileRoute, useLocation, useNavigate } from "@tanstack/react-router"; 2 2 import { useAtom } from "jotai"; 3 3 import * as React from "react"; 4 4 import { useLayoutEffect, useState } from "react"; ··· 22 22 useQueryIdentity, 23 23 useQueryPreferences, 24 24 } from "~/utils/useQuery"; 25 + 26 + import { FeedIcon } from "./feeds"; 25 27 26 28 export const Route = createFileRoute("/")({ 27 29 // loader: async ({ context }) => { ··· 180 182 return savedFeedsPref?.items || []; 181 183 }, [prefs]); 182 184 185 + const pinnedFeeds = React.useMemo(() => { 186 + return savedFeeds.filter((feed: any) => feed.pinned); 187 + }, [savedFeeds]); 188 + 183 189 const [persistentSelectedFeed, setPersistentSelectedFeed] = useAtom(selectedFeedUriAtom); 184 190 const [unauthedSelectedFeed, setUnauthedSelectedFeed] = useState(persistentSelectedFeed); 185 191 const selectedFeed = agent?.did ··· 363 369 <div 364 370 className={`relative flex flex-col ${hidden && "hidden"}`} 365 371 > 366 - {!isAuthRestoring && savedFeeds.length > 0 ? ( 372 + {!isAuthRestoring && pinnedFeeds.length > 0 ? ( 367 373 <div className={`flex items-center px-4 py-2 h-[52px] sticky top-0 bg-[var(--header-bg-light)] dark:bg-[var(--header-bg-dark)] ${!isAtTop && "shadow-sm"} sm:shadow-none sm:bg-white sm:dark:bg-gray-950 z-10 border-0 sm:border-b border-gray-200 dark:border-gray-700 overflow-x-auto overflow-y-hidden scroll-thin`}> 368 - {savedFeeds.map((item: any, idx: number) => { return <FeedTabOnTop key={item} item={item} idx={idx} /> })} 374 + {pinnedFeeds.map((item: any, idx: number) => { return <FeedTabOnTop key={item} item={item} idx={idx} /> })} 369 375 </div> 370 376 ) : ( 371 377 // <span className="text-xl font-bold ml-2">Home</span> ··· 424 430 425 431 // todo please use types this is dangerous very dangerous. 426 432 // todo fix this whenever proper preferences is handled 427 - function FeedTabOnTop({ item, idx }: { item: any, idx: number }) { 433 + export function FeedTabOnTop({ 434 + item, 435 + idx, 436 + rightDesktopSidebar = false 437 + } : { 438 + item: any, 439 + idx: number, 440 + rightDesktopSidebar?: boolean 441 + }) { 442 + const location = useLocation(); 443 + const navigate = useNavigate(); 444 + const isAtHome = location.pathname == "/" || location.pathname == ""; 428 445 const [persistentSelectedFeed, setPersistentSelectedFeed] = useAtom(selectedFeedUriAtom); 429 446 const selectedFeed = persistentSelectedFeed 430 447 const setSelectedFeed = setPersistentSelectedFeed ··· 435 452 return ( 436 453 <button 437 454 key={item.value || idx} 438 - className={`px-3 py-1 rounded-full whitespace-nowrap font-medium transition-colors ${isActive 439 - ? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-700 bg-gray-200 hover:dark:bg-gray-600" 455 + className={`${rightDesktopSidebar ? "flex flex-row items-center gap-2 pr-4 pl-2.5 py-1.5": "px-3 py-1 font-medium"} rounded-full whitespace-nowrap transition-colors ${isActive 456 + ? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-700 bg-gray-200 hover:dark:bg-gray-600 font-medium" 440 457 : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800" 441 458 // ? "bg-gray-500 text-white" 442 459 // : item.pinned 443 460 // ? "bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-200" 444 461 // : "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-200" 445 462 }`} 446 - onClick={() => setSelectedFeed(item.value)} 463 + onClick={() => { 464 + if (rightDesktopSidebar && !isAtHome) { 465 + navigate({ 466 + to: "/" 467 + }) 468 + } 469 + setSelectedFeed(item.value) 470 + }} 447 471 title={item.value} 448 472 > 473 + {rightDesktopSidebar && ( 474 + <FeedIcon feedUri={item.value} className="w-5 h-5 rounded-sm object-cover" /> 475 + )} 449 476 {label} 450 - {item.pinned && ( 477 + {/* {!rightDesktopSidebar && item.pinned && ( 451 478 <span 452 479 className={`ml-1 text-xs ${isActive 453 480 ? "text-gray-900 dark:text-gray-100" ··· 456 483 > 457 484 458 485 </span> 459 - )} 486 + )} */} 460 487 </button> 461 488 ); 462 489 }
+55 -2
src/routes/settings.tsx
··· 6 6 import { HOST_TITLE } from "~/../policy"; 7 7 import { Header } from "~/components/Header"; 8 8 import Login from "~/components/Login"; 9 + import { useAuth } from "~/providers/UnifiedAuthProvider"; 9 10 import { 10 11 constellationURLAtom, 11 12 defaultconstellationURL, ··· 32 33 33 34 export function Settings() { 34 35 const navigate = useNavigate(); 36 + const { agent } = useAuth(); 35 37 return ( 36 38 <> 37 39 <Header ··· 44 46 } 45 47 }} 46 48 /> 47 - <div className="lg:hidden"> 48 - <Login /> 49 + {/* <div className="lg:hidden"> */} 50 + <div className="flex flex-col justify-around mt-4"> 51 + <SettingHeading title="Account Management" top /> 52 + <div className="mx-4"> 53 + <Login /> 54 + </div> 49 55 </div> 56 + {/* Small viewport nav overflow */} 50 57 <div className="sm:hidden flex flex-col justify-around mt-4"> 51 58 <SettingHeading title="Other Pages" top /> 52 59 <MaterialNavItem 60 + visible={!agent?.did} 61 + InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 62 + ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 63 + active={false} 64 + onClickCallbback={() => 65 + navigate({ 66 + to: "/search", 67 + //params: { did: agent.assertDid }, 68 + }) 69 + } 70 + text="Search" 71 + /> 72 + <MaterialNavItem 73 + visible={!!agent?.did} 53 74 InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 54 75 ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 55 76 active={false} ··· 62 83 text="Feeds" 63 84 /> 64 85 <MaterialNavItem 86 + visible={!!agent?.did} 65 87 InactiveIcon={<IconMdiShieldOutline className="w-6 h-6" />} 66 88 ActiveIcon={<IconMdiShield className="w-6 h-6" />} 67 89 active={false} ··· 72 94 }) 73 95 } 74 96 text="Moderation" 97 + /> 98 + <MaterialNavItem 99 + visible={true} 100 + InactiveIcon={<IconMaterialSymbolsInfoOutline className="w-6 h-6" />} 101 + ActiveIcon={<IconMaterialSymbolsInfoOutline className="w-6 h-6" />} 102 + active={false} 103 + onClickCallbback={() => 104 + navigate({ 105 + to: "/about", 106 + //params: { did: agent.assertDid }, 107 + }) 108 + } 109 + text="About" 110 + /> 111 + </div> 112 + {/* <div className="lg:hidden sm:flex hidden flex-col justify-around mt-4"> */} 113 + {/* Large viewport nav overflow */} 114 + <div className=" sm:flex hidden flex-col justify-around mt-4"> 115 + <SettingHeading title="Other Pages" top /> 116 + <MaterialNavItem 117 + visible={true} 118 + InactiveIcon={<IconMaterialSymbolsInfoOutline className="w-6 h-6" />} 119 + ActiveIcon={<IconMaterialSymbolsInfoOutline className="w-6 h-6" />} 120 + active={false} 121 + onClickCallbback={() => 122 + navigate({ 123 + to: "/about", 124 + //params: { did: agent.assertDid }, 125 + }) 126 + } 127 + text="About" 75 128 /> 76 129 </div> 77 130 <div className="h-4" />