Merge branch 'next-ts-strict' into 'next'
Put Typescript in "strict" mode, fix or ignore errors See merge request soapbox-pub/soapbox-fe!1145
This commit is contained in:
commit
15f2e46bc9
29 changed files with 177 additions and 116 deletions
|
@ -44,7 +44,6 @@ exports[`<TimelineQueueButtonHeader /> renders correctly 2`] = `
|
|||
</div>
|
||||
<p
|
||||
className="text-sm text-inherit font-normal tracking-normal font-sans"
|
||||
style={null}
|
||||
>
|
||||
Click to see 1 new post
|
||||
</p>
|
||||
|
@ -73,7 +72,6 @@ exports[`<TimelineQueueButtonHeader /> renders correctly 3`] = `
|
|||
</div>
|
||||
<p
|
||||
className="text-sm text-inherit font-normal tracking-normal font-sans"
|
||||
style={null}
|
||||
>
|
||||
Click to see 9999999 new posts
|
||||
</p>
|
||||
|
|
|
@ -54,6 +54,7 @@ const Account = ({
|
|||
}: IAccount) => {
|
||||
const overflowRef = React.useRef<HTMLDivElement>(null);
|
||||
const actionRef = React.useRef<HTMLDivElement>(null);
|
||||
// @ts-ignore
|
||||
const isOnScreen = useOnScreen(overflowRef);
|
||||
|
||||
const [style, setStyle] = React.useState<React.CSSProperties>({ visibility: 'hidden' });
|
||||
|
@ -62,6 +63,7 @@ const Account = ({
|
|||
const username = useAppSelector((state) => account ? getAcct(account, displayFqn(state)) : null);
|
||||
|
||||
const handleAction = () => {
|
||||
// @ts-ignore
|
||||
onActionClick(account);
|
||||
};
|
||||
|
||||
|
@ -95,7 +97,7 @@ const Account = ({
|
|||
|
||||
React.useEffect(() => {
|
||||
const style: React.CSSProperties = {};
|
||||
const actionWidth = actionRef.current?.clientWidth;
|
||||
const actionWidth = actionRef.current?.clientWidth || 0;
|
||||
|
||||
if (overflowRef.current) {
|
||||
style.maxWidth = overflowRef.current.clientWidth - 30 - avatarSize - actionWidth;
|
||||
|
|
|
@ -46,6 +46,7 @@ function Blurhash({
|
|||
const ctx = canvas.getContext('2d');
|
||||
const imageData = new ImageData(pixels, width, height);
|
||||
|
||||
// @ts-ignore
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
} catch (err) {
|
||||
console.error('Blurhash decoding failure', { err, hash });
|
||||
|
|
|
@ -8,7 +8,7 @@ interface MissingIndicatorProps {
|
|||
}
|
||||
|
||||
const MissingIndicator = ({ nested = false }: MissingIndicatorProps): JSX.Element => (
|
||||
<Card variant={nested ? null : 'rounded'} size='lg'>
|
||||
<Card variant={nested ? undefined : 'rounded'} size='lg'>
|
||||
<CardBody>
|
||||
<Stack space={2}>
|
||||
<Text weight='medium' align='center' size='lg'>
|
||||
|
|
|
@ -25,9 +25,9 @@ const PullToRefresh = ({ children, onRefresh, ...rest }: IPullToRefresh) => {
|
|||
return (
|
||||
<PTRComponent
|
||||
onRefresh={handleRefresh}
|
||||
pullingContent={null}
|
||||
// `undefined` will fallback to the default, while `null` will render nothing
|
||||
refreshingContent={onRefresh ? <Spinner size={30} withText={false} /> : null}
|
||||
pullingContent={<></>}
|
||||
// `undefined` will fallback to the default, while `<></>` will render nothing
|
||||
refreshingContent={onRefresh ? <Spinner size={30} withText={false} /> : <></>}
|
||||
pullDownThreshold={67}
|
||||
maxPullDownDistance={95}
|
||||
resistance={2}
|
||||
|
|
|
@ -21,9 +21,7 @@ const Avatar = (props: IAvatar) => {
|
|||
|
||||
return (
|
||||
<StillImage
|
||||
className={classNames('rounded-full', {
|
||||
[className]: typeof className !== 'undefined',
|
||||
})}
|
||||
className={classNames('rounded-full', className)}
|
||||
style={style}
|
||||
src={src}
|
||||
alt='Avatar'
|
||||
|
|
|
@ -28,8 +28,7 @@ const Card: React.FC<ICard> = React.forwardRef(({ children, variant, size = 'md'
|
|||
'space-y-4': true,
|
||||
'bg-white dark:bg-slate-800 sm:shadow-lg dark:sm:shadow-inset overflow-hidden': variant === 'rounded',
|
||||
[sizes[size]]: true,
|
||||
[className]: typeof className !== 'undefined',
|
||||
})}
|
||||
}, className)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
@ -39,12 +39,14 @@ const HStack: React.FC<IHStack> = (props) => {
|
|||
<div
|
||||
{...filteredProps}
|
||||
className={classNames('flex', {
|
||||
// @ts-ignore
|
||||
[alignItemsOptions[alignItems]]: typeof alignItems !== 'undefined',
|
||||
// @ts-ignore
|
||||
[justifyContentOptions[justifyContent]]: typeof justifyContent !== 'undefined',
|
||||
// @ts-ignore
|
||||
[spaces[space]]: typeof space !== 'undefined',
|
||||
[className]: typeof className !== 'undefined',
|
||||
'flex-grow': grow,
|
||||
})}
|
||||
}, className)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -25,8 +25,7 @@ const IconButton = React.forwardRef((props: IIconButton, ref: React.ForwardedRef
|
|||
type='button'
|
||||
className={classNames('flex items-center space-x-2 p-1 rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 dark:ring-offset-0 focus:ring-primary-500', {
|
||||
'bg-white dark:bg-transparent': !transparent,
|
||||
[className]: typeof className !== 'undefined',
|
||||
})}
|
||||
}, className)}
|
||||
{...filteredProps}
|
||||
>
|
||||
<InlineSVG src={src} className={iconClassName} />
|
||||
|
|
|
@ -54,8 +54,7 @@ const Input = React.forwardRef<HTMLInputElement, IInput>(
|
|||
true,
|
||||
'pr-7': isPassword,
|
||||
'pl-8': typeof icon !== 'undefined',
|
||||
[className]: typeof className !== 'undefined',
|
||||
})}
|
||||
}, className)}
|
||||
/>
|
||||
|
||||
{isPassword ? (
|
||||
|
|
|
@ -35,11 +35,13 @@ const Stack: React.FC<IStack> = (props) => {
|
|||
<div
|
||||
{...filteredProps}
|
||||
className={classNames('flex flex-col', {
|
||||
// @ts-ignore
|
||||
[spaces[space]]: typeof space !== 'undefined',
|
||||
// @ts-ignore
|
||||
[alignItemsOptions[alignItems]]: typeof alignItems !== 'undefined',
|
||||
// @ts-ignore
|
||||
[justifyContentOptions[justifyContent]]: typeof justifyContent !== 'undefined',
|
||||
[className]: typeof className !== 'undefined',
|
||||
})}
|
||||
}, className)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -24,13 +24,21 @@ const AnimatedTabs: React.FC<IAnimatedInterface> = ({ children, ...rest }) => {
|
|||
const ref = React.useRef();
|
||||
const rect = useRect(ref);
|
||||
|
||||
// @ts-ignore
|
||||
const top: number = (activeRect && activeRect.bottom) - (rect && rect.top);
|
||||
// @ts-ignore
|
||||
const width: number = activeRect && activeRect.width - HORIZONTAL_PADDING * 2;
|
||||
// @ts-ignore
|
||||
const left: number = (activeRect && activeRect.left) - (rect && rect.left) + HORIZONTAL_PADDING;
|
||||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<AnimatedContext.Provider value={setActiveRect}>
|
||||
<ReachTabs {...rest} ref={ref}>
|
||||
<ReachTabs
|
||||
{...rest}
|
||||
// @ts-ignore
|
||||
ref={ref}
|
||||
>
|
||||
<div
|
||||
className='w-full h-[3px] bg-primary-200 absolute'
|
||||
style={{ top }}
|
||||
|
@ -70,11 +78,13 @@ const AnimatedTab: React.FC<IAnimatedTab> = ({ index, ...props }) => {
|
|||
// callup to set styles whenever we're active
|
||||
React.useLayoutEffect(() => {
|
||||
if (isSelected) {
|
||||
// @ts-ignore
|
||||
setActiveRect(rect);
|
||||
}
|
||||
}, [isSelected, rect, setActiveRect]);
|
||||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<ReachTab ref={ref} {...props} />
|
||||
);
|
||||
};
|
||||
|
@ -115,6 +125,7 @@ const Tabs = ({ items, activeItem }: ITabs) => {
|
|||
key={name}
|
||||
as='button'
|
||||
role='button'
|
||||
// @ts-ignore
|
||||
title={title}
|
||||
index={idx}
|
||||
>
|
||||
|
|
|
@ -83,11 +83,13 @@ const Text: React.FC<IText> = React.forwardRef(
|
|||
|
||||
const Comp: React.ElementType = tag;
|
||||
|
||||
const alignmentClass = typeof align === 'string' ? alignments[align] : '';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
{...filteredProps}
|
||||
ref={ref}
|
||||
style={tag === 'abbr' ? { textDecoration: 'underline dotted' } : null}
|
||||
style={tag === 'abbr' ? { textDecoration: 'underline dotted' } : undefined}
|
||||
className={classNames({
|
||||
'cursor-default': tag === 'abbr',
|
||||
truncate: truncate,
|
||||
|
@ -96,9 +98,8 @@ const Text: React.FC<IText> = React.forwardRef(
|
|||
[weights[weight]]: true,
|
||||
[trackingSizes[tracking]]: true,
|
||||
[families[family]]: true,
|
||||
[alignments[align]]: typeof align !== 'undefined',
|
||||
[className]: typeof className !== 'undefined',
|
||||
})}
|
||||
[alignmentClass]: typeof align !== 'undefined',
|
||||
}, className)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -90,7 +90,7 @@ const Search = (props: ISearch) => {
|
|||
|
||||
handleSubmit();
|
||||
} else if (event.key === 'Escape') {
|
||||
document.querySelector('.ui').parentElement.focus();
|
||||
document.querySelector('.ui')?.parentElement?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ interface IProfileDropdown {
|
|||
}
|
||||
|
||||
type IMenuItem = {
|
||||
text: string | React.ReactElement,
|
||||
text: string | React.ReactElement | null,
|
||||
to?: string,
|
||||
icon?: string,
|
||||
action?: (event: React.MouseEvent) => void
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
|
||||
import type Account from 'soapbox/types/entities/account';
|
||||
import type { Account } from 'soapbox/types/entities';
|
||||
|
||||
// FIXME: There is no reason this selector shouldn't be global accross the whole app
|
||||
// FIXME: getAccount() has the wrong type??
|
||||
|
|
|
@ -17,17 +17,19 @@ import { acctFull } from 'soapbox/utils/accounts';
|
|||
import { unescapeHTML } from 'soapbox/utils/html';
|
||||
import { mergeDefined, makeEmojiMap } from 'soapbox/utils/normalizers';
|
||||
|
||||
import type { Emoji, Field, EmbeddedEntity } from 'soapbox/types/entities';
|
||||
|
||||
// https://docs.joinmastodon.org/entities/account/
|
||||
export const AccountRecord = ImmutableRecord({
|
||||
acct: '',
|
||||
avatar: '',
|
||||
avatar_static: '',
|
||||
birthday: undefined,
|
||||
birthday: undefined as Date | undefined,
|
||||
bot: false,
|
||||
created_at: new Date(),
|
||||
display_name: '',
|
||||
emojis: ImmutableList(),
|
||||
fields: ImmutableList(),
|
||||
emojis: ImmutableList<Emoji>(),
|
||||
fields: ImmutableList<Field>(),
|
||||
followers_count: 0,
|
||||
following_count: 0,
|
||||
fqn: '',
|
||||
|
@ -37,10 +39,10 @@ export const AccountRecord = ImmutableRecord({
|
|||
last_status_at: new Date(),
|
||||
location: '',
|
||||
locked: false,
|
||||
moved: null,
|
||||
moved: null as EmbeddedEntity<any> | null,
|
||||
note: '',
|
||||
pleroma: ImmutableMap(),
|
||||
source: ImmutableMap(),
|
||||
pleroma: ImmutableMap<string, any>(),
|
||||
source: ImmutableMap<string, any>(),
|
||||
statuses_count: 0,
|
||||
uri: '',
|
||||
url: '',
|
||||
|
@ -52,8 +54,8 @@ export const AccountRecord = ImmutableRecord({
|
|||
display_name_html: '',
|
||||
note_emojified: '',
|
||||
note_plain: '',
|
||||
patron: ImmutableMap(),
|
||||
relationship: ImmutableList(),
|
||||
patron: ImmutableMap<string, any>(),
|
||||
relationship: ImmutableList<ImmutableMap<string, any>>(),
|
||||
should_refetch: false,
|
||||
});
|
||||
|
||||
|
@ -61,7 +63,7 @@ export const AccountRecord = ImmutableRecord({
|
|||
export const FieldRecord = ImmutableRecord({
|
||||
name: '',
|
||||
value: '',
|
||||
verified_at: null,
|
||||
verified_at: null as Date | null,
|
||||
|
||||
// Internal fields
|
||||
name_emojified: '',
|
||||
|
|
|
@ -82,7 +82,7 @@ const pleromaToMastodonConfig = (instance: ImmutableMap<string, any>) => {
|
|||
};
|
||||
|
||||
// Get the software's default attachment limit
|
||||
const getAttachmentLimit = (software: string) => software === PLEROMA ? Infinity : 4;
|
||||
const getAttachmentLimit = (software: string | null) => software === PLEROMA ? Infinity : 4;
|
||||
|
||||
// Normalize version
|
||||
const normalizeVersion = (instance: ImmutableMap<string, any>) => {
|
||||
|
|
|
@ -15,17 +15,19 @@ import emojify from 'soapbox/features/emoji/emoji';
|
|||
import { normalizeEmoji } from 'soapbox/normalizers/emoji';
|
||||
import { makeEmojiMap } from 'soapbox/utils/normalizers';
|
||||
|
||||
import type { Emoji, PollOption } from 'soapbox/types/entities';
|
||||
|
||||
// https://docs.joinmastodon.org/entities/poll/
|
||||
export const PollRecord = ImmutableRecord({
|
||||
emojis: ImmutableList(),
|
||||
emojis: ImmutableList<Emoji>(),
|
||||
expired: false,
|
||||
expires_at: new Date(),
|
||||
id: '',
|
||||
multiple: false,
|
||||
options: ImmutableList(),
|
||||
options: ImmutableList<PollOption>(),
|
||||
voters_count: 0,
|
||||
votes_count: 0,
|
||||
own_votes: null,
|
||||
own_votes: null as ImmutableList<number> | null,
|
||||
voted: false,
|
||||
});
|
||||
|
||||
|
|
|
@ -16,38 +16,42 @@ import { normalizeEmoji } from 'soapbox/normalizers/emoji';
|
|||
import { normalizeMention } from 'soapbox/normalizers/mention';
|
||||
import { normalizePoll } from 'soapbox/normalizers/poll';
|
||||
|
||||
import type { Account, Attachment, Card, Emoji, Mention, Poll, EmbeddedEntity } from 'soapbox/types/entities';
|
||||
|
||||
type StatusVisibility = 'public' | 'unlisted' | 'private' | 'direct';
|
||||
|
||||
// https://docs.joinmastodon.org/entities/status/
|
||||
export const StatusRecord = ImmutableRecord({
|
||||
account: null,
|
||||
application: null,
|
||||
account: null as EmbeddedEntity<Account>,
|
||||
application: null as ImmutableMap<string, any> | null,
|
||||
bookmarked: false,
|
||||
card: null,
|
||||
card: null as EmbeddedEntity<Card>,
|
||||
content: '',
|
||||
created_at: new Date(),
|
||||
emojis: ImmutableList(),
|
||||
emojis: ImmutableList<Emoji>(),
|
||||
favourited: false,
|
||||
favourites_count: 0,
|
||||
in_reply_to_account_id: null,
|
||||
in_reply_to_id: null,
|
||||
in_reply_to_account_id: null as string | null,
|
||||
in_reply_to_id: null as string | null,
|
||||
id: '',
|
||||
language: null,
|
||||
media_attachments: ImmutableList(),
|
||||
mentions: ImmutableList(),
|
||||
language: null as string | null,
|
||||
media_attachments: ImmutableList<Attachment>(),
|
||||
mentions: ImmutableList<Mention>(),
|
||||
muted: false,
|
||||
pinned: false,
|
||||
pleroma: ImmutableMap(),
|
||||
poll: null,
|
||||
quote: null,
|
||||
reblog: null,
|
||||
pleroma: ImmutableMap<string, any>(),
|
||||
poll: null as EmbeddedEntity<Poll>,
|
||||
quote: null as EmbeddedEntity<any>,
|
||||
reblog: null as EmbeddedEntity<any>,
|
||||
reblogged: false,
|
||||
reblogs_count: 0,
|
||||
replies_count: 0,
|
||||
sensitive: false,
|
||||
spoiler_text: '',
|
||||
tags: ImmutableList(),
|
||||
tags: ImmutableList<ImmutableMap<string, any>>(),
|
||||
uri: '',
|
||||
url: '',
|
||||
visibility: 'public',
|
||||
visibility: 'public' as StatusVisibility,
|
||||
|
||||
// Internal fields
|
||||
contentHtml: '',
|
||||
|
|
|
@ -32,6 +32,7 @@ import {
|
|||
import { CHATS_FETCH_SUCCESS, CHATS_EXPAND_SUCCESS, CHAT_FETCH_SUCCESS } from 'soapbox/actions/chats';
|
||||
import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming';
|
||||
import { normalizeAccount } from 'soapbox/normalizers/account';
|
||||
import { normalizeId } from 'soapbox/utils/normalizers';
|
||||
|
||||
import {
|
||||
ACCOUNT_IMPORT,
|
||||
|
@ -50,7 +51,7 @@ const initialState: State = ImmutableMap();
|
|||
|
||||
const minifyAccount = (account: AccountRecord): AccountRecord => {
|
||||
return account.mergeWith((o, n) => n || o, {
|
||||
moved: account.getIn(['moved', 'id']),
|
||||
moved: normalizeId(account.getIn(['moved', 'id'])),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -201,8 +202,8 @@ const importAdminUser = (state: State, adminUser: ImmutableMap<string, any>): St
|
|||
|
||||
const importAdminUsers = (state: State, adminUsers: Array<Record<string, any>>): State => {
|
||||
return state.withMutations((state: State) => {
|
||||
fromJS(adminUsers).forEach(adminUser => {
|
||||
importAdminUser(state, ImmutableMap(adminUser));
|
||||
adminUsers.forEach(adminUser => {
|
||||
importAdminUser(state, ImmutableMap(fromJS(adminUser)));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@ import emojify from 'soapbox/features/emoji/emoji';
|
|||
import { normalizeStatus } from 'soapbox/normalizers';
|
||||
import { simulateEmojiReact, simulateUnEmojiReact } from 'soapbox/utils/emoji_reacts';
|
||||
import { stripCompatibilityFeatures, unescapeHTML } from 'soapbox/utils/html';
|
||||
import { makeEmojiMap } from 'soapbox/utils/normalizers';
|
||||
import { makeEmojiMap, normalizeId } from 'soapbox/utils/normalizers';
|
||||
|
||||
import {
|
||||
EMOJI_REACT_REQUEST,
|
||||
|
@ -42,16 +42,20 @@ type State = ImmutableMap<string, StatusRecord>;
|
|||
|
||||
const minifyStatus = (status: StatusRecord): StatusRecord => {
|
||||
return status.mergeWith((o, n) => n || o, {
|
||||
account: status.getIn(['account', 'id']),
|
||||
reblog: status.getIn(['reblog', 'id']),
|
||||
poll: status.getIn(['poll', 'id']),
|
||||
quote: status.getIn(['quote', 'id']),
|
||||
account: normalizeId(status.getIn(['account', 'id'])),
|
||||
reblog: normalizeId(status.getIn(['reblog', 'id'])),
|
||||
poll: normalizeId(status.getIn(['poll', 'id'])),
|
||||
quote: normalizeId(status.getIn(['quote', 'id'])),
|
||||
});
|
||||
};
|
||||
|
||||
// Gets titles of poll options from status
|
||||
const getPollOptionTitles = (status: StatusRecord): Array<string> => {
|
||||
return status.poll?.options.map(({ title }: { title: string }) => title);
|
||||
const getPollOptionTitles = ({ poll }: StatusRecord): ImmutableList<string> => {
|
||||
if (poll && typeof poll === 'object') {
|
||||
return poll.options.map(({ title }) => title);
|
||||
} else {
|
||||
return ImmutableList();
|
||||
}
|
||||
};
|
||||
|
||||
// Creates search text from the status
|
||||
|
@ -63,14 +67,14 @@ const buildSearchContent = (status: StatusRecord): string => {
|
|||
status.content,
|
||||
]).concat(pollOptionTitles);
|
||||
|
||||
return unescapeHTML(fields.join('\n\n'));
|
||||
return unescapeHTML(fields.join('\n\n')) || '';
|
||||
};
|
||||
|
||||
// Only calculate these values when status first encountered
|
||||
// Otherwise keep the ones already in the reducer
|
||||
export const calculateStatus = (
|
||||
status: StatusRecord,
|
||||
oldStatus: StatusRecord,
|
||||
oldStatus?: StatusRecord,
|
||||
expandSpoilers: boolean = false,
|
||||
): StatusRecord => {
|
||||
if (oldStatus) {
|
||||
|
@ -86,7 +90,7 @@ export const calculateStatus = (
|
|||
const emojiMap = makeEmojiMap(status.emojis);
|
||||
|
||||
return status.merge({
|
||||
search_index: domParser.parseFromString(searchContent, 'text/html').documentElement.textContent || undefined,
|
||||
search_index: domParser.parseFromString(searchContent, 'text/html').documentElement.textContent || '',
|
||||
contentHtml: stripCompatibilityFeatures(emojify(status.content, emojiMap)),
|
||||
spoilerHtml: emojify(escapeTextContentForBrowser(spoilerText), emojiMap),
|
||||
hidden: expandSpoilers ? false : spoilerText.length > 0 || status.sensitive,
|
||||
|
@ -100,7 +104,7 @@ const isQuote = (status: StatusRecord) => {
|
|||
};
|
||||
|
||||
// Preserve quote if an existing status already has it
|
||||
const fixQuote = (status: StatusRecord, oldStatus: StatusRecord): StatusRecord => {
|
||||
const fixQuote = (status: StatusRecord, oldStatus?: StatusRecord): StatusRecord => {
|
||||
if (oldStatus && !status.quote && isQuote(status)) {
|
||||
return status
|
||||
.set('quote', oldStatus.quote)
|
||||
|
@ -111,7 +115,7 @@ const fixQuote = (status: StatusRecord, oldStatus: StatusRecord): StatusRecord =
|
|||
};
|
||||
|
||||
const fixStatus = (state: State, status: APIEntity, expandSpoilers: boolean): StatusRecord => {
|
||||
const oldStatus: StatusRecord = state.get(status.id);
|
||||
const oldStatus = state.get(status.id);
|
||||
|
||||
return normalizeStatus(status).withMutations(status => {
|
||||
fixQuote(status, oldStatus);
|
||||
|
@ -154,6 +158,25 @@ const deletePendingStatus = (state: State, { in_reply_to_id }: APIEntity) => {
|
|||
}
|
||||
};
|
||||
|
||||
/** Simulate favourite/unfavourite of status for optimistic interactions */
|
||||
const simulateFavourite = (
|
||||
state: State,
|
||||
statusId: string,
|
||||
favourited: boolean,
|
||||
): State => {
|
||||
const status = state.get(statusId);
|
||||
if (!status) return state;
|
||||
|
||||
const delta = favourited ? +1 : -1;
|
||||
|
||||
const updatedStatus = status.merge({
|
||||
favourited,
|
||||
favourites_count: Math.max(0, status.favourites_count + delta),
|
||||
});
|
||||
|
||||
return state.set(statusId, updatedStatus);
|
||||
};
|
||||
|
||||
const initialState: State = ImmutableMap();
|
||||
|
||||
export default function statuses(state = initialState, action: AnyAction): State {
|
||||
|
@ -167,15 +190,9 @@ export default function statuses(state = initialState, action: AnyAction): State
|
|||
case STATUS_CREATE_FAIL:
|
||||
return deletePendingStatus(state, action.params);
|
||||
case FAVOURITE_REQUEST:
|
||||
return state.update(action.status.get('id'), status =>
|
||||
status
|
||||
.set('favourited', true)
|
||||
.update('favourites_count', count => count + 1));
|
||||
return simulateFavourite(state, action.status.id, true);
|
||||
case UNFAVOURITE_REQUEST:
|
||||
return state.update(action.status.get('id'), status =>
|
||||
status
|
||||
.set('favourited', false)
|
||||
.update('favourites_count', count => Math.max(0, count - 1)));
|
||||
return simulateFavourite(state, action.status.id, false);
|
||||
case EMOJI_REACT_REQUEST:
|
||||
return state
|
||||
.updateIn(
|
||||
|
|
47
app/soapbox/types/entities.ts
Normal file
47
app/soapbox/types/entities.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import {
|
||||
AccountRecord,
|
||||
AttachmentRecord,
|
||||
CardRecord,
|
||||
EmojiRecord,
|
||||
FieldRecord,
|
||||
InstanceRecord,
|
||||
MentionRecord,
|
||||
NotificationRecord,
|
||||
PollRecord,
|
||||
PollOptionRecord,
|
||||
StatusRecord,
|
||||
} from 'soapbox/normalizers';
|
||||
|
||||
import type { Record as ImmutableRecord } from 'immutable';
|
||||
|
||||
type Account = ReturnType<typeof AccountRecord>;
|
||||
type Attachment = ReturnType<typeof AttachmentRecord>;
|
||||
type Card = ReturnType<typeof CardRecord>;
|
||||
type Emoji = ReturnType<typeof EmojiRecord>;
|
||||
type Field = ReturnType<typeof FieldRecord>;
|
||||
type Instance = ReturnType<typeof InstanceRecord>;
|
||||
type Mention = ReturnType<typeof MentionRecord>;
|
||||
type Notification = ReturnType<typeof NotificationRecord>;
|
||||
type Poll = ReturnType<typeof PollRecord>;
|
||||
type PollOption = ReturnType<typeof PollOptionRecord>;
|
||||
type Status = ReturnType<typeof StatusRecord>;
|
||||
|
||||
// Utility types
|
||||
type EmbeddedEntity<T extends object> = null | string | ReturnType<ImmutableRecord.Factory<T>>;
|
||||
|
||||
export {
|
||||
Account,
|
||||
Attachment,
|
||||
Card,
|
||||
Emoji,
|
||||
Field,
|
||||
Instance,
|
||||
Mention,
|
||||
Notification,
|
||||
Poll,
|
||||
PollOption,
|
||||
Status,
|
||||
|
||||
// Utility types
|
||||
EmbeddedEntity,
|
||||
};
|
|
@ -1,10 +0,0 @@
|
|||
/**
|
||||
* Account entity.
|
||||
* https://docs.joinmastodon.org/entities/account/
|
||||
**/
|
||||
|
||||
import { AccountRecord } from 'soapbox/normalizers';
|
||||
|
||||
type Account = ReturnType<typeof AccountRecord>
|
||||
|
||||
export default Account;
|
|
@ -1,2 +0,0 @@
|
|||
export { default as Account } from './account';
|
||||
export { default as Status } from './status';
|
|
@ -1,10 +0,0 @@
|
|||
/**
|
||||
* Status entity.
|
||||
* https://docs.joinmastodon.org/entities/status/
|
||||
**/
|
||||
|
||||
import { StatusRecord } from 'soapbox/normalizers';
|
||||
|
||||
type Status = ReturnType<typeof StatusRecord>
|
||||
|
||||
export default Status;
|
|
@ -1,7 +0,0 @@
|
|||
// Use new value only if old value is undefined
|
||||
export const mergeDefined = (oldVal, newVal) => oldVal === undefined ? newVal : oldVal;
|
||||
|
||||
export const makeEmojiMap = emojis => emojis.reduce((obj, emoji) => {
|
||||
obj[`:${emoji.shortcode}:`] = emoji;
|
||||
return obj;
|
||||
}, {});
|
12
app/soapbox/utils/normalizers.ts
Normal file
12
app/soapbox/utils/normalizers.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
// Use new value only if old value is undefined
|
||||
export const mergeDefined = (oldVal: any, newVal: any) => oldVal === undefined ? newVal : oldVal;
|
||||
|
||||
export const makeEmojiMap = (emojis: any) => emojis.reduce((obj: any, emoji: any) => {
|
||||
obj[`:${emoji.shortcode}:`] = emoji;
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
/** Normalize entity ID */
|
||||
export const normalizeId = (id: any): string | null => {
|
||||
return typeof id === 'string' ? id : null;
|
||||
};
|
|
@ -2,14 +2,7 @@
|
|||
"compilerOptions": {
|
||||
"baseUrl": "app/",
|
||||
"sourceMap": true,
|
||||
"alwaysStrict": true,
|
||||
"strictNullChecks": false,
|
||||
"strictBindCallApply": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitThis": true,
|
||||
"useUnknownInCatchVariables": true,
|
||||
"strict": true,
|
||||
"module": "es6",
|
||||
"target": "es5",
|
||||
"jsx": "react",
|
||||
|
|
Loading…
Reference in a new issue