Merge branch 'fix-self-reply' into 'develop'

Move status normalizer code into its own module

See merge request soapbox-pub/soapbox-fe!1046
This commit is contained in:
Alex Gleason 2022-02-24 16:00:04 +00:00
commit dc979a8039
28 changed files with 1536 additions and 275 deletions

View File

@ -0,0 +1,127 @@
{
"acct": "alex",
"avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png",
"avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png",
"bot": false,
"created_at": "2020-01-08T01:25:43.000Z",
"display_name": "Alex Gleason",
"emojis": [],
"fields": [
{
"name": "Website",
"value": "<a href=\"https://alexgleason.me\" rel=\"ugc\">https://alexgleason.me</a>"
},
{
"name": "Soapbox",
"value": "<a href=\"https://soapbox.pub\" rel=\"ugc\">https://soapbox.pub</a>"
},
{
"name": "Email",
"value": "alex@alexgleason.me"
},
{
"name": "Gender identity",
"value": "Soyboy"
},
{
"name": "Donate (PayPal)",
"value": "<a href=\"https://paypal.me/gleasonator\" rel=\"ugc\">https://paypal.me/gleasonator</a>"
},
{
"name": "$BTC",
"value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n"
},
{
"name": "$ETH",
"value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717"
},
{
"name": "$DOGE",
"value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D"
},
{
"name": "$XMR",
"value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK"
}
],
"followers_count": 2378,
"following_count": 1571,
"fqn": "alex@gleasonator.com",
"header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
"header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
"id": "9v5bmRalQvjOy0ECcC",
"last_status_at": "2022-02-20T04:14:49",
"locked": false,
"note": "I create Fediverse software that empowers people online.<br/><br/>I&#39;m vegan btw<br/><br/>Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.",
"pleroma": {
"accepts_chat_messages": true,
"also_known_as": [
"https://mitra.social/users/alex"
],
"ap_id": "https://gleasonator.com/users/alex",
"background_image": null,
"birthday": "1993-07-03",
"favicon": "https://gleasonator.com/favicon.png",
"hide_favorites": true,
"hide_followers": false,
"hide_followers_count": false,
"hide_follows": false,
"hide_follows_count": false,
"is_admin": true,
"is_confirmed": true,
"is_moderator": false,
"is_suggested": true,
"relationship": {},
"skip_thread_containment": false,
"tags": []
},
"source": {
"fields": [
{
"name": "Website",
"value": "https://alexgleason.me"
},
{
"name": "Soapbox",
"value": "https://soapbox.pub"
},
{
"name": "Email",
"value": "alex@alexgleason.me"
},
{
"name": "Gender identity",
"value": "Soyboy"
},
{
"name": "Donate (PayPal)",
"value": "https://paypal.me/gleasonator"
},
{
"name": "$BTC",
"value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n"
},
{
"name": "$ETH",
"value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717"
},
{
"name": "$DOGE",
"value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D"
},
{
"name": "$XMR",
"value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK"
}
],
"note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.",
"pleroma": {
"actor_type": "Person",
"discoverable": false
},
"sensitive": false
},
"statuses_count": 23477,
"url": "https://gleasonator.com/users/alex",
"username": "alex"
}

View File

@ -0,0 +1,185 @@
{
"uri": "fedibird.com",
"title": "Fedibird",
"short_description": "多くの独自機能を備えた、連合志向の汎用Mastodonサーバです。Fediverseの活動拠点としてご利用ください。",
"description": "多くの独自機能を備えた、連合志向の汎用Mastodonサーバです。Fediverseの活動拠点としてご利用ください。",
"email": "support@fedibird.com",
"version": "3.4.1",
"urls": {
"streaming_api": "wss://fedibird.com"
},
"stats": {
"user_count": 1964,
"status_count": 4590304,
"domain_count": 9024
},
"thumbnail": "https://s3.fedibird.com/site_uploads/files/000/000/001/original/fedibird_hero_image.png",
"languages": [
"ja"
],
"registrations": true,
"approval_required": false,
"invites_enabled": true,
"configuration": {
"statuses": {
"max_characters": 500,
"max_media_attachments": 4,
"characters_reserved_per_url": 23,
"min_expiration": 60,
"max_expiration": 37152000,
"supported_expires_actions": [
"delete",
"mark"
]
},
"media_attachments": {
"supported_mime_types": [
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
"image/heif",
"image/heic",
"video/webm",
"video/mp4",
"video/quicktime",
"video/ogg",
"audio/wave",
"audio/wav",
"audio/x-wav",
"audio/x-pn-wave",
"audio/ogg",
"audio/mpeg",
"audio/mp3",
"audio/webm",
"audio/flac",
"audio/aac",
"audio/m4a",
"audio/x-m4a",
"audio/mp4",
"audio/3gpp",
"video/x-ms-asf"
],
"image_size_limit": 10485760,
"image_matrix_limit": 16777216,
"video_size_limit": 41943040,
"video_frame_rate_limit": 60,
"video_matrix_limit": 2304000
},
"polls": {
"max_options": 4,
"max_characters_per_option": 50,
"min_expiration": 300,
"max_expiration": 2629746
},
"emoji_reactions": {
"max_reactions": 20
}
},
"feature_quote": true,
"fedibird_capabilities": [
"favourite_hashtag",
"favourite_domain",
"favourite_list",
"status_expire",
"follow_no_delivery",
"follow_hashtag",
"subscribe_account",
"subscribe_domain",
"subscribe_keyword",
"timeline_home_visibility",
"timeline_no_local",
"timeline_domain",
"timeline_group",
"timeline_group_directory",
"visibility_mutual",
"visibility_limited",
"emoji_reaction",
"misskey_birthday",
"misskey_location"
],
"contact_account": {
"id": "1",
"username": "noellabo",
"acct": "noellabo",
"display_name": "のえる",
"locked": false,
"bot": false,
"cat": false,
"discoverable": true,
"group": false,
"created_at": "2019-08-15T00:00:00.000Z",
"note": "<p>主に、Fediverseへの関心に基づいた投稿を行うアカウントです。DTP・印刷に関する話をしたり、同人の話をしたり、カレーをブーストしたりします。</p><p>Mastodonサーバ『Fedibird』の管理者アカウントでもあります。ご連絡は当アカウントへ、サーバインフォメーションについては <a href=\"https://fedibird.com/about/more\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">fedibird.com/about/more</span><span class=\"invisible\"></span></a> と <span class=\"h-card\"><a href=\"https://fedibird.com/@info\" class=\"u-url mention\">@<span>info</span></a></span> を参照してください。</p>",
"url": "https://fedibird.com/@noellabo",
"avatar": "https://s3.fedibird.com/accounts/avatars/000/000/001/original/6ef3b7f18f726755.png",
"avatar_static": "https://s3.fedibird.com/accounts/avatars/000/000/001/original/6ef3b7f18f726755.png",
"header": "https://s3.fedibird.com/accounts/headers/000/000/001/original/6a5a51722c094835.jpg",
"header_static": "https://s3.fedibird.com/accounts/headers/000/000/001/original/6a5a51722c094835.jpg",
"followers_count": 1560,
"following_count": 758,
"subscribing_count": 121,
"statuses_count": 61325,
"last_status_at": "2022-02-24",
"emojis": [
{
"shortcode": "liberapay",
"url": "https://s3.fedibird.com/custom_emojis/images/000/025/634/original/5b8620742973f844.png",
"static_url": "https://s3.fedibird.com/custom_emojis/images/000/025/634/static/5b8620742973f844.png",
"visible_in_picker": true
},
{
"shortcode": "mastodon",
"url": "https://s3.fedibird.com/custom_emojis/images/000/008/396/original/1317b6f8efcf8318.png",
"static_url": "https://s3.fedibird.com/custom_emojis/images/000/008/396/static/1317b6f8efcf8318.png",
"visible_in_picker": true
}
],
"fields": [
{
"name": ":liberapay: Liberapay",
"value": "<a href=\"https://liberapay.com/noellabo\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">liberapay.com/noellabo</span><span class=\"invisible\"></span></a>",
"verified_at": "2020-10-22T03:04:43.206+00:00"
},
{
"name": ":mastodon: DTP-Mstdn.jp",
"value": "<a class=\"account-url-link\" data-account-acct=\"noellabo@dtp-mstdn.jp\" data-account-actor-type=\"Person\" data-account-id=\"55\" href=\"https://dtp-mstdn.jp/@noellabo\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">dtp-mstdn.jp/@noellabo</span><span class=\"invisible\"></span></a>",
"verified_at": "2020-05-23T00:14:02.232+00:00"
},
{
"name": "別宅",
"value": "<a class=\"account-url-link\" data-account-acct=\"noellabo@gorone.xyz\" data-account-actor-type=\"Person\" data-account-id=\"14504\" href=\"https://gorone.xyz/@noellabo\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">gorone.xyz/@noellabo</span><span class=\"invisible\"></span></a>",
"verified_at": "2021-08-11T07:48:53.479+00:00"
},
{
"name": "bluesky community",
"value": "<a class=\"account-url-link\" data-account-acct=\"noellabo@mastodon.blueskycommunity.net\" data-account-actor-type=\"Person\" data-account-id=\"107267866207603606\" href=\"https://mastodon.blueskycommunity.net/@noellabo\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"ellipsis\">mastodon.blueskycommunity.net/</span><span class=\"invisible\">@noellabo</span></a>",
"verified_at": "2021-11-13T04:28:30.593+00:00"
}
],
"other_settings": {
"birthday": null,
"location": "埼玉県",
"cat_ears_color": "#d5c5c0",
"noindex": false,
"hide_network": false,
"hide_statuses_count": false,
"hide_following_count": false,
"hide_followers_count": false,
"enable_reaction": true
}
},
"rules": [
{
"id": "2",
"text": "日本の法律と社会規範に従った行動を心がけてください"
},
{
"id": "3",
"text": "不快や脅威に対してはブロック・ミュート・フィルターで距離をとってください"
},
{
"id": "1",
"text": "投稿する際は、適切な公開範囲・CW・閲覧注意を使用してください"
}
]
}

