Display replies on the post page

main
Charlotte Som 2022-05-11 11:53:41 +01:00
parent 55030b1e54
commit a8f518f4b8
8 changed files with 148 additions and 15 deletions

2
src/app.d.ts vendored
View File

@ -3,6 +3,6 @@
// See https://kit.svelte.dev/docs/types#app
declare namespace App {
interface Session {
instance: { url: string; token?: string };
instance: { url: string; token: string };
}
}

View File

@ -2,7 +2,7 @@ import type { GetSession } from '@sveltejs/kit';
export const getSession: GetSession = async () => {
const session: App.Session = {
instance: { url: 'https://trans.enby.town', token: 'bweep' }
instance: { url: 'https://trans.enby.town', token: '' }
};
return session;
};

View File

@ -3,22 +3,27 @@
import { now } from '$lib/global-now';
import { linkAccount } from '$lib/mastoapi/account';
import { linkStatus, type MastodonStatus } from '$lib/mastoapi/status';
import {
linkStatus,
type MastodonStatus,
type MastodonStatusContext
} from '$lib/mastoapi/status';
import StatusControls from './StatusControls.svelte';
export let status: MastodonStatus;
export let focused: boolean = false;
</script>
<article class="status">
<article class="status" class:focused>
<div class="status-content">
<aside>
<aside class="status-author">
<a href={linkAccount(status.account)}>
<img class="avatar" alt={status.account.acct} src={status.account.avatar_static} />
</a>
</aside>
<div class="status-main">
<div class="top-line">
<section class="top-line">
<a href={linkAccount(status.account)} class="author-link">
<strong class="author-display-name">{status.account.display_name}</strong>
<span class="author-handle">@{status.account.fqn}</span>
@ -27,11 +32,21 @@
<a class="link-muted" href={linkStatus(status)}>
<time>{ago(new Date(status.created_at), $now)}</time>
</a>
</div>
</section>
<section class="status-body">
{#if status.spoiler_text}
<p>[TODO: content warning <q>{status.spoiler_text}</q>]</p>
{/if}
<p class="status-body">
{@html status.content}
</p>
</section>
<section class="status-attachments">
{#each status.media_attachments as attachment}
<p>[TODO: attachment <q>{attachment.description || '(no description)'}</q>]</p>
{/each}
</section>
</div>
</div>
@ -44,6 +59,10 @@
padding: 0.25em;
}
.status.focused {
background-color: var(--col-bg-2);
}
.status-content {
display: flex;
flex-direction: row;

View File

@ -0,0 +1,60 @@
import type { MastodonStatus, MastodonStatusContext } from './status';
// Oh jeez
export function reorderAndFilterPleromaReplies(
status: MastodonStatus,
context: MastodonStatusContext
): MastodonStatusContext {
const findInOrig = (id: string): MastodonStatus | undefined =>
context.ancestors.find((status) => status.id === id) ||
context.descendants.find((status) => status.id === id);
const newContext: MastodonStatusContext = { ancestors: [], descendants: [] };
let currParent: MastodonStatus | undefined = status;
while (currParent != null) {
let replyingTo = currParent.in_reply_to_id;
if (replyingTo != null) {
currParent = findInOrig(replyingTo);
if (currParent != null) {
newContext.ancestors.unshift(currParent);
}
} else {
currParent = undefined;
}
}
const replyTree = new Map<string, MastodonStatus[]>();
for (const descendant of context.descendants) {
if (descendant.in_reply_to_id == null) {
continue;
}
if (!replyTree.has(descendant.in_reply_to_id)) {
replyTree.set(descendant.in_reply_to_id, []);
}
replyTree.get(descendant.in_reply_to_id)?.push(descendant);
}
let queue = [status];
newContext.descendants.push(status);
let potentialParent: MastodonStatus | undefined;
while ((potentialParent = queue.shift()) != null) {
const replies = replyTree.get(potentialParent.id);
if (replies) {
for (const reply of replies.reverse()) {
const parentIndex = newContext.descendants.indexOf(potentialParent);
newContext.descendants.splice(parentIndex + 1, 0, reply);
}
for (const reply of replies) {
queue.push(reply);
}
}
}
newContext.descendants.shift();
return newContext;
}

View File

@ -1,13 +1,28 @@
import type { MastodonAccount } from './account';
import type { MastodonObject } from './base';
import { reorderAndFilterPleromaReplies } from './pleroma_fixes';
import { fetchAPI, MastodonAPIError, type InstanceInfo } from './util';
export interface MastodonMediaAttachment {
type: string;
url: string;
description: string;
preview_url: string;
}
export interface MastodonStatus extends MastodonObject {
account: MastodonAccount;
content: string;
spoiler_text: string;
url: string;
in_reply_to_id: string | null;
media_attachments: MastodonMediaAttachment[];
}
export interface MastodonStatusContext {
ancestors: MastodonStatus[];
descendants: MastodonStatus[];
}
export function linkStatus(status: MastodonStatus) {
@ -19,3 +34,18 @@ export async function fetchStatus(instance: InstanceInfo, id: string): Promise<M
.then((r) => r.json())
.then((b) => b as MastodonStatus);
}
export async function fetchStatusContext(
instance: InstanceInfo,
status: MastodonStatus
): Promise<MastodonStatusContext> {
const context = await fetchAPI(instance, `/api/v1/statuses/${status.id}/context`)
.then((r) => r.json())
.then((r) => r as MastodonStatusContext);
if ('pleroma' in status) {
return reorderAndFilterPleromaReplies(status, context);
}
return context;
}

View File

@ -1,6 +1,6 @@
export interface InstanceInfo {
url: string;
token?: string;
token: string;
}
export class MastodonAPIError extends Error {
@ -24,7 +24,7 @@ export async function fetchAPI(
const opts = { ...options };
if (opts.headers == null) opts.headers = {};
if (instance.token != null) {
if (isLoggedIn(instance)) {
opts.headers['Authorization'] = instance.token;
}

View File

@ -1,11 +1,17 @@
<script lang="ts" context="module">
import type { Load } from '@sveltejs/kit';
import { fetchStatus, type MastodonStatus } from '$lib/mastoapi/status';
import {
fetchStatus,
fetchStatusContext,
type MastodonStatus,
type MastodonStatusContext
} from '$lib/mastoapi/status';
export const load: Load = async ({ session, params }) => {
try {
const status = await fetchStatus(session.instance, params.id);
return { props: { status } };
let contextPromise = await fetchStatusContext(session.instance, status);
return { props: { status, contextPromise } };
} catch (err) {
return { status: 404, error: err as Error };
}
@ -15,10 +21,27 @@
<script lang="ts">
import Status from '$lib/components/Status.svelte';
export let status: MastodonStatus;
export let contextPromise: Promise<MastodonStatusContext>;
</script>
{#await contextPromise then context}
<div class="ancestors">
{#each context.ancestors as ancestor}
<Status status={ancestor} />
{/each}
</div>
{/await}
{#if status != null}
<Status {status} />
<Status {status} focused={true} />
{:else}
<p>a</p>
{/if}
{#await contextPromise then context}
<div class="descendants">
{#each context.descendants as descendant}
<Status status={descendant} />
{/each}
</div>
{/await}

View File

@ -1,6 +1,7 @@
:root {
--col-bg-0: #18161d;
--col-bg-1: #221e28;
--col-bg-2: #30293a;
--col-fg-0: #f2ecff;
--col-fg-1: #ae96cc;