From 1c36d1b91cb8b0d19520c01cbaad4b3483f66189 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 8 Mar 2022 22:02:02 -0600 Subject: [PATCH 1/6] Store statuses as StatusRecord --- app/soapbox/normalizers/status.js | 36 ++++++++++++++++--------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/app/soapbox/normalizers/status.js b/app/soapbox/normalizers/status.js index ab6ce6312..fc790d846 100644 --- a/app/soapbox/normalizers/status.js +++ b/app/soapbox/normalizers/status.js @@ -1,10 +1,10 @@ -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList, Record } from 'immutable'; import { accountToMention } from 'soapbox/utils/accounts'; import { mergeDefined } from 'soapbox/utils/normalizers'; -// Some backends can return null, or omit these required fields -const baseStatus = ImmutableMap({ +const StatusRecord = Record({ + account: ImmutableMap(), application: null, bookmarked: false, card: null, @@ -14,14 +14,20 @@ const baseStatus = ImmutableMap({ favourites_count: 0, in_reply_to_account_id: null, in_reply_to_id: null, + id: '', language: null, + media_attachments: ImmutableList(), mentions: ImmutableList(), muted: false, pinned: false, + pleroma: ImmutableMap(), + poll: null, + quote: null, reblog: null, reblogged: false, reblogs_count: 0, replies_count: 0, + sensitive: false, spoiler_text: '', tags: ImmutableList(), uri: '', @@ -41,11 +47,6 @@ const basePoll = ImmutableMap({ votes_count: 0, }); -// Merge base status -const mergeBase = status => { - return status.mergeDeepWith(mergeDefined, baseStatus); -}; - // Ensure attachments have required fields // https://docs.joinmastodon.org/entities/attachment/ const normalizeAttachment = attachment => { @@ -147,13 +148,14 @@ const fixQuote = status => { }; export const normalizeStatus = status => { - return status.withMutations(status => { - mergeBase(status); - normalizeAttachments(status); - normalizeMentions(status); - normalizePoll(status); - fixMentionsOrder(status); - addSelfMention(status); - fixQuote(status); - }); + return StatusRecord( + status.withMutations(status => { + normalizeAttachments(status); + normalizeMentions(status); + normalizePoll(status); + fixMentionsOrder(status); + addSelfMention(status); + fixQuote(status); + }), + ); }; From 7a18f8b9c8a46b8b1f2f01a97a3f0d707898a716 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 8 Mar 2022 23:25:30 -0600 Subject: [PATCH 2/6] Create Records for Account and Status --- app/soapbox/actions/importer/normalizer.js | 7 -- .../normalizers/__tests__/instance-test.js | 53 +++++++++---- app/soapbox/normalizers/account.js | 52 +++++++++++-- app/soapbox/normalizers/instance.js | 75 ++++++++++++++----- app/soapbox/normalizers/status.js | 6 ++ .../reducers/__tests__/instance-test.js | 22 +++--- app/soapbox/reducers/accounts.js | 20 +---- 7 files changed, 160 insertions(+), 75 deletions(-) diff --git a/app/soapbox/actions/importer/normalizer.js b/app/soapbox/actions/importer/normalizer.js index 4de148c0b..f2e82c33d 100644 --- a/app/soapbox/actions/importer/normalizer.js +++ b/app/soapbox/actions/importer/normalizer.js @@ -11,13 +11,6 @@ const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => { export function normalizeAccount(account) { account = { ...account }; - // Some backends can return null, or omit these required fields - if (!account.emojis) account.emojis = []; - if (!account.display_name) account.display_name = ''; - if (!account.note) account.note = ''; - if (!account.avatar) account.avatar = account.avatar_static || require('images/avatar-missing.png'); - if (!account.avatar_static) account.avatar_static = account.avatar; - const emojiMap = makeEmojiMap(account); const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name; diff --git a/app/soapbox/normalizers/__tests__/instance-test.js b/app/soapbox/normalizers/__tests__/instance-test.js index 8f992bb84..30b7aa3d2 100644 --- a/app/soapbox/normalizers/__tests__/instance-test.js +++ b/app/soapbox/normalizers/__tests__/instance-test.js @@ -4,25 +4,48 @@ import { normalizeInstance } from '../instance'; describe('normalizeInstance()', () => { it('normalizes an empty Map', () => { - const expected = ImmutableMap({ - description_limit: 1500, - configuration: ImmutableMap({ - statuses: ImmutableMap({ - max_characters: 500, - max_media_attachments: 4, - }), - polls: ImmutableMap({ + const expected = { + 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, + }, + polls: { max_options: 4, max_characters_per_option: 25, min_expiration: 300, max_expiration: 2629746, - }), - }), + }, + statuses: { + max_characters: 500, + max_media_attachments: 4, + }, + }, + description: '', + description_limit: 1500, + email: '', + fedibird_capabilities: [], + invites_enabled: false, + languages: [], + pleroma: {}, + registrations: false, + rules: [], + short_description: '', + stats: {}, + title: '', + thumbnail: '', + uri: '', + urls: {}, version: '0.0.0', - }); + }; const result = normalizeInstance(ImmutableMap()); - expect(result).toEqual(expected); + expect(result.toJS()).toEqual(expected); }); it('normalizes Pleroma instance with Mastodon configuration format', () => { @@ -104,10 +127,10 @@ describe('normalizeInstance()', () => { const result = normalizeInstance(instance); // Sets description_limit - expect(result.get('description_limit')).toEqual(1500); + expect(result.description_limit).toEqual(1500); - // But otherwise, it's the same - expect(result.delete('description_limit')).toEqual(instance); + // Preserves fedibird_capabilities + expect(result.fedibird_capabilities).toEqual(instance.get('fedibird_capabilities')); }); it('normalizes Mitra instance', () => { diff --git a/app/soapbox/normalizers/account.js b/app/soapbox/normalizers/account.js index 3b4a7dc02..a2a9289b4 100644 --- a/app/soapbox/normalizers/account.js +++ b/app/soapbox/normalizers/account.js @@ -1,7 +1,43 @@ -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList, Record } from 'immutable'; import { mergeDefined } from 'soapbox/utils/normalizers'; +const AccountRecord = Record({ + acct: '', + avatar: '', + avatar_static: '', + birthday: undefined, + bot: false, + created_at: new Date(), + display_name: '', + emojis: ImmutableList(), + fields: ImmutableList(), + followers_count: 0, + following_count: 0, + fqn: '', + header: '', + header_static: '', + id: '', + last_status_at: new Date(), + location: '', + locked: false, + moved: null, + note: '', + pleroma: ImmutableMap(), + source: ImmutableMap(), + statuses_count: 0, + uri: '', + url: '', + username: '', + verified: false, + + // Internal fields + display_name_html: '', + note_emojified: '', + note_plain: '', + should_refetch: false, +}); + // https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/549 const normalizePleromaLegacyFields = account => { return account.update('pleroma', ImmutableMap(), pleroma => { @@ -49,10 +85,12 @@ const normalizeLocation = account => { }; export const normalizeAccount = account => { - return account.withMutations(account => { - normalizePleromaLegacyFields(account); - normalizeVerified(account); - normalizeBirthday(account); - normalizeLocation(account); - }); + return AccountRecord( + account.withMutations(account => { + normalizePleromaLegacyFields(account); + normalizeVerified(account); + normalizeBirthday(account); + normalizeLocation(account); + }), + ); }; diff --git a/app/soapbox/normalizers/instance.js b/app/soapbox/normalizers/instance.js index 155f6c8ae..22f90fc8d 100644 --- a/app/soapbox/normalizers/instance.js +++ b/app/soapbox/normalizers/instance.js @@ -1,16 +1,20 @@ -import { Map as ImmutableMap } from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList, Record } 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 baseInstance = ImmutableMap({ - description_limit: 1500, +const InstanceRecord = Record({ + approval_required: false, + contact_account: ImmutableMap(), configuration: ImmutableMap({ - statuses: ImmutableMap({ - max_characters: 500, - max_media_attachments: 4, + media_attachments: ImmutableMap({ + image_size_limit: 10485760, + image_matrix_limit: 16777216, + video_size_limit: 41943040, + video_frame_rate_limit: 60, + video_matrix_limit: 2304000, }), polls: ImmutableMap({ max_options: 4, @@ -18,7 +22,38 @@ const baseInstance = ImmutableMap({ min_expiration: 300, max_expiration: 2629746, }), + statuses: ImmutableMap({ + max_characters: 500, + max_media_attachments: 4, + }), }), + description: '', + description_limit: 1500, + email: '', + fedibird_capabilities: ImmutableList(), + invites_enabled: false, + languages: ImmutableList(), + pleroma: ImmutableMap({ + metadata: ImmutableMap({ + account_activation_required: false, + birthday_min_age: 0, + birthday_required: false, + features: ImmutableList(), + federation: ImmutableMap({ + enabled: true, + exclusions: false, + }), + }), + stats: ImmutableMap(), + }), + registrations: false, + rules: ImmutableList(), + short_description: '', + stats: ImmutableMap(), + title: '', + thumbnail: '', + uri: '', + urls: ImmutableMap(), version: '0.0.0', }); @@ -45,19 +80,21 @@ export const normalizeInstance = instance => { const { software } = parseVersion(instance.get('version')); const mastodonConfig = pleromaToMastodonConfig(instance); - return instance.withMutations(instance => { - // Merge configuration - instance.update('configuration', ImmutableMap(), configuration => ( - configuration.mergeDeepWith(mergeDefined, mastodonConfig) - )); + return InstanceRecord( + instance.withMutations(instance => { + // Merge configuration + instance.update('configuration', ImmutableMap(), configuration => ( + configuration.mergeDeepWith(mergeDefined, mastodonConfig) + )); - // If max attachments isn't set, check the backend software - instance.updateIn(['configuration', 'statuses', 'max_media_attachments'], value => { - return isNumber(value) ? value : getAttachmentLimit(software); - }); + // If max attachments isn't set, check the backend software + instance.updateIn(['configuration', 'statuses', 'max_media_attachments'], value => { + return isNumber(value) ? value : getAttachmentLimit(software); + }); - // Merge defaults & cleanup - instance.mergeDeepWith(mergeDefined, baseInstance); - instance.deleteAll(['max_toot_chars', 'poll_limits']); - }); + // Merge defaults & cleanup + instance.mergeDeepWith(mergeDefined, InstanceRecord()); + instance.deleteAll(['max_toot_chars', 'poll_limits']); + }), + ); }; diff --git a/app/soapbox/normalizers/status.js b/app/soapbox/normalizers/status.js index fc790d846..8087f1d85 100644 --- a/app/soapbox/normalizers/status.js +++ b/app/soapbox/normalizers/status.js @@ -33,6 +33,12 @@ const StatusRecord = Record({ uri: '', url: '', visibility: 'public', + + // Internal fields + contentHtml: '', + hidden: false, + search_index: '', + spoilerHtml: '', }); const basePollOption = ImmutableMap({ title: '', votes_count: 0 }); diff --git a/app/soapbox/reducers/__tests__/instance-test.js b/app/soapbox/reducers/__tests__/instance-test.js index 17eae2e52..dcac9e29a 100644 --- a/app/soapbox/reducers/__tests__/instance-test.js +++ b/app/soapbox/reducers/__tests__/instance-test.js @@ -1,27 +1,29 @@ -import { Map as ImmutableMap } from 'immutable'; - import { INSTANCE_REMEMBER_SUCCESS } from 'soapbox/actions/instance'; import reducer from '../instance'; describe('instance reducer', () => { it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual(ImmutableMap({ + const result = reducer(undefined, {}); + + const expected = { description_limit: 1500, - configuration: ImmutableMap({ - statuses: ImmutableMap({ + configuration: { + statuses: { max_characters: 500, max_media_attachments: 4, - }), - polls: ImmutableMap({ + }, + polls: { max_options: 4, max_characters_per_option: 25, min_expiration: 300, max_expiration: 2629746, - }), - }), + }, + }, version: '0.0.0', - })); + }; + + expect(result.toJS()).toMatchObject(expected); }); describe('INSTANCE_REMEMBER_SUCCESS', () => { diff --git a/app/soapbox/reducers/accounts.js b/app/soapbox/reducers/accounts.js index c3cbcb9d6..3e573244d 100644 --- a/app/soapbox/reducers/accounts.js +++ b/app/soapbox/reducers/accounts.js @@ -40,17 +40,8 @@ import { const initialState = ImmutableMap(); -const minifyAccount = account => { - return account.deleteAll([ - 'followers_count', - 'following_count', - 'statuses_count', - 'source', - ]); -}; - const fixAccount = (state, account) => { - const normalized = minifyAccount(normalizeAccount(fromJS(account))); + const normalized = normalizeAccount(fromJS(account)); return state.set(account.id, normalized); }; @@ -125,20 +116,15 @@ const removePermission = (state, accountIds, permissionGroup) => { }); }; -const buildAccount = adminUser => fromJS({ +const buildAccount = adminUser => normalizeAccount(fromJS({ id: adminUser.get('id'), username: adminUser.get('nickname').split('@')[0], acct: adminUser.get('nickname'), display_name: adminUser.get('display_name'), display_name_html: adminUser.get('display_name'), - note: '', url: adminUser.get('url'), avatar: adminUser.get('avatar'), avatar_static: adminUser.get('avatar'), - header: '', - header_static: '', - emojis: [], - fields: [], created_at: adminUser.get('created_at'), pleroma: { is_active: adminUser.get('is_active'), @@ -153,7 +139,7 @@ const buildAccount = adminUser => fromJS({ }, }, should_refetch: true, -}); +})); const mergeAdminUser = (account, adminUser) => { return account.withMutations(account => { From 10116a312a8e63163fff794fa5844d158fac1cff Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 8 Mar 2022 23:47:30 -0600 Subject: [PATCH 3/6] Normalizers: fix tests --- app/soapbox/actions/importer/normalizer.js | 7 +++++++ app/soapbox/normalizers/__tests__/instance-test.js | 14 +++++++++++++- app/soapbox/normalizers/status.js | 1 + app/soapbox/reducers/statuses.js | 3 +-- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/app/soapbox/actions/importer/normalizer.js b/app/soapbox/actions/importer/normalizer.js index f2e82c33d..4de148c0b 100644 --- a/app/soapbox/actions/importer/normalizer.js +++ b/app/soapbox/actions/importer/normalizer.js @@ -11,6 +11,13 @@ const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => { export function normalizeAccount(account) { account = { ...account }; + // Some backends can return null, or omit these required fields + if (!account.emojis) account.emojis = []; + if (!account.display_name) account.display_name = ''; + if (!account.note) account.note = ''; + if (!account.avatar) account.avatar = account.avatar_static || require('images/avatar-missing.png'); + if (!account.avatar_static) account.avatar_static = account.avatar; + const emojiMap = makeEmojiMap(account); const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name; diff --git a/app/soapbox/normalizers/__tests__/instance-test.js b/app/soapbox/normalizers/__tests__/instance-test.js index 30b7aa3d2..f30f27492 100644 --- a/app/soapbox/normalizers/__tests__/instance-test.js +++ b/app/soapbox/normalizers/__tests__/instance-test.js @@ -32,7 +32,19 @@ describe('normalizeInstance()', () => { fedibird_capabilities: [], invites_enabled: false, languages: [], - pleroma: {}, + pleroma: { + metadata: { + account_activation_required: false, + birthday_min_age: 0, + birthday_required: false, + features: [], + federation: { + enabled: true, + exclusions: false, + }, + }, + stats: {}, + }, registrations: false, rules: [], short_description: '', diff --git a/app/soapbox/normalizers/status.js b/app/soapbox/normalizers/status.js index 8087f1d85..494a0ced7 100644 --- a/app/soapbox/normalizers/status.js +++ b/app/soapbox/normalizers/status.js @@ -8,6 +8,7 @@ const StatusRecord = Record({ application: null, bookmarked: false, card: null, + content: '', created_at: new Date(), emojis: ImmutableList(), favourited: false, diff --git a/app/soapbox/reducers/statuses.js b/app/soapbox/reducers/statuses.js index 2f2b0ac7f..a7fe3b479 100644 --- a/app/soapbox/reducers/statuses.js +++ b/app/soapbox/reducers/statuses.js @@ -89,8 +89,7 @@ const fixQuote = (status, oldStatus) => { const fixStatus = (state, status, expandSpoilers) => { const oldStatus = state.get(status.get('id')); - return status.withMutations(status => { - normalizeStatus(status); + return normalizeStatus(status).withMutations(status => { fixQuote(status, oldStatus); calculateStatus(status, oldStatus, expandSpoilers); minifyStatus(status); From 831741bea5de05fececc3170431066c64ae93014 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 8 Mar 2022 23:59:59 -0600 Subject: [PATCH 4/6] Test that reducers parse as Records --- app/soapbox/reducers/__tests__/accounts-test.js | 14 +++++++++++++- app/soapbox/reducers/__tests__/instance-test.js | 3 +++ app/soapbox/reducers/__tests__/statuses-test.js | 10 +++++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/app/soapbox/reducers/__tests__/accounts-test.js b/app/soapbox/reducers/__tests__/accounts-test.js index f70695927..973ac4f1e 100644 --- a/app/soapbox/reducers/__tests__/accounts-test.js +++ b/app/soapbox/reducers/__tests__/accounts-test.js @@ -1,4 +1,6 @@ -import { Map as ImmutableMap } from 'immutable'; +import { Map as ImmutableMap, Record } from 'immutable'; + +import { ACCOUNT_IMPORT } from 'soapbox/actions/importer'; import reducer from '../accounts'; // import * as actions from 'soapbox/actions/importer'; @@ -10,6 +12,16 @@ describe('accounts reducer', () => { expect(reducer(undefined, {})).toEqual(ImmutableMap()); }); + describe('ACCOUNT_IMPORT', () => { + it('parses the account as a Record', () => { + const account = require('soapbox/__fixtures__/pleroma-account.json'); + const action = { type: ACCOUNT_IMPORT, account }; + const result = reducer(undefined, action).get('9v5bmRalQvjOy0ECcC'); + + expect(Record.isRecord(result)).toBe(true); + }); + }); + // fails to add normalized accounts to state // it('should handle ACCOUNT_IMPORT', () => { // const state = ImmutableMap({ }); diff --git a/app/soapbox/reducers/__tests__/instance-test.js b/app/soapbox/reducers/__tests__/instance-test.js index dcac9e29a..06eb3091b 100644 --- a/app/soapbox/reducers/__tests__/instance-test.js +++ b/app/soapbox/reducers/__tests__/instance-test.js @@ -1,3 +1,5 @@ +import { Record } from 'immutable'; + import { INSTANCE_REMEMBER_SUCCESS } from 'soapbox/actions/instance'; import reducer from '../instance'; @@ -23,6 +25,7 @@ describe('instance reducer', () => { version: '0.0.0', }; + expect(Record.isRecord(result)).toBe(true); expect(result.toJS()).toMatchObject(expected); }); diff --git a/app/soapbox/reducers/__tests__/statuses-test.js b/app/soapbox/reducers/__tests__/statuses-test.js index 3a81f2004..3ea21ab64 100644 --- a/app/soapbox/reducers/__tests__/statuses-test.js +++ b/app/soapbox/reducers/__tests__/statuses-test.js @@ -1,4 +1,4 @@ -import { Map as ImmutableMap, fromJS } from 'immutable'; +import { Map as ImmutableMap, Record, fromJS } from 'immutable'; import { STATUS_IMPORT } from 'soapbox/actions/importer'; import { @@ -14,6 +14,14 @@ describe('statuses reducer', () => { }); describe('STATUS_IMPORT', () => { + it('parses the status as a Record', () => { + const status = require('soapbox/__fixtures__/pleroma-quote-post.json'); + const action = { type: STATUS_IMPORT, status }; + const result = reducer(undefined, action).get('AFmFMSpITT9xcOJKcK'); + + expect(Record.isRecord(result)).toBe(true); + }); + it('fixes the order of mentions', () => { const status = require('soapbox/__fixtures__/status-unordered-mentions.json'); const action = { type: STATUS_IMPORT, status }; From 4e254928febe99b565a7e9fe982ab7c7cc285147 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 9 Mar 2022 00:12:27 -0600 Subject: [PATCH 5/6] EditProfile: convert to Map before mutations --- app/soapbox/features/edit_profile/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/features/edit_profile/index.js b/app/soapbox/features/edit_profile/index.js index 300d965a7..6fb9a0320 100644 --- a/app/soapbox/features/edit_profile/index.js +++ b/app/soapbox/features/edit_profile/index.js @@ -116,7 +116,7 @@ class EditProfile extends ImmutablePureComponent { const birthday = account.get('birthday'); const showBirthday = account.getIn(['source', 'pleroma', 'show_birthday']); - const initialState = account.withMutations(map => { + const initialState = ImmutableMap(account).withMutations(map => { map.merge(map.get('source')); map.delete('source'); map.set('fields', normalizeFields(map.get('fields'), Math.min(maxFields, 4))); From 38fbd703e4b411e77aea7ca939479d7edbf471e7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 9 Mar 2022 00:38:28 -0600 Subject: [PATCH 6/6] Fix account relationships --- app/soapbox/normalizers/account.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/soapbox/normalizers/account.js b/app/soapbox/normalizers/account.js index a2a9289b4..9515e1217 100644 --- a/app/soapbox/normalizers/account.js +++ b/app/soapbox/normalizers/account.js @@ -35,6 +35,8 @@ const AccountRecord = Record({ display_name_html: '', note_emojified: '', note_plain: '', + patron: ImmutableMap(), + relationship: ImmutableList(), should_refetch: false, });