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