Compare commits
5 commits
7db5ec7fae
...
daaa819e6c
Author | SHA1 | Date | |
---|---|---|---|
daaa819e6c | |||
ea1a492dc0 | |||
3b8ca902f1 | |||
99def58c8b | |||
c51a0b1e5d |
8 changed files with 219 additions and 49 deletions
|
@ -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;
|
||||||
}
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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">
|
||||||
|
|
36
src/routes/favourites/+page.svelte
Normal file
36
src/routes/favourites/+page.svelte
Normal 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>
|
|
@ -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();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue