tangled
alpha
login
or
join now
whey.party
/
red-dwarf
82
fork
atom
an independent Bluesky client using Constellation, PDS Queries, and other services
reddwarf.app
frontend
spa
bluesky
reddwarf
microcosm
client
app
82
fork
atom
overview
issues
25
pulls
pipelines
sidebar redesign and about page integration
whey.party
3 weeks ago
8aa33571
d59fa537
+490
-106
6 changed files
expand all
collapse all
unified
split
src
components
Login.tsx
LogoSvg.tsx
routes
__root.tsx
feeds.tsx
index.tsx
settings.tsx
+6
-4
src/components/Login.tsx
···
24
24
className={
25
25
compact
26
26
? "flex items-center justify-center p-1"
27
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
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
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
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
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
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
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
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
249
-
Warning: Less secure. Use an App Password.
251
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
4
+
import { HOST_LOGO_USE_FAVICON } from '~/../policy';
5
5
+
6
6
+
export default function LogoSVG(props: SVGProps<SVGSVGElement>) {
7
7
+
if (HOST_LOGO_USE_FAVICON) {
8
8
+
return (<img src={"/favicon.png"} width={32} height={32} {...props as any}/>)
9
9
+
}
10
10
+
return (<FluentEmojiHighContrastGlowingStar {...props} />)
11
11
+
}
12
12
+
4
13
// FluentEmojiHighContrastGlowingStar
5
5
-
export default function FluentEmojiHighContrastGlowingStar(props: SVGProps<SVGSVGElement>) {
14
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
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
21
-
import { HOST_TITLE } from "~/../policy";
22
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
25
-
import Login from "~/components/Login";
26
26
+
//import Login from "~/components/Login";
26
27
import Logo from "~/components/LogoSvg";
27
27
-
import { ModerationBatcher } from "~/components/ModerationBatcher";
28
28
-
import { ModerationInitializer } from "~/components/ModerationInitializer";
28
28
+
//import { ModerationBatcher } from "~/components/ModerationBatcher";
29
29
+
//import { ModerationInitializer } from "~/components/ModerationInitializer";
29
30
import { NotFound } from "~/components/NotFound";
31
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
33
-
import { composerAtom, hueAtom, useAtomCssVar } from "~/utils/atoms";
35
35
+
import { FeedTabOnTop } from "~/routes/index";
36
36
+
import { composerAtom, hueAtom, imgCDNAtom, quickAuthAtom, useAtomCssVar } from "~/utils/atoms";
34
37
import { seo } from "~/utils/seo";
38
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
78
-
<RootDocument>
79
79
-
<DefaultCatchBoundary {...props} />
80
80
-
</RootDocument>
81
81
-
),
82
82
+
<RootDocument>
83
83
+
<DefaultCatchBoundary {...props} />
84
84
+
</RootDocument>
85
85
+
),
82
86
notFoundComponent: () => <NotFound />,
83
87
component: RootComponent,
84
88
});
···
86
90
function RootComponent() {
87
91
return (
88
92
<UnifiedAuthProvider>
89
89
-
<LikeMutationQueueProvider>
90
90
-
<PollMutationQueueProvider>
91
91
-
<ModerationInitializer />
92
92
-
<ModerationBatcher />
93
93
-
<RootDocument>
94
94
-
<KeepAliveProvider>
95
95
-
<AppToaster />
96
96
-
<KeepAliveOutlet />
97
97
-
</KeepAliveProvider>
98
98
-
</RootDocument>
99
99
-
</PollMutationQueueProvider>
100
100
-
</LikeMutationQueueProvider>
93
93
+
<AutoLabelProvider>
94
94
+
<LikeMutationQueueProvider>
95
95
+
<PollMutationQueueProvider>
96
96
+
{/* <ModerationInitializer />
97
97
+
<ModerationBatcher /> */}
98
98
+
<RootDocument>
99
99
+
<KeepAliveProvider>
100
100
+
<AppToaster />
101
101
+
<KeepAliveOutlet />
102
102
+
</KeepAliveProvider>
103
103
+
</RootDocument>
104
104
+
</PollMutationQueueProvider>
105
105
+
</LikeMutationQueueProvider>
106
106
+
</AutoLabelProvider>
101
107
</UnifiedAuthProvider>
102
108
);
103
109
}
···
126
132
button={
127
133
button?.label
128
134
? {
129
129
-
label: button?.label,
130
130
-
onClick: () => {
131
131
-
button?.onClick?.();
132
132
-
},
133
133
-
}
135
135
+
label: button?.label,
136
136
+
onClick: () => {
137
137
+
button?.onClick?.();
138
138
+
},
139
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
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
240
+
| "about"
233
241
| "home" = isFeeds
234
234
-
? "feeds"
235
235
-
: isSearch
236
236
-
? "search"
237
237
-
: isSettings
238
238
-
? "settings"
239
239
-
: isNotifications
240
240
-
? "notifications"
241
241
-
: isProfile
242
242
-
? "profile"
243
243
-
: isModeration
244
244
-
? "moderation"
245
245
-
: "home";
242
242
+
? "feeds"
243
243
+
: isSearch
244
244
+
? "search"
245
245
+
: isSettings
246
246
+
? "settings"
247
247
+
: isNotifications
248
248
+
? "notifications"
249
249
+
: isProfile
250
250
+
? "profile"
251
251
+
: isModeration
252
252
+
? "moderation"
253
253
+
: isAbout ?
254
254
+
"about"
255
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
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
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
264
-
{HOST_TITLE}{" "}
265
265
-
{/* <span className="text-gray-500 dark:text-gray-400 text-sm">
266
266
-
lite
267
267
-
</span> */}
274
274
+
{HOST_MAIN_TITLE}
275
275
+
{HOST_SUB_TITLE && (<span className="text-gray-500 dark:text-gray-400 text-sm">
276
276
+
{HOST_SUB_TITLE}
277
277
+
</span>) }
268
278
</span>
269
279
</div>
270
280
<MaterialNavItem
···
295
305
text="Explore"
296
306
/>
297
307
<MaterialNavItem
308
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
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
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
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
370
-
<div className="flex flex-row items-center justify-center mt-3">
371
371
-
<MaterialPillButton
372
372
-
InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
373
373
-
ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
374
374
-
//active={true}
375
375
-
onClickCallbback={() => setComposerPost({ kind: "root" })}
376
376
-
text="Post"
384
384
+
{!agent?.did && (
385
385
+
<MaterialNavItem
386
386
+
InactiveIcon={
387
387
+
<IconMaterialSymbolsInfoOutline className="w-6 h-6" />
388
388
+
}
389
389
+
ActiveIcon={<IconMaterialSymbolsInfo className="w-6 h-6" />}
390
390
+
active={locationEnum === "about"}
391
391
+
onClickCallbback={() =>
392
392
+
navigate({
393
393
+
to: "/about",
394
394
+
//params: { did: agent.assertDid },
395
395
+
})
396
396
+
}
397
397
+
text="About"
377
398
/>
378
378
-
</div>
399
399
+
)}
400
400
+
{agent?.did && (
401
401
+
<div className="flex flex-row items-center justify-center mt-3">
402
402
+
<MaterialPillButton
403
403
+
InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
404
404
+
ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
405
405
+
//active={true}
406
406
+
onClickCallbback={() => setComposerPost({ kind: "root" })}
407
407
+
text="Post"
408
408
+
/>
409
409
+
</div>
410
410
+
)}
411
411
+
{!agent?.did && (
412
412
+
<>
413
413
+
<div className="mt-4 mb-2 w-full h-[1px] bg-gray-200 dark:bg-gray-800" />
414
414
+
{/* <Login /> */}
415
415
+
<LoginRedirect />
416
416
+
</>
417
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
521
+
{!!agent?.did && (
522
522
+
<div className="flex flex-row items-center lg:mb-1">
523
523
+
<div className="flex p-2 h-12 flex-1 rounded-full hover:dark:bg-gray-800 hover:bg-gray-200">
524
524
+
<ProfileSmall did={agent.did} />
525
525
+
</div>
526
526
+
<Link
527
527
+
to="/settings"
528
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
529
+
>
530
530
+
<IconMaterialSymbolsMoreVert />
531
531
+
</Link>
532
532
+
</div>
533
533
+
)}
534
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
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
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
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
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
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
628
-
<div className="flex flex-row items-center justify-center mt-3">
629
629
-
<MaterialPillButton
686
686
+
687
687
+
{!agent?.did && (
688
688
+
<MaterialNavItem
630
689
small
631
631
-
InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
632
632
-
ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
633
633
-
//active={true}
634
634
-
onClickCallbback={() => setComposerPost({ kind: "root" })}
635
635
-
text="Post"
690
690
+
InactiveIcon={
691
691
+
<IconMaterialSymbolsInfoOutline className="w-6 h-6" />
692
692
+
}
693
693
+
ActiveIcon={<IconMaterialSymbolsInfo className="w-6 h-6" />}
694
694
+
active={locationEnum === "about"}
695
695
+
onClickCallbback={() =>
696
696
+
navigate({
697
697
+
to: "/about",
698
698
+
//params: { did: agent.assertDid },
699
699
+
})
700
700
+
}
701
701
+
text="About"
636
702
/>
637
637
-
</div>
703
703
+
)}
704
704
+
{!!agent?.did && (
705
705
+
<div className="flex flex-row items-center justify-center mt-3">
706
706
+
<MaterialPillButton
707
707
+
small
708
708
+
InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
709
709
+
ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
710
710
+
//active={true}
711
711
+
onClickCallbback={() => setComposerPost({ kind: "root" })}
712
712
+
text="Post"
713
713
+
/>
714
714
+
</div>
715
715
+
)}
638
716
</nav>
639
717
640
718
{agent?.did && (
···
657
735
{children}
658
736
</main>
659
737
660
660
-
<aside className="hidden lg:flex h-screen w-[250px] sticky top-0 self-start flex-col">
661
661
-
<div className="px-4 pt-4">
738
738
+
<aside className="hidden lg:flex h-screen xl:w-[300px] w-[250px] sticky top-0 self-start flex-col">
739
739
+
<div className="px-4 pt-4 gap-4 flex flex-col">
662
740
<Import />
663
741
</div>
664
664
-
<Login />
742
742
+
<div className="px-4 pt-4 gap-4 flex flex-col max-h-[calc(100dvh - 80px)] overflow-y-auto">
743
743
+
{(
744
744
+
(!agent?.did && HOST_UNAUTHED_DEFAULT_FEEDS.length > 0)
745
745
+
|| (!!agent?.did)
746
746
+
) && (
747
747
+
<FeedListDesktopSidebar />
748
748
+
)}
749
749
+
{!agent?.did && (
750
750
+
<>
751
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
752
+
<img className="rounded-sm" src={HOST_HERO} />
753
753
+
<span className=" text-gray-500 dark:text-gray-400 text-sm">{HOST_DESCRIPTION}</span>
754
754
+
<div className="flex flex-col gap-1 ">
755
755
+
<span className="text-gray-500 dark:text-gray-400 text-sm font-bold">ADMINISTERED BY:</span>
756
756
+
<ProfileSmall did={HOST_ADMIN} />
757
757
+
</div>
758
758
+
</>
759
759
+
)}
760
760
+
</div>
665
761
<div className="flex-1"></div>
666
666
-
<p className="text-xs text-gray-400 dark:text-gray-500 text-justify mx-4 mb-4">
667
667
-
{HOST_TITLE} is a Bluesky client that does not rely on any Bluesky API
668
668
-
App Servers. Instead, it uses Microcosm to fetch records directly
669
669
-
from each users' PDS (via Slingshot) and connect them using
670
670
-
backlinks (via Constellation)
671
671
-
</p>
762
762
+
{/* todo */}
763
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
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
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
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
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
858
-
{HOST_TITLE}{" "}
859
859
-
{/* <span className="text-gray-500 dark:text-gray-400 text-sm">
860
860
-
lite
861
861
-
</span> */}
950
950
+
{HOST_MAIN_TITLE}
951
951
+
{HOST_SUB_TITLE && (<span className="text-gray-500 dark:text-gray-400 text-sm">
952
952
+
{HOST_SUB_TITLE}
953
953
+
</span>) }
862
954
</span>
863
955
</div>
864
956
<div className="flex items-center gap-2">
865
865
-
<Login compact={true} popup={true} />
957
957
+
{/* <Login compact={true} popup={true} /> */}
958
958
+
<Link
959
959
+
to="/settings"
960
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
961
+
>
962
962
+
Log in
963
963
+
</Link>
866
964
</div>
867
965
</div>
868
966
)}
···
874
972
}
875
973
876
974
export function MaterialNavItem({
975
975
+
visible = true,
877
976
InactiveIcon,
878
977
ActiveIcon,
879
978
text,
···
881
980
onClickCallbback,
882
981
small,
883
982
}: {
983
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
991
+
if (!visible) return null
891
992
if (small)
892
993
return (
893
994
<button
894
894
-
className={`flex flex-col items-center rounded-lg transition-colors ${small} gap-1 ${
895
895
-
active
896
896
-
? "text-gray-900 dark:text-gray-100"
897
897
-
: "text-gray-600 dark:text-gray-400"
898
898
-
}`}
995
995
+
className={`flex flex-col items-center rounded-lg transition-colors ${small} gap-1 ${active
996
996
+
? "text-gray-900 dark:text-gray-100"
997
997
+
: "text-gray-600 dark:text-gray-400"
998
998
+
}`}
899
999
onClick={() => {
900
1000
onClickCallbback();
901
1001
}}
···
915
1015
916
1016
return (
917
1017
<button
918
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
919
-
active
920
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
921
-
: "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-900"
922
922
-
}`}
1018
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
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
1020
+
: "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-900"
1021
1021
+
}`}
923
1022
onClick={() => {
924
1023
onClickCallbback();
925
1024
}}
···
954
1053
const active = false;
955
1054
return (
956
1055
<button
957
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
958
-
active
959
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
960
-
: "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800"
961
961
-
}`}
1056
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
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
1058
+
: "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800"
1059
1059
+
}`}
962
1060
onClick={() => {
963
1061
onClickCallbback();
964
1062
}}
···
976
1074
</button>
977
1075
);
978
1076
}
1077
1077
+
1078
1078
+
1079
1079
+
export const ProfileSmall = ({
1080
1080
+
did,
1081
1081
+
large = false,
1082
1082
+
}: {
1083
1083
+
did: string,
1084
1084
+
large?: boolean;
1085
1085
+
}) => {
1086
1086
+
const navigate = useNavigate();
1087
1087
+
const { data: identity } = useQueryIdentity(did);
1088
1088
+
const { data: profiledata } = useQueryProfile(
1089
1089
+
`at://${did}/app.bsky.actor.profile/self`
1090
1090
+
);
1091
1091
+
const profile = profiledata?.value;
1092
1092
+
1093
1093
+
const [imgcdn] = useAtom(imgCDNAtom)
1094
1094
+
1095
1095
+
function getAvatarUrl(p: typeof profile) {
1096
1096
+
const link = p?.avatar?.ref?.["$link"];
1097
1097
+
if (!link || !did) return null;
1098
1098
+
return `https://${imgcdn}/img/avatar/plain/${did}/${link}@jpeg`;
1099
1099
+
}
1100
1100
+
1101
1101
+
const onProfileClick = (e: React.MouseEvent<Element, MouseEvent>) => {
1102
1102
+
e.stopPropagation();
1103
1103
+
navigate({
1104
1104
+
to: "/profile/$did",
1105
1105
+
params: { did: did },
1106
1106
+
});
1107
1107
+
}
1108
1108
+
1109
1109
+
if (!profiledata) {
1110
1110
+
return (
1111
1111
+
// Skeleton loader
1112
1112
+
<div
1113
1113
+
onClick={onProfileClick}
1114
1114
+
className={`hover:cursor-pointer flex items-center gap-2.5 animate-pulse ${large ? "mb-1" : ""}`}
1115
1115
+
>
1116
1116
+
<div
1117
1117
+
className={`rounded-full bg-gray-300 dark:bg-gray-700 ${large ? "w-10 h-10" : "w-[30px] h-[30px]"}`}
1118
1118
+
/>
1119
1119
+
<div className="flex flex-col gap-2">
1120
1120
+
<div
1121
1121
+
className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? "h-4 w-28" : "h-3 w-20"}`}
1122
1122
+
/>
1123
1123
+
<div
1124
1124
+
className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? "h-4 w-20" : "h-3 w-16"}`}
1125
1125
+
/>
1126
1126
+
</div>
1127
1127
+
</div>
1128
1128
+
);
1129
1129
+
}
1130
1130
+
1131
1131
+
return (
1132
1132
+
<div
1133
1133
+
onClick={onProfileClick}
1134
1134
+
className={`hover:cursor-pointer flex flex-row items-center gap-2.5 ${large ? "mb-1" : ""}`}
1135
1135
+
>
1136
1136
+
<img
1137
1137
+
src={getAvatarUrl(profile) ?? undefined}
1138
1138
+
alt="avatar"
1139
1139
+
className={`object-cover rounded-full ${large ? "w-10 h-10" : "w-[30px] h-[30px]"}`}
1140
1140
+
/>
1141
1141
+
<div className="flex flex-col items-start text-left">
1142
1142
+
<div
1143
1143
+
className={`font-medium ${large ? "text-gray-800 dark:text-gray-100 text-md" : "text-gray-800 dark:text-gray-100 text-sm"}`}
1144
1144
+
>
1145
1145
+
{profile?.displayName}
1146
1146
+
</div>
1147
1147
+
<div
1148
1148
+
className={` ${large ? "text-gray-500 dark:text-gray-400 text-sm" : "text-gray-500 dark:text-gray-400 text-xs"}`}
1149
1149
+
>
1150
1150
+
@{identity?.handle}
1151
1151
+
</div>
1152
1152
+
</div>
1153
1153
+
</div>
1154
1154
+
);
1155
1155
+
};
1156
1156
+
1157
1157
+
1158
1158
+
function FeedListDesktopSidebar() {
1159
1159
+
const { agent, status } = useAuth();
1160
1160
+
const [quickAuth] = useAtom(quickAuthAtom);
1161
1161
+
const isAuthRestoring = quickAuth ? status === "loading" : false;
1162
1162
+
1163
1163
+
const identityresultmaybe = useQueryIdentity(
1164
1164
+
!isAuthRestoring ? agent?.did : undefined,
1165
1165
+
);
1166
1166
+
const identity = identityresultmaybe?.data;
1167
1167
+
1168
1168
+
const prefsresultmaybe = useQueryPreferences({
1169
1169
+
agent: !isAuthRestoring ? (agent ?? undefined) : undefined,
1170
1170
+
pdsUrl: !isAuthRestoring ? identity?.pds : undefined,
1171
1171
+
});
1172
1172
+
const prefs = prefsresultmaybe?.data;
1173
1173
+
1174
1174
+
const savedFeeds = React.useMemo(() => {
1175
1175
+
const savedFeedsPref = prefs?.preferences?.find(
1176
1176
+
(p: any) => p?.$type === "app.bsky.actor.defs#savedFeedsPrefV2",
1177
1177
+
);
1178
1178
+
return savedFeedsPref?.items || [];
1179
1179
+
}, [prefs]);
1180
1180
+
1181
1181
+
const pinnedFeeds = React.useMemo(() => {
1182
1182
+
return savedFeeds.filter((feed: any) => feed.pinned);
1183
1183
+
}, [savedFeeds]);
1184
1184
+
1185
1185
+
const shimmedunautheddefault = HOST_UNAUTHED_DEFAULT_FEEDS.map((aturi: string, idx: number) => {
1186
1186
+
return {
1187
1187
+
value: aturi,
1188
1188
+
pinned: true,
1189
1189
+
}
1190
1190
+
})
1191
1191
+
1192
1192
+
const feedsmap = agent?.did ? pinnedFeeds : shimmedunautheddefault;
1193
1193
+
1194
1194
+
return (
1195
1195
+
<div className="flex flex-col gap-1 items-start ">
1196
1196
+
{feedsmap.map((item: any, idx: number) => { return <FeedTabOnTop key={item} item={item} idx={idx} rightDesktopSidebar={true} /> })}
1197
1197
+
</div>
1198
1198
+
)
1199
1199
+
}
1200
1200
+
1201
1201
+
function LoginRedirect() {
1202
1202
+
const location = useLocation();
1203
1203
+
const dontShowLoginButton = location.pathname === "/settings"
1204
1204
+
return (
1205
1205
+
<div className="">
1206
1206
+
<span className="text-gray-500 dark:text-gray-400 text-sm leading-tight">
1207
1207
+
{HOST_LOGIN_BLURB}
1208
1208
+
</span>
1209
1209
+
1210
1210
+
<div className="flex flex-col gap-2 my-4">
1211
1211
+
{!dontShowLoginButton && (<Link
1212
1212
+
to="/settings"
1213
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
1214
+
>
1215
1215
+
Log in
1216
1216
+
</Link>)}
1217
1217
+
1218
1218
+
{HOST_SIGNUP_PDS && (
1219
1219
+
// todo make signup actually work
1220
1220
+
<button
1221
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
1222
+
>
1223
1223
+
Sign up
1224
1224
+
</button>
1225
1225
+
)}
1226
1226
+
</div>
1227
1227
+
</div>
1228
1228
+
)
1229
1229
+
}
+42
src/routes/feeds.tsx
···
166
166
</Link>
167
167
);
168
168
}
169
169
+
170
170
+
export function FeedIcon({ feedUri, className = "w-10 h-10 rounded-sm object-cover" }: {feedUri: string, className?: string }) {
171
171
+
const { data: feedData } = useQueryArbitrary(feedUri);
172
172
+
const feed = feedData?.value as ATPAPI.AppBskyFeedGenerator.Record;
173
173
+
const [imgcdn] = useAtom(imgCDNAtom);
174
174
+
let aturi: ATPAPI.AtUri | null = null;
175
175
+
try {
176
176
+
aturi = new ATPAPI.AtUri(feedUri);
177
177
+
} catch (err) {
178
178
+
// todo terrible hack lmaoo (hack type: forcing following feed to fallback to rinds fresh feed)
179
179
+
aturi = new ATPAPI.AtUri("at://did:plc:mn45tewwnse5btfftvd3powc/app.bsky.feed.generator/rinds");
180
180
+
}
181
181
+
182
182
+
function getAvatarUrl() {
183
183
+
const link = feed?.avatar?.ref?.["$link"];
184
184
+
if (!link) return null;
185
185
+
return `https://${imgcdn}/img/avatar/plain/${aturi?.host}/${link}@jpeg`;
186
186
+
}
187
187
+
188
188
+
const avatarUrl = getAvatarUrl();
189
189
+
if (!avatarUrl) {
190
190
+
return (
191
191
+
<div
192
192
+
className={className}
193
193
+
>
194
194
+
<IconMaterialSymbolsRssFeed className="text-gray-200 p-0.5 rounded-sm bg-gray-600" />
195
195
+
</div>
196
196
+
)
197
197
+
}
198
198
+
return (
199
199
+
<img
200
200
+
src={avatarUrl}
201
201
+
alt={feed?.displayName || "Feed avatar"}
202
202
+
className={className}
203
203
+
onError={(e) => {
204
204
+
const target = e.target as HTMLImageElement;
205
205
+
target.onerror = null;
206
206
+
target.src = "/defaultpfp.png";
207
207
+
}}
208
208
+
/>
209
209
+
)
210
210
+
}
+36
-9
src/routes/index.tsx
···
1
1
-
import { createFileRoute } from "@tanstack/react-router";
1
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
25
+
26
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
185
+
const pinnedFeeds = React.useMemo(() => {
186
186
+
return savedFeeds.filter((feed: any) => feed.pinned);
187
187
+
}, [savedFeeds]);
188
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
366
-
{!isAuthRestoring && savedFeeds.length > 0 ? (
372
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
368
-
{savedFeeds.map((item: any, idx: number) => { return <FeedTabOnTop key={item} item={item} idx={idx} /> })}
374
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
427
-
function FeedTabOnTop({ item, idx }: { item: any, idx: number }) {
433
433
+
export function FeedTabOnTop({
434
434
+
item,
435
435
+
idx,
436
436
+
rightDesktopSidebar = false
437
437
+
} : {
438
438
+
item: any,
439
439
+
idx: number,
440
440
+
rightDesktopSidebar?: boolean
441
441
+
}) {
442
442
+
const location = useLocation();
443
443
+
const navigate = useNavigate();
444
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
438
-
className={`px-3 py-1 rounded-full whitespace-nowrap font-medium transition-colors ${isActive
439
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
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
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
446
-
onClick={() => setSelectedFeed(item.value)}
463
463
+
onClick={() => {
464
464
+
if (rightDesktopSidebar && !isAtHome) {
465
465
+
navigate({
466
466
+
to: "/"
467
467
+
})
468
468
+
}
469
469
+
setSelectedFeed(item.value)
470
470
+
}}
447
471
title={item.value}
448
472
>
473
473
+
{rightDesktopSidebar && (
474
474
+
<FeedIcon feedUri={item.value} className="w-5 h-5 rounded-sm object-cover" />
475
475
+
)}
449
476
{label}
450
450
-
{item.pinned && (
477
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
459
-
)}
486
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
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
36
+
const { agent } = useAuth();
35
37
return (
36
38
<>
37
39
<Header
···
44
46
}
45
47
}}
46
48
/>
47
47
-
<div className="lg:hidden">
48
48
-
<Login />
49
49
+
{/* <div className="lg:hidden"> */}
50
50
+
<div className="flex flex-col justify-around mt-4">
51
51
+
<SettingHeading title="Account Management" top />
52
52
+
<div className="mx-4">
53
53
+
<Login />
54
54
+
</div>
49
55
</div>
56
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
60
+
visible={!agent?.did}
61
61
+
InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
62
62
+
ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
63
63
+
active={false}
64
64
+
onClickCallbback={() =>
65
65
+
navigate({
66
66
+
to: "/search",
67
67
+
//params: { did: agent.assertDid },
68
68
+
})
69
69
+
}
70
70
+
text="Search"
71
71
+
/>
72
72
+
<MaterialNavItem
73
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
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
97
+
/>
98
98
+
<MaterialNavItem
99
99
+
visible={true}
100
100
+
InactiveIcon={<IconMaterialSymbolsInfoOutline className="w-6 h-6" />}
101
101
+
ActiveIcon={<IconMaterialSymbolsInfoOutline className="w-6 h-6" />}
102
102
+
active={false}
103
103
+
onClickCallbback={() =>
104
104
+
navigate({
105
105
+
to: "/about",
106
106
+
//params: { did: agent.assertDid },
107
107
+
})
108
108
+
}
109
109
+
text="About"
110
110
+
/>
111
111
+
</div>
112
112
+
{/* <div className="lg:hidden sm:flex hidden flex-col justify-around mt-4"> */}
113
113
+
{/* Large viewport nav overflow */}
114
114
+
<div className=" sm:flex hidden flex-col justify-around mt-4">
115
115
+
<SettingHeading title="Other Pages" top />
116
116
+
<MaterialNavItem
117
117
+
visible={true}
118
118
+
InactiveIcon={<IconMaterialSymbolsInfoOutline className="w-6 h-6" />}
119
119
+
ActiveIcon={<IconMaterialSymbolsInfoOutline className="w-6 h-6" />}
120
120
+
active={false}
121
121
+
onClickCallbback={() =>
122
122
+
navigate({
123
123
+
to: "/about",
124
124
+
//params: { did: agent.assertDid },
125
125
+
})
126
126
+
}
127
127
+
text="About"
75
128
/>
76
129
</div>
77
130
<div className="h-4" />