= ({ index, ...props }) => {
// callup to set styles whenever we're active
React.useLayoutEffect(() => {
if (isSelected) {
+ // @ts-ignore
setActiveRect(rect);
}
}, [isSelected, rect, setActiveRect]);
return (
+ // @ts-ignore
);
};
@@ -115,6 +125,7 @@ const Tabs = ({ items, activeItem }: ITabs) => {
key={name}
as='button'
role='button'
+ // @ts-ignore
title={title}
index={idx}
>
diff --git a/app/soapbox/components/ui/text/text.tsx b/app/soapbox/components/ui/text/text.tsx
index b7815c6a3..c1b8f4c25 100644
--- a/app/soapbox/components/ui/text/text.tsx
+++ b/app/soapbox/components/ui/text/text.tsx
@@ -83,11 +83,13 @@ const Text: React.FC
= React.forwardRef(
const Comp: React.ElementType = tag;
+ const alignmentClass = typeof align === 'string' ? alignments[align] : '';
+
return (
= 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)}
/>
);
},
diff --git a/app/soapbox/features/compose/components/search.tsx b/app/soapbox/features/compose/components/search.tsx
index 791aa0dbf..695163f3c 100644
--- a/app/soapbox/features/compose/components/search.tsx
+++ b/app/soapbox/features/compose/components/search.tsx
@@ -90,7 +90,7 @@ const Search = (props: ISearch) => {
handleSubmit();
} else if (event.key === 'Escape') {
- document.querySelector('.ui').parentElement.focus();
+ document.querySelector('.ui')?.parentElement?.focus();
}
};
diff --git a/app/soapbox/features/ui/components/profile-dropdown.tsx b/app/soapbox/features/ui/components/profile-dropdown.tsx
index 7ff98ea1a..0887afe61 100644
--- a/app/soapbox/features/ui/components/profile-dropdown.tsx
+++ b/app/soapbox/features/ui/components/profile-dropdown.tsx
@@ -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
diff --git a/app/soapbox/hooks/useOwnAccount.ts b/app/soapbox/hooks/useOwnAccount.ts
index 6a5937f0b..ea541dfee 100644
--- a/app/soapbox/hooks/useOwnAccount.ts
+++ b/app/soapbox/hooks/useOwnAccount.ts
@@ -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??
diff --git a/app/soapbox/normalizers/account.ts b/app/soapbox/normalizers/account.ts
index 41dd1dd0f..333e151a8 100644
--- a/app/soapbox/normalizers/account.ts
+++ b/app/soapbox/normalizers/account.ts
@@ -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(),
+ fields: ImmutableList(),
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 | null,
note: '',
- pleroma: ImmutableMap(),
- source: ImmutableMap(),
+ pleroma: ImmutableMap(),
+ source: ImmutableMap(),
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(),
+ relationship: ImmutableList>(),
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: '',
diff --git a/app/soapbox/normalizers/instance.ts b/app/soapbox/normalizers/instance.ts
index a33601bf9..047e6a1e4 100644
--- a/app/soapbox/normalizers/instance.ts
+++ b/app/soapbox/normalizers/instance.ts
@@ -82,7 +82,7 @@ const pleromaToMastodonConfig = (instance: ImmutableMap) => {
};
// 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) => {
diff --git a/app/soapbox/normalizers/poll.ts b/app/soapbox/normalizers/poll.ts
index 592f9aa65..d2b1bf492 100644
--- a/app/soapbox/normalizers/poll.ts
+++ b/app/soapbox/normalizers/poll.ts
@@ -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(),
expired: false,
expires_at: new Date(),
id: '',
multiple: false,
- options: ImmutableList(),
+ options: ImmutableList(),
voters_count: 0,
votes_count: 0,
- own_votes: null,
+ own_votes: null as ImmutableList | null,
voted: false,
});
diff --git a/app/soapbox/normalizers/status.ts b/app/soapbox/normalizers/status.ts
index 21cdb42bd..13b68fdbb 100644
--- a/app/soapbox/normalizers/status.ts
+++ b/app/soapbox/normalizers/status.ts
@@ -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,
+ application: null as ImmutableMap | null,
bookmarked: false,
- card: null,
+ card: null as EmbeddedEntity,
content: '',
created_at: new Date(),
- emojis: ImmutableList(),
+ emojis: ImmutableList(),
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(),
+ mentions: ImmutableList(),
muted: false,
pinned: false,
- pleroma: ImmutableMap(),
- poll: null,
- quote: null,
- reblog: null,
+ pleroma: ImmutableMap(),
+ poll: null as EmbeddedEntity,
+ quote: null as EmbeddedEntity,
+ reblog: null as EmbeddedEntity,
reblogged: false,
reblogs_count: 0,
replies_count: 0,
sensitive: false,
spoiler_text: '',
- tags: ImmutableList(),
+ tags: ImmutableList>(),
uri: '',
url: '',
- visibility: 'public',
+ visibility: 'public' as StatusVisibility,
// Internal fields
contentHtml: '',
diff --git a/app/soapbox/reducers/accounts.ts b/app/soapbox/reducers/accounts.ts
index bb9458cf1..444d7ac97 100644
--- a/app/soapbox/reducers/accounts.ts
+++ b/app/soapbox/reducers/accounts.ts
@@ -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): St
const importAdminUsers = (state: State, adminUsers: Array>): State => {
return state.withMutations((state: State) => {
- fromJS(adminUsers).forEach(adminUser => {
- importAdminUser(state, ImmutableMap(adminUser));
+ adminUsers.forEach(adminUser => {
+ importAdminUser(state, ImmutableMap(fromJS(adminUser)));
});
});
};
diff --git a/app/soapbox/reducers/statuses.ts b/app/soapbox/reducers/statuses.ts
index c115a9089..617d0c5f4 100644
--- a/app/soapbox/reducers/statuses.ts
+++ b/app/soapbox/reducers/statuses.ts
@@ -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;
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 => {
- return status.poll?.options.map(({ title }: { title: string }) => title);
+const getPollOptionTitles = ({ poll }: StatusRecord): ImmutableList => {
+ 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(
diff --git a/app/soapbox/types/entities.ts b/app/soapbox/types/entities.ts
new file mode 100644
index 000000000..65a5bf057
--- /dev/null
+++ b/app/soapbox/types/entities.ts
@@ -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;
+type Attachment = ReturnType;
+type Card = ReturnType;
+type Emoji = ReturnType;
+type Field = ReturnType;
+type Instance = ReturnType;
+type Mention = ReturnType;
+type Notification = ReturnType;
+type Poll = ReturnType;
+type PollOption = ReturnType;
+type Status = ReturnType;
+
+// Utility types
+type EmbeddedEntity = null | string | ReturnType>;
+
+export {
+ Account,
+ Attachment,
+ Card,
+ Emoji,
+ Field,
+ Instance,
+ Mention,
+ Notification,
+ Poll,
+ PollOption,
+ Status,
+
+ // Utility types
+ EmbeddedEntity,
+};
diff --git a/app/soapbox/types/entities/account.ts b/app/soapbox/types/entities/account.ts
deleted file mode 100644
index eeb06dd90..000000000
--- a/app/soapbox/types/entities/account.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
- * Account entity.
- * https://docs.joinmastodon.org/entities/account/
- **/
-
-import { AccountRecord } from 'soapbox/normalizers';
-
-type Account = ReturnType
-
-export default Account;
diff --git a/app/soapbox/types/entities/index.ts b/app/soapbox/types/entities/index.ts
deleted file mode 100644
index 2bfc0daf8..000000000
--- a/app/soapbox/types/entities/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default as Account } from './account';
-export { default as Status } from './status';
diff --git a/app/soapbox/types/entities/status.ts b/app/soapbox/types/entities/status.ts
deleted file mode 100644
index 3c7f66c8b..000000000
--- a/app/soapbox/types/entities/status.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
- * Status entity.
- * https://docs.joinmastodon.org/entities/status/
- **/
-
-import { StatusRecord } from 'soapbox/normalizers';
-
-type Status = ReturnType
-
-export default Status;
diff --git a/app/soapbox/utils/normalizers.js b/app/soapbox/utils/normalizers.js
deleted file mode 100644
index d16b2a07c..000000000
--- a/app/soapbox/utils/normalizers.js
+++ /dev/null
@@ -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;
-}, {});
diff --git a/app/soapbox/utils/normalizers.ts b/app/soapbox/utils/normalizers.ts
new file mode 100644
index 000000000..a74ca582d
--- /dev/null
+++ b/app/soapbox/utils/normalizers.ts
@@ -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;
+};
diff --git a/tsconfig.json b/tsconfig.json
index 8412a8997..6af5fd813 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -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",