mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-02-25 09:18:51 +01:00
Merge branch 'cheeaun:main' into main
This commit is contained in:
commit
aa1b2e30cf
29 changed files with 285 additions and 152 deletions
Binary file not shown.
|
@ -39,7 +39,7 @@
|
|||
property="og:description"
|
||||
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>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
49
package-lock.json
generated
49
package-lock.json
generated
|
@ -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": {
|
||||
|
|
|
@ -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",
|
||||
|
|
BIN
public/og-image-2.jpg
Normal file
BIN
public/og-image-2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 49 KiB |
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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({
|
|||
<Avatar url={avatar} size={avatarSize} squircle={bot} />
|
||||
<span>
|
||||
{displayName ? (
|
||||
<b
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: displayNameWithEmoji,
|
||||
}}
|
||||
/>
|
||||
<b>
|
||||
<EmojiText text={displayName} emojis={emojis} />
|
||||
</b>
|
||||
) : (
|
||||
<b>{username}</b>
|
||||
)}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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({
|
|||
</span>
|
||||
</>
|
||||
)}
|
||||
{group && (
|
||||
<>
|
||||
<span class="tag">
|
||||
<Icon icon="group" /> Group
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
class="note"
|
||||
onClick={handleContentLinks({
|
||||
|
@ -294,11 +302,7 @@ function AccountInfo({
|
|||
key={name}
|
||||
>
|
||||
<b>
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: emojifyText(name, emojis),
|
||||
}}
|
||||
/>{' '}
|
||||
<EmojiText text={name} emojis={emojis} />{' '}
|
||||
{!!verifiedAt && <Icon icon="check-circle" size="s" />}
|
||||
</b>
|
||||
<p
|
||||
|
@ -673,7 +677,7 @@ function RelatedActions({ info, instance, authenticated }) {
|
|||
openTrigger="clickOnly"
|
||||
direction="bottom"
|
||||
overflow="auto"
|
||||
offsetX={-16}
|
||||
shift={16}
|
||||
label={
|
||||
<>
|
||||
<Icon icon="mute" />
|
||||
|
|
|
@ -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;
|
||||
|
|
42
src/components/emoji-text.jsx
Normal file
42
src/components/emoji-text.jsx
Normal 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;
|
|
@ -191,7 +191,7 @@ function MediaModal({
|
|||
align="end"
|
||||
position="anchor"
|
||||
boundingBoxPadding="8 8 8 8"
|
||||
offsetY={4}
|
||||
gap={4}
|
||||
menuClassName="glass-menu"
|
||||
menuButton={
|
||||
<button type="button" class="carousel-button plain3">
|
||||
|
@ -219,14 +219,14 @@ function MediaModal({
|
|||
: ''
|
||||
}`}
|
||||
class="button carousel-button media-post-link plain3"
|
||||
onClick={() => {
|
||||
// if small screen (not media query min-width 40em + 350px), run onClose
|
||||
if (
|
||||
!window.matchMedia('(min-width: calc(40em + 350px))').matches
|
||||
) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
// onClick={() => {
|
||||
// // if small screen (not media query min-width 40em + 350px), run onClose
|
||||
// if (
|
||||
// !window.matchMedia('(min-width: calc(40em + 350px))').matches
|
||||
// ) {
|
||||
// onClose();
|
||||
// }
|
||||
// }}
|
||||
>
|
||||
<span class="button-label">See post </span>»
|
||||
</Link>
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
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 Icon from './icon';
|
||||
|
@ -95,16 +101,33 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
|
|||
[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
|
||||
quickPinchZoomProps.containerProps.style.display = 'inherit';
|
||||
return (
|
||||
<Parent
|
||||
ref={parentRef}
|
||||
class={`media media-image`}
|
||||
onClick={onClick}
|
||||
style={
|
||||
showOriginal && {
|
||||
backgroundImage: `url(${previewUrl})`,
|
||||
backgroundSize: imageSmallerThanParent
|
||||
? `${width}px ${height}px`
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
|
|
31
src/components/menu2.jsx
Normal file
31
src/components/menu2.jsx
Normal 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;
|
|
@ -1,9 +1,9 @@
|
|||
import './name-text.css';
|
||||
|
||||
import emojifyText from '../utils/emojify-text';
|
||||
import states from '../utils/states';
|
||||
|
||||
import Avatar from './avatar';
|
||||
import EmojiText from './emoji-text';
|
||||
|
||||
function NameText({
|
||||
account,
|
||||
|
@ -18,8 +18,6 @@ function NameText({
|
|||
account;
|
||||
let { username } = account;
|
||||
|
||||
const displayNameWithEmoji = emojifyText(displayName, emojis);
|
||||
|
||||
const trimmedUsername = username.toLowerCase().trim();
|
||||
const trimmedDisplayName = (displayName || '').toLowerCase().trim();
|
||||
const shortenedDisplayName = trimmedDisplayName
|
||||
|
@ -58,11 +56,9 @@ function NameText({
|
|||
)}
|
||||
{displayName && !short ? (
|
||||
<>
|
||||
<b
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: displayNameWithEmoji,
|
||||
}}
|
||||
/>
|
||||
<b>
|
||||
<EmojiText text={displayName} emojis={emojis} />
|
||||
</b>
|
||||
{!showAcct && username && (
|
||||
<>
|
||||
{' '}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
|
||||
import emojifyText from '../utils/emojify-text';
|
||||
import shortenNumber from '../utils/shorten-number';
|
||||
|
||||
import EmojiText from './emoji-text';
|
||||
import Icon from './icon';
|
||||
import RelativeTime from './relative-time';
|
||||
|
||||
|
@ -112,11 +112,9 @@ export default function Poll({
|
|||
}}
|
||||
>
|
||||
<div class="poll-option-title">
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: emojifyText(title, emojis),
|
||||
}}
|
||||
/>
|
||||
<span>
|
||||
<EmojiText text={title} emojis={emojis} />
|
||||
</span>
|
||||
{voted && ownVotes.includes(i) && (
|
||||
<>
|
||||
{' '}
|
||||
|
@ -179,12 +177,9 @@ export default function Poll({
|
|||
disabled={uiState === 'loading'}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
<span
|
||||
class="poll-option-title"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: emojifyText(title, emojis),
|
||||
}}
|
||||
/>
|
||||
<span class="poll-option-title">
|
||||
<EmojiText text={title} emojis={emojis} />
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -132,7 +132,7 @@ function Shortcuts() {
|
|||
viewScroll="close"
|
||||
boundingBoxPadding="8 8 8 8"
|
||||
menuClassName="glass-menu shortcuts-menu"
|
||||
offsetY={8}
|
||||
gap={8}
|
||||
position="anchor"
|
||||
menuButton={
|
||||
<button
|
||||
|
|
|
@ -520,6 +520,9 @@
|
|||
margin-inline: 0;
|
||||
padding-inline-start: 1.5em;
|
||||
}
|
||||
.status .content ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
.status .content .invisible {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -12,7 +12,13 @@ import { decodeBlurHash } from 'fast-blurhash';
|
|||
import mem from 'mem';
|
||||
import pThrottle from 'p-throttle';
|
||||
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 { useLongPress } from 'use-long-press';
|
||||
import useResizeObserver from 'use-resize-observer';
|
||||
|
@ -20,12 +26,12 @@ import { useSnapshot } from 'valtio';
|
|||
import { snapshot } from 'valtio/vanilla';
|
||||
|
||||
import AccountBlock from '../components/account-block';
|
||||
import EmojiText from '../components/emoji-text';
|
||||
import Loader from '../components/loader';
|
||||
import Modal from '../components/modal';
|
||||
import NameText from '../components/name-text';
|
||||
import Poll from '../components/poll';
|
||||
import { api } from '../utils/api';
|
||||
import emojifyText from '../utils/emojify-text';
|
||||
import enhanceContent from '../utils/enhance-content';
|
||||
import getTranslateTargetLanguage from '../utils/get-translate-target-language';
|
||||
import getHTMLText from '../utils/getHTMLText';
|
||||
|
@ -34,6 +40,7 @@ import htmlContentLength from '../utils/html-content-length';
|
|||
import isMastodonLinkMaybe from '../utils/isMastodonLinkMaybe';
|
||||
import localeMatch from '../utils/locale-match';
|
||||
import niceDateTime from '../utils/nice-date-time';
|
||||
import safeBoundingBoxPadding from '../utils/safe-bounding-box-padding';
|
||||
import shortenNumber from '../utils/shorten-number';
|
||||
import showToast from '../utils/show-toast';
|
||||
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 textWeight = () =>
|
||||
const textWeight = useCallback(
|
||||
() =>
|
||||
Math.max(
|
||||
Math.round((spoilerText.length + htmlContentLength(content)) / 140) || 1,
|
||||
Math.round((spoilerText.length + htmlContentLength(content)) / 140) ||
|
||||
1,
|
||||
1,
|
||||
),
|
||||
[spoilerText, content],
|
||||
);
|
||||
|
||||
const createdDateText = niceDateTime(createdAtDate);
|
||||
|
@ -829,7 +840,7 @@ function Status({
|
|||
},
|
||||
}}
|
||||
align="end"
|
||||
offsetY={4}
|
||||
gap={4}
|
||||
overflow="auto"
|
||||
viewScroll="close"
|
||||
boundingBoxPadding="8 8 8 8"
|
||||
|
@ -920,11 +931,9 @@ function Status({
|
|||
ref={spoilerContentRef}
|
||||
data-read-more={readMoreText}
|
||||
>
|
||||
<p
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: emojifyText(spoilerText, emojis),
|
||||
}}
|
||||
/>
|
||||
<p>
|
||||
<EmojiText text={spoilerText} emojis={emojis} />
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class={`light spoiler ${showSpoiler ? 'spoiling' : ''}`}
|
||||
|
@ -1187,7 +1196,7 @@ function Status({
|
|||
document.querySelector('.status-deck') || document.body,
|
||||
}}
|
||||
align="end"
|
||||
offsetY={4}
|
||||
gap={4}
|
||||
overflow="auto"
|
||||
viewScroll="close"
|
||||
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 = {} }) {
|
||||
const {
|
||||
account: { avatar, avatarStatic, bot },
|
||||
|
|
|
@ -4,11 +4,12 @@ import { useParams, useSearchParams } from 'react-router-dom';
|
|||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import AccountInfo from '../components/account-info';
|
||||
import EmojiText from '../components/emoji-text';
|
||||
import Icon from '../components/icon';
|
||||
import Link from '../components/link';
|
||||
import Menu2 from '../components/menu2';
|
||||
import Timeline from '../components/timeline';
|
||||
import { api } from '../utils/api';
|
||||
import emojifyText from '../utils/emojify-text';
|
||||
import showToast from '../utils/show-toast';
|
||||
import states from '../utils/states';
|
||||
import { saveStatus } from '../utils/states';
|
||||
|
@ -235,11 +236,9 @@ function AccountStatuses() {
|
|||
// };
|
||||
// }}
|
||||
>
|
||||
<b
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: emojifyText(displayName, emojis),
|
||||
}}
|
||||
/>
|
||||
<b>
|
||||
<EmojiText text={displayName} emojis={emojis} />
|
||||
</b>
|
||||
<div>
|
||||
<span>@{acct}</span>
|
||||
</div>
|
||||
|
@ -255,15 +254,12 @@ function AccountStatuses() {
|
|||
timelineStart={TimelineStart}
|
||||
refresh={excludeReplies + excludeBoosts + tagged + media}
|
||||
headerEnd={
|
||||
<Menu
|
||||
portal={{
|
||||
target: document.body,
|
||||
}}
|
||||
<Menu2
|
||||
portal
|
||||
// setDownOverflow
|
||||
overflow="auto"
|
||||
viewScroll="close"
|
||||
position="anchor"
|
||||
boundingBoxPadding="8 8 8 8"
|
||||
menuButton={
|
||||
<button type="button" class="plain">
|
||||
<Icon icon="more" size="l" />
|
||||
|
@ -295,7 +291,7 @@ function AccountStatuses() {
|
|||
Switch to account's instance (<b>{accountInstance}</b>)
|
||||
</small>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Menu2>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -9,6 +9,7 @@ import { useEffect, useRef, useState } from 'preact/hooks';
|
|||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import Icon from '../components/icon';
|
||||
import Menu2 from '../components/menu2';
|
||||
import Timeline from '../components/timeline';
|
||||
import { api } from '../utils/api';
|
||||
import showToast from '../utils/show-toast';
|
||||
|
@ -122,15 +123,12 @@ function Hashtags(props) {
|
|||
checkForUpdates={checkForUpdates}
|
||||
useItemID
|
||||
headerEnd={
|
||||
<Menu
|
||||
portal={{
|
||||
target: document.body,
|
||||
}}
|
||||
<Menu2
|
||||
portal
|
||||
setDownOverflow
|
||||
overflow="auto"
|
||||
viewScroll="close"
|
||||
position="anchor"
|
||||
boundingBoxPadding="8 8 8 8"
|
||||
menuButton={
|
||||
<button type="button" class="plain">
|
||||
<Icon icon="more" size="l" />
|
||||
|
@ -306,7 +304,7 @@ function Hashtags(props) {
|
|||
>
|
||||
<Icon icon="bus" /> <span>Go to another instance…</span>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Menu2>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -10,6 +10,7 @@ import AccountBlock from '../components/account-block';
|
|||
import Icon from '../components/icon';
|
||||
import Link from '../components/link';
|
||||
import ListAddEdit from '../components/list-add-edit';
|
||||
import Menu2 from '../components/menu2';
|
||||
import Modal from '../components/modal';
|
||||
import Timeline from '../components/timeline';
|
||||
import { api } from '../utils/api';
|
||||
|
@ -108,15 +109,12 @@ function List(props) {
|
|||
</Link>
|
||||
}
|
||||
headerEnd={
|
||||
<Menu
|
||||
portal={{
|
||||
target: document.body,
|
||||
}}
|
||||
<Menu2
|
||||
portal
|
||||
setDownOverflow
|
||||
overflow="auto"
|
||||
viewScroll="close"
|
||||
position="anchor"
|
||||
boundingBoxPadding="8 8 8 8"
|
||||
menuButton={
|
||||
<button type="button" class="plain">
|
||||
<Icon icon="more" size="l" />
|
||||
|
@ -137,7 +135,7 @@ function List(props) {
|
|||
<Icon icon="group" size="l" />
|
||||
<span>Manage members</span>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Menu2>
|
||||
}
|
||||
/>
|
||||
{showListAddEditModal && (
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useNavigate, useParams } from 'react-router-dom';
|
|||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import Icon from '../components/icon';
|
||||
import Menu2 from '../components/menu2';
|
||||
import Timeline from '../components/timeline';
|
||||
import { api } from '../utils/api';
|
||||
import { filteredItems } from '../utils/filters';
|
||||
|
@ -92,15 +93,12 @@ function Public({ local, ...props }) {
|
|||
boostsCarousel={snapStates.settings.boostsCarousel}
|
||||
allowFilters
|
||||
headerEnd={
|
||||
<Menu
|
||||
portal={{
|
||||
target: document.body,
|
||||
}}
|
||||
<Menu2
|
||||
portal
|
||||
// setDownOverflow
|
||||
overflow="auto"
|
||||
viewScroll="close"
|
||||
position="anchor"
|
||||
boundingBoxPadding="8 8 8 8"
|
||||
menuButton={
|
||||
<button type="button" class="plain">
|
||||
<Icon icon="more" size="l" />
|
||||
|
@ -136,7 +134,7 @@ function Public({ local, ...props }) {
|
|||
>
|
||||
<Icon icon="bus" /> <span>Go to another instance…</span>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Menu2>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -7,6 +7,7 @@ import { memo } from 'preact/compat';
|
|||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
|
@ -1089,7 +1090,7 @@ function SubComments({
|
|||
}, []);
|
||||
|
||||
const detailsRef = useRef();
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
function handleScroll(e) {
|
||||
e.target.dataset.scrollLeft = e.target.scrollLeft;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useNavigate, useParams } from 'react-router-dom';
|
|||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import Icon from '../components/icon';
|
||||
import Menu2 from '../components/menu2';
|
||||
import Timeline from '../components/timeline';
|
||||
import { api } from '../utils/api';
|
||||
import { filteredItems } from '../utils/filters';
|
||||
|
@ -92,15 +93,12 @@ function Trending(props) {
|
|||
boostsCarousel={snapStates.settings.boostsCarousel}
|
||||
allowFilters
|
||||
headerEnd={
|
||||
<Menu
|
||||
portal={{
|
||||
target: document.body,
|
||||
}}
|
||||
<Menu2
|
||||
portal
|
||||
// setDownOverflow
|
||||
overflow="auto"
|
||||
viewScroll="close"
|
||||
position="anchor"
|
||||
boundingBoxPadding="8 8 8 8"
|
||||
menuButton={
|
||||
<button type="button" class="plain">
|
||||
<Icon icon="more" size="l" />
|
||||
|
@ -124,7 +122,7 @@ function Trending(props) {
|
|||
>
|
||||
<Icon icon="bus" /> <span>Go to another instance…</span>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Menu2>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
function emojifyText(text, emojis = []) {
|
||||
if (!text) return '';
|
||||
if (!emojis.length) return text;
|
||||
if (text.indexOf(':') === -1) return text;
|
||||
// Replace shortcodes in text with emoji
|
||||
// emojis = [{ shortcode: 'smile', url: 'https://example.com/emoji.png' }]
|
||||
emojis.forEach((emoji) => {
|
||||
const { shortcode, staticUrl, url } = emoji;
|
||||
text = text.replace(
|
||||
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);
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
export default function isMastodonLinkMaybe(url) {
|
||||
const { pathname } = new URL(url);
|
||||
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
|
||||
);
|
||||
}
|
||||
|
|
27
src/utils/safe-bounding-box-padding.jsx
Normal file
27
src/utils/safe-bounding-box-padding.jsx
Normal 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;
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useState } from 'preact/hooks';
|
||||
import { useLayoutEffect, useState } from 'preact/hooks';
|
||||
|
||||
export default function useScroll({
|
||||
scrollableRef,
|
||||
|
@ -17,7 +17,7 @@ export default function useScroll({
|
|||
const [nearReachEnd, setNearReachEnd] = useState(false);
|
||||
const isVertical = direction === 'vertical';
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
const scrollableElement = scrollableRef.current;
|
||||
if (!scrollableElement) return {};
|
||||
let previousScrollStart = isVertical
|
||||
|
|
Loading…
Reference in a new issue