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
4 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
className={
25
compact
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]"
28
}
29
>
30
<span
···
43
// Large view
44
if (!compact) {
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">
47
<div className="flex flex-col items-center justify-center text-center">
48
<p className="text-lg font-semibold mb-4 text-gray-800 dark:text-gray-100">
49
You are logged in!
···
77
if (!compact) {
78
// Large view renders the form directly in the card
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">
81
<UnifiedLoginForm />
82
</div>
83
);
···
177
178
useEffect(() => {
179
const lastHandle = localStorage.getItem("lastHandle");
0
180
if (lastHandle) setHandle(lastHandle);
181
}, []);
182
···
229
230
useEffect(() => {
231
const lastHandle = localStorage.getItem("lastHandle");
0
232
if (lastHandle) setUser(lastHandle);
233
}, []);
234
···
246
return (
247
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
248
<p className="text-xs text-red-500 dark:text-red-400">
249
-
Warning: Less secure. Use an App Password.
250
</p>
251
{/* <input
252
type="text"
···
24
className={
25
compact
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 flex justify-center items-center h-[280px]"
28
}
29
>
30
<span
···
43
// Large view
44
if (!compact) {
45
return (
46
+
<div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800">
47
<div className="flex flex-col items-center justify-center text-center">
48
<p className="text-lg font-semibold mb-4 text-gray-800 dark:text-gray-100">
49
You are logged in!
···
77
if (!compact) {
78
// Large view renders the form directly in the card
79
return (
80
+
<div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800">
81
<UnifiedLoginForm />
82
</div>
83
);
···
177
178
useEffect(() => {
179
const lastHandle = localStorage.getItem("lastHandle");
180
+
// eslint-disable-next-line react-hooks/set-state-in-effect
181
if (lastHandle) setHandle(lastHandle);
182
}, []);
183
···
230
231
useEffect(() => {
232
const lastHandle = localStorage.getItem("lastHandle");
233
+
// eslint-disable-next-line react-hooks/set-state-in-effect
234
if (lastHandle) setUser(lastHandle);
235
}, []);
236
···
248
return (
249
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
250
<p className="text-xs text-red-500 dark:text-red-400">
251
+
Less secure. Do not use your main password, please use an App Password.
252
</p>
253
{/* <input
254
type="text"
+10
-1
src/components/LogoSvg.tsx
···
1
import type { SVGProps } from 'react';
2
import React from 'react';
3
0
0
0
0
0
0
0
0
0
4
// FluentEmojiHighContrastGlowingStar
5
-
export default function FluentEmojiHighContrastGlowingStar(props: SVGProps<SVGSVGElement>) {
6
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
}
8
···
1
import type { SVGProps } from 'react';
2
import React from 'react';
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
+
13
// FluentEmojiHighContrastGlowingStar
14
+
export function FluentEmojiHighContrastGlowingStar(props: SVGProps<SVGSVGElement>) {
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>);
16
}
17
+341
-90
src/routes/__root.tsx
···
5
import type { QueryClient } from "@tanstack/react-query";
6
import {
7
createRootRouteWithContext,
0
8
// Link,
9
// Outlet,
10
Scripts,
···
18
import { Toaster } from "sonner";
19
import { KeepAliveOutlet, KeepAliveProvider } from "tanstack-router-keepalive";
20
21
-
import { HOST_TITLE } from "~/../policy";
22
import { Composer } from "~/components/Composer";
23
import { DefaultCatchBoundary } from "~/components/DefaultCatchBoundary";
24
import { Import } from "~/components/Import";
25
-
import Login from "~/components/Login";
26
import Logo from "~/components/LogoSvg";
27
-
import { ModerationBatcher } from "~/components/ModerationBatcher";
28
-
import { ModerationInitializer } from "~/components/ModerationInitializer";
29
import { NotFound } from "~/components/NotFound";
0
30
import { LikeMutationQueueProvider } from "~/providers/LikeMutationQueueProvider";
31
import { PollMutationQueueProvider } from "~/providers/PollMutationQueueProvider";
32
import { UnifiedAuthProvider, useAuth } from "~/providers/UnifiedAuthProvider";
33
-
import { composerAtom, hueAtom, useAtomCssVar } from "~/utils/atoms";
0
34
import { seo } from "~/utils/seo";
0
35
36
export const Route = createRootRouteWithContext<{
37
queryClient: QueryClient;
···
75
errorComponent: import.meta.env.DEV
76
? undefined
77
: (props) => (
78
-
<RootDocument>
79
-
<DefaultCatchBoundary {...props} />
80
-
</RootDocument>
81
-
),
82
notFoundComponent: () => <NotFound />,
83
component: RootComponent,
84
});
···
86
function RootComponent() {
87
return (
88
<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>
0
0
101
</UnifiedAuthProvider>
102
);
103
}
···
126
button={
127
button?.label
128
? {
129
-
label: button?.label,
130
-
onClick: () => {
131
-
button?.onClick?.();
132
-
},
133
-
}
134
: undefined
135
}
136
/>
···
222
const isSearch = location.pathname.startsWith("/search");
223
const isFeeds = location.pathname.startsWith("/feeds");
224
const isModeration = location.pathname.startsWith("/moderation");
0
225
226
const locationEnum:
227
| "feeds"
···
230
| "notifications"
231
| "profile"
232
| "moderation"
0
233
| "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";
0
0
246
247
const [, setComposerPost] = useAtom(composerAtom);
248
···
251
<Composer />
252
253
<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">
255
<div className="flex items-center gap-3 mb-4 pl-3">
256
<Logo
257
className="h-8 w-8"
···
261
}}
262
/>
263
<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> */}
268
</span>
269
</div>
270
<MaterialNavItem
···
295
text="Explore"
296
/>
297
<MaterialNavItem
0
298
InactiveIcon={
299
<IconMaterialSymbolsNotificationsOutline className="w-6 h-6" />
300
}
···
311
text="Notifications"
312
/>
313
<MaterialNavItem
0
314
InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
315
ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
316
active={locationEnum === "feeds"}
···
323
text="Feeds"
324
/>
325
<MaterialNavItem
0
326
InactiveIcon={<IconMdiShieldOutline className="w-6 h-6" />}
327
ActiveIcon={<IconMdiShield className="w-6 h-6" />}
328
active={locationEnum === "moderation"}
···
335
text="Moderation"
336
/>
337
<MaterialNavItem
0
338
InactiveIcon={
339
<IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" />
340
}
···
367
}
368
text="Settings"
369
/>
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"
0
0
0
0
0
0
0
377
/>
378
-
</div>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
379
{/* <Link
380
to="/"
381
className={
···
479
<span>Post</span>
480
</button> */}
481
<div className="flex-1"></div>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
482
<a
483
href="https://tangled.sh/@whey.party/red-dwarf"
484
target="_blank"
···
506
microcosm.blue
507
</a>
508
</div>
0
509
</nav>
510
511
<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
/>
550
<MaterialNavItem
551
small
0
552
InactiveIcon={
553
<IconMaterialSymbolsNotificationsOutline className="w-6 h-6" />
554
}
···
566
/>
567
<MaterialNavItem
568
small
0
569
InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
570
ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
571
active={locationEnum === "feeds"}
···
579
/>
580
<MaterialNavItem
581
small
0
582
InactiveIcon={<IconMdiShieldOutline className="w-6 h-6" />}
583
ActiveIcon={<IconMdiShield className="w-6 h-6" />}
584
active={locationEnum === "moderation"}
···
592
/>
593
<MaterialNavItem
594
small
0
595
InactiveIcon={
596
<IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" />
597
}
···
625
}
626
text="Settings"
627
/>
628
-
<div className="flex flex-row items-center justify-center mt-3">
629
-
<MaterialPillButton
0
630
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"
0
0
0
0
0
0
0
636
/>
637
-
</div>
0
0
0
0
0
0
0
0
0
0
0
0
638
</nav>
639
640
{agent?.did && (
···
657
{children}
658
</main>
659
660
-
<aside className="hidden lg:flex h-screen w-[250px] sticky top-0 self-start flex-col">
661
-
<div className="px-4 pt-4">
662
<Import />
663
</div>
664
-
<Login />
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
665
<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>
672
</aside>
673
</div>
674
675
{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">
677
<div className="flex justify-around items-center p-2">
678
<MaterialNavItem
679
small
···
845
</div>
846
</nav>
847
) : (
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">
849
<div className="flex items-center gap-2">
850
<Logo
851
className="h-6 w-6"
···
855
}}
856
/>
857
<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> */}
862
</span>
863
</div>
864
<div className="flex items-center gap-2">
865
-
<Login compact={true} popup={true} />
0
0
0
0
0
0
866
</div>
867
</div>
868
)}
···
874
}
875
876
export function MaterialNavItem({
0
877
InactiveIcon,
878
ActiveIcon,
879
text,
···
881
onClickCallbback,
882
small,
883
}: {
0
884
InactiveIcon: React.ReactElement;
885
ActiveIcon: React.ReactElement;
886
text: string;
···
888
onClickCallbback: () => void;
889
small?: boolean | string;
890
}) {
0
891
if (small)
892
return (
893
<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
-
}`}
899
onClick={() => {
900
onClickCallbback();
901
}}
···
915
916
return (
917
<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
-
}`}
923
onClick={() => {
924
onClickCallbback();
925
}}
···
954
const active = false;
955
return (
956
<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
-
}`}
962
onClick={() => {
963
onClickCallbback();
964
}}
···
976
</button>
977
);
978
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
5
import type { QueryClient } from "@tanstack/react-query";
6
import {
7
createRootRouteWithContext,
8
+
Link,
9
// Link,
10
// Outlet,
11
Scripts,
···
19
import { Toaster } from "sonner";
20
import { KeepAliveOutlet, KeepAliveProvider } from "tanstack-router-keepalive";
21
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";
23
import { Composer } from "~/components/Composer";
24
import { DefaultCatchBoundary } from "~/components/DefaultCatchBoundary";
25
import { Import } from "~/components/Import";
26
+
//import Login from "~/components/Login";
27
import Logo from "~/components/LogoSvg";
28
+
//import { ModerationBatcher } from "~/components/ModerationBatcher";
29
+
//import { ModerationInitializer } from "~/components/ModerationInitializer";
30
import { NotFound } from "~/components/NotFound";
31
+
import { AutoLabelProvider } from "~/providers/AutoLabelProvider";
32
import { LikeMutationQueueProvider } from "~/providers/LikeMutationQueueProvider";
33
import { PollMutationQueueProvider } from "~/providers/PollMutationQueueProvider";
34
import { UnifiedAuthProvider, useAuth } from "~/providers/UnifiedAuthProvider";
35
+
import { FeedTabOnTop } from "~/routes/index";
36
+
import { composerAtom, hueAtom, imgCDNAtom, quickAuthAtom, useAtomCssVar } from "~/utils/atoms";
37
import { seo } from "~/utils/seo";
38
+
import { useQueryIdentity, useQueryPreferences, useQueryProfile } from "~/utils/useQuery";
39
40
export const Route = createRootRouteWithContext<{
41
queryClient: QueryClient;
···
79
errorComponent: import.meta.env.DEV
80
? undefined
81
: (props) => (
82
+
<RootDocument>
83
+
<DefaultCatchBoundary {...props} />
84
+
</RootDocument>
85
+
),
86
notFoundComponent: () => <NotFound />,
87
component: RootComponent,
88
});
···
90
function RootComponent() {
91
return (
92
<UnifiedAuthProvider>
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>
107
</UnifiedAuthProvider>
108
);
109
}
···
132
button={
133
button?.label
134
? {
135
+
label: button?.label,
136
+
onClick: () => {
137
+
button?.onClick?.();
138
+
},
139
+
}
140
: undefined
141
}
142
/>
···
228
const isSearch = location.pathname.startsWith("/search");
229
const isFeeds = location.pathname.startsWith("/feeds");
230
const isModeration = location.pathname.startsWith("/moderation");
231
+
const isAbout = location.pathname.startsWith("/about");
232
233
const locationEnum:
234
| "feeds"
···
237
| "notifications"
238
| "profile"
239
| "moderation"
240
+
| "about"
241
| "home" = isFeeds
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";
256
257
const [, setComposerPost] = useAtom(composerAtom);
258
···
261
<Composer />
262
263
<div className="min-h-screen flex justify-center bg-gray-50 dark:bg-gray-950">
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">
265
<div className="flex items-center gap-3 mb-4 pl-3">
266
<Logo
267
className="h-8 w-8"
···
271
}}
272
/>
273
<span className="font-extrabold text-2xl text-gray-900 dark:text-gray-100">
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>) }
278
</span>
279
</div>
280
<MaterialNavItem
···
305
text="Explore"
306
/>
307
<MaterialNavItem
308
+
visible={!!agent?.did}
309
InactiveIcon={
310
<IconMaterialSymbolsNotificationsOutline className="w-6 h-6" />
311
}
···
322
text="Notifications"
323
/>
324
<MaterialNavItem
325
+
visible={!!agent?.did}
326
InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
327
ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
328
active={locationEnum === "feeds"}
···
335
text="Feeds"
336
/>
337
<MaterialNavItem
338
+
visible={!!agent?.did}
339
InactiveIcon={<IconMdiShieldOutline className="w-6 h-6" />}
340
ActiveIcon={<IconMdiShield className="w-6 h-6" />}
341
active={locationEnum === "moderation"}
···
348
text="Moderation"
349
/>
350
<MaterialNavItem
351
+
visible={!!agent?.did}
352
InactiveIcon={
353
<IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" />
354
}
···
381
}
382
text="Settings"
383
/>
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"
398
/>
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
+
)}
418
{/* <Link
419
to="/"
420
className={
···
518
<span>Post</span>
519
</button> */}
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
+
{/*
535
<a
536
href="https://tangled.sh/@whey.party/red-dwarf"
537
target="_blank"
···
559
microcosm.blue
560
</a>
561
</div>
562
+
*/}
563
</nav>
564
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">
···
603
/>
604
<MaterialNavItem
605
small
606
+
visible={!!agent?.did}
607
InactiveIcon={
608
<IconMaterialSymbolsNotificationsOutline className="w-6 h-6" />
609
}
···
621
/>
622
<MaterialNavItem
623
small
624
+
visible={!!agent?.did}
625
InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
626
ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
627
active={locationEnum === "feeds"}
···
635
/>
636
<MaterialNavItem
637
small
638
+
visible={!!agent?.did}
639
InactiveIcon={<IconMdiShieldOutline className="w-6 h-6" />}
640
ActiveIcon={<IconMdiShield className="w-6 h-6" />}
641
active={locationEnum === "moderation"}
···
649
/>
650
<MaterialNavItem
651
small
652
+
visible={!!agent?.did}
653
InactiveIcon={
654
<IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" />
655
}
···
683
}
684
text="Settings"
685
/>
686
+
687
+
{!agent?.did && (
688
+
<MaterialNavItem
689
small
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"
702
/>
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
+
)}
716
</nav>
717
718
{agent?.did && (
···
735
{children}
736
</main>
737
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">
740
<Import />
741
</div>
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>
761
<div className="flex-1"></div>
762
+
{/* todo */}
763
+
<span>TODO: add red dwarf the software policy along with instance policy here</span>
0
0
0
0
764
</aside>
765
</div>
766
767
{agent?.did ? (
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">
769
<div className="flex justify-around items-center p-2">
770
<MaterialNavItem
771
small
···
937
</div>
938
</nav>
939
) : (
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">
941
<div className="flex items-center gap-2">
942
<Logo
943
className="h-6 w-6"
···
947
}}
948
/>
949
<span className="font-bold text-lg text-gray-900 dark:text-gray-100">
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>) }
954
</span>
955
</div>
956
<div className="flex items-center gap-2">
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>
964
</div>
965
</div>
966
)}
···
972
}
973
974
export function MaterialNavItem({
975
+
visible = true,
976
InactiveIcon,
977
ActiveIcon,
978
text,
···
980
onClickCallbback,
981
small,
982
}: {
983
+
visible?: boolean;
984
InactiveIcon: React.ReactElement;
985
ActiveIcon: React.ReactElement;
986
text: string;
···
988
onClickCallbback: () => void;
989
small?: boolean | string;
990
}) {
991
+
if (!visible) return null
992
if (small)
993
return (
994
<button
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
+
}`}
0
999
onClick={() => {
1000
onClickCallbback();
1001
}}
···
1015
1016
return (
1017
<button
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
+
}`}
0
1022
onClick={() => {
1023
onClickCallbback();
1024
}}
···
1053
const active = false;
1054
return (
1055
<button
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
+
}`}
0
1060
onClick={() => {
1061
onClickCallbback();
1062
}}
···
1074
</button>
1075
);
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
</Link>
167
);
168
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
166
</Link>
167
);
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";
2
import { useAtom } from "jotai";
3
import * as React from "react";
4
import { useLayoutEffect, useState } from "react";
···
22
useQueryIdentity,
23
useQueryPreferences,
24
} from "~/utils/useQuery";
0
0
25
26
export const Route = createFileRoute("/")({
27
// loader: async ({ context }) => {
···
180
return savedFeedsPref?.items || [];
181
}, [prefs]);
182
0
0
0
0
183
const [persistentSelectedFeed, setPersistentSelectedFeed] = useAtom(selectedFeedUriAtom);
184
const [unauthedSelectedFeed, setUnauthedSelectedFeed] = useState(persistentSelectedFeed);
185
const selectedFeed = agent?.did
···
363
<div
364
className={`relative flex flex-col ${hidden && "hidden"}`}
365
>
366
-
{!isAuthRestoring && savedFeeds.length > 0 ? (
367
<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} /> })}
369
</div>
370
) : (
371
// <span className="text-xl font-bold ml-2">Home</span>
···
424
425
// todo please use types this is dangerous very dangerous.
426
// todo fix this whenever proper preferences is handled
427
-
function FeedTabOnTop({ item, idx }: { item: any, idx: number }) {
0
0
0
0
0
0
0
0
0
0
0
428
const [persistentSelectedFeed, setPersistentSelectedFeed] = useAtom(selectedFeedUriAtom);
429
const selectedFeed = persistentSelectedFeed
430
const setSelectedFeed = setPersistentSelectedFeed
···
435
return (
436
<button
437
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"
440
: "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800"
441
// ? "bg-gray-500 text-white"
442
// : item.pinned
443
// ? "bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-200"
444
// : "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-200"
445
}`}
446
-
onClick={() => setSelectedFeed(item.value)}
0
0
0
0
0
0
0
447
title={item.value}
448
>
0
0
0
449
{label}
450
-
{item.pinned && (
451
<span
452
className={`ml-1 text-xs ${isActive
453
? "text-gray-900 dark:text-gray-100"
···
456
>
457
★
458
</span>
459
-
)}
460
</button>
461
);
462
}
···
1
+
import { createFileRoute, useLocation, useNavigate } from "@tanstack/react-router";
2
import { useAtom } from "jotai";
3
import * as React from "react";
4
import { useLayoutEffect, useState } from "react";
···
22
useQueryIdentity,
23
useQueryPreferences,
24
} from "~/utils/useQuery";
25
+
26
+
import { FeedIcon } from "./feeds";
27
28
export const Route = createFileRoute("/")({
29
// loader: async ({ context }) => {
···
182
return savedFeedsPref?.items || [];
183
}, [prefs]);
184
185
+
const pinnedFeeds = React.useMemo(() => {
186
+
return savedFeeds.filter((feed: any) => feed.pinned);
187
+
}, [savedFeeds]);
188
+
189
const [persistentSelectedFeed, setPersistentSelectedFeed] = useAtom(selectedFeedUriAtom);
190
const [unauthedSelectedFeed, setUnauthedSelectedFeed] = useState(persistentSelectedFeed);
191
const selectedFeed = agent?.did
···
369
<div
370
className={`relative flex flex-col ${hidden && "hidden"}`}
371
>
372
+
{!isAuthRestoring && pinnedFeeds.length > 0 ? (
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`}>
374
+
{pinnedFeeds.map((item: any, idx: number) => { return <FeedTabOnTop key={item} item={item} idx={idx} /> })}
375
</div>
376
) : (
377
// <span className="text-xl font-bold ml-2">Home</span>
···
430
431
// todo please use types this is dangerous very dangerous.
432
// todo fix this whenever proper preferences is handled
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 == "";
445
const [persistentSelectedFeed, setPersistentSelectedFeed] = useAtom(selectedFeedUriAtom);
446
const selectedFeed = persistentSelectedFeed
447
const setSelectedFeed = setPersistentSelectedFeed
···
452
return (
453
<button
454
key={item.value || idx}
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"
457
: "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800"
458
// ? "bg-gray-500 text-white"
459
// : item.pinned
460
// ? "bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-200"
461
// : "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-200"
462
}`}
463
+
onClick={() => {
464
+
if (rightDesktopSidebar && !isAtHome) {
465
+
navigate({
466
+
to: "/"
467
+
})
468
+
}
469
+
setSelectedFeed(item.value)
470
+
}}
471
title={item.value}
472
>
473
+
{rightDesktopSidebar && (
474
+
<FeedIcon feedUri={item.value} className="w-5 h-5 rounded-sm object-cover" />
475
+
)}
476
{label}
477
+
{/* {!rightDesktopSidebar && item.pinned && (
478
<span
479
className={`ml-1 text-xs ${isActive
480
? "text-gray-900 dark:text-gray-100"
···
483
>
484
★
485
</span>
486
+
)} */}
487
</button>
488
);
489
}
+55
-2
src/routes/settings.tsx
···
6
import { HOST_TITLE } from "~/../policy";
7
import { Header } from "~/components/Header";
8
import Login from "~/components/Login";
0
9
import {
10
constellationURLAtom,
11
defaultconstellationURL,
···
32
33
export function Settings() {
34
const navigate = useNavigate();
0
35
return (
36
<>
37
<Header
···
44
}
45
}}
46
/>
47
-
<div className="lg:hidden">
48
-
<Login />
0
0
0
0
49
</div>
0
50
<div className="sm:hidden flex flex-col justify-around mt-4">
51
<SettingHeading title="Other Pages" top />
52
<MaterialNavItem
0
0
0
0
0
0
0
0
0
0
0
0
0
0
53
InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
54
ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
55
active={false}
···
62
text="Feeds"
63
/>
64
<MaterialNavItem
0
65
InactiveIcon={<IconMdiShieldOutline className="w-6 h-6" />}
66
ActiveIcon={<IconMdiShield className="w-6 h-6" />}
67
active={false}
···
72
})
73
}
74
text="Moderation"
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
75
/>
76
</div>
77
<div className="h-4" />
···
6
import { HOST_TITLE } from "~/../policy";
7
import { Header } from "~/components/Header";
8
import Login from "~/components/Login";
9
+
import { useAuth } from "~/providers/UnifiedAuthProvider";
10
import {
11
constellationURLAtom,
12
defaultconstellationURL,
···
33
34
export function Settings() {
35
const navigate = useNavigate();
36
+
const { agent } = useAuth();
37
return (
38
<>
39
<Header
···
46
}
47
}}
48
/>
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>
55
</div>
56
+
{/* Small viewport nav overflow */}
57
<div className="sm:hidden flex flex-col justify-around mt-4">
58
<SettingHeading title="Other Pages" top />
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}
74
InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
75
ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
76
active={false}
···
83
text="Feeds"
84
/>
85
<MaterialNavItem
86
+
visible={!!agent?.did}
87
InactiveIcon={<IconMdiShieldOutline className="w-6 h-6" />}
88
ActiveIcon={<IconMdiShield className="w-6 h-6" />}
89
active={false}
···
94
})
95
}
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"
128
/>
129
</div>
130
<div className="h-4" />