diff --git a/app/images/avatar-missing.png b/app/images/avatar-missing.png new file mode 100644 index 000000000..b3e6b5709 Binary files /dev/null and b/app/images/avatar-missing.png differ diff --git a/app/images/avatar-missing.svg b/app/images/avatar-missing.svg new file mode 100644 index 000000000..7eb156089 --- /dev/null +++ b/app/images/avatar-missing.svg @@ -0,0 +1,116 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/app/soapbox/__fixtures__/mitra-context.json b/app/soapbox/__fixtures__/mitra-context.json new file mode 100644 index 000000000..91b48420c --- /dev/null +++ b/app/soapbox/__fixtures__/mitra-context.json @@ -0,0 +1,107 @@ +[ + { + "id": "017ed503-bc96-301a-e871-2c23b30ddd05", + "uri": "https://mitra.social/objects/017ed503-bc96-301a-e871-2c23b30ddd05", + "created_at": "2022-02-07T16:28:18.966874Z", + "account": { + "id": "017ed4f9-c121-2ae6-0805-15516cce02c3", + "username": "alex", + "acct": "alex", + "url": "https://mitra.social/users/alex", + "display_name": null, + "created_at": "2022-02-07T16:17:24.769229Z", + "note": null, + "avatar": null, + "header": null, + "fields": [], + "followers_count": 1, + "following_count": 1, + "statuses_count": 3, + "source": null, + "wallet_address": null + }, + "content": "@silverpill sup!", + "in_reply_to_id": null, + "reblog": null, + "visibility": "public", + "replies_count": 1, + "favourites_count": 0, + "reblogs_count": 0, + "media_attachments": [], + "mentions": [ + { + "id": "dd4ebc18-269d-4c7b-a310-03d29c6ab551", + "username": "silverpill", + "acct": "silverpill", + "url": "https://mitra.social/users/silverpill" + } + ], + "tags": [], + "favourited": false, + "reblogged": false, + "ipfs_cid": null, + "token_id": null, + "token_tx_id": null + }, + { + "id": "017ed505-5926-392f-256a-f86d5075df70", + "uri": "https://mitra.social/objects/017ed505-5926-392f-256a-f86d5075df70", + "created_at": "2022-02-07T16:30:04.582771Z", + "account": { + "id": "dd4ebc18-269d-4c7b-a310-03d29c6ab551", + "username": "silverpill", + "acct": "silverpill", + "url": "https://mitra.social/users/silverpill", + "display_name": "silverpill", + "created_at": "2021-11-06T21:08:57.441927Z", + "note": "Admin of mitra.social instance. It is running experimental ActivityPub server Mitra.", + "avatar": "https://mitra.social/media/6a785bf7dd05f61c3590e8935aa49156a499ac30fd1e402f79e7e164adb36e2c.png", + "header": null, + "fields": [ + { + "name": "Matrix", + "value": "@silverpill:poa.st" + }, + { + "name": "Alt", + "value": "@silverpill@poa.st" + }, + { + "name": "Code", + "value": "https://codeberg.org/silverpill/" + }, + { + "name": "$XMR", + "value": "884y9LmsWY7PQNsyR7bJy1dvj91tuF5spVabyCnPk4KfQtSuzFbQobTFC7xSemJgVW1FWAwnJbjTZX5zZWbBrfkv62DB62d" + } + ], + "followers_count": 27, + "following_count": 15, + "statuses_count": 110, + "source": null, + "wallet_address": null + }, + "content": "@alex welcome", + "in_reply_to_id": "017ed503-bc96-301a-e871-2c23b30ddd05", + "reblog": null, + "visibility": "public", + "replies_count": 0, + "favourites_count": 1, + "reblogs_count": 0, + "media_attachments": [], + "mentions": [ + { + "id": "017ed4f9-c121-2ae6-0805-15516cce02c3", + "username": "alex", + "acct": "alex", + "url": "https://mitra.social/users/alex" + } + ], + "tags": [], + "favourited": true, + "reblogged": false, + "ipfs_cid": null, + "token_id": null, + "token_tx_id": null + } +] diff --git a/app/soapbox/actions/__tests__/statuses-test.js b/app/soapbox/actions/__tests__/statuses-test.js new file mode 100644 index 000000000..71a0596a4 --- /dev/null +++ b/app/soapbox/actions/__tests__/statuses-test.js @@ -0,0 +1,29 @@ +import { Map as ImmutableMap } from 'immutable'; + +import { STATUSES_IMPORT } from 'soapbox/actions/importer'; +import { __stub } from 'soapbox/api'; +import { mockStore } from 'soapbox/test_helpers'; + +import { fetchContext } from '../statuses'; + +describe('fetchContext()', () => { + it('handles Mitra context', done => { + const statuses = require('soapbox/__fixtures__/mitra-context.json'); + + __stub(mock => { + mock.onGet('/api/v1/statuses/017ed505-5926-392f-256a-f86d5075df70/context') + .reply(200, statuses); + }); + + const store = mockStore(ImmutableMap()); + + store.dispatch(fetchContext('017ed505-5926-392f-256a-f86d5075df70')).then(context => { + const actions = store.getActions(); + + expect(actions[3].type).toEqual(STATUSES_IMPORT); + expect(actions[3].statuses[0].id).toEqual('017ed503-bc96-301a-e871-2c23b30ddd05'); + + done(); + }).catch(console.error); + }); +}); diff --git a/app/soapbox/actions/importer/normalizer.js b/app/soapbox/actions/importer/normalizer.js index 66524b556..7c5aa8bd3 100644 --- a/app/soapbox/actions/importer/normalizer.js +++ b/app/soapbox/actions/importer/normalizer.js @@ -15,6 +15,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; @@ -41,6 +48,10 @@ export function normalizeAccount(account) { export function normalizeStatus(status, normalOldStatus, expandSpoilers) { const normalStatus = { ...status }; + // Some backends can return null, or omit these required fields + if (!normalStatus.emojis) normalStatus.emojis = []; + if (!normalStatus.spoiler_text) normalStatus.spoiler_text = ''; + // Copy the pleroma object too, so we can modify our copy if (status.pleroma) { normalStatus.pleroma = { ...status.pleroma }; diff --git a/app/soapbox/actions/statuses.js b/app/soapbox/actions/statuses.js index 150b6d8c7..12dfaa576 100644 --- a/app/soapbox/actions/statuses.js +++ b/app/soapbox/actions/statuses.js @@ -143,10 +143,18 @@ export function fetchContext(id) { dispatch({ type: CONTEXT_FETCH_REQUEST, id }); return api(getState).get(`/api/v1/statuses/${id}/context`).then(({ data: context }) => { - const { ancestors, descendants } = context; - const statuses = ancestors.concat(descendants); - dispatch(importFetchedStatuses(statuses)); - dispatch({ type: CONTEXT_FETCH_SUCCESS, id, ancestors, descendants }); + if (Array.isArray(context)) { + // Mitra: returns a list of statuses + dispatch(importFetchedStatuses(context)); + } else if (typeof context === 'object') { + // Standard Mastodon API returns a map with `ancestors` and `descendants` + const { ancestors, descendants } = context; + const statuses = ancestors.concat(descendants); + dispatch(importFetchedStatuses(statuses)); + dispatch({ type: CONTEXT_FETCH_SUCCESS, id, ancestors, descendants }); + } else { + throw context; + } return context; }).catch(error => { if (error.response && error.response.status === 404) { diff --git a/app/soapbox/components/avatar.js b/app/soapbox/components/avatar.js index d0df7959b..1bbca72cc 100644 --- a/app/soapbox/components/avatar.js +++ b/app/soapbox/components/avatar.js @@ -28,22 +28,14 @@ export default class Avatar extends React.PureComponent { height: `${size}px`, }; - // Only render the image if src is provided - if (account.get('avatar')) { - return ( - - ); - } else { - // Fall back on rendering an empty div - return ( -
- ); - } + return ( + + ); } } diff --git a/app/soapbox/components/media_gallery.js b/app/soapbox/components/media_gallery.js index bad4c1b48..34c79c140 100644 --- a/app/soapbox/components/media_gallery.js +++ b/app/soapbox/components/media_gallery.js @@ -168,7 +168,7 @@ class Item extends React.PureComponent { onClick={this.handleClick} target='_blank' > - + ); } else if (attachment.get('type') === 'gifv') { diff --git a/app/soapbox/features/account_gallery/components/media_item.js b/app/soapbox/features/account_gallery/components/media_item.js index 1cccfe070..0c5b0afdb 100644 --- a/app/soapbox/features/account_gallery/components/media_item.js +++ b/app/soapbox/features/account_gallery/components/media_item.js @@ -88,7 +88,7 @@ class MediaItem extends ImmutablePureComponent { thumbnail = ( diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index bfcfdb3cd..1e0ed2d51 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -670,14 +670,12 @@ class UI extends React.PureComponent { } render() { - const { streamingUrl, features, soapbox } = this.props; + const { features, soapbox } = this.props; const { draggingOver, mobile } = this.state; const { intl, children, location, dropdownMenuIsOpen, me } = this.props; // Wait for login to succeed or fail if (me === null) return null; - // If login didn't fail, wait for streaming to become available - if (me !== false && !streamingUrl) return null; const handlers = me ? { help: this.handleHotkeyToggleHelp,