Merge branch 'cheeaun:main' into main

This commit is contained in:
Osma Ahvenlampi 2023-06-14 17:26:54 +03:00 committed by GitHub
commit aa1b2e30cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 285 additions and 152 deletions

Binary file not shown.

View file

@ -39,7 +39,7 @@
property="og:description" property="og:description"
content="Minimalistic opinionated Mastodon web client" content="Minimalistic opinionated Mastodon web client"
/> />
<meta property="og:image" content="%VITE_WEBSITE%/og-image.png" /> <meta property="og:image" content="%VITE_WEBSITE%/og-image-2.png" />
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

49
package-lock.json generated
View file

@ -12,7 +12,8 @@
"@github/text-expander-element": "~2.3.0", "@github/text-expander-element": "~2.3.0",
"@iconify-icons/mingcute": "~1.2.5", "@iconify-icons/mingcute": "~1.2.5",
"@justinribeiro/lite-youtube": "~1.5.0", "@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": "~1.11.8",
"dayjs-twitter": "~0.5.0", "dayjs-twitter": "~0.5.0",
"fast-blurhash": "~1.1.2", "fast-blurhash": "~1.1.2",
@ -3126,12 +3127,12 @@
} }
}, },
"node_modules/@szhsin/react-menu": { "node_modules/@szhsin/react-menu": {
"version": "3.5.3", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/@szhsin/react-menu/-/react-menu-3.5.3.tgz", "resolved": "https://registry.npmjs.org/@szhsin/react-menu/-/react-menu-4.0.0.tgz",
"integrity": "sha512-jxo8oaRwxmVjUzkyOi/ZJiXaZiuFPMIxFzyJdUKfnhBLYiEOVTU9M2CiPuEkirILoareR2GJj2K3y8a81CBPlw==", "integrity": "sha512-DOl+IWddgHofcEzSTJfILGvpU67O/y8r07LOVUhfThke9VEZ5LAZNkp2Q3mEFaN7PkmnmJtjPBEdIK3oN1/ZfQ==",
"dependencies": { "dependencies": {
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react-transition-state": "^1.1.5" "react-transition-state": "^2.1.0"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">=16.14.0", "react": ">=16.14.0",
@ -3271,6 +3272,18 @@
"integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==", "integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==",
"dev": true "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": { "node_modules/@vue/compiler-core": {
"version": "3.2.45", "version": "3.2.45",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.45.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.45.tgz",
@ -6334,9 +6347,9 @@
} }
}, },
"node_modules/react-transition-state": { "node_modules/react-transition-state": {
"version": "1.1.5", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/react-transition-state/-/react-transition-state-1.1.5.tgz", "resolved": "https://registry.npmjs.org/react-transition-state/-/react-transition-state-2.1.0.tgz",
"integrity": "sha512-ITY2mZqc2dWG2eitJkYNdcSFW8aKeOlkL2A/vowRrLL8GH3J6Re/SpD/BLvQzrVOTqjsP0b5S9N10vgNNzwMUQ==", "integrity": "sha512-b8ldw2pbZk++XM43vcD4ETaFWlzTsjpUX33CmT8BBPPFYlQ2R50wxcY4ZeJ1TesJYziYZ9/rNPFnyA9tR0iKDw==",
"peerDependencies": { "peerDependencies": {
"react": ">=16.8.0", "react": ">=16.8.0",
"react-dom": ">=16.8.0" "react-dom": ">=16.8.0"
@ -9619,12 +9632,12 @@
} }
}, },
"@szhsin/react-menu": { "@szhsin/react-menu": {
"version": "3.5.3", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/@szhsin/react-menu/-/react-menu-3.5.3.tgz", "resolved": "https://registry.npmjs.org/@szhsin/react-menu/-/react-menu-4.0.0.tgz",
"integrity": "sha512-jxo8oaRwxmVjUzkyOi/ZJiXaZiuFPMIxFzyJdUKfnhBLYiEOVTU9M2CiPuEkirILoareR2GJj2K3y8a81CBPlw==", "integrity": "sha512-DOl+IWddgHofcEzSTJfILGvpU67O/y8r07LOVUhfThke9VEZ5LAZNkp2Q3mEFaN7PkmnmJtjPBEdIK3oN1/ZfQ==",
"requires": { "requires": {
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react-transition-state": "^1.1.5" "react-transition-state": "^2.1.0"
} }
}, },
"@trivago/prettier-plugin-sort-imports": { "@trivago/prettier-plugin-sort-imports": {
@ -9740,6 +9753,12 @@
"integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==", "integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==",
"dev": true "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": { "@vue/compiler-core": {
"version": "3.2.45", "version": "3.2.45",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.45.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.45.tgz",
@ -11832,9 +11851,9 @@
} }
}, },
"react-transition-state": { "react-transition-state": {
"version": "1.1.5", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/react-transition-state/-/react-transition-state-1.1.5.tgz", "resolved": "https://registry.npmjs.org/react-transition-state/-/react-transition-state-2.1.0.tgz",
"integrity": "sha512-ITY2mZqc2dWG2eitJkYNdcSFW8aKeOlkL2A/vowRrLL8GH3J6Re/SpD/BLvQzrVOTqjsP0b5S9N10vgNNzwMUQ==", "integrity": "sha512-b8ldw2pbZk++XM43vcD4ETaFWlzTsjpUX33CmT8BBPPFYlQ2R50wxcY4ZeJ1TesJYziYZ9/rNPFnyA9tR0iKDw==",
"requires": {} "requires": {}
}, },
"regenerate": { "regenerate": {

View file

@ -14,7 +14,8 @@
"@github/text-expander-element": "~2.3.0", "@github/text-expander-element": "~2.3.0",
"@iconify-icons/mingcute": "~1.2.5", "@iconify-icons/mingcute": "~1.2.5",
"@justinribeiro/lite-youtube": "~1.5.0", "@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": "~1.11.8",
"dayjs-twitter": "~0.5.0", "dayjs-twitter": "~0.5.0",
"fast-blurhash": "~1.1.2", "fast-blurhash": "~1.1.2",

BIN
public/og-image-2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View file

@ -1,3 +1,7 @@
body.cloak a {
text-decoration-color: var(--link-color);
}
body.cloak .name-text, body.cloak .name-text,
body.cloak .name-text *, body.cloak .name-text *,
body.cloak .status .content-container, body.cloak .status .content-container,
@ -25,3 +29,11 @@ body.cloak .header-banner {
filter: contrast(0) !important; filter: contrast(0) !important;
background-color: #000 !important; background-color: #000 !important;
} }
/* SPECIAL CASES */
@supports (display: -webkit-box) {
body.cloak .card :is(.title, .meta) {
background-color: var(--text-color) !important;
}
}

View file

@ -2,11 +2,11 @@ import './account-block.css';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import emojifyText from '../utils/emojify-text';
import niceDateTime from '../utils/nice-date-time'; import niceDateTime from '../utils/nice-date-time';
import states from '../utils/states'; import states from '../utils/states';
import Avatar from './avatar'; import Avatar from './avatar';
import EmojiText from './emoji-text';
function AccountBlock({ function AccountBlock({
skeleton, skeleton,
@ -46,7 +46,6 @@ function AccountBlock({
lastStatusAt, lastStatusAt,
bot, bot,
} = account; } = account;
const displayNameWithEmoji = emojifyText(displayName, emojis);
const [_, acct1, acct2] = acct.match(/([^@]+)(@.+)/i) || [, acct]; const [_, acct1, acct2] = acct.match(/([^@]+)(@.+)/i) || [, acct];
return ( return (
@ -72,11 +71,9 @@ function AccountBlock({
<Avatar url={avatar} size={avatarSize} squircle={bot} /> <Avatar url={avatar} size={avatarSize} squircle={bot} />
<span> <span>
{displayName ? ( {displayName ? (
<b <b>
dangerouslySetInnerHTML={{ <EmojiText text={displayName} emojis={emojis} />
__html: displayNameWithEmoji, </b>
}}
/>
) : ( ) : (
<b>{username}</b> <b>{username}</b>
)} )}

View file

@ -38,6 +38,11 @@
margin-bottom: -44px; margin-bottom: -44px;
user-select: none; user-select: none;
-webkit-user-drag: 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 { .sheet .account-container .header-banner {
border-top-left-radius: 16px; border-top-left-radius: 16px;

View file

@ -4,7 +4,6 @@ import { Menu, MenuDivider, MenuItem, SubMenu } from '@szhsin/react-menu';
import { useEffect, useReducer, useRef, useState } from 'preact/hooks'; import { useEffect, useReducer, useRef, useState } from 'preact/hooks';
import { api } from '../utils/api'; import { api } from '../utils/api';
import emojifyText from '../utils/emojify-text';
import enhanceContent from '../utils/enhance-content'; import enhanceContent from '../utils/enhance-content';
import getHTMLText from '../utils/getHTMLText'; import getHTMLText from '../utils/getHTMLText';
import handleContentLinks from '../utils/handle-content-links'; import handleContentLinks from '../utils/handle-content-links';
@ -16,6 +15,7 @@ import store from '../utils/store';
import AccountBlock from './account-block'; import AccountBlock from './account-block';
import Avatar from './avatar'; import Avatar from './avatar';
import EmojiText from './emoji-text';
import Icon from './icon'; import Icon from './icon';
import Link from './link'; import Link from './link';
import ListAddEdit from './list-add-edit'; import ListAddEdit from './list-add-edit';
@ -186,6 +186,7 @@ function AccountInfo({
}} }}
crossOrigin="anonymous" crossOrigin="anonymous"
onLoad={(e) => { onLoad={(e) => {
e.target.classList.add('loaded');
try { try {
// Get color from four corners of image // Get color from four corners of image
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
@ -275,6 +276,13 @@ function AccountInfo({
</span> </span>
</> </>
)} )}
{group && (
<>
<span class="tag">
<Icon icon="group" /> Group
</span>
</>
)}
<div <div
class="note" class="note"
onClick={handleContentLinks({ onClick={handleContentLinks({
@ -294,11 +302,7 @@ function AccountInfo({
key={name} key={name}
> >
<b> <b>
<span <EmojiText text={name} emojis={emojis} />{' '}
dangerouslySetInnerHTML={{
__html: emojifyText(name, emojis),
}}
/>{' '}
{!!verifiedAt && <Icon icon="check-circle" size="s" />} {!!verifiedAt && <Icon icon="check-circle" size="s" />}
</b> </b>
<p <p
@ -673,7 +677,7 @@ function RelatedActions({ info, instance, authenticated }) {
openTrigger="clickOnly" openTrigger="clickOnly"
direction="bottom" direction="bottom"
overflow="auto" overflow="auto"
offsetX={-16} shift={16}
label={ label={
<> <>
<Icon icon="mute" /> <Icon icon="mute" />

View file

@ -13,6 +13,11 @@ const SIZES = {
const alphaCache = {}; 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 }) { function Avatar({ url, size, alt = '', squircle, ...props }) {
size = SIZES[size] || size || SIZES.m; size = SIZES[size] || size || SIZES.m;
const avatarRef = useRef(); const avatarRef = useRef();
@ -37,6 +42,7 @@ function Avatar({ url, size, alt = '', squircle, ...props }) {
height={size} height={size}
alt={alt} alt={alt}
loading="lazy" loading="lazy"
decoding="async"
crossOrigin={ crossOrigin={
alphaCache[url] === undefined && !isMissing alphaCache[url] === undefined && !isMissing
? 'anonymous' ? 'anonymous'
@ -54,17 +60,11 @@ function Avatar({ url, size, alt = '', squircle, ...props }) {
if (isMissing) return; if (isMissing) return;
try { try {
// Check if image has alpha channel // Check if image has alpha channel
const canvas = document.createElement('canvas'); const { width, height } = e.target;
const ctx = canvas.getContext('2d'); if (canvas.width !== width) canvas.width = width;
canvas.width = e.target.width; if (canvas.height !== height) canvas.height = height;
canvas.height = e.target.height;
ctx.drawImage(e.target, 0, 0); ctx.drawImage(e.target, 0, 0);
const allPixels = ctx.getImageData( const allPixels = ctx.getImageData(0, 0, width, height);
0,
0,
canvas.width,
canvas.height,
);
// At least 10% of pixels have alpha <= 128 // At least 10% of pixels have alpha <= 128
const hasAlpha = const hasAlpha =
allPixels.data.filter((pixel, i) => i % 4 === 3 && pixel <= 128) 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'); avatarRef.current.classList.add('has-alpha');
} }
alphaCache[url] = hasAlpha; alphaCache[url] = hasAlpha;
ctx.clearRect(0, 0, width, height);
} catch (e) { } catch (e) {
// Silent fail // Silent fail
alphaCache[url] = false; alphaCache[url] = false;

View file

@ -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(
<img
src={url}
alt={shortcode}
class="shortcode-emoji emoji"
width="12"
height="12"
loading="lazy"
decoding="async"
/>,
);
lastIndex = match.index + match[0].length;
}
});
const afterText = text.substring(lastIndex);
if (afterText) {
components.push(afterText);
}
return components;
}
export default EmojiText;

View file

@ -191,7 +191,7 @@ function MediaModal({
align="end" align="end"
position="anchor" position="anchor"
boundingBoxPadding="8 8 8 8" boundingBoxPadding="8 8 8 8"
offsetY={4} gap={4}
menuClassName="glass-menu" menuClassName="glass-menu"
menuButton={ menuButton={
<button type="button" class="carousel-button plain3"> <button type="button" class="carousel-button plain3">
@ -219,14 +219,14 @@ function MediaModal({
: '' : ''
}`} }`}
class="button carousel-button media-post-link plain3" class="button carousel-button media-post-link plain3"
onClick={() => { // onClick={() => {
// if small screen (not media query min-width 40em + 350px), run onClose // // if small screen (not media query min-width 40em + 350px), run onClose
if ( // if (
!window.matchMedia('(min-width: calc(40em + 350px))').matches // !window.matchMedia('(min-width: calc(40em + 350px))').matches
) { // ) {
onClose(); // onClose();
} // }
}} // }}
> >
<span class="button-label">See post </span>&raquo; <span class="button-label">See post </span>&raquo;
</Link> </Link>

View file

@ -1,5 +1,11 @@
import { getBlurHashAverageColor } from 'fast-blurhash'; import { getBlurHashAverageColor } from 'fast-blurhash';
import { useCallback, useMemo, useRef, useState } from 'preact/hooks'; import {
useCallback,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'preact/hooks';
import QuickPinchZoom, { make3dTransformValue } from 'react-quick-pinch-zoom'; import QuickPinchZoom, { make3dTransformValue } from 'react-quick-pinch-zoom';
import Icon from './icon'; import Icon from './icon';
@ -95,16 +101,33 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
[to], [to],
); );
if (type === 'image' || (type === 'unknown' && previewUrl && url)) { const isImage = type === 'image' || (type === 'unknown' && previewUrl);
const parentRef = useRef();
const [imageSmallerThanParent, setImageSmallerThanParent] = useState(false);
useLayoutEffect(() => {
if (!isImage) return;
if (!showOriginal) return;
if (!parentRef.current) return;
const { offsetWidth, offsetHeight } = parentRef.current;
const smaller = width < offsetWidth && height < offsetHeight;
if (smaller) setImageSmallerThanParent(smaller);
}, [width, height]);
if (isImage) {
// Note: type: unknown might not have width/height // Note: type: unknown might not have width/height
quickPinchZoomProps.containerProps.style.display = 'inherit'; quickPinchZoomProps.containerProps.style.display = 'inherit';
return ( return (
<Parent <Parent
ref={parentRef}
class={`media media-image`} class={`media media-image`}
onClick={onClick} onClick={onClick}
style={ style={
showOriginal && { showOriginal && {
backgroundImage: `url(${previewUrl})`, backgroundImage: `url(${previewUrl})`,
backgroundSize: imageSmallerThanParent
? `${width}px ${height}px`
: undefined,
} }
} }
> >

31
src/components/menu2.jsx Normal file
View file

@ -0,0 +1,31 @@
import { Menu } from '@szhsin/react-menu';
import { useWindowSize } from '@uidotdev/usehooks';
import { useRef } from 'preact/hooks';
import safeBoundingBoxPadding from '../utils/safe-bounding-box-padding';
// It's like Menu but with sensible defaults, bug fixes and improvements.
function Menu2(props) {
const { containerProps } = props;
const size = useWindowSize();
const instanceRef = useRef();
return (
<Menu
boundingBoxPadding={safeBoundingBoxPadding()}
repositionFlag={`${size.width}x${size.height}`}
{...props}
instanceRef={instanceRef}
containerProps={{
onClick: (e) => {
if (e.target === e.currentTarget) {
instanceRef.current?.closeMenu?.();
}
containerProps?.onClick?.(e);
},
...containerProps,
}}
/>
);
}
export default Menu2;

View file

@ -1,9 +1,9 @@
import './name-text.css'; import './name-text.css';
import emojifyText from '../utils/emojify-text';
import states from '../utils/states'; import states from '../utils/states';
import Avatar from './avatar'; import Avatar from './avatar';
import EmojiText from './emoji-text';
function NameText({ function NameText({
account, account,
@ -18,8 +18,6 @@ function NameText({
account; account;
let { username } = account; let { username } = account;
const displayNameWithEmoji = emojifyText(displayName, emojis);
const trimmedUsername = username.toLowerCase().trim(); const trimmedUsername = username.toLowerCase().trim();
const trimmedDisplayName = (displayName || '').toLowerCase().trim(); const trimmedDisplayName = (displayName || '').toLowerCase().trim();
const shortenedDisplayName = trimmedDisplayName const shortenedDisplayName = trimmedDisplayName
@ -58,11 +56,9 @@ function NameText({
)} )}
{displayName && !short ? ( {displayName && !short ? (
<> <>
<b <b>
dangerouslySetInnerHTML={{ <EmojiText text={displayName} emojis={emojis} />
__html: displayNameWithEmoji, </b>
}}
/>
{!showAcct && username && ( {!showAcct && username && (
<> <>
{' '} {' '}

View file

@ -1,8 +1,8 @@
import { useEffect, useRef, useState } from 'preact/hooks'; import { useEffect, useRef, useState } from 'preact/hooks';
import emojifyText from '../utils/emojify-text';
import shortenNumber from '../utils/shorten-number'; import shortenNumber from '../utils/shorten-number';
import EmojiText from './emoji-text';
import Icon from './icon'; import Icon from './icon';
import RelativeTime from './relative-time'; import RelativeTime from './relative-time';
@ -112,11 +112,9 @@ export default function Poll({
}} }}
> >
<div class="poll-option-title"> <div class="poll-option-title">
<span <span>
dangerouslySetInnerHTML={{ <EmojiText text={title} emojis={emojis} />
__html: emojifyText(title, emojis), </span>
}}
/>
{voted && ownVotes.includes(i) && ( {voted && ownVotes.includes(i) && (
<> <>
{' '} {' '}
@ -179,12 +177,9 @@ export default function Poll({
disabled={uiState === 'loading'} disabled={uiState === 'loading'}
readOnly={readOnly} readOnly={readOnly}
/> />
<span <span class="poll-option-title">
class="poll-option-title" <EmojiText text={title} emojis={emojis} />
dangerouslySetInnerHTML={{ </span>
__html: emojifyText(title, emojis),
}}
/>
</label> </label>
</div> </div>
); );

View file

@ -132,7 +132,7 @@ function Shortcuts() {
viewScroll="close" viewScroll="close"
boundingBoxPadding="8 8 8 8" boundingBoxPadding="8 8 8 8"
menuClassName="glass-menu shortcuts-menu" menuClassName="glass-menu shortcuts-menu"
offsetY={8} gap={8}
position="anchor" position="anchor"
menuButton={ menuButton={
<button <button

View file

@ -520,6 +520,9 @@
margin-inline: 0; margin-inline: 0;
padding-inline-start: 1.5em; padding-inline-start: 1.5em;
} }
.status .content ul {
list-style-type: disc;
}
.status .content .invisible { .status .content .invisible {
display: none; display: none;
} }

View file

@ -12,7 +12,13 @@ import { decodeBlurHash } from 'fast-blurhash';
import mem from 'mem'; import mem from 'mem';
import pThrottle from 'p-throttle'; import pThrottle from 'p-throttle';
import { memo } from 'preact/compat'; import { memo } from 'preact/compat';
import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'preact/hooks';
import { InView } from 'react-intersection-observer'; import { InView } from 'react-intersection-observer';
import { useLongPress } from 'use-long-press'; import { useLongPress } from 'use-long-press';
import useResizeObserver from 'use-resize-observer'; import useResizeObserver from 'use-resize-observer';
@ -20,12 +26,12 @@ import { useSnapshot } from 'valtio';
import { snapshot } from 'valtio/vanilla'; import { snapshot } from 'valtio/vanilla';
import AccountBlock from '../components/account-block'; import AccountBlock from '../components/account-block';
import EmojiText from '../components/emoji-text';
import Loader from '../components/loader'; import Loader from '../components/loader';
import Modal from '../components/modal'; import Modal from '../components/modal';
import NameText from '../components/name-text'; import NameText from '../components/name-text';
import Poll from '../components/poll'; import Poll from '../components/poll';
import { api } from '../utils/api'; import { api } from '../utils/api';
import emojifyText from '../utils/emojify-text';
import enhanceContent from '../utils/enhance-content'; import enhanceContent from '../utils/enhance-content';
import getTranslateTargetLanguage from '../utils/get-translate-target-language'; import getTranslateTargetLanguage from '../utils/get-translate-target-language';
import getHTMLText from '../utils/getHTMLText'; import getHTMLText from '../utils/getHTMLText';
@ -34,6 +40,7 @@ import htmlContentLength from '../utils/html-content-length';
import isMastodonLinkMaybe from '../utils/isMastodonLinkMaybe'; import isMastodonLinkMaybe from '../utils/isMastodonLinkMaybe';
import localeMatch from '../utils/locale-match'; import localeMatch from '../utils/locale-match';
import niceDateTime from '../utils/nice-date-time'; import niceDateTime from '../utils/nice-date-time';
import safeBoundingBoxPadding from '../utils/safe-bounding-box-padding';
import shortenNumber from '../utils/shorten-number'; import shortenNumber from '../utils/shorten-number';
import showToast from '../utils/show-toast'; import showToast from '../utils/show-toast';
import states, { getStatus, saveStatus, statusKey } from '../utils/states'; import states, { getStatus, saveStatus, statusKey } from '../utils/states';
@ -285,10 +292,14 @@ function Status({
const unauthInteractionErrorMessage = `Sorry, your current logged-in instance can't interact with this post from another instance.`; const unauthInteractionErrorMessage = `Sorry, your current logged-in instance can't interact with this post from another instance.`;
const textWeight = () => const textWeight = useCallback(
() =>
Math.max( Math.max(
Math.round((spoilerText.length + htmlContentLength(content)) / 140) || 1, Math.round((spoilerText.length + htmlContentLength(content)) / 140) ||
1, 1,
1,
),
[spoilerText, content],
); );
const createdDateText = niceDateTime(createdAtDate); const createdDateText = niceDateTime(createdAtDate);
@ -829,7 +840,7 @@ function Status({
}, },
}} }}
align="end" align="end"
offsetY={4} gap={4}
overflow="auto" overflow="auto"
viewScroll="close" viewScroll="close"
boundingBoxPadding="8 8 8 8" boundingBoxPadding="8 8 8 8"
@ -920,11 +931,9 @@ function Status({
ref={spoilerContentRef} ref={spoilerContentRef}
data-read-more={readMoreText} data-read-more={readMoreText}
> >
<p <p>
dangerouslySetInnerHTML={{ <EmojiText text={spoilerText} emojis={emojis} />
__html: emojifyText(spoilerText, emojis), </p>
}}
/>
</div> </div>
<button <button
class={`light spoiler ${showSpoiler ? 'spoiling' : ''}`} class={`light spoiler ${showSpoiler ? 'spoiling' : ''}`}
@ -1187,7 +1196,7 @@ function Status({
document.querySelector('.status-deck') || document.body, document.querySelector('.status-deck') || document.body,
}} }}
align="end" align="end"
offsetY={4} gap={4}
overflow="auto" overflow="auto"
viewScroll="close" viewScroll="close"
boundingBoxPadding="8 8 8 8" boundingBoxPadding="8 8 8 8"
@ -1822,30 +1831,6 @@ const unfurlMastodonLink = throttle(
}), }),
); );
const root = document.documentElement;
const defaultBoundingBoxPadding = 8;
function _safeBoundingBoxPadding() {
// Get safe area inset variables from root
const style = getComputedStyle(root);
const safeAreaInsetTop = style.getPropertyValue('--sai-top');
const safeAreaInsetRight = style.getPropertyValue('--sai-right');
const safeAreaInsetBottom = style.getPropertyValue('--sai-bottom');
const safeAreaInsetLeft = style.getPropertyValue('--sai-left');
const str = [
safeAreaInsetTop,
safeAreaInsetRight,
safeAreaInsetBottom,
safeAreaInsetLeft,
]
.map((v) => parseInt(v, 10) || defaultBoundingBoxPadding)
.join(' ');
// console.log(str);
return str;
}
const safeBoundingBoxPadding = mem(_safeBoundingBoxPadding, {
maxAge: 10_000, // 10 seconds
});
function FilteredStatus({ status, filterInfo, instance, containerProps = {} }) { function FilteredStatus({ status, filterInfo, instance, containerProps = {} }) {
const { const {
account: { avatar, avatarStatic, bot }, account: { avatar, avatarStatic, bot },

View file

@ -4,11 +4,12 @@ import { useParams, useSearchParams } from 'react-router-dom';
import { useSnapshot } from 'valtio'; import { useSnapshot } from 'valtio';
import AccountInfo from '../components/account-info'; import AccountInfo from '../components/account-info';
import EmojiText from '../components/emoji-text';
import Icon from '../components/icon'; import Icon from '../components/icon';
import Link from '../components/link'; import Link from '../components/link';
import Menu2 from '../components/menu2';
import Timeline from '../components/timeline'; import Timeline from '../components/timeline';
import { api } from '../utils/api'; import { api } from '../utils/api';
import emojifyText from '../utils/emojify-text';
import showToast from '../utils/show-toast'; import showToast from '../utils/show-toast';
import states from '../utils/states'; import states from '../utils/states';
import { saveStatus } from '../utils/states'; import { saveStatus } from '../utils/states';
@ -235,11 +236,9 @@ function AccountStatuses() {
// }; // };
// }} // }}
> >
<b <b>
dangerouslySetInnerHTML={{ <EmojiText text={displayName} emojis={emojis} />
__html: emojifyText(displayName, emojis), </b>
}}
/>
<div> <div>
<span>@{acct}</span> <span>@{acct}</span>
</div> </div>
@ -255,15 +254,12 @@ function AccountStatuses() {
timelineStart={TimelineStart} timelineStart={TimelineStart}
refresh={excludeReplies + excludeBoosts + tagged + media} refresh={excludeReplies + excludeBoosts + tagged + media}
headerEnd={ headerEnd={
<Menu <Menu2
portal={{ portal
target: document.body,
}}
// setDownOverflow // setDownOverflow
overflow="auto" overflow="auto"
viewScroll="close" viewScroll="close"
position="anchor" position="anchor"
boundingBoxPadding="8 8 8 8"
menuButton={ menuButton={
<button type="button" class="plain"> <button type="button" class="plain">
<Icon icon="more" size="l" /> <Icon icon="more" size="l" />
@ -295,7 +291,7 @@ function AccountStatuses() {
Switch to account's instance (<b>{accountInstance}</b>) Switch to account's instance (<b>{accountInstance}</b>)
</small> </small>
</MenuItem> </MenuItem>
</Menu> </Menu2>
} }
/> />
); );

View file

@ -9,6 +9,7 @@ import { useEffect, useRef, useState } from 'preact/hooks';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import Icon from '../components/icon'; import Icon from '../components/icon';
import Menu2 from '../components/menu2';
import Timeline from '../components/timeline'; import Timeline from '../components/timeline';
import { api } from '../utils/api'; import { api } from '../utils/api';
import showToast from '../utils/show-toast'; import showToast from '../utils/show-toast';
@ -122,15 +123,12 @@ function Hashtags(props) {
checkForUpdates={checkForUpdates} checkForUpdates={checkForUpdates}
useItemID useItemID
headerEnd={ headerEnd={
<Menu <Menu2
portal={{ portal
target: document.body,
}}
setDownOverflow setDownOverflow
overflow="auto" overflow="auto"
viewScroll="close" viewScroll="close"
position="anchor" position="anchor"
boundingBoxPadding="8 8 8 8"
menuButton={ menuButton={
<button type="button" class="plain"> <button type="button" class="plain">
<Icon icon="more" size="l" /> <Icon icon="more" size="l" />
@ -306,7 +304,7 @@ function Hashtags(props) {
> >
<Icon icon="bus" /> <span>Go to another instance</span> <Icon icon="bus" /> <span>Go to another instance</span>
</MenuItem> </MenuItem>
</Menu> </Menu2>
} }
/> />
); );

View file

@ -10,6 +10,7 @@ import AccountBlock from '../components/account-block';
import Icon from '../components/icon'; import Icon from '../components/icon';
import Link from '../components/link'; import Link from '../components/link';
import ListAddEdit from '../components/list-add-edit'; import ListAddEdit from '../components/list-add-edit';
import Menu2 from '../components/menu2';
import Modal from '../components/modal'; import Modal from '../components/modal';
import Timeline from '../components/timeline'; import Timeline from '../components/timeline';
import { api } from '../utils/api'; import { api } from '../utils/api';
@ -108,15 +109,12 @@ function List(props) {
</Link> </Link>
} }
headerEnd={ headerEnd={
<Menu <Menu2
portal={{ portal
target: document.body,
}}
setDownOverflow setDownOverflow
overflow="auto" overflow="auto"
viewScroll="close" viewScroll="close"
position="anchor" position="anchor"
boundingBoxPadding="8 8 8 8"
menuButton={ menuButton={
<button type="button" class="plain"> <button type="button" class="plain">
<Icon icon="more" size="l" /> <Icon icon="more" size="l" />
@ -137,7 +135,7 @@ function List(props) {
<Icon icon="group" size="l" /> <Icon icon="group" size="l" />
<span>Manage members</span> <span>Manage members</span>
</MenuItem> </MenuItem>
</Menu> </Menu2>
} }
/> />
{showListAddEditModal && ( {showListAddEditModal && (

View file

@ -4,6 +4,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import { useSnapshot } from 'valtio'; import { useSnapshot } from 'valtio';
import Icon from '../components/icon'; import Icon from '../components/icon';
import Menu2 from '../components/menu2';
import Timeline from '../components/timeline'; import Timeline from '../components/timeline';
import { api } from '../utils/api'; import { api } from '../utils/api';
import { filteredItems } from '../utils/filters'; import { filteredItems } from '../utils/filters';
@ -92,15 +93,12 @@ function Public({ local, ...props }) {
boostsCarousel={snapStates.settings.boostsCarousel} boostsCarousel={snapStates.settings.boostsCarousel}
allowFilters allowFilters
headerEnd={ headerEnd={
<Menu <Menu2
portal={{ portal
target: document.body,
}}
// setDownOverflow // setDownOverflow
overflow="auto" overflow="auto"
viewScroll="close" viewScroll="close"
position="anchor" position="anchor"
boundingBoxPadding="8 8 8 8"
menuButton={ menuButton={
<button type="button" class="plain"> <button type="button" class="plain">
<Icon icon="more" size="l" /> <Icon icon="more" size="l" />
@ -136,7 +134,7 @@ function Public({ local, ...props }) {
> >
<Icon icon="bus" /> <span>Go to another instance</span> <Icon icon="bus" /> <span>Go to another instance</span>
</MenuItem> </MenuItem>
</Menu> </Menu2>
} }
/> />
); );

View file

@ -7,6 +7,7 @@ import { memo } from 'preact/compat';
import { import {
useCallback, useCallback,
useEffect, useEffect,
useLayoutEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
@ -1089,7 +1090,7 @@ function SubComments({
}, []); }, []);
const detailsRef = useRef(); const detailsRef = useRef();
useEffect(() => { useLayoutEffect(() => {
function handleScroll(e) { function handleScroll(e) {
e.target.dataset.scrollLeft = e.target.scrollLeft; e.target.dataset.scrollLeft = e.target.scrollLeft;
} }

View file

@ -4,6 +4,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import { useSnapshot } from 'valtio'; import { useSnapshot } from 'valtio';
import Icon from '../components/icon'; import Icon from '../components/icon';
import Menu2 from '../components/menu2';
import Timeline from '../components/timeline'; import Timeline from '../components/timeline';
import { api } from '../utils/api'; import { api } from '../utils/api';
import { filteredItems } from '../utils/filters'; import { filteredItems } from '../utils/filters';
@ -92,15 +93,12 @@ function Trending(props) {
boostsCarousel={snapStates.settings.boostsCarousel} boostsCarousel={snapStates.settings.boostsCarousel}
allowFilters allowFilters
headerEnd={ headerEnd={
<Menu <Menu2
portal={{ portal
target: document.body,
}}
// setDownOverflow // setDownOverflow
overflow="auto" overflow="auto"
viewScroll="close" viewScroll="close"
position="anchor" position="anchor"
boundingBoxPadding="8 8 8 8"
menuButton={ menuButton={
<button type="button" class="plain"> <button type="button" class="plain">
<Icon icon="more" size="l" /> <Icon icon="more" size="l" />
@ -124,7 +122,7 @@ function Trending(props) {
> >
<Icon icon="bus" /> <span>Go to another instance</span> <Icon icon="bus" /> <span>Go to another instance</span>
</MenuItem> </MenuItem>
</Menu> </Menu2>
} }
/> />
); );

View file

@ -1,13 +1,14 @@
function emojifyText(text, emojis = []) { function emojifyText(text, emojis = []) {
if (!text) return ''; if (!text) return '';
if (!emojis.length) return text; if (!emojis.length) return text;
if (text.indexOf(':') === -1) return text;
// Replace shortcodes in text with emoji // Replace shortcodes in text with emoji
// emojis = [{ shortcode: 'smile', url: 'https://example.com/emoji.png' }] // emojis = [{ shortcode: 'smile', url: 'https://example.com/emoji.png' }]
emojis.forEach((emoji) => { emojis.forEach((emoji) => {
const { shortcode, staticUrl, url } = emoji; const { shortcode, staticUrl, url } = emoji;
text = text.replace( text = text.replace(
new RegExp(`:${shortcode}:`, 'g'), new RegExp(`:${shortcode}:`, 'g'),
`<img class="shortcode-emoji emoji" src="${url}" alt=":${shortcode}:" width="12" height="12" loading="lazy" />`, `<img class="shortcode-emoji emoji" src="${url}" alt=":${shortcode}:" width="12" height="12" loading="lazy" decoding="async" />`,
); );
}); });
// console.log(text, emojis); // console.log(text, emojis);

View file

@ -1,6 +1,8 @@
export default function isMastodonLinkMaybe(url) { export default function isMastodonLinkMaybe(url) {
const { pathname } = new URL(url); const { pathname } = new URL(url);
return ( return (
/^\/.*\/\d+$/i.test(pathname) || /^\/notes\/[a-z0-9]+$/i.test(pathname) // Misskey, Calckey /^\/.*\/\d+$/i.test(pathname) ||
/^\/@[^/]+\/statuses\/\w+$/i.test(pathname) || // GoToSocial
/^\/notes\/[a-z0-9]+$/i.test(pathname) // Misskey, Calckey
); );
} }

View file

@ -0,0 +1,27 @@
import mem from 'mem';
const root = document.documentElement;
const style = getComputedStyle(root);
const defaultBoundingBoxPadding = 8;
function _safeBoundingBoxPadding() {
// Get safe area inset variables from root
const safeAreaInsetTop = style.getPropertyValue('--sai-top');
const safeAreaInsetRight = style.getPropertyValue('--sai-right');
const safeAreaInsetBottom = style.getPropertyValue('--sai-bottom');
const safeAreaInsetLeft = style.getPropertyValue('--sai-left');
const str = [
safeAreaInsetTop,
safeAreaInsetRight,
safeAreaInsetBottom,
safeAreaInsetLeft,
]
.map((v) => parseInt(v, 10) || defaultBoundingBoxPadding)
.join(' ');
// console.log(str);
return str;
}
const safeBoundingBoxPadding = mem(_safeBoundingBoxPadding, {
maxAge: 10000, // 10 seconds
});
export default safeBoundingBoxPadding;

View file

@ -1,4 +1,4 @@
import { useEffect, useState } from 'preact/hooks'; import { useLayoutEffect, useState } from 'preact/hooks';
export default function useScroll({ export default function useScroll({
scrollableRef, scrollableRef,
@ -17,7 +17,7 @@ export default function useScroll({
const [nearReachEnd, setNearReachEnd] = useState(false); const [nearReachEnd, setNearReachEnd] = useState(false);
const isVertical = direction === 'vertical'; const isVertical = direction === 'vertical';
useEffect(() => { useLayoutEffect(() => {
const scrollableElement = scrollableRef.current; const scrollableElement = scrollableRef.current;
if (!scrollableElement) return {}; if (!scrollableElement) return {};
let previousScrollStart = isVertical let previousScrollStart = isVertical