From a1ec63b7ec7f436f7cb6cbb78f9c55ebb48a573e Mon Sep 17 00:00:00 2001 From: ari melody Date: Thu, 10 Jul 2025 02:10:01 +0100 Subject: [PATCH 01/49] improve light theme contrast --- src/lib/app.css | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lib/app.css b/src/lib/app.css index d6656b4..2119752 100644 --- a/src/lib/app.css +++ b/src/lib/app.css @@ -1,13 +1,13 @@ @import url("../font/inter/inter.css"); :root { - --bg-1000: #fff6de; - --bg-900: #f9f1db; - --bg-800: #f1e8cf; - --bg-700: #d2c9b1; - --bg-600: #f0f6c2; - --accent: #8d9936; - --text: #322e1f; + --bg-1000: #fffbf2; + --bg-900: #fff7e2; + --bg-800: #eee7d3; + --bg-700: #c8c0ac; + --bg-600: #edf5ba; + --accent: #899911; + --text: #11100d; } @media (prefers-color-scheme: dark) { From 0a563e6121803636a340f4fc6537b99151d3e66e Mon Sep 17 00:00:00 2001 From: ari melody Date: Thu, 10 Jul 2025 18:11:49 +0100 Subject: [PATCH 02/49] rework post design; refine light theme --- src/lib/app.css | 16 ++++++++++++---- src/lib/ui/Navigation.svelte | 4 ++++ src/lib/ui/post/Body.svelte | 1 - src/lib/ui/post/Post.svelte | 10 +++++----- src/lib/ui/post/ReplyContext.svelte | 3 +-- src/routes/+page.svelte | 1 + 6 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/lib/app.css b/src/lib/app.css index 2119752..cca7bbd 100644 --- a/src/lib/app.css +++ b/src/lib/app.css @@ -8,6 +8,12 @@ --bg-600: #edf5ba; --accent: #899911; --text: #11100d; + + --bg-1000: #fffcf7; + --bg-800: #f2e8d7; + --bg-700: #d9ccad; + --accent: #92a40a; + --text: #322e1f; } @media (prefers-color-scheme: dark) { @@ -31,10 +37,6 @@ } } -@supports (font-variation-settings: normal) { - body { font-family: InterVariable, sans-serif; } -} - body { margin: 0; padding: 0; @@ -48,6 +50,12 @@ body { box-sizing: border-box; } +@supports (font-variation-settings: normal) { + body { + font-family: InterVariable, sans-serif; + } +} + a { color: var(--accent); text-decoration: none; diff --git a/src/lib/ui/Navigation.svelte b/src/lib/ui/Navigation.svelte index a5bedfd..bb09632 100644 --- a/src/lib/ui/Navigation.svelte +++ b/src/lib/ui/Navigation.svelte @@ -200,6 +200,8 @@ height: calc(100vh - 32px); border-radius: 8px; background-color: var(--bg-800); + transition: background-color .1s linear; + user-select: none; } .server-header { @@ -213,6 +215,7 @@ background-size: cover; background-color: var(--bg-600); background-image: linear-gradient(to top, var(--bg-800), var(--bg-600)); + transition: background .1s linear; } .server-icon { @@ -270,6 +273,7 @@ font-size: .9em; opacity: .6; text-align: center; + user-select: text; } .version ul { diff --git a/src/lib/ui/post/Body.svelte b/src/lib/ui/post/Body.svelte index 2760b56..5cb160f 100644 --- a/src/lib/ui/post/Body.svelte +++ b/src/lib/ui/post/Body.svelte @@ -84,7 +84,6 @@ } .post-text { - font-size: .9em; line-height: 1.45em; word-wrap: break-word; } diff --git a/src/lib/ui/post/Post.svelte b/src/lib/ui/post/Post.svelte index 1178a24..819d994 100644 --- a/src/lib/ui/post/Post.svelte +++ b/src/lib/ui/post/Post.svelte @@ -73,19 +73,19 @@ \ No newline at end of file From e326ac858e3bec20e1edb757f0134b0693251686 Mon Sep 17 00:00:00 2001 From: ari melody Date: Sun, 13 Jul 2025 18:35:26 +0100 Subject: [PATCH 09/49] add localisation support currently only en_GB (TODO: dynamic language pack imports) --- src/lang/en_GB.json | 88 +++++++++++++++++++++++++++ src/lib/lang.js | 60 ++++++++++++++++++ src/lib/time.js | 5 +- src/lib/ui/Composer.svelte | 15 ++--- src/lib/ui/LoginForm.svelte | 25 ++++---- src/lib/ui/Navigation.svelte | 30 ++++----- src/lib/ui/Notification.svelte | 23 ++++--- src/lib/ui/Widgets.svelte | 8 ++- src/lib/ui/post/ActionBar.svelte | 15 +++-- src/lib/ui/post/Body.svelte | 20 +++--- src/lib/ui/post/BoostContext.svelte | 15 +++-- src/lib/ui/post/PostHeader.svelte | 9 +-- src/lib/ui/post/ReactionBar.svelte | 6 +- src/lib/ui/post/ReactionButton.svelte | 7 ++- src/routes/+page.svelte | 13 ++-- src/routes/notifications/+page.svelte | 7 ++- svelte.config.js | 7 ++- 17 files changed, 263 insertions(+), 90 deletions(-) create mode 100644 src/lang/en_GB.json create mode 100644 src/lib/lang.js diff --git a/src/lang/en_GB.json b/src/lang/en_GB.json new file mode 100644 index 0000000..ea6a3b6 --- /dev/null +++ b/src/lang/en_GB.json @@ -0,0 +1,88 @@ +{ + "compose_placeholders": [ + "What's cooking, $1?", + "Speak your mind!", + "Federate something...", + "I sure love posting!", + "Another day, another $1 post!" + ], + + "login": { + "welcome": "Welcome, fediverse user!", + "enter_domain": "Please enter your server domain to log in.", + "experimental": "Please note this is\nextremely experimental software;\nthings are likely to break!\n
\nIf that's all cool with you, welcome aboard!", + "button": "Log in", + "error": { + "no_domain": "Please enter an server domain.", + "connection_failed": "Failed to connect to the server.\nCheck the browser console for details!", + "create_app": "Failed to create an application for this server." + }, + "made_with_tagline": "made with ❤ by bliss town" + }, + + "navigation": { + "timeline": "Timeline", + "notifications": "Notifications", + "explore": "Explore", + "lists": "Lists", + + "favourites": "Favourites", + "bookmarks": "Bookmarks", + "hashtags": "Hashtags", + + "profile_information": "Profile information", + "settings": "Settings", + "log_out": "Log out" + }, + + "timeline": { + "home": "Home", + "local": "Local", + "federated": "Federated", + "fetching": "getting the feed..." + }, + + "notification": { + "and_others": "and %1 others", + "mention": "%1 mentioned you.", + "reblog": "%1 boosted your post.", + "reaction": "%1 reacted to your post.", + "follow": "%1 followed you.", + "follow_request": "%1 requested to follow you.", + "favourite": "%1 favourited your post.", + "poll": "%1's poll as ended.", + "update": "%1 updated their post.", + "default": "%1 poked you!", + "fetching": "fetching notifications..." + }, + + "post": { + "time": "%1 ago", + "boosted": "%1 boosted this post.", + "actions": { + "reply": "Reply", + "boost": "Boost", + "favourite": "Favourite", + "quote": "Quote", + "react": "React", + "more": "More", + "delete": "Delete" + }, + "warning": { + "show": "(click to reveal)", + "hide": "(click to hide)" + }, + "visibility": { + "public": "public", + "unlisted": "unlisted", + "private": "private", + "direct": "direct" + } + }, + + "compose": "Post", + "search": "Search", + + "source": "source", + "issues": "issues" +} diff --git a/src/lib/lang.js b/src/lib/lang.js new file mode 100644 index 0000000..8e90901 --- /dev/null +++ b/src/lib/lang.js @@ -0,0 +1,60 @@ +import * as en_GB from '@cf/lang/en_GB.json'; + +/** + * @param {string} lang IETH language tag (i.e. en_GB) + * @returns Map + */ +export default function init(lang) { + let i18n = new Object(); + let language; + + // TODO: dynamic imports seem to fail here; it can't find the file. + // try { + // language = import(`../lang/${lang}.json`); + // } catch (error) { + // throw error; + // } + + language = en_GB; + + i18n.lang = language; + i18n.lang_code = lang; + i18n.string = function(/* @type string */ key) { + const tokens = key.split('.'); + + let i = 0; + let token = tokens[i]; + let res = this.lang; + while (true) { + res = res[token]; + if (res === undefined) { + console.warn(`${key} not found for language ${this.lang_code}`); + return key; + } + if (typeof res === 'string' || res instanceof String) + return res; + i++; + token = tokens[i]; + } + } + i18n.stringArray = function(/* @type string */ key) { + const tokens = key.split('.'); + + let i = 0; + let token = tokens[i]; + let res = this.lang; + while (true) { + res = res[token]; + if (res === undefined) { + console.warn(`${key} not found for language ${this.lang_code}`); + return key; + } + if (Array.isArray(res)) + return res; + i++; + token = tokens[i]; + } + } + + return i18n; +} diff --git a/src/lib/time.js b/src/lib/time.js index 27ff200..4717b74 100644 --- a/src/lib/time.js +++ b/src/lib/time.js @@ -1,3 +1,6 @@ +import Lang from '$lib/lang.js'; +const lang = Lang('en_GB'); + const denoms = [ { unit: 's', min: 0 }, { unit: 'm', min: 60 }, @@ -18,6 +21,6 @@ export function shorthand(date) { unit = denoms[index].unit; } if (value > 0) - return Math.floor(value) + unit + " ago"; + return lang.string('post.time').replaceAll('%1', Math.floor(value) + unit); return "in " + Math.floor(value) + unit; } diff --git a/src/lib/ui/Composer.svelte b/src/lib/ui/Composer.svelte index b290d14..4935038 100644 --- a/src/lib/ui/Composer.svelte +++ b/src/lib/ui/Composer.svelte @@ -7,6 +7,7 @@ import { timeline } from '$lib/timeline.js'; import { createEventDispatcher } from 'svelte'; import { playSound } from '$lib/sound'; + import Lang from '$lib/lang.js' import Button from '@cf/ui/Button.svelte'; import PostIcon from '@cf/icons/post.svg'; @@ -19,6 +20,8 @@ import FollowersVisIcon from '@cf/icons/followers.svg'; import PrivateVisIcon from '@cf/icons/dm.svg'; + const lang = Lang('en_GB'); + export let reply_id; let content_warning = "" @@ -27,15 +30,9 @@ let show_cw = false; let visibility = "Public"; - const placeholders = [ - "What's cooking, $1?", - "Speak your mind!", - "Federate something...", - "I sure love posting!", - "Another day, another $1 post!", - ]; - let placeholder = placeholders[Math.floor(placeholders.length * Math.random())] - .replaceAll("$1", $account.username); + const placeholders = lang.stringArray('compose_placeholders'); + let placeholder = Array.isArray(placeholders) ? placeholders[Math.floor(placeholders.length * Math.random())] + .replaceAll("$1", $account.username) : placeholders; const dispatch = createEventDispatcher(); diff --git a/src/lib/ui/LoginForm.svelte b/src/lib/ui/LoginForm.svelte index 1a64f88..fe3b08f 100644 --- a/src/lib/ui/LoginForm.svelte +++ b/src/lib/ui/LoginForm.svelte @@ -3,9 +3,12 @@ import { server, createServer } from '$lib/client/server.js'; import { app } from '$lib/client/app.js'; import { get } from 'svelte/store'; + import Lang from '$lib/lang.js'; import Logo from '$lib/../img/campfire-logo.svg'; + const lang = Lang('en_GB'); + let display_error = false; let logging_in = false; @@ -17,21 +20,21 @@ const host = event.target.host.value; if (!host || host === "") { - display_error = "Please enter an server domain."; + display_error = lang.string('login.error.no_domain'); logging_in = false; return; } server.set(await createServer(host)); if (!get(server)) { - display_error = "Failed to connect to the server.\nCheck the browser console for details!" + display_error = lang.string('login.error.connection_failed'); logging_in = false; return; } app.set(await api.createApp(get(server).host)); if (!get(app)) { - display_error = "Failed to create an application for this server." + display_error = lang.string('login.error.create_app'); logging_in = false; return; } @@ -44,8 +47,8 @@ -

Welcome, fediverse user!

-

Please enter your server domain to log in.

+

{lang.string('login.welcome')}

+

{lang.string('login.enter_domain')}

{#if display_error} @@ -53,16 +56,10 @@ {/if}

- -

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

+ +

{@html lang.string('login.experimental')}

- + \ No newline at end of file + From 449a11ee55c8f0cf71222e3fb756a55326ef6616 Mon Sep 17 00:00:00 2001 From: ari melody Date: Mon, 14 Jul 2025 00:19:42 +0100 Subject: [PATCH 15/49] initial profile implementation! --- src/lang/en_GB.json | 9 +- src/lib/account.js | 5 + src/lib/api.js | 27 +++ src/lib/client/server.js | 3 + src/lib/ui/Button.svelte | 14 +- src/lib/ui/Composer.svelte | 3 +- src/lib/ui/Navigation.svelte | 14 +- src/lib/ui/post/BoostContext.svelte | 3 +- src/lib/ui/post/PostHeader.svelte | 5 +- src/routes/[server]/+page.js | 2 +- src/routes/[server]/[account]/+page.js | 10 ++ src/routes/[server]/[account]/+page.svelte | 166 +++++++++++++----- src/routes/[server]/[account]/[post]/+page.js | 4 + .../[server]/[account]/[post]/+page.svelte | 4 +- 14 files changed, 212 insertions(+), 57 deletions(-) create mode 100644 src/routes/[server]/[account]/+page.js diff --git a/src/lang/en_GB.json b/src/lang/en_GB.json index 0ffc265..38b7f75 100644 --- a/src/lang/en_GB.json +++ b/src/lang/en_GB.json @@ -94,7 +94,9 @@ "posts": "Posts", "replies": "Replies", - "media": "Media" + "media": "Media", + + "loading": "loading profile..." }, "logs": { @@ -104,9 +106,12 @@ "no_hostname": "Attempted to connect to a server without providing a hostname", "no_https": "Cowardly refusing to connect to an insecure server", "connection_failed": "Failed to connect to %1", - "post_fetch_failed": "Failed to fetch post %1", + "post_fetch_failed": "Failed to fetch post", + "post_fetch_failed_id": "Failed to fetch post %1", "post_parse_failed": "Failed to parse post", "post_parse_failed_id": "Failed to parse post %1", + "profile_fetch_failed": "Failed to fetch profile", + "profile_fetch_failed_id": "Failed to fetch profile %1", "token_revoke_failed": "Token revocation failed! Dumping data anyways", "sound_does_not_exist": "Attempted to play sound \"%1\", which does not exist!", "account_data_empty": "Attempted to parse account data but no data was provided", diff --git a/src/lib/account.js b/src/lib/account.js index 394fe0f..50ff0b3 100644 --- a/src/lib/account.js +++ b/src/lib/account.js @@ -27,7 +27,12 @@ export function parseAccount(data) { account.username = data.username; account.name = account.nickname || account.username; account.avatar_url = data.avatar; + account.banner_url = data.header; account.url = data.url; + account.followers_count = data.followers_count; + account.following_count = data.following_count; + account.posts_count = data.statuses_count; + account.bio = data.note; if (data.acct.includes('@')) account.host = data.acct.split('@')[1]; diff --git a/src/lib/api.js b/src/lib/api.js index 61f6bd9..c9b97ba 100644 --- a/src/lib/api.js +++ b/src/lib/api.js @@ -1,3 +1,7 @@ +const errors = { + AUTHENTICATION_FAILED: "AUTHENTICATION_FAILED", +}; + /** * GET /api/v1/instance * @param {string} host - The domain of the target server. @@ -421,3 +425,26 @@ export async function getUser(host, token, user_id) { return data; } + +/** + * GET /api/v1/accounts/lookup?acct={handle} + * @param {string} host - The domain of the target server. + * @param {string} token - The application token. + * @param {string} handle - The handle of the user to fetch. + */ +export async function lookupUser(host, token, handle) { + let url = `https://${host}/api/v1/accounts/lookup?acct=${handle}`; + + const res = await fetch(url, { + method: 'GET', + headers: { "Authorization": token ? `Bearer ${token}` : null } + }); + if (!res.ok) { + const json = await res.json(); + if (json.error = errors.AUTHENTICATION_FAILED) + throw new Error("This method requires authentication"); + } + const data = await res.json(); + + return data; +} diff --git a/src/lib/client/server.js b/src/lib/client/server.js index 3c85647..c1d6426 100644 --- a/src/lib/client/server.js +++ b/src/lib/client/server.js @@ -2,6 +2,9 @@ import * as api from '$lib/api.js'; import { writable } from 'svelte/store'; import { app_name } from '$lib/config.js'; import { browser } from "$app/environment"; +import Lang from '$lib/lang'; + +const lang = Lang('en_GB'); const server_types = { UNSUPPORTED: "unsupported", diff --git a/src/lib/ui/Button.svelte b/src/lib/ui/Button.svelte index c20c923..788828f 100644 --- a/src/lib/ui/Button.svelte +++ b/src/lib/ui/Button.svelte @@ -18,9 +18,21 @@ function click() { if (disabled) return; if (href) { - location = href; + const link = document.createElement('a'); + link.href = href; + link.dispatchEvent(new MouseEvent('click', { + bubbles: true, + cancelable: true, + view: window, + ctrlKey: event.ctrlKey, + metaKey: event.metaKey, + shiftKey: event.shiftKey, + altKey: event.altKey, + button: event.button, + })); return; } + playSound(sound); dispatch('click'); } diff --git a/src/lib/ui/Composer.svelte b/src/lib/ui/Composer.svelte index 7859a7e..f2664fa 100644 --- a/src/lib/ui/Composer.svelte +++ b/src/lib/ui/Composer.svelte @@ -90,7 +90,8 @@
- + +
diff --git a/src/lib/ui/Navigation.svelte b/src/lib/ui/Navigation.svelte index 96f4879..8cbb351 100644 --- a/src/lib/ui/Navigation.svelte +++ b/src/lib/ui/Navigation.svelte @@ -61,6 +61,16 @@ goto(route); } + function gotoProfile() { + if (!$account) return; + playSound(); + window.scrollTo({ + top: 0, + behavior: "smooth" + }); + goto(`/${$server.host}/${$account.username}`); + } + async function log_out() { if (!confirm("This will log you out. Are you sure?")) return; @@ -171,9 +181,9 @@
- playSound()}> + gotoProfile()}>