Display replies on the post page
This commit is contained in:
		
							parent
							
								
									55030b1e54
								
							
						
					
					
						commit
						a8f518f4b8
					
				
					 8 changed files with 148 additions and 15 deletions
				
			
		
							
								
								
									
										2
									
								
								src/app.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								src/app.d.ts
									
									
									
									
										vendored
									
									
								
							|  | @ -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 }; | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
| }; | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
							
								
								
									
										60
									
								
								src/lib/mastoapi/pleroma_fixes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/lib/mastoapi/pleroma_fixes.ts
									
									
									
									
									
										Normal 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; | ||||
| } | ||||
|  | @ -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; | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
|   } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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} | ||||
|  |  | |||
|  | @ -1,7 +1,8 @@ | |||
| :root { | ||||
|   --col-bg-0: #18161d; | ||||
|   --col-bg-1: #221e28; | ||||
|    | ||||
|   --col-bg-2: #30293a; | ||||
| 
 | ||||
|   --col-fg-0: #f2ecff; | ||||
|   --col-fg-1: #ae96cc; | ||||
|   --col-fg-2: #85739a; | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue