sforkowany z mirror/soapbox
Merge remote-tracking branch 'soapbox/develop' into familiar-followers
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>ci-review-rules
commit
960dd0ed89
|
@ -4,4 +4,5 @@
|
|||
/public/**
|
||||
/tmp/**
|
||||
/coverage/**
|
||||
/custom/**
|
||||
!.eslintrc.js
|
||||
|
|
|
@ -8,17 +8,22 @@ cache:
|
|||
files:
|
||||
- yarn.lock
|
||||
paths:
|
||||
- node_modules
|
||||
- node_modules/
|
||||
|
||||
stages:
|
||||
- install
|
||||
- lint
|
||||
- test
|
||||
- build
|
||||
- deploy
|
||||
|
||||
before_script:
|
||||
- env
|
||||
- yarn
|
||||
install-dependencies:
|
||||
stage: install
|
||||
script:
|
||||
- yarn install --ignore-scripts
|
||||
artifacts:
|
||||
paths:
|
||||
- node_modules/
|
||||
|
||||
lint-js:
|
||||
stage: lint
|
||||
|
@ -87,6 +92,14 @@ docs-deploy:
|
|||
# - yarn
|
||||
# - yarn build
|
||||
|
||||
review:
|
||||
stage: deploy
|
||||
environment:
|
||||
name: review/$CI_COMMIT_REF_NAME
|
||||
url: https://$CI_COMMIT_REF_SLUG.git.soapbox.pub
|
||||
script:
|
||||
- npx -y surge static $CI_COMMIT_REF_SLUG.git.soapbox.pub
|
||||
|
||||
pages:
|
||||
stage: deploy
|
||||
before_script: []
|
||||
|
|
|
@ -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": "<a href=\"https://alexgleason.me\" rel=\"ugc\">https://alexgleason.me</a>"
|
||||
},
|
||||
{
|
||||
"name": "Pleroma+Soapbox",
|
||||
"value": "<a href=\"https://soapbox.pub\" rel=\"ugc\">https://soapbox.pub</a>"
|
||||
},
|
||||
{
|
||||
"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.<br/><br/><a class=\"hashtag\" data-tag=\"vegan\" href=\"https://gleasonator.com/tag/vegan\">#vegan</a> <a class=\"hashtag\" data-tag=\"freeculture\" href=\"https://gleasonator.com/tag/freeculture\">#freeculture</a> <a class=\"hashtag\" data-tag=\"atheist\" href=\"https://gleasonator.com/tag/atheist\">#atheist</a> <a class=\"hashtag\" data-tag=\"antiporn\" href=\"https://gleasonator.com/tag/antiporn\">#antiporn</a> <a class=\"hashtag\" data-tag=\"gendercritical\" href=\"https://gleasonator.com/tag/gendercritical\">#gendercritical</a>.<br/><br/>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": "<p>A</p>",
|
||||
"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": "<a href=\"https://alexgleason.me\" rel=\"ugc\">https://alexgleason.me</a>"
|
||||
},
|
||||
{
|
||||
"name": "Pleroma+Soapbox",
|
||||
"value": "<a href=\"https://soapbox.pub\" rel=\"ugc\">https://soapbox.pub</a>"
|
||||
},
|
||||
{
|
||||
"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.<br/><br/><a class=\"hashtag\" data-tag=\"vegan\" href=\"https://gleasonator.com/tag/vegan\">#vegan</a> <a class=\"hashtag\" data-tag=\"freeculture\" href=\"https://gleasonator.com/tag/freeculture\">#freeculture</a> <a class=\"hashtag\" data-tag=\"atheist\" href=\"https://gleasonator.com/tag/atheist\">#atheist</a> <a class=\"hashtag\" data-tag=\"antiporn\" href=\"https://gleasonator.com/tag/antiporn\">#antiporn</a> <a class=\"hashtag\" data-tag=\"gendercritical\" href=\"https://gleasonator.com/tag/gendercritical\">#gendercritical</a>.<br/><br/>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": "<p>B</p>",
|
||||
"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": "<a href=\"https://alexgleason.me\" rel=\"ugc\">https://alexgleason.me</a>"
|
||||
},
|
||||
{
|
||||
"name": "Pleroma+Soapbox",
|
||||
"value": "<a href=\"https://soapbox.pub\" rel=\"ugc\">https://soapbox.pub</a>"
|
||||
},
|
||||
{
|
||||
"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.<br/><br/><a class=\"hashtag\" data-tag=\"vegan\" href=\"https://gleasonator.com/tag/vegan\">#vegan</a> <a class=\"hashtag\" data-tag=\"freeculture\" href=\"https://gleasonator.com/tag/freeculture\">#freeculture</a> <a class=\"hashtag\" data-tag=\"atheist\" href=\"https://gleasonator.com/tag/atheist\">#atheist</a> <a class=\"hashtag\" data-tag=\"antiporn\" href=\"https://gleasonator.com/tag/antiporn\">#antiporn</a> <a class=\"hashtag\" data-tag=\"gendercritical\" href=\"https://gleasonator.com/tag/gendercritical\">#gendercritical</a>.<br/><br/>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": "<p>C</p>",
|
||||
"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": "<a href=\"https://alexgleason.me\" rel=\"ugc\">https://alexgleason.me</a>"
|
||||
},
|
||||
{
|
||||
"name": "Pleroma+Soapbox",
|
||||
"value": "<a href=\"https://soapbox.pub\" rel=\"ugc\">https://soapbox.pub</a>"
|
||||
},
|
||||
{
|
||||
"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.<br/><br/><a class=\"hashtag\" data-tag=\"vegan\" href=\"https://gleasonator.com/tag/vegan\">#vegan</a> <a class=\"hashtag\" data-tag=\"freeculture\" href=\"https://gleasonator.com/tag/freeculture\">#freeculture</a> <a class=\"hashtag\" data-tag=\"atheist\" href=\"https://gleasonator.com/tag/atheist\">#atheist</a> <a class=\"hashtag\" data-tag=\"antiporn\" href=\"https://gleasonator.com/tag/antiporn\">#antiporn</a> <a class=\"hashtag\" data-tag=\"gendercritical\" href=\"https://gleasonator.com/tag/gendercritical\">#gendercritical</a>.<br/><br/>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": "<p>E</p>",
|
||||
"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": "<a href=\"https://alexgleason.me\" rel=\"ugc\">https://alexgleason.me</a>"
|
||||
},
|
||||
{
|
||||
"name": "Pleroma+Soapbox",
|
||||
"value": "<a href=\"https://soapbox.pub\" rel=\"ugc\">https://soapbox.pub</a>"
|
||||
},
|
||||
{
|
||||
"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.<br/><br/><a class=\"hashtag\" data-tag=\"vegan\" href=\"https://gleasonator.com/tag/vegan\">#vegan</a> <a class=\"hashtag\" data-tag=\"freeculture\" href=\"https://gleasonator.com/tag/freeculture\">#freeculture</a> <a class=\"hashtag\" data-tag=\"atheist\" href=\"https://gleasonator.com/tag/atheist\">#atheist</a> <a class=\"hashtag\" data-tag=\"antiporn\" href=\"https://gleasonator.com/tag/antiporn\">#antiporn</a> <a class=\"hashtag\" data-tag=\"gendercritical\" href=\"https://gleasonator.com/tag/gendercritical\">#gendercritical</a>.<br/><br/>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": "<p>F</p>",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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": "<a href=\"https://alexgleason.me\" rel=\"ugc\">https://alexgleason.me</a>"
|
||||
},
|
||||
{
|
||||
"name": "Pleroma+Soapbox",
|
||||
"value": "<a href=\"https://soapbox.pub\" rel=\"ugc\">https://soapbox.pub</a>"
|
||||
},
|
||||
{
|
||||
"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.<br/><br/><a class=\"hashtag\" data-tag=\"vegan\" href=\"https://gleasonator.com/tag/vegan\">#vegan</a> <a class=\"hashtag\" data-tag=\"freeculture\" href=\"https://gleasonator.com/tag/freeculture\">#freeculture</a> <a class=\"hashtag\" data-tag=\"atheist\" href=\"https://gleasonator.com/tag/atheist\">#atheist</a> <a class=\"hashtag\" data-tag=\"antiporn\" href=\"https://gleasonator.com/tag/antiporn\">#antiporn</a> <a class=\"hashtag\" data-tag=\"gendercritical\" href=\"https://gleasonator.com/tag/gendercritical\">#gendercritical</a>.<br/><br/>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": "<p>A</p>",
|
||||
"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": "<a href=\"https://alexgleason.me\" rel=\"ugc\">https://alexgleason.me</a>"
|
||||
},
|
||||
{
|
||||
"name": "Pleroma+Soapbox",
|
||||
"value": "<a href=\"https://soapbox.pub\" rel=\"ugc\">https://soapbox.pub</a>"
|
||||
},
|
||||
{
|
||||
"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.<br/><br/><a class=\"hashtag\" data-tag=\"vegan\" href=\"https://gleasonator.com/tag/vegan\">#vegan</a> <a class=\"hashtag\" data-tag=\"freeculture\" href=\"https://gleasonator.com/tag/freeculture\">#freeculture</a> <a class=\"hashtag\" data-tag=\"atheist\" href=\"https://gleasonator.com/tag/atheist\">#atheist</a> <a class=\"hashtag\" data-tag=\"antiporn\" href=\"https://gleasonator.com/tag/antiporn\">#antiporn</a> <a class=\"hashtag\" data-tag=\"gendercritical\" href=\"https://gleasonator.com/tag/gendercritical\">#gendercritical</a>.<br/><br/>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": "<p>C</p>",
|
||||
"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": "<a href=\"https://alexgleason.me\" rel=\"ugc\">https://alexgleason.me</a>"
|
||||
},
|
||||
{
|
||||
"name": "Pleroma+Soapbox",
|
||||
"value": "<a href=\"https://soapbox.pub\" rel=\"ugc\">https://soapbox.pub</a>"
|
||||
},
|
||||
{
|
||||
"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.<br/><br/><a class=\"hashtag\" data-tag=\"vegan\" href=\"https://gleasonator.com/tag/vegan\">#vegan</a> <a class=\"hashtag\" data-tag=\"freeculture\" href=\"https://gleasonator.com/tag/freeculture\">#freeculture</a> <a class=\"hashtag\" data-tag=\"atheist\" href=\"https://gleasonator.com/tag/atheist\">#atheist</a> <a class=\"hashtag\" data-tag=\"antiporn\" href=\"https://gleasonator.com/tag/antiporn\">#antiporn</a> <a class=\"hashtag\" data-tag=\"gendercritical\" href=\"https://gleasonator.com/tag/gendercritical\">#gendercritical</a>.<br/><br/>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": "<p>D</p>",
|
||||
"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": "<a href=\"https://alexgleason.me\" rel=\"ugc\">https://alexgleason.me</a>"
|
||||
},
|
||||
{
|
||||
"name": "Pleroma+Soapbox",
|
||||
"value": "<a href=\"https://soapbox.pub\" rel=\"ugc\">https://soapbox.pub</a>"
|
||||
},
|
||||
{
|
||||
"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.<br/><br/><a class=\"hashtag\" data-tag=\"vegan\" href=\"https://gleasonator.com/tag/vegan\">#vegan</a> <a class=\"hashtag\" data-tag=\"freeculture\" href=\"https://gleasonator.com/tag/freeculture\">#freeculture</a> <a class=\"hashtag\" data-tag=\"atheist\" href=\"https://gleasonator.com/tag/atheist\">#atheist</a> <a class=\"hashtag\" data-tag=\"antiporn\" href=\"https://gleasonator.com/tag/antiporn\">#antiporn</a> <a class=\"hashtag\" data-tag=\"gendercritical\" href=\"https://gleasonator.com/tag/gendercritical\">#gendercritical</a>.<br/><br/>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": "<p>E</p>",
|
||||
"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": "<a href=\"https://alexgleason.me\" rel=\"ugc\">https://alexgleason.me</a>"
|
||||
},
|
||||
{
|
||||
"name": "Pleroma+Soapbox",
|
||||
"value": "<a href=\"https://soapbox.pub\" rel=\"ugc\">https://soapbox.pub</a>"
|
||||
},
|
||||
{
|
||||
"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.<br/><br/><a class=\"hashtag\" data-tag=\"vegan\" href=\"https://gleasonator.com/tag/vegan\">#vegan</a> <a class=\"hashtag\" data-tag=\"freeculture\" href=\"https://gleasonator.com/tag/freeculture\">#freeculture</a> <a class=\"hashtag\" data-tag=\"atheist\" href=\"https://gleasonator.com/tag/atheist\">#atheist</a> <a class=\"hashtag\" data-tag=\"antiporn\" href=\"https://gleasonator.com/tag/antiporn\">#antiporn</a> <a class=\"hashtag\" data-tag=\"gendercritical\" href=\"https://gleasonator.com/tag/gendercritical\">#gendercritical</a>.<br/><br/>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": "<p>F</p>",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -98,7 +98,7 @@ export const ensureComposeIsVisible = (getState, routerHistory) => {
|
|||
}
|
||||
};
|
||||
|
||||
export function setComposeToStatus(status, rawText, spoilerText, contentType) {
|
||||
export function setComposeToStatus(status, rawText, spoilerText, contentType, withRedraft) {
|
||||
return (dispatch, getState) => {
|
||||
const { instance } = getState();
|
||||
const { explicitAddressing } = getFeatures(instance);
|
||||
|
@ -111,6 +111,7 @@ export function setComposeToStatus(status, rawText, spoilerText, contentType) {
|
|||
spoilerText,
|
||||
contentType,
|
||||
v: parseVersion(instance.version),
|
||||
withRedraft,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,6 +6,9 @@ export const CUSTOM_EMOJIS_FETCH_FAIL = 'CUSTOM_EMOJIS_FETCH_FAIL';
|
|||
|
||||
export function fetchCustomEmojis() {
|
||||
return (dispatch, getState) => {
|
||||
const me = getState().get('me');
|
||||
if (!me) return;
|
||||
|
||||
dispatch(fetchCustomEmojisRequest());
|
||||
|
||||
api(getState).get('/api/v1/custom_emojis').then(response => {
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
import api from '../api';
|
||||
|
||||
export const IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST = 'IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST';
|
||||
export const IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS = 'IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS';
|
||||
export const IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL = 'IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL';
|
||||
|
||||
export const fetchAccountIdentityProofs = accountId => (dispatch, getState) => {
|
||||
dispatch(fetchAccountIdentityProofsRequest(accountId));
|
||||
|
||||
api(getState).get(`/api/v1/accounts/${accountId}/identity_proofs`)
|
||||
.then(({ data }) => dispatch(fetchAccountIdentityProofsSuccess(accountId, data)))
|
||||
.catch(err => dispatch(fetchAccountIdentityProofsFail(accountId, err)));
|
||||
};
|
||||
|
||||
export const fetchAccountIdentityProofsRequest = id => ({
|
||||
type: IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const fetchAccountIdentityProofsSuccess = (accountId, identity_proofs) => ({
|
||||
type: IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS,
|
||||
accountId,
|
||||
identity_proofs,
|
||||
});
|
||||
|
||||
export const fetchAccountIdentityProofsFail = (accountId, error) => ({
|
||||
type: IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL,
|
||||
accountId,
|
||||
error,
|
||||
});
|
|
@ -149,7 +149,6 @@ export function favourite(status) {
|
|||
dispatch(favouriteRequest(status));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/favourite`).then(function(response) {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(favouriteSuccess(status));
|
||||
}).catch(function(error) {
|
||||
dispatch(favouriteFail(status, error));
|
||||
|
|
|
@ -86,7 +86,7 @@ export function changePassword(oldPassword, newPassword, confirmation) {
|
|||
export function resetPassword(usernameOrEmail) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const v = parseVersion(state.instance);
|
||||
const v = parseVersion(state.instance.version);
|
||||
|
||||
dispatch({ type: RESET_PASSWORD_REQUEST });
|
||||
|
||||
|
|
|
@ -64,8 +64,9 @@ export function fetchSoapboxConfig(host) {
|
|||
return dispatch(fetchFrontendConfigurations()).then(data => {
|
||||
if (data.soapbox_fe) {
|
||||
dispatch(importSoapboxConfig(data.soapbox_fe, host));
|
||||
return data.soapbox_fe;
|
||||
} else {
|
||||
dispatch(fetchSoapboxJson(host));
|
||||
return dispatch(fetchSoapboxJson(host));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
@ -79,7 +80,7 @@ export function loadSoapboxConfig() {
|
|||
return (dispatch, getState) => {
|
||||
const host = getHost(getState());
|
||||
|
||||
return dispatch(rememberSoapboxConfig(host)).finally(() => {
|
||||
return dispatch(rememberSoapboxConfig(host)).then(() => {
|
||||
return dispatch(fetchSoapboxConfig(host));
|
||||
});
|
||||
};
|
||||
|
@ -87,9 +88,10 @@ export function loadSoapboxConfig() {
|
|||
|
||||
export function fetchSoapboxJson(host) {
|
||||
return (dispatch, getState) => {
|
||||
staticClient.get('/instance/soapbox.json').then(({ data }) => {
|
||||
return staticClient.get('/instance/soapbox.json').then(({ data }) => {
|
||||
if (!isObject(data)) throw 'soapbox.json failed';
|
||||
dispatch(importSoapboxConfig(data, host));
|
||||
return data;
|
||||
}).catch(error => {
|
||||
dispatch(soapboxConfigFail(error, host));
|
||||
});
|
||||
|
|
|
@ -98,7 +98,7 @@ export const editStatus = (id) => (dispatch, getState) => {
|
|||
|
||||
api(getState).get(`/api/v1/statuses/${id}/source`).then(response => {
|
||||
dispatch({ type: STATUS_FETCH_SOURCE_SUCCESS });
|
||||
dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text));
|
||||
dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text, false));
|
||||
dispatch(openModal('COMPOSE'));
|
||||
}).catch(error => {
|
||||
dispatch({ type: STATUS_FETCH_SOURCE_FAIL, error });
|
||||
|
@ -139,7 +139,7 @@ export function deleteStatus(id, routerHistory, withRedraft = false) {
|
|||
dispatch(deleteFromTimelines(id));
|
||||
|
||||
if (withRedraft) {
|
||||
dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text, response.data.pleroma?.content_type));
|
||||
dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text, response.data.pleroma?.content_type, withRedraft));
|
||||
dispatch(openModal('COMPOSE'));
|
||||
}
|
||||
}).catch(error => {
|
||||
|
@ -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 {
|
||||
|
|
|
@ -8,13 +8,13 @@ import { importFetchedAccounts } from './importer';
|
|||
|
||||
export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST';
|
||||
export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS';
|
||||
export const SUGGESTIONS_FETCH_FAIL = 'SUGGESTIONS_FETCH_FAIL';
|
||||
export const SUGGESTIONS_FETCH_FAIL = 'SUGGESTIONS_FETCH_FAIL';
|
||||
|
||||
export const SUGGESTIONS_DISMISS = 'SUGGESTIONS_DISMISS';
|
||||
|
||||
export const SUGGESTIONS_V2_FETCH_REQUEST = 'SUGGESTIONS_V2_FETCH_REQUEST';
|
||||
export const SUGGESTIONS_V2_FETCH_SUCCESS = 'SUGGESTIONS_V2_FETCH_SUCCESS';
|
||||
export const SUGGESTIONS_V2_FETCH_FAIL = 'SUGGESTIONS_V2_FETCH_FAIL';
|
||||
export const SUGGESTIONS_V2_FETCH_FAIL = 'SUGGESTIONS_V2_FETCH_FAIL';
|
||||
|
||||
export function fetchSuggestionsV1(params = {}) {
|
||||
return (dispatch, getState) => {
|
||||
|
@ -48,23 +48,26 @@ export function fetchSuggestionsV2(params = {}) {
|
|||
export function fetchSuggestions(params = { limit: 50 }) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const me = state.get('me');
|
||||
const instance = state.get('instance');
|
||||
const features = getFeatures(instance);
|
||||
|
||||
if (!me) return;
|
||||
|
||||
if (features.suggestionsV2) {
|
||||
dispatch(fetchSuggestionsV2(params))
|
||||
.then(suggestions => {
|
||||
const accountIds = suggestions.map(({ account }) => account.id);
|
||||
dispatch(fetchRelationships(accountIds));
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch(() => { });
|
||||
} else if (features.suggestions) {
|
||||
dispatch(fetchSuggestionsV1(params))
|
||||
.then(accounts => {
|
||||
const accountIds = accounts.map(({ id }) => id);
|
||||
dispatch(fetchRelationships(accountIds));
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch(() => { });
|
||||
} else {
|
||||
// Do nothing
|
||||
}
|
||||
|
|
|
@ -323,6 +323,20 @@ function requestPhoneVerification(phone) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the user's phone number to Pepe to re-request confirmation
|
||||
* @param {string} phone
|
||||
* @returns {promise}
|
||||
*/
|
||||
function reRequestPhoneVerification(phone) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: SET_LOADING });
|
||||
|
||||
return api(getState).post('/api/v1/pepe/reverify_sms/request', { phone })
|
||||
.finally(() => dispatch({ type: SET_LOADING, value: false }));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm the user's phone number with Pepe
|
||||
* @param {string} code
|
||||
|
@ -345,6 +359,20 @@ function confirmPhoneVerification(code) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-Confirm the user's phone number with Pepe
|
||||
* @param {string} code
|
||||
* @returns {promise}
|
||||
*/
|
||||
function reConfirmPhoneVerification(code) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: SET_LOADING });
|
||||
|
||||
return api(getState).post('/api/v1/pepe/reverify_sms/confirm', { code })
|
||||
.finally(() => dispatch({ type: SET_LOADING, value: false }));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm the user's age with Pepe
|
||||
* @param {date} birthday
|
||||
|
@ -404,6 +432,8 @@ export {
|
|||
requestEmailVerification,
|
||||
checkEmailVerification,
|
||||
postEmailVerification,
|
||||
reConfirmPhoneVerification,
|
||||
requestPhoneVerification,
|
||||
reRequestPhoneVerification,
|
||||
verifyAge,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
import React from 'react';
|
||||
|
||||
import { render, screen } from '../../jest/test-helpers';
|
||||
import { normalizeAccount } from '../../normalizers';
|
||||
import Account from '../account';
|
||||
|
||||
describe('<Account />', () => {
|
||||
it('renders account name and username', () => {
|
||||
const account = normalizeAccount({
|
||||
id: '1',
|
||||
acct: 'justin-username',
|
||||
display_name: 'Justin L',
|
||||
avatar: 'test.jpg',
|
||||
});
|
||||
|
||||
const store = {
|
||||
accounts: ImmutableMap({
|
||||
'1': account,
|
||||
}),
|
||||
};
|
||||
|
||||
render(<Account account={account} />, null, store);
|
||||
expect(screen.getByTestId('account')).toHaveTextContent('Justin L');
|
||||
expect(screen.getByTestId('account')).toHaveTextContent(/justin-username/i);
|
||||
});
|
||||
|
||||
describe('verification badge', () => {
|
||||
it('renders verification badge', () => {
|
||||
const account = normalizeAccount({
|
||||
id: '1',
|
||||
acct: 'justin-username',
|
||||
display_name: 'Justin L',
|
||||
avatar: 'test.jpg',
|
||||
verified: true,
|
||||
});
|
||||
|
||||
const store = {
|
||||
accounts: ImmutableMap({
|
||||
'1': account,
|
||||
}),
|
||||
};
|
||||
|
||||
render(<Account account={account} />, null, store);
|
||||
expect(screen.getByTestId('verified-badge')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render verification badge', () => {
|
||||
const account = normalizeAccount({
|
||||
id: '1',
|
||||
acct: 'justin-username',
|
||||
display_name: 'Justin L',
|
||||
avatar: 'test.jpg',
|
||||
verified: false,
|
||||
});
|
||||
|
||||
const store = {
|
||||
accounts: ImmutableMap({
|
||||
'1': account,
|
||||
}),
|
||||
};
|
||||
|
||||
render(<Account account={account} />, null, store);
|
||||
expect(screen.queryAllByTestId('verified-badge')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,12 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { render, screen } from '../../jest/test-helpers';
|
||||
import ColumnBackButton from '../column_back_button';
|
||||
|
||||
describe('<ColumnBackButton />', () => {
|
||||
it('renders correctly', () => {
|
||||
render(<ColumnBackButton />);
|
||||
|
||||
expect(screen.getByRole('button')).toHaveTextContent('Back');
|
||||
});
|
||||
});
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||
import { normalizeAccount } from 'soapbox/normalizers';
|
||||
|
||||
import { render, screen } from '../../jest/test-helpers';
|
||||
import DisplayName from '../display_name';
|
||||
import DisplayName from '../display-name';
|
||||
|
||||
describe('<DisplayName />', () => {
|
||||
it('renders display name + account name', () => {
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import Bundle from 'soapbox/features/ui/components/bundle';
|
||||
import { MediaGallery } from 'soapbox/features/ui/util/async-components';
|
||||
|
||||
import type { List as ImmutableList } from 'immutable';
|
||||
|
||||
interface IAttachmentThumbs {
|
||||
media: ImmutableList<Immutable.Record<any>>
|
||||
onClick?(): void
|
||||
sensitive?: boolean
|
||||
}
|
||||
|
||||
const AttachmentThumbs = (props: IAttachmentThumbs) => {
|
||||
const { media, onClick, sensitive } = props;
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const renderLoading = () => <div className='media-gallery--compact' />;
|
||||
const onOpenMedia = (media: Immutable.Record<any>, index: number) => dispatch(openModal('MEDIA', { media, index }));
|
||||
|
||||
return (
|
||||
<div className='attachment-thumbs'>
|
||||
<Bundle fetchComponent={MediaGallery} loading={renderLoading}>
|
||||
{(Component: any) => (
|
||||
<Component
|
||||
media={media}
|
||||
onOpenMedia={onOpenMedia}
|
||||
height={50}
|
||||
compact
|
||||
sensitive={sensitive}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
|
||||
{onClick && (
|
||||
<div className='attachment-thumbs__clickable-region' onClick={onClick} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AttachmentThumbs;
|
|
@ -1,52 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import Bundle from 'soapbox/features/ui/components/bundle';
|
||||
import { MediaGallery } from 'soapbox/features/ui/util/async-components';
|
||||
|
||||
export default @connect()
|
||||
class AttachmentThumbs extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
media: ImmutablePropTypes.list.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
sensitive: PropTypes.bool,
|
||||
};
|
||||
|
||||
renderLoading() {
|
||||
return <div className='media-gallery--compact' />;
|
||||
}
|
||||
|
||||
onOpenMedia = (media, index) => {
|
||||
this.props.dispatch(openModal('MEDIA', { media, index }));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { media, onClick, sensitive } = this.props;
|
||||
|
||||
return (
|
||||
<div className='attachment-thumbs'>
|
||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoading}>
|
||||
{Component => (
|
||||
<Component
|
||||
media={media}
|
||||
onOpenMedia={this.onOpenMedia}
|
||||
height={50}
|
||||
compact
|
||||
sensitive={sensitive}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
{onClick && (
|
||||
<div className='attachment-thumbs__clickable-region' onClick={onClick} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -14,7 +14,7 @@ import AutosuggestEmoji from './autosuggest_emoji';
|
|||
const textAtCursorMatchesToken = (str, caretPosition) => {
|
||||
let word;
|
||||
|
||||
const left = str.slice(0, caretPosition).search(/\S+$/);
|
||||
const left = str.slice(0, caretPosition).search(/\S+$/);
|
||||
const right = str.slice(caretPosition).search(/\s/);
|
||||
|
||||
if (right < 0) {
|
||||
|
@ -69,7 +69,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
onChange = (e) => {
|
||||
const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart);
|
||||
const [tokenStart, token] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart);
|
||||
|
||||
if (token !== null && this.state.lastToken !== token) {
|
||||
this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
|
||||
|
@ -123,7 +123,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
break;
|
||||
case 'Enter':
|
||||
case 'Tab':
|
||||
// Select suggestion
|
||||
// Select suggestion
|
||||
if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
@ -200,13 +200,13 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
|
||||
if (typeof suggestion === 'object') {
|
||||
inner = <AutosuggestEmoji emoji={suggestion} />;
|
||||
key = suggestion.id;
|
||||
key = suggestion.id;
|
||||
} else if (suggestion[0] === '#') {
|
||||
inner = suggestion;
|
||||
key = suggestion;
|
||||
key = suggestion;
|
||||
} else {
|
||||
inner = <AutosuggestAccount id={suggestion} />;
|
||||
key = suggestion;
|
||||
key = suggestion;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -257,7 +257,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
|
||||
<Textarea
|
||||
ref={this.setTextarea}
|
||||
className={classNames('transition-[min-height] motion-reduce:transition-none dark:bg-slate-800 px-0 border-0 text-gray-800 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500 resize-none w-full focus:shadow-none focus:border-0 focus:ring-0', {
|
||||
className={classNames('transition-[min-height] motion-reduce:transition-none dark:bg-slate-800 px-0 mt-1 border-0 text-gray-800 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500 resize-none w-full focus:shadow-none focus:border-0 focus:ring-0', {
|
||||
'min-h-[40px]': condensed,
|
||||
'min-h-[100px]': !condensed,
|
||||
})}
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import Icon from 'soapbox/components/icon';
|
||||
|
||||
export default @withRouter
|
||||
class ColumnBackButton extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
to: PropTypes.string,
|
||||
history: PropTypes.object,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
const { to } = this.props;
|
||||
|
||||
if (window.history?.length === 1) {
|
||||
this.props.history.push(to ? to : '/');
|
||||
} else {
|
||||
this.props.history.goBack();
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyUp = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.handleClick();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<button onClick={this.handleClick} onKeyUp={this.handleKeyUp} className='column-back-button'>
|
||||
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
|
||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
|
||||
import { useSoapboxConfig } from 'soapbox/hooks';
|
||||
|
||||
import { getAcct } from '../utils/accounts';
|
||||
|
||||
import Icon from './icon';
|
||||
import RelativeTimestamp from './relative_timestamp';
|
||||
import VerificationBadge from './verification_badge';
|
||||
|
||||
import type { Account } from 'soapbox/types/entities';
|
||||
|
||||
interface IDisplayName {
|
||||
account: Account
|
||||
withDate?: boolean
|
||||
}
|
||||
|
||||
const DisplayName: React.FC<IDisplayName> = ({ account, children, withDate = false }) => {
|
||||
const { displayFqn = false } = useSoapboxConfig();
|
||||
const { created_at: createdAt, verified } = account;
|
||||
|
||||
const joinedAt = createdAt ? (
|
||||
<div className='account__joined-at'>
|
||||
<Icon src={require('@tabler/icons/icons/clock.svg')} />
|
||||
<RelativeTimestamp timestamp={createdAt} />
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const displayName = (
|
||||
<span className='display-name__name'>
|
||||
<bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>
|
||||
{verified && <VerificationBadge />}
|
||||
{withDate && joinedAt}
|
||||
</span>
|
||||
);
|
||||
|
||||
const suffix = (<span className='display-name__account'>@{getAcct(account, displayFqn)}</span>);
|
||||
|
||||
return (
|
||||
<span className='display-name' data-testid='display-name'>
|
||||
<HoverRefWrapper accountId={account.get('id')} inline>
|
||||
{displayName}
|
||||
</HoverRefWrapper>
|
||||
{suffix}
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default DisplayName;
|
|
@ -1,85 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
|
||||
import { displayFqn } from 'soapbox/utils/state';
|
||||
|
||||
import { getAcct } from '../utils/accounts';
|
||||
|
||||
import Icon from './icon';
|
||||
import RelativeTimestamp from './relative_timestamp';
|
||||
import VerificationBadge from './verification_badge';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
displayFqn: displayFqn(state),
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class DisplayName extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.record.isRequired,
|
||||
displayFqn: PropTypes.bool,
|
||||
others: ImmutablePropTypes.list,
|
||||
children: PropTypes.node,
|
||||
withDate: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
withDate: false,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { account, displayFqn, others, children, withDate } = this.props;
|
||||
|
||||
let displayName, suffix;
|
||||
const verified = account.get('verified');
|
||||
|
||||
const createdAt = account.get('created_at');
|
||||
|
||||
const joinedAt = createdAt ? (
|
||||
<div className='account__joined-at'>
|
||||
<Icon src={require('@tabler/icons/icons/clock.svg')} />
|
||||
<RelativeTimestamp timestamp={createdAt} />
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
if (others?.size > 1) {
|
||||
displayName = others.take(2).map(a => (
|
||||
<span className='display-name__name' key={a.get('id')}>
|
||||
<bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi>
|
||||
{verified && <VerificationBadge />}
|
||||
{withDate && joinedAt}
|
||||
</span>
|
||||
)).reduce((prev, cur) => [prev, ', ', cur]);
|
||||
|
||||
if (others.size - 2 > 0) {
|
||||
suffix = `+${others.size - 2}`;
|
||||
}
|
||||
} else {
|
||||
displayName = (
|
||||
<span className='display-name__name'>
|
||||
<bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>
|
||||
{verified && <VerificationBadge />}
|
||||
{withDate && joinedAt}
|
||||
</span>
|
||||
);
|
||||
suffix = <span className='display-name__account'>@{getAcct(account, displayFqn)}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className='display-name' data-testid='display-name'>
|
||||
<HoverRefWrapper accountId={account.get('id')} inline>
|
||||
{displayName}
|
||||
</HoverRefWrapper>
|
||||
{suffix}
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -42,7 +42,7 @@ const Hashtag: React.FC<IHashtag> = ({ hashtag }) => {
|
|||
</Stack>
|
||||
|
||||
{hashtag.get('history') && (
|
||||
<div className='w-[40px]'>
|
||||
<div className='w-[40px]' data-testid='sparklines'>
|
||||
<Sparklines
|
||||
width={40}
|
||||
height={28}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Virtuoso, Components } from 'react-virtuoso';
|
||||
import { Virtuoso, Components, VirtuosoProps, VirtuosoHandle } from 'react-virtuoso';
|
||||
|
||||
import PullToRefresh from 'soapbox/components/pull-to-refresh';
|
||||
import { useSettings } from 'soapbox/hooks';
|
||||
|
@ -25,7 +25,7 @@ const List: Components<Context>['List'] = React.forwardRef((props, ref) => {
|
|||
return <div ref={ref} className={context?.listClassName} {...rest} />;
|
||||
});
|
||||
|
||||
interface IScrollableList {
|
||||
interface IScrollableList extends VirtuosoProps<any, any> {
|
||||
scrollKey?: string,
|
||||
onLoadMore?: () => void,
|
||||
isLoading?: boolean,
|
||||
|
@ -45,7 +45,7 @@ interface IScrollableList {
|
|||
}
|
||||
|
||||
/** Legacy ScrollableList with Virtuoso for backwards-compatibility */
|
||||
const ScrollableList: React.FC<IScrollableList> = ({
|
||||
const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
|
||||
prepend = null,
|
||||
alwaysPrepend,
|
||||
children,
|
||||
|
@ -61,7 +61,9 @@ const ScrollableList: React.FC<IScrollableList> = ({
|
|||
hasMore,
|
||||
placeholderComponent: Placeholder,
|
||||
placeholderCount = 0,
|
||||
}) => {
|
||||
initialTopMostItemIndex = 0,
|
||||
scrollerRef,
|
||||
}, ref) => {
|
||||
const settings = useSettings();
|
||||
const autoloadMore = settings.get('autoloadMore');
|
||||
|
||||
|
@ -126,6 +128,7 @@ const ScrollableList: React.FC<IScrollableList> = ({
|
|||
/** Render the actual Virtuoso list */
|
||||
const renderFeed = (): JSX.Element => (
|
||||
<Virtuoso
|
||||
ref={ref}
|
||||
useWindowScroll
|
||||
className={className}
|
||||
data={data}
|
||||
|
@ -133,6 +136,7 @@ const ScrollableList: React.FC<IScrollableList> = ({
|
|||
endReached={handleEndReached}
|
||||
isScrolling={isScrolling => isScrolling && onScroll && onScroll()}
|
||||
itemContent={renderItem}
|
||||
initialTopMostItemIndex={showLoading ? 0 : initialTopMostItemIndex}
|
||||
context={{
|
||||
listClassName: className,
|
||||
itemClassName,
|
||||
|
@ -145,6 +149,7 @@ const ScrollableList: React.FC<IScrollableList> = ({
|
|||
Item,
|
||||
Footer: loadMore,
|
||||
}}
|
||||
scrollerRef={scrollerRef}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -162,6 +167,6 @@ const ScrollableList: React.FC<IScrollableList> = ({
|
|||
{renderBody()}
|
||||
</PullToRefresh>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default ScrollableList;
|
||||
|
|
|
@ -2,20 +2,25 @@ import classNames from 'classnames';
|
|||
import React from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import { Icon, Text, Counter } from './ui';
|
||||
import { Icon, Text } from './ui';
|
||||
|
||||
interface ISidebarNavigationLink {
|
||||
/** Notification count, if any. */
|
||||
count?: number,
|
||||
/** URL to an SVG icon. */
|
||||
icon: string,
|
||||
text: string | React.ReactElement,
|
||||
/** Link label. */
|
||||
text: React.ReactElement,
|
||||
/** Route to an internal page. */
|
||||
to?: string,
|
||||
/** Callback when the link is clicked. */
|
||||
onClick?: React.EventHandler<React.MouseEvent>,
|
||||
}
|
||||
|
||||
/** Desktop sidebar navigation link. */
|
||||
const SidebarNavigationLink = React.forwardRef((props: ISidebarNavigationLink, ref: React.ForwardedRef<HTMLAnchorElement>): JSX.Element => {
|
||||
const { icon, text, to = '', count, onClick } = props;
|
||||
const isActive = location.pathname === to;
|
||||
const withCounter = typeof count !== 'undefined';
|
||||
|
||||
const handleClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
if (onClick) {
|
||||
|
@ -32,29 +37,18 @@ const SidebarNavigationLink = React.forwardRef((props: ISidebarNavigationLink, r
|
|||
ref={ref}
|
||||
onClick={handleClick}
|
||||
className={classNames({
|
||||
'flex items-center py-2 text-sm font-semibold space-x-4': true,
|
||||
'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200': !isActive,
|
||||
'text-gray-900 dark:text-white': isActive,
|
||||
'flex items-center p-3 text-sm font-semibold space-x-4 rounded-full group hover:bg-primary-200/80 dark:hover:bg-primary-900/60 hover:text-primary-600 dark:hover:text-gray-200': true,
|
||||
'text-gray-500 dark:text-gray-400': !isActive,
|
||||
'text-primary-600 dark:text-white': isActive,
|
||||
})}
|
||||
>
|
||||
<span className={classNames({
|
||||
'relative rounded-lg inline-flex p-3': true,
|
||||
'bg-primary-50 dark:bg-slate-700': !isActive,
|
||||
'bg-primary-600': isActive,
|
||||
})}
|
||||
>
|
||||
{withCounter && count > 0 ? (
|
||||
<span className='absolute -top-2 -right-2'>
|
||||
<Counter count={count} />
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
<span className='relative'>
|
||||
<Icon
|
||||
src={icon}
|
||||
count={count}
|
||||
className={classNames({
|
||||
'h-5 w-5': true,
|
||||
'text-primary-700 dark:text-white': !isActive,
|
||||
'text-white': isActive,
|
||||
'h-6 w-6 dark:group-hover:text-primary-500': true,
|
||||
'dark:text-primary-500': isActive,
|
||||
})}
|
||||
/>
|
||||
</span>
|
||||
|
|
|
@ -149,12 +149,6 @@ const SidebarNavigation = () => {
|
|||
|
||||
{account && (
|
||||
<>
|
||||
<SidebarNavigationLink
|
||||
to={`/@${account.acct}`}
|
||||
icon={require('@tabler/icons/icons/user.svg')}
|
||||
text={<FormattedMessage id='tabs_bar.profile' defaultMessage='Profile' />}
|
||||
/>
|
||||
|
||||
<SidebarNavigationLink
|
||||
to='/notifications'
|
||||
icon={require('@tabler/icons/icons/bell.svg')}
|
||||
|
@ -162,6 +156,14 @@ const SidebarNavigation = () => {
|
|||
text={<FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' />}
|
||||
/>
|
||||
|
||||
{renderMessagesLink()}
|
||||
|
||||
<SidebarNavigationLink
|
||||
to={`/@${account.acct}`}
|
||||
icon={require('@tabler/icons/icons/user.svg')}
|
||||
text={<FormattedMessage id='tabs_bar.profile' defaultMessage='Profile' />}
|
||||
/>
|
||||
|
||||
<SidebarNavigationLink
|
||||
to='/settings'
|
||||
icon={require('@tabler/icons/icons/settings.svg')}
|
||||
|
@ -170,8 +172,6 @@ const SidebarNavigation = () => {
|
|||
</>
|
||||
)}
|
||||
|
||||
{account && renderMessagesLink()}
|
||||
|
||||
{menu.length > 0 && (
|
||||
<DropdownMenu items={menu}>
|
||||
<SidebarNavigationLink
|
||||
|
|
|
@ -161,7 +161,9 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
|||
<Stack>
|
||||
<button type='button' onClick={handleSwitcherClick} className='py-1'>
|
||||
<HStack alignItems='center' justifyContent='between'>
|
||||
<Text tag='span' size='sm' weight='medium'>Switch accounts</Text>
|
||||
<Text tag='span' size='sm' weight='medium'>
|
||||
<FormattedMessage id='profile_dropdown.switch_account' defaultMessage='Switch accounts' />
|
||||
</Text>
|
||||
|
||||
<Icon
|
||||
src={require('@tabler/icons/icons/chevron-down.svg')}
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
import { useSoapboxConfig, useSettings, useSystemTheme } from 'soapbox/hooks';
|
||||
|
||||
interface ISiteLogo extends React.ComponentProps<'img'> {
|
||||
/** Extra class names for the <img> element. */
|
||||
className?: string,
|
||||
}
|
||||
|
||||
/** Display the most appropriate site logo based on the theme and configuration. */
|
||||
const SiteLogo: React.FC<React.ComponentProps<'img'>> = (props) => {
|
||||
const SiteLogo: React.FC<ISiteLogo> = ({ className, ...rest }) => {
|
||||
const { logo, logoDarkMode } = useSoapboxConfig();
|
||||
const settings = useSettings();
|
||||
|
||||
|
@ -28,7 +34,11 @@ const SiteLogo: React.FC<React.ComponentProps<'img'>> = (props) => {
|
|||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/alt-text
|
||||
<img src={getSrc()} {...props} />
|
||||
<img
|
||||
className={classNames('object-contain', className)}
|
||||
src={getSrc()}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import Card from '../features/status/components/card';
|
|||
import Bundle from '../features/ui/components/bundle';
|
||||
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
|
||||
|
||||
import AttachmentThumbs from './attachment_thumbs';
|
||||
import AttachmentThumbs from './attachment-thumbs';
|
||||
import StatusActionBar from './status_action_bar';
|
||||
import StatusContent from './status_content';
|
||||
import StatusReplyMentions from './status_reply_mentions';
|
||||
|
@ -160,7 +160,7 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
|
|||
|
||||
// Compensate height changes
|
||||
componentDidUpdate(_prevProps: IStatus, _prevState: IStatusState, snapshot?: ScrollPosition): void {
|
||||
const doShowCard: boolean = Boolean(!this.props.muted && !this.props.hidden && this.props.status && this.props.status.card);
|
||||
const doShowCard: boolean = Boolean(!this.props.muted && !this.props.hidden && this.props.status && this.props.status.card);
|
||||
|
||||
if (doShowCard && !this.didShowCard) {
|
||||
this.didShowCard = true;
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Text } from 'soapbox/components/ui';
|
||||
|
||||
/** Represents a deleted item. */
|
||||
const Tombstone: React.FC = () => {
|
||||
return (
|
||||
<div className='my-4 p-9 flex items-center justify-center sm:rounded-xl bg-gray-100 border border-solid border-gray-200 dark:bg-slate-900 dark:border-slate-700'>
|
||||
<Text>
|
||||
<FormattedMessage id='statuses.tombstone' defaultMessage='One or more posts is unavailable.' />
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tombstone;
|
|
@ -26,7 +26,7 @@ const useButtonStyles = ({
|
|||
accent: 'border-transparent text-white bg-accent-500 hover:bg-accent-300 focus:ring-pink-500 focus:ring-2 focus:ring-offset-2',
|
||||
danger: 'border-transparent text-danger-700 bg-danger-100 hover:bg-danger-200 focus:ring-danger-500 focus:ring-2 focus:ring-offset-2',
|
||||
transparent: 'border-transparent text-gray-800 backdrop-blur-sm bg-white/75 hover:bg-white/80',
|
||||
link: 'border-transparent text-primary-600 dark:text-primary-400 hover:bg-gray-100 hover:text-primary-700 dark:hover:bg-slate-900/50',
|
||||
link: 'border-transparent text-primary-600 dark:text-primary-400 hover:bg-primary-100 hover:text-primary-700 dark:hover:bg-slate-900/50',
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
|
||||
interface IFileInput extends Pick<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'required' | 'disabled' | 'name' | 'accept'> { }
|
||||
|
||||
const FileInput = forwardRef<HTMLInputElement, IFileInput>((props, ref) => {
|
||||
return (
|
||||
<input
|
||||
{...props}
|
||||
ref={ref}
|
||||
type='file'
|
||||
className='block w-full text-sm text-gray-800 dark:text-slate-200 file:cursor-pointer file:mr-2 file:py-1.5 file:px-3 file:rounded-full file:text-xs file:leading-4 file:font-medium file:border-gray-200 file:border file:border-solid file:bg-white file:text-gray-700 hover:file:bg-gray-100 dark:file:border-slate-700 dark:file:bg-slate-800 dark:file:text-slate-200'
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default FileInput;
|
|
@ -6,6 +6,7 @@ export { default as Column } from './column/column';
|
|||
export { default as Counter } from './counter/counter';
|
||||
export { default as Emoji } from './emoji/emoji';
|
||||
export { default as EmojiSelector } from './emoji-selector/emoji-selector';
|
||||
export { default as FileInput } from './file-input/file-input';
|
||||
export { default as Form } from './form/form';
|
||||
export { default as FormActions } from './form-actions/form-actions';
|
||||
export { default as FormGroup } from './form-group/form-group';
|
||||
|
|
|
@ -10,13 +10,30 @@ const messages = defineMessages({
|
|||
confirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||
});
|
||||
|
||||
type Widths = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl'
|
||||
|
||||
const widths = {
|
||||
xs: 'max-w-xs',
|
||||
sm: 'max-w-sm',
|
||||
md: 'max-w-base',
|
||||
lg: 'max-w-lg',
|
||||
xl: 'max-w-xl',
|
||||
'2xl': 'max-w-2xl',
|
||||
'3xl': 'max-w-3xl',
|
||||
'4xl': 'max-w-4xl',
|
||||
};
|
||||
|
||||
interface IModal {
|
||||
/** Callback when the modal is cancelled. */
|
||||
cancelAction?: () => void,
|
||||
/** Cancel button text. */
|
||||
cancelText?: string,
|
||||
/** URL to an SVG icon for the close button. */
|
||||
closeIcon?: string,
|
||||
/** Position of the close button. */
|
||||
closePosition?: 'left' | 'right',
|
||||
/** Callback when the modal is confirmed. */
|
||||
confirmationAction?: () => void,
|
||||
confirmationAction?: (event?: React.MouseEvent<HTMLButtonElement>) => void,
|
||||
/** Whether the confirmation button is disabled. */
|
||||
confirmationDisabled?: boolean,
|
||||
/** Confirmation button text. */
|
||||
|
@ -26,13 +43,15 @@ interface IModal {
|
|||
/** Callback when the modal is closed. */
|
||||
onClose?: () => void,
|
||||
/** Callback when the secondary action is chosen. */
|
||||
secondaryAction?: () => void,
|
||||
secondaryAction?: (event?: React.MouseEvent<HTMLButtonElement>) => void,
|
||||
/** Secondary button text. */
|
||||
secondaryText?: React.ReactNode,
|
||||
secondaryDisabled?: boolean,
|
||||
/** Don't focus the "confirm" button on mount. */
|
||||
skipFocus?: boolean,
|
||||
/** Title text for the modal. */
|
||||
title: string | React.ReactNode,
|
||||
width?: Widths,
|
||||
}
|
||||
|
||||
/** Displays a modal dialog box. */
|
||||
|
@ -40,15 +59,19 @@ const Modal: React.FC<IModal> = ({
|
|||
cancelAction,
|
||||
cancelText,
|
||||
children,
|
||||
closeIcon = require('@tabler/icons/icons/x.svg'),
|
||||
closePosition = 'right',
|
||||
confirmationAction,
|
||||
confirmationDisabled,
|
||||
confirmationText,
|
||||
confirmationTheme,
|
||||
onClose,
|
||||
secondaryAction,
|
||||
secondaryDisabled = false,
|
||||
secondaryText,
|
||||
skipFocus = false,
|
||||
title,
|
||||
width = 'xl',
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const buttonRef = React.useRef<HTMLButtonElement>(null);
|
||||
|
@ -60,17 +83,21 @@ const Modal: React.FC<IModal> = ({
|
|||
}, [skipFocus, buttonRef]);
|
||||
|
||||
return (
|
||||
<div data-testid='modal' className='block w-full max-w-xl p-6 mx-auto overflow-hidden text-left align-middle transition-all transform bg-white dark:bg-slate-800 text-black dark:text-white shadow-xl rounded-2xl pointer-events-auto'>
|
||||
<div data-testid='modal' className={classNames('block w-full p-6 mx-auto overflow-hidden text-left align-middle transition-all transform bg-white dark:bg-slate-800 text-black dark:text-white shadow-xl rounded-2xl pointer-events-auto', widths[width])}>
|
||||
<div className='sm:flex sm:items-start w-full justify-between'>
|
||||
<div className='w-full'>
|
||||
<div className='w-full flex flex-row justify-between items-center'>
|
||||
<h3 className='text-lg leading-6 font-medium text-gray-900 dark:text-white'>
|
||||
<div
|
||||
className={classNames('w-full flex items-center gap-2', {
|
||||
'flex-row-reverse': closePosition === 'left',
|
||||
})}
|
||||
>
|
||||
<h3 className='flex-grow text-lg leading-6 font-medium text-gray-900 dark:text-white'>
|
||||
{title}
|
||||
</h3>
|
||||
|
||||
{onClose && (
|
||||
<IconButton
|
||||
src={require('@tabler/icons/icons/x.svg')}
|
||||
src={closeIcon}
|
||||
title={intl.formatMessage(messages.close)}
|
||||
onClick={onClose}
|
||||
className='text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-gray-200'
|
||||
|
@ -103,6 +130,7 @@ const Modal: React.FC<IModal> = ({
|
|||
<Button
|
||||
theme='secondary'
|
||||
onClick={secondaryAction}
|
||||
disabled={secondaryDisabled}
|
||||
>
|
||||
{secondaryText}
|
||||
</Button>
|
||||
|
|
|
@ -24,7 +24,7 @@ const VerificationBadge: React.FC<IVerificationBadge> = ({ className }) => {
|
|||
const Element = icon.endsWith('.svg') ? Icon : 'img';
|
||||
|
||||
return (
|
||||
<span className='verified-icon'>
|
||||
<span className='verified-icon' data-testid='verified-badge'>
|
||||
<Element className={classNames('w-4 text-accent-500', className)} src={icon} alt={intl.formatMessage(messages.verified)} />
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -18,16 +18,19 @@ import AuthLayout from 'soapbox/features/auth_layout';
|
|||
import OnboardingWizard from 'soapbox/features/onboarding/onboarding-wizard';
|
||||
import PublicLayout from 'soapbox/features/public_layout';
|
||||
import NotificationsContainer from 'soapbox/features/ui/containers/notifications_container';
|
||||
import { ModalContainer } from 'soapbox/features/ui/util/async-components';
|
||||
import WaitlistPage from 'soapbox/features/verification/waitlist_page';
|
||||
import { createGlobals } from 'soapbox/globals';
|
||||
import { useAppSelector, useAppDispatch, useOwnAccount, useFeatures, useSoapboxConfig, useSettings, useSystemTheme } from 'soapbox/hooks';
|
||||
import MESSAGES from 'soapbox/locales/messages';
|
||||
import { useCachedLocationHandler } from 'soapbox/utils/redirect';
|
||||
import { generateThemeCss } from 'soapbox/utils/theme';
|
||||
|
||||
import { checkOnboardingStatus } from '../actions/onboarding';
|
||||
import { preload } from '../actions/preload';
|
||||
import ErrorBoundary from '../components/error_boundary';
|
||||
import UI from '../features/ui';
|
||||
import BundleContainer from '../features/ui/containers/bundle_container';
|
||||
import { store } from '../store';
|
||||
|
||||
/** Ensure the given locale exists in our codebase */
|
||||
|
@ -64,6 +67,7 @@ const loadInitial = () => {
|
|||
};
|
||||
|
||||
const SoapboxMount = () => {
|
||||
useCachedLocationHandler();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const me = useAppSelector(state => state.me);
|
||||
|
@ -94,7 +98,7 @@ const SoapboxMount = () => {
|
|||
MESSAGES[locale]().then(messages => {
|
||||
setMessages(messages);
|
||||
setLocaleLoading(false);
|
||||
}).catch(() => {});
|
||||
}).catch(() => { });
|
||||
}, [locale]);
|
||||
|
||||
// Load initial data from the API
|
||||
|
@ -170,7 +174,13 @@ const SoapboxMount = () => {
|
|||
)}
|
||||
|
||||
{waitlisted && (
|
||||
<Route render={(props) => <WaitlistPage {...props} account={account} />} />
|
||||
<>
|
||||
<Route render={(props) => <WaitlistPage {...props} account={account} />} />
|
||||
|
||||
<BundleContainer fetchComponent={ModalContainer}>
|
||||
{Component => <Component />}
|
||||
</BundleContainer>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!me && (singleUserMode
|
||||
|
|
|
@ -86,7 +86,6 @@ class Header extends ImmutablePureComponent {
|
|||
static propTypes = {
|
||||
account: ImmutablePropTypes.record,
|
||||
meaccount: ImmutablePropTypes.record,
|
||||
identity_props: ImmutablePropTypes.list,
|
||||
intl: PropTypes.object.isRequired,
|
||||
username: PropTypes.string,
|
||||
features: PropTypes.object,
|
||||
|
@ -608,8 +607,8 @@ class Header extends ImmutablePureComponent {
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<div className='mt-6 sm:flex-1 sm:min-w-0 sm:flex sm:items-center sm:justify-end sm:space-x-6 sm:pb-1'>
|
||||
<div className='mt-10 flex justify-stretch flex-row space-y-0 space-x-2'>
|
||||
<div className='mt-6 flex justify-end w-full sm:pb-1'>
|
||||
<div className='mt-10 flex flex-row space-y-0 space-x-2'>
|
||||
{me && (
|
||||
<Menu>
|
||||
<MenuButton
|
||||
|
|
|
@ -13,7 +13,6 @@ class Header extends ImmutablePureComponent {
|
|||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.record,
|
||||
identity_proofs: ImmutablePropTypes.list,
|
||||
onFollow: PropTypes.func.isRequired,
|
||||
onBlock: PropTypes.func.isRequired,
|
||||
onMention: PropTypes.func.isRequired,
|
||||
|
@ -143,7 +142,7 @@ class Header extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { account, identity_proofs } = this.props;
|
||||
const { account } = this.props;
|
||||
const moved = (account) ? account.get('moved') : false;
|
||||
|
||||
return (
|
||||
|
@ -152,7 +151,6 @@ class Header extends ImmutablePureComponent {
|
|||
|
||||
<InnerHeader
|
||||
account={account}
|
||||
identity_proofs={identity_proofs}
|
||||
onFollow={this.handleFollow}
|
||||
onBlock={this.handleBlock}
|
||||
onMention={this.handleMention}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { FormattedMessage } from 'react-intl';
|
|||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import AvatarOverlay from 'soapbox/components/avatar_overlay';
|
||||
import DisplayName from 'soapbox/components/display_name';
|
||||
import DisplayName from 'soapbox/components/display-name';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
|
||||
import type { Account as AccountEntity } from 'soapbox/types/entities';
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { List as ImmutableList } from 'immutable';
|
||||
import React from 'react';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
@ -64,7 +63,6 @@ const makeMapStateToProps = () => {
|
|||
|
||||
const mapStateToProps = (state, { accountId }) => ({
|
||||
account: getAccount(state, accountId),
|
||||
identity_proofs: state.getIn(['identity_proofs', accountId], ImmutableList()),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
|
|
|
@ -13,7 +13,6 @@ import { makeGetStatusIds, findAccountByUsername } from 'soapbox/selectors';
|
|||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
import { fetchAccount, fetchAccountByUsername } from '../../actions/accounts';
|
||||
import { fetchAccountIdentityProofs } from '../../actions/identity_proofs';
|
||||
import { fetchPatronAccount } from '../../actions/patron';
|
||||
import { expandAccountFeaturedTimeline, expandAccountTimeline } from '../../actions/timelines';
|
||||
import StatusList from '../../components/status_list';
|
||||
|
@ -84,11 +83,10 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { params: { username }, accountId, accountApId, withReplies, me, patronEnabled } = this.props;
|
||||
const { params: { username }, accountId, accountApId, withReplies, patronEnabled } = this.props;
|
||||
|
||||
if (accountId && accountId !== -1) {
|
||||
this.props.dispatch(fetchAccount(accountId));
|
||||
if (me) this.props.dispatch(fetchAccountIdentityProofs(accountId));
|
||||
|
||||
if (!withReplies) {
|
||||
this.props.dispatch(expandAccountFeaturedTimeline(accountId));
|
||||
|
@ -105,11 +103,10 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { params: { username }, me, accountId, withReplies, accountApId, patronEnabled } = this.props;
|
||||
const { params: { username }, accountId, withReplies, accountApId, patronEnabled } = this.props;
|
||||
|
||||
if (accountId && (accountId !== -1) && (accountId !== prevProps.accountId) || withReplies !== prevProps.withReplies) {
|
||||
this.props.dispatch(fetchAccount(accountId));
|
||||
if (me) this.props.dispatch(fetchAccountIdentityProofs(accountId));
|
||||
|
||||
if (!withReplies) {
|
||||
this.props.dispatch(expandAccountFeaturedTimeline(accountId));
|
||||
|
|
|
@ -7,7 +7,7 @@ import { connect } from 'react-redux';
|
|||
|
||||
import { addToAliases } from 'soapbox/actions/aliases';
|
||||
import Avatar from 'soapbox/components/avatar';
|
||||
import DisplayName from 'soapbox/components/display_name';
|
||||
import DisplayName from 'soapbox/components/display-name';
|
||||
import IconButton from 'soapbox/components/icon_button';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import React from 'react';
|
||||
import { Link, Redirect, Route, Switch } from 'react-router-dom';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { Link, Redirect, Route, Switch, useHistory } from 'react-router-dom';
|
||||
|
||||
import LandingGradient from 'soapbox/components/landing-gradient';
|
||||
import SiteLogo from 'soapbox/components/site-logo';
|
||||
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
|
||||
import { NotificationsContainer } from 'soapbox/features/ui/util/async-components';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { useAppSelector, useFeatures, useSoapboxConfig } from 'soapbox/hooks';
|
||||
|
||||
import { Card, CardBody } from '../../components/ui';
|
||||
import { Button, Card, CardBody } from '../../components/ui';
|
||||
import LoginPage from '../auth_login/components/login_page';
|
||||
import PasswordReset from '../auth_login/components/password_reset';
|
||||
import PasswordResetConfirm from '../auth_login/components/password_reset_confirm';
|
||||
|
@ -17,22 +18,52 @@ import RegisterInvite from '../register_invite';
|
|||
import Verification from '../verification';
|
||||
import EmailPassthru from '../verification/email_passthru';
|
||||
|
||||
const messages = defineMessages({
|
||||
register: { id: 'auth_layout.register', defaultMessage: 'Create an account' },
|
||||
});
|
||||
|
||||
const AuthLayout = () => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
|
||||
const siteTitle = useAppSelector(state => state.instance.title);
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
const pepeEnabled = soapboxConfig.getIn(['extensions', 'pepe', 'enabled']) === true;
|
||||
|
||||
const features = useFeatures();
|
||||
const instance = useAppSelector((state) => state.instance);
|
||||
const isOpen = features.accountCreation && instance.registrations;
|
||||
const pepeOpen = useAppSelector(state => state.verification.getIn(['instance', 'registrations'], false) === true);
|
||||
const isLoginPage = history.location.pathname === '/login';
|
||||
const shouldShowRegisterLink = (isLoginPage && (isOpen || (pepeEnabled && pepeOpen)));
|
||||
|
||||
return (
|
||||
<div className='h-full'>
|
||||
<LandingGradient />
|
||||
|
||||
<main className='relative min-h-full sm:flex sm:items-center sm:justify-center py-12'>
|
||||
<div className='w-full sm:max-w-lg md:max-w-2xl space-y-8'>
|
||||
<header className='flex justify-center relative'>
|
||||
<Link to='/' className='cursor-pointer'>
|
||||
<SiteLogo alt={siteTitle} className='h-7' />
|
||||
</Link>
|
||||
<main className='relative min-h-full sm:flex sm:justify-center'>
|
||||
<div className='w-full sm:max-w-lg md:max-w-2xl lg:max-w-6xl'>
|
||||
<header className='flex justify-between relative py-12 px-2'>
|
||||
<div className='relative z-0 flex-1 px-2 lg:flex lg:items-center lg:justify-center lg:absolute lg:inset-0'>
|
||||
<Link to='/' className='cursor-pointer'>
|
||||
<SiteLogo alt={siteTitle} className='h-7' />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{shouldShowRegisterLink && (
|
||||
<div className='relative z-10 ml-auto flex items-center'>
|
||||
<Button
|
||||
theme='link'
|
||||
icon={require('@tabler/icons/icons/user.svg')}
|
||||
to='/signup'
|
||||
>
|
||||
{intl.formatMessage(messages.register)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
|
||||
<div className='flex flex-col justify-center items-center'>
|
||||
<div className='flex flex-col h-full justify-center items-center'>
|
||||
<div className='pb-10 sm:mx-auto w-full sm:max-w-lg md:max-w-2xl'>
|
||||
<Card variant='rounded' size='xl'>
|
||||
<CardBody>
|
||||
|
|
|
@ -7,6 +7,7 @@ import { Redirect } from 'react-router-dom';
|
|||
import { logIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth';
|
||||
import { fetchInstance } from 'soapbox/actions/instance';
|
||||
import { closeModal } from 'soapbox/actions/modals';
|
||||
import { getRedirectUrl } from 'soapbox/utils/redirect';
|
||||
import { isStandalone } from 'soapbox/utils/state';
|
||||
|
||||
import LoginForm from './login_form';
|
||||
|
@ -78,7 +79,10 @@ class LoginPage extends ImmutablePureComponent {
|
|||
|
||||
if (standalone) return <Redirect to='/login/external' />;
|
||||
|
||||
if (shouldRedirect) return <Redirect to='/' />;
|
||||
if (shouldRedirect) {
|
||||
const redirectUri = getRedirectUrl();
|
||||
return <Redirect to={redirectUri} />;
|
||||
}
|
||||
|
||||
if (mfa_auth_needed) return <OtpAuthForm mfa_token={mfa_token} />;
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ const AuthToken: React.FC<IAuthToken> = ({ token }) => {
|
|||
const AuthTokenList: React.FC = () =>{
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
const tokens = useAppSelector(state => state.security.get('tokens'));
|
||||
const tokens = useAppSelector(state => state.security.get('tokens').reverse());
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchOAuthTokens());
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import Avatar from 'soapbox/components/avatar';
|
||||
import DisplayName from 'soapbox/components/display_name';
|
||||
import DisplayName from 'soapbox/components/display-name';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import Permalink from 'soapbox/components/permalink';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
|
|
@ -77,7 +77,6 @@ class ChatRoom extends ImmutablePureComponent {
|
|||
return (
|
||||
<Column label={`@${getAcct(account, displayFqn)}`}>
|
||||
{/* <div className='chatroom__back'>
|
||||
<ColumnBackButton />
|
||||
<Link to={`/@${account.get('acct')}`} className='chatroom__header'>
|
||||
<Avatar account={account} size={18} />
|
||||
<div className='chatroom__title'>
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import Avatar from 'soapbox/components/avatar';
|
||||
import DisplayName from 'soapbox/components/display_name';
|
||||
import DisplayName from 'soapbox/components/display-name';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import emojify from 'soapbox/features/emoji/emoji';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import AttachmentThumbs from 'soapbox/components/attachment_thumbs';
|
||||
import AttachmentThumbs from 'soapbox/components/attachment-thumbs';
|
||||
import { Stack, Text } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account_container';
|
||||
|
||||
|
@ -28,7 +28,7 @@ export default class ReplyIndicator extends ImmutablePureComponent {
|
|||
return null;
|
||||
}
|
||||
|
||||
const style = {
|
||||
const style = {
|
||||
direction: isRtl(status.get('search_index')) ? 'rtl' : 'ltr',
|
||||
};
|
||||
|
||||
|
@ -59,7 +59,6 @@ export default class ReplyIndicator extends ImmutablePureComponent {
|
|||
|
||||
{status.get('media_attachments').size > 0 && (
|
||||
<AttachmentThumbs
|
||||
compact
|
||||
media={status.get('media_attachments')}
|
||||
sensitive={status.get('sensitive')}
|
||||
/>
|
||||
|
|
|
@ -6,7 +6,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import Pullable from 'soapbox/components/pullable';
|
||||
import ScrollableList from 'soapbox/components/scrollable_list';
|
||||
import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder_account';
|
||||
import PlaceholderHashtag from 'soapbox/features/placeholder/components/placeholder_hashtag';
|
||||
|
@ -151,24 +150,22 @@ class SearchResults extends ImmutablePureComponent {
|
|||
{this.renderFilterBar()}
|
||||
|
||||
{noResultsMessage || (
|
||||
<Pullable>
|
||||
<ScrollableList
|
||||
key={selectedFilter}
|
||||
scrollKey={`${selectedFilter}:${value}`}
|
||||
isLoading={submitted && !loaded}
|
||||
showLoading={submitted && !loaded && results.isEmpty()}
|
||||
hasMore={hasMore}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
placeholderComponent={placeholderComponent}
|
||||
placeholderCount={20}
|
||||
className={classNames({
|
||||
'divide-gray-200 dark:divide-slate-700 divide-solid divide-y': selectedFilter === 'statuses',
|
||||
'space-y-4': selectedFilter === 'accounts',
|
||||
})}
|
||||
>
|
||||
{searchResults}
|
||||
</ScrollableList>
|
||||
</Pullable>
|
||||
<ScrollableList
|
||||
key={selectedFilter}
|
||||
scrollKey={`${selectedFilter}:${value}`}
|
||||
isLoading={submitted && !loaded}
|
||||
showLoading={submitted && !loaded && results.isEmpty()}
|
||||
hasMore={hasMore}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
placeholderComponent={placeholderComponent}
|
||||
placeholderCount={20}
|
||||
className={classNames({
|
||||
'divide-gray-200 dark:divide-slate-700 divide-solid divide-y': selectedFilter === 'statuses',
|
||||
})}
|
||||
itemClassName={classNames({ 'pb-4': selectedFilter === 'accounts' })}
|
||||
>
|
||||
{searchResults}
|
||||
</ScrollableList>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -8,7 +8,7 @@ import { connect } from 'react-redux';
|
|||
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import Avatar from 'soapbox/components/avatar';
|
||||
import DisplayName from 'soapbox/components/display_name';
|
||||
import DisplayName from 'soapbox/components/display-name';
|
||||
import Permalink from 'soapbox/components/permalink';
|
||||
import RelativeTimestamp from 'soapbox/components/relative_timestamp';
|
||||
import { Text } from 'soapbox/components/ui';
|
||||
|
|
|
@ -29,7 +29,7 @@ const ProfilePreview: React.FC<IProfilePreview> = ({ account }) => {
|
|||
<StillImage alt='' className='h-12 w-12 rounded-full' src={account.avatar} />
|
||||
</div>
|
||||
|
||||
{!account.verified && <div className='absolute -top-1.5 -right-1.5'><VerificationBadge /></div>}
|
||||
{account.verified && <div className='absolute -top-1.5 -right-1.5'><VerificationBadge /></div>}
|
||||
</div>
|
||||
|
||||
<Stack className='truncate'>
|
||||
|
|
|
@ -9,7 +9,7 @@ import { useAppSelector, useAppDispatch, useOwnAccount, useFeatures } from 'soap
|
|||
import { normalizeAccount } from 'soapbox/normalizers';
|
||||
import resizeImage from 'soapbox/utils/resize_image';
|
||||
|
||||
import { Button, Column, Form, FormActions, FormGroup, Input, Textarea, HStack, Toggle } from '../../components/ui';
|
||||
import { Button, Column, Form, FormActions, FormGroup, Input, Textarea, HStack, Toggle, FileInput } from '../../components/ui';
|
||||
import Streamfield, { StreamfieldComponent } from '../../components/ui/streamfield/streamfield';
|
||||
|
||||
import ProfilePreview from './components/profile-preview';
|
||||
|
@ -179,8 +179,8 @@ const EditProfile: React.FC = () => {
|
|||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const account = useOwnAccount();
|
||||
const features = useFeatures();
|
||||
const account = useOwnAccount();
|
||||
const features = useFeatures();
|
||||
const maxFields = useAppSelector(state => state.instance.pleroma.getIn(['metadata', 'fields_limits', 'max_fields'], 4) as number);
|
||||
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
|
@ -378,14 +378,14 @@ const EditProfile: React.FC = () => {
|
|||
labelText={<FormattedMessage id='edit_profile.fields.header_label' defaultMessage='Choose Background Picture' />}
|
||||
hintText={<FormattedMessage id='edit_profile.hints.header' defaultMessage='PNG, GIF or JPG. Will be downscaled to {size}' values={{ size: '1920x1080px' }} />}
|
||||
>
|
||||
<input type='file' onChange={handleFileChange('header', 1920 * 1080)} className='text-sm' />
|
||||
<FileInput onChange={handleFileChange('header', 1920 * 1080)} />
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
labelText={<FormattedMessage id='edit_profile.fields.avatar_label' defaultMessage='Choose Profile Picture' />}
|
||||
hintText={<FormattedMessage id='edit_profile.hints.avatar' defaultMessage='PNG, GIF or JPG. Will be downscaled to {size}' values={{ size: '400x400px' }} />}
|
||||
>
|
||||
<input type='file' onChange={handleFileChange('avatar', 400 * 400)} className='text-sm' />
|
||||
<FileInput onChange={handleFileChange('avatar', 400 * 400)} />
|
||||
</FormGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
import Avatar from 'soapbox/components/avatar';
|
||||
import DisplayName from 'soapbox/components/display_name';
|
||||
import DisplayName from 'soapbox/components/display-name';
|
||||
import Permalink from 'soapbox/components/permalink';
|
||||
import ActionButton from 'soapbox/features/ui/components/action-button';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useDispatch } from 'react-redux';
|
|||
|
||||
import { authorizeFollowRequest, rejectFollowRequest } from 'soapbox/actions/accounts';
|
||||
import Avatar from 'soapbox/components/avatar';
|
||||
import DisplayName from 'soapbox/components/display_name';
|
||||
import DisplayName from 'soapbox/components/display-name';
|
||||
import IconButton from 'soapbox/components/icon_button';
|
||||
import Permalink from 'soapbox/components/permalink';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
|
|
@ -5,7 +5,7 @@ import { injectIntl } from 'react-intl';
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import Avatar from '../../../components/avatar';
|
||||
import DisplayName from '../../../components/display_name';
|
||||
import DisplayName from '../../../components/display-name';
|
||||
import { makeGetAccount } from '../../../selectors';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
|
|
|
@ -7,7 +7,7 @@ import { connect } from 'react-redux';
|
|||
|
||||
import { removeFromListEditor, addToListEditor } from '../../../actions/lists';
|
||||
import Avatar from '../../../components/avatar';
|
||||
import DisplayName from '../../../components/display_name';
|
||||
import DisplayName from '../../../components/display-name';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import { makeGetAccount } from '../../../selectors';
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import Avatar from 'soapbox/components/avatar';
|
||||
import DisplayName from 'soapbox/components/display_name';
|
||||
import DisplayName from 'soapbox/components/display-name';
|
||||
import IconButton from 'soapbox/components/icon_button';
|
||||
import Permalink from 'soapbox/components/permalink';
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@ import { HotKeys } from 'react-hotkeys';
|
|||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import Icon from '../../../components/icon';
|
||||
import Permalink from '../../../components/permalink';
|
||||
import { HStack, Text, Emoji } from '../../../components/ui';
|
||||
|
@ -50,6 +52,7 @@ const icons: Record<NotificationType, string> = {
|
|||
move: require('@tabler/icons/icons/briefcase.svg'),
|
||||
'pleroma:chat_mention': require('@tabler/icons/icons/messages.svg'),
|
||||
'pleroma:emoji_reaction': require('@tabler/icons/icons/mood-happy.svg'),
|
||||
user_approved: require('@tabler/icons/icons/user-plus.svg'),
|
||||
};
|
||||
|
||||
const messages: Record<NotificationType, { id: string, defaultMessage: string }> = {
|
||||
|
@ -93,16 +96,20 @@ const messages: Record<NotificationType, { id: string, defaultMessage: string }>
|
|||
id: 'notification.pleroma:emoji_reaction',
|
||||
defaultMessage: '{name} reacted to your post',
|
||||
},
|
||||
user_approved: {
|
||||
id: 'notification.user_approved',
|
||||
defaultMessage: 'Welcome to {instance}!',
|
||||
},
|
||||
};
|
||||
|
||||
const buildMessage = (type: NotificationType, account: Account, targetName?: string): JSX.Element => {
|
||||
const buildMessage = (type: NotificationType, account: Account, targetName: string, instanceTitle: string): JSX.Element => {
|
||||
const link = buildLink(account);
|
||||
|
||||
return (
|
||||
<FormattedMessageFixed
|
||||
id={messages[type].id}
|
||||
defaultMessage={messages[type].defaultMessage}
|
||||
values={{ name: link, targetName }}
|
||||
values={{ name: link, targetName, instance: instanceTitle }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -128,6 +135,7 @@ const Notification: React.FC<INotificaton> = (props) => {
|
|||
|
||||
const history = useHistory();
|
||||
const intl = useIntl();
|
||||
const instance = useAppSelector((state) => state.instance);
|
||||
|
||||
const type = notification.type;
|
||||
const { account, status } = notification;
|
||||
|
@ -216,6 +224,7 @@ const Notification: React.FC<INotificaton> = (props) => {
|
|||
switch (type) {
|
||||
case 'follow':
|
||||
case 'follow_request':
|
||||
case 'user_approved':
|
||||
return account && typeof account === 'object' ? (
|
||||
<AccountContainer
|
||||
id={account.id}
|
||||
|
@ -239,7 +248,7 @@ const Notification: React.FC<INotificaton> = (props) => {
|
|||
case 'pleroma:emoji_reaction':
|
||||
return status && typeof status === 'object' ? (
|
||||
<StatusContainer
|
||||
// @ts-ignore
|
||||
// @ts-ignore
|
||||
id={status.id}
|
||||
withDismiss
|
||||
hidden={hidden}
|
||||
|
@ -259,7 +268,7 @@ const Notification: React.FC<INotificaton> = (props) => {
|
|||
|
||||
const targetName = notification.target && typeof notification.target === 'object' ? notification.target.acct : '';
|
||||
|
||||
const message: React.ReactNode = type && account && typeof account === 'object' ? buildMessage(type, account, targetName) : null;
|
||||
const message: React.ReactNode = type && account && typeof account === 'object' ? buildMessage(type, account, targetName, instance.title) : null;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={getHandlers()} data-testid='notification'>
|
||||
|
|
|
@ -7,7 +7,7 @@ import { connect } from 'react-redux';
|
|||
import { fetchAccount } from 'soapbox/actions/accounts';
|
||||
import { addToMentions, removeFromMentions } from 'soapbox/actions/compose';
|
||||
import Avatar from 'soapbox/components/avatar';
|
||||
import DisplayName from 'soapbox/components/display_name';
|
||||
import DisplayName from 'soapbox/components/display-name';
|
||||
import IconButton from 'soapbox/components/icon_button';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
|
||||
|
|
|
@ -4,9 +4,9 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||
import { connect } from 'react-redux';
|
||||
import { Link, NavLink } from 'react-router-dom';
|
||||
|
||||
import AttachmentThumbs from 'soapbox/components/attachment_thumbs';
|
||||
import AttachmentThumbs from 'soapbox/components/attachment-thumbs';
|
||||
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 StatusContent from 'soapbox/components/status_content';
|
||||
import StatusReplyMentions from 'soapbox/components/status_reply_mentions';
|
||||
|
@ -74,7 +74,6 @@ class ScheduledStatus extends ImmutablePureComponent {
|
|||
|
||||
{status.get('media_attachments').size > 0 && (
|
||||
<AttachmentThumbs
|
||||
compact
|
||||
media={status.get('media_attachments')}
|
||||
sensitive={status.get('sensitive')}
|
||||
/>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import * as React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { fetchMfa } from 'soapbox/actions/mfa';
|
||||
import List, { ListItem } from 'soapbox/components/list';
|
||||
import { Button, Card, CardBody, CardHeader, CardTitle, Column } from 'soapbox/components/ui';
|
||||
import { Card, CardBody, CardHeader, CardTitle, Column } from 'soapbox/components/ui';
|
||||
import { useAppSelector, useOwnAccount } from 'soapbox/hooks';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
|
@ -22,6 +22,7 @@ const messages = defineMessages({
|
|||
configureMfa: { id: 'settings.configure_mfa', defaultMessage: 'Configure MFA' },
|
||||
sessions: { id: 'settings.sessions', defaultMessage: 'Active sessions' },
|
||||
deleteAccount: { id: 'settings.delete_account', defaultMessage: 'Delete Account' },
|
||||
other: { id: 'settings.other', defaultMessage: 'Other options' },
|
||||
});
|
||||
|
||||
/** User settings page. */
|
||||
|
@ -34,15 +35,16 @@ const Settings = () => {
|
|||
const features = useAppSelector((state) => getFeatures(state.instance));
|
||||
const account = useOwnAccount();
|
||||
|
||||
const navigateToChangeEmail = React.useCallback(() => history.push('/settings/email'), [history]);
|
||||
const navigateToChangePassword = React.useCallback(() => history.push('/settings/password'), [history]);
|
||||
const navigateToMfa = React.useCallback(() => history.push('/settings/mfa'), [history]);
|
||||
const navigateToSessions = React.useCallback(() => history.push('/settings/tokens'), [history]);
|
||||
const navigateToEditProfile = React.useCallback(() => history.push('/settings/profile'), [history]);
|
||||
const navigateToChangeEmail = () => history.push('/settings/email');
|
||||
const navigateToChangePassword = () => history.push('/settings/password');
|
||||
const navigateToMfa = () => history.push('/settings/mfa');
|
||||
const navigateToSessions = () => history.push('/settings/tokens');
|
||||
const navigateToEditProfile = () => history.push('/settings/profile');
|
||||
const navigateToDeleteAccount = () => history.push('/settings/account');
|
||||
|
||||
const isMfaEnabled = mfa.getIn(['settings', 'totp']);
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
dispatch(fetchMfa());
|
||||
}, [dispatch]);
|
||||
|
||||
|
@ -92,12 +94,14 @@ const Settings = () => {
|
|||
<Preferences />
|
||||
</CardBody>
|
||||
|
||||
<CardHeader>
|
||||
<CardTitle title={intl.formatMessage(messages.other)} />
|
||||
</CardHeader>
|
||||
|
||||
<CardBody>
|
||||
<div className='mt-4 w-full flex justify-center'>
|
||||
<Button theme='danger' to='/settings/account'>
|
||||
{intl.formatMessage(messages.deleteAccount)}
|
||||
</Button>
|
||||
</div>
|
||||
<List>
|
||||
<ListItem label={intl.formatMessage(messages.deleteAccount)} onClick={navigateToDeleteAccount} />
|
||||
</List>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Column>
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
FormActions,
|
||||
FormGroup,
|
||||
Input,
|
||||
FileInput,
|
||||
Textarea,
|
||||
Button,
|
||||
Toggle,
|
||||
|
@ -199,10 +200,8 @@ const SoapboxConfig: React.FC = () => {
|
|||
labelText={<FormattedMessage id='soapbox_config.fields.logo_label' defaultMessage='Logo' />}
|
||||
hintText={<FormattedMessage id='soapbox_config.hints.logo' defaultMessage='SVG. At most 2 MB. Will be displayed to 50px height, maintaining aspect ratio' />}
|
||||
>
|
||||
<input
|
||||
type='file'
|
||||
<FileInput
|
||||
onChange={handleFileChange(['logo'])}
|
||||
className='text-sm'
|
||||
accept='image/svg,image/png'
|
||||
/>
|
||||
</FormGroup>
|
||||
|
|
|
@ -5,7 +5,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||
import { defineMessages, injectIntl, FormattedMessage, IntlShape } from 'react-intl';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import AttachmentThumbs from 'soapbox/components/attachment_thumbs';
|
||||
import AttachmentThumbs from 'soapbox/components/attachment-thumbs';
|
||||
import { Stack, Text } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account_container';
|
||||
|
||||
|
@ -143,7 +143,6 @@ class QuotedStatus extends ImmutablePureComponent<IQuotedStatus> {
|
|||
|
||||
{status.media_attachments.size > 0 && (
|
||||
<AttachmentThumbs
|
||||
compact
|
||||
media={status.media_attachments}
|
||||
sensitive={status.sensitive}
|
||||
/>
|
||||
|
|
|
@ -11,11 +11,12 @@ interface IThreadStatus {
|
|||
focusedStatusId: string,
|
||||
}
|
||||
|
||||
/** Status with reply-connector in threads. */
|
||||
const ThreadStatus: React.FC<IThreadStatus> = (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 => {
|
||||
|
|
|
@ -19,6 +19,7 @@ import { getSettings } from 'soapbox/actions/settings';
|
|||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import ScrollableList from 'soapbox/components/scrollable_list';
|
||||
import SubNavigation from 'soapbox/components/sub_navigation';
|
||||
import Tombstone from 'soapbox/components/tombstone';
|
||||
import { Column, Stack } from 'soapbox/components/ui';
|
||||
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status';
|
||||
import PendingStatus from 'soapbox/features/ui/components/pending_status';
|
||||
|
@ -65,6 +66,7 @@ import ThreadStatus from './components/thread-status';
|
|||
|
||||
import type { AxiosError } from 'axios';
|
||||
import type { History } from 'history';
|
||||
import type { VirtuosoHandle } from 'react-virtuoso';
|
||||
import type { AnyAction } from 'redux';
|
||||
import type { ThunkDispatch } from 'redux-thunk';
|
||||
import type { RootState } from 'soapbox/store';
|
||||
|
@ -97,11 +99,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<string>();
|
||||
let id: string | undefined = statusId;
|
||||
|
||||
while (id && !ancestorsIds.includes(id)) {
|
||||
ancestorsIds = ImmutableOrderedSet([id]).union(ancestorsIds);
|
||||
|
@ -113,13 +115,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)) {
|
||||
|
@ -147,7 +151,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);
|
||||
|
@ -210,6 +214,7 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
|
|||
|
||||
node: HTMLDivElement | null = null;
|
||||
status: HTMLDivElement | null = null;
|
||||
scroller: VirtuosoHandle | null = null;
|
||||
_scrolledIntoView: boolean = false;
|
||||
|
||||
fetchData = async() => {
|
||||
|
@ -556,8 +561,8 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
|
|||
|
||||
renderTombstone(id: string) {
|
||||
return (
|
||||
<div className='tombstone' key={id}>
|
||||
<p><FormattedMessage id='statuses.tombstone' defaultMessage='One or more posts is unavailable.' /></p>
|
||||
<div className='pb-4'>
|
||||
<Tombstone key={id} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -615,11 +620,10 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
|
|||
}
|
||||
|
||||
componentDidUpdate(prevProps: IStatus, prevState: IStatusState) {
|
||||
const { params, status, displayMedia } = this.props;
|
||||
const { ancestorsIds } = prevProps;
|
||||
const { params, status, displayMedia, ancestorsIds } = this.props;
|
||||
const { isLoaded } = this.state;
|
||||
|
||||
if (params.statusId !== prevProps.params.statusId) {
|
||||
this._scrolledIntoView = false;
|
||||
this.fetchData();
|
||||
}
|
||||
|
||||
|
@ -627,17 +631,11 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
|
|||
this.setState({ showMedia: defaultMediaVisibility(status, displayMedia), loadedStatusId: status.id });
|
||||
}
|
||||
|
||||
if (this._scrolledIntoView) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (prevProps.status && ancestorsIds && ancestorsIds.size > 0 && this.node) {
|
||||
const element = this.node.querySelector('.detailed-status');
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
element?.scrollIntoView(true);
|
||||
if (params.statusId !== prevProps.params.statusId || status?.id !== prevProps.status?.id || ancestorsIds.size > prevProps.ancestorsIds.size || isLoaded !== prevState.isLoaded) {
|
||||
this.scroller?.scrollToIndex({
|
||||
index: this.props.ancestorsIds.size,
|
||||
offset: -80,
|
||||
});
|
||||
this._scrolledIntoView = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -654,10 +652,11 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
|
|||
}
|
||||
|
||||
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(() => {});
|
||||
}
|
||||
|
@ -671,6 +670,10 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
|
|||
}));
|
||||
}
|
||||
|
||||
setScrollerRef = (c: VirtuosoHandle) => {
|
||||
this.scroller = c;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { me, status, ancestorsIds, descendantsIds, intl } = this.props;
|
||||
|
||||
|
@ -788,10 +791,12 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
|
|||
<Stack space={2}>
|
||||
<div ref={this.setRef} className='thread'>
|
||||
<ScrollableList
|
||||
ref={this.setScrollerRef}
|
||||
onRefresh={this.handleRefresh}
|
||||
hasMore={!!this.state.next}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
placeholderComponent={() => <PlaceholderStatus thread />}
|
||||
initialTopMostItemIndex={ancestorsIds.size}
|
||||
>
|
||||
{children}
|
||||
</ScrollableList>
|
||||
|
|
|
@ -10,13 +10,19 @@ describe('<TrendsPanel />', () => {
|
|||
trends: ImmutableMap({
|
||||
items: fromJS([{
|
||||
name: 'hashtag 1',
|
||||
history: [{ accounts: [] }],
|
||||
history: [{
|
||||
day: '1652745600',
|
||||
uses: '294',
|
||||
accounts: '180',
|
||||
}],
|
||||
}]),
|
||||
}),
|
||||
};
|
||||
|
||||
render(<TrendsPanel limit={1} />, null, store);
|
||||
expect(screen.getByTestId('hashtag')).toHaveTextContent(/hashtag 1/i);
|
||||
expect(screen.getByTestId('hashtag')).toHaveTextContent(/180 people talking/i);
|
||||
expect(screen.getByTestId('sparklines')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders multiple trends', () => {
|
||||
|
|
|
@ -30,13 +30,21 @@ const messages = defineMessages({
|
|||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||
});
|
||||
|
||||
interface iActionButton {
|
||||
interface IActionButton {
|
||||
/** Target account for the action. */
|
||||
account: AccountEntity
|
||||
/** Type of action to prioritize, eg on Blocks and Mutes pages. */
|
||||
actionType?: 'muting' | 'blocking'
|
||||
/** Displays shorter text on the "Awaiting approval" button. */
|
||||
small?: boolean
|
||||
}
|
||||
|
||||
const ActionButton = ({ account, actionType, small }: iActionButton) => {
|
||||
/**
|
||||
* Circumstantial action button (usually "Follow") to display on accounts.
|
||||
* May say "Unblock" or something else, depending on the relationship and
|
||||
* `actionType` prop.
|
||||
*/
|
||||
const ActionButton: React.FC<IActionButton> = ({ account, actionType, small }) => {
|
||||
const dispatch = useDispatch();
|
||||
const features = useFeatures();
|
||||
const intl = useIntl();
|
||||
|
@ -45,40 +53,41 @@ const ActionButton = ({ account, actionType, small }: iActionButton) => {
|
|||
|
||||
const handleFollow = () => {
|
||||
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
|
||||
dispatch(unfollowAccount(account.get('id')));
|
||||
dispatch(unfollowAccount(account.id));
|
||||
} else {
|
||||
dispatch(followAccount(account.get('id')));
|
||||
dispatch(followAccount(account.id));
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlock = () => {
|
||||
if (account.getIn(['relationship', 'blocking'])) {
|
||||
dispatch(unblockAccount(account.get('id')));
|
||||
dispatch(unblockAccount(account.id));
|
||||
} else {
|
||||
dispatch(blockAccount(account.get('id')));
|
||||
dispatch(blockAccount(account.id));
|
||||
}
|
||||
};
|
||||
|
||||
const handleMute = () => {
|
||||
if (account.getIn(['relationship', 'muting'])) {
|
||||
dispatch(unmuteAccount(account.get('id')));
|
||||
dispatch(unmuteAccount(account.id));
|
||||
} else {
|
||||
dispatch(muteAccount(account.get('id')));
|
||||
dispatch(muteAccount(account.id));
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoteFollow = () => {
|
||||
dispatch(openModal('UNAUTHORIZED', {
|
||||
action: 'FOLLOW',
|
||||
account: account.get('id'),
|
||||
ap_id: account.get('url'),
|
||||
account: account.id,
|
||||
ap_id: account.url,
|
||||
}));
|
||||
};
|
||||
|
||||
/** Handles actionType='muting' */
|
||||
const mutingAction = () => {
|
||||
const isMuted = account.getIn(['relationship', 'muting']);
|
||||
const messageKey = isMuted ? messages.unmute : messages.mute;
|
||||
const text = intl.formatMessage(messageKey, { name: account.get('username') });
|
||||
const text = intl.formatMessage(messageKey, { name: account.username });
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
@ -90,10 +99,11 @@ const ActionButton = ({ account, actionType, small }: iActionButton) => {
|
|||
);
|
||||
};
|
||||
|
||||
/** Handles actionType='blocking' */
|
||||
const blockingAction = () => {
|
||||
const isBlocked = account.getIn(['relationship', 'blocking']);
|
||||
const messageKey = isBlocked ? messages.unblock : messages.block;
|
||||
const text = intl.formatMessage(messageKey, { name: account.get('username') });
|
||||
const text = intl.formatMessage(messageKey, { name: account.username });
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
@ -105,10 +115,9 @@ const ActionButton = ({ account, actionType, small }: iActionButton) => {
|
|||
);
|
||||
};
|
||||
|
||||
const empty = <></>;
|
||||
|
||||
if (!me) {
|
||||
// Remote follow
|
||||
/** Render a remote follow button, depending on features. */
|
||||
const renderRemoteFollow = () => {
|
||||
// Remote follow through the API.
|
||||
if (features.remoteInteractionsAPI) {
|
||||
return (
|
||||
<Button
|
||||
|
@ -117,18 +126,34 @@ const ActionButton = ({ account, actionType, small }: iActionButton) => {
|
|||
text={intl.formatMessage(messages.follow)}
|
||||
/>
|
||||
);
|
||||
// Pleroma's classic remote follow form.
|
||||
} else if (features.pleromaRemoteFollow) {
|
||||
return (
|
||||
<form method='POST' action='/main/ostatus'>
|
||||
<input type='hidden' name='nickname' value={account.acct} />
|
||||
<input type='hidden' name='profile' value='' />
|
||||
<Button text={intl.formatMessage(messages.remote_follow)} type='submit' />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form method='POST' action='/main/ostatus'>
|
||||
<input type='hidden' name='nickname' value={account.get('acct')} />
|
||||
<input type='hidden' name='profile' value='' />
|
||||
<Button text={intl.formatMessage(messages.remote_follow)} type='submit' />
|
||||
</form>
|
||||
);
|
||||
return null;
|
||||
};
|
||||
|
||||
/** Render remote follow if federating, otherwise hide the button. */
|
||||
const renderLoggedOut = () => {
|
||||
if (features.federating) {
|
||||
return renderRemoteFollow();
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
if (!me) {
|
||||
return renderLoggedOut();
|
||||
}
|
||||
|
||||
if (me !== account.get('id')) {
|
||||
if (me !== account.id) {
|
||||
const isFollowing = account.getIn(['relationship', 'following']);
|
||||
const blockedBy = account.getIn(['relationship', 'blocked_by']) as boolean;
|
||||
|
||||
|
@ -140,9 +165,9 @@ const ActionButton = ({ account, actionType, small }: iActionButton) => {
|
|||
}
|
||||
}
|
||||
|
||||
if (!account.get('relationship')) {
|
||||
if (account.relationship.isEmpty()) {
|
||||
// Wait until the relationship is loaded
|
||||
return empty;
|
||||
return null;
|
||||
} else if (account.getIn(['relationship', 'requested'])) {
|
||||
// Awaiting acceptance
|
||||
return (
|
||||
|
@ -176,7 +201,7 @@ const ActionButton = ({ account, actionType, small }: iActionButton) => {
|
|||
<Button
|
||||
theme='danger'
|
||||
size='sm'
|
||||
text={intl.formatMessage(messages.unblock, { name: account.get('username') })}
|
||||
text={intl.formatMessage(messages.unblock, { name: account.username })}
|
||||
onClick={handleBlock}
|
||||
/>
|
||||
);
|
||||
|
@ -193,7 +218,7 @@ const ActionButton = ({ account, actionType, small }: iActionButton) => {
|
|||
);
|
||||
}
|
||||
|
||||
return empty;
|
||||
return null;
|
||||
};
|
||||
|
||||
export default ActionButton;
|
||||
|
|
|
@ -4,7 +4,7 @@ import React, { useEffect } from 'react';
|
|||
import { FormattedDate, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { fetchHistory } from 'soapbox/actions/history';
|
||||
import AttachmentThumbs from 'soapbox/components/attachment_thumbs';
|
||||
import AttachmentThumbs from 'soapbox/components/attachment-thumbs';
|
||||
import { HStack, Modal, Spinner, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
|
@ -75,10 +75,7 @@ const CompareHistoryModal: React.FC<ICompareHistoryModal> = ({ onClose, statusId
|
|||
)}
|
||||
|
||||
{version.media_attachments.size > 0 && (
|
||||
<AttachmentThumbs
|
||||
compact
|
||||
media={version.media_attachments}
|
||||
/>
|
||||
<AttachmentThumbs media={version.media_attachments} />
|
||||
)}
|
||||
|
||||
<Text align='right' tag='span' theme='muted' size='sm'>
|
||||
|
|
|
@ -9,6 +9,18 @@ interface IHotkeysModal {
|
|||
onClose: () => void,
|
||||
}
|
||||
|
||||
const Hotkey: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||
<kbd className='px-1.5 py-1 bg-primary-50 dark:bg-slate-700 border border-solid border-primary-200 rounded-md dark:border-slate-500 text-xs font-sans'>
|
||||
{children}
|
||||
</kbd>
|
||||
);
|
||||
|
||||
const TableCell: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||
<td className='pb-3 px-2'>
|
||||
{children}
|
||||
</td>
|
||||
);
|
||||
|
||||
const HotkeysModal: React.FC<IHotkeysModal> = ({ onClose }) => {
|
||||
const features = useAppSelector((state) => getFeatures(state.instance));
|
||||
|
||||
|
@ -16,142 +28,145 @@ const HotkeysModal: React.FC<IHotkeysModal> = ({ onClose }) => {
|
|||
<Modal
|
||||
title={<FormattedMessage id='keyboard_shortcuts.heading' defaultMessage='Keyboard shortcuts' />}
|
||||
onClose={onClose}
|
||||
width='4xl'
|
||||
>
|
||||
<div className='compose-modal__content'>
|
||||
<div className='flex flex-col lg:flex-row text-xs'>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><FormattedMessage id='keyboard_shortcuts.hotkey' defaultMessage='Hotkey' /></th>
|
||||
<th className='pb-2 font-bold'><FormattedMessage id='keyboard_shortcuts.hotkey' defaultMessage='Hotkey' /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><kbd>r</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.reply' defaultMessage='to reply' /></td>
|
||||
<TableCell><Hotkey>r</Hotkey></TableCell>
|
||||
<TableCell><FormattedMessage id='keyboard_shortcuts.reply' defaultMessage='to reply' /></TableCell>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>m</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.mention' defaultMessage='to mention author' /></td>
|
||||
<TableCell><Hotkey>m</Hotkey></TableCell>
|
||||
<TableCell><FormattedMessage id='keyboard_shortcuts.mention' defaultMessage='to mention author' /></TableCell>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>p</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.profile' defaultMessage="to open author's profile" /></td>
|
||||
<TableCell><Hotkey>p</Hotkey></TableCell>
|
||||
<TableCell><FormattedMessage id='keyboard_shortcuts.profile' defaultMessage="to open author's profile" /></TableCell>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>f</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.favourite' defaultMessage='to like' /></td>
|
||||
<TableCell><Hotkey>f</Hotkey></TableCell>
|
||||
<TableCell><FormattedMessage id='keyboard_shortcuts.favourite' defaultMessage='to like' /></TableCell>
|
||||
</tr>
|
||||
{features.emojiReacts && (
|
||||
<tr>
|
||||
<td><kbd>e</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.react' defaultMessage='to react' /></td>
|
||||
<TableCell><Hotkey>e</Hotkey></TableCell>
|
||||
<TableCell><FormattedMessage id='keyboard_shortcuts.react' defaultMessage='to react' /></TableCell>
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
<td><kbd>b</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.boost' defaultMessage='to repost' /></td>
|
||||
<TableCell><Hotkey>b</Hotkey></TableCell>
|
||||
<TableCell><FormattedMessage id='keyboard_shortcuts.boost' defaultMessage='to repost' /></TableCell>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>enter</kbd>, <kbd>o</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.enter' defaultMessage='to open post' /></td>
|
||||
<TableCell><Hotkey>enter</Hotkey>, <Hotkey>o</Hotkey></TableCell>
|
||||
<TableCell><FormattedMessage id='keyboard_shortcuts.enter' defaultMessage='to open post' /></TableCell>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>a</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.open_media' defaultMessage='to open media' /></td>
|
||||
<TableCell><Hotkey>a</Hotkey></TableCell>
|
||||
<TableCell><FormattedMessage id='keyboard_shortcuts.open_media' defaultMessage='to open media' /></TableCell>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><FormattedMessage id='keyboard_shortcuts.hotkey' defaultMessage='Hotkey' /></th>
|
||||
<th className='pb-2 font-bold'><FormattedMessage id='keyboard_shortcuts.hotkey' defaultMessage='Hotkey' /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{features.spoilers && (
|
||||
<tr>
|
||||
<td><kbd>x</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.toggle_hidden' defaultMessage='to show/hide text behind CW' /></td>
|
||||
<TableCell><Hotkey>x</Hotkey></TableCell>
|
||||
<TableCell><FormattedMessage id='keyboard_shortcuts.toggle_hidden' defaultMessage='to show/hide text behind CW' /></TableCell>
|
||||
</tr>
|
||||
)}
|
||||
{features.spoilers && (
|
||||
<tr>
|
||||
<td><kbd>h</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.toggle_sensitivity' defaultMessage='to show/hide media' /></td>
|
||||
<TableCell><Hotkey>h</Hotkey></TableCell>
|
||||
<TableCell><FormattedMessage id='keyboard_shortcuts.toggle_sensitivity' defaultMessage='to show/hide media' /></TableCell>
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
<td><kbd>up</kbd>, <kbd>k</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.up' defaultMessage='to move up in the list' /></td>
|
||||
<TableCell><Hotkey>up</Hotkey>, <Hotkey>k</Hotkey></TableCell>
|
||||
<TableCell><FormattedMessage id='keyboard_shortcuts.up' defaultMessage='to move up in the list' /></TableCell>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>down</kbd>, <kbd>j</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.down' defaultMessage='to move down in the list' /></td>
|
||||
<TableCell><Hotkey>down</Hotkey>, <Hotkey>j</Hotkey></TableCell>
|
||||
<TableCell><FormattedMessage id='keyboard_shortcuts.down' defaultMessage='to move down in the list' /></TableCell>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>n</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.compose' defaultMessage='to focus the compose textarea' /></td>
|
||||
<TableCell><Hotkey>n</Hotkey></TableCell>
|
||||
<TableCell><FormattedMessage id='keyboard_shortcuts.compose' defaultMessage='to focus the compose textarea' /></TableCell>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>alt</kbd> + <kbd>n</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.toot' defaultMessage='to start a new post' /></td>
|
||||
<TableCell><Hotkey>alt</Hotkey> + <Hotkey>n</Hotkey></TableCell>
|
||||
<TableCell><FormattedMessage id='keyboard_shortcuts.toot' defaultMessage='to start a new post' /></TableCell>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>backspace</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.back' defaultMessage='to navigate back' /></td>
|
||||
<TableCell><Hotkey>backspace</Hotkey></TableCell>
|
||||
<TableCell><FormattedMessage id='keyboard_shortcuts.back' defaultMessage='to navigate back' /></TableCell>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>s</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.search' defaultMessage='to focus search' /></td>
|
||||
<TableCell><Hotkey>s</Hotkey></TableCell>
|
||||
<TableCell><FormattedMessage id='keyboard_shortcuts.search' defaultMessage='to focus search' /></TableCell>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>esc</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.unfocus' defaultMessage='to un-focus compose textarea/search' /></td>
|
||||
<TableCell><Hotkey>esc</Hotkey></TableCell>
|
||||
<TableCell><FormattedMessage id='keyboard_shortcuts.unfocus' defaultMessage='to un-focus compose textarea/search' /></TableCell>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><FormattedMessage id='keyboard_shortcuts.hotkey' defaultMessage='Hotkey' /></th>
|
||||
<th className='pb-2 font-bold'><FormattedMessage id='keyboard_shortcuts.hotkey' defaultMessage='Hotkey' /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><kbd>g</kbd> + <kbd>h</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.home' defaultMessage='to open home timeline' /></td>
|
||||
<TableCell><Hotkey>g</Hotkey> + <Hotkey>h</Hotkey></TableCell>
|
||||
<TableCell><FormattedMessage id='keyboard_shortcuts.home' defaultMessage='to open home timeline' /></TableCell>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>g</kbd> + <kbd>n</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.notifications' defaultMessage='to open notifications column' /></td>
|
||||
<TableCell><Hotkey>g</Hotkey> + <Hotkey>n</Hotkey></TableCell>
|
||||
<TableCell><FormattedMessage id='keyboard_shortcuts.notifications' defaultMessage='to open notifications column' /></TableCell>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>g</kbd> + <kbd>f</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.favourites' defaultMessage='to open likes list' /></td>
|
||||
<TableCell><Hotkey>g</Hotkey> + <Hotkey>f</Hotkey></TableCell>
|
||||
<TableCell><FormattedMessage id='keyboard_shortcuts.favourites' defaultMessage='to open likes list' /></TableCell>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>g</kbd> + <kbd>p</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.pinned' defaultMessage='to open pinned posts list' /></td>
|
||||
<TableCell><Hotkey>g</Hotkey> + <Hotkey>p</Hotkey></TableCell>
|
||||
<TableCell><FormattedMessage id='keyboard_shortcuts.pinned' defaultMessage='to open pinned posts list' /></TableCell>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>g</kbd> + <kbd>u</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.my_profile' defaultMessage='to open your profile' /></td>
|
||||
<TableCell><Hotkey>g</Hotkey> + <Hotkey>u</Hotkey></TableCell>
|
||||
<TableCell><FormattedMessage id='keyboard_shortcuts.my_profile' defaultMessage='to open your profile' /></TableCell>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>g</kbd> + <kbd>b</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.blocked' defaultMessage='to open blocked users list' /></td>
|
||||
<TableCell><Hotkey>g</Hotkey> + <Hotkey>b</Hotkey></TableCell>
|
||||
<TableCell><FormattedMessage id='keyboard_shortcuts.blocked' defaultMessage='to open blocked users list' /></TableCell>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>g</kbd> + <kbd>m</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.muted' defaultMessage='to open muted users list' /></td>
|
||||
<TableCell><Hotkey>g</Hotkey> + <Hotkey>m</Hotkey></TableCell>
|
||||
<TableCell><FormattedMessage id='keyboard_shortcuts.muted' defaultMessage='to open muted users list' /></TableCell>
|
||||
</tr>
|
||||
{features.followRequests && (
|
||||
<tr>
|
||||
<TableCell><Hotkey>g</Hotkey> + <Hotkey>r</Hotkey></TableCell>
|
||||
<TableCell><FormattedMessage id='keyboard_shortcuts.requests' defaultMessage='to open follow requests list' /></TableCell>
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
<td><kbd>g</kbd> + <kbd>r</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.requests' defaultMessage='to open follow requests list' /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>?</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.legend' defaultMessage='to display this legend' /></td>
|
||||
<TableCell><Hotkey>?</Hotkey></TableCell>
|
||||
<TableCell><FormattedMessage id='keyboard_shortcuts.legend' defaultMessage='to display this legend' /></TableCell>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -30,6 +30,7 @@ import {
|
|||
BirthdaysModal,
|
||||
AccountNoteModal,
|
||||
CompareHistoryModal,
|
||||
VerifySmsModal,
|
||||
FamiliarFollowersModal,
|
||||
} from 'soapbox/features/ui/util/async-components';
|
||||
|
||||
|
@ -67,6 +68,7 @@ const MODAL_COMPONENTS = {
|
|||
'BIRTHDAYS': BirthdaysModal,
|
||||
'ACCOUNT_NOTE': AccountNoteModal,
|
||||
'COMPARE_HISTORY': CompareHistoryModal,
|
||||
'VERIFY_SMS': VerifySmsModal,
|
||||
'FAMILIAR_FOLLOWERS': FamiliarFollowersModal,
|
||||
};
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|||
import { blockAccount } from 'soapbox/actions/accounts';
|
||||
import { submitReport, submitReportSuccess, submitReportFail } from 'soapbox/actions/reports';
|
||||
import { expandAccountTimeline } from 'soapbox/actions/timelines';
|
||||
import AttachmentThumbs from 'soapbox/components/attachment_thumbs';
|
||||
import AttachmentThumbs from 'soapbox/components/attachment-thumbs';
|
||||
import StatusContent from 'soapbox/components/status_content';
|
||||
import { Modal, ProgressBar, Stack, Text } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account_container';
|
||||
|
@ -61,7 +61,6 @@ const SelectedStatus = ({ statusId }: { statusId: string }) => {
|
|||
|
||||
{status.get('media_attachments').size > 0 && (
|
||||
<AttachmentThumbs
|
||||
compact
|
||||
media={status.get('media_attachments')}
|
||||
sensitive={status.get('sensitive')}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,233 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import OtpInput from 'react-otp-input';
|
||||
|
||||
import { verifyCredentials } from 'soapbox/actions/auth';
|
||||
import { closeModal } from 'soapbox/actions/modals';
|
||||
import snackbar from 'soapbox/actions/snackbar';
|
||||
import { reConfirmPhoneVerification, reRequestPhoneVerification } from 'soapbox/actions/verification';
|
||||
import { FormGroup, Input, Modal, Stack, Text } from 'soapbox/components/ui';
|
||||
import { validPhoneNumberRegex } from 'soapbox/features/verification/steps/sms-verification';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { getAccessToken } from 'soapbox/utils/auth';
|
||||
import { formatPhoneNumber } from 'soapbox/utils/phone';
|
||||
|
||||
interface IVerifySmsModal {
|
||||
onClose: (type: string) => void,
|
||||
}
|
||||
|
||||
enum Statuses {
|
||||
IDLE = 'IDLE',
|
||||
READY = 'READY',
|
||||
REQUESTED = 'REQUESTED',
|
||||
FAIL = 'FAIL',
|
||||
SUCCESS = 'SUCCESS',
|
||||
}
|
||||
|
||||
const VerifySmsModal: React.FC<IVerifySmsModal> = ({ onClose }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
const accessToken = useAppSelector((state) => getAccessToken(state));
|
||||
const title = useAppSelector((state) => state.instance.title);
|
||||
const isLoading = useAppSelector((state) => state.verification.get('isLoading') as boolean);
|
||||
|
||||
const [status, setStatus] = useState<Statuses>(Statuses.IDLE);
|
||||
const [phone, setPhone] = useState<string>('');
|
||||
const [verificationCode, setVerificationCode] = useState('');
|
||||
const [requestedAnother, setAlreadyRequestedAnother] = useState(false);
|
||||
|
||||
const isValid = validPhoneNumberRegex.test(phone);
|
||||
|
||||
const onChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const formattedPhone = formatPhoneNumber(event.target.value);
|
||||
|
||||
setPhone(formattedPhone);
|
||||
}, []);
|
||||
|
||||
const handleSubmit = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!isValid) {
|
||||
setStatus(Statuses.IDLE);
|
||||
dispatch(
|
||||
snackbar.error(
|
||||
intl.formatMessage({
|
||||
id: 'sms_verification.invalid',
|
||||
defaultMessage: 'Please enter a valid phone number.',
|
||||
}),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(reRequestPhoneVerification(phone)).then(() => {
|
||||
dispatch(
|
||||
snackbar.success(
|
||||
intl.formatMessage({
|
||||
id: 'sms_verification.success',
|
||||
defaultMessage: 'A verification code has been sent to your phone number.',
|
||||
}),
|
||||
),
|
||||
);
|
||||
})
|
||||
.finally(() => setStatus(Statuses.REQUESTED))
|
||||
.catch(() => {
|
||||
dispatch(
|
||||
snackbar.error(
|
||||
intl.formatMessage({
|
||||
id: 'sms_verification.fail',
|
||||
defaultMessage: 'Failed to send SMS message to your phone number.',
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const resendVerificationCode = (event?: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAlreadyRequestedAnother(true);
|
||||
handleSubmit(event as React.MouseEvent<HTMLButtonElement>);
|
||||
};
|
||||
|
||||
const onConfirmationClick = (event: any) => {
|
||||
switch (status) {
|
||||
case Statuses.IDLE:
|
||||
setStatus(Statuses.READY);
|
||||
break;
|
||||
case Statuses.READY:
|
||||
handleSubmit(event);
|
||||
break;
|
||||
case Statuses.REQUESTED:
|
||||
submitVerification();
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
};
|
||||
|
||||
const confirmationText = useMemo(() => {
|
||||
switch (status) {
|
||||
case Statuses.IDLE:
|
||||
return intl.formatMessage({
|
||||
id: 'sms_verification.modal.verify_sms',
|
||||
defaultMessage: 'Verify SMS',
|
||||
});
|
||||
case Statuses.READY:
|
||||
return intl.formatMessage({
|
||||
id: 'sms_verification.modal.verify_number',
|
||||
defaultMessage: 'Verify phone number',
|
||||
});
|
||||
case Statuses.REQUESTED:
|
||||
return intl.formatMessage({
|
||||
id: 'sms_verification.modal.verify_code',
|
||||
defaultMessage: 'Verify code',
|
||||
});
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
const renderModalBody = () => {
|
||||
switch (status) {
|
||||
case Statuses.IDLE:
|
||||
return (
|
||||
<Text theme='muted'>
|
||||
{intl.formatMessage({
|
||||
id: 'sms_verification.modal.verify_help_text',
|
||||
defaultMessage: 'Verify your phone number to start using {instance}.',
|
||||
}, {
|
||||
instance: title,
|
||||
})}
|
||||
</Text>
|
||||
);
|
||||
case Statuses.READY:
|
||||
return (
|
||||
<FormGroup labelText='Phone Number'>
|
||||
<Input
|
||||
type='text'
|
||||
value={phone}
|
||||
onChange={onChange}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
case Statuses.REQUESTED:
|
||||
return (
|
||||
<>
|
||||
<Text theme='muted' size='sm' align='center'>
|
||||
{intl.formatMessage({
|
||||
id: 'sms_verification.modal.enter_code',
|
||||
defaultMessage: 'We sent you a 6-digit code via SMS. Enter it below.',
|
||||
})}
|
||||
</Text>
|
||||
|
||||
<OtpInput
|
||||
value={verificationCode}
|
||||
onChange={setVerificationCode}
|
||||
numInputs={6}
|
||||
isInputNum
|
||||
shouldAutoFocus
|
||||
isDisabled={isLoading}
|
||||
containerStyle='flex justify-center mt-2 space-x-4'
|
||||
inputStyle='w-10i border-gray-300 rounded-md focus:ring-indigo-500 focus:border-indigo-500'
|
||||
/>
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const submitVerification = () => {
|
||||
// TODO: handle proper validation from Pepe -- expired vs invalid
|
||||
dispatch(reConfirmPhoneVerification(verificationCode))
|
||||
.then(() => {
|
||||
setStatus(Statuses.SUCCESS);
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
dispatch(verifyCredentials(accessToken))
|
||||
.then(() => dispatch(closeModal('VERIFY_SMS')));
|
||||
|
||||
})
|
||||
.catch(() => dispatch(
|
||||
snackbar.error(
|
||||
intl.formatMessage({
|
||||
id: 'sms_verification.invalid',
|
||||
defaultMessage: 'Your SMS token has expired.',
|
||||
}),
|
||||
),
|
||||
));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (verificationCode.length === 6) {
|
||||
submitVerification();
|
||||
}
|
||||
}, [verificationCode]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
intl.formatMessage({
|
||||
id: 'sms_verification.modal.verify_title',
|
||||
defaultMessage: 'Verify your phone number',
|
||||
})
|
||||
}
|
||||
onClose={() => onClose('VERIFY_SMS')}
|
||||
cancelAction={status === Statuses.IDLE ? () => onClose('VERIFY_SMS') : undefined}
|
||||
cancelText='Skip for now'
|
||||
confirmationAction={onConfirmationClick}
|
||||
confirmationText={confirmationText}
|
||||
secondaryAction={status === Statuses.REQUESTED ? resendVerificationCode : undefined}
|
||||
secondaryText={status === Statuses.REQUESTED ? intl.formatMessage({
|
||||
id: 'sms_verification.modal.resend_code',
|
||||
defaultMessage: 'Resend verification code?',
|
||||
}) : undefined}
|
||||
secondaryDisabled={requestedAnother}
|
||||
>
|
||||
<Stack space={4}>
|
||||
{renderModalBody()}
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default VerifySmsModal;
|
|
@ -28,7 +28,7 @@ const ProfileStats: React.FC<IProfileStats> = ({ account, onClickHandler }) => {
|
|||
|
||||
return (
|
||||
<HStack alignItems='center' space={3}>
|
||||
<NavLink to={`/@${account.acct}/followers`} onClick={onClickHandler} title={intl.formatNumber(account.followers_count)}>
|
||||
<NavLink to={`/@${account.acct}/followers`} onClick={onClickHandler} title={intl.formatNumber(account.followers_count)} className='hover:underline'>
|
||||
<HStack alignItems='center' space={1}>
|
||||
<Text theme='primary' weight='bold' size='sm'>
|
||||
{shortNumberFormat(account.followers_count)}
|
||||
|
@ -39,7 +39,7 @@ const ProfileStats: React.FC<IProfileStats> = ({ account, onClickHandler }) => {
|
|||
</HStack>
|
||||
</NavLink>
|
||||
|
||||
<NavLink to={`/@${account.acct}/following`} onClick={onClickHandler} title={intl.formatNumber(account.following_count)}>
|
||||
<NavLink to={`/@${account.acct}/following`} onClick={onClickHandler} title={intl.formatNumber(account.following_count)} className='hover:underline'>
|
||||
<HStack alignItems='center' space={1}>
|
||||
<Text theme='primary' weight='bold' size='sm'>
|
||||
{shortNumberFormat(account.following_count)}
|
||||
|
|
|
@ -29,6 +29,8 @@ const ReplyMentionsModal: React.FC<IReplyMentionsModal> = ({ onClose }) => {
|
|||
<Modal
|
||||
title={<FormattedMessage id='navigation_bar.in_reply_to' defaultMessage='In reply to' />}
|
||||
onClose={onClickClose}
|
||||
closeIcon={require('@tabler/icons/icons/arrow-left.svg')}
|
||||
closePosition='left'
|
||||
>
|
||||
<div className='reply-mentions-modal__accounts'>
|
||||
{mentions.map(accountId => <Account key={accountId} accountId={accountId} added author={author === accountId} />)}
|
||||
|
|
|
@ -86,19 +86,6 @@ class UserPanel extends ImmutablePureComponent {
|
|||
</Stack>
|
||||
|
||||
<HStack alignItems='center' space={3}>
|
||||
{account.get('statuses_count') >= 0 && (
|
||||
<Link to={`/@${account.get('acct')}`} title={intl.formatNumber(account.get('statuses_count'))}>
|
||||
<HStack alignItems='center' space={1}>
|
||||
<Text theme='primary' weight='bold' size='sm'>
|
||||
{shortNumberFormat(account.get('statuses_count'))}
|
||||
</Text>
|
||||
<Text weight='bold' size='sm'>
|
||||
<FormattedMessage className='user-panel-stats-item__label' id='account.posts' defaultMessage='Posts' />
|
||||
</Text>
|
||||
</HStack>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{account.get('followers_count') >= 0 && (
|
||||
<Link to={`/@${account.get('acct')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
|
||||
<HStack alignItems='center' space={1}>
|
||||
|
|
|
@ -37,6 +37,7 @@ import RemoteInstancePage from 'soapbox/pages/remote_instance_page';
|
|||
import StatusPage from 'soapbox/pages/status_page';
|
||||
import { getAccessToken } from 'soapbox/utils/auth';
|
||||
import { getVapidKey } from 'soapbox/utils/auth';
|
||||
import { cacheCurrentUrl } from 'soapbox/utils/redirect';
|
||||
import { isStandalone } from 'soapbox/utils/state';
|
||||
// import GroupSidebarPanel from '../groups/sidebar_panel';
|
||||
|
||||
|
@ -278,7 +279,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {
|
|||
<WrappedRoute path='/@:username/following' publicRoute={!authenticatedProfile} component={Following} page={ProfilePage} content={children} />
|
||||
<WrappedRoute path='/@:username/media' publicRoute={!authenticatedProfile} component={AccountGallery} page={ProfilePage} content={children} />
|
||||
<WrappedRoute path='/@:username/tagged/:tag' exact component={AccountTimeline} page={ProfilePage} content={children} />
|
||||
<WrappedRoute path='/@:username/favorites' component={FavouritedStatuses} page={ProfilePage} content={children} />
|
||||
<WrappedRoute path='/@:username/favorites' component={FavouritedStatuses} page={ProfilePage} content={children} />
|
||||
<WrappedRoute path='/@:username/pins' component={PinnedStatuses} page={ProfilePage} content={children} />
|
||||
<WrappedRoute path='/@:username/posts/:statusId' publicRoute exact page={StatusPage} component={Status} content={children} />
|
||||
<Redirect from='/@:username/:statusId' to='/@:username/posts/:statusId' />
|
||||
|
@ -330,6 +331,7 @@ const UI: React.FC = ({ children }) => {
|
|||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const dispatch = useDispatch();
|
||||
const { guestExperience } = useSoapboxConfig();
|
||||
|
||||
const [draggingOver, setDraggingOver] = useState<boolean>(false);
|
||||
const [mobile, setMobile] = useState<boolean>(isMobile(window.innerWidth));
|
||||
|
@ -479,7 +481,7 @@ const UI: React.FC = ({ children }) => {
|
|||
document.addEventListener('drop', handleDrop, false);
|
||||
document.addEventListener('dragleave', handleDragLeave, false);
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.addEventListener('message', handleServiceWorkerPostMessage);
|
||||
}
|
||||
|
||||
|
@ -608,6 +610,11 @@ const UI: React.FC = ({ children }) => {
|
|||
// Wait for login to succeed or fail
|
||||
if (me === null) return null;
|
||||
|
||||
if (!me && !guestExperience) {
|
||||
cacheCurrentUrl(history.location);
|
||||
return <Redirect to='/login' />;
|
||||
}
|
||||
|
||||
type HotkeyHandlers = { [key: string]: (keyEvent?: KeyboardEvent) => void };
|
||||
|
||||
const handlers: HotkeyHandlers = {
|
||||
|
|
|
@ -403,7 +403,7 @@ export function ModalContainer() {
|
|||
}
|
||||
|
||||
export function ProfileHoverCard() {
|
||||
return import(/* webpackChunkName: "features/ui" */'soapbox/components/profile_hover_card');
|
||||
return import(/* webpackChunkName: "features/ui" */'soapbox/components/profile-hover-card');
|
||||
}
|
||||
|
||||
export function CryptoDonate() {
|
||||
|
@ -502,6 +502,10 @@ export function AuthTokenList() {
|
|||
return import(/* webpackChunkName: "features/auth_token_list" */'../../auth_token_list');
|
||||
}
|
||||
|
||||
export function VerifySmsModal() {
|
||||
return import(/* webpackChunkName: "features/ui" */'../components/modals/verify-sms-modal');
|
||||
}
|
||||
|
||||
export function FamiliarFollowersModal() {
|
||||
return import(/*webpackChunkName: "modals/familiar_followers_modal" */'../components/familiar_followers_modal');
|
||||
}
|
||||
|
|
|
@ -86,13 +86,14 @@ const WrappedRoute: React.FC<IWrappedRoute> = ({
|
|||
</>
|
||||
);
|
||||
|
||||
const renderLoading = () => renderWithLayout(<ColumnLoading />);
|
||||
const renderLoading = () => renderWithLayout(<ColumnLoading />);
|
||||
const renderForbidden = () => renderWithLayout(<ColumnForbidden />);
|
||||
const renderError = (props: any) => renderWithLayout(<BundleColumnError {...props} />);
|
||||
const renderError = (props: any) => renderWithLayout(<BundleColumnError {...props} />);
|
||||
|
||||
const loginRedirect = () => {
|
||||
const actualUrl = encodeURIComponent(`${history.location.pathname}${history.location.search}`);
|
||||
return <Redirect to={`/login?redirect_uri=${actualUrl}`} />;
|
||||
localStorage.setItem('soapbox:redirect_uri', actualUrl);
|
||||
return <Redirect to='/login' />;
|
||||
};
|
||||
|
||||
const authorized = [
|
||||
|
|
|
@ -11,6 +11,7 @@ import snackbar from 'soapbox/actions/snackbar';
|
|||
import { createAccount } from 'soapbox/actions/verification';
|
||||
import { removeStoredVerification } from 'soapbox/actions/verification';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { getRedirectUrl } from 'soapbox/utils/redirect';
|
||||
|
||||
import { Button, Form, FormGroup, Input } from '../../components/ui';
|
||||
|
||||
|
@ -81,7 +82,8 @@ const Registration = () => {
|
|||
}, []);
|
||||
|
||||
if (shouldRedirect) {
|
||||
return <Redirect to='/' />;
|
||||
const redirectUri = getRedirectUrl();
|
||||
return <Redirect to={redirectUri} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -49,7 +49,10 @@ const AgeVerification = () => {
|
|||
snackbar.error(
|
||||
intl.formatMessage({
|
||||
id: 'age_verification.fail',
|
||||
defaultMessage: `You must be ${ageMinimum} years old or older.`,
|
||||
defaultMessage: 'You must be {ageMinimum, plural, one {# year} other {# years}} old or older.',
|
||||
values: {
|
||||
ageMinimum,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import OtpInput from 'react-otp-input';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import snackbar from 'soapbox/actions/snackbar';
|
||||
import { confirmPhoneVerification, requestPhoneVerification } from 'soapbox/actions/verification';
|
||||
|
@ -167,4 +167,4 @@ const SmsVerification = () => {
|
|||
};
|
||||
|
||||
|
||||
export default SmsVerification;
|
||||
export { SmsVerification as default, validPhoneNumberRegex };
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import LandingGradient from 'soapbox/components/landing-gradient';
|
||||
import SiteLogo from 'soapbox/components/site-logo';
|
||||
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
|
||||
import { NotificationsContainer } from 'soapbox/features/ui/util/async-components';
|
||||
import { useAppSelector, useOwnAccount } from 'soapbox/hooks';
|
||||
|
||||
import { logOut } from '../../actions/auth';
|
||||
import { Button, Stack, Text } from '../../components/ui';
|
||||
|
@ -15,12 +17,24 @@ import { Button, Stack, Text } from '../../components/ui';
|
|||
const WaitlistPage = ({ account }) => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
const title = useAppSelector((state) => state.instance.title);
|
||||
|
||||
const me = useOwnAccount();
|
||||
const isSmsVerified = me.getIn(['source', 'sms_verified']);
|
||||
|
||||
const onClickLogOut = (event) => {
|
||||
event.preventDefault();
|
||||
dispatch(logOut(intl));
|
||||
};
|
||||
|
||||
const openVerifySmsModal = () => dispatch(openModal('VERIFY_SMS'));
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSmsVerified) {
|
||||
openVerifySmsModal();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LandingGradient />
|
||||
|
@ -41,19 +55,20 @@ const WaitlistPage = ({ account }) => {
|
|||
</header>
|
||||
|
||||
<div className='-mt-16 flex flex-col justify-center items-center h-full'>
|
||||
<div className='max-w-2xl'>
|
||||
<div className='max-w-xl'>
|
||||
<Stack space={4}>
|
||||
<img src='/instance/images/waitlist.png' className='mx-auto w-32 h-32' alt='Waitlisted' />
|
||||
|
||||
<Stack space={2}>
|
||||
<Text size='2xl' align='center' weight='bold'>
|
||||
@{account.acct} has been created successfully!
|
||||
</Text>
|
||||
<Text size='lg' theme='muted' align='center' weight='medium'>
|
||||
Due to massive demand, we have placed you on our waitlist.
|
||||
We love you, and you're not just another number to us.
|
||||
We are working to get you on our platform. Stay tuned!
|
||||
Welcome back to {title}! You were previously placed on our
|
||||
waitlist. Please verify your phone number to receive
|
||||
immediate access to your account!
|
||||
</Text>
|
||||
|
||||
<div className='text-center'>
|
||||
<Button onClick={openVerifySmsModal} theme='primary'>Verify phone number</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
"account.column_settings.title": "Ustawienia osi czasu",
|
||||
"account.deactivated": "Dezaktywowany(-a)",
|
||||
"account.direct": "Wyślij wiadomość bezpośrednią do @{name}",
|
||||
"account.domain_blocked": "Wyciszono domenę",
|
||||
"account.edit_profile": "Edytuj profil",
|
||||
"account.endorse": "Polecaj na profilu",
|
||||
"account.familiar_followers": "Obserwowany(-a) przez {accounts}",
|
||||
|
@ -35,6 +36,7 @@
|
|||
"account.mention": "Wspomnij",
|
||||
"account.moved_to": "{name} przeniósł(-osła) się do:",
|
||||
"account.mute": "Wycisz @{name}",
|
||||
"account.muted": "Wyciszono",
|
||||
"account.never_active": "Nigdy",
|
||||
"account.posts": "Wpisy",
|
||||
"account.posts_with_replies": "Wpisy i odpowiedzi",
|
||||
|
@ -81,7 +83,7 @@
|
|||
"admin.dashcounters.user_count_label": "użytkownicy łącznie",
|
||||
"admin.dashwidgets.email_list_header": "Lista adresów e-mail",
|
||||
"admin.dashwidgets.software_header": "Oprogramowanie",
|
||||
"admin.latest_accounts_panel.more": "Click to see {count} {count, plural, one {account} other {accounts}}",
|
||||
"admin.latest_accounts_panel.more": "Naciśnij, aby zobaczyć {count} {count, plural, one {konto} few {konta} many {kont}}",
|
||||
"admin.latest_accounts_panel.title": "Najnowsze konta",
|
||||
"admin.moderation_log.empty_message": "Nie wykonałeś(-aś) jeszcze żadnych działań moderacyjnych. Kiedy jakieś zostaną wykonane, ich historia pojawi się tutaj.",
|
||||
"admin.reports.actions.close": "Zamknij",
|
||||
|
@ -124,7 +126,9 @@
|
|||
"admin_nav.awaiting_approval": "Oczekujące zgłoszenia",
|
||||
"admin_nav.dashboard": "Panel administracyjny",
|
||||
"admin_nav.reports": "Zgłoszenia",
|
||||
"alert.unexpected.body": "Przepraszamy za niedogodność. Jeśli problem nie zniknie, skontaktuj się z naszym wsparciem technicznym. Możesz też spróbować {clearCookies} (zostaniesz wylogowany(-a) z konta).",
|
||||
"age_verification.header": "Wprowadź datę urodzenia",
|
||||
"age_verification.fail": "Musisz mieć przynajmniej {ageMinimum, plural, one {# rok} few {# lata} many {# lat} other {# lat}}.",
|
||||
"alert.unexpected.body": "Przepraszamy za niedogodności. Jeżeli problem nie ustanie, skontaktuj się z naszym wsparciem technicznym. Możesz też spróbować {clearCookies} (zostaniesz wylogowany(-a)).",
|
||||
"alert.unexpected.browser": "Przeglądarka",
|
||||
"alert.unexpected.clear_cookies": "wyczyścić pliki cookies i dane przeglądarki",
|
||||
"alert.unexpected.links.help": "Centrum pomocy",
|
||||
|
@ -151,6 +155,7 @@
|
|||
"app_create.scopes_placeholder": "np. „read write follow”",
|
||||
"app_create.submit": "Utwórz aplikację",
|
||||
"app_create.website_label": "Strona",
|
||||
"auth_layout.register": "Utwórz konto",
|
||||
"auth.invalid_credentials": "Nieprawidłowa nazwa użytkownika lub hasło",
|
||||
"auth.logged_out": "Wylogowano.",
|
||||
"backups.actions.create": "Utwórz kopię zapasową",
|
||||
|
@ -158,7 +163,7 @@
|
|||
"backups.empty_message.action": "Chcesz utworzyć?",
|
||||
"backups.pending": "Oczekująca",
|
||||
"beta.also_available": "Dostępne w językach:",
|
||||
"birthdays_modal.empty": "Nikt kogo znasz nie ma dziś urodzin.",
|
||||
"birthdays_modal.empty": "Żaden z Twoich znajomych nie ma dziś urodzin.",
|
||||
"birthday_panel.title": "Urodziny",
|
||||
"boost_modal.combo": "Naciśnij {combo}, aby pominąć to następnym razem",
|
||||
"bundle_column_error.body": "Coś poszło nie tak podczas ładowania tego składnika.",
|
||||
|
@ -367,7 +372,7 @@
|
|||
"developers.navigation.app_create_label": "Utwórz aplikację",
|
||||
"developers.navigation.intentional_error_label": "Wywołaj błąd",
|
||||
"developers.navigation.leave_developers_label": "Opuść programistów",
|
||||
"developers.navigation.network_error_label": "Network error",
|
||||
"developers.navigation.network_error_label": "Błąd sieci",
|
||||
"developers.navigation.settings_store_label": "Settings store",
|
||||
"developers.navigation.test_timeline_label": "Testowa oś czasu",
|
||||
"developers.settings_store.hint": "Możesz tu bezpośrednio edytować swoje ustawienia. UWAŻAJ! Edytowanie tej sekcji może uszkodzić Twoje konto, co może zostać naprawione tylko przez API.",
|
||||
|
@ -378,6 +383,7 @@
|
|||
"directory.recently_active": "Ostatnio aktywni",
|
||||
"donate": "Wesprzyj",
|
||||
"donate_crypto": "Przekaż kryptowalutę",
|
||||
"edit_email.header": "Zmień e-mail",
|
||||
"edit_federation.followers_only": "Ukryj wpisy z wyjątkiem obserwowanych",
|
||||
"edit_federation.force_nsfw": "Wymuś oznaczanie załączników jako wrażliwe",
|
||||
"edit_federation.media_removal": "Wycinaj media",
|
||||
|
@ -420,14 +426,22 @@
|
|||
"edit_profile.hints.stranger_notifications": "Wyświetlaj tylko powiadomienia od osób, które obserwujesz",
|
||||
"edit_profile.save": "Zapisz",
|
||||
"edit_profile.success": "Zapisano profil!",
|
||||
"email_confirmation.success": "Twój adres e-mail został potwierdzony!",
|
||||
"email_passthru.confirmed.body": "Zamknij tę kartę i kontynuuj rejestrację z {bold}, z którego został wysłany ten e-mail.",
|
||||
"email_passthru.confirmed.heading": "Potwierdzono adres e-mail!",
|
||||
"email_passthru.fail.expired": "Token z maila wygasł.",
|
||||
"email_passthru.fail.generic": "Nie udało się potwierdzić adresu e-mail",
|
||||
"email_passthru.fail.not_found": "Token z maila jest nieprawidłowy.",
|
||||
"email_passthru.generic_fail.body": "Poproś o nowe potwierdzenie adresu e-mail.",
|
||||
"email_passthru.generic_fail.heading": "Coś poszło nie tak",
|
||||
"email_passthru.success": "Twój adres e-mail został zweryfikowany",
|
||||
"email_passthru.token_expired.body": "Token e-mail wygasł. Poproś o nowe potwierdzenie adresu e-mail z {bold}, z którego został wysłany ten e-mail.",
|
||||
"email_passthru.token_expired.heading": "Token wygasł",
|
||||
"email_passthru.token_not_found.body": "Nie odnaleziono tokenu e-mail. Poproś o nowe potwierdzenie adresu e-mail z {bold}, z którego został wysłany ten e-mail.",
|
||||
"email_passthru.token_not_found.heading": "Nieprawidłowy token",
|
||||
"email_verification.exists": "Ten adres e-mail już jest zajęty",
|
||||
"email_verification.fail": "Nie udało się zażądać weryfikacji e-mail",
|
||||
"email_verification.header": "Wprowadź adres e-mail",
|
||||
"embed.instructions": "Osadź ten wpis na swojej stronie wklejając poniższy kod.",
|
||||
"embed.preview": "Tak będzie to wyglądać:",
|
||||
"emoji_button.activity": "Aktywność",
|
||||
|
@ -595,34 +609,34 @@
|
|||
"introduction.welcome.action": "Let's go!",
|
||||
"introduction.welcome.headline": "First steps",
|
||||
"introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.",
|
||||
"keyboard_shortcuts.back": "aby cofnąć się",
|
||||
"keyboard_shortcuts.blocked": "aby przejść do listy zablokowanych użytkowników",
|
||||
"keyboard_shortcuts.boost": "aby podbić wpis",
|
||||
"keyboard_shortcuts.compose": "aby przejść do pola tworzenia wpisu",
|
||||
"keyboard_shortcuts.down": "aby przejść na dół listy",
|
||||
"keyboard_shortcuts.enter": "aby otworzyć wpis",
|
||||
"keyboard_shortcuts.favourite": "aby dodać do ulubionych",
|
||||
"keyboard_shortcuts.favourites": "aby przejść do listy ulubionych wpisów",
|
||||
"keyboard_shortcuts.back": "cofnij się",
|
||||
"keyboard_shortcuts.blocked": "przejdź do listy zablokowanych",
|
||||
"keyboard_shortcuts.boost": "podbij wpis",
|
||||
"keyboard_shortcuts.compose": "przejdź do pola tworzenia wpisu",
|
||||
"keyboard_shortcuts.down": "przejdź na dół listy",
|
||||
"keyboard_shortcuts.enter": "otwórz wpis",
|
||||
"keyboard_shortcuts.favourite": "dodaj do ulubionych",
|
||||
"keyboard_shortcuts.favourites": "przejdź do listy ulubionych wpisów",
|
||||
"keyboard_shortcuts.heading": "Skróty klawiszowe",
|
||||
"keyboard_shortcuts.home": "aby otworzyć stronę główną",
|
||||
"keyboard_shortcuts.home": "otwórz stronę główną",
|
||||
"keyboard_shortcuts.hotkey": "Klawisz",
|
||||
"keyboard_shortcuts.legend": "aby wyświetlić tę legendę",
|
||||
"keyboard_shortcuts.mention": "aby wspomnieć o autorze",
|
||||
"keyboard_shortcuts.muted": "aby przejść do listy wyciszonych użytkowników",
|
||||
"keyboard_shortcuts.my_profile": "aby otworzyć własny profil",
|
||||
"keyboard_shortcuts.notifications": "aby otworzyć kolumnę powiadomień",
|
||||
"keyboard_shortcuts.open_media": "aby otworzyć media",
|
||||
"keyboard_shortcuts.pinned": "aby przejść do listy przypiętych wpisów",
|
||||
"keyboard_shortcuts.profile": "aby przejść do profilu autora wpisu",
|
||||
"keyboard_shortcuts.react": "aby zareagować na wpis",
|
||||
"keyboard_shortcuts.reply": "aby odpowiedzieć",
|
||||
"keyboard_shortcuts.requests": "aby przejść do listy próśb o możliwość śledzenia",
|
||||
"keyboard_shortcuts.search": "aby przejść do pola wyszukiwania",
|
||||
"keyboard_shortcuts.toggle_hidden": "aby wyświetlić lub ukryć wpis spod CW",
|
||||
"keyboard_shortcuts.toggle_sensitivity": "by pokazać/ukryć multimedia",
|
||||
"keyboard_shortcuts.toot": "aby utworzyć nowy wpis",
|
||||
"keyboard_shortcuts.unfocus": "aby opuścić pole wyszukiwania/pisania",
|
||||
"keyboard_shortcuts.up": "aby przejść na górę listy",
|
||||
"keyboard_shortcuts.legend": "wyświetl tę legendę",
|
||||
"keyboard_shortcuts.mention": "wspomnij o autorze",
|
||||
"keyboard_shortcuts.muted": "przejdź do listy wyciszonych",
|
||||
"keyboard_shortcuts.my_profile": "otwórz własny profil",
|
||||
"keyboard_shortcuts.notifications": "otwórz kolumnę powiadomień",
|
||||
"keyboard_shortcuts.open_media": "otwórz media",
|
||||
"keyboard_shortcuts.pinned": "przejdź do listy przypiętych wpisów",
|
||||
"keyboard_shortcuts.profile": "przejdź do profilu autora wpisu",
|
||||
"keyboard_shortcuts.react": "zareaguj na wpis",
|
||||
"keyboard_shortcuts.reply": "odpowiedz",
|
||||
"keyboard_shortcuts.requests": "przejdź do listy próśb o śledzenie",
|
||||
"keyboard_shortcuts.search": "przejdź do pola wyszukiwania",
|
||||
"keyboard_shortcuts.toggle_hidden": "wyświetl lub ukryć wpis spod CW",
|
||||
"keyboard_shortcuts.toggle_sensitivity": "pokaż/ukryj multimedia",
|
||||
"keyboard_shortcuts.toot": "utwórz nowy wpis",
|
||||
"keyboard_shortcuts.unfocus": "opuść pole wyszukiwania/pisania",
|
||||
"keyboard_shortcuts.up": "przejdź na górę listy",
|
||||
"landing_page_modal.download": "Pobierz",
|
||||
"landing_page_modal.helpCenter": "Centrum pomocy",
|
||||
"lightbox.close": "Zamknij",
|
||||
|
@ -648,17 +662,21 @@
|
|||
"login.fields.otp_code_hint": "Wprowadź kod uwierzytelniania dwuetapowego wygenerowany przez aplikację mobilną lub jeden z kodów zapasowych",
|
||||
"login.fields.otp_code_label": "Kod uwierzytelniania dwuetapowego:",
|
||||
"login.fields.password_placeholder": "Hasło",
|
||||
"login.fields.username_label": "Adres e-mail lub nazwa użytkownika",
|
||||
"login.fields.username_placeholder": "Nazwa użytkownika",
|
||||
"login.log_in": "Zaloguj się",
|
||||
"login.otp_log_in": "Login OTP",
|
||||
"login.otp_log_in.fail": "Nieprawidłowy kod, spróbuj ponownie później.",
|
||||
"login.reset_password_hint": "Problem z zalogowaniem?",
|
||||
"login.sign_in": "Zaloguj się",
|
||||
"login_form.header": "Zaloguj się",
|
||||
"media_gallery.toggle_visible": "Przełącz widoczność",
|
||||
"media_panel.empty_message": "Nie znaleziono mediów.",
|
||||
"media_panel.title": "Media",
|
||||
"mfa.confirm.success_message": "Potwierdzono MFA",
|
||||
"mfa.disable.success_message": "Wyłączono MFA",
|
||||
"mfa.disabled": "Wyłączone",
|
||||
"mfa.enabled": "Włączone",
|
||||
"mfa.mfa_disable_enter_password": "Wprowadź obecne hasło, aby wyłączyć uwierzytelnianie dwuetapowe:",
|
||||
"mfa.mfa_setup.code_hint": "Wprowadź kod z aplikacji do uwierzytelniania dwuskładnikowego.",
|
||||
"mfa.mfa_setup.code_placeholder": "Kod",
|
||||
|
@ -691,6 +709,10 @@
|
|||
"morefollows.followers_label": "…i {count} więcej {count, plural, one {obserwujący(-a)} few {obserwujących} many {obserwujących} other {obserwujących}} na zdalnych stronach.",
|
||||
"morefollows.following_label": "…i {count} więcej {count, plural, one {obserwowany(-a)} few {obserwowanych} many {obserwowanych} other {obserwowanych}} na zdalnych stronach.",
|
||||
"mute_modal.hide_notifications": "Chcesz ukryć powiadomienia od tego użytkownika?",
|
||||
"navbar.login.action": "Zaloguj się",
|
||||
"navbar.login.forgot_password": "Nie pamiętasz hasła?",
|
||||
"navbar.login.password.label": "Hasło",
|
||||
"navbar.login.username.placeholder": "Adres e-mail lub nazwa użytkownika",
|
||||
"navigation.chats": "Czaty",
|
||||
"navigation.compose": "Utwórz wpis",
|
||||
"navigation.dashboard": "Administracja",
|
||||
|
@ -775,7 +797,7 @@
|
|||
"onboarding.display_name.title": "Wybierz wyświetlaną nazwę",
|
||||
"onboarding.done": "Gotowe",
|
||||
"onboarding.finished.message": "Cieszymy się, że możemy powitać Cię w naszej społeczności! Naciśnij poniższy przycisk, aby rozpocząć.",
|
||||
"onboarding.finished.title": "Wprowadzenie",
|
||||
"onboarding.finished.title": "Wprowadzenie ukończone",
|
||||
"onboarding.header.subtitle": "Będzie widoczny w górnej części Twojego profilu",
|
||||
"onboarding.header.title": "Wybierz obraz tła",
|
||||
"onboarding.next": "Dalej",
|
||||
|
@ -786,11 +808,13 @@
|
|||
"onboarding.suggestions.subtitle": "Oto kilka najbardziej popularnych kont, które mogą Ci się spodobać.",
|
||||
"onboarding.suggestions.title": "Proponowane konta",
|
||||
"onboarding.view_feed": "Pokaż strumień",
|
||||
"password_reset.confirmation_hint": "Nie otrzymałeś(-aś) maila potwierdzającego?",
|
||||
"password_reset.confirmation": "Sprawdź swoją pocztę e-mail, aby potwierdzić.",
|
||||
"password_reset.fields.username_placeholder": "Adres e-mail lub nazwa użytkownika",
|
||||
"password_reset.header": "Resetuj hasło",
|
||||
"password_reset.reset": "Resetuj hasło",
|
||||
"patron.donate": "Wesprzyj",
|
||||
"patron.title": "Cel wsparcia",
|
||||
"patron.title": "Cel wsparcia",
|
||||
"pinned_accounts.title": "Polecani przez {name}",
|
||||
"pinned_statuses.none": "Brak przypięć do pokazania.",
|
||||
"poll.closed": "Zamknięte",
|
||||
|
@ -826,14 +850,18 @@
|
|||
"privacy.unlisted.short": "Niewidoczny",
|
||||
"profile_dropdown.add_account": "Dodaj istniejące konto",
|
||||
"profile_dropdown.logout": "Wyloguj @{acct}",
|
||||
"profile_dropdown.switch_account": "Przełącz konta",
|
||||
"profile_dropdown.theme": "Motyw",
|
||||
"profile_fields_panel.title": "Pola konta",
|
||||
"profile_fields_panel.title": "Pola profilu",
|
||||
"public.column_settings.title": "Ustawienia osi czasu Fediwersum",
|
||||
"reactions.all": "Wszystkie",
|
||||
"regeneration_indicator.label": "Ładuję…",
|
||||
"regeneration_indicator.sublabel": "Twoja oś czasu jest przygotowywana!",
|
||||
"register_invite.lead": "Wypełnij poniższy formularz, aby utworzyć konto.",
|
||||
"register_invite.title": "Otrzymałeś(-aś) zaproszenie na {siteTitle}!",
|
||||
"registrations.error": "Nie udało się zarejestrować konta.",
|
||||
"registrations.success": "Witamy na {siteTitle}!",
|
||||
"registrations.unprocessable_entity": "Ta nazwa użytkownika jest już zajęta.",
|
||||
"registration.agreement": "Akceptuję {tos}.",
|
||||
"registration.captcha.hint": "Naciśnij na obrazek, aby uzyskać nową captchę",
|
||||
"registration.captcha.placeholder": "Wprowadź kod z obrazka",
|
||||
|
@ -845,6 +873,7 @@
|
|||
"registration.fields.password_placeholder": "Hasło",
|
||||
"registration.fields.username_hint": "Możesz używać tylko liter, cyfr i podkreślników.",
|
||||
"registration.fields.username_placeholder": "Nazwa użytkownika",
|
||||
"registration.header": "Zarejestruj konto",
|
||||
"registration.newsletter": "Zasubskrybuj newsletter.",
|
||||
"registration.password_mismatch": "Hasła nie pasują do siebie.",
|
||||
"registration.reason": "Dlaczego chcesz dołączyć?",
|
||||
|
@ -902,6 +931,7 @@
|
|||
"report.reason.title": "Powód zgłoszenia",
|
||||
"report.submit": "Wyślij",
|
||||
"report.target": "Zgłaszanie {target}",
|
||||
"reset_password.fail": "Token wygasł, spróbuj ponownie.",
|
||||
"reset_password.header": "Ustaw nowe hasło",
|
||||
"schedule.post_time": "Data/godzina publikacji",
|
||||
"schedule.remove": "Usuń zaplanowany wpis",
|
||||
|
@ -947,15 +977,22 @@
|
|||
"settings.configure_mfa": "Konfiguruj uwierzytelnianie wieloskładnikowe",
|
||||
"settings.delete_account": "Usuń konto",
|
||||
"settings.edit_profile": "Edytuj profil",
|
||||
"settings.other": "Inne",
|
||||
"settings.preferences": "Preferencje",
|
||||
"settings.profile": "Profil",
|
||||
"settings.save.success": "Pomyślnie zapisano preferencje!",
|
||||
"settings.security": "Bezpieczeństwo",
|
||||
"settings.sessions": "Aktywne sesje",
|
||||
"settings.settings": "Ustawienia",
|
||||
"shared.tos": "Zasady użytkowania",
|
||||
"signup_panel.subtitle": "Zarejestruj się, aby przyłączyć się do dyskusji.",
|
||||
"signup_panel.title": "Nowi na {site_title}?",
|
||||
"site_preview.preview": "Podgląd",
|
||||
"sms_verification.header": "Wprowadź numer telefonu",
|
||||
"sms_verification.fail": "Nie udało się wysłać kodu weryfikującego na Twój numer telefonu.",
|
||||
"sms_verification.invalid": "Wprowadź prawidłowy numer telefonu.",
|
||||
"sms_verification.sent.header": "Kod weryfikujący",
|
||||
"sms_verification.success": "Kod weryfikujący został wysłany na Twój numer telefonu.",
|
||||
"snackbar.view": "Wyświetl",
|
||||
"soapbox_config.authenticated_profile_hint": "Użytkownicy muszą być zalogowani, aby zobaczyć odpowiedzi i media na profilach użytkowników.",
|
||||
"soapbox_config.authenticated_profile_label": "Profile wymagają uwierzytelniania",
|
||||
|
@ -1086,6 +1123,10 @@
|
|||
"theme_toggle.dark": "Ciemny",
|
||||
"theme_toggle.light": "Jasny",
|
||||
"theme_toggle.system": "Systemowy",
|
||||
"thread_login.login": "Zaloguj się",
|
||||
"thread_login.message": "Dołącz do {siteTitle}, aby dowiedzieć się więcej.",
|
||||
"thread_login.signup": "Zarejestruj się",
|
||||
"thread_login.title": "Kontynuuj dyskusję",
|
||||
"time_remaining.days": "{number, plural, one {Pozostał # dzień} few {Pozostały # dni} many {Pozostało # dni} other {Pozostało # dni}}",
|
||||
"time_remaining.hours": "{number, plural, one {Pozostała # godzina} few {Pozostały # godziny} many {Pozostało # godzin} other {Pozostało # godzin}}",
|
||||
"time_remaining.minutes": "{number, plural, one {Pozostała # minuta} few {Pozostały # minuty} many {Pozostało # minut} other {Pozostało # minut}}",
|
||||
|
|
|
@ -21,7 +21,8 @@ export type NotificationType =
|
|||
| 'status'
|
||||
| 'move'
|
||||
| 'pleroma:chat_mention'
|
||||
| 'pleroma:emoji_reaction';
|
||||
| 'pleroma:emoji_reaction'
|
||||
| 'user_approved';
|
||||
|
||||
// https://docs.joinmastodon.org/entities/notification/
|
||||
export const NotificationRecord = ImmutableRecord({
|
||||
|
|
|
@ -115,6 +115,7 @@ export const SoapboxConfigRecord = ImmutableRecord({
|
|||
singleUserMode: false,
|
||||
singleUserModeProfile: '',
|
||||
linkFooterMessage: '',
|
||||
guestExperience: true,
|
||||
links: ImmutableMap<string, string>(),
|
||||
}, 'SoapboxConfig');
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ const HomePage: React.FC = ({ children }) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Layout.Main className='pt-4 sm:pt-0 divide-y divide-gray-200 dark:divide-slate-700 divide-solid space-y-4 divide-none'>
|
||||
<Layout.Main className='pt-4 sm:pt-0 dark:divide-slate-700 space-y-4'>
|
||||
{me && (
|
||||
<Card variant='rounded' ref={composeBlock}>
|
||||
<CardBody>
|
||||
|
|
|
@ -59,6 +59,28 @@ describe('compose reducer', () => {
|
|||
const result = reducer(undefined, action);
|
||||
expect(result.getIn(['media_attachments', 0, 'id'])).toEqual('508107650');
|
||||
});
|
||||
|
||||
it('sets the id when editing a post', () => {
|
||||
const action = {
|
||||
withRedraft: false,
|
||||
type: actions.COMPOSE_SET_STATUS,
|
||||
status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))),
|
||||
};
|
||||
|
||||
const result = reducer(undefined, action);
|
||||
expect(result.get('id')).toEqual('AHU2RrX0wdcwzCYjFQ');
|
||||
});
|
||||
|
||||
it('does not set the id when redrafting a post', () => {
|
||||
const action = {
|
||||
withRedraft: true,
|
||||
type: actions.COMPOSE_SET_STATUS,
|
||||
status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))),
|
||||
};
|
||||
|
||||
const result = reducer(undefined, action);
|
||||
expect(result.get('id')).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
it('uses \'public\' scope as default', () => {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import * as actions from 'soapbox/actions/identity_proofs';
|
||||
|
||||
import reducer from '../identity_proofs';
|
||||
|
||||
describe('identity_proofs reducer', () => {
|
||||
it('should return the initial state', () => {
|
||||
expect(reducer(undefined, {})).toEqual(ImmutableMap());
|
||||
});
|
||||
|
||||
it('should handle IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST', () => {
|
||||
const state = ImmutableMap({ isLoading: false });
|
||||
const action = {
|
||||
type: actions.IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST,
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
isLoading: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL', () => {
|
||||
const state = ImmutableMap({ isLoading: true });
|
||||
const action = {
|
||||
type: actions.IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL,
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
isLoading: false,
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -429,7 +429,9 @@ export default function compose(state = initialState, action) {
|
|||
}));
|
||||
case COMPOSE_SET_STATUS:
|
||||
return state.withMutations(map => {
|
||||
map.set('id', action.status.get('id'));
|
||||
if (!action.withRedraft) {
|
||||
map.set('id', action.status.get('id'));
|
||||
}
|
||||
map.set('text', action.rawText || unescapeHTML(expandMentions(action.status)));
|
||||
map.set('to', action.explicitAddressing ? getExplicitMentions(action.status.get('account', 'id'), action.status) : ImmutableOrderedSet());
|
||||
map.set('in_reply_to', action.status.get('in_reply_to_id'));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<string, string>(),
|
||||
replies: ImmutableMap<string, ImmutableOrderedSet<string>>(),
|
||||
});
|
||||
|
||||
type State = ReturnType<typeof ReducerRecord>;
|
||||
|
||||
/** 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<string, ReducerStatus>,
|
||||
): 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;
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||
|
||||
import {
|
||||
IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST,
|
||||
IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS,
|
||||
IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL,
|
||||
} from '../actions/identity_proofs';
|
||||
|
||||
const initialState = ImmutableMap();
|
||||
|
||||
export default function identityProofsReducer(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST:
|
||||
return state.set('isLoading', true);
|
||||
case IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL:
|
||||
return state.set('isLoading', false);
|
||||
case IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS:
|
||||
return state.update(identity_proofs => identity_proofs.withMutations(map => {
|
||||
map.set('isLoading', false);
|
||||
map.set('loaded', true);
|
||||
map.set(action.accountId, fromJS(action.identity_proofs));
|
||||
}));
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -29,7 +29,6 @@ import group_lists from './group_lists';
|
|||
import group_relationships from './group_relationships';
|
||||
import groups from './groups';
|
||||
import history from './history';
|
||||
import identity_proofs from './identity_proofs';
|
||||
import instance from './instance';
|
||||
import listAdder from './list_adder';
|
||||
import listEditor from './list_editor';
|
||||
|
@ -86,7 +85,6 @@ const reducers = {
|
|||
search,
|
||||
notifications,
|
||||
custom_emojis,
|
||||
identity_proofs,
|
||||
lists,
|
||||
listEditor,
|
||||
listAdder,
|
||||
|
|
|
@ -362,6 +362,12 @@ const getInstanceFeatures = (instance: Instance) => {
|
|||
*/
|
||||
paginatedContext: v.software === TRUTHSOCIAL,
|
||||
|
||||
/**
|
||||
* Displays a form to follow a user when logged out.
|
||||
* @see POST /main/ostatus
|
||||
*/
|
||||
pleromaRemoteFollow: v.software === PLEROMA,
|
||||
|
||||
/**
|
||||
* Can add polls to statuses.
|
||||
* @see POST /api/v1/statuses
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import { Location } from 'history';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const LOCAL_STORAGE_REDIRECT_KEY = 'soapbox:redirect-uri';
|
||||
|
||||
const cacheCurrentUrl = (location: Location<unknown>) => {
|
||||
const actualUrl = encodeURIComponent(`${location.pathname}${location.search}`);
|
||||
localStorage.setItem(LOCAL_STORAGE_REDIRECT_KEY, actualUrl);
|
||||
return actualUrl;
|
||||
};
|
||||
|
||||
const getRedirectUrl = () => {
|
||||
let redirectUri = localStorage.getItem(LOCAL_STORAGE_REDIRECT_KEY);
|
||||
if (redirectUri) {
|
||||
redirectUri = decodeURIComponent(redirectUri);
|
||||
}
|
||||
|
||||
localStorage.removeItem(LOCAL_STORAGE_REDIRECT_KEY);
|
||||
return redirectUri || '/';
|
||||
};
|
||||
|
||||
const useCachedLocationHandler = () => {
|
||||
const removeCachedRedirectUri = () => localStorage.removeItem(LOCAL_STORAGE_REDIRECT_KEY);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('beforeunload', removeCachedRedirectUri);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', removeCachedRedirectUri);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export { cacheCurrentUrl, getRedirectUrl, useCachedLocationHandler };
|
|
@ -43,7 +43,6 @@
|
|||
@import 'components/user-panel';
|
||||
@import 'components/compose-form';
|
||||
@import 'components/sidebar-menu';
|
||||
@import 'components/hotkeys-modal';
|
||||
@import 'components/emoji-reacts';
|
||||
@import 'components/status';
|
||||
@import 'components/reply-indicator';
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Ładowanie…
Reference in New Issue