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 // See https://kit.svelte.dev/docs/types#app
declare namespace App { declare namespace App {
interface Session { 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 () => { export const getSession: GetSession = async () => {
const session: App.Session = { const session: App.Session = {
instance: { url: 'https://trans.enby.town', token: 'bweep' } instance: { url: 'https://trans.enby.town', token: '' }
}; };
return session; return session;
}; };

View File

@ -3,22 +3,27 @@
import { now } from '$lib/global-now'; import { now } from '$lib/global-now';
import { linkAccount } from '$lib/mastoapi/account'; 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'; import StatusControls from './StatusControls.svelte';
export let status: MastodonStatus; export let status: MastodonStatus;
export let focused: boolean = false;
</script> </script>
<article class="status"> <article class="status" class:focused>
<div class="status-content"> <div class="status-content">
<aside> <aside class="status-author">
<a href={linkAccount(status.account)}> <a href={linkAccount(status.account)}>
<img class="avatar" alt={status.account.acct} src={status.account.avatar_static} /> <img class="avatar" alt={status.account.acct} src={status.account.avatar_static} />
</a> </a>
</aside> </aside>
<div class="status-main"> <div class="status-main">
<div class="top-line"> <section class="top-line">
<a href={linkAccount(status.account)} class="author-link"> <a href={linkAccount(status.account)} class="author-link">
<strong class="author-display-name">{status.account.display_name}</strong> <strong class="author-display-name">{status.account.display_name}</strong>
<span class="author-handle">@{status.account.fqn}</span> <span class="author-handle">@{status.account.fqn}</span>
@ -27,11 +32,21 @@
<a class="link-muted" href={linkStatus(status)}> <a class="link-muted" href={linkStatus(status)}>
<time>{ago(new Date(status.created_at), $now)}</time> <time>{ago(new Date(status.created_at), $now)}</time>
</a> </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} {@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>
</div> </div>
@ -44,6 +59,10 @@
padding: 0.25em; padding: 0.25em;
} }
.status.focused {
background-color: var(--col-bg-2);
}
.status-content { .status-content {
display: flex; display: flex;
flex-direction: row; 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 { MastodonAccount } from './account';
import type { MastodonObject } from './base'; import type { MastodonObject } from './base';
import { reorderAndFilterPleromaReplies } from './pleroma_fixes';
import { fetchAPI, MastodonAPIError, type InstanceInfo } from './util'; import { fetchAPI, MastodonAPIError, type InstanceInfo } from './util';
export interface MastodonMediaAttachment {
type: string;
url: string;
description: string;
preview_url: string;
}
export interface MastodonStatus extends MastodonObject { export interface MastodonStatus extends MastodonObject {
account: MastodonAccount; account: MastodonAccount;
content: string; content: string;
spoiler_text: string; spoiler_text: string;
url: string; url: string;
in_reply_to_id: string | null;
media_attachments: MastodonMediaAttachment[];
}
export interface MastodonStatusContext {
ancestors: MastodonStatus[];
descendants: MastodonStatus[];
} }
export function linkStatus(status: 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((r) => r.json())
.then((b) => b as MastodonStatus); .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 { export interface InstanceInfo {
url: string; url: string;
token?: string; token: string;
} }
export class MastodonAPIError extends Error { export class MastodonAPIError extends Error {
@ -24,7 +24,7 @@ export async function fetchAPI(
const opts = { ...options }; const opts = { ...options };
if (opts.headers == null) opts.headers = {}; if (opts.headers == null) opts.headers = {};
if (instance.token != null) { if (isLoggedIn(instance)) {
opts.headers['Authorization'] = instance.token; opts.headers['Authorization'] = instance.token;
} }

View File

@ -1,11 +1,17 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import type { Load } from '@sveltejs/kit'; 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 }) => { export const load: Load = async ({ session, params }) => {
try { try {
const status = await fetchStatus(session.instance, params.id); const status = await fetchStatus(session.instance, params.id);
return { props: { status } }; let contextPromise = await fetchStatusContext(session.instance, status);
return { props: { status, contextPromise } };
} catch (err) { } catch (err) {
return { status: 404, error: err as Error }; return { status: 404, error: err as Error };
} }
@ -15,10 +21,27 @@
<script lang="ts"> <script lang="ts">
import Status from '$lib/components/Status.svelte'; import Status from '$lib/components/Status.svelte';
export let status: MastodonStatus; export let status: MastodonStatus;
export let contextPromise: Promise<MastodonStatusContext>;
</script> </script>
{#await contextPromise then context}
<div class="ancestors">
{#each context.ancestors as ancestor}
<Status status={ancestor} />
{/each}
</div>
{/await}
{#if status != null} {#if status != null}
<Status {status} /> <Status {status} focused={true} />
{:else} {:else}
<p>a</p> <p>a</p>
{/if} {/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 { :root {
--col-bg-0: #18161d; --col-bg-0: #18161d;
--col-bg-1: #221e28; --col-bg-1: #221e28;
--col-bg-2: #30293a;
--col-fg-0: #f2ecff; --col-fg-0: #f2ecff;
--col-fg-1: #ae96cc; --col-fg-1: #ae96cc;