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 178 lines 6.4 kB view raw
1import { FeedController } from "$/lib/api/feeds"; 2import { getFeedName } from "$/lib/feeds"; 3import type { FeedGeneratorView, SavedFeedItem } from "$/lib/types"; 4import * as logger from "@tauri-apps/plugin-log"; 5import { createSignal, For, onMount, Show } from "solid-js"; 6import { FeedChipAvatar } from "../feeds/FeedChipAvatar"; 7import { Icon, LoadingIcon } from "../shared/Icon"; 8import type { FeedPickerSelection } from "./types"; 9 10function feedKindLabel(feed: SavedFeedItem) { 11 switch (feed.type) { 12 case "timeline": { 13 return "Timeline"; 14 } 15 case "list": { 16 return "List"; 17 } 18 default: { 19 return "Feed"; 20 } 21 } 22} 23 24export function FeedPicker(props: { onSelect: (selection: FeedPickerSelection) => void }) { 25 const [feeds, setFeeds] = createSignal<SavedFeedItem[]>([]); 26 const [generators, setGenerators] = createSignal<Record<string, FeedGeneratorView>>({}); 27 const [loading, setLoading] = createSignal(true); 28 29 onMount(async () => { 30 try { 31 const prefs = await FeedController.getPreferences(); 32 setFeeds(prefs.savedFeeds); 33 34 const uris = [...new Set(prefs.savedFeeds.filter((feed) => feed.type === "feed").map((feed) => feed.value))]; 35 if (uris.length > 0) { 36 const hydrated = await FeedController.getFeedGenerators(uris); 37 setGenerators(Object.fromEntries(hydrated.feeds.map((generator) => [generator.uri, generator]))); 38 } 39 } catch (err) { 40 logger.error(`Failed to load feeds for column picker: ${String(err)}`); 41 } finally { 42 setLoading(false); 43 } 44 }); 45 46 return ( 47 <div class="grid gap-2"> 48 <Show when={loading()}> 49 <div class="flex items-center justify-center py-6"> 50 <LoadingIcon isLoading class="text-on-surface-variant" /> 51 </div> 52 </Show> 53 54 <Show when={!loading() && feeds().length === 0}> 55 <p class="py-4 text-center text-sm text-on-surface-variant">No saved feeds found.</p> 56 </Show> 57 58 <For 59 each={feeds()} 60 fallback={ 61 <Show when={!loading()}> 62 <p class="py-4 text-center text-sm text-on-surface-variant">No saved feeds found.</p> 63 </Show> 64 }> 65 {(feed) => ( 66 <button 67 type="button" 68 class="tone-muted flex w-full items-center gap-3 rounded-xl border-0 px-4 py-3 text-left transition duration-150 hover:-translate-y-px hover:bg-surface-bright" 69 onClick={() => props.onSelect({ feed, title: getFeedName(feed, generators()[feed.value]?.displayName) })}> 70 <FeedChipAvatar feed={feed} generator={generators()[feed.value]} /> 71 <span class="min-w-0 flex-1"> 72 <span class="block truncate text-sm font-medium text-on-surface"> 73 {getFeedName(feed, generators()[feed.value]?.displayName)} 74 </span> 75 <span class="block truncate text-xs text-on-surface-variant">{feedKindLabel(feed)}</span> 76 </span> 77 </button> 78 )} 79 </For> 80 </div> 81 ); 82} 83 84export function ExplorerPicker(props: { onSubmit: (uri: string) => void }) { 85 const [value, setValue] = createSignal(""); 86 87 function handleSubmit(e: Event) { 88 e.preventDefault(); 89 const uri = value().trim(); 90 if (uri) { 91 props.onSubmit(uri); 92 } 93 } 94 95 return ( 96 <form onSubmit={handleSubmit} class="grid gap-3"> 97 <label class="grid gap-1.5"> 98 <span class="text-xs font-medium uppercase tracking-wide text-on-surface-variant"> 99 Target URI / handle / DID / PDS URL 100 </span> 101 <input 102 type="text" 103 class="ui-input ui-input-strong rounded-xl px-4 py-2.5" 104 placeholder="at://did:plc:… or handle.bsky.social" 105 value={value()} 106 onInput={(e) => setValue(e.currentTarget.value)} /> 107 </label> 108 109 <button 110 type="submit" 111 disabled={!value().trim()} 112 class="flex items-center justify-center gap-2 rounded-xl border-0 bg-primary/15 px-4 py-2.5 text-sm font-medium text-primary transition duration-150 hover:-translate-y-px hover:bg-primary/25 disabled:cursor-not-allowed disabled:opacity-40"> 113 <Icon kind="explore" /> 114 Open in column 115 </button> 116 </form> 117 ); 118} 119 120export function DiagnosticsPicker(props: { onSubmit: (did: string) => void }) { 121 const [value, setValue] = createSignal(""); 122 123 function handleSubmit(e: Event) { 124 e.preventDefault(); 125 const did = value().trim(); 126 if (did) { 127 props.onSubmit(did); 128 } 129 } 130 131 return ( 132 <form onSubmit={handleSubmit} class="grid gap-3"> 133 <label class="grid gap-1.5"> 134 <span class="text-xs font-medium uppercase tracking-wide text-on-surface-variant">Handle or DID</span> 135 <input 136 type="text" 137 class="ui-input ui-input-strong rounded-xl px-4 py-2.5" 138 placeholder="handle.bsky.social or did:plc:…" 139 value={value()} 140 onInput={(e) => setValue(e.currentTarget.value)} /> 141 </label> 142 143 <button 144 type="submit" 145 disabled={!value().trim()} 146 class="flex items-center justify-center gap-2 rounded-xl border-0 bg-primary/15 px-4 py-2.5 text-sm font-medium text-primary transition duration-150 hover:-translate-y-px hover:bg-primary/25 disabled:cursor-not-allowed disabled:opacity-40"> 147 <Icon kind="diagnostics" /> 148 Open diagnostics 149 </button> 150 </form> 151 ); 152} 153 154export function MessagesPicker(props: { onSubmit: () => void }) { 155 return ( 156 <div class="grid gap-4"> 157 <div class="rounded-2xl bg-surface-container-high p-4 shadow-(--inset-shadow)"> 158 <div class="flex items-start gap-3"> 159 <Icon kind="messages" class="text-primary mt-0.5" /> 160 <div class="grid gap-1.5"> 161 <p class="m-0 text-sm font-medium text-on-surface">Direct messages</p> 162 <p class="m-0 text-xs leading-relaxed text-on-surface-variant"> 163 Opens your DM inbox inside the deck. Message content is blurred until you hover or focus the column. 164 </p> 165 </div> 166 </div> 167 </div> 168 169 <button 170 type="button" 171 class="flex items-center justify-center gap-2 rounded-xl border-0 bg-primary/15 px-4 py-2.5 text-sm font-medium text-primary transition duration-150 hover:-translate-y-px hover:bg-primary/25" 172 onClick={() => props.onSubmit()}> 173 <Icon kind="deck" /> 174 Add DM column 175 </button> 176 </div> 177 ); 178}