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 108 lines 2.9 kB view raw
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}