BlueSky & more on desktop
lazurite.stormlightlabs.org/
tauri
rust
typescript
bluesky
appview
atproto
solid
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}