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
 | // 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; | ||||||
|  |  | ||||||
							
								
								
									
										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 { 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 a new issue