diff --git a/src/components/filtered-status.tsx b/src/components/filtered-status.tsx index a3311d89..19cda191 100644 --- a/src/components/filtered-status.tsx +++ b/src/components/filtered-status.tsx @@ -17,9 +17,7 @@ import Modal from './modal'; import NameText from './name-text'; import RelativeTime from './relative-time'; import { readMoreText } from './status-helpers'; -import type { AnyAccount, AnyStatus } from './status-types'; - -type NameTextAccountShim = Parameters[0]['account']; +import type { AnyStatus } from './status-types'; interface FilteredStatusProps { status: AnyStatus; @@ -50,18 +48,12 @@ export default function FilteredStatus({ const { t, i18n } = useLingui(); const _ = i18n._.bind(i18n); const snapStates = useSnapshot(states); - const { id: statusID, account, createdAt, visibility, reblog } = - status as AnyStatus & { - account?: Partial; - reblog?: AnyStatus | null; - }; + const { id: statusID, account, createdAt, visibility, reblog } = status; const { avatar, avatarStatic, bot, group } = account || {}; const isReblog = !!reblog; const filterTitleStr = filterInfo?.titlesStr || ''; const createdAtDate = new Date(createdAt); - const statusPeekText = statusPeek( - (reblog || status) as unknown as Parameters[0], - ); + const statusPeekText = statusPeek(reblog || status); const [showPeek, setShowPeek] = useState(false); const bindLongPressPeek = useLongPress( @@ -141,17 +133,15 @@ export default function FilteredStatus({ {isReblog ? ( {' '} {' '} @@ -160,17 +150,15 @@ export default function FilteredStatus({ ) : isFollowedTags ? ( <> {' '} {' '} @@ -189,17 +177,15 @@ export default function FilteredStatus({ ) : ( <> {' '} {' '} @@ -212,8 +198,7 @@ export default function FilteredStatus({ <> ).avatarStatic || - (reblog.account as Partial).avatar + reblog.account.avatarStatic || reblog.account.avatar } squircle={bot} />{' '} diff --git a/src/components/status-content.tsx b/src/components/status-content.tsx index 8d30de8e..057e6a66 100644 --- a/src/components/status-content.tsx +++ b/src/components/status-content.tsx @@ -36,16 +36,16 @@ import StatusPostBody from './status-post-body'; import useStatusQuotePolicy from './status-quote-policy'; import useStatusReplyParent from './status-reply-parent'; import type { - AnyAccount, + AnyMediaAttachment, AnyStatus, FullMasto, + StatusAtprotoMeta, } from './status-types'; import type { StatusComponentProps, StatusRouterProps } from './status-view'; import StatusCompact from './status-compact'; -const EMPTY_MEDIA_ATTACHMENTS = Object.freeze( - [], -) as unknown as mastodon.v1.MediaAttachment[]; +const EMPTY_MEDIA_ATTACHMENTS: AnyMediaAttachment[] = []; +Object.freeze(EMPTY_MEDIA_ATTACHMENTS); interface StatusContentProps extends StatusRouterProps { renderStatus: (props: StatusComponentProps) => ComponentChildren; @@ -90,7 +90,6 @@ export default function StatusContent({ const snapStates = useSnapshot(states); const sKey = resolvedSKey; - const statusAny = status as unknown as AnyStatus; const { account, id, @@ -129,21 +128,7 @@ export default function StatusContent({ // _filtered, // Non-Mastodon emojiReactions, - } = statusAny as AnyStatus & { - account?: Partial; - quoteApproval?: { - currentUser?: string; - automatic?: readonly string[]; - manual?: readonly string[]; - }; - emojiReactions?: readonly Record[]; - _deleted?: boolean; - _pinned?: boolean; - _atproto?: { - replyParentAccount?: AnyAccount | null; - replyParentUnavailable?: boolean; - }; - }; + } = status; const { acct, avatar, @@ -189,14 +174,9 @@ export default function StatusContent({ ); const createdAtDate = new Date(createdAt); - const editedAtDate = new Date(editedAt as string); + const editedAtDate = new Date(editedAt); - const atproto = statusAny._atproto as - | { - replyParentAccount?: AnyAccount | null; - replyParentUnavailable?: boolean; - } - | undefined; + const atproto: StatusAtprotoMeta | undefined = status._atproto; const { inReplyToAccount, mentionSelf, showReplyBadge } = useStatusReplyParent({ instance, @@ -335,7 +315,7 @@ export default function StatusContent({ favourited, favouritesCount, bookmarked, - mediaAttachments, + mediaAttachments: mediaAttachments as unknown as mastodon.v1.MediaAttachment[], createdAt, }); @@ -449,7 +429,7 @@ export default function StatusContent({ showMultipleMediaCaptions, captionChildren, } = useStatusMediaCaptions({ - mediaAttachments, + mediaAttachments: mediaAttachments as unknown as mastodon.v1.MediaAttachment[], isSizeLarge, language, }); @@ -645,7 +625,7 @@ export default function StatusContent({ visibility={visibility} editedAt={editedAt} createdAtDate={createdAtDate} - inReplyToAccount={inReplyToAccount as unknown as AnyAccount | null} + inReplyToAccount={inReplyToAccount as unknown as AnyStatus['account'] | null} showReplyBadge={showReplyBadge} /> - renderStatus(statusProps as StatusComponentProps) + renderStatus(statusProps) } /> diff --git a/src/components/status-header.tsx b/src/components/status-header.tsx index fef57719..7ee25d40 100644 --- a/src/components/status-header.tsx +++ b/src/components/status-header.tsx @@ -13,8 +13,6 @@ import type { ContextMenuPropsShape } from './status-context-menu'; import type { AnyAccount, AnyStatus } from './status-types'; import ThreadBadge from './thread-badge'; -type NameTextAccountShim = Parameters[0]['account']; - interface StatusHeaderProps { size: string; status: AnyStatus; @@ -134,7 +132,7 @@ export default function StatusHeader({
{' '} {inReplyToAccount ? ( diff --git a/src/components/status-helpers.ts b/src/components/status-helpers.ts index 89bbc008..e595c721 100644 --- a/src/components/status-helpers.ts +++ b/src/components/status-helpers.ts @@ -114,7 +114,7 @@ export function getPostText( } : undefined, }), - getPollText(poll as AnyPoll | null | undefined), + getPollText(poll), ] .join('\n\n') .trim(); diff --git a/src/components/status-media-embeds.tsx b/src/components/status-media-embeds.tsx index 454c0a6e..0ec25930 100644 --- a/src/components/status-media-embeds.tsx +++ b/src/components/status-media-embeds.tsx @@ -1,5 +1,4 @@ import { Trans, useLingui } from '@lingui/react/macro'; -import type { mastodon } from 'masto'; import type { ComponentChildren, RefObject } from 'preact'; import states from '../utils/states'; @@ -7,7 +6,7 @@ import states from '../utils/states'; import Icon from './icon'; import Media from './media'; import MultipleMediaFigure from './multiple-media-figure'; -import type { AnyStatus } from './status-types'; +import type { AnyMediaAttachment, AnyStatus } from './status-types'; type FilterInfoMaybe = { action: 'hide' | 'blur' | 'warn'; @@ -21,7 +20,7 @@ interface StatusMediaEmbedsProps { readingExpandMedia?: string; readingExpandSpoilers: boolean; spoilerText?: string | null; - mediaAttachments: mastodon.v1.MediaAttachment[]; + mediaAttachments: AnyMediaAttachment[]; showSpoilerMedia: boolean; isSizeLarge: boolean; withinContext?: boolean; @@ -32,14 +31,14 @@ interface StatusMediaEmbedsProps { onMediaClick?: ( e: MouseEvent, index: number, - media: mastodon.v1.MediaAttachment, + media: AnyMediaAttachment, status: AnyStatus, ) => void; status: AnyStatus; showMultipleMediaCaptions: boolean; captionChildren: ComponentChildren; mediaContainerRef: RefObject; - displayedMediaAttachments: mastodon.v1.MediaAttachment[]; + displayedMediaAttachments: AnyMediaAttachment[]; content?: string | null; } @@ -110,10 +109,10 @@ export default function StatusMediaEmbeds({ (isSizeLarge || (withinContext && size === 'm')) ? (
{mediaAttachments.map( - (media: mastodon.v1.MediaAttachment, i: number) => ( + (media: AnyMediaAttachment, i: number) => (
[0]['media']} + media={media} autoAnimate showCaption allowLongerCaption={!content || isSizeLarge} @@ -146,10 +145,10 @@ export default function StatusMediaEmbeds({ } ${mediaAttachments.length > 4 ? 'media-gt4' : ''}`} > {displayedMediaAttachments.map( - (media: mastodon.v1.MediaAttachment, i: number) => ( + (media: AnyMediaAttachment, i: number) => ( [0]['media']} + media={media} autoAnimate={isSizeLarge} showCaption={mediaAttachments.length === 1} allowLongerCaption={!content && mediaAttachments.length === 1} diff --git a/src/components/status-modals.tsx b/src/components/status-modals.tsx index 487456fa..e1584d0a 100644 --- a/src/components/status-modals.tsx +++ b/src/components/status-modals.tsx @@ -10,6 +10,8 @@ import QuotesModal from './quotes-modal'; import EditedAtModal from './status-edit-history-modal'; import type { AnyStatus, RenderStatus } from './status-types'; +type QuoteSettingsPost = Parameters[0]['post']; + interface StatusModalsProps { showEdited: string | false; setShowEdited: (value: string | false) => void; @@ -83,9 +85,7 @@ export default function StatusModals({ }} > [0]['post'] - } + post={status} instance={instance} onClose={() => { setShowEmbed(false); @@ -105,11 +105,7 @@ export default function StatusModals({ setShowQuoteSettings(false); states.reloadStatusPage++; }} - post={ - status as unknown as Parameters< - typeof QuoteSettingsSheet - >[0]['post'] - } + post={status as unknown as QuoteSettingsPost} currentPolicy={postQuoteApprovalPolicy} renderStatus={renderStatus} /> diff --git a/src/components/status-post-body.tsx b/src/components/status-post-body.tsx index 09e2ea32..92e01a6a 100644 --- a/src/components/status-post-body.tsx +++ b/src/components/status-post-body.tsx @@ -14,7 +14,13 @@ import QuoteStatuses, { type FallbackQuote } from './status-quotes'; import StatusCard from './status-card'; import StatusMediaEmbeds from './status-media-embeds'; import { getPostText, isTranslateble, readMoreText } from './status-helpers'; -import type { AnyStatus, FullMasto } from './status-types'; +import type { + AnyMediaAttachment, + AnyPoll, + AnyPreviewCard, + AnyStatus, + FullMasto, +} from './status-types'; import StatusTags from './status-tags'; import TranslationBlock from './translation-block'; import type { StatusComponentProps } from './status-view'; @@ -41,7 +47,7 @@ interface StatusPostBodyProps { spoilerContentRef: RefObject; emojis?: mastodon.v1.CustomEmoji[]; id: string; - mediaAttachments: mastodon.v1.MediaAttachment[]; + mediaAttachments: AnyMediaAttachment[]; instance: string; content?: string | null; contentRef: RefObject; @@ -49,7 +55,7 @@ interface StatusPostBodyProps { previewMode?: boolean; reloadPostContentCount: number; reloadPostContent: () => void; - poll?: mastodon.v1.Poll | null; + poll?: AnyPoll | null; readOnly?: boolean; sameInstance: boolean; authenticated?: boolean; @@ -61,20 +67,20 @@ interface StatusPostBodyProps { forceTranslate?: boolean; withinContext?: boolean; languageAutoDetected?: boolean; - displayedMediaAttachments: mastodon.v1.MediaAttachment[]; + displayedMediaAttachments: AnyMediaAttachment[]; showMultipleMediaCaptions: boolean; captionChildren: ComponentChildren; mediaContainerRef: RefObject; onMediaClick?: ( e: MouseEvent, index: number, - media: mastodon.v1.MediaAttachment, + media: AnyMediaAttachment, status: AnyStatus, ) => void; quoted?: boolean | number; - quote?: unknown; + quote?: FallbackQuote | null; renderStatus: (props: StatusComponentProps) => ComponentChildren; - card?: mastodon.v1.PreviewCard | null; + card?: AnyPreviewCard | null; statusQuoteState?: unknown; currentInstance: string; accountURL?: string | null; @@ -191,11 +197,7 @@ export default function StatusPostBody({ )} [0]['mediaAttachments'] - } + mediaAttachments={mediaAttachments} language={language ?? undefined} postID={id} instance={instance} @@ -203,7 +205,7 @@ export default function StatusPostBody({ {!!content && (
[0]['post']} + post={status} instance={instance} previewMode={previewMode} /> @@ -247,7 +249,7 @@ export default function StatusPostBody({ > [0]['post']} + post={status} instance={instance} previewMode={previewMode} /> @@ -262,38 +264,32 @@ export default function StatusPostBody({ )} {!!poll && ( { - (states.statuses[sKey] as Record).poll = - newPoll; - }, - refresh: () => { - return masto.v1.polls - .$select(poll.id) - .fetch() - .then((pollResponse) => { - (states.statuses[sKey] as Record).poll = - pollResponse; - return undefined; - }) - .catch((_e: unknown) => {}); - }, - votePoll: (choices: number[]) => { - return masto.v1.polls - .$select(poll.id) - .votes.create({ - choices, - }) - .then((pollResponse) => { - (states.statuses[sKey] as Record).poll = - pollResponse; - return undefined; - }); - }, - } as unknown as Parameters[0])} + lang={language ?? undefined} + poll={poll} + readOnly={readOnly || !sameInstance || !authenticated} + refresh={() => { + return masto.v1.polls + .$select(poll.id) + .fetch() + .then((pollResponse) => { + (states.statuses[sKey] as Record).poll = + pollResponse; + return undefined; + }) + .catch((_e: unknown) => {}); + }} + votePoll={(choices: number[]) => { + return masto.v1.polls + .$select(poll.id) + .votes.create({ + choices, + }) + .then((pollResponse) => { + (states.statuses[sKey] as Record).poll = + pollResponse; + return undefined; + }); + }} /> )} {(((!!content && @@ -341,7 +337,7 @@ export default function StatusPostBody({ instance={instance} level={typeof quoted === 'number' ? quoted : undefined} collapsed={!isSizeLarge && !withinContext} - fallbackQuote={quote as FallbackQuote | null | undefined} + fallbackQuote={quote} renderStatus={(quoteStatusProps) => renderStatus({ ...quoteStatusProps, @@ -351,18 +347,18 @@ export default function StatusPostBody({ }) } /> - {!!card && - /^https/i.test(card?.url) && + {!!card?.url && + /^https/i.test(card.url) && !sensitive && !spoilerText && !poll && !mediaAttachments.length && !statusQuoteState && ( [0]['card']} + card={card} selfReferential={card?.url === status.url || card?.url === status.uri} selfAuthor={card?.authors?.some( - (a: mastodon.v1.PreviewCardAuthor) => a.account?.url === accountURL, + (a) => a.account?.url === accountURL, )} instance={currentInstance} /> diff --git a/src/components/status-reblog.tsx b/src/components/status-reblog.tsx index 762b9c96..7f0665c2 100644 --- a/src/components/status-reblog.tsx +++ b/src/components/status-reblog.tsx @@ -59,11 +59,7 @@ export default function StatusReblog({
{' '} [0]['account'] - } + account={wrapperStatus.account} instance={instance} showAvatar /> @@ -91,11 +87,7 @@ export default function StatusReblog({ {' '} [0]['account'] - } + account={wrapperStatus.account} instance={instance} showAvatar />{' '} diff --git a/src/components/status-types.ts b/src/components/status-types.ts index f603c608..68f58a22 100644 --- a/src/components/status-types.ts +++ b/src/components/status-types.ts @@ -3,17 +3,130 @@ import type { ComponentChildren } from 'preact'; import type { api } from '../utils/api'; -// Loose status type: some non-API extension fields (e.g. `_atproto`, `_deleted`, -// `_pinned`, `emojiReactions`, `quoteApproval`) are added at runtime. We keep -// the mastodon shape as a base and treat the runtime additions as untyped. -export type AnyStatus = mastodon.v1.Status & Record; - export type AnyAccount = mastodon.v1.Account & Record; export type AnyPoll = mastodon.v1.Poll & { emojis?: mastodon.v1.CustomEmoji[]; } & Record; +export type AnyPreviewCard = Omit< + mastodon.v1.PreviewCard, + | 'authorName' + | 'authorUrl' + | 'authors' + | 'blurhash' + | 'description' + | 'embedUrl' + | 'html' + | 'image' + | 'imageDescription' + | 'language' + | 'providerName' + | 'providerUrl' + | 'publishedAt' + | 'title' + | 'type' + | 'url' + | 'width' + | 'height' +> & { + authors?: Array< + { + account?: { id?: string } & Record; + } & Record + >; + authorName?: string; + authorUrl?: string; + blurhash?: string; + description?: string; + embedUrl?: string; + html?: string; + image?: string; + imageDescription?: string; + language?: string; + providerName?: string; + providerUrl?: string; + publishedAt?: string; + title?: string; + type?: string; + url?: string; + width?: number; + height?: number; +} & Record; + +export type AnyMediaAttachment = Omit< + mastodon.v1.MediaAttachment, + | 'blurhash' + | 'description' + | 'meta' + | 'previewRemoteUrl' + | 'previewUrl' + | 'remoteUrl' + | 'type' + | 'url' +> & { + blurhash?: string; + description?: string; + meta?: { + original?: { width?: number; height?: number; duration?: number }; + small?: { width?: number; height?: number }; + focus?: { x: number; y: number }; + }; + previewRemoteUrl?: string; + previewUrl: string; + remoteUrl?: string; + type: mastodon.v1.MediaAttachment['type']; + url: string; +} & Record; + +interface AnyQuote { + quotedStatus?: AnyStatus; + state?: string; +} + +export interface StatusAtprotoMeta { + replyParentAccount?: AnyAccount | null; + replyParentUnavailable?: boolean; +} + +export interface StatusQuoteApproval { + currentUser?: string; + automatic?: readonly string[]; + manual?: readonly string[]; +} + +// Loose status type: some non-API extension fields (e.g. `_atproto`, `_deleted`, +// `_pinned`, `emojiReactions`, `quoteApproval`) are added at runtime. Keep the +// Mastodon base shape but override status-rendering fields that the app mutates. +export type AnyStatus = Omit< + mastodon.v1.Status, + | 'account' + | 'card' + | 'editedAt' + | 'language' + | 'mediaAttachments' + | 'poll' + | 'quote' + | 'reblog' + | 'url' +> & { + account: AnyAccount; + card?: AnyPreviewCard | null; + editedAt: string; + language?: string; + mediaAttachments: AnyMediaAttachment[]; + poll?: AnyPoll; + quote?: AnyQuote | null; + reblog?: AnyStatus | null; + url?: string; + __replies?: AnyStatus[]; + _atproto?: StatusAtprotoMeta; + _deleted?: boolean; + _pinned?: boolean; + emojiReactions?: readonly Record[]; + quoteApproval?: StatusQuoteApproval; +} & Record; + export type StatusSize = 's' | 'm' | 'l'; export interface StatusRenderProps extends Record { diff --git a/src/components/status-view.tsx b/src/components/status-view.tsx index ecd5a8a0..83262eb8 100644 --- a/src/components/status-view.tsx +++ b/src/components/status-view.tsx @@ -1,7 +1,6 @@ import './status.css'; import { shallowEqual } from 'fast-equals'; -import type { mastodon } from 'masto'; import { memo } from 'preact/compat'; import { useCallback, useContext } from 'preact/hooks'; import { useSnapshot } from 'valtio'; @@ -18,15 +17,14 @@ import { StatusGhost, StatusSkeleton } from './status-placeholders'; import StatusContent from './status-content'; import StatusReblog from './status-reblog'; import type { - AnyAccount, + AnyMediaAttachment, AnyStatus, GhostInfo, StatusSize, } from './status-types'; -const EMPTY_MEDIA_ATTACHMENTS = Object.freeze( - [], -) as unknown as mastodon.v1.MediaAttachment[]; +const EMPTY_MEDIA_ATTACHMENTS: AnyStatus['mediaAttachments'] = []; +Object.freeze(EMPTY_MEDIA_ATTACHMENTS); export interface StatusComponentProps { statusID?: string | null; @@ -45,7 +43,7 @@ export interface StatusComponentProps { onMediaClick?: ( e: MouseEvent, i: number, - media: mastodon.v1.MediaAttachment, + media: AnyMediaAttachment, status: AnyStatus, ) => void; quoted?: number | boolean; @@ -153,24 +151,21 @@ function StatusRouter({ if (eStatus) { status = { ...status, - ...(eStatus as object), - } as unknown as AnyStatus; + ...eStatus, + }; } } else { // Revert back to original status // Don't need to do anything, re-render will use the original status above } - const statusAny = status as unknown as AnyStatus; const { account, id, filtered, mediaAttachments: statusMediaAttachments, reblog, - } = statusAny as AnyStatus & { - account?: Partial; - }; + } = status; const accountId = account?.id; const group = account?.group; const mediaAttachments = statusMediaAttachments || EMPTY_MEDIA_ATTACHMENTS; @@ -244,7 +239,7 @@ function StatusRouter({ return ( ? 'large' : 'small'; +function rawStatusFromState(status: unknown): RawStatus | undefined { + if (!status || typeof status !== 'object') return undefined; + return status as RawStatus; +} + +type SaveStatusInput = Parameters[0]; +type ThreadifyStatusInput = Parameters[0]; + +function saveRawStatus( + status: RawStatus, + instance?: string | Parameters[1], + opts?: Parameters[2], +): void { + saveStatus(status as SaveStatusInput, instance, opts); +} + +function threadifyRawStatus(status: RawStatus, instance?: string | null): void { + threadifyStatus(status as ThreadifyStatusInput, instance); +} + interface StatusPageParams { id: string; instance?: string; @@ -196,11 +229,12 @@ function StatusPage(params: StatusPageParams) { // string here. Fall back to `id` defensively for the type system. const sKey: string = statusKey(id, instance) ?? id; const [heroStatus, setHeroStatus] = useState( - states.statuses[sKey] as unknown as RawStatus | undefined, + rawStatusFromState(states.statuses[sKey]), ); useEffect(() => { - if (states.statuses[sKey]) { - setHeroStatus(states.statuses[sKey] as unknown as RawStatus); + const cachedStatus = rawStatusFromState(states.statuses[sKey]); + if (cachedStatus) { + setHeroStatus(cachedStatus); } }, [sKey]); @@ -273,10 +307,7 @@ function StatusPage(params: StatusPageParams) { try { const status = await statusesEndpoint.$select(snapshotId).fetch(); if (stale) return; - saveStatus( - status as unknown as Parameters[0], - snapshotInstance, - ); + saveRawStatus(status, snapshotInstance); setHeroStatus(status); } catch (err) { if (stale) return; @@ -295,11 +326,7 @@ function StatusPage(params: StatusPageParams) { const mediaStatusKey = statusKey(mediaStatusID, instance); const mediaAttachments = mediaStatusID ? mediaStatusKey - ? ( - snapStates.statuses[mediaStatusKey] as unknown as - | RawStatus - | undefined - )?.mediaAttachments + ? rawStatusFromState(snapStates.statuses[mediaStatusKey])?.mediaAttachments : undefined : heroStatus?.mediaAttachments; @@ -426,14 +453,10 @@ function StatusPage(params: StatusPageParams) { {showMedia ? ( mediaAttachments?.length ? ( [0]['mediaAttachments'] - } + mediaAttachments={mediaAttachments} statusID={mediaStatusID || id} instance={instance} - lang={heroStatus?.language as string | undefined} + lang={heroStatus?.language ?? undefined} index={mediaIndex - 1} onClose={handleMediaClose} /> @@ -579,7 +602,8 @@ function StatusThread({ const restructureContext = (): RestructureResult | undefined => { console.log({ fullContext: fullContext.current }); if (!fullContext.current) return undefined; - let { ancestors, descendants, heroStatus } = fullContext.current; + let ancestors: StatusThreadItem[] = fullContext.current.ancestors; + let { descendants, heroStatus } = fullContext.current; if (editHistoryMode && descendants?.length) { // Filter descendants based on createdAt/editedAt dates @@ -611,14 +635,10 @@ function StatusThread({ // Ghost posts - detect missing ancestors const missingAncestorIds = new Set(); - ancestors.forEach((status) => { - saveStatus( - status as unknown as Parameters[0], - instance, - { - skipThreading: true, - }, - ); + fullContext.current.ancestors.forEach((status) => { + saveRawStatus(status, instance, { + skipThreading: true, + }); if ( status.inReplyToId && !ancestors.find((s) => s.id === status.inReplyToId) @@ -636,15 +656,17 @@ function StatusThread({ // Insert ghost statuses missingAncestorIds.forEach((missingId) => { const referencingStatus: RawStatus | null = - ancestors.find((s) => s.inReplyToId === missingId) || + ancestors.find( + (s): s is RawStatus => !isGhostStatus(s) && s.inReplyToId === missingId, + ) || (heroStatus.inReplyToId === missingId ? heroStatus : null); if (referencingStatus) { - const ghostStatus = { + const ghostStatus: GhostStatus = { id: missingId, ghost: { inReplyToAccountId: referencingStatus.inReplyToAccountId, }, - } as unknown as RawStatus & { ghost?: GhostMeta }; + }; if (referencingStatus === heroStatus) { ancestors.push(ghostStatus); } else { @@ -656,19 +678,13 @@ function StatusThread({ const missingStatuses = new Set(); const ancestorsIsThread = ancestors.every( - (s) => - (s as RawStatus & { ghost?: GhostMeta }).ghost || - s.account?.id === heroStatus.account?.id, + (s) => isGhostStatus(s) || s.account?.id === heroStatus.account?.id, ); const nestedDescendants: RawStatus[] = []; descendants.forEach((status) => { - saveStatus( - status as unknown as Parameters[0], - instance, - { - // skipThreading: true, - }, - ); + saveRawStatus(status, instance, { + // skipThreading: true, + }); if ( status.inReplyToId && @@ -760,7 +776,9 @@ function StatusThread({ ); const allStatuses: DisplayStatus[] = [ ...ancestors.map((s) => { - const ghost = (s as RawStatus & { ghost?: GhostMeta }).ghost; + const isGhost = isGhostStatus(s); + const ghost = isGhost ? s.ghost : undefined; + const repliesCount = isGhost ? undefined : s.repliesCount; return { id: s.id, ancestor: true, @@ -768,8 +786,8 @@ function StatusThread({ isThread: ancestorsIsThread && !ghost, accountID: s.account?.id, account: s.account, - repliesCount: s.repliesCount, - weight: ghost ? 0 : calcStatusWeight(s), + repliesCount, + weight: isGhost ? 0 : calcStatusWeight(s), createdAt: s.createdAt, }; }), @@ -833,18 +851,13 @@ function StatusThread({ ); const hasStatus = !!snapStates.statuses[sKey]; - let heroStatus = snapStates.statuses[sKey] as unknown as - | RawStatus - | undefined; + let heroStatus = rawStatusFromState(snapStates.statuses[sKey]); if (hasStatus && !reloadHero) { console.debug('Hero status is cached'); } else { try { heroStatus = await heroFetch(); - saveStatus( - heroStatus as unknown as Parameters[0], - instance, - ); + saveRawStatus(heroStatus, instance); // Give time for context to appear await new Promise((resolve) => { setTimeout(resolve, 100); @@ -900,10 +913,7 @@ function StatusThread({ // Let's threadify this one // Note that all non-hero statuses will trigger saveStatus which will threadify them too // By right, at this point, all descendant statuses should be cached - threadifyStatus( - heroStatus as unknown as Parameters[0], - instance, - ); + threadifyRawStatus(heroStatus, instance); } catch (e) { console.error(e); setUIState('error'); @@ -1028,8 +1038,9 @@ function StatusThread({ // module-level caches. Empty deps array is intentional. }, []); - const heroStatus = (snapStates.statuses[sKey] || - snapStates.statuses[id]) as unknown as RawStatus | undefined; + const heroStatus = + rawStatusFromState(snapStates.statuses[sKey]) || + rawStatusFromState(snapStates.statuses[id]); const heroDisplayName = useMemo(() => { // Remove shortcodes from display name if (!heroStatus) return ''; @@ -1040,9 +1051,7 @@ function StatusThread({ }, [heroStatus]); const heroContentText = useMemo(() => { if (!heroStatus) return ''; - let text = statusPeek( - heroStatus as unknown as Parameters[0], - ); + let text = statusPeek(heroStatus); if (text.length > 64) { // "The title should ideally be less than 64 characters in length" // https://www.w3.org/Provider/Style/TITLE.html @@ -1608,7 +1617,7 @@ function StatusThread({ status.replies.forEach(getIDs); } } - statuses.forEach((s) => getIDs(s as unknown as StatusKeyish)); + statuses.forEach(getIDs); return ids.map((sId) => statusKey(sId, instance)); }, [statuses, instance]); @@ -1760,11 +1769,7 @@ function StatusThread({ <> [0]['account'] - } + account={heroStatus.account} instance={instance} showAvatar short @@ -2115,24 +2120,13 @@ function SubComments({ const sameCount = replies.length === totalComments; // Get the first 3 accounts, unique by id - const accountsRaw = replies + const accounts = replies .map((r) => r.account) .filter( (a, i, arr) => - arr.findIndex( - (b) => - (b as { id?: string } | undefined)?.id === - (a as { id?: string } | undefined)?.id, - ) === i, + arr.findIndex((b) => b?.id === a?.id) === i, ) .slice(0, 3); - const accounts = accountsRaw as unknown as Array<{ - id?: string; - avatarStatic?: string; - displayName?: string; - username?: string; - bot?: boolean; - }>; const totalWeight = useMemo(() => { return (replies ?? []).reduce((acc, reply) => { @@ -2148,7 +2142,7 @@ function SubComments({ } else if (totalWeight <= MAX_WEIGHT) { open = true; } else if (!hasParentThread && totalComments === 1) { - const shortReply = calcStatusWeight(replies[0] as unknown as RawStatus) < 2; + const shortReply = calcStatusWeight(replies[0]) < 2; if (shortReply) open = true; } const openBefore = cachedRepliesToggle[replies[0].id]; @@ -2356,34 +2350,32 @@ const statusWeightCache = new Map(); // `"undefined" + content` (9 extra characters); preserve that arithmetic // here so cached/computed weights match the prior behavior exactly. interface CalcStatusWeightInput { - id?: string; + id: string; spoilerText?: unknown; content?: unknown; - mediaAttachments?: unknown; - poll?: unknown; + mediaAttachments?: { length?: number } | null; + poll?: { options?: { length?: number } } | null; card?: unknown; } function calcStatusWeight(status: CalcStatusWeightInput | RawStatus): number { - const s = status as CalcStatusWeightInput; - const cachedWeight = statusWeightCache.get(s.id as string); + const cachedWeight = statusWeightCache.get(status.id); if (cachedWeight) return cachedWeight; - const { spoilerText, content, mediaAttachments, poll, card } = s; + const { spoilerText, content, mediaAttachments, poll, card } = status; // Preserve original JS string-concat semantics: `undefined + content` // yields `"undefined" + content`. Cast via `String()` to keep that // coercion under TypeScript's checker. const length = htmlContentLength(String(spoilerText) + String(content)); - const ma = mediaAttachments as { length?: number } | null | undefined; - const mediaLength = ma?.length ? MEDIA_VIRTUAL_LENGTH : 0; - const pollOptions = ( - poll as { options?: { length?: number } } | null | undefined - )?.options; + const mediaLength = mediaAttachments?.length ? MEDIA_VIRTUAL_LENGTH : 0; + const pollOptions = poll?.options; const pollLength = (pollOptions?.length || 0) * POLL_VIRTUAL_LENGTH; const cardLength = - card && (ma?.length || pollOptions?.length) ? 0 : CARD_VIRTUAL_LENGTH; + card && (mediaAttachments?.length || pollOptions?.length) + ? 0 + : CARD_VIRTUAL_LENGTH; const totalLength = length + mediaLength + pollLength + cardLength; const weight = totalLength / WEIGHT_SEGMENT; - statusWeightCache.set(s.id as string, weight); + statusWeightCache.set(status.id, weight); return weight; }