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"
|
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
49
package-lock.json
generated
|
@ -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": {
|
||||||
|
|
|
@ -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
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 .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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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;
|
||||||
|
|
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"
|
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>»
|
<span class="button-label">See post </span>»
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -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
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 './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 && (
|
||||||
<>
|
<>
|
||||||
{' '}
|
{' '}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,11 +292,15 @@ 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.round((spoilerText.length + htmlContentLength(content)) / 140) || 1,
|
Math.max(
|
||||||
1,
|
Math.round((spoilerText.length + htmlContentLength(content)) / 140) ||
|
||||||
);
|
1,
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
[spoilerText, content],
|
||||||
|
);
|
||||||
|
|
||||||
const createdDateText = niceDateTime(createdAtDate);
|
const createdDateText = niceDateTime(createdAtDate);
|
||||||
const editedDateText = editedAt && niceDateTime(editedAtDate);
|
const editedDateText = editedAt && niceDateTime(editedAtDate);
|
||||||
|
@ -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 },
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 && (
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
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({
|
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
|
||||||
|
|
Loading…
Reference in a new issue