diff --git a/VERSION b/VERSION index 8ec19862..755bc5dc 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -20250226.1 +20250226.2 diff --git a/assets/common.js/loading.jsx b/assets/common.js/loading.jsx index 340f0e14..50c6d563 100644 --- a/assets/common.js/loading.jsx +++ b/assets/common.js/loading.jsx @@ -18,10 +18,13 @@ const MszLoadingIcon = function() { let tsLastUpdate; let counter = 0; let playing = false; + let delay = 50; + let playResolve; + let pauseResolve; const update = tsCurrent => { try { - if(tsLastUpdate !== undefined && (tsCurrent - tsLastUpdate) < 50) + if(tsLastUpdate !== undefined && (tsCurrent - tsLastUpdate) < delay) return; tsLastUpdate = tsCurrent; @@ -30,29 +33,94 @@ const MszLoadingIcon = function() { ++counter; } finally { + if(playResolve) + try { + playResolve(); + } finally { + playResolve = undefined; + playing = true; + } + + if(pauseResolve) + try { + pauseResolve(); + } finally { + pauseResolve = undefined; + playing = false; + } + if(playing) requestAnimationFrame(update); } }; const play = () => { - if(playing) - return; - playing = true; - requestAnimationFrame(update); + return new Promise(resolve => { + if(playing || playResolve) { + resolve(); + return; + } + + playResolve = resolve; + requestAnimationFrame(update); + }); + }; + const pause = () => { + return new Promise(resolve => { + if(!playing || pauseResolve) { + resolve(); + return; + } + + pauseResolve = resolve; + }); + }; + const stop = async () => { + await pause(); + counter = 0; + }; + const restart = async () => { + await stop(); + await play(); + }; + const reverse = () => { + blocks.reverse(); + }; + const setBlock = (num, state=null) => { + element.children[num].classList.toggle('msz-loading-icon-block-hidden', !state); + }; + const batsu = () => { + setBlock(0, true);setBlock(1, false);setBlock(2, true); + setBlock(3, false);setBlock(4, true);setBlock(5, false); + setBlock(6, true);setBlock(7, false);setBlock(8, true); + }; + const maru = () => { + setBlock(0, true);setBlock(1, true);setBlock(2, true); + setBlock(3, true);setBlock(4, false);setBlock(5, true); + setBlock(6, true);setBlock(7, true);setBlock(8, true); }; - const pause = () => { playing = false; }; - const stop = () => { pause(); counter = 0; }; - const restart = () => { stop(); play(); }; return { get element() { return element; }, get playing() { return playing; }, + get delay() { return delay; }, + set delay(value) { + if(typeof value !== 'number') + value = parseFloat(value); + if(isNaN(value) || !isFinite(value)) + return; + if(value < 0) + value = Math.abs(value); + delay = value; + }, - play: play, - pause: pause, - stop: stop, - restart: restart, + play, + pause, + stop, + restart, + reverse, + batsu, + maru, }; }; diff --git a/assets/misuzu.css/container.css b/assets/misuzu.css/container.css index 49d1a1cd..2038a825 100644 --- a/assets/misuzu.css/container.css +++ b/assets/misuzu.css/container.css @@ -30,7 +30,6 @@ width: 100%; height: 100%; mask-image: linear-gradient(0deg, transparent 10%, var(--background-colour) 100%); - -webkit-mask-image: linear-gradient(0deg, transparent 10%, var(--background-colour) 100%); background: var(--background-pattern); background-color: var(--accent-colour); background-blend-mode: multiply; diff --git a/assets/redir-bsky.js/main.js b/assets/redir-bsky.js/main.js deleted file mode 100644 index a8f66016..00000000 --- a/assets/redir-bsky.js/main.js +++ /dev/null @@ -1,25 +0,0 @@ -(async () => { - const status = document.querySelector('.js-status'); - status.textContent = 'Looking up DID...'; - - let did = null; - - try { - const response = await fetch(`${location.protocol}//${BSKY_HANDLE}/.well-known/atproto-did`); - did = await response.text(); - } catch(ex) { - status.style.color = 'red'; - status.textContent = `Could not find DID! ${ex}`; - return; - } - - if(typeof did !== 'string' || !did.startsWith('did:')) { - status.style.color = 'red'; - status.textContent = 'Look up result was not a valid DID.'; - return; - } - - const url = BSKY_FORMAT.replace('%s', did); - status.textContent = `Redirecting to ${url}...`; - location.replace(url); -})(); diff --git a/assets/redir-fedi.js/main.js b/assets/redir-fedi.js/main.js deleted file mode 100644 index 00a05946..00000000 --- a/assets/redir-fedi.js/main.js +++ /dev/null @@ -1,39 +0,0 @@ -(async () => { - const status = document.querySelector('.js-status'); - status.textContent = 'Looking up Fediverse profile...'; - - let url = null; - try { - const response = await fetch(`${location.protocol}//${FEDI_INSTANCE}/.well-known/webfinger?format=json&resource=acct:${FEDI_USERNAME}@${FEDI_INSTANCE}`); - const webfinger = await response.json(); - - if(typeof webfinger === 'object') { - if(Array.isArray(webfinger.links)) - for(const link of webfinger.links) - if(typeof link === 'object' && link.rel === 'http://webfinger.net/rel/profile-page' && typeof link.href === 'string') { - url = link.href; - break; - } - - if(typeof url !== 'string' && Array.isArray(webfinger.aliases)) - for(const alias of webfinger.aliases) - if(typeof alias === 'string') { - url = alias; - break; - } - } - } catch(ex) { - status.style.color = 'red'; - status.textContent = `Could not complete Webfinger lookup! ${ex}`; - return; - } - - if(typeof url !== 'string' || (!url.startsWith('https://') && !url.startsWith('http://'))) { - status.style.color = 'red'; - status.textContent = 'Could not find an acceptable profile URL.'; - return; - } - - status.textContent = `Redirecting to ${url}...`; - location.replace(url); -})(); diff --git a/assets/redirects.css/landing.css b/assets/redirects.css/landing.css new file mode 100644 index 00000000..54b8fe0a --- /dev/null +++ b/assets/redirects.css/landing.css @@ -0,0 +1,42 @@ +.redir-landing { + display: flex; + flex: 1 0 auto; + padding: 10px; + width: 100%; + align-items: center; + justify-content: center; +} + +.redir-landing-content { + max-width: 500px; + width: 100%; + background: #191919; + box-shadow: 0 1px 2px #0009; + display: flex; + flex-direction: column; +} + +.redir-landing-body { + text-align: center; +} +.redir-landing-body p { + margin: .5em; +} + +.redir-landing-footer { + font-size: .8em; + line-height: 1.4em; + text-align: center; + margin: 10px; +} + +.redir-landing-logo { + display: inline-block; + vertical-align: middle; + background-color: #fff; + mask: url('/images/flashii.svg') no-repeat center; + width: 140px; + height: 140px; + margin: 10px; + font-size: 0; +} diff --git a/assets/redirects.css/main.css b/assets/redirects.css/main.css new file mode 100644 index 00000000..eaac758d --- /dev/null +++ b/assets/redirects.css/main.css @@ -0,0 +1,54 @@ +:root { + --accent-colour: #8559a5; +} + +body { + width: 100%; + height: 100%; + background-color: #111; + color: #fff; + font-size: 16px; + line-height: 25px; + font-family: var(--font-regular); + overflow-y: scroll; + position: static; + display: flex; +} + +pre, code { + font-family: var(--font-monospace); +} + +a { + color: #1e90ff; + text-decoration: none; +} +a:visited { + color: #6B4F80; +} +a:hover, +a:focus { + text-decoration: underline; +} + +.redir-background { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + mask-image: linear-gradient(315deg, #000f 0, #0000 40%); + background: url('/images/clouds.png'); + background-color: var(--accent-colour); + background-blend-mode: multiply; +} + +.redir-foreground { + display: flex; + flex-direction: column; + flex: 1 0 auto; + margin-bottom: 10px; +} + +@include landing.css; +@include social.css; diff --git a/assets/redirects.css/social.css b/assets/redirects.css/social.css new file mode 100644 index 00000000..b25d91f5 --- /dev/null +++ b/assets/redirects.css/social.css @@ -0,0 +1,26 @@ +.redir-social { + display: flex; + flex: 1 0 auto; + padding: 10px; + width: 100%; + align-items: center; + justify-content: center; +} + +.redir-social-content { + max-width: 500px; + width: 100%; + background: #191919; + box-shadow: 0 1px 2px #0009; + display: flex; + flex-direction: column; + padding: 1em; +} + +.redir-social-body { + text-align: center; +} +.redir-social-body p { + margin: .5em; + line-height: 1.4em; +} diff --git a/assets/redirects.js/bsky.jsx b/assets/redirects.js/bsky.jsx new file mode 100644 index 00000000..e59e3ad3 --- /dev/null +++ b/assets/redirects.js/bsky.jsx @@ -0,0 +1,32 @@ +const MszRedirectsBsky = async () => { + const loading = new MszLoading({ element: '.js-loading', size: 2, inline: true }); + + const statusBig = document.querySelector('.js-status-big'); + const statusSmall = document.querySelector('.js-status-small'); + const setStatusSmall = body => { + $removeChildren(statusSmall); + $appendChild(statusSmall, body); + }; + + setStatusSmall(<>Resolving ATProto DID for @{BSKY_HANDLE}...>); + + try { + const { body } = await $xhr.get(`${location.protocol}//${BSKY_HANDLE}/.well-known/atproto-did`); + if(typeof body !== 'string' || !body.startsWith('did:')) + throw new Error('Was unable to resolve the given handle.'); + + const url = BSKY_FORMAT.replace('%s', body); + setStatusSmall(<>Redirecting to {url}...>); + location.replace(url); + } catch(ex) { + await loading.icon.stop(); + loading.icon.batsu(); + + let errorText = ex.toString(); + if(errorText.startsWith('[')) + errorText = 'Something went wrong.'; + + statusBig.textContent = 'Profile not found!'; + setStatusSmall({errorText}); + } +}; diff --git a/assets/redirects.js/fedi.jsx b/assets/redirects.js/fedi.jsx new file mode 100644 index 00000000..e8bf4442 --- /dev/null +++ b/assets/redirects.js/fedi.jsx @@ -0,0 +1,49 @@ +const MszRedirectsFedi = async () => { + const loading = new MszLoading({ element: '.js-loading', size: 2, inline: true }); + + const statusBig = document.querySelector('.js-status-big'); + const statusSmall = document.querySelector('.js-status-small'); + const setStatusSmall = body => { + $removeChildren(statusSmall); + $appendChild(statusSmall, body); + }; + + setStatusSmall(<>Resolving Webfinger resource for @{FEDI_USERNAME}@{FEDI_INSTANCE}...>); + + try { + const { body } = await $xhr.get(`${location.protocol}//${FEDI_INSTANCE}/.well-known/webfinger?format=json&resource=acct:${FEDI_USERNAME}@${FEDI_INSTANCE}`, { type: 'json' }); + + let url; + if(typeof body === 'object') { + if(Array.isArray(body.links)) + for(const link of body.links) + if(typeof link === 'object' && link.rel === 'http://webfinger.net/rel/profile-page' && typeof link.href === 'string') { + url = link.href; + break; + } + + if(typeof url !== 'string' && Array.isArray(body.aliases)) + for(const alias of body.aliases) + if(typeof alias === 'string') { + url = alias; + break; + } + } + + if(typeof url !== 'string' || (!url.startsWith('https://') && !url.startsWith('http://'))) + throw new Error('Unable to resolve profile URL for given handle.'); + + setStatusSmall(<>Redirecting to {url}...>); + location.replace(url); + } catch(ex) { + await loading.icon.stop(); + loading.icon.batsu(); + + let errorText = ex.toString(); + if(errorText.startsWith('[')) + errorText = 'Something went wrong.'; + + statusBig.textContent = 'Profile not found!'; + setStatusSmall({errorText}); + } +}; diff --git a/assets/redirects.js/main.js b/assets/redirects.js/main.js new file mode 100644 index 00000000..123982ce --- /dev/null +++ b/assets/redirects.js/main.js @@ -0,0 +1,9 @@ +#include bsky.jsx +#include fedi.jsx + +(() => { + if(location.pathname === '/bsky' || location.pathname.startsWith('/bsky/')) + MszRedirectsBsky(); + if(location.pathname === '/fedi' || location.pathname.startsWith('/fedi/')) + MszRedirectsFedi(); +})(); diff --git a/build.js b/build.js index adbae5bb..a77140a1 100644 --- a/build.js +++ b/build.js @@ -25,14 +25,14 @@ const fs = require('fs'); { source: 'common.js', target: '/assets', name: 'common.{hash}.js', }, { source: 'misuzu.js', target: '/assets', name: 'misuzu.{hash}.js', }, { source: 'oauth2.js', target: '/assets', name: 'oauth2.{hash}.js', }, - { source: 'redir-bsky.js', target: '/assets', name: 'redir-bsky.{hash}.js', }, - { source: 'redir-fedi.js', target: '/assets', name: 'redir-fedi.{hash}.js', }, + { source: 'redirects.js', target: '/assets', name: 'redirects.{hash}.js', }, ], css: [ { source: 'errors.css', target: '/', name: 'errors.css', }, { source: 'common.css', target: '/assets', name: 'common.{hash}.css', }, { source: 'misuzu.css', target: '/assets', name: 'misuzu.{hash}.css', }, { source: 'oauth2.css', target: '/assets', name: 'oauth2.{hash}.css', }, + { source: 'redirects.css', target: '/assets', name: 'redirects.{hash}.css', }, ], twig: [ { source: 'errors/400', target: '/', name: 'error-400.html', }, diff --git a/src/Redirects/LandingRedirectsRoutes.php b/src/Redirects/LandingRedirectsRoutes.php index 793d9d78..19535e50 100644 --- a/src/Redirects/LandingRedirectsRoutes.php +++ b/src/Redirects/LandingRedirectsRoutes.php @@ -1,21 +1,14 @@ - -
Short URL Service for Flashii
-This is a temporary landing page until backend is less tied up.
-flashwave 2017-2025
- HTML; + return Template::renderRaw('redirects.landing'); } } diff --git a/src/Redirects/RedirectsContext.php b/src/Redirects/RedirectsContext.php index 768f9cab..0d285708 100644 --- a/src/Redirects/RedirectsContext.php +++ b/src/Redirects/RedirectsContext.php @@ -8,6 +8,10 @@ class RedirectsContext { public private(set) NamedRedirectsData $named; public private(set) IncrementalRedirectsData $incremental; + public string $bskyProfileUrlFormat { + get => $this->config->getString('bsky_profile', 'https://bsky.app/profile/%s'); + } + public function __construct( DbConnection $dbConn, public private(set) Config $config diff --git a/src/Redirects/SocialRedirectsRoutes.php b/src/Redirects/SocialRedirectsRoutes.php index 34830d4b..2b921296 100644 --- a/src/Redirects/SocialRedirectsRoutes.php +++ b/src/Redirects/SocialRedirectsRoutes.php @@ -1,22 +1,16 @@ config = $redirectsCtx->config->scopeTo('social'); - } + private RedirectsContext $redirectsCtx + ) {} #[HttpGet('/bsky/((did:[a-z0-9]+:[A-Za-z0-9.\-_:%]+)|(([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])))')] public function getBlueskyRedirect(HttpResponseBuilder $response, HttpRequest $request, string $handle): int|string { @@ -41,38 +35,23 @@ class SocialRedirectsRoutes implements RouteHandler { } } - $format = $this->config->getString('bsky_profile', 'https://bsky.app/profile/%s'); + $format = $this->redirectsCtx->bskyProfileUrlFormat; if(is_string($did)) { $response->redirect(sprintf($format, $did), true); return 301; } - $handle = rawurlencode($handle); - $script = $this->assetInfo->getAssetUrl('redir-bsky.js'); - - return << - -