diff --git a/app/soapbox/actions/compose.ts b/app/soapbox/actions/compose.ts index c5cd29428..dd68b5bb0 100644 --- a/app/soapbox/actions/compose.ts +++ b/app/soapbox/actions/compose.ts @@ -22,9 +22,9 @@ import { createStatus } from './statuses'; import type { AutoSuggestion } from 'soapbox/components/autosuggest-input'; import type { Emoji } from 'soapbox/features/emoji'; -import type { Account, Group } from 'soapbox/schemas'; +import type { Account, Group, Status } from 'soapbox/schemas'; import type { AppDispatch, RootState } from 'soapbox/store'; -import type { APIEntity, Status, Tag } from 'soapbox/types/entities'; +import type { APIEntity, Tag } from 'soapbox/types/entities'; import type { History } from 'soapbox/types/history'; const { CancelToken, isCancel } = axios; diff --git a/app/soapbox/actions/interactions.ts b/app/soapbox/actions/interactions.ts index 77bccb41f..288e405ac 100644 --- a/app/soapbox/actions/interactions.ts +++ b/app/soapbox/actions/interactions.ts @@ -10,8 +10,9 @@ import { importFetchedAccounts, importFetchedStatus } from './importer'; import { expandGroupFeaturedTimeline } from './timelines'; import type { AxiosError } from 'axios'; +import type { Status as StatusEntity } from 'soapbox/schemas'; import type { AppDispatch, RootState } from 'soapbox/store'; -import type { APIEntity, Group, Status as StatusEntity } from 'soapbox/types/entities'; +import type { APIEntity, Group } from 'soapbox/types/entities'; const REBLOG_REQUEST = 'REBLOG_REQUEST'; const REBLOG_SUCCESS = 'REBLOG_SUCCESS'; @@ -85,7 +86,7 @@ const reblog = (status: StatusEntity) => dispatch(reblogRequest(status)); - api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`).then(function(response) { + api(getState).post(`/api/v1/statuses/${status.id}/reblog`).then(function(response) { // The reblog API method returns a new status wrapped around the original. In this case we are only // interested in how the original is modified, hence passing it skipping the wrapper dispatch(importFetchedStatus(response.data.reblog)); @@ -101,7 +102,7 @@ const unreblog = (status: StatusEntity) => dispatch(unreblogRequest(status)); - api(getState).post(`/api/v1/statuses/${status.get('id')}/unreblog`).then(() => { + api(getState).post(`/api/v1/statuses/${status.id}/unreblog`).then(() => { dispatch(unreblogSuccess(status)); }).catch(error => { dispatch(unreblogFail(status, error)); @@ -234,7 +235,7 @@ const dislike = (status: StatusEntity) => dispatch(dislikeRequest(status)); - api(getState).post(`/api/friendica/statuses/${status.get('id')}/dislike`).then(function() { + api(getState).post(`/api/friendica/statuses/${status.id}/dislike`).then(function() { dispatch(dislikeSuccess(status)); }).catch(function(error) { dispatch(dislikeFail(status, error)); @@ -247,7 +248,7 @@ const undislike = (status: StatusEntity) => dispatch(undislikeRequest(status)); - api(getState).post(`/api/friendica/statuses/${status.get('id')}/undislike`).then(() => { + api(getState).post(`/api/friendica/statuses/${status.id}/undislike`).then(() => { dispatch(undislikeSuccess(status)); }).catch(error => { dispatch(undislikeFail(status, error)); @@ -305,7 +306,7 @@ const bookmark = (status: StatusEntity) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(bookmarkRequest(status)); - api(getState).post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function(response) { + api(getState).post(`/api/v1/statuses/${status.id}/bookmark`).then(function(response) { dispatch(importFetchedStatus(response.data)); dispatch(bookmarkSuccess(status, response.data)); toast.success(messages.bookmarkAdded, { @@ -321,7 +322,7 @@ const unbookmark = (status: StatusEntity) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(unbookmarkRequest(status)); - api(getState).post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => { + api(getState).post(`/api/v1/statuses/${status.id}/unbookmark`).then(response => { dispatch(importFetchedStatus(response.data)); dispatch(unbookmarkSuccess(status, response.data)); toast.success(messages.bookmarkRemoved); @@ -504,7 +505,7 @@ const pin = (status: StatusEntity) => dispatch(pinRequest(status)); - api(getState).post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => { + api(getState).post(`/api/v1/statuses/${status.id}/pin`).then(response => { dispatch(importFetchedStatus(response.data)); dispatch(pinSuccess(status)); }).catch(error => { @@ -515,14 +516,14 @@ const pin = (status: StatusEntity) => const pinToGroup = (status: StatusEntity, group: Group) => (dispatch: AppDispatch, getState: () => RootState) => { return api(getState) - .post(`/api/v1/groups/${group.id}/statuses/${status.get('id')}/pin`) + .post(`/api/v1/groups/${group.id}/statuses/${status.id}/pin`) .then(() => dispatch(expandGroupFeaturedTimeline(group.id))); }; const unpinFromGroup = (status: StatusEntity, group: Group) => (dispatch: AppDispatch, getState: () => RootState) => { return api(getState) - .post(`/api/v1/groups/${group.id}/statuses/${status.get('id')}/unpin`) + .post(`/api/v1/groups/${group.id}/statuses/${status.id}/unpin`) .then(() => dispatch(expandGroupFeaturedTimeline(group.id))); }; @@ -551,7 +552,7 @@ const unpin = (status: StatusEntity) => dispatch(unpinRequest(status)); - api(getState).post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => { + api(getState).post(`/api/v1/statuses/${status.id}/unpin`).then(response => { dispatch(importFetchedStatus(response.data)); dispatch(unpinSuccess(status)); }).catch(error => { diff --git a/app/soapbox/actions/reports.ts b/app/soapbox/actions/reports.ts index be6a60ed8..e2c0ed3a3 100644 --- a/app/soapbox/actions/reports.ts +++ b/app/soapbox/actions/reports.ts @@ -3,9 +3,8 @@ import api from '../api'; import { openModal } from './modals'; import type { AxiosError } from 'axios'; -import type { Account } from 'soapbox/schemas'; +import type { Account, ChatMessage, Group, Status } from 'soapbox/schemas'; import type { AppDispatch, RootState } from 'soapbox/store'; -import type { ChatMessage, Group, Status } from 'soapbox/types/entities'; const REPORT_INIT = 'REPORT_INIT'; const REPORT_CANCEL = 'REPORT_CANCEL'; diff --git a/app/soapbox/actions/statuses.ts b/app/soapbox/actions/statuses.ts index ab6a855ee..86bef5637 100644 --- a/app/soapbox/actions/statuses.ts +++ b/app/soapbox/actions/statuses.ts @@ -10,8 +10,9 @@ import { importFetchedStatus, importFetchedStatuses } from './importer'; import { openModal } from './modals'; import { deleteFromTimelines } from './timelines'; +import type { Status } from 'soapbox/schemas'; import type { AppDispatch, RootState } from 'soapbox/store'; -import type { APIEntity, Status } from 'soapbox/types/entities'; +import type { APIEntity } from 'soapbox/types/entities'; const STATUS_CREATE_REQUEST = 'STATUS_CREATE_REQUEST'; const STATUS_CREATE_SUCCESS = 'STATUS_CREATE_SUCCESS'; diff --git a/app/soapbox/api/hooks/index.ts b/app/soapbox/api/hooks/index.ts index cdfe0c16f..a2b40da78 100644 --- a/app/soapbox/api/hooks/index.ts +++ b/app/soapbox/api/hooks/index.ts @@ -12,6 +12,9 @@ export { useFollow } from './accounts/useFollow'; export { useRelationships } from './accounts/useRelationships'; export { usePatronUser } from './accounts/usePatronUser'; +// Statuses +export { useStatus } from './statuses/useStatus'; + // Groups export { useBlockGroupMember } from './groups/useBlockGroupMember'; export { useCancelMembershipRequest } from './groups/useCancelMembershipRequest'; diff --git a/app/soapbox/api/hooks/statuses/useStatus.ts b/app/soapbox/api/hooks/statuses/useStatus.ts new file mode 100644 index 000000000..708ce4e33 --- /dev/null +++ b/app/soapbox/api/hooks/statuses/useStatus.ts @@ -0,0 +1,18 @@ +import { Entities } from 'soapbox/entity-store/entities'; +import { useEntity } from 'soapbox/entity-store/hooks'; +import { useApi } from 'soapbox/hooks/useApi'; +import { type Status, statusSchema } from 'soapbox/schemas'; + +function useStatus(statusId: string | undefined) { + const api = useApi(); + + const { entity: status, ...rest } = useEntity( + [Entities.STATUSES, statusId!], + () => api.get(`/api/v1/statuses/${statusId}`), + { schema: statusSchema, enabled: !!statusId }, + ); + + return { status, ...rest }; +} + +export { useStatus }; \ No newline at end of file diff --git a/app/soapbox/components/attachment-thumbs.tsx b/app/soapbox/components/attachment-thumbs.tsx index 25b4bec00..9f6d94510 100644 --- a/app/soapbox/components/attachment-thumbs.tsx +++ b/app/soapbox/components/attachment-thumbs.tsx @@ -6,10 +6,10 @@ import { MediaGallery } from 'soapbox/features/ui/util/async-components'; import { useAppDispatch } from 'soapbox/hooks'; import type { List as ImmutableList } from 'immutable'; -import type { Attachment } from 'soapbox/types/entities'; +import type { Attachment } from 'soapbox/schemas'; interface IAttachmentThumbs { - media: ImmutableList + media: Attachment[] onClick?(): void sensitive?: boolean } diff --git a/app/soapbox/components/dropdown-menu/dropdown-menu.tsx b/app/soapbox/components/dropdown-menu/dropdown-menu.tsx index 48fff7398..a3d72fe3a 100644 --- a/app/soapbox/components/dropdown-menu/dropdown-menu.tsx +++ b/app/soapbox/components/dropdown-menu/dropdown-menu.tsx @@ -13,7 +13,7 @@ import { IconButton, Portal } from '../ui'; import DropdownMenuItem, { MenuItem } from './dropdown-menu-item'; -import type { Status } from 'soapbox/types/entities'; +import type { Status } from 'soapbox/schemas'; export type Menu = Array; diff --git a/app/soapbox/components/event-preview.tsx b/app/soapbox/components/event-preview.tsx index 9cfd1da81..5927dcf00 100644 --- a/app/soapbox/components/event-preview.tsx +++ b/app/soapbox/components/event-preview.tsx @@ -10,7 +10,7 @@ import Icon from './icon'; import { Button, HStack, Stack, Text } from './ui'; import VerificationBadge from './verification-badge'; -import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities'; +import type { Status, Event } from 'soapbox/schemas'; const messages = defineMessages({ eventBanner: { id: 'event.banner', defaultMessage: 'Event banner' }, @@ -19,7 +19,7 @@ const messages = defineMessages({ }); interface IEventPreview { - status: StatusEntity + status: Status & { event: Event } className?: string hideAction?: boolean floatingAction?: boolean @@ -30,7 +30,7 @@ const EventPreview: React.FC = ({ status, className, hideAction, const me = useAppSelector((state) => state.me); - const account = status.account as AccountEntity; + const account = status.account; const event = status.event!; const banner = event.banner; @@ -74,13 +74,13 @@ const EventPreview: React.FC = ({ status, className, hideAction, - + {event.location && ( - {event.location.get('name')} + {event.location.name} )} diff --git a/app/soapbox/components/status-action-bar.tsx b/app/soapbox/components/status-action-bar.tsx index b071cc945..cbcb5d012 100644 --- a/app/soapbox/components/status-action-bar.tsx +++ b/app/soapbox/components/status-action-bar.tsx @@ -1,4 +1,4 @@ -import { List as ImmutableList } from 'immutable'; +import { fromJS, List as ImmutableList } from 'immutable'; import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { useHistory } from 'react-router-dom'; @@ -30,7 +30,7 @@ import { getReactForStatus, reduceEmoji } from 'soapbox/utils/emoji-reacts'; import GroupPopover from './groups/popover/group-popover'; import type { Menu } from 'soapbox/components/dropdown-menu'; -import type { Account, Group, Status } from 'soapbox/types/entities'; +import type { Status } from 'soapbox/schemas'; const messages = defineMessages({ adminAccount: { id: 'status.admin_account', defaultMessage: 'Moderate @{name}' }, @@ -120,7 +120,7 @@ const StatusActionBar: React.FC = ({ const features = useFeatures(); const settings = useSettings(); const soapboxConfig = useSoapboxConfig(); - const deleteGroupStatus = useDeleteGroupStatus(status?.group as Group, status.id); + const deleteGroupStatus = useDeleteGroupStatus(status?.group!, status.id); const { allowedEmoji } = soapboxConfig; @@ -237,7 +237,7 @@ const StatusActionBar: React.FC = ({ }; const handleGroupPinClick: React.EventHandler = () => { - const group = status.group as Group; + const group = status.group!; if (status.pinned) { dispatch(unpinFromGroup(status, group)); @@ -249,24 +249,24 @@ const StatusActionBar: React.FC = ({ }; const handleMentionClick: React.EventHandler = (e) => { - dispatch(mentionCompose(status.account as Account)); + dispatch(mentionCompose(status.account)); }; const handleDirectClick: React.EventHandler = (e) => { - dispatch(directCompose(status.account as Account)); + dispatch(directCompose(status.account)); }; const handleChatClick: React.EventHandler = (e) => { - const account = status.account as Account; + const account = status.account; dispatch(launchChat(account.id, history)); }; const handleMuteClick: React.EventHandler = (e) => { - dispatch(initMuteModal(status.account as Account)); + dispatch(initMuteModal(status.account)); }; const handleBlockClick: React.EventHandler = (e) => { - const account = status.get('account') as Account; + const account = status.account; dispatch(openModal('CONFIRM', { icon: require('@tabler/icons/ban.svg'), @@ -288,13 +288,13 @@ const StatusActionBar: React.FC = ({ const handleEmbed = () => { dispatch(openModal('EMBED', { - url: status.get('url'), + url: status.url, onError: (error: any) => toast.showAlertForError(error), })); }; const handleReport: React.EventHandler = (e) => { - dispatch(initReport(ReportableEntities.STATUS, status.account as Account, { status })); + dispatch(initReport(ReportableEntities.STATUS, status.account, { status })); }; const handleConversationMuteClick: React.EventHandler = (e) => { @@ -308,7 +308,7 @@ const StatusActionBar: React.FC = ({ }; const onModerate: React.MouseEventHandler = (e) => { - const account = status.account as Account; + const account = status.account; dispatch(openModal('ACCOUNT_MODERATION', { accountId: account.id })); }; @@ -321,7 +321,7 @@ const StatusActionBar: React.FC = ({ }; const handleDeleteFromGroup: React.EventHandler = () => { - const account = status.account as Account; + const account = status.account; dispatch(openModal('CONFIRM', { heading: intl.formatMessage(messages.deleteHeading), @@ -490,8 +490,8 @@ const StatusActionBar: React.FC = ({ } if (isGroupStatus && !!status.group) { - const group = status.group as Group; - const account = status.account as Account; + const group = status.group; + const account = status.account; const isGroupOwner = groupRelationship?.role === GroupRoles.OWNER; const isGroupAdmin = groupRelationship?.role === GroupRoles.ADMIN; const isStatusFromOwner = group.owner.id === account.id; @@ -551,7 +551,7 @@ const StatusActionBar: React.FC = ({ const favouriteCount = status.favourites_count; const emojiReactCount = reduceEmoji( - (status.pleroma.get('emoji_reactions') || ImmutableList()) as ImmutableList, + fromJS(status.pleroma?.emoji_reactions ?? []) as ImmutableList, favouriteCount, status.favourited, allowedEmoji, @@ -583,7 +583,7 @@ const StatusActionBar: React.FC = ({ reblogIcon = require('@tabler/icons/lock.svg'); } - if ((status.group as Group)?.membership_required && !groupRelationship?.member) { + if (status.group?.membership_required && !groupRelationship?.member) { replyDisabled = true; replyTitle = intl.formatMessage(messages.replies_disabled_group); } diff --git a/app/soapbox/components/status-content.tsx b/app/soapbox/components/status-content.tsx index 5fc530769..75a31fa31 100644 --- a/app/soapbox/components/status-content.tsx +++ b/app/soapbox/components/status-content.tsx @@ -12,7 +12,7 @@ import Markup from './markup'; import Poll from './polls/poll'; import type { Sizes } from 'soapbox/components/ui/text/text'; -import type { Status, Mention } from 'soapbox/types/entities'; +import type { Status, Mention } from 'soapbox/schemas'; const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top) const BIG_EMOJI_LIMIT = 10; @@ -130,7 +130,7 @@ const StatusContent: React.FC = ({ }); const parsedHtml = useMemo((): string => { - return translatable && status.translation ? status.translation.get('content')! : status.contentHtml; + return translatable && status.translation ? status.translation.content : status.contentHtml; }, [status.contentHtml, status.translation]); if (status.content.length === 0) { @@ -168,12 +168,11 @@ const StatusContent: React.FC = ({ output.push(); } - const hasPoll = status.poll && typeof status.poll === 'string'; - if (hasPoll) { - output.push(); + if (status.poll) { + output.push(); } - return
{output}
; + return
{output}
; } else { const output = [ /** Whether to display compact media. */ muted?: boolean /** Callback when compact media is clicked. */ @@ -41,8 +40,8 @@ const StatusMedia: React.FC = ({ const [mediaWrapperWidth, setMediaWrapperWidth] = useState(undefined); - const size = status.media_attachments.size; - const firstAttachment = status.media_attachments.first(); + const size = status.media_attachments.length; + const [firstAttachment] = status.media_attachments; let media: JSX.Element | null = null; @@ -64,7 +63,7 @@ const StatusMedia: React.FC = ({ return
; }; - const openMedia = (media: ImmutableList, index: number) => { + const openMedia = (media: Attachment[], index: number) => { dispatch(openModal('MEDIA', { media, status, index })); }; @@ -82,8 +81,8 @@ const StatusMedia: React.FC = ({ if (video.external_video_id && status.card) { const getHeight = (): number => { - const width = Number(video.meta.getIn(['original', 'width'])); - const height = Number(video.meta.getIn(['original', 'height'])); + const width = Number(video.meta.original?.width); + const height = Number(video.meta.original?.height); return Number(mediaWrapperWidth) / (width / height); }; @@ -110,7 +109,7 @@ const StatusMedia: React.FC = ({ blurhash={video.blurhash} src={video.url} alt={video.description} - aspectRatio={Number(video.meta.getIn(['original', 'aspect']))} + aspectRatio={video.meta.original?.aspect} height={285} visible={showMedia} inline @@ -128,11 +127,11 @@ const StatusMedia: React.FC = ({ )} diff --git a/app/soapbox/components/status-reply-mentions.tsx b/app/soapbox/components/status-reply-mentions.tsx index e03d0c7f7..d5cff3abd 100644 --- a/app/soapbox/components/status-reply-mentions.tsx +++ b/app/soapbox/components/status-reply-mentions.tsx @@ -8,21 +8,20 @@ import HoverStatusWrapper from 'soapbox/components/hover-status-wrapper'; import { useAppDispatch } from 'soapbox/hooks'; import { isPubkey } from 'soapbox/utils/nostr'; -import type { Account, Status } from 'soapbox/types/entities'; +import type { Status } from 'soapbox/schemas'; interface IStatusReplyMentions { - status: Status + status: Pick hoverable?: boolean } const StatusReplyMentions: React.FC = ({ status, hoverable = true }) => { const dispatch = useAppDispatch(); + const { account, mentions } = status; const handleOpenMentionsModal: React.MouseEventHandler = (e) => { e.stopPropagation(); - const account = status.account as Account; - dispatch(openModal('MENTIONS', { username: account.acct, statusId: status.id, @@ -33,11 +32,9 @@ const StatusReplyMentions: React.FC = ({ status, hoverable return null; } - const to = status.mentions; - // The post is a reply, but it has no mentions. // Rare, but it can happen. - if (to.size === 0) { + if (mentions.length === 0) { return (
= ({ status, hoverable } // The typical case with a reply-to and a list of mentions. - const accounts = to.slice(0, 2).map(account => { + const accounts = mentions.slice(0, 2).map((mention) => { const link = ( e.stopPropagation()} > - @{isPubkey(account.username) ? account.username.slice(0, 8) : account.username} + @{isPubkey(mention.username) ? mention.username.slice(0, 8) : mention.username} ); @@ -70,12 +67,12 @@ const StatusReplyMentions: React.FC = ({ status, hoverable } else { return link; } - }).toArray(); + }); - if (to.size > 2) { + if (mentions.length > 2) { accounts.push( - + , ); } diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx index 46a37b21e..c91133505 100644 --- a/app/soapbox/components/status.tsx +++ b/app/soapbox/components/status.tsx @@ -26,8 +26,9 @@ import { Card, Icon, Stack, Text } from './ui'; import type { Account as AccountEntity, + Event, Status as StatusEntity, -} from 'soapbox/types/entities'; +} from 'soapbox/schemas'; // Defined in components/scrollable-list export type ScrollPosition = { height: number, top: number }; @@ -92,7 +93,7 @@ const Status: React.FC = (props) => { const statusUrl = `/@${actualStatus.account.acct}/posts/${actualStatus.id}`; const group = actualStatus.group; - const filtered = (status.filtered.size || actualStatus.filtered.size) > 0; + const filtered = (status.filtered.length || actualStatus.filtered.length) > 0; // Track height changes we know about to compensate scrolling. useEffect(() => { @@ -134,7 +135,7 @@ const Status: React.FC = (props) => { const handleHotkeyOpenMedia = (e?: KeyboardEvent): void => { const status = actualStatus; - const firstAttachment = status.media_attachments.first(); + const [firstAttachment] = status.media_attachments; e?.preventDefault(); @@ -203,7 +204,7 @@ const Status: React.FC = (props) => { _expandEmojiSelector(); }; - const handleUnfilter = () => dispatch(unfilterStatus(status.filtered.size ? status.id : actualStatus.id)); + const handleUnfilter = () => dispatch(unfilterStatus(status.filtered.length ? status.id : actualStatus.id)); const _expandEmojiSelector = (): void => { const firstEmoji: HTMLDivElement | null | undefined = node.current?.querySelector('.emoji-react-selector .emoji-react-selector__emoji'); @@ -360,14 +361,14 @@ const Status: React.FC = (props) => { let quote; if (actualStatus.quote) { - if (actualStatus.pleroma.get('quote_visible', true) === false) { + if ((actualStatus.pleroma?.quote_visible ?? true) === false) { quote = (

); } else { - quote = ; + quote = ; } } @@ -453,7 +454,9 @@ const Status: React.FC = (props) => { /> )} - {actualStatus.event ? : ( + {actualStatus.event ? ( + + ) : ( = (props) => { - {(quote || actualStatus.card || actualStatus.media_attachments.size > 0) && ( + {(quote || actualStatus.card || actualStatus.media_attachments.length > 0) && ( onToggleVisibility?(): void visible?: boolean } @@ -42,7 +42,7 @@ const SensitiveContentOverlay = React.forwardRef(defaultMediaVisibility(status, displayMedia)); diff --git a/app/soapbox/components/translate-button.tsx b/app/soapbox/components/translate-button.tsx index 68358ff60..1c874f90f 100644 --- a/app/soapbox/components/translate-button.tsx +++ b/app/soapbox/components/translate-button.tsx @@ -8,10 +8,10 @@ import { isLocal } from 'soapbox/utils/accounts'; import { Stack, Button, Text } from './ui'; -import type { Account, Status } from 'soapbox/types/entities'; +import type { Status } from 'soapbox/schemas'; interface ITranslateButton { - status: Status + status: Pick } const TranslateButton: React.FC = ({ status }) => { @@ -28,7 +28,7 @@ const TranslateButton: React.FC = ({ status }) => { const sourceLanguages = instance.pleroma.getIn(['metadata', 'translation', 'source_languages']) as ImmutableList; const targetLanguages = instance.pleroma.getIn(['metadata', 'translation', 'target_languages']) as ImmutableList; - const renderTranslate = (me || allowUnauthenticated) && (allowRemote || isLocal(status.account as Account)) && ['public', 'unlisted'].includes(status.visibility) && status.contentHtml.length > 0 && status.language !== null && intl.locale !== status.language; + const renderTranslate = (me || allowUnauthenticated) && (allowRemote || isLocal(status.account)) && ['public', 'unlisted'].includes(status.visibility) && status.contentHtml.length > 0 && status.language !== null && intl.locale !== status.language; const supportsLanguages = (!sourceLanguages || sourceLanguages.includes(status.language!)) && (!targetLanguages || targetLanguages.includes(intl.locale)); @@ -47,7 +47,7 @@ const TranslateButton: React.FC = ({ status }) => { if (status.translation) { const languageNames = new Intl.DisplayNames([intl.locale], { type: 'language' }); const languageName = languageNames.of(status.language!); - const provider = status.translation.get('provider'); + const provider = status.translation.provider; return ( diff --git a/app/soapbox/containers/status-container.tsx b/app/soapbox/containers/status-container.tsx index 1efad6adc..494e7751c 100644 --- a/app/soapbox/containers/status-container.tsx +++ b/app/soapbox/containers/status-container.tsx @@ -1,18 +1,11 @@ -import React, { useCallback } from 'react'; +import React from 'react'; +import { useStatus } from 'soapbox/api/hooks'; import Status, { IStatus } from 'soapbox/components/status'; -import { useAppSelector } from 'soapbox/hooks'; -import { makeGetStatus } from 'soapbox/selectors'; interface IStatusContainer extends Omit { id: string contextType?: string - /** @deprecated Unused. */ - otherAccounts?: any - /** @deprecated Unused. */ - getScrollPosition?: any - /** @deprecated Unused. */ - updateScrollBottom?: any } /** @@ -20,10 +13,8 @@ interface IStatusContainer extends Omit { * @deprecated Use the Status component directly. */ const StatusContainer: React.FC = (props) => { - const { id, contextType, ...rest } = props; - - const getStatus = useCallback(makeGetStatus(), []); - const status = useAppSelector(state => getStatus(state, { id, contextType })); + const { id, ...rest } = props; + const { status } = useStatus(id); if (status) { return ; diff --git a/app/soapbox/features/event/components/event-action-button.tsx b/app/soapbox/features/event/components/event-action-button.tsx index 1c86e0181..da9d04195 100644 --- a/app/soapbox/features/event/components/event-action-button.tsx +++ b/app/soapbox/features/event/components/event-action-button.tsx @@ -7,7 +7,7 @@ import { Button } from 'soapbox/components/ui'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; import type { ButtonThemes } from 'soapbox/components/ui/button/useButtonStyles'; -import type { Status as StatusEntity } from 'soapbox/types/entities'; +import type { Status } from 'soapbox/schemas'; const messages = defineMessages({ leaveConfirm: { id: 'confirmations.leave_event.confirm', defaultMessage: 'Leave event' }, @@ -15,7 +15,7 @@ const messages = defineMessages({ }); interface IEventAction { - status: StatusEntity + status: Status theme?: ButtonThemes } diff --git a/app/soapbox/features/event/components/event-date.tsx b/app/soapbox/features/event/components/event-date.tsx index dcdd01516..4c4bfc0a9 100644 --- a/app/soapbox/features/event/components/event-date.tsx +++ b/app/soapbox/features/event/components/event-date.tsx @@ -4,15 +4,13 @@ import { FormattedDate } from 'react-intl'; import Icon from 'soapbox/components/icon'; import { HStack } from 'soapbox/components/ui'; -import type { Status as StatusEntity } from 'soapbox/types/entities'; +import type { Event } from 'soapbox/schemas'; interface IEventDate { - status: StatusEntity + event: Event } -const EventDate: React.FC = ({ status }) => { - const event = status.event!; - +const EventDate: React.FC = ({ event }) => { if (!event.start_time) return null; const startDate = new Date(event.start_time); diff --git a/app/soapbox/features/status/components/card.tsx b/app/soapbox/features/status/components/card.tsx index e7925cb5d..c26badc28 100644 --- a/app/soapbox/features/status/components/card.tsx +++ b/app/soapbox/features/status/components/card.tsx @@ -1,14 +1,12 @@ import clsx from 'clsx'; -import { List as ImmutableList } from 'immutable'; import React, { useState, useEffect } from 'react'; import Blurhash from 'soapbox/components/blurhash'; import Icon from 'soapbox/components/icon'; import { HStack, Stack, Text } from 'soapbox/components/ui'; -import { normalizeAttachment } from 'soapbox/normalizers'; import { addAutoPlay } from 'soapbox/utils/media'; -import type { Card as CardEntity, Attachment } from 'soapbox/types/entities'; +import type { Card as CardEntity, Attachment } from 'soapbox/schemas'; const trim = (text: string, len: number): string => { const cut = text.indexOf(' ', len); @@ -24,7 +22,7 @@ interface ICard { card: CardEntity maxTitle?: number maxDescription?: number - onOpenMedia: (attachments: ImmutableList, index: number) => void + onOpenMedia: (attachments: Attachment[], index: number) => void compact?: boolean defaultWidth?: number cacheWidth?: (width: number) => void @@ -52,19 +50,24 @@ const Card: React.FC = ({ const trimmedDescription = trim(card.description, maxDescription); const handlePhotoClick = () => { - const attachment = normalizeAttachment({ + const attachment: Attachment = { + id: '', type: 'image', url: card.embed_url, + preview_url: card.embed_url, + remote_url: null, description: trimmedTitle, + blurhash: null, meta: { original: { width: card.width, height: card.height, + aspect: card.width / card.height, }, }, - }); + }; - onOpenMedia(ImmutableList([attachment]), 0); + onOpenMedia([attachment], 0); }; const handleEmbedClick: React.MouseEventHandler = (e) => { diff --git a/app/soapbox/features/video/index.tsx b/app/soapbox/features/video/index.tsx index 9c533e10f..3e7062467 100644 --- a/app/soapbox/features/video/index.tsx +++ b/app/soapbox/features/video/index.tsx @@ -110,7 +110,7 @@ interface IVideo { inline?: boolean cacheWidth?: (width: number) => void visible?: boolean - blurhash?: string + blurhash?: string | null link?: React.ReactNode aspectRatio?: number displayMedia?: string diff --git a/app/soapbox/reducers/index.ts b/app/soapbox/reducers/index.ts index e2504a968..3b83c675d 100644 --- a/app/soapbox/reducers/index.ts +++ b/app/soapbox/reducers/index.ts @@ -61,7 +61,6 @@ import sidebar from './sidebar'; import soapbox from './soapbox'; import status_hover_card from './status-hover-card'; import status_lists from './status-lists'; -import statuses from './statuses'; import suggestions from './suggestions'; import tags from './tags'; import timelines from './timelines'; @@ -71,11 +70,13 @@ import user_lists from './user-lists'; import verification from './verification'; import type { AnyAction, Reducer } from 'redux'; -import type { EntityStore } from 'soapbox/entity-store/types'; -import type { Account } from 'soapbox/schemas'; +import type { Entity, EntityStore } from 'soapbox/entity-store/types'; +import type { Account, Status } from 'soapbox/schemas'; + +type LegacyReducer = EntityStore & LegacyStore const reducers = { - accounts: ((state: any = {}) => state) as (state: any) => EntityStore & LegacyStore, + accounts: ((state: any = {}) => state) as (state: any) => LegacyReducer, account_notes, accounts_meta, admin, @@ -130,7 +131,7 @@ const reducers = { soapbox, status_hover_card, status_lists, - statuses, + statuses: ((state: any = {}) => state) as (state: any) => LegacyReducer, suggestions, tags, timelines, @@ -182,12 +183,19 @@ const accountsSelector = createSelector( (accounts) => immutableizeStore>(accounts), ); +const statusesSelector = createSelector( + (state: InferState) => state.entities[Entities.STATUSES]?.store as EntityStore || {}, + (statuses) => immutableizeStore>(statuses), +); + const extendedRootReducer = ( state: InferState, action: AnyAction, ): ReturnType => { const extendedState = rootReducer(state, action); - return extendedState.set('accounts', accountsSelector(extendedState)); + return extendedState + .set('accounts', accountsSelector(extendedState)) + .set('statuses', statusesSelector(extendedState)); }; export default extendedRootReducer as Reducer>; diff --git a/app/soapbox/schemas/index.ts b/app/soapbox/schemas/index.ts index 2c99ef8b8..fe3b48a59 100644 --- a/app/soapbox/schemas/index.ts +++ b/app/soapbox/schemas/index.ts @@ -4,6 +4,7 @@ export { cardSchema, type Card } from './card'; export { chatMessageSchema, type ChatMessage } from './chat-message'; export { customEmojiSchema, type CustomEmoji } from './custom-emoji'; export { emojiReactionSchema, type EmojiReaction } from './emoji-reaction'; +export { eventSchema, type Event } from './event'; export { groupSchema, type Group } from './group'; export { groupMemberSchema, type GroupMember } from './group-member'; export { groupRelationshipSchema, type GroupRelationship } from './group-relationship'; diff --git a/app/soapbox/types/entities.ts b/app/soapbox/types/entities.ts index 4b7823e9e..afcc09067 100644 --- a/app/soapbox/types/entities.ts +++ b/app/soapbox/types/entities.ts @@ -18,7 +18,6 @@ import { MentionRecord, NotificationRecord, StatusEditRecord, - StatusRecord, TagRecord, } from 'soapbox/normalizers'; import { LogEntryRecord } from 'soapbox/reducers/admin-log'; @@ -51,12 +50,6 @@ type Tag = ReturnType; type Account = SchemaAccount & LegacyMap; -interface Status extends ReturnType { - // HACK: same as above - quote: EmbeddedEntity - reblog: EmbeddedEntity -} - // Utility types type APIEntity = Record; type EmbeddedEntity = null | string | ReturnType>; @@ -82,7 +75,6 @@ export { Location, Mention, Notification, - Status, StatusEdit, Tag, @@ -100,4 +92,5 @@ export type { Poll, PollOption, Relationship, + Status, } from 'soapbox/schemas'; \ No newline at end of file