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

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" />