commit
d34cce6d18
|
@ -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;
|
||||
|
|
|
@ -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'),
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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' : ''}`}>
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in a new issue