BlueSky & more on desktop
lazurite.stormlightlabs.org/
tauri
rust
typescript
bluesky
appview
atproto
solid
1import { usePostInteractions } from "$/components/posts/hooks/usePostInteractions";
2import { FeedController } from "$/lib/api/feeds";
3import { patchFeedItems } from "$/lib/feeds";
4import type { FeedViewPost, SavedFeedItem } from "$/lib/types";
5import * as logger from "@tauri-apps/plugin-log";
6import { onCleanup, onMount } from "solid-js";
7import { createStore } from "solid-js/store";
8
9const PAGE_LIMIT = 20;
10
11type FeedColumnState = {
12 bookmarkPendingByUri: Record<string, boolean>;
13 cursor: string | null;
14 error: string | null;
15 items: FeedViewPost[];
16 loading: boolean;
17 loadingMore: boolean;
18};
19
20export function useFeedColumnState(getFeed: () => SavedFeedItem) {
21 const [state, setState] = createStore<FeedColumnState>({
22 bookmarkPendingByUri: {},
23 cursor: null,
24 error: null,
25 items: [],
26 loading: true,
27 loadingMore: false,
28 });
29 const interactions = usePostInteractions({
30 onError(message) {
31 logger.error(message);
32 },
33 patchPost(uri, updater) {
34 setState("items", (items) => patchFeedItems(items, uri, updater));
35 },
36 });
37
38 let observer: IntersectionObserver | undefined;
39
40 async function load(cursor: string | null = null) {
41 try {
42 const page = await FeedController.getFeedPage(getFeed(), cursor, PAGE_LIMIT);
43
44 if (cursor) {
45 setState("items", (prev) => [...prev, ...page.feed]);
46 } else {
47 setState("items", page.feed);
48 }
49 setState("cursor", page.cursor ?? null);
50 setState("error", null);
51 } catch (err) {
52 const message = err instanceof Error ? err.message : String(err);
53 logger.error(`Feed column load failed: ${message}`);
54 setState("error", message);
55 } finally {
56 setState("loading", false);
57 setState("loadingMore", false);
58 }
59 }
60
61 async function loadMore() {
62 if (state.loadingMore || state.loading || !state.cursor) return;
63 setState("loadingMore", true);
64 await load(state.cursor);
65 }
66
67 async function refresh() {
68 setState("loading", true);
69 setState("cursor", null);
70 setState("items", []);
71 await load(null);
72 }
73
74 function registerSentinel(element: HTMLDivElement) {
75 observer?.disconnect();
76
77 if (!element) return;
78
79 observer = new IntersectionObserver((entries) => {
80 const entry = entries[0];
81 if (entry?.isIntersecting) {
82 void loadMore();
83 }
84 }, { threshold: 0.1 });
85
86 observer.observe(element);
87 }
88
89 onMount(() => {
90 void load(null);
91 });
92
93 onCleanup(() => {
94 observer?.disconnect();
95 });
96
97 return {
98 bookmarkPendingByUri: interactions.bookmarkPendingByUri,
99 likePendingByUri: interactions.likePendingByUri,
100 refresh,
101 registerSentinel,
102 repostPendingByUri: interactions.repostPendingByUri,
103 state,
104 toggleBookmark: interactions.toggleBookmark,
105 toggleLike: interactions.toggleLike,
106 toggleRepost: interactions.toggleRepost,
107 };
108}