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 ``;
- else
- return `${this.name}`;
- }
+export function parseEmoji(shortcode, url) {
+ let emoji = { shortcode, url };
+ if (emoji.shortcode == '❤') emoji.shortcode = '❤️'; // stupid heart unicode
+ emoji.html = ``;
+ 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}${md_layer.tag}>`;
- if (md_layer.token === '```')
- out = `
`;
- 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 @@
${md_layer.text}
Welcome, fediverse user!
-Please enter your instance domain to log in.
+Please enter your server domain to log in.
{instance_url_error}
+ + {#if display_error} +{display_error}
{/if}