BlueSky & more on desktop
lazurite.stormlightlabs.org/
tauri
rust
typescript
bluesky
appview
atproto
solid
1import type {
2 ColumnWidth,
3 DiagnosticsColumnConfig,
4 ExplorerColumnConfig,
5 FeedColumnConfig,
6 ProfileColumnConfig,
7 SearchColumnConfig,
8} from "$/lib/api/types/columns";
9import { getFeedName } from "$/lib/feeds";
10import type { FeedGeneratorView, SavedFeedItem } from "$/lib/types";
11
12export const COLUMN_WIDTH_PX: Record<ColumnWidth, number> = { narrow: 320, standard: 420, wide: 560 };
13
14export type ResolvedFeedColumn = {
15 config: FeedColumnConfig;
16 feed: SavedFeedItem;
17 generator?: FeedGeneratorView;
18 title: string;
19};
20
21export function cycleWidth(current: ColumnWidth): ColumnWidth {
22 switch (current) {
23 case "narrow": {
24 return "standard";
25 }
26 case "standard": {
27 return "wide";
28 }
29 case "wide": {
30 return "narrow";
31 }
32 }
33}
34
35function isFeedType(value: unknown): value is FeedColumnConfig["feedType"] {
36 return value === "timeline" || value === "feed" || value === "list";
37}
38
39export function parseFeedConfig(config: string): FeedColumnConfig | null {
40 try {
41 const parsed = JSON.parse(config) as Record<string, unknown>;
42 if (!parsed || typeof parsed !== "object") {
43 return null;
44 }
45
46 if (!isFeedType(parsed.feedType) || typeof parsed.feedUri !== "string") {
47 return null;
48 }
49
50 if (parsed.title !== undefined && parsed.title !== null && typeof parsed.title !== "string") {
51 return null;
52 }
53
54 return { feedType: parsed.feedType, feedUri: parsed.feedUri, title: parsed.title as string | null | undefined };
55 } catch {
56 return null;
57 }
58}
59
60function parseExplorerConfig(config: string): ExplorerColumnConfig | null {
61 try {
62 const parsed = JSON.parse(config) as unknown;
63 if (parsed && typeof parsed === "object" && "targetUri" in parsed) {
64 return parsed as ExplorerColumnConfig;
65 }
66 return null;
67 } catch {
68 return null;
69 }
70}
71
72export function parseDiagnosticsConfig(config: string): DiagnosticsColumnConfig | null {
73 try {
74 const parsed = JSON.parse(config) as unknown;
75 if (parsed && typeof parsed === "object" && "did" in parsed) {
76 return parsed as DiagnosticsColumnConfig;
77 }
78 return null;
79 } catch {
80 return null;
81 }
82}
83
84export function parseSearchConfig(config: string): SearchColumnConfig | null {
85 try {
86 const parsed = JSON.parse(config) as unknown;
87 if (parsed && typeof parsed === "object" && "mode" in parsed && "query" in parsed) {
88 return parsed as SearchColumnConfig;
89 }
90 return null;
91 } catch {
92 return null;
93 }
94}
95
96export function parseProfileConfig(config: string): ProfileColumnConfig | null {
97 try {
98 const parsed = JSON.parse(config) as unknown;
99 if (parsed && typeof parsed === "object" && "actor" in parsed) {
100 return parsed as ProfileColumnConfig;
101 }
102 return null;
103 } catch {
104 return null;
105 }
106}
107
108function feedConfigToSavedFeedItem(config: FeedColumnConfig): SavedFeedItem {
109 return {
110 id: config.feedUri || "following",
111 pinned: false,
112 type: config.feedType,
113 value: config.feedUri || "following",
114 };
115}
116
117export function resolveFeedColumn(
118 config: FeedColumnConfig,
119 options: { generator?: FeedGeneratorView; savedFeedTitle?: string | null } = {},
120): ResolvedFeedColumn {
121 const feed = feedConfigToSavedFeedItem(config);
122 const hydratedTitle = options.generator?.displayName || config.title?.trim() || options.savedFeedTitle?.trim();
123
124 return { config, feed, generator: options.generator, title: getFeedName(feed, hydratedTitle) };
125}
126
127export function columnTitle(kind: string, config: string): string {
128 switch (kind) {
129 case "feed": {
130 return "Feed";
131 }
132 case "explorer": {
133 const parsed = parseExplorerConfig(config);
134 if (!parsed?.targetUri) return "Explorer";
135 return parsed.targetUri.length > 30 ? `${parsed.targetUri.slice(0, 30)}…` : parsed.targetUri;
136 }
137 case "diagnostics": {
138 const parsed = parseDiagnosticsConfig(config);
139 return parsed?.did ?? "Diagnostics";
140 }
141 case "messages": {
142 return "Messages";
143 }
144 case "search": {
145 const parsed = parseSearchConfig(config);
146 const query = parsed?.query.trim();
147 return query ? `Search: ${query}` : "Search";
148 }
149 case "profile": {
150 const parsed = parseProfileConfig(config);
151 return parsed?.displayName?.trim() || parsed?.handle?.trim() || parsed?.actor || "Profile";
152 }
153 default: {
154 return "Column";
155 }
156 }
157}
158
159export type FeedPickerSelection = { feed: SavedFeedItem; title: string };
160
161export type ProfileSelection = {
162 actor: string;
163 did?: string | null;
164 displayName?: string | null;
165 handle?: string | null;
166};