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"
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
View file

@ -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": {

View file

@ -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

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 .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;
}
}

View file

@ -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>
)}

View file

@ -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;

View file

@ -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" />

View file

@ -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;

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"
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>&raquo;
</Link>

View file

@ -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
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 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 && (
<>
{' '}

View file

@ -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>
);

View file

@ -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

View file

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

View file

@ -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,11 +292,15 @@ function Status({
const unauthInteractionErrorMessage = `Sorry, your current logged-in instance can't interact with this post from another instance.`;
const textWeight = () =>
Math.max(
Math.round((spoilerText.length + htmlContentLength(content)) / 140) || 1,
1,
);
const textWeight = useCallback(
() =>
Math.max(
Math.round((spoilerText.length + htmlContentLength(content)) / 140) ||
1,
1,
),
[spoilerText, content],
);
const createdDateText = niceDateTime(createdAtDate);
const editedDateText = editedAt && niceDateTime(editedAtDate);
@ -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 },

View file

@ -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>
}
/>
);

View file

@ -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>
}
/>
);

View file

@ -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 && (

View file

@ -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>
}
/>
);

View file

@ -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;
}

View file

@ -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>
}
/>
);

View file

@ -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);

View file

@ -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
);
}

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({
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