BlueSky & more on desktop
lazurite.stormlightlabs.org/
tauri
rust
typescript
bluesky
appview
atproto
solid
1import { getFeedName } from "$/lib/feeds";
2import type { FeedGeneratorView, FeedViewPrefItem, SavedFeedItem } from "$/lib/types";
3import { For, type ParentProps, Show } from "solid-js";
4import { FeedChipAvatar } from "./FeedChipAvatar";
5
6type FeedWorkspaceSidebarProps = {
7 activePref: FeedViewPrefItem;
8 drawerFeeds: SavedFeedItem[];
9 generators: Record<string, FeedGeneratorView>;
10 onFeedSelect: (feedId: string) => void;
11 onPrefChange: <K extends keyof FeedViewPrefItem>(key: K, value: FeedViewPrefItem[K]) => void;
12};
13
14export function FeedWorkspaceSidebar(props: FeedWorkspaceSidebarProps) {
15 return (
16 <aside class="grid min-h-0 min-w-0 gap-4 overflow-hidden md:grid-cols-2 xl:grid-cols-1 xl:overflow-y-auto xl:overscroll-contain">
17 <SavedFeedsCard drawerFeeds={props.drawerFeeds} generators={props.generators} onFeedSelect={props.onFeedSelect} />
18 <DisplayFiltersCard activePref={props.activePref} onPrefChange={props.onPrefChange} />
19 <ShortcutsCard />
20 </aside>
21 );
22}
23
24function SavedFeedsCard(
25 props: {
26 drawerFeeds: SavedFeedItem[];
27 generators: Record<string, FeedGeneratorView>;
28 onFeedSelect: (feedId: string) => void;
29 },
30) {
31 return (
32 <SidebarCard title="Saved Feeds" subtitle="All your feeds">
33 <div class="grid gap-2">
34 <For each={props.drawerFeeds.slice(0, 4)}>
35 {(feed) => (
36 <SidebarFeedButton feed={feed} generator={props.generators[feed.value]} onSelect={props.onFeedSelect} />
37 )}
38 </For>
39 <Show when={props.drawerFeeds.length === 0}>
40 <p class="m-0 text-[0.8rem] leading-[1.6] text-on-surface-variant">
41 All saved feeds are already pinned as tabs.
42 </p>
43 </Show>
44 </div>
45 </SidebarCard>
46 );
47}
48
49function SidebarFeedButton(
50 props: { feed: SavedFeedItem; generator?: FeedGeneratorView; onSelect: (feedId: string) => void },
51) {
52 return (
53 <button
54 class="tone-muted flex w-full items-center gap-3 rounded-1xl border-0 px-3 py-3 text-left text-on-surface shadow-(--inset-shadow) transition duration-150 ease-out hover:-translate-y-px hover:bg-surface-bright"
55 type="button"
56 onClick={() => props.onSelect(props.feed.id)}>
57 <FeedChipAvatar feed={props.feed} generator={props.generator} />
58 <div class="min-w-0 flex-1">
59 <p class="m-0 truncate text-sm font-medium">{getFeedName(props.feed, props.generator?.displayName)}</p>
60 <p class="m-0 text-xs uppercase tracking-[0.08em] text-on-surface-variant">{props.feed.type}</p>
61 </div>
62 </button>
63 );
64}
65
66function DisplayFiltersCard(
67 props: {
68 activePref: FeedViewPrefItem;
69 onPrefChange: <K extends keyof FeedViewPrefItem>(key: K, value: FeedViewPrefItem[K]) => void;
70 },
71) {
72 return (
73 <SidebarCard title="Display Filters" subtitle="Per-feed">
74 <div class="grid gap-3">
75 <ToggleRow
76 checked={props.activePref.hideReposts}
77 label="Hide reposts"
78 onChange={(checked) => void props.onPrefChange("hideReposts", checked)} />
79 <ToggleRow
80 checked={props.activePref.hideReplies}
81 label="Hide replies"
82 onChange={(checked) => void props.onPrefChange("hideReplies", checked)} />
83 <ToggleRow
84 checked={props.activePref.hideQuotePosts}
85 label="Hide quotes"
86 onChange={(checked) => void props.onPrefChange("hideQuotePosts", checked)} />
87 <ReplyLikeThreshold
88 value={props.activePref.hideRepliesByLikeCount}
89 onChange={(value) => void props.onPrefChange("hideRepliesByLikeCount", value)} />
90 </div>
91 </SidebarCard>
92 );
93}
94
95function ReplyLikeThreshold(props: { value: number | null; onChange: (value: number | null) => void }) {
96 return (
97 <label class="grid gap-2 text-[0.8rem] text-on-surface-variant">
98 <span>Minimum likes for replies</span>
99 <p class="m-0 text-[0.72rem] leading-normal text-on-surface-variant/80">Only reply posts are affected.</p>
100 <input
101 class="ui-input ui-input-strong rounded-full px-4 py-2 text-on-surface focus:outline focus:outline-primary/50"
102 min="0"
103 type="number"
104 placeholder='e.g. "10"'
105 value={props.value ?? ""}
106 onInput={(event) => {
107 const value = event.currentTarget.value.trim();
108 if (value !== "" && (Number.isNaN(Number(value)) || Number(value) < 0)) {
109 return;
110 }
111 props.onChange(value ? Number(value) : null);
112 }} />
113 </label>
114 );
115}
116
117function ShortcutsCard() {
118 return (
119 <SidebarCard title="Shortcuts" subtitle="Feed controls">
120 <div class="grid gap-2 text-[0.8rem] text-on-surface-variant">
121 <ShortcutLine keys="1-9" label="Switch pinned feeds" />
122 <ShortcutLine keys="j / k" label="Move focus" />
123 <ShortcutLine keys="l" label="Like focused post" />
124 <ShortcutLine keys="r" label="Reply to focused post" />
125 <ShortcutLine keys="t" label="Repost focused post" />
126 <ShortcutLine keys="o" label="Open thread" />
127 <ShortcutLine keys="n" label="Open composer" />
128 </div>
129 </SidebarCard>
130 );
131}
132
133function SidebarCard(props: ParentProps & { subtitle: string; title: string }) {
134 return (
135 <section class="tone-muted rounded-3xl p-4 shadow-(--inset-shadow)">
136 <p class="m-0 text-base font-semibold text-on-surface">{props.title}</p>
137 <p class="mt-1 text-xs uppercase tracking-[0.12em] text-on-surface-variant">{props.subtitle}</p>
138 <div class="mt-4">{props.children}</div>
139 </section>
140 );
141}
142
143function ToggleRow(props: { checked: boolean; label: string; onChange: (checked: boolean) => void }) {
144 return (
145 <label class="tone-muted flex items-center justify-between gap-3 rounded-2xl px-3 py-3 text-sm text-on-surface shadow-(--inset-shadow)">
146 <span>{props.label}</span>
147 <input checked={props.checked} type="checkbox" onInput={(event) => props.onChange(event.currentTarget.checked)} />
148 </label>
149 );
150}
151
152function ShortcutLine(props: { keys: string; label: string }) {
153 return (
154 <div class="tone-muted flex items-center justify-between gap-3 rounded-2xl px-3 py-2.5 shadow-(--inset-shadow)">
155 <span>{props.label}</span>
156 <span class="ui-input-strong rounded-full px-2 py-1 text-[0.68rem] uppercase tracking-[0.08em] text-primary">
157 {props.keys}
158 </span>
159 </div>
160 );
161}