diff --git a/CHANGELOGS.md b/CHANGELOGS.md
index d4daa7b..4c81497 100644
--- a/CHANGELOGS.md
+++ b/CHANGELOGS.md
@@ -1,8 +1,3 @@
-# Campfire v0.4.0
-- Infinite scrolling notifications
-- Fixed custom emotes in the sidebar profile display
-- Huge refactor, along with some improved documentation
-
# Campfire v0.3.0
- Added notifications view
- Many more background tweaks, fixes, and optimisations
diff --git a/src/lib/account.js b/src/lib/account.js
deleted file mode 100644
index 7bfd696..0000000
--- a/src/lib/account.js
+++ /dev/null
@@ -1,52 +0,0 @@
-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
deleted file mode 100644
index 8006213..0000000
--- a/src/lib/api.js
+++ /dev/null
@@ -1,339 +0,0 @@
-/**
- * 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
new file mode 100644
index 0000000..09e6514
--- /dev/null
+++ b/src/lib/client/api.js
@@ -0,0 +1,339 @@
+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
deleted file mode 100644
index dc549a7..0000000
--- a/src/lib/client/app.js
+++ /dev/null
@@ -1,34 +0,0 @@
-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
new file mode 100644
index 0000000..b1beb6e
--- /dev/null
+++ b/src/lib/client/client.js
@@ -0,0 +1,192 @@
+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
new file mode 100644
index 0000000..92003e8
--- /dev/null
+++ b/src/lib/client/instance.js
@@ -0,0 +1,70 @@
+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
deleted file mode 100644
index 292ed5d..0000000
--- a/src/lib/client/server.js
+++ /dev/null
@@ -1,138 +0,0 @@
-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
deleted file mode 100644
index ccb3cdc..0000000
--- a/src/lib/config.js
+++ /dev/null
@@ -1 +0,0 @@
-export const app_name = "campfire";
diff --git a/src/lib/emoji.js b/src/lib/emoji.js
index 29385c3..89df2d1 100644
--- a/src/lib/emoji.js
+++ b/src/lib/emoji.js
@@ -1,27 +1,52 @@
+import { client } from './client/client.js';
import { get } from 'svelte/store';
-export const EMOJI_REGEX = /:[\w\-.]{0,32}:/g;
-export function parseEmoji(shortcode, url) {
- let emoji = { shortcode, url };
- if (emoji.shortcode == '❤') emoji.shortcode = '❤️'; // stupid heart unicode
- emoji.html = ``;
- return emoji;
+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 renderEmoji(text, emoji_list) {
+export function parseText(text, host) {
if (!text) return text;
- let index = text.search(EMOJI_REGEX);
+ let index = text.search(EMOJI_NAME_REGEX);
if (index === -1) return text;
- // find the closing comma
+ // 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 = get(client).getEmoji(emoji_name + '@' + 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;
-
- return text.substring(0, index) + replace + renderEmoji(text.substring(index + length + 2), emoji_list);
+ 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);
+}
+
+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;
}
diff --git a/src/lib/notifications.js b/src/lib/notifications.js
index cb7945f..bbdc69f 100644
--- a/src/lib/notifications.js
+++ b/src/lib/notifications.js
@@ -1,88 +1,40 @@
-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 { client } from '$lib/client/client.js';
+import * as api from '$lib/client/api.js';
import { get, writable } from 'svelte/store';
-import { browser } from '$app/environment';
-import { parsePost } from '$lib/post.js';
-import { parseAccount } from '$lib/account.js';
-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;
-}
+export let notifications = writable([]);
+export let unread_notif_count = writable(0);
+export let last_read_notif_id = writable(0);
let loading;
-export async function getNotifications(clean) {
+export async function getNotifications() {
if (loading) return; // no spamming!!
loading = true;
- 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;
+ 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;
+ }
}
}
+ notif.status = notif.status ? await api.parsePost(notif.status, 0, false) : null;
+ notifications.update(notifications => [...notifications, notif]);
}
- 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;
+ last_read_notif_id.set(data[0].id);
+ unread_notif_count.set(0);
+ get(client).save();
+ loading = false;
+ });
}
diff --git a/src/lib/post.js b/src/lib/post.js
index e8700f5..9b6d10f 100644
--- a/src/lib/post.js
+++ b/src/lib/post.js
@@ -1,82 +1,177 @@
-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';
+import { parseText as parseEmoji } from './emoji.js';
-const cache = writable({});
+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;
-/**
- * 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() {
+ return parseEmoji(this.text, this.user.host);
}
- post.boost = data.reblog ? await parsePost(data.reblog, 1, false) : null;
+ /*
+ async rich_text() {
+ let text = this.text;
+ if (!text) return text;
+ let client = Client.get();
- 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;
+ const markdown_tokens = [
+ { tag: "pre", token: "```" },
+ { tag: "code", token: "`" },
+ { tag: "strong", token: "**" },
+ { tag: "strong", token: "__" },
+ { tag: "em", token: "*" },
+ { tag: "em", token: "_" },
+ ];
- post.emojis = [];
- data.emojis.forEach(emoji => {
- post.emojis[emoji.shortcode] = parseEmoji(emoji.shortcode, emoji.url);
- });
+ 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);
- if (data.reactions) post.reactions = parseReactions(data.reactions);
+ // handle newlines
+ if (md_nostack && sample.startsWith('\n')) {
+ response += "
";
+ index++;
+ continue;
+ }
- post.rich_text = renderEmoji(post.html, post.emojis);
+ // 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++;
- return post;
+ let mention = text.substring(index, index + length);
- // 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;
- // });
- // }
+ // 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;
+ }
- // 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;
+ // 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;
+ }
+ */
}
diff --git a/src/lib/stores/account.js b/src/lib/stores/account.js
deleted file mode 100644
index 8361624..0000000
--- a/src/lib/stores/account.js
+++ /dev/null
@@ -1,4 +0,0 @@
-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
new file mode 100644
index 0000000..fb9c2c4
--- /dev/null
+++ b/src/lib/stores/user.js
@@ -0,0 +1,22 @@
+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 ae8a5e3..0ef7b8f 100644
--- a/src/lib/timeline.js
+++ b/src/lib/timeline.js
@@ -1,10 +1,8 @@
-import * as api from '$lib/api.js';
-import { server } from '$lib/client/server.js';
-import { app } from '$lib/client/app.js';
+import { client } from '$lib/client/client.js';
import { get, writable } from 'svelte/store';
-import { parsePost } from '$lib/post.js';
+import { parsePost } from '$lib/client/api.js';
-export const timeline = writable([]);
+export let timeline = writable([]);
let loading = false;
@@ -12,16 +10,9 @@ export async function getTimeline(clean) {
if (loading) return; // no spamming!!
loading = true;
- 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
- );
+ 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);
if (!timeline_data) {
console.error(`Failed to retrieve timeline.`);
@@ -33,7 +24,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);
+ const post = await parsePost(post_data, 1, false);
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 13cfc99..8cf9b07 100644
--- a/src/lib/ui/LoginForm.svelte
+++ b/src/lib/ui/LoginForm.svelte
@@ -1,42 +1,37 @@
@@ -45,11 +40,11 @@
${md_layer.text}
Welcome, fediverse user!
-Please enter your server domain to log in.
+Please enter your instance domain to log in.
{display_error}
+ + {#if instance_url_error} +{instance_url_error}
{/if}