BlueSky & more on desktop lazurite.stormlightlabs.org/
tauri rust typescript bluesky appview atproto solid
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 161 lines 6.4 kB view raw
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}