frontend-rw #1

Merged
marcin merged 347 commits from frontend-rw into develop 2024-12-05 15:32:18 -08:00
15 changed files with 91 additions and 187 deletions
Showing only changes of commit 4722bccb5c - Show all commits

View file

@ -71,7 +71,7 @@ const updateNotifications = (notification: BaseNotification) =>
dispatch(importEntities({ dispatch(importEntities({
accounts: [notification.account, notification.type === 'move' ? notification.target : undefined], accounts: [notification.account, notification.type === 'move' ? notification.target : undefined],
statuses: [getNotificationStatus(notification)], statuses: [getNotificationStatus(notification) as any],
})); }));
if (showInColumn) { if (showInColumn) {

View file

@ -42,12 +42,6 @@ const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST' as const;
const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS' as const; const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS' as const;
const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL' as const; const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL' as const;
const STATUS_REVEAL_MEDIA = 'STATUS_REVEAL_MEDIA' as const;
const STATUS_HIDE_MEDIA = 'STATUS_HIDE_MEDIA' as const;
const STATUS_EXPAND_SPOILER = 'STATUS_EXPAND_SPOILER' as const;
const STATUS_COLLAPSE_SPOILER = 'STATUS_COLLAPSE_SPOILER' as const;
const STATUS_UNFILTER = 'STATUS_UNFILTER' as const; const STATUS_UNFILTER = 'STATUS_UNFILTER' as const;
const STATUS_LANGUAGE_CHANGE = 'STATUS_LANGUAGE_CHANGE' as const; const STATUS_LANGUAGE_CHANGE = 'STATUS_LANGUAGE_CHANGE' as const;
@ -212,58 +206,6 @@ const toggleMuteStatus = (status: Pick<Status, 'id' | 'muted'>) =>
} }
}; };
const hideStatusMedia = (statusIds: string[] | string) => {
if (!Array.isArray(statusIds)) {
statusIds = [statusIds];
}
return {
type: STATUS_HIDE_MEDIA,
statusIds,
};
};
const revealStatusMedia = (statusIds: string[] | string) => {
if (!Array.isArray(statusIds)) {
statusIds = [statusIds];
}
return {
type: STATUS_REVEAL_MEDIA,
statusIds,
};
};
const toggleStatusMediaHidden = (status: Pick<Status, 'id' | 'hidden'>) => {
if (status.hidden) {
return revealStatusMedia(status.id);
} else {
return hideStatusMedia(status.id);
}
};
const collapseStatusSpoiler = (statusIds: string[] | string) => {
if (!Array.isArray(statusIds)) {
statusIds = [statusIds];
}
return {
type: STATUS_COLLAPSE_SPOILER,
statusIds,
};
};
const expandStatusSpoiler = (statusIds: string[] | string) => {
if (!Array.isArray(statusIds)) {
statusIds = [statusIds];
}
return {
type: STATUS_EXPAND_SPOILER,
statusIds,
};
};
// let TRANSLATIONS_QUEUE: Set<string> = new Set(); // let TRANSLATIONS_QUEUE: Set<string> = new Set();
// let TRANSLATIONS_TIMEOUT: NodeJS.Timeout | null = null; // let TRANSLATIONS_TIMEOUT: NodeJS.Timeout | null = null;
@ -346,10 +288,6 @@ type StatusesAction =
| { type: typeof STATUS_UNMUTE_REQUEST; statusId: string } | { type: typeof STATUS_UNMUTE_REQUEST; statusId: string }
| { type: typeof STATUS_UNMUTE_SUCCESS; statusId: string } | { type: typeof STATUS_UNMUTE_SUCCESS; statusId: string }
| { type: typeof STATUS_UNMUTE_FAIL; statusId: string; error: unknown } | { type: typeof STATUS_UNMUTE_FAIL; statusId: string; error: unknown }
| ReturnType<typeof hideStatusMedia>
| ReturnType<typeof revealStatusMedia>
| ReturnType<typeof collapseStatusSpoiler>
| ReturnType<typeof expandStatusSpoiler>
| ReturnType<typeof unfilterStatus> | ReturnType<typeof unfilterStatus>
| ReturnType<typeof changeStatusLanguage>; | ReturnType<typeof changeStatusLanguage>;
@ -375,10 +313,6 @@ export {
STATUS_UNMUTE_REQUEST, STATUS_UNMUTE_REQUEST,
STATUS_UNMUTE_SUCCESS, STATUS_UNMUTE_SUCCESS,
STATUS_UNMUTE_FAIL, STATUS_UNMUTE_FAIL,
STATUS_REVEAL_MEDIA,
STATUS_HIDE_MEDIA,
STATUS_EXPAND_SPOILER,
STATUS_COLLAPSE_SPOILER,
STATUS_UNFILTER, STATUS_UNFILTER,
STATUS_LANGUAGE_CHANGE, STATUS_LANGUAGE_CHANGE,
createStatus, createStatus,
@ -391,11 +325,6 @@ export {
muteStatus, muteStatus,
unmuteStatus, unmuteStatus,
toggleMuteStatus, toggleMuteStatus,
hideStatusMedia,
revealStatusMedia,
toggleStatusMediaHidden,
expandStatusSpoiler,
collapseStatusSpoiler,
unfilterStatus, unfilterStatus,
changeStatusLanguage, changeStatusLanguage,
type StatusesAction, type StatusesAction,

View file

@ -4,7 +4,7 @@ import { MediaGallery } from 'pl-fe/features/ui/util/async-components';
import { useSettings } from 'pl-fe/hooks/use-settings'; import { useSettings } from 'pl-fe/hooks/use-settings';
import { useModalsStore } from 'pl-fe/stores/modals'; import { useModalsStore } from 'pl-fe/stores/modals';
import { isMediaVisible } from './statuses/sensitive-content-overlay'; import { useMediaVisible } from './statuses/sensitive-content-overlay';
import type { MediaAttachment } from 'pl-api'; import type { MediaAttachment } from 'pl-api';
import type { Status } from 'pl-fe/normalizers/status'; import type { Status } from 'pl-fe/normalizers/status';
@ -21,7 +21,7 @@ const AttachmentThumbs = ({ status, onClick }: IAttachmentThumbs) => {
const fallback = <div className='media-gallery--compact' />; const fallback = <div className='media-gallery--compact' />;
const onOpenMedia = (media: Array<MediaAttachment>, index: number) => openModal('MEDIA', { media, index }); const onOpenMedia = (media: Array<MediaAttachment>, index: number) => openModal('MEDIA', { media, index });
const visible = isMediaVisible(status, displayMedia); const visible = useMediaVisible(status, displayMedia);
return ( return (
<div className='relative'> <div className='relative'>

View file

@ -708,7 +708,7 @@ const MenuButton: React.FC<IMenuButton> = ({
}); });
}; };
const handleOpenReactionsModal = (): void => { const handleOpenReactionsModal = () => {
openModal('REACTIONS', { statusId: status.id }); openModal('REACTIONS', { statusId: status.id });
}; };

View file

@ -2,7 +2,6 @@ import clsx from 'clsx';
import React, { useState, useRef, useLayoutEffect, useMemo, useEffect } from 'react'; import React, { useState, useRef, useLayoutEffect, useMemo, useEffect } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { collapseStatusSpoiler, expandStatusSpoiler } from 'pl-fe/actions/statuses';
import { useStatusTranslation } from 'pl-fe/api/hooks/statuses/use-status-translation'; import { useStatusTranslation } from 'pl-fe/api/hooks/statuses/use-status-translation';
import Icon from 'pl-fe/components/icon'; import Icon from 'pl-fe/components/icon';
import Button from 'pl-fe/components/ui/button'; import Button from 'pl-fe/components/ui/button';
@ -10,7 +9,6 @@ import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text'; import Text from 'pl-fe/components/ui/text';
import Emojify from 'pl-fe/features/emoji/emojify'; import Emojify from 'pl-fe/features/emoji/emojify';
import QuotedStatus from 'pl-fe/features/status/containers/quoted-status-container'; import QuotedStatus from 'pl-fe/features/status/containers/quoted-status-container';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useSettings } from 'pl-fe/hooks/use-settings'; import { useSettings } from 'pl-fe/hooks/use-settings';
import { useStatusMetaStore } from 'pl-fe/stores/status-meta'; import { useStatusMetaStore } from 'pl-fe/stores/status-meta';
import { onlyEmoji as isOnlyEmoji } from 'pl-fe/utils/rich-content'; import { onlyEmoji as isOnlyEmoji } from 'pl-fe/utils/rich-content';
@ -83,7 +81,6 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
preview, preview,
withMedia, withMedia,
}) => { }) => {
const dispatch = useAppDispatch();
const { displaySpoilers } = useSettings(); const { displaySpoilers } = useSettings();
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
@ -93,8 +90,9 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
const node = useRef<HTMLDivElement>(null); const node = useRef<HTMLDivElement>(null);
const spoilerNode = useRef<HTMLSpanElement>(null); const spoilerNode = useRef<HTMLSpanElement>(null);
const { statuses: statusesMeta } = useStatusMetaStore(); const { statuses: statusesMeta, collapseStatus, expandStatus } = useStatusMetaStore();
const { data: translation } = useStatusTranslation(status.id, statusesMeta[status.id]?.targetLanguage); const statusMeta = statusesMeta[status.id] || {};
const { data: translation } = useStatusTranslation(status.id, statusMeta.targetLanguage);
const maybeSetCollapsed = (): void => { const maybeSetCollapsed = (): void => {
if (!node.current) return; if (!node.current) return;
@ -120,8 +118,8 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (expanded) dispatch(collapseStatusSpoiler(status.id)); if (expanded) collapseStatus(status.id);
else dispatch(expandStatusSpoiler(status.id)); else expandStatus(status.id);
}; };
useLayoutEffect(() => { useLayoutEffect(() => {
@ -166,7 +164,7 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
}); });
const expandable = !displaySpoilers; const expandable = !displaySpoilers;
const expanded = !withSpoiler || status.expanded || false; const expanded = !withSpoiler || statusMeta.expanded || false;
const output = []; const output = [];

View file

@ -7,14 +7,14 @@ import { MediaGallery, Video, Audio } from 'pl-fe/features/ui/util/async-compone
import { useSettings } from 'pl-fe/hooks/use-settings'; import { useSettings } from 'pl-fe/hooks/use-settings';
import { useModalsStore } from 'pl-fe/stores/modals'; import { useModalsStore } from 'pl-fe/stores/modals';
import { isMediaVisible } from './statuses/sensitive-content-overlay'; import { useMediaVisible } from './statuses/sensitive-content-overlay';
import type { MediaAttachment } from 'pl-api'; import type { MediaAttachment } from 'pl-api';
import type { Status } from 'pl-fe/normalizers/status'; import type { Status } from 'pl-fe/normalizers/status';
interface IStatusMedia { interface IStatusMedia {
/** Status entity to render media for. */ /** Status entity to render media for. */
status: Pick<Status, 'id' | 'account' | 'card' | 'expectsCard' | 'hidden' | 'media_attachments' | 'quote_id' | 'sensitive' | 'spoiler_text' | 'visibility'>; status: Pick<Status, 'id' | 'account' | 'card' | 'expectsCard' | 'media_attachments' | 'quote_id' | 'sensitive' | 'spoiler_text' | 'visibility'>;
/** Whether to display compact media. */ /** Whether to display compact media. */
muted?: boolean; muted?: boolean;
/** Callback when compact media is clicked. */ /** Callback when compact media is clicked. */
@ -30,7 +30,7 @@ const StatusMedia: React.FC<IStatusMedia> = ({
const { openModal } = useModalsStore(); const { openModal } = useModalsStore();
const { displayMedia } = useSettings(); const { displayMedia } = useSettings();
const visible = isMediaVisible(status, displayMedia); const visible = useMediaVisible(status, displayMedia);
const size = status.media_attachments.length; const size = status.media_attachments.length;
const firstAttachment = status.media_attachments[0]; const firstAttachment = status.media_attachments[0];

View file

@ -5,7 +5,7 @@ import { Link, useHistory } from 'react-router-dom';
import { mentionCompose, replyCompose } from 'pl-fe/actions/compose'; import { mentionCompose, replyCompose } from 'pl-fe/actions/compose';
import { toggleFavourite, toggleReblog } from 'pl-fe/actions/interactions'; import { toggleFavourite, toggleReblog } from 'pl-fe/actions/interactions';
import { toggleStatusMediaHidden, unfilterStatus } from 'pl-fe/actions/statuses'; import { unfilterStatus } from 'pl-fe/actions/statuses';
import Card from 'pl-fe/components/ui/card'; import Card from 'pl-fe/components/ui/card';
import Icon from 'pl-fe/components/ui/icon'; import Icon from 'pl-fe/components/ui/icon';
import Stack from 'pl-fe/components/ui/stack'; import Stack from 'pl-fe/components/ui/stack';
@ -19,6 +19,7 @@ import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import { useSettings } from 'pl-fe/hooks/use-settings'; import { useSettings } from 'pl-fe/hooks/use-settings';
import { makeGetStatus, type SelectedStatus } from 'pl-fe/selectors'; import { makeGetStatus, type SelectedStatus } from 'pl-fe/selectors';
import { useModalsStore } from 'pl-fe/stores/modals'; import { useModalsStore } from 'pl-fe/stores/modals';
import { useStatusMetaStore } from 'pl-fe/stores/status-meta';
import { textForScreenReader } from 'pl-fe/utils/status'; import { textForScreenReader } from 'pl-fe/utils/status';
import EventPreview from './event-preview'; import EventPreview from './event-preview';
@ -77,6 +78,7 @@ const Status: React.FC<IStatus> = (props) => {
const history = useHistory(); const history = useHistory();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { toggleStatusMediaHidden } = useStatusMetaStore();
const { openModal } = useModalsStore(); const { openModal } = useModalsStore();
const { boostModal } = useSettings(); const { boostModal } = useSettings();
const didShowCard = useRef(false); const didShowCard = useRef(false);
@ -96,7 +98,7 @@ const Status: React.FC<IStatus> = (props) => {
didShowCard.current = Boolean(!muted && !hidden && status?.card); didShowCard.current = Boolean(!muted && !hidden && status?.card);
}, []); }, []);
const handleClick = (e?: React.MouseEvent): void => { const handleClick = (e?: React.MouseEvent) => {
e?.stopPropagation(); e?.stopPropagation();
// If the user is selecting text, don't focus the status. // If the user is selecting text, don't focus the status.
@ -115,7 +117,7 @@ const Status: React.FC<IStatus> = (props) => {
} }
}; };
const handleHotkeyOpenMedia = (e?: KeyboardEvent): void => { const handleHotkeyOpenMedia = (e?: KeyboardEvent) => {
const status = actualStatus; const status = actualStatus;
const firstAttachment = status.media_attachments[0]; const firstAttachment = status.media_attachments[0];
@ -130,17 +132,17 @@ const Status: React.FC<IStatus> = (props) => {
} }
}; };
const handleHotkeyReply = (e?: KeyboardEvent): void => { const handleHotkeyReply = (e?: KeyboardEvent) => {
e?.preventDefault(); e?.preventDefault();
dispatch(replyCompose(actualStatus, status.reblog_id ? status.account : undefined)); dispatch(replyCompose(actualStatus, status.reblog_id ? status.account : undefined));
}; };
const handleHotkeyFavourite = (e?: KeyboardEvent): void => { const handleHotkeyFavourite = (e?: KeyboardEvent) => {
e?.preventDefault(); e?.preventDefault();
dispatch(toggleFavourite(actualStatus)); dispatch(toggleFavourite(actualStatus));
}; };
const handleHotkeyBoost = (e?: KeyboardEvent): void => { const handleHotkeyBoost = (e?: KeyboardEvent) => {
const modalReblog = () => dispatch(toggleReblog(actualStatus)); const modalReblog = () => dispatch(toggleReblog(actualStatus));
if ((e && e.shiftKey) || !boostModal) { if ((e && e.shiftKey) || !boostModal) {
modalReblog(); modalReblog();
@ -149,36 +151,36 @@ const Status: React.FC<IStatus> = (props) => {
} }
}; };
const handleHotkeyMention = (e?: KeyboardEvent): void => { const handleHotkeyMention = (e?: KeyboardEvent) => {
e?.preventDefault(); e?.preventDefault();
dispatch(mentionCompose(actualStatus.account)); dispatch(mentionCompose(actualStatus.account));
}; };
const handleHotkeyOpen = (): void => { const handleHotkeyOpen = () => {
history.push(statusUrl); history.push(statusUrl);
}; };
const handleHotkeyOpenProfile = (): void => { const handleHotkeyOpenProfile = () => {
history.push(`/@${actualStatus.account.acct}`); history.push(`/@${actualStatus.account.acct}`);
}; };
const handleHotkeyMoveUp = (e?: KeyboardEvent): void => { const handleHotkeyMoveUp = (e?: KeyboardEvent) => {
if (onMoveUp) { if (onMoveUp) {
onMoveUp(status.id, featured); onMoveUp(status.id, featured);
} }
}; };
const handleHotkeyMoveDown = (e?: KeyboardEvent): void => { const handleHotkeyMoveDown = (e?: KeyboardEvent) => {
if (onMoveDown) { if (onMoveDown) {
onMoveDown(status.id, featured); onMoveDown(status.id, featured);
} }
}; };
const handleHotkeyToggleSensitive = (): void => { const handleHotkeyToggleSensitive = () => {
dispatch(toggleStatusMediaHidden(actualStatus)); toggleStatusMediaHidden(actualStatus.id);
}; };
const handleHotkeyReact = (): void => { const handleHotkeyReact = () => {
(node.current?.querySelector('.emoji-picker-dropdown') as HTMLButtonElement)?.click(); (node.current?.querySelector('.emoji-picker-dropdown') as HTMLButtonElement)?.click();
}; };

View file

@ -2,27 +2,29 @@ import clsx from 'clsx';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { hideStatusMedia, revealStatusMedia } from 'pl-fe/actions/statuses';
import Button from 'pl-fe/components/ui/button'; import Button from 'pl-fe/components/ui/button';
import HStack from 'pl-fe/components/ui/hstack'; import HStack from 'pl-fe/components/ui/hstack';
import Text from 'pl-fe/components/ui/text'; import Text from 'pl-fe/components/ui/text';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useSettings } from 'pl-fe/hooks/use-settings'; import { useSettings } from 'pl-fe/hooks/use-settings';
import { useStatusMetaStore } from 'pl-fe/stores/status-meta';
import type { Status } from 'pl-fe/normalizers/status'; import type { Status } from 'pl-fe/normalizers/status';
const isMediaVisible = (status: Pick<Status, 'media_attachments' | 'sensitive' | 'spoiler_text'> & { hidden?: boolean | null }, displayMedia: 'default' | 'show_all' | 'hide_all') => { const useMediaVisible = (status: Pick<Status, 'media_attachments' | 'sensitive' | 'spoiler_text'> & { id?: string }, displayMedia: 'default' | 'show_all' | 'hide_all') => {
let visible = !(status.sensitive || status.spoiler_text); let visible = !(status.sensitive || status.spoiler_text);
if (status.hidden !== null) visible = !status.hidden; const statusesMeta = useStatusMetaStore().statuses;
const mediaVisible = status.id ? statusesMeta[status.id]?.mediaVisible : undefined;
if (mediaVisible !== undefined) visible = mediaVisible;
else if (displayMedia === 'show_all') visible = true; else if (displayMedia === 'show_all') visible = true;
else if (displayMedia === 'hide_all' && status.media_attachments.length) visible = false; else if (displayMedia === 'hide_all' && status.media_attachments.length) visible = false;
return visible; return visible;
}; };
const showOverlay = (status: Pick<Status, 'hidden' | 'media_attachments' | 'sensitive' | 'spoiler_text'>, displayMedia: 'default' | 'show_all' | 'hide_all') => { const useShowOverlay = (status: Pick<Status, 'id' | 'media_attachments' | 'sensitive' | 'spoiler_text'>, displayMedia: 'default' | 'show_all' | 'hide_all') => {
const visible = isMediaVisible(status, displayMedia); const visible = useMediaVisible(status, displayMedia);
const showHideButton = status.sensitive || (status.media_attachments.length && displayMedia === 'hide_all'); const showHideButton = status.sensitive || (status.media_attachments.length && displayMedia === 'hide_all');
@ -41,26 +43,27 @@ const messages = defineMessages({
}); });
interface ISensitiveContentOverlay { interface ISensitiveContentOverlay {
status: Pick<Status, 'id' | 'sensitive' | 'spoiler_text' | 'hidden' | 'media_attachments' | 'currentLanguage'>; status: Pick<Status, 'id' | 'sensitive' | 'spoiler_text' | 'media_attachments' | 'currentLanguage'>;
} }
const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveContentOverlay>((props, ref) => { const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveContentOverlay>((props, ref) => {
const { status } = props; const { status } = props;
const dispatch = useAppDispatch();
const intl = useIntl(); const intl = useIntl();
const { displayMedia } = useSettings(); const { displayMedia } = useSettings();
const visible = isMediaVisible(status, displayMedia); const visible = useMediaVisible(status, displayMedia);
const { hideStatusMedia, revealStatusMedia } = useStatusMetaStore();
const toggleVisibility = (event: React.MouseEvent<HTMLButtonElement>) => { const toggleVisibility = (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation(); event.stopPropagation();
if (visible) dispatch(hideStatusMedia(status.id)); if (visible) hideStatusMedia(status.id);
else dispatch(revealStatusMedia(status.id)); else revealStatusMedia(status.id);
}; };
if (!showOverlay(status, displayMedia)) return null; if (!useShowOverlay(status, displayMedia)) return null;
return ( return (
<div <div
@ -110,4 +113,4 @@ const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveConte
); );
}); });
export { SensitiveContentOverlay as default, isMediaVisible }; export { SensitiveContentOverlay as default, useMediaVisible };

View file

@ -12,7 +12,7 @@ import type { Status } from 'pl-fe/normalizers/status';
interface IReplyIndicator { interface IReplyIndicator {
className?: string; className?: string;
status?: Pick<Status, 'account_id' | 'content' | 'created_at' | 'emojis' | 'hidden' | 'media_attachments' | 'mentions' | 'search_index' | 'sensitive' | 'spoiler_text' | 'quote_id'>; status?: Pick<Status, 'account_id' | 'content' | 'created_at' | 'emojis' | 'media_attachments' | 'mentions' | 'search_index' | 'sensitive' | 'spoiler_text' | 'quote_id'>;
onCancel?: () => void; onCancel?: () => void;
hideActions: boolean; hideActions: boolean;
} }

View file

@ -4,7 +4,6 @@ import { Link, useHistory } from 'react-router-dom';
import { mentionCompose } from 'pl-fe/actions/compose'; import { mentionCompose } from 'pl-fe/actions/compose';
import { reblog, favourite, unreblog, unfavourite } from 'pl-fe/actions/interactions'; import { reblog, favourite, unreblog, unfavourite } from 'pl-fe/actions/interactions';
import { toggleStatusMediaHidden } from 'pl-fe/actions/statuses';
import HoverAccountWrapper from 'pl-fe/components/hover-account-wrapper'; import HoverAccountWrapper from 'pl-fe/components/hover-account-wrapper';
import Icon from 'pl-fe/components/icon'; import Icon from 'pl-fe/components/icon';
import RelativeTimestamp from 'pl-fe/components/relative-timestamp'; import RelativeTimestamp from 'pl-fe/components/relative-timestamp';
@ -22,6 +21,7 @@ import { useLoggedIn } from 'pl-fe/hooks/use-logged-in';
import { makeGetNotification } from 'pl-fe/selectors'; import { makeGetNotification } from 'pl-fe/selectors';
import { useModalsStore } from 'pl-fe/stores/modals'; import { useModalsStore } from 'pl-fe/stores/modals';
import { useSettingsStore } from 'pl-fe/stores/settings'; import { useSettingsStore } from 'pl-fe/stores/settings';
import { useStatusMetaStore } from 'pl-fe/stores/status-meta';
import { NotificationType } from 'pl-fe/utils/notification'; import { NotificationType } from 'pl-fe/utils/notification';
import type { NotificationGroup } from 'pl-api'; import type { NotificationGroup } from 'pl-api';
@ -188,7 +188,7 @@ interface INotification {
onReblog?: (status: StatusEntity, e?: KeyboardEvent) => void; onReblog?: (status: StatusEntity, e?: KeyboardEvent) => void;
} }
const getNotificationStatus = (n: Pick<NotificationGroup, 'type'> & ({ status: StatusEntity } | { })) => { const getNotificationStatus = (n: Pick<NotificationGroup, 'type'> & ({ status: StatusEntity } | { })): StatusEntity | null => {
if (['mention', 'status', 'reblog', 'favourite', 'poll', 'update', 'emoji_reaction', 'event_reminder', 'participation_accepted', 'participation_request'].includes(n.type)) if (['mention', 'status', 'reblog', 'favourite', 'poll', 'update', 'emoji_reaction', 'event_reminder', 'participation_accepted', 'participation_request'].includes(n.type))
// @ts-ignore // @ts-ignore
return n.status; return n.status;
@ -203,6 +203,7 @@ const Notification: React.FC<INotification> = (props) => {
const getNotification = useCallback(makeGetNotification(), []); const getNotification = useCallback(makeGetNotification(), []);
const { me } = useLoggedIn(); const { me } = useLoggedIn();
const { toggleStatusMediaHidden } = useStatusMetaStore();
const { openModal } = useModalsStore(); const { openModal } = useModalsStore();
const { settings } = useSettingsStore(); const { settings } = useSettingsStore();
@ -281,9 +282,9 @@ const Notification: React.FC<INotification> = (props) => {
} }
}, [status]); }, [status]);
const handleHotkeyToggleSensitive = useCallback((e?: KeyboardEvent) => { const handleHotkeyToggleSensitive = useCallback(() => {
if (status && typeof status === 'object') { if (status && typeof status === 'object') {
dispatch(toggleStatusMediaHidden(status)); toggleStatusMediaHidden(status.id);
} }
}, [status]); }, [status]);
@ -299,7 +300,7 @@ const Notification: React.FC<INotification> = (props) => {
} }
}; };
const displayedType = notification.type === 'mention' && (notification.subtype === 'reply' || status.in_reply_to_account_id === me) ? 'reply' : notification.type; const displayedType = notification.type === 'mention' && (notification.subtype === 'reply' || status?.in_reply_to_account_id === me) ? 'reply' : notification.type;
const renderIcon = (): React.ReactNode => { const renderIcon = (): React.ReactNode => {
if (type === 'emoji_reaction' && notification.emoji) { if (type === 'emoji_reaction' && notification.emoji) {

View file

@ -7,7 +7,6 @@ import { useHistory } from 'react-router-dom';
import { type ComposeReplyAction, mentionCompose, replyCompose } from 'pl-fe/actions/compose'; import { type ComposeReplyAction, mentionCompose, replyCompose } from 'pl-fe/actions/compose';
import { reblog, toggleFavourite, unreblog } from 'pl-fe/actions/interactions'; import { reblog, toggleFavourite, unreblog } from 'pl-fe/actions/interactions';
import { toggleStatusMediaHidden } from 'pl-fe/actions/statuses';
import ScrollableList from 'pl-fe/components/scrollable-list'; import ScrollableList from 'pl-fe/components/scrollable-list';
import StatusActionBar from 'pl-fe/components/status-action-bar'; import StatusActionBar from 'pl-fe/components/status-action-bar';
import Tombstone from 'pl-fe/components/tombstone'; import Tombstone from 'pl-fe/components/tombstone';
@ -20,6 +19,7 @@ import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import { RootState } from 'pl-fe/store'; import { RootState } from 'pl-fe/store';
import { useModalsStore } from 'pl-fe/stores/modals'; import { useModalsStore } from 'pl-fe/stores/modals';
import { useSettingsStore } from 'pl-fe/stores/settings'; import { useSettingsStore } from 'pl-fe/stores/settings';
import { useStatusMetaStore } from 'pl-fe/stores/status-meta';
import { textForScreenReader } from 'pl-fe/utils/status'; import { textForScreenReader } from 'pl-fe/utils/status';
import DetailedStatus from './detailed-status'; import DetailedStatus from './detailed-status';
@ -113,6 +113,7 @@ const Thread: React.FC<IThread> = ({
const history = useHistory(); const history = useHistory();
const intl = useIntl(); const intl = useIntl();
const { toggleStatusMediaHidden } = useStatusMetaStore();
const { openModal } = useModalsStore(); const { openModal } = useModalsStore();
const { settings } = useSettingsStore(); const { settings } = useSettingsStore();
@ -159,7 +160,7 @@ const Thread: React.FC<IThread> = ({
const handleMentionClick = (account: Pick<Account, 'acct'>) => dispatch(mentionCompose(account)); const handleMentionClick = (account: Pick<Account, 'acct'>) => dispatch(mentionCompose(account));
const handleHotkeyOpenMedia = (e?: KeyboardEvent) => { const handleHotkeyOpenMedia = (e?: KeyboardEvent) => {
const media = status?.media_attachments; const media = status.media_attachments;
e?.preventDefault(); e?.preventDefault();
@ -175,43 +176,43 @@ const Thread: React.FC<IThread> = ({
}; };
const handleHotkeyMoveUp = () => { const handleHotkeyMoveUp = () => {
handleMoveUp(status!.id); handleMoveUp(status.id);
}; };
const handleHotkeyMoveDown = () => { const handleHotkeyMoveDown = () => {
handleMoveDown(status!.id); handleMoveDown(status.id);
}; };
const handleHotkeyReply = (e?: KeyboardEvent) => { const handleHotkeyReply = (e?: KeyboardEvent) => {
e?.preventDefault(); e?.preventDefault();
handleReplyClick(status!); handleReplyClick(status);
}; };
const handleHotkeyFavourite = () => { const handleHotkeyFavourite = () => {
handleFavouriteClick(status!); handleFavouriteClick(status);
}; };
const handleHotkeyBoost = () => { const handleHotkeyBoost = () => {
handleReblogClick(status!); handleReblogClick(status);
}; };
const handleHotkeyMention = (e?: KeyboardEvent) => { const handleHotkeyMention = (e?: KeyboardEvent) => {
e?.preventDefault(); e?.preventDefault();
const { account } = status!; const { account } = status;
if (!account || typeof account !== 'object') return; if (!account || typeof account !== 'object') return;
handleMentionClick(account); handleMentionClick(account);
}; };
const handleHotkeyOpenProfile = () => { const handleHotkeyOpenProfile = () => {
history.push(`/@${status!.account.acct}`); history.push(`/@${status.account.acct}`);
}; };
const handleHotkeyToggleSensitive = () => { const handleHotkeyToggleSensitive = () => {
dispatch(toggleStatusMediaHidden(status)); toggleStatusMediaHidden(status.id);
}; };
const handleMoveUp = (id: string) => { const handleMoveUp = (id: string) => {
if (id === status?.id) { if (id === status.id) {
_selectChild(ancestorsIds.length - 1); _selectChild(ancestorsIds.length - 1);
} else { } else {
let index = ancestorsIds.indexOf(id); let index = ancestorsIds.indexOf(id);
@ -226,7 +227,7 @@ const Thread: React.FC<IThread> = ({
}; };
const handleMoveDown = (id: string) => { const handleMoveDown = (id: string) => {
if (id === status?.id) { if (id === status.id) {
_selectChild(ancestorsIds.length + 1); _selectChild(ancestorsIds.length + 1);
} else { } else {
let index = ancestorsIds.indexOf(id); let index = ancestorsIds.indexOf(id);
@ -269,7 +270,7 @@ const Thread: React.FC<IThread> = ({
<ThreadStatus <ThreadStatus
key={id} key={id}
id={id} id={id}
focusedStatusId={status!.id} focusedStatusId={status.id}
onMoveUp={handleMoveUp} onMoveUp={handleMoveUp}
onMoveDown={handleMoveDown} onMoveDown={handleMoveDown}
contextType='thread' contextType='thread'

View file

@ -18,8 +18,6 @@ type StatusVisibility = 'public' | 'unlisted' | 'private' | 'direct' | 'group' |
type CalculatedValues = { type CalculatedValues = {
search_index: string; search_index: string;
expanded?: boolean | null;
hidden?: boolean | null;
currentLanguage?: string; currentLanguage?: string;
}; };
@ -56,11 +54,11 @@ const buildSearchContent = (status: Pick<BaseStatus, 'poll' | 'mentions' | 'spoi
const calculateStatus = (status: BaseStatus, oldStatus?: OldStatus): CalculatedValues => { const calculateStatus = (status: BaseStatus, oldStatus?: OldStatus): CalculatedValues => {
if (oldStatus && oldStatus.content === status.content && oldStatus.spoiler_text === status.spoiler_text) { if (oldStatus && oldStatus.content === status.content && oldStatus.spoiler_text === status.spoiler_text) {
const { const {
search_index, hidden, expanded, currentLanguage, search_index, currentLanguage,
} = oldStatus; } = oldStatus;
return { return {
search_index, hidden, expanded, currentLanguage, search_index, currentLanguage,
}; };
} else { } else {
const searchContent = buildSearchContent(status); const searchContent = buildSearchContent(status);
@ -135,8 +133,6 @@ const normalizeStatus = (status: BaseStatus & {
account: normalizeAccount(status.account), account: normalizeAccount(status.account),
accounts: status.accounts?.map(normalizeAccount), accounts: status.accounts?.map(normalizeAccount),
mentions, mentions,
expanded: null,
hidden: null,
filtered: status.filtered?.map(result => result.filter.title), filtered: status.filtered?.map(result => result.filter.title),
event, event,
group, group,

View file

@ -36,14 +36,10 @@ import {
STATUS_CREATE_FAIL, STATUS_CREATE_FAIL,
STATUS_DELETE_REQUEST, STATUS_DELETE_REQUEST,
STATUS_DELETE_FAIL, STATUS_DELETE_FAIL,
STATUS_HIDE_MEDIA,
STATUS_MUTE_SUCCESS, STATUS_MUTE_SUCCESS,
STATUS_REVEAL_MEDIA,
STATUS_UNFILTER, STATUS_UNFILTER,
STATUS_UNMUTE_SUCCESS, STATUS_UNMUTE_SUCCESS,
STATUS_LANGUAGE_CHANGE, STATUS_LANGUAGE_CHANGE,
STATUS_COLLAPSE_SPOILER,
STATUS_EXPAND_SPOILER,
type StatusesAction, type StatusesAction,
} from '../actions/statuses'; } from '../actions/statuses';
import { TIMELINE_DELETE, type TimelineAction } from '../actions/timelines'; import { TIMELINE_DELETE, type TimelineAction } from '../actions/timelines';
@ -251,42 +247,6 @@ const statuses = (state = initialState, action: EmojiReactsAction | EventsAction
status.muted = false; status.muted = false;
} }
}); });
case STATUS_REVEAL_MEDIA:
return create(state, (draft) => {
action.statusIds.forEach((id: string) => {
const status = draft[id];
if (status) {
status.hidden = false;
}
});
});
case STATUS_HIDE_MEDIA:
return create(state, (draft) => {
action.statusIds.forEach((id: string) => {
const status = draft[id];
if (status) {
status.hidden = true;
}
});
});
case STATUS_EXPAND_SPOILER:
return create(state, (draft) => {
action.statusIds.forEach((id: string) => {
const status = draft[id];
if (status) {
status.expanded = true;
}
});
});
case STATUS_COLLAPSE_SPOILER:
return create(state, (draft) => {
action.statusIds.forEach((id: string) => {
const status = draft[id];
if (status) {
status.expanded = false;
}
});
});
case STATUS_DELETE_REQUEST: case STATUS_DELETE_REQUEST:
return create(state, (draft) => decrementReplyCount(draft, action.params)); return create(state, (draft) => decrementReplyCount(draft, action.params));
case STATUS_DELETE_FAIL: case STATUS_DELETE_FAIL:

View file

@ -2,25 +2,39 @@ import { create } from 'zustand';
import { mutative } from 'zustand-mutative'; import { mutative } from 'zustand-mutative';
type State = { type State = {
statuses: Record<string, { visible?: boolean; targetLanguage?: string }>; statuses: Record<string, { expanded?: boolean; mediaVisible?: boolean; targetLanguage?: string }>;
revealStatus: (statusId: string) => void; expandStatus: (statusId: string) => void;
hideStatus: (statusId: string) => void; collapseStatus: (statusId: string) => void;
revealStatusMedia: (statusId: string) => void;
hideStatusMedia: (statusId: string) => void;
toggleStatusMediaHidden: (statusId: string) => void;
fetchTranslation: (statusId: string, targetLanguage: string) => void; fetchTranslation: (statusId: string, targetLanguage: string) => void;
hideTranslation: (statusId: string) => void; hideTranslation: (statusId: string) => void;
}; };
const useStatusMetaStore = create<State>()(mutative((set) => ({ const useStatusMetaStore = create<State>()(mutative((set) => ({
statuses: {}, statuses: {},
revealStatus: (statusId) => set((state: State) => { expandStatus: (statusId) => set((state: State) => {
if (!state.statuses[statusId]) state.statuses[statusId] = {}; if (!state.statuses[statusId]) state.statuses[statusId] = {};
state.statuses[statusId].visible = true; state.statuses[statusId].expanded = true;
}), }),
hideStatus: (statusId) => set((state: State) => { collapseStatus: (statusId) => set((state: State) => {
if (!state.statuses[statusId]) state.statuses[statusId] = {}; if (!state.statuses[statusId]) state.statuses[statusId] = {};
state.statuses[statusId].visible = false; state.statuses[statusId].expanded = false;
}), }),
revealStatusMedia: (statusId) => set((state: State) => {
if (!state.statuses[statusId]) state.statuses[statusId] = {};
state.statuses[statusId].mediaVisible = true;
}),
hideStatusMedia: (statusId) => set((state: State) => {
if (!state.statuses[statusId]) state.statuses[statusId] = {};
state.statuses[statusId].mediaVisible = false;
}),
toggleStatusMediaHidden: (statusId) => (state: State) => state[state.statuses[statusId].mediaVisible ? 'hideStatusMedia' : 'revealStatusMedia'](statusId),
fetchTranslation: (statusId, targetLanguage) => set((state: State) => { fetchTranslation: (statusId, targetLanguage) => set((state: State) => {
if (!state.statuses[statusId]) state.statuses[statusId] = {}; if (!state.statuses[statusId]) state.statuses[statusId] = {};

View file

@ -34,7 +34,7 @@ const hasIntegerMediaIds = (status: Pick<Status, 'media_attachments'>): boolean
/** Sanitize status text for use with screen readers. */ /** Sanitize status text for use with screen readers. */
const textForScreenReader = ( const textForScreenReader = (
intl: IntlShape, intl: IntlShape,
status: Pick<Status, 'account' | 'spoiler_text' | 'hidden' | 'search_index' | 'created_at'>, status: Pick<Status, 'account' | 'spoiler_text' | 'search_index' | 'created_at'>,
rebloggedByText?: string, rebloggedByText?: string,
): string => { ): string => {
const { account } = status; const { account } = status;
@ -44,7 +44,7 @@ const textForScreenReader = (
const values = [ const values = [
displayName.length === 0 ? account.acct.split('@')[0] : displayName, displayName.length === 0 ? account.acct.split('@')[0] : displayName,
status.spoiler_text && status.hidden ? status.spoiler_text : status.search_index?.slice(status.spoiler_text.length) || '', status.spoiler_text ? status.spoiler_text : status.search_index?.slice(status.spoiler_text.length) || '',
intl.formatDate(status.created_at, { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }), intl.formatDate(status.created_at, { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }),
account.acct, account.acct,
]; ];