Compare commits

...

5 commits

8 changed files with 219 additions and 49 deletions

View file

@ -2,6 +2,31 @@ const errors = {
AUTHENTICATION_FAILED: "AUTHENTICATION_FAILED", 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(`<https://wetdry.world/api/v1/timelines/home?max_id=114857293229157171>; rel="next", <https://wetdry.world/api/v1/timelines/home?min_id=114857736990577458>; rel="prev"`)
/** /**
* GET /api/v1/instance * GET /api/v1/instance
* @param {string} host - The domain of the target server. * @param {string} host - The domain of the target server.
@ -242,10 +267,42 @@ export async function rejectFollowRequest(host, token, account_id) {
* @param {string} token - The application token. * @param {string} token - The application token.
* @param {string} timeline - The name of the timeline to pull (default "home"). * @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 {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) { export async function getTimeline(host, token, timeline, max_id, local_only, remote_only) {
let url = `https://${host}/api/v1/timelines/${timeline || "home"}`; 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;
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")
};
}
/**
* 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(); let params = new URLSearchParams();
if (max_id) params.append("max_id", max_id); if (max_id) params.append("max_id", max_id);
const params_string = params.toString(); const params_string = params.toString();
@ -254,9 +311,15 @@ export async function getTimeline(host, token, timeline, max_id) {
const data = await fetch(url, { const data = await fetch(url, {
method: 'GET', method: 'GET',
headers: { "Authorization": token ? `Bearer ${token}` : null } 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")
};
} }
/** /**
@ -555,4 +618,4 @@ export async function getUserPinnedPosts(host, token, user_id) {
}).then(res => res.json()); }).then(res => res.json());
return data; return data;
} }

View file

@ -10,21 +10,38 @@ export const timeline = writable([]);
const lang = Lang(); const lang = Lang();
let loading = false; let loading = false;
let last_post = false;
export async function getTimeline(clean) { export async function getTimeline(timelineType = "home", clean, localOnly = false, remoteOnly = false) {
if (loading) return; // no spamming!! if (loading) return; // no spamming!!
loading = true; loading = true;
let last_post = false; if(clean) {
if (!clean && get(timeline).length > 0) timeline.set([]);
last_post = get(timeline)[get(timeline).length - 1].id; last_post = false;
}
const timeline_data = await api.getTimeline( let timeline_data;
get(server).host, switch(timelineType) {
get(app).token, case "favourites":
"home", timeline_data = await api.getFavourites(
last_post 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) { if (!timeline_data) {
console.error(lang.string('logs.timeline_fetch_failed')); console.error(lang.string('logs.timeline_fetch_failed'));
@ -32,10 +49,12 @@ export async function getTimeline(clean) {
return; return;
} }
if (clean) timeline.set([]); if (!clean) {
last_post = timeline_data.next.url.searchParams.get("max_id")
}
for (let i in timeline_data) { for (let i in timeline_data.data) {
const post_data = timeline_data[i]; const post_data = timeline_data.data[i];
const post = await parsePost(post_data, 1); const post = await parsePost(post_data, 1);
if (!post) { if (!post) {
if (post === null || post === undefined) { if (post === null || post === undefined) {

View file

@ -14,18 +14,18 @@
import Logo from '$lib/../img/campfire-logo.svg'; import Logo from '$lib/../img/campfire-logo.svg';
import Button from './Button.svelte'; import Button from './Button.svelte';
import TimelineIcon from '../../img/icons/timeline.svg'; import TimelineIcon from '@cf/icons/timeline.svg';
import NotificationsIcon from '../../img/icons/notifications.svg'; import NotificationsIcon from '@cf/icons/notifications.svg';
import ExploreIcon from '../../img/icons/explore.svg'; import ExploreIcon from '@cf/icons/explore.svg';
import ListIcon from '../../img/icons/lists.svg'; import ListIcon from '@cf/icons/lists.svg';
import FavouritesIcon from '../../img/icons/like_fill.svg'; import FavouritesIcon from '@cf/icons/like_fill.svg';
import BookmarkIcon from '../../img/icons/bookmark.svg'; import BookmarkIcon from '@cf/icons/bookmark.svg';
import HashtagIcon from '../../img/icons/hashtag.svg'; import HashtagIcon from '@cf/icons/hashtag.svg';
import PostIcon from '../../img/icons/post.svg'; import PostIcon from '@cf/icons/post.svg';
import InfoIcon from '../../img/icons/info.svg'; import InfoIcon from '@cf/icons/info.svg';
import SettingsIcon from '../../img/icons/settings.svg'; import SettingsIcon from '@cf/icons/settings.svg';
import LogoutIcon from '../../img/icons/logout.svg'; import LogoutIcon from '@cf/icons/logout.svg';
import FollowersIcon from '../../img/icons/followers.svg'; import FollowersIcon from '@cf/icons/followers.svg';
const VERSION = APP_VERSION; const VERSION = APP_VERSION;
const lang = Lang(); const lang = Lang();
@ -124,7 +124,10 @@
</Button> </Button>
<div class="flex-row"> <div class="flex-row">
<Button centered label="{lang.string('navigation.favourites')}" disabled> <Button centered
label="{lang.string('navigation.favourites')}"
href="/favourites"}
active={$page.url.pathname === "/favourites"}>
<svelte:fragment slot="icon"> <svelte:fragment slot="icon">
<FavouritesIcon/> <FavouritesIcon/>
</svelte:fragment> </svelte:fragment>

View file

@ -2,10 +2,10 @@
import { server } from '$lib/client/server'; import { server } from '$lib/client/server';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import ReplyIcon from '$lib/../img/icons/reply.svg'; import ReplyIcon from '@cf/icons/reply.svg';
import RepostIcon from '$lib/../img/icons/repost.svg'; import RepostIcon from '@cf/icons/repost.svg';
import FavouriteIcon from '$lib/../img/icons/like.svg'; import FavouriteIcon from '@cf/icons/like.svg';
import ReactIcon from '$lib/../img/icons/react.svg'; import ReactIcon from '@cf/icons/react.svg';
// import QuoteIcon from '$lib/../img/icons/quote.svg'; // import QuoteIcon from '$lib/../img/icons/quote.svg';
import ReactionBar from '$lib/ui/post/ReactionBar.svelte'; import ReactionBar from '$lib/ui/post/ReactionBar.svelte';
import ActionBar from '$lib/ui/post/ActionBar.svelte'; import ActionBar from '$lib/ui/post/ActionBar.svelte';

View file

@ -10,13 +10,13 @@
import ActionButton from './ActionButton.svelte'; import ActionButton from './ActionButton.svelte';
import ReplyIcon from '../../../img/icons/reply.svg'; import ReplyIcon from '@cf/icons/reply.svg';
import RepostIcon from '../../../img/icons/repost.svg'; import RepostIcon from '@cf/icons/repost.svg';
import FavouriteIcon from '../../../img/icons/like.svg'; import FavouriteIcon from '@cf/icons/like.svg';
import FavouriteIconFill from '../../../img/icons/like_fill.svg'; import FavouriteIconFill from '@cf/icons/like_fill.svg';
import QuoteIcon from '../../../img/icons/quote.svg'; import QuoteIcon from '@cf/icons/quote.svg';
import MoreIcon from '../../../img/icons/more.svg'; import MoreIcon from '@cf/icons/more.svg';
import DeleteIcon from '../../../img/icons/bin.svg'; import DeleteIcon from '@cf/icons/bin.svg';
export let post; export let post;

View file

@ -2,6 +2,7 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import { account } from '$lib/stores/account.js'; import { account } from '$lib/stores/account.js';
import { timeline, getTimeline } from '$lib/timeline.js'; import { timeline, getTimeline } from '$lib/timeline.js';
import { app_name } from '$lib/config.js';
import Lang from '$lib/lang'; import Lang from '$lib/lang';
import LoginForm from '$lib/ui/LoginForm.svelte'; import LoginForm from '$lib/ui/LoginForm.svelte';
@ -11,23 +12,71 @@
const lang = Lang(); 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 => { account.subscribe(account => {
if (account) getTimeline(); if (account) getCurrentTimeline();
}); });
document.addEventListener('scroll', () => { document.addEventListener('scroll', () => {
if ($account && $page.url.pathname !== "/") return; if ($account && $page.url.pathname !== "/") return;
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 2048) { if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 2048) {
getTimeline(); getCurrentTimeline();
} }
}); });
</script> </script>
{#if $account} {#if $account}
<PageHeader title={lang.string('timeline.home')}> <PageHeader title={lang.string(`timeline.${timelineType}`)}>
<Button centered active>{lang.string('timeline.home')}</Button> <Button centered
<Button centered disabled>{lang.string('timeline.local')}</Button> active={(timelineType == "home")}
<Button centered disabled>{lang.string('timeline.federated')}</Button> on:click={() => timelineType = "home"}>
{lang.string('timeline.home')}
</Button>
<Button centered
active={(timelineType == "local")}
on:click={() => timelineType = "local"}>
{lang.string('timeline.local')}
</Button>
<Button centered
active={(timelineType == "federated")}
on:click={() => timelineType = "federated"}>
{lang.string('timeline.federated')}
</Button>
</PageHeader> </PageHeader>
<div id="feed" role="feed"> <div id="feed" role="feed">

View file

@ -0,0 +1,36 @@
<script>
import { page } from '$app/stores';
import { account } from '@cf/store/account.js';
import { timeline, getTimeline } from '$lib/timeline.js';
import Button from '@cf/ui/Button.svelte';
import Post from '@cf/ui/post/Post.svelte';
import PageHeader from '@cf/ui/core/PageHeader.svelte';
import Lang from '$lib/lang';
const lang = Lang();
if (!$account) goto("/");
getTimeline("favourites", true);
document.addEventListener('scroll', () => {
if ($account && $page.url.pathname !== "/favourites") return;
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 2048) {
getTimeline("favourites");
}
});
</script>
<PageHeader title={lang.string(`navigation.favourites`)}/>
<div id="feed" role="feed">
{#if $timeline.length <= 0}
<div class="loading throb">
<span>{lang.string('timeline.fetching')}</span>
</div>
{/if}
{#each $timeline as post}
<Post post_data={post} />
{/each}
</div>

View file

@ -7,8 +7,8 @@
import Button from '../../lib/ui/Button.svelte'; import Button from '../../lib/ui/Button.svelte';
import * as api from '$lib/api' import * as api from '$lib/api'
import TickIcon from '../../img/icons/tick.svg' import TickIcon from '@cf/icons/tick.svg'
import CrossIcon from '../../img/icons/cross.svg' import CrossIcon from '@cf/icons/cross.svg'
import { get } from 'svelte/store'; import { get } from 'svelte/store';
const lang = Lang(); const lang = Lang();