diff --git a/src/lib/account.js b/src/lib/account.js new file mode 100644 index 0000000..7bfd696 --- /dev/null +++ b/src/lib/account.js @@ -0,0 +1,52 @@ +import { server } from '$lib/client/server.js'; +import { parseEmoji, renderEmoji } from '$lib/emoji.js'; +import { get, writable } from 'svelte/store'; + +const cache = writable({}); + +/** + * Parses an account using API data, and returns a writable store object. + * @param {Object} data + * @param {number} ancestor_count + */ +export function parseAccount(data) { + if (!data) { + console.error("Attempted to parse account data but no data was provided"); + return null; + } + let account = get(cache)[data.id]; + if (account) return account; + // cache miss! + + account = {}; + account.id = data.id; + account.nickname = data.display_name.trim(); + account.username = data.username; + account.name = account.nickname || account.username; + account.avatar_url = data.avatar; + account.url = data.url; + + if (data.acct.includes('@')) + account.host = data.acct.split('@')[1]; + else + account.host = get(server).host; + + account.mention = "@" + account.username; + if (account.host != get(server).host) + account.mention += "@" + account.host; + + account.emojis = {}; + data.emojis.forEach(emoji => { + account.emojis[emoji.shortcode] = parseEmoji(emoji.shortcode, emoji.url); + }); + + account.rich_name = account.nickname ? renderEmoji(account.nickname, account.emojis) : account.username; + + cache.update(cache => { + cache[account.id] = account; + return cache; + }); + + return account; +} + diff --git a/src/lib/api.js b/src/lib/api.js new file mode 100644 index 0000000..8006213 --- /dev/null +++ b/src/lib/api.js @@ -0,0 +1,339 @@ +/** + * GET /api/v1/instance + * @param {string} host - The domain of the target server. + */ +export async function getInstance(host) { + const data = await fetch(`https://${host}/api/v1/instance`) + .then(res => res.json()) + .catch(error => console.error(error)); + return data ? data : false; +} + +/** + * POST /api/v1/apps + * Attempts to create an application for a given server host. + * @param {string} host - The domain of the target server. + */ +export async function createApp(host) { + let form = new FormData(); + form.append("client_name", "Campfire"); + form.append("redirect_uris", `${location.origin}/callback`); + form.append("scopes", "read write push"); + form.append("website", "https://campfire.bliss.town"); + + const res = await fetch(`https://${host}/api/v1/apps`, { + method: "POST", + body: form, + }) + .then(res => res.json()) + .catch(error => { + console.error(error); + return false; + }); + + if (!res || !res.client_id) return false; + + return { + id: res.client_id, + secret: res.client_secret, + }; +} + +/** + * Returns the OAuth authorization url for the target server. + * @param {string} host - The domain of the target server. + * @param {string} app_id - The application id for the target server. + */ +export function getOAuthUrl(host, app_id) { + return `https://${host}/oauth/authorize` + + `?client_id=${app_id}` + + "&scope=read+write+push" + + `&redirect_uri=${location.origin}/callback` + + "&response_type=code"; +} + +/** + * POST /oauth/token + * Attempts to generate an OAuth token. + * Returns false on failure. + * @param {string} host - The domain of the target server. + * @param {string} client_id - The application id. + * @param {string} secret - The application secret. + * @param {string} code - The authorization code provided by OAuth. + */ +export async function getToken(host, client_id, secret, code) { + let form = new FormData(); + form.append("client_id", client_id); + form.append("client_secret", 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://${host}/oauth/token`, { + method: "POST", + body: form, + }) + .then(res => res.json()) + .catch(error => { + console.error(error); + return false; + }); + + if (!res || !res.access_token) return false; + + return res.access_token; +} + +/** + * POST /oauth/revoke + * Attempts to revoke an OAuth token. + * Returns false on failure. + * @param {string} host - The domain of the target server. + * @param {string} client_id - The application id. + * @param {string} secret - The application secret. + * @param {string} token - The application token. + */ +export async function revokeToken(host, client_id, secret, token) { + let form = new FormData(); + form.append("client_id", client_id); + form.append("client_secret", secret); + form.append("token", token); + + const res = await fetch(`https://${host}/oauth/revoke`, { + method: "POST", + body: form, + }) + .catch(error => { + console.error(error); + return false; + }); + + if (!res.ok) return false; + return true; +} + +/** + * GET /api/v1/accounts/verify_credentials + * This endpoint returns information about the client account, + * and other useful data. + * Returns false on failure. + * @param {string} host - The domain of the target server. + * @param {string} token - The application token. + */ +export async function verifyCredentials(host, token) { + let url = `https://${host}/api/v1/accounts/verify_credentials`; + const data = await fetch(url, { + method: 'GET', + headers: { "Authorization": "Bearer " + token } + }).then(res => res.json()); + + return data; +} + +/** + * GET /api/v1/notifications + * @param {string} host - The domain of the target server. + * @param {string} token - The application token. + * @param {string} max_id - If provided, only shows notifications after this ID. + * @param {string} limit - The maximum number of notifications to retrieve (default 40). + * @param {string} types - A list of notification types to filter to. + */ +export async function getNotifications(host, token, max_id, limit, types) { + let url = `https://${host}/api/v1/notifications`; + + let params = new URLSearchParams(); + if (max_id) params.append("max_id", max_id); + if (limit) params.append("limit", limit); + if (types) params.append("types", types.join(',')); + const params_string = params.toString(); + if (params_string) url += '?' + params_string; + + const data = await fetch(url, { + method: 'GET', + headers: { "Authorization": "Bearer " + token } + }).then(res => res.json()); + + return data; +} + +/** + * GET /api/v1/timelines/{timeline} + * @param {string} host - The domain of the target server. + * @param {string} token - The application token. + * @param {string} timeline - The name of the timeline to pull (default "home"). + * @param {string} max_id - If provided, only shows posts after this ID. + */ +export async function getTimeline(host, token, timeline, max_id) { + let url = `https://${host}/api/v1/timelines/${timeline || "home"}`; + + let params = new URLSearchParams(); + if (max_id) params.append("max_id", max_id); + const params_string = params.toString(); + if (params_string) url += '?' + params_string; + + const data = await fetch(url, { + method: 'GET', + headers: { "Authorization": token ? `Bearer ${token}` : null } + }).then(res => res.json()); + + return data; +} + +/** + * GET /api/v1/statuses/{post_id}. + * @param {string} host - The domain of the target server. + * @param {string} token - The application token. + * @param {string} post_id - The ID of the post to fetch. + */ +export async function getPost(host, token, post_id) { + let url = `https://${host}/api/v1/statuses/${post_id}`; + + const data = await fetch(url, { + method: 'GET', + headers: { "Authorization": token ? `Bearer ${token}` : null } + }).then(res => res.json()); + + return data; +} + +/** + * GET /api/v1/statuses/{post_id}/context. + * @param {string} host - The domain of the target server. + * @param {string} token - The application token. + * @param {string} post_id - The ID of the post to fetch. + */ +export async function getPostContext(host, token, post_id) { + let url = `https://${host}/api/v1/statuses/${post_id}/context`; + + const data = await fetch(url, { + method: 'GET', + headers: { "Authorization": token ? `Bearer ${token}` : null } + }).then(res => res.json()); + + return data; +} + +/** + * POST /api/v1/statuses/{post_id}/reblog. + * @param {string} host - The domain of the target server. + * @param {string} token - The application token. + * @param {string} post_id - The ID of the post to boost. + */ +export async function boostPost(host, token, post_id) { + let url = `https://${host}/api/v1/statuses/${post_id}/reblog`; + + const data = await fetch(url, { + method: 'POST', + headers: { "Authorization": `Bearer ${token}` } + }).then(res => res.json()); + + return data; +} + +/** + * POST /api/v1/statuses/{post_id}/unreblog. + * @param {string} host - The domain of the target server. + * @param {string} token - The application token. + * @param {string} post_id - The ID of the post to unboost. + */ +export async function unboostPost(host, token, post_id) { + let url = `https://${host}/api/v1/statuses/${post_id}/unreblog`; + + const data = await fetch(url, { + method: 'POST', + headers: { "Authorization": `Bearer ${token}` } + }).then(res => res.json()); + + return data; +} + +/** + * POST /api/v1/statuses/{post_id}/favourite. + * @param {string} host - The domain of the target server. + * @param {string} token - The application token. + * @param {string} post_id - The ID of the post to favourite. + */ +export async function favouritePost(host, token, post_id) { + let url = `https://${host}/api/v1/statuses/${post_id}/favourite`; + + const data = await fetch(url, { + method: 'POST', + headers: { "Authorization": `Bearer ${token}` } + }).then(res => res.json()); + + return data; +} + +/** + * POST /api/v1/statuses/{post_id}/unfavourite. + * @param {string} host - The domain of the target server. + * @param {string} token - The application token. + * @param {string} post_id - The ID of the post to unfavourite. + */ +export async function unfavouritePost(host, token, post_id) { + let url = `https://${host}/api/v1/statuses/${post_id}/unfavourite`; + + const data = await fetch(url, { + method: 'POST', + headers: { "Authorization": `Bearer ${token}` } + }).then(res => res.json()); + + return data; +} + +/** + * POST /api/v1/statuses/{post_id}/react/{shortcode} + * @param {string} host - The domain of the target server. + * @param {string} token - The application token. + * @param {string} post_id - The ID of the post to favourite. + * @param {string} shortcode - The shortcode of the emote to react with. + */ +export async function reactPost(host, token, post_id, shortcode) { + // note: reacting with foreign emotes is unsupported on most servers + // chuckya appears to allow this, but other servers tested have + // not demonstrated this. + let url = `https://${host}/api/v1/statuses/${post_id}/react/${encodeURIComponent(shortcode)}`; + + const data = await fetch(url, { + method: 'POST', + headers: { "Authorization": `Bearer ${token}` } + }).then(res => res.json()); + + return data; +} + +/** + * POST /api/v1/statuses/{post_id}/unreact/{shortcode} + * @param {string} host - The domain of the target server. + * @param {string} token - The application token. + * @param {string} post_id - The ID of the post to favourite. + * @param {string} shortcode - The shortcode of the reaction emote to remove. + */ +export async function unreactPost(host, token, post_id, shortcode) { + let url = `https://${host}/api/v1/statuses/${post_id}/unreact/${encodeURIComponent(shortcode)}`; + + const data = await fetch(url, { + method: 'POST', + headers: { "Authorization": `Bearer ${token}` } + }).then(res => res.json()); + + return data; +} + +/** + * GET /api/v1/accounts/{user_id} + * @param {string} host - The domain of the target server. + * @param {string} token - The application token. + * @param {string} user_id - The ID of the user to fetch. + */ +export async function getUser(host, token, user_id) { + let url = `https://${host}/api/v1/accounts/${user_id}`; + + const data = await fetch(url, { + method: 'GET', + headers: { "Authorization": token ? `Bearer ${token}` : null } + }).then(res => res.json()); + + return data; +} diff --git a/src/lib/client/api.js b/src/lib/client/api.js deleted file mode 100644 index 09e6514..0000000 --- a/src/lib/client/api.js +++ /dev/null @@ -1,339 +0,0 @@ -import { client } from '$lib/client/client.js'; -import { user } from '$lib/stores/user.js'; -import { capabilities } from '../client/instance.js'; -import Post from '$lib/post.js'; -import User from '$lib/user/user.js'; -import Emoji from '$lib/emoji.js'; -import { get } from 'svelte/store'; - -export async function createApp(host) { - let form = new FormData(); - form.append("client_name", "Campfire"); - form.append("redirect_uris", `${location.origin}/callback`); - form.append("scopes", "read write push"); - form.append("website", "https://campfire.bliss.town"); - - const res = await fetch(`https://${host}/api/v1/apps`, { - method: "POST", - body: form, - }) - .then(res => res.json()) - .catch(error => { - console.error(error); - return false; - }); - - if (!res || !res.client_id) return false; - - return { - id: res.client_id, - secret: res.client_secret, - }; -} - -export function getOAuthUrl() { - 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 form = new FormData(); - 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://${get(client).instance.host}/oauth/token`, { - method: "POST", - body: form, - }) - .then(res => res.json()) - .catch(error => { - console.error(error); - return false; - }); - - if (!res || !res.access_token) return false; - - return res.access_token; -} - -export async function revokeToken() { - let form = new FormData(); - 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://${get(client).instance.host}/oauth/revoke`, { - method: "POST", - body: form, - }) - .catch(error => { - console.error(error); - return false; - }); - - if (!res.ok) return false; - return true; -} - -export async function verifyCredentials() { - let url = `https://${get(client).instance.host}/api/v1/accounts/verify_credentials`; - const data = await fetch(url, { - method: 'GET', - headers: { "Authorization": "Bearer " + get(client).app.token } - }).then(res => res.json()); - - return data; -} - -export async function getNotifications(since_id, limit, types) { - if (!get(user)) return false; - - let url = `https://${get(client).instance.host}/api/v1/notifications`; - - let params = new URLSearchParams(); - if (since_id) params.append("since_id", since_id); - if (limit) params.append("limit", limit); - if (types) params.append("types", types.join(',')); - const params_string = params.toString(); - if (params_string) url += '?' + params_string; - - const data = await fetch(url, { - method: 'GET', - headers: { "Authorization": "Bearer " + get(client).app.token } - }).then(res => res.json()); - - return data; -} - -export async function getTimeline(last_post_id) { - if (!get(user)) 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 " + get(client).app.token } - }).then(res => res.json()); - - return data; -} - -export async function getPost(post_id, ancestor_count) { - let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}`; - const data = await fetch(url, { - method: 'GET', - 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 getPostContext(post_id) { - let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/context`; - const data = await fetch(url, { - method: 'GET', - 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 boostPost(post_id) { - let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/reblog`; - const data = await fetch(url, { - method: 'POST', - 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 unboostPost(post_id) { - let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/unreblog`; - const data = await fetch(url, { - method: 'POST', - 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 favouritePost(post_id) { - let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/favourite`; - const data = await fetch(url, { - method: 'POST', - 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 unfavouritePost(post_id) { - let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/unfavourite`; - const data = await fetch(url, { - method: 'POST', - 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 reactPost(post_id, shortcode) { - // for whatever reason (at least in my testing on iceshrimp) - // using shortcodes for external emoji results in a fallback - // to the default like emote. - // identical api calls on chuckya instances do not display - // this behaviour. - 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 " + get(client).app.token } - }).then(res => { return res.ok ? res.json() : false }); - - if (data === false) return false; - return data; -} - -export async function unreactPost(post_id, 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 " + 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) { - let post = new Post(); - - post.text = data.content; - post.html = data.content; - - post.reply = null; - 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); - // 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 (!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.id = data.id; - post.created_at = new Date(data.created_at); - post.user = await parseUser(data.account); - post.warning = data.spoiler_text; - post.boost_count = data.reblogs_count; - post.reply_count = data.replies_count; - post.favourite_count = data.favourites_count; - post.favourited = data.favourited; - post.boosted = data.reblogged; - post.mentions = data.mentions; - post.files = data.media_attachments; - post.url = data.url; - post.visibility = data.visibility; - - post.emojis = []; - if (data.emojis) { - data.emojis.forEach(emoji_data => { - let name = emoji_data.shortcode.split('@')[0]; - post.emojis.push(parseEmoji({ - id: name + '@' + post.user.host, - name: name, - host: post.user.host, - url: emoji_data.url, - })); - }); - } - - if (data.reactions && get(client).instance.capabilities.includes(capabilities.REACTIONS)) { - post.reactions = parseReactions(data.reactions); - } - return post; -} - -export async function parseUser(data) { - if (!data) { - console.error("Attempted to parse user data but no data was provided"); - return null; - } - let user = await get(client).getCacheUser(data.id); - - if (user) return user; - // cache miss! - - user = new User(); - user.id = data.id; - user.nickname = data.display_name.trim(); - user.username = data.username; - user.avatar_url = data.avatar; - user.url = data.url; - - if (data.acct.includes('@')) - user.host = data.acct.split('@')[1]; - else - user.host = get(client).instance.host; - - user.emojis = []; - data.emojis.forEach(emoji_data => { - emoji_data.id = emoji_data.shortcode + '@' + user.host; - emoji_data.name = emoji_data.shortcode; - emoji_data.host = user.host; - user.emojis.push(parseEmoji(emoji_data)); - }); - - get(client).putCacheUser(user); - return user; -} - -export function parseReactions(data) { - let reactions = []; - data.forEach(reaction_data => { - let reaction = { - count: reaction_data.count, - name: reaction_data.name, - me: reaction_data.me, - }; - if (reaction_data.url) reaction.url = reaction_data.url; - reactions.push(reaction); - }); - return reactions; -} - -export function parseEmoji(data) { - let emoji = new Emoji( - data.id, - data.name, - data.host, - data.url, - ); - get(client).putCacheEmoji(emoji); - return emoji; -} - -export async function getUser(user_id) { - let url = `https://${get(client).instance.host}/api/v1/accounts/${user_id}`; - const data = await fetch(url, { - method: 'GET', - headers: { "Authorization": "Bearer " + get(client).app.token } - }).then(res => res.json()); - - return data; -} diff --git a/src/lib/client/app.js b/src/lib/client/app.js new file mode 100644 index 0000000..dc549a7 --- /dev/null +++ b/src/lib/client/app.js @@ -0,0 +1,34 @@ +import { writable } from 'svelte/store'; +import { app_name } from '$lib/config.js'; +import { browser } from "$app/environment"; + +// if app is falsy, assume user has not begun the login process. +// if app.token is falsy, assume user has not logged in. +export const app = writable(loadApp()); + +// write to localStorage on each update +app.subscribe(app => { + saveApp(app); +}); + +/** + * Saves the provided app to localStorage. + * If `app` is falsy, data is removed from localStorage. + * @param {Object} app + */ +function saveApp(app) { + if (!browser) return; + if (!app) localStorage.removeItem(app_name + "_app"); + localStorage.setItem(app_name + "_app", JSON.stringify(app)); +} + +/** + * Returns application data loaded from localStorage, if it exists. + * Otherwise, returns false. + */ +function loadApp() { + if (!browser) return; + let data = localStorage.getItem(app_name + "_app"); + if (!data) return false; + return JSON.parse(data); +} diff --git a/src/lib/client/client.js b/src/lib/client/client.js deleted file mode 100644 index b1beb6e..0000000 --- a/src/lib/client/client.js +++ /dev/null @@ -1,192 +0,0 @@ -import { Instance, server_types } from './instance.js'; -import * as api from './api.js'; -import { get, writable } from 'svelte/store'; -import { last_read_notif_id } from '$lib/notifications.js'; -import { user, logged_in } from '$lib/stores/user.js'; - -export const client = writable(false); - -const save_name = "campfire"; - -export class Client { - instance; - app; - #cache; - - constructor() { - this.instance = null; - this.app = null; - this.cache = { - users: {}, - emojis: {}, - }; - } - - async init(host) { - if (host.startsWith("https://")) host = host.substring(8); - const url = `https://${host}/api/v1/instance`; - const data = await fetch(url).then(res => res.json()).catch(error => { console.error(error) }); - if (!data) { - console.error(`Failed to connect to ${host}`); - return `Failed to connect to ${host}!`; - } - - this.instance = new Instance(host, data.version); - if (this.instance.type == server_types.UNSUPPORTED) { - console.warn(`Server ${host} is unsupported - ${data.version}`); - if (!confirm( - `This app does not officially support ${host}. ` + - `Things may break, or otherwise not work as epxected! ` + - `Are you sure you wish to continue?` - )) return false; - } else { - console.log(`Server is "${this.instance.type}" (or compatible) with capabilities: [${this.instance.capabilities}].`); - } - - this.app = await api.createApp(host); - - if (!this.app || !this.instance) { - console.error("Failed to create app. Check the network logs for details."); - return false; - } - - this.save(); - - client.set(this); - - return true; - } - - getOAuthUrl() { - return api.getOAuthUrl(this.app.secret); - } - - async getToken(code) { - const token = await api.getToken(code); - if (!token) { - console.error("Failed to obtain access token"); - return false; - } - return token; - } - - async revokeToken() { - return await api.revokeToken(); - } - - async getNotifications(since_id, limit, types) { - return await api.getNotifications(since_id, limit, types); - } - - async getTimeline(last_post_id) { - return await api.getTimeline(last_post_id); - } - - async getPost(post_id, parent_replies, child_replies) { - 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); - } - - async unboostPost(post_id) { - return await api.unboostPost(post_id); - } - - async favouritePost(post_id) { - return await api.favouritePost(post_id); - } - - async unfavouritePost(post_id) { - return await api.unfavouritePost(post_id); - } - - async reactPost(post_id, shortcode) { - return await api.reactPost(post_id, shortcode); - } - - async unreactPost(post_id, shortcode) { - return await api.unreactPost(post_id, shortcode); - } - - putCacheUser(user) { - this.cache.users[user.id] = user; - client.set(this); - } - - async getCacheUser(user_id) { - let user = this.cache.users[user_id]; - if (user) return user; - - return false; - } - - async getUserByMention(mention) { - let users = Object.values(this.cache.users); - for (let i in users) { - const user = users[i]; - if (user.mention == mention) return user; - } - return false; - } - - putCacheEmoji(emoji) { - this.cache.emojis[emoji.id] = emoji; - client.set(this); - } - - getEmoji(emoji_id) { - let emoji = this.cache.emojis[emoji_id]; - if (!emoji) return false; - return emoji; - } - - async getUser(user_id) { - return await api.getUser(user_id); - } - - save() { - if (typeof localStorage === typeof undefined) return; - localStorage.setItem(save_name, JSON.stringify({ - version: APP_VERSION, - instance: { - host: this.instance.host, - version: this.instance.version, - }, - last_read_notif_id: get(last_read_notif_id), - app: this.app, - })); - } - - load() { - if (typeof localStorage === typeof undefined) return; - let json = localStorage.getItem(save_name); - if (!json) return false; - let saved = JSON.parse(json); - if (!saved.version || saved.version !== APP_VERSION) { - localStorage.removeItem(save_name); - return false; - } - this.instance = new Instance(saved.instance.host, saved.instance.version); - last_read_notif_id.set(saved.last_read_notif_id || 0); - this.app = saved.app; - client.set(this); - return true; - } - - async logout() { - if (!this.instance || !this.app) return; - if (!await this.revokeToken()) { - console.warn("Failed to log out correctly; ditching the old tokens anyways."); - } - localStorage.removeItem(save_name); - logged_in.set(false); - client.set(new Client()); - console.log("Logged out successfully."); - } -} diff --git a/src/lib/client/instance.js b/src/lib/client/instance.js deleted file mode 100644 index 92003e8..0000000 --- a/src/lib/client/instance.js +++ /dev/null @@ -1,70 +0,0 @@ -export const server_types = { - UNSUPPORTED: "unsupported", - MASTODON: "mastodon", - GLITCHSOC: "glitchsoc", - CHUCKYA: "chuckya", - FIREFISH: "firefish", - ICESHRIMP: "iceshrimp", - SHARKEY: "sharkey", -}; - -export const capabilities = { - MARKDOWN_CONTENT: "mdcontent", - REACTIONS: "reactions", -}; - -export class Instance { - host; - version; - capabilities; - type = server_types.UNSUPPORTED; - - constructor(host, version) { - this.host = host; - this.version = version; - this.#setType(version); - this.capabilities = this.#getCapabilities(this.type); - } - - #setType(version) { - this.type = server_types.UNSUPPORTED; - if (version.constructor !== String) return; - let version_lower = version.toLowerCase(); - for (let i = 1; i < Object.keys(server_types).length; i++) { - const check_type = Object.values(server_types)[i]; - if (version_lower.includes(check_type)) { - this.type = check_type; - return; - } - } - } - - #getCapabilities(type) { - let c = []; - switch (type) { - case server_types.MASTODON: - break; - case server_types.GLITCHSOC: - c.push(capabilities.REACTIONS); - break; - case server_types.CHUCKYA: - c.push(capabilities.REACTIONS); - break; - case server_types.FIREFISH: - c.push(capabilities.REACTIONS); - break; - case server_types.ICESHRIMP: - // more trouble than it's worth atm - // the server already hands this to us ;p - //c.push(capabilities.MARKDOWN_CONTENT); - c.push(capabilities.REACTIONS); - break; - case server_types.SHARKEY: - c.push(capabilities.REACTIONS); - break; - default: - break; - } - return c; - } -} diff --git a/src/lib/client/server.js b/src/lib/client/server.js new file mode 100644 index 0000000..292ed5d --- /dev/null +++ b/src/lib/client/server.js @@ -0,0 +1,138 @@ +import * as api from '$lib/api.js'; +import { writable } from 'svelte/store'; +import { app_name } from '$lib/config.js'; +import { browser } from "$app/environment"; + +const server_types = { + UNSUPPORTED: "unsupported", + MASTODON: "mastodon", + GLITCHSOC: "glitchsoc", + CHUCKYA: "chuckya", + FIREFISH: "firefish", + ICESHRIMP: "iceshrimp", + SHARKEY: "sharkey", + AKKOMA: "akkoma", // TODO: verify + PLEROMA: "pleroma", // TODO: verify +}; + +export const capabilities = { + MARKDOWN_CONTENT: "mdcontent", + REACTIONS: "reactions", +}; + +// if server is falsy, assume user has not begun the login process. +export let server = writable(loadServer()); + +// write to localStorage on each update +server.subscribe(server => { + saveServer(server); +}); + +/** + * Attempts to create an server object using a given hostname. + * @param {string} host - The domain of the target server. + */ +export async function createServer(host) { + if (!host) { + console.error("Attempted to create server without providing a hostname"); + return false; + } + if (host.startsWith("http://")) { + console.error("Cowardly refusing to connect to an insecure server"); + return false; + } + + let server = {}; + server.host = host; + + if (host.startsWith("https://")) host = host.substring(8); + const data = await api.getInstance(host); + if (!data) { + console.error(`Failed to connect to ${host}`); + return false; + } + + server.version = data.version; + server.type = getType(server.version); + server.capabilities = getCapabilities(server.type); + + if (server.type === server_types.UNSUPPORTED) { + console.warn(`Server ${host} is unsupported (${server.version}). Things may break, or not work as expected`); + } else { + console.log(`Server detected as "${server.type}" (${server.version}) with capabilities: {${server.capabilities.join(', ')}}`); + } + + return server; +} + +/** + * Saves the provided server to localStorage. + * If `server` is falsy, data is removed from localStorage. + * @param {Object} server + */ +function saveServer(server) { + if (!browser) return; + if (!server) localStorage.removeItem(app_name + "_server"); + localStorage.setItem(app_name + "_server", JSON.stringify(server)); +} + +/** + * Returns server data loaded from localStorage, if it exists. + * Otherwise, returns false. + */ +function loadServer() { + if (!browser) return; + let data = localStorage.getItem(app_name + "_server"); + if (!data) return false; + return JSON.parse(data); +} + +/** + * Returns the type of an server, inferred from its version string. + * @param {string} version + * @returns the inferred server_type + */ +function getType(version) { + if (version.constructor !== String) return; + let version_lower = version.toLowerCase(); + for (let i = 1; i < Object.keys(server_types).length; i++) { + const type = Object.values(server_types)[i]; + if (version_lower.includes(type)) { + return type; + } + } + return server_types.UNSUPPORTED; +} + +/** + * Returns a list of capabilities for a given server_type. + * @param {string} type + */ +function getCapabilities(type) { + let c = []; + switch (type) { + case server_types.MASTODON: + break; + case server_types.GLITCHSOC: + c.push(capabilities.REACTIONS); + break; + case server_types.CHUCKYA: + c.push(capabilities.REACTIONS); + break; + case server_types.FIREFISH: + c.push(capabilities.REACTIONS); + break; + case server_types.ICESHRIMP: + // more trouble than it's worth atm + // mastodon API already hands html to us + //c.push(capabilities.MARKDOWN_CONTENT); + c.push(capabilities.REACTIONS); + break; + case server_types.SHARKEY: + c.push(capabilities.REACTIONS); + break; + default: + break; + } + return c; +} diff --git a/src/lib/config.js b/src/lib/config.js new file mode 100644 index 0000000..ccb3cdc --- /dev/null +++ b/src/lib/config.js @@ -0,0 +1 @@ +export const app_name = "campfire"; diff --git a/src/lib/emoji.js b/src/lib/emoji.js index 89df2d1..29385c3 100644 --- a/src/lib/emoji.js +++ b/src/lib/emoji.js @@ -1,52 +1,27 @@ -import { client } from './client/client.js'; import { get } from 'svelte/store'; +export const EMOJI_REGEX = /:[\w\-.]{0,32}:/g; -export const EMOJI_REGEX = /:[\w\-.]{0,32}@[\w\-.]{0,32}:/g; -export const EMOJI_NAME_REGEX = /:[\w\-.]{0,32}:/g; - -export default class Emoji { - name; - url; - - constructor(id, name, host, url) { - this.id = id; - this.name = name; - this.host = host; - this.url = url; - } - - get html() { - if (this.url) - return `${this.name}`; - else - return `${this.name}`; - } +export function parseEmoji(shortcode, url) { + let emoji = { shortcode, url }; + if (emoji.shortcode == '❤') emoji.shortcode = '❤️'; // stupid heart unicode + emoji.html = `${emoji.shortcode}`; + return emoji; } -export function parseText(text, host) { +export function renderEmoji(text, emoji_list) { if (!text) return text; - let index = text.search(EMOJI_NAME_REGEX); + let index = text.search(EMOJI_REGEX); if (index === -1) return text; - // find the emoji name + // find the closing comma 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).getEmoji(emoji_name + '@' + host); - if (emoji) { - return text.substring(0, index) + emoji.html + - parseText(text.substring(index + length + 2), host); - } - return text.substring(0, index + length + 1) + - parseText(text.substring(index + length + 1), host); -} + // see if emoji is valid + let shortcode = text.substring(index + 1, index + length + 1); + let emoji = emoji_list[shortcode]; + let replace = emoji ? emoji.html : shortcode; -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).getEmoji(emoji_id); - if (!cached_emoji) return emoji_id; - return cached_emoji.html; + return text.substring(0, index) + replace + renderEmoji(text.substring(index + length + 2), emoji_list); } diff --git a/src/lib/notifications.js b/src/lib/notifications.js index bbdc69f..cb7945f 100644 --- a/src/lib/notifications.js +++ b/src/lib/notifications.js @@ -1,40 +1,88 @@ -import { client } from '$lib/client/client.js'; -import * as api from '$lib/client/api.js'; +import * as api from '$lib/api.js'; +import { server } from '$lib/client/server.js'; +import { app } from '$lib/client/app.js'; +import { app_name } from '$lib/config.js'; import { get, writable } from 'svelte/store'; +import { browser } from '$app/environment'; +import { parsePost } from '$lib/post.js'; +import { parseAccount } from '$lib/account.js'; -export let notifications = writable([]); -export let unread_notif_count = writable(0); -export let last_read_notif_id = writable(0); +const prefix = app_name + '_notif_'; + +export const notifications = writable([]); +export const unread_notif_count = writable(load("unread_count")); +export const last_read_notif_id = writable(load("last_read")); + +unread_notif_count.subscribe(count => save("unread_count", count)); +last_read_notif_id.subscribe(id => save("last_read", id)); + +/** + * Saves the provided data to localStorage. + * If `data` is falsy, the record is removed from localStorage. + * @param {Object} name + * @param {any} data + */ +function save(name, data) { + if (!browser) return; + if (data) { + localStorage.setItem(prefix + name, data); + } else { + localStorage.removeItem(prefix + name); + } +} + +/** + * Returns named data loaded from localStorage, if it exists. + * Otherwise, returns false. + */ +function load(name) { + if (!browser) return; + let data = localStorage.getItem(prefix + name); + return data ? data : false; +} let loading; -export async function getNotifications() { +export async function getNotifications(clean) { if (loading) return; // no spamming!! loading = true; - api.getNotifications().then(async data => { - if (!data || data.length <= 0) return; - notifications.set([]); - for (let i in data) { - let notif = data[i]; - notif.accounts = [ await api.parseUser(notif.account) ]; - if (get(notifications).length > 0) { - let prev = get(notifications)[get(notifications).length - 1]; - if (notif.type === prev.type) { - if (prev.status && notif.status && prev.status.id === notif.status.id) { - notifications.update(notifications => { - notifications[notifications.length - 1].accounts.push(notif.accounts[0]); - return notifications; - }); - continue; - } + let last_id = false; + if (!clean && get(notifications).length > 0) + last_id = get(notifications)[get(notifications).length - 1].id; + + const notif_data = await api.getNotifications( + get(server).host, + get(app).token, + last_id + ); + + if (!notif_data) { + console.error(`Failed to retrieve notifications.`); + loading = false; + return; + } + + if (clean) notifications.set([]); + + for (let i in notif_data) { + let notif = notif_data[i]; + notif.accounts = [ await parseAccount(notif.account) ]; + if (get(notifications).length > 0) { + let prev = get(notifications)[get(notifications).length - 1]; + if (notif.type === prev.type) { + if (prev.status && notif.status && prev.status.id === notif.status.id) { + notifications.update(notifications => { + notifications[notifications.length - 1].accounts.push(notif.accounts[0]); + return notifications; + }); + continue; } } - notif.status = notif.status ? await api.parsePost(notif.status, 0, false) : null; - notifications.update(notifications => [...notifications, notif]); } - last_read_notif_id.set(data[0].id); - unread_notif_count.set(0); - get(client).save(); - loading = false; - }); + notif.status = notif.status ? await parsePost(notif.status, 0, false) : null; + notifications.update(notifications => [...notifications, notif]); + } + if (!last_id) last_read_notif_id.set(notif_data[0].id); + if (!last_id) unread_notif_count.set(0); + loading = false; } diff --git a/src/lib/post.js b/src/lib/post.js index 9b6d10f..e8700f5 100644 --- a/src/lib/post.js +++ b/src/lib/post.js @@ -1,177 +1,82 @@ -import { parseText as parseEmoji } from './emoji.js'; +import * as api from '$lib/api.js'; +import { server } from '$lib/client/server.js'; +import { app } from '$lib/client/app.js'; +import { parseAccount } from '$lib/account.js'; +import { parseEmoji, renderEmoji } from '$lib/emoji.js'; +import { get, writable } from 'svelte/store'; -export default class Post { - id; - created_at; - user; - text; - warning; - boost_count; - reply_count; - favourite_count; - favourited; - boosted; - mentions; - reactions; - emojis; - files; - url; - reply; - reply_id; - replies; - boost; - visibility; +const cache = writable({}); - async rich_text() { - return parseEmoji(this.text, this.user.host); +/** + * Parses a post using API data, and returns a writable store object. + * @param {Object} data + * @param {number} ancestor_count + */ +export async function parsePost(data, ancestor_count) { + let post = {}; + if (!ancestor_count) ancestor_count = 0; + + post.html = data.content; + + post.reply = null; + if ((data.in_reply_to_id || data.reply) && ancestor_count !== 0) { + const reply_data = data.reply || await api.getPost(get(server).host, get(app).token, data.in_reply_to_id); + // 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 (!reply_data) return false; + post.reply = await parsePost(reply_data, ancestor_count - 1, false); } - /* - async rich_text() { - let text = this.text; - if (!text) return text; - let client = Client.get(); + post.boost = data.reblog ? await parsePost(data.reblog, 1, false) : null; - const markdown_tokens = [ - { tag: "pre", token: "```" }, - { tag: "code", token: "`" }, - { tag: "strong", token: "**" }, - { tag: "strong", token: "__" }, - { tag: "em", token: "*" }, - { tag: "em", token: "_" }, - ]; + post.id = data.id; + post.created_at = new Date(data.created_at); + post.account = await parseAccount(data.account); + post.warning = data.spoiler_text; + post.reply_count = data.replies_count; + post.boost_count = data.reblogs_count; + post.boosted = data.reblogged; + post.favourite_count = data.favourites_count; + post.favourited = data.favourited; + post.mentions = data.mentions; + post.media = data.media_attachments; + post.url = data.url; + post.visibility = data.visibility; - let response = ""; - let md_layer; - let index = 0; - while (index < text.length) { - let sample = text.substring(index); - let md_nostack = !(md_layer && md_layer.nostack); + post.emojis = []; + data.emojis.forEach(emoji => { + post.emojis[emoji.shortcode] = parseEmoji(emoji.shortcode, emoji.url); + }); - // handle newlines - if (md_nostack && sample.startsWith('\n')) { - response += "
"; - index++; - continue; - } + if (data.reactions) post.reactions = parseReactions(data.reactions); - // handle mentions - if (client.instance.capabilities.includes(capabilities.MARKDOWN_CONTENT) - && md_nostack - && sample.match(/^@[\w\-.]+@[\w\-.]+/g) - ) { - // find end of the mention - let length = 1; - while (index + length < text.length && /[a-z0-9-_.]/.test(text[index + length])) length++; - length++; // skim the middle @ - while (index + length < text.length && /[a-z0-9-_.]/.test(text[index + length])) length++; + post.rich_text = renderEmoji(post.html, post.emojis); - let mention = text.substring(index, index + length); + return post; - // attempt to resolve mention to a user - let user = await client.getUserByMention(mention); - if (user) { - const out = `` + - `` + - '@' + user.username + '@' + user.host + ""; - if (md_layer) md_layer.text += out; - else response += out; - } else { - response += mention; - } - index += mention.length; - continue; - } + // let cache_post = get(cache)[post.id]; + // if (cache_post) { + // cache_post.set(post); + // } else { + // cache.update(cache => { + // cache[post.id] = writable(post); + // return cache; + // }); + // } - // handle links - if (client.instance.capabilities.includes(capabilities.MARKDOWN_CONTENT) - && md_nostack - && sample.match(/^[a-z]{3,6}:\/\/[^\s]+/g) - ) { - // get length of link - let length = text.substring(index).search(/\s|$/g); - let url = text.substring(index, index + length); - let out = `${url}`; - if (md_layer) md_layer.text += out; - else response += out; - index += length; - continue; - } - - // handle emojis - if (md_nostack && sample.match(/^:[\w\-.]{0,32}:/g)) { - // find the emoji name - let length = text.substring(index + 1).search(':'); - if (length <= 0) return text; - let emoji_name = text.substring(index + 1, index + length + 1); - let emoji = client.getEmoji(emoji_name + '@' + this.user.host); - - index += length + 2; - - if (!emoji) { - let out = ':' + emoji_name + ':'; - if (md_layer) md_layer.text += out; - else response += out; - continue; - } - - let out = emoji.html; - if (md_layer) md_layer.text += out; - else response += out; - continue; - } - - // handle markdown - // TODO: handle misskey-flavoured markdown(?) - if (md_layer) { - // try to pop layer - if (sample.startsWith(md_layer.token)) { - index += md_layer.token.length; - let out = `<${md_layer.tag}>${md_layer.text}`; - if (md_layer.token === '```') - out = `
${md_layer.text}
`; - if (md_layer.parent) md_layer.parent.text += out; - else response += out; - md_layer = md_layer.parent; - } else { - md_layer.text += sample[0]; - index++; - } - } else if (md_nostack) { - // should we add a layer? - let pushed = false; - for (let i = 0; i < markdown_tokens.length; i++) { - let item = markdown_tokens[i]; - if (sample.startsWith(item.token)) { - let new_md_layer = { - token: item.token, - tag: item.tag, - text: "", - parent: md_layer, - }; - if (item.token === '```' || item.token === '`') new_md_layer.nostack = true; - md_layer = new_md_layer; - pushed = true; - index += md_layer.token.length; - break; - } - } - if (!pushed) { - response += sample[0]; - index++; - } - } - } - - // destroy the remaining stack - while (md_layer) { - let out = md_layer.token + md_layer.text; - if (md_layer.parent) md_layer.parent.text += out; - else response += out; - md_layer = md_layer.parent; - } - - return response; - } - */ + // return get(cache)[post.id]; +} + +export function parseReactions(data) { + let reactions = []; + data.forEach(reaction_data => { + let reaction = { + count: reaction_data.count, + name: reaction_data.name, + me: reaction_data.me, + }; + if (reaction_data.url) reaction.url = reaction_data.url; + reactions.push(reaction); + }); + return reactions; } diff --git a/src/lib/stores/account.js b/src/lib/stores/account.js new file mode 100644 index 0000000..8361624 --- /dev/null +++ b/src/lib/stores/account.js @@ -0,0 +1,4 @@ +import { writable } from 'svelte/store'; + +export let account = writable(false); +export let logged_in = writable(false); diff --git a/src/lib/stores/user.js b/src/lib/stores/user.js deleted file mode 100644 index fb9c2c4..0000000 --- a/src/lib/stores/user.js +++ /dev/null @@ -1,22 +0,0 @@ -import { client } from '$lib/client/client.js'; -import * as api from '$lib/client/api.js'; -import { get, writable } from 'svelte/store'; - -export let user = writable(0); -export let logged_in = writable(false); - -export async function getUser() { - // already known - if (get(user)) return get(user); - - // cannot provide- not logged in - if (!get(client).app || !get(client).app.token) return false; - - // logged in- attempt to retrieve using token - const data = await api.verifyCredentials(); - if (!data) return false; - - user.set(await api.parseUser(data)); - console.log(`Logged in as @${get(user).username}@${get(user).host}`); - return get(user); -} diff --git a/src/lib/timeline.js b/src/lib/timeline.js index 0ef7b8f..ae8a5e3 100644 --- a/src/lib/timeline.js +++ b/src/lib/timeline.js @@ -1,8 +1,10 @@ -import { client } from '$lib/client/client.js'; +import * as api from '$lib/api.js'; +import { server } from '$lib/client/server.js'; +import { app } from '$lib/client/app.js'; import { get, writable } from 'svelte/store'; -import { parsePost } from '$lib/client/api.js'; +import { parsePost } from '$lib/post.js'; -export let timeline = writable([]); +export const timeline = writable([]); let loading = false; @@ -10,9 +12,16 @@ export async function getTimeline(clean) { if (loading) return; // no spamming!! loading = true; - let timeline_data; - if (clean || get(timeline).length === 0) timeline_data = await get(client).getTimeline() - else timeline_data = await get(client).getTimeline(get(timeline)[get(timeline).length - 1].id); + let last_post = false; + if (!clean && get(timeline).length > 0) + last_post = get(timeline)[get(timeline).length - 1].id; + + const timeline_data = await api.getTimeline( + get(server).host, + get(app).token, + "home", + last_post + ); if (!timeline_data) { console.error(`Failed to retrieve timeline.`); @@ -24,7 +33,7 @@ export async function getTimeline(clean) { for (let i in timeline_data) { const post_data = timeline_data[i]; - const post = await parsePost(post_data, 1, false); + const post = await parsePost(post_data, 1); if (!post) { if (post === null || post === undefined) { if (post_data.id) { diff --git a/src/lib/ui/LoginForm.svelte b/src/lib/ui/LoginForm.svelte index 8cf9b07..13cfc99 100644 --- a/src/lib/ui/LoginForm.svelte +++ b/src/lib/ui/LoginForm.svelte @@ -1,37 +1,42 @@ @@ -40,11 +45,11 @@

Welcome, fediverse user!

-

Please enter your instance domain to log in.

+

Please enter your server domain to log in.

- - {#if instance_url_error} -

{instance_url_error}

+ + {#if display_error} +

{display_error}

{/if}

diff --git a/src/lib/ui/Navigation.svelte b/src/lib/ui/Navigation.svelte index 26cbed5..6d514ef 100644 --- a/src/lib/ui/Navigation.svelte +++ b/src/lib/ui/Navigation.svelte @@ -1,18 +1,17 @@
- play_sound()}> + play_sound()}>
@@ -183,7 +182,7 @@ background-color: var(--bg-800); } - .instance-header { + .server-header { width: 100%; height: 172px; display: flex; @@ -196,7 +195,7 @@ background-image: linear-gradient(to top, var(--bg-800), var(--bg-600)); } - .instance-icon { + .server-icon { height: 50%; border-radius: 8px; } diff --git a/src/lib/ui/Notification.svelte b/src/lib/ui/Notification.svelte index f16abec..ecf5b26 100644 --- a/src/lib/ui/Notification.svelte +++ b/src/lib/ui/Notification.svelte @@ -1,5 +1,5 @@ diff --git a/src/lib/ui/post/Body.svelte b/src/lib/ui/post/Body.svelte index 6bc85f7..7b06bc6 100644 --- a/src/lib/ui/post/Body.svelte +++ b/src/lib/ui/post/Body.svelte @@ -1,8 +1,6 @@ @@ -22,22 +20,22 @@ {/if} {#if !post.warning || open_warned} - {#if post.text} - {@html rich_text} + {#if post.html} + {@html post.html} {/if} - {#if post.files && post.files.length > 0} -
- {#each post.files as file} -
- {#if ["image", "gifv", "gif"].includes(file.type)} - - {file.description} + {#if post.media && post.media.length > 0} +
+ {#each post.media as media} +
+ {#if ["image", "gifv", "gif"].includes(media.type)} + + {media.description} - {:else if file.type === "video"} + {:else if media.type === "video"} {/if}
diff --git a/src/lib/ui/post/BoostContext.svelte b/src/lib/ui/post/BoostContext.svelte index 2194ef2..8e2a97d 100644 --- a/src/lib/ui/post/BoostContext.svelte +++ b/src/lib/ui/post/BoostContext.svelte @@ -1,17 +1,16 @@
🔁 - - {@html parseEmojis(post.user.rich_name)} + + {@html post.account.rich_name} boosted this post. diff --git a/src/lib/ui/post/Post.svelte b/src/lib/ui/post/Post.svelte index 15f01aa..c3f430a 100644 --- a/src/lib/ui/post/Post.svelte +++ b/src/lib/ui/post/Post.svelte @@ -1,5 +1,4 @@
diff --git a/src/lib/ui/post/PostHeader.svelte b/src/lib/ui/post/PostHeader.svelte index 93b0949..3bb59b0 100644 --- a/src/lib/ui/post/PostHeader.svelte +++ b/src/lib/ui/post/PostHeader.svelte @@ -1,6 +1,5 @@
- - + +
- {#await ready} + {#await init()}
just a moment...
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 05c2764..c13c9bd 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,13 +1,11 @@
diff --git a/src/routes/post/[id]/+page.svelte b/src/routes/post/[id]/+page.svelte index e2e27aa..50593e0 100644 --- a/src/routes/post/[id]/+page.svelte +++ b/src/routes/post/[id]/+page.svelte @@ -1,7 +1,9 @@