diff --git a/src/api/hooks/statuses/useBookmarks.ts b/src/api/hooks/statuses/useBookmarks.ts index 012870ec6..8235d917c 100644 --- a/src/api/hooks/statuses/useBookmarks.ts +++ b/src/api/hooks/statuses/useBookmarks.ts @@ -1,14 +1,20 @@ -import { EntityTypes, Entities } from 'soapbox/entity-store/entities.ts'; +import { Entities } from 'soapbox/entity-store/entities.ts'; import { useEntities } from 'soapbox/entity-store/hooks/index.ts'; import { useApi } from 'soapbox/hooks/useApi.ts'; import { useFeatures } from 'soapbox/hooks/useFeatures.ts'; -import { statusSchema } from 'soapbox/schemas/status.ts'; +import { Status as StatusEntity, statusSchema } from 'soapbox/schemas/index.ts'; +/** + * Get all the statuses the user has bookmarked. + * https://docs.joinmastodon.org/methods/bookmarks/#get + * GET /api/v1/bookmarks + * TODO: add 'limit' + */ function useBookmarks() { const api = useApi(); const features = useFeatures(); - const { entities, ...result } = useEntities( + const { entities, ...result } = useEntities( [Entities.STATUSES, 'bookmarks'], () => api.get('/api/v1/bookmarks'), { enabled: features.bookmarks, schema: statusSchema }, diff --git a/src/components/attachment-thumbs.tsx b/src/components/attachment-thumbs.tsx index 4829b7688..2b4aadf75 100644 --- a/src/components/attachment-thumbs.tsx +++ b/src/components/attachment-thumbs.tsx @@ -3,12 +3,10 @@ import { Suspense } from 'react'; import { openModal } from 'soapbox/actions/modals.ts'; import { MediaGallery } from 'soapbox/features/ui/util/async-components.ts'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; - -import type { List as ImmutableList } from 'immutable'; -import type { Attachment } from 'soapbox/types/entities.ts'; +import { Attachment } from 'soapbox/schemas/index.ts'; interface IAttachmentThumbs { - media: ImmutableList; + media: readonly Attachment[]; onClick?(): void; sensitive?: boolean; } @@ -18,7 +16,7 @@ const AttachmentThumbs = (props: IAttachmentThumbs) => { const dispatch = useAppDispatch(); const fallback =
; - const onOpenMedia = (media: ImmutableList, index: number) => dispatch(openModal('MEDIA', { media, index })); + const onOpenMedia = (media: readonly Attachment[], index: number) => dispatch(openModal('MEDIA', { media, index })); return (
diff --git a/src/components/dropdown-menu/dropdown-menu.tsx b/src/components/dropdown-menu/dropdown-menu.tsx index 161d92307..08189700f 100644 --- a/src/components/dropdown-menu/dropdown-menu.tsx +++ b/src/components/dropdown-menu/dropdown-menu.tsx @@ -11,11 +11,10 @@ import IconButton from 'soapbox/components/ui/icon-button.tsx'; import Portal from 'soapbox/components/ui/portal.tsx'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { userTouching } from 'soapbox/is-mobile.ts'; +import { Status as StatusEntity } from 'soapbox/schemas/index.ts'; import DropdownMenuItem, { MenuItem } from './dropdown-menu-item.tsx'; -import type { Status } from 'soapbox/types/entities.ts'; - export type Menu = Array; interface IDropdownMenu { @@ -28,7 +27,7 @@ interface IDropdownMenu { onShiftClick?: React.EventHandler; placement?: Placement; src?: string; - status?: Status; + status?: StatusEntity; title?: string; } diff --git a/src/components/media-gallery.tsx b/src/components/media-gallery.tsx index 085d95e61..dfa824542 100644 --- a/src/components/media-gallery.tsx +++ b/src/components/media-gallery.tsx @@ -8,7 +8,7 @@ import StillImage from 'soapbox/components/still-image.tsx'; import { MIMETYPE_ICONS } from 'soapbox/components/upload.tsx'; import { useSettings } from 'soapbox/hooks/useSettings.ts'; import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts'; -import { Attachment } from 'soapbox/types/entities.ts'; +import { Attachment } from 'soapbox/schemas/index.ts'; import { truncateFilename } from 'soapbox/utils/media.ts'; import { isIOS } from '../is-mobile.ts'; @@ -16,8 +16,6 @@ import { isPanoramic, isPortrait, isNonConformingRatio, minimumAspectRatio, maxi import SvgIcon from './ui/svg-icon.tsx'; -import type { List as ImmutableList } from 'immutable'; - // const Gameboy = lazy(() => import('./gameboy')); const ATTACHMENT_LIMIT = 4; @@ -46,7 +44,8 @@ const withinLimits = (aspectRatio: number) => { }; const shouldLetterbox = (attachment: Attachment): boolean => { - const aspectRatio = attachment.getIn(['meta', 'original', 'aspect']) as number | undefined; + const aspectRatio = 'meta' in attachment && 'original' in attachment.meta && (attachment).meta.original?.aspect; + if (!aspectRatio) return true; return !withinLimits(aspectRatio); @@ -159,7 +158,7 @@ const Item: React.FC = ({ const attachmentIcon = ( ); @@ -291,9 +290,9 @@ const Item: React.FC = ({ export interface IMediaGallery { sensitive?: boolean; - media: ImmutableList; + media: readonly Attachment[]; height?: number; - onOpenMedia: (media: ImmutableList, index: number) => void; + onOpenMedia: (media: readonly Attachment[], index: number) => void; defaultWidth?: number; cacheWidth?: (width: number) => void; visible?: boolean; @@ -323,7 +322,7 @@ const MediaGallery: React.FC = (props) => { const getSizeDataSingle = (): SizeData => { const w = width || defaultWidth; - const aspectRatio = media.getIn([0, 'meta', 'original', 'aspect']) as number | undefined; + const aspectRatio = 'meta' in media[0] && 'original' in media[0].meta && (media[0])?.meta.original?.aspect; const getHeight = () => { if (!aspectRatio) return w * 9 / 16; @@ -349,7 +348,9 @@ const MediaGallery: React.FC = (props) => { let itemsDimensions: Dimensions[] = []; const ratios = Array(size).fill(null).map((_, i) => - media.getIn([i, 'meta', 'original', 'aspect']) as number, + 'meta' in media[i] && 'original' in media[i].meta && typeof media[i].meta.original?.aspect === 'number' + ? media[i].meta.original.aspect + : undefined as unknown as number, // NOTE: the old logic returned undefined anyways, and the implementation of the functions below call 'isNaN', such as the 'isPortrait' function ); const [ar1, ar2, ar3, ar4] = ratios; @@ -547,9 +548,9 @@ const MediaGallery: React.FC = (props) => { }; }; - const sizeData: SizeData = getSizeData(media.size); + const sizeData: SizeData = getSizeData(media.length); - const children = media.take(ATTACHMENT_LIMIT).map((attachment, i) => ( + const children = media.slice(0, ATTACHMENT_LIMIT).map((attachment, i) => ( = (props) => { visible={!!props.visible} dimensions={sizeData.itemsDimensions[i]} last={i === ATTACHMENT_LIMIT - 1} - total={media.size} + total={media.length} compact={compact} /> )); diff --git a/src/components/preview-card.tsx b/src/components/preview-card.tsx index c9f70c981..3e8282396 100644 --- a/src/components/preview-card.tsx +++ b/src/components/preview-card.tsx @@ -3,7 +3,6 @@ import linkIcon from '@tabler/icons/outline/link.svg'; import playerPlayIcon from '@tabler/icons/outline/player-play.svg'; import zoomInIcon from '@tabler/icons/outline/zoom-in.svg'; import clsx from 'clsx'; -import { List as ImmutableList } from 'immutable'; import { useState, useEffect, useRef } from 'react'; import Blurhash from 'soapbox/components/blurhash.tsx'; @@ -12,17 +11,18 @@ import Stack from 'soapbox/components/ui/stack.tsx'; import SvgIcon from 'soapbox/components/ui/svg-icon.tsx'; import Text from 'soapbox/components/ui/text.tsx'; import { normalizeAttachment } from 'soapbox/normalizers/index.ts'; +import { Attachment } from 'soapbox/schemas/index.ts'; import { addAutoPlay } from 'soapbox/utils/media.ts'; import { getTextDirection } from 'soapbox/utils/rtl.ts'; -import type { Card as CardEntity, Attachment } from 'soapbox/types/entities.ts'; +import type { Card as CardEntity } from 'soapbox/types/entities.ts'; /** Props for `PreviewCard`. */ interface IPreviewCard { card: CardEntity; maxTitle?: number; maxDescription?: number; - onOpenMedia: (attachments: ImmutableList, index: number) => void; + onOpenMedia: (attachments: readonly Attachment[], index: number) => void; compact?: boolean; defaultWidth?: number; cacheWidth?: (width: number) => void; @@ -73,9 +73,9 @@ const PreviewCard: React.FC = ({ height: card.height, }, }, - }); + }).toJS(); - onOpenMedia(ImmutableList([attachment]), 0); + onOpenMedia([{ ...attachment, blurhash: attachment.blurhash === undefined ? null : attachment.blurhash } as Attachment], 0); }; const handleEmbedClick: React.MouseEventHandler = (e) => { diff --git a/src/components/pure-event-preview.tsx b/src/components/pure-event-preview.tsx index 1c06724ec..57915e364 100644 --- a/src/components/pure-event-preview.tsx +++ b/src/components/pure-event-preview.tsx @@ -7,7 +7,6 @@ import Button from 'soapbox/components/ui/button.tsx'; import HStack from 'soapbox/components/ui/hstack.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; import Text from 'soapbox/components/ui/text.tsx'; -import { EntityTypes, Entities } from 'soapbox/entity-store/entities.ts'; import PureEventActionButton from 'soapbox/features/event/components/pure-event-action-button.tsx'; import PureEventDate from 'soapbox/features/event/components/pure-event-date.tsx'; import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; @@ -15,6 +14,7 @@ import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; import Icon from './icon.tsx'; import VerificationBadge from './verification-badge.tsx'; +import type { Status as StatusEntity } from 'soapbox/schemas/index.ts'; const messages = defineMessages({ eventBanner: { id: 'event.banner', defaultMessage: 'Event banner' }, @@ -23,7 +23,7 @@ const messages = defineMessages({ }); interface IPureEventPreview { - status: EntityTypes[Entities.STATUSES]; + status: StatusEntity; className?: string; hideAction?: boolean; floatingAction?: boolean; diff --git a/src/components/pure-status-content.tsx b/src/components/pure-status-content.tsx index a728ecd6b..b28d27c37 100644 --- a/src/components/pure-status-content.tsx +++ b/src/components/pure-status-content.tsx @@ -4,7 +4,7 @@ import { useState, useRef, useLayoutEffect, useMemo, memo } from 'react'; import { FormattedMessage } from 'react-intl'; import Icon from 'soapbox/components/icon.tsx'; -import { Entities, EntityTypes } from 'soapbox/entity-store/entities.ts'; +import { Status as StatusEntity } from 'soapbox/schemas/index.ts'; import { isOnlyEmoji as _isOnlyEmoji } from 'soapbox/utils/only-emoji.ts'; import { getTextDirection } from 'soapbox/utils/rtl.ts'; @@ -28,7 +28,7 @@ const ReadMoreButton: React.FC = ({ onClick }) => ( ); interface IPureStatusContent { - status: EntityTypes[Entities.STATUSES]; + status: StatusEntity; onClick?: () => void; collapsable?: boolean; translatable?: boolean; diff --git a/src/components/pure-status-list.tsx b/src/components/pure-status-list.tsx index 11a12907b..be3ed8914 100644 --- a/src/components/pure-status-list.tsx +++ b/src/components/pure-status-list.tsx @@ -6,11 +6,11 @@ import { FormattedMessage } from 'react-intl'; import LoadGap from 'soapbox/components/load-gap.tsx'; import PureStatus from 'soapbox/components/pure-status.tsx'; import ScrollableList from 'soapbox/components/scrollable-list.tsx'; -import { EntityTypes, Entities } from 'soapbox/entity-store/entities.ts'; import FeedSuggestions from 'soapbox/features/feed-suggestions/feed-suggestions.tsx'; import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder-status.tsx'; import PendingStatus from 'soapbox/features/ui/components/pending-status.tsx'; import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts'; +import { Status as StatusEntity } from 'soapbox/schemas/index.ts'; import type { VirtuosoHandle } from 'react-virtuoso'; import type { IScrollableList } from 'soapbox/components/scrollable-list.tsx'; @@ -19,11 +19,11 @@ interface IPureStatusList extends Omit void; /** Whether the data is currently being fetched. */ @@ -124,7 +124,7 @@ const PureStatusList: React.FC = ({ ); }; - const renderStatus = (status: EntityTypes[Entities.STATUSES]) => { + const renderStatus = (status: StatusEntity) => { return ( void; muted?: boolean; hidden?: boolean; @@ -127,16 +126,7 @@ const PureStatus: React.FC = (props) => { } }, [overlay.current]); - // TODO: remove this code, it will be removed once all components in this file are pure. - useEffect(() => { - dispatch(importFetchedStatuses([status])); - }, []); - const getStatus = useCallback(makeGetStatus(), []); - const statusImmutable = useAppSelector(state => getStatus(state, { id: status.id })); - if (!statusImmutable) { - return null; - } - // END TODO + const statusImmutable = normalizeStatus(status) as LegacyStatus; // TODO: remove this line, it will be removed once all components in this file are pure. const handleToggleMediaVisibility = (): void => { setShowMedia(!showMedia); @@ -486,7 +476,7 @@ const PureStatus: React.FC = (props) => { {(quote || actualStatus.card || actualStatus.media_attachments.length > 0) && ( = ({ status }) => { diff --git a/src/components/quoted-status.tsx b/src/components/quoted-status.tsx index 657240f3b..e509222c5 100644 --- a/src/components/quoted-status.tsx +++ b/src/components/quoted-status.tsx @@ -8,6 +8,7 @@ import StatusMedia from 'soapbox/components/status-media.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; import AccountContainer from 'soapbox/containers/account-container.tsx'; import { useSettings } from 'soapbox/hooks/useSettings.ts'; +import { Status as StatusEntity } from 'soapbox/schemas/index.ts'; import { defaultMediaVisibility } from 'soapbox/utils/status.ts'; import EventPreview from './event-preview.tsx'; @@ -17,7 +18,7 @@ import StatusContent from './status-content.tsx'; import StatusReplyMentions from './status-reply-mentions.tsx'; import SensitiveContentOverlay from './statuses/sensitive-content-overlay.tsx'; -import type { Status as StatusEntity } from 'soapbox/types/entities.ts'; +import type { Status as LegacyStatus } from 'soapbox/types/entities.ts'; const messages = defineMessages({ cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' }, @@ -25,7 +26,7 @@ const messages = defineMessages({ interface IQuotedStatus { /** The quoted status entity. */ - status?: StatusEntity; + status?: LegacyStatus; /** Callback when cancelled (during compose). */ onCancel?: Function; /** Whether the status is shown in the post composer. */ @@ -138,7 +139,7 @@ const QuotedStatus: React.FC = ({ status, onCancel, compose }) => {status.media_attachments.size > 0 && ( = ({ /> )} - + = ({ }) => { const dispatch = useAppDispatch(); - const size = status.media_attachments.size; - const firstAttachment = status.media_attachments.first(); + const size = status.media_attachments.length; + const firstAttachment = status.media_attachments[0]; let media: JSX.Element | null = null; @@ -52,7 +50,7 @@ const StatusMedia: React.FC = ({ return
; }; - const openMedia = (media: ImmutableList, index: number) => { + const openMedia = (media: readonly Attachment[], index: number) => { dispatch(openModal('MEDIA', { media, status, index })); }; @@ -72,10 +70,10 @@ const StatusMedia: React.FC = ({ diff --git a/src/components/status.tsx b/src/components/status.tsx index dc36ae974..ccde634b8 100644 --- a/src/components/status.tsx +++ b/src/components/status.tsx @@ -20,6 +20,7 @@ import QuotedStatus from 'soapbox/features/status/containers/quoted-status-conta import { HotKeys } from 'soapbox/features/ui/components/hotkeys.tsx'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useSettings } from 'soapbox/hooks/useSettings.ts'; +import { Status as StatusEntity } from 'soapbox/schemas/index.ts'; import { emojifyText } from 'soapbox/utils/emojify.tsx'; import { defaultMediaVisibility, textForScreenReader, getActualStatus } from 'soapbox/utils/status.ts'; @@ -32,7 +33,7 @@ import SensitiveContentOverlay from './statuses/sensitive-content-overlay.tsx'; import StatusInfo from './statuses/status-info.tsx'; import Tombstone from './tombstone.tsx'; -import type { Status as StatusEntity } from 'soapbox/types/entities.ts'; +import type { Status as LegacyStatus } from 'soapbox/types/entities.ts'; // Defined in components/scrollable-list export type ScrollPosition = { height: number; top: number }; @@ -44,7 +45,7 @@ const messages = defineMessages({ export interface IStatus { id?: string; avatarSize?: number; - status: StatusEntity; + status: LegacyStatus; onClick?: () => void; muted?: boolean; hidden?: boolean; @@ -150,7 +151,7 @@ const Status: React.FC = (props) => { if (firstAttachment.type === 'video') { dispatch(openModal('VIDEO', { status, media: firstAttachment, time: 0 })); } else { - dispatch(openModal('MEDIA', { status, media: status.media_attachments, index: 0 })); + dispatch(openModal('MEDIA', { status: status.toJS(), media: status.media_attachments.toJS(), index: 0 })); } } }; @@ -465,7 +466,7 @@ const Status: React.FC = (props) => { {(quote || actualStatus.card || actualStatus.media_attachments.size > 0) && ( = ({ }; const handleOpenModal = () => { - dispatch(openModal('MEDIA', { media: ImmutableList.of(media), index: 0 })); + dispatch(openModal('MEDIA', { media: ImmutableList.of(media).toJS(), index: 0 })); }; const active = hovered || focused; diff --git a/src/features/account-gallery/index.tsx b/src/features/account-gallery/index.tsx index 9d163bc46..ddc7fe962 100644 --- a/src/features/account-gallery/index.tsx +++ b/src/features/account-gallery/index.tsx @@ -74,7 +74,7 @@ const AccountGallery = () => { const media = (attachment.status as Status).media_attachments; const index = media.findIndex((x) => x.id === attachment.id); - dispatch(openModal('MEDIA', { media, index, status: attachment.status })); + dispatch(openModal('MEDIA', { media: media.toJS(), index, status: attachment?.status?.toJS() ?? attachment.status })); } }; diff --git a/src/features/account/components/header.tsx b/src/features/account/components/header.tsx index 76f8919a4..952a02146 100644 --- a/src/features/account/components/header.tsx +++ b/src/features/account/components/header.tsx @@ -266,7 +266,7 @@ const Header: React.FC = ({ account }) => { type: 'image', url: account.avatar, }); - dispatch(openModal('MEDIA', { media: ImmutableList.of(avatar), index: 0 })); + dispatch(openModal('MEDIA', { media: ImmutableList.of(avatar).toJS(), index: 0 })); }; const handleAvatarClick: React.MouseEventHandler = (e) => { @@ -281,7 +281,7 @@ const Header: React.FC = ({ account }) => { type: 'image', url: account.header, }); - dispatch(openModal('MEDIA', { media: ImmutableList.of(header), index: 0 })); + dispatch(openModal('MEDIA', { media: ImmutableList.of(header).toJS(), index: 0 })); }; const handleHeaderClick: React.MouseEventHandler = (e) => { diff --git a/src/features/admin/components/report-status.tsx b/src/features/admin/components/report-status.tsx index 33079ce37..8ff0d91e3 100644 --- a/src/features/admin/components/report-status.tsx +++ b/src/features/admin/components/report-status.tsx @@ -10,8 +10,9 @@ import StatusMedia from 'soapbox/components/status-media.tsx'; import HStack from 'soapbox/components/ui/hstack.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; +import { Status as StatusEntity } from 'soapbox/schemas/index.ts'; -import type { AdminReport, Status } from 'soapbox/types/entities.ts'; +import type { AdminReport, Status as LegacyStatus } from 'soapbox/types/entities.ts'; const messages = defineMessages({ viewStatus: { id: 'admin.reports.actions.view_status', defaultMessage: 'View post' }, @@ -19,7 +20,7 @@ const messages = defineMessages({ }); interface IReportStatus { - status: Status; + status: LegacyStatus; report?: AdminReport; } @@ -52,7 +53,7 @@ const ReportStatus: React.FC = ({ status }) => { - +
diff --git a/src/features/chats/components/chat-message.tsx b/src/features/chats/components/chat-message.tsx index 22204041d..5d5c26190 100644 --- a/src/features/chats/components/chat-message.tsx +++ b/src/features/chats/components/chat-message.tsx @@ -23,6 +23,7 @@ import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; import { useFeatures } from 'soapbox/hooks/useFeatures.ts'; import { ChatKeys, IChat, useChatActions } from 'soapbox/queries/chats.ts'; import { queryClient } from 'soapbox/queries/client.ts'; +import { Attachment } from 'soapbox/schemas/index.ts'; import { htmlToPlaintext } from 'soapbox/utils/html.ts'; import { isOnlyEmoji as _isOnlyEmoji } from 'soapbox/utils/only-emoji.ts'; @@ -112,7 +113,7 @@ const ChatMessage = (props: IChatMessage) => { 'rounded-br-sm': isMyMessage && content, 'rounded-bl-sm': !isMyMessage && content, })} - media={chatMessage.media_attachments} + media={chatMessage.media_attachments.toJS() as unknown as Attachment[]} onOpenMedia={onOpenMedia} visible /> diff --git a/src/features/chats/components/chat-upload.tsx b/src/features/chats/components/chat-upload.tsx index 4caf51998..b701966cd 100644 --- a/src/features/chats/components/chat-upload.tsx +++ b/src/features/chats/components/chat-upload.tsx @@ -22,7 +22,7 @@ const ChatUpload: React.FC = ({ attachment, onDelete }) => { const clickable = attachment.type !== 'unknown'; const handleOpenModal = () => { - dispatch(openModal('MEDIA', { media: ImmutableList.of(attachment), index: 0 })); + dispatch(openModal('MEDIA', { media: ImmutableList.of(attachment).toJS(), index: 0 })); }; return ( diff --git a/src/features/compose/components/reply-indicator.tsx b/src/features/compose/components/reply-indicator.tsx index 5570ec7cc..5c7bea75d 100644 --- a/src/features/compose/components/reply-indicator.tsx +++ b/src/features/compose/components/reply-indicator.tsx @@ -5,13 +5,12 @@ import AttachmentThumbs from 'soapbox/components/attachment-thumbs.tsx'; import Markup from 'soapbox/components/markup.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; import AccountContainer from 'soapbox/containers/account-container.tsx'; +import { Status as StatusEntity } from 'soapbox/schemas/index.ts'; import { getTextDirection } from 'soapbox/utils/rtl.ts'; -import type { Status } from 'soapbox/types/entities.ts'; - interface IReplyIndicator { className?: string; - status?: Status; + status?: StatusEntity; onCancel?: () => void; hideActions: boolean; } @@ -50,12 +49,12 @@ const ReplyIndicator: React.FC = ({ className, status, hideActi className='break-words' size='sm' direction={getTextDirection(status.search_index)} - emojis={status?.emojis?.toJS() ?? status.emojis} // Use toJS() if status.emojis is immutable; otherwise, fallback to plain status.emojis - mentions={status.mentions.toJS()} + emojis={status.emojis} + mentions={status.mentions} html={{ __html: status.content }} /> - {status.media_attachments.size > 0 && ( + {status.media_attachments.length > 0 && ( { const getStatus = makeGetStatus(); @@ -16,7 +18,7 @@ const makeMapStateToProps = () => { const editing = !!state.compose.get(composeId)?.id; return { - status: getStatus(state, { id: statusId }) as Status, + status: (getStatus(state, { id: statusId }) as LegacyStatus)?.toJS() as StatusEntity, hideActions: editing, }; }; diff --git a/src/features/event/components/event-header.tsx b/src/features/event/components/event-header.tsx index 4c191a5c9..1a3d382d2 100644 --- a/src/features/event/components/event-header.tsx +++ b/src/features/event/components/event-header.tsx @@ -128,7 +128,7 @@ const EventHeader: React.FC = ({ status }) => { e.preventDefault(); e.stopPropagation(); - dispatch(openModal('MEDIA', { media: ImmutableList([event.banner]) })); + dispatch(openModal('MEDIA', { media: ImmutableList([event.banner]).toJS() })); }; const handleExportClick = () => { diff --git a/src/features/event/components/pure-event-action-button.tsx b/src/features/event/components/pure-event-action-button.tsx index d2a9a0e19..598f4536e 100644 --- a/src/features/event/components/pure-event-action-button.tsx +++ b/src/features/event/components/pure-event-action-button.tsx @@ -5,9 +5,9 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { joinEvent, leaveEvent } from 'soapbox/actions/events.ts'; import { openModal } from 'soapbox/actions/modals.ts'; import Button from 'soapbox/components/ui/button.tsx'; -import { Entities, EntityTypes } from 'soapbox/entity-store/entities.ts'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; +import { Status as StatusEntity } from 'soapbox/schemas/index.ts'; import type { ButtonThemes } from 'soapbox/components/ui/useButtonStyles.ts'; @@ -17,7 +17,7 @@ const messages = defineMessages({ }); interface IPureEventAction { - status: EntityTypes[Entities.STATUSES]; + status: StatusEntity; theme?: ButtonThemes; } diff --git a/src/features/event/components/pure-event-date.tsx b/src/features/event/components/pure-event-date.tsx index 59bcef9bb..6b5aed7a7 100644 --- a/src/features/event/components/pure-event-date.tsx +++ b/src/features/event/components/pure-event-date.tsx @@ -3,11 +3,10 @@ import { FormattedDate } from 'react-intl'; import Icon from 'soapbox/components/icon.tsx'; import HStack from 'soapbox/components/ui/hstack.tsx'; -import { Entities, EntityTypes } from 'soapbox/entity-store/entities.ts'; - +import { Status as StatusEntity } from 'soapbox/schemas/index.ts'; interface IPureEventDate { - status: EntityTypes[Entities.STATUSES]; + status: StatusEntity; } const PureEventDate: React.FC = ({ status }) => { diff --git a/src/features/event/event-information.tsx b/src/features/event/event-information.tsx index 0b01c27dd..461c275ca 100644 --- a/src/features/event/event-information.tsx +++ b/src/features/event/event-information.tsx @@ -20,10 +20,11 @@ import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; import { useSettings } from 'soapbox/hooks/useSettings.ts'; import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts'; +import { Status as StatusEntity } from 'soapbox/schemas/index.ts'; import { makeGetStatus } from 'soapbox/selectors/index.ts'; import { defaultMediaVisibility } from 'soapbox/utils/status.ts'; -import type { Status as StatusEntity } from 'soapbox/types/entities.ts'; +import type { Status as StatusLegacy } from 'soapbox/types/entities.ts'; type RouteParams = { statusId: string }; @@ -35,7 +36,7 @@ const EventInformation: React.FC = ({ params }) => { const dispatch = useAppDispatch(); const getStatus = useCallback(makeGetStatus(), []); - const status = useAppSelector(state => getStatus(state, { id: params.statusId })) as StatusEntity; + const status = useAppSelector(state => getStatus(state, { id: params.statusId })) as StatusLegacy; const { tileServer } = useSoapboxConfig(); const { displayMedia } = useSettings(); @@ -208,7 +209,7 @@ const EventInformation: React.FC = ({ params }) => { )} diff --git a/src/features/group/components/group-header.tsx b/src/features/group/components/group-header.tsx index 71fdc2dcc..714cc25e6 100644 --- a/src/features/group/components/group-header.tsx +++ b/src/features/group/components/group-header.tsx @@ -63,7 +63,7 @@ const GroupHeader: React.FC = ({ group }) => { type: 'image', url: group.avatar, }); - dispatch(openModal('MEDIA', { media: ImmutableList.of(avatar), index: 0 })); + dispatch(openModal('MEDIA', { media: ImmutableList.of(avatar).toJS(), index: 0 })); }; const handleAvatarClick: React.MouseEventHandler = (e) => { @@ -78,7 +78,7 @@ const GroupHeader: React.FC = ({ group }) => { type: 'image', url: group.header, }); - dispatch(openModal('MEDIA', { media: ImmutableList.of(header), index: 0 })); + dispatch(openModal('MEDIA', { media: ImmutableList.of(header).toJS(), index: 0 })); }; const handleHeaderClick: React.MouseEventHandler = (e) => { diff --git a/src/features/group/group-gallery.tsx b/src/features/group/group-gallery.tsx index 4adeaec0f..28b2d2e0f 100644 --- a/src/features/group/group-gallery.tsx +++ b/src/features/group/group-gallery.tsx @@ -43,7 +43,7 @@ const GroupGallery: React.FC = (props) => { const media = (attachment.status as Status).media_attachments; const index = media.findIndex((x) => x.id === attachment.id); - dispatch(openModal('MEDIA', { media, index, status: attachment.status })); + dispatch(openModal('MEDIA', { media: media.toJS(), index, status: attachment?.status?.toJS() ?? attachment.status })); } }; diff --git a/src/features/report/components/status-check-box.tsx b/src/features/report/components/status-check-box.tsx index 149a5b352..29c63ee3f 100644 --- a/src/features/report/components/status-check-box.tsx +++ b/src/features/report/components/status-check-box.tsx @@ -7,6 +7,7 @@ import Toggle from 'soapbox/components/ui/toggle.tsx'; import { MediaGallery, Video, Audio } from 'soapbox/features/ui/util/async-components.ts'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; +import { Attachment } from 'soapbox/schemas/index.ts'; interface IStatusCheckBox { id: string; @@ -62,7 +63,7 @@ const StatusCheckBox: React.FC = ({ id, disabled }) => { } else { media = ( {}} diff --git a/src/features/scheduled-statuses/components/scheduled-status.tsx b/src/features/scheduled-statuses/components/scheduled-status.tsx index ea45ef9aa..137f85ce8 100644 --- a/src/features/scheduled-statuses/components/scheduled-status.tsx +++ b/src/features/scheduled-statuses/components/scheduled-status.tsx @@ -8,6 +8,7 @@ import HStack from 'soapbox/components/ui/hstack.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; import PollPreview from 'soapbox/features/ui/components/poll-preview.tsx'; import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; +import { Attachment } from 'soapbox/schemas/index.ts'; import { buildStatus } from '../builder.tsx'; @@ -55,7 +56,7 @@ const ScheduledStatus: React.FC = ({ statusId, ...other }) => {status.media_attachments.size > 0 && ( )} diff --git a/src/features/status/components/detailed-status.tsx b/src/features/status/components/detailed-status.tsx index 39bf23c7f..e4d471a4e 100644 --- a/src/features/status/components/detailed-status.tsx +++ b/src/features/status/components/detailed-status.tsx @@ -17,17 +17,18 @@ import Icon from 'soapbox/components/ui/icon.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; import Text from 'soapbox/components/ui/text.tsx'; import QuotedStatus from 'soapbox/features/status/containers/quoted-status-container.tsx'; +import { Status as StatusEntity } from 'soapbox/schemas/index.ts'; import { getActualStatus } from 'soapbox/utils/status.ts'; import StatusInteractionBar from './status-interaction-bar.tsx'; -import type { Group, Status as StatusEntity } from 'soapbox/types/entities.ts'; +import type { Group, Status as LegacyStatus } from 'soapbox/types/entities.ts'; interface IDetailedStatus { - status: StatusEntity; + status: LegacyStatus; showMedia?: boolean; withMedia?: boolean; - onOpenCompareHistoryModal: (status: StatusEntity) => void; + onOpenCompareHistoryModal: (status: LegacyStatus) => void; onToggleMediaVisibility: () => void; } @@ -162,7 +163,7 @@ const DetailedStatus: React.FC = ({ {(withMedia && (quote || actualStatus.card || actualStatus.media_attachments.size > 0)) && ( diff --git a/src/features/status/components/thread.tsx b/src/features/status/components/thread.tsx index 3d307b1cd..bd0734098 100644 --- a/src/features/status/components/thread.tsx +++ b/src/features/status/components/thread.tsx @@ -179,7 +179,7 @@ const Thread = (props: IThread) => { if (media.size === 1 && firstAttachment.type === 'video') { dispatch(openModal('VIDEO', { media: firstAttachment, status: status })); } else { - dispatch(openModal('MEDIA', { media, index: 0, status: status })); + dispatch(openModal('MEDIA', { media: media.toJS(), index: 0, status: status.toJS() })); } } }; diff --git a/src/features/ui/components/group-media-panel.tsx b/src/features/ui/components/group-media-panel.tsx index 801bb403e..fc4147b2f 100644 --- a/src/features/ui/components/group-media-panel.tsx +++ b/src/features/ui/components/group-media-panel.tsx @@ -36,7 +36,7 @@ const GroupMediaPanel: React.FC = ({ group }) => { const media = attachment.getIn(['status', 'media_attachments']) as ImmutableList; const index = media.findIndex(x => x.id === attachment.id); - dispatch(openModal('MEDIA', { media, index, status: attachment.status, account: attachment.account })); + dispatch(openModal('MEDIA', { media: media.toJS(), index, status: attachment?.status?.toJS() ?? attachment.status, account: attachment.account })); // NOTE: why 'account' field is here? it doesn't exist in MediaModal component } }; diff --git a/src/features/ui/components/modals/actions-modal.tsx b/src/features/ui/components/modals/actions-modal.tsx index 6641298b6..4cbeca94a 100644 --- a/src/features/ui/components/modals/actions-modal.tsx +++ b/src/features/ui/components/modals/actions-modal.tsx @@ -5,11 +5,11 @@ import { spring } from 'react-motion'; import HStack from 'soapbox/components/ui/hstack.tsx'; import SvgIcon from 'soapbox/components/ui/svg-icon.tsx'; import ReplyIndicator from 'soapbox/features/compose/components/reply-indicator.tsx'; +import { Status as StatusEntity } from 'soapbox/schemas/index.ts'; import Motion from '../../util/optional-motion.tsx'; import type { Menu, MenuItem } from 'soapbox/components/dropdown-menu/index.ts'; -import type { Status as StatusEntity } from 'soapbox/types/entities.ts'; interface IActionsModal { status: StatusEntity; diff --git a/src/features/ui/components/modals/boost-modal.tsx b/src/features/ui/components/modals/boost-modal.tsx index 425e8580a..730586dcd 100644 --- a/src/features/ui/components/modals/boost-modal.tsx +++ b/src/features/ui/components/modals/boost-modal.tsx @@ -6,8 +6,9 @@ import Modal from 'soapbox/components/ui/modal.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; import Text from 'soapbox/components/ui/text.tsx'; import ReplyIndicator from 'soapbox/features/compose/components/reply-indicator.tsx'; +import { Status as StatusEntity } from 'soapbox/schemas/index.ts'; -import type { Status as StatusEntity } from 'soapbox/types/entities.ts'; +import type { Status as LegacyStatus } from 'soapbox/types/entities.ts'; const messages = defineMessages({ cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' }, @@ -15,8 +16,8 @@ const messages = defineMessages({ }); interface IBoostModal { - status: StatusEntity; - onReblog: (status: StatusEntity) => void; + status: LegacyStatus; + onReblog: (status: LegacyStatus) => void; onClose: () => void; } @@ -37,7 +38,7 @@ const BoostModal: React.FC = ({ status, onReblog, onClose }) => { confirmationText={intl.formatMessage(buttonText)} > - + {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} diff --git a/src/features/ui/components/modals/compare-history-modal.tsx b/src/features/ui/components/modals/compare-history-modal.tsx index 313de9291..133359516 100644 --- a/src/features/ui/components/modals/compare-history-modal.tsx +++ b/src/features/ui/components/modals/compare-history-modal.tsx @@ -12,6 +12,7 @@ import Stack from 'soapbox/components/ui/stack.tsx'; import Text from 'soapbox/components/ui/text.tsx'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; +import { Attachment } from 'soapbox/schemas/index.ts'; import { emojifyText } from 'soapbox/utils/emojify.tsx'; import type { StatusEdit as StatusEditEntity } from 'soapbox/types/entities.ts'; @@ -81,7 +82,7 @@ const CompareHistoryModal: React.FC = ({ onClose, statusId )} {version.media_attachments.size > 0 && ( - + )} diff --git a/src/features/ui/components/modals/media-modal.tsx b/src/features/ui/components/modals/media-modal.tsx index 2faa1de1d..62573f1b3 100644 --- a/src/features/ui/components/modals/media-modal.tsx +++ b/src/features/ui/components/modals/media-modal.tsx @@ -24,15 +24,14 @@ import PlaceholderStatus from 'soapbox/features/placeholder/components/placehold import Thread from 'soapbox/features/status/components/thread.tsx'; import Video from 'soapbox/features/video/index.tsx'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; -import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; import { userTouching } from 'soapbox/is-mobile.ts'; -import { makeGetStatus } from 'soapbox/selectors/index.ts'; +import { normalizeStatus } from 'soapbox/normalizers/index.ts'; +import { Status as StatusEntity, Attachment } from 'soapbox/schemas/index.ts'; +import { Status as LegacyStatus } from 'soapbox/types/entities.ts'; +import { getActualStatus } from 'soapbox/utils/status.ts'; import ImageLoader from '../image-loader.tsx'; -import type { List as ImmutableList } from 'immutable'; -import type { Attachment, Status } from 'soapbox/types/entities.ts'; - const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, expand: { id: 'lightbox.expand', defaultMessage: 'Expand' }, @@ -55,8 +54,8 @@ const containerStyle: React.CSSProperties = { }; interface IMediaModal { - media: ImmutableList; - status?: Status; + media: readonly Attachment[]; + status?: StatusEntity; index: number; time?: number; onClose(): void; @@ -74,8 +73,7 @@ const MediaModal: React.FC = (props) => { const history = useHistory(); const intl = useIntl(); - const getStatus = useCallback(makeGetStatus(), []); - const actualStatus = useAppSelector((state) => getStatus(state, { id: status?.id as string })); + const actualStatus = status ? getActualStatus(status) : undefined; const [isLoaded, setIsLoaded] = useState(!!status); const [next, setNext] = useState(); @@ -83,11 +81,11 @@ const MediaModal: React.FC = (props) => { const [navigationHidden, setNavigationHidden] = useState(false); const [isFullScreen, setIsFullScreen] = useState(!status); - const hasMultipleImages = media.size > 1; + const hasMultipleImages = media.length > 1; - const handleSwipe = (index: number) => setIndex(index % media.size); - const handleNextClick = () => setIndex((getIndex() + 1) % media.size); - const handlePrevClick = () => setIndex((media.size + getIndex() - 1) % media.size); + const handleSwipe = (index: number) => setIndex(index % media.length); + const handleNextClick = () => setIndex((getIndex() + 1) % media.length); + const handlePrevClick = () => setIndex((media.length + getIndex() - 1) % media.length); const navigationHiddenClassName = navigationHidden ? 'pointer-events-none opacity-0' : ''; @@ -107,7 +105,7 @@ const MediaModal: React.FC = (props) => { }; const handleDownload = () => { - const mediaItem = hasMultipleImages ? media.get(index as number) : media.get(0); + const mediaItem = hasMultipleImages ? media[index as number] : media[0]; window.open(mediaItem?.url); }; @@ -126,8 +124,8 @@ const MediaModal: React.FC = (props) => { }; const content = media.map((attachment, i) => { - const width = (attachment.meta.getIn(['original', 'width']) || undefined) as number | undefined; - const height = (attachment.meta.getIn(['original', 'height']) || undefined) as number | undefined; + const width = 'meta' in attachment && 'original' in attachment.meta ? (attachment)?.meta?.original?.width : undefined; + const height = 'meta' in attachment && 'original' in attachment.meta ? (attachment)?.meta?.original?.height : undefined; const link = (status && ( @@ -151,7 +149,7 @@ const MediaModal: React.FC = (props) => { return (