Compare commits

...

3 commits

20 changed files with 424 additions and 426 deletions

View file

@ -1,4 +1,4 @@
import { Client } from '../client/client.js';
import { client } from '../client/client.js';
import { capabilities } from '../client/instance.js';
import Post from '../post.js';
import User from '../user/user.js';
@ -31,25 +31,23 @@ export async function createApp(host) {
}
export function getOAuthUrl() {
let client = get(Client.get());
return `https://${client.instance.host}/oauth/authorize` +
`?client_id=${client.app.id}` +
return `https://${get(client).instance.host}/oauth/authorize` +
`?client_id=${get(client).app.id}` +
"&scope=read+write+push" +
`&redirect_uri=${location.origin}/callback` +
"&response_type=code";
}
export async function getToken(code) {
let client = get(Client.get());
let form = new FormData();
form.append("client_id", client.app.id);
form.append("client_secret", client.app.secret);
form.append("client_id", get(client).app.id);
form.append("client_secret", get(client).app.secret);
form.append("redirect_uri", `${location.origin}/callback`);
form.append("grant_type", "authorization_code");
form.append("code", code);
form.append("scope", "read write push");
const res = await fetch(`https://${client.instance.host}/oauth/token`, {
const res = await fetch(`https://${get(client).instance.host}/oauth/token`, {
method: "POST",
body: form,
})
@ -65,13 +63,12 @@ export async function getToken(code) {
}
export async function revokeToken() {
let client = get(Client.get());
let form = new FormData();
form.append("client_id", client.app.id);
form.append("client_secret", client.app.secret);
form.append("token", client.app.token);
form.append("client_id", get(client).app.id);
form.append("client_secret", get(client).app.secret);
form.append("token", get(client).app.token);
const res = await fetch(`https://${client.instance.host}/oauth/revoke`, {
const res = await fetch(`https://${get(client).instance.host}/oauth/revoke`, {
method: "POST",
body: form,
})
@ -85,34 +82,32 @@ export async function revokeToken() {
}
export async function verifyCredentials() {
let client = get(Client.get());
let url = `https://${client.instance.host}/api/v1/accounts/verify_credentials`;
let url = `https://${get(client).instance.host}/api/v1/accounts/verify_credentials`;
const data = await fetch(url, {
method: 'GET',
headers: { "Authorization": "Bearer " + client.app.token }
headers: { "Authorization": "Bearer " + get(client).app.token }
}).then(res => res.json());
return data;
}
export async function getTimeline(last_post_id) {
let client = get(Client.get());
let url = `https://${client.instance.host}/api/v1/timelines/home`;
if (!get(client).instance || !get(client).app) return false;
let url = `https://${get(client).instance.host}/api/v1/timelines/home`;
if (last_post_id) url += "?max_id=" + last_post_id;
const data = await fetch(url, {
method: 'GET',
headers: { "Authorization": "Bearer " + client.app.token }
headers: { "Authorization": "Bearer " + get(client).app.token }
}).then(res => res.json());
return data;
}
export async function getPost(post_id, ancestor_count) {
let client = get(Client.get());
let url = `https://${client.instance.host}/api/v1/statuses/${post_id}`;
let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}`;
const data = await fetch(url, {
method: 'GET',
headers: { "Authorization": "Bearer " + client.app.token }
headers: { "Authorization": "Bearer " + get(client).app.token }
}).then(res => { return res.ok ? res.json() : false });
if (data === false) return false;
@ -120,11 +115,10 @@ export async function getPost(post_id, ancestor_count) {
}
export async function getPostContext(post_id) {
let client = get(Client.get());
let url = `https://${client.instance.host}/api/v1/statuses/${post_id}/context`;
let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/context`;
const data = await fetch(url, {
method: 'GET',
headers: { "Authorization": "Bearer " + client.app.token }
headers: { "Authorization": "Bearer " + get(client).app.token }
}).then(res => { return res.ok ? res.json() : false });
if (data === false) return false;
@ -132,11 +126,10 @@ export async function getPostContext(post_id) {
}
export async function boostPost(post_id) {
let client = get(Client.get());
let url = `https://${client.instance.host}/api/v1/statuses/${post_id}/reblog`;
let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/reblog`;
const data = await fetch(url, {
method: 'POST',
headers: { "Authorization": "Bearer " + client.app.token }
headers: { "Authorization": "Bearer " + get(client).app.token }
}).then(res => { return res.ok ? res.json() : false });
if (data === false) return false;
@ -144,11 +137,10 @@ export async function boostPost(post_id) {
}
export async function unboostPost(post_id) {
let client = get(Client.get());
let url = `https://${client.instance.host}/api/v1/statuses/${post_id}/unreblog`;
let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/unreblog`;
const data = await fetch(url, {
method: 'POST',
headers: { "Authorization": "Bearer " + client.app.token }
headers: { "Authorization": "Bearer " + get(client).app.token }
}).then(res => { return res.ok ? res.json() : false });
if (data === false) return false;
@ -156,11 +148,10 @@ export async function unboostPost(post_id) {
}
export async function favouritePost(post_id) {
let client = get(Client.get());
let url = `https://${client.instance.host}/api/v1/statuses/${post_id}/favourite`;
let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/favourite`;
const data = await fetch(url, {
method: 'POST',
headers: { "Authorization": "Bearer " + client.app.token }
headers: { "Authorization": "Bearer " + get(client).app.token }
}).then(res => { return res.ok ? res.json() : false });
if (data === false) return false;
@ -168,11 +159,10 @@ export async function favouritePost(post_id) {
}
export async function unfavouritePost(post_id) {
let client = get(Client.get());
let url = `https://${client.instance.host}/api/v1/statuses/${post_id}/unfavourite`;
let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/unfavourite`;
const data = await fetch(url, {
method: 'POST',
headers: { "Authorization": "Bearer " + client.app.token }
headers: { "Authorization": "Bearer " + get(client).app.token }
}).then(res => { return res.ok ? res.json() : false });
if (data === false) return false;
@ -185,11 +175,10 @@ export async function reactPost(post_id, shortcode) {
// to the default like emote.
// identical api calls on chuckya instances do not display
// this behaviour.
let client = get(Client.get());
let url = `https://${client.instance.host}/api/v1/statuses/${post_id}/react/${encodeURIComponent(shortcode)}`;
let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/react/${encodeURIComponent(shortcode)}`;
const data = await fetch(url, {
method: 'POST',
headers: { "Authorization": "Bearer " + client.app.token }
headers: { "Authorization": "Bearer " + get(client).app.token }
}).then(res => { return res.ok ? res.json() : false });
if (data === false) return false;
@ -197,26 +186,23 @@ export async function reactPost(post_id, shortcode) {
}
export async function unreactPost(post_id, shortcode) {
let client = get(Client.get());
let url = `https://${client.instance.host}/api/v1/statuses/${post_id}/unreact/${encodeURIComponent(shortcode)}`;
let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/unreact/${encodeURIComponent(shortcode)}`;
const data = await fetch(url, {
method: 'POST',
headers: { "Authorization": "Bearer " + client.app.token }
headers: { "Authorization": "Bearer " + get(client).app.token }
}).then(res => { return res.ok ? res.json() : false });
if (data === false) return false;
return data;
}
export async function parsePost(data, ancestor_count, with_context) {
let client = get(Client.get());
export async function parsePost(data, ancestor_count) {
let post = new Post();
post.text = data.content;
post.reply = null;
if (!with_context && // ancestor replies are handled in full later
(data.in_reply_to_id || data.reply) &&
if ((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);
@ -225,28 +211,8 @@ export async function parsePost(data, ancestor_count, with_context) {
if (!reply_data) return false;
post.reply = await parsePost(reply_data, ancestor_count - 1, false);
}
post.boost = data.reblog ? await parsePost(data.reblog, 1, false) : null;
post.replies = [];
if (with_context) {
const replies_data = await getPostContext(data.id);
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.boost = data.reblog ? await parsePost(data.reblog, 1, false) : null;
post.id = data.id;
post.created_at = new Date(data.created_at);
@ -275,7 +241,7 @@ export async function parsePost(data, ancestor_count, with_context) {
});
}
if (data.reactions && client.instance.capabilities.includes(capabilities.REACTIONS)) {
if (data.reactions && get(client).instance.capabilities.includes(capabilities.REACTIONS)) {
post.reactions = parseReactions(data.reactions);
}
return post;
@ -286,8 +252,7 @@ export async function parseUser(data) {
console.error("Attempted to parse user data but no data was provided");
return null;
}
let client = get(Client.get());
let user = await client.getCacheUser(data.id);
let user = await get(client).getCacheUser(data.id);
if (user) return user;
// cache miss!
@ -302,7 +267,7 @@ export async function parseUser(data) {
if (data.acct.includes('@'))
user.host = data.acct.split('@')[1];
else
user.host = client.instance.host;
user.host = get(client).instance.host;
user.emojis = [];
data.emojis.forEach(emoji_data => {
@ -312,12 +277,11 @@ export async function parseUser(data) {
user.emojis.push(parseEmoji(emoji_data));
});
client.putCacheUser(user);
get(client).putCacheUser(user);
return user;
}
export function parseReactions(data) {
let client = get(Client.get());
let reactions = [];
data.forEach(reaction_data => {
let reaction = {
@ -338,16 +302,15 @@ export function parseEmoji(data) {
data.host,
data.url,
);
get(Client.get()).putCacheEmoji(emoji);
get(client).putCacheEmoji(emoji);
return emoji;
}
export async function getUser(user_id) {
let client = get(Client.get());
let url = `https://${client.instance.host}/api/v1/accounts/${user_id}`;
let url = `https://${get(client).instance.host}/api/v1/accounts/${user_id}`;
const data = await fetch(url, {
method: 'GET',
headers: { "Authorization": "Bearer " + client.app.token }
headers: { "Authorization": "Bearer " + get(client).app.token }
}).then(res => res.json());
const user = await parseUser(data);

View file

@ -2,7 +2,7 @@ import { Instance, server_types } from './instance.js';
import * as api from './api.js';
import { get, writable } from 'svelte/store';
let client = writable(false);
export const client = writable(false);
const save_name = "campfire";
@ -22,15 +22,6 @@ export class Client {
};
}
static get() {
let current = get(client);
if (current && current.app) return client;
let new_client = new Client();
new_client.load();
client.set(new_client);
return client;
}
async init(host) {
if (host.startsWith("https://")) host = host.substring(8);
const url = `https://${host}/api/v1/instance`;
@ -76,30 +67,30 @@ export class Client {
console.error("Failed to obtain access token");
return false;
}
this.app.token = token;
client.set(this);
return token;
}
async revokeToken() {
return await api.revokeToken();
}
async verifyCredentials() {
async getUser() {
// already known
if (this.user) return this.user;
// cannot provide- not logged in
if (!this.app || !this.app.token) {
this.user = false;
return false;
}
// logged in- attempt to retrieve using token
const data = await api.verifyCredentials();
if (!data) {
this.user = false;
return false;
}
await client.update(async c => {
c.user = await api.parseUser(data);
console.log(`Logged in as @${c.user.username}@${c.user.host}`);
});
return this.user;
const user = await api.parseUser(data);
console.log(`Logged in as @${user.username}@${user.host}`);
return user;
}
async getTimeline(last_post_id) {
@ -110,6 +101,10 @@ export class Client {
return await api.getPost(post_id, parent_replies, child_replies);
}
async getPostContext(post_id) {
return await api.getPostContext(post_id);
}
async boostPost(post_id) {
return await api.boostPost(post_id);
}
@ -199,7 +194,7 @@ export class Client {
console.warn("Failed to log out correctly; ditching the old tokens anyways.");
}
localStorage.removeItem(save_name);
client.set(false);
client.set(new Client());
console.log("Logged out successfully.");
}
}

View file

@ -1,4 +1,4 @@
import { Client } from './client/client.js';
import { client } from './client/client.js';
import { get } from 'svelte/store';
export const EMOJI_REGEX = /:[\w\-.]{0,32}@[\w\-.]{0,32}:/g;
@ -33,7 +33,7 @@ export function parseText(text, host) {
let length = text.substring(index + 1).search(':');
if (length <= 0) return text;
let emoji_name = text.substring(index + 1, index + length + 1);
let emoji = get(Client.get()).getEmoji(emoji_name + '@' + host);
let emoji = get(client).getEmoji(emoji_name + '@' + host);
if (emoji) {
return text.substring(0, index) + emoji.html +
@ -46,7 +46,7 @@ export function parseText(text, host) {
export function parseOne(emoji_id) {
if (emoji_id == '❤') return '❤️'; // stupid heart unicode
if (EMOJI_REGEX.exec(':' + emoji_id + ':')) return emoji_id;
let cached_emoji = get(Client.get()).getEmoji(emoji_id);
let cached_emoji = get(client).getEmoji(emoji_id);
if (!cached_emoji) return emoji_id;
return cached_emoji.html;
}

View file

@ -1,4 +1,4 @@
import { Client } from '$lib/client/client.js';
import { client } from '$lib/client/client.js';
import { get, writable } from 'svelte/store';
import { parsePost } from '$lib/client/api.js';
@ -10,11 +10,9 @@ export async function getTimeline(clean) {
if (loading) return; // no spamming!!
loading = true;
let client = get(Client.get());
let timeline_data;
if (clean || get(posts).length === 0) timeline_data = await client.getTimeline()
else timeline_data = await client.getTimeline(get(posts)[get(posts).length - 1].id);
if (clean || get(posts).length === 0) timeline_data = await get(client).getTimeline()
else timeline_data = await get(client).getTimeline(get(posts)[get(posts).length - 1].id);
if (!timeline_data) {
console.error(`Failed to retrieve timeline.`);

View file

@ -1,10 +1,6 @@
<script>
import Button from './Button.svelte';
import Post from './post/Post.svelte';
import Error from './Error.svelte';
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';
getTimeline();
@ -15,6 +11,15 @@
});
</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" role="feed">
{#if posts.length <= 0}
<div class="loading throb">

162
src/lib/ui/LoginForm.svelte Normal file
View file

@ -0,0 +1,162 @@
<script>
import { client } from '$lib/client/client.js';
import { get } from 'svelte/store';
import Logo from '$lib/../img/campfire-logo.svg';
let instance_url_error = false;
let logging_in = false;
function log_in(event) {
event.preventDefault();
instance_url_error = false;
logging_in = true;
const host = event.target.host.value;
if (!host || host === "") {
instance_url_error = "Please enter an instance domain.";
logging_in = false;
return;
}
console.log(client);
get(client).init(host).then(res => {
logging_in = false;
if (!res) return;
if (res.constructor === String) {
instance_url_error = res;
return;
};
let oauth_url = get(client).getOAuthUrl();
location = oauth_url;
});
}
</script>
<form on:submit={log_in} id="login-form">
<div class="app-logo">
<Logo />
</div>
<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://bliss.town">bliss town</a>, 2024</p>
</form>
<style>
form#login-form {
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
}
.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;
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

@ -2,7 +2,7 @@
import Logo from '$lib/../img/campfire-logo.svg';
import Button from './Button.svelte';
import Feed from './Feed.svelte';
import { Client } from '$lib/client/client.js';
import { client } from '$lib/client/client.js';
import { play_sound } from '$lib/sound.js';
import { getTimeline } from '$lib/timeline.js';
import { goto } from '$app/navigation';
@ -22,11 +22,6 @@
const VERSION = APP_VERSION;
let client = false;
Client.get().subscribe(c => {
client = c;
});
let notification_count = 0;
if (notification_count > 99) notification_count = "99+";
@ -44,26 +39,20 @@
async function log_out() {
if (!confirm("This will log you out. Are you sure?")) return;
await get(Client.get()).logout();
await get(client).logout();
goto("/");
}
</script>
<div id="navigation">
{#if client.instance && client.instance.icon_url && client.instance.banner_url}
<header class="instance-header" style="background-image: url({client.instance.banner_url})">
<img src={client.instance.icon_url} class="instance-icon" height="92px" aria-hidden="true">
</header>
{:else}
<header class="instance-header">
<div class="app-logo">
<Logo />
</div>
</header>
{/if}
<div id="nav-items">
<Button label="Timeline" on:click={() => goTimeline()} active={client.user}>
<Button label="Timeline" on:click={() => goTimeline()} active={!!$client.user}>
<svelte:fragment slot="icon">
<TimelineIcon/>
</svelte:fragment>
@ -117,7 +106,7 @@
</Button>
</div>
{#if (client.user)}
{#if $client.user}
<div id="account-items">
<div class="flex-row">
<Button centered label="Profile information" disabled>
@ -138,11 +127,11 @@
</div>
<div id="account-button">
<img src={client.user.avatar_url} class="account-avatar" height="64px" alt="" 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}`}>
{`@${client.user.username}@${client.user.host}`}
<span class="nickname" title={$client.user.nickname}>{$client.user.nickname}</span>
<span class="username" title={`@${$client.user.username}@${$client.user.host}`}>
{`@${$client.user.username}@${$client.user.host}`}
</span>
</div>
</div>

View file

@ -1,5 +1,5 @@
<script>
import { Client } from '../../client/client.js';
import { client } from '../../client/client.js';
import * as api from '../../client/api.js';
import { get } from 'svelte/store';
@ -16,12 +16,11 @@
export let post;
async function toggleBoost() {
let client = get(Client.get());
let data;
if (post.boosted)
data = await client.unboostPost(post.id);
data = await get(client).unboostPost(post.id);
else
data = await client.boostPost(post.id);
data = await get(client).boostPost(post.id);
if (!data) {
console.error(`Failed to boost post ${post.id}`);
return;
@ -31,12 +30,11 @@
}
async function toggleFavourite() {
let client = get(Client.get());
let data;
if (post.favourited)
data = await client.unfavouritePost(post.id);
data = await get(client).unfavouritePost(post.id);
else
data = await client.favouritePost(post.id);
data = await get(client).favouritePost(post.id);
if (!data) {
console.error(`Failed to favourite post ${post.id}`);
return;
@ -48,13 +46,12 @@
async function toggleReaction(reaction) {
if (reaction.name.includes('@')) return;
let client = get(Client.get());
let data;
if (reaction.me)
data = await client.unreactPost(post.id, reaction.name);
data = await get(client).unreactPost(post.id, reaction.name);
else
data = await client.reactPost(post.id, reaction.name);
data = await get(client).reactPost(post.id, reaction.name);
if (!data) {
console.error(`Failed to favourite post ${post.id}`);
return;

View file

@ -10,7 +10,10 @@
<div class="post-context">
<span class="post-context-icon">🔁</span>
<span class="post-context-action">
<a href={post.user.url} target="_blank">{@html parseEmojis(post.user.rich_name)}</a> boosted this post.
<a href={post.user.url} target="_blank"><span class="name">
{@html parseEmojis(post.user.rich_name)}</span>
</a>
boosted this post.
</span>
<span class="post-context-time">
<time title="{time_string}">{short_time(post.created_at)}</time>
@ -48,6 +51,12 @@
text-decoration: underline;
}
.post-context .name :global(.emoji) {
position: relative;
top: .2em;
height: 1.2em;
}
.post-context-time {
margin-left: auto;
}

View file

@ -50,7 +50,9 @@
<div class="post-container">
{#if post.reply}
<ReplyContext post={post.reply} />
{#await post.reply then reply}
<ReplyContext post={reply} />
{/await}
{/if}
{#if is_boost && !post_context.text}
<BoostContext post={post_context} />
@ -65,7 +67,9 @@
<PostHeader post={post} />
<Body post={post} />
<footer class="post-footer">
{#if post.reactions}
<ReactionBar post={post} />
{/if}
<ActionBar post={post} />
</footer>
</article>

View file

@ -8,16 +8,16 @@
let time_string = post.created_at.toLocaleString();
</script>
<div class={"post-header-container" + (reply ? " reply" : "")} on:mouseup|stopPropagation>
<a href={post.user.url} target="_blank" class="post-avatar-container">
<div class={"post-header-container" + (reply ? " reply" : "")}>
<a href={post.user.url} target="_blank" class="post-avatar-container" on:mouseup|stopPropagation>
<img src={post.user.avatar_url} type={post.user.avatar_type} alt="" width="48" height="48" class="post-avatar" loading="lazy" decoding="async">
</a>
<header class="post-header">
<div class="post-user-info">
<div class="post-user-info" on:mouseup|stopPropagation>
<a href={post.user.url} target="_blank" class="name">{@html post.user.rich_name}</a>
<span class="username">{post.user.mention}</span>
</div>
<div class="post-info">
<div class="post-info" on:mouseup|stopPropagation>
<a href={post.url} target="_blank" class="created-at">
<time title={time_string}>{short_time(post.created_at)}</time>
{#if post.visibility !== "public"}
@ -82,8 +82,8 @@
.post-user-info .name :global(.emoji) {
position: relative;
top: 4px;
height: 20px;
top: .2em;
height: 1.2em;
}
.post-user-info .username {

View file

@ -1,8 +1,6 @@
<script>
import { parseText as parseEmojis, parseOne as parseEmoji } from '../../emoji.js';
import { shorthand as short_time } from '../../time.js';
import { get } from 'svelte/store';
import { Client } from '../../client/client.js';
import * as api from '../../client/api.js';
import { goto } from '$app/navigation';
@ -20,19 +18,20 @@
function gotoPost() {
if (event && event.key && event.key !== "Enter") return;
console.log(`/post/${post.id}`);
goto(`/post/${post.id}`);
}
</script>
{#if post.reply}
<svelte:self post={post.reply} />
{#await post.reply then reply}
<svelte:self post={reply} />
{/await}
{/if}
<article
class="post-reply"
aria-label={aria_label}
on:mousedown={e => {mouse_pos.left = e.pageX; mouse_pos.top = e.pageY; console.log(mouse_pos)}}
on:mousedown={e => {mouse_pos.left = e.pageX; mouse_pos.top = e.pageY}}
on:mouseup={e => {if (e.pageX == mouse_pos.left && e.pageY == mouse_pos.top) gotoPost()}}
on:keydown={gotoPost}>
<div class="line"></div>
@ -43,7 +42,9 @@
<Body post={post} />
<footer class="post-footer">
{#if post.reactions}
<ReactionBar post={post} />
{/if}
<ActionBar post={post} />
</footer>
</div>

View file

@ -1,4 +1,4 @@
import { Client } from '../client/client.js';
import { client } from '../client/client.js';
import { parseText as parseEmojis } from '../emoji.js';
import { get } from 'svelte/store';
@ -17,7 +17,7 @@ export default class User {
get mention() {
let res = "@" + this.username;
if (this.host != get(Client.get()).instance.host)
if (this.host != get(client).instance.host)
res += "@" + this.host;
return res;
}

View file

@ -2,10 +2,26 @@
import '$lib/app.css';
import Navigation from '$lib/ui/Navigation.svelte';
import Widgets from '$lib/ui/Widgets.svelte';
import { Client } from '$lib/client/client.js';
import { client, Client } from '$lib/client/client.js';
import { get } from 'svelte/store';
let client = get(Client.get());
let ready = new Promise(resolve => {
if (get(client)) {
return resolve();
}
let new_client = new Client();
new_client.load();
return new_client.getUser().then(user => {
if (!user) {
client.set(new_client);
return resolve();
}
new_client.user = user;
client.set(new_client);
return resolve();
});
});
</script>
<div id="app">
@ -15,7 +31,7 @@
</header>
<main>
{#await client.verifyCredentials()}
{#await ready}
<div class="loading throb">
<span>just a moment...</span>
</div>
@ -29,3 +45,15 @@
</div>
</div>
<style>
.loading {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-size: 2em;
font-weight: bold;
}
</style>

View file

@ -1,15 +1,2 @@
import Feed from '$lib/ui/Feed.svelte';
import { Client } from '$lib/client/client.js';
import Button from '$lib/ui/Button.svelte';
import { get } from 'svelte/store';
export const prerender = true;
export const ssr = false;
export async function load() {
let client = get(Client.get());
await client.verifyCredentials();
return {
client: client
};
}

View file

@ -1,92 +1,19 @@
<script>
import Logo from '$lib/../img/campfire-logo.svg';
import { client } from '$lib/client/client.js';
import LoginForm from '$lib/ui/LoginForm.svelte';
import Feed from '$lib/ui/Feed.svelte';
import { Client } from '$lib/client/client.js';
import User from '$lib/user/user.js';
import Button from '$lib/ui/Button.svelte';
import { get } from 'svelte/store';
export let data;
let client = data.client;
let logged_in = client.user && client.user.constructor === User;
let instance_url_error = false;
let logging_in = false;
function log_in(event) {
event.preventDefault();
instance_url_error = false;
logging_in = true;
const host = event.target.host.value;
if (!host || host === "") {
instance_url_error = "Please enter an instance domain.";
logging_in = false;
return;
}
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>
{#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>
{#if $client.user}
<Feed />
{:else}
<form on:submit={log_in} id="login-form">
<div class="app-logo">
<Logo />
</div>
<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://bliss.town">bliss town</a>, 2024</p>
</form>
<LoginForm />
{/if}
<style>
form#login-form {
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
}
a {
color: var(--accent);
text-decoration: none;
@ -96,106 +23,6 @@
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;
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;
}
.loading {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-size: 2em;
font-weight: bold;
}
header {
width: 100%;
margin: 16px 0 8px 0;

View file

@ -1,20 +1,5 @@
import { Client } from '$lib/client/client.js';
import { goto } from '$app/navigation';
import { error } from '@sveltejs/kit';
import { get } from 'svelte/store';
export const ssr = false;
export async function load({ params, url }) {
const client = get(Client.get());
let auth_code = url.searchParams.get("code");
if (auth_code) {
client.getToken(auth_code).then(() => {
client.save();
goto("/");
});
}
error(400, {
message: "Bad request"
});
export async function load({ url }) {
return {
code: url.searchParams.get("code") || false
};
}

View file

@ -0,0 +1,35 @@
<script>
import { client } from '$lib/client/client.js';
import { goto } from '$app/navigation';
import { error } from '@sveltejs/kit';
import { get } from 'svelte/store';
export let data;
let auth_code = data.code;
if (!auth_code) {
error(400, { message: "Bad request" });
} else {
get(client).getToken(auth_code).then(token => {
if (!token) {
error(400, { message: "Invalid auth code provided" });
return;
}
client.update(c => {
c.app.token = token;
c.save();
return c;
});
get(client).getUser().then(user => {
if (user) client.update(client => {
client.user = user
return client;
});
goto("/");
});
});
}
</script>

View file

@ -1,39 +1,5 @@
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';
import { goto } from '$app/navigation';
export const ssr = false;
export async function load({ params }) {
let client = get(Client.get());
if (!client.instance || !client.user) {
goto("/");
}
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
post_id: params.id
};
}

View file

@ -1,37 +1,80 @@
<script>
import '$lib/app.css';
import { client } from '$lib/client/client.js';
import * as api from '$lib/client/api.js';
import { get } from 'svelte/store';
import Post from '$lib/ui/post/Post.svelte';
import Button from '$lib/ui/Button.svelte';
export let data;
$: main_post = data.posts[0];
$: replies = data.posts.slice(1);
let error = false;
if (!get(client).instance || !get(client).user) {
goto("/");
}
$: post = (async resolve => {
const post_data = await get(client).getPost(data.post_id, 0, false);
if (!post_data) {
error = `Failed to retrieve post <code>${data.post_id}</code>.`;
console.error(`Failed to retrieve post ${data.post_id}.`);
return;
}
let post = await api.parsePost(post_data, 0, false);
const post_context = await get(client).getPostContext(data.post_id);
if (!post_context || !post_context.ancestors || !post_context.descendants)
return post;
// handle ancestors (above post)
let thread_top = post;
while (post_context.ancestors.length > 0) {
thread_top.reply = await api.parsePost(post_context.ancestors.pop(), 0, false);
thread_top = thread_top.reply;
}
// handle descendants (below post)
post.replies = [];
for (let i in post_context.descendants) {
post.replies.push(
api.parsePost(post_context.descendants[i], 0, false)
);
}
console.log(post);
return post;
})();
</script>
{#if !error}
<header>
<h1>Home</h1>
{#await post then post}
<h1>Post by {@html post.user.rich_name}</h1>
{/await}
<nav>
<Button centered active>Home</Button>
<Button centered disabled>Local</Button>
<Button centered disabled>Federated</Button>
<Button centered>Back</Button>
</nav>
</header>
<div id="feed" role="feed">
{#if data.posts.length <= 0}
{#await post}
<div class="throb">
<span>just a moment...</span>
<span>loading post...</span>
</div>
{:else}
{#key data}
<Post post_data={main_post} focused />
{:then post}
<Post post_data={post} focused />
<br>
{#each replies as post}
<Post post_data={post} />
{#each post.replies as reply}
{#await reply then reply}
<Post post_data={reply} />
{/await}
{/each}
{/key}
{/if}
{/await}
</div>
{:else}
<p>{@html error}</p>
{/if}
<style>
header {
@ -42,7 +85,11 @@
}
header h1 {
margin: auto auto auto 8px;
font-size: 1.5em;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
header nav {