Merge pull request #450 from cheeaun/main

Update from main
This commit is contained in:
Chee Aun 2024-03-11 17:14:49 +08:00 committed by GitHub
commit d34cce6d18
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 293 additions and 42 deletions

View file

@ -1788,6 +1788,7 @@ body > .szh-menu-container {
animation-duration: 0.3s;
animation-timing-function: ease-in-out;
width: auto;
min-width: min(12em, 90vw);
}
.szh-menu .footer {
margin: 8px 0 -8px;

View file

@ -103,4 +103,5 @@ export const ICONS = {
'arrows-right': () => import('@iconify-icons/mingcute/arrows-right-line'),
code: () => import('@iconify-icons/mingcute/code-line'),
copy: () => import('@iconify-icons/mingcute/copy-2-line'),
quote: () => import('@iconify-icons/mingcute/quote-left-line'),
};

View file

@ -453,12 +453,15 @@ function AccountInfo({
e.target.classList.add('loaded');
try {
// Get color from four corners of image
const canvas = document.createElement('canvas');
const canvas = window.OffscreenCanvas
? new OffscreenCanvas(1, 1)
: document.createElement('canvas');
const ctx = canvas.getContext('2d', {
willReadFrequently: true,
});
canvas.width = e.target.width;
canvas.height = e.target.height;
ctx.imageSmoothingEnabled = false;
ctx.drawImage(e.target, 0, 0);
// const colors = [
// ctx.getImageData(0, 0, 1, 1).data,

View file

@ -21,6 +21,7 @@ const canvas = window.OffscreenCanvas
const ctx = canvas.getContext('2d', {
willReadFrequently: true,
});
ctx.imageSmoothingEnabled = false;
function Avatar({ url, size, alt = '', squircle, ...props }) {
size = SIZES[size] || size || SIZES.m;

View file

@ -235,6 +235,12 @@ function Compose({
};
const focusTextarea = () => {
setTimeout(() => {
if (!textareaRef.current) return;
// status starts with newline, focus on first position
if (draftStatus?.status?.startsWith?.('\n')) {
textareaRef.current.selectionStart = 0;
textareaRef.current.selectionEnd = 0;
}
console.debug('FOCUS textarea');
textareaRef.current?.focus();
}, 300);

View file

@ -1,4 +1,4 @@
import { Menu, MenuItem, SubMenu } from '@szhsin/react-menu';
import { MenuItem, SubMenu } from '@szhsin/react-menu';
import { cloneElement } from 'preact';
import { useRef } from 'preact/hooks';
@ -10,6 +10,7 @@ function MenuConfirm({
confirmLabel,
menuItemClassName,
menuFooter,
menuExtras,
...props
}) {
const { children, onClick, ...restProps } = props;
@ -53,6 +54,7 @@ function MenuConfirm({
<MenuItem className={menuItemClassName} onClick={onClick}>
{confirmLabel}
</MenuItem>
{menuExtras}
{menuFooter}
</Parent>
);

View file

@ -748,10 +748,24 @@ function Status({
confirmLabel={
<>
<Icon icon="rocket" />
<span>{reblogged ? 'Unboost?' : 'Boost to everyone?'}</span>
<span>{reblogged ? 'Unboost' : 'Boost'}</span>
</>
}
className={`menu-reblog ${reblogged ? 'checked' : ''}`}
menuExtras={
<MenuItem
onClick={() => {
states.showCompose = {
draftStatus: {
status: `\n${url}`,
},
};
}}
>
<Icon icon="quote" />
<span>Quote</span>
</MenuItem>
}
menuFooter={
mediaNoDesc &&
!reblogged && (
@ -1100,7 +1114,12 @@ function Status({
const { clientX, clientY } = e.touches?.[0] || e;
// link detection copied from onContextMenu because here it works
const link = e.target.closest('a');
if (link && statusRef.current.contains(link)) return;
if (
link &&
statusRef.current.contains(link) &&
!link.getAttribute('href').startsWith('#')
)
return;
e.preventDefault();
setContextMenuProps({
anchorPoint: {
@ -1346,7 +1365,12 @@ function Status({
if (e.metaKey) return;
// console.log('context menu', e);
const link = e.target.closest('a');
if (link && statusRef.current.contains(link)) return;
if (
link &&
statusRef.current.contains(link) &&
!link.getAttribute('href').startsWith('#')
)
return;
// If there's selected text, don't show custom context menu
const selection = window.getSelection?.();
@ -1910,11 +1934,23 @@ function Status({
confirmLabel={
<>
<Icon icon="rocket" />
<span>
{reblogged ? 'Unboost?' : 'Boost to everyone?'}
</span>
<span>{reblogged ? 'Unboost' : 'Boost'}</span>
</>
}
menuExtras={
<MenuItem
onClick={() => {
states.showCompose = {
draftStatus: {
status: `\n${url}`,
},
};
}}
>
<Icon icon="quote" />
<span>Quote</span>
</MenuItem>
}
menuFooter={
mediaNoDesc &&
!reblogged && (
@ -2131,10 +2167,13 @@ function Card({ card, selfReferential, instance }) {
const w = 44;
const h = 44;
const blurhashPixels = decodeBlurHash(blurhash, w, h);
const canvas = document.createElement('canvas');
const canvas = window.OffscreenCanvas
? new OffscreenCanvas(1, 1)
: document.createElement('canvas');
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
const imageData = ctx.createImageData(w, h);
imageData.data.set(blurhashPixels);
ctx.putImageData(imageData, 0, 0);

View file

@ -626,10 +626,24 @@
gap: 4px;
align-items: center;
flex-shrink: 0;
min-height: 24px;
.icon {
> .avatar {
outline: 1px solid var(--bg-blur-color);
}
> .avatar ~ .avatar {
margin-left: -8px;
}
> .icon {
color: var(--reblog-color);
}
> .name-text {
opacity: 0.75;
filter: grayscale(0.75);
}
}
.post-author {

View file

@ -429,9 +429,29 @@ function Catchup() {
return postFilterMatches;
});
// Deduplicate boosts
const boostedPosts = {};
filteredPosts = filteredPosts.filter((post) => {
if (post.reblog) {
if (boostedPosts[post.reblog.id]) {
if (boostedPosts[post.reblog.id].__BOOSTERS) {
boostedPosts[post.reblog.id].__BOOSTERS.add(post.account);
} else {
boostedPosts[post.reblog.id].__BOOSTERS = new Set([post.account]);
}
return false;
} else {
boostedPosts[post.reblog.id] = post;
}
}
return true;
});
if (selectedAuthor && authorCountsMap.has(selectedAuthor)) {
filteredPosts = filteredPosts.filter(
(post) => post.account.id === selectedAuthor,
(post) =>
post.account.id === selectedAuthor ||
[...(post.__BOOSTERS || [])].find((a) => a.id === selectedAuthor),
);
}
@ -589,35 +609,40 @@ function Catchup() {
authors,
]);
const prevSelectedAuthorMissing = useRef(false);
useEffect(() => {
// console.log({
// prevSelectedAuthorMissing,
// selectedAuthor,
// authors,
// });
let timer;
if (selectedAuthor) {
if (authors[selectedAuthor]) {
if (prevSelectedAuthorMissing.current) {
timer = setTimeout(() => {
authorsListParent.current
.querySelector(`[data-author="${selectedAuthor}"]`)
?.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center',
});
}, 500);
prevSelectedAuthorMissing.current = false;
// Check if author is visible and within the scrollable area viewport
const authorElement = authorsListParent.current.querySelector(
`[data-author="${selectedAuthor}"]`,
);
const scrollableRect =
authorsListParent.current?.getBoundingClientRect();
const authorRect = authorElement?.getBoundingClientRect();
console.log({
sLeft: scrollableRect.left,
sRight: scrollableRect.right,
aLeft: authorRect.left,
aRight: authorRect.right,
});
if (
authorRect.left < scrollableRect.left ||
authorRect.right > scrollableRect.right
) {
authorElement.scrollIntoView({
block: 'nearest',
inline: 'center',
behavior: 'smooth',
});
} else if (authorRect.top < 0) {
authorElement.scrollIntoView({
block: 'nearest',
inline: 'nearest',
behavior: 'smooth',
});
}
} else {
prevSelectedAuthorMissing.current = true;
}
}
return () => {
clearTimeout(timer);
};
}, [selectedAuthor, authors]);
const [showHelp, setShowHelp] = useState(false);
@ -663,6 +688,76 @@ function Catchup() {
},
{
preventDefault: true,
ignoreModifiers: true,
},
);
useHotkeys(
'k',
() => {
const activeItem = document.activeElement.closest(itemsSelector);
const activeItemRect = activeItem?.getBoundingClientRect();
const allItems = Array.from(
scrollableRef.current.querySelectorAll(itemsSelector),
);
if (
activeItem &&
activeItemRect.top < scrollableRef.current.clientHeight &&
activeItemRect.bottom > 0
) {
const activeItemIndex = allItems.indexOf(activeItem);
let prevItem = allItems[activeItemIndex - 1];
if (prevItem) {
prevItem.focus();
prevItem.scrollIntoView({
block: 'center',
inline: 'center',
behavior: 'smooth',
});
}
} else {
const topmostItem = allItems.find((item) => {
const itemRect = item.getBoundingClientRect();
return itemRect.top >= 44 && itemRect.left >= 0;
});
if (topmostItem) {
topmostItem.focus();
topmostItem.scrollIntoView({
block: 'nearest',
inline: 'center',
behavior: 'smooth',
});
}
}
},
{
preventDefault: true,
ignoreModifiers: true,
},
);
useHotkeys(
'h, l',
(_, handler) => {
// Go next/prev selectedAuthor in authorCountsList list
if (selectedAuthor) {
const key = handler.keys[0];
const index = authorCountsList.indexOf(selectedAuthor);
if (key === 'h') {
if (index > 0 && index < authorCountsList.length) {
setSelectedAuthor(authorCountsList[index - 1]);
}
} else if (key === 'l') {
if (index < authorCountsList.length - 1 && index >= 0) {
setSelectedAuthor(authorCountsList[index + 1]);
}
}
}
},
{
preventDefault: true,
ignoreModifiers: true,
enableOnFormTags: true,
},
);
@ -1351,6 +1446,7 @@ const PostLine = memo(
_followedTags: isFollowedTags,
_filtered: filterInfo,
visibility,
__BOOSTERS,
} = post;
const isReplyTo = inReplyToId && inReplyToAccountId !== account.id;
const isFiltered = !!filterInfo;
@ -1384,7 +1480,12 @@ const PostLine = memo(
<Avatar
url={account.avatarStatic || account.avatar}
squircle={account.bot}
/>{' '}
/>
{__BOOSTERS?.size > 0
? [...__BOOSTERS].map((b) => (
<Avatar url={b.avatarStatic || b.avatar} squircle={b.bot} />
))
: ''}{' '}
<Icon icon="rocket" />{' '}
{/* <Avatar
url={reblog.account.avatarStatic || reblog.account.avatar}

View file

@ -143,6 +143,7 @@
border-color: var(--reply-to-color);
box-shadow: 0 0 0 3px var(--reply-to-faded-color);
}
.notification:focus-visible .status-link,
.notification .status-link:is(:hover, :focus) {
background-color: var(--bg-blur-color);
filter: saturate(1);

View file

@ -3,6 +3,7 @@ import './notifications.css';
import { Fragment } from 'preact';
import { memo } from 'preact/compat';
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
import { useHotkeys } from 'react-hotkeys-hook';
import { InView } from 'react-intersection-observer';
import { useSearchParams } from 'react-router-dom';
import { useSnapshot } from 'valtio';
@ -31,6 +32,12 @@ import useTitle from '../utils/useTitle';
const LIMIT = 30; // 30 is the maximum limit :(
const emptySearchParams = new URLSearchParams();
const scrollIntoViewOptions = {
block: 'center',
inline: 'center',
behavior: 'smooth',
};
function Notifications({ columnMode }) {
useTitle('Notifications', '/notifications');
const { masto, instance } = api();
@ -273,11 +280,84 @@ function Notifications({ columnMode }) {
// }
// }, [uiState]);
const itemsSelector = '.notification';
const jRef = useHotkeys('j', () => {
const activeItem = document.activeElement.closest(itemsSelector);
const activeItemRect = activeItem?.getBoundingClientRect();
const allItems = Array.from(
scrollableRef.current.querySelectorAll(itemsSelector),
);
if (
activeItem &&
activeItemRect.top < scrollableRef.current.clientHeight &&
activeItemRect.bottom > 0
) {
const activeItemIndex = allItems.indexOf(activeItem);
let nextItem = allItems[activeItemIndex + 1];
if (nextItem) {
nextItem.focus();
nextItem.scrollIntoView(scrollIntoViewOptions);
}
} else {
const topmostItem = allItems.find((item) => {
const itemRect = item.getBoundingClientRect();
return itemRect.top >= 44 && itemRect.left >= 0;
});
if (topmostItem) {
topmostItem.focus();
topmostItem.scrollIntoView(scrollIntoViewOptions);
}
}
});
const kRef = useHotkeys('k', () => {
// focus on previous status after active item
const activeItem = document.activeElement.closest(itemsSelector);
const activeItemRect = activeItem?.getBoundingClientRect();
const allItems = Array.from(
scrollableRef.current.querySelectorAll(itemsSelector),
);
if (
activeItem &&
activeItemRect.top < scrollableRef.current.clientHeight &&
activeItemRect.bottom > 0
) {
const activeItemIndex = allItems.indexOf(activeItem);
let prevItem = allItems[activeItemIndex - 1];
if (prevItem) {
prevItem.focus();
prevItem.scrollIntoView(scrollIntoViewOptions);
}
} else {
const topmostItem = allItems.find((item) => {
const itemRect = item.getBoundingClientRect();
return itemRect.top >= 44 && itemRect.left >= 0;
});
if (topmostItem) {
topmostItem.focus();
topmostItem.scrollIntoView(scrollIntoViewOptions);
}
}
});
const oRef = useHotkeys(['enter', 'o'], () => {
const activeItem = document.activeElement.closest(itemsSelector);
const statusLink = activeItem?.querySelector('.status-link');
if (statusLink) {
statusLink.click();
}
});
return (
<div
id="notifications-page"
class="deck-container"
ref={scrollableRef}
ref={(node) => {
scrollableRef.current = node;
jRef.current = node;
kRef.current = node;
oRef.current = node;
}}
tabIndex="-1"
>
<div class={`timeline-deck deck ${onlyMentions ? 'only-mentions' : ''}`}>

View file

@ -24,12 +24,14 @@ function handleContentLinks(opts) {
).innerText.trim();
const username = targetText.replace(/^@/, '');
const url = target.getAttribute('href');
const mention = mentions.find(
(mention) =>
mention.username === username ||
mention.acct === username ||
mention.url === url,
);
// Only fallback to acct/username check if url doesn't match
const mention =
mentions.find((mention) => mention.url === url) ||
mentions.find(
(mention) =>
mention.acct === username || mention.username === username,
);
console.warn('MENTION', mention, url);
if (mention) {
e.preventDefault();
e.stopPropagation();