finish sk restructure, a11y and optimisations

This commit is contained in:
ari melody 2024-06-29 14:48:34 +01:00
parent 9ef27fd2a2
commit 3ae05b3f9f
Signed by: ari
GPG key ID: CF99829C92678188
61 changed files with 416 additions and 429 deletions

13
package-lock.json generated
View file

@ -7,9 +7,10 @@
"": {
"name": "spacesocial-client",
"version": "0.2.0_rev2",
"license": "ISC",
"license": "GPL-3.0",
"devDependencies": {
"@sveltejs/adapter-auto": "^3.2.2",
"@sveltejs/adapter-static": "^3.0.2",
"@sveltejs/kit": "^2.5.17",
"@sveltejs/vite-plugin-svelte": "^3.1.1",
"svelte": "^4.2.18",
@ -718,6 +719,16 @@
"@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": {
"version": "2.5.17",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.17.tgz",

View file

@ -13,6 +13,7 @@
"license": "GPL-3.0",
"devDependencies": {
"@sveltejs/adapter-auto": "^3.2.2",
"@sveltejs/adapter-static": "^3.0.2",
"@sveltejs/kit": "^2.5.17",
"@sveltejs/vite-plugin-svelte": "^3.1.1",
"svelte": "^4.2.18",

View file

@ -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>

View file

@ -1,4 +1,4 @@
@import url("/font/inter/inter.css");
@import url("../font/inter/inter.css");
:root {
--bg-1000: #fff6de;
@ -40,6 +40,32 @@ body {
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 {
animation: .25s throb alternate infinite ease-in;
}

View file

@ -8,7 +8,7 @@ import { get } from 'svelte/store';
export async function createApp(host) {
let form = new FormData();
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("website", "https://spacesocial.arimelody.me");
@ -35,7 +35,7 @@ export function getOAuthUrl() {
return `https://${client.instance.host}/oauth/authorize` +
`?client_id=${client.app.id}` +
"&scope=read+write+push" +
`&redirect_uri=${location.origin}/callback` +
`&redirect_uri=${location.origin}` +
"&response_type=code";
}
@ -44,7 +44,7 @@ export async function getToken(code) {
let form = new FormData();
form.append("client_id", client.app.id);
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("code", code);
form.append("scope", "read write push");
@ -107,7 +107,7 @@ export async function getTimeline(last_post_id) {
return data;
}
export async function getPost(post_id, parent_replies) {
export async function getPost(post_id, ancestor_count) {
let client = get(Client.get());
let url = `https://${client.instance.host}/api/v1/statuses/${post_id}`;
const data = await fetch(url, {
@ -208,7 +208,7 @@ export async function unreactPost(post_id, shortcode) {
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 post = new Post();
@ -218,9 +218,12 @@ export async function parsePost(data, parent_replies, child_replies) {
post.text = data.content;
post.reply = null;
if ((data.in_reply_to_id || data.reply) && parent_replies !== 0) {
const reply_data = data.reply || await getPost(data.in_reply_to_id, parent_replies - 1);
post.reply = await parsePost(reply_data, parent_replies - 1, false);
if (!with_context && // ancestor replies are handled in full later
(data.in_reply_to_id || data.reply) &&
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.
// we'll respect the thread's privacy, and leave it alone :)
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.replies = [];
if (child_replies) {
if (with_context) {
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) {
post.replies.push(await parsePost(replies_data.descendants[i], 0, false));
}
}
}
}
post.id = data.id;
post.created_at = new Date(data.created_at);

View file

@ -17,6 +17,7 @@ export default class Post {
files;
url;
reply;
reply_id;
replies;
boost;
visibility;

View file

@ -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;
if (typeof Audio !== typeof undefined) {
sounds = {
"default": new Audio("/sound/log.ogg"),
"post": new Audio("/sound/success.ogg"),
"boost": new Audio("/sound/hello.ogg"),
"default": new Audio(sound_log),
"post": new Audio(sound_hello),
"boost": new Audio(sound_success),
};
}

43
src/lib/timeline.js Normal file
View 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;
}

View file

@ -2,104 +2,33 @@
import Button from './Button.svelte';
import Post from './post/Post.svelte';
import Error from './Error.svelte';
import { Client } from '../client/client.js';
import { parsePost } from '../client/api.js';
import { Client } from '$lib/client/client.js';
import { parsePost } from '$lib/client/api.js';
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();
document.addEventListener("scroll", event => {
if (!loading && window.innerHeight + window.scrollY >= document.body.offsetHeight - 2048) {
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 2048) {
getTimeline();
}
});
}
</script>
<header>
<h1>Home</h1>
<nav>
<Button centered active>Home</Button>
<Button centered disabled>Local</Button>
<Button centered disabled>Federated</Button>
</nav>
</header>
<div id="feed">
<div id="feed" role="feed">
{#if posts.length <= 0}
<div class="throb">
<span>just a moment...</span>
<div class="loading throb">
<span>getting the feed...</span>
</div>
{/if}
{#each posts as post}
<Post post_data={post} focused={post.id === focus_post_id} />
{#each $posts as post}
<Post post_data={post} />
{/each}
</div>
<style>
header {
width: 100%;
margin: 16px 0 8px 0;
display: flex;
flex-direction: row;
@ -120,7 +49,7 @@
margin-bottom: 20vh;
}
.throb {
.loading {
width: 100%;
height: 80vh;
display: flex;

View file

@ -5,6 +5,8 @@
import { Client } from '../client/client.js';
import { play_sound } from '../sound.js';
const VERSION = APP_VERSION;
let client = false;
Client.get().subscribe(c => {
client = c;
@ -14,6 +16,13 @@
if (notification_count > 99) notification_count = "99+";
function goTimeline() {
if (location.pathname === "/") {
window.scrollTo({
top: 0,
behavior: "smooth"
});
return;
}
location = "/";
}
@ -61,7 +70,7 @@
</div>
<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">
<span class="nickname" title={client.user.nickname}>{client.user.nickname}</span>
<span class="username" title={`@${client.user.username}@${client.user.host}`}>
@ -73,7 +82,7 @@
</div>
{/if}
<span class="version">
space social v{APP_VERSION}
space social v{VERSION}
<br>
<ul>
<li><a href="https://git.arimelody.me/ari/spacesocial-client">source</a></li>
@ -252,26 +261,6 @@
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 {
display: flex;
flex-direction: row;

View file

@ -15,22 +15,24 @@
<span class="post-context-time">
<time title="{time_string}">{short_time(post.created_at)}</time>
{#if post.visibility !== "public"}
<span class="post-visibility">({post.visibility})</span>
<span class="post-visibility">- {post.visibility}</span>
{/if}
</span>
</div>
<style>
.post-context {
margin-bottom: 8px;
padding-left: 58px;
padding: 12px 16px 0 74px;
display: flex;
flex-direction: row;
align-items: center;
font-size: .8em;
font-weight: 600;
color: var(--text);
opacity: .8;
transition: opacity .1s;
transition: opacity .1s, background-color .1s;
border-radius: 8px;
z-index: 1;
}
.post-context-icon {
@ -49,4 +51,8 @@
.post-context-time {
margin-left: auto;
}
.post-visibility {
opacity: .7;
}
</style>

View file

@ -11,6 +11,7 @@
import { get } from 'svelte/store';
import { Client } from '../../client/client.js';
import * as api from '../../client/api.js';
import { goto } from '$app/navigation';
export let post_data;
export let focused = false;
@ -25,7 +26,9 @@
}
function gotoPost() {
location = `/post/${post.id}`;
if (focused) return;
if (event.key && event.key !== "Enter") return;
goto(`/post/${post.id}`);
}
async function toggleBoost() {
@ -80,25 +83,30 @@
let el;
onMount(() => {
if (focused) {
window.scrollTo(0, el.scrollHeight - 700);
window.scrollTo(0, el.scrollHeight);
}
});
let aria_label = post.user.username + '; ' + post.text + '; ' + post.created_at;
</script>
<div class="post-container" aria-label={aria_label} bind:this={el}>
<div class="post-container">
{#if post.reply}
<ReplyContext post={post.reply} />
{/if}
{#if is_boost && !post_context.text}
<BoostContext post={post_context} />
{/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} />
<Body post={post} />
<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}
<ReactionButton
type="reaction"
@ -116,7 +124,7 @@
</ReactionButton>
{/each}
</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="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>
@ -130,37 +138,39 @@
<style>
.post-container {
width: 700px;
max-width: 700px;
width: 732px;
max-width: 732px;
margin-bottom: 8px;
padding: 16px;
display: flex;
flex-direction: column;
border-radius: 8px;
background-color: var(--bg-800);
}
.post {
padding: 16px;
border-radius: 8px;
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) {
cursor: pointer;
}
.post.focused {
padding: 16px;
margin: -16px;
border-radius: 8px;
border: 1px solid 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) {
width: fit-content;
display: flex;

View file

@ -9,12 +9,16 @@
import { get } from 'svelte/store';
import { Client } from '../../client/client.js';
import * as api from '../../client/api.js';
import { goto } from '$app/navigation';
export let post;
let time_string = post.created_at.toLocaleString();
let aria_label = post.user.username + '; ' + post.text + '; ' + post.created_at;
function gotoPost() {
location = `/post/${post.id}`;
if (focused) return;
if (event.key && event.key !== "Enter") return;
goto(`/post/${post.id}`);
}
async function toggleBoost() {
@ -71,7 +75,11 @@
<svelte:self post={post.reply} />
{/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="post-reply-main">
@ -80,7 +88,7 @@
<Body post={post} />
<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}
<ReactionButton
type="reaction"
@ -98,7 +106,7 @@
</ReactionButton>
{/each}
</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="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>
@ -112,11 +120,18 @@
<style>
.post-reply {
padding-bottom: 24px;
padding: 16px 16px 16px 16px;
display: flex;
flex-direction: row;
color: var(--text);
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 {
@ -125,8 +140,8 @@
.line {
position: relative;
top: 24px;
left: 25px;
top: 32px;
left: 23px;
border-right: 2px solid var(--bg-700);
}

View file

@ -8,7 +8,8 @@
import { get } from 'svelte/store';
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 logging_in = false;
@ -57,6 +58,16 @@
<main>
{#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 />
{:else}
<div>
@ -84,6 +95,11 @@
</form>
</div>
{/if}
{:else}
<div class="loading throb">
<span>just a moment...</span>
</div>
{/if}
</main>
<div id="widgets">
@ -93,31 +109,6 @@
</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;
@ -221,4 +212,32 @@
.form-footer {
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>

View 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>

View 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
};
}

View 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>

View file

@ -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';
/** @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.
// 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.
adapter: adapter(),
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: undefined,
precompress: false,
strict: true,
}),
version: {
name: child_process.execSync('git rev-parse HEAD').toString().trim()
}