Merge commit 'cd0ca4b99473f54464e5134f91b7b1c8d5544011' into glitch-soc/merge-upstream

This commit is contained in:
Claire committed 2024-07-23 19:31:23 +02:00
commit 68000956f4
30 files changed
+398 -185

No files matched your search

@@ -1,4 +1,4 @@
import { useCallback } from 'react';
import { useCallback, useRef } from 'react';
import { FormattedMessage } from 'react-intl';
@@ -22,6 +22,7 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
statusId,
}) => {
const history = useHistory();
const clickCoordinatesRef = useRef<[number, number] | null>();
const status = useAppSelector(
(state) => state.statuses.get(statusId) as Status | undefined,
@@ -31,11 +32,69 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
state.accounts.get(status?.get('account') as string),
);
const handleClick = useCallback(() => {
if (!account) return;
const handleMouseDown = useCallback<React.MouseEventHandler<HTMLDivElement>>(
({ clientX, clientY }) => {
clickCoordinatesRef.current = [clientX, clientY];
},
[clickCoordinatesRef],
);
history.push(`/@${account.acct}/${statusId}`);
}, [statusId, account, history]);
const handleMouseUp = useCallback<React.MouseEventHandler<HTMLDivElement>>(
({ clientX, clientY, target, button }) => {
const [startX, startY] = clickCoordinatesRef.current ?? [0, 0];
const [deltaX, deltaY] = [
Math.abs(clientX - startX),
Math.abs(clientY - startY),
];
let element: HTMLDivElement | null = target as HTMLDivElement;
while (element) {
if (
element.localName === 'button' ||
element.localName === 'a' ||
element.localName === 'label'
) {
return;
}
element = element.parentNode as HTMLDivElement | null;
}
if (deltaX + deltaY < 5 && button === 0 && account) {
history.push(`/@${account.acct}/${statusId}`);
}
clickCoordinatesRef.current = null;
},
[clickCoordinatesRef, statusId, account, history],
);
const handleMouseEnter = useCallback<React.MouseEventHandler<HTMLDivElement>>(
({ currentTarget }) => {
const emojis =
currentTarget.querySelectorAll<HTMLImageElement>('.custom-emoji');
for (const emoji of emojis) {
const newSrc = emoji.getAttribute('data-original');
if (newSrc) emoji.src = newSrc;
}
},
[],
);
const handleMouseLeave = useCallback<React.MouseEventHandler<HTMLDivElement>>(
({ currentTarget }) => {
const emojis =
currentTarget.querySelectorAll<HTMLImageElement>('.custom-emoji');
for (const emoji of emojis) {
const newSrc = emoji.getAttribute('data-static');
if (newSrc) emoji.src = newSrc;
}
},
[],
);
if (!status) {
return null;
@@ -51,7 +110,15 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
).size;
return (
<div className='notification-group__embedded-status'>
<div
className='notification-group__embedded-status'
role='button'
tabIndex={-1}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div className='notification-group__embedded-status__account'>
<Avatar account={account} size={16} />
<DisplayName account={account} />
@@ -62,7 +129,6 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
content={contentHtml}
language={language}
mentions={mentions}
onClick={handleClick}
/>
{(poll || mediaAttachmentsSize > 0) && (
@@ -1,4 +1,4 @@
import { useCallback, useRef } from 'react';
import { useCallback } from 'react';
import { useHistory } from 'react-router-dom';
@@ -34,76 +34,10 @@ export const EmbeddedStatusContent: React.FC<{
content: string;
mentions: List<Mention>;
language: string;
onClick?: () => void;
className?: string;
}> = ({ content, mentions, language, onClick, className }) => {
const clickCoordinatesRef = useRef<[number, number] | null>();
}> = ({ content, mentions, language, className }) => {
const history = useHistory();
const handleMouseDown = useCallback<React.MouseEventHandler<HTMLDivElement>>(
({ clientX, clientY }) => {
clickCoordinatesRef.current = [clientX, clientY];
},
[clickCoordinatesRef],
);
const handleMouseUp = useCallback<React.MouseEventHandler<HTMLDivElement>>(
({ clientX, clientY, target, button }) => {
const [startX, startY] = clickCoordinatesRef.current ?? [0, 0];
const [deltaX, deltaY] = [
Math.abs(clientX - startX),
Math.abs(clientY - startY),
];
let element: HTMLDivElement | null = target as HTMLDivElement;
while (element) {
if (
element.localName === 'button' ||
element.localName === 'a' ||
element.localName === 'label'
) {
return;
}
element = element.parentNode as HTMLDivElement | null;
}
if (deltaX + deltaY < 5 && button === 0 && onClick) {
onClick();
}
clickCoordinatesRef.current = null;
},
[clickCoordinatesRef, onClick],
);
const handleMouseEnter = useCallback<React.MouseEventHandler<HTMLDivElement>>(
({ currentTarget }) => {
const emojis =
currentTarget.querySelectorAll<HTMLImageElement>('.custom-emoji');
for (const emoji of emojis) {
const newSrc = emoji.getAttribute('data-original');
if (newSrc) emoji.src = newSrc;
}
},
[],
);
const handleMouseLeave = useCallback<React.MouseEventHandler<HTMLDivElement>>(
({ currentTarget }) => {
const emojis =
currentTarget.querySelectorAll<HTMLImageElement>('.custom-emoji');
for (const emoji of emojis) {
const newSrc = emoji.getAttribute('data-static');
if (newSrc) emoji.src = newSrc;
}
},
[],
);
const handleContentRef = useCallback(
(node: HTMLDivElement | null) => {
if (!node) {
@@ -150,16 +84,10 @@ export const EmbeddedStatusContent: React.FC<{
return (
<div
role='button'
tabIndex={0}
className={className}
ref={handleContentRef}
lang={language}
dangerouslySetInnerHTML={{ __html: content }}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
/>
);
};
@@ -2,8 +2,10 @@ import { useMemo } from 'react';
import { HotKeys } from 'react-hotkeys';
import { navigateToProfile } from 'mastodon/actions/accounts';
import { mentionComposeById } from 'mastodon/actions/compose';
import type { NotificationGroup as NotificationGroupModel } from 'mastodon/models/notification_group';
import { useAppSelector } from 'mastodon/store';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { NotificationAdminReport } from './notification_admin_report';
import { NotificationAdminSignUp } from './notification_admin_sign_up';
@@ -30,6 +32,13 @@ export const NotificationGroup: React.FC<{
),
);
const dispatch = useAppDispatch();
const accountId =
notificationGroup?.type === 'gap'
? undefined
: notificationGroup?.sampleAccountIds[0];
const handlers = useMemo(
() => ({
moveUp: () => {
@@ -39,8 +48,16 @@ export const NotificationGroup: React.FC<{
moveDown: () => {
onMoveDown(notificationGroupId);
},
openProfile: () => {
if (accountId) dispatch(navigateToProfile(accountId));
},
mention: () => {
if (accountId) dispatch(mentionComposeById(accountId));
},
}),
[notificationGroupId, onMoveUp, onMoveDown],
[dispatch, notificationGroupId, accountId, onMoveUp, onMoveDown],
);
if (!notificationGroup || notificationGroup.type === 'gap') return null;
@@ -2,9 +2,14 @@ import { useMemo } from 'react';
import classNames from 'classnames';
import { HotKeys } from 'react-hotkeys';
import { replyComposeById } from 'mastodon/actions/compose';
import { navigateToStatus } from 'mastodon/actions/statuses';
import type { IconProp } from 'mastodon/components/icon';
import { Icon } from 'mastodon/components/icon';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import { useAppDispatch } from 'mastodon/store';
import { AvatarGroup } from './avatar_group';
import { EmbeddedStatus } from './embedded_status';
@@ -39,6 +44,8 @@ export const NotificationGroupWithStatus: React.FC<{
type,
unread,
}) => {
const dispatch = useAppDispatch();
const label = useMemo(
() =>
labelRenderer({
@@ -53,39 +60,54 @@ export const NotificationGroupWithStatus: React.FC<{
[labelRenderer, accountIds, count, labelSeeMoreHref],
);
const handlers = useMemo(
() => ({
open: () => {
dispatch(navigateToStatus(statusId));
},
reply: () => {
dispatch(replyComposeById(statusId));
},
}),
[dispatch, statusId],
);
return (
<div
role='button'
className={classNames(
`notification-group focusable notification-group--${type}`,
{ 'notification-group--unread': unread },
)}
tabIndex={0}
>
<div className='notification-group__icon'>
<Icon icon={icon} id={iconId} />
</div>
<div className='notification-group__main'>
<div className='notification-group__main__header'>
<div className='notification-group__main__header__wrapper'>
<AvatarGroup accountIds={accountIds} />
{actions}
</div>
<div className='notification-group__main__header__label'>
{label}
{timestamp && <RelativeTimestamp timestamp={timestamp} />}
</div>
<HotKeys handlers={handlers}>
<div
role='button'
className={classNames(
`notification-group focusable notification-group--${type}`,
{ 'notification-group--unread': unread },
)}
tabIndex={0}
>
<div className='notification-group__icon'>
<Icon icon={icon} id={iconId} />
</div>
{statusId && (
<div className='notification-group__main__status'>
<EmbeddedStatus statusId={statusId} />
<div className='notification-group__main'>
<div className='notification-group__main__header'>
<div className='notification-group__main__header__wrapper'>
<AvatarGroup accountIds={accountIds} />
{actions}
</div>
<div className='notification-group__main__header__label'>
{label}
{timestamp && <RelativeTimestamp timestamp={timestamp} />}
</div>
</div>
)}
{statusId && (
<div className='notification-group__main__status'>
<EmbeddedStatus statusId={statusId} />
</div>
)}
</div>
</div>
</div>
</HotKeys>
);
};
@@ -2,10 +2,18 @@ import { useMemo } from 'react';
import classNames from 'classnames';
import { HotKeys } from 'react-hotkeys';
import { replyComposeById } from 'mastodon/actions/compose';
import { toggleReblog, toggleFavourite } from 'mastodon/actions/interactions';
import {
navigateToStatus,
toggleStatusSpoilers,
} from 'mastodon/actions/statuses';
import type { IconProp } from 'mastodon/components/icon';
import { Icon } from 'mastodon/components/icon';
import Status from 'mastodon/containers/status_container';
import { useAppSelector } from 'mastodon/store';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { NamesList } from './names_list';
import type { LabelRenderer } from './notification_group_with_status';
@@ -29,6 +37,8 @@ export const NotificationWithStatus: React.FC<{
type,
unread,
}) => {
const dispatch = useAppDispatch();
const label = useMemo(
() =>
labelRenderer({
@@ -41,33 +51,61 @@ export const NotificationWithStatus: React.FC<{
(state) => state.statuses.getIn([statusId, 'visibility']) === 'direct',
);
return (
<div
role='button'
className={classNames(
`notification-ungrouped focusable notification-ungrouped--${type}`,
{
'notification-ungrouped--unread': unread,
'notification-ungrouped--direct': isPrivateMention,
},
)}
tabIndex={0}
>
<div className='notification-ungrouped__header'>
<div className='notification-ungrouped__header__icon'>
<Icon icon={icon} id={iconId} />
</div>
{label}
</div>
const handlers = useMemo(
() => ({
open: () => {
dispatch(navigateToStatus(statusId));
},
<Status
// @ts-expect-error -- <Status> is not yet typed
id={statusId}
contextType='notifications'
withDismiss
skipPrepend
avatarSize={40}
/>
</div>
reply: () => {
dispatch(replyComposeById(statusId));
},
boost: () => {
dispatch(toggleReblog(statusId));
},
favourite: () => {
dispatch(toggleFavourite(statusId));
},
toggleHidden: () => {
dispatch(toggleStatusSpoilers(statusId));
},
}),
[dispatch, statusId],
);
return (
<HotKeys handlers={handlers}>
<div
role='button'
className={classNames(
`notification-ungrouped focusable notification-ungrouped--${type}`,
{
'notification-ungrouped--unread': unread,
'notification-ungrouped--direct': isPrivateMention,
},
)}
tabIndex={0}
>
<div className='notification-ungrouped__header'>
<div className='notification-ungrouped__header__icon'>
<Icon icon={icon} id={iconId} />
</div>
{label}
</div>
<Status
// @ts-expect-error -- <Status> is not yet typed
id={statusId}
contextType='notifications'
withDismiss
skipPrepend
avatarSize={40}
unfocusable
/>
</div>
</HotKeys>
);
};