diff --git a/design/logo.afdesign b/design/logo.afdesign index 6549ae9d..60b1cc27 100644 Binary files a/design/logo.afdesign and b/design/logo.afdesign differ diff --git a/index.html b/index.html index c0103ab8..bcf4c312 100644 --- a/index.html +++ b/index.html @@ -39,7 +39,7 @@ property="og:description" content="Minimalistic opinionated Mastodon web client" /> - +
diff --git a/package-lock.json b/package-lock.json index 0d3bb36e..619956a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "@github/text-expander-element": "~2.3.0", "@iconify-icons/mingcute": "~1.2.5", "@justinribeiro/lite-youtube": "~1.5.0", - "@szhsin/react-menu": "~3.5.3", + "@szhsin/react-menu": "~4.0.0", + "@uidotdev/usehooks": "~2.0.1", "dayjs": "~1.11.8", "dayjs-twitter": "~0.5.0", "fast-blurhash": "~1.1.2", @@ -3126,12 +3127,12 @@ } }, "node_modules/@szhsin/react-menu": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@szhsin/react-menu/-/react-menu-3.5.3.tgz", - "integrity": "sha512-jxo8oaRwxmVjUzkyOi/ZJiXaZiuFPMIxFzyJdUKfnhBLYiEOVTU9M2CiPuEkirILoareR2GJj2K3y8a81CBPlw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@szhsin/react-menu/-/react-menu-4.0.0.tgz", + "integrity": "sha512-DOl+IWddgHofcEzSTJfILGvpU67O/y8r07LOVUhfThke9VEZ5LAZNkp2Q3mEFaN7PkmnmJtjPBEdIK3oN1/ZfQ==", "dependencies": { "prop-types": "^15.7.2", - "react-transition-state": "^1.1.5" + "react-transition-state": "^2.1.0" }, "peerDependencies": { "react": ">=16.14.0", @@ -3271,6 +3272,18 @@ "integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==", "dev": true }, + "node_modules/@uidotdev/usehooks": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@uidotdev/usehooks/-/usehooks-2.0.1.tgz", + "integrity": "sha512-rJXxE3Y8g9utRbOS9Pj9tIvrnOdaakHIhLbMxBlErV8HydnGD0DveD82aLBfVTh1hBp5IXqpeHpMrPE9WIT7vQ==", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, "node_modules/@vue/compiler-core": { "version": "3.2.45", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.45.tgz", @@ -6334,9 +6347,9 @@ } }, "node_modules/react-transition-state": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/react-transition-state/-/react-transition-state-1.1.5.tgz", - "integrity": "sha512-ITY2mZqc2dWG2eitJkYNdcSFW8aKeOlkL2A/vowRrLL8GH3J6Re/SpD/BLvQzrVOTqjsP0b5S9N10vgNNzwMUQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/react-transition-state/-/react-transition-state-2.1.0.tgz", + "integrity": "sha512-b8ldw2pbZk++XM43vcD4ETaFWlzTsjpUX33CmT8BBPPFYlQ2R50wxcY4ZeJ1TesJYziYZ9/rNPFnyA9tR0iKDw==", "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" @@ -9619,12 +9632,12 @@ } }, "@szhsin/react-menu": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@szhsin/react-menu/-/react-menu-3.5.3.tgz", - "integrity": "sha512-jxo8oaRwxmVjUzkyOi/ZJiXaZiuFPMIxFzyJdUKfnhBLYiEOVTU9M2CiPuEkirILoareR2GJj2K3y8a81CBPlw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@szhsin/react-menu/-/react-menu-4.0.0.tgz", + "integrity": "sha512-DOl+IWddgHofcEzSTJfILGvpU67O/y8r07LOVUhfThke9VEZ5LAZNkp2Q3mEFaN7PkmnmJtjPBEdIK3oN1/ZfQ==", "requires": { "prop-types": "^15.7.2", - "react-transition-state": "^1.1.5" + "react-transition-state": "^2.1.0" } }, "@trivago/prettier-plugin-sort-imports": { @@ -9740,6 +9753,12 @@ "integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==", "dev": true }, + "@uidotdev/usehooks": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@uidotdev/usehooks/-/usehooks-2.0.1.tgz", + "integrity": "sha512-rJXxE3Y8g9utRbOS9Pj9tIvrnOdaakHIhLbMxBlErV8HydnGD0DveD82aLBfVTh1hBp5IXqpeHpMrPE9WIT7vQ==", + "requires": {} + }, "@vue/compiler-core": { "version": "3.2.45", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.45.tgz", @@ -11832,9 +11851,9 @@ } }, "react-transition-state": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/react-transition-state/-/react-transition-state-1.1.5.tgz", - "integrity": "sha512-ITY2mZqc2dWG2eitJkYNdcSFW8aKeOlkL2A/vowRrLL8GH3J6Re/SpD/BLvQzrVOTqjsP0b5S9N10vgNNzwMUQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/react-transition-state/-/react-transition-state-2.1.0.tgz", + "integrity": "sha512-b8ldw2pbZk++XM43vcD4ETaFWlzTsjpUX33CmT8BBPPFYlQ2R50wxcY4ZeJ1TesJYziYZ9/rNPFnyA9tR0iKDw==", "requires": {} }, "regenerate": { diff --git a/package.json b/package.json index 715884bd..e1234e6e 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "@github/text-expander-element": "~2.3.0", "@iconify-icons/mingcute": "~1.2.5", "@justinribeiro/lite-youtube": "~1.5.0", - "@szhsin/react-menu": "~3.5.3", + "@szhsin/react-menu": "~4.0.0", + "@uidotdev/usehooks": "~2.0.1", "dayjs": "~1.11.8", "dayjs-twitter": "~0.5.0", "fast-blurhash": "~1.1.2", diff --git a/public/og-image-2.jpg b/public/og-image-2.jpg new file mode 100644 index 00000000..9bc10c7b Binary files /dev/null and b/public/og-image-2.jpg differ diff --git a/src/cloak-mode.css b/src/cloak-mode.css index d97a6d44..ee68dd1f 100644 --- a/src/cloak-mode.css +++ b/src/cloak-mode.css @@ -1,3 +1,7 @@ +body.cloak a { + text-decoration-color: var(--link-color); +} + body.cloak .name-text, body.cloak .name-text *, body.cloak .status .content-container, @@ -25,3 +29,11 @@ body.cloak .header-banner { filter: contrast(0) !important; background-color: #000 !important; } + +/* SPECIAL CASES */ + +@supports (display: -webkit-box) { + body.cloak .card :is(.title, .meta) { + background-color: var(--text-color) !important; + } +} diff --git a/src/components/account-block.jsx b/src/components/account-block.jsx index a8598284..a9b93da2 100644 --- a/src/components/account-block.jsx +++ b/src/components/account-block.jsx @@ -2,11 +2,11 @@ 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'; import Avatar from './avatar'; +import EmojiText from './emoji-text'; function AccountBlock({ skeleton, @@ -46,7 +46,6 @@ function AccountBlock({ lastStatusAt, bot, } = account; - const displayNameWithEmoji = emojifyText(displayName, emojis); const [_, acct1, acct2] = acct.match(/([^@]+)(@.+)/i) || [, acct]; return ( @@ -72,11 +71,9 @@ function AccountBlock({ {displayName ? ( - + + + ) : ( {username} )} diff --git a/src/components/account-info.css b/src/components/account-info.css index ad10b696..526059b1 100644 --- a/src/components/account-info.css +++ b/src/components/account-info.css @@ -38,6 +38,11 @@ margin-bottom: -44px; user-select: none; -webkit-user-drag: none; + opacity: 0; + transition: opacity 0.3s ease-out; +} +.account-container .header-banner.loaded { + opacity: 1; } .sheet .account-container .header-banner { border-top-left-radius: 16px; diff --git a/src/components/account-info.jsx b/src/components/account-info.jsx index 751c062d..91fd45af 100644 --- a/src/components/account-info.jsx +++ b/src/components/account-info.jsx @@ -4,7 +4,6 @@ import { Menu, MenuDivider, MenuItem, SubMenu } from '@szhsin/react-menu'; import { useEffect, useReducer, useRef, useState } from 'preact/hooks'; import { api } from '../utils/api'; -import emojifyText from '../utils/emojify-text'; import enhanceContent from '../utils/enhance-content'; import getHTMLText from '../utils/getHTMLText'; import handleContentLinks from '../utils/handle-content-links'; @@ -16,6 +15,7 @@ import store from '../utils/store'; import AccountBlock from './account-block'; import Avatar from './avatar'; +import EmojiText from './emoji-text'; import Icon from './icon'; import Link from './link'; import ListAddEdit from './list-add-edit'; @@ -186,6 +186,7 @@ function AccountInfo({ }} crossOrigin="anonymous" onLoad={(e) => { + e.target.classList.add('loaded'); try { // Get color from four corners of image const canvas = document.createElement('canvas'); @@ -275,6 +276,13 @@ function AccountInfo({ )} + {group && ( + <> + + Group + + + )}
- {' '} + {' '} {!!verifiedAt && }

diff --git a/src/components/avatar.jsx b/src/components/avatar.jsx index a9fef01d..3c30d4bc 100644 --- a/src/components/avatar.jsx +++ b/src/components/avatar.jsx @@ -13,6 +13,11 @@ const SIZES = { const alphaCache = {}; +const canvas = window.OffscreenCanvas + ? new OffscreenCanvas(1, 1) + : document.createElement('canvas'); +const ctx = canvas.getContext('2d'); + function Avatar({ url, size, alt = '', squircle, ...props }) { size = SIZES[size] || size || SIZES.m; const avatarRef = useRef(); @@ -37,6 +42,7 @@ function Avatar({ url, size, alt = '', squircle, ...props }) { height={size} alt={alt} loading="lazy" + decoding="async" crossOrigin={ alphaCache[url] === undefined && !isMissing ? 'anonymous' @@ -54,17 +60,11 @@ function Avatar({ url, size, alt = '', squircle, ...props }) { if (isMissing) return; try { // Check if image has alpha channel - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - canvas.width = e.target.width; - canvas.height = e.target.height; + const { width, height } = e.target; + if (canvas.width !== width) canvas.width = width; + if (canvas.height !== height) canvas.height = height; ctx.drawImage(e.target, 0, 0); - const allPixels = ctx.getImageData( - 0, - 0, - canvas.width, - canvas.height, - ); + const allPixels = ctx.getImageData(0, 0, width, height); // At least 10% of pixels have alpha <= 128 const hasAlpha = allPixels.data.filter((pixel, i) => i % 4 === 3 && pixel <= 128) @@ -76,6 +76,7 @@ function Avatar({ url, size, alt = '', squircle, ...props }) { avatarRef.current.classList.add('has-alpha'); } alphaCache[url] = hasAlpha; + ctx.clearRect(0, 0, width, height); } catch (e) { // Silent fail alphaCache[url] = false; diff --git a/src/components/emoji-text.jsx b/src/components/emoji-text.jsx new file mode 100644 index 00000000..041f3c62 --- /dev/null +++ b/src/components/emoji-text.jsx @@ -0,0 +1,42 @@ +function EmojiText({ text, emojis }) { + if (!text) return ''; + if (!emojis?.length) return text; + if (text.indexOf(':') === -1) return text; + + const components = []; + let lastIndex = 0; + + emojis.forEach((shortcodeObj) => { + const { shortcode, staticUrl, url } = shortcodeObj; + const regex = new RegExp(`:${shortcode}:`, 'g'); + let match; + + while ((match = regex.exec(text))) { + const beforeText = text.substring(lastIndex, match.index); + if (beforeText) { + components.push(beforeText); + } + components.push( + {shortcode}, + ); + lastIndex = match.index + match[0].length; + } + }); + + const afterText = text.substring(lastIndex); + if (afterText) { + components.push(afterText); + } + + return components; +} + +export default EmojiText; diff --git a/src/components/media-modal.jsx b/src/components/media-modal.jsx index 8f569aa5..0c70fbbc 100644 --- a/src/components/media-modal.jsx +++ b/src/components/media-modal.jsx @@ -191,7 +191,7 @@ function MediaModal({ align="end" position="anchor" boundingBoxPadding="8 8 8 8" - offsetY={4} + gap={4} menuClassName="glass-menu" menuButton={