diff --git a/app/soapbox/__fixtures__/pleroma-status-with-poll.json b/app/soapbox/__fixtures__/pleroma-status-with-poll.json new file mode 100644 index 000000000..452a5acb7 --- /dev/null +++ b/app/soapbox/__fixtures__/pleroma-status-with-poll.json @@ -0,0 +1,201 @@ +{ + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "followers_count": 2465, + "following_count": 1581, + "fqn": "alex@gleasonator.com", + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "last_status_at": "2022-03-10T18:19:50", + "locked": false, + "note": "I create Fediverse software that empowers people online.

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [ + "https://mitra.social/users/alex" + ], + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "birthday": "1993-07-03", + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": true, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 23648, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": null, + "bookmarked": false, + "card": null, + "content": "

What is tolerance?

", + "created_at": "2020-03-23T19:33:06.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 49, + "id": "103874034847713213", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [], + "muted": false, + "pinned": true, + "pleroma": { + "content": { + "text/plain": "What is tolerance?" + }, + "conversation_id": "3023268", + "direct_conversation_id": null, + "emoji_reactions": [ + { + "count": 3, + "me": false, + "name": "❤️" + } + ], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "parent_visible": false, + "pinned_at": "2021-11-23T01:38:44.000Z", + "quote": null, + "quote_url": null, + "quote_visible": false, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": { + "emojis": [], + "expired": true, + "expires_at": "2020-03-24T19:33:06.000Z", + "id": "4930", + "multiple": false, + "options": [ + { + "title": "Banning, censoring, and deplatforming anyone you disagree with", + "votes_count": 2 + }, + { + "title": "Promoting free speech, even for people and ideas you dislike", + "votes_count": 36 + } + ], + "voters_count": 2, + "votes_count": 38 + }, + "reblog": null, + "reblogged": false, + "reblogs_count": 27, + "replies_count": 15, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/users/alex/statuses/103874034847713213", + "url": "https://gleasonator.com/notice/103874034847713213", + "visibility": "public" +} diff --git a/app/soapbox/normalizers/__tests__/status-test.js b/app/soapbox/normalizers/__tests__/status-test.js index 26428e3ba..54117e167 100644 --- a/app/soapbox/normalizers/__tests__/status-test.js +++ b/app/soapbox/normalizers/__tests__/status-test.js @@ -1,4 +1,4 @@ -import { fromJS } from 'immutable'; +import { Record as ImmutableRecord, fromJS } from 'immutable'; import { normalizeStatus } from '../status'; @@ -7,12 +7,13 @@ describe('normalizeStatus', () => { const status = fromJS({}); const result = normalizeStatus(status); - expect(result.get('emojis')).toEqual(fromJS([])); - expect(result.get('favourites_count')).toBe(0); - expect(result.get('mentions')).toEqual(fromJS([])); - expect(result.get('reblog')).toBe(null); - expect(result.get('uri')).toBe(''); - expect(result.get('visibility')).toBe('public'); + expect(ImmutableRecord.isRecord(result)).toBe(true); + expect(result.emojis).toEqual(fromJS([])); + expect(result.favourites_count).toBe(0); + expect(result.mentions).toEqual(fromJS([])); + expect(result.reblog).toBe(null); + expect(result.uri).toBe(''); + expect(result.visibility).toBe('public'); }); it('fixes the order of mentions', () => { @@ -88,22 +89,22 @@ describe('normalizeStatus', () => { const result = normalizeStatus(status); - expect(result.get('media_attachments')).toEqual(expected); + expect(result.media_attachments).toEqual(expected); }); it('leaves Pleroma attachments alone', () => { const status = fromJS(require('soapbox/__fixtures__/pleroma-status-with-attachments.json')); const result = normalizeStatus(status); - expect(status.get('media_attachments')).toEqual(result.get('media_attachments')); + expect(status.get('media_attachments')).toEqual(result.media_attachments); }); it('normalizes Pleroma quote post', () => { const status = fromJS(require('soapbox/__fixtures__/pleroma-quote-post.json')); const result = normalizeStatus(status); - expect(result.get('quote')).toEqual(status.getIn(['pleroma', 'quote'])); - expect(result.getIn(['pleroma', 'quote'])).toBe(undefined); + expect(result.quote).toEqual(status.getIn(['pleroma', 'quote'])); + expect(result.pleroma.get('quote')).toBe(undefined); }); it('normalizes GoToSocial status', () => { @@ -119,7 +120,7 @@ describe('normalizeStatus', () => { quote: null, }; - expect(result.toJS()).toMatchObject(missing); + expect(result).toMatchObject(missing); }); it('normalizes Friendica status', () => { @@ -132,7 +133,7 @@ describe('normalizeStatus', () => { quote: null, }; - expect(result.toJS()).toMatchObject(missing); + expect(result).toMatchObject(missing); }); it('normalizes poll and poll options', () => { @@ -146,9 +147,22 @@ describe('normalizeStatus', () => { multiple: false, voters_count: 0, votes_count: 0, + own_votes: [], + voted: false, }; - expect(result.get('poll').toJS()).toMatchObject(expected); - expect(result.getIn(['poll', 'expires_at']) instanceof Date).toBe(true); + expect(ImmutableRecord.isRecord(result.poll)).toBe(true); + expect(ImmutableRecord.isRecord(result.poll.options.get(0))).toBe(true); + expect(result.poll.toJS()).toMatchObject(expected); + expect(result.poll.expires_at instanceof Date).toBe(true); + }); + + it('normalizes a Pleroma logged-out poll', () => { + const status = fromJS(require('soapbox/__fixtures__/pleroma-status-with-poll.json')); + const result = normalizeStatus(status); + + // Adds logged-in fields + expect(result.poll.voted).toBe(false); + expect(result.poll.own_votes).toEqual(fromJS([])); }); }); diff --git a/app/soapbox/normalizers/status.ts b/app/soapbox/normalizers/status.ts index 02c9b1f54..71210c502 100644 --- a/app/soapbox/normalizers/status.ts +++ b/app/soapbox/normalizers/status.ts @@ -47,16 +47,22 @@ const StatusRecord = ImmutableRecord({ spoilerHtml: '', }); -const basePollOption = ImmutableMap({ title: '', votes_count: 0 }); +const PollOptionRecord = ImmutableRecord({ + title: '', + votes_count: 0, +}); -const basePoll = ImmutableMap({ +// https://docs.joinmastodon.org/entities/poll/ +const PollRecord = ImmutableRecord({ emojis: ImmutableList(), expired: false, - expires_at: new Date(Date.now() + 1000 * (60 * 5)), // 5 minutes + expires_at: new Date(), multiple: false, options: ImmutableList(), voters_count: 0, votes_count: 0, + own_votes: ImmutableList(), + voted: false, }); // Ensure attachments have required fields @@ -100,23 +106,19 @@ const normalizeMentions = (status: ImmutableMap) => { }); }; -// Normalize poll option -const normalizePollOption = (option: ImmutableMap) => { - return option.mergeWith(mergeDefined, basePollOption); -}; - // Normalize poll const normalizePoll = (status: ImmutableMap) => { if (status.hasIn(['poll', 'options'])) { return status.update('poll', ImmutableMap(), poll => { - return poll.mergeWith(mergeDefined, basePoll).update('options', (options: ImmutableList>) => { - return options.map(normalizePollOption); + return PollRecord(poll).update('options', (options: ImmutableList>) => { + return options.map(PollOptionRecord); }); }); } else { return status.set('poll', null); } }; + // Fix order of mentions const fixMentionsOrder = (status: ImmutableMap) => { const mentions = status.get('mentions', ImmutableList()); diff --git a/app/soapbox/reducers/__tests__/polls-test.js b/app/soapbox/reducers/__tests__/polls-test.js index 55b53a8b5..c3ecd5152 100644 --- a/app/soapbox/reducers/__tests__/polls-test.js +++ b/app/soapbox/reducers/__tests__/polls-test.js @@ -1,9 +1,35 @@ import { Map as ImmutableMap } from 'immutable'; +import { POLLS_IMPORT } from 'soapbox/actions/importer'; + import reducer from '../polls'; describe('polls reducer', () => { it('should return the initial state', () => { expect(reducer(undefined, {})).toEqual(ImmutableMap()); }); + + describe('POLLS_IMPORT', () => { + it('normalizes the poll', () => { + const polls = [{ id: '3', options: [{ title: 'Apples' }] }]; + const action = { type: POLLS_IMPORT, polls }; + + const result = reducer(undefined, action); + + const expected = { + '3': { + options: [{ title: 'Apples', votes_count: 0 }], + emojis: [], + expired: false, + multiple: false, + voters_count: 0, + votes_count: 0, + own_votes: [], + voted: false, + }, + }; + + expect(result.toJS()).toMatchObject(expected); + }); + }); }); diff --git a/app/soapbox/reducers/polls.js b/app/soapbox/reducers/polls.js index 24c805457..f1a06fdc1 100644 --- a/app/soapbox/reducers/polls.js +++ b/app/soapbox/reducers/polls.js @@ -1,8 +1,20 @@ import { Map as ImmutableMap, fromJS } from 'immutable'; import { POLLS_IMPORT } from 'soapbox/actions/importer'; +import { normalizeStatus } from 'soapbox/normalizers/status'; -const importPolls = (state, polls) => state.withMutations(map => polls.forEach(poll => map.set(poll.id, fromJS(poll)))); +// HOTFIX: Convert the poll into a fake status to normalize it... +// TODO: get rid of POLLS_IMPORT and use STATUS_IMPORT here. +const normalizePoll = poll => { + const status = fromJS({ poll }); + return normalizeStatus(status).poll; +}; + +const importPolls = (state, polls) => { + return state.withMutations(map => { + return polls.forEach(poll => map.set(poll.id, normalizePoll(poll))); + }); +}; const initialState = ImmutableMap();