From c51a0b1e5def05fa7cf0870107b804e7eacbd9de Mon Sep 17 00:00:00 2001 From: mae taylor Date: Tue, 15 Jul 2025 15:08:37 +0100 Subject: [PATCH 1/5] feat: local and federated timelines --- src/lib/api.js | 4 ++- src/lib/timeline.js | 10 ++++--- src/routes/+page.svelte | 61 +++++++++++++++++++++++++++++++++++++---- 3 files changed, 64 insertions(+), 11 deletions(-) diff --git a/src/lib/api.js b/src/lib/api.js index 07644a3..4fc3606 100644 --- a/src/lib/api.js +++ b/src/lib/api.js @@ -243,11 +243,13 @@ export async function rejectFollowRequest(host, token, account_id) { * @param {string} timeline - The name of the timeline to pull (default "home"). * @param {string} max_id - If provided, only shows posts after this ID. */ -export async function getTimeline(host, token, timeline, max_id) { +export async function getTimeline(host, token, timeline, max_id, local_only, remote_only) { let url = `https://${host}/api/v1/timelines/${timeline || "home"}`; let params = new URLSearchParams(); if (max_id) params.append("max_id", max_id); + if (remote_only) params.append("remote", remote_only); + if (local_only) params.append("local", local_only); const params_string = params.toString(); if (params_string) url += '?' + params_string; diff --git a/src/lib/timeline.js b/src/lib/timeline.js index 66db44d..b5f8c2e 100644 --- a/src/lib/timeline.js +++ b/src/lib/timeline.js @@ -11,7 +11,7 @@ const lang = Lang(); let loading = false; -export async function getTimeline(clean) { +export async function getTimeline(timelineType = "home", clean, localOnly = false, remoteOnly = false) { if (loading) return; // no spamming!! loading = true; @@ -22,9 +22,11 @@ export async function getTimeline(clean) { const timeline_data = await api.getTimeline( get(server).host, get(app).token, - "home", - last_post - ); + timelineType, + last_post, + localOnly, + remoteOnly +); if (!timeline_data) { console.error(lang.string('logs.timeline_fetch_failed')); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 766a6a4..3375ab4 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -2,6 +2,7 @@ import { page } from '$app/stores'; import { account } from '$lib/stores/account.js'; import { timeline, getTimeline } from '$lib/timeline.js'; + import { app_name } from '$lib/config.js'; import Lang from '$lib/lang'; import LoginForm from '$lib/ui/LoginForm.svelte'; @@ -11,23 +12,71 @@ const lang = Lang(); + // TODO: refactor to enum when moving to TS + let timelineType = localStorage.getItem(app_name + '_selected_timeline') || "home"; + + $: { + // awful hack to update timeline fresh + // when timelineType is updated + // + // TODO: migrate to $effect when migrating to svelte 5 + timelineType = timelineType + + // set in localStorage + localStorage.setItem(app_name + '_selected_timeline', timelineType); + + // erase the timeline here so the ui reacts instantly + // mae: i could write an awesome undertale reference here + timeline.set([]); + + getCurrentTimeline() + } + + function getCurrentTimeline(clean = false) { + switch(timelineType) { + case "home": + getTimeline("home", clean); + break; + + case "local": + getTimeline("public", clean, true) + break; + + case "federated": + getTimeline("public", clean, false, true) + break; + } + } + account.subscribe(account => { - if (account) getTimeline(); + if (account) getCurrentTimeline(); }); document.addEventListener('scroll', () => { if ($account && $page.url.pathname !== "/") return; if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 2048) { - getTimeline(); + getCurrentTimeline(); } }); {#if $account} - - - - + + + +
From 99def58c8b1799ad4f18c0e8f8752259d1fc4f15 Mon Sep 17 00:00:00 2001 From: mae taylor Date: Tue, 15 Jul 2025 15:37:03 +0100 Subject: [PATCH 2/5] feat: partial favourites --- src/lib/api.js | 24 ++++++++++++++++++++ src/lib/timeline.js | 29 +++++++++++++++++------- src/routes/favourites/+page.svelte | 36 ++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 src/routes/favourites/+page.svelte diff --git a/src/lib/api.js b/src/lib/api.js index 4fc3606..4c8c38c 100644 --- a/src/lib/api.js +++ b/src/lib/api.js @@ -242,6 +242,8 @@ export async function rejectFollowRequest(host, token, account_id) { * @param {string} token - The application token. * @param {string} timeline - The name of the timeline to pull (default "home"). * @param {string} max_id - If provided, only shows posts after this ID. + * @param {boolean} local_only - If provided, only shows posts from the local instance + * @param {boolean} remote_only - If provided, only shows posts from other instances */ export async function getTimeline(host, token, timeline, max_id, local_only, remote_only) { let url = `https://${host}/api/v1/timelines/${timeline || "home"}`; @@ -558,3 +560,25 @@ export async function getUserPinnedPosts(host, token, user_id) { return data; } + +/** + * GET /api/v1/favourites + * @param {string} host - The domain of the target server. + * @param {string} token - The application token. + * @param {string} max_id - If provided, only shows posts after this ID. + */ +export async function getFavourites(host, token, timeline, max_id) { + let url = `https://${host}/api/v1/favourites`; + + let params = new URLSearchParams(); + if (max_id) params.append("max_id", max_id); + const params_string = params.toString(); + if (params_string) url += '?' + params_string; + + const data = await fetch(url, { + method: 'GET', + headers: { "Authorization": token ? `Bearer ${token}` : null } + }).then(res => res.json()); + + return data; +} diff --git a/src/lib/timeline.js b/src/lib/timeline.js index b5f8c2e..0f54e91 100644 --- a/src/lib/timeline.js +++ b/src/lib/timeline.js @@ -19,14 +19,27 @@ export async function getTimeline(timelineType = "home", clean, localOnly = fals if (!clean && get(timeline).length > 0) last_post = get(timeline)[get(timeline).length - 1].id; - const timeline_data = await api.getTimeline( - get(server).host, - get(app).token, - timelineType, - last_post, - localOnly, - remoteOnly -); + let timeline_data; + switch(timelineType) { + case "favourites": + timeline_data = await api.getFavourites( + get(server).host, + get(app).token, + last_post, + ) + break; + + default: + timeline_data = await api.getTimeline( + get(server).host, + get(app).token, + timelineType, + last_post, + localOnly, + remoteOnly + ); + break; + } if (!timeline_data) { console.error(lang.string('logs.timeline_fetch_failed')); diff --git a/src/routes/favourites/+page.svelte b/src/routes/favourites/+page.svelte new file mode 100644 index 0000000..0fa8d44 --- /dev/null +++ b/src/routes/favourites/+page.svelte @@ -0,0 +1,36 @@ + + + + +
+ {#if $timeline.length <= 0} +
+ {lang.string('timeline.fetching')} +
+ {/if} + {#each $timeline as post} + + {/each} +
\ No newline at end of file From 3b8ca902f1457d2d2ea28c9af7ef99f2fc41643b Mon Sep 17 00:00:00 2001 From: mae taylor Date: Tue, 15 Jul 2025 16:34:23 +0100 Subject: [PATCH 3/5] refactor: use Link HTTP header for pagination --- src/lib/api.js | 87 ++++++++++++++++++++++++++++++++------------- src/lib/timeline.js | 19 ++++++---- 2 files changed, 74 insertions(+), 32 deletions(-) diff --git a/src/lib/api.js b/src/lib/api.js index 4c8c38c..812ddd0 100644 --- a/src/lib/api.js +++ b/src/lib/api.js @@ -2,6 +2,31 @@ const errors = { AUTHENTICATION_FAILED: "AUTHENTICATION_FAILED", }; +/** + * Parses a HTTP Link header + * @param {string} header - the HTTP Link header string + */ +function _parseLinkHeader(header) { + // remove whitespace and split + let links = header.replace(/\ /g, "").split(","); + + return links.map(l => { + let parts = l.split(";"); + + // assuming 0th is URL, removing <> + let url = new URL(parts[0].slice(1, -1)); + + // get rel inbetween double-quotes + let rel = parts[1].match(/"(.*?)"/g)[0].slice(1, -1); + + return { + url, rel + } + }) +} + +_parseLinkHeader(`; rel="next", ; rel="prev"`) + /** * GET /api/v1/instance * @param {string} host - The domain of the target server. @@ -258,9 +283,43 @@ export async function getTimeline(host, token, timeline, max_id, local_only, rem const data = await fetch(url, { method: 'GET', headers: { "Authorization": token ? `Bearer ${token}` : null } - }).then(res => res.json()); + }) - return data; + let links = _parseLinkHeader(data.headers.get("Link")); + + return { + data: await data.json(), + prev: links.find(f=>f.rel=="prev"), + next: links.find(f=>f.rel=="next") + }; +} + +/** + * GET /api/v1/favourites + * @param {string} host - The domain of the target server. + * @param {string} token - The application token. + * @param {string} max_id - If provided, only shows posts after this ID. + */ +export async function getFavourites(host, token, max_id) { + let url = `https://${host}/api/v1/favourites`; + + let params = new URLSearchParams(); + if (max_id) params.append("max_id", max_id); + const params_string = params.toString(); + if (params_string) url += '?' + params_string; + + const data = await fetch(url, { + method: 'GET', + headers: { "Authorization": token ? `Bearer ${token}` : null } + }) + + let links = _parseLinkHeader(data.headers.get("Link")); + + return { + data: await data.json(), + prev: links.find(f=>f.rel=="prev"), + next: links.find(f=>f.rel=="next") + }; } /** @@ -559,26 +618,4 @@ export async function getUserPinnedPosts(host, token, user_id) { }).then(res => res.json()); return data; -} - -/** - * GET /api/v1/favourites - * @param {string} host - The domain of the target server. - * @param {string} token - The application token. - * @param {string} max_id - If provided, only shows posts after this ID. - */ -export async function getFavourites(host, token, timeline, max_id) { - let url = `https://${host}/api/v1/favourites`; - - let params = new URLSearchParams(); - if (max_id) params.append("max_id", max_id); - const params_string = params.toString(); - if (params_string) url += '?' + params_string; - - const data = await fetch(url, { - method: 'GET', - headers: { "Authorization": token ? `Bearer ${token}` : null } - }).then(res => res.json()); - - return data; -} +} \ No newline at end of file diff --git a/src/lib/timeline.js b/src/lib/timeline.js index 0f54e91..6b6050f 100644 --- a/src/lib/timeline.js +++ b/src/lib/timeline.js @@ -10,14 +10,14 @@ export const timeline = writable([]); const lang = Lang(); let loading = false; +let last_post = false; export async function getTimeline(timelineType = "home", clean, localOnly = false, remoteOnly = false) { if (loading) return; // no spamming!! loading = true; - let last_post = false; - if (!clean && get(timeline).length > 0) - last_post = get(timeline)[get(timeline).length - 1].id; + // if (!clean && get(timeline).length > 0) + // last_post = get(timeline)[get(timeline).length - 1].id; let timeline_data; switch(timelineType) { @@ -25,7 +25,7 @@ export async function getTimeline(timelineType = "home", clean, localOnly = fals timeline_data = await api.getFavourites( get(server).host, get(app).token, - last_post, + last_post ) break; @@ -47,10 +47,15 @@ export async function getTimeline(timelineType = "home", clean, localOnly = fals return; } - if (clean) timeline.set([]); + if (clean) { + timeline.set([]); + last_post = false; + } else { + last_post = timeline_data.next.url.searchParams.get("max_id") + } - for (let i in timeline_data) { - const post_data = timeline_data[i]; + for (let i in timeline_data.data) { + const post_data = timeline_data.data[i]; const post = await parsePost(post_data, 1); if (!post) { if (post === null || post === undefined) { From ea1a492dc0e83aa44ffd9e5aa90abb1a0aa22b22 Mon Sep 17 00:00:00 2001 From: mae taylor Date: Tue, 15 Jul 2025 16:42:21 +0100 Subject: [PATCH 4/5] fix: timing update on timeline fetcher --- src/lib/timeline.js | 11 +++++------ src/lib/ui/Navigation.svelte | 5 ++++- src/routes/favourites/+page.svelte | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/lib/timeline.js b/src/lib/timeline.js index 6b6050f..68aa2b2 100644 --- a/src/lib/timeline.js +++ b/src/lib/timeline.js @@ -16,8 +16,10 @@ export async function getTimeline(timelineType = "home", clean, localOnly = fals if (loading) return; // no spamming!! loading = true; - // if (!clean && get(timeline).length > 0) - // last_post = get(timeline)[get(timeline).length - 1].id; + if(clean) { + timeline.set([]); + last_post = false; + } let timeline_data; switch(timelineType) { @@ -47,10 +49,7 @@ export async function getTimeline(timelineType = "home", clean, localOnly = fals return; } - if (clean) { - timeline.set([]); - last_post = false; - } else { + if (!clean) { last_post = timeline_data.next.url.searchParams.get("max_id") } diff --git a/src/lib/ui/Navigation.svelte b/src/lib/ui/Navigation.svelte index 384450d..b0fa105 100644 --- a/src/lib/ui/Navigation.svelte +++ b/src/lib/ui/Navigation.svelte @@ -124,7 +124,10 @@
-