mirror of
https://github.com/mastodon/mastodon.git
synced 2026-08-20 02:54:23 -05:00
Add Status component stories (#39475)
This commit is contained in:
@@ -7,7 +7,7 @@ export type MediaAttachmentType =
|
||||
| 'unknown'
|
||||
| 'audio';
|
||||
|
||||
interface BaseApiMediaAttachmentJSON {
|
||||
export interface BaseApiMediaAttachmentJSON {
|
||||
id: string;
|
||||
type: MediaAttachmentType;
|
||||
url: string;
|
||||
@@ -19,7 +19,7 @@ interface BaseApiMediaAttachmentJSON {
|
||||
blurhash: string;
|
||||
}
|
||||
|
||||
interface ApiImageAttachmentJSON extends BaseApiMediaAttachmentJSON {
|
||||
export interface ApiImageAttachmentJSON extends BaseApiMediaAttachmentJSON {
|
||||
type: 'image';
|
||||
meta: {
|
||||
original: ApiImageAttachmentMetaJSON;
|
||||
@@ -27,7 +27,7 @@ interface ApiImageAttachmentJSON extends BaseApiMediaAttachmentJSON {
|
||||
};
|
||||
}
|
||||
|
||||
interface ApiAudioAttachmentJSON extends BaseApiMediaAttachmentJSON {
|
||||
export interface ApiAudioAttachmentJSON extends BaseApiMediaAttachmentJSON {
|
||||
type: 'audio';
|
||||
meta: {
|
||||
colors: ApiColorsAttachmentMetaJSON;
|
||||
@@ -36,7 +36,7 @@ interface ApiAudioAttachmentJSON extends BaseApiMediaAttachmentJSON {
|
||||
};
|
||||
}
|
||||
|
||||
interface ApiVideoAttachmentJSON extends BaseApiMediaAttachmentJSON {
|
||||
export interface ApiVideoAttachmentJSON extends BaseApiMediaAttachmentJSON {
|
||||
type: 'video';
|
||||
meta: {
|
||||
colors: ApiColorsAttachmentMetaJSON;
|
||||
@@ -49,7 +49,7 @@ interface ApiVideoAttachmentJSON extends BaseApiMediaAttachmentJSON {
|
||||
};
|
||||
}
|
||||
|
||||
interface ApiGifvAttachmentJSON extends BaseApiMediaAttachmentJSON {
|
||||
export interface ApiGifvAttachmentJSON extends BaseApiMediaAttachmentJSON {
|
||||
type: 'gifv';
|
||||
meta: {
|
||||
original: ApiVideoAttachmentMetaJSON;
|
||||
@@ -57,7 +57,7 @@ interface ApiGifvAttachmentJSON extends BaseApiMediaAttachmentJSON {
|
||||
};
|
||||
}
|
||||
|
||||
interface ApiUnknownAttachmentJSON extends BaseApiMediaAttachmentJSON {
|
||||
export interface ApiUnknownAttachmentJSON extends BaseApiMediaAttachmentJSON {
|
||||
type: 'unknown';
|
||||
meta: unknown;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ interface ApiNestedQuoteJSON {
|
||||
interface ApiQuoteAcceptedJSON {
|
||||
state: 'accepted';
|
||||
quoted_status: Omit<ApiStatusJSON, 'quote'> & {
|
||||
quote: ApiNestedQuoteJSON | ApiQuoteEmptyJSON;
|
||||
quote?: ApiNestedQuoteJSON | ApiQuoteEmptyJSON;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
540
app/javascript/mastodon/components/status/status.stories.tsx
Normal file
540
app/javascript/mastodon/components/status/status.stories.tsx
Normal file
@@ -0,0 +1,540 @@
|
||||
import type { FC } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { fn } from 'storybook/test';
|
||||
|
||||
import type { ApiMediaAttachmentJSON } from '@/mastodon/api_types/media_attachments';
|
||||
import type { StatusVisibility } from '@/mastodon/api_types/statuses';
|
||||
import {
|
||||
accountFactoryState,
|
||||
mediaAttachmentFactory,
|
||||
pollFactory,
|
||||
statusFactory,
|
||||
statusFactoryState,
|
||||
} from '@/testing/factories';
|
||||
|
||||
import { TypedStatus } from './types';
|
||||
|
||||
type ContextTypes =
|
||||
| 'account'
|
||||
| 'bookmarks'
|
||||
| 'detailed'
|
||||
| 'favourites'
|
||||
| 'home'
|
||||
| 'notifications'
|
||||
| 'public'
|
||||
| 'search'
|
||||
| 'thread';
|
||||
|
||||
type AttachmentTypes =
|
||||
| 'image-1'
|
||||
| 'image-2'
|
||||
| 'image-3'
|
||||
| 'video'
|
||||
| 'audio'
|
||||
| 'gifv'
|
||||
| 'unknown';
|
||||
|
||||
interface StatusStoryProps {
|
||||
// Contents
|
||||
text: string;
|
||||
visibility: StatusVisibility;
|
||||
isReblog?: boolean;
|
||||
isReply?: boolean;
|
||||
isPoll?: boolean;
|
||||
isQuote?: boolean;
|
||||
attachments?: AttachmentTypes;
|
||||
contentWarning?: string;
|
||||
|
||||
// Interactions
|
||||
hasFavourited?: boolean;
|
||||
hasReblogged?: boolean;
|
||||
hasBookmarked?: boolean;
|
||||
hasReplied?: boolean;
|
||||
hasFilter?: boolean;
|
||||
hasVoted?: boolean;
|
||||
disableActions?: boolean;
|
||||
showTranslate?: boolean;
|
||||
|
||||
// Display
|
||||
showThread?: boolean;
|
||||
contextType?: ContextTypes;
|
||||
showCounters?: boolean;
|
||||
favouriteCount?: number;
|
||||
reblogCount?: number;
|
||||
replyCount?: number;
|
||||
hidden?: boolean;
|
||||
muted?: boolean;
|
||||
showPrepend?: boolean;
|
||||
}
|
||||
|
||||
const otherAccount = accountFactoryState({
|
||||
id: '2',
|
||||
display_name: 'Another user',
|
||||
});
|
||||
|
||||
const StatusStoryComponent: FC<StatusStoryProps> = (props) => {
|
||||
const {
|
||||
text,
|
||||
visibility,
|
||||
isReblog,
|
||||
isReply,
|
||||
isPoll,
|
||||
isQuote,
|
||||
attachments,
|
||||
contentWarning,
|
||||
|
||||
hasFavourited,
|
||||
hasReblogged,
|
||||
hasBookmarked,
|
||||
hasFilter,
|
||||
hasVoted,
|
||||
showTranslate,
|
||||
disableActions = false,
|
||||
|
||||
contextType,
|
||||
showThread,
|
||||
showCounters,
|
||||
favouriteCount = 0,
|
||||
replyCount = 0,
|
||||
reblogCount = 0,
|
||||
hidden,
|
||||
muted,
|
||||
showPrepend = true,
|
||||
} = props;
|
||||
const { account, status } = useMemo(() => {
|
||||
const account = accountFactoryState();
|
||||
|
||||
const media_attachments: ApiMediaAttachmentJSON[] = [];
|
||||
switch (attachments) {
|
||||
// Use fall through add attachments depending on count.
|
||||
case 'image-3':
|
||||
media_attachments.push(
|
||||
mediaAttachmentFactory({
|
||||
id: '2',
|
||||
url: 'https://cataas.com/cat/EbVq9zMc4Xxv7s73',
|
||||
meta: {
|
||||
original: {
|
||||
width: 960,
|
||||
height: 1280,
|
||||
size: '960x1280',
|
||||
aspect: 0.75,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case 'image-2':
|
||||
media_attachments.push(
|
||||
mediaAttachmentFactory({
|
||||
id: '3',
|
||||
url: 'https://cataas.com/cat/YFaQ4xWYoWURSz37',
|
||||
meta: {
|
||||
original: {
|
||||
width: 964,
|
||||
height: 1280,
|
||||
size: '964x1280',
|
||||
aspect: 0.753125,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case 'image-1':
|
||||
media_attachments.push(
|
||||
mediaAttachmentFactory({
|
||||
id: '4',
|
||||
url: 'https://cataas.com/cat/bYBTjiFUqjUPIBUD',
|
||||
meta: {
|
||||
original: {
|
||||
width: 1280,
|
||||
height: 964,
|
||||
size: '1280x964',
|
||||
aspect: 1.32780083,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case 'video':
|
||||
media_attachments.push(
|
||||
mediaAttachmentFactory({
|
||||
type: 'video',
|
||||
url: 'https://www.pexels.com/download/video/11760787/',
|
||||
meta: {
|
||||
original: {
|
||||
width: 2160,
|
||||
height: 4096,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case 'audio':
|
||||
media_attachments.push(
|
||||
mediaAttachmentFactory({
|
||||
type: 'audio',
|
||||
url: 'https://upload.wikimedia.org/wikipedia/commons/4/40/Elephant_voice_-_trumpeting.ogg',
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case 'gifv':
|
||||
media_attachments.push(
|
||||
mediaAttachmentFactory({
|
||||
type: 'gifv',
|
||||
url: 'https://www.pexels.com/download/video/11760787/',
|
||||
meta: {
|
||||
original: {
|
||||
width: 2160,
|
||||
height: 4096,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case 'unknown':
|
||||
media_attachments.push(mediaAttachmentFactory({ type: attachments }));
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
account,
|
||||
status: statusFactoryState({
|
||||
text,
|
||||
spoiler_text: contentWarning,
|
||||
visibility,
|
||||
media_attachments,
|
||||
reblogged: hasReblogged,
|
||||
favourited: hasFavourited,
|
||||
bookmarked: hasBookmarked,
|
||||
in_reply_to_account_id: isReply ? '2' : undefined,
|
||||
in_reply_to_id: isReply ? '2' : undefined,
|
||||
quote: isQuote
|
||||
? {
|
||||
state: 'accepted',
|
||||
quoted_status: { ...statusFactory(), quote: undefined },
|
||||
}
|
||||
: undefined,
|
||||
favourites_count: favouriteCount,
|
||||
reblogs_count: reblogCount,
|
||||
replies_count: replyCount,
|
||||
language: showTranslate ? 'xx' : undefined,
|
||||
}).withMutations((status) => {
|
||||
status.set('account', account);
|
||||
status.set('matched_filters', hasFilter ? ['test'] : false);
|
||||
status.set('matched_media_filters', hasFilter ? ['test'] : false);
|
||||
status.set('hidden', hidden);
|
||||
|
||||
// StatusActionBar checks specifically for null so undefined doesn't work.
|
||||
if (!status.get('in_reply_to_id')) {
|
||||
status.set('in_reply_to_id', null);
|
||||
}
|
||||
|
||||
if (isReblog) {
|
||||
status.set(
|
||||
'reblog',
|
||||
statusFactoryState({ id: '2' }).set('account', otherAccount),
|
||||
);
|
||||
}
|
||||
if (isPoll) {
|
||||
status.set('poll', hasVoted ? '2' : '1');
|
||||
}
|
||||
}),
|
||||
};
|
||||
}, [
|
||||
attachments,
|
||||
text,
|
||||
contentWarning,
|
||||
visibility,
|
||||
hasReblogged,
|
||||
hasFavourited,
|
||||
hasBookmarked,
|
||||
isReply,
|
||||
isQuote,
|
||||
favouriteCount,
|
||||
reblogCount,
|
||||
replyCount,
|
||||
showTranslate,
|
||||
hasFilter,
|
||||
hidden,
|
||||
isReblog,
|
||||
isPoll,
|
||||
hasVoted,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div style={{ width: 'min(600px, 80vw)' }}>
|
||||
<TypedStatus
|
||||
{...staticProps}
|
||||
key={JSON.stringify(props)} // Update on any props change. Required because Status has updateOnProps set.
|
||||
status={status}
|
||||
account={isReblog ? account : undefined}
|
||||
isQuotedPost={isQuote}
|
||||
showActions={!disableActions}
|
||||
contextType={contextType}
|
||||
withCounters={showCounters}
|
||||
// Either we are showing a thread (in a timeline) or it's a full reply chain view.
|
||||
showThread={isReply && showThread}
|
||||
previousId={isReply && !showThread ? '2' : undefined}
|
||||
rootId={isReply && !showThread ? '2' : undefined}
|
||||
nextInReplyToId={isReply && !showThread ? '1' : undefined}
|
||||
muted={muted}
|
||||
hidden={hidden && !contentWarning && !hasFilter}
|
||||
skipPrepend={!showPrepend}
|
||||
withDismiss={contextType === 'notifications'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const staticProps = Object.fromEntries(
|
||||
// As Storybook auto-names from args only,
|
||||
// we need to manually name these for proper action tracking.
|
||||
Object.entries({
|
||||
onReply: fn(),
|
||||
onFavourite: fn(),
|
||||
onMention: fn(),
|
||||
onOpenMedia: fn(),
|
||||
onOpenVideo: fn(),
|
||||
onQuote: fn(),
|
||||
onReblog: fn(),
|
||||
onToggleCollapsed: fn(),
|
||||
onToggleHidden: fn(),
|
||||
onTranslate: fn(),
|
||||
onAddFilter: fn(),
|
||||
onBlock: fn(),
|
||||
onClick: fn(),
|
||||
onDelete: fn(),
|
||||
onDirect: fn(),
|
||||
onEmbed: fn(),
|
||||
onHeightChange: fn(),
|
||||
onInteractionModal: fn(),
|
||||
onPin: fn(),
|
||||
deployPictureInPicture: fn(),
|
||||
} as const)
|
||||
.map(([key, value]) => [key, value.mockName(key)])
|
||||
.concat([
|
||||
[
|
||||
'pictureInPicture',
|
||||
ImmutableMap<'inUse' | 'available', boolean>({
|
||||
inUse: false,
|
||||
available: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Casting to solves infinite recursion errors.
|
||||
}) as any,
|
||||
],
|
||||
]),
|
||||
);
|
||||
|
||||
const categoryContents = {
|
||||
table: {
|
||||
category: 'contents',
|
||||
},
|
||||
} as const;
|
||||
const categoryInteraction = {
|
||||
table: {
|
||||
category: 'interactions',
|
||||
},
|
||||
} as const;
|
||||
const categoryDisplay = {
|
||||
table: {
|
||||
category: 'display',
|
||||
},
|
||||
} as const;
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Status/Status',
|
||||
component: StatusStoryComponent,
|
||||
argTypes: {
|
||||
// Contents
|
||||
visibility: {
|
||||
...categoryContents,
|
||||
control: 'inline-radio',
|
||||
options: [
|
||||
'direct',
|
||||
'private',
|
||||
'public',
|
||||
'unlisted',
|
||||
] satisfies StatusVisibility[],
|
||||
},
|
||||
isReblog: categoryContents,
|
||||
isReply: categoryContents,
|
||||
isPoll: categoryContents,
|
||||
isQuote: categoryContents,
|
||||
text: categoryContents,
|
||||
attachments: {
|
||||
...categoryContents,
|
||||
control: 'select',
|
||||
options: [
|
||||
'One image',
|
||||
'Two images',
|
||||
'Three images',
|
||||
'Video',
|
||||
'Audio',
|
||||
'GIF',
|
||||
'Other',
|
||||
],
|
||||
mapping: {
|
||||
'One image': 'image-1',
|
||||
'Two images': 'image-2',
|
||||
'Three images': 'image-3',
|
||||
Video: 'video',
|
||||
Audio: 'audio',
|
||||
GIF: 'gifv',
|
||||
Other: 'unknown',
|
||||
} satisfies Record<string, AttachmentTypes>,
|
||||
},
|
||||
contentWarning: categoryContents,
|
||||
|
||||
// Interactions
|
||||
hasFavourited: categoryInteraction,
|
||||
hasReblogged: categoryInteraction,
|
||||
hasBookmarked: categoryInteraction,
|
||||
hasFilter: categoryInteraction,
|
||||
hasVoted: {
|
||||
...categoryInteraction,
|
||||
if: {
|
||||
arg: 'isPoll',
|
||||
truthy: true,
|
||||
},
|
||||
},
|
||||
disableActions: categoryInteraction,
|
||||
showTranslate: categoryInteraction,
|
||||
|
||||
// Display
|
||||
showCounters: categoryDisplay,
|
||||
favouriteCount: categoryDisplay,
|
||||
reblogCount: categoryDisplay,
|
||||
replyCount: categoryDisplay,
|
||||
showPrepend: categoryDisplay,
|
||||
showThread: {
|
||||
...categoryDisplay,
|
||||
if: {
|
||||
arg: 'showPrepend',
|
||||
truthy: true,
|
||||
},
|
||||
},
|
||||
contextType: {
|
||||
...categoryDisplay,
|
||||
control: 'select',
|
||||
options: [
|
||||
'account',
|
||||
'bookmarks',
|
||||
'detailed',
|
||||
'favourites',
|
||||
'home',
|
||||
'notifications',
|
||||
'public',
|
||||
'search',
|
||||
'thread',
|
||||
] satisfies ContextTypes[],
|
||||
},
|
||||
hidden: categoryDisplay,
|
||||
muted: categoryDisplay,
|
||||
},
|
||||
args: {
|
||||
text: 'This is a status',
|
||||
visibility: 'public',
|
||||
isReblog: false,
|
||||
isReply: false,
|
||||
isPoll: false,
|
||||
isQuote: false,
|
||||
contentWarning: '',
|
||||
attachments: undefined,
|
||||
|
||||
hasFavourited: false,
|
||||
hasReblogged: false,
|
||||
hasBookmarked: false,
|
||||
hasFilter: false,
|
||||
hasVoted: false,
|
||||
disableActions: false,
|
||||
showTranslate: false,
|
||||
|
||||
favouriteCount: 0,
|
||||
reblogCount: 0,
|
||||
replyCount: 0,
|
||||
showCounters: true,
|
||||
contextType: 'home',
|
||||
showPrepend: true,
|
||||
showThread: false,
|
||||
hidden: false,
|
||||
muted: false,
|
||||
} satisfies StatusStoryProps,
|
||||
parameters: {
|
||||
state: {
|
||||
accounts: {
|
||||
'2': otherAccount,
|
||||
},
|
||||
polls: {
|
||||
'1': pollFactory(),
|
||||
'2': pollFactory({
|
||||
voted: true,
|
||||
voters_count: 1,
|
||||
votes_count: 1,
|
||||
own_votes: [0],
|
||||
}),
|
||||
},
|
||||
server: {
|
||||
translationLanguages: {
|
||||
item: {
|
||||
xx: ['en', 'de', 'fr'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
controls: {
|
||||
disableSaveFromUI: true,
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof StatusStoryComponent>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Reply: Story = {
|
||||
args: {
|
||||
isReply: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const LongText: Story = {
|
||||
args: {
|
||||
text: [
|
||||
'This is a long-form piece of text that wraps multiple lines.',
|
||||
'It is here to test what a longer status looks like.',
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
|
||||
'Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
|
||||
].join('\n'),
|
||||
},
|
||||
};
|
||||
|
||||
export const Images: Story = {
|
||||
args: {
|
||||
attachments: 'image-3',
|
||||
},
|
||||
};
|
||||
|
||||
export const Video: Story = {
|
||||
args: {
|
||||
attachments: 'video',
|
||||
},
|
||||
};
|
||||
|
||||
export const Audio: Story = {
|
||||
args: {
|
||||
attachments: 'audio',
|
||||
},
|
||||
};
|
||||
|
||||
export const Poll: Story = {
|
||||
args: {
|
||||
isPoll: true,
|
||||
hasVoted: true,
|
||||
},
|
||||
};
|
||||
@@ -1,16 +1,49 @@
|
||||
import type { ComponentClass, MouseEventHandler, ReactNode } from 'react';
|
||||
import type { ComponentType, MouseEventHandler, ReactNode } from 'react';
|
||||
|
||||
import type { Account } from '@/mastodon/models/account';
|
||||
import type { Account as TAccount } from '@/mastodon/models/account';
|
||||
import type { Status as TStatus } from '@/mastodon/models/status';
|
||||
|
||||
import Status from '../status';
|
||||
|
||||
import type { StatusHeaderRenderFn } from './header';
|
||||
|
||||
// Taken from the Status component.
|
||||
export interface StatusProps {
|
||||
account?: Account;
|
||||
status: TStatus;
|
||||
account?: TAccount;
|
||||
children?: ReactNode;
|
||||
previousId?: string;
|
||||
nextInReplyToId?: string;
|
||||
rootId?: string;
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
onReply: (status: TStatus) => void;
|
||||
onFavourite: (status: TStatus) => void;
|
||||
onReblog: (status: TStatus, event?: unknown) => void;
|
||||
onQuote: (status: TStatus) => void;
|
||||
onDelete?: (status: TStatus) => void;
|
||||
onDirect?: (status: TStatus) => void;
|
||||
onMention: (account: TAccount) => void;
|
||||
onPin?: (status: TStatus) => void;
|
||||
onOpenMedia: (
|
||||
statusId: string,
|
||||
media: unknown,
|
||||
index: number,
|
||||
lang?: string,
|
||||
) => void;
|
||||
onOpenVideo: (
|
||||
statusId: string,
|
||||
media: unknown,
|
||||
lang?: string,
|
||||
options?: unknown,
|
||||
) => void;
|
||||
onBlock?: (status: TStatus) => void;
|
||||
onAddFilter?: (status: TStatus) => void;
|
||||
onEmbed?: (status: TStatus) => void;
|
||||
onHeightChange?: () => void;
|
||||
onToggleHidden: (status: TStatus) => void;
|
||||
onToggleCollapsed: (status: TStatus, isCollapsed: boolean) => void;
|
||||
onTranslate: (status: TStatus) => void;
|
||||
onInteractionModal?: (type: string, status: TStatus) => void;
|
||||
muted?: boolean;
|
||||
hidden?: boolean;
|
||||
unread?: boolean;
|
||||
@@ -26,12 +59,16 @@ export interface StatusProps {
|
||||
scrollKey?: string;
|
||||
skipPrepend?: boolean;
|
||||
avatarSize?: number;
|
||||
deployPictureInPicture: (
|
||||
status: TStatus,
|
||||
type: string,
|
||||
mediaProps: unknown,
|
||||
) => void;
|
||||
unfocusable?: boolean;
|
||||
headerRenderFn?: StatusHeaderRenderFn;
|
||||
pictureInPicture: Immutable.Map<'inUse' | 'available', boolean>;
|
||||
contextType?: string;
|
||||
withCounters?: boolean;
|
||||
}
|
||||
|
||||
export type StatusComponent = ComponentClass<
|
||||
StatusProps,
|
||||
{ showMedia?: boolean; showDespiteFilter?: boolean }
|
||||
>;
|
||||
export const TypedStatus = Status as ComponentType<StatusProps>;
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
* type PersonWithSomeOptional = SomeOptional<Person, 'name' >;
|
||||
*/
|
||||
|
||||
export type DeepPartial<T> = T extends object
|
||||
? {
|
||||
[K in keyof T]?: DeepPartial<T[K]>;
|
||||
}
|
||||
: T;
|
||||
|
||||
export type SomeRequired<T, K extends keyof T> = T & Required<Pick<T, K>>;
|
||||
export type SomeOptional<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>> &
|
||||
Partial<Pick<T, K>>;
|
||||
@@ -21,10 +27,14 @@ export type OmitValueType<T, V> = {
|
||||
[K in keyof T as T[K] extends V ? never : K]: T[K];
|
||||
};
|
||||
|
||||
export type AnyFunction = (...args: never) => unknown;
|
||||
|
||||
export type OmitUnion<TUnion, TBase> = TBase & Omit<TUnion, keyof TBase>;
|
||||
|
||||
export type PickValueType<T, V> = {
|
||||
[K in keyof T as T[K] extends V | undefined ? K : never]: T[K];
|
||||
};
|
||||
|
||||
export type AnyFunction = (...args: never) => unknown;
|
||||
|
||||
export type SnakeToCamelCase<S extends string> =
|
||||
S extends `${infer T}_${infer U}`
|
||||
? `${T}${Capitalize<SnakeToCamelCase<U>>}`
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import { Map as ImmutableMap, List } from 'immutable';
|
||||
import { fromJS } from 'immutable';
|
||||
|
||||
import { normalizeStatus } from '@/mastodon/actions/importer/statuses';
|
||||
import type {
|
||||
ApiAudioAttachmentJSON,
|
||||
ApiGifvAttachmentJSON,
|
||||
ApiImageAttachmentJSON,
|
||||
ApiMediaAttachmentJSON,
|
||||
ApiVideoAttachmentJSON,
|
||||
BaseApiMediaAttachmentJSON,
|
||||
} from '@/mastodon/api_types/media_attachments';
|
||||
import type { ApiPollJSON } from '@/mastodon/api_types/polls';
|
||||
import type { ApiRelationshipJSON } from '@/mastodon/api_types/relationships';
|
||||
import type { ApiStatusJSON } from '@/mastodon/api_types/statuses';
|
||||
import type {
|
||||
@@ -9,6 +19,7 @@ import type {
|
||||
import { createAccountFromServerJSON } from '@/mastodon/models/account';
|
||||
import type { AnnualReport } from '@/mastodon/models/annual_report';
|
||||
import type { Status } from '@/mastodon/models/status';
|
||||
import type { DeepPartial } from '@/mastodon/utils/types';
|
||||
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||
|
||||
type FactoryOptions<T> = {
|
||||
@@ -87,18 +98,153 @@ export const statusFactory: FactoryFunction<ApiStatusJSON> = ({
|
||||
tags: [],
|
||||
emojis: [],
|
||||
tagged_collections: [],
|
||||
contentHtml: data.text ?? '<p>This is a test status.</p>',
|
||||
content:
|
||||
data.text
|
||||
?.split('\n')
|
||||
.map((line) => `<p>${line}</p>`)
|
||||
.join('\n') ?? '<p>This is a test status.</p>',
|
||||
...data,
|
||||
});
|
||||
|
||||
export const statusFactoryState = (
|
||||
options: FactoryOptions<ApiStatusJSON> = {},
|
||||
) =>
|
||||
ImmutableMap<string, unknown>({
|
||||
...(statusFactory(options) as unknown as Record<string, unknown>),
|
||||
account: options.account?.id ?? '1',
|
||||
tags: List(options.tags),
|
||||
}) as unknown as Status;
|
||||
) => fromJS(normalizeStatus(statusFactory(options))) as unknown as Status;
|
||||
|
||||
const baseAttachment = {
|
||||
id: '1',
|
||||
url: 'https://example.com/image/1',
|
||||
preview_url: 'https://example.com/image/1/preview',
|
||||
blurhash: '',
|
||||
} as const;
|
||||
const imageMeta = {
|
||||
width: 100,
|
||||
height: 100,
|
||||
aspect: 1,
|
||||
size: '100x100',
|
||||
} as const;
|
||||
const videoMeta = {
|
||||
width: 100,
|
||||
height: 100,
|
||||
frame_rate: '24',
|
||||
duration: 120,
|
||||
bitrate: 100,
|
||||
} as const;
|
||||
const colorsMeta = {
|
||||
background: '#ffffff',
|
||||
foreground: '#000000',
|
||||
accent: '#ff0000',
|
||||
} as const;
|
||||
|
||||
type MediaFactoryArg<T extends BaseApiMediaAttachmentJSON> = Omit<
|
||||
DeepPartial<T>,
|
||||
'type'
|
||||
>;
|
||||
|
||||
export const imageAttachmentFactory = (
|
||||
data: MediaFactoryArg<ApiImageAttachmentJSON> = {},
|
||||
): ApiImageAttachmentJSON => ({
|
||||
...baseAttachment,
|
||||
...data,
|
||||
type: 'image',
|
||||
meta: {
|
||||
original: { ...imageMeta, ...data.meta?.original },
|
||||
small: { ...imageMeta, ...data.meta?.small },
|
||||
},
|
||||
});
|
||||
|
||||
export const videoAttachmentFactory = (
|
||||
data: MediaFactoryArg<ApiVideoAttachmentJSON> = {},
|
||||
): ApiVideoAttachmentJSON => ({
|
||||
...baseAttachment,
|
||||
...data,
|
||||
type: 'video',
|
||||
meta: {
|
||||
colors: { ...colorsMeta, ...data.meta?.colors },
|
||||
original: { ...videoMeta, ...data.meta?.original },
|
||||
small: { ...imageMeta, ...data.meta?.small },
|
||||
focus: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
...data.meta?.focus,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const audioAttachmentFactory = (
|
||||
data: MediaFactoryArg<ApiAudioAttachmentJSON> = {},
|
||||
): ApiAudioAttachmentJSON => ({
|
||||
...baseAttachment,
|
||||
...data,
|
||||
type: 'audio',
|
||||
meta: {
|
||||
colors: { ...colorsMeta, ...data.meta?.colors },
|
||||
original: { ...videoMeta, ...data.meta?.original },
|
||||
small: { ...imageMeta, ...data.meta?.small },
|
||||
},
|
||||
});
|
||||
|
||||
export const gifvAttachmentFactory = (
|
||||
data: MediaFactoryArg<ApiGifvAttachmentJSON> = {},
|
||||
): ApiGifvAttachmentJSON => ({
|
||||
...baseAttachment,
|
||||
...data,
|
||||
type: 'gifv',
|
||||
meta: {
|
||||
original: { ...videoMeta, ...data.meta?.original },
|
||||
small: { ...imageMeta, ...data.meta?.small },
|
||||
},
|
||||
});
|
||||
|
||||
export function mediaAttachmentFactory(
|
||||
data: DeepPartial<ApiMediaAttachmentJSON> = {},
|
||||
): ApiMediaAttachmentJSON {
|
||||
switch (data.type ?? 'image') {
|
||||
case 'image':
|
||||
return imageAttachmentFactory(
|
||||
data as DeepPartial<ApiImageAttachmentJSON>,
|
||||
);
|
||||
case 'video':
|
||||
return videoAttachmentFactory(
|
||||
data as DeepPartial<ApiVideoAttachmentJSON>,
|
||||
);
|
||||
case 'audio':
|
||||
return audioAttachmentFactory(
|
||||
data as DeepPartial<ApiAudioAttachmentJSON>,
|
||||
);
|
||||
case 'gifv':
|
||||
return gifvAttachmentFactory(data as DeepPartial<ApiGifvAttachmentJSON>);
|
||||
default: {
|
||||
return {
|
||||
...baseAttachment,
|
||||
meta: {},
|
||||
...data,
|
||||
type: 'unknown',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const pollFactory: FactoryFunction<ApiPollJSON> = (data = {}) => ({
|
||||
id: '1',
|
||||
expires_at: '',
|
||||
expired: false,
|
||||
multiple: false,
|
||||
voters_count: 0,
|
||||
votes_count: 0,
|
||||
voted: false,
|
||||
options: [
|
||||
{
|
||||
title: 'Option 1',
|
||||
votes_count: 0,
|
||||
},
|
||||
{
|
||||
title: 'Option 2',
|
||||
votes_count: 0,
|
||||
},
|
||||
],
|
||||
emojis: [],
|
||||
...data,
|
||||
});
|
||||
|
||||
export const relationshipsFactory: FactoryFunction<ApiRelationshipJSON> = ({
|
||||
id,
|
||||
|
||||
Reference in New Issue
Block a user