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;
+ }
+}