diff --git a/README.md b/README.md index 4538075ee..584868d7c 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,8 @@ Customization details can be found in the [Customization doc](docs/customization Soapbox FE is based on [Gab Social](https://code.gab.com/gab/social/gab-social)'s frontend which is in turn based on [Mastodon](https://github.com/tootsuite/mastodon/)'s frontend. +- `static/sounds/chat.mp3` and `static/sounds/chat.oga` are from [notificationsounds.com](https://notificationsounds.com/notification-sounds/intuition-561) licensed under CC BY 4.0. + Soapbox FE is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or diff --git a/app/images/halloween/clouds.png b/app/images/halloween/clouds.png new file mode 100644 index 000000000..29962c104 Binary files /dev/null and b/app/images/halloween/clouds.png differ diff --git a/app/images/halloween/halloween-emblem.svg b/app/images/halloween/halloween-emblem.svg new file mode 100644 index 000000000..ad23be14c --- /dev/null +++ b/app/images/halloween/halloween-emblem.svg @@ -0,0 +1,311 @@ + + + + Flying Witch during Full Moon + + + + + image/svg+xml + + Flying Witch during Full Moon + 2017-10-10 + + + Urs Roesch + + + + + + OpenClipart + + + + + remix+287475 + remix+288242 + remix+170669 + yellow + moon + yellow moon + full moon + moon + witch + cat + silhouette + bat + bats + flying bat + flying witch + black + dark + night + halloween + walpurgis night + walpurgis + + + Flying witch with cat flying during full moon. + + + gnokii + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/images/halloween/spider.svg b/app/images/halloween/spider.svg new file mode 100644 index 000000000..077b60d65 --- /dev/null +++ b/app/images/halloween/spider.svg @@ -0,0 +1,69 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/app/images/halloween/spiderweb.svg b/app/images/halloween/spiderweb.svg new file mode 100644 index 000000000..16ae81984 --- /dev/null +++ b/app/images/halloween/spiderweb.svg @@ -0,0 +1,78 @@ + + + + + Realistic spider web + + + + + + + image/svg+xml + + + + + Openclipart + + + Realistic spider web + + + + + + + + + diff --git a/app/images/halloween/starfield.png b/app/images/halloween/starfield.png new file mode 100644 index 000000000..1e7995895 Binary files /dev/null and b/app/images/halloween/starfield.png differ diff --git a/app/images/halloween/twinkle.svg b/app/images/halloween/twinkle.svg new file mode 100644 index 000000000..9869cb094 --- /dev/null +++ b/app/images/halloween/twinkle.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/app/soapbox/__fixtures__/context_1.json b/app/soapbox/__fixtures__/context_1.json new file mode 100644 index 000000000..2e37a5502 --- /dev/null +++ b/app/soapbox/__fixtures__/context_1.json @@ -0,0 +1,739 @@ +{ + "ancestors": [ + { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", + "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "follow_requests_count": 0, + "followers_count": 725, + "following_count": 1211, + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "locked": false, + "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

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

A

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

#vegan #freeculture #atheist #antiporn #gendercritical.

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

B

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

#vegan #freeculture #atheist #antiporn #gendercritical.

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

C

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

#vegan #freeculture #atheist #antiporn #gendercritical.

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

E

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

#vegan #freeculture #atheist #antiporn #gendercritical.

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

F

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

#vegan #freeculture #atheist #antiporn #gendercritical.

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

A

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

#vegan #freeculture #atheist #antiporn #gendercritical.

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

C

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

#vegan #freeculture #atheist #antiporn #gendercritical.

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

D

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

#vegan #freeculture #atheist #antiporn #gendercritical.

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

E

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

#vegan #freeculture #atheist #antiporn #gendercritical.

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

F

", + "created_at": "2020-09-18T20:07:42.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 0, + "id": "9zIH9fhaP9atiJoOJc", + "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", + "in_reply_to_id": "9zIH8WYwtnUx4yDzUm", + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "alex", + "id": "9v5bmRalQvjOy0ECcC", + "url": "https://gleasonator.com/users/alex", + "username": "alex" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "F" + }, + "conversation_id": 5089485, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": "alex", + "local": true, + "parent_visible": true, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/ee661cf9-35d4-4e84-88ff-13b5950f7556", + "url": "https://gleasonator.com/notice/9zIH9fhaP9atiJoOJc", + "visibility": "direct" + } + ] +} diff --git a/app/soapbox/actions/chats.js b/app/soapbox/actions/chats.js index 729dc128e..6f8d1c788 100644 --- a/app/soapbox/actions/chats.js +++ b/app/soapbox/actions/chats.js @@ -23,6 +23,10 @@ export const CHAT_READ_REQUEST = 'CHAT_READ_REQUEST'; export const CHAT_READ_SUCCESS = 'CHAT_READ_SUCCESS'; export const CHAT_READ_FAIL = 'CHAT_READ_FAIL'; +export const CHAT_MESSAGE_DELETE_REQUEST = 'CHAT_MESSAGE_DELETE_REQUEST'; +export const CHAT_MESSAGE_DELETE_SUCCESS = 'CHAT_MESSAGE_DELETE_SUCCESS'; +export const CHAT_MESSAGE_DELETE_FAIL = 'CHAT_MESSAGE_DELETE_FAIL'; + export function fetchChats() { return (dispatch, getState) => { dispatch({ type: CHATS_FETCH_REQUEST }); @@ -150,3 +154,14 @@ export function markChatRead(chatId, lastReadId) { }); }; } + +export function deleteChatMessage(chatId, messageId) { + return (dispatch, getState) => { + dispatch({ type: CHAT_MESSAGE_DELETE_REQUEST, chatId, messageId }); + api(getState).delete(`/api/v1/pleroma/chats/${chatId}/messages/${messageId}`).then(({ data }) => { + dispatch({ type: CHAT_MESSAGE_DELETE_SUCCESS, chatId, messageId, chatMessage: data }); + }).catch(error => { + dispatch({ type: CHAT_MESSAGE_DELETE_FAIL, chatId, messageId, error }); + }); + }; +} diff --git a/app/soapbox/actions/importer/index.js b/app/soapbox/actions/importer/index.js index 0736dd7ce..44de245cf 100644 --- a/app/soapbox/actions/importer/index.js +++ b/app/soapbox/actions/importer/index.js @@ -46,6 +46,8 @@ export function importFetchedAccounts(accounts) { const normalAccounts = []; function processAccount(account) { + if (!account.id) return; + pushUnique(normalAccounts, normalizeAccount(account)); if (account.moved) { @@ -69,6 +71,8 @@ export function importFetchedStatuses(statuses) { const polls = []; function processStatus(status) { + if (!status.account.id) return; + const normalOldStatus = getState().getIn(['statuses', status.id]); const expandSpoilers = getSettings(getState()).get('expandSpoilers'); diff --git a/app/soapbox/actions/notifications.js b/app/soapbox/actions/notifications.js index 1346c36c0..1a1175700 100644 --- a/app/soapbox/actions/notifications.js +++ b/app/soapbox/actions/notifications.js @@ -10,7 +10,11 @@ import { } from './importer'; import { getSettings, saveSettings } from './settings'; import { defineMessages } from 'react-intl'; -import { List as ImmutableList } from 'immutable'; +import { + List as ImmutableList, + Map as ImmutableMap, + OrderedMap as ImmutableOrderedMap, +} from 'immutable'; import { unescapeHTML } from '../utils/html'; import { getFilters, regexFromFilters } from '../selectors'; @@ -121,7 +125,7 @@ export function updateNotificationsQueue(notification, intlMessages, intlLocale, export function dequeueNotifications() { return (dispatch, getState) => { - const queuedNotifications = getState().getIn(['notifications', 'queuedNotifications'], ImmutableList()); + const queuedNotifications = getState().getIn(['notifications', 'queuedNotifications'], ImmutableOrderedMap()); const totalQueuedNotificationsCount = getState().getIn(['notifications', 'totalQueuedNotificationsCount'], 0); if (totalQueuedNotificationsCount === 0) { @@ -252,9 +256,12 @@ export function setFilter(filterType) { export function markReadNotifications() { return (dispatch, getState) => { - if (!getState().get('me')) return; - const topNotification = parseInt(getState().getIn(['notifications', 'items', 0, 'id'])); - const lastRead = getState().getIn(['notifications', 'lastRead']); + const state = getState(); + if (!state.get('me')) return; + + const topNotification = state.getIn(['notifications', 'items'], ImmutableOrderedMap()).first(ImmutableMap()).get('id'); + const lastRead = state.getIn(['notifications', 'lastRead']); + if (!(topNotification && topNotification > lastRead)) return; dispatch({ diff --git a/app/soapbox/actions/reports.js b/app/soapbox/actions/reports.js index a1214fc56..9328e0141 100644 --- a/app/soapbox/actions/reports.js +++ b/app/soapbox/actions/reports.js @@ -25,6 +25,17 @@ export function initReport(account, status) { }; }; +export function initReportById(accountId) { + return (dispatch, getState) => { + dispatch({ + type: REPORT_INIT, + account: getState().getIn(['accounts', accountId]), + }); + + dispatch(openModal('REPORT')); + }; +}; + export function cancelReport() { return { type: REPORT_CANCEL, diff --git a/app/soapbox/actions/settings.js b/app/soapbox/actions/settings.js index 4432fa6e0..097265814 100644 --- a/app/soapbox/actions/settings.js +++ b/app/soapbox/actions/settings.js @@ -32,6 +32,7 @@ const defaultSettings = ImmutableMap({ chats: ImmutableMap({ panes: ImmutableList(), mainWindow: 'minimized', + sound: true, }), home: ImmutableMap({ diff --git a/app/soapbox/actions/statuses.js b/app/soapbox/actions/statuses.js index acbbea1f4..bdf31da8f 100644 --- a/app/soapbox/actions/statuses.js +++ b/app/soapbox/actions/statuses.js @@ -219,7 +219,6 @@ export function fetchContextSuccess(id, ancestors, descendants) { id, ancestors, descendants, - statuses: ancestors.concat(descendants), }; }; diff --git a/app/soapbox/actions/streaming.js b/app/soapbox/actions/streaming.js index a581ae0fe..b3ca60fb3 100644 --- a/app/soapbox/actions/streaming.js +++ b/app/soapbox/actions/streaming.js @@ -55,7 +55,17 @@ export function connectTimelineStream(timelineId, path, pollingRefresh = null, a dispatch(fetchFilters()); break; case 'pleroma:chat_update': - dispatch({ type: STREAMING_CHAT_UPDATE, chat: JSON.parse(data.payload), me: getState().get('me') }); + dispatch((dispatch, getState) => { + const chat = JSON.parse(data.payload); + const messageOwned = !(chat.last_message && chat.last_message.account_id !== getState().get('me')); + + dispatch({ + type: STREAMING_CHAT_UPDATE, + chat, + // Only play sounds for recipient messages + meta: !messageOwned && getSettings(getState()).getIn(['chats', 'sound']) && { sound: 'chat' }, + }); + }); break; } }, diff --git a/app/soapbox/components/__tests__/__snapshots__/display_name-test.js.snap b/app/soapbox/components/__tests__/__snapshots__/display_name-test.js.snap index d26a406a2..6d04016b5 100644 --- a/app/soapbox/components/__tests__/__snapshots__/display_name-test.js.snap +++ b/app/soapbox/components/__tests__/__snapshots__/display_name-test.js.snap @@ -6,6 +6,7 @@ exports[` renders display name + account name 1`] = ` > diff --git a/app/soapbox/components/helmet.js b/app/soapbox/components/helmet.js index f3bff8740..c9c1eb5a6 100644 --- a/app/soapbox/components/helmet.js +++ b/app/soapbox/components/helmet.js @@ -36,6 +36,7 @@ class SoapboxHelmet extends React.Component { {children} diff --git a/app/soapbox/components/hover_ref_wrapper.js b/app/soapbox/components/hover_ref_wrapper.js index fe6ac2bed..652e4a387 100644 --- a/app/soapbox/components/hover_ref_wrapper.js +++ b/app/soapbox/components/hover_ref_wrapper.js @@ -26,6 +26,13 @@ const handleMouseLeave = (dispatch) => { }; }; +const handleClick = (dispatch) => { + return e => { + showProfileHoverCard.cancel(); + dispatch(closeProfileHoverCard(true)); + }; +}; + export const HoverRefWrapper = ({ accountId, children, inline }) => { const dispatch = useDispatch(); const ref = useRef(); @@ -37,6 +44,7 @@ export const HoverRefWrapper = ({ accountId, children, inline }) => { className='hover-ref-wrapper' onMouseEnter={handleMouseEnter(dispatch, ref, accountId)} onMouseLeave={handleMouseLeave(dispatch)} + onClick={handleClick(dispatch)} > {children} diff --git a/app/soapbox/containers/soapbox.js b/app/soapbox/containers/soapbox.js index b948811d3..ed4549d27 100644 --- a/app/soapbox/containers/soapbox.js +++ b/app/soapbox/containers/soapbox.js @@ -59,6 +59,7 @@ const mapStateToProps = (state) => { locale: validLocale(locale) ? locale : 'en', themeCss: generateThemeCss(soapboxConfig.get('brandColor')), themeMode: settings.get('themeMode'), + halloween: settings.get('halloween'), customCss: soapboxConfig.get('customCss'), }; }; @@ -77,6 +78,7 @@ class SoapboxMount extends React.PureComponent { themeCss: PropTypes.string, themeMode: PropTypes.string, customCss: ImmutablePropTypes.list, + halloween: PropTypes.bool, dispatch: PropTypes.func, }; @@ -122,6 +124,7 @@ class SoapboxMount extends React.PureComponent { 'no-reduce-motion': !this.props.reduceMotion, 'dyslexic': this.props.dyslexicFont, 'demetricator': this.props.demetricator, + 'halloween': this.props.halloween, }); return ( diff --git a/app/soapbox/features/auth_login/components/login_page.js b/app/soapbox/features/auth_login/components/login_page.js index 934ce12ea..c660c467e 100644 --- a/app/soapbox/features/auth_login/components/login_page.js +++ b/app/soapbox/features/auth_login/components/login_page.js @@ -20,17 +20,18 @@ class LoginPage extends ImmutablePureComponent { this.handleSubmit = this.handleSubmit.bind(this); } + state = { + isLoading: false, + mfa_auth_needed: false, + mfa_token: '', + } + getFormData = (form) => { return Object.fromEntries( Array.from(form).map(i => [i.name, i.value]) ); } - state = { - mfa_auth_needed: false, - mfa_token: '', - } - handleSubmit = (event) => { const { dispatch } = this.props; const { username, password } = this.getFormData(event.target); @@ -47,8 +48,8 @@ class LoginPage extends ImmutablePureComponent { } render() { - const { me, isLoading } = this.props; - const { mfa_auth_needed, mfa_token } = this.state; + const { me } = this.props; + const { isLoading, mfa_auth_needed, mfa_token } = this.state; if (me) return ; if (mfa_auth_needed) return ; diff --git a/app/soapbox/features/chats/components/audio_toggle.js b/app/soapbox/features/chats/components/audio_toggle.js new file mode 100644 index 000000000..9b207273a --- /dev/null +++ b/app/soapbox/features/chats/components/audio_toggle.js @@ -0,0 +1,61 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { injectIntl, defineMessages } from 'react-intl'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import Icon from 'soapbox/components/icon'; +import { changeSetting, getSettings } from 'soapbox/actions/settings'; +import SettingToggle from 'soapbox/features/notifications/components/setting_toggle'; + +const messages = defineMessages({ + switchToOn: { id: 'chats.audio_toggle_on', defaultMessage: 'Audio notification on' }, + switchToOff: { id: 'chats.audio_toggle_off', defaultMessage: 'Audio notification off' }, +}); + +const mapStateToProps = state => { + return { + settings: getSettings(state), + }; +}; + +const mapDispatchToProps = (dispatch) => ({ + toggleAudio(setting) { + dispatch(changeSetting(['chats', 'sound'], setting)); + }, +}); + +export default @connect(mapStateToProps, mapDispatchToProps) +@injectIntl +class AudioToggle extends React.PureComponent { + + static propTypes = { + intl: PropTypes.object.isRequired, + settings: ImmutablePropTypes.map.isRequired, + toggleAudio: PropTypes.func, + showLabel: PropTypes.bool, + }; + + handleToggleAudio = () => { + this.props.toggleAudio(this.props.settings.getIn(['chats', 'sound']) === true ? false : true); + } + + render() { + const { intl, settings, showLabel } = this.props; + let toggle = ( + , unchecked: }} ariaLabel={settings.get('chats', 'sound') === true ? intl.formatMessage(messages.switchToOff) : intl.formatMessage(messages.switchToOn)} /> + ); + + if (showLabel) { + toggle = ( + , unchecked: }} label={settings.get('chats', 'sound') === true ? intl.formatMessage(messages.switchToOff) : intl.formatMessage(messages.switchToOn)} /> + ); + } + + return ( +
+ {toggle} +
+ ); + } + +} diff --git a/app/soapbox/features/chats/components/chat_box.js b/app/soapbox/features/chats/components/chat_box.js index ee2b7cd20..2a0b72e89 100644 --- a/app/soapbox/features/chats/components/chat_box.js +++ b/app/soapbox/features/chats/components/chat_box.js @@ -18,6 +18,7 @@ import IconButton from 'soapbox/components/icon_button'; const messages = defineMessages({ placeholder: { id: 'chat_box.input.placeholder', defaultMessage: 'Send a message…' }, + send: { id: 'chat_box.actions.send', defaultMessage: 'Send' }, }); const mapStateToProps = (state, { chatId }) => ({ @@ -94,6 +95,7 @@ class ChatBox extends ImmutablePureComponent { } handleKeyDown = (e) => { + this.markRead(); if (e.key === 'Enter' && e.shiftKey) { this.insertLine(); e.preventDefault(); @@ -122,17 +124,6 @@ class ChatBox extends ImmutablePureComponent { onSetInputRef(el); }; - componentDidUpdate(prevProps) { - const markReadConditions = [ - () => this.props.chat !== undefined, - () => document.activeElement === this.inputElem, - () => this.props.chat.get('unread') > 0, - ]; - - if (markReadConditions.every(c => c() === true)) - this.markRead(); - } - handleRemoveFile = (e) => { this.setState({ attachment: undefined, resetFileKey: fileKeyGen() }); } @@ -174,11 +165,17 @@ class ChatBox extends ImmutablePureComponent { } renderActionButton = () => { + const { intl } = this.props; const { resetFileKey } = this.state; return this.canSubmit() ? (
- +
) : ( diff --git a/app/soapbox/features/chats/components/chat_message_list.js b/app/soapbox/features/chats/components/chat_message_list.js index 1cfc41442..af35f216d 100644 --- a/app/soapbox/features/chats/components/chat_message_list.js +++ b/app/soapbox/features/chats/components/chat_message_list.js @@ -2,16 +2,37 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { injectIntl } from 'react-intl'; +import { injectIntl, defineMessages } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -import { fetchChatMessages } from 'soapbox/actions/chats'; +import { fetchChatMessages, deleteChatMessage } from 'soapbox/actions/chats'; import emojify from 'soapbox/features/emoji/emoji'; import classNames from 'classnames'; import { openModal } from 'soapbox/actions/modal'; import { escape, throttle } from 'lodash'; import { MediaGallery } from 'soapbox/features/ui/util/async-components'; import Bundle from 'soapbox/features/ui/components/bundle'; +import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container'; +import { initReportById } from 'soapbox/actions/reports'; + +const messages = defineMessages({ + today: { id: 'chats.dividers.today', defaultMessage: 'Today' }, + more: { id: 'chats.actions.more', defaultMessage: 'More' }, + delete: { id: 'chats.actions.delete', defaultMessage: 'Delete message' }, + report: { id: 'chats.actions.report', defaultMessage: 'Report user' }, +}); + +const timeChange = (prev, curr) => { + const prevDate = new Date(prev.get('created_at')).getDate(); + const currDate = new Date(curr.get('created_at')).getDate(); + const nowDate = new Date().getDate(); + + if (prevDate !== currDate) { + return currDate === nowDate ? 'today' : 'date'; + }; + + return null; +}; const makeEmojiMap = record => record.get('emojis', ImmutableList()).reduce((map, emoji) => { return map.set(`:${emoji.get('shortcode')}:`, emoji); @@ -89,11 +110,16 @@ class ChatMessageList extends ImmutablePureComponent { return scrollBottom < elem.offsetHeight * 1.5; } + handleResize = (e) => { + if (this.isNearBottom()) this.scrollToBottom(); + } + componentDidMount() { const { dispatch, chatId } = this.props; dispatch(fetchChatMessages(chatId)); this.node.addEventListener('scroll', this.handleScroll); + window.addEventListener('resize', this.handleResize); this.scrollToBottom(); } @@ -125,6 +151,7 @@ class ChatMessageList extends ImmutablePureComponent { componentWillUnmount() { this.node.removeEventListener('scroll', this.handleScroll); + window.removeEventListener('resize', this.handleResize); } handleLoadMore = () => { @@ -176,7 +203,8 @@ class ChatMessageList extends ImmutablePureComponent { parseContent = chatMessage => { const content = chatMessage.get('content') || ''; const pending = chatMessage.get('pending', false); - const formatted = pending ? this.parsePendingContent(content) : content; + const deleting = chatMessage.get('deleting', false); + const formatted = (pending && !deleting) ? this.parsePendingContent(content) : content; const emojiMap = makeEmojiMap(chatMessage); return emojify(formatted, emojiMap.toJS()); } @@ -185,32 +213,85 @@ class ChatMessageList extends ImmutablePureComponent { this.node = c; } + renderDivider = (key, text) => ( +
{text}
+ ) + + handleDeleteMessage = (chatId, messageId) => { + return () => { + this.props.dispatch(deleteChatMessage(chatId, messageId)); + }; + } + + handleReportUser = (userId) => { + return () => { + this.props.dispatch(initReportById(userId)); + }; + } + + renderMessage = (chatMessage) => { + const { me, intl } = this.props; + const menu = [ + { text: intl.formatMessage(messages.delete), action: this.handleDeleteMessage(chatMessage.get('chat_id'), chatMessage.get('id')) }, + { text: intl.formatMessage(messages.report), action: this.handleReportUser(chatMessage.get('account_id')) }, + ]; + + return ( +
+
+ {this.maybeRenderMedia(chatMessage)} + +
+ +
+
+
+ ); + } + render() { - const { chatMessages, me } = this.props; + const { chatMessages, intl } = this.props; return (
- {chatMessages.map(chatMessage => ( -
-
- {this.maybeRenderMedia(chatMessage)} - -
-
- ))} + {chatMessages.reduce((acc, curr, idx) => { + const lastMessage = chatMessages.get(idx-1); + + if (lastMessage) { + const key = `${curr.get('id')}_divider`; + switch(timeChange(lastMessage, curr)) { + case 'today': + acc.push(this.renderDivider(key, intl.formatMessage(messages.today))); + break; + case 'date': + acc.push(this.renderDivider(key, new Date(curr.get('created_at')).toDateString())); + break; + } + } + + acc.push(this.renderMessage(curr)); + return acc; + }, [])}
); diff --git a/app/soapbox/features/chats/components/chat_panes.js b/app/soapbox/features/chats/components/chat_panes.js index 1bd7167bd..876ff9666 100644 --- a/app/soapbox/features/chats/components/chat_panes.js +++ b/app/soapbox/features/chats/components/chat_panes.js @@ -11,6 +11,7 @@ import { makeGetChat } from 'soapbox/selectors'; import { openChat, toggleMainWindow } from 'soapbox/actions/chats'; import ChatWindow from './chat_window'; import { shortNumberFormat } from 'soapbox/utils/numbers'; +import AudioToggle from 'soapbox/features/chats/components/audio_toggle'; const addChatsToPanes = (state, panesData) => { const getChat = makeGetChat(); @@ -62,6 +63,7 @@ class ChatPanes extends ImmutablePureComponent { +
+
{ const containerClass = classNames('input', { 'with_label': props.label, @@ -226,98 +220,6 @@ export class IconPicker extends ImmutablePureComponent { } -export class ColorPicker extends React.PureComponent { - - static propTypes = { - style: PropTypes.object, - value: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - onClose: PropTypes.func, - } - - handleDocumentClick = e => { - if (this.node && !this.node.contains(e.target)) { - this.props.onClose(); - } - } - - componentDidMount() { - document.addEventListener('click', this.handleDocumentClick, false); - document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - componentWillUnmount() { - document.removeEventListener('click', this.handleDocumentClick, false); - document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - setRef = c => { - this.node = c; - } - - render() { - const { style, value, onChange } = this.props; - let margin_left_picker = isMobile(window.innerWidth) ? '20px' : '12px'; - - return ( -
- -
- ); - } - -} - -export class ColorWithPicker extends ImmutablePureComponent { - - static propTypes = { - buttonId: PropTypes.string.isRequired, - label: FormPropTypes.label, - value: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - } - - onToggle = (e) => { - if (!e.key || e.key === 'Enter') { - if (this.state.active) { - this.onHidePicker(); - } else { - this.onShowPicker(e); - } - } - } - - state = { - active: false, - placement: null, - } - - onHidePicker = () => { - this.setState({ active: false }); - } - - onShowPicker = ({ target }) => { - this.setState({ active: true }); - this.setState({ placement: isMobile(window.innerWidth) ? 'bottom' : 'right' }); - } - - render() { - const { buttonId, label, value, onChange } = this.props; - const { active, placement } = this.state; - - return ( -
- - - ); - } - -} - export class RadioItem extends ImmutablePureComponent { static propTypes = { diff --git a/app/soapbox/features/notifications/index.js b/app/soapbox/features/notifications/index.js index 88a980369..a62eea8b4 100644 --- a/app/soapbox/features/notifications/index.js +++ b/app/soapbox/features/notifications/index.js @@ -30,7 +30,7 @@ const getNotifications = createSelector([ state => getSettings(state).getIn(['notifications', 'quickFilter', 'show']), state => getSettings(state).getIn(['notifications', 'quickFilter', 'active']), state => ImmutableList(getSettings(state).getIn(['notifications', 'shows']).filter(item => !item).keys()), - state => state.getIn(['notifications', 'items']), + state => state.getIn(['notifications', 'items']).toList(), ], (showFilterBar, allowedType, excludedTypes, notifications) => { if (!showFilterBar || allowedType === 'all') { // used if user changed the notification settings after loading the notifications from the server diff --git a/app/soapbox/features/preferences/index.js b/app/soapbox/features/preferences/index.js index beaab5c25..9a517acd7 100644 --- a/app/soapbox/features/preferences/index.js +++ b/app/soapbox/features/preferences/index.js @@ -185,6 +185,11 @@ class Preferences extends ImmutablePureComponent { path={['dyslexicFont']} />
+ } + hint={} + path={['halloween']} + /> } hint={} diff --git a/app/soapbox/features/public_layout/components/header.js b/app/soapbox/features/public_layout/components/header.js index 70c20dcaa..f80c483e7 100644 --- a/app/soapbox/features/public_layout/components/header.js +++ b/app/soapbox/features/public_layout/components/header.js @@ -6,14 +6,18 @@ import { Link } from 'react-router-dom'; import LoginForm from 'soapbox/features/auth_login/components/login_form'; import SiteLogo from './site_logo'; import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types'; +import { defineMessages, injectIntl } from 'react-intl'; +import PropTypes from 'prop-types'; import { logIn } from 'soapbox/actions/auth'; import { fetchMe } from 'soapbox/actions/me'; -import PropTypes from 'prop-types'; import OtpAuthForm from 'soapbox/features/auth_login/components/otp_auth_form'; import IconButton from 'soapbox/components/icon_button'; -import { defineMessages, injectIntl } from 'react-intl'; const messages = defineMessages({ + home: { id: 'header.home.label', defaultMessage: 'Home' }, + about: { id: 'header.about.label', defaultMessage: 'About' }, + backTo: { id: 'header.back_to.label', defaultMessage: 'Back to {siteTitle}' }, + login: { id: 'header.login.label', defaultMessage: 'Log in' }, close: { id: 'lightbox.close', defaultMessage: 'Close' }, }); @@ -32,48 +36,50 @@ class Header extends ImmutablePureComponent { this.handleSubmit = this.handleSubmit.bind(this); } - getFormData = (form) => { - return Object.fromEntries( - Array.from(form).map(i => [i.name, i.value]) - ); - } - - static contextTypes = { - router: PropTypes.object, - }; - - handleSubmit = (event) => { - const { dispatch } = this.props; - const { username, password } = this.getFormData(event.target); - dispatch(logIn(username, password)).then(() => { - return dispatch(fetchMe()); - }).catch(error => { - if (error.response.data.error === 'mfa_required') { - this.setState({ mfa_auth_needed: true, mfa_token: error.response.data.mfa_token }); - } - this.setState({ isLoading: false }); - }); - this.setState({ isLoading: true }); - event.preventDefault(); - } - - onClickClose = (event) => { - this.setState({ mfa_auth_needed: false, mfa_token: '' }); - } - - static propTypes = { - me: SoapboxPropTypes.me, - instance: ImmutablePropTypes.map, - } - state = { + isLoading: false, mfa_auth_needed: false, mfa_token: '', } + getFormData = (form) => { + return Object.fromEntries( + Array.from(form).map(i => [i.name, i.value]) + ); + } + + static contextTypes = { + router: PropTypes.object, + }; + + handleSubmit = (event) => { + const { dispatch } = this.props; + const { username, password } = this.getFormData(event.target); + dispatch(logIn(username, password)).then(() => { + return dispatch(fetchMe()); + }).catch(error => { + if (error.response.data.error === 'mfa_required') { + this.setState({ mfa_auth_needed: true, mfa_token: error.response.data.mfa_token }); + } + this.setState({ isLoading: false }); + }); + this.setState({ isLoading: true }); + event.preventDefault(); + } + + onClickClose = (event) => { + this.setState({ mfa_auth_needed: false, mfa_token: '' }); + } + + static propTypes = { + me: SoapboxPropTypes.me, + instance: ImmutablePropTypes.map, + intl: PropTypes.object.isRequired, + } + render() { - const { me, instance, isLoading, intl } = this.props; - const { mfa_auth_needed, mfa_token } = this.state; + const { me, instance, intl } = this.props; + const { isLoading, mfa_auth_needed, mfa_token } = this.state; return (
{me - ? Back to {instance.get('title')} + ? {intl.formatMessage(messages.backTo, { siteTitle: instance.get('title') })} : }
{me - ? Back to {instance.get('title')} - : Log in + ? {intl.formatMessage(messages.backTo, { siteTitle: instance.get('title') })} + : {intl.formatMessage(messages.login)} }
diff --git a/app/soapbox/features/soapbox_config/index.js b/app/soapbox/features/soapbox_config/index.js index 45eb65128..6c6b679df 100644 --- a/app/soapbox/features/soapbox_config/index.js +++ b/app/soapbox/features/soapbox_config/index.js @@ -12,15 +12,19 @@ import { Checkbox, FileChooser, SimpleTextarea, - ColorWithPicker, FileChooserLogo, IconPicker, + FormPropTypes, } from 'soapbox/features/forms'; import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; import { updateAdminConfig } from 'soapbox/actions/admin'; import Icon from 'soapbox/components/icon'; import { defaultConfig } from 'soapbox/actions/soapbox'; import { uploadMedia } from 'soapbox/actions/media'; +import { SketchPicker } from 'react-color'; +import Overlay from 'react-overlays/lib/Overlay'; +import { isMobile } from 'soapbox/is_mobile'; +import detectPassiveEvents from 'detect-passive-events'; const messages = defineMessages({ heading: { id: 'column.soapbox_config', defaultMessage: 'Soapbox config' }, @@ -35,6 +39,8 @@ const messages = defineMessages({ rawJSONHint: { id: 'soapbox_config.raw_json_hint', defaultMessage: 'Advanced: Edit the settings data directly.' }, }); +const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; + const templates = { promoPanelItem: ImmutableMap({ icon: '', text: '', url: '' }), footerItem: ImmutableMap({ title: '', url: '' }), @@ -364,3 +370,95 @@ class SoapboxConfig extends ImmutablePureComponent { } } + +class ColorPicker extends React.PureComponent { + + static propTypes = { + style: PropTypes.object, + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + onClose: PropTypes.func, + } + + handleDocumentClick = e => { + if (this.node && !this.node.contains(e.target)) { + this.props.onClose(); + } + } + + componentDidMount() { + document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); + } + + componentWillUnmount() { + document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); + } + + setRef = c => { + this.node = c; + } + + render() { + const { style, value, onChange } = this.props; + let margin_left_picker = isMobile(window.innerWidth) ? '20px' : '12px'; + + return ( +
+ +
+ ); + } + +} + +class ColorWithPicker extends ImmutablePureComponent { + + static propTypes = { + buttonId: PropTypes.string.isRequired, + label: FormPropTypes.label, + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + } + + onToggle = (e) => { + if (!e.key || e.key === 'Enter') { + if (this.state.active) { + this.onHidePicker(); + } else { + this.onShowPicker(e); + } + } + } + + state = { + active: false, + placement: null, + } + + onHidePicker = () => { + this.setState({ active: false }); + } + + onShowPicker = ({ target }) => { + this.setState({ active: true }); + this.setState({ placement: isMobile(window.innerWidth) ? 'bottom' : 'right' }); + } + + render() { + const { buttonId, label, value, onChange } = this.props; + const { active, placement } = this.state; + + return ( +
+ + + ); + } + +} diff --git a/app/soapbox/features/ui/components/user_panel.js b/app/soapbox/features/ui/components/user_panel.js index 411b9b70b..82415cd9d 100644 --- a/app/soapbox/features/ui/components/user_panel.js +++ b/app/soapbox/features/ui/components/user_panel.js @@ -56,21 +56,21 @@ class UserPanel extends ImmutablePureComponent {
- {account.get('statuses_count') &&
+ {account.get('statuses_count') >= 0 &&
{shortNumberFormat(account.get('statuses_count'))}
} - {account.get('followers_count') &&
+ {account.get('followers_count') >= 0 &&
{shortNumberFormat(account.get('followers_count'))}
} - {account.get('following_count') &&
+ {account.get('following_count') >= 0 &&
{shortNumberFormat(account.get('following_count'))} diff --git a/app/soapbox/middleware/sounds.js b/app/soapbox/middleware/sounds.js index 032bc804b..e819a02ee 100644 --- a/app/soapbox/middleware/sounds.js +++ b/app/soapbox/middleware/sounds.js @@ -36,6 +36,16 @@ export default function soundsMiddleware() { type: 'audio/mpeg', }, ]), + chat: createAudio([ + { + src: '/sounds/chat.oga', + type: 'audio/ogg', + }, + { + src: '/sounds/chat.mp3', + type: 'audio/mpeg', + }, + ]), }; return () => next => action => { diff --git a/app/soapbox/reducers/__tests__/contexts-test.js b/app/soapbox/reducers/__tests__/contexts-test.js index 3130f2e53..8b5a30d50 100644 --- a/app/soapbox/reducers/__tests__/contexts-test.js +++ b/app/soapbox/reducers/__tests__/contexts-test.js @@ -1,5 +1,8 @@ import reducer from '../contexts'; -import { Map as ImmutableMap } from 'immutable'; +import { CONTEXT_FETCH_SUCCESS } from 'soapbox/actions/statuses'; +import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable'; +import context1 from 'soapbox/__fixtures__/context_1.json'; +import context2 from 'soapbox/__fixtures__/context_2.json'; describe('contexts reducer', () => { it('should return the initial state', () => { @@ -8,4 +11,34 @@ describe('contexts reducer', () => { 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 }); + + expect(result).toEqual(ImmutableMap({ + inReplyTos: ImmutableMap({ + '9zIH7PUdhK3Ircg4hM': '9zIH6kDXA10YqhMKqO', + '9zIH7mMGgc1RmJwDLM': '9zIH6kDXA10YqhMKqO', + '9zIH9GTCDWEFSRt2um': '9zIH7PUdhK3Ircg4hM', + '9zIH9fhaP9atiJoOJc': '9zIH8WYwtnUx4yDzUm', + '9zIH8WYwtnUx4yDzUm': '9zIH7PUdhK3Ircg4hM', + }), + replies: ImmutableMap({ + '9zIH6kDXA10YqhMKqO': ImmutableOrderedSet([ + '9zIH7PUdhK3Ircg4hM', + '9zIH7mMGgc1RmJwDLM', + ]), + '9zIH7PUdhK3Ircg4hM': ImmutableOrderedSet([ + '9zIH8WYwtnUx4yDzUm', + '9zIH9GTCDWEFSRt2um', + ]), + '9zIH8WYwtnUx4yDzUm': ImmutableOrderedSet([ + '9zIH9fhaP9atiJoOJc', + ]), + }), + })); + }); }); diff --git a/app/soapbox/reducers/__tests__/notifications-test.js b/app/soapbox/reducers/__tests__/notifications-test.js index 97141c756..d2d434c3d 100644 --- a/app/soapbox/reducers/__tests__/notifications-test.js +++ b/app/soapbox/reducers/__tests__/notifications-test.js @@ -1,23 +1,23 @@ import * as actions from 'soapbox/actions/notifications'; import reducer from '../notifications'; import notifications from 'soapbox/__fixtures__/notifications.json'; -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import { Map as ImmutableMap, OrderedMap as ImmutableOrderedMap } from 'immutable'; import { take } from 'lodash'; import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'soapbox/actions/accounts'; import notification from 'soapbox/__fixtures__/notification.json'; import intlMessages from 'soapbox/__fixtures__/intlMessages.json'; import relationship from 'soapbox/__fixtures__/relationship.json'; -import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from 'soapbox/actions/timelines'; +import { TIMELINE_DELETE } from 'soapbox/actions/timelines'; describe('notifications reducer', () => { it('should return the initial state', () => { expect(reducer(undefined, {})).toEqual(ImmutableMap({ - items: ImmutableList(), + items: ImmutableOrderedMap(), hasMore: true, top: false, unread: 0, isLoading: false, - queuedNotifications: ImmutableList(), + queuedNotifications: ImmutableOrderedMap(), totalQueuedNotificationsCount: 0, lastRead: -1, })); @@ -32,8 +32,8 @@ describe('notifications reducer', () => { skipLoading: true, }; expect(reducer(state, action)).toEqual(ImmutableMap({ - items: ImmutableList([ - ImmutableMap({ + items: ImmutableOrderedMap([ + ['10744', ImmutableMap({ id: '10744', type: 'pleroma:emoji_reaction', account: '9vMAje101ngtjlMj7w', @@ -42,8 +42,8 @@ describe('notifications reducer', () => { emoji: '😢', chat_message: undefined, is_seen: false, - }), - ImmutableMap({ + })], + ['10743', ImmutableMap({ id: '10743', type: 'favourite', account: '9v5c6xSEgAi3Zu1Lv6', @@ -52,8 +52,8 @@ describe('notifications reducer', () => { emoji: undefined, chat_message: undefined, is_seen: true, - }), - ImmutableMap({ + })], + ['10741', ImmutableMap({ id: '10741', type: 'favourite', account: '9v5cKMOPGqPcgfcWp6', @@ -62,13 +62,13 @@ describe('notifications reducer', () => { emoji: undefined, chat_message: undefined, is_seen: true, - }), + })], ]), hasMore: false, top: false, unread: 1, isLoading: false, - queuedNotifications: ImmutableList(), + queuedNotifications: ImmutableOrderedMap(), totalQueuedNotificationsCount: 0, lastRead: -1, })); @@ -100,8 +100,8 @@ describe('notifications reducer', () => { it('should handle NOTIFICATIONS_FILTER_SET', () => { const state = ImmutableMap({ - items: ImmutableList([ - ImmutableMap({ + items: ImmutableOrderedMap([ + ['10744', ImmutableMap({ id: '10744', type: 'pleroma:emoji_reaction', account: '9vMAje101ngtjlMj7w', @@ -110,8 +110,8 @@ describe('notifications reducer', () => { emoji: '😢', chat_message: undefined, is_seen: false, - }), - ImmutableMap({ + })], + ['10743', ImmutableMap({ id: '10743', type: 'favourite', account: '9v5c6xSEgAi3Zu1Lv6', @@ -120,8 +120,8 @@ describe('notifications reducer', () => { emoji: undefined, chat_message: undefined, is_seen: true, - }), - ImmutableMap({ + })], + ['10741', ImmutableMap({ id: '10741', type: 'favourite', account: '9v5cKMOPGqPcgfcWp6', @@ -130,13 +130,13 @@ describe('notifications reducer', () => { emoji: undefined, chat_message: undefined, is_seen: true, - }), + })], ]), hasMore: false, top: false, unread: 1, isLoading: false, - queuedNotifications: ImmutableList(), + queuedNotifications: ImmutableOrderedMap(), totalQueuedNotificationsCount: 0, lastRead: -1, }); @@ -144,12 +144,12 @@ describe('notifications reducer', () => { type: actions.NOTIFICATIONS_FILTER_SET, }; expect(reducer(state, action)).toEqual(ImmutableMap({ - items: ImmutableList(), + items: ImmutableOrderedMap(), hasMore: true, top: false, unread: 1, isLoading: false, - queuedNotifications: ImmutableList(), + queuedNotifications: ImmutableOrderedMap(), totalQueuedNotificationsCount: 0, lastRead: -1, })); @@ -185,7 +185,7 @@ describe('notifications reducer', () => { it('should handle NOTIFICATIONS_UPDATE, when top = false, increment unread', () => { const state = ImmutableMap({ - items: ImmutableList(), + items: ImmutableOrderedMap(), top: false, unread: 1, }); @@ -194,8 +194,8 @@ describe('notifications reducer', () => { notification: notification, }; expect(reducer(state, action)).toEqual(ImmutableMap({ - items: ImmutableList([ - ImmutableMap({ + items: ImmutableOrderedMap([ + ['10743', ImmutableMap({ id: '10743', type: 'favourite', account: '9v5c6xSEgAi3Zu1Lv6', @@ -204,7 +204,7 @@ describe('notifications reducer', () => { emoji: undefined, chat_message: undefined, is_seen: true, - }), + })], ]), top: false, unread: 2, @@ -213,8 +213,8 @@ describe('notifications reducer', () => { it('should handle NOTIFICATIONS_UPDATE_QUEUE', () => { const state = ImmutableMap({ - items: ImmutableList([]), - queuedNotifications: ImmutableList([]), + items: ImmutableOrderedMap(), + queuedNotifications: ImmutableOrderedMap(), totalQueuedNotificationsCount: 0, }); const action = { @@ -224,19 +224,19 @@ describe('notifications reducer', () => { intlLocale: 'en', }; expect(reducer(state, action)).toEqual(ImmutableMap({ - items: ImmutableList([]), - queuedNotifications: ImmutableList([{ + items: ImmutableOrderedMap(), + queuedNotifications: ImmutableOrderedMap([[notification.id, { notification: notification, intlMessages: intlMessages, intlLocale: 'en', - }]), + }]]), totalQueuedNotificationsCount: 1, })); }); it('should handle NOTIFICATIONS_DEQUEUE', () => { const state = ImmutableMap({ - items: ImmutableList([]), + items: ImmutableOrderedMap(), queuedNotifications: take(notifications, 1), totalQueuedNotificationsCount: 1, }); @@ -244,16 +244,16 @@ describe('notifications reducer', () => { type: actions.NOTIFICATIONS_DEQUEUE, }; expect(reducer(state, action)).toEqual(ImmutableMap({ - items: ImmutableList([]), - queuedNotifications: ImmutableList([]), + items: ImmutableOrderedMap(), + queuedNotifications: ImmutableOrderedMap(), totalQueuedNotificationsCount: 0, })); }); it('should handle NOTIFICATIONS_EXPAND_SUCCESS with non-empty items and next set true', () => { const state = ImmutableMap({ - items: ImmutableList([ - ImmutableMap({ + items: ImmutableOrderedMap([ + ['10734', ImmutableMap({ id: '10734', type: 'pleroma:emoji_reaction', account: '9vMAje101ngtjlMj7w', @@ -262,7 +262,7 @@ describe('notifications reducer', () => { emoji: '😢', chat_message: undefined, is_seen: false, - }), + })], ]), unread: 1, hasMore: true, @@ -274,8 +274,8 @@ describe('notifications reducer', () => { next: true, }; expect(reducer(state, action)).toEqual(ImmutableMap({ - items: ImmutableList([ - ImmutableMap({ + items: ImmutableOrderedMap([ + ['10744', ImmutableMap({ id: '10744', type: 'pleroma:emoji_reaction', account: '9vMAje101ngtjlMj7w', @@ -284,8 +284,8 @@ describe('notifications reducer', () => { emoji: '😢', chat_message: undefined, is_seen: false, - }), - ImmutableMap({ + })], + ['10743', ImmutableMap({ id: '10743', type: 'favourite', account: '9v5c6xSEgAi3Zu1Lv6', @@ -294,8 +294,8 @@ describe('notifications reducer', () => { emoji: undefined, chat_message: undefined, is_seen: true, - }), - ImmutableMap({ + })], + ['10741', ImmutableMap({ id: '10741', type: 'favourite', account: '9v5cKMOPGqPcgfcWp6', @@ -304,8 +304,8 @@ describe('notifications reducer', () => { emoji: undefined, chat_message: undefined, is_seen: true, - }), - ImmutableMap({ + })], + ['10734', ImmutableMap({ id: '10734', type: 'pleroma:emoji_reaction', account: '9vMAje101ngtjlMj7w', @@ -314,7 +314,7 @@ describe('notifications reducer', () => { emoji: '😢', chat_message: undefined, is_seen: false, - }), + })], ]), unread: 1, hasMore: true, @@ -324,7 +324,7 @@ describe('notifications reducer', () => { it('should handle NOTIFICATIONS_EXPAND_SUCCESS with empty items and next set true', () => { const state = ImmutableMap({ - items: ImmutableList([]), + items: ImmutableOrderedMap(), unread: 1, hasMore: true, isLoading: false, @@ -335,8 +335,8 @@ describe('notifications reducer', () => { next: true, }; expect(reducer(state, action)).toEqual(ImmutableMap({ - items: ImmutableList([ - ImmutableMap({ + items: ImmutableOrderedMap([ + ['10744', ImmutableMap({ id: '10744', type: 'pleroma:emoji_reaction', account: '9vMAje101ngtjlMj7w', @@ -345,8 +345,8 @@ describe('notifications reducer', () => { emoji: '😢', chat_message: undefined, is_seen: false, - }), - ImmutableMap({ + })], + ['10743', ImmutableMap({ id: '10743', type: 'favourite', account: '9v5c6xSEgAi3Zu1Lv6', @@ -355,8 +355,8 @@ describe('notifications reducer', () => { emoji: undefined, chat_message: undefined, is_seen: true, - }), - ImmutableMap({ + })], + ['10741', ImmutableMap({ id: '10741', type: 'favourite', account: '9v5cKMOPGqPcgfcWp6', @@ -365,7 +365,7 @@ describe('notifications reducer', () => { emoji: undefined, chat_message: undefined, is_seen: true, - }), + })], ]), unread: 1, hasMore: true, @@ -375,8 +375,8 @@ describe('notifications reducer', () => { it('should handle ACCOUNT_BLOCK_SUCCESS', () => { const state = ImmutableMap({ - items: ImmutableList([ - ImmutableMap({ + items: ImmutableOrderedMap([ + ['10744', ImmutableMap({ id: '10744', type: 'pleroma:emoji_reaction', account: '9vMAje101ngtjlMj7w', @@ -385,8 +385,8 @@ describe('notifications reducer', () => { emoji: '😢', chat_message: undefined, is_seen: false, - }), - ImmutableMap({ + })], + ['10743', ImmutableMap({ id: '10743', type: 'favourite', account: '9v5c6xSEgAi3Zu1Lv6', @@ -395,8 +395,8 @@ describe('notifications reducer', () => { emoji: undefined, chat_message: undefined, is_seen: true, - }), - ImmutableMap({ + })], + ['10741', ImmutableMap({ id: '10741', type: 'favourite', account: '9v5cKMOPGqPcgfcWp6', @@ -405,7 +405,7 @@ describe('notifications reducer', () => { emoji: undefined, chat_message: undefined, is_seen: true, - }), + })], ]), }); const action = { @@ -413,8 +413,8 @@ describe('notifications reducer', () => { relationship: relationship, }; expect(reducer(state, action)).toEqual(ImmutableMap({ - items: ImmutableList([ - ImmutableMap({ + items: ImmutableOrderedMap([ + ['10743', ImmutableMap({ id: '10743', type: 'favourite', account: '9v5c6xSEgAi3Zu1Lv6', @@ -423,8 +423,8 @@ describe('notifications reducer', () => { emoji: undefined, chat_message: undefined, is_seen: true, - }), - ImmutableMap({ + })], + ['10741', ImmutableMap({ id: '10741', type: 'favourite', account: '9v5cKMOPGqPcgfcWp6', @@ -433,15 +433,15 @@ describe('notifications reducer', () => { emoji: undefined, chat_message: undefined, is_seen: true, - }), + })], ]), })); }); it('should handle ACCOUNT_MUTE_SUCCESS', () => { const state = ImmutableMap({ - items: ImmutableList([ - ImmutableMap({ + items: ImmutableOrderedMap([ + ['10744', ImmutableMap({ id: '10744', type: 'pleroma:emoji_reaction', account: '9vMAje101ngtjlMj7w', @@ -450,8 +450,8 @@ describe('notifications reducer', () => { emoji: '😢', chat_message: undefined, is_seen: false, - }), - ImmutableMap({ + })], + ['10743', ImmutableMap({ id: '10743', type: 'favourite', account: '9v5c6xSEgAi3Zu1Lv6', @@ -460,8 +460,8 @@ describe('notifications reducer', () => { emoji: undefined, chat_message: undefined, is_seen: true, - }), - ImmutableMap({ + })], + ['10741', ImmutableMap({ id: '10741', type: 'favourite', account: '9v5cKMOPGqPcgfcWp6', @@ -470,7 +470,7 @@ describe('notifications reducer', () => { emoji: undefined, chat_message: undefined, is_seen: true, - }), + })], ]), }); const action = { @@ -478,8 +478,8 @@ describe('notifications reducer', () => { relationship: relationship, }; expect(reducer(state, action)).toEqual(ImmutableMap({ - items: ImmutableList([ - ImmutableMap({ + items: ImmutableOrderedMap([ + ['10743', ImmutableMap({ id: '10743', type: 'favourite', account: '9v5c6xSEgAi3Zu1Lv6', @@ -488,8 +488,8 @@ describe('notifications reducer', () => { emoji: undefined, chat_message: undefined, is_seen: true, - }), - ImmutableMap({ + })], + ['10741', ImmutableMap({ id: '10741', type: 'favourite', account: '9v5cKMOPGqPcgfcWp6', @@ -498,43 +498,43 @@ describe('notifications reducer', () => { emoji: undefined, chat_message: undefined, is_seen: true, - }), + })], ]), })); }); it('should handle NOTIFICATIONS_CLEAR', () => { const state = ImmutableMap({ - items: ImmutableList([]), + items: ImmutableOrderedMap(), hasMore: true, }); const action = { type: actions.NOTIFICATIONS_CLEAR, }; expect(reducer(state, action)).toEqual(ImmutableMap({ - items: ImmutableList([]), + items: ImmutableOrderedMap(), hasMore: false, })); }); it('should handle NOTIFICATIONS_MARK_READ_REQUEST', () => { const state = ImmutableMap({ - items: ImmutableList([]), + items: ImmutableOrderedMap(), }); const action = { type: actions.NOTIFICATIONS_MARK_READ_REQUEST, lastRead: 35098814, }; expect(reducer(state, action)).toEqual(ImmutableMap({ - items: ImmutableList([]), + items: ImmutableOrderedMap(), lastRead: 35098814, })); }); it('should handle TIMELINE_DELETE', () => { const state = ImmutableMap({ - items: ImmutableList([ - ImmutableMap({ + items: ImmutableOrderedMap([ + ['10744', ImmutableMap({ id: '10744', type: 'pleroma:emoji_reaction', account: '9vMAje101ngtjlMj7w', @@ -543,8 +543,8 @@ describe('notifications reducer', () => { emoji: '😢', chat_message: undefined, is_seen: false, - }), - ImmutableMap({ + })], + ['10743', ImmutableMap({ id: '10743', type: 'favourite', account: '9v5c6xSEgAi3Zu1Lv6', @@ -553,8 +553,8 @@ describe('notifications reducer', () => { emoji: undefined, chat_message: undefined, is_seen: true, - }), - ImmutableMap({ + })], + ['10741', ImmutableMap({ id: '10741', type: 'favourite', account: '9v5cKMOPGqPcgfcWp6', @@ -563,7 +563,7 @@ describe('notifications reducer', () => { emoji: undefined, chat_message: undefined, is_seen: true, - }), + })], ]), }); const action = { @@ -571,84 +571,87 @@ describe('notifications reducer', () => { id: '9vvNxoo5EFbbnfdXQu', }; expect(reducer(state, action)).toEqual(ImmutableMap({ - items: ImmutableList([]), + items: ImmutableOrderedMap(), })); }); - it('should handle TIMELINE_DISCONNECT', () => { - const state = ImmutableMap({ - items: ImmutableList([ - ImmutableMap({ - id: '10744', - type: 'pleroma:emoji_reaction', - account: '9vMAje101ngtjlMj7w', - created_at: '2020-06-10T02:54:39.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: '😢', - chat_message: undefined, - is_seen: false, - }), - ImmutableMap({ - id: '10743', - type: 'favourite', - account: '9v5c6xSEgAi3Zu1Lv6', - created_at: '2020-06-10T02:51:05.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: undefined, - chat_message: undefined, - is_seen: true, - }), - ImmutableMap({ - id: '10741', - type: 'favourite', - account: '9v5cKMOPGqPcgfcWp6', - created_at: '2020-06-10T02:05:06.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: undefined, - chat_message: undefined, - is_seen: true, - }), - ]), - }); - const action = { - type: TIMELINE_DISCONNECT, - timeline: 'home', - }; - expect(reducer(state, action)).toEqual(ImmutableMap({ - items: ImmutableList([ - null, - ImmutableMap({ - id: '10744', - type: 'pleroma:emoji_reaction', - account: '9vMAje101ngtjlMj7w', - created_at: '2020-06-10T02:54:39.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: '😢', - chat_message: undefined, - is_seen: false, - }), - ImmutableMap({ - id: '10743', - type: 'favourite', - account: '9v5c6xSEgAi3Zu1Lv6', - created_at: '2020-06-10T02:51:05.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: undefined, - chat_message: undefined, - is_seen: true, - }), - ImmutableMap({ - id: '10741', - type: 'favourite', - account: '9v5cKMOPGqPcgfcWp6', - created_at: '2020-06-10T02:05:06.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: undefined, - chat_message: undefined, - is_seen: true, - }), - ]), - })); - }); + // Disable for now + // https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/432 + // + // it('should handle TIMELINE_DISCONNECT', () => { + // const state = ImmutableMap({ + // items: ImmutableOrderedSet([ + // ImmutableMap({ + // id: '10744', + // type: 'pleroma:emoji_reaction', + // account: '9vMAje101ngtjlMj7w', + // created_at: '2020-06-10T02:54:39.000Z', + // status: '9vvNxoo5EFbbnfdXQu', + // emoji: '😢', + // chat_message: undefined, + // is_seen: false, + // }), + // ImmutableMap({ + // id: '10743', + // type: 'favourite', + // account: '9v5c6xSEgAi3Zu1Lv6', + // created_at: '2020-06-10T02:51:05.000Z', + // status: '9vvNxoo5EFbbnfdXQu', + // emoji: undefined, + // chat_message: undefined, + // is_seen: true, + // }), + // ImmutableMap({ + // id: '10741', + // type: 'favourite', + // account: '9v5cKMOPGqPcgfcWp6', + // created_at: '2020-06-10T02:05:06.000Z', + // status: '9vvNxoo5EFbbnfdXQu', + // emoji: undefined, + // chat_message: undefined, + // is_seen: true, + // }), + // ]), + // }); + // const action = { + // type: TIMELINE_DISCONNECT, + // timeline: 'home', + // }; + // expect(reducer(state, action)).toEqual(ImmutableMap({ + // items: ImmutableOrderedSet([ + // null, + // ImmutableMap({ + // id: '10744', + // type: 'pleroma:emoji_reaction', + // account: '9vMAje101ngtjlMj7w', + // created_at: '2020-06-10T02:54:39.000Z', + // status: '9vvNxoo5EFbbnfdXQu', + // emoji: '😢', + // chat_message: undefined, + // is_seen: false, + // }), + // ImmutableMap({ + // id: '10743', + // type: 'favourite', + // account: '9v5c6xSEgAi3Zu1Lv6', + // created_at: '2020-06-10T02:51:05.000Z', + // status: '9vvNxoo5EFbbnfdXQu', + // emoji: undefined, + // chat_message: undefined, + // is_seen: true, + // }), + // ImmutableMap({ + // id: '10741', + // type: 'favourite', + // account: '9v5cKMOPGqPcgfcWp6', + // created_at: '2020-06-10T02:05:06.000Z', + // status: '9vvNxoo5EFbbnfdXQu', + // emoji: undefined, + // chat_message: undefined, + // is_seen: true, + // }), + // ]), + // })); + // }); }); diff --git a/app/soapbox/reducers/chat_message_lists.js b/app/soapbox/reducers/chat_message_lists.js index 3e2d1dbae..8848a8389 100644 --- a/app/soapbox/reducers/chat_message_lists.js +++ b/app/soapbox/reducers/chat_message_lists.js @@ -3,6 +3,7 @@ import { CHAT_MESSAGES_FETCH_SUCCESS, CHAT_MESSAGE_SEND_REQUEST, CHAT_MESSAGE_SEND_SUCCESS, + CHAT_MESSAGE_DELETE_SUCCESS, } from 'soapbox/actions/chats'; import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming'; import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable'; @@ -59,6 +60,8 @@ export default function chatMessageLists(state = initialState, action) { return updateList(state, action.chatId, action.chatMessages.map(chat => chat.id)); case CHAT_MESSAGE_SEND_SUCCESS: return replaceMessage(state, action.chatId, action.uuid, action.chatMessage.id); + case CHAT_MESSAGE_DELETE_SUCCESS: + return state.update(action.chatId, chat => chat.delete(action.messageId)); default: return state; } diff --git a/app/soapbox/reducers/chat_messages.js b/app/soapbox/reducers/chat_messages.js index 74d83ef79..ababe85bd 100644 --- a/app/soapbox/reducers/chat_messages.js +++ b/app/soapbox/reducers/chat_messages.js @@ -3,6 +3,8 @@ import { CHAT_MESSAGES_FETCH_SUCCESS, CHAT_MESSAGE_SEND_REQUEST, CHAT_MESSAGE_SEND_SUCCESS, + CHAT_MESSAGE_DELETE_REQUEST, + CHAT_MESSAGE_DELETE_SUCCESS, } from 'soapbox/actions/chats'; import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming'; import { Map as ImmutableMap, fromJS } from 'immutable'; @@ -43,6 +45,11 @@ export default function chatMessages(state = initialState, action) { return importMessage(state, fromJS(action.chatMessage)).delete(action.uuid); case STREAMING_CHAT_UPDATE: return importLastMessages(state, fromJS([action.chat])); + case CHAT_MESSAGE_DELETE_REQUEST: + return state.update(action.messageId, chatMessage => + chatMessage.set('pending', true).set('deleting', true)); + case CHAT_MESSAGE_DELETE_SUCCESS: + return state.delete(action.messageId); default: return state; } diff --git a/app/soapbox/reducers/contexts.js b/app/soapbox/reducers/contexts.js index 4c2d6cc8a..8df462f81 100644 --- a/app/soapbox/reducers/contexts.js +++ b/app/soapbox/reducers/contexts.js @@ -4,8 +4,7 @@ import { } from '../actions/accounts'; import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses'; import { TIMELINE_DELETE, TIMELINE_UPDATE } from '../actions/timelines'; -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -import compareId from '../compare_id'; +import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable'; const initialState = ImmutableMap({ inReplyTos: ImmutableMap(), @@ -16,26 +15,16 @@ const normalizeContext = (immutableState, id, ancestors, descendants) => immutab state.update('inReplyTos', immutableAncestors => immutableAncestors.withMutations(inReplyTos => { state.update('replies', immutableDescendants => immutableDescendants.withMutations(replies => { function addReply({ id, in_reply_to_id }) { - if (in_reply_to_id && !inReplyTos.has(id)) { - - replies.update(in_reply_to_id, ImmutableList(), siblings => { - const index = siblings.findLastIndex(sibling => compareId(sibling, id) < 0); - return siblings.insert(index + 1, id); + if (in_reply_to_id) { + replies.update(in_reply_to_id, ImmutableOrderedSet(), siblings => { + return siblings.add(id).sort(); }); inReplyTos.set(id, in_reply_to_id); } } - // We know in_reply_to_id of statuses but `id` itself. - // So we assume that the status of the id replies to last ancestors. - ancestors.forEach(addReply); - - if (ancestors[0]) { - addReply({ id, in_reply_to_id: ancestors[ancestors.length - 1].id }); - } - descendants.forEach(addReply); })); })); @@ -76,12 +65,12 @@ const filterContexts = (state, relationship, statuses) => { const updateContext = (state, status) => { if (status.in_reply_to_id) { return state.withMutations(mutable => { - const replies = mutable.getIn(['replies', status.in_reply_to_id], ImmutableList()); + const replies = mutable.getIn(['replies', status.in_reply_to_id], ImmutableOrderedSet()); mutable.setIn(['inReplyTos', status.id], status.in_reply_to_id); if (!replies.includes(status.id)) { - mutable.setIn(['replies', status.in_reply_to_id], replies.push(status.id)); + mutable.setIn(['replies', status.in_reply_to_id], replies.add(status.id).sort()); } }); } diff --git a/app/soapbox/reducers/notifications.js b/app/soapbox/reducers/notifications.js index c4c97c73f..7dbda2c64 100644 --- a/app/soapbox/reducers/notifications.js +++ b/app/soapbox/reducers/notifications.js @@ -15,22 +15,28 @@ import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS, } from '../actions/accounts'; -import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines'; -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -import compareId from '../compare_id'; +import { TIMELINE_DELETE } from '../actions/timelines'; +import { Map as ImmutableMap, OrderedMap as ImmutableOrderedMap } from 'immutable'; import { get } from 'lodash'; const initialState = ImmutableMap({ - items: ImmutableList(), + items: ImmutableOrderedMap(), hasMore: true, top: false, unread: 0, isLoading: false, - queuedNotifications: ImmutableList(), //max = MAX_QUEUED_NOTIFICATIONS + queuedNotifications: ImmutableOrderedMap(), //max = MAX_QUEUED_NOTIFICATIONS totalQueuedNotificationsCount: 0, //used for queuedItems overflow for MAX_QUEUED_NOTIFICATIONS+ lastRead: -1, }); +// For sorting the notifications +const comparator = (a, b) => { + if (a.get('id') < b.get('id')) return 1; + if (a.get('id') > b.get('id')) return -1; + return 0; +}; + const notificationToMap = notification => ImmutableMap({ id: notification.id, type: notification.type, @@ -42,85 +48,67 @@ const notificationToMap = notification => ImmutableMap({ is_seen: get(notification, ['pleroma', 'is_seen'], true), }); +// https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/424 +const isValid = notification => Boolean(notification.account.id); + const normalizeNotification = (state, notification) => { const top = state.get('top'); - if (!top) { - state = state.update('unread', unread => unread + 1); - } + if (!top) state = state.update('unread', unread => unread + 1); - return state.update('items', list => { - if (top && list.size > 40) { - list = list.take(20); + return state.update('items', map => { + if (top && map.size > 40) { + map = map.take(20); } - return list.unshift(notificationToMap(notification)); + return map.set(notification.id, notificationToMap(notification)).sort(comparator); }); }; -const expandNormalizedNotifications = (state, notifications, next) => { - let items = ImmutableList(); +const processRawNotifications = notifications => ( + ImmutableOrderedMap( + notifications + .filter(isValid) + .map(n => [n.id, notificationToMap(n)]) + )); - notifications.forEach((n, i) => { - items = items.set(i, notificationToMap(n)); - }); +const expandNormalizedNotifications = (state, notifications, next) => { + const items = processRawNotifications(notifications); return state.withMutations(mutable => { - if (!items.isEmpty()) { - mutable.update('items', list => { - const lastIndex = 1 + list.findLastIndex( - item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id')) - ); - - const firstIndex = 1 + list.take(lastIndex).findLastIndex( - item => item !== null && compareId(item.get('id'), items.first().get('id')) > 0 - ); - - return list.take(firstIndex).concat(items, list.skip(lastIndex)); - }); - } - - if (!next) { - mutable.set('hasMore', false); - } + mutable.update('items', map => map.merge(items).sort(comparator)); + if (!next) mutable.set('hasMore', false); mutable.set('isLoading', false); }); }; const filterNotifications = (state, relationship) => { - return state.update('items', list => list.filterNot(item => item !== null && item.get('account') === relationship.id)); + return state.update('items', map => map.filterNot(item => item !== null && item.get('account') === relationship.id)); }; const updateTop = (state, top) => { - if (top) { - state = state.set('unread', 0); - } - + if (top) state = state.set('unread', 0); return state.set('top', top); }; const deleteByStatus = (state, statusId) => { - return state.update('items', list => list.filterNot(item => item !== null && item.get('status') === statusId)); + return state.update('items', map => map.filterNot(item => item !== null && item.get('status') === statusId)); }; const updateNotificationsQueue = (state, notification, intlMessages, intlLocale) => { - const queuedNotifications = state.getIn(['queuedNotifications'], ImmutableList()); - const listedNotifications = state.getIn(['items'], ImmutableList()); + const queuedNotifications = state.getIn(['queuedNotifications'], ImmutableOrderedMap()); + const listedNotifications = state.getIn(['items'], ImmutableOrderedMap()); const totalQueuedNotificationsCount = state.getIn(['totalQueuedNotificationsCount'], 0); - let alreadyExists = queuedNotifications.find(existingQueuedNotification => existingQueuedNotification.id === notification.id); - if (!alreadyExists) alreadyExists = listedNotifications.find(existingListedNotification => existingListedNotification.get('id') === notification.id); - - if (alreadyExists) { - return state; - } + const alreadyExists = queuedNotifications.has(notification.id) || listedNotifications.has(notification.id); + if (alreadyExists) return state; let newQueuedNotifications = queuedNotifications; return state.withMutations(mutable => { if (totalQueuedNotificationsCount <= MAX_QUEUED_NOTIFICATIONS) { - mutable.set('queuedNotifications', newQueuedNotifications.push({ + mutable.set('queuedNotifications', newQueuedNotifications.set(notification.id, { notification, intlMessages, intlLocale, @@ -130,6 +118,9 @@ const updateNotificationsQueue = (state, notification, intlMessages, intlLocale) }); }; +const countUnseen = notifications => notifications.reduce((acc, cur) => + get(cur, ['pleroma', 'is_seen'], false) === false ? acc + 1 : acc, 0); + export default function notifications(state = initialState, action) { switch(action.type) { case NOTIFICATIONS_EXPAND_REQUEST: @@ -137,7 +128,7 @@ export default function notifications(state = initialState, action) { case NOTIFICATIONS_EXPAND_FAIL: return state.set('isLoading', false); case NOTIFICATIONS_FILTER_SET: - return state.set('items', ImmutableList()).set('hasMore', true); + return state.set('items', ImmutableOrderedMap()).set('hasMore', true); case NOTIFICATIONS_SCROLL_TOP: return updateTop(state, action.top); case NOTIFICATIONS_UPDATE: @@ -146,12 +137,11 @@ export default function notifications(state = initialState, action) { return updateNotificationsQueue(state, action.notification, action.intlMessages, action.intlLocale); case NOTIFICATIONS_DEQUEUE: return state.withMutations(mutable => { - mutable.set('queuedNotifications', ImmutableList()); + mutable.set('queuedNotifications', ImmutableOrderedMap()); mutable.set('totalQueuedNotificationsCount', 0); }); case NOTIFICATIONS_EXPAND_SUCCESS: - const legacyUnread = action.notifications.reduce((acc, cur) => - get(cur, ['pleroma', 'is_seen'], false) === false ? acc + 1 : acc, 0); + const legacyUnread = countUnseen(action.notifications); return expandNormalizedNotifications(state, action.notifications, action.next) .merge({ unread: Math.max(legacyUnread, state.get('unread')) }); case ACCOUNT_BLOCK_SUCCESS: @@ -159,15 +149,21 @@ export default function notifications(state = initialState, action) { case ACCOUNT_MUTE_SUCCESS: return action.relationship.muting_notifications ? filterNotifications(state, action.relationship) : state; case NOTIFICATIONS_CLEAR: - return state.set('items', ImmutableList()).set('hasMore', false); + return state.set('items', ImmutableOrderedMap()).set('hasMore', false); case NOTIFICATIONS_MARK_READ_REQUEST: return state.set('lastRead', action.lastRead); case TIMELINE_DELETE: return deleteByStatus(state, action.id); - case TIMELINE_DISCONNECT: - return action.timeline === 'home' ? - state.update('items', items => items.first() ? items.unshift(null) : items) : - state; + + // Disable for now + // https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/432 + // + // case TIMELINE_DISCONNECT: + // // This is kind of a hack - `null` renders a LoadGap in the component + // // https://github.com/tootsuite/mastodon/pull/6886 + // return action.timeline === 'home' ? + // state.update('items', items => items.first() ? ImmutableOrderedSet([null]).union(items) : items) : + // state; default: return state; } diff --git a/app/styles/application.scss b/app/styles/application.scss index fdce329ad..cb171dffd 100644 --- a/app/styles/application.scss +++ b/app/styles/application.scss @@ -76,3 +76,6 @@ @import 'components/profile_hover_card'; @import 'components/filters'; @import 'components/mfa_form'; + +// Holiday +@import 'holiday/halloween'; diff --git a/app/styles/chats.scss b/app/styles/chats.scss index 3e2fc2e24..e6fc5d601 100644 --- a/app/styles/chats.scss +++ b/app/styles/chats.scss @@ -94,6 +94,41 @@ overflow: hidden; } } + + .audio-toggle .react-toggle-thumb { + height: 14px; + width: 14px; + border: 1px solid var(--brand-color--med); + } + + .audio-toggle .react-toggle { + height: 16px; + top: 4px; + } + + .audio-toggle .react-toggle-track { + height: 16px; + width: 34px; + background-color: var(--accent-color); + } + + .audio-toggle .react-toggle-track-check { + left: 4px; + bottom: 4px; + } + + .react-toggle--checked .react-toggle-thumb { + left: 19px; + } + + .audio-toggle .react-toggle-track-x { + right: 4px; + bottom: 4px; + } + + .fa { + font-size: 14px; + } } .chat-messages { @@ -111,14 +146,23 @@ max-width: 70%; border-radius: 10px; background-color: var(--background-color); - overflow: hidden; text-overflow: ellipsis; overflow-wrap: break-word; white-space: break-spaces; + position: relative; a { color: var(--brand-color--hicontrast); } + + &:hover, + &:focus, + &:active, { + .chat-message__menu { + opacity: 1; + pointer-events: all; + } + } } &--me .chat-message__bubble { @@ -129,6 +173,17 @@ &--pending .chat-message__bubble { opacity: 0.5; } + + &__menu { + position: absolute; + top: -8px; + right: -8px; + background: var(--background-color); + border-radius: 999px; + opacity: 0; + pointer-events: none; + transition: 0.2s; + } } .chat-list { @@ -152,6 +207,10 @@ .display-name { display: flex; + .hover-ref-wrapper { + display: flex; + } + bdi { overflow: hidden; text-overflow: ellipsis; @@ -274,7 +333,38 @@ border-radius: 0 0 10px 10px; &__actions textarea { - padding: 10px; + padding: 10px 40px 10px 10px; + } + } + } + + @media(max-width: 630px) { + .columns-area__panels__main .columns-area { + padding: 0; + } + + .columns-area__panels__main { + padding: 0; + max-width: none; + } + + .columns-area--mobile .column { + border-radius: 0; + } + + .page { + .chat-box { + border-radius: 0; + border: 2px solid var(--foreground-color); + + &__actions { + padding: 0; + + textarea { + height: 4em; + border-radius: 0; + } + } } } } @@ -297,6 +387,7 @@ margin-left: auto; padding-right: 15px; overflow: hidden; + text-decoration: none; .account__avatar { margin-right: 7px; @@ -368,3 +459,11 @@ object-fit: contain; } } + +.chat-messages__divider { + text-align: center; + text-transform: uppercase; + font-size: 13px; + padding: 14px 0 2px; + opacity: 0.8; +} diff --git a/app/styles/components/columns.scss b/app/styles/components/columns.scss index 10e46e441..9dd1a58ea 100644 --- a/app/styles/components/columns.scss +++ b/app/styles/components/columns.scss @@ -703,3 +703,16 @@ .column-link--transparent .icon-with-badge__badge { border-color: var(--background-color); } + +.column__switch .audio-toggle { + position: absolute; + z-index: 4; + top: 12px; + right: 14px; + + .react-toggle-track-check, + .react-toggle-track-x { + height: 16px; + color: white; + } +} diff --git a/app/styles/components/profile_hover_card.scss b/app/styles/components/profile_hover_card.scss index b3fb4fc31..b5b9da716 100644 --- a/app/styles/components/profile_hover_card.scss +++ b/app/styles/components/profile_hover_card.scss @@ -105,16 +105,3 @@ } } } - -.detailed-status { - .profile-hover-card { - top: 0; - left: 0; - } -} - -/* Hide the popper when the reference is hidden */ -#popper[data-popper-reference-hidden] { - visibility: hidden; - pointer-events: none; -} diff --git a/app/styles/components/sidebar-menu.scss b/app/styles/components/sidebar-menu.scss index e6b4d15a5..2207a2aed 100644 --- a/app/styles/components/sidebar-menu.scss +++ b/app/styles/components/sidebar-menu.scss @@ -3,7 +3,6 @@ position: fixed; flex-direction: column; width: 275px; - height: 100vh; top: 0; bottom: 0; left: 0; @@ -30,12 +29,10 @@ } &__content { - display: flex; - flex: 1 1; - flex-direction: column; - padding-bottom: 40px; overflow-y: scroll; - -webkit-overflow-scrolling: touch; + overflow: auto; + height: 100%; + width: 100%; } &__section { diff --git a/app/styles/holiday/halloween.scss b/app/styles/holiday/halloween.scss new file mode 100644 index 000000000..8f04e74c1 --- /dev/null +++ b/app/styles/holiday/halloween.scss @@ -0,0 +1,158 @@ +body.halloween { + // Set brand color to orange + --brand-color_h: 29.727272727272727; + --brand-color_s: 100%; + --brand-color_l: 43.13725490196079%; + + // Stars BG + background-color: #904700; // Color matches twinkle.svg + background-image: url('../images/halloween/starfield.png'); + background-size: cover; + background-attachment: fixed; + background-position: center; + + // Full-screen pseudo-elements to hold BG graphics + &::before, + &::after, + .app-holder::before, + .app-holder::after { + content: ''; + display: block; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-size: cover; + background-position: center; + width: 100%; + height: 100%; + z-index: -100; + } + + // Spiderweb BG + &::before { + background-image: url('../images/halloween/spiderweb.svg'); + } + + // Twinkle effect by masking with semi-transparent animated circles + &::after { + z-index: -101; + background: transparent url("../images/halloween/twinkle.svg") repeat top center; + animation: halloween-twinkle 200s linear infinite; + } + + .app-holder { + // Black vignette + &::before { + background-image: radial-gradient( + circle, + transparent 0%, + transparent 60%, + #000 100% + ); + } + + // Floating clouds BG + &::after { + background: transparent url("../images/halloween/clouds.png") repeat top center; + animation: halloween-clouds 200s linear infinite; + } + } + + // Dangling spider + .ui .page__top::after, + .ui .page__columns::after { + content: ''; + display: block; + width: 100px; + height: 100px; + right: 20px; + background-image: url('../images/halloween/spider.svg'); + background-size: contain; + background-repeat: no-repeat; + background-position: top right; + z-index: -1; + pointer-events: none; + } + + .ui .page__columns::after { + position: fixed; + top: 50px; + } + + .ui .page__top::after { + position: absolute; + bottom: -100px; + } + + .ui .page__top + .page__columns::after { + display: none; + } + + // Witch emblem + .getting-started__footer::before { + content: ''; + display: block; + background-image: url('../images/halloween/halloween-emblem.svg'); + background-size: contain; + background-position: left; + background-repeat: no-repeat; + width: 100%; + height: 100px; + margin-bottom: 20px; + } + + // Color fixes + // Elements directly over the BG need static colors that don't change + // regardless of the theme-mode + .getting-started__footer { + color: #fff; + + a { + color: hsla(0, 0%, 100%, 0.4); + } + + p { + color: hsla(0, 0%, 100%, 0.8); + } + } + + .profile-info-panel { + color: #fff; + + &-content__name h1 { + span:first-of-type { + color: hsla(0, 0%, 100%, 0.6); + } + + small { + color: #fff; + } + } + + &-content__bio { + color: #fff; + } + + &-content__bio a, + &-content__fields a { + color: hsl( + var(--brand-color_h), + var(--brand-color_s), + calc(var(--brand-color_l) + 8%) + ); + } + } +} + +// Animations +@keyframes halloween-twinkle { + from { background-position: 0 0; } + to { background-position: -10000px 5000px; } +} + +@keyframes halloween-clouds { + from { background-position: 0 0; } + to { background-position: 10000px 0; } +} diff --git a/docs/administration/install-yunohost.md b/docs/administration/install-yunohost.md new file mode 100644 index 000000000..d6b64f68c --- /dev/null +++ b/docs/administration/install-yunohost.md @@ -0,0 +1,25 @@ +# Installing Soapbox FE via YunoHost + +If you want to install Soapbox FE to a Pleroma instance installed using [YunoHost](https://yunohost.org), you can do so by following these steps. + +## 1. Download the build + +First, download the latest build of Soapbox FE from GitLab. + +```sh +curl -L https://gitlab.com/soapbox-pub/soapbox-fe/-/jobs/artifacts/v1.0.0/download?job=build-production -o soapbox-fe.zip +``` + +## 2. Unzip the build + +Then, unzip the build to the Pleroma directory under YunoHost's directory: + +```sh +busybox unzip soapbox-fe.zip -o -d /home/yunohost.app/pleroma/ +``` + +**That's it! 🎉 Soapbox FE is installed.** The change will take effect immediately, just refresh your browser tab. It's not necessary to restart the Pleroma service. + +--- + +Thank you to [@jeroen@social.franssen.xyz](https://social.franssen.xyz/@jeroen) for discovering this method. diff --git a/renovate.json b/renovate.json new file mode 100644 index 000000000..f45d8f110 --- /dev/null +++ b/renovate.json @@ -0,0 +1,5 @@ +{ + "extends": [ + "config:base" + ] +} diff --git a/static/sounds/chat.mp3 b/static/sounds/chat.mp3 new file mode 100644 index 000000000..c86114292 Binary files /dev/null and b/static/sounds/chat.mp3 differ diff --git a/static/sounds/chat.oga b/static/sounds/chat.oga new file mode 100644 index 000000000..76d4bda7a Binary files /dev/null and b/static/sounds/chat.oga differ