From d86a69903fa8e916b2e5fc569e887cac9042aed5 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Fri, 3 Mar 2023 18:11:37 +0800 Subject: [PATCH 01/76] Need more datetime detail for Edit History --- src/components/status.jsx | 9 ++++++++- src/utils/nice-date-time.js | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/status.jsx b/src/components/status.jsx index ab57b354..6b316be7 100644 --- a/src/components/status.jsx +++ b/src/components/status.jsx @@ -1295,7 +1295,14 @@ function EditedAtModal({ return (
  • - +

    Date: Fri, 3 Mar 2023 20:34:53 +0800 Subject: [PATCH 02/76] Fix shortcuts button/tab-bar hidden on other pages This is because the CSS only check the home-page's hidden header, not the other pages. This fixes it with a super advanced CSS selector. --- src/components/shortcuts.css | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/components/shortcuts.css b/src/components/shortcuts.css index 5c57a920..ef79e698 100644 --- a/src/components/shortcuts.css +++ b/src/components/shortcuts.css @@ -15,7 +15,9 @@ #shortcuts-button .icon { transform: translateY(2px); /* Balance the icon's vertical alignment */ } -#app:has(header[hidden]) #shortcuts-button, +#app:has(#home-page):not(:has(#home-page ~ .deck-container)):has(header[hidden]) + #shortcuts-button, +#app:has(#home-page ~ .deck-container header[hidden]) #shortcuts-button, #shortcuts-button[hidden] { transform: translateY(200%); pointer-events: none; @@ -39,7 +41,11 @@ top: max(16px, env(safe-area-inset-top)); bottom: auto; } - #app:has(header[hidden]) #shortcuts-button, + #app:has(#home-page):not(:has(#home-page ~ .deck-container)):has( + header[hidden] + ) + #shortcuts-button, + #app:has(#home-page ~ .deck-container header[hidden]) #shortcuts-button, #shortcuts-button[hidden] { transform: translateY(-200%); } @@ -114,7 +120,10 @@ transparent ); } -#app:has(header[hidden]) #shortcuts .tab-bar, +#app:has(#home-page):not(:has(#home-page ~ .deck-container)):has(header[hidden]) + #shortcuts + .tab-bar, +#app:has(#home-page ~ .deck-container header[hidden]) #shortcuts .tab-bar, shortcuts .tab-bar[hidden] { transform: translateY(200%); pointer-events: none; From f0442b20e84632f75c32a216243827ab1363e938 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Fri, 3 Mar 2023 20:42:17 +0800 Subject: [PATCH 03/76] Missed this part --- src/components/shortcuts.css | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/shortcuts.css b/src/components/shortcuts.css index ef79e698..1c1040cf 100644 --- a/src/components/shortcuts.css +++ b/src/components/shortcuts.css @@ -172,7 +172,12 @@ shortcuts .tab-bar[hidden] { height: 44px; gap: 4px; } - #app:has(header[hidden]) #shortcuts .tab-bar, + #app:has(#home-page):not(:has(#home-page ~ .deck-container)):has( + header[hidden] + ) + #shortcuts + .tab-bar, + #app:has(#home-page ~ .deck-container header[hidden]) #shortcuts .tab-bar, shortcuts .tab-bar[hidden] { transform: translateY(-150%); pointer-events: none; From af7c9bc1b136d4d0c54a2959f3d90ff0d5a083b9 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Mon, 6 Mar 2023 16:01:33 +0800 Subject: [PATCH 04/76] Differentiate clickable vs static toasts --- src/app.css | 26 ++++++++++++++++++-------- src/utils/show-toast.js | 6 +++--- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/app.css b/src/app.css index 77c03e4c..04128217 100644 --- a/src/app.css +++ b/src/app.css @@ -1219,35 +1219,45 @@ meter.donut:is(.danger, .explode):after { /* SHINY PILL */ -.shiny-pill { +:is(.shiny-pill, :root .toastify.shiny-pill) { + pointer-events: auto; color: var(--button-text-color); text-shadow: 0 calc(var(--hairline-width) * -1) var(--drop-shadow-color); background-color: var(--button-bg-color); background-image: linear-gradient( 160deg, rgba(255, 255, 255, 0.5), - rgba(255, 255, 255, 0) 50% + rgba(0, 0, 0, 0.1) ); box-shadow: 0 3px 8px -1px var(--drop-shadow-color), 0 10px 36px -4px var(--button-bg-blur-color), inset var(--hairline-width) var(--hairline-width) rgba(255, 255, 255, 0.5); + transition: filter 0.3s; +} +:is(.shiny-pill, :root .toastify.shiny-pill):hover { + filter: brightness(1.2); +} +:is(.shiny-pill, :root .toastify.shiny-pill):active { + transition: none; + filter: brightness(0.9); } /* TOAST */ :root .toastify { + user-select: none; padding: 8px 16px; border-radius: 999px; + pointer-events: none; + color: var(--button-text-color); + text-shadow: 0 calc(var(--hairline-width) * -1) var(--drop-shadow-color); + background-color: var(--button-bg-blur-color); + background-image: none; + backdrop-filter: blur(16px); } .toastify-bottom { margin-bottom: env(safe-area-inset-bottom); } -:root .toastify:hover { - filter: brightness(1.2); -} -:root .toastify:active { - filter: brightness(0.8); -} /* AVATARS STACK */ diff --git a/src/utils/show-toast.js b/src/utils/show-toast.js index 8cb02521..f9469ce2 100644 --- a/src/utils/show-toast.js +++ b/src/utils/show-toast.js @@ -4,14 +4,14 @@ function showToast(props) { if (typeof props === 'string') { props = { text: props }; } - const { onClick = () => {}, delay, ...rest } = props; + const { onClick, delay, ...rest } = props; const toast = Toastify({ - className: 'shiny-pill', + className: `${onClick || props.destination ? 'shiny-pill' : ''}`, gravity: 'bottom', position: 'center', ...rest, onClick: () => { - onClick(toast); // Pass in the object itself! + onClick?.(toast); // Pass in the object itself! }, }); if (delay) { From 9f6657d9a277f967110a7e36ebde2bbd05470e28 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Mon, 6 Mar 2023 18:19:37 +0800 Subject: [PATCH 05/76] Fix wrong shadow color for sheets --- src/app.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.css b/src/app.css index 04128217..62cc23de 100644 --- a/src/app.css +++ b/src/app.css @@ -987,7 +987,7 @@ body:has(.status-deck) .media-post-link { width: 100%; max-width: calc(var(--main-width) - 50px - 16px); border-radius: 16px 16px 0 0; - box-shadow: 0 -1px 32px var(--divider-color); + box-shadow: 0 -1px 32px var(--drop-shadow-color); animation: slide-up 0.3s var(--timing-function); border: 1px solid var(--outline-color); } From d32f4b95a2a46ab42a65a236ca922aaa8cea1f1f Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Mon, 6 Mar 2023 18:19:46 +0800 Subject: [PATCH 06/76] Add Trunks --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index bcf52dc4..71ef021a 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,7 @@ And here I am. Building a Mastodon web client. - [Soapbox](https://fe.soapbox.pub/) - [Elk](https://elk.zone/) - [Mastodeck](https://mastodeck.com/) +- [Trunks (alpha)](https://alpha.trunks.social/) - [Tooty](https://github.com/n1k0/tooty) - [More...](https://github.com/hueyy/awesome-mastodon/#clients) From 2cb22c34e392f37a2f1fad7adc4462ee489b109a Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Mon, 6 Mar 2023 18:20:49 +0800 Subject: [PATCH 07/76] Extra checks for prevent weird font size --- src/components/status.css | 2 +- src/components/status.jsx | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/status.css b/src/components/status.css index 424c0f87..1c77c1c2 100644 --- a/src/components/status.css +++ b/src/components/status.css @@ -317,7 +317,7 @@ } .status.large .content { font-size: 150%; - font-size: calc(100% + 50% / var(--content-text-weight)); + font-size: min(calc(100% + 50% / var(--content-text-weight)), 150%); } .status.large .poll, .status.large .actions { diff --git a/src/components/status.jsx b/src/components/status.jsx index 6b316be7..fca28e24 100644 --- a/src/components/status.jsx +++ b/src/components/status.jsx @@ -229,7 +229,10 @@ function Status({ const unauthInteractionErrorMessage = `Sorry, your current logged-in instance can't interact with this status from another instance.`; const textWeight = () => - Math.round((spoilerText.length + htmlContentLength(content)) / 140) || 1; + Math.max( + Math.round((spoilerText.length + htmlContentLength(content)) / 140) || 1, + 1, + ); const createdDateText = niceDateTime(createdAtDate); const editedDateText = editedAt && niceDateTime(editedAtDate); From 8d501668d03f1ea8ab45a3be7efcb6238566a2d3 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Tue, 7 Mar 2023 12:58:43 +0800 Subject: [PATCH 08/76] =?UTF-8?q?Weird=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/media-modal.jsx | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/src/components/media-modal.jsx b/src/components/media-modal.jsx index 0cf99387..d79d7388 100644 --- a/src/components/media-modal.jsx +++ b/src/components/media-modal.jsx @@ -250,31 +250,6 @@ function MediaModal({ )} - {!!showMediaAlt && ( - { - if (e.target === e.currentTarget) { - setShowMediaAlt(false); - } - }} - > -
    -
    -

    Media description

    -
    -
    -

    - {showMediaAlt} -

    -
    -
    -
    - )} ); } From f4275d27fe7d00914234cbbc2847d55e083ed07f Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Tue, 7 Mar 2023 22:36:12 +0800 Subject: [PATCH 09/76] Testing showing activity on Account Block Meh --- src/components/account-block.jsx | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/components/account-block.jsx b/src/components/account-block.jsx index c8c2e54b..84d3792b 100644 --- a/src/components/account-block.jsx +++ b/src/components/account-block.jsx @@ -1,6 +1,7 @@ import './account-block.css'; import emojifyText from '../utils/emojify-text'; +import niceDateTime from '../utils/nice-date-time'; import states from '../utils/states'; import Avatar from './avatar'; @@ -12,6 +13,7 @@ function AccountBlock({ instance, external, onClick, + showActivity = false, }) { if (skeleton) { return ( @@ -26,8 +28,17 @@ function AccountBlock({ ); } - const { acct, avatar, avatarStatic, displayName, username, emojis, url } = - account; + const { + acct, + avatar, + avatarStatic, + displayName, + username, + emojis, + url, + statusesCount, + lastStatusAt, + } = account; const displayNameWithEmoji = emojifyText(displayName, emojis); return ( @@ -58,6 +69,23 @@ function AccountBlock({ {username} )}
    @{acct} + {showActivity && ( + <> +
    + + Posts: {statusesCount} + {!!lastStatusAt && ( + <> + {' '} + · Last posted:{' '} + {niceDateTime(lastStatusAt, { + hideTime: true, + })} + + )} + + + )} ); From 355b3be6e95b2f7e05e8b21819a34c66b651dbb5 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Tue, 7 Mar 2023 22:38:06 +0800 Subject: [PATCH 10/76] Alrighty, let's test this post translation out! --- scripts/fetch-lingva-languages.js | 18 + src/app.css | 6 + src/components/icon.jsx | 1 + src/components/loader.css | 1 + src/components/media-modal.jsx | 60 ++- src/components/status.jsx | 49 ++ src/components/translation-block.css | 86 ++++ src/components/translation-block.jsx | 154 ++++++ src/data/lingva-source-languages.json | 534 ++++++++++++++++++++ src/data/lingva-target-languages.json | 534 ++++++++++++++++++++ src/pages/settings.css | 4 + src/pages/settings.jsx | 55 ++ src/pages/status.jsx | 3 + src/utils/get-translate-target-language.jsx | 24 + src/utils/localeCode2Text.jsx | 5 + src/utils/states.js | 10 + 16 files changed, 1530 insertions(+), 14 deletions(-) create mode 100644 scripts/fetch-lingva-languages.js create mode 100644 src/components/translation-block.css create mode 100644 src/components/translation-block.jsx create mode 100644 src/data/lingva-source-languages.json create mode 100644 src/data/lingva-target-languages.json create mode 100644 src/utils/get-translate-target-language.jsx create mode 100644 src/utils/localeCode2Text.jsx diff --git a/scripts/fetch-lingva-languages.js b/scripts/fetch-lingva-languages.js new file mode 100644 index 00000000..f270cabe --- /dev/null +++ b/scripts/fetch-lingva-languages.js @@ -0,0 +1,18 @@ +// Fetch https://lingva.ml/api/v1/languages/{source|target} +import fs from 'fs'; + +fetch('https://lingva.ml/api/v1/languages/source') + .then((response) => response.json()) + .then((json) => { + const file = './src/data/lingva-source-languages.json'; + console.log(`Writing ${file}...`); + fs.writeFileSync(file, JSON.stringify(json.languages, null, '\t'), 'utf8'); + }); + +fetch('https://lingva.ml/api/v1/languages/target') + .then((response) => response.json()) + .then((json) => { + const file = './src/data/lingva-target-languages.json'; + console.log(`Writing ${file}...`); + fs.writeFileSync(file, JSON.stringify(json.languages, null, '\t'), 'utf8'); + }); diff --git a/src/app.css b/src/app.css index 62cc23de..beae550b 100644 --- a/src/app.css +++ b/src/app.css @@ -1007,6 +1007,12 @@ body:has(.status-deck) .media-post-link { .sheet header :is(h1, h2, h3) { margin: 0; } +.sheet header.header-grid { + display: grid; + grid-template-columns: 1fr auto; + grid-gap: 8px; + align-items: center; +} .sheet main { overflow: auto; overflow-x: hidden; diff --git a/src/components/icon.jsx b/src/components/icon.jsx index 7a5edd00..805823d9 100644 --- a/src/components/icon.jsx +++ b/src/components/icon.jsx @@ -63,6 +63,7 @@ const ICONS = { share: 'mingcute:share-2-line', sparkles: 'mingcute:sparkles-line', exit: 'mingcute:exit-line', + translate: 'mingcute:translate-line', }; const modules = import.meta.glob('/node_modules/@iconify-icons/mingcute/*.js'); diff --git a/src/components/loader.css b/src/components/loader.css index dd327cb4..c93214bb 100644 --- a/src/components/loader.css +++ b/src/components/loader.css @@ -6,6 +6,7 @@ animation: appear 0.3s ease-in-out 1s both; vertical-align: middle; margin: 8px; + vertical-align: baseline !important; } .loader-container.abrupt { animation: none; diff --git a/src/components/media-modal.jsx b/src/components/media-modal.jsx index d79d7388..6c85b3dd 100644 --- a/src/components/media-modal.jsx +++ b/src/components/media-modal.jsx @@ -1,3 +1,4 @@ +import { Menu, MenuItem } from '@szhsin/react-menu'; import { getBlurHashAverageColor } from 'fast-blurhash'; import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -6,6 +7,7 @@ import Icon from './icon'; import Link from './link'; import Media from './media'; import Modal from './modal'; +import TranslationBlock from './translation-block'; function MediaModal({ mediaAttachments, @@ -234,24 +236,54 @@ function MediaModal({ } }} > -
    -
    -

    Media description

    -
    -
    -

    - {showMediaAlt} -

    -
    -
    + )} ); } +function MediaAltModal({ alt }) { + const [forceTranslate, setForceTranslate] = useState(false); + return ( +
    +
    +

    Media description

    +
    + + + + } + > + { + setForceTranslate(true); + }} + > + + Translate + + +
    +
    +
    +

    + {alt} +

    + {forceTranslate && ( + + )} +
    +
    + ); +} + export default MediaModal; diff --git a/src/components/status.jsx b/src/components/status.jsx index fca28e24..04645030 100644 --- a/src/components/status.jsx +++ b/src/components/status.jsx @@ -20,6 +20,7 @@ import Modal from '../components/modal'; import NameText from '../components/name-text'; import { api } from '../utils/api'; import enhanceContent from '../utils/enhance-content'; +import getTranslateTargetLanguage from '../utils/get-translate-target-language'; import handleContentLinks from '../utils/handle-content-links'; import htmlContentLength from '../utils/html-content-length'; import niceDateTime from '../utils/nice-date-time'; @@ -35,6 +36,7 @@ import Link from './link'; import Media from './media'; import MenuLink from './MenuLink'; import RelativeTime from './relative-time'; +import TranslationBlock from './translation-block'; const throttle = pThrottle({ limit: 1, @@ -66,6 +68,7 @@ function Status({ skeleton, readOnly, contentTextWeight, + enableTranslate, }) { if (skeleton) { return ( @@ -194,6 +197,10 @@ function Status({ ); } + const [forceTranslate, setForceTranslate] = useState(false); + const targetLanguage = getTranslateTargetLanguage(true); + if (!snapStates.settings.contentTranslation) enableTranslate = false; + const [showEdited, setShowEdited] = useState(false); const spoilerContentRef = useRef(null); @@ -450,6 +457,17 @@ function Status({ Copy link to post + {enableTranslate && ( + { + setForceTranslate(true); + }} + > + + Translate + + )} {navigator?.share && navigator?.canShare?.({ url, @@ -770,6 +788,25 @@ function Status({ }} /> )} + {((enableTranslate && + !!content.trim() && + language && + language !== targetLanguage) || + forceTranslate) && ( + `- ${option.title}`) + .join('\n')}` + : '') + } + /> + )} {!spoilerText && sensitive && !!mediaAttachments.length && (
  • \n'); + div.querySelectorAll('br').forEach((br) => { + br.replaceWith('\n'); + }); + return div.innerText.replace(/[\r\n]{3,}/g, '\n\n').trim(); +} + export default memo(Status); diff --git a/src/components/translation-block.css b/src/components/translation-block.css new file mode 100644 index 00000000..4e3b0d3d --- /dev/null +++ b/src/components/translation-block.css @@ -0,0 +1,86 @@ +.status-translation-block { + margin: 8px 0 0; + padding: 0; + font-size: 90%; + border-radius: 8px; +} +.status-translation-block summary { + list-style: none; + display: inline-block; +} +.status-translation-block summary::-webkit-details-marker { + display: none; +} +.status-translation-block summary button { + border-radius: 8px; + border: 1px solid var(--outline-color); + padding: 8px; + background-color: var(--bg-color); + font-size: 12px; + color: var(--text-insignificant-color); +} +.status-translation-block summary button:is(:hover, :focus) { + color: var(--text-color); + filter: none !important; +} +.status-translation-block details:not([open]) .detected { + display: none; +} +/* .status-translation-block details summary button:active, */ +.status-translation-block details[open] summary button { + /* color: var(--text-color); */ + /* background-color: var(--bg-faded-color); */ + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + border-bottom: 0; + margin-bottom: -1px; + background-image: linear-gradient( + to top left, + var(--bg-color) 50%, + var(--bg-faded-blur-color) + ); + box-shadow: inset 0 0 0 1px var(--bg-color); +} +.status-translation-block .translated-block { + border: 1px solid var(--outline-color); + line-height: 1.3; + border-radius: 0 8px 8px 8px; + margin: 0; + padding: 8px; + background-color: var(--bg-color); + background-image: linear-gradient( + to bottom right, + var(--bg-color), + var(--bg-faded-blur-color) + ); + white-space: pre-wrap; + box-shadow: inset 0 0 0 1px var(--bg-color), + 0 1px 5px -2px var(--drop-shadow-color); + text-shadow: 0 1px var(--bg-color); +} +.status-translation-block .translated-block .translation-info * { + vertical-align: middle; +} +.status-translation-block .translated-source-select { + appearance: none; + display: inline-block; + margin: 0; + padding: 4px 8px; + border: 0; + border-radius: 8px; + background-color: var(--bg-faded-color); + color: inherit; + width: min-content; +} +.status-translation-block .translated-block output { + display: block; + margin-top: 1em; +} +.status-translation-block + .translated-block + output.translated-pronunciation-content { + opacity: 0.75; + padding-bottom: 1em; + border-top: var(--hairline-width) solid var(--bg-color); + border-bottom: var(--hairline-width) solid var(--outline-color); +} diff --git a/src/components/translation-block.jsx b/src/components/translation-block.jsx new file mode 100644 index 00000000..aa7b4479 --- /dev/null +++ b/src/components/translation-block.jsx @@ -0,0 +1,154 @@ +import './translation-block.css'; + +import { useEffect, useRef, useState } from 'preact/hooks'; + +import sourceLanguages from '../data/lingva-source-languages'; +import getTranslateTargetLanguage from '../utils/get-translate-target-language'; +import localeCode2Text from '../utils/localeCode2Text'; + +import Icon from './icon'; +import Loader from './loader'; + +function TranslationBlock({ + forceTranslate, + sourceLanguage, + onTranslate, + text = '', +}) { + const targetLang = getTranslateTargetLanguage(true); + const [uiState, setUIState] = useState('default'); + const [pronunciationContent, setPronunciationContent] = useState(null); + const [translatedContent, setTranslatedContent] = useState(null); + const [detectedLang, setDetectedLang] = useState(null); + const detailsRef = useRef(); + + const sourceLangText = sourceLanguage + ? localeCode2Text(sourceLanguage) + : null; + const targetLangText = localeCode2Text(targetLang); + const apiSourceLang = useRef('auto'); + + if (!onTranslate) + onTranslate = (source, target) => { + console.log('TRANSLATE', source, target, text); + // Using another API instance instead of lingva.ml because of this bug (slashes don't work): + // https://github.com/thedaviddelta/lingva-translate/issues/68 + return fetch( + `https://lingva.garudalinux.org/api/v1/${source}/${target}/${encodeURIComponent( + text, + )}`, + ) + .then((res) => res.json()) + .then((res) => { + return { + provider: 'lingva', + content: res.translation, + detectedSourceLanguage: res.info.detectedSource, + info: res.info, + }; + }); + // return masto.v1.statuses.translate(id, { + // lang: DEFAULT_LANG, + // }); + }; + + const translate = async () => { + setUIState('loading'); + const { content, detectedSourceLanguage, provider, ...props } = + await onTranslate(apiSourceLang.current, targetLang); + if (content) { + if (detectedSourceLanguage) { + const detectedLangText = localeCode2Text(detectedSourceLanguage); + setDetectedLang(detectedLangText); + } + if (provider === 'lingva') { + const pronunciation = props?.info?.pronunciation?.query; + if (pronunciation) { + setPronunciationContent(pronunciation); + } + } + setTranslatedContent(content); + setUIState('default'); + detailsRef.current.open = true; + detailsRef.current.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + }); + } else { + console.error(result); + setUIState('error'); + } + }; + + useEffect(() => { + if (forceTranslate) { + translate(); + } + }, [forceTranslate]); + + return ( +
    +
    + + + +
    +
    + {' '} + → {targetLangText} +
    + {uiState === 'error' ? ( +

    Failed to translate

    + ) : ( + !!translatedContent && ( + <> + {!!pronunciationContent && ( + + {pronunciationContent} + + )} + + {translatedContent} + + + ) + )} +
    +
    +
    + ); +} + +export default TranslationBlock; diff --git a/src/data/lingva-source-languages.json b/src/data/lingva-source-languages.json new file mode 100644 index 00000000..bcde98d2 --- /dev/null +++ b/src/data/lingva-source-languages.json @@ -0,0 +1,534 @@ +[ + { + "code": "auto", + "name": "Detect" + }, + { + "code": "af", + "name": "Afrikaans" + }, + { + "code": "sq", + "name": "Albanian" + }, + { + "code": "am", + "name": "Amharic" + }, + { + "code": "ar", + "name": "Arabic" + }, + { + "code": "hy", + "name": "Armenian" + }, + { + "code": "as", + "name": "Assamese" + }, + { + "code": "ay", + "name": "Aymara" + }, + { + "code": "az", + "name": "Azerbaijani" + }, + { + "code": "bm", + "name": "Bambara" + }, + { + "code": "eu", + "name": "Basque" + }, + { + "code": "be", + "name": "Belarusian" + }, + { + "code": "bn", + "name": "Bengali" + }, + { + "code": "bho", + "name": "Bhojpuri" + }, + { + "code": "bs", + "name": "Bosnian" + }, + { + "code": "bg", + "name": "Bulgarian" + }, + { + "code": "ca", + "name": "Catalan" + }, + { + "code": "ceb", + "name": "Cebuano" + }, + { + "code": "ny", + "name": "Chichewa" + }, + { + "code": "zh", + "name": "Chinese" + }, + { + "code": "co", + "name": "Corsican" + }, + { + "code": "hr", + "name": "Croatian" + }, + { + "code": "cs", + "name": "Czech" + }, + { + "code": "da", + "name": "Danish" + }, + { + "code": "dv", + "name": "Dhivehi" + }, + { + "code": "doi", + "name": "Dogri" + }, + { + "code": "nl", + "name": "Dutch" + }, + { + "code": "en", + "name": "English" + }, + { + "code": "eo", + "name": "Esperanto" + }, + { + "code": "et", + "name": "Estonian" + }, + { + "code": "ee", + "name": "Ewe" + }, + { + "code": "tl", + "name": "Filipino" + }, + { + "code": "fi", + "name": "Finnish" + }, + { + "code": "fr", + "name": "French" + }, + { + "code": "fy", + "name": "Frisian" + }, + { + "code": "gl", + "name": "Galician" + }, + { + "code": "ka", + "name": "Georgian" + }, + { + "code": "de", + "name": "German" + }, + { + "code": "el", + "name": "Greek" + }, + { + "code": "gn", + "name": "Guarani" + }, + { + "code": "gu", + "name": "Gujarati" + }, + { + "code": "ht", + "name": "Haitian Creole" + }, + { + "code": "ha", + "name": "Hausa" + }, + { + "code": "haw", + "name": "Hawaiian" + }, + { + "code": "iw", + "name": "Hebrew" + }, + { + "code": "hi", + "name": "Hindi" + }, + { + "code": "hmn", + "name": "Hmong" + }, + { + "code": "hu", + "name": "Hungarian" + }, + { + "code": "is", + "name": "Icelandic" + }, + { + "code": "ig", + "name": "Igbo" + }, + { + "code": "ilo", + "name": "Ilocano" + }, + { + "code": "id", + "name": "Indonesian" + }, + { + "code": "ga", + "name": "Irish" + }, + { + "code": "it", + "name": "Italian" + }, + { + "code": "ja", + "name": "Japanese" + }, + { + "code": "jw", + "name": "Javanese" + }, + { + "code": "kn", + "name": "Kannada" + }, + { + "code": "kk", + "name": "Kazakh" + }, + { + "code": "km", + "name": "Khmer" + }, + { + "code": "rw", + "name": "Kinyarwanda" + }, + { + "code": "gom", + "name": "Konkani" + }, + { + "code": "ko", + "name": "Korean" + }, + { + "code": "kri", + "name": "Krio" + }, + { + "code": "ku", + "name": "Kurdish (Kurmanji)" + }, + { + "code": "ckb", + "name": "Kurdish (Sorani)" + }, + { + "code": "ky", + "name": "Kyrgyz" + }, + { + "code": "lo", + "name": "Lao" + }, + { + "code": "la", + "name": "Latin" + }, + { + "code": "lv", + "name": "Latvian" + }, + { + "code": "ln", + "name": "Lingala" + }, + { + "code": "lt", + "name": "Lithuanian" + }, + { + "code": "lg", + "name": "Luganda" + }, + { + "code": "lb", + "name": "Luxembourgish" + }, + { + "code": "mk", + "name": "Macedonian" + }, + { + "code": "mai", + "name": "Maithili" + }, + { + "code": "mg", + "name": "Malagasy" + }, + { + "code": "ms", + "name": "Malay" + }, + { + "code": "ml", + "name": "Malayalam" + }, + { + "code": "mt", + "name": "Maltese" + }, + { + "code": "mi", + "name": "Maori" + }, + { + "code": "mr", + "name": "Marathi" + }, + { + "code": "mni-Mtei", + "name": "Meiteilon (Manipuri)" + }, + { + "code": "lus", + "name": "Mizo" + }, + { + "code": "mn", + "name": "Mongolian" + }, + { + "code": "my", + "name": "Myanmar (Burmese)" + }, + { + "code": "ne", + "name": "Nepali" + }, + { + "code": "no", + "name": "Norwegian" + }, + { + "code": "or", + "name": "Odia (Oriya)" + }, + { + "code": "om", + "name": "Oromo" + }, + { + "code": "ps", + "name": "Pashto" + }, + { + "code": "fa", + "name": "Persian" + }, + { + "code": "pl", + "name": "Polish" + }, + { + "code": "pt", + "name": "Portuguese" + }, + { + "code": "pa", + "name": "Punjabi" + }, + { + "code": "qu", + "name": "Quechua" + }, + { + "code": "ro", + "name": "Romanian" + }, + { + "code": "ru", + "name": "Russian" + }, + { + "code": "sm", + "name": "Samoan" + }, + { + "code": "sa", + "name": "Sanskrit" + }, + { + "code": "gd", + "name": "Scots Gaelic" + }, + { + "code": "nso", + "name": "Sepedi" + }, + { + "code": "sr", + "name": "Serbian" + }, + { + "code": "st", + "name": "Sesotho" + }, + { + "code": "sn", + "name": "Shona" + }, + { + "code": "sd", + "name": "Sindhi" + }, + { + "code": "si", + "name": "Sinhala" + }, + { + "code": "sk", + "name": "Slovak" + }, + { + "code": "sl", + "name": "Slovenian" + }, + { + "code": "so", + "name": "Somali" + }, + { + "code": "es", + "name": "Spanish" + }, + { + "code": "su", + "name": "Sundanese" + }, + { + "code": "sw", + "name": "Swahili" + }, + { + "code": "sv", + "name": "Swedish" + }, + { + "code": "tg", + "name": "Tajik" + }, + { + "code": "ta", + "name": "Tamil" + }, + { + "code": "tt", + "name": "Tatar" + }, + { + "code": "te", + "name": "Telugu" + }, + { + "code": "th", + "name": "Thai" + }, + { + "code": "ti", + "name": "Tigrinya" + }, + { + "code": "ts", + "name": "Tsonga" + }, + { + "code": "tr", + "name": "Turkish" + }, + { + "code": "tk", + "name": "Turkmen" + }, + { + "code": "ak", + "name": "Twi" + }, + { + "code": "uk", + "name": "Ukrainian" + }, + { + "code": "ur", + "name": "Urdu" + }, + { + "code": "ug", + "name": "Uyghur" + }, + { + "code": "uz", + "name": "Uzbek" + }, + { + "code": "vi", + "name": "Vietnamese" + }, + { + "code": "cy", + "name": "Welsh" + }, + { + "code": "xh", + "name": "Xhosa" + }, + { + "code": "yi", + "name": "Yiddish" + }, + { + "code": "yo", + "name": "Yoruba" + }, + { + "code": "zu", + "name": "Zulu" + } +] \ No newline at end of file diff --git a/src/data/lingva-target-languages.json b/src/data/lingva-target-languages.json new file mode 100644 index 00000000..b8c760de --- /dev/null +++ b/src/data/lingva-target-languages.json @@ -0,0 +1,534 @@ +[ + { + "code": "af", + "name": "Afrikaans" + }, + { + "code": "sq", + "name": "Albanian" + }, + { + "code": "am", + "name": "Amharic" + }, + { + "code": "ar", + "name": "Arabic" + }, + { + "code": "hy", + "name": "Armenian" + }, + { + "code": "as", + "name": "Assamese" + }, + { + "code": "ay", + "name": "Aymara" + }, + { + "code": "az", + "name": "Azerbaijani" + }, + { + "code": "bm", + "name": "Bambara" + }, + { + "code": "eu", + "name": "Basque" + }, + { + "code": "be", + "name": "Belarusian" + }, + { + "code": "bn", + "name": "Bengali" + }, + { + "code": "bho", + "name": "Bhojpuri" + }, + { + "code": "bs", + "name": "Bosnian" + }, + { + "code": "bg", + "name": "Bulgarian" + }, + { + "code": "ca", + "name": "Catalan" + }, + { + "code": "ceb", + "name": "Cebuano" + }, + { + "code": "ny", + "name": "Chichewa" + }, + { + "code": "zh", + "name": "Chinese" + }, + { + "code": "zh_HANT", + "name": "Chinese (Traditional)" + }, + { + "code": "co", + "name": "Corsican" + }, + { + "code": "hr", + "name": "Croatian" + }, + { + "code": "cs", + "name": "Czech" + }, + { + "code": "da", + "name": "Danish" + }, + { + "code": "dv", + "name": "Dhivehi" + }, + { + "code": "doi", + "name": "Dogri" + }, + { + "code": "nl", + "name": "Dutch" + }, + { + "code": "en", + "name": "English" + }, + { + "code": "eo", + "name": "Esperanto" + }, + { + "code": "et", + "name": "Estonian" + }, + { + "code": "ee", + "name": "Ewe" + }, + { + "code": "tl", + "name": "Filipino" + }, + { + "code": "fi", + "name": "Finnish" + }, + { + "code": "fr", + "name": "French" + }, + { + "code": "fy", + "name": "Frisian" + }, + { + "code": "gl", + "name": "Galician" + }, + { + "code": "ka", + "name": "Georgian" + }, + { + "code": "de", + "name": "German" + }, + { + "code": "el", + "name": "Greek" + }, + { + "code": "gn", + "name": "Guarani" + }, + { + "code": "gu", + "name": "Gujarati" + }, + { + "code": "ht", + "name": "Haitian Creole" + }, + { + "code": "ha", + "name": "Hausa" + }, + { + "code": "haw", + "name": "Hawaiian" + }, + { + "code": "iw", + "name": "Hebrew" + }, + { + "code": "hi", + "name": "Hindi" + }, + { + "code": "hmn", + "name": "Hmong" + }, + { + "code": "hu", + "name": "Hungarian" + }, + { + "code": "is", + "name": "Icelandic" + }, + { + "code": "ig", + "name": "Igbo" + }, + { + "code": "ilo", + "name": "Ilocano" + }, + { + "code": "id", + "name": "Indonesian" + }, + { + "code": "ga", + "name": "Irish" + }, + { + "code": "it", + "name": "Italian" + }, + { + "code": "ja", + "name": "Japanese" + }, + { + "code": "jw", + "name": "Javanese" + }, + { + "code": "kn", + "name": "Kannada" + }, + { + "code": "kk", + "name": "Kazakh" + }, + { + "code": "km", + "name": "Khmer" + }, + { + "code": "rw", + "name": "Kinyarwanda" + }, + { + "code": "gom", + "name": "Konkani" + }, + { + "code": "ko", + "name": "Korean" + }, + { + "code": "kri", + "name": "Krio" + }, + { + "code": "ku", + "name": "Kurdish (Kurmanji)" + }, + { + "code": "ckb", + "name": "Kurdish (Sorani)" + }, + { + "code": "ky", + "name": "Kyrgyz" + }, + { + "code": "lo", + "name": "Lao" + }, + { + "code": "la", + "name": "Latin" + }, + { + "code": "lv", + "name": "Latvian" + }, + { + "code": "ln", + "name": "Lingala" + }, + { + "code": "lt", + "name": "Lithuanian" + }, + { + "code": "lg", + "name": "Luganda" + }, + { + "code": "lb", + "name": "Luxembourgish" + }, + { + "code": "mk", + "name": "Macedonian" + }, + { + "code": "mai", + "name": "Maithili" + }, + { + "code": "mg", + "name": "Malagasy" + }, + { + "code": "ms", + "name": "Malay" + }, + { + "code": "ml", + "name": "Malayalam" + }, + { + "code": "mt", + "name": "Maltese" + }, + { + "code": "mi", + "name": "Maori" + }, + { + "code": "mr", + "name": "Marathi" + }, + { + "code": "mni-Mtei", + "name": "Meiteilon (Manipuri)" + }, + { + "code": "lus", + "name": "Mizo" + }, + { + "code": "mn", + "name": "Mongolian" + }, + { + "code": "my", + "name": "Myanmar (Burmese)" + }, + { + "code": "ne", + "name": "Nepali" + }, + { + "code": "no", + "name": "Norwegian" + }, + { + "code": "or", + "name": "Odia (Oriya)" + }, + { + "code": "om", + "name": "Oromo" + }, + { + "code": "ps", + "name": "Pashto" + }, + { + "code": "fa", + "name": "Persian" + }, + { + "code": "pl", + "name": "Polish" + }, + { + "code": "pt", + "name": "Portuguese" + }, + { + "code": "pa", + "name": "Punjabi" + }, + { + "code": "qu", + "name": "Quechua" + }, + { + "code": "ro", + "name": "Romanian" + }, + { + "code": "ru", + "name": "Russian" + }, + { + "code": "sm", + "name": "Samoan" + }, + { + "code": "sa", + "name": "Sanskrit" + }, + { + "code": "gd", + "name": "Scots Gaelic" + }, + { + "code": "nso", + "name": "Sepedi" + }, + { + "code": "sr", + "name": "Serbian" + }, + { + "code": "st", + "name": "Sesotho" + }, + { + "code": "sn", + "name": "Shona" + }, + { + "code": "sd", + "name": "Sindhi" + }, + { + "code": "si", + "name": "Sinhala" + }, + { + "code": "sk", + "name": "Slovak" + }, + { + "code": "sl", + "name": "Slovenian" + }, + { + "code": "so", + "name": "Somali" + }, + { + "code": "es", + "name": "Spanish" + }, + { + "code": "su", + "name": "Sundanese" + }, + { + "code": "sw", + "name": "Swahili" + }, + { + "code": "sv", + "name": "Swedish" + }, + { + "code": "tg", + "name": "Tajik" + }, + { + "code": "ta", + "name": "Tamil" + }, + { + "code": "tt", + "name": "Tatar" + }, + { + "code": "te", + "name": "Telugu" + }, + { + "code": "th", + "name": "Thai" + }, + { + "code": "ti", + "name": "Tigrinya" + }, + { + "code": "ts", + "name": "Tsonga" + }, + { + "code": "tr", + "name": "Turkish" + }, + { + "code": "tk", + "name": "Turkmen" + }, + { + "code": "ak", + "name": "Twi" + }, + { + "code": "uk", + "name": "Ukrainian" + }, + { + "code": "ur", + "name": "Urdu" + }, + { + "code": "ug", + "name": "Uyghur" + }, + { + "code": "uz", + "name": "Uzbek" + }, + { + "code": "vi", + "name": "Vietnamese" + }, + { + "code": "cy", + "name": "Welsh" + }, + { + "code": "xh", + "name": "Xhosa" + }, + { + "code": "yi", + "name": "Yiddish" + }, + { + "code": "yo", + "name": "Yoruba" + }, + { + "code": "zu", + "name": "Zulu" + } +] \ No newline at end of file diff --git a/src/pages/settings.css b/src/pages/settings.css index 8ff32ff8..9b1cf279 100644 --- a/src/pages/settings.css +++ b/src/pages/settings.css @@ -59,6 +59,10 @@ #settings-container section > ul > li > div:last-child { text-align: right; } +#settings-container section > ul > li .sub-section { + text-align: left !important; + margin-top: 8px; +} #settings-container div, #settings-container div > * { vertical-align: middle; diff --git a/src/pages/settings.jsx b/src/pages/settings.jsx index ce12ec71..2f06cbfa 100644 --- a/src/pages/settings.jsx +++ b/src/pages/settings.jsx @@ -10,7 +10,10 @@ import Icon from '../components/icon'; import Link from '../components/link'; import NameText from '../components/name-text'; import RelativeTime from '../components/relative-time'; +import targetLanguages from '../data/lingva-target-languages'; import { api } from '../utils/api'; +import getTranslateTargetLanguage from '../utils/get-translate-target-language'; +import localeCode2Text from '../utils/localeCode2Text'; import states from '../utils/states'; import store from '../utils/store'; @@ -33,6 +36,11 @@ function Settings({ onClose }) { const [_, reload] = useReducer((x) => x + 1, 0); + const targetLanguage = + snapStates.settings.contentTranslationTargetLanguage || null; + const systemTargetLanguage = getTranslateTargetLanguage(); + const systemTargetLanguageText = localeCode2Text(systemTargetLanguage); + return (
    @@ -240,6 +248,53 @@ function Settings({ onClose }) { Boosts carousel (experimental) +
  • + + {snapStates.settings.contentTranslation && ( +
    + +

    + + Note: This feature uses an external API to translate, + powered by{' '} + + Lingva Translate + + . + +

    +
    + )} +
  • Hidden features

    diff --git a/src/pages/status.jsx b/src/pages/status.jsx index 273df5f9..832e38fd 100644 --- a/src/pages/status.jsx +++ b/src/pages/status.jsx @@ -624,6 +624,7 @@ function StatusPage() { instance={instance} withinContext size="l" + enableTranslate /> {uiState !== 'loading' && !authenticated ? ( @@ -700,6 +701,7 @@ function StatusPage() { instance={instance} withinContext size={thread || ancestor ? 'm' : 's'} + enableTranslate /> {/* {replies?.length > LIMIT && ( ); From 03c0d6143304b08f3e76cdbbc91ce78441847ec1 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Thu, 9 Mar 2023 23:37:25 +0800 Subject: [PATCH 23/76] Beautify shortcuts settings --- src/assets/floating-button.svg | 5 +++ src/assets/multi-column.svg | 6 ++++ src/assets/tab-menu-bar.svg | 10 ++++++ src/components/shortcuts-settings.css | 48 +++++++++++++++++++++++++++ src/components/shortcuts-settings.jsx | 43 +++++++++++++++++++++--- 5 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 src/assets/floating-button.svg create mode 100644 src/assets/multi-column.svg create mode 100644 src/assets/tab-menu-bar.svg diff --git a/src/assets/floating-button.svg b/src/assets/floating-button.svg new file mode 100644 index 00000000..6a9ad117 --- /dev/null +++ b/src/assets/floating-button.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/assets/multi-column.svg b/src/assets/multi-column.svg new file mode 100644 index 00000000..5e8deff7 --- /dev/null +++ b/src/assets/multi-column.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/tab-menu-bar.svg b/src/assets/tab-menu-bar.svg new file mode 100644 index 00000000..64b48b03 --- /dev/null +++ b/src/assets/tab-menu-bar.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/components/shortcuts-settings.css b/src/components/shortcuts-settings.css index 0758f967..7cd8e494 100644 --- a/src/components/shortcuts-settings.css +++ b/src/components/shortcuts-settings.css @@ -33,7 +33,55 @@ #shortcuts-settings-container .shortcuts-view-mode { display: flex; align-items: center; + gap: 2px; + margin: 8px 0 0; +} +#shortcuts-settings-container .shortcuts-view-mode label { + border-radius: 4px; + background-color: var(--bg-faded-color); + padding: 16px; + text-align: center; + cursor: pointer; + display: block; + flex-grow: 1; + display: flex; gap: 8px; + flex-direction: column; + align-items: center; +} +#shortcuts-settings-container .shortcuts-view-mode label:first-child { + border-top-left-radius: 16px; + border-bottom-left-radius: 16px; +} +#shortcuts-settings-container .shortcuts-view-mode label:last-child { + border-top-right-radius: 16px; + border-bottom-right-radius: 16px; +} +#shortcuts-settings-container .shortcuts-view-mode label img { + max-height: 64px; +} +#shortcuts-settings-container .shortcuts-view-mode label span { + text-align: center; + font-size: 80%; +} +#shortcuts-settings-container .shortcuts-view-mode label input { + position: absolute; + opacity: 0; + pointer-events: none; +} +#shortcuts-settings-container .shortcuts-view-mode label input ~ * { + opacity: 0.5; + transition: opacity 0.2s; +} +#shortcuts-settings-container .shortcuts-view-mode label:has(input:checked) { + box-shadow: inset 0 0 0 3px var(--link-color); +} +#shortcuts-settings-container + .shortcuts-view-mode + label + input:is(:hover, :active, :checked) + ~ * { + opacity: 1; } #shortcuts-settings-container summary { diff --git a/src/components/shortcuts-settings.jsx b/src/components/shortcuts-settings.jsx index ecf39c3d..77bdbf0b 100644 --- a/src/components/shortcuts-settings.jsx +++ b/src/components/shortcuts-settings.jsx @@ -4,6 +4,9 @@ import mem from 'mem'; import { useEffect, useState } from 'preact/hooks'; import { useSnapshot } from 'valtio'; +import floatingButtonUrl from '../assets/floating-button.svg'; +import multiColumnUrl from '../assets/multi-column.svg'; +import tabMenuBarUrl from '../assets/tab-menu-bar.svg'; import { api } from '../utils/api'; import states from '../utils/states'; @@ -208,9 +211,40 @@ function ShortcutsSettings() {

    - + ))} +

    + {/* - + */}

    {/*

    From 79345bd6aa37ec7658c3310556269f6bbd2abd90 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Thu, 9 Mar 2023 23:46:55 +0800 Subject: [PATCH 24/76] Fix weird line height bug --- src/app.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app.css b/src/app.css index 62026ad4..288e5508 100644 --- a/src/app.css +++ b/src/app.css @@ -1156,6 +1156,8 @@ body:has(.status-deck) .media-post-link { } .szh-menu .menu-double-lines span { white-space: normal; + line-height: inherit; + font-size: inherit; } .szh-menu .menu-horizontal { display: flex; From bb6e0ac3839920e47f73825e1ef5cd04f912615d Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Fri, 10 Mar 2023 15:06:14 +0800 Subject: [PATCH 25/76] Upgrade preact --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 83ad2962..d658f20d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "mem": "~9.0.2", "p-retry": "~5.1.2", "p-throttle": "~5.0.0", - "preact": "~10.13.0", + "preact": "~10.13.1", "react-hotkeys-hook": "~4.3.7", "react-intersection-observer": "~9.4.3", "react-router-dom": "6.6.2", @@ -5659,9 +5659,9 @@ "dev": true }, "node_modules/preact": { - "version": "10.13.0", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.13.0.tgz", - "integrity": "sha512-ERdIdUpR6doqdaSIh80hvzebHB7O6JxycOhyzAeLEchqOq/4yueslQbfnPwXaNhAYacFTyCclhwkEbOumT0tHw==", + "version": "10.13.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.13.1.tgz", + "integrity": "sha512-KyoXVDU5OqTpG9LXlB3+y639JAGzl8JSBXLn1J9HTSB3gbKcuInga7bZnXLlxmK94ntTs1EFeZp0lrja2AuBYQ==", "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -10977,9 +10977,9 @@ "dev": true }, "preact": { - "version": "10.13.0", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.13.0.tgz", - "integrity": "sha512-ERdIdUpR6doqdaSIh80hvzebHB7O6JxycOhyzAeLEchqOq/4yueslQbfnPwXaNhAYacFTyCclhwkEbOumT0tHw==" + "version": "10.13.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.13.1.tgz", + "integrity": "sha512-KyoXVDU5OqTpG9LXlB3+y639JAGzl8JSBXLn1J9HTSB3gbKcuInga7bZnXLlxmK94ntTs1EFeZp0lrja2AuBYQ==" }, "prettier": { "version": "2.8.0", diff --git a/package.json b/package.json index 60eb769e..24370bc5 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "mem": "~9.0.2", "p-retry": "~5.1.2", "p-throttle": "~5.0.0", - "preact": "~10.13.0", + "preact": "~10.13.1", "react-hotkeys-hook": "~4.3.7", "react-intersection-observer": "~9.4.3", "react-router-dom": "6.6.2", From f15b8599719adc28dcb1a5fc9764b70ea8be5590 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Fri, 10 Mar 2023 15:49:23 +0800 Subject: [PATCH 26/76] Fix boostability for private-visibility posts --- src/components/status.jsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/status.jsx b/src/components/status.jsx index b85c91e5..934da50d 100644 --- a/src/components/status.jsx +++ b/src/components/status.jsx @@ -246,8 +246,15 @@ function Status({ const editedDateText = editedAt && niceDateTime(editedAtDate); const isSizeLarge = size === 'l'; - // TODO: if visibility = private, only can boost own statuses - const canBoost = authenticated && visibility !== 'direct'; + // Can boost if: + // - authenticated AND + // - visibility != direct OR + // - visibility = private AND isSelf + let canBoost = + authenticated && visibility !== 'direct' && visibility !== 'private'; + if (visibility === 'private' && isSelf) { + canBoost = true; + } const replyStatus = () => { if (!sameInstance || !authenticated) { From b12b96b8e17b5d9f59099ed3207f082fa7cd3316 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Fri, 10 Mar 2023 16:49:16 +0800 Subject: [PATCH 27/76] Fix focus not focusing into the status page --- src/app.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app.jsx b/src/app.jsx index a3d3d7f8..350c3f12 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -151,6 +151,8 @@ function App() { // Focus first column columns.querySelector('.deck-container')?.focus?.(); } else { + const backDrop = document.querySelector('.deck-backdrop'); + if (backDrop) return; // Focus last deck const pages = document.querySelectorAll('.deck-container'); const page = pages[pages.length - 1]; // last one From 8100a90421b50299f73f7dc98151e0d93606cacc Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Fri, 10 Mar 2023 17:36:42 +0800 Subject: [PATCH 28/76] Try add the account header --- src/app.css | 2 +- src/components/account.css | 45 ++++++++++++++++++++++++++++++++++++++ src/components/account.jsx | 3 +++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/app.css b/src/app.css index 288e5508..12c43776 100644 --- a/src/app.css +++ b/src/app.css @@ -990,7 +990,7 @@ body:has(.status-deck) .media-post-link { border-radius: 16px 16px 0 0; box-shadow: 0 -1px 32px var(--drop-shadow-color); animation: slide-up 0.3s var(--timing-function); - border: 1px solid var(--outline-color); + /* border: 1px solid var(--outline-color); */ } .sheet-max { width: 90vw; diff --git a/src/components/account.css b/src/components/account.css index 2d66c31b..f1609f7c 100644 --- a/src/components/account.css +++ b/src/components/account.css @@ -2,10 +2,55 @@ color: var(--outline-color); } +#account-container .header-banner { + pointer-events: none; + aspect-ratio: 6 / 1; + width: 100%; + height: auto; + object-fit: cover; + /* mask fade out bottom of banner */ + mask-image: linear-gradient( + to bottom, + hsl(0, 0%, 0%) 0%, + hsla(0, 0%, 0%, 0.987) 14%, + hsla(0, 0%, 0%, 0.951) 26.2%, + hsla(0, 0%, 0%, 0.896) 36.8%, + hsla(0, 0%, 0%, 0.825) 45.9%, + hsla(0, 0%, 0%, 0.741) 53.7%, + hsla(0, 0%, 0%, 0.648) 60.4%, + hsla(0, 0%, 0%, 0.55) 66.2%, + hsla(0, 0%, 0%, 0.45) 71.2%, + hsla(0, 0%, 0%, 0.352) 75.6%, + hsla(0, 0%, 0%, 0.259) 79.6%, + hsla(0, 0%, 0%, 0.175) 83.4%, + hsla(0, 0%, 0%, 0.104) 87.2%, + hsla(0, 0%, 0%, 0.049) 91.1%, + hsla(0, 0%, 0%, 0.013) 95.3%, + hsla(0, 0%, 0%, 0) 100% + ); + margin-bottom: -44px; +} + +@media (min-height: 480px) { + #account-container .header-banner { + aspect-ratio: 3 / 1; + } +} +@media (min-height: 720px) { + #account-container .header-banner { + aspect-ratio: 16 / 9; + } +} + #account-container header { + position: relative; display: flex; align-items: center; gap: 8px; + text-shadow: 0 0 24px var(--bg-color); +} +#account-container header .avatar { + box-shadow: 0 0 24px var(--bg-color); } #account-container .note { diff --git a/src/components/account.jsx b/src/components/account.jsx index 76dc18c7..8f100660 100644 --- a/src/components/account.jsx +++ b/src/components/account.jsx @@ -122,6 +122,9 @@ function Account({ account, instance: propInstance, onClose }) { ) : ( info && ( <> + {header && !/missing\.png$/.test(header) && ( + + )}
    Date: Fri, 10 Mar 2023 19:00:20 +0800 Subject: [PATCH 29/76] Add fallback image if banner image failed --- src/components/account.jsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/account.jsx b/src/components/account.jsx index 8f100660..1f787c82 100644 --- a/src/components/account.jsx +++ b/src/components/account.jsx @@ -123,7 +123,14 @@ function Account({ account, instance: propInstance, onClose }) { info && ( <> {header && !/missing\.png$/.test(header) && ( - + { + e.target.src = headerStatic; + }} + /> )}
    Date: Fri, 10 Mar 2023 19:00:48 +0800 Subject: [PATCH 30/76] Bring the image position magic to the banner too --- src/components/account.css | 5 ++++- src/components/status.css | 14 -------------- src/index.css | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/components/account.css b/src/components/account.css index f1609f7c..aa81192c 100644 --- a/src/components/account.css +++ b/src/components/account.css @@ -3,7 +3,7 @@ } #account-container .header-banner { - pointer-events: none; + /* pointer-events: none; */ aspect-ratio: 6 / 1; width: 100%; height: auto; @@ -30,6 +30,9 @@ ); margin-bottom: -44px; } +#account-container .header-banner:hover { + animation: position-object 5s ease-in-out 1s 5; +} @media (min-height: 480px) { #account-container .header-banner { diff --git a/src/components/status.css b/src/components/status.css index 1c77c1c2..cd040375 100644 --- a/src/components/status.css +++ b/src/components/status.css @@ -426,20 +426,6 @@ .status .media { cursor: pointer; } -@keyframes position-object { - 0% { - object-position: 50% 50%; - } - 25% { - object-position: 0% 0%; - } - 75% { - object-position: 100% 100%; - } - 100% { - object-position: 50% 50%; - } -} .status .media img:is(:hover, :focus), a:focus-visible .status .media img { animation: position-object 5s ease-in-out 1s 5; diff --git a/src/index.css b/src/index.css index 10f83f35..f26c71e4 100644 --- a/src/index.css +++ b/src/index.css @@ -354,3 +354,18 @@ code { transform: translateY(0); } } + +@keyframes position-object { + 0% { + object-position: 50% 50%; + } + 25% { + object-position: 0% 0%; + } + 75% { + object-position: 100% 100%; + } + 100% { + object-position: 50% 50%; + } +} From 740b0ad4972f0d3a137b06d9b3bee9ac92fb748f Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Fri, 10 Mar 2023 19:34:04 +0800 Subject: [PATCH 31/76] Sprinkle one wbr here --- src/components/status.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/status.jsx b/src/components/status.jsx index 934da50d..350a1deb 100644 --- a/src/components/status.jsx +++ b/src/components/status.jsx @@ -1576,6 +1576,7 @@ function nicePostURL(url) { {username ? ( <> /{username} + /{restPath} ) : ( From 5cd5242b9a7d852146e85face01b7ffbf738d764 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Fri, 10 Mar 2023 21:08:40 +0800 Subject: [PATCH 32/76] Just sticky to 3 / 1 ratio as advised by Mastodon --- src/components/account.css | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/components/account.css b/src/components/account.css index aa81192c..f16dc935 100644 --- a/src/components/account.css +++ b/src/components/account.css @@ -39,11 +39,6 @@ aspect-ratio: 3 / 1; } } -@media (min-height: 720px) { - #account-container .header-banner { - aspect-ratio: 16 / 9; - } -} #account-container header { position: relative; From cf437a83da9f3388657cb456f7096537894a6618 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Fri, 10 Mar 2023 23:01:26 +0800 Subject: [PATCH 33/76] Make statuses in notifications reactive --- src/pages/notifications.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/notifications.jsx b/src/pages/notifications.jsx index ec1ea480..739ea1f9 100644 --- a/src/pages/notifications.jsx +++ b/src/pages/notifications.jsx @@ -429,7 +429,7 @@ function Notification({ notification, instance }) { : `/s/${actualStatusID}` } > - + )} From b4f8f924315c3543c1b20a8dbca444dab254ab37 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Sat, 11 Mar 2023 09:13:11 +0800 Subject: [PATCH 34/76] Another bug fix --- src/utils/handle-content-links.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utils/handle-content-links.js b/src/utils/handle-content-links.js index 153cae15..792dbf3e 100644 --- a/src/utils/handle-content-links.js +++ b/src/utils/handle-content-links.js @@ -45,7 +45,9 @@ function handleContentLinks(opts) { } else if (states.unfurledLinks[target.href]?.url) { e.preventDefault(); e.stopPropagation(); - states.prevLocation = location; + states.prevLocation = { + pathname: location.hash.replace(/^#/, ''), + }; location.hash = `#${states.unfurledLinks[target.href].url}`; } }; From 6fd9c106c62fdfc86f85526c1e20d73645fe58a6 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Sat, 11 Mar 2023 14:05:56 +0800 Subject: [PATCH 35/76] Add account info into Account statuses page --- src/app.jsx | 4 +- src/components/account-block.jsx | 28 ++- src/components/account-info.css | 225 ++++++++++++++++++ .../{account.jsx => account-info.jsx} | 121 ++++++---- src/components/account-sheet.jsx | 56 +++++ src/components/account.css | 134 ----------- src/components/timeline.jsx | 2 + src/pages/account-statuses.jsx | 25 +- 8 files changed, 401 insertions(+), 194 deletions(-) create mode 100644 src/components/account-info.css rename src/components/{account.jsx => account-info.jsx} (80%) create mode 100644 src/components/account-sheet.jsx delete mode 100644 src/components/account.css diff --git a/src/app.jsx b/src/app.jsx index 350c3f12..d2a19ca4 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -16,7 +16,7 @@ import { } from 'react-router-dom'; import { useSnapshot } from 'valtio'; -import Account from './components/account'; +import AccountSheet from './components/account-sheet'; import Compose from './components/compose'; import Drafts from './components/drafts'; import Loader from './components/loader'; @@ -409,7 +409,7 @@ function App() { } }} > - { diff --git a/src/components/account-block.jsx b/src/components/account-block.jsx index 84d3792b..b4c40a0a 100644 --- a/src/components/account-block.jsx +++ b/src/components/account-block.jsx @@ -1,5 +1,7 @@ import './account-block.css'; +import { useNavigate } from 'react-router-dom'; + import emojifyText from '../utils/emojify-text'; import niceDateTime from '../utils/nice-date-time'; import states from '../utils/states'; @@ -12,6 +14,7 @@ function AccountBlock({ avatarSize = 'xl', instance, external, + internal, onClick, showActivity = false, }) { @@ -22,13 +25,16 @@ function AccountBlock({ ████████
    - @██████ +
    ); } + const navigate = useNavigate(); + const { + id, acct, avatar, avatarStatic, @@ -40,6 +46,7 @@ function AccountBlock({ lastStatusAt, } = account; const displayNameWithEmoji = emojifyText(displayName, emojis); + const [_, acct1, acct2] = acct.match(/([^@]+)(@.+)/i) || [, acct]; return ( @@ -68,7 +79,12 @@ function AccountBlock({ ) : ( {username} )} -
    @{acct} +
    + {showActivity && ( <>
    diff --git a/src/components/account-info.css b/src/components/account-info.css new file mode 100644 index 00000000..1e8ddd58 --- /dev/null +++ b/src/components/account-info.css @@ -0,0 +1,225 @@ +.account-container { + display: flex; + flex-direction: column; + overflow: hidden; + max-width: 100%; +} + +.account-container.skeleton { + color: var(--outline-color); +} + +.account-container .header-banner { + /* pointer-events: none; */ + aspect-ratio: 6 / 1; + width: 100%; + height: auto; + object-fit: cover; + /* mask fade out bottom of banner */ + mask-image: linear-gradient( + to bottom, + hsl(0, 0%, 0%) 0%, + hsla(0, 0%, 0%, 0.987) 14%, + hsla(0, 0%, 0%, 0.951) 26.2%, + hsla(0, 0%, 0%, 0.896) 36.8%, + hsla(0, 0%, 0%, 0.825) 45.9%, + hsla(0, 0%, 0%, 0.741) 53.7%, + hsla(0, 0%, 0%, 0.648) 60.4%, + hsla(0, 0%, 0%, 0.55) 66.2%, + hsla(0, 0%, 0%, 0.45) 71.2%, + hsla(0, 0%, 0%, 0.352) 75.6%, + hsla(0, 0%, 0%, 0.259) 79.6%, + hsla(0, 0%, 0%, 0.175) 83.4%, + hsla(0, 0%, 0%, 0.104) 87.2%, + hsla(0, 0%, 0%, 0.049) 91.1%, + hsla(0, 0%, 0%, 0.013) 95.3%, + hsla(0, 0%, 0%, 0) 100% + ); + margin-bottom: -44px; +} +.account-container .header-banner:hover { + animation: position-object 5s ease-in-out 1s 5; +} + +@media (min-height: 480px) { + .account-container .header-banner { + aspect-ratio: 3 / 1; + } +} + +.account-container header { + position: relative; + z-index: 1; + display: flex; + align-items: center; + gap: 8px; + text-shadow: -8px 0 12px -6px var(--bg-color), 8px 0 12px -6px var(--bg-color), + -8px 0 24px var(--header-color-3, --bg-color), + 8px 0 24px var(--header-color-4, --bg-color); +} +.account-container header .avatar { + box-shadow: -8px 0 24px var(--header-color-3, --bg-color), + 8px 0 24px var(--header-color-4, --bg-color); +} + +.account-container .note { + font-size: 95%; + line-height: 1.4; +} +.account-container .note:not(:has(p)):not(:empty) { + /* Some notes don't have

    tags, so we need to add some padding */ + padding: 1em 0; +} + +.account-container .stats { + display: flex; + flex-wrap: wrap; + justify-content: space-around; + gap: 16px; + opacity: 0.75; + font-size: 90%; + background-color: var(--bg-faded-color); + padding: 12px; + border-radius: 8px; + line-height: 1.25; +} +.account-container .stats > * { + text-align: center; +} +.account-container .stats a { + color: inherit; +} + +.account-container .actions { + display: flex; + gap: 8px; + justify-content: space-between; + min-height: 2.5em; +} +.account-container .actions button { + align-self: flex-end; +} + +.account-container .profile-metadata { + display: flex; + flex-wrap: wrap; + gap: 12px; +} +.account-container .profile-field { + min-width: 0; + flex-grow: 1; + font-size: 90%; + background-color: var(--bg-faded-color); + padding: 12px; + border-radius: 8px; + filter: saturate(0.75); + line-height: 1.25; +} + +.account-container :is(.note, .profile-field) .invisible { + display: none; +} +.account-container :is(.note, .profile-field) .ellipsis::after { + content: '…'; +} + +.account-container .profile-field b { + font-size: 90%; + color: var(--text-insignificant-color); + text-transform: uppercase; +} +.account-container .profile-field b .icon { + color: var(--green-color); +} +.account-container .profile-field p { + margin: 0; +} + +.account-container .common-followers { + border-top: 1px solid var(--outline-color); + border-bottom: 1px solid var(--outline-color); + padding: 8px 0; + font-size: 90%; + line-height: 1.5; + color: var(--text-insignificant-color); +} + +.timeline-start .account-container { + border-bottom: 1px solid var(--outline-color); +} +.timeline-start .account-container :is(header, main) { + padding: 16px 16px 4px; +} +.timeline-start .account-container .account-block .account-block-acct { + opacity: 0.5; +} +.timeline-start .account-container .actions { + min-height: 0; +} + +@keyframes shine { + 0% { + left: -100%; + } + 100% { + left: 100%; + } +} +.timeline-start .account-container { + position: relative; + overflow: hidden; +} +.timeline-start .account-container:before { + content: ''; + position: absolute; + z-index: 2; + width: 100%; + height: 100%; + background-image: linear-gradient( + 100deg, + rgba(255, 255, 255, 0) 30%, + rgba(255, 255, 255, 0.25), + rgba(255, 255, 255, 0) 70% + ); + top: 0; + left: -100%; + pointer-events: none; +} +.timeline-start .account-container:hover:before { + animation: shine 1s ease-in-out 1s; +} + +@media (min-width: 40em) { + .timeline-start .account-container { + --item-radius: 16px; + border: 1px solid var(--divider-color); + margin: 16px 0; + background-color: var(--bg-color); + border-radius: var(--item-radius); + overflow: hidden; + /* box-shadow: 0px 1px var(--bg-blur-color), 0 0 64px var(--bg-color); */ + --shadow-offset: 16px; + --shadow-blur: 32px; + --shadow-spread: calc(var(--shadow-blur) * -0.75); + box-shadow: calc(var(--shadow-offset) * -1) var(--shadow-offset) + var(--shadow-blur) var(--shadow-spread) + var(--header-color-1, var(--drop-shadow-color)), + var(--shadow-offset) var(--shadow-offset) var(--shadow-blur) + var(--shadow-spread) var(--header-color-2, var(--drop-shadow-color)); + } + .timeline-start .account-container .header-banner { + margin-bottom: -77px; + } + .timeline-start .account-container header .account-block { + font-size: 175%; + margin-bottom: -8px; + line-height: 1.1; + letter-spacing: -1px; + mix-blend-mode: multiply; + gap: 12px; + } + .timeline-start .account-container header .account-block .avatar { + width: 112px !important; + height: 112px !important; + } +} diff --git a/src/components/account.jsx b/src/components/account-info.jsx similarity index 80% rename from src/components/account.jsx rename to src/components/account-info.jsx index 1f787c82..8c84ce4e 100644 --- a/src/components/account.jsx +++ b/src/components/account-info.jsx @@ -1,7 +1,6 @@ -import './account.css'; +import './account-info.css'; import { useEffect, useRef, useState } from 'preact/hooks'; -import { useHotkeys } from 'react-hotkeys-hook'; import { api } from '../utils/api'; import emojifyText from '../utils/emojify-text'; @@ -17,49 +16,32 @@ import Avatar from './avatar'; import Icon from './icon'; import Link from './link'; -function Account({ account, instance: propInstance, onClose }) { - const { masto, instance, authenticated } = api({ instance: propInstance }); +function AccountInfo({ + account, + fetchAccount = () => {}, + standalone, + instance, + authenticated, +}) { const [uiState, setUIState] = useState('default'); const isString = typeof account === 'string'; const [info, setInfo] = useState(isString ? null : account); useEffect(() => { - if (isString) { - setUIState('loading'); - (async () => { - try { - const info = await masto.v1.accounts.lookup({ - acct: account, - skip_webfinger: false, - }); - setInfo(info); - setUIState('default'); - } catch (e) { - try { - const result = await masto.v2.search({ - q: account, - type: 'accounts', - limit: 1, - resolve: authenticated, - }); - if (result.accounts.length) { - setInfo(result.accounts[0]); - setUIState('default'); - return; - } - setInfo(null); - setUIState('error'); - } catch (err) { - console.error(err); - setInfo(null); - setUIState('error'); - } - } - })(); - } else { - setInfo(account); - } - }, [account]); + if (!isString) return; + setUIState('loading'); + (async () => { + try { + const info = await fetchAccount(); + setInfo(info); + setUIState('default'); + } catch (e) { + console.error(e); + setInfo(null); + setUIState('error'); + } + })(); + }, [isString, fetchAccount]); const { acct, @@ -84,13 +66,17 @@ function Account({ account, instance: propInstance, onClose }) { username, } = info || {}; - const escRef = useHotkeys('esc', onClose, [onClose]); + const [headerCornerColors, setHeaderCornerColors] = useState([]); return (

    {uiState === 'error' && (
    @@ -128,7 +114,47 @@ function Account({ account, instance: propInstance, onClose }) { alt="" class="header-banner" onError={(e) => { - e.target.src = headerStatic; + if (e.target.crossOrigin) { + if (e.target.src !== headerStatic) { + e.target.src = headerStatic; + } else { + e.target.removeAttribute('crossorigin'); + e.target.src = header; + } + } else if (e.target.src !== headerStatic) { + e.target.src = headerStatic; + } else { + e.target.remove(); + } + }} + crossOrigin="anonymous" + onLoad={(e) => { + try { + // Get color from four corners of image + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + canvas.width = e.target.width; + canvas.height = e.target.height; + ctx.drawImage(e.target, 0, 0); + const colors = [ + ctx.getImageData(0, 0, 1, 1).data, + ctx.getImageData(e.target.width - 1, 0, 1, 1).data, + ctx.getImageData(0, e.target.height - 1, 1, 1).data, + ctx.getImageData( + e.target.width - 1, + e.target.height - 1, + 1, + 1, + ).data, + ]; + const rgbColors = colors.map((color) => { + return `rgb(${color[0]}, ${color[1]}, ${color[2]}, 0.3)`; + }); + setHeaderCornerColors(rgbColors); + console.log({ colors, rgbColors }); + } catch (e) { + // Silently fail + } }} /> )} @@ -137,7 +163,8 @@ function Account({ account, instance: propInstance, onClose }) { account={info} instance={instance} avatarSize="xxxl" - external + external={standalone} + internal={!standalone} />
    @@ -429,4 +456,4 @@ function RelatedActions({ info, instance, authenticated }) { ); } -export default Account; +export default AccountInfo; diff --git a/src/components/account-sheet.jsx b/src/components/account-sheet.jsx new file mode 100644 index 00000000..d202866c --- /dev/null +++ b/src/components/account-sheet.jsx @@ -0,0 +1,56 @@ +import { useHotkeys } from 'react-hotkeys-hook'; + +import { api } from '../utils/api'; + +import AccountInfo from './account-info'; + +function AccountSheet({ account, instance: propInstance, onClose }) { + const { masto, instance, authenticated } = api({ instance: propInstance }); + const isString = typeof account === 'string'; + + const escRef = useHotkeys('esc', onClose, [onClose]); + + return ( +
    { + const accountBlock = e.target.closest('.account-block'); + if (accountBlock) { + onClose(); + } + }} + > + { + if (isString) { + try { + const info = await masto.v1.accounts.lookup({ + acct: account, + skip_webfinger: false, + }); + return info; + } catch (e) { + const result = await masto.v2.search({ + q: account, + type: 'accounts', + limit: 1, + resolve: authenticated, + }); + if (result.accounts.length) { + return result.accounts[0]; + } + } + } else { + return account; + } + }} + /> +
    + ); +} + +export default AccountSheet; diff --git a/src/components/account.css b/src/components/account.css deleted file mode 100644 index f16dc935..00000000 --- a/src/components/account.css +++ /dev/null @@ -1,134 +0,0 @@ -#account-container.skeleton { - color: var(--outline-color); -} - -#account-container .header-banner { - /* pointer-events: none; */ - aspect-ratio: 6 / 1; - width: 100%; - height: auto; - object-fit: cover; - /* mask fade out bottom of banner */ - mask-image: linear-gradient( - to bottom, - hsl(0, 0%, 0%) 0%, - hsla(0, 0%, 0%, 0.987) 14%, - hsla(0, 0%, 0%, 0.951) 26.2%, - hsla(0, 0%, 0%, 0.896) 36.8%, - hsla(0, 0%, 0%, 0.825) 45.9%, - hsla(0, 0%, 0%, 0.741) 53.7%, - hsla(0, 0%, 0%, 0.648) 60.4%, - hsla(0, 0%, 0%, 0.55) 66.2%, - hsla(0, 0%, 0%, 0.45) 71.2%, - hsla(0, 0%, 0%, 0.352) 75.6%, - hsla(0, 0%, 0%, 0.259) 79.6%, - hsla(0, 0%, 0%, 0.175) 83.4%, - hsla(0, 0%, 0%, 0.104) 87.2%, - hsla(0, 0%, 0%, 0.049) 91.1%, - hsla(0, 0%, 0%, 0.013) 95.3%, - hsla(0, 0%, 0%, 0) 100% - ); - margin-bottom: -44px; -} -#account-container .header-banner:hover { - animation: position-object 5s ease-in-out 1s 5; -} - -@media (min-height: 480px) { - #account-container .header-banner { - aspect-ratio: 3 / 1; - } -} - -#account-container header { - position: relative; - display: flex; - align-items: center; - gap: 8px; - text-shadow: 0 0 24px var(--bg-color); -} -#account-container header .avatar { - box-shadow: 0 0 24px var(--bg-color); -} - -#account-container .note { - font-size: 95%; - line-height: 1.4; -} -#account-container .note:not(:has(p)):not(:empty) { - /* Some notes don't have

    tags, so we need to add some padding */ - padding: 1em 0; -} - -#account-container .stats { - display: flex; - flex-wrap: wrap; - justify-content: space-around; - gap: 16px; - opacity: 0.75; - font-size: 90%; - background-color: var(--bg-faded-color); - padding: 12px; - border-radius: 8px; - line-height: 1.25; -} -#account-container .stats > * { - text-align: center; -} -#account-container .stats a { - color: inherit; -} - -#account-container .actions { - display: flex; - gap: 8px; - justify-content: space-between; - min-height: 2.5em; -} -#account-container .actions button { - align-self: flex-end; -} - -#account-container .profile-metadata { - display: flex; - flex-wrap: wrap; - gap: 12px; -} -#account-container .profile-field { - min-width: 0; - flex-grow: 1; - font-size: 90%; - background-color: var(--bg-faded-color); - padding: 12px; - border-radius: 8px; - filter: saturate(0.75); - line-height: 1.25; -} - -#account-container :is(.note, .profile-field) .invisible { - display: none; -} -#account-container :is(.note, .profile-field) .ellipsis::after { - content: '…'; -} - -#account-container .profile-field b { - font-size: 90%; - color: var(--text-insignificant-color); - text-transform: uppercase; -} -#account-container .profile-field b .icon { - color: var(--green-color); -} -#account-container .profile-field p { - margin: 0; -} - -#account-container .common-followers { - border-top: 1px solid var(--outline-color); - border-bottom: 1px solid var(--outline-color); - padding: 8px 0; - font-size: 90%; - line-height: 1.5; - color: var(--text-insignificant-color); -} diff --git a/src/components/timeline.jsx b/src/components/timeline.jsx index 491b72be..52911397 100644 --- a/src/components/timeline.jsx +++ b/src/components/timeline.jsx @@ -27,6 +27,7 @@ function Timeline({ checkForUpdatesInterval = 60_000, // 1 minute headerStart, headerEnd, + timelineStart, }) { const [items, setItems] = useState([]); const [uiState, setUIState] = useState('default'); @@ -292,6 +293,7 @@ function Timeline({ )}

    + {!!timelineStart &&
    {timelineStart}
    } {!!items.length ? ( <>