diff --git a/.eslintrc.js b/.eslintrc.js index b478bcc3f..f4b59595a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -40,9 +40,7 @@ module.exports = { react: { version: 'detect', }, - 'import/extensions': [ - '.js', - ], + 'import/extensions': ['.js', '.jsx', '.ts', '.tsx'], 'import/ignore': [ 'node_modules', '\\.(css|scss|json)$', @@ -257,9 +255,17 @@ module.exports = { overrides: [ { files: ['**/*.tsx'], - 'rules': { + rules: { 'react/prop-types': 'off', }, }, + // Disable no-undef in TypeScript + // https://stackoverflow.com/a/69155899 + { + files: ['*.ts', '*.tsx'], + rules: { + 'no-undef': 'off', + }, + }, ], }; diff --git a/app/soapbox/normalizers/__tests__/instance-test.js b/app/soapbox/normalizers/__tests__/instance-test.js index f30f27492..597fbbeee 100644 --- a/app/soapbox/normalizers/__tests__/instance-test.js +++ b/app/soapbox/normalizers/__tests__/instance-test.js @@ -8,13 +8,7 @@ describe('normalizeInstance()', () => { approval_required: false, contact_account: {}, configuration: { - media_attachments: { - image_size_limit: 10485760, - image_matrix_limit: 16777216, - video_size_limit: 41943040, - video_frame_rate_limit: 60, - video_matrix_limit: 2304000, - }, + media_attachments: {}, polls: { max_options: 4, max_characters_per_option: 25, @@ -48,7 +42,11 @@ describe('normalizeInstance()', () => { registrations: false, rules: [], short_description: '', - stats: {}, + stats: { + domain_count: 0, + status_count: 0, + user_count: 0, + }, title: '', thumbnail: '', uri: '', diff --git a/app/soapbox/normalizers/account.js b/app/soapbox/normalizers/account.ts similarity index 68% rename from app/soapbox/normalizers/account.js rename to app/soapbox/normalizers/account.ts index 9515e1217..f3006f3c2 100644 --- a/app/soapbox/normalizers/account.js +++ b/app/soapbox/normalizers/account.ts @@ -1,8 +1,13 @@ -import { Map as ImmutableMap, List as ImmutableList, Record } from 'immutable'; +import { + Map as ImmutableMap, + List as ImmutableList, + Record as ImmutableRecord, +} from 'immutable'; +import { IAccount } from 'soapbox/types'; import { mergeDefined } from 'soapbox/utils/normalizers'; -const AccountRecord = Record({ +const AccountRecord = ImmutableRecord({ acct: '', avatar: '', avatar_static: '', @@ -41,8 +46,8 @@ const AccountRecord = Record({ }); // https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/549 -const normalizePleromaLegacyFields = account => { - return account.update('pleroma', ImmutableMap(), pleroma => { +const normalizePleromaLegacyFields = (account: ImmutableMap) => { + return account.update('pleroma', ImmutableMap(), (pleroma: ImmutableMap) => { return pleroma.withMutations(pleroma => { const legacy = ImmutableMap({ is_active: !pleroma.get('deactivated'), @@ -57,7 +62,7 @@ const normalizePleromaLegacyFields = account => { }; // Normalize Pleroma/Fedibird birthday -const normalizeBirthday = account => { +const normalizeBirthday = (account: ImmutableMap) => { const birthday = [ account.getIn(['pleroma', 'birthday']), account.getIn(['other_settings', 'birthday']), @@ -66,18 +71,24 @@ const normalizeBirthday = account => { return account.set('birthday', birthday); }; +// Get Pleroma tags +const getTags = (account: ImmutableMap): ImmutableList => { + const tags = account.getIn(['pleroma', 'tags']); + return ImmutableList(ImmutableList.isList(tags) ? tags : []); +}; + // Normalize Truth Social/Pleroma verified -const normalizeVerified = account => { +const normalizeVerified = (account: ImmutableMap) => { return account.update('verified', verified => { return [ verified === true, - account.getIn(['pleroma', 'tags'], ImmutableList()).includes('verified'), + getTags(account).includes('verified'), ].some(Boolean); }); }; // Normalize Fedibird/Truth Social location -const normalizeLocation = account => { +const normalizeLocation = (account: ImmutableMap) => { return account.update('location', location => { return [ location, @@ -86,7 +97,7 @@ const normalizeLocation = account => { }); }; -export const normalizeAccount = account => { +export const normalizeAccount = (account: ImmutableMap): IAccount => { return AccountRecord( account.withMutations(account => { normalizePleromaLegacyFields(account); diff --git a/app/soapbox/normalizers/instance.js b/app/soapbox/normalizers/instance.js index 22f90fc8d..8ec6fde85 100644 --- a/app/soapbox/normalizers/instance.js +++ b/app/soapbox/normalizers/instance.js @@ -1,21 +1,19 @@ -import { Map as ImmutableMap, List as ImmutableList, Record } from 'immutable'; +import { + Map as ImmutableMap, + List as ImmutableList, + Record as ImmutableRecord, +} from 'immutable'; import { parseVersion, PLEROMA } from 'soapbox/utils/features'; import { mergeDefined } from 'soapbox/utils/normalizers'; import { isNumber } from 'soapbox/utils/numbers'; // Use Mastodon defaults -const InstanceRecord = Record({ +const InstanceRecord = ImmutableRecord({ approval_required: false, contact_account: ImmutableMap(), configuration: ImmutableMap({ - media_attachments: ImmutableMap({ - image_size_limit: 10485760, - image_matrix_limit: 16777216, - video_size_limit: 41943040, - video_frame_rate_limit: 60, - video_matrix_limit: 2304000, - }), + media_attachments: ImmutableMap(), polls: ImmutableMap({ max_options: 4, max_characters_per_option: 25, @@ -49,7 +47,11 @@ const InstanceRecord = Record({ registrations: false, rules: ImmutableList(), short_description: '', - stats: ImmutableMap(), + stats: ImmutableMap({ + domain_count: 0, + status_count: 0, + user_count: 0, + }), title: '', thumbnail: '', uri: '', diff --git a/app/soapbox/normalizers/status.js b/app/soapbox/normalizers/status.ts similarity index 78% rename from app/soapbox/normalizers/status.js rename to app/soapbox/normalizers/status.ts index 494a0ced7..02c9b1f54 100644 --- a/app/soapbox/normalizers/status.js +++ b/app/soapbox/normalizers/status.ts @@ -1,9 +1,14 @@ -import { Map as ImmutableMap, List as ImmutableList, Record } from 'immutable'; +import { + Map as ImmutableMap, + List as ImmutableList, + Record as ImmutableRecord, +} from 'immutable'; +import { IStatus } from 'soapbox/types'; import { accountToMention } from 'soapbox/utils/accounts'; import { mergeDefined } from 'soapbox/utils/normalizers'; -const StatusRecord = Record({ +const StatusRecord = ImmutableRecord({ account: ImmutableMap(), application: null, bookmarked: false, @@ -56,7 +61,7 @@ const basePoll = ImmutableMap({ // Ensure attachments have required fields // https://docs.joinmastodon.org/entities/attachment/ -const normalizeAttachment = attachment => { +const normalizeAttachment = (attachment: ImmutableMap) => { const url = [ attachment.get('url'), attachment.get('preview_url'), @@ -72,14 +77,14 @@ const normalizeAttachment = attachment => { return attachment.mergeWith(mergeDefined, base); }; -const normalizeAttachments = status => { +const normalizeAttachments = (status: ImmutableMap) => { return status.update('media_attachments', ImmutableList(), attachments => { return attachments.map(normalizeAttachment); }); }; // Normalize mentions -const normalizeMention = mention => { +const normalizeMention = (mention: ImmutableMap) => { const base = ImmutableMap({ acct: '', username: (mention.get('acct') || '').split('@')[0], @@ -89,22 +94,22 @@ const normalizeMention = mention => { return mention.mergeWith(mergeDefined, base); }; -const normalizeMentions = status => { +const normalizeMentions = (status: ImmutableMap) => { return status.update('mentions', ImmutableList(), mentions => { return mentions.map(normalizeMention); }); }; // Normalize poll option -const normalizePollOption = option => { +const normalizePollOption = (option: ImmutableMap) => { return option.mergeWith(mergeDefined, basePollOption); }; // Normalize poll -const normalizePoll = status => { +const normalizePoll = (status: ImmutableMap) => { if (status.hasIn(['poll', 'options'])) { return status.update('poll', ImmutableMap(), poll => { - return poll.mergeWith(mergeDefined, basePoll).update('options', options => { + return poll.mergeWith(mergeDefined, basePoll).update('options', (options: ImmutableList>) => { return options.map(normalizePollOption); }); }); @@ -113,12 +118,12 @@ const normalizePoll = status => { } }; // Fix order of mentions -const fixMentionsOrder = status => { +const fixMentionsOrder = (status: ImmutableMap) => { const mentions = status.get('mentions', ImmutableList()); const inReplyToAccountId = status.get('in_reply_to_account_id'); // Sort the replied-to mention to the top - const sorted = mentions.sort((a, b) => { + const sorted = mentions.sort((a: ImmutableMap, _b: ImmutableMap) => { if (a.get('id') === inReplyToAccountId) { return -1; } else { @@ -130,7 +135,7 @@ const fixMentionsOrder = status => { }; // Add self to mentions if it's a reply to self -const addSelfMention = status => { +const addSelfMention = (status: ImmutableMap) => { const accountId = status.getIn(['account', 'id']); const isSelfReply = accountId === status.get('in_reply_to_account_id'); @@ -147,14 +152,14 @@ const addSelfMention = status => { }; // Move the quote to the top-level -const fixQuote = status => { +const fixQuote = (status: ImmutableMap) => { return status.withMutations(status => { status.update('quote', quote => quote || status.getIn(['pleroma', 'quote']) || null); status.deleteIn(['pleroma', 'quote']); }); }; -export const normalizeStatus = status => { +export const normalizeStatus = (status: ImmutableMap): IStatus => { return StatusRecord( status.withMutations(status => { normalizeAttachments(status); diff --git a/app/soapbox/reducers/__tests__/index-test.js b/app/soapbox/reducers/__tests__/index-test.js index 2b4a21266..15572f337 100644 --- a/app/soapbox/reducers/__tests__/index-test.js +++ b/app/soapbox/reducers/__tests__/index-test.js @@ -7,6 +7,6 @@ describe('root reducer', () => { const result = reducer(undefined, {}); expect(ImmutableRecord.isRecord(result)).toBe(true); expect(result.accounts.get('')).toBe(undefined); - expect(result.instance.get('version')).toEqual('0.0.0'); + expect(result.instance.version).toEqual('0.0.0'); }); }); diff --git a/app/soapbox/types/account.ts b/app/soapbox/types/account.ts new file mode 100644 index 000000000..b17935c35 --- /dev/null +++ b/app/soapbox/types/account.ts @@ -0,0 +1,44 @@ +/** + * Account entity. + * https://docs.joinmastodon.org/entities/account/ + **/ + +interface IAccount { + acct: string; + avatar: string; + avatar_static: string; + birthday: Date | undefined; + bot: boolean; + created_at: Date; + display_name: string; + emojis: Iterable; + fields: Iterable; + followers_count: number; + following_count: number; + fqn: string; + header: string; + header_static: string; + id: string; + last_status_at: Date; + location: string; + locked: boolean; + moved: null; + note: string; + pleroma: Record; + source: Record; + statuses_count: number; + uri: string; + url: string; + username: string; + verified: boolean; + + // Internal fields + display_name_html: string; + note_emojified: string; + note_plain: string; + patron: Record; + relationship: Iterable; + should_refetch: boolean; +} + +export { IAccount }; diff --git a/app/soapbox/types/index.ts b/app/soapbox/types/index.ts new file mode 100644 index 000000000..df14c2720 --- /dev/null +++ b/app/soapbox/types/index.ts @@ -0,0 +1,4 @@ +import { IAccount } from './account'; +import { IStatus } from './status'; + +export { IAccount, IStatus }; diff --git a/app/soapbox/types/status.ts b/app/soapbox/types/status.ts new file mode 100644 index 000000000..a1a1ed061 --- /dev/null +++ b/app/soapbox/types/status.ts @@ -0,0 +1,45 @@ +/** + * Status entity. + * https://docs.joinmastodon.org/entities/status/ + **/ + +interface IStatus { + account: Record; + application: Record | null; + bookmarked: boolean; + card: Record | null; + content: string; + created_at: Date; + emojis: Iterable; + favourited: boolean; + favourites_count: number; + in_reply_to_account_id: string | null; + in_reply_to_id: string | null; + id: string; + language: null; + media_attachments: Iterable; + mentions: Iterable; + muted: boolean; + pinned: boolean; + pleroma: Record; + poll: null; + quote: null; + reblog: null; + reblogged: boolean; + reblogs_count: number; + replies_count: number; + sensitive: boolean; + spoiler_text: string; + tags: Iterable; + uri: string; + url: string; + visibility: string; + + // Internal fields + contentHtml: string; + hidden: boolean; + search_index: string; + spoilerHtml: string; +} + +export { IStatus };