Fix the remaining lint issues

Renaud Chaput 2024-06-12 15:51:06 +02:00
parent be36081f54
commit 8805b9d1bd
No known key found for this signature in database
GPG Key ID: BCFC859D49B46990
21 changed files with 253 additions and 77 deletions

View File

@ -17,9 +17,13 @@ export const fetchNotifications = createDataLoadingThunk(
fetchedAccounts.push(...notification.sample_accounts);
}
// if (notification.type === 'admin.report') {
// fetchedAccounts.push(...notification.report.target_account);
// }
if (notification.type === 'admin.report') {
fetchedAccounts.push(notification.report.target_account);
}
if (notification.type === 'moderation_warning') {
fetchedAccounts.push(notification.moderation_warning.target_account);
}
if ('status' in notification) {
fetchedStatuses.push(notification.status);

View File

@ -1,6 +1,9 @@
// See app/serializers/rest/notification_group_serializer.rb
import type { AccountWarningAction } from 'mastodon/models/notification_group';
import type { ApiAccountJSON } from './accounts';
import type { ApiReportJSON } from './reports';
import type { ApiStatusJSON } from './statuses';
// See app/model/notification.rb
@ -26,7 +29,7 @@ export interface BaseNotificationGroupJSON {
notifications_count: number;
type: NotificationType;
sample_accounts: ApiAccountJSON[];
latest_page_notification_at?: string;
latest_page_notification_at: string; // FIXME: This will only be present if the notification group is returned in a paginated list, not requested directly
page_min_id?: string;
page_max_id?: string;
}
@ -38,19 +41,39 @@ interface NotificationGroupWithStatusJSON extends BaseNotificationGroupJSON {
interface ReportNotificationGroupJSON extends BaseNotificationGroupJSON {
type: 'admin.report';
report: unknown;
report: ApiReportJSON;
}
export interface ApiAccountWarningJSON {
id: string;
action: AccountWarningAction;
text: string;
status_ids: string[];
created_at: string;
target_account: ApiAccountJSON;
appeal: unknown;
}
interface ModerationWarningNotificationGroupJSON
extends BaseNotificationGroupJSON {
type: 'moderation_warning';
moderation_warning: unknown;
moderation_warning: ApiAccountWarningJSON;
}
export interface ApiAccountRelationshipSeveranceEventJSON {
id: string;
type: 'account_suspension' | 'domain_block' | 'user_domain_block';
purged: boolean;
target_name: string;
followers_count: number;
following_count: number;
created_at: string;
}
interface AccountRelationshipSeveranceNotificationGroupJSON
extends BaseNotificationGroupJSON {
type: 'severed_relationships';
account_relationship_severance_event: unknown;
event: ApiAccountRelationshipSeveranceEventJSON;
}
export type NotificationGroupJSON =

View File

@ -0,0 +1,16 @@
import type { ApiAccountJSON } from './accounts';
export type ReportCategory = 'other' | 'spam' | 'legal' | 'violation';
export interface ApiReportJSON {
id: string;
action_taken: unknown;
action_taken_at: unknown;
category: ReportCategory;
comment: string;
forwarded: boolean;
created_at: string;
status_ids: string[];
rule_ids: string[];
target_account: ApiAccountJSON;
}

View File

@ -4,6 +4,7 @@ import classNames from 'classnames';
import GavelIcon from '@/material-icons/400-24px/gavel.svg?react';
import { Icon } from 'mastodon/components/icon';
import type { AccountWarningAction } from 'mastodon/models/notification_group';
// This needs to be kept in sync with app/models/account_warning.rb
const messages = defineMessages({
@ -38,17 +39,10 @@ const messages = defineMessages({
});
interface Props {
action:
| 'none'
| 'disable'
| 'mark_statuses_as_sensitive'
| 'delete_statuses'
| 'sensitive'
| 'silence'
| 'suspend';
action: AccountWarningAction;
id: string;
hidden: boolean;
unread: boolean;
hidden?: boolean;
unread?: boolean;
}
export const ModerationWarning: React.FC<Props> = ({
@ -70,7 +64,7 @@ export const ModerationWarning: React.FC<Props> = ({
'notification-group notification-group--link notification-group--moderation-warning focusable',
{ 'notification-group--unread': unread },
)}
tabIndex='0'
tabIndex={0}
>
<div className='notification-group__icon'>
<Icon id='warning' icon={GavelIcon} />

View File

@ -1,8 +1,10 @@
import { useCallback } from 'react';
import { useHistory } from 'react-router-dom';
import { FormattedMessage } from 'react-intl';
import type { List } from 'immutable';
import { useHistory } from 'react-router-dom';
import type { List as ImmutableList, RecordOf } from 'immutable';
import BarChart4BarsIcon from '@/material-icons/400-24px/bar_chart_4_bars.svg?react';
import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react';
@ -11,8 +13,11 @@ import { DisplayName } from 'mastodon/components/display_name';
import { Icon } from 'mastodon/components/icon';
import type { Status } from 'mastodon/models/status';
import { useAppSelector } from 'mastodon/store';
import { EmbeddedStatusContent } from './embedded_status_content';
export type Mention = RecordOf<{ url: string; acct: string }>;
export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
statusId,
}) => {
@ -27,6 +32,8 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
);
const handleClick = useCallback(() => {
if (!account) return;
history.push(`/@${account.acct}/${statusId}`);
}, [statusId, account, history]);
@ -38,9 +45,9 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
const contentHtml = status.get('contentHtml') as string;
const poll = status.get('poll');
const language = status.get('language') as string;
const mentions = status.get('mentions');
const mentions = status.get('mentions') as ImmutableList<Mention>;
const mediaAttachmentsSize = (
status.get('media_attachments') as List<unknown>
status.get('media_attachments') as ImmutableList<unknown>
).size;
return (
@ -62,7 +69,7 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
<div className='notification-group__embedded-status__attachments reply-indicator__attachments'>
{!!poll && (
<>
<Icon icon={BarChart4BarsIcon} />
<Icon icon={BarChart4BarsIcon} id='bar-chart-4-bars' />
<FormattedMessage
id='reply_indicator.poll'
defaultMessage='Poll'
@ -71,7 +78,7 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
)}
{mediaAttachmentsSize > 0 && (
<>
<Icon icon={PhotoLibraryIcon} />
<Icon icon={PhotoLibraryIcon} id='photo-library' />
<FormattedMessage
id='reply_indicator.attachments'
defaultMessage='{count, plural, one {# attachment} other {# attachments}}'

View File

@ -1,14 +1,29 @@
import { useCallback, useRef } from 'react';
import { useHistory } from 'react-router-dom';
const handleMentionClick = (history: unknown, mention: unknown, e: Event) => {
import type { List } from 'immutable';
import type { History } from 'history';
import type { Mention } from './embedded_status';
const handleMentionClick = (
history: History,
mention: Mention,
e: MouseEvent,
) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
history.push(`/@${mention.get('acct')}`);
}
};
const handleHashtagClick = (history: unknown, hashtag: string, e: Event) => {
const handleHashtagClick = (
history: History,
hashtag: string,
e: MouseEvent,
) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
history.push(`/tags/${hashtag.replace(/^#/, '')}`);
@ -17,22 +32,22 @@ const handleHashtagClick = (history: unknown, hashtag: string, e: Event) => {
export const EmbeddedStatusContent: React.FC<{
content: string;
mentions: unknown;
mentions: List<Mention>;
language: string;
onClick?: unknown;
onClick?: () => void;
className?: string;
}> = ({ content, mentions, language, onClick, className }) => {
const clickCoordinatesRef = useRef();
const clickCoordinatesRef = useRef<[number, number] | null>();
const history = useHistory();
const handleMouseDown = useCallback(
const handleMouseDown = useCallback<React.MouseEventHandler<HTMLDivElement>>(
({ clientX, clientY }) => {
clickCoordinatesRef.current = [clientX, clientY];
},
[clickCoordinatesRef],
);
const handleMouseUp = useCallback(
const handleMouseUp = useCallback<React.MouseEventHandler<HTMLDivElement>>(
({ clientX, clientY, target, button }) => {
const [startX, startY] = clickCoordinatesRef.current ?? [0, 0];
const [deltaX, deltaY] = [
@ -40,7 +55,7 @@ export const EmbeddedStatusContent: React.FC<{
Math.abs(clientY - startY),
];
let element = target;
let element: HTMLDivElement | null = target as HTMLDivElement;
while (element) {
if (
@ -51,7 +66,7 @@ export const EmbeddedStatusContent: React.FC<{
return;
}
element = element.parentNode;
element = element.parentNode as HTMLDivElement | null;
}
if (deltaX + deltaY < 5 && button === 0 && onClick) {
@ -63,29 +78,39 @@ export const EmbeddedStatusContent: React.FC<{
[clickCoordinatesRef, onClick],
);
const handleMouseEnter = useCallback(({ currentTarget }) => {
const emojis = currentTarget.querySelectorAll('.custom-emoji');
const handleMouseEnter = useCallback<React.MouseEventHandler<HTMLDivElement>>(
({ currentTarget }) => {
const emojis =
currentTarget.querySelectorAll<HTMLImageElement>('.custom-emoji');
for (const emoji of emojis) {
emoji.src = emoji.getAttribute('data-original');
}
}, []);
for (const emoji of emojis) {
const newSrc = emoji.getAttribute('data-original');
if (newSrc) emoji.src = newSrc;
}
},
[],
);
const handleMouseLeave = useCallback(({ currentTarget }) => {
const emojis = currentTarget.querySelectorAll('.custom-emoji');
const handleMouseLeave = useCallback<React.MouseEventHandler<HTMLDivElement>>(
({ currentTarget }) => {
const emojis =
currentTarget.querySelectorAll<HTMLImageElement>('.custom-emoji');
for (const emoji of emojis) {
emoji.src = emoji.getAttribute('data-static');
}
}, []);
for (const emoji of emojis) {
const newSrc = emoji.getAttribute('data-static');
if (newSrc) emoji.src = newSrc;
}
},
[],
);
const handleContentRef = useCallback(
(node) => {
(node: HTMLDivElement | null) => {
if (!node) {
return;
}
const links = node.querySelectorAll('a');
const links = node.querySelectorAll<HTMLAnchorElement>('a');
for (const link of links) {
if (link.classList.contains('status-link')) {
@ -105,12 +130,8 @@ export const EmbeddedStatusContent: React.FC<{
link.setAttribute('title', `@${mention.get('acct')}`);
link.setAttribute('href', `/@${mention.get('acct')}`);
} else if (
link.textContent[0] === '#' ||
(link.previousSibling &&
link.previousSibling.textContent &&
link.previousSibling.textContent[
link.previousSibling.textContent.length - 1
] === '#')
link.textContent?.[0] === '#' ||
link.previousSibling?.textContent?.endsWith('#')
) {
link.addEventListener(
'click',
@ -130,7 +151,7 @@ export const EmbeddedStatusContent: React.FC<{
return (
<div
role='button'
tabIndex='0'
tabIndex={0}
className={className}
ref={handleContentRef}
lang={language}

View File

@ -30,15 +30,18 @@ const messages = defineMessages({
export const NotificationAdminReport: React.FC<{
notification: NotificationGroupAdminReport;
unread: boolean;
unread?: boolean;
}> = ({ notification, notification: { report }, unread }) => {
const intl = useIntl();
const targetAccount = useAppSelector((state) =>
state.getIn(['accounts', report.target_account.id]),
state.accounts.get(report.targetAccountId),
);
const account = useAppSelector((state) =>
state.getIn(['accounts', notification.sampleAccountsIds[0]]),
state.accounts.get(notification.sampleAccountsIds[0] ?? '0'),
);
if (!account || !targetAccount) return null;
const values = {
name: (
<bdi

View File

@ -21,6 +21,7 @@ export const NotificationAdminSignUp: React.FC<{
<NotificationGroupWithStatus
type='admin-sign-up'
icon={PersonAddIcon}
iconId='person-add'
accountIds={notification.sampleAccountsIds}
timestamp={notification.latest_page_notification_at}
count={notification.notifications_count}

View File

@ -21,6 +21,7 @@ export const NotificationFavourite: React.FC<{
<NotificationGroupWithStatus
type='favourite'
icon={StarIcon}
iconId='star'
accountIds={notification.sampleAccountsIds}
statusId={notification.statusId}
timestamp={notification.latest_page_notification_at}

View File

@ -21,6 +21,7 @@ export const NotificationFollow: React.FC<{
<NotificationGroupWithStatus
type='follow'
icon={PersonAddIcon}
iconId='person-add'
accountIds={notification.sampleAccountsIds}
timestamp={notification.latest_page_notification_at}
count={notification.notifications_count}

View File

@ -21,6 +21,7 @@ export const NotificationFollowRequest: React.FC<{
<NotificationGroupWithStatus
type='follow-request'
icon={PersonAddIcon}
iconId='person-add'
accountIds={notification.sampleAccountsIds}
timestamp={notification.latest_page_notification_at}
count={notification.notifications_count}

View File

@ -21,8 +21,8 @@ import { NotificationUpdate } from './notification_update';
export const NotificationGroup: React.FC<{
notificationGroupId: NotificationGroupModel['group_key'];
unread: boolean;
onMoveUp: unknown;
onMoveDown: unknown;
onMoveUp: (groupId: string) => void;
onMoveDown: (groupId: string) => void;
}> = ({ notificationGroupId, unread, onMoveUp, onMoveDown }) => {
const notificationGroup = useAppSelector((state) =>
state.notificationsGroups.groups.find(

View File

@ -16,6 +16,7 @@ export type LabelRenderer = (
export const NotificationGroupWithStatus: React.FC<{
icon: IconProp;
iconId: string;
statusId?: string;
count: number;
accountIds: string[];
@ -25,6 +26,7 @@ export const NotificationGroupWithStatus: React.FC<{
unread: boolean;
}> = ({
icon,
iconId,
timestamp,
accountIds,
count,
@ -48,10 +50,10 @@ export const NotificationGroupWithStatus: React.FC<{
`notification-group focusable notification-group--${type}`,
{ 'notification-group--unread': unread },
)}
tabIndex='0'
tabIndex={0}
>
<div className='notification-group__icon'>
<Icon icon={icon} />
<Icon icon={icon} id={iconId} />
</div>
<div className='notification-group__main'>

View File

@ -21,6 +21,7 @@ export const NotificationMention: React.FC<{
<NotificationWithStatus
type='mention'
icon={ReplyIcon}
iconId='reply'
accountIds={notification.sampleAccountsIds}
count={notification.notifications_count}
statusId={notification.statusId}

View File

@ -4,6 +4,10 @@ import type { NotificationGroupModerationWarning } from 'mastodon/models/notific
export const NotificationModerationWarning: React.FC<{
notification: NotificationGroupModerationWarning;
unread: boolean;
}> = ({ notification: { event }, unread }) => (
<ModerationWarning action={event.action} id={event.id} unread={unread} />
}> = ({ notification: { moderationWarning }, unread }) => (
<ModerationWarning
action={moderationWarning.action}
id={moderationWarning.id}
unread={unread}
/>
);

View File

@ -27,6 +27,7 @@ export const NotificationPoll: React.FC<{
<NotificationWithStatus
type='poll'
icon={BarChart4BarsIcon}
iconId='bar-chart-4-bars'
accountIds={notification.sampleAccountsIds}
count={notification.notifications_count}
statusId={notification.statusId}

View File

@ -21,6 +21,7 @@ export const NotificationReblog: React.FC<{
<NotificationGroupWithStatus
type='reblog'
icon={RepeatIcon}
iconId='repeat'
accountIds={notification.sampleAccountsIds}
statusId={notification.statusId}
timestamp={notification.latest_page_notification_at}

View File

@ -21,6 +21,7 @@ export const NotificationStatus: React.FC<{
<NotificationWithStatus
type='status'
icon={NotificationsActiveIcon}
iconId='notifications-active'
accountIds={notification.sampleAccountsIds}
count={notification.notifications_count}
statusId={notification.statusId}

View File

@ -21,6 +21,7 @@ export const NotificationUpdate: React.FC<{
<NotificationWithStatus
type='update'
icon={EditIcon}
iconId='edit'
accountIds={notification.sampleAccountsIds}
count={notification.notifications_count}
statusId={notification.statusId}

View File

@ -1,8 +1,9 @@
import { useMemo } from 'react';
import classNames from 'classnames';
import type { IconProp } from 'mastodon/components/icon';
import { Icon } from 'mastodon/components/icon';
import classNames from 'classnames';
import Status from 'mastodon/containers/status_container';
import { NamesList } from './names_list';
@ -11,12 +12,22 @@ import type { LabelRenderer } from './notification_group_with_status';
export const NotificationWithStatus: React.FC<{
type: string;
icon: IconProp;
iconId: string;
accountIds: string[];
statusId: string;
count: number;
labelRenderer: LabelRenderer;
unread: boolean;
}> = ({ icon, accountIds, statusId, count, labelRenderer, type, unread }) => {
}> = ({
icon,
iconId,
accountIds,
statusId,
count,
labelRenderer,
type,
unread,
}) => {
const label = useMemo(
() =>
labelRenderer({
@ -32,11 +43,11 @@ export const NotificationWithStatus: React.FC<{
`notification-ungrouped focusable notification-ungrouped--${type}`,
{ 'notification-ungrouped--unread': unread },
)}
tabIndex='0'
tabIndex={0}
>
<div className='notification-ungrouped__header'>
<div className='notification-ungrouped__header__icon'>
<Icon icon={icon} />
<Icon icon={icon} id={iconId} />
</div>
{label}
</div>

View File

@ -1,9 +1,12 @@
import type {
ApiAccountRelationshipSeveranceEventJSON,
ApiAccountWarningJSON,
BaseNotificationGroupJSON,
NotificationGroupJSON,
NotificationType,
NotificationWithStatusType,
} from 'mastodon/api_types/notifications';
import type { ApiReportJSON } from 'mastodon/api_types/reports';
interface BaseNotificationGroup
extends Omit<BaseNotificationGroupJSON, 'sample_accounts'> {
@ -32,12 +35,39 @@ export type NotificationGroupFollow = BaseNotification<'follow'>;
export type NotificationGroupFollowRequest = BaseNotification<'follow_request'>;
export type NotificationGroupAdminSignUp = BaseNotification<'admin.sign_up'>;
// TODO: those two will need special types
export type NotificationGroupModerationWarning =
BaseNotification<'moderation_warning'>;
export type NotificationGroupAdminReport = BaseNotification<'admin.report'>;
export type NotificationGroupSeveredRelationships =
BaseNotification<'severed_relationships'>;
export type AccountWarningAction =
| 'none'
| 'disable'
| 'mark_statuses_as_sensitive'
| 'delete_statuses'
| 'sensitive'
| 'silence'
| 'suspend';
export interface AccountWarning
extends Omit<ApiAccountWarningJSON, 'target_account'> {
targetAccountId: string;
}
export interface NotificationGroupModerationWarning
extends BaseNotification<'moderation_warning'> {
moderationWarning: AccountWarning;
}
type AccountRelationshipSeveranceEvent =
ApiAccountRelationshipSeveranceEventJSON;
export interface NotificationGroupSeveredRelationships
extends BaseNotification<'severed_relationships'> {
event: AccountRelationshipSeveranceEvent;
}
interface Report extends Omit<ApiReportJSON, 'target_account'> {
targetAccountId: string;
}
export interface NotificationGroupAdminReport
extends BaseNotification<'admin.report'> {
report: Report;
}
export type NotificationGroup =
| NotificationGroupFavourite
@ -53,6 +83,30 @@ export type NotificationGroup =
| NotificationGroupAdminSignUp
| NotificationGroupAdminReport;
function createReportFromJSON(reportJSON: ApiReportJSON): Report {
const { target_account, ...report } = reportJSON;
return {
targetAccountId: target_account.id,
...report,
};
}
function createAccountWarningFromJSON(
warningJSON: ApiAccountWarningJSON,
): AccountWarning {
const { target_account, ...warning } = warningJSON;
return {
targetAccountId: target_account.id,
...warning,
};
}
function createAccountRelationshipSeveranceEventFromJSON(
eventJson: ApiAccountRelationshipSeveranceEventJSON,
): AccountRelationshipSeveranceEvent {
return eventJson;
}
export function createNotificationGroupFromJSON(
groupJson: NotificationGroupJSON,
): NotificationGroup {
@ -68,8 +122,36 @@ export function createNotificationGroupFromJSON(
};
}
return {
sampleAccountsIds,
...group,
};
if ('report' in group) {
const { report, ...groupWithoutTargetAccount } = group;
return {
report: createReportFromJSON(report),
sampleAccountsIds,
...groupWithoutTargetAccount,
};
}
switch (group.type) {
case 'severed_relationships':
return {
...group,
event: createAccountRelationshipSeveranceEventFromJSON(group.event),
sampleAccountsIds,
};
case 'moderation_warning': {
const { moderation_warning, ...groupWithoutModerationWarning } = group;
return {
...groupWithoutModerationWarning,
moderationWarning: createAccountWarningFromJSON(moderation_warning),
sampleAccountsIds,
};
}
// This is commented out because all group types are covered in the previous statement and have their returns
// default:
// return {
// sampleAccountsIds,
// ...group,
// };
}
}