263 lines
6.9 KiB
TypeScript
263 lines
6.9 KiB
TypeScript
import { useCallback, useMemo } from 'react';
|
|
|
|
import { defineMessages, useIntl } from 'react-intl';
|
|
|
|
import { useHistory } from 'react-router';
|
|
|
|
import { mentionComposeById } from '@/mastodon/actions/compose';
|
|
import type { StatusInteractionIntent } from '@/mastodon/actions/interactions_typed';
|
|
import { statusInteraction } from '@/mastodon/actions/interactions_typed';
|
|
import { openModal } from '@/mastodon/actions/modal';
|
|
import { toggleStatusSpoilers } from '@/mastodon/actions/statuses';
|
|
import { useExpandedStatus } from '@/mastodon/hooks/useStatus';
|
|
import { useToggle } from '@/mastodon/hooks/useToggle';
|
|
import type { ExpandedStatusShape } from '@/mastodon/models/status';
|
|
import { selectStatusFilters } from '@/mastodon/selectors/filters';
|
|
import { useAppSelector, useAppDispatch } from '@/mastodon/store';
|
|
|
|
import { FOCUS_TARGET } from '../navigation_focus_target';
|
|
|
|
import type { StatusContextType } from './types';
|
|
|
|
const messages = defineMessages({
|
|
quote_noun: {
|
|
id: 'status.quote_noun',
|
|
defaultMessage: 'Quote',
|
|
description: 'Quote as a noun',
|
|
},
|
|
contains_quote: {
|
|
id: 'status.contains_quote',
|
|
defaultMessage: 'Contains quote',
|
|
},
|
|
boosted: { id: 'status.reblogged_by', defaultMessage: '{name} boosted' },
|
|
});
|
|
|
|
export function useStatusHandlers({
|
|
status,
|
|
contextType,
|
|
onClick,
|
|
}: {
|
|
status?: ExpandedStatusShape;
|
|
contextType?: StatusContextType;
|
|
onClick?: () => void;
|
|
}) {
|
|
const matchedFilters = useAppSelector((state) =>
|
|
selectStatusFilters(state, { contextType, statusId: status?.id }),
|
|
);
|
|
const [showDespiteFilter, { onToggle: onFilterToggle }] = useToggle(false);
|
|
|
|
const dispatch = useAppDispatch();
|
|
const statusId = status?.id;
|
|
|
|
// Display handlers
|
|
const onExpandedToggle = useCallback(() => {
|
|
dispatch(toggleStatusSpoilers(statusId));
|
|
}, [dispatch, statusId]);
|
|
|
|
const onToggleHidden = useCallback(() => {
|
|
if (!status) {
|
|
return;
|
|
}
|
|
if (!matchedFilters.length || showDespiteFilter) {
|
|
dispatch(toggleStatusSpoilers(status.id));
|
|
}
|
|
|
|
if (!status.hidden || !status.spoiler_text) {
|
|
onFilterToggle();
|
|
}
|
|
}, [
|
|
dispatch,
|
|
matchedFilters.length,
|
|
onFilterToggle,
|
|
showDespiteFilter,
|
|
status,
|
|
]);
|
|
|
|
// Interaction handlers
|
|
const handlerFactory = useCallback(
|
|
(intent: StatusInteractionIntent) => {
|
|
return () => {
|
|
dispatch(statusInteraction({ statusId, intent, contextType }));
|
|
};
|
|
},
|
|
[contextType, dispatch, statusId],
|
|
);
|
|
|
|
const accountId = status?.account.id;
|
|
const onMention = useCallback(() => {
|
|
dispatch(mentionComposeById(accountId));
|
|
}, [dispatch, accountId]);
|
|
|
|
// Navigation handlers
|
|
const history = useHistory();
|
|
|
|
const onOpen = useCallback(
|
|
(newTab = false) => {
|
|
if (onClick || !status) {
|
|
onClick?.();
|
|
return;
|
|
}
|
|
|
|
const path = `/@${status.account.acct}/${status.id}`;
|
|
|
|
if (newTab) {
|
|
window.open(path, '_blank', 'noopener');
|
|
} else if (history.location.pathname.replace('/deck/', '/') === path) {
|
|
history.replace(path, { focusTarget: FOCUS_TARGET.POST });
|
|
} else {
|
|
history.push(path, { focusTarget: FOCUS_TARGET.POST });
|
|
}
|
|
},
|
|
[history, onClick, status],
|
|
);
|
|
|
|
const onOpenClick: React.MouseEventHandler = useCallback(
|
|
(event) => {
|
|
event.preventDefault();
|
|
|
|
if (event.button === 0 && !(event.ctrlKey || event.metaKey)) {
|
|
onOpen();
|
|
} else if (
|
|
event.button === 1 ||
|
|
(event.button === 0 && (event.ctrlKey || event.metaKey))
|
|
) {
|
|
onOpen(true);
|
|
}
|
|
},
|
|
[onOpen],
|
|
);
|
|
|
|
const onHeaderClick: React.MouseEventHandler = useCallback(
|
|
(event) => {
|
|
// Only handle clicks on the empty space above the content
|
|
if (event.target !== event.currentTarget && event.detail >= 1) {
|
|
return;
|
|
}
|
|
|
|
onOpenClick(event);
|
|
},
|
|
[onOpenClick],
|
|
);
|
|
|
|
const acct = status?.account.acct;
|
|
const onOpenProfile = useCallback(() => {
|
|
if (acct) {
|
|
history.push(`/@${acct}`);
|
|
}
|
|
}, [history, acct]);
|
|
|
|
const onOpenMedia = useCallback(() => {
|
|
const attachment = status?.media_attachments[0];
|
|
if (!attachment) {
|
|
return;
|
|
}
|
|
|
|
const lang = status.translation?.language ?? status.language;
|
|
if (attachment.type === 'video') {
|
|
dispatch(
|
|
openModal({
|
|
modalType: 'VIDEO',
|
|
modalProps: {
|
|
statusId: status.id,
|
|
media: attachment,
|
|
lang,
|
|
options: { startTime: 0 },
|
|
},
|
|
}),
|
|
);
|
|
} else {
|
|
dispatch(
|
|
openModal({
|
|
modalType: 'MEDIA',
|
|
modalProps: {
|
|
statusId: status.id,
|
|
media: status.media_attachments,
|
|
lang,
|
|
index: 0,
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
}, [dispatch, status]);
|
|
|
|
return useMemo(
|
|
() => ({
|
|
showDespiteFilter,
|
|
onOpenClick,
|
|
onExpandedToggle,
|
|
onFilterToggle,
|
|
onHeaderClick,
|
|
onMention,
|
|
onOpen,
|
|
onOpenMedia,
|
|
onOpenProfile,
|
|
onToggleHidden,
|
|
onReply: handlerFactory('reply'),
|
|
onFavourite: handlerFactory('favourite'),
|
|
onBoost: handlerFactory('reblog'),
|
|
onQuote: handlerFactory('quote'),
|
|
onTranslate: handlerFactory('translate'),
|
|
}),
|
|
[
|
|
handlerFactory,
|
|
onExpandedToggle,
|
|
onFilterToggle,
|
|
onHeaderClick,
|
|
onMention,
|
|
onOpen,
|
|
onOpenClick,
|
|
onOpenMedia,
|
|
onOpenProfile,
|
|
onToggleHidden,
|
|
showDespiteFilter,
|
|
],
|
|
);
|
|
}
|
|
export type StatusHandlers = ReturnType<typeof useStatusHandlers>;
|
|
|
|
const domParser = new DOMParser();
|
|
export function useTextForScreenReader({
|
|
statusId,
|
|
reblogAcct,
|
|
isQuote = false,
|
|
}: {
|
|
statusId?: string | null;
|
|
reblogAcct?: string;
|
|
isQuote?: boolean;
|
|
}) {
|
|
const intl = useIntl();
|
|
const status = useExpandedStatus(statusId);
|
|
return useMemo(() => {
|
|
if (!status) {
|
|
return '';
|
|
}
|
|
const displayName = status.account.display_name;
|
|
|
|
const spoilerText = status.translation?.spoiler_text ?? status.spoiler_text;
|
|
const contentHtml = status.translation?.contentHtml ?? status.contentHtml;
|
|
const contentText = domParser.parseFromString(contentHtml, 'text/html')
|
|
.documentElement.textContent;
|
|
|
|
const values = [
|
|
isQuote ? intl.formatMessage(messages.quote_noun) : undefined,
|
|
displayName.length === 0
|
|
? status.account.acct.split('@')[0]
|
|
: displayName,
|
|
spoilerText && status.hidden ? spoilerText : contentText,
|
|
status.quote ? intl.formatMessage(messages.contains_quote) : undefined,
|
|
intl.formatDate(status.created_at, {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
}),
|
|
status.account.acct,
|
|
reblogAcct
|
|
? intl.formatMessage(messages.boosted, { name: reblogAcct })
|
|
: false,
|
|
].filter((val) => !!val);
|
|
|
|
return values.join(', ');
|
|
}, [intl, isQuote, reblogAcct, status]);
|
|
}
|