diff --git a/CHANGELOGS.md b/CHANGELOGS.md deleted file mode 100644 index 4c81497..0000000 --- a/CHANGELOGS.md +++ /dev/null @@ -1,10 +0,0 @@ -# Campfire v0.3.0 -- Added notifications view -- Many more background tweaks, fixes, and optimisations - -# Campfire v0.2.0 -- Complete UI overhaul (thanks mae!) -- Added light and dark themes -- Added ability to like and boost posts -- Added ability to view threads in context -- Many background tweaks, fixes, and optimisations diff --git a/package-lock.json b/package-lock.json index 68c3b16..2388e03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "campfire-client", - "version": "0.3.0", + "version": "0.2.0_rev3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "campfire-client", - "version": "0.3.0", + "version": "0.2.0_rev3", "license": "GPL-3.0", "devDependencies": { "@poppanator/sveltekit-svg": "^4.2.1", diff --git a/package.json b/package.json index 301641e..80a43c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "campfire-client", - "version": "0.3.0", + "version": "0.2.0", "description": "social media for the galaxy-wide-web! 🌌", "private": true, "type": "module", diff --git a/src/app.html b/src/app.html index 9f07942..ce47f4e 100644 --- a/src/app.html +++ b/src/app.html @@ -12,14 +12,14 @@ - + - + %sveltekit.head% diff --git a/src/img/icons/quote.svg b/src/img/icons/quote.svg index 98ce541..8e47670 100644 --- a/src/img/icons/quote.svg +++ b/src/img/icons/quote.svg @@ -1,4 +1,3 @@ - - + diff --git a/src/lib/client/api.js b/src/lib/client/api.js index 09e6514..27f0e0b 100644 --- a/src/lib/client/api.js +++ b/src/lib/client/api.js @@ -1,9 +1,8 @@ -import { client } from '$lib/client/client.js'; -import { user } from '$lib/stores/user.js'; +import { Client } from '../client/client.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 Post from '../post.js'; +import User from '../user/user.js'; +import Emoji from '../emoji.js'; import { get } from 'svelte/store'; export async function createApp(host) { @@ -32,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, }) @@ -64,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, }) @@ -83,52 +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 } - }).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 } + headers: { "Authorization": "Bearer " + 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`; + 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; @@ -136,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; @@ -147,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; @@ -158,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; @@ -169,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; @@ -180,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; @@ -196,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; @@ -207,24 +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.html = 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); @@ -233,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); @@ -263,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; @@ -274,14 +286,15 @@ 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! user = new User(); user.id = data.id; - user.nickname = data.display_name.trim(); + user.nickname = data.display_name; user.username = data.username; user.avatar_url = data.avatar; user.url = data.url; @@ -289,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 => { @@ -299,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 = { @@ -324,16 +338,27 @@ 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()); - return data; + const user = await parseUser(data); + if (user === null || user === undefined) { + if (data.id) { + console.warn("Failed to parse user data #" + data.id); + } else { + console.warn("Failed to parse user data:"); + console.warn(data); + } + return false; + } + return user; } diff --git a/src/lib/client/client.js b/src/lib/client/client.js index e6d79ac..f4e3788 100644 --- a/src/lib/client/client.js +++ b/src/lib/client/client.js @@ -1,10 +1,8 @@ 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); +let client = writable(false); const save_name = "campfire"; @@ -24,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`; @@ -69,34 +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 getClientUser() { - // 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; - } - - async getNotifications(since_id, limit, types) { - return await api.getNotifications(since_id, limit, types); + 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) { @@ -107,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); } @@ -167,10 +166,6 @@ export class Client { 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({ @@ -179,7 +174,6 @@ export class Client { host: this.instance.host, version: this.instance.version, }, - last_read_notif_id: get(last_read_notif_id), app: this.app, })); } @@ -194,7 +188,6 @@ export class Client { 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; @@ -206,8 +199,7 @@ export class Client { console.warn("Failed to log out correctly; ditching the old tokens anyways."); } localStorage.removeItem(save_name); - logged_in.set(false); - 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/notifications.js b/src/lib/notifications.js deleted file mode 100644 index bbdc69f..0000000 --- a/src/lib/notifications.js +++ /dev/null @@ -1,40 +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 notifications = writable([]); -export let unread_notif_count = writable(0); -export let last_read_notif_id = writable(0); - -let loading; -export async function getNotifications() { - 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; - } - } - } - 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; - }); -} 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..5858199 100644 --- a/src/lib/timeline.js +++ b/src/lib/timeline.js @@ -1,8 +1,8 @@ -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'; -export let timeline = writable([]); +export let posts = writable([]); let loading = false; @@ -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(timeline).length === 0) timeline_data = await get(client).getTimeline() - else timeline_data = await get(client).getTimeline(get(timeline)[get(timeline).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.`); @@ -20,7 +22,7 @@ export async function getTimeline(clean) { return; } - if (clean) timeline.set([]); + if (clean) posts.set([]); for (let i in timeline_data) { const post_data = timeline_data[i]; @@ -36,7 +38,7 @@ export async function getTimeline(clean) { } continue; } - timeline.update(current => [...current, post]); + posts.update(current => [...current, post]); } loading = false; } diff --git a/src/lib/ui/Button.svelte b/src/lib/ui/Button.svelte index ea68fc4..702d3fa 100644 --- a/src/lib/ui/Button.svelte +++ b/src/lib/ui/Button.svelte @@ -1,8 +1,6 @@ - - - - -
{#if posts.length <= 0}
getting the feed...
{/if} - {#each posts as post} + {#each $posts as post} {/each}
@@ -29,7 +29,6 @@ diff --git a/src/lib/ui/Navigation.svelte b/src/lib/ui/Navigation.svelte index c4ce6e5..a513649 100644 --- a/src/lib/ui/Navigation.svelte +++ b/src/lib/ui/Navigation.svelte @@ -2,16 +2,11 @@ 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 { user } from '$lib/stores/user.js'; + import { Client } from '$lib/client/client.js'; import { play_sound } from '$lib/sound.js'; import { getTimeline } from '$lib/timeline.js'; - import { getNotifications } from '$lib/notifications.js'; import { goto } from '$app/navigation'; - import { page } from '$app/stores'; import { get } from 'svelte/store'; - import { logged_in } from '$lib/stores/user.js'; - import { unread_notif_count, last_read_notif_id } from '$lib/notifications.js'; import TimelineIcon from '../../img/icons/timeline.svg'; import NotificationsIcon from '../../img/icons/notifications.svg'; @@ -26,70 +21,61 @@ import LogoutIcon from '../../img/icons/logout.svg'; const VERSION = APP_VERSION; + + let client = false; + Client.get().subscribe(c => { + client = c; + }); - function handle_btn(name) { - if (!get(logged_in)) return; - let route; - switch (name) { - case "timeline": - route = "/"; - getTimeline(true); - break; - case "notifications": - route = "/notifications"; - getNotifications(); - break; - case "explore": - case "lists": - case "favourites": - case "bookmarks": - case "hashtags": - default: - return; + let notification_count = 0; + if (notification_count > 99) notification_count = "99+"; + + function goTimeline() { + if (location.pathname === "/") { + getTimeline(true); + window.scrollTo({ + top: 0, + behavior: "smooth" + }); + return; } - if (!route) return; - window.scrollTo({ - top: 0, - behavior: "smooth" - }); - goto(route); + goto("/"); } 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("/"); }