refactor timelines, fix following timeline showing threads that are only replies
14 unmodified lines151617181919202122232425262728138 unmodified linesExpand all16716816917017117217317417517617717817918018117117217317417513 unmodified lines14 unmodified lines resetFeed, checkForNewPosts, fetchInteractionsToFeedTimelineEnd } from '$lib/state.svelte'; import Icon from '@iconify/svelte'; import NotLoggedIn from './NotLoggedIn.svelte'; import { fetchFeedGenerator } from '$lib/at/feeds'; import LoadingSpinner from './LoadingSpinner.svelte'; import EndOfList from './EndOfList.svelte'; import LoadError from './LoadError.svelte'; import LoadNewPosts from './LoadNewPosts.svelte';
interface Props { client?: AtpClient | null; postComposerState: PostComposerState;138 unmodified linesExpand all<div class="min-h-full p-2 [scrollbar-color:var(--nucleus-accent)_transparent] {className}" bind:this={scrollContainer}> {#if newPostsAvailable} <div class="sticky top-2 z-20 mb-4 flex w-full justify-center"> <button class="flex action-button items-center gap-2 bg-(--nucleus-bg) hover:scale-115! enabled:hover:bg-(--nucleus-bg)!" onclick={clearFeed} > <Icon icon="heroicons:arrow-up-16-solid" width="20" /> load new posts </button> </div> {/if} <LoadNewPosts visible={newPostsAvailable} onclick={clearFeed} /> {#if userDid || $accounts.length > 0} <InfiniteLoader {loaderState} triggerLoad={loadMore} loopDetectionTimeout={0}> {@render feedPostsView()} {#snippet noData()}13 unmodified lines12234658610789101116121314181915162122232417181920217 unmodified lines29303132403342343536372 unmodified lines404142435344454647482 unmodified lines515253545556575859606970717261626364656667686970717273747576777879808182838485868788788990808191928384858687888990919293939495969798991009410196979899100101102102104105103104105109110111106107108109110111112113114115114115116117118116117121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208118119120121122123124125126127<script lang="ts"> import BskyPost from './BskyPost.svelte'; import { type State as PostComposerState } from './PostComposer.svelte'; import { AtpClient } from '$lib/at/client.svelte'; import { accounts } from '$lib/accounts'; import { type ResourceUri } from '@atcute/lexicons'; import { SvelteSet } from 'svelte/reactivity'; import { InfiniteLoader, LoaderState } from 'svelte-infinite'; import { followingCursors, fetchFollowingTimeline, allPosts, followingFeed, accountPreferences, fetchInteractionsToFollowingTimelineEnd, follows follows, followingCursors } from '$lib/state.svelte'; import Icon from '@iconify/svelte'; import { buildThreads, filterThreads, type ThreadPost } from '$lib/thread'; import { buildThreads, filterThreads } from '$lib/thread'; import type { Did } from '@atcute/lexicons/syntax'; import NotLoggedIn from './NotLoggedIn.svelte'; import LoadingSpinner from './LoadingSpinner.svelte'; import EndOfList from './EndOfList.svelte'; import LoadError from './LoadError.svelte'; import GenericTimelineView from './GenericTimelineView.svelte';
interface Props { client?: AtpClient | null; postComposerState: PostComposerState;7 unmodified lines class: className = '', targetDid = undefined }: Props = $props();
let reverseChronological = $state(true); let viewOwnPosts = $state(true); const expandedThreads = new SvelteSet<ResourceUri>();
const userDid = $derived(targetDid ?? client?.user?.did);
const currentPrefs = $derived(userDid ? accountPreferences.get(userDid) : null);2 unmodified lines const followedDids = $derived.by(() => { if (!userDid) return new Set<Did>(); const map = follows.get(userDid); if (!map) return new Set<Did>(); return new Set(Array.from(map.values()).map((f) => f.subject)); return new Set(map.keys()); });
const threads = $derived( filterThreads(2 unmodified lines : [], $accounts, { viewOwnPosts, filterReplies: true, filterRootsToDids: followedDids } ) );
const loaderState = new LoaderState(); let scrollContainer = $state<HTMLDivElement>(); let loading = $state(false); let loadError = $state(''); 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 (loading || !client || !userDid) return; if (!client || !userDid) return;
loading = true; loaderState.status = 'LOADING'; await fetchFollowingTimeline(client, userDid);
try { await fetchFollowingTimeline(client, userDid);
if (client.user && userDid) { if (!fetchingInteractions) { scheduledFetchInteractions = false; fetchingInteractions = true; await fetchInteractionsToFollowingTimelineEnd(client, userDid); fetchingInteractions = false; } else { scheduledFetchInteractions = true; if (client.user && userDid) { if (!fetchingInteractions) { scheduledFetchInteractions = false; fetchingInteractions = true; await fetchInteractionsToFollowingTimelineEnd(client, userDid); fetchingInteractions = false; } else { scheduledFetchInteractions = true; } }
loaderState.loaded(); } catch (error) { loadError = `${error}`; loaderState.error(); loading = false; return; }
loading = false; };
$effect(() => { const isEmpty = threads.length === 0; if (isEmpty && !loading && userDid) { loadMore(); if (client && scheduledFetchInteractions && userDid) { if (!fetchingInteractions) { scheduledFetchInteractions = false; fetchingInteractions = true; fetchInteractionsToFollowingTimelineEnd(client, userDid).finally( () => (fetchingInteractions = false) ); } } });
$effect(() => { userDid; loaderState.reset(); });</script>
{#snippet replyPost(post: ThreadPost, reverse: boolean = reverseChronological)} <span class="mb-1.5 flex items-center gap-1.5 overflow-hidden text-nowrap wrap-break-word overflow-ellipsis" > <span class="text-sm text-nowrap opacity-60">{reverse ? '↱' : '↳'}</span> <BskyPost mini client={client!} {...post} /> </span>{/snippet}
{#snippet threadsView()} {#each threads as thread, i (thread.rootUri)} <div class="flex w-full shrink-0 {reverseChronological ? 'flex-col' : 'flex-col-reverse'}"> {#if thread.branchParentPost} {@render replyPost(thread.branchParentPost)} {/if} {#each thread.posts as post, idx (post.data.uri)} {@const mini = !expandedThreads.has(thread.rootUri) && thread.posts.length > 4 && idx > 0 && idx < thread.posts.length - 2} {#if !mini} <div class="mb-1.5"> <BskyPost client={client!} onQuote={(post) => { postComposerState.focus = 'focused'; postComposerState.quoting = post; }} onReply={(post) => { postComposerState.focus = 'focused'; postComposerState.replying = post; }} {...post} /> </div> {:else if mini} {#if idx === 1} {@render replyPost(post, !reverseChronological)} <button class="mx-1.5 mt-1.5 mb-2.5 flex items-center gap-1.5 text-[color-mix(in_srgb,var(--nucleus-fg)_50%,var(--nucleus-accent))]/70 transition-colors hover:text-(--nucleus-accent)" onclick={() => expandedThreads.add(thread.rootUri)} > <div class="mr-1 h-px w-20 rounded border-y-2 border-dashed opacity-50"></div> <Icon class="shrink-0" icon={reverseChronological ? 'heroicons:bars-arrow-up-solid' : 'heroicons:bars-arrow-down-solid'} width={32} /><span class="shrink-0 pb-1">view full chain</span> <div class="ml-1 h-px w-full rounded border-y-2 border-dashed opacity-50"></div> </button> {:else if idx === thread.posts.length - 3} {@render replyPost(post)} {/if} {/if} {/each} </div> {#if i < threads.length - 1} <div class="mx-8 mt-3 mb-4 h-px bg-linear-to-r from-(--nucleus-accent)/30 to-(--nucleus-accent2)/30" ></div> {/if} {/each}{/snippet}
<div class="min-h-full p-2 [scrollbar-color:var(--nucleus-accent)_transparent] {className}" bind:this={scrollContainer}> {#if userDid || $accounts.length > 0} <InfiniteLoader {loaderState} triggerLoad={loadMore} loopDetectionTimeout={0}> {@render threadsView()} {#snippet noData()} <EndOfList /> {/snippet} {#snippet loading()} <LoadingSpinner /> {/snippet} {#snippet error()} <LoadError error={loadError} onRetry={loadMore} /> {/snippet} </InfiniteLoader> {:else} <NotLoggedIn /> {/if}</div><GenericTimelineView {client} {threads} bind:postComposerState class={className} isLoggedIn={!!(userDid || $accounts.length > 0)} canLoad={!!(client && userDid)} onLoadMore={loadMore} {isComplete}/>47 unmodified lines4849505152535455565758595253546263646566676869705556575859606193 unmodified lines47 unmodified lines await new Promise((resolve) => setTimeout(resolve, 0));
const interactionScores = followingSort === 'conversational' ? calculateInteractionScores( selectedDid, followsMap, allPosts, allBacklinks, replyIndex, staticNow ) ? calculateInteractionScores(selectedDid, allPosts, allBacklinks, replyIndex, staticNow) : null;
const userStatsList = followsMap.values().map((f) => ({ did: f.subject, data: calculateFollowedUserStats( followingSort, f.subject, allPosts, interactionScores, staticNow ) const userStatsList = followsMap.keys().map((did) => ({ did, data: calculateFollowedUserStats(followingSort, did, allPosts, interactionScores, staticNow) }));
const following = userStatsList.filter((u) => u.data !== null); const sorted = [...following].sort((a, b) => sortFollowedUser(followingSort, a.data!, b.data!));93 unmodified lines123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193<script lang="ts"> import BskyPost from './BskyPost.svelte'; import { type State as PostComposerState } from './PostComposer.svelte'; import { AtpClient } from '$lib/at/client.svelte'; import { accounts } from '$lib/accounts'; import { type ResourceUri } from '@atcute/lexicons'; import { SvelteSet } from 'svelte/reactivity'; import { InfiniteLoader, LoaderState } from 'svelte-infinite'; import Icon from '@iconify/svelte'; import { type ThreadPost, type Thread } from '$lib/thread'; import type { Did } from '@atcute/lexicons/syntax'; import NotLoggedIn from './NotLoggedIn.svelte'; import LoadingSpinner from './LoadingSpinner.svelte'; import EndOfList from './EndOfList.svelte'; import LoadError from './LoadError.svelte'; import LoadNewPosts from './LoadNewPosts.svelte';
interface Props { client?: AtpClient | null; threads: Thread[]; postComposerState: PostComposerState; class?: string; isLoggedIn?: boolean; // Controls rendering of the list vs NotLoggedIn canLoad?: boolean; // Controls whether we can actually load more data onLoadMore: () => Promise<void>; isComplete?: boolean; }
let { client = null, threads, postComposerState = $bindable(), class: className = '', isLoggedIn = false, canLoad = undefined, onLoadMore, isComplete = false }: Props = $props();
// Default canLoad to isLoggedIn if not provided, for backward compatibility/simpler usage const shouldLoad = $derived(canLoad ?? isLoggedIn);
let reverseChronological = $state(true); const expandedThreads = new SvelteSet<ResourceUri>();
let isAtTop = $state(true); let boundaryTime = $state<number | null>(null);
const visibleThreads = $derived.by(() => { if (boundaryTime === null) return threads; return threads.filter((t) => t.newestTime <= boundaryTime!); });
$effect(() => { if (threads.length > 0) { if (isAtTop) { boundaryTime = threads[0].newestTime; } else if (boundaryTime === null) { boundaryTime = threads[0].newestTime; } } });
const showNewPosts = () => { boundaryTime = threads[0]?.newestTime ?? null; window.scrollTo({ top: 0, behavior: 'instant' }); isAtTop = true; };
const onScroll = () => (isAtTop = window.scrollY < 300);
const loaderState = new LoaderState(); let loading = $state(false); let loadError = $state('');
const loadMore = async () => { if (loading || !shouldLoad) return;
loading = true; loaderState.status = 'LOADING'; loadError = '';
try { await onLoadMore(); loaderState.loaded(); if (isComplete) loaderState.complete(); } catch (error) { loadError = `${error}`; loaderState.error(); loading = false; return; }
loading = false; };
$effect(() => { const isEmpty = threads.length === 0; if (isEmpty && !loading && shouldLoad && !isComplete) loadMore(); });</script>
<svelte:window onscroll={onScroll} />
{#snippet replyPost(post: ThreadPost, reverse: boolean = reverseChronological)} <span class="mb-1.5 flex items-center gap-1.5 overflow-hidden text-nowrap wrap-break-word overflow-ellipsis" > <span class="text-sm text-nowrap opacity-60">{reverse ? '↱' : '↳'}</span> <BskyPost mini client={client!} {...post} /> </span>{/snippet}
{#snippet threadsView()} {#each visibleThreads as thread, i (thread.rootUri)} <div class="flex w-full shrink-0 {reverseChronological ? 'flex-col' : 'flex-col-reverse'}"> {#if thread.branchParentPost} {@render replyPost(thread.branchParentPost)} {/if} {#each thread.posts as post, idx (post.data.uri)} {@const mini = !expandedThreads.has(thread.rootUri) && thread.posts.length > 4 && idx > 0 && idx < thread.posts.length - 2} {#if !mini} <div class="mb-1.5"> <BskyPost client={client!} onQuote={(post) => { postComposerState.focus = 'focused'; postComposerState.quoting = post; }} onReply={(post) => { postComposerState.focus = 'focused'; postComposerState.replying = post; }} {...post} /> </div> {:else if mini} {#if idx === 1} {@render replyPost(post, !reverseChronological)} <button class="mx-1.5 mt-1.5 mb-2.5 flex items-center gap-1.5 text-[color-mix(in_srgb,var(--nucleus-fg)_50%,var(--nucleus-accent))]/70 transition-colors hover:text-(--nucleus-accent)" onclick={() => expandedThreads.add(thread.rootUri)} > <div class="mr-1 h-px w-20 rounded border-y-2 border-dashed opacity-50"></div> <Icon class="shrink-0" icon={reverseChronological ? 'heroicons:bars-arrow-up-solid' : 'heroicons:bars-arrow-down-solid'} width={32} /><span class="shrink-0 pb-1">view full chain</span> <div class="ml-1 h-px w-full rounded border-y-2 border-dashed opacity-50"></div> </button> {:else if idx === thread.posts.length - 3} {@render replyPost(post)} {/if} {/if} {/each} </div> {#if i < visibleThreads.length - 1} <div class="mx-8 mt-3 mb-4 h-px bg-linear-to-r from-(--nucleus-accent)/30 to-(--nucleus-accent2)/30" ></div> {/if} {/each}{/snippet}
<div class="min-h-full p-2 [scrollbar-color:var(--nucleus-accent)_transparent] {className}"> <LoadNewPosts visible={threads.length > 0 && boundaryTime !== null && threads[0].newestTime > boundaryTime} onclick={showNewPosts} /> {#if isLoggedIn} <InfiniteLoader {loaderState} triggerLoad={loadMore} loopDetectionTimeout={0}> {@render threadsView()} {#snippet noData()} <EndOfList /> {/snippet} {#snippet loading()} <LoadingSpinner /> {/snippet} {#snippet error()} <LoadError error={loadError} onRetry={loadMore} /> {/snippet} </InfiniteLoader> {:else} <NotLoggedIn /> {/if}</div>12345678910111213141516171819202122<script lang="ts"> import Icon from '@iconify/svelte';
interface Props { visible: boolean; onclick: () => void; }
let { visible, onclick }: Props = $props();</script>
{#if visible} <div class="sticky top-2 z-20 mb-4 flex w-full justify-center"> <button class="flex action-button items-center gap-2 bg-(--nucleus-bg) hover:scale-115! enabled:hover:bg-(--nucleus-bg)!" {onclick} > <Icon icon="heroicons:arrow-up-16-solid" width="20" /> load new posts </button> </div>{/if}320 unmodified linesExpand all321322323324325326325326327328429 unmodified linesExpand all320 unmodified linesExpand all _state.attachedMedia = images.length > 0 ? { ..._state.attachedMedia, images } : undefined; };
const doPost = () => { if (_state.text.length === 0 || _state.text.length > 300) return;
postError = ''; posting = true; post(_state.text) .then((res) => {429 unmodified linesExpand all6 unmodified lines78910111112131416 unmodified lines31323334363738394035363738399 unmodified lines49505152585953545556575820 unmodified lines79808182888983848586878876 unmodified lines6 unmodified lines accountPreferences, createBlock, deleteBlock, follows, setAccountPreferences, updateAccountPreferences } from '$lib/state.svelte'; import { generateColorForDid } from '$lib/accounts'; import { now as tidNow } from '@atcute/tid';16 unmodified lines let actionsOpen = $state(false); let actionsPos = $state({ x: 0, y: 0 });
const followsMap = $derived(userDid ? follows.get(userDid) : undefined); const follow = $derived( followsMap ? Array.from(followsMap.entries()).find(([, follow]) => follow.subject === targetDid) : undefined ); const follow = $derived(followsMap ? followsMap.get(targetDid) : undefined);
const currentPrefs = $derived(userDid ? accountPreferences.get(userDid) : null); const mutes = $derived(currentPrefs?.mutes ?? []); const muted = $derived(mutes.includes(targetDid));9 unmodified lines const handleFollow = async () => { if (!userDid || !client.user) return;
if (follow) { const [uri] = follow; followsMap?.delete(uri); const { uri } = follow; followsMap?.delete(targetDid);
// extract rkey from uri const parsedUri = parseCanonicalResourceUri(uri); if (!parsedUri.ok) return;20 unmodified lines collection: 'app.bsky.graph.follow', rkey });
if (!followsMap) follows.set(userDid, new SvelteMap([[uri, record]])); else followsMap.set(uri, record); if (!followsMap) follows.set(userDid, new SvelteMap([[targetDid, { uri, record }]])); else followsMap.set(targetDid, { uri, record });
await client.user.atcute.post('com.atproto.repo.createRecord', { input: { repo: userDid,76 unmodified lines1223467856789101112171813142021222315161718199 unmodified lines29303132413343343536377 unmodified lines45464748596061626349505152685354707172737475767778798081828384555657585960616263646585668788899091929394679697686970101102103104105106107108109110111112113114115116717273746 unmodified lines8182838413113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721885868788899091929394<script lang="ts"> import BskyPost from './BskyPost.svelte'; import { type State as PostComposerState } from './PostComposer.svelte'; import { AtpClient } from '$lib/at/client.svelte'; import { accounts } from '$lib/accounts'; import { type ResourceUri } from '@atcute/lexicons'; import { SvelteSet } from 'svelte/reactivity'; import { InfiniteLoader, LoaderState } from 'svelte-infinite'; import { postCursors, fetchTimeline, allPosts, timelines, fetchInteractionsToTimelineEnd, accountPreferences } from '$lib/state.svelte'; import Icon from '@iconify/svelte'; import { buildThreads, filterThreads, type ThreadPost } from '$lib/thread'; import { buildThreads, filterThreads } from '$lib/thread'; import type { Did } from '@atcute/lexicons/syntax'; import NotLoggedIn from './NotLoggedIn.svelte'; import LoadingSpinner from './LoadingSpinner.svelte'; import EndOfList from './EndOfList.svelte'; import LoadError from './LoadError.svelte'; import GenericTimelineView from './GenericTimelineView.svelte';
interface Props { client?: AtpClient | null; targetDid?: Did;9 unmodified lines postComposerState = $bindable(), class: className = '' }: Props = $props();
let reverseChronological = $state(true); let viewOwnPosts = $state(true); const expandedThreads = new SvelteSet<ResourceUri>();
const userDid = $derived(client?.user?.did); const did = $derived(targetDid ?? userDid);
7 unmodified lines { viewOwnPosts } ) );
const loaderState = new LoaderState(); let scrollContainer = $state<HTMLDivElement>(); let loading = $state(false); let loadError = $state('');
let fetchingInteractions = $state(false); let scheduledFetchInteractions = $state(false);
const loadMore = async () => { if (loading || !client || !userDid || !did) return; if (!client || !userDid || !did) return;
loading = true; loaderState.status = 'LOADING';
try { await fetchTimeline(client, did, 7, showReplies, { downwards: userDid === did ? 'sameAuthor' : 'none' }); if (client.user && userDid) { if (!fetchingInteractions) { scheduledFetchInteractions = false; fetchingInteractions = true; await fetchInteractionsToTimelineEnd(client, userDid, did); fetchingInteractions = false; } else { scheduledFetchInteractions = true; await fetchTimeline(client, did, 7, showReplies, { downwards: userDid === did ? 'sameAuthor' : 'none' }); if (client.user && userDid) { if (!fetchingInteractions) { scheduledFetchInteractions = false; fetchingInteractions = true; await fetchInteractionsToTimelineEnd(client, userDid, did); fetchingInteractions = false; } else { scheduledFetchInteractions = true; } } loaderState.loaded(); const cursor = postCursors.get(did); if (cursor?.end) loaderState.complete(); } catch (error) { loadError = `${error}`; loaderState.error(); loading = false; return; }
loading = false; };
$effect(() => { const isEmpty = threads.length === 0; if (isEmpty && !loading && userDid && did) { const cursor = postCursors.get(did); if (!cursor?.end) loadMore(); } });
$effect(() => { did; loaderState.reset(); });
// we want to load interactions when changing logged in user // only on timelines that arent logged in users, because those are already // loaded by loadMore $effect(() => { if (client && scheduledFetchInteractions && userDid && did && did !== userDid) { if (!fetchingInteractions) { scheduledFetchInteractions = false; fetchingInteractions = true;6 unmodified lines } });</script>
{#snippet replyPost(post: ThreadPost, reverse: boolean = reverseChronological)} <span class="mb-1.5 flex items-center gap-1.5 overflow-hidden text-nowrap wrap-break-word overflow-ellipsis" > <span class="text-sm text-nowrap opacity-60">{reverse ? '↱' : '↳'}</span> <BskyPost mini client={client!} {...post} /> </span>{/snippet}
{#snippet threadsView()} {#each threads as thread, i (thread.rootUri)} <div class="flex w-full shrink-0 {reverseChronological ? 'flex-col' : 'flex-col-reverse'}"> {#if thread.branchParentPost} {@render replyPost(thread.branchParentPost)} {/if} {#each thread.posts as post, idx (post.data.uri)} {@const mini = !expandedThreads.has(thread.rootUri) && thread.posts.length > 4 && idx > 0 && idx < thread.posts.length - 2} {#if !mini} <div class="mb-1.5"> <BskyPost client={client!} onQuote={(post) => { postComposerState.focus = 'focused'; postComposerState.quoting = post; }} onReply={(post) => { postComposerState.focus = 'focused'; postComposerState.replying = post; }} {...post} /> </div> {:else if mini} {#if idx === 1} {@render replyPost(post, !reverseChronological)} <button class="mx-1.5 mt-1.5 mb-2.5 flex items-center gap-1.5 text-[color-mix(in_srgb,var(--nucleus-fg)_50%,var(--nucleus-accent))]/70 transition-colors hover:text-(--nucleus-accent)" onclick={() => expandedThreads.add(thread.rootUri)} > <div class="mr-1 h-px w-20 rounded border-y-2 border-dashed opacity-50"></div> <Icon class="shrink-0" icon={reverseChronological ? 'heroicons:bars-arrow-up-solid' : 'heroicons:bars-arrow-down-solid'} width={32} /><span class="shrink-0 pb-1">view full chain</span> <div class="ml-1 h-px w-full rounded border-y-2 border-dashed opacity-50"></div> </button> {:else if idx === thread.posts.length - 3} {@render replyPost(post)} {/if} {/if} {/each} </div> {#if i < threads.length - 1} <div class="mx-8 mt-3 mb-4 h-px bg-linear-to-r from-(--nucleus-accent)/30 to-(--nucleus-accent2)/30" ></div> {/if} {/each}{/snippet}
<div class="min-h-full p-2 [scrollbar-color:var(--nucleus-accent)_transparent] {className}" bind:this={scrollContainer}> {#if did || $accounts.length > 0} <InfiniteLoader {loaderState} triggerLoad={loadMore} loopDetectionTimeout={0}> {@render threadsView()} {#snippet noData()} <EndOfList /> {/snippet} {#snippet loading()} <LoadingSpinner /> {/snippet} {#snippet error()} <LoadError error={loadError} onRetry={loadMore} /> {/snippet} </InfiniteLoader> {:else} <NotLoggedIn /> {/if}</div><GenericTimelineView {client} {threads} bind:postComposerState class={className} isLoggedIn={!!(did || $accounts.length > 0)} canLoad={!!(client && userDid && did)} onLoadMore={loadMore} isComplete={did ? postCursors.get(did)?.end : false}/>134 unmodified linesExpand all13513613713813913914014114297 unmodified lines134 unmodified linesExpand all};
export const calculateInteractionScores = ( user: Did, followsMap: Map<ResourceUri, AppBskyGraphFollow.Main>, allPosts: Map<Did, Map<ResourceUri, PostWithUri>>, allBacklinks: Map<BacklinksSource, Map<ResourceUri, Map<Did, Set<string>>>>, replyIndex: Map<Did, Set<ResourceUri>>, now: number97 unmodified lines29 unmodified lines3031323334353637384 unmodified lines29 unmodified linesexport const replySource: BacklinksSource = 'app.bsky.feed.post:reply.parent.uri';export const replyRootSource: BacklinksSource = 'app.bsky.feed.post:reply.root.uri';export const blockSource: BacklinksSource = 'app.bsky.graph.block:subject';
// returns nanosexport const timestampFromCursor = (cursor: string | undefined) => { if (!cursor) return undefined; try { return parseTid(cursor).timestamp;4 unmodified lines33 unmodified lines343536373839404142123 unmodified linesExpand all166167168169169170171170171172173174134 unmodified linesExpand all30931031131231331431531631731831932063 unmodified lines33 unmodified lines import { settings } from '$lib/settings'; import type { Sort } from '$lib/following'; import { SvelteMap } from 'svelte/reactivity'; import FeedSelector from '$components/FeedSelector.svelte'; import { extractDidFromUri } from '$lib';
const { data: loadData }: PageProps = $props();
const currentRoute = $derived(router.current);123 unmodified linesExpand all });
$effect(() => { const wantedDids: Did[] = ['did:web:guestbook.gaze.systems']; const followDids = follows .values() .flatMap((followMap) => followMap.values().map((follow) => follow.subject)); const followDids = follows.values().flatMap((followMap) => followMap.keys()); const accountDids = $accounts.values().map((account) => account.did); wantedDids.push(...followDids, ...accountDids); // console.log('updating jetstream options:', wantedDids); $jetstream?.updateOptions({ wantedDids });134 unmodified linesExpand all client={selectedClient} onPostSent={(post) => { addPosts([post]); addTimeline(selectedDid!, [post.uri]); if (post.record.reply) { const parentDid = extractDidFromUri(post.record.reply.parent.uri)!; addTimeline(parentDid, [post.uri]); } }} bind:_state={postComposerState} /> </div>63 unmodified lines