diff --git a/app/soapbox/__fixtures__/context_1.json b/app/soapbox/__fixtures__/context_1.json deleted file mode 100644 index 2e37a5502..000000000 --- a/app/soapbox/__fixtures__/context_1.json +++ /dev/null @@ -1,739 +0,0 @@ -{ - "ancestors": [ - { - "account": { - "acct": "alex", - "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "bot": false, - "created_at": "2020-01-08T01:25:43.000Z", - "display_name": "Alex Gleason", - "emojis": [], - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "follow_requests_count": 0, - "followers_count": 725, - "following_count": 1211, - "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", - "locked": false, - "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", - "pleroma": { - "accepts_chat_messages": true, - "allow_following_move": true, - "ap_id": "https://gleasonator.com/users/alex", - "background_image": null, - "confirmation_pending": false, - "deactivated": false, - "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_moderator": false, - "notification_settings": { - "block_from_strangers": false, - "hide_notification_contents": false - }, - "relationship": {}, - "skip_thread_containment": false, - "tags": [], - "unread_conversation_count": 95, - "unread_notifications_count": 0 - }, - "source": { - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", - "pleroma": { - "actor_type": "Person", - "discoverable": false, - "no_rich_text": false, - "show_role": true - }, - "privacy": "public", - "sensitive": false - }, - "statuses_count": 9157, - "url": "https://gleasonator.com/users/alex", - "username": "alex" - }, - "application": { - "name": "Web", - "website": null - }, - "bookmarked": false, - "card": null, - "content": "

A

", - "created_at": "2020-09-18T20:07:10.000Z", - "emojis": [], - "favourited": false, - "favourites_count": 0, - "id": "9zIH6kDXA10YqhMKqO", - "in_reply_to_account_id": null, - "in_reply_to_id": null, - "language": null, - "media_attachments": [], - "mentions": [], - "muted": false, - "pinned": false, - "pleroma": { - "content": { - "text/plain": "A" - }, - "conversation_id": 5089485, - "direct_conversation_id": null, - "emoji_reactions": [], - "expires_at": null, - "in_reply_to_account_acct": null, - "local": true, - "parent_visible": false, - "spoiler_text": { - "text/plain": "" - }, - "thread_muted": false - }, - "poll": null, - "reblog": null, - "reblogged": false, - "reblogs_count": 0, - "replies_count": 0, - "sensitive": false, - "spoiler_text": "", - "tags": [], - "text": null, - "uri": "https://gleasonator.com/objects/9995c074-2ff6-4a01-b596-7ef6971ed5d2", - "url": "https://gleasonator.com/notice/9zIH6kDXA10YqhMKqO", - "visibility": "direct" - }, - { - "account": { - "acct": "alex", - "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "bot": false, - "created_at": "2020-01-08T01:25:43.000Z", - "display_name": "Alex Gleason", - "emojis": [], - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "follow_requests_count": 0, - "followers_count": 725, - "following_count": 1211, - "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", - "locked": false, - "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", - "pleroma": { - "accepts_chat_messages": true, - "allow_following_move": true, - "ap_id": "https://gleasonator.com/users/alex", - "background_image": null, - "confirmation_pending": false, - "deactivated": false, - "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_moderator": false, - "notification_settings": { - "block_from_strangers": false, - "hide_notification_contents": false - }, - "relationship": {}, - "skip_thread_containment": false, - "tags": [], - "unread_conversation_count": 95, - "unread_notifications_count": 0 - }, - "source": { - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", - "pleroma": { - "actor_type": "Person", - "discoverable": false, - "no_rich_text": false, - "show_role": true - }, - "privacy": "public", - "sensitive": false - }, - "statuses_count": 9157, - "url": "https://gleasonator.com/users/alex", - "username": "alex" - }, - "application": { - "name": "Web", - "website": null - }, - "bookmarked": false, - "card": null, - "content": "

B

", - "created_at": "2020-09-18T20:07:18.000Z", - "emojis": [], - "favourited": false, - "favourites_count": 0, - "id": "9zIH7PUdhK3Ircg4hM", - "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", - "in_reply_to_id": "9zIH6kDXA10YqhMKqO", - "language": null, - "media_attachments": [], - "mentions": [ - { - "acct": "alex", - "id": "9v5bmRalQvjOy0ECcC", - "url": "https://gleasonator.com/users/alex", - "username": "alex" - } - ], - "muted": false, - "pinned": false, - "pleroma": { - "content": { - "text/plain": "B" - }, - "conversation_id": 5089485, - "direct_conversation_id": null, - "emoji_reactions": [], - "expires_at": null, - "in_reply_to_account_acct": "alex", - "local": true, - "parent_visible": true, - "spoiler_text": { - "text/plain": "" - }, - "thread_muted": false - }, - "poll": null, - "reblog": null, - "reblogged": false, - "reblogs_count": 0, - "replies_count": 0, - "sensitive": false, - "spoiler_text": "", - "tags": [], - "text": null, - "uri": "https://gleasonator.com/objects/992ca99a-425d-46eb-b094-60412e9fb141", - "url": "https://gleasonator.com/notice/9zIH7PUdhK3Ircg4hM", - "visibility": "direct" - }, - { - "account": { - "acct": "alex", - "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "bot": false, - "created_at": "2020-01-08T01:25:43.000Z", - "display_name": "Alex Gleason", - "emojis": [], - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "follow_requests_count": 0, - "followers_count": 725, - "following_count": 1211, - "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", - "locked": false, - "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", - "pleroma": { - "accepts_chat_messages": true, - "allow_following_move": true, - "ap_id": "https://gleasonator.com/users/alex", - "background_image": null, - "confirmation_pending": false, - "deactivated": false, - "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_moderator": false, - "notification_settings": { - "block_from_strangers": false, - "hide_notification_contents": false - }, - "relationship": {}, - "skip_thread_containment": false, - "tags": [], - "unread_conversation_count": 95, - "unread_notifications_count": 0 - }, - "source": { - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", - "pleroma": { - "actor_type": "Person", - "discoverable": false, - "no_rich_text": false, - "show_role": true - }, - "privacy": "public", - "sensitive": false - }, - "statuses_count": 9157, - "url": "https://gleasonator.com/users/alex", - "username": "alex" - }, - "application": { - "name": "Web", - "website": null - }, - "bookmarked": false, - "card": null, - "content": "

C

", - "created_at": "2020-09-18T20:07:22.000Z", - "emojis": [], - "favourited": false, - "favourites_count": 0, - "id": "9zIH7mMGgc1RmJwDLM", - "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", - "in_reply_to_id": "9zIH6kDXA10YqhMKqO", - "language": null, - "media_attachments": [], - "mentions": [ - { - "acct": "alex", - "id": "9v5bmRalQvjOy0ECcC", - "url": "https://gleasonator.com/users/alex", - "username": "alex" - } - ], - "muted": false, - "pinned": false, - "pleroma": { - "content": { - "text/plain": "C" - }, - "conversation_id": 5089485, - "direct_conversation_id": null, - "emoji_reactions": [], - "expires_at": null, - "in_reply_to_account_acct": "alex", - "local": true, - "parent_visible": true, - "spoiler_text": { - "text/plain": "" - }, - "thread_muted": false - }, - "poll": null, - "reblog": null, - "reblogged": false, - "reblogs_count": 0, - "replies_count": 0, - "sensitive": false, - "spoiler_text": "", - "tags": [], - "text": null, - "uri": "https://gleasonator.com/objects/a2c25ef5-a40e-4098-b07e-b468989ef749", - "url": "https://gleasonator.com/notice/9zIH7mMGgc1RmJwDLM", - "visibility": "direct" - } - ], - "descendants": [ - { - "account": { - "acct": "alex", - "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "bot": false, - "created_at": "2020-01-08T01:25:43.000Z", - "display_name": "Alex Gleason", - "emojis": [], - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "follow_requests_count": 0, - "followers_count": 725, - "following_count": 1211, - "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", - "locked": false, - "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", - "pleroma": { - "accepts_chat_messages": true, - "allow_following_move": true, - "ap_id": "https://gleasonator.com/users/alex", - "background_image": null, - "confirmation_pending": false, - "deactivated": false, - "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_moderator": false, - "notification_settings": { - "block_from_strangers": false, - "hide_notification_contents": false - }, - "relationship": {}, - "skip_thread_containment": false, - "tags": [], - "unread_conversation_count": 95, - "unread_notifications_count": 0 - }, - "source": { - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", - "pleroma": { - "actor_type": "Person", - "discoverable": false, - "no_rich_text": false, - "show_role": true - }, - "privacy": "public", - "sensitive": false - }, - "statuses_count": 9157, - "url": "https://gleasonator.com/users/alex", - "username": "alex" - }, - "application": { - "name": "Web", - "website": null - }, - "bookmarked": false, - "card": null, - "content": "

E

", - "created_at": "2020-09-18T20:07:38.000Z", - "emojis": [], - "favourited": false, - "favourites_count": 0, - "id": "9zIH9GTCDWEFSRt2um", - "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", - "in_reply_to_id": "9zIH7PUdhK3Ircg4hM", - "language": null, - "media_attachments": [], - "mentions": [ - { - "acct": "alex", - "id": "9v5bmRalQvjOy0ECcC", - "url": "https://gleasonator.com/users/alex", - "username": "alex" - } - ], - "muted": false, - "pinned": false, - "pleroma": { - "content": { - "text/plain": "E" - }, - "conversation_id": 5089485, - "direct_conversation_id": null, - "emoji_reactions": [], - "expires_at": null, - "in_reply_to_account_acct": "alex", - "local": true, - "parent_visible": true, - "spoiler_text": { - "text/plain": "" - }, - "thread_muted": false - }, - "poll": null, - "reblog": null, - "reblogged": false, - "reblogs_count": 0, - "replies_count": 0, - "sensitive": false, - "spoiler_text": "", - "tags": [], - "text": null, - "uri": "https://gleasonator.com/objects/a1e45493-2158-4f11-88ca-ba621429dbe5", - "url": "https://gleasonator.com/notice/9zIH9GTCDWEFSRt2um", - "visibility": "direct" - }, - { - "account": { - "acct": "alex", - "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "bot": false, - "created_at": "2020-01-08T01:25:43.000Z", - "display_name": "Alex Gleason", - "emojis": [], - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "follow_requests_count": 0, - "followers_count": 725, - "following_count": 1211, - "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", - "locked": false, - "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", - "pleroma": { - "accepts_chat_messages": true, - "allow_following_move": true, - "ap_id": "https://gleasonator.com/users/alex", - "background_image": null, - "confirmation_pending": false, - "deactivated": false, - "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_moderator": false, - "notification_settings": { - "block_from_strangers": false, - "hide_notification_contents": false - }, - "relationship": {}, - "skip_thread_containment": false, - "tags": [], - "unread_conversation_count": 95, - "unread_notifications_count": 0 - }, - "source": { - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", - "pleroma": { - "actor_type": "Person", - "discoverable": false, - "no_rich_text": false, - "show_role": true - }, - "privacy": "public", - "sensitive": false - }, - "statuses_count": 9157, - "url": "https://gleasonator.com/users/alex", - "username": "alex" - }, - "application": { - "name": "Web", - "website": null - }, - "bookmarked": false, - "card": null, - "content": "

F

", - "created_at": "2020-09-18T20:07:42.000Z", - "emojis": [], - "favourited": false, - "favourites_count": 0, - "id": "9zIH9fhaP9atiJoOJc", - "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", - "in_reply_to_id": "9zIH8WYwtnUx4yDzUm", - "language": null, - "media_attachments": [], - "mentions": [ - { - "acct": "alex", - "id": "9v5bmRalQvjOy0ECcC", - "url": "https://gleasonator.com/users/alex", - "username": "alex" - } - ], - "muted": false, - "pinned": false, - "pleroma": { - "content": { - "text/plain": "F" - }, - "conversation_id": 5089485, - "direct_conversation_id": null, - "emoji_reactions": [], - "expires_at": null, - "in_reply_to_account_acct": "alex", - "local": true, - "parent_visible": true, - "spoiler_text": { - "text/plain": "" - }, - "thread_muted": false - }, - "poll": null, - "reblog": null, - "reblogged": false, - "reblogs_count": 0, - "replies_count": 0, - "sensitive": false, - "spoiler_text": "", - "tags": [], - "text": null, - "uri": "https://gleasonator.com/objects/ee661cf9-35d4-4e84-88ff-13b5950f7556", - "url": "https://gleasonator.com/notice/9zIH9fhaP9atiJoOJc", - "visibility": "direct" - } - ] -} diff --git a/app/soapbox/__fixtures__/context_2.json b/app/soapbox/__fixtures__/context_2.json deleted file mode 100644 index c5cf2a813..000000000 --- a/app/soapbox/__fixtures__/context_2.json +++ /dev/null @@ -1,739 +0,0 @@ -{ - "ancestors": [ - { - "account": { - "acct": "alex", - "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "bot": false, - "created_at": "2020-01-08T01:25:43.000Z", - "display_name": "Alex Gleason", - "emojis": [], - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "follow_requests_count": 0, - "followers_count": 725, - "following_count": 1211, - "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", - "locked": false, - "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", - "pleroma": { - "accepts_chat_messages": true, - "allow_following_move": true, - "ap_id": "https://gleasonator.com/users/alex", - "background_image": null, - "confirmation_pending": false, - "deactivated": false, - "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_moderator": false, - "notification_settings": { - "block_from_strangers": false, - "hide_notification_contents": false - }, - "relationship": {}, - "skip_thread_containment": false, - "tags": [], - "unread_conversation_count": 95, - "unread_notifications_count": 0 - }, - "source": { - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", - "pleroma": { - "actor_type": "Person", - "discoverable": false, - "no_rich_text": false, - "show_role": true - }, - "privacy": "public", - "sensitive": false - }, - "statuses_count": 9157, - "url": "https://gleasonator.com/users/alex", - "username": "alex" - }, - "application": { - "name": "Web", - "website": null - }, - "bookmarked": false, - "card": null, - "content": "

A

", - "created_at": "2020-09-18T20:07:10.000Z", - "emojis": [], - "favourited": false, - "favourites_count": 0, - "id": "9zIH6kDXA10YqhMKqO", - "in_reply_to_account_id": null, - "in_reply_to_id": null, - "language": null, - "media_attachments": [], - "mentions": [], - "muted": false, - "pinned": false, - "pleroma": { - "content": { - "text/plain": "A" - }, - "conversation_id": 5089485, - "direct_conversation_id": null, - "emoji_reactions": [], - "expires_at": null, - "in_reply_to_account_acct": null, - "local": true, - "parent_visible": false, - "spoiler_text": { - "text/plain": "" - }, - "thread_muted": false - }, - "poll": null, - "reblog": null, - "reblogged": false, - "reblogs_count": 0, - "replies_count": 0, - "sensitive": false, - "spoiler_text": "", - "tags": [], - "text": null, - "uri": "https://gleasonator.com/objects/9995c074-2ff6-4a01-b596-7ef6971ed5d2", - "url": "https://gleasonator.com/notice/9zIH6kDXA10YqhMKqO", - "visibility": "direct" - } - ], - "descendants": [ - { - "account": { - "acct": "alex", - "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "bot": false, - "created_at": "2020-01-08T01:25:43.000Z", - "display_name": "Alex Gleason", - "emojis": [], - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "follow_requests_count": 0, - "followers_count": 725, - "following_count": 1211, - "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", - "locked": false, - "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", - "pleroma": { - "accepts_chat_messages": true, - "allow_following_move": true, - "ap_id": "https://gleasonator.com/users/alex", - "background_image": null, - "confirmation_pending": false, - "deactivated": false, - "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_moderator": false, - "notification_settings": { - "block_from_strangers": false, - "hide_notification_contents": false - }, - "relationship": {}, - "skip_thread_containment": false, - "tags": [], - "unread_conversation_count": 95, - "unread_notifications_count": 0 - }, - "source": { - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", - "pleroma": { - "actor_type": "Person", - "discoverable": false, - "no_rich_text": false, - "show_role": true - }, - "privacy": "public", - "sensitive": false - }, - "statuses_count": 9157, - "url": "https://gleasonator.com/users/alex", - "username": "alex" - }, - "application": { - "name": "Web", - "website": null - }, - "bookmarked": false, - "card": null, - "content": "

C

", - "created_at": "2020-09-18T20:07:22.000Z", - "emojis": [], - "favourited": false, - "favourites_count": 0, - "id": "9zIH7mMGgc1RmJwDLM", - "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", - "in_reply_to_id": "9zIH6kDXA10YqhMKqO", - "language": null, - "media_attachments": [], - "mentions": [ - { - "acct": "alex", - "id": "9v5bmRalQvjOy0ECcC", - "url": "https://gleasonator.com/users/alex", - "username": "alex" - } - ], - "muted": false, - "pinned": false, - "pleroma": { - "content": { - "text/plain": "C" - }, - "conversation_id": 5089485, - "direct_conversation_id": null, - "emoji_reactions": [], - "expires_at": null, - "in_reply_to_account_acct": "alex", - "local": true, - "parent_visible": true, - "spoiler_text": { - "text/plain": "" - }, - "thread_muted": false - }, - "poll": null, - "reblog": null, - "reblogged": false, - "reblogs_count": 0, - "replies_count": 0, - "sensitive": false, - "spoiler_text": "", - "tags": [], - "text": null, - "uri": "https://gleasonator.com/objects/a2c25ef5-a40e-4098-b07e-b468989ef749", - "url": "https://gleasonator.com/notice/9zIH7mMGgc1RmJwDLM", - "visibility": "direct" - }, - { - "account": { - "acct": "alex", - "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "bot": false, - "created_at": "2020-01-08T01:25:43.000Z", - "display_name": "Alex Gleason", - "emojis": [], - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "follow_requests_count": 0, - "followers_count": 725, - "following_count": 1211, - "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", - "locked": false, - "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", - "pleroma": { - "accepts_chat_messages": true, - "allow_following_move": true, - "ap_id": "https://gleasonator.com/users/alex", - "background_image": null, - "confirmation_pending": false, - "deactivated": false, - "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_moderator": false, - "notification_settings": { - "block_from_strangers": false, - "hide_notification_contents": false - }, - "relationship": {}, - "skip_thread_containment": false, - "tags": [], - "unread_conversation_count": 95, - "unread_notifications_count": 0 - }, - "source": { - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", - "pleroma": { - "actor_type": "Person", - "discoverable": false, - "no_rich_text": false, - "show_role": true - }, - "privacy": "public", - "sensitive": false - }, - "statuses_count": 9157, - "url": "https://gleasonator.com/users/alex", - "username": "alex" - }, - "application": { - "name": "Web", - "website": null - }, - "bookmarked": false, - "card": null, - "content": "

D

", - "created_at": "2020-09-18T20:07:30.000Z", - "emojis": [], - "favourited": false, - "favourites_count": 0, - "id": "9zIH8WYwtnUx4yDzUm", - "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", - "in_reply_to_id": "9zIH7PUdhK3Ircg4hM", - "language": null, - "media_attachments": [], - "mentions": [ - { - "acct": "alex", - "id": "9v5bmRalQvjOy0ECcC", - "url": "https://gleasonator.com/users/alex", - "username": "alex" - } - ], - "muted": false, - "pinned": false, - "pleroma": { - "content": { - "text/plain": "D" - }, - "conversation_id": 5089485, - "direct_conversation_id": null, - "emoji_reactions": [], - "expires_at": null, - "in_reply_to_account_acct": "alex", - "local": true, - "parent_visible": true, - "spoiler_text": { - "text/plain": "" - }, - "thread_muted": false - }, - "poll": null, - "reblog": null, - "reblogged": false, - "reblogs_count": 0, - "replies_count": 0, - "sensitive": false, - "spoiler_text": "", - "tags": [], - "text": null, - "uri": "https://gleasonator.com/objects/bb423adc-ed86-42d8-942e-84efbe7b1acf", - "url": "https://gleasonator.com/notice/9zIH8WYwtnUx4yDzUm", - "visibility": "direct" - }, - { - "account": { - "acct": "alex", - "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "bot": false, - "created_at": "2020-01-08T01:25:43.000Z", - "display_name": "Alex Gleason", - "emojis": [], - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "follow_requests_count": 0, - "followers_count": 725, - "following_count": 1211, - "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", - "locked": false, - "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", - "pleroma": { - "accepts_chat_messages": true, - "allow_following_move": true, - "ap_id": "https://gleasonator.com/users/alex", - "background_image": null, - "confirmation_pending": false, - "deactivated": false, - "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_moderator": false, - "notification_settings": { - "block_from_strangers": false, - "hide_notification_contents": false - }, - "relationship": {}, - "skip_thread_containment": false, - "tags": [], - "unread_conversation_count": 95, - "unread_notifications_count": 0 - }, - "source": { - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", - "pleroma": { - "actor_type": "Person", - "discoverable": false, - "no_rich_text": false, - "show_role": true - }, - "privacy": "public", - "sensitive": false - }, - "statuses_count": 9157, - "url": "https://gleasonator.com/users/alex", - "username": "alex" - }, - "application": { - "name": "Web", - "website": null - }, - "bookmarked": false, - "card": null, - "content": "

E

", - "created_at": "2020-09-18T20:07:38.000Z", - "emojis": [], - "favourited": false, - "favourites_count": 0, - "id": "9zIH9GTCDWEFSRt2um", - "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", - "in_reply_to_id": "9zIH7PUdhK3Ircg4hM", - "language": null, - "media_attachments": [], - "mentions": [ - { - "acct": "alex", - "id": "9v5bmRalQvjOy0ECcC", - "url": "https://gleasonator.com/users/alex", - "username": "alex" - } - ], - "muted": false, - "pinned": false, - "pleroma": { - "content": { - "text/plain": "E" - }, - "conversation_id": 5089485, - "direct_conversation_id": null, - "emoji_reactions": [], - "expires_at": null, - "in_reply_to_account_acct": "alex", - "local": true, - "parent_visible": true, - "spoiler_text": { - "text/plain": "" - }, - "thread_muted": false - }, - "poll": null, - "reblog": null, - "reblogged": false, - "reblogs_count": 0, - "replies_count": 0, - "sensitive": false, - "spoiler_text": "", - "tags": [], - "text": null, - "uri": "https://gleasonator.com/objects/a1e45493-2158-4f11-88ca-ba621429dbe5", - "url": "https://gleasonator.com/notice/9zIH9GTCDWEFSRt2um", - "visibility": "direct" - }, - { - "account": { - "acct": "alex", - "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "bot": false, - "created_at": "2020-01-08T01:25:43.000Z", - "display_name": "Alex Gleason", - "emojis": [], - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "follow_requests_count": 0, - "followers_count": 725, - "following_count": 1211, - "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", - "locked": false, - "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", - "pleroma": { - "accepts_chat_messages": true, - "allow_following_move": true, - "ap_id": "https://gleasonator.com/users/alex", - "background_image": null, - "confirmation_pending": false, - "deactivated": false, - "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_moderator": false, - "notification_settings": { - "block_from_strangers": false, - "hide_notification_contents": false - }, - "relationship": {}, - "skip_thread_containment": false, - "tags": [], - "unread_conversation_count": 95, - "unread_notifications_count": 0 - }, - "source": { - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", - "pleroma": { - "actor_type": "Person", - "discoverable": false, - "no_rich_text": false, - "show_role": true - }, - "privacy": "public", - "sensitive": false - }, - "statuses_count": 9157, - "url": "https://gleasonator.com/users/alex", - "username": "alex" - }, - "application": { - "name": "Web", - "website": null - }, - "bookmarked": false, - "card": null, - "content": "

F

", - "created_at": "2020-09-18T20:07:42.000Z", - "emojis": [], - "favourited": false, - "favourites_count": 0, - "id": "9zIH9fhaP9atiJoOJc", - "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", - "in_reply_to_id": "9zIH8WYwtnUx4yDzUm", - "language": null, - "media_attachments": [], - "mentions": [ - { - "acct": "alex", - "id": "9v5bmRalQvjOy0ECcC", - "url": "https://gleasonator.com/users/alex", - "username": "alex" - } - ], - "muted": false, - "pinned": false, - "pleroma": { - "content": { - "text/plain": "F" - }, - "conversation_id": 5089485, - "direct_conversation_id": null, - "emoji_reactions": [], - "expires_at": null, - "in_reply_to_account_acct": "alex", - "local": true, - "parent_visible": true, - "spoiler_text": { - "text/plain": "" - }, - "thread_muted": false - }, - "poll": null, - "reblog": null, - "reblogged": false, - "reblogs_count": 0, - "replies_count": 0, - "sensitive": false, - "spoiler_text": "", - "tags": [], - "text": null, - "uri": "https://gleasonator.com/objects/ee661cf9-35d4-4e84-88ff-13b5950f7556", - "url": "https://gleasonator.com/notice/9zIH9fhaP9atiJoOJc", - "visibility": "direct" - } - ] -} diff --git a/app/soapbox/actions/statuses.js b/app/soapbox/actions/statuses.js index 176f7325f..a274f181d 100644 --- a/app/soapbox/actions/statuses.js +++ b/app/soapbox/actions/statuses.js @@ -179,10 +179,18 @@ export function fetchContext(id) { }; } -export function fetchNext(next) { +export function fetchNext(statusId, next) { return async(dispatch, getState) => { const response = await api(getState).get(next); dispatch(importFetchedStatuses(response.data)); + + dispatch({ + type: CONTEXT_FETCH_SUCCESS, + id: statusId, + ancestors: [], + descendants: response.data, + }); + return { next: getNextLink(response) }; }; } @@ -208,11 +216,19 @@ export function fetchStatusWithContext(id) { const features = getFeatures(getState().instance); if (features.paginatedContext) { + await dispatch(fetchStatus(id)); const responses = await Promise.all([ dispatch(fetchAncestors(id)), dispatch(fetchDescendants(id)), - dispatch(fetchStatus(id)), ]); + + dispatch({ + type: CONTEXT_FETCH_SUCCESS, + id, + ancestors: responses[0].data, + descendants: responses[1].data, + }); + const next = getNextLink(responses[1]); return { next }; } else { diff --git a/app/soapbox/features/status/components/thread-status.tsx b/app/soapbox/features/status/components/thread-status.tsx index 13a486756..6919e5e8d 100644 --- a/app/soapbox/features/status/components/thread-status.tsx +++ b/app/soapbox/features/status/components/thread-status.tsx @@ -11,11 +11,12 @@ interface IThreadStatus { focusedStatusId: string, } +/** Status with reply-connector in threads. */ const ThreadStatus: React.FC = (props): JSX.Element => { const { id, focusedStatusId } = props; - const replyToId = useAppSelector(state => state.contexts.getIn(['inReplyTos', id])); - const replyCount = useAppSelector(state => state.contexts.getIn(['replies', id], ImmutableOrderedSet()).size); + const replyToId = useAppSelector(state => state.contexts.inReplyTos.get(id)); + const replyCount = useAppSelector(state => state.contexts.replies.get(id, ImmutableOrderedSet()).size); const isLoaded = useAppSelector(state => Boolean(state.statuses.get(id))); const renderConnector = (): JSX.Element | null => { diff --git a/app/soapbox/features/status/index.tsx b/app/soapbox/features/status/index.tsx index 16c398df7..f7f1a8268 100644 --- a/app/soapbox/features/status/index.tsx +++ b/app/soapbox/features/status/index.tsx @@ -98,11 +98,11 @@ const makeMapStateToProps = () => { const getStatus = makeGetStatus(); const getAncestorsIds = createSelector([ - (_: RootState, statusId: string) => statusId, - (state: RootState) => state.contexts.get('inReplyTos'), + (_: RootState, statusId: string | undefined) => statusId, + (state: RootState) => state.contexts.inReplyTos, ], (statusId, inReplyTos) => { - let ancestorsIds = ImmutableOrderedSet(); - let id = statusId; + let ancestorsIds = ImmutableOrderedSet(); + let id: string | undefined = statusId; while (id && !ancestorsIds.includes(id)) { ancestorsIds = ImmutableOrderedSet([id]).union(ancestorsIds); @@ -114,13 +114,15 @@ const makeMapStateToProps = () => { const getDescendantsIds = createSelector([ (_: RootState, statusId: string) => statusId, - (state: RootState) => state.contexts.get('replies'), + (state: RootState) => state.contexts.replies, ], (statusId, contextReplies) => { let descendantsIds = ImmutableOrderedSet(); const ids = [statusId]; while (ids.length > 0) { - const id = ids.shift(); + const id = ids.shift(); + if (!id) break; + const replies = contextReplies.get(id); if (descendantsIds.includes(id)) { @@ -148,7 +150,7 @@ const makeMapStateToProps = () => { if (status) { const statusId = status.id; - ancestorsIds = getAncestorsIds(state, state.contexts.getIn(['inReplyTos', statusId])); + ancestorsIds = getAncestorsIds(state, state.contexts.inReplyTos.get(statusId)); descendantsIds = getDescendantsIds(state, statusId); ancestorsIds = ancestorsIds.delete(statusId).subtract(descendantsIds); descendantsIds = descendantsIds.delete(statusId).subtract(ancestorsIds); @@ -649,10 +651,11 @@ class Status extends ImmutablePureComponent { } handleLoadMore = () => { + const { status } = this.props; const { next } = this.state; if (next) { - this.props.dispatch(fetchNext(next)).then(({ next }) => { + this.props.dispatch(fetchNext(status.id, next)).then(({ next }) => { this.setState({ next }); }).catch(() => {}); } diff --git a/app/soapbox/jest/test-helpers.tsx b/app/soapbox/jest/test-helpers.tsx index 49bf08bf2..459049b52 100644 --- a/app/soapbox/jest/test-helpers.tsx +++ b/app/soapbox/jest/test-helpers.tsx @@ -18,7 +18,7 @@ const middlewares = [thunk]; const mockStore = configureMockStore(middlewares); let rootState = rootReducer(undefined, {} as Action); -// Apply actions to the state, one at a time +/** Apply actions to the state, one at a time. */ const applyActions = (state: any, actions: any, reducer: any) => { return actions.reduce((state: any, action: any) => reducer(state, action), state); }; diff --git a/app/soapbox/reducers/__tests__/contexts-test.js b/app/soapbox/reducers/__tests__/contexts-test.js index 26d6ecb39..d5270f0d8 100644 --- a/app/soapbox/reducers/__tests__/contexts-test.js +++ b/app/soapbox/reducers/__tests__/contexts-test.js @@ -4,78 +4,111 @@ import { fromJS, } from 'immutable'; -import context1 from 'soapbox/__fixtures__/context_1.json'; -import context2 from 'soapbox/__fixtures__/context_2.json'; +import { STATUS_IMPORT } from 'soapbox/actions/importer'; import { CONTEXT_FETCH_SUCCESS } from 'soapbox/actions/statuses'; import { TIMELINE_DELETE } from 'soapbox/actions/timelines'; +import { applyActions } from 'soapbox/jest/test-helpers'; -import reducer from '../contexts'; +import reducer, { ReducerRecord } from '../contexts'; describe('contexts reducer', () => { it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual(ImmutableMap({ + expect(reducer(undefined, {})).toEqual(ReducerRecord({ inReplyTos: ImmutableMap(), replies: ImmutableMap(), })); }); - it('should support rendering a complete tree', () => { - // https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/422 - let result; - result = reducer(result, { type: CONTEXT_FETCH_SUCCESS, id: '9zIH8WYwtnUx4yDzUm', ancestors: context1.ancestors, descendants: context1.descendants }); - result = reducer(result, { type: CONTEXT_FETCH_SUCCESS, id: '9zIH7PUdhK3Ircg4hM', ancestors: context2.ancestors, descendants: context2.descendants }); + describe(CONTEXT_FETCH_SUCCESS, () => { + it('inserts a tombstone connecting an orphaned descendant', () => { + const status = { id: 'A', in_reply_to_id: null }; - expect(result).toEqual(ImmutableMap({ - inReplyTos: ImmutableMap({ - '9zIH7PUdhK3Ircg4hM': '9zIH6kDXA10YqhMKqO', - '9zIH7mMGgc1RmJwDLM': '9zIH6kDXA10YqhMKqO', - '9zIH9GTCDWEFSRt2um': '9zIH7PUdhK3Ircg4hM', - '9zIH9fhaP9atiJoOJc': '9zIH8WYwtnUx4yDzUm', - '9zIH8WYwtnUx4yDzUm': '9zIH7PUdhK3Ircg4hM', - '9zIH8WYwtnUx4yDzUm-tombstone': '9zIH7mMGgc1RmJwDLM', - }), - replies: ImmutableMap({ - '9zIH6kDXA10YqhMKqO': ImmutableOrderedSet([ - '9zIH7PUdhK3Ircg4hM', - '9zIH7mMGgc1RmJwDLM', - ]), - '9zIH7PUdhK3Ircg4hM': ImmutableOrderedSet([ - '9zIH8WYwtnUx4yDzUm', - '9zIH9GTCDWEFSRt2um', - ]), - '9zIH8WYwtnUx4yDzUm': ImmutableOrderedSet([ - '9zIH9fhaP9atiJoOJc', - ]), - '9zIH8WYwtnUx4yDzUm-tombstone': ImmutableOrderedSet([ - '9zIH8WYwtnUx4yDzUm', - ]), - '9zIH7mMGgc1RmJwDLM': ImmutableOrderedSet([ - '9zIH8WYwtnUx4yDzUm-tombstone', - ]), - }), - })); + const context = { + id: 'A', + ancestors: [], + descendants: [ + { id: 'C', in_reply_to_id: 'B' }, + ], + }; + + const actions = [ + { type: STATUS_IMPORT, status }, + { type: CONTEXT_FETCH_SUCCESS, ...context }, + ]; + + const result = applyActions(undefined, actions, reducer); + expect(result.inReplyTos.get('C')).toBe('C-tombstone'); + expect(result.replies.get('A').toArray()).toEqual(['C-tombstone']); + }); + + it('inserts a tombstone connecting an orphaned descendant (with null in_reply_to_id)', () => { + const status = { id: 'A', in_reply_to_id: null }; + + const context = { + id: 'A', + ancestors: [], + descendants: [ + { id: 'C', in_reply_to_id: null }, + ], + }; + + const actions = [ + { type: STATUS_IMPORT, status }, + { type: CONTEXT_FETCH_SUCCESS, ...context }, + ]; + + const result = applyActions(undefined, actions, reducer); + expect(result.inReplyTos.get('C')).toBe('C-tombstone'); + expect(result.replies.get('A').toArray()).toEqual(['C-tombstone']); + }); + + it('doesn\'t explode when it encounters a loop', () => { + const status = { id: 'A', in_reply_to_id: null }; + + const context = { + id: 'A', + ancestors: [], + descendants: [ + { id: 'C', in_reply_to_id: 'E' }, + { id: 'D', in_reply_to_id: 'C' }, + { id: 'E', in_reply_to_id: 'D' }, + { id: 'F', in_reply_to_id: 'F' }, + ], + }; + + const actions = [ + { type: STATUS_IMPORT, status }, + { type: CONTEXT_FETCH_SUCCESS, ...context }, + ]; + + const result = applyActions(undefined, actions, reducer); + + // These checks are superficial. We just don't want a stack overflow! + expect(result.inReplyTos.get('C')).toBe('C-tombstone'); + expect(result.replies.get('A').toArray()).toEqual(['C-tombstone', 'F-tombstone']); + }); }); describe(TIMELINE_DELETE, () => { it('deletes the status', () => { const action = { type: TIMELINE_DELETE, id: 'B' }; - const state = fromJS({ - inReplyTos: { + const state = ReducerRecord({ + inReplyTos: fromJS({ B: 'A', C: 'B', - }, - replies: { + }), + replies: fromJS({ A: ImmutableOrderedSet(['B']), B: ImmutableOrderedSet(['C']), - }, + }), }); - const expected = fromJS({ - inReplyTos: {}, - replies: { + const expected = ReducerRecord({ + inReplyTos: fromJS({}), + replies: fromJS({ A: ImmutableOrderedSet(), - }, + }), }); expect(reducer(state, action)).toEqual(expected); diff --git a/app/soapbox/reducers/contexts.js b/app/soapbox/reducers/contexts.js deleted file mode 100644 index c92ae503d..000000000 --- a/app/soapbox/reducers/contexts.js +++ /dev/null @@ -1,163 +0,0 @@ -import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable'; - -import { STATUS_IMPORT, STATUSES_IMPORT } from 'soapbox/actions/importer'; - -import { - ACCOUNT_BLOCK_SUCCESS, - ACCOUNT_MUTE_SUCCESS, -} from '../actions/accounts'; -import { - STATUS_CREATE_REQUEST, - STATUS_CREATE_SUCCESS, -} from '../actions/statuses'; -import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses'; -import { TIMELINE_DELETE } from '../actions/timelines'; - -const initialState = ImmutableMap({ - inReplyTos: ImmutableMap(), - replies: ImmutableMap(), -}); - -const importStatus = (state, status, idempotencyKey) => { - const { id, in_reply_to_id } = status; - if (!in_reply_to_id) return state; - - return state.withMutations(state => { - state.setIn(['inReplyTos', id], in_reply_to_id); - - state.updateIn(['replies', in_reply_to_id], ImmutableOrderedSet(), ids => { - return ids.add(id).sort(); - }); - - if (idempotencyKey) { - deletePendingStatus(state, status, idempotencyKey); - } - }); -}; - -const importStatuses = (state, statuses) => { - return state.withMutations(state => { - statuses.forEach(status => importStatus(state, status)); - }); -}; - -const isReplyTo = (state, childId, parentId, initialId = null) => { - if (!childId) return false; - - // Prevent cycles - if (childId === initialId) return false; - initialId = initialId || childId; - - if (childId === parentId) { - return true; - } else { - const nextId = state.getIn(['inReplyTos', childId]); - return isReplyTo(state, nextId, parentId, initialId); - } -}; - -const insertTombstone = (state, ancestorId, descendantId) => { - // Prevent infinite loop if the API returns a bogus response - if (isReplyTo(state, ancestorId, descendantId)) return state; - - const tombstoneId = `${descendantId}-tombstone`; - return state.withMutations(state => { - importStatus(state, { id: tombstoneId, in_reply_to_id: ancestorId }); - importStatus(state, { id: descendantId, in_reply_to_id: tombstoneId }); - }); -}; - -const importBranch = (state, statuses, rootId) => { - return state.withMutations(state => { - statuses.forEach((status, i) => { - const lastId = rootId && i === 0 ? rootId : (statuses[i - 1] || {}).id; - - if (status.in_reply_to_id) { - importStatus(state, status); - } else if (lastId) { - insertTombstone(state, lastId, status.id); - } - }); - }); -}; - -const normalizeContext = (state, id, ancestors, descendants) => state.withMutations(state => { - importBranch(state, ancestors); - importBranch(state, descendants, id); - - if (ancestors.length > 0 && !state.getIn(['inReplyTos', id])) { - insertTombstone(state, ancestors[ancestors.length - 1].id, id); - } -}); - -const deleteStatus = (state, id) => { - return state.withMutations(state => { - const parentId = state.getIn(['inReplyTos', id]); - const replies = state.getIn(['replies', id], ImmutableOrderedSet()); - - // Delete from its parent's tree - state.updateIn(['replies', parentId], ImmutableOrderedSet(), ids => ids.delete(id)); - - // Dereference children - replies.forEach(reply => state.deleteIn(['inReplyTos', reply])); - - state.deleteIn(['inReplyTos', id]); - state.deleteIn(['replies', id]); - }); -}; - -const deleteStatuses = (state, ids) => { - return state.withMutations(state => { - ids.forEach(id => deleteStatus(state, id)); - }); -}; - -const filterContexts = (state, relationship, statuses) => { - const ownedStatusIds = statuses - .filter(status => status.get('account') === relationship.id) - .map(status => status.get('id')); - - return deleteStatuses(state, ownedStatusIds); -}; - -const importPendingStatus = (state, params, idempotencyKey) => { - const id = `末pending-${idempotencyKey}`; - const { in_reply_to_id } = params; - return importStatus(state, { id, in_reply_to_id }); -}; - -const deletePendingStatus = (state, { in_reply_to_id }, idempotencyKey) => { - const id = `末pending-${idempotencyKey}`; - - return state.withMutations(state => { - state.deleteIn(['inReplyTos', id]); - - if (in_reply_to_id) { - state.updateIn(['replies', in_reply_to_id], ImmutableOrderedSet(), ids => { - return ids.delete(id).sort(); - }); - } - }); -}; - -export default function replies(state = initialState, action) { - switch (action.type) { - case ACCOUNT_BLOCK_SUCCESS: - case ACCOUNT_MUTE_SUCCESS: - return filterContexts(state, action.relationship, action.statuses); - case CONTEXT_FETCH_SUCCESS: - return normalizeContext(state, action.id, action.ancestors, action.descendants); - case TIMELINE_DELETE: - return deleteStatuses(state, [action.id]); - case STATUS_CREATE_REQUEST: - return importPendingStatus(state, action.params, action.idempotencyKey); - case STATUS_CREATE_SUCCESS: - return deletePendingStatus(state, action.status, action.idempotencyKey); - case STATUS_IMPORT: - return importStatus(state, action.status, action.idempotencyKey); - case STATUSES_IMPORT: - return importStatuses(state, action.statuses); - default: - return state; - } -} diff --git a/app/soapbox/reducers/contexts.ts b/app/soapbox/reducers/contexts.ts new file mode 100644 index 000000000..bfc830c93 --- /dev/null +++ b/app/soapbox/reducers/contexts.ts @@ -0,0 +1,221 @@ +import { + Map as ImmutableMap, + Record as ImmutableRecord, + OrderedSet as ImmutableOrderedSet, +} from 'immutable'; + +import { STATUS_IMPORT, STATUSES_IMPORT } from 'soapbox/actions/importer'; + +import { + ACCOUNT_BLOCK_SUCCESS, + ACCOUNT_MUTE_SUCCESS, +} from '../actions/accounts'; +import { + STATUS_CREATE_REQUEST, + STATUS_CREATE_SUCCESS, +} from '../actions/statuses'; +import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses'; +import { TIMELINE_DELETE } from '../actions/timelines'; + +import type { ReducerStatus } from './statuses'; +import type { AnyAction } from 'redux'; + +export const ReducerRecord = ImmutableRecord({ + inReplyTos: ImmutableMap(), + replies: ImmutableMap>(), +}); + +type State = ReturnType; + +/** Minimal status fields needed to process context. */ +type ContextStatus = { + id: string, + in_reply_to_id: string | null, +} + +/** Import a single status into the reducer, setting replies and replyTos. */ +const importStatus = (state: State, status: ContextStatus, idempotencyKey?: string): State => { + const { id, in_reply_to_id: inReplyToId } = status; + if (!inReplyToId) return state; + + return state.withMutations(state => { + const replies = state.replies.get(inReplyToId) || ImmutableOrderedSet(); + const newReplies = replies.add(id).sort(); + + state.setIn(['replies', inReplyToId], newReplies); + state.setIn(['inReplyTos', id], inReplyToId); + + if (idempotencyKey) { + deletePendingStatus(state, status, idempotencyKey); + } + }); +}; + +/** Import multiple statuses into the state. */ +const importStatuses = (state: State, statuses: ContextStatus[]): State => { + return state.withMutations(state => { + statuses.forEach(status => importStatus(state, status)); + }); +}; + +/** Insert a fake status ID connecting descendant to ancestor. */ +const insertTombstone = (state: State, ancestorId: string, descendantId: string): State => { + const tombstoneId = `${descendantId}-tombstone`; + return state.withMutations(state => { + importStatus(state, { id: tombstoneId, in_reply_to_id: ancestorId }); + importStatus(state, { id: descendantId, in_reply_to_id: tombstoneId }); + }); +}; + +/** Find the highest level status from this statusId. */ +const getRootNode = (state: State, statusId: string, initialId = statusId): string => { + const parent = state.inReplyTos.get(statusId); + + if (!parent) { + return statusId; + } else if (parent === initialId) { + // Prevent cycles + return parent; + } else { + return getRootNode(state, parent, initialId); + } +}; + +/** Route fromId to toId by inserting tombstones. */ +const connectNodes = (state: State, fromId: string, toId: string): State => { + const fromRoot = getRootNode(state, fromId); + const toRoot = getRootNode(state, toId); + + if (fromRoot !== toRoot) { + return insertTombstone(state, toId, fromId); + } else { + return state; + } +}; + +/** Import a branch of ancestors or descendants, in relation to statusId. */ +const importBranch = (state: State, statuses: ContextStatus[], statusId?: string): State => { + return state.withMutations(state => { + statuses.forEach((status, i) => { + const prevId = statusId && i === 0 ? statusId : (statuses[i - 1] || {}).id; + + if (status.in_reply_to_id) { + importStatus(state, status); + + // On Mastodon, in_reply_to_id can refer to an unavailable status, + // so traverse the tree up and insert a connecting tombstone if needed. + if (statusId) { + connectNodes(state, status.id, statusId); + } + } else if (prevId) { + // On Pleroma, in_reply_to_id will be null if the parent is unavailable, + // so insert the tombstone now. + insertTombstone(state, prevId, status.id); + } + }); + }); +}; + +/** Import a status's ancestors and descendants. */ +const normalizeContext = ( + state: State, + id: string, + ancestors: ContextStatus[], + descendants: ContextStatus[], +) => state.withMutations(state => { + importBranch(state, ancestors); + importBranch(state, descendants, id); + + if (ancestors.length > 0 && !state.getIn(['inReplyTos', id])) { + insertTombstone(state, ancestors[ancestors.length - 1].id, id); + } +}); + +/** Remove a status from the reducer. */ +const deleteStatus = (state: State, id: string): State => { + return state.withMutations(state => { + // Delete from its parent's tree + const parentId = state.inReplyTos.get(id); + if (parentId) { + const parentReplies = state.replies.get(parentId) || ImmutableOrderedSet(); + const newParentReplies = parentReplies.delete(id); + state.setIn(['replies', parentId], newParentReplies); + } + + // Dereference children + const replies = state.replies.get(id) || ImmutableOrderedSet(); + replies.forEach(reply => state.deleteIn(['inReplyTos', reply])); + + state.deleteIn(['inReplyTos', id]); + state.deleteIn(['replies', id]); + }); +}; + +/** Delete multiple statuses from the reducer. */ +const deleteStatuses = (state: State, ids: string[]): State => { + return state.withMutations(state => { + ids.forEach(id => deleteStatus(state, id)); + }); +}; + +/** Delete statuses upon blocking or muting a user. */ +const filterContexts = ( + state: State, + relationship: { id: string }, + /** The entire statuses map from the store. */ + statuses: ImmutableMap, +): State => { + const ownedStatusIds = statuses + .filter(status => status.account === relationship.id) + .map(status => status.id) + .toList() + .toArray(); + + return deleteStatuses(state, ownedStatusIds); +}; + +/** Add a fake status ID for a pending status. */ +const importPendingStatus = (state: State, params: ContextStatus, idempotencyKey: string): State => { + const id = `末pending-${idempotencyKey}`; + const { in_reply_to_id } = params; + return importStatus(state, { id, in_reply_to_id }); +}; + +/** Delete a pending status from the reducer. */ +const deletePendingStatus = (state: State, params: ContextStatus, idempotencyKey: string): State => { + const id = `末pending-${idempotencyKey}`; + const { in_reply_to_id: inReplyToId } = params; + + return state.withMutations(state => { + state.deleteIn(['inReplyTos', id]); + + if (inReplyToId) { + const replies = state.replies.get(inReplyToId) || ImmutableOrderedSet(); + const newReplies = replies.delete(id).sort(); + state.setIn(['replies', inReplyToId], newReplies); + } + }); +}; + +/** Contexts reducer. Used for building a nested tree structure for threads. */ +export default function replies(state = ReducerRecord(), action: AnyAction) { + switch (action.type) { + case ACCOUNT_BLOCK_SUCCESS: + case ACCOUNT_MUTE_SUCCESS: + return filterContexts(state, action.relationship, action.statuses); + case CONTEXT_FETCH_SUCCESS: + return normalizeContext(state, action.id, action.ancestors, action.descendants); + case TIMELINE_DELETE: + return deleteStatuses(state, [action.id]); + case STATUS_CREATE_REQUEST: + return importPendingStatus(state, action.params, action.idempotencyKey); + case STATUS_CREATE_SUCCESS: + return deletePendingStatus(state, action.status, action.idempotencyKey); + case STATUS_IMPORT: + return importStatus(state, action.status, action.idempotencyKey); + case STATUSES_IMPORT: + return importStatuses(state, action.statuses); + default: + return state; + } +}