View File

@ -0,0 +1,46 @@
{
"uri": "https://ica.mkljczk.pl",
"title": "Friendica Social Network",
"short_description": "",
"description": "",
"email": "me@mkljczk.pl",
"version": "2022.05-dev",
"urls": null,
"stats": {
"user_count": 0,
"status_count": 0,
"domain_count": 0
},
"thumbnail": "https://ica.mkljczk.plimages/friendica-32.png",
"languages": [
"pl"
],
"max_toot_chars": 200000,
"registrations": true,
"approval_required": false,
"invites_enabled": false,
"contact_account": {
"id": "2",
"username": "nofriend",
"acct": "nofriend",
"display_name": "marcin mikołajczak",
"locked": true,
"bot": false,
"discoverable": true,
"group": false,
"created_at": "2022-02-19T14:51:00.000Z",
"note": "",
"url": "https://ica.mkljczk.pl/profile/nofriend",
"avatar": "https://ica.mkljczk.pl/photo/contact/300/68a16c11-1262-1103-d40b-806159848009?ts=1645292106",
"avatar_static": "https://ica.mkljczk.pl/photo/contact/300/68a16c11-1262-1103-d40b-806159848009?ts=1645292106",
"header": "https://ica.mkljczk.pl/photo/header/68a16c11-1262-1103-d40b-806159848009?ts=1645292106",
"header_static": "https://ica.mkljczk.pl/photo/header/68a16c11-1262-1103-d40b-806159848009?ts=1645292106",
"followers_count": 0,
"following_count": 1,
"statuses_count": 0,
"last_status_at": "2022-02-20",
"emojis": [],
"fields": []
},
"rules": []
}

View File

@ -0,0 +1,53 @@
{
"id": "106",
"created_at": "2022-02-19T18:19:40.000Z",
"in_reply_to_id": null,
"in_reply_to_account_id": null,
"sensitive": false,
"spoiler_text": "",
"visibility": "public",
"language": "fa",
"uri": "https://ica.mkljczk.pl/objects/68a16c11-4262-1134-bc4e-0db298374337",
"url": "https://ica.mkljczk.pl/display/68a16c11-4262-1134-bc4e-0db298374337",
"replies_count": 0,
"reblogs_count": 0,
"favourites_count": 0,
"favourited": false,
"reblogged": false,
"muted": false,
"bookmarked": true,
"content": "Hello to Friendica from fe.soapbox.pub!",
"reblog": null,
"application": {
"name": "Soapbox FE"
},
"account": {
"id": "95",
"username": "alex",
"acct": "alex",
"display_name": "Alex Gleason",
"locked": true,
"bot": false,
"discoverable": false,
"group": false,
"created_at": "2022-02-19T18:17:43.000Z",
"note": "",
"url": "https://ica.mkljczk.pl/profile/alex",
"avatar": "https://ica.mkljczk.pl/photo/contact/300/68a16c11-1862-1134-4779-f98088458845?ts=1645294804",
"avatar_static": "https://ica.mkljczk.pl/photo/contact/300/68a16c11-1862-1134-4779-f98088458845?ts=1645294804",
"header": "https://ica.mkljczk.pl/photo/header/68a16c11-1862-1134-4779-f98088458845?ts=1645294804",
"header_static": "https://ica.mkljczk.pl/photo/header/68a16c11-1862-1134-4779-f98088458845?ts=1645294804",
"followers_count": 0,
"following_count": 0,
"statuses_count": 2,
"last_status_at": "2022-02-19",
"emojis": [],
"fields": []
},
"media_attachments": [],
"mentions": [],
"tags": [],
"emojis": [],
"card": null,
"poll": null
}

View File

@ -0,0 +1,27 @@
{
"id": "00YSECR4P7E64BD5MBA639PRVT",
"username": "alex",
"acct": "alex",
"display_name": "Alex Gleason",
"locked": false,
"bot": false,
"created_at": "2022-02-23T22:43:55Z",
"note": "<p>My GoToSocial profile</p>",
"url": "http://localhost/@alex",
"avatar": "",
"avatar_static": "",
"header": "",
"header_static": "",
"followers_count": 0,
"following_count": 0,
"statuses_count": 1,
"last_status_at": "2022-02-23T22:54:14Z",
"emojis": [],
"fields": [],
"source": {
"privacy": "unlisted",
"language": "en",
"note": "<p>My GoToSocial profile</p>",
"fields": []
}
}

View File

@ -0,0 +1,42 @@
{
"uri": "http://localhost",
"title": "localhost",
"description": "",
"short_description": "",
"email": "",
"version": "0.2.0 31935ee",
"registrations": true,
"approval_required": true,
"invites_enabled": false,
"urls": {
"streaming_api": "wss://localhost"
},
"stats": {
"domain_count": 0,
"status_count": 1,
"user_count": 1
},
"thumbnail": "",
"contact_account": {
"id": "",
"username": "",
"acct": "",
"display_name": "",
"locked": false,
"bot": false,
"created_at": "",
"note": "",
"url": "",
"avatar": "",
"avatar_static": "",
"header": "",
"header_static": "",
"followers_count": 0,
"following_count": 0,
"statuses_count": 0,
"last_status_at": "",
"emojis": null,
"fields": null
},
"max_toot_chars": 5000
}

View File

@ -0,0 +1,50 @@
{
"id": "01FWMCNM07GGDV8HF40NZ9YTGR",
"created_at": "2022-02-23T22:54:14Z",
"sensitive": false,
"spoiler_text": "",
"visibility": "public",
"language": "en",
"uri": "http://localhost/users/alex/statuses/01FWMCNM07GGDV8HF40NZ9YTGR",
"url": "http://localhost/@alex/statuses/01FWMCNM07GGDV8HF40NZ9YTGR",
"replies_count": 0,
"reblogs_count": 0,
"favourites_count": 0,
"favourited": false,
"reblogged": false,
"muted": false,
"bookmarked": false,
"content": "<p>Hello GoToSocial!</p>",
"application": {
"name": "Soapbox FE",
"website": "https://soapbox.pub/"
},
"account": {
"id": "00YSECR4P7E64BD5MBA639PRVT",
"username": "alex",
"acct": "alex",
"display_name": "alex",
"locked": false,
"bot": false,
"created_at": "2022-02-23T22:43:55Z",
"note": "",
"url": "http://localhost/@alex",
"avatar": "",
"avatar_static": "",
"header": "",
"header_static": "",
"followers_count": 0,
"following_count": 0,
"statuses_count": 1,
"last_status_at": "2022-02-23T22:54:14Z",
"emojis": [],
"fields": []
},
"media_attachments": [],
"mentions": [],
"tags": [],
"emojis": [],
"card": null,
"poll": null,
"text": "Hello GoToSocial!"
}

View File

@ -0,0 +1,51 @@
{
"id": "107828148293766288",
"created_at": "2022-02-20T03:16:09.812Z",
"in_reply_to_id": "107828147870368566",
"in_reply_to_account_id": "106801667066418367",
"sensitive": false,
"spoiler_text": "",
"visibility": "public",
"language": "en",
"uri": "https://mastodon.social/users/benis911/statuses/107828148293766288",
"url": "https://mastodon.social/@benis911/107828148293766288",
"replies_count": 0,
"reblogs_count": 0,
"favourites_count": 0,
"edited_at": null,
"content": "<p>test reply to self</p>",
"reblog": null,
"application": {
"name": "Soapbox FE",
"website": "https://soapbox.pub/"
},
"account": {
"id": "106801667066418367",
"username": "benis911",
"acct": "benis911",
"display_name": "",
"locked": false,
"bot": false,
"discoverable": null,
"group": false,
"created_at": "2021-08-22T00:00:00.000Z",
"note": "<p></p>",
"url": "https://mastodon.social/@benis911",
"avatar": "https://mastodon.social/avatars/original/missing.png",
"avatar_static": "https://mastodon.social/avatars/original/missing.png",
"header": "https://mastodon.social/headers/original/missing.png",
"header_static": "https://mastodon.social/headers/original/missing.png",
"followers_count": 0,
"following_count": 0,
"statuses_count": 3,
"last_status_at": "2022-02-20",
"emojis": [],
"fields": []
},
"media_attachments": [],
"mentions": [],
"tags": [],
"emojis": [],
"card": null,
"poll": null
}

