<script lang="ts">
import { type State as PostComposerState } from './PostComposer.svelte';
import { AtpClient } from '$lib/at/client.svelte';
import { accounts } from '$lib/accounts';
import { SvelteSet } from 'svelte/reactivity';
import {
fetchFollowingTimeline,
allPosts,
followingFeed,
accountPreferences,
fetchInteractionsToFollowingTimelineEnd,
follows,
followingCursors,
initialDone
} from '$lib/state.svelte';
import { buildThreadsFiltered } from '$lib/thread';
import type { Did } from '@atcute/lexicons/syntax';
import GenericTimelineView from './GenericTimelineView.svelte';
interface Props {
client?: AtpClient | null;
postComposerState: PostComposerState;
class?: string;
targetDid?: Did;
}
let {
client = null,
postComposerState = $bindable(),
class: className = '',
targetDid = undefined
}: Props = $props();
let viewOwnPosts = $state(true);
let displayCount = $state(10);
const userDid = $derived(targetDid ?? client?.user?.did);
const currentPrefs = $derived(userDid ? accountPreferences.get(userDid) : null);
const mutes = $derived(new Set(currentPrefs?.mutes ?? []));
const followedDids = $derived.by(() => {
if (!userDid) return new Set<Did>();
const map = follows.get(userDid);
if (!map) return new Set<Did>();
return new Set(map.keys());
});
const threads = $derived(
userDid
? buildThreadsFiltered(
userDid,
followingFeed.get(userDid) ?? new SvelteSet(),
allPosts,
mutes,
$accounts,
{ viewOwnPosts, filterReplies: true, filterRootsToDids: followedDids },
displayCount
)
: []
);
const isComplete = $derived.by(() => {
if (!userDid) return false;
const cursors = followingCursors.get(userDid);
const subjects = follows.get(userDid);
// if no cursors yet, we haven't started
if (!cursors) return false;
// Check self
if (cursors.get(userDid) !== null) return false;
// Check follows
if (subjects) {
for (const subject of subjects.keys()) {
// if checking logic in state.svelte.ts:
// undefined means "not fetched", null means "exhausted"
// if any cursor is undefined or string, it's not complete.
if (cursors.get(subject) !== null) return false;
}
}
return true;
});
let fetchingInteractions = $state(false);
let scheduledFetchInteractions = $state(false);
const loadMore = async () => {
if (!client || !userDid) return;
await fetchFollowingTimeline(client, userDid);
if (client.user && userDid) {
if (!fetchingInteractions) {
scheduledFetchInteractions = false;
fetchingInteractions = true;
await fetchInteractionsToFollowingTimelineEnd(client, userDid);
fetchingInteractions = false;
} else {
scheduledFetchInteractions = true;
}
}
};
$effect(() => {
if (client && scheduledFetchInteractions && userDid) {
if (!fetchingInteractions) {
scheduledFetchInteractions = false;
fetchingInteractions = true;
fetchInteractionsToFollowingTimelineEnd(client, userDid).finally(
() => (fetchingInteractions = false)
);
}
}
});
</script>
<GenericTimelineView
{client}
{threads}
timelineId={`following:${userDid}`}
bind:postComposerState
bind:displayCount
class={className}
isLoggedIn={!!(userDid || $accounts.length > 0)}
canLoad={!!(client && userDid && initialDone.has(userDid))}
onLoadMore={loadMore}
{isComplete}
/>