appview-less bluesky client
1<script lang="ts">
2 import { type State as PostComposerState } from './PostComposer.svelte';
3 import { AtpClient } from '$lib/at/client.svelte';
4 import { accounts } from '$lib/accounts';
5 import { SvelteSet } from 'svelte/reactivity';
6 import {
7 fetchFollowingTimeline,
8 allPosts,
9 followingFeed,
10 accountPreferences,
11 fetchInteractionsToFollowingTimelineEnd,
12 follows,
13 followingCursors,
14 initialDone
15 } from '$lib/state.svelte';
16 import { buildThreadsFiltered } from '$lib/thread';
17 import type { Did } from '@atcute/lexicons/syntax';
18 import GenericTimelineView from './GenericTimelineView.svelte';
19
20 interface Props {
21 client?: AtpClient | null;
22 postComposerState: PostComposerState;
23 class?: string;
24 targetDid?: Did;
25 }
26
27 let {
28 client = null,
29 postComposerState = $bindable(),
30 class: className = '',
31 targetDid = undefined
32 }: Props = $props();
33
34 let viewOwnPosts = $state(true);
35 let displayCount = $state(10);
36
37 const userDid = $derived(targetDid ?? client?.user?.did);
38
39 const currentPrefs = $derived(userDid ? accountPreferences.get(userDid) : null);
40 const mutes = $derived(new Set(currentPrefs?.mutes ?? []));
41
42 const followedDids = $derived.by(() => {
43 if (!userDid) return new Set<Did>();
44 const map = follows.get(userDid);
45 if (!map) return new Set<Did>();
46 return new Set(map.keys());
47 });
48
49 const threads = $derived(
50 userDid
51 ? buildThreadsFiltered(
52 userDid,
53 followingFeed.get(userDid) ?? new SvelteSet(),
54 allPosts,
55 mutes,
56 $accounts,
57 { viewOwnPosts, filterReplies: true, filterRootsToDids: followedDids },
58 displayCount
59 )
60 : []
61 );
62
63 const isComplete = $derived.by(() => {
64 if (!userDid) return false;
65 const cursors = followingCursors.get(userDid);
66 const subjects = follows.get(userDid);
67
68 // if no cursors yet, we haven't started
69 if (!cursors) return false;
70
71 // Check self
72 if (cursors.get(userDid) !== null) return false;
73
74 // Check follows
75 if (subjects) {
76 for (const subject of subjects.keys()) {
77 // if checking logic in state.svelte.ts:
78 // undefined means "not fetched", null means "exhausted"
79 // if any cursor is undefined or string, it's not complete.
80 if (cursors.get(subject) !== null) return false;
81 }
82 }
83
84 return true;
85 });
86
87 let fetchingInteractions = $state(false);
88 let scheduledFetchInteractions = $state(false);
89
90 const loadMore = async () => {
91 if (!client || !userDid) return;
92
93 await fetchFollowingTimeline(client, userDid);
94
95 if (client.user && userDid) {
96 if (!fetchingInteractions) {
97 scheduledFetchInteractions = false;
98 fetchingInteractions = true;
99 await fetchInteractionsToFollowingTimelineEnd(client, userDid);
100 fetchingInteractions = false;
101 } else {
102 scheduledFetchInteractions = true;
103 }
104 }
105 };
106
107 $effect(() => {
108 if (client && scheduledFetchInteractions && userDid) {
109 if (!fetchingInteractions) {
110 scheduledFetchInteractions = false;
111 fetchingInteractions = true;
112 fetchInteractionsToFollowingTimelineEnd(client, userDid).finally(
113 () => (fetchingInteractions = false)
114 );
115 }
116 }
117 });
118</script>
119
120<GenericTimelineView
121 {client}
122 {threads}
123 timelineId={`following:${userDid}`}
124 bind:postComposerState
125 bind:displayCount
126 class={className}
127 isLoggedIn={!!(userDid || $accounts.length > 0)}
128 canLoad={!!(client && userDid && initialDone.has(userDid))}
129 onLoadMore={loadMore}
130 {isComplete}
131/>