View File

@ -0,0 +1,13 @@
{
"uri": "mitra.social",
"title": "Mitra",
"short_description": "Federated social network with smart contracts",
"description": "This is an instance of [Mitra](https://codeberg.org/silverpill/mitra), federated social network built on [ActivityPub](https://activitypub.rocks/) protocol.\nRegistration is invitation-only.\nAdmin:\n - [@silverpill@mitra.social](https://mitra.social/profile/dd4ebc18-269d-4c7b-a310-03d29c6ab551)\n - Matrix: @silverpill:poa.st\n",
"version": "3.0.0 (compatible; Mitra 0.4.0)",
"registrations": false,
"login_message": "Sign this message to log in to https://mitra.social. Do not sign this message on other sites!",
"post_character_limit": 5000,
"blockchain_explorer_url": null,
"blockchain_contract_address": null,
"ipfs_gateway_url": "https://ipfs.mitra.social"
}

View File

@ -0,0 +1,126 @@
{
"account": {
"acct": "benis911",
"avatar": "https://gleasonator.com/images/avi.png",
"avatar_static": "https://gleasonator.com/images/avi.png",
"bot": false,
"created_at": "2021-03-26T20:42:11.000Z",
"display_name": "benis911",
"emojis": [],
"fields": [],
"followers_count": 0,
"following_count": 0,
"fqn": "benis911@gleasonator.com",
"header": "https://media.gleasonator.com/fc595bbbcf5aabefecd1c2adfe5b7f5457db59847992881668653a0338ba25bd.jpg",
"header_static": "https://media.gleasonator.com/fc595bbbcf5aabefecd1c2adfe5b7f5457db59847992881668653a0338ba25bd.jpg",
"id": "A5c5LK7EJTFR0u26Pg",
"last_status_at": "2022-02-23T17:31:08",
"locked": true,
"note": "hello world 2",
"pleroma": {
"accepts_chat_messages": true,
"also_known_as": [
"https://gleasonator.com/users/alex",
"https://poa.st/users/alex"
],
"ap_id": "https://gleasonator.com/users/benis911",
"background_image": null,
"birthday": "2000-01-25",
"favicon": "https://gleasonator.com/favicon.png",
"hide_favorites": true,
"hide_followers": true,
"hide_followers_count": true,
"hide_follows": true,
"hide_follows_count": true,
"is_admin": false,
"is_confirmed": true,
"is_moderator": false,
"is_suggested": false,
"relationship": {},
"skip_thread_containment": false,
"tags": []
},
"source": {
"fields": [],
"note": "hello world 2",
"pleroma": {
"actor_type": "Person",
"discoverable": false
},
"sensitive": false
},
"statuses_count": 152,
"url": "https://gleasonator.com/users/benis911",
"username": "benis911"
},
"application": {
"name": "Soapbox FE",
"website": "https://soapbox.pub/"
},
"bookmarked": false,
"card": null,
"content": "Hello :ablobcathyper: :ageblobcat: 😂 world 😋 test :blobcatphoto:",
"created_at": "2022-02-23T17:31:07.000Z",
"emojis": [
{
"shortcode": "ablobcathyper",
"static_url": "https://gleasonator.com/emoji/blobcat/ablobcathyper.png",
"url": "https://gleasonator.com/emoji/blobcat/ablobcathyper.png",
"visible_in_picker": false
},
{
"shortcode": "ageblobcat",
"static_url": "https://gleasonator.com/emoji/blobcat/ageblobcat.png",
"url": "https://gleasonator.com/emoji/blobcat/ageblobcat.png",
"visible_in_picker": false
},
{
"shortcode": "blobcatphoto",
"static_url": "https://gleasonator.com/emoji/blobcat/blobcatphoto.png",
"url": "https://gleasonator.com/emoji/blobcat/blobcatphoto.png",
"visible_in_picker": false
}
],
"favourited": false,
"favourites_count": 0,
"id": "AGm7uC9DaAIGUa4KYK",
"in_reply_to_account_id": null,
"in_reply_to_id": null,
"language": null,
"media_attachments": [],
"mentions": [],
"muted": false,
"pinned": false,
"pleroma": {
"content": {
"text/plain": "Hello :ablobcathyper: :ageblobcat: 😂 world 😋 test :blobcatphoto:"
},
"conversation_id": "AGm7uC3BwZTOBtFW9w",
"direct_conversation_id": null,
"emoji_reactions": [],
"expires_at": null,
"in_reply_to_account_acct": null,
"local": true,
"parent_visible": false,
"pinned_at": null,
"quote": null,
"quote_url": null,
"quote_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/2dc79219-aed6-40c0-8818-0c2d26ed3436",
"url": "https://gleasonator.com/notice/AGm7uC9DaAIGUa4KYK",
"visibility": "public"
}

View File

@ -0,0 +1,63 @@
{
"id": "107831528995252317",
"created_at": "2022-02-20T17:35:55.224Z",
"in_reply_to_id": null,
"in_reply_to_account_id": null,
"sensitive": true,
"spoiler_text": "testing",
"visibility": "public",
"language": "en",
"uri": "https://fedibird.com/users/alex/statuses/107831528995252317",
"url": "https://fedibird.com/@alex/107831528995252317",
"replies_count": 0,
"reblogs_count": 0,
"favourites_count": 0,
"emoji_reactions_count": 0,
"emoji_reactions": [],
"content": "<p>hello world</p>",
"reblog": null,
"application": {
"name": "Web",
"website": null
},
"account": {
"id": "66768",
"username": "alex",
"acct": "alex",
"display_name": "",
"locked": false,
"bot": false,
"cat": false,
"discoverable": null,
"group": false,
"created_at": "2020-01-27T00:00:00.000Z",
"note": "<p></p>",
"url": "https://fedibird.com/@alex",
"avatar": "https://fedibird.com/avatars/original/missing.png",
"avatar_static": "https://fedibird.com/avatars/original/missing.png",
"header": "https://fedibird.com/headers/original/missing.png",
"header_static": "https://fedibird.com/headers/original/missing.png",
"followers_count": 1,
"following_count": 1,
"subscribing_count": 0,
"statuses_count": 5,
"last_status_at": "2022-02-20",
"emojis": [],
"fields": [],
"other_settings": {
"noindex": false,
"hide_network": false,
"hide_statuses_count": false,
"hide_following_count": false,
"hide_followers_count": false,
"enable_reaction": true
}
},
"media_attachments": [],
"mentions": [],
"tags": [],
"emojis": [],
"card": null,
"poll": null,
"quote": null
}

View File

@ -0,0 +1,201 @@
{
"account": {
"acct": "alex",
"avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png",
"avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png",
"bot": false,
"created_at": "2020-01-08T01:25:43.000Z",
"display_name": "Alex Gleason",
"emojis": [],
"fields": [
{
"name": "Website",
"value": "<a href=\"https://alexgleason.me\" rel=\"ugc\">https://alexgleason.me</a>"
},
{
"name": "Soapbox",
"value": "<a href=\"https://soapbox.pub\" rel=\"ugc\">https://soapbox.pub</a>"
},
{
"name": "Email",
"value": "alex@alexgleason.me"
},
{
"name": "Gender identity",
"value": "Soyboy"
},
{
"name": "Donate (PayPal)",
"value": "<a href=\"https://paypal.me/gleasonator\" rel=\"ugc\">https://paypal.me/gleasonator</a>"
},
{
"name": "$BTC",
"value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n"
},
{
"name": "$ETH",
"value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717"
},
{
"name": "$DOGE",
"value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D"
},
{
"name": "$XMR",
"value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK"
}
],
"followers_count": 2390,
"following_count": 1574,
"fqn": "alex@gleasonator.com",
"header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
"header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
"id": "9v5bmRalQvjOy0ECcC",
"last_status_at": "2022-02-23T17:54:41",
"locked": false,
"note": "I create Fediverse software that empowers people online.<br/><br/>I&#39;m vegan btw<br/><br/>Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.",
"pleroma": {
"accepts_chat_messages": true,
"also_known_as": [
"https://mitra.social/users/alex"
],
"ap_id": "https://gleasonator.com/users/alex",
"background_image": null,
"birthday": "1993-07-03",
"favicon": "https://gleasonator.com/favicon.png",
"hide_favorites": true,
"hide_followers": false,
"hide_followers_count": false,
"hide_follows": false,
"hide_follows_count": false,
"is_admin": true,
"is_confirmed": true,
"is_moderator": false,
"is_suggested": true,
"relationship": {},
"skip_thread_containment": false,
"tags": []
},
"source": {
"fields": [
{
"name": "Website",
"value": "https://alexgleason.me"
},
{
"name": "Soapbox",
"value": "https://soapbox.pub"
},
{
"name": "Email",
"value": "alex@alexgleason.me"
},
{
"name": "Gender identity",
"value": "Soyboy"
},
{
"name": "Donate (PayPal)",
"value": "https://paypal.me/gleasonator"
},
{
"name": "$BTC",
"value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n"
},
{
"name": "$ETH",
"value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717"
},
{
"name": "$DOGE",
"value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D"
},
{
"name": "$XMR",
"value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK"
}
],
"note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.",
"pleroma": {
"actor_type": "Person",
"discoverable": false
},
"sensitive": false
},
"statuses_count": 23502,
"url": "https://gleasonator.com/users/alex",
"username": "alex"
},
"application": null,
"bookmarked": false,
"card": null,
"content": "<p>What is tolerance?</p>",
"created_at": "2020-03-23T19:33:06.000Z",
"emojis": [],
"favourited": false,
"favourites_count": 47,
"id": "103874034847713213",
"in_reply_to_account_id": null,
"in_reply_to_id": null,
"language": null,
"media_attachments": [],
"mentions": [],
"muted": false,
"pinned": true,
"pleroma": {
"content": {
"text/plain": "What is tolerance?"
},
"conversation_id": "3023268",
"direct_conversation_id": null,
"emoji_reactions": [
{
"count": 3,
"me": false,
"name": "❤️"
}
],
"expires_at": null,
"in_reply_to_account_acct": null,
"local": true,
"parent_visible": false,
"pinned_at": "2021-11-23T01:38:44.000Z",
"quote": null,
"quote_url": null,
"quote_visible": false,
"spoiler_text": {
"text/plain": ""
},
"thread_muted": false
},
"poll": {
"emojis": [],
"expired": true,
"expires_at": "2020-03-24T19:33:06.000Z",
"id": "4930",
"multiple": false,
"options": [
{
"title": "Banning, censoring, and deplatforming anyone you disagree with",
"votes_count": 2
},
{
"title": "Promoting free speech, even for people and ideas you dislike",
"votes_count": 36
}
],
"voters_count": 2,
"votes_count": 38
},
"reblog": null,
"reblogged": false,
"reblogs_count": 26,
"replies_count": 14,
"sensitive": false,
"spoiler_text": "",
"tags": [],
"text": null,
"uri": "https://gleasonator.com/users/alex/statuses/103874034847713213",
"url": "https://gleasonator.com/notice/103874034847713213",
"visibility": "public"
}

View File

@ -2,7 +2,6 @@ import { getSettings } from '../settings';
import { import {
normalizeAccount, normalizeAccount,
normalizeStatus,
normalizePoll, normalizePoll,
} from './normalizer'; } from './normalizer';
@ -22,11 +21,17 @@ export function importAccounts(accounts) {
} }
export function importStatus(status, idempotencyKey) { export function importStatus(status, idempotencyKey) {
return { type: STATUS_IMPORT, status, idempotencyKey }; return (dispatch, getState) => {
const expandSpoilers = getSettings(getState()).get('expandSpoilers');
return dispatch({ type: STATUS_IMPORT, status, idempotencyKey, expandSpoilers });
};
} }
export function importStatuses(statuses) { export function importStatuses(statuses) {
return { type: STATUSES_IMPORT, statuses }; return (dispatch, getState) => {
const expandSpoilers = getSettings(getState()).get('expandSpoilers');
return dispatch({ type: STATUSES_IMPORT, statuses, expandSpoilers });
};
} }
export function importPolls(polls) { export function importPolls(polls) {
@ -60,11 +65,6 @@ export function importFetchedStatus(status, idempotencyKey) {
// Skip broken statuses // Skip broken statuses
if (isBroken(status)) return; if (isBroken(status)) return;
const normalOldStatus = getState().getIn(['statuses', status.id]);
const expandSpoilers = getSettings(getState()).get('expandSpoilers');
const normalizedStatus = normalizeStatus(status, normalOldStatus, expandSpoilers);
if (status.reblog?.id) { if (status.reblog?.id) {
dispatch(importFetchedStatus(status.reblog)); dispatch(importFetchedStatus(status.reblog));
} }
@ -83,7 +83,7 @@ export function importFetchedStatus(status, idempotencyKey) {
} }
dispatch(importFetchedAccount(status.account)); dispatch(importFetchedAccount(status.account));
dispatch(importStatus(normalizedStatus, idempotencyKey)); dispatch(importStatus(status, idempotencyKey));
}; };
} }
@ -113,10 +113,7 @@ export function importFetchedStatuses(statuses) {
// Skip broken statuses // Skip broken statuses
if (isBroken(status)) return; if (isBroken(status)) return;
const normalOldStatus = getState().getIn(['statuses', status.id]); normalStatuses.push(status);
const expandSpoilers = getSettings(getState()).get('expandSpoilers');
normalStatuses.push(normalizeStatus(status, normalOldStatus, expandSpoilers));
accounts.push(status.account); accounts.push(status.account);
if (status.reblog?.id) { if (status.reblog?.id) {

View File

@ -1,12 +1,8 @@
import escapeTextContentForBrowser from 'escape-html'; import escapeTextContentForBrowser from 'escape-html';
import { stripCompatibilityFeatures } from 'soapbox/utils/html';
import emojify from '../../features/emoji/emoji'; import emojify from '../../features/emoji/emoji';
import { unescapeHTML } from '../../utils/html'; import { unescapeHTML } from '../../utils/html';
const domParser = new DOMParser();
const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => { const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => {
obj[`:${emoji.shortcode}:`] = emoji; obj[`:${emoji.shortcode}:`] = emoji;
return obj; return obj;
@ -45,61 +41,6 @@ export function normalizeAccount(account) {
return account; return account;
} }
export function normalizeStatus(status, normalOldStatus, expandSpoilers) {
const normalStatus = { ...status };
// Some backends can return null, or omit these required fields
if (!normalStatus.emojis) normalStatus.emojis = [];
if (!normalStatus.spoiler_text) normalStatus.spoiler_text = '';
// Copy the pleroma object too, so we can modify our copy
if (status.pleroma) {
normalStatus.pleroma = { ...status.pleroma };
}
normalStatus.account = status.account.id;
if (status.reblog?.id) {
normalStatus.reblog = status.reblog.id;
}
if (status.poll?.id) {
normalStatus.poll = status.poll.id;
}
if (status.pleroma?.quote?.id) {
// Normalize quote to the top-level, so delete the original for performance
normalStatus.quote = status.pleroma.quote.id;
delete normalStatus.pleroma.quote;
} else if (status.quote?.id) {
// Fedibird compatibility, because why not
normalStatus.quote = status.quote.id;
} else if (status.quote_id) {
// Fedibird: fall back to quote_id
normalStatus.quote = status.quote_id;
}
// Only calculate these values when status first encountered
// Otherwise keep the ones already in the reducer
if (normalOldStatus) {
normalStatus.search_index = normalOldStatus.get('search_index');
normalStatus.contentHtml = normalOldStatus.get('contentHtml');
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
normalStatus.hidden = normalOldStatus.get('hidden');
} else {
const spoilerText = normalStatus.spoiler_text || '';
const searchContent = ([spoilerText, status.content].concat((status.poll?.options) ? status.poll.options.map(option => option.title) : [])).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
const emojiMap = makeEmojiMap(normalStatus);
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
normalStatus.contentHtml = stripCompatibilityFeatures(emojify(normalStatus.content, emojiMap));
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
}
return normalStatus;
}
export function normalizePoll(poll) { export function normalizePoll(poll) {
const normalPoll = { ...poll }; const normalPoll = { ...poll };

View File

@ -4,6 +4,7 @@ import { FormattedMessage } from 'react-intl';
import Icon from 'soapbox/components/icon'; import Icon from 'soapbox/components/icon';
import { captureException } from 'soapbox/monitoring'; import { captureException } from 'soapbox/monitoring';
import sourceCode from 'soapbox/utils/code';
export default class ErrorBoundary extends React.PureComponent { export default class ErrorBoundary extends React.PureComponent {
@ -72,7 +73,7 @@ export default class ErrorBoundary extends React.PureComponent {
<Icon src={require('@tabler/icons/icons/mood-sad.svg')} className='sad-face' /> <Icon src={require('@tabler/icons/icons/mood-sad.svg')} className='sad-face' />
<FormattedMessage id='alert.unexpected.message' defaultMessage='An unexpected error occurred.' /> <FormattedMessage id='alert.unexpected.message' defaultMessage='An unexpected error occurred.' />
<div className='return-home'> <div className='return-home'>
<a a href='/'> <a href='/'>
<Icon src={require('@tabler/icons/icons/arrow-back.svg')} /> <Icon src={require('@tabler/icons/icons/arrow-back.svg')} />
<FormattedMessage id='alert.unexpected.return_home' defaultMessage='Return Home' /> <FormattedMessage id='alert.unexpected.return_home' defaultMessage='Return Home' />
</a> </a>
@ -87,6 +88,7 @@ export default class ErrorBoundary extends React.PureComponent {
{browser && <p className='error-boundary__browser'> {browser && <p className='error-boundary__browser'>
{browser.getBrowserName()} {browser.getBrowserVersion()} {browser.getBrowserName()} {browser.getBrowserVersion()}
</p>} </p>}
<p className='error-boundary__version'>{sourceCode.displayName} {sourceCode.version}</p>
<p className='help-text'> <p className='help-text'>
<FormattedMessage <FormattedMessage
id='alert.unexpected.help_text' id='alert.unexpected.help_text'

View File

@ -43,39 +43,18 @@ class StatusReplyMentions extends ImmutablePureComponent {
const to = status.get('mentions', []); const to = status.get('mentions', []);
// The post is a reply, but it has no mentions. // The post is a reply, but it has no mentions.
// Rare, but it can happen.
if (to.size === 0) { if (to.size === 0) {
// The author is replying to themself. return (
if (status.get('in_reply_to_account_id') === status.getIn(['account', 'id'])) { <div className='reply-mentions'>
return ( <FormattedMessage
<div className='reply-mentions'> id='reply_mentions.reply_empty'
<FormattedMessage defaultMessage='Replying to post'
id='reply_mentions.reply' />
defaultMessage='Replying to {accounts}{more}' </div>
values={{ );
accounts: (<>
<HoverRefWrapper accountId={status.getIn(['account', 'id'])} inline>
<Link to={`/@${status.getIn(['account', 'acct'])}`} className='reply-mentions__account'>@{status.getIn(['account', 'username'])}</Link>
</HoverRefWrapper>
</>),
more: false,
}}
/>
</div>
);
} else {
// The reply-to is unknown. Rare, but it can happen.
return (
<div className='reply-mentions'>
<FormattedMessage
id='reply_mentions.reply_empty'
defaultMessage='Replying to post'
/>
</div>
);
}
} }
// The typical case with a reply-to and a list of mentions. // The typical case with a reply-to and a list of mentions.
return ( return (
<div className='reply-mentions'> <div className='reply-mentions'>

View File

@ -1,6 +1,7 @@
import { fromJS } from 'immutable'; import { Map as ImmutableMap } from 'immutable';
import { normalizeStatus } from 'soapbox/actions/importer/normalizer'; import { normalizeStatus } from 'soapbox/normalizers/status';
import { calculateStatus } from 'soapbox/reducers/statuses';
import { makeGetAccount } from 'soapbox/selectors'; import { makeGetAccount } from 'soapbox/selectors';
export const buildStatus = (state, scheduledStatus) => { export const buildStatus = (state, scheduledStatus) => {
@ -10,37 +11,19 @@ export const buildStatus = (state, scheduledStatus) => {
const params = scheduledStatus.get('params'); const params = scheduledStatus.get('params');
const account = getAccount(state, me); const account = getAccount(state, me);
const status = normalizeStatus({ const status = ImmutableMap({
account, account,
application: null,
bookmarked: false,
card: null,
content: params.get('text', '').replace(new RegExp('\n', 'g'), '<br>'), /* eslint-disable-line no-control-regex */ content: params.get('text', '').replace(new RegExp('\n', 'g'), '<br>'), /* eslint-disable-line no-control-regex */
created_at: params.get('scheduled_at'), created_at: params.get('scheduled_at'),
emojis: [],
favourited: false,
favourites_count: 0,
id: scheduledStatus.get('id'), id: scheduledStatus.get('id'),
in_reply_to_account_id: null,
in_reply_to_id: params.get('in_reply_to_id'), in_reply_to_id: params.get('in_reply_to_id'),
language: null,
media_attachments: scheduledStatus.get('media_attachments'), media_attachments: scheduledStatus.get('media_attachments'),
mentions: [],
muted: false,
pinned: false,
poll: params.get('poll'), poll: params.get('poll'),
reblog: null,
reblogged: false,
reblogs_count: 0,
replies_count: 0,
sensitive: params.get('sensitive'), sensitive: params.get('sensitive'),
spoiler_text: '',
tags: [],
text: null,
uri: `/scheduled_statuses/${scheduledStatus.get('id')}`, uri: `/scheduled_statuses/${scheduledStatus.get('id')}`,
url: `/scheduled_statuses/${scheduledStatus.get('id')}`, url: `/scheduled_statuses/${scheduledStatus.get('id')}`,
visibility: params.get('visibility'), visibility: params.get('visibility'),
}); });
return fromJS(status).set('account', account); return calculateStatus(normalizeStatus(status));
}; };

View File

@ -9,6 +9,7 @@ import Avatar from 'soapbox/components/avatar';
import DisplayName from 'soapbox/components/display_name'; import DisplayName from 'soapbox/components/display_name';
import RelativeTimestamp from 'soapbox/components/relative_timestamp'; import RelativeTimestamp from 'soapbox/components/relative_timestamp';
import StatusContent from 'soapbox/components/status_content'; import StatusContent from 'soapbox/components/status_content';
import StatusReplyMentions from 'soapbox/components/status_reply_mentions';
import PollPreview from 'soapbox/features/ui/components/poll_preview'; import PollPreview from 'soapbox/features/ui/components/poll_preview';
import { getDomain } from 'soapbox/utils/accounts'; import { getDomain } from 'soapbox/utils/accounts';
@ -63,6 +64,8 @@ class ScheduledStatus extends ImmutablePureComponent {
</div> </div>
</div> </div>
<StatusReplyMentions status={status} />
<StatusContent <StatusContent
status={status} status={status}
expanded expanded

View File

@ -1,7 +1,7 @@
import classNames from 'classnames'; import classNames from 'classnames';
import React from 'react'; import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage, injectIntl } from 'react-intl'; import { injectIntl } from 'react-intl';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Link, NavLink } from 'react-router-dom'; import { Link, NavLink } from 'react-router-dom';
@ -9,6 +9,7 @@ import Avatar from 'soapbox/components/avatar';
import DisplayName from 'soapbox/components/display_name'; import DisplayName from 'soapbox/components/display_name';
import RelativeTimestamp from 'soapbox/components/relative_timestamp'; import RelativeTimestamp from 'soapbox/components/relative_timestamp';
import StatusContent from 'soapbox/components/status_content'; import StatusContent from 'soapbox/components/status_content';
import StatusReplyMentions from 'soapbox/components/status_reply_mentions';
import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder_card'; import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder_card';
import PlaceholderMediaGallery from 'soapbox/features/placeholder/components/placeholder_media_gallery'; import PlaceholderMediaGallery from 'soapbox/features/placeholder/components/placeholder_media_gallery';
import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container'; import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container';
@ -50,56 +51,6 @@ class PendingStatus extends ImmutablePureComponent {
} }
} }
renderReplyMentions = () => {
const { status } = this.props;
if (!status.get('in_reply_to_id')) {
return null;
}
const to = status.get('mentions', []);
if (to.size === 0) {
if (status.get('in_reply_to_account_id') === status.getIn(['account', 'id'])) {
return (
<div className='reply-mentions'>
<FormattedMessage
id='reply_mentions.reply'
defaultMessage='Replying to {accounts}{more}'
values={{
accounts: <span className='reply-mentions__account'>@{status.getIn(['account', 'username'])}</span>,
more: false,
}}
/>
</div>
);
} else {
return (
<div className='reply-mentions'>
<FormattedMessage id='reply_mentions.reply_empty' defaultMessage='Replying to post' />
</div>
);
}
}
return (
<div className='reply-mentions'>
<FormattedMessage
id='reply_mentions.reply'
defaultMessage='Replying to {accounts}{more}'
values={{
accounts: to.slice(0, 2).map(account => (<>
<span key={account.username} className='reply-mentions__account'>@{account.username}</span>
{' '}
</>)),
more: to.size > 2 && <FormattedMessage id='reply_mentions.more' defaultMessage='and {count} more' values={{ count: to.size - 2 }} />,
}}
/>
</div>
);
}
render() { render() {
const { status, className } = this.props; const { status, className } = this.props;
if (!status) return null; if (!status) return null;
@ -137,7 +88,7 @@ class PendingStatus extends ImmutablePureComponent {
</div> </div>
</div> </div>
{this.renderReplyMentions()} <StatusReplyMentions status={status} />
<StatusContent <StatusContent
status={status} status={status}

View File

@ -1,63 +1,47 @@
import { fromJS } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import { normalizeStatus } from 'soapbox/actions/importer/normalizer'; import { normalizeStatus } from 'soapbox/normalizers/status';
import { makeGetAccount, makeGetStatus } from 'soapbox/selectors'; import { calculateStatus } from 'soapbox/reducers/statuses';
import { makeGetAccount } from 'soapbox/selectors';
const getAccount = makeGetAccount();
const buildMentions = pendingStatus => {
if (pendingStatus.get('in_reply_to_id')) {
return ImmutableList(pendingStatus.get('to') || []).map(acct => ImmutableMap({ acct }));
} else {
return ImmutableList();
}
};
const buildPoll = pendingStatus => {
if (pendingStatus.hasIn(['poll', 'options'])) {
return pendingStatus.get('poll').update('options', options => {
return options.map(title => ImmutableMap({ title }));
});
} else {
return null;
}
};
export const buildStatus = (state, pendingStatus, idempotencyKey) => { export const buildStatus = (state, pendingStatus, idempotencyKey) => {
const getAccount = makeGetAccount();
const getStatus = makeGetStatus();
const me = state.get('me'); const me = state.get('me');
const account = getAccount(state, me); const account = getAccount(state, me);
const inReplyToId = pendingStatus.get('in_reply_to_id');
let mentions; const status = ImmutableMap({
if (pendingStatus.get('in_reply_to_id')) {
const inReplyTo = getStatus(state, { id: pendingStatus.get('in_reply_to_id') });
if (inReplyTo.getIn(['account', 'id']) === me) {
mentions = ImmutableOrderedSet([account.get('acct')]).union(pendingStatus.get('to', []));
} else {
mentions = pendingStatus.get('to', []);
}
mentions = mentions.map(mention => ({
username: mention.split('@')[0],
}));
}
const status = normalizeStatus({
account, account,
application: null,
bookmarked: false,
card: null,
content: pendingStatus.get('status', '').replace(new RegExp('\n', 'g'), '<br>'), /* eslint-disable-line no-control-regex */ content: pendingStatus.get('status', '').replace(new RegExp('\n', 'g'), '<br>'), /* eslint-disable-line no-control-regex */
created_at: new Date(),
emojis: [],
favourited: false,
favourites_count: 0,
id: `末pending-${idempotencyKey}`, id: `末pending-${idempotencyKey}`,
in_reply_to_account_id: null, in_reply_to_account_id: state.getIn(['statuses', inReplyToId, 'account'], null),
in_reply_to_id: pendingStatus.get('in_reply_to_id'), in_reply_to_id: inReplyToId,
language: null, media_attachments: pendingStatus.get('media_ids', ImmutableList()).map(id => ImmutableMap({ id })),
media_attachments: pendingStatus.get('media_ids').map(id => ({ id })), mentions: buildMentions(pendingStatus),
mentions, poll: buildPoll(pendingStatus),
muted: false,
pinned: false,
poll: pendingStatus.get('poll', null),
quote: pendingStatus.get('quote_id', null), quote: pendingStatus.get('quote_id', null),
reblog: null,
reblogged: false,
reblogs_count: 0,
replies_count: 0,
sensitive: pendingStatus.get('sensitive', false), sensitive: pendingStatus.get('sensitive', false),
spoiler_text: '',
tags: [],
text: null,
uri: '',
url: '',
visibility: pendingStatus.get('visibility', 'public'), visibility: pendingStatus.get('visibility', 'public'),
}); });
return fromJS(status).set('account', account); return calculateStatus(normalizeStatus(status));
}; };

View File

@ -98,4 +98,50 @@ describe('normalizeInstance()', () => {
const result = normalizeInstance(instance); const result = normalizeInstance(instance);
expect(result.toJS()).toMatchObject(expected); expect(result.toJS()).toMatchObject(expected);
}); });
it('normalizes Fedibird instance', () => {
const instance = fromJS(require('soapbox/__fixtures__/fedibird-instance.json'));
const result = normalizeInstance(instance);
// Sets description_limit
expect(result.get('description_limit')).toEqual(1500);
// But otherwise, it's the same
expect(result.delete('description_limit')).toEqual(instance);
});
it('normalizes Mitra instance', () => {
const instance = fromJS(require('soapbox/__fixtures__/mitra-instance.json'));
const result = normalizeInstance(instance);
// Adds configuration and description_limit
expect(result.get('configuration') instanceof ImmutableMap).toBe(true);
expect(result.get('description_limit')).toBe(1500);
});
it('normalizes GoToSocial instance', () => {
const instance = fromJS(require('soapbox/__fixtures__/gotosocial-instance.json'));
const result = normalizeInstance(instance);
// Normalizes max_toot_chars
expect(result.getIn(['configuration', 'statuses', 'max_characters'])).toEqual(5000);
expect(result.has('max_toot_chars')).toBe(false);
// Adds configuration and description_limit
expect(result.get('configuration') instanceof ImmutableMap).toBe(true);
expect(result.get('description_limit')).toBe(1500);
});
it('normalizes Friendica instance', () => {
const instance = fromJS(require('soapbox/__fixtures__/friendica-instance.json'));
const result = normalizeInstance(instance);
// Normalizes max_toot_chars
expect(result.getIn(['configuration', 'statuses', 'max_characters'])).toEqual(200000);
expect(result.has('max_toot_chars')).toBe(false);
// Adds configuration and description_limit
expect(result.get('configuration') instanceof ImmutableMap).toBe(true);
expect(result.get('description_limit')).toBe(1500);
});
}); });

View File

@ -0,0 +1,154 @@
import { fromJS } from 'immutable';
import { normalizeStatus } from '../status';
describe('normalizeStatus', () => {
it('adds base fields', () => {
const status = fromJS({});
const result = normalizeStatus(status);
expect(result.get('emojis')).toEqual(fromJS([]));
expect(result.get('favourites_count')).toBe(0);
expect(result.get('mentions')).toEqual(fromJS([]));
expect(result.get('reblog')).toBe(null);
expect(result.get('uri')).toBe('');
expect(result.get('visibility')).toBe('public');
});
it('fixes the order of mentions', () => {
const status = fromJS(require('soapbox/__fixtures__/status-unordered-mentions.json'));
const expected = ['NEETzsche', 'alex', 'Lumeinshin', 'sneeden'];
const result = normalizeStatus(status)
.get('mentions')
.map(mention => mention.get('username'))
.toJS();
expect(result).toEqual(expected);
});
it('adds mention to self in self-reply on Mastodon', () => {
const status = fromJS(require('soapbox/__fixtures__/mastodon-reply-to-self.json'));
const expected = fromJS([{
id: '106801667066418367',
username: 'benis911',
acct: 'benis911',
url: 'https://mastodon.social/@benis911',
}]);
const result = normalizeStatus(status).get('mentions');
expect(result).toEqual(expected);
});
it('normalizes mentions with only acct', () => {
const status = fromJS({ mentions: [{ acct: 'alex@gleasonator.com' }] });
const expected = fromJS([{
acct: 'alex@gleasonator.com',
username: 'alex',
url: '',
}]);
const result = normalizeStatus(status).get('mentions');
expect(result).toEqual(expected);
});
it('normalizes Mitra attachments', () => {
const status = fromJS(require('soapbox/__fixtures__/mitra-status-with-attachments.json'));
const expected = fromJS([{
id: '017eeb0e-e5df-30a4-77a7-a929145cb836',
type: 'image',
url: 'https://mitra.social/media/8e04e6091bbbac79641b5812508683ce72c38693661c18d16040553f2371e18d.png',
preview_url: 'https://mitra.social/media/8e04e6091bbbac79641b5812508683ce72c38693661c18d16040553f2371e18d.png',
remote_url: 'https://mitra.social/media/8e04e6091bbbac79641b5812508683ce72c38693661c18d16040553f2371e18d.png',
}, {
id: '017eeb0e-e5e4-2a48-2889-afdebf368a54',
type: 'unknown',
url: 'https://mitra.social/media/8f72dc2e98572eb4ba7c3a902bca5f69c448fc4391837e5f8f0d4556280440ac',
preview_url: 'https://mitra.social/media/8f72dc2e98572eb4ba7c3a902bca5f69c448fc4391837e5f8f0d4556280440ac',
remote_url: 'https://mitra.social/media/8f72dc2e98572eb4ba7c3a902bca5f69c448fc4391837e5f8f0d4556280440ac',
}, {
id: '017eeb0e-e5e5-79fd-6054-8b6869b1db49',
type: 'unknown',
url: 'https://mitra.social/media/55a81a090247cc4fc127e5716bcf7964f6e0df9b584f85f4696c0b994747a4d0.oga',
preview_url: 'https://mitra.social/media/55a81a090247cc4fc127e5716bcf7964f6e0df9b584f85f4696c0b994747a4d0.oga',
remote_url: 'https://mitra.social/media/55a81a090247cc4fc127e5716bcf7964f6e0df9b584f85f4696c0b994747a4d0.oga',
}, {
id: '017eeb0e-e5e6-c416-a444-21e560c47839',
type: 'unknown',
url: 'https://mitra.social/media/0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0',
preview_url: 'https://mitra.social/media/0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0',
remote_url: 'https://mitra.social/media/0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0',
}]);
const result = normalizeStatus(status);
expect(result.get('media_attachments')).toEqual(expected);
});
it('leaves Pleroma attachments alone', () => {
const status = fromJS(require('soapbox/__fixtures__/pleroma-status-with-attachments.json'));
const result = normalizeStatus(status);
expect(status.get('media_attachments')).toEqual(result.get('media_attachments'));
});
it('normalizes Pleroma quote post', () => {
const status = fromJS(require('soapbox/__fixtures__/pleroma-quote-post.json'));
const result = normalizeStatus(status);
expect(result.get('quote')).toEqual(status.getIn(['pleroma', 'quote']));
expect(result.getIn(['pleroma', 'quote'])).toBe(undefined);
});
it('normalizes GoToSocial status', () => {
const status = fromJS(require('soapbox/__fixtures__/gotosocial-status.json'));
const result = normalizeStatus(status);
// Adds missing fields
const missing = {
in_reply_to_account_id: null,
in_reply_to_id: null,
reblog: null,
pinned: false,
quote: null,
};
expect(result.toJS()).toMatchObject(missing);
});
it('normalizes Friendica status', () => {
const status = fromJS(require('soapbox/__fixtures__/friendica-status.json'));
const result = normalizeStatus(status);
// Adds missing fields
const missing = {
pinned: false,
quote: null,
};
expect(result.toJS()).toMatchObject(missing);
});
it('normalizes poll and poll options', () => {
const status = fromJS({ poll: { options: [{ title: 'Apples' }] } });
const result = normalizeStatus(status);
const expected = {
options: [{ title: 'Apples', votes_count: 0 }],
emojis: [],
expired: false,
multiple: false,
voters_count: 0,
votes_count: 0,
};
expect(result.get('poll').toJS()).toMatchObject(expected);
expect(result.getIn(['poll', 'expires_at']) instanceof Date).toBe(true);
});
});

View File

@ -0,0 +1,161 @@
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { accountToMention } from 'soapbox/utils/accounts';
// Some backends can return null, or omit these required fields
const baseStatus = ImmutableMap({
application: null,
bookmarked: false,
card: null,
created_at: new Date(),
emojis: ImmutableList(),
favourited: false,
favourites_count: 0,
in_reply_to_account_id: null,
in_reply_to_id: null,
language: null,
mentions: ImmutableList(),
muted: false,
pinned: false,
reblog: null,
reblogged: false,
reblogs_count: 0,
replies_count: 0,
spoiler_text: '',
tags: ImmutableList(),
uri: '',
url: '',
visibility: 'public',
});
const basePollOption = ImmutableMap({ title: '', votes_count: 0 });
const basePoll = ImmutableMap({
emojis: ImmutableList(),
expired: false,
expires_at: new Date(Date.now() + 1000 * (60 * 5)), // 5 minutes
multiple: false,
options: ImmutableList(),
voters_count: 0,
votes_count: 0,
});
// Merger function for only overriding undefined values
const mergeDefined = (oldVal, newVal) => oldVal === undefined ? newVal : oldVal;
// Merge base status
const mergeBase = status => {
return status.mergeDeepWith(mergeDefined, baseStatus);
};
// Ensure attachments have required fields
// https://docs.joinmastodon.org/entities/attachment/
const normalizeAttachment = attachment => {
const url = [
attachment.get('url'),
attachment.get('preview_url'),
attachment.get('remote_url'),
].find(url => url) || '';
const base = ImmutableMap({
url,
preview_url: url,
remote_url: url,
});
return attachment.mergeWith(mergeDefined, base);
};
const normalizeAttachments = status => {
return status.update('media_attachments', ImmutableList(), attachments => {
return attachments.map(normalizeAttachment);
});
};
// Normalize mentions
const normalizeMention = mention => {
const base = ImmutableMap({
acct: '',
username: (mention.get('acct') || '').split('@')[0],
url: '',
});
return mention.mergeWith(mergeDefined, base);
};
const normalizeMentions = status => {
return status.update('mentions', ImmutableList(), mentions => {
return mentions.map(normalizeMention);
});
};
// Normalize poll option
const normalizePollOption = option => {
return option.mergeWith(mergeDefined, basePollOption);
};
// Normalize poll
const normalizePoll = status => {
if (status.hasIn(['poll', 'options'])) {
return status.update('poll', ImmutableMap(), poll => {
return poll.mergeWith(mergeDefined, basePoll).update('options', options => {
return options.map(normalizePollOption);
});
});
} else {
return status.set('poll', null);
}
};
// Fix order of mentions
const fixMentionsOrder = status => {
const mentions = status.get('mentions', ImmutableList());
const inReplyToAccountId = status.get('in_reply_to_account_id');
// Sort the replied-to mention to the top
const sorted = mentions.sort((a, b) => {
if (a.get('id') === inReplyToAccountId) {
return -1;
} else {
return 0;
}
});
return status.set('mentions', sorted);
};
// Add self to mentions if it's a reply to self
const addSelfMention = status => {
const accountId = status.getIn(['account', 'id']);
const isSelfReply = accountId === status.get('in_reply_to_account_id');
const hasSelfMention = accountId === status.getIn(['mentions', 0, 'id']);
if (isSelfReply && !hasSelfMention) {
const mention = accountToMention(status.get('account'));
return status.update('mentions', ImmutableList(), mentions => (
ImmutableList([mention]).concat(mentions)
));
} else {
return status;
}
};
// Move the quote to the top-level
const fixQuote = status => {
return status.withMutations(status => {
status.update('quote', quote => quote || status.getIn(['pleroma', 'quote']) || null);
status.deleteIn(['pleroma', 'quote']);
});
};
export const normalizeStatus = status => {
return status.withMutations(status => {
mergeBase(status);
normalizeAttachments(status);
normalizeMentions(status);
normalizePoll(status);
fixMentionsOrder(status);
addSelfMention(status);
fixQuote(status);
});
};

View File

@ -1,7 +1,6 @@
import { Map as ImmutableMap, fromJS } from 'immutable'; import { Map as ImmutableMap, fromJS } from 'immutable';
import { STATUS_IMPORT } from 'soapbox/actions/importer'; import { STATUS_IMPORT } from 'soapbox/actions/importer';
import { normalizeStatus } from 'soapbox/actions/importer/normalizer';
import { import {
STATUS_CREATE_REQUEST, STATUS_CREATE_REQUEST,
STATUS_CREATE_FAIL, STATUS_CREATE_FAIL,
@ -34,8 +33,8 @@ describe('statuses reducer', () => {
const quotedQuotePost = require('soapbox/__fixtures__/pleroma-quote-of-quote-post.json'); const quotedQuotePost = require('soapbox/__fixtures__/pleroma-quote-of-quote-post.json');
let state = undefined; let state = undefined;
state = reducer(state, { type: STATUS_IMPORT, status: normalizeStatus(quotePost) }); state = reducer(state, { type: STATUS_IMPORT, status: quotePost });
state = reducer(state, { type: STATUS_IMPORT, status: normalizeStatus(quotedQuotePost.pleroma.quote) }); state = reducer(state, { type: STATUS_IMPORT, status: quotedQuotePost.pleroma.quote });
expect(state.getIn(['AFmFMSpITT9xcOJKcK', 'quote'])).toEqual('AFmFLcd6XYVdjWCrOS'); expect(state.getIn(['AFmFMSpITT9xcOJKcK', 'quote'])).toEqual('AFmFLcd6XYVdjWCrOS');
}); });
@ -43,7 +42,7 @@ describe('statuses reducer', () => {
it('normalizes Mitra attachments', () => { it('normalizes Mitra attachments', () => {
const status = require('soapbox/__fixtures__/mitra-status-with-attachments.json'); const status = require('soapbox/__fixtures__/mitra-status-with-attachments.json');
const state = reducer(undefined, { type: STATUS_IMPORT, status: normalizeStatus(status) }); const state = reducer(undefined, { type: STATUS_IMPORT, status });
const expected = fromJS([{ const expected = fromJS([{
id: '017eeb0e-e5df-30a4-77a7-a929145cb836', id: '017eeb0e-e5df-30a4-77a7-a929145cb836',
@ -76,12 +75,52 @@ describe('statuses reducer', () => {
it('leaves Pleroma attachments alone', () => { it('leaves Pleroma attachments alone', () => {
const status = require('soapbox/__fixtures__/pleroma-status-with-attachments.json'); const status = require('soapbox/__fixtures__/pleroma-status-with-attachments.json');
const action = { type: STATUS_IMPORT, status: normalizeStatus(status) }; const action = { type: STATUS_IMPORT, status };
const state = reducer(undefined, action); const state = reducer(undefined, action);
const expected = fromJS(status.media_attachments); const expected = fromJS(status.media_attachments);
expect(state.getIn(['AGNkA21auFR5lnEAHw', 'media_attachments'])).toEqual(expected); expect(state.getIn(['AGNkA21auFR5lnEAHw', 'media_attachments'])).toEqual(expected);
}); });
it('hides CWs', () => {
const status = require('soapbox/__fixtures__/status-cw.json');
const action = { type: STATUS_IMPORT, status };
const hidden = reducer(undefined, action).getIn(['107831528995252317', 'hidden']);
expect(hidden).toBe(true);
});
it('expands CWs when expandSpoilers is enabled', () => {
const status = require('soapbox/__fixtures__/status-cw.json');
const action = { type: STATUS_IMPORT, status, expandSpoilers: true };
const hidden = reducer(undefined, action).getIn(['107831528995252317', 'hidden']);
expect(hidden).toBe(false);
});
it('parses custom emojis', () => {
const status = require('soapbox/__fixtures__/status-custom-emoji.json');
const action = { type: STATUS_IMPORT, status };
const expected = 'Hello <img draggable="false" class="emojione" alt=":ablobcathyper:" title=":ablobcathyper:" src="https://gleasonator.com/emoji/blobcat/ablobcathyper.png"> <img draggable="false" class="emojione" alt=":ageblobcat:" title=":ageblobcat:" src="https://gleasonator.com/emoji/blobcat/ageblobcat.png"> <img draggable="false" class="emojione" alt="😂" title=":joy:" src="/packs/emoji/1f602.svg"> world <img draggable="false" class="emojione" alt="😋" title=":yum:" src="/packs/emoji/1f60b.svg"> test <img draggable="false" class="emojione" alt=":blobcatphoto:" title=":blobcatphoto:" src="https://gleasonator.com/emoji/blobcat/blobcatphoto.png">';
const result = reducer(undefined, action).getIn(['AGm7uC9DaAIGUa4KYK', 'contentHtml']);
expect(result).toBe(expected);
});
it('builds search_index', () => {
const status = require('soapbox/__fixtures__/status-with-poll.json');
const action = { type: STATUS_IMPORT, status };
const expected = `What is tolerance?
Banning, censoring, and deplatforming anyone you disagree with
Promoting free speech, even for people and ideas you dislike`;
const result = reducer(undefined, action).getIn(['103874034847713213', 'search_index']);
expect(result).toEqual(expected);
});
}); });
describe('STATUS_CREATE_REQUEST', () => { describe('STATUS_CREATE_REQUEST', () => {

View File

@ -1,6 +1,10 @@
import escapeTextContentForBrowser from 'escape-html';
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import emojify from 'soapbox/features/emoji/emoji';
import { normalizeStatus } from 'soapbox/normalizers/status';
import { simulateEmojiReact, simulateUnEmojiReact } from 'soapbox/utils/emoji_reacts'; import { simulateEmojiReact, simulateUnEmojiReact } from 'soapbox/utils/emoji_reacts';
import { stripCompatibilityFeatures } from 'soapbox/utils/html';
import { import {
EMOJI_REACT_REQUEST, EMOJI_REACT_REQUEST,
@ -26,55 +30,53 @@ import {
} from '../actions/statuses'; } from '../actions/statuses';
import { TIMELINE_DELETE } from '../actions/timelines'; import { TIMELINE_DELETE } from '../actions/timelines';
// Ensure attachments have required fields const domParser = new DOMParser();
// https://docs.joinmastodon.org/entities/attachment/
const normalizeAttachment = attachment => {
const url = [
attachment.get('url'),
attachment.get('preview_url'),
attachment.get('remote_url'),
].find(url => url) || '';
const base = ImmutableMap({ const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
url, obj[`:${emoji.get('shortcode')}:`] = emoji.toJS();
preview_url: url, return obj;
remote_url: url, }, {});
});
return attachment.mergeWith((o, n) => o || n, base); const minifyStatus = status => {
}; return status.mergeWith((o, n) => n || o, {
account: status.getIn(['account', 'id']),
const normalizeAttachments = status => { reblog: status.getIn(['reblog', 'id']),
return status.update('media_attachments', ImmutableList(), attachments => { poll: status.getIn(['poll', 'id']),
return attachments.map(normalizeAttachment); quote: status.getIn(['quote', 'id']),
}); });
}; };
// Fix order of mentions // Only calculate these values when status first encountered
const fixMentions = status => { // Otherwise keep the ones already in the reducer
const mentions = status.get('mentions'); export const calculateStatus = (status, oldStatus, expandSpoilers = false) => {
const inReplyToAccountId = status.get('in_reply_to_account_id'); if (oldStatus) {
return status.merge({
search_index: oldStatus.get('search_index'),
contentHtml: oldStatus.get('contentHtml'),
spoilerHtml: oldStatus.get('spoilerHtml'),
hidden: oldStatus.get('hidden'),
});
} else {
const spoilerText = status.get('spoiler_text') || '';
const searchContent = (ImmutableList([spoilerText, status.get('content')]).concat(status.getIn(['poll', 'options'], ImmutableList()).map(option => option.get('title')))).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
const emojiMap = makeEmojiMap(status);
// Sort the replied-to mention to the top return status.merge({
const sorted = mentions.sort((a, b) => { search_index: domParser.parseFromString(searchContent, 'text/html').documentElement.textContent,
if (a.get('id') === inReplyToAccountId) { contentHtml: stripCompatibilityFeatures(emojify(status.get('content'), emojiMap)),
return -1; spoilerHtml: emojify(escapeTextContentForBrowser(spoilerText), emojiMap),
} else { hidden: expandSpoilers ? false : spoilerText.length > 0 || status.get('sensitive'),
return 0; });
} }
});
return status.set('mentions', sorted);
}; };
// Check whether a status is a quote by secondary characteristics
const isQuote = status => { const isQuote = status => {
return Boolean(status.get('quote_id') || status.getIn(['pleroma', 'quote_url'])); return Boolean(status.get('quote_id') || status.getIn(['pleroma', 'quote_url']));
}; };
// Preserve quote if an existing status already has it // Preserve quote if an existing status already has it
const fixQuote = (state, status) => { const fixQuote = (status, oldStatus) => {
const oldStatus = state.get(status.get('id'));
if (oldStatus && !status.get('quote') && isQuote(status)) { if (oldStatus && !status.get('quote') && isQuote(status)) {
return status return status
.set('quote', oldStatus.get('quote')) .set('quote', oldStatus.get('quote'))
@ -84,18 +86,22 @@ const fixQuote = (state, status) => {
} }
}; };
const normalizeStatus = (state, status) => { const fixStatus = (state, status, expandSpoilers) => {
const oldStatus = state.get(status.get('id'));
return status.withMutations(status => { return status.withMutations(status => {
fixMentions(status); normalizeStatus(status);
fixQuote(state, status); fixQuote(status, oldStatus);
normalizeAttachments(status); calculateStatus(status, oldStatus, expandSpoilers);
minifyStatus(status);
}); });
}; };
const importStatus = (state, status) => state.set(status.id, normalizeStatus(state, fromJS(status))); const importStatus = (state, status, expandSpoilers) =>
state.set(status.id, fixStatus(state, fromJS(status), expandSpoilers));
const importStatuses = (state, statuses) => const importStatuses = (state, statuses, expandSpoilers) =>
state.withMutations(mutable => statuses.forEach(status => importStatus(mutable, status))); state.withMutations(mutable => statuses.forEach(status => importStatus(mutable, status, expandSpoilers)));
const deleteStatus = (state, id, references) => { const deleteStatus = (state, id, references) => {
references.forEach(ref => { references.forEach(ref => {
@ -126,9 +132,9 @@ const initialState = ImmutableMap();
export default function statuses(state = initialState, action) { export default function statuses(state = initialState, action) {
switch(action.type) { switch(action.type) {
case STATUS_IMPORT: case STATUS_IMPORT:
return importStatus(state, action.status); return importStatus(state, action.status, action.expandSpoilers);
case STATUSES_IMPORT: case STATUSES_IMPORT:
return importStatuses(state, action.statuses); return importStatuses(state, action.statuses, action.expandSpoilers);
case STATUS_CREATE_REQUEST: case STATUS_CREATE_REQUEST:
return importPendingStatus(state, action.params); return importPendingStatus(state, action.params);
case STATUS_CREATE_FAIL: case STATUS_CREATE_FAIL:

View File

@ -6,6 +6,7 @@ import {
isStaff, isStaff,
isAdmin, isAdmin,
isModerator, isModerator,
accountToMention,
} from '../accounts'; } from '../accounts';
describe('getDomain', () => { describe('getDomain', () => {
@ -115,3 +116,19 @@ describe('isModerator', () => {
}); });
}); });
}); });
describe('accountToMention', () => {
it('converts the account to a mention', () => {
const account = fromJS(require('soapbox/__fixtures__/alex.json'));
const expected = fromJS({
id: '9v5bmRalQvjOy0ECcC',
username: 'alex',
acct: 'alex',
url: 'https://gleasonator.com/users/alex',
});
const result = accountToMention(account);
expect(result).toEqual(expected);
});
});

View File

@ -67,3 +67,12 @@ export const isRemote = account => !isLocal(account);
export const isVerified = account => ( export const isVerified = account => (
account.getIn(['pleroma', 'tags'], ImmutableList()).includes('verified') account.getIn(['pleroma', 'tags'], ImmutableList()).includes('verified')
); );
export const accountToMention = account => {
return ImmutableMap({
id: account.get('id'),
username: account.get('username'),
acct: account.get('acct'),
url: account.get('url'),
});
};

View File

@ -63,6 +63,11 @@
} }
} }
&__version {
font-size: 12px;
margin: 6px 0;
}
p.help-text { p.help-text {
text-align: left; text-align: left;
font-style: italic; font-style: italic;