diff --git a/src/lib/client/api.js b/src/lib/client/api.js index c02517a..27f0e0b 100644 --- a/src/lib/client/api.js +++ b/src/lib/client/api.js @@ -1,4 +1,4 @@ -import { client } from '../client/client.js'; +import { Client } from '../client/client.js'; import { capabilities } from '../client/instance.js'; import Post from '../post.js'; import User from '../user/user.js'; @@ -31,23 +31,25 @@ export async function createApp(host) { } export function getOAuthUrl() { - return `https://${get(client).instance.host}/oauth/authorize` + - `?client_id=${get(client).app.id}` + + let client = get(Client.get()); + return `https://${client.instance.host}/oauth/authorize` + + `?client_id=${client.app.id}` + "&scope=read+write+push" + `&redirect_uri=${location.origin}/callback` + "&response_type=code"; } export async function getToken(code) { + let client = get(Client.get()); let form = new FormData(); - form.append("client_id", get(client).app.id); - form.append("client_secret", get(client).app.secret); + form.append("client_id", client.app.id); + form.append("client_secret", 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`, { + const res = await fetch(`https://${client.instance.host}/oauth/token`, { method: "POST", body: form, }) @@ -63,12 +65,13 @@ export async function getToken(code) { } export async function revokeToken() { + let client = get(Client.get()); 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); + form.append("client_id", client.app.id); + form.append("client_secret", client.app.secret); + form.append("token", client.app.token); - const res = await fetch(`https://${get(client).instance.host}/oauth/revoke`, { + const res = await fetch(`https://${client.instance.host}/oauth/revoke`, { method: "POST", body: form, }) @@ -82,32 +85,34 @@ export async function revokeToken() { } export async function verifyCredentials() { - let url = `https://${get(client).instance.host}/api/v1/accounts/verify_credentials`; + let client = get(Client.get()); + let url = `https://${client.instance.host}/api/v1/accounts/verify_credentials`; const data = await fetch(url, { method: 'GET', - headers: { "Authorization": "Bearer " + get(client).app.token } + headers: { "Authorization": "Bearer " + client.app.token } }).then(res => res.json()); return data; } export async function getTimeline(last_post_id) { - if (!get(client).instance || !get(client).app) return false; - let url = `https://${get(client).instance.host}/api/v1/timelines/home`; + let client = get(Client.get()); + let url = `https://${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 } + headers: { "Authorization": "Bearer " + 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}`; + let client = get(Client.get()); + let url = `https://${client.instance.host}/api/v1/statuses/${post_id}`; const data = await fetch(url, { method: 'GET', - headers: { "Authorization": "Bearer " + get(client).app.token } + headers: { "Authorization": "Bearer " + client.app.token } }).then(res => { return res.ok ? res.json() : false }); if (data === false) return false; @@ -115,10 +120,11 @@ export async function getPost(post_id, ancestor_count) { } export async function getPostContext(post_id) { - let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/context`; + let client = get(Client.get()); + let url = `https://${client.instance.host}/api/v1/statuses/${post_id}/context`; const data = await fetch(url, { method: 'GET', - headers: { "Authorization": "Bearer " + get(client).app.token } + headers: { "Authorization": "Bearer " + client.app.token } }).then(res => { return res.ok ? res.json() : false }); if (data === false) return false; @@ -126,10 +132,11 @@ export async function getPostContext(post_id) { } export async function boostPost(post_id) { - let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/reblog`; + let client = get(Client.get()); + let url = `https://${client.instance.host}/api/v1/statuses/${post_id}/reblog`; const data = await fetch(url, { method: 'POST', - headers: { "Authorization": "Bearer " + get(client).app.token } + headers: { "Authorization": "Bearer " + client.app.token } }).then(res => { return res.ok ? res.json() : false }); if (data === false) return false; @@ -137,10 +144,11 @@ export async function boostPost(post_id) { } export async function unboostPost(post_id) { - let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/unreblog`; + let client = get(Client.get()); + let url = `https://${client.instance.host}/api/v1/statuses/${post_id}/unreblog`; const data = await fetch(url, { method: 'POST', - headers: { "Authorization": "Bearer " + get(client).app.token } + headers: { "Authorization": "Bearer " + client.app.token } }).then(res => { return res.ok ? res.json() : false }); if (data === false) return false; @@ -148,10 +156,11 @@ export async function unboostPost(post_id) { } export async function favouritePost(post_id) { - let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/favourite`; + let client = get(Client.get()); + let url = `https://${client.instance.host}/api/v1/statuses/${post_id}/favourite`; const data = await fetch(url, { method: 'POST', - headers: { "Authorization": "Bearer " + get(client).app.token } + headers: { "Authorization": "Bearer " + client.app.token } }).then(res => { return res.ok ? res.json() : false }); if (data === false) return false; @@ -159,10 +168,11 @@ export async function favouritePost(post_id) { } export async function unfavouritePost(post_id) { - let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/unfavourite`; + let client = get(Client.get()); + let url = `https://${client.instance.host}/api/v1/statuses/${post_id}/unfavourite`; const data = await fetch(url, { method: 'POST', - headers: { "Authorization": "Bearer " + get(client).app.token } + headers: { "Authorization": "Bearer " + client.app.token } }).then(res => { return res.ok ? res.json() : false }); if (data === false) return false; @@ -175,10 +185,11 @@ export async function reactPost(post_id, shortcode) { // 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)}`; + let client = get(Client.get()); + let url = `https://${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 } + headers: { "Authorization": "Bearer " + client.app.token } }).then(res => { return res.ok ? res.json() : false }); if (data === false) return false; @@ -186,23 +197,26 @@ export async function reactPost(post_id, shortcode) { } export async function unreactPost(post_id, shortcode) { - let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/unreact/${encodeURIComponent(shortcode)}`; + let client = get(Client.get()); + let url = `https://${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 } + headers: { "Authorization": "Bearer " + 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) { +export async function parsePost(data, ancestor_count, with_context) { + let client = get(Client.get()); let post = new Post(); post.text = data.content; post.reply = null; - if ((data.in_reply_to_id || data.reply) && + if (!with_context && // ancestor replies are handled in full later + (data.in_reply_to_id || data.reply) && ancestor_count !== 0 ) { const reply_data = data.reply || await getPost(data.in_reply_to_id, ancestor_count - 1); @@ -211,9 +225,29 @@ export async function parsePost(data, ancestor_count) { if (!reply_data) return false; post.reply = await parsePost(reply_data, ancestor_count - 1, false); } - post.boost = data.reblog ? await parsePost(data.reblog, 1, false) : null; + post.replies = []; + if (with_context) { + const replies_data = await getPostContext(data.id); + if (replies_data) { + // posts this is replying to + if (replies_data.ancestors) { + let head = post; + while (replies_data.ancestors.length > 0) { + head.reply = await parsePost(replies_data.ancestors.pop(), 0, false); + head = head.reply; + } + } + // posts in reply to this + if (replies_data.descendants) { + for (let i in replies_data.descendants) { + post.replies.push(await parsePost(replies_data.descendants[i], 0, false)); + } + } + } + } + post.id = data.id; post.created_at = new Date(data.created_at); post.user = await parseUser(data.account); @@ -241,7 +275,7 @@ export async function parsePost(data, ancestor_count) { }); } - if (data.reactions && get(client).instance.capabilities.includes(capabilities.REACTIONS)) { + if (data.reactions && client.instance.capabilities.includes(capabilities.REACTIONS)) { post.reactions = parseReactions(data.reactions); } return post; @@ -252,7 +286,8 @@ export async function parseUser(data) { console.error("Attempted to parse user data but no data was provided"); return null; } - let user = await get(client).getCacheUser(data.id); + let client = get(Client.get()); + let user = await client.getCacheUser(data.id); if (user) return user; // cache miss! @@ -267,7 +302,7 @@ export async function parseUser(data) { if (data.acct.includes('@')) user.host = data.acct.split('@')[1]; else - user.host = get(client).instance.host; + user.host = client.instance.host; user.emojis = []; data.emojis.forEach(emoji_data => { @@ -277,11 +312,12 @@ export async function parseUser(data) { user.emojis.push(parseEmoji(emoji_data)); }); - get(client).putCacheUser(user); + client.putCacheUser(user); return user; } export function parseReactions(data) { + let client = get(Client.get()); let reactions = []; data.forEach(reaction_data => { let reaction = { @@ -302,15 +338,16 @@ export function parseEmoji(data) { data.host, data.url, ); - get(client).putCacheEmoji(emoji); + get(Client.get()).putCacheEmoji(emoji); return emoji; } export async function getUser(user_id) { - let url = `https://${get(client).instance.host}/api/v1/accounts/${user_id}`; + let client = get(Client.get()); + let url = `https://${client.instance.host}/api/v1/accounts/${user_id}`; const data = await fetch(url, { method: 'GET', - headers: { "Authorization": "Bearer " + get(client).app.token } + headers: { "Authorization": "Bearer " + client.app.token } }).then(res => res.json()); const user = await parseUser(data); diff --git a/src/lib/client/client.js b/src/lib/client/client.js index 6e96f19..f4e3788 100644 --- a/src/lib/client/client.js +++ b/src/lib/client/client.js @@ -2,7 +2,7 @@ import { Instance, server_types } from './instance.js'; import * as api from './api.js'; import { get, writable } from 'svelte/store'; -export const client = writable(false); +let client = writable(false); const save_name = "campfire"; @@ -22,6 +22,15 @@ export class Client { }; } + static get() { + let current = get(client); + if (current && current.app) return client; + let new_client = new Client(); + new_client.load(); + client.set(new_client); + return client; + } + async init(host) { if (host.startsWith("https://")) host = host.substring(8); const url = `https://${host}/api/v1/instance`; @@ -67,30 +76,30 @@ export class Client { console.error("Failed to obtain access token"); return false; } - return token; + this.app.token = token; + client.set(this); } async revokeToken() { return await api.revokeToken(); } - async getUser() { - // already known + async verifyCredentials() { if (this.user) return this.user; - - // cannot provide- not logged in if (!this.app || !this.app.token) { + this.user = false; return false; } - - // logged in- attempt to retrieve using token const data = await api.verifyCredentials(); if (!data) { + this.user = false; return false; } - const user = await api.parseUser(data); - console.log(`Logged in as @${user.username}@${user.host}`); - return user; + await client.update(async c => { + c.user = await api.parseUser(data); + console.log(`Logged in as @${c.user.username}@${c.user.host}`); + }); + return this.user; } async getTimeline(last_post_id) { @@ -101,10 +110,6 @@ export class Client { return await api.getPost(post_id, parent_replies, child_replies); } - async getPostContext(post_id) { - return await api.getPostContext(post_id); - } - async boostPost(post_id) { return await api.boostPost(post_id); } @@ -194,7 +199,7 @@ export class Client { console.warn("Failed to log out correctly; ditching the old tokens anyways."); } localStorage.removeItem(save_name); - client.set(new Client()); + client.set(false); console.log("Logged out successfully."); } } diff --git a/src/lib/emoji.js b/src/lib/emoji.js index 89df2d1..4fdd161 100644 --- a/src/lib/emoji.js +++ b/src/lib/emoji.js @@ -1,4 +1,4 @@ -import { client } from './client/client.js'; +import { Client } from './client/client.js'; import { get } from 'svelte/store'; export const EMOJI_REGEX = /:[\w\-.]{0,32}@[\w\-.]{0,32}:/g; @@ -33,7 +33,7 @@ export function parseText(text, host) { let length = text.substring(index + 1).search(':'); if (length <= 0) return text; let emoji_name = text.substring(index + 1, index + length + 1); - let emoji = get(client).getEmoji(emoji_name + '@' + host); + let emoji = get(Client.get()).getEmoji(emoji_name + '@' + host); if (emoji) { return text.substring(0, index) + emoji.html + @@ -46,7 +46,7 @@ export function parseText(text, host) { export function parseOne(emoji_id) { if (emoji_id == '❤') return '❤️'; // stupid heart unicode if (EMOJI_REGEX.exec(':' + emoji_id + ':')) return emoji_id; - let cached_emoji = get(client).getEmoji(emoji_id); + let cached_emoji = get(Client.get()).getEmoji(emoji_id); if (!cached_emoji) return emoji_id; return cached_emoji.html; } diff --git a/src/lib/timeline.js b/src/lib/timeline.js index a9fcdbf..5858199 100644 --- a/src/lib/timeline.js +++ b/src/lib/timeline.js @@ -1,4 +1,4 @@ -import { client } from '$lib/client/client.js'; +import { Client } from '$lib/client/client.js'; import { get, writable } from 'svelte/store'; import { parsePost } from '$lib/client/api.js'; @@ -10,9 +10,11 @@ export async function getTimeline(clean) { if (loading) return; // no spamming!! loading = true; + let client = get(Client.get()); + let timeline_data; - if (clean || get(posts).length === 0) timeline_data = await get(client).getTimeline() - else timeline_data = await get(client).getTimeline(get(posts)[get(posts).length - 1].id); + if (clean || get(posts).length === 0) timeline_data = await client.getTimeline() + else timeline_data = await client.getTimeline(get(posts)[get(posts).length - 1].id); if (!timeline_data) { console.error(`Failed to retrieve timeline.`); diff --git a/src/lib/ui/Feed.svelte b/src/lib/ui/Feed.svelte index d249165..bd3cef4 100644 --- a/src/lib/ui/Feed.svelte +++ b/src/lib/ui/Feed.svelte @@ -1,6 +1,10 @@ -
-

Home

- -
-
{#if posts.length <= 0}
diff --git a/src/lib/ui/LoginForm.svelte b/src/lib/ui/LoginForm.svelte deleted file mode 100644 index 8cf9b07..0000000 --- a/src/lib/ui/LoginForm.svelte +++ /dev/null @@ -1,162 +0,0 @@ - - -
- -

Welcome, fediverse user!

-

Please enter your instance domain to log in.

-
- - {#if instance_url_error} -

{instance_url_error}

- {/if} -
-
- -

- Please note this is - extremely experimental software; - things are likely to break! -
- If that's all cool with you, welcome aboard! -

- - -
- - diff --git a/src/lib/ui/Navigation.svelte b/src/lib/ui/Navigation.svelte index 5ebbc5e..a513649 100644 --- a/src/lib/ui/Navigation.svelte +++ b/src/lib/ui/Navigation.svelte @@ -2,7 +2,7 @@ import Logo from '$lib/../img/campfire-logo.svg'; import Button from './Button.svelte'; import Feed from './Feed.svelte'; - import { client } from '$lib/client/client.js'; + import { Client } from '$lib/client/client.js'; import { play_sound } from '$lib/sound.js'; import { getTimeline } from '$lib/timeline.js'; import { goto } from '$app/navigation'; @@ -22,6 +22,11 @@ const VERSION = APP_VERSION; + let client = false; + Client.get().subscribe(c => { + client = c; + }); + let notification_count = 0; if (notification_count > 99) notification_count = "99+"; @@ -39,20 +44,26 @@ async function log_out() { if (!confirm("This will log you out. Are you sure?")) return; - await get(client).logout(); + await get(Client.get()).logout(); goto("/"); }