finish sk restructure, a11y and optimisations
This commit is contained in:
		
							parent
							
								
									9ef27fd2a2
								
							
						
					
					
						commit
						3ae05b3f9f
					
				
					 61 changed files with 416 additions and 429 deletions
				
			
		
							
								
								
									
										13
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										13
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -7,9 +7,10 @@ | ||||||
|         "": { |         "": { | ||||||
|             "name": "spacesocial-client", |             "name": "spacesocial-client", | ||||||
|             "version": "0.2.0_rev2", |             "version": "0.2.0_rev2", | ||||||
|             "license": "ISC", |             "license": "GPL-3.0", | ||||||
|             "devDependencies": { |             "devDependencies": { | ||||||
|                 "@sveltejs/adapter-auto": "^3.2.2", |                 "@sveltejs/adapter-auto": "^3.2.2", | ||||||
|  |                 "@sveltejs/adapter-static": "^3.0.2", | ||||||
|                 "@sveltejs/kit": "^2.5.17", |                 "@sveltejs/kit": "^2.5.17", | ||||||
|                 "@sveltejs/vite-plugin-svelte": "^3.1.1", |                 "@sveltejs/vite-plugin-svelte": "^3.1.1", | ||||||
|                 "svelte": "^4.2.18", |                 "svelte": "^4.2.18", | ||||||
|  | @ -718,6 +719,16 @@ | ||||||
|                 "@sveltejs/kit": "^2.0.0" |                 "@sveltejs/kit": "^2.0.0" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|  |         "node_modules/@sveltejs/adapter-static": { | ||||||
|  |             "version": "3.0.2", | ||||||
|  |             "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.2.tgz", | ||||||
|  |             "integrity": "sha512-/EBFydZDwfwFfFEuF1vzUseBoRziwKP7AoHAwv+Ot3M084sE/HTVBHf9mCmXfdM9ijprY5YEugZjleflncX5fQ==", | ||||||
|  |             "dev": true, | ||||||
|  |             "license": "MIT", | ||||||
|  |             "peerDependencies": { | ||||||
|  |                 "@sveltejs/kit": "^2.0.0" | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|         "node_modules/@sveltejs/kit": { |         "node_modules/@sveltejs/kit": { | ||||||
|             "version": "2.5.17", |             "version": "2.5.17", | ||||||
|             "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.17.tgz", |             "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.17.tgz", | ||||||
|  |  | ||||||
|  | @ -13,6 +13,7 @@ | ||||||
|     "license": "GPL-3.0", |     "license": "GPL-3.0", | ||||||
|     "devDependencies": { |     "devDependencies": { | ||||||
|         "@sveltejs/adapter-auto": "^3.2.2", |         "@sveltejs/adapter-auto": "^3.2.2", | ||||||
|  |         "@sveltejs/adapter-static": "^3.0.2", | ||||||
|         "@sveltejs/kit": "^2.5.17", |         "@sveltejs/kit": "^2.5.17", | ||||||
|         "@sveltejs/vite-plugin-svelte": "^3.1.1", |         "@sveltejs/vite-plugin-svelte": "^3.1.1", | ||||||
|         "svelte": "^4.2.18", |         "svelte": "^4.2.18", | ||||||
|  |  | ||||||
|  | @ -1,221 +0,0 @@ | ||||||
| <script> |  | ||||||
|     import Navigation from './ui/Navigation.svelte'; |  | ||||||
|     import Widgets from './ui/Widgets.svelte'; |  | ||||||
|     import Feed from './ui/Feed.svelte'; |  | ||||||
|     import { Client } from './client/client.js'; |  | ||||||
|     import Button from './ui/Button.svelte'; |  | ||||||
|     import { get } from 'svelte/store'; |  | ||||||
| 
 |  | ||||||
|     let client = get(Client.get()); |  | ||||||
|     let ready = client.app && client.app.token; |  | ||||||
|     let instance_url_error = false; |  | ||||||
|     let logging_in = false; |  | ||||||
| 
 |  | ||||||
|     let auth_code = new URLSearchParams(location.search).get("code"); |  | ||||||
|     if (auth_code) { |  | ||||||
|         client.getToken(auth_code).then(() => { |  | ||||||
|             client.save(); |  | ||||||
|             location = location.origin; |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (client.app && client.app.token) { |  | ||||||
|         // this triggers the client actually getting the authenticated user's data. |  | ||||||
|         client.verifyCredentials().then(res => { |  | ||||||
|             if (res) { |  | ||||||
|                 console.log(`Logged in as @${client.user.username}@${client.user.host}`); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     function log_in(event) { |  | ||||||
|         logging_in = true; |  | ||||||
|         event.preventDefault(); |  | ||||||
|         const host = event.target.host.value; |  | ||||||
| 
 |  | ||||||
|         client.init(host).then(res => { |  | ||||||
|             logging_in = false; |  | ||||||
|             if (!res) return; |  | ||||||
|             if (res.constructor === String) { |  | ||||||
|                 instance_url_error = res; |  | ||||||
|                 return; |  | ||||||
|             }; |  | ||||||
|             let oauth_url = client.getOAuthUrl(); |  | ||||||
|             location = oauth_url; |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <div id="spacesocial-app"> |  | ||||||
| 
 |  | ||||||
|     <header> |  | ||||||
|         <Navigation /> |  | ||||||
|     </header> |  | ||||||
| 
 |  | ||||||
|     <main> |  | ||||||
|         {#if ready} |  | ||||||
|             <Feed /> |  | ||||||
|         {:else} |  | ||||||
|             <div> |  | ||||||
|                 <form on:submit={log_in} id="login"> |  | ||||||
|                     <h1>Space Social</h1> |  | ||||||
|                     <p>Welcome, fediverse user!</p> |  | ||||||
|                     <p>Please enter your instance domain to log in.</p> |  | ||||||
|                     <div class="input-wrapper"> |  | ||||||
|                         <input type="text" id="host" aria-label="instance domain" class={logging_in ? "throb" : ""}> |  | ||||||
|                         {#if instance_url_error} |  | ||||||
|                             <p class="error">{instance_url_error}</p> |  | ||||||
|                         {/if} |  | ||||||
|                     </div> |  | ||||||
|                     <br> |  | ||||||
|                     <button type="submit" id="login" class={logging_in ? "disabled" : ""}>Log in</button> |  | ||||||
|                     <p><small> |  | ||||||
|                         Please note this is |  | ||||||
|                         <strong><em>extremely experimental software</em></strong>; |  | ||||||
|                         things are likely to break! |  | ||||||
|                         <br> |  | ||||||
|                         If that's all cool with you, welcome aboard! |  | ||||||
|                     </small></p> |  | ||||||
| 
 |  | ||||||
|                     <p class="form-footer">made with ❤️ by <a href="https://arimelody.me">ari melody</a>, 2024</p> |  | ||||||
|                 </form> |  | ||||||
|             </div> |  | ||||||
|         {/if} |  | ||||||
|     </main> |  | ||||||
| 
 |  | ||||||
|     <div id="widgets"> |  | ||||||
|         <Widgets /> |  | ||||||
|     </div> |  | ||||||
| 
 |  | ||||||
| </div> |  | ||||||
| 
 |  | ||||||
| <style> |  | ||||||
|     #spacesocial-app { |  | ||||||
|         margin: auto 0; |  | ||||||
|         padding: 0 16px; |  | ||||||
|         display: flex; |  | ||||||
|         flex-direction: row; |  | ||||||
|         justify-content: center; |  | ||||||
|         gap: 16px; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     header, #widgets { |  | ||||||
|         width: 300px; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     main { |  | ||||||
|         width: 732px; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     div.pane { |  | ||||||
|         margin-top: 16px; |  | ||||||
|         padding: 20px 32px; |  | ||||||
|         border: 1px solid #8884; |  | ||||||
|         border-radius: 16px; |  | ||||||
|         background-color: var(--bg1); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     form#login { |  | ||||||
|         margin: 25vh 0 32px 0; |  | ||||||
|         text-align: center; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     a { |  | ||||||
|         color: var(--accent); |  | ||||||
|         text-decoration: none; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     a:hover { |  | ||||||
|         text-decoration: underline; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     .input-wrapper { |  | ||||||
|         width: 360px; |  | ||||||
|         margin: 0 auto; |  | ||||||
|         display: flex; |  | ||||||
|         flex-direction: column; |  | ||||||
|         align-items: center; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     input[type=text] { |  | ||||||
|         width: 100%; |  | ||||||
|         padding: 12px; |  | ||||||
|         display: block; |  | ||||||
|         border-radius: 8px; |  | ||||||
|         border: 1px solid var(--accent); |  | ||||||
|         background-color: var(--bg-800); |  | ||||||
| 
 |  | ||||||
|         font-family: inherit; |  | ||||||
|         font-weight: bold; |  | ||||||
|         font-size: inherit; |  | ||||||
|         color: var(--text); |  | ||||||
| 
 |  | ||||||
|         transition: box-shadow .2s; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     input[type=text]::placeholder { |  | ||||||
|         opacity: .8; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     input[type=text]:focus { |  | ||||||
|         outline: none; |  | ||||||
|         box-shadow: 0 0 16px color-mix(in srgb, transparent, var(--accent) 25%); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     .error { |  | ||||||
|         margin: 6px; |  | ||||||
|         font-style: italic; |  | ||||||
|         font-size: .9em; |  | ||||||
|         color: red; |  | ||||||
|         opacity: .7; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     button#login { |  | ||||||
|         margin: -8px auto 0 auto; |  | ||||||
|         padding: 12px 24px; |  | ||||||
|         display: flex; |  | ||||||
|         flex-direction: row; |  | ||||||
|         justify-content: space-between; |  | ||||||
|         align-items: center; |  | ||||||
| 
 |  | ||||||
|         font-family: inherit; |  | ||||||
|         font-size: 1rem; |  | ||||||
|         font-weight: 600; |  | ||||||
|         text-align: left; |  | ||||||
| 
 |  | ||||||
|         border-radius: 8px; |  | ||||||
|         border-width: 2px; |  | ||||||
|         border-style: solid; |  | ||||||
| 
 |  | ||||||
|         background-color: var(--bg-700); |  | ||||||
|         color: var(--text); |  | ||||||
|         border-color: transparent; |  | ||||||
| 
 |  | ||||||
|         transition-property: border-color, background-color, color; |  | ||||||
|         transition-timing-function: ease-out; |  | ||||||
|         transition-duration: .1s; |  | ||||||
| 
 |  | ||||||
|         cursor: pointer; |  | ||||||
|         text-align: center; |  | ||||||
|         justify-content: center; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     button#login:hover { |  | ||||||
|         background-color: color-mix(in srgb, var(--bg-700), var(--accent) 10%); |  | ||||||
|         border-color: color-mix(in srgb, var(--bg-700), var(--accent) 20%); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     button#login:active { |  | ||||||
|         background-color: color-mix(in srgb, var(--bg-700), var(--bg-800) 50%); |  | ||||||
|         border-color: color-mix(in srgb, var(--bg-700), var(--bg-800) 10%); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     button#login.disabled { |  | ||||||
|         opacity: .5; |  | ||||||
|         cursor: initial; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     .form-footer { |  | ||||||
|         opacity: .7; |  | ||||||
|     } |  | ||||||
| </style> |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| @import url("/font/inter/inter.css"); | @import url("../font/inter/inter.css"); | ||||||
| 
 | 
 | ||||||
| :root { | :root { | ||||||
|     --bg-1000: #fff6de; |     --bg-1000: #fff6de; | ||||||
|  | @ -40,6 +40,32 @@ body { | ||||||
|     box-sizing: border-box; |     box-sizing: border-box; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | a { | ||||||
|  |     color: var(--accent); | ||||||
|  |     text-decoration: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | a:hover { | ||||||
|  |     text-decoration: underline; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #spacesocial-app { | ||||||
|  |     margin: auto 0; | ||||||
|  |     padding: 0 16px; | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: row; | ||||||
|  |     justify-content: center; | ||||||
|  |     gap: 16px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | header, #widgets { | ||||||
|  |     width: 300px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | main { | ||||||
|  |     width: 732px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .throb { | .throb { | ||||||
|     animation: .25s throb alternate infinite ease-in; |     animation: .25s throb alternate infinite ease-in; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ import { get } from 'svelte/store'; | ||||||
| export async function createApp(host) { | export async function createApp(host) { | ||||||
|     let form = new FormData(); |     let form = new FormData(); | ||||||
|     form.append("client_name", "space social"); |     form.append("client_name", "space social"); | ||||||
|     form.append("redirect_uris", `${location.origin}/callback`); |     form.append("redirect_uris", `${location.origin}`); | ||||||
|     form.append("scopes", "read write push"); |     form.append("scopes", "read write push"); | ||||||
|     form.append("website", "https://spacesocial.arimelody.me"); |     form.append("website", "https://spacesocial.arimelody.me"); | ||||||
| 
 | 
 | ||||||
|  | @ -35,7 +35,7 @@ export function getOAuthUrl() { | ||||||
|     return `https://${client.instance.host}/oauth/authorize` + |     return `https://${client.instance.host}/oauth/authorize` + | ||||||
|         `?client_id=${client.app.id}` + |         `?client_id=${client.app.id}` + | ||||||
|         "&scope=read+write+push" + |         "&scope=read+write+push" + | ||||||
|         `&redirect_uri=${location.origin}/callback` + |         `&redirect_uri=${location.origin}` + | ||||||
|         "&response_type=code"; |         "&response_type=code"; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -44,7 +44,7 @@ export async function getToken(code) { | ||||||
|     let form = new FormData(); |     let form = new FormData(); | ||||||
|     form.append("client_id", client.app.id); |     form.append("client_id", client.app.id); | ||||||
|     form.append("client_secret", client.app.secret); |     form.append("client_secret", client.app.secret); | ||||||
|     form.append("redirect_uri", `${location.origin}/callback`); |     form.append("redirect_uri", `${location.origin}`); | ||||||
|     form.append("grant_type", "authorization_code"); |     form.append("grant_type", "authorization_code"); | ||||||
|     form.append("code", code); |     form.append("code", code); | ||||||
|     form.append("scope", "read write push"); |     form.append("scope", "read write push"); | ||||||
|  | @ -107,7 +107,7 @@ export async function getTimeline(last_post_id) { | ||||||
|     return data; |     return data; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getPost(post_id, parent_replies) { | export async function getPost(post_id, ancestor_count) { | ||||||
|     let client = get(Client.get()); |     let client = get(Client.get()); | ||||||
|     let url = `https://${client.instance.host}/api/v1/statuses/${post_id}`; |     let url = `https://${client.instance.host}/api/v1/statuses/${post_id}`; | ||||||
|     const data = await fetch(url, { |     const data = await fetch(url, { | ||||||
|  | @ -208,7 +208,7 @@ export async function unreactPost(post_id, shortcode) { | ||||||
|     return data; |     return data; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function parsePost(data, parent_replies, child_replies) { | export async function parsePost(data, ancestor_count, with_context) { | ||||||
|     let client = get(Client.get()); |     let client = get(Client.get()); | ||||||
|     let post = new Post(); |     let post = new Post(); | ||||||
| 
 | 
 | ||||||
|  | @ -218,9 +218,12 @@ export async function parsePost(data, parent_replies, child_replies) { | ||||||
|     post.text = data.content; |     post.text = data.content; | ||||||
| 
 | 
 | ||||||
|     post.reply = null; |     post.reply = null; | ||||||
|     if ((data.in_reply_to_id || data.reply) && parent_replies !== 0) { |     if (!with_context && // ancestor replies are handled in full later
 | ||||||
|         const reply_data = data.reply || await getPost(data.in_reply_to_id, parent_replies - 1); |         (data.in_reply_to_id || data.reply) && | ||||||
|         post.reply = await parsePost(reply_data, parent_replies - 1, false); |         ancestor_count !== 0 | ||||||
|  |     ) { | ||||||
|  |         const reply_data = data.reply || await getPost(data.in_reply_to_id, ancestor_count - 1); | ||||||
|  |         post.reply = await parsePost(reply_data, ancestor_count - 1, false); | ||||||
|         // if the post returns false, we probably don't have permission to read it.
 |         // if the post returns false, we probably don't have permission to read it.
 | ||||||
|         // we'll respect the thread's privacy, and leave it alone :)
 |         // we'll respect the thread's privacy, and leave it alone :)
 | ||||||
|         if (post.reply === false) return false; |         if (post.reply === false) return false; | ||||||
|  | @ -228,14 +231,25 @@ export async function parsePost(data, parent_replies, child_replies) { | ||||||
|     post.boost = data.reblog ? await parsePost(data.reblog, 1, false) : null; |     post.boost = data.reblog ? await parsePost(data.reblog, 1, false) : null; | ||||||
| 
 | 
 | ||||||
|     post.replies = []; |     post.replies = []; | ||||||
|     if (child_replies) { |     if (with_context) { | ||||||
|         const replies_data = await getPostContext(data.id); |         const replies_data = await getPostContext(data.id); | ||||||
|         if (replies_data && replies_data.descendants) { |         if (replies_data) { | ||||||
|  |             // posts this is replying to
 | ||||||
|  |             if (replies_data.ancestors) { | ||||||
|  |                 let head = post; | ||||||
|  |                 while (replies_data.ancestors.length > 0) { | ||||||
|  |                     head.reply = await parsePost(replies_data.ancestors.pop(), 0, false); | ||||||
|  |                     head = head.reply; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             // posts in reply to this
 | ||||||
|  |             if (replies_data.descendants) { | ||||||
|                 for (let i in replies_data.descendants) { |                 for (let i in replies_data.descendants) { | ||||||
|                     post.replies.push(await parsePost(replies_data.descendants[i], 0, false)); |                     post.replies.push(await parsePost(replies_data.descendants[i], 0, false)); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     post.id = data.id; |     post.id = data.id; | ||||||
|     post.created_at = new Date(data.created_at); |     post.created_at = new Date(data.created_at); | ||||||
|  |  | ||||||
|  | @ -17,6 +17,7 @@ export default class Post { | ||||||
|     files; |     files; | ||||||
|     url; |     url; | ||||||
|     reply; |     reply; | ||||||
|  |     reply_id; | ||||||
|     replies; |     replies; | ||||||
|     boost; |     boost; | ||||||
|     visibility; |     visibility; | ||||||
|  |  | ||||||
|  | @ -1,10 +1,13 @@ | ||||||
|  | import sound_log from '../sound/log.ogg'; | ||||||
|  | import sound_hello from '../sound/hello.ogg'; | ||||||
|  | import sound_success from '../sound/success.ogg'; | ||||||
| let sounds; | let sounds; | ||||||
| 
 | 
 | ||||||
| if (typeof Audio !== typeof undefined) { | if (typeof Audio !== typeof undefined) { | ||||||
|     sounds = { |     sounds = { | ||||||
|         "default": new Audio("/sound/log.ogg"), |         "default": new Audio(sound_log), | ||||||
|         "post": new Audio("/sound/success.ogg"), |         "post": new Audio(sound_hello), | ||||||
|         "boost": new Audio("/sound/hello.ogg"), |         "boost": new Audio(sound_success), | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										43
									
								
								src/lib/timeline.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/lib/timeline.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,43 @@ | ||||||
|  | import { Client } from '$lib/client/client.js'; | ||||||
|  | import { get, writable } from 'svelte/store'; | ||||||
|  | import { parsePost } from '$lib/client/api.js'; | ||||||
|  | 
 | ||||||
|  | export let posts = writable([]); | ||||||
|  | 
 | ||||||
|  | let client = get(Client.get()); | ||||||
|  | let loading = false; | ||||||
|  | 
 | ||||||
|  | export async function getTimeline(clean) { | ||||||
|  |     if (loading) return; // no spamming!!
 | ||||||
|  |     loading = true; | ||||||
|  | 
 | ||||||
|  |     let timeline_data; | ||||||
|  |     if (get(posts).length === 0) timeline_data = await client.getTimeline() | ||||||
|  |     else timeline_data = await client.getTimeline(get(posts)[get(posts).length - 1].id); | ||||||
|  | 
 | ||||||
|  |     if (!timeline_data) { | ||||||
|  |         console.error(`Failed to retrieve timeline.`); | ||||||
|  |         loading = false; | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (clean) posts.set([]); | ||||||
|  | 
 | ||||||
|  |     for (let i in timeline_data) { | ||||||
|  |         const post_data = timeline_data[i]; | ||||||
|  |         const post = await parsePost(post_data, 1, false); | ||||||
|  |         if (!post) { | ||||||
|  |             if (post === null || post === undefined) { | ||||||
|  |                 if (post_data.id) { | ||||||
|  |                     console.warn("Failed to parse post #" + post_data.id); | ||||||
|  |                 } else { | ||||||
|  |                     console.warn("Failed to parse post:"); | ||||||
|  |                     console.warn(post_data); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             continue; | ||||||
|  |         } | ||||||
|  |         posts.update(current => [...current, post]); | ||||||
|  |     } | ||||||
|  |     loading = false; | ||||||
|  | } | ||||||
|  | @ -2,104 +2,33 @@ | ||||||
|     import Button from './Button.svelte'; |     import Button from './Button.svelte'; | ||||||
|     import Post from './post/Post.svelte'; |     import Post from './post/Post.svelte'; | ||||||
|     import Error from './Error.svelte'; |     import Error from './Error.svelte'; | ||||||
|     import { Client } from '../client/client.js'; |     import { Client } from '$lib/client/client.js'; | ||||||
|     import { parsePost } from '../client/api.js'; |     import { parsePost } from '$lib/client/api.js'; | ||||||
|     import { get } from 'svelte/store'; |     import { get } from 'svelte/store'; | ||||||
|  |     import { posts, getTimeline } from '$lib/timeline.js'; | ||||||
| 
 | 
 | ||||||
|     let params = new URLSearchParams(location.search); |  | ||||||
| 
 |  | ||||||
|     let client = get(Client.get()); |  | ||||||
|     let posts = []; |  | ||||||
|     let loading = false; |  | ||||||
|     let focus_post_id = location.pathname.startsWith("/post/") ? location.pathname.substring(6) : false; |  | ||||||
| 
 |  | ||||||
|     let error; |  | ||||||
| 
 |  | ||||||
|     async function getTimeline() { |  | ||||||
|         if (loading) return; // no spamming!! |  | ||||||
|         loading = true; |  | ||||||
| 
 |  | ||||||
|         let timeline_data; |  | ||||||
|         if (posts.length === 0) timeline_data = await client.getTimeline() |  | ||||||
|         else timeline_data = await client.getTimeline(posts[posts.length - 1].id); |  | ||||||
| 
 |  | ||||||
|         if (!timeline_data) { |  | ||||||
|             console.error(`Failed to retrieve timeline.`); |  | ||||||
|             loading = false; |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         for (let i in timeline_data) { |  | ||||||
|             const post_data = timeline_data[i]; |  | ||||||
|             const post = await parsePost(post_data, 1, false); |  | ||||||
|             if (!post) { |  | ||||||
|                 if (post === null || post === undefined) { |  | ||||||
|                     if (post_data.id) { |  | ||||||
|                         console.warn("Failed to parse post #" + post_data.id); |  | ||||||
|                     } else { |  | ||||||
|                         console.warn("Failed to parse post:"); |  | ||||||
|                         console.warn(post_data); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
|             posts = [...posts, post]; |  | ||||||
|         } |  | ||||||
|         loading = false; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     async function getPost(post_id) { |  | ||||||
|         loading = true; |  | ||||||
| 
 |  | ||||||
|         const post_data = await client.getPost(post_id); |  | ||||||
|         if (!post_data) { |  | ||||||
|             console.error(`Failed to retrieve post ${post_id}.`); |  | ||||||
|             loading = false; |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         const post = await parsePost(post_data, 10, true); |  | ||||||
|         posts = [post]; |  | ||||||
|         for (let i in post.replies) { |  | ||||||
|             posts = [...posts, post.replies[i]]; |  | ||||||
|         } |  | ||||||
|         loading = false; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (focus_post_id) { |  | ||||||
|         getPost(focus_post_id); |  | ||||||
|     } else { |  | ||||||
|     getTimeline(); |     getTimeline(); | ||||||
|     document.addEventListener("scroll", event => { |     document.addEventListener("scroll", event => { | ||||||
|             if (!loading && window.innerHeight + window.scrollY >= document.body.offsetHeight - 2048) { |         if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 2048) { | ||||||
|             getTimeline(); |             getTimeline(); | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
|     } |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <header> | <div id="feed" role="feed"> | ||||||
|     <h1>Home</h1> |  | ||||||
|     <nav> |  | ||||||
|         <Button centered active>Home</Button> |  | ||||||
|         <Button centered disabled>Local</Button> |  | ||||||
|         <Button centered disabled>Federated</Button> |  | ||||||
|     </nav> |  | ||||||
| </header> |  | ||||||
| 
 |  | ||||||
| <div id="feed"> |  | ||||||
|     {#if posts.length <= 0} |     {#if posts.length <= 0} | ||||||
|         <div class="throb"> |         <div class="loading throb"> | ||||||
|             <span>just a moment...</span> |             <span>getting the feed...</span> | ||||||
|         </div> |         </div> | ||||||
|     {/if} |     {/if} | ||||||
|     {#each posts as post} |     {#each $posts as post} | ||||||
|         <Post post_data={post} focused={post.id === focus_post_id} /> |         <Post post_data={post} /> | ||||||
|     {/each} |     {/each} | ||||||
| </div> | </div> | ||||||
| 
 | 
 | ||||||
| <style> | <style> | ||||||
|     header { |     header { | ||||||
|  |         width: 100%; | ||||||
|         margin: 16px 0 8px 0; |         margin: 16px 0 8px 0; | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: row; |         flex-direction: row; | ||||||
|  | @ -120,7 +49,7 @@ | ||||||
|         margin-bottom: 20vh; |         margin-bottom: 20vh; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .throb { |     .loading { | ||||||
|         width: 100%; |         width: 100%; | ||||||
|         height: 80vh; |         height: 80vh; | ||||||
|         display: flex; |         display: flex; | ||||||
|  |  | ||||||
|  | @ -5,6 +5,8 @@ | ||||||
|     import { Client } from '../client/client.js'; |     import { Client } from '../client/client.js'; | ||||||
|     import { play_sound } from '../sound.js'; |     import { play_sound } from '../sound.js'; | ||||||
| 
 | 
 | ||||||
|  |     const VERSION = APP_VERSION; | ||||||
|  | 
 | ||||||
|     let client = false; |     let client = false; | ||||||
|     Client.get().subscribe(c => { |     Client.get().subscribe(c => { | ||||||
|         client = c; |         client = c; | ||||||
|  | @ -14,6 +16,13 @@ | ||||||
|     if (notification_count > 99) notification_count = "99+"; |     if (notification_count > 99) notification_count = "99+"; | ||||||
| 
 | 
 | ||||||
|     function goTimeline() { |     function goTimeline() { | ||||||
|  |         if (location.pathname === "/") { | ||||||
|  |             window.scrollTo({ | ||||||
|  |                 top: 0, | ||||||
|  |                 behavior: "smooth" | ||||||
|  |             }); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|         location = "/"; |         location = "/"; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -61,7 +70,7 @@ | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <div id="account-button"> |         <div id="account-button"> | ||||||
|             <img src={client.user.avatar_url} class="account-avatar" height="64px" aria-hidden="true" on:click={() => play_sound()}> |             <img src={client.user.avatar_url} class="account-avatar" height="64px" alt="" aria-hidden="true" on:click={() => play_sound()}> | ||||||
|             <div class="account-name" aria-hidden="true"> |             <div class="account-name" aria-hidden="true"> | ||||||
|                 <span class="nickname" title={client.user.nickname}>{client.user.nickname}</span> |                 <span class="nickname" title={client.user.nickname}>{client.user.nickname}</span> | ||||||
|                 <span class="username" title={`@${client.user.username}@${client.user.host}`}> |                 <span class="username" title={`@${client.user.username}@${client.user.host}`}> | ||||||
|  | @ -73,7 +82,7 @@ | ||||||
|     </div> |     </div> | ||||||
|     {/if} |     {/if} | ||||||
|     <span class="version"> |     <span class="version"> | ||||||
|         space social v{APP_VERSION} |         space social v{VERSION} | ||||||
|         <br> |         <br> | ||||||
|         <ul> |         <ul> | ||||||
|             <li><a href="https://git.arimelody.me/ari/spacesocial-client">source</a></li> |             <li><a href="https://git.arimelody.me/ari/spacesocial-client">source</a></li> | ||||||
|  | @ -252,26 +261,6 @@ | ||||||
|         font-size: .65em; |         font-size: .65em; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .settings { |  | ||||||
|         width: 32px; |  | ||||||
|         height: 32px; |  | ||||||
|         padding: none; |  | ||||||
|         border: none; |  | ||||||
|         font-size: inherit; |  | ||||||
|         font-family: inherit; |  | ||||||
|         background: none; |  | ||||||
|         border-radius: 8px; |  | ||||||
|         transition: background-color .1s; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     .settings:hover { |  | ||||||
|         background-color: color-mix(in srgb, var(--bg-700), var(--text) 15%); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     .settings:active { |  | ||||||
|         background-color: color-mix(in srgb, var(--bg-700), var(--bg-1000) 30%); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     .flex-row { |     .flex-row { | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: row; |         flex-direction: row; | ||||||
|  |  | ||||||
|  | @ -15,22 +15,24 @@ | ||||||
|     <span class="post-context-time"> |     <span class="post-context-time"> | ||||||
|         <time title="{time_string}">{short_time(post.created_at)}</time> |         <time title="{time_string}">{short_time(post.created_at)}</time> | ||||||
|         {#if post.visibility !== "public"} |         {#if post.visibility !== "public"} | ||||||
|             <span class="post-visibility">({post.visibility})</span> |             <span class="post-visibility">- {post.visibility}</span> | ||||||
|         {/if} |         {/if} | ||||||
|     </span> |     </span> | ||||||
| </div> | </div> | ||||||
| 
 | 
 | ||||||
| <style> | <style> | ||||||
|     .post-context { |     .post-context { | ||||||
|         margin-bottom: 8px; |         padding: 12px 16px 0 74px; | ||||||
|         padding-left: 58px; |  | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: row; |         flex-direction: row; | ||||||
|         align-items: center; |         align-items: center; | ||||||
|  |         font-size: .8em; | ||||||
|         font-weight: 600; |         font-weight: 600; | ||||||
|         color: var(--text); |         color: var(--text); | ||||||
|         opacity: .8; |         opacity: .8; | ||||||
|         transition: opacity .1s; |         transition: opacity .1s, background-color .1s; | ||||||
|  |         border-radius: 8px; | ||||||
|  |         z-index: 1; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .post-context-icon { |     .post-context-icon { | ||||||
|  | @ -49,4 +51,8 @@ | ||||||
|     .post-context-time { |     .post-context-time { | ||||||
|         margin-left: auto; |         margin-left: auto; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     .post-visibility { | ||||||
|  |         opacity: .7; | ||||||
|  |     } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ | ||||||
|     import { get } from 'svelte/store'; |     import { get } from 'svelte/store'; | ||||||
|     import { Client } from '../../client/client.js'; |     import { Client } from '../../client/client.js'; | ||||||
|     import * as api from '../../client/api.js'; |     import * as api from '../../client/api.js'; | ||||||
|  |     import { goto } from '$app/navigation'; | ||||||
| 
 | 
 | ||||||
|     export let post_data; |     export let post_data; | ||||||
|     export let focused = false; |     export let focused = false; | ||||||
|  | @ -25,7 +26,9 @@ | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     function gotoPost() { |     function gotoPost() { | ||||||
|         location = `/post/${post.id}`; |         if (focused) return; | ||||||
|  |         if (event.key && event.key !== "Enter") return; | ||||||
|  |         goto(`/post/${post.id}`); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async function toggleBoost() { |     async function toggleBoost() { | ||||||
|  | @ -80,25 +83,30 @@ | ||||||
|     let el; |     let el; | ||||||
|     onMount(() => { |     onMount(() => { | ||||||
|         if (focused) { |         if (focused) { | ||||||
|             window.scrollTo(0, el.scrollHeight - 700); |             window.scrollTo(0, el.scrollHeight); | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     let aria_label = post.user.username + '; ' + post.text + '; ' + post.created_at; |     let aria_label = post.user.username + '; ' + post.text + '; ' + post.created_at; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div class="post-container" aria-label={aria_label} bind:this={el}> | <div class="post-container"> | ||||||
|     {#if post.reply} |     {#if post.reply} | ||||||
|         <ReplyContext post={post.reply} /> |         <ReplyContext post={post.reply} /> | ||||||
|     {/if} |     {/if} | ||||||
|     {#if is_boost && !post_context.text} |     {#if is_boost && !post_context.text} | ||||||
|         <BoostContext post={post_context} /> |         <BoostContext post={post_context} /> | ||||||
|     {/if} |     {/if} | ||||||
|     <article class={"post" + (focused ? " focused" : "")} on:click={!focused ? gotoPost() : null}> |     <article | ||||||
|  |             class={"post" + (focused ? " focused" : "")} | ||||||
|  |             aria-label={aria_label} | ||||||
|  |             bind:this={el} | ||||||
|  |             on:click={gotoPost} | ||||||
|  |             on:keydown={gotoPost}> | ||||||
|         <PostHeader post={post} /> |         <PostHeader post={post} /> | ||||||
|         <Body post={post} /> |         <Body post={post} /> | ||||||
|         <footer class="post-footer"> |         <footer class="post-footer"> | ||||||
|             <div class="post-reactions" on:click|stopPropagation> |             <div class="post-reactions" aria-label="Reactions" on:click|stopPropagation on:keydown|stopPropagation> | ||||||
|                 {#each post.reactions as reaction} |                 {#each post.reactions as reaction} | ||||||
|                     <ReactionButton |                     <ReactionButton | ||||||
|                             type="reaction" |                             type="reaction" | ||||||
|  | @ -116,7 +124,7 @@ | ||||||
|                     </ReactionButton> |                     </ReactionButton> | ||||||
|                 {/each} |                 {/each} | ||||||
|             </div> |             </div> | ||||||
|             <div class="post-actions" on:click|stopPropagation> |             <div class="post-actions" aria-label="Post actions" on:click|stopPropagation on:keydown|stopPropagation> | ||||||
|                 <ActionButton type="reply" label="Reply" bind:count={post.reply_count} sound="post" disabled>🗨️</ActionButton> |                 <ActionButton type="reply" label="Reply" bind:count={post.reply_count} sound="post" disabled>🗨️</ActionButton> | ||||||
|                 <ActionButton type="boost" label="Boost" on:click={() => toggleBoost()} bind:active={post.boosted} bind:count={post.boost_count} sound="boost">🔁</ActionButton> |                 <ActionButton type="boost" label="Boost" on:click={() => toggleBoost()} bind:active={post.boosted} bind:count={post.boost_count} sound="boost">🔁</ActionButton> | ||||||
|                 <ActionButton type="favourite" label="Favourite" on:click={() => toggleFavourite()} bind:active={post.favourited} bind:count={post.favourite_count}>⭐</ActionButton> |                 <ActionButton type="favourite" label="Favourite" on:click={() => toggleFavourite()} bind:active={post.favourited} bind:count={post.favourite_count}>⭐</ActionButton> | ||||||
|  | @ -130,37 +138,39 @@ | ||||||
| 
 | 
 | ||||||
| <style> | <style> | ||||||
|     .post-container { |     .post-container { | ||||||
|         width: 700px; |         width: 732px; | ||||||
|         max-width: 700px; |         max-width: 732px; | ||||||
|         margin-bottom: 8px; |         margin-bottom: 8px; | ||||||
|         padding: 16px; |  | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: column; |         flex-direction: column; | ||||||
|         border-radius: 8px; |         border-radius: 8px; | ||||||
|         background-color: var(--bg-800); |         background-color: var(--bg-800); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post { | ||||||
|  |         padding: 16px; | ||||||
|  |         border-radius: 8px; | ||||||
|         transition: background-color .1s; |         transition: background-color .1s; | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     .post-container:hover { |  | ||||||
|         background-color: color-mix(in srgb, var(--bg-800), black 5%); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     .post-container:hover :global(.post-context) { |  | ||||||
|         opacity: 1; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     .post:not(.focused) { |     .post:not(.focused) { | ||||||
|         cursor: pointer; |         cursor: pointer; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .post.focused { |     .post.focused { | ||||||
|         padding: 16px; |  | ||||||
|         margin: -16px; |  | ||||||
|         border-radius: 8px; |  | ||||||
|         border: 1px solid color-mix(in srgb, transparent, var(--accent) 20%); |         border: 1px solid color-mix(in srgb, transparent, var(--accent) 20%); | ||||||
|         box-shadow: 0 0 16px color-mix(in srgb, transparent, var(--accent) 20%); |         box-shadow: 0 0 16px color-mix(in srgb, transparent, var(--accent) 20%); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     .post:hover { | ||||||
|  |         background-color: color-mix(in srgb, var(--bg-800), black 5%); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-container:has(.post-context) .post { | ||||||
|  |         padding-top: 40px; | ||||||
|  |         margin-top: -32px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     :global(.post-reactions) { |     :global(.post-reactions) { | ||||||
|         width: fit-content; |         width: fit-content; | ||||||
|         display: flex; |         display: flex; | ||||||
|  |  | ||||||
|  | @ -9,12 +9,16 @@ | ||||||
|     import { get } from 'svelte/store'; |     import { get } from 'svelte/store'; | ||||||
|     import { Client } from '../../client/client.js'; |     import { Client } from '../../client/client.js'; | ||||||
|     import * as api from '../../client/api.js'; |     import * as api from '../../client/api.js'; | ||||||
|  |     import { goto } from '$app/navigation'; | ||||||
| 
 | 
 | ||||||
|     export let post; |     export let post; | ||||||
|     let time_string = post.created_at.toLocaleString(); |     let time_string = post.created_at.toLocaleString(); | ||||||
|  |     let aria_label = post.user.username + '; ' + post.text + '; ' + post.created_at; | ||||||
| 
 | 
 | ||||||
|     function gotoPost() { |     function gotoPost() { | ||||||
|         location = `/post/${post.id}`; |         if (focused) return; | ||||||
|  |         if (event.key && event.key !== "Enter") return; | ||||||
|  |         goto(`/post/${post.id}`); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async function toggleBoost() { |     async function toggleBoost() { | ||||||
|  | @ -71,7 +75,11 @@ | ||||||
|     <svelte:self post={post.reply} /> |     <svelte:self post={post.reply} /> | ||||||
| {/if} | {/if} | ||||||
| 
 | 
 | ||||||
| <article class="post-reply" on:click={() => gotoPost()}> | <article | ||||||
|  |         class="post-reply" | ||||||
|  |         aria-label={aria_label} | ||||||
|  |         on:click={gotoPost} | ||||||
|  |         on:keydown={gotoPost}> | ||||||
|     <div class="line"></div> |     <div class="line"></div> | ||||||
|          |          | ||||||
|     <div class="post-reply-main"> |     <div class="post-reply-main"> | ||||||
|  | @ -80,7 +88,7 @@ | ||||||
|         <Body post={post} /> |         <Body post={post} /> | ||||||
| 
 | 
 | ||||||
|         <footer class="post-footer"> |         <footer class="post-footer"> | ||||||
|             <div class="post-reactions" on:click|stopPropagation> |             <div class="post-reactions" aria-label="Reactions" on:click|stopPropagation on:keydown|stopPropagation> | ||||||
|                 {#each post.reactions as reaction} |                 {#each post.reactions as reaction} | ||||||
|                     <ReactionButton |                     <ReactionButton | ||||||
|                             type="reaction" |                             type="reaction" | ||||||
|  | @ -98,7 +106,7 @@ | ||||||
|                     </ReactionButton> |                     </ReactionButton> | ||||||
|                 {/each} |                 {/each} | ||||||
|             </div> |             </div> | ||||||
|             <div class="post-actions" on:click|stopPropagation> |             <div class="post-actions" aria-label="Post actions" on:click|stopPropagation on:keydown|stopPropagation> | ||||||
|                 <ActionButton type="reply" label="Reply" bind:count={post.reply_count} sound="post" disabled>🗨️</ActionButton> |                 <ActionButton type="reply" label="Reply" bind:count={post.reply_count} sound="post" disabled>🗨️</ActionButton> | ||||||
|                 <ActionButton type="boost" label="Boost" on:click={() => toggleBoost()} bind:active={post.boosted} bind:count={post.boost_count} sound="boost">🔁</ActionButton> |                 <ActionButton type="boost" label="Boost" on:click={() => toggleBoost()} bind:active={post.boosted} bind:count={post.boost_count} sound="boost">🔁</ActionButton> | ||||||
|                 <ActionButton type="favourite" label="Favourite" on:click={() => toggleFavourite()} bind:active={post.favourited} bind:count={post.favourite_count}>⭐</ActionButton> |                 <ActionButton type="favourite" label="Favourite" on:click={() => toggleFavourite()} bind:active={post.favourited} bind:count={post.favourite_count}>⭐</ActionButton> | ||||||
|  | @ -112,11 +120,18 @@ | ||||||
| 
 | 
 | ||||||
| <style> | <style> | ||||||
|     .post-reply { |     .post-reply { | ||||||
|         padding-bottom: 24px; |         padding: 16px 16px 16px 16px; | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: row; |         flex-direction: row; | ||||||
|         color: var(--text); |         color: var(--text); | ||||||
|         align-items: stretch; |         align-items: stretch; | ||||||
|  |         border-radius: 8px; | ||||||
|  |         transition: background-color .1s; | ||||||
|  |         cursor: pointer; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .post-reply:hover { | ||||||
|  |         background-color: color-mix(in srgb, var(--bg-800), black 5%); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .post-avatar-container { |     .post-avatar-container { | ||||||
|  | @ -125,8 +140,8 @@ | ||||||
| 
 | 
 | ||||||
|     .line { |     .line { | ||||||
|         position: relative; |         position: relative; | ||||||
|         top: 24px; |         top: 32px; | ||||||
|         left: 25px; |         left: 23px; | ||||||
|         border-right: 2px solid var(--bg-700); |         border-right: 2px solid var(--bg-700); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -8,7 +8,8 @@ | ||||||
|     import { get } from 'svelte/store'; |     import { get } from 'svelte/store'; | ||||||
| 
 | 
 | ||||||
|     let client = get(Client.get()); |     let client = get(Client.get()); | ||||||
|     let ready = client.app && client.app.token; |     let ready = client.app; | ||||||
|  |     let logged_in = ready && client.app.token; | ||||||
|     let instance_url_error = false; |     let instance_url_error = false; | ||||||
|     let logging_in = false; |     let logging_in = false; | ||||||
| 
 | 
 | ||||||
|  | @ -57,6 +58,16 @@ | ||||||
| 
 | 
 | ||||||
|     <main> |     <main> | ||||||
|         {#if ready} |         {#if ready} | ||||||
|  |             {#if logged_in} | ||||||
|  |                 <header> | ||||||
|  |                     <h1>Home</h1> | ||||||
|  |                     <nav> | ||||||
|  |                         <Button centered active>Home</Button> | ||||||
|  |                         <Button centered disabled>Local</Button> | ||||||
|  |                         <Button centered disabled>Federated</Button> | ||||||
|  |                     </nav> | ||||||
|  |                 </header> | ||||||
|  | 
 | ||||||
|                 <Feed /> |                 <Feed /> | ||||||
|             {:else} |             {:else} | ||||||
|                 <div> |                 <div> | ||||||
|  | @ -84,6 +95,11 @@ | ||||||
|                     </form> |                     </form> | ||||||
|                 </div> |                 </div> | ||||||
|             {/if} |             {/if} | ||||||
|  |         {:else} | ||||||
|  |             <div class="loading throb"> | ||||||
|  |                 <span>just a moment...</span> | ||||||
|  |             </div> | ||||||
|  |         {/if} | ||||||
|     </main> |     </main> | ||||||
| 
 | 
 | ||||||
|     <div id="widgets"> |     <div id="widgets"> | ||||||
|  | @ -93,31 +109,6 @@ | ||||||
| </div> | </div> | ||||||
| 
 | 
 | ||||||
| <style> | <style> | ||||||
|     #spacesocial-app { |  | ||||||
|         margin: auto 0; |  | ||||||
|         padding: 0 16px; |  | ||||||
|         display: flex; |  | ||||||
|         flex-direction: row; |  | ||||||
|         justify-content: center; |  | ||||||
|         gap: 16px; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     header, #widgets { |  | ||||||
|         width: 300px; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     main { |  | ||||||
|         width: 732px; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     div.pane { |  | ||||||
|         margin-top: 16px; |  | ||||||
|         padding: 20px 32px; |  | ||||||
|         border: 1px solid #8884; |  | ||||||
|         border-radius: 16px; |  | ||||||
|         background-color: var(--bg1); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     form#login { |     form#login { | ||||||
|         margin: 25vh 0 32px 0; |         margin: 25vh 0 32px 0; | ||||||
|         text-align: center; |         text-align: center; | ||||||
|  | @ -221,4 +212,32 @@ | ||||||
|     .form-footer { |     .form-footer { | ||||||
|         opacity: .7; |         opacity: .7; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     .loading { | ||||||
|  |         width: 100%; | ||||||
|  |         height: 80vh; | ||||||
|  |         display: flex; | ||||||
|  |         justify-content: center; | ||||||
|  |         align-items: center; | ||||||
|  |         font-size: 2em; | ||||||
|  |         font-weight: bold; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     main header { | ||||||
|  |         width: 100%; | ||||||
|  |         margin: 16px 0 8px 0; | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: row; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     main header h1 { | ||||||
|  |         font-size: 1.5em; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     main header nav { | ||||||
|  |         margin-left: auto; | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: row; | ||||||
|  |         gap: 8px; | ||||||
|  |     } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
							
								
								
									
										50
									
								
								src/routes/post/[id]/+layout.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/routes/post/[id]/+layout.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,50 @@ | ||||||
|  | <script> | ||||||
|  |     import Navigation from '$lib/ui/Navigation.svelte'; | ||||||
|  |     import Widgets from '$lib/ui/Widgets.svelte'; | ||||||
|  |     import Button from '$lib/ui/Button.svelte'; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <div id="spacesocial-app"> | ||||||
|  | 
 | ||||||
|  |     <header> | ||||||
|  |         <Navigation /> | ||||||
|  |     </header> | ||||||
|  | 
 | ||||||
|  |     <main> | ||||||
|  |         <header> | ||||||
|  |             <h1>Home</h1> | ||||||
|  |             <nav> | ||||||
|  |                 <Button centered active>Home</Button> | ||||||
|  |                 <Button centered disabled>Local</Button> | ||||||
|  |                 <Button centered disabled>Federated</Button> | ||||||
|  |             </nav> | ||||||
|  |         </header> | ||||||
|  | 
 | ||||||
|  |         <slot></slot> | ||||||
|  |     </main> | ||||||
|  | 
 | ||||||
|  |     <div id="widgets"> | ||||||
|  |         <Widgets /> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  |     main header { | ||||||
|  |         width: 100%; | ||||||
|  |         margin: 16px 0 8px 0; | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: row; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     main header h1 { | ||||||
|  |         font-size: 1.5em; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     main header nav { | ||||||
|  |         margin-left: auto; | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: row; | ||||||
|  |         gap: 8px; | ||||||
|  |     } | ||||||
|  | </style> | ||||||
							
								
								
									
										46
									
								
								src/routes/post/[id]/+page.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/routes/post/[id]/+page.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,46 @@ | ||||||
|  | import Post from '$lib/ui/post/Post.svelte'; | ||||||
|  | import { Client } from '$lib/client/client.js'; | ||||||
|  | import { parsePost } from '$lib/client/api.js'; | ||||||
|  | import { get } from 'svelte/store'; | ||||||
|  | 
 | ||||||
|  | export const prerender = true; | ||||||
|  | export const ssr = false; | ||||||
|  | 
 | ||||||
|  | export async function load({ params }) { | ||||||
|  |     let client = get(Client.get()); | ||||||
|  |     if (client.app && client.app.token) { | ||||||
|  |         // this triggers the client actually getting the authenticated user's data.
 | ||||||
|  |         const res = await client.verifyCredentials() | ||||||
|  |         if (res) { | ||||||
|  |             console.log(`Logged in as @${client.user.username}@${client.user.host}`); | ||||||
|  |         } else { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |     } else { | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const post_id = params.id; | ||||||
|  | 
 | ||||||
|  |     const post_data = await client.getPost(post_id); | ||||||
|  |     if (!post_data) { | ||||||
|  |         console.error(`Failed to retrieve post ${post_id}.`); | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const post = await parsePost(post_data, 10, true); | ||||||
|  |     let posts = [post]; | ||||||
|  |     for (let i in post.replies) { | ||||||
|  |         const reply = post.replies[i]; | ||||||
|  |         // if (i > 1 && reply.reply_id === post.replies[i - 1].id) {
 | ||||||
|  |         //     let reply_head = posts.pop();
 | ||||||
|  |         //     reply.reply = reply_head;
 | ||||||
|  |         // }
 | ||||||
|  |         posts.push(reply); | ||||||
|  |         // console.log(reply);
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |         posts: posts | ||||||
|  |     }; | ||||||
|  | } | ||||||
							
								
								
									
										38
									
								
								src/routes/post/[id]/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/routes/post/[id]/+page.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,38 @@ | ||||||
|  | <script> | ||||||
|  |     import '$lib/app.css'; | ||||||
|  |     import Post from '$lib/ui/post/Post.svelte'; | ||||||
|  | 
 | ||||||
|  |     export let data; | ||||||
|  |     const main_post = data.posts[0]; | ||||||
|  |     const replies = data.posts.slice(1); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <div id="feed" role="feed"> | ||||||
|  |     {#if data.posts.length <= 0} | ||||||
|  |         <div class="throb"> | ||||||
|  |             <span>just a moment...</span> | ||||||
|  |         </div> | ||||||
|  |     {:else} | ||||||
|  |         <Post post_data={main_post} focused /> | ||||||
|  |         <br> | ||||||
|  |         {#each replies as post} | ||||||
|  |             <Post post_data={post} /> | ||||||
|  |         {/each} | ||||||
|  |     {/if} | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  |     #feed { | ||||||
|  |         margin-bottom: 20vh; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .throb { | ||||||
|  |         width: 100%; | ||||||
|  |         height: 80vh; | ||||||
|  |         display: flex; | ||||||
|  |         justify-content: center; | ||||||
|  |         align-items: center; | ||||||
|  |         font-size: 2em; | ||||||
|  |         font-weight: bold; | ||||||
|  |     } | ||||||
|  | </style> | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| import adapter from '@sveltejs/adapter-auto'; | // import adapter from '@sveltejs/adapter-auto';
 | ||||||
|  | import adapter from '@sveltejs/adapter-static'; | ||||||
| import * as child_process from 'node:child_process'; | import * as child_process from 'node:child_process'; | ||||||
| 
 | 
 | ||||||
| /** @type {import('@sveltejs/kit').Config} */ | /** @type {import('@sveltejs/kit').Config} */ | ||||||
|  | @ -7,7 +8,13 @@ const config = { | ||||||
| 		// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
 | 		// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
 | ||||||
| 		// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
 | 		// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
 | ||||||
| 		// See https://kit.svelte.dev/docs/adapters for more information about adapters.
 | 		// See https://kit.svelte.dev/docs/adapters for more information about adapters.
 | ||||||
| 		adapter: adapter(), |         adapter: adapter({ | ||||||
|  |             pages: 'build', | ||||||
|  |             assets: 'build', | ||||||
|  |             fallback: undefined, | ||||||
|  |             precompress: false, | ||||||
|  |             strict: true, | ||||||
|  |         }), | ||||||
|         version: { |         version: { | ||||||
|             name: child_process.execSync('git rev-parse HEAD').toString().trim() |             name: child_process.execSync('git rev-parse HEAD').toString().trim() | ||||||
|         } |         } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue