Display replies on the post page
parent
55030b1e54
commit
a8f518f4b8
|
@ -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 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue