Files
moshing-mammut/src/routes/+page.svelte

218 lines
6.2 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte';
import type { PageData } from './$types';
import type { Post } from '$lib/mastodon/response';
import {
PUBLIC_REFRESH_INTERVAL,
PUBLIC_MASTODON_INSTANCE_DISPLAY_NAME
} from '$env/static/public';
import PostComponent from '$lib/components/PostComponent.svelte';
import LoadMoreComponent from '$lib/components/LoadMoreComponent.svelte';
import { fly, type FlyParams } from 'svelte/transition';
import { cubicInOut } from 'svelte/easing';
import { errorToast } from '$lib/errorToast';
interface Props {
data: PageData;
}
let { data = $bindable() }: Props = $props();
let posts: Post[] = $state(data.posts);
interface FetchOptions {
since?: string;
before?: string;
count?: number;
}
interface EdgeFlyParams extends FlyParams {
created_at: string;
}
const refreshInterval = parseInt(PUBLIC_REFRESH_INTERVAL);
let interval: ReturnType<typeof setTimeout> | null = null;
let moreOlderPostsAvailable = $state(true);
let loadingOlderPosts = $state(false);
// Needed, so that edgeFly() can do its thing:
// To determine whether a newly loaded post is older than the existing ones, is required to know what the oldest
// post was, before the fetch happened.
let oldestBeforeLastFetch: number | null = null;
/**
* Animate either from the top, or the bottom of the window, depending if the post is
* newer than the existing ones or older.
*/
function edgeFly(node: Element, opts: EdgeFlyParams) {
const createdAt = new Date(opts.created_at).getTime();
const diffNewest = Math.abs(new Date(posts[0].created_at).getTime() - createdAt);
const oldest =
oldestBeforeLastFetch !== null
? oldestBeforeLastFetch
: new Date(posts[posts.length - 1].created_at).getTime();
const diffOldest = Math.abs(oldest - createdAt);
const fromTop = diffNewest <= diffOldest;
const rect = node.getBoundingClientRect();
const paramY = +`${opts.y}`;
let offset = isNaN(paramY) ? 0 : paramY + rect.height;
opts.y = fromTop ? -offset : window.innerHeight + offset;
return fly(node, opts);
}
async function fetchPosts(options: FetchOptions): Promise<Post[]> {
const params = new URLSearchParams();
if (options?.since !== undefined) {
params.set('since', options.since);
}
if (options?.before !== undefined) {
params.set('before', options.before);
}
if (options?.count !== undefined) {
params.set('count', options.count.toFixed(0));
}
const response = await fetch(`/api/posts?${params}`);
return await response.json();
}
function filterDuplicates(posts: Post[]): Post[] {
return posts.filter((obj, index, arr) => {
return arr.map((mapObj) => mapObj.url).indexOf(obj.url) === index;
});
}
function refresh() {
let filter: FetchOptions = {};
if (posts.length > 0) {
filter = { since: posts[0].created_at };
}
fetchPosts(filter)
.then((resp) => {
if (resp.length > 0) {
// Prepend new posts, filter dupes
// There shouldn't be any duplicates, but better be safe than sorry
posts = filterDuplicates(resp.concat(posts));
}
})
.catch((e: Error) => {
errorToast('Error loading newest posts: ' + e.message);
});
}
onMount(async () => {
posts = data.posts;
if (posts.length > 0) {
oldestBeforeLastFetch = new Date(posts[posts.length - 1].created_at).getTime();
}
interval = setInterval(refresh, refreshInterval);
// - If the page is hidden, slow down refresh rate
// - If the page is shown, bump up refresh rate
document.addEventListener('visibilitychange', () => {
const delay = document.hidden ? refreshInterval * 10 : refreshInterval;
if (interval) {
clearInterval(interval);
}
interval = setInterval(refresh, delay);
});
return () => {
if (interval !== null) {
clearInterval(interval);
}
};
});
function loadOlderPosts() {
loadingOlderPosts = true;
const filter: FetchOptions = { count: 20 };
if (posts.length > 0) {
const before = posts[posts.length - 1].created_at;
filter.before = before;
oldestBeforeLastFetch = new Date(before).getTime();
}
fetchPosts(filter)
.then((resp) => {
if (resp.length > 0) {
// Append old posts, filter dupes
// There shouldn't be any duplicates, but better be safe than sorry
posts = filterDuplicates(posts.concat(resp));
// If we got less than we expected, there are no older posts available
moreOlderPostsAvailable = resp.length >= (filter.count ?? 20);
} else {
moreOlderPostsAvailable = false;
}
loadingOlderPosts = false;
})
.catch((e) => {
loadingOlderPosts = false;
errorToast('Error loading older posts: ' + e.message);
});
}
</script>
<svelte:head>
<title>{PUBLIC_MASTODON_INSTANCE_DISPLAY_NAME} music list</title>
</svelte:head>
<h2>{PUBLIC_MASTODON_INSTANCE_DISPLAY_NAME} music list</h2>
<div class="wrapper">
<div></div>
<div class="posts">
{#if posts.length === 0}
Sorry, no posts recommending music have been found yet
{/if}
{#each posts as post (post.url)}
<div
class="post"
transition:edgeFly|global={{
y: 10,
created_at: post.created_at,
duration: 300,
easing: cubicInOut
}}
>
<PostComponent {post} />
</div>
{/each}
<LoadMoreComponent
{loadOlderPosts}
moreAvailable={moreOlderPostsAvailable}
isLoading={loadingOlderPosts}
/>
</div>
<div></div>
</div>
<style>
.posts {
display: flex;
flex-direction: column;
align-items: center;
}
.post {
width: 100%;
max-width: min(800px, 80vw);
margin-bottom: 1em;
border-bottom: 1px solid var(--color-border);
padding: 1em;
}
.wrapper {
display: flex;
justify-content: space-between;
}
h2 {
text-align: center;
z-index: 100;
}
@media only screen and (max-width: 650px) {
.post {
max-width: calc(100vw - 16px);
padding: 1em 0;
}
}
</style>