commit
0d0d12489e
@ -0,0 +1,5 @@
|
|||||||
|
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository
|
||||||
|
|
||||||
|
github: soapbox-pub
|
||||||
|
liberapay: soapbox
|
||||||
|
custom: "https://soapbox.pub/donate/"
|
@ -1 +1 @@
|
|||||||
nodejs 14.17.6
|
nodejs 16.14.2
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import loadPolyfills from './soapbox/load_polyfills';
|
import loadPolyfills from './soapbox/load_polyfills';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
require.context('./images/', true);
|
require.context('./images/', true);
|
||||||
|
|
||||||
// Load stylesheet
|
// Load stylesheet
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,290 @@
|
|||||||
|
{
|
||||||
|
"account": {
|
||||||
|
"acct": "Hollahollara@spinster.xyz",
|
||||||
|
"avatar": "https://gleasonator.com/proxy/LArKQiIrW265rGIJGwdgX7rRsao/aHR0cHM6Ly9tZWRpYS5zcGluc3Rlci54eXovYWNjb3VudHMvYXZhdGFycy8wMDAvMTQxLzI5NC9vcmlnaW5hbC9lNjA1NjljMjBjNGY3ODNjLnBuZw/e60569c20c4f783c.png",
|
||||||
|
"avatar_static": "https://gleasonator.com/proxy/LArKQiIrW265rGIJGwdgX7rRsao/aHR0cHM6Ly9tZWRpYS5zcGluc3Rlci54eXovYWNjb3VudHMvYXZhdGFycy8wMDAvMTQxLzI5NC9vcmlnaW5hbC9lNjA1NjljMjBjNGY3ODNjLnBuZw/e60569c20c4f783c.png",
|
||||||
|
"bot": false,
|
||||||
|
"created_at": "2020-05-29T03:15:59.000Z",
|
||||||
|
"display_name": "Hollahollara",
|
||||||
|
"emojis": [],
|
||||||
|
"fields": [],
|
||||||
|
"followers_count": 0,
|
||||||
|
"following_count": 0,
|
||||||
|
"fqn": "Hollahollara@spinster.xyz",
|
||||||
|
"header": "https://gleasonator.com/proxy/XSANC57uDBL3tM0LBLEer7yMyaA/aHR0cHM6Ly9tZWRpYS5zcGluc3Rlci54eXovYWNjb3VudHMvaGVhZGVycy8wMDAvMTQxLzI5NC9vcmlnaW5hbC84NTMzMWEzMjJkMTIyN2Q0LnBuZw/85331a322d1227d4.png",
|
||||||
|
"header_static": "https://gleasonator.com/proxy/XSANC57uDBL3tM0LBLEer7yMyaA/aHR0cHM6Ly9tZWRpYS5zcGluc3Rlci54eXovYWNjb3VudHMvaGVhZGVycy8wMDAvMTQxLzI5NC9vcmlnaW5hbC84NTMzMWEzMjJkMTIyN2Q0LnBuZw/85331a322d1227d4.png",
|
||||||
|
"id": "9vWfJdLwuJSyJXqCeG",
|
||||||
|
"last_status_at": "2022-04-16T20:33:32",
|
||||||
|
"locked": true,
|
||||||
|
"note": "Adult human female. Artist. Evil terv. Millennial, killing all the things. Public Universal Friend.<br/><br/><a href=\"http://www.jenniferaldridge.com\">www.jenniferaldridge.com</a><br/><br/><br/>",
|
||||||
|
"pleroma": {
|
||||||
|
"accepts_chat_messages": true,
|
||||||
|
"also_known_as": [],
|
||||||
|
"ap_id": "https://spinster.xyz/users/Hollahollara",
|
||||||
|
"background_image": null,
|
||||||
|
"deactivated": false,
|
||||||
|
"favicon": "https://gleasonator.com/proxy/owo6QgsHm_0ogz5enHyvD68wDUA/aHR0cHM6Ly9zcGluc3Rlci54eXovZmF2aWNvbi5wbmc/favicon.png",
|
||||||
|
"hide_favorites": true,
|
||||||
|
"hide_followers": true,
|
||||||
|
"hide_followers_count": false,
|
||||||
|
"hide_follows": true,
|
||||||
|
"hide_follows_count": false,
|
||||||
|
"is_admin": false,
|
||||||
|
"is_confirmed": true,
|
||||||
|
"is_moderator": false,
|
||||||
|
"is_suggested": false,
|
||||||
|
"location": null,
|
||||||
|
"relationship": {},
|
||||||
|
"skip_thread_containment": false,
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"fields": [],
|
||||||
|
"note": "",
|
||||||
|
"pleroma": {
|
||||||
|
"actor_type": "Person",
|
||||||
|
"discoverable": false
|
||||||
|
},
|
||||||
|
"sensitive": false
|
||||||
|
},
|
||||||
|
"statuses_count": 7191,
|
||||||
|
"url": "https://spinster.xyz/users/Hollahollara",
|
||||||
|
"username": "Hollahollara"
|
||||||
|
},
|
||||||
|
"created_at": "2022-04-14T20:36:52.000Z",
|
||||||
|
"id": "427825",
|
||||||
|
"pleroma": {
|
||||||
|
"is_muted": false,
|
||||||
|
"is_seen": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"account": {
|
||||||
|
"acct": "alex",
|
||||||
|
"avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png",
|
||||||
|
"avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png",
|
||||||
|
"bot": false,
|
||||||
|
"created_at": "2020-01-08T01:25:43.000Z",
|
||||||
|
"display_name": "Alex Gleason",
|
||||||
|
"emojis": [],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "Website",
|
||||||
|
"value": "<a href=\"https://alexgleason.me\" rel=\"ugc\">https://alexgleason.me</a>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Soapbox",
|
||||||
|
"value": "<a href=\"https://soapbox.pub\" rel=\"ugc\">https://soapbox.pub</a>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Email",
|
||||||
|
"value": "alex@alexgleason.me"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gender identity",
|
||||||
|
"value": "Soyboy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Donate (PayPal)",
|
||||||
|
"value": "<a href=\"https://paypal.me/gleasonator\" rel=\"ugc\">https://paypal.me/gleasonator</a>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "$BTC",
|
||||||
|
"value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "$ETH",
|
||||||
|
"value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "$DOGE",
|
||||||
|
"value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "$XMR",
|
||||||
|
"value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"follow_requests_count": 0,
|
||||||
|
"followers_count": 2602,
|
||||||
|
"following_count": 1603,
|
||||||
|
"fqn": "alex@gleasonator.com",
|
||||||
|
"header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
|
||||||
|
"header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
|
||||||
|
"id": "9v5bmRalQvjOy0ECcC",
|
||||||
|
"last_status_at": "2022-04-16T19:23:50",
|
||||||
|
"locked": false,
|
||||||
|
"note": "I create Fediverse software that empowers people online.<br/><br/>I'm vegan btw<br/><br/>Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.",
|
||||||
|
"pleroma": {
|
||||||
|
"accepts_chat_messages": true,
|
||||||
|
"accepts_email_list": true,
|
||||||
|
"allow_following_move": true,
|
||||||
|
"also_known_as": [
|
||||||
|
"https://mitra.social/users/alex"
|
||||||
|
],
|
||||||
|
"ap_id": "https://gleasonator.com/users/alex",
|
||||||
|
"background_image": null,
|
||||||
|
"birthday": "1993-07-03",
|
||||||
|
"deactivated": false,
|
||||||
|
"email": "alex@alexgleason.me",
|
||||||
|
"favicon": "https://gleasonator.com/favicon.png",
|
||||||
|
"hide_favorites": true,
|
||||||
|
"hide_followers": false,
|
||||||
|
"hide_followers_count": false,
|
||||||
|
"hide_follows": false,
|
||||||
|
"hide_follows_count": false,
|
||||||
|
"is_admin": true,
|
||||||
|
"is_confirmed": true,
|
||||||
|
"is_moderator": false,
|
||||||
|
"is_suggested": true,
|
||||||
|
"location": "Texas",
|
||||||
|
"notification_settings": {
|
||||||
|
"block_from_strangers": false,
|
||||||
|
"hide_notification_contents": false
|
||||||
|
},
|
||||||
|
"relationship": {},
|
||||||
|
"skip_thread_containment": false,
|
||||||
|
"tags": [],
|
||||||
|
"unread_conversation_count": 392,
|
||||||
|
"unread_notifications_count": 2
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "Website",
|
||||||
|
"value": "https://alexgleason.me"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Soapbox",
|
||||||
|
"value": "https://soapbox.pub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Email",
|
||||||
|
"value": "alex@alexgleason.me"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gender identity",
|
||||||
|
"value": "Soyboy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Donate (PayPal)",
|
||||||
|
"value": "https://paypal.me/gleasonator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "$BTC",
|
||||||
|
"value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "$ETH",
|
||||||
|
"value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "$DOGE",
|
||||||
|
"value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "$XMR",
|
||||||
|
"value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.",
|
||||||
|
"pleroma": {
|
||||||
|
"actor_type": "Person",
|
||||||
|
"discoverable": false,
|
||||||
|
"no_rich_text": false,
|
||||||
|
"show_birthday": true,
|
||||||
|
"show_role": true
|
||||||
|
},
|
||||||
|
"privacy": "public",
|
||||||
|
"sensitive": false
|
||||||
|
},
|
||||||
|
"statuses_count": 24050,
|
||||||
|
"url": "https://gleasonator.com/users/alex",
|
||||||
|
"username": "alex"
|
||||||
|
},
|
||||||
|
"application": {
|
||||||
|
"name": "Soapbox FE",
|
||||||
|
"website": "https://soapbox.pub/"
|
||||||
|
},
|
||||||
|
"bookmarked": false,
|
||||||
|
"card": null,
|
||||||
|
"content": "",
|
||||||
|
"created_at": "2022-04-12T01:31:00.000Z",
|
||||||
|
"emojis": [],
|
||||||
|
"favourited": false,
|
||||||
|
"favourites_count": 11,
|
||||||
|
"id": "AIMEslRcKrcu02D3HU",
|
||||||
|
"in_reply_to_account_id": null,
|
||||||
|
"in_reply_to_id": null,
|
||||||
|
"language": null,
|
||||||
|
"media_attachments": [
|
||||||
|
{
|
||||||
|
"blurhash": "etMZzVWq%1%1o#_NayWCofae_Ns:R*kDjYS5a{jYoJj]V@a}WBbGof",
|
||||||
|
"description": "",
|
||||||
|
"id": "AIMEqtBeZtvpQvqfIG",
|
||||||
|
"meta": {
|
||||||
|
"original": {
|
||||||
|
"aspect": 0.9726443768996961,
|
||||||
|
"height": 658,
|
||||||
|
"width": 640
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pleroma": {
|
||||||
|
"mime_type": "image/jpeg"
|
||||||
|
},
|
||||||
|
"preview_url": "https://media.gleasonator.com/6c0a1d878b7c9d1d737f415645cf34cdacdf6438c468348f4fa7534a15798023.jpg",
|
||||||
|
"remote_url": "https://media.gleasonator.com/6c0a1d878b7c9d1d737f415645cf34cdacdf6438c468348f4fa7534a15798023.jpg",
|
||||||
|
"text_url": "https://media.gleasonator.com/6c0a1d878b7c9d1d737f415645cf34cdacdf6438c468348f4fa7534a15798023.jpg",
|
||||||
|
"type": "image",
|
||||||
|
"url": "https://media.gleasonator.com/6c0a1d878b7c9d1d737f415645cf34cdacdf6438c468348f4fa7534a15798023.jpg"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mentions": [],
|
||||||
|
"muted": false,
|
||||||
|
"pinned": false,
|
||||||
|
"pleroma": {
|
||||||
|
"content": {
|
||||||
|
"text/plain": ""
|
||||||
|
},
|
||||||
|
"content_type": null,
|
||||||
|
"conversation_id": "AIMEslPqRSCzuXNdWC",
|
||||||
|
"direct_conversation_id": null,
|
||||||
|
"emoji_reactions": [
|
||||||
|
{
|
||||||
|
"count": 4,
|
||||||
|
"me": false,
|
||||||
|
"name": "😆"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"me": false,
|
||||||
|
"name": "🤢"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expires_at": null,
|
||||||
|
"in_reply_to_account_acct": null,
|
||||||
|
"local": true,
|
||||||
|
"parent_visible": false,
|
||||||
|
"pinned_at": null,
|
||||||
|
"quote": null,
|
||||||
|
"quote_url": null,
|
||||||
|
"quote_visible": false,
|
||||||
|
"spoiler_text": {
|
||||||
|
"text/plain": ""
|
||||||
|
},
|
||||||
|
"thread_muted": false
|
||||||
|
},
|
||||||
|
"poll": null,
|
||||||
|
"reblog": null,
|
||||||
|
"reblogged": false,
|
||||||
|
"reblogs_count": 4,
|
||||||
|
"replies_count": 0,
|
||||||
|
"sensitive": false,
|
||||||
|
"spoiler_text": "",
|
||||||
|
"tags": [],
|
||||||
|
"text": null,
|
||||||
|
"uri": "https://gleasonator.com/objects/7953f9fb-d3d7-4f50-b9d8-27e311ac1f5e",
|
||||||
|
"url": "https://gleasonator.com/notice/AIMEslRcKrcu02D3HU",
|
||||||
|
"visibility": "public"
|
||||||
|
},
|
||||||
|
"type": "favourite"
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"account": {
|
||||||
|
"acct": "neko@rdrama.cc",
|
||||||
|
"avatar": "https://gleasonator.com/proxy/QJ3einzsXdobgWPsyZowxnor1zY/aHR0cHM6Ly9yZHJhbWEuY2MvbWVkaWEvODcyNDhjYjctZWYwNC00ZThjLWEwYzEtNTYxNWMyNWM0MTk1L2Jsb2I/blob",
|
||||||
|
"avatar_static": "https://gleasonator.com/proxy/QJ3einzsXdobgWPsyZowxnor1zY/aHR0cHM6Ly9yZHJhbWEuY2MvbWVkaWEvODcyNDhjYjctZWYwNC00ZThjLWEwYzEtNTYxNWMyNWM0MTk1L2Jsb2I/blob",
|
||||||
|
"bot": false,
|
||||||
|
"created_at": "2022-04-16T20:23:16.000Z",
|
||||||
|
"display_name": "Nekobit",
|
||||||
|
"emojis": [],
|
||||||
|
"fields": [],
|
||||||
|
"followers_count": 19,
|
||||||
|
"following_count": 357,
|
||||||
|
"fqn": "neko@rdrama.cc",
|
||||||
|
"header": "https://gleasonator.com/proxy/ojpBSVKfePvLnb7pwqepQspzIko/aHR0cHM6Ly9yZHJhbWEuY2MvbWVkaWEvNjBkMTJjOWYtOTNkNi00ODBmLThhMGUtMTE3M2ZkNjg5MzhmL3dhbGxwYXBlcmZsYXJlLmNvbV93YWxscGFwZXItd2ViLmpwZw/wallpaperflare.com_wallpaper-web.jpg",
|
||||||
|
"header_static": "https://gleasonator.com/proxy/ojpBSVKfePvLnb7pwqepQspzIko/aHR0cHM6Ly9yZHJhbWEuY2MvbWVkaWEvNjBkMTJjOWYtOTNkNi00ODBmLThhMGUtMTE3M2ZkNjg5MzhmL3dhbGxwYXBlcmZsYXJlLmNvbV93YWxscGFwZXItd2ViLmpwZw/wallpaperflare.com_wallpaper-web.jpg",
|
||||||
|
"id": "AIW9zGESDwdT27vk0W",
|
||||||
|
"last_status_at": "2022-04-16T21:49:29",
|
||||||
|
"locked": false,
|
||||||
|
"note": "New instance, hello!<br/><br/>Please follow if you followed my <a href=\"http://desuposter.club\">desuposter.club</a> alt",
|
||||||
|
"pleroma": {
|
||||||
|
"accepts_chat_messages": true,
|
||||||
|
"also_known_as": [],
|
||||||
|
"ap_id": "https://rdrama.cc/users/neko",
|
||||||
|
"background_image": null,
|
||||||
|
"deactivated": false,
|
||||||
|
"favicon": "https://gleasonator.com/proxy/dbCdmChqVRi0vjYTCpRj5lDLtNM/aHR0cHM6Ly9yZHJhbWEuY2MvZmF2aWNvbi5wbmc/favicon.png",
|
||||||
|
"hide_favorites": true,
|
||||||
|
"hide_followers": false,
|
||||||
|
"hide_followers_count": false,
|
||||||
|
"hide_follows": false,
|
||||||
|
"hide_follows_count": false,
|
||||||
|
"is_admin": false,
|
||||||
|
"is_confirmed": true,
|
||||||
|
"is_moderator": false,
|
||||||
|
"is_suggested": false,
|
||||||
|
"location": null,
|
||||||
|
"relationship": {},
|
||||||
|
"skip_thread_containment": false,
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"fields": [],
|
||||||
|
"note": "",
|
||||||
|
"pleroma": {
|
||||||
|
"actor_type": "Person",
|
||||||
|
"discoverable": false
|
||||||
|
},
|
||||||
|
"sensitive": false
|
||||||
|
},
|
||||||
|
"statuses_count": 6,
|
||||||
|
"url": "https://rdrama.cc/users/neko",
|
||||||
|
"username": "neko"
|
||||||
|
},
|
||||||
|
"created_at": "2022-04-16T20:24:03.000Z",
|
||||||
|
"id": "429280",
|
||||||
|
"pleroma": {
|
||||||
|
"is_muted": false,
|
||||||
|
"is_seen": true
|
||||||
|
},
|
||||||
|
"type": "follow"
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"account": {
|
||||||
|
"acct": "alex@spinster.xyz",
|
||||||
|
"avatar": "https://gleasonator.com/images/avi.png",
|
||||||
|
"avatar_static": "https://gleasonator.com/images/avi.png",
|
||||||
|
"bot": false,
|
||||||
|
"created_at": "2020-01-08T03:08:22.000Z",
|
||||||
|
"display_name": "**MOVED**",
|
||||||
|
"emojis": [],
|
||||||
|
"fields": [],
|
||||||
|
"followers_count": 1005,
|
||||||
|
"following_count": 724,
|
||||||
|
"fqn": "alex@spinster.xyz",
|
||||||
|
"header": "https://gleasonator.com/proxy/yxa7ucolLFAsmBHYJzksSh_zoao/aHR0cHM6Ly9tZWRpYS5zcGluc3Rlci54eXovYWNjb3VudHMvaGVhZGVycy8wMDAvMDAwLzAwMS9vcmlnaW5hbC83ZmE4MWY5ZmZiYWVjZDk3LnBuZw/7fa81f9ffbaecd97.png",
|
||||||
|
"header_static": "https://gleasonator.com/proxy/yxa7ucolLFAsmBHYJzksSh_zoao/aHR0cHM6Ly9tZWRpYS5zcGluc3Rlci54eXovYWNjb3VudHMvaGVhZGVycy8wMDAvMDAwLzAwMS9vcmlnaW5hbC83ZmE4MWY5ZmZiYWVjZDk3LnBuZw/7fa81f9ffbaecd97.png",
|
||||||
|
"id": "9v5bmXkCYkqU30gp9s",
|
||||||
|
"last_status_at": null,
|
||||||
|
"locked": true,
|
||||||
|
"note": "Moved to <a href=\"https://spinster.xyz/@alex@gleasonator.com\">https://spinster.xyz/@alex@gleasonator.com</a>",
|
||||||
|
"pleroma": {
|
||||||
|
"accepts_chat_messages": true,
|
||||||
|
"also_known_as": [],
|
||||||
|
"ap_id": "https://spinster.xyz/users/alex",
|
||||||
|
"background_image": null,
|
||||||
|
"deactivated": false,
|
||||||
|
"favicon": "https://gleasonator.com/proxy/owo6QgsHm_0ogz5enHyvD68wDUA/aHR0cHM6Ly9zcGluc3Rlci54eXovZmF2aWNvbi5wbmc/favicon.png",
|
||||||
|
"hide_favorites": true,
|
||||||
|
"hide_followers": false,
|
||||||
|
"hide_followers_count": false,
|
||||||
|
"hide_follows": false,
|
||||||
|
"hide_follows_count": false,
|
||||||
|
"is_admin": false,
|
||||||
|
"is_confirmed": false,
|
||||||
|
"is_moderator": false,
|
||||||
|
"is_suggested": false,
|
||||||
|
"location": null,
|
||||||
|
"relationship": {},
|
||||||
|
"skip_thread_containment": false,
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"fields": [],
|
||||||
|
"note": "",
|
||||||
|
"pleroma": {
|
||||||
|
"actor_type": "Person",
|
||||||
|
"discoverable": false
|
||||||
|
},
|
||||||
|
"sensitive": false
|
||||||
|
},
|
||||||
|
"statuses_count": 2687,
|
||||||
|
"url": "https://spinster.xyz/users/alex",
|
||||||
|
"username": "alex"
|
||||||
|
},
|
||||||
|
"created_at": "2020-12-30T02:23:35.000Z",
|
||||||
|
"id": "87967",
|
||||||
|
"pleroma": {
|
||||||
|
"is_muted": false,
|
||||||
|
"is_seen": true
|
||||||
|
},
|
||||||
|
"type": "follow_request"
|
||||||
|
}
|
@ -0,0 +1,226 @@
|
|||||||
|
{
|
||||||
|
"account": {
|
||||||
|
"acct": "silverpill@mitra.social",
|
||||||
|
"avatar": "https://gleasonator.com/proxy/ZbLqy9s8Hxn9I5K23y2mffsL6iY/aHR0cHM6Ly9taXRyYS5zb2NpYWwvbWVkaWEvNmE3ODViZjdkZDA1ZjYxYzM1OTBlODkzNWFhNDkxNTZhNDk5YWMzMGZkMWU0MDJmNzllN2UxNjRhZGIzNmUyYy5wbmc/6a785bf7dd05f61c3590e8935aa49156a499ac30fd1e402f79e7e164adb36e2c.png",
|
||||||
|
"avatar_static": "https://gleasonator.com/proxy/ZbLqy9s8Hxn9I5K23y2mffsL6iY/aHR0cHM6Ly9taXRyYS5zb2NpYWwvbWVkaWEvNmE3ODViZjdkZDA1ZjYxYzM1OTBlODkzNWFhNDkxNTZhNDk5YWMzMGZkMWU0MDJmNzllN2UxNjRhZGIzNmUyYy5wbmc/6a785bf7dd05f61c3590e8935aa49156a499ac30fd1e402f79e7e164adb36e2c.png",
|
||||||
|
"bot": false,
|
||||||
|
"created_at": "2021-11-11T22:31:51.000Z",
|
||||||
|
"display_name": "silverpill",
|
||||||
|
"emojis": [],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "Matrix",
|
||||||
|
"value": "@silverpill:poa.st"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Alt",
|
||||||
|
"value": "@silverpill@poa.st"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Code",
|
||||||
|
"value": "<a href=\"https://codeberg.org/silverpill/\">https://codeberg.org/silverpill/</a>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "$XMR",
|
||||||
|
"value": "884y9LmsWY7PQNsyR7bJy1dvj91tuF5spVabyCnPk4KfQtSuzFbQobTFC7xSemJgVW1FWAwnJbjTZX5zZWbBrfkv62DB62d"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"followers_count": 0,
|
||||||
|
"following_count": 0,
|
||||||
|
"fqn": "silverpill@mitra.social",
|
||||||
|
"header": "https://gleasonator.com/images/banner.png",
|
||||||
|
"header_static": "https://gleasonator.com/images/banner.png",
|
||||||
|
"id": "ADIzJ7q9gExPvDKBCS",
|
||||||
|
"last_status_at": "2022-04-15T11:27:33",
|
||||||
|
"locked": false,
|
||||||
|
"note": "",
|
||||||
|
"pleroma": {
|
||||||
|
"accepts_chat_messages": false,
|
||||||
|
"also_known_as": [],
|
||||||
|
"ap_id": "https://mitra.social/users/silverpill",
|
||||||
|
"background_image": null,
|
||||||
|
"deactivated": false,
|
||||||
|
"favicon": "https://gleasonator.com/proxy/XSE9_kQbQyYcSFWszWx2GgCbBuY/aHR0cHM6Ly9taXRyYS5zb2NpYWwvZmF2aWNvbi5pY28/favicon.ico",
|
||||||
|
"hide_favorites": true,
|
||||||
|
"hide_followers": true,
|
||||||
|
"hide_followers_count": false,
|
||||||
|
"hide_follows": true,
|
||||||
|
"hide_follows_count": false,
|
||||||
|
"is_admin": false,
|
||||||
|
"is_confirmed": true,
|
||||||
|
"is_moderator": false,
|
||||||
|
"is_suggested": false,
|
||||||
|
"location": null,
|
||||||
|
"relationship": {},
|
||||||
|
"skip_thread_containment": false,
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"fields": [],
|
||||||
|
"note": "",
|
||||||
|
"pleroma": {
|
||||||
|
"actor_type": "Person",
|
||||||
|
"discoverable": false
|
||||||
|
},
|
||||||
|
"sensitive": false
|
||||||
|
},
|
||||||
|
"statuses_count": 135,
|
||||||
|
"url": "https://mitra.social/users/silverpill",
|
||||||
|
"username": "silverpill"
|
||||||
|
},
|
||||||
|
"created_at": "2022-04-15T11:27:33.000Z",
|
||||||
|
"id": "428172",
|
||||||
|
"pleroma": {
|
||||||
|
"is_muted": false,
|
||||||
|
"is_seen": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"account": {
|
||||||
|
"acct": "silverpill@mitra.social",
|
||||||
|
"avatar": "https://gleasonator.com/proxy/ZbLqy9s8Hxn9I5K23y2mffsL6iY/aHR0cHM6Ly9taXRyYS5zb2NpYWwvbWVkaWEvNmE3ODViZjdkZDA1ZjYxYzM1OTBlODkzNWFhNDkxNTZhNDk5YWMzMGZkMWU0MDJmNzllN2UxNjRhZGIzNmUyYy5wbmc/6a785bf7dd05f61c3590e8935aa49156a499ac30fd1e402f79e7e164adb36e2c.png",
|
||||||
|
"avatar_static": "https://gleasonator.com/proxy/ZbLqy9s8Hxn9I5K23y2mffsL6iY/aHR0cHM6Ly9taXRyYS5zb2NpYWwvbWVkaWEvNmE3ODViZjdkZDA1ZjYxYzM1OTBlODkzNWFhNDkxNTZhNDk5YWMzMGZkMWU0MDJmNzllN2UxNjRhZGIzNmUyYy5wbmc/6a785bf7dd05f61c3590e8935aa49156a499ac30fd1e402f79e7e164adb36e2c.png",
|
||||||
|
"bot": false,
|
||||||
|
"created_at": "2021-11-11T22:31:51.000Z",
|
||||||
|
"display_name": "silverpill",
|
||||||
|
"emojis": [],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "Matrix",
|
||||||
|
"value": "@silverpill:poa.st"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Alt",
|
||||||
|
"value": "@silverpill@poa.st"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Code",
|
||||||
|
"value": "<a href=\"https://codeberg.org/silverpill/\">https://codeberg.org/silverpill/</a>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "$XMR",
|
||||||
|
"value": "884y9LmsWY7PQNsyR7bJy1dvj91tuF5spVabyCnPk4KfQtSuzFbQobTFC7xSemJgVW1FWAwnJbjTZX5zZWbBrfkv62DB62d"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"followers_count": 0,
|
||||||
|
"following_count": 0,
|
||||||
|
"fqn": "silverpill@mitra.social",
|
||||||
|
"header": "https://gleasonator.com/images/banner.png",
|
||||||
|
"header_static": "https://gleasonator.com/images/banner.png",
|
||||||
|
"id": "ADIzJ7q9gExPvDKBCS",
|
||||||
|
"last_status_at": "2022-04-15T11:27:33",
|
||||||
|
"locked": false,
|
||||||
|
"note": "",
|
||||||
|
"pleroma": {
|
||||||
|
"accepts_chat_messages": false,
|
||||||
|
"also_known_as": [],
|
||||||
|
"ap_id": "https://mitra.social/users/silverpill",
|
||||||
|
"background_image": null,
|
||||||
|
"deactivated": false,
|
||||||
|
"favicon": "https://gleasonator.com/proxy/XSE9_kQbQyYcSFWszWx2GgCbBuY/aHR0cHM6Ly9taXRyYS5zb2NpYWwvZmF2aWNvbi5pY28/favicon.ico",
|
||||||
|
"hide_favorites": true,
|
||||||
|
"hide_followers": true,
|
||||||
|
"hide_followers_count": false,
|
||||||
|
"hide_follows": true,
|
||||||
|
"hide_follows_count": false,
|
||||||
|
"is_admin": false,
|
||||||
|
"is_confirmed": true,
|
||||||
|
"is_moderator": false,
|
||||||
|
"is_suggested": false,
|
||||||
|
"location": null,
|
||||||
|
"relationship": {},
|
||||||
|
"skip_thread_containment": false,
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"fields": [],
|
||||||
|
"note": "",
|
||||||
|
"pleroma": {
|
||||||
|
"actor_type": "Person",
|
||||||
|
"discoverable": false
|
||||||
|
},
|
||||||
|
"sensitive": false
|
||||||
|
},
|
||||||
|
"statuses_count": 135,
|
||||||
|
"url": "https://mitra.social/users/silverpill",
|
||||||
|
"username": "silverpill"
|
||||||
|
},
|
||||||
|
"application": null,
|
||||||
|
"bookmarked": true,
|
||||||
|
"card": {
|
||||||
|
"author_name": "",
|
||||||
|
"author_url": "",
|
||||||
|
"blurhash": null,
|
||||||
|
"description": "The ActivityPub protocol is a decentralized social networking protocol\n based upon the [ActivityStreams] 2.0 data format.\n It provides a client to server API for creating, updating and deleting\n content, as well as a federated server to server API for delivering\n notifications and content.",
|
||||||
|
"embed_url": "",
|
||||||
|
"height": 0,
|
||||||
|
"html": "",
|
||||||
|
"image": null,
|
||||||
|
"provider_name": "www.w3.org",
|
||||||
|
"provider_url": "https://www.w3.org",
|
||||||
|
"title": "ActivityPub",
|
||||||
|
"type": "link",
|
||||||
|
"url": "https://www.w3.org/TR/activitypub/#retrieving-objects",
|
||||||
|
"width": 0
|
||||||
|
},
|
||||||
|
"content": "<span class=\"h-card\"><a class=\"u-url mention\" href=\"https://gleasonator.com/users/alex\">@alex</a></span> <span class=\"h-card\"><a class=\"u-url mention\" href=\"https://lain.com/users/lain\">@lain</a></span> The second one is suggested by ActivityPub spec: <a href=\"https://www.w3.org/TR/activitypub/#retrieving-objects\">https://www.w3.org/TR/activitypub/#retrieving-objects</a><br/>\nThe first one is likely a legacy of earlier ActivityStreams standards, I'm not sure",
|
||||||
|
"created_at": "2022-04-15T11:27:28.000Z",
|
||||||
|
"emojis": [],
|
||||||
|
"favourited": true,
|
||||||
|
"favourites_count": 2,
|
||||||
|
"id": "AITJf9Wpr0msWChNBI",
|
||||||
|
"in_reply_to_account_id": "9v5bmRalQvjOy0ECcC",
|
||||||
|
"in_reply_to_id": "AISPFI5nnPaS7J94rI",
|
||||||
|
"language": null,
|
||||||
|
"media_attachments": [],
|
||||||
|
"mentions": [
|
||||||
|
{
|
||||||
|
"acct": "alex",
|
||||||
|
"id": "9v5bmRalQvjOy0ECcC",
|
||||||
|
"url": "https://gleasonator.com/users/alex",
|
||||||
|
"username": "alex"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"acct": "lain@lain.com",
|
||||||
|
"id": "9v5bqYwY2jfmvPNhTM",
|
||||||
|
"url": "https://lain.com/users/lain",
|
||||||
|
"username": "lain"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muted": false,
|
||||||
|
"pinned": false,
|
||||||
|
"pleroma": {
|
||||||
|
"content": {
|
||||||
|
"text/plain": "@alex @lain The second one is suggested by ActivityPub spec: https://www.w3.org/TR/activitypub/#retrieving-objects\nThe first one is likely a legacy of earlier ActivityStreams standards, I'm not sure"
|
||||||
|
},
|
||||||
|
"content_type": null,
|
||||||
|
"conversation_id": "AISPFI2bzH2DxPeWsy",
|
||||||
|
"direct_conversation_id": null,
|
||||||
|
"emoji_reactions": [],
|
||||||
|
"expires_at": null,
|
||||||
|
"in_reply_to_account_acct": "alex",
|
||||||
|
"local": false,
|
||||||
|
"parent_visible": true,
|
||||||
|
"pinned_at": null,
|
||||||
|
"quote": null,
|
||||||
|
"quote_url": null,
|
||||||
|
"quote_visible": false,
|
||||||
|
"spoiler_text": {
|
||||||
|
"text/plain": ""
|
||||||
|
},
|
||||||
|
"thread_muted": false
|
||||||
|
},
|
||||||
|
"poll": null,
|
||||||
|
"reblog": null,
|
||||||
|
"reblogged": false,
|
||||||
|
"reblogs_count": 0,
|
||||||
|
"replies_count": 0,
|
||||||
|
"sensitive": false,
|
||||||
|
"spoiler_text": "",
|
||||||
|
"tags": [],
|
||||||
|
"text": null,
|
||||||
|
"uri": "https://mitra.social/objects/01802cfa-633c-1c2c-e9cf-e6e0ffef0afe",
|
||||||
|
"url": "https://mitra.social/objects/01802cfa-633c-1c2c-e9cf-e6e0ffef0afe",
|
||||||
|
"visibility": "public"
|
||||||
|
},
|
||||||
|
"type": "mention"
|
||||||
|
}
|
@ -0,0 +1,119 @@
|
|||||||
|
{
|
||||||
|
"account": {
|
||||||
|
"acct": "alex@fedibird.com",
|
||||||
|
"avatar": "https://gleasonator.com/images/avi.png",
|
||||||
|
"avatar_static": "https://gleasonator.com/images/avi.png",
|
||||||
|
"bot": false,
|
||||||
|
"created_at": "2022-01-24T21:25:37.000Z",
|
||||||
|
"display_name": "alex@fedibird.com",
|
||||||
|
"emojis": [],
|
||||||
|
"fields": [],
|
||||||
|
"followers_count": 0,
|
||||||
|
"following_count": 2,
|
||||||
|
"fqn": "alex@fedibird.com",
|
||||||
|
"header": "https://gleasonator.com/images/banner.png",
|
||||||
|
"header_static": "https://gleasonator.com/images/banner.png",
|
||||||
|
"id": "AFmHQ18XZ7Lco68MW8",
|
||||||
|
"last_status_at": "2022-03-16T22:07:53",
|
||||||
|
"locked": false,
|
||||||
|
"note": "<p></p>",
|
||||||
|
"pleroma": {
|
||||||
|
"accepts_chat_messages": null,
|
||||||
|
"also_known_as": [],
|
||||||
|
"ap_id": "https://fedibird.com/users/alex",
|
||||||
|
"background_image": null,
|
||||||
|
"birthday": "1993-07-03",
|
||||||
|
"deactivated": false,
|
||||||
|
"favicon": "https://gleasonator.com/proxy/HzfsidHss3CuA7aM2zxXN-tAjF8/aHR0cHM6Ly9mZWRpYmlyZC5jb20vZmF2aWNvbi5pY28/favicon.ico",
|
||||||
|
"hide_favorites": true,
|
||||||
|
"hide_followers": false,
|
||||||
|
"hide_followers_count": false,
|
||||||
|
"hide_follows": false,
|
||||||
|
"hide_follows_count": false,
|
||||||
|
"is_admin": false,
|
||||||
|
"is_confirmed": true,
|
||||||
|
"is_moderator": false,
|
||||||
|
"is_suggested": false,
|
||||||
|
"location": "Texas",
|
||||||
|
"relationship": {},
|
||||||
|
"skip_thread_containment": false,
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"fields": [],
|
||||||
|
"note": "",
|
||||||
|
"pleroma": {
|
||||||
|
"actor_type": "Person",
|
||||||
|
"discoverable": false
|
||||||
|
},
|
||||||
|
"sensitive": false
|
||||||
|
},
|
||||||
|
"statuses_count": 5,
|
||||||
|
"url": "https://fedibird.com/@alex",
|
||||||
|
"username": "alex"
|
||||||
|
},
|
||||||
|
"created_at": "2022-03-17T00:08:48.000Z",
|
||||||
|
"id": "406814",
|
||||||
|
"pleroma": {
|
||||||
|
"is_muted": false,
|
||||||
|
"is_seen": true
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"acct": "benis911",
|
||||||
|
"avatar": "https://gleasonator.com/images/avi.png",
|
||||||
|
"avatar_static": "https://gleasonator.com/images/avi.png",
|
||||||
|
"bot": false,
|
||||||
|
"created_at": "2021-03-26T20:42:11.000Z",
|
||||||
|
"display_name": "benis911",
|
||||||
|
"emojis": [],
|
||||||
|
"fields": [],
|
||||||
|
"followers_count": 0,
|
||||||
|
"following_count": 0,
|
||||||
|
"fqn": "benis911@gleasonator.com",
|
||||||
|
"header": "https://media.gleasonator.com/fc595bbbcf5aabefecd1c2adfe5b7f5457db59847992881668653a0338ba25bd.jpg",
|
||||||
|
"header_static": "https://media.gleasonator.com/fc595bbbcf5aabefecd1c2adfe5b7f5457db59847992881668653a0338ba25bd.jpg",
|
||||||
|
"id": "A5c5LK7EJTFR0u26Pg",
|
||||||
|
"last_status_at": "2022-03-19T22:33:38",
|
||||||
|
"locked": false,
|
||||||
|
"note": "hello world 2",
|
||||||
|
"pleroma": {
|
||||||
|
"accepts_chat_messages": true,
|
||||||
|
"also_known_as": [
|
||||||
|
"https://gleasonator.com/users/alex",
|
||||||
|
"https://poa.st/users/alex",
|
||||||
|
"https://fedibird.com/users/alex"
|
||||||
|
],
|
||||||
|
"ap_id": "https://gleasonator.com/users/benis911",
|
||||||
|
"background_image": null,
|
||||||
|
"birthday": "2000-01-25",
|
||||||
|
"deactivated": false,
|
||||||
|
"favicon": "https://gleasonator.com/favicon.png",
|
||||||
|
"hide_favorites": true,
|
||||||
|
"hide_followers": true,
|
||||||
|
"hide_followers_count": true,
|
||||||
|
"hide_follows": true,
|
||||||
|
"hide_follows_count": true,
|
||||||
|
"is_admin": false,
|
||||||
|
"is_confirmed": true,
|
||||||
|
"is_moderator": false,
|
||||||
|
"is_suggested": false,
|
||||||
|
"location": null,
|
||||||
|
"relationship": {},
|
||||||
|
"skip_thread_containment": false,
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"fields": [],
|
||||||
|
"note": "hello world 2",
|
||||||
|
"pleroma": {
|
||||||
|
"actor_type": "Person",
|
||||||
|
"discoverable": false
|
||||||
|
},
|
||||||
|
"sensitive": false
|
||||||
|
},
|
||||||
|
"statuses_count": 174,
|
||||||
|
"url": "https://gleasonator.com/users/benis911",
|
||||||
|
"username": "benis911"
|
||||||
|
},
|
||||||
|
"type": "move"
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"account": {
|
||||||
|
"acct": "dave",
|
||||||
|
"avatar": "https://media.gleasonator.com/68c29c30c18f30dd2898f85466bf1670312dda816617e6d31421c7e4c30a8265.png",
|
||||||
|
"avatar_static": "https://media.gleasonator.com/68c29c30c18f30dd2898f85466bf1670312dda816617e6d31421c7e4c30a8265.png",
|
||||||
|
"bot": false,
|
||||||
|
"created_at": "2020-02-01T07:28:46.000Z",
|
||||||
|
"display_name": "Elden Beedle 🇺🇦 🇫🇷",
|
||||||
|
"emojis": [],
|
||||||
|
"fields": [],
|
||||||
|
"followers_count": 490,
|
||||||
|
"following_count": 367,
|
||||||
|
"fqn": "dave@gleasonator.com",
|
||||||
|
"header": "https://media.gleasonator.com/47e8907c322a0e55d12b211846aa27c6b386e947326fe14bb09c89ef7317901d.jpg",
|
||||||
|
"header_static": "https://media.gleasonator.com/47e8907c322a0e55d12b211846aa27c6b386e947326fe14bb09c89ef7317901d.jpg",
|
||||||
|
"id": "9v5c0Pkz3MT5KTfam8",
|
||||||
|
"last_status_at": "2022-04-16T19:57:10",
|
||||||
|
"locked": false,
|
||||||
|
"note": "Beedle is back, baby!<br/><br/>Mostly just crosspost memes and stuff I find on the internet",
|
||||||
|
"pleroma": {
|
||||||
|
"accepts_chat_messages": true,
|
||||||
|
"also_known_as": [],
|
||||||
|
"ap_id": "https://gleasonator.com/users/dave",
|
||||||
|
"background_image": null,
|
||||||
|
"birthday": "1990-01-01",
|
||||||
|
"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": false,
|
||||||
|
"is_confirmed": true,
|
||||||
|
"is_moderator": false,
|
||||||
|
"is_suggested": true,
|
||||||
|
"location": null,
|
||||||
|
"relationship": {},
|
||||||
|
"skip_thread_containment": false,
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"fields": [],
|
||||||
|
"note": "Beedle is back, baby!\r\n\r\nMostly just crosspost memes and stuff I find on the internet",
|
||||||
|
"pleroma": {
|
||||||
|
"actor_type": "Person",
|
||||||
|
"discoverable": false
|
||||||
|
},
|
||||||
|
"sensitive": false
|
||||||
|
},
|
||||||
|
"statuses_count": 16758,
|
||||||
|
"url": "https://gleasonator.com/users/dave",
|
||||||
|
"username": "dave"
|
||||||
|
},
|
||||||
|
"chat_message": {
|
||||||
|
"account_id": "9v5c0Pkz3MT5KTfam8",
|
||||||
|
"attachment": null,
|
||||||
|
"card": null,
|
||||||
|
"chat_id": "9yX4Q9DiC2te6lvk5g",
|
||||||
|
"content": "Cool, it works, I'll keep letting you know when I find broken stuff",
|
||||||
|
"created_at": "2022-04-16T19:22:54.000Z",
|
||||||
|
"emojis": [],
|
||||||
|
"id": "AIW4bHoICoZ9CsRTW4",
|
||||||
|
"unread": false
|
||||||
|
},
|
||||||
|
"created_at": "2022-04-16T19:22:55.000Z",
|
||||||
|
"id": "429247",
|
||||||
|
"pleroma": {
|
||||||
|
"is_muted": false,
|
||||||
|
"is_seen": true
|
||||||
|
},
|
||||||
|
"type": "pleroma:chat_mention"
|
||||||
|
}
|
@ -0,0 +1,301 @@
|
|||||||
|
{
|
||||||
|
"account": {
|
||||||
|
"acct": "dave",
|
||||||
|
"avatar": "https://media.gleasonator.com/68c29c30c18f30dd2898f85466bf1670312dda816617e6d31421c7e4c30a8265.png",
|
||||||
|
"avatar_static": "https://media.gleasonator.com/68c29c30c18f30dd2898f85466bf1670312dda816617e6d31421c7e4c30a8265.png",
|
||||||
|
"bot": false,
|
||||||
|
"created_at": "2020-02-01T07:28:46.000Z",
|
||||||
|
"display_name": "Elden Beedle 🇺🇦 🇫🇷",
|
||||||
|
"emojis": [],
|
||||||
|
"fields": [],
|
||||||
|
"followers_count": 490,
|
||||||
|
"following_count": 367,
|
||||||
|
"fqn": "dave@gleasonator.com",
|
||||||
|
"header": "https://media.gleasonator.com/47e8907c322a0e55d12b211846aa27c6b386e947326fe14bb09c89ef7317901d.jpg",
|
||||||
|
"header_static": "https://media.gleasonator.com/47e8907c322a0e55d12b211846aa27c6b386e947326fe14bb09c89ef7317901d.jpg",
|
||||||
|
"id": "9v5c0Pkz3MT5KTfam8",
|
||||||
|
"last_status_at": "2022-04-16T19:57:10",
|
||||||
|
"locked": false,
|
||||||
|
"note": "Beedle is back, baby!<br/><br/>Mostly just crosspost memes and stuff I find on the internet",
|
||||||
|
"pleroma": {
|
||||||
|
"accepts_chat_messages": true,
|
||||||
|
"also_known_as": [],
|
||||||
|
"ap_id": "https://gleasonator.com/users/dave",
|
||||||
|
"background_image": null,
|
||||||
|
"birthday": "1990-01-01",
|
||||||
|
"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": false,
|
||||||
|
"is_confirmed": true,
|
||||||
|
"is_moderator": false,
|
||||||
|
"is_suggested": true,
|
||||||
|
"location": null,
|
||||||
|
"relationship": {},
|
||||||
|
"skip_thread_containment": false,
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"fields": [],
|
||||||
|
"note": "Beedle is back, baby!\r\n\r\nMostly just crosspost memes and stuff I find on the internet",
|
||||||
|
"pleroma": {
|
||||||
|
"actor_type": "Person",
|
||||||
|
"discoverable": false
|
||||||
|
},
|
||||||
|
"sensitive": false
|
||||||
|
},
|
||||||
|
"statuses_count": 16758,
|
||||||
|
"url": "https://gleasonator.com/users/dave",
|
||||||
|
"username": "dave"
|
||||||
|
},
|
||||||
|
"created_at": "2022-04-16T16:52:15.000Z",
|
||||||
|
"emoji": "😮",
|
||||||
|
"id": "429071",
|
||||||
|
"pleroma": {
|
||||||
|
"is_muted": false,
|
||||||
|
"is_seen": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"account": {
|
||||||
|
"acct": "alex",
|
||||||
|
"avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png",
|
||||||
|
"avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png",
|
||||||
|
"bot": false,
|
||||||
|
"created_at": "2020-01-08T01:25:43.000Z",
|
||||||
|
"display_name": "Alex Gleason",
|
||||||
|
"emojis": [],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "Website",
|
||||||
|
"value": "<a href=\"https://alexgleason.me\" rel=\"ugc\">https://alexgleason.me</a>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Soapbox",
|
||||||
|
"value": "<a href=\"https://soapbox.pub\" rel=\"ugc\">https://soapbox.pub</a>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Email",
|
||||||
|
"value": "alex@alexgleason.me"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gender identity",
|
||||||
|
"value": "Soyboy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Donate (PayPal)",
|
||||||
|
"value": "<a href=\"https://paypal.me/gleasonator\" rel=\"ugc\">https://paypal.me/gleasonator</a>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "$BTC",
|
||||||
|
"value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "$ETH",
|
||||||
|
"value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "$DOGE",
|
||||||
|
"value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "$XMR",
|
||||||
|
"value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"follow_requests_count": 0,
|
||||||
|
"followers_count": 2602,
|
||||||
|
"following_count": 1603,
|
||||||
|
"fqn": "alex@gleasonator.com",
|
||||||
|
"header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
|
||||||
|
"header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
|
||||||
|
"id": "9v5bmRalQvjOy0ECcC",
|
||||||
|
"last_status_at": "2022-04-16T19:23:50",
|
||||||
|
"locked": false,
|
||||||
|
"note": "I create Fediverse software that empowers people online.<br/><br/>I'm vegan btw<br/><br/>Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.",
|
||||||
|
"pleroma": {
|
||||||
|
"accepts_chat_messages": true,
|
||||||
|
"accepts_email_list": true,
|
||||||
|
"allow_following_move": true,
|
||||||
|
"also_known_as": [
|
||||||
|
"https://mitra.social/users/alex"
|
||||||
|
],
|
||||||
|
"ap_id": "https://gleasonator.com/users/alex",
|
||||||
|
"background_image": null,
|
||||||
|
"birthday": "1993-07-03",
|
||||||
|
"deactivated": false,
|
||||||
|
"email": "alex@alexgleason.me",
|
||||||
|
"favicon": "https://gleasonator.com/favicon.png",
|
||||||
|
"hide_favorites": true,
|
||||||
|
"hide_followers": false,
|
||||||
|
"hide_followers_count": false,
|
||||||
|
"hide_follows": false,
|
||||||
|
"hide_follows_count": false,
|
||||||
|
"is_admin": true,
|
||||||
|
"is_confirmed": true,
|
||||||
|
"is_moderator": false,
|
||||||
|
"is_suggested": true,
|
||||||
|
"location": "Texas",
|
||||||
|
"notification_settings": {
|
||||||
|
"block_from_strangers": false,
|
||||||
|
"hide_notification_contents": false
|
||||||
|
},
|
||||||
|
"relationship": {},
|
||||||
|
"skip_thread_containment": false,
|
||||||
|
"tags": [],
|
||||||
|
"unread_conversation_count": 392,
|
||||||
|
"unread_notifications_count": 0
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "Website",
|
||||||
|
"value": "https://alexgleason.me"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Soapbox",
|
||||||
|
"value": "https://soapbox.pub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Email",
|
||||||
|
"value": "alex@alexgleason.me"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gender identity",
|
||||||
|
"value": "Soyboy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Donate (PayPal)",
|
||||||
|
"value": "https://paypal.me/gleasonator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "$BTC",
|
||||||
|
"value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "$ETH",
|
||||||
|
"value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "$DOGE",
|
||||||
|
"value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "$XMR",
|
||||||
|
"value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.",
|
||||||
|
"pleroma": {
|
||||||
|
"actor_type": "Person",
|
||||||
|
"discoverable": false,
|
||||||
|
"no_rich_text": false,
|
||||||
|
"show_birthday": true,
|
||||||
|
"show_role": true
|
||||||
|
},
|
||||||
|
"privacy": "public",
|
||||||
|
"sensitive": false
|
||||||
|
},
|
||||||
|
"statuses_count": 24050,
|
||||||
|
"url": "https://gleasonator.com/users/alex",
|
||||||
|
"username": "alex"
|
||||||
|
},
|
||||||
|
"application": {
|
||||||
|
"name": "Soapbox FE",
|
||||||
|
"website": "https://soapbox.pub/"
|
||||||
|
},
|
||||||
|
"bookmarked": false,
|
||||||
|
"card": {
|
||||||
|
"author_name": "Kaze Emanuar",
|
||||||
|
"author_url": "https://www.youtube.com/c/KazeEmanuar",
|
||||||
|
"blurhash": null,
|
||||||
|
"description": "",
|
||||||
|
"embed_url": null,
|
||||||
|
"height": 113,
|
||||||
|
"html": "<iframe width=\"200\" height=\"113\" src=\"https://www.youtube.com/embed/t_rzYnXEQlE?feature=oembed\" allowfullscreen=\"\"></iframe>",
|
||||||
|
"image": "https://gleasonator.com/proxy/mI004Vq00johZtAUmMp0fC_XAuM/aHR0cHM6Ly9pLnl0aW1nLmNvbS92aS90X3J6WW5YRVFsRS9ocWRlZmF1bHQuanBn/hqdefault.jpg",
|
||||||
|
"provider_name": "YouTube",
|
||||||
|
"provider_url": "https://www.youtube.com/",
|
||||||
|
"title": "FIXING the ENTIRE SM64 Source Code (INSANE N64 performance)",
|
||||||
|
"type": "video",
|
||||||
|
"url": "https://youtu.be/t_rzYnXEQlE",
|
||||||
|
"width": 200
|
||||||
|
},
|
||||||
|
"content": "<p>Bruh. This guy rewrote the reversed engineered Super Mario 64 code for 10x performance. Games need to be open source. <a href=\"https://youtu.be/t_rzYnXEQlE\">https://youtu.be/t_rzYnXEQlE</a></p>",
|
||||||
|
"created_at": "2022-04-16T16:40:28.000Z",
|
||||||
|
"emojis": [],
|
||||||
|
"favourited": false,
|
||||||
|
"favourites_count": 11,
|
||||||
|
"id": "AIVq6SrJg5yb8eGVsm",
|
||||||
|
"in_reply_to_account_id": null,
|
||||||
|
"in_reply_to_id": null,
|
||||||
|
"language": null,
|
||||||
|
"media_attachments": [],
|
||||||
|
"mentions": [],
|
||||||
|
"muted": false,
|
||||||
|
"pinned": false,
|
||||||
|
"pleroma": {
|
||||||
|
"content": {
|
||||||
|
"text/plain": "Bruh. This guy rewrote the reversed engineered Super Mario 64 code for 10x performance. Games need to be open source. https://youtu.be/t_rzYnXEQlE"
|
||||||
|
},
|
||||||
|
"content_type": null,
|
||||||
|
"conversation_id": "AIVq6SqFk37r5LlfE0",
|
||||||
|
"direct_conversation_id": null,
|
||||||
|
"emoji_reactions": [
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"me": false,
|
||||||
|
"name": "❤️"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 2,
|
||||||
|
"me": false,
|
||||||
|
"name": "😮"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"me": false,
|
||||||
|
"name": "😆"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"me": false,
|
||||||
|
"name": "👍🏻"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"me": false,
|
||||||
|
"name": "🔥"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expires_at": null,
|
||||||
|
"in_reply_to_account_acct": null,
|
||||||
|
"local": true,
|
||||||
|
"parent_visible": false,
|
||||||
|
"pinned_at": null,
|
||||||
|
"quote": null,
|
||||||
|
"quote_url": null,
|
||||||
|
"quote_visible": false,
|
||||||
|
"spoiler_text": {
|
||||||
|
"text/plain": ""
|
||||||
|
},
|
||||||
|
"thread_muted": false
|
||||||
|
},
|
||||||
|
"poll": null,
|
||||||
|
"reblog": null,
|
||||||
|
"reblogged": false,
|
||||||
|
"reblogs_count": 7,
|
||||||
|
"replies_count": 2,
|
||||||
|
"sensitive": false,
|
||||||
|
"spoiler_text": "",
|
||||||
|
"tags": [],
|
||||||
|
"text": null,
|
||||||
|
"uri": "https://gleasonator.com/objects/160dcbb2-73bc-4cd2-971e-e7f6a38602a0",
|
||||||
|
"url": "https://gleasonator.com/notice/AIVq6SrJg5yb8eGVsm",
|
||||||
|
"visibility": "public"
|
||||||
|
},
|
||||||
|
"type": "pleroma:emoji_reaction"
|
||||||
|
}
|
@ -0,0 +1,202 @@
|
|||||||
|
{
|
||||||
|
"account": {
|
||||||
|
"acct": "dave",
|
||||||
|
"avatar": "https://media.gleasonator.com/68c29c30c18f30dd2898f85466bf1670312dda816617e6d31421c7e4c30a8265.png",
|
||||||
|
"avatar_static": "https://media.gleasonator.com/68c29c30c18f30dd2898f85466bf1670312dda816617e6d31421c7e4c30a8265.png",
|
||||||
|
"bot": false,
|
||||||
|
"created_at": "2020-02-01T07:28:46.000Z",
|
||||||
|
"display_name": "Elden Beedle 🇺🇦 🇫🇷",
|
||||||
|
"emojis": [],
|
||||||
|
"fields": [],
|
||||||
|
"followers_count": 490,
|
||||||
|
"following_count": 367,
|
||||||
|
"fqn": "dave@gleasonator.com",
|
||||||
|
"header": "https://media.gleasonator.com/47e8907c322a0e55d12b211846aa27c6b386e947326fe14bb09c89ef7317901d.jpg",
|
||||||
|
"header_static": "https://media.gleasonator.com/47e8907c322a0e55d12b211846aa27c6b386e947326fe14bb09c89ef7317901d.jpg",
|
||||||
|
"id": "9v5c0Pkz3MT5KTfam8",
|
||||||
|
"last_status_at": "2022-04-16T19:57:10",
|
||||||
|
"locked": false,
|
||||||
|
"note": "Beedle is back, baby!<br/><br/>Mostly just crosspost memes and stuff I find on the internet",
|
||||||
|
"pleroma": {
|
||||||
|
"accepts_chat_messages": true,
|
||||||
|
"also_known_as": [],
|
||||||
|
"ap_id": "https://gleasonator.com/users/dave",
|
||||||
|
"background_image": null,
|
||||||
|
"birthday": "1990-01-01",
|
||||||
|
"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": false,
|
||||||
|
"is_confirmed": true,
|
||||||
|
"is_moderator": false,
|
||||||
|
"is_suggested": true,
|
||||||
|
"location": null,
|
||||||
|
"relationship": {},
|
||||||
|
"skip_thread_containment": false,
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"fields": [],
|
||||||
|
"note": "Beedle is back, baby!\r\n\r\nMostly just crosspost memes and stuff I find on the internet",
|
||||||
|
"pleroma": {
|
||||||
|
"actor_type": "Person",
|
||||||
|
"discoverable": false
|
||||||
|
},
|
||||||
|
"sensitive": false
|
||||||
|
},
|
||||||
|
"statuses_count": 16758,
|
||||||
|
"url": "https://gleasonator.com/users/dave",
|
||||||
|
"username": "dave"
|
||||||
|
},
|
||||||
|
"created_at": "2022-04-14T01:12:27.000Z",
|
||||||
|
"id": "427339",
|
||||||
|
"pleroma": {
|
||||||
|
"is_muted": false,
|
||||||
|
"is_seen": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"account": {
|
||||||
|
"acct": "dave",
|
||||||
|
"avatar": "https://media.gleasonator.com/68c29c30c18f30dd2898f85466bf1670312dda816617e6d31421c7e4c30a8265.png",
|
||||||
|
"avatar_static": "https://media.gleasonator.com/68c29c30c18f30dd2898f85466bf1670312dda816617e6d31421c7e4c30a8265.png",
|
||||||
|
"bot": false,
|
||||||
|
"created_at": "2020-02-01T07:28:46.000Z",
|
||||||
|
"display_name": "Elden Beedle 🇺🇦 🇫🇷",
|
||||||
|
"emojis": [],
|
||||||
|
"fields": [],
|
||||||
|
"followers_count": 490,
|
||||||
|
"following_count": 367,
|
||||||
|
"fqn": "dave@gleasonator.com",
|
||||||
|
"header": "https://media.gleasonator.com/47e8907c322a0e55d12b211846aa27c6b386e947326fe14bb09c89ef7317901d.jpg",
|
||||||
|
"header_static": "https://media.gleasonator.com/47e8907c322a0e55d12b211846aa27c6b386e947326fe14bb09c89ef7317901d.jpg",
|
||||||
|
"id": "9v5c0Pkz3MT5KTfam8",
|
||||||
|
"last_status_at": "2022-04-16T19:57:10",
|
||||||
|
"locked": false,
|
||||||
|
"note": "Beedle is back, baby!<br/><br/>Mostly just crosspost memes and stuff I find on the internet",
|
||||||
|
"pleroma": {
|
||||||
|
"accepts_chat_messages": true,
|
||||||
|
"also_known_as": [],
|
||||||
|
"ap_id": "https://gleasonator.com/users/dave",
|
||||||
|
"background_image": null,
|
||||||
|
"birthday": "1990-01-01",
|
||||||
|
"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": false,
|
||||||
|
"is_confirmed": true,
|
||||||
|
"is_moderator": false,
|
||||||
|
"is_suggested": true,
|
||||||
|
"location": null,
|
||||||
|
"relationship": {},
|
||||||
|
"skip_thread_containment": false,
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"fields": [],
|
||||||
|
"note": "Beedle is back, baby!\r\n\r\nMostly just crosspost memes and stuff I find on the internet",
|
||||||
|
"pleroma": {
|
||||||
|
"actor_type": "Person",
|
||||||
|
"discoverable": false
|
||||||
|
},
|
||||||
|
"sensitive": false
|
||||||
|
},
|
||||||
|
"statuses_count": 16758,
|
||||||
|
"url": "https://gleasonator.com/users/dave",
|
||||||
|
"username": "dave"
|
||||||
|
},
|
||||||
|
"application": {
|
||||||
|
"name": "Soapbox FE",
|
||||||
|
"website": "https://soapbox.pub/"
|
||||||
|
},
|
||||||
|
"bookmarked": false,
|
||||||
|
"card": null,
|
||||||
|
"content": "<p>Focusing on just the look, what do you guys think?</p>",
|
||||||
|
"created_at": "2022-04-13T01:12:26.000Z",
|
||||||
|
"emojis": [],
|
||||||
|
"favourited": false,
|
||||||
|
"favourites_count": 1,
|
||||||
|
"id": "AIOHjtGEaqUHoXGVf6",
|
||||||
|
"in_reply_to_account_id": "9v5c0Pkz3MT5KTfam8",
|
||||||
|
"in_reply_to_id": "AIOFTLqQrljhdNBNHE",
|
||||||
|
"language": null,
|
||||||
|
"media_attachments": [],
|
||||||
|
"mentions": [
|
||||||
|
{
|
||||||
|
"acct": "dave",
|
||||||
|
"id": "9v5c0Pkz3MT5KTfam8",
|
||||||
|
"url": "https://gleasonator.com/users/dave",
|
||||||
|
"username": "dave"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muted": false,
|
||||||
|
"pinned": false,
|
||||||
|
"pleroma": {
|
||||||
|
"content": {
|
||||||
|
"text/plain": "Focusing on just the look, what do you guys think?"
|
||||||
|
},
|
||||||
|
"content_type": null,
|
||||||
|
"conversation_id": "AIOFTLp0x2bNYyWF4C",
|
||||||
|
"direct_conversation_id": null,
|
||||||
|
"emoji_reactions": [],
|
||||||
|
"expires_at": null,
|
||||||
|
"in_reply_to_account_acct": "dave",
|
||||||
|
"local": true,
|
||||||
|
"parent_visible": true,
|
||||||
|
"pinned_at": null,
|
||||||
|
"quote": null,
|
||||||
|
"quote_url": null,
|
||||||
|
"quote_visible": false,
|
||||||
|
"spoiler_text": {
|
||||||
|
"text/plain": ""
|
||||||
|
},
|
||||||
|
"thread_muted": false
|
||||||
|
},
|
||||||
|
"poll": {
|
||||||
|
"emojis": [],
|
||||||
|
"expired": true,
|
||||||
|
"expires_at": "2022-04-14T01:12:26.000Z",
|
||||||
|
"id": "AIOHjtAuucEZY2mGNE",
|
||||||
|
"multiple": false,
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"title": "Looks good, looking forward to wider deployment",
|
||||||
|
"votes_count": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Not a fan, l'll stick to the current UI thanks",
|
||||||
|
"votes_count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Hard to say, need to actually try to decide honestly",
|
||||||
|
"votes_count": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"own_votes": [
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"voted": true,
|
||||||
|
"voters_count": 12,
|
||||||
|
"votes_count": 12
|
||||||
|
},
|
||||||
|
"reblog": null,
|
||||||
|
"reblogged": false,
|
||||||
|
"reblogs_count": 1,
|
||||||
|
"replies_count": 1,
|
||||||
|
"sensitive": false,
|
||||||
|
"spoiler_text": "",
|
||||||
|
"tags": [],
|
||||||
|
"text": null,
|
||||||
|
"uri": "https://gleasonator.com/objects/a8465271-a48d-4c39-a0a9-d3eda3ab2735",
|
||||||
|
"url": "https://gleasonator.com/notice/AIOHjtGEaqUHoXGVf6",
|
||||||
|
"visibility": "public"
|
||||||
|
},
|
||||||
|
"type": "poll"
|
||||||
|
}
|
@ -0,0 +1,284 @@
|
|||||||
|
{
|
||||||
|
"account": {
|
||||||
|
"acct": "rob@nicecrew.digital",
|
||||||
|
"avatar": "https://gleasonator.com/proxy/RcEgR4-0InIpw_sCpDWV-XrAbmY/aHR0cHM6Ly9uaWNlY3Jldy5kaWdpdGFsL21lZGlhL2M0MTllMTk1Nzg0MmEzMTY5M2MzNDExNTZlMTBhNmQwMTY2ZTM5YzQzM2ExZTczMmVmYWNlYmJkYjAyMDYzZjEucG5n/c419e1957842a31693c341156e10a6d0166e39c433a1e732efacebbdb02063f1.png",
|
||||||
|
"avatar_static": "https://gleasonator.com/proxy/RcEgR4-0InIpw_sCpDWV-XrAbmY/aHR0cHM6Ly9uaWNlY3Jldy5kaWdpdGFsL21lZGlhL2M0MTllMTk1Nzg0MmEzMTY5M2MzNDExNTZlMTBhNmQwMTY2ZTM5YzQzM2ExZTczMmVmYWNlYmJkYjAyMDYzZjEucG5n/c419e1957842a31693c341156e10a6d0166e39c433a1e732efacebbdb02063f1.png",
|
||||||
|
"bot": false,
|
||||||
|
"created_at": "2022-03-10T12:30:41.000Z",
|
||||||
|
"display_name": "Rob Colbert",
|
||||||
|
"emojis": [],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "Shing.tv",
|
||||||
|
"value": "<a href=\"https://shing.tv\" rel=\"ugc\">https://shing.tv</a>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "LibertyLinks",
|
||||||
|
"value": "<a href=\"https://libertylinks.io\" rel=\"ugc\">https://libertylinks.io</a>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GiveSendGo",
|
||||||
|
"value": "<a href=\"https://givesendgo.com/dtp\" rel=\"ugc\">https://givesendgo.com/dtp</a>"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"followers_count": 0,
|
||||||
|
"following_count": 0,
|
||||||
|
"fqn": "rob@nicecrew.digital",
|
||||||
|
"header": "https://gleasonator.com/proxy/t4--aro68-XZlasaR2bYiuiZMcA/aHR0cHM6Ly9uaWNlY3Jldy5kaWdpdGFsL21lZGlhL2E5ODYzYWE4YjEzM2QwMzkxNmU1N2MzNDgzMzBhZmE5MTM5MDFlNGZiMDEwYjk1Y2FiZjlmYmZiZTA4N2QxODMucG5n/a9863aa8b133d03916e57c348330afa913901e4fb010b95cabf9fbfbe087d183.png",
|
||||||
|
"header_static": "https://gleasonator.com/proxy/t4--aro68-XZlasaR2bYiuiZMcA/aHR0cHM6Ly9uaWNlY3Jldy5kaWdpdGFsL21lZGlhL2E5ODYzYWE4YjEzM2QwMzkxNmU1N2MzNDgzMzBhZmE5MTM5MDFlNGZiMDEwYjk1Y2FiZjlmYmZiZTA4N2QxODMucG5n/a9863aa8b133d03916e57c348330afa913901e4fb010b95cabf9fbfbe087d183.png",
|
||||||
|
"id": "AHGmnebARD1aa1IiBc",
|
||||||
|
"last_status_at": "2022-04-16T21:08:35",
|
||||||
|
"locked": false,
|
||||||
|
"note": "Creator and CTO of the Digital Telepresence Platform and DTP Technologies, LLC.",
|
||||||
|
"pleroma": {
|
||||||
|
"accepts_chat_messages": true,
|
||||||
|
"also_known_as": [],
|
||||||
|
"ap_id": "https://nicecrew.digital/users/rob",
|
||||||
|
"background_image": null,
|
||||||
|
"deactivated": false,
|
||||||
|
"favicon": "https://gleasonator.com/proxy/gb2NPo0Kv_svADN1_J9_9iSwlrY/aHR0cHM6Ly9uaWNlY3Jldy5kaWdpdGFsL2Zhdmljb24ucG5n/favicon.png",
|
||||||
|
"hide_favorites": true,
|
||||||
|
"hide_followers": true,
|
||||||
|
"hide_followers_count": false,
|
||||||
|
"hide_follows": true,
|
||||||
|
"hide_follows_count": false,
|
||||||
|
"is_admin": false,
|
||||||
|
"is_confirmed": true,
|
||||||
|
"is_moderator": false,
|
||||||
|
"is_suggested": false,
|
||||||
|
"location": null,
|
||||||
|
"relationship": {},
|
||||||
|
"skip_thread_containment": false,
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"fields": [],
|
||||||
|
"note": "",
|
||||||
|
"pleroma": {
|
||||||
|
"actor_type": "Person",
|
||||||
|
"discoverable": false
|
||||||
|
},
|
||||||
|
"sensitive": false
|
||||||
|
},
|
||||||
|
"statuses_count": 761,
|
||||||
|
"url": "https://nicecrew.digital/users/rob",
|
||||||
|
"username": "rob"
|
||||||
|
},
|
||||||
|
"created_at": "2022-04-16T03:43:24.000Z",
|
||||||
|
"id": "428608",
|
||||||
|
"pleroma": {
|
||||||
|
"is_muted": false,
|
||||||
|
"is_seen": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"account": {
|
||||||
|
"acct": "alex",
|
||||||
|
"avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png",
|
||||||
|
"avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png",
|
||||||
|
"bot": false,
|
||||||
|
"created_at": "2020-01-08T01:25:43.000Z",
|
||||||
|
"display_name": "Alex Gleason",
|
||||||
|
"emojis": [],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "Website",
|
||||||
|
"value": "<a href=\"https://alexgleason.me\" rel=\"ugc\">https://alexgleason.me</a>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Soapbox",
|
||||||
|
"value": "<a href=\"https://soapbox.pub\" rel=\"ugc\">https://soapbox.pub</a>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Email",
|
||||||
|
"value": "alex@alexgleason.me"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gender identity",
|
||||||
|
"value": "Soyboy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Donate (PayPal)",
|
||||||
|
"value": "<a href=\"https://paypal.me/gleasonator\" rel=\"ugc\">https://paypal.me/gleasonator</a>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "$BTC",
|
||||||
|
"value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "$ETH",
|
||||||
|
"value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "$DOGE",
|
||||||
|
"value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "$XMR",
|
||||||
|
"value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"follow_requests_count": 0,
|
||||||
|
"followers_count": 2602,
|
||||||
|
"following_count": 1603,
|
||||||
|
"fqn": "alex@gleasonator.com",
|
||||||
|
"header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
|
||||||
|
"header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
|
||||||
|
"id": "9v5bmRalQvjOy0ECcC",
|
||||||
|
"last_status_at": "2022-04-16T19:23:50",
|
||||||
|
"locked": false,
|
||||||
|
"note": "I create Fediverse software that empowers people online.<br/><br/>I'm vegan btw<br/><br/>Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.",
|
||||||
|
"pleroma": {
|
||||||
|
"accepts_chat_messages": true,
|
||||||
|
"accepts_email_list": true,
|
||||||
|
"allow_following_move": true,
|
||||||
|
"also_known_as": [
|
||||||
|
"https://mitra.social/users/alex"
|
||||||
|
],
|
||||||
|
"ap_id": "https://gleasonator.com/users/alex",
|
||||||
|
"background_image": null,
|
||||||
|
"birthday": "1993-07-03",
|
||||||
|
"deactivated": false,
|
||||||
|
"email": "alex@alexgleason.me",
|
||||||
|
"favicon": "https://gleasonator.com/favicon.png",
|
||||||
|
"hide_favorites": true,
|
||||||
|
"hide_followers": false,
|
||||||
|
"hide_followers_count": false,
|
||||||
|
"hide_follows": false,
|
||||||
|
"hide_follows_count": false,
|
||||||
|
"is_admin": true,
|
||||||
|
"is_confirmed": true,
|
||||||
|
"is_moderator": false,
|
||||||
|
"is_suggested": true,
|
||||||
|
"location": "Texas",
|
||||||
|
"notification_settings": {
|
||||||
|
"block_from_strangers": false,
|
||||||
|
"hide_notification_contents": false
|
||||||
|
},
|
||||||
|
"relationship": {},
|
||||||
|
"skip_thread_containment": false,
|
||||||
|
"tags": [],
|
||||||
|
"unread_conversation_count": 392,
|
||||||
|
"unread_notifications_count": 0
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "Website",
|
||||||
|
"value": "https://alexgleason.me"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Soapbox",
|
||||||
|
"value": "https://soapbox.pub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Email",
|
||||||
|
"value": "alex@alexgleason.me"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gender identity",
|
||||||
|
"value": "Soyboy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Donate (PayPal)",
|
||||||
|
"value": "https://paypal.me/gleasonator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "$BTC",
|
||||||
|
"value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "$ETH",
|
||||||
|
"value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "$DOGE",
|
||||||
|
"value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "$XMR",
|
||||||
|
"value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.",
|
||||||
|
"pleroma": {
|
||||||
|
"actor_type": "Person",
|
||||||
|
"discoverable": false,
|
||||||
|
"no_rich_text": false,
|
||||||
|
"show_birthday": true,
|
||||||
|
"show_role": true
|
||||||
|
},
|
||||||
|
"privacy": "public",
|
||||||
|
"sensitive": false
|
||||||
|
},
|
||||||
|
"statuses_count": 24050,
|
||||||
|
"url": "https://gleasonator.com/users/alex",
|
||||||
|
"username": "alex"
|
||||||
|
},
|
||||||
|
"application": {
|
||||||
|
"name": "Soapbox FE",
|
||||||
|
"website": "https://soapbox.pub/"
|
||||||
|
},
|
||||||
|
"bookmarked": false,
|
||||||
|
"card": null,
|
||||||
|
"content": "<p>The <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"9v5boQSsaxVc3AU8u0\" href=\"https://status.fsf.org/fsf\" rel=\"ugc\">@<span>fsf</span></a></span> needs to give out an award to every American who has never downloaded TikTok.</p>",
|
||||||
|
"created_at": "2022-04-16T03:42:50.000Z",
|
||||||
|
"emojis": [],
|
||||||
|
"favourited": false,
|
||||||
|
"favourites_count": 15,
|
||||||
|
"id": "AIUihbqUEe5Uvv7P9s",
|
||||||
|
"in_reply_to_account_id": null,
|
||||||
|
"in_reply_to_id": null,
|
||||||
|
"language": null,
|
||||||
|
"media_attachments": [],
|
||||||
|
"mentions": [
|
||||||
|
{
|
||||||
|
"acct": "fsf@status.fsf.org",
|
||||||
|
"id": "9v5boQSsaxVc3AU8u0",
|
||||||
|
"url": "https://status.fsf.org/fsf",
|
||||||
|
"username": "fsf"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muted": false,
|
||||||
|
"pinned": false,
|
||||||
|
"pleroma": {
|
||||||
|
"content": {
|
||||||
|
"text/plain": "The @fsf needs to give out an award to every American who has never downloaded TikTok."
|
||||||
|
},
|
||||||
|
"content_type": null,
|
||||||
|
"conversation_id": "AIUihbp4JuxArWSGwq",
|
||||||
|
"direct_conversation_id": null,
|
||||||
|
"emoji_reactions": [
|
||||||
|
{
|
||||||
|
"count": 2,
|
||||||
|
"me": false,
|
||||||
|
"name": "🔥"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expires_at": null,
|
||||||
|
"in_reply_to_account_acct": null,
|
||||||
|
"local": true,
|
||||||
|
"parent_visible": false,
|
||||||
|
"pinned_at": null,
|
||||||
|
"quote": null,
|
||||||
|
"quote_url": null,
|
||||||
|
"quote_visible": false,
|
||||||
|
"spoiler_text": {
|
||||||
|
"text/plain": ""
|
||||||
|
},
|
||||||
|
"thread_muted": false
|
||||||
|
},
|
||||||
|
"poll": null,
|
||||||
|
"reblog": null,
|
||||||
|
"reblogged": false,
|
||||||
|
"reblogs_count": 8,
|
||||||
|
"replies_count": 4,
|
||||||
|
"sensitive": false,
|
||||||
|
"spoiler_text": "",
|
||||||
|
"tags": [],
|
||||||
|
"text": null,
|
||||||
|
"uri": "https://gleasonator.com/objects/6be95787-fb9c-41cd-96cf-9652b2680863",
|
||||||
|
"url": "https://gleasonator.com/notice/AIUihbqUEe5Uvv7P9s",
|
||||||
|
"visibility": "public"
|
||||||
|
},
|
||||||
|
"type": "reblog"
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"funding": {
|
||||||
|
"amount": 3500,
|
||||||
|
"patrons": 3,
|
||||||
|
"currency": "usd",
|
||||||
|
"interval": "monthly"
|
||||||
|
},
|
||||||
|
"goals": [
|
||||||
|
{
|
||||||
|
"amount": 20000,
|
||||||
|
"currency": "usd",
|
||||||
|
"interval": "monthly",
|
||||||
|
"text": "I'll be able to afford an avocado."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": "https://patron.gleasonator.com"
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"is_patron": true,
|
||||||
|
"url": "https://gleasonator.com/users/dave"
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"uri": "pixelfed.social",
|
||||||
|
"title": "pixelfed",
|
||||||
|
"short_description": "Pixelfed is an image sharing platform, an ethical alternative to centralized platforms",
|
||||||
|
"description": "Pixelfed is an image sharing platform, an ethical alternative to centralized platforms",
|
||||||
|
"email": "hello@pixelfed.org",
|
||||||
|
"version": "2.7.2 (compatible; Pixelfed 0.11.2)",
|
||||||
|
"urls": {
|
||||||
|
"streaming_api": "wss://pixelfed.social"
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"user_count": 45061,
|
||||||
|
"status_count": 301357,
|
||||||
|
"domain_count": 5028
|
||||||
|
},
|
||||||
|
"thumbnail": "https://pixelfed.social/img/pixelfed-icon-color.png",
|
||||||
|
"languages": [
|
||||||
|
"en"
|
||||||
|
],
|
||||||
|
"registrations": true,
|
||||||
|
"approval_required": false,
|
||||||
|
"contact_account": {
|
||||||
|
"id": "1",
|
||||||
|
"username": "admin",
|
||||||
|
"acct": "admin",
|
||||||
|
"display_name": "Admin",
|
||||||
|
"discoverable": true,
|
||||||
|
"locked": false,
|
||||||
|
"followers_count": 419,
|
||||||
|
"following_count": 2,
|
||||||
|
"statuses_count": 6,
|
||||||
|
"note": "pixelfed.social Admin. Managed by @dansup",
|
||||||
|
"url": "https://pixelfed.social/admin",
|
||||||
|
"avatar": "https://pixelfed.social/storage/avatars/000/000/000/001/LSHNCgwbby7wu3iCYV6H_avatar.png?v=4",
|
||||||
|
"created_at": "2018-06-01T03:54:08.000000Z",
|
||||||
|
"avatar_static": "https://pixelfed.social/storage/avatars/000/000/000/001/LSHNCgwbby7wu3iCYV6H_avatar.png?v=4",
|
||||||
|
"bot": false,
|
||||||
|
"emojis": [],
|
||||||
|
"fields": [],
|
||||||
|
"header": "https://pixelfed.social/storage/headers/missing.png",
|
||||||
|
"header_static": "https://pixelfed.social/storage/headers/missing.png",
|
||||||
|
"last_status_at": null
|
||||||
|
},
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"text": "Sexually explicit or violent media must be marked as sensitive when posting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"text": "No racism, sexism, homophobia, transphobia, xenophobia, or casteism"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"text": "No incitement of violence or promotion of violent ideologies"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4",
|
||||||
|
"text": "No harassment, dogpiling or doxxing of other users"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5",
|
||||||
|
"text": "No content illegal in United States"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,108 @@
|
|||||||
|
{
|
||||||
|
"account": {
|
||||||
|
"acct": "alex",
|
||||||
|
"avatar": "https://freespeechextremist.com/images/avi.png",
|
||||||
|
"avatar_static": "https://freespeechextremist.com/images/avi.png",
|
||||||
|
"bot": false,
|
||||||
|
"created_at": "2022-02-28T01:55:05.000Z",
|
||||||
|
"display_name": "Alex Gleason",
|
||||||
|
"emojis": [],
|
||||||
|
"fields": [],
|
||||||
|
"followers_count": 1,
|
||||||
|
"following_count": 0,
|
||||||
|
"header": "https://freespeechextremist.com/images/banner.png",
|
||||||
|
"header_static": "https://freespeechextremist.com/images/banner.png",
|
||||||
|
"id": "AGv8wCadU7DqWgMqNk",
|
||||||
|
"locked": false,
|
||||||
|
"note": "I'm testing out compatibility with an older Pleroma version",
|
||||||
|
"pleroma": {
|
||||||
|
"accepts_chat_messages": true,
|
||||||
|
"ap_id": "https://freespeechextremist.com/users/alex",
|
||||||
|
"background_image": null,
|
||||||
|
"confirmation_pending": false,
|
||||||
|
"favicon": null,
|
||||||
|
"hide_favorites": true,
|
||||||
|
"hide_followers": false,
|
||||||
|
"hide_followers_count": false,
|
||||||
|
"hide_follows": false,
|
||||||
|
"hide_follows_count": false,
|
||||||
|
"is_admin": false,
|
||||||
|
"is_moderator": false,
|
||||||
|
"relationship": {},
|
||||||
|
"skip_thread_containment": false,
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"fields": [],
|
||||||
|
"note": "I'm testing out compatibility with an older Pleroma version",
|
||||||
|
"pleroma": {
|
||||||
|
"actor_type": "Person",
|
||||||
|
"discoverable": true
|
||||||
|
},
|
||||||
|
"sensitive": false
|
||||||
|
},
|
||||||
|
"statuses_count": 1,
|
||||||
|
"url": "https://freespeechextremist.com/users/alex",
|
||||||
|
"username": "alex"
|
||||||
|
},
|
||||||
|
"application": {
|
||||||
|
"name": "Web",
|
||||||
|
"website": null
|
||||||
|
},
|
||||||
|
"bookmarked": false,
|
||||||
|
"card": null,
|
||||||
|
"content": "<br/><a href=\"https://freespeechextremist.com/media/3e34b808-1c84-4ef3-ba56-67cc86b7911a/0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm?name=0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm\">0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm</a>",
|
||||||
|
"created_at": "2022-04-14T19:42:48.000Z",
|
||||||
|
"emojis": [],
|
||||||
|
"favourited": false,
|
||||||
|
"favourites_count": 0,
|
||||||
|
"id": "AIRxLeIzncpCtsr2hs",
|
||||||
|
"in_reply_to_account_id": null,
|
||||||
|
"in_reply_to_id": null,
|
||||||
|
"language": null,
|
||||||
|
"media_attachments": [
|
||||||
|
{
|
||||||
|
"description": "0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm",
|
||||||
|
"id": "1142674091",
|
||||||
|
"pleroma": {
|
||||||
|
"mime_type": "video/webm"
|
||||||
|
},
|
||||||
|
"preview_url": "https://freespeechextremist.com/media/3e34b808-1c84-4ef3-ba56-67cc86b7911a/0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm?name=0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm",
|
||||||
|
"remote_url": "https://freespeechextremist.com/media/3e34b808-1c84-4ef3-ba56-67cc86b7911a/0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm?name=0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm",
|
||||||
|
"text_url": "https://freespeechextremist.com/media/3e34b808-1c84-4ef3-ba56-67cc86b7911a/0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm?name=0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm",
|
||||||
|
"type": "video",
|
||||||
|
"url": "https://freespeechextremist.com/media/3e34b808-1c84-4ef3-ba56-67cc86b7911a/0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm?name=0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mentions": [],
|
||||||
|
"muted": false,
|
||||||
|
"pinned": false,
|
||||||
|
"pleroma": {
|
||||||
|
"content": {
|
||||||
|
"text/plain": "0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm"
|
||||||
|
},
|
||||||
|
"conversation_id": 97191096,
|
||||||
|
"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://freespeechextremist.com/objects/419b2cad-656a-4dbc-b2b5-94bb75e0afc8",
|
||||||
|
"url": "https://freespeechextremist.com/notice/AIRxLeIzncpCtsr2hs",
|
||||||
|
"visibility": "public"
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"text": "Illegal activity and behavior",
|
||||||
|
"subtext": "Content that depicts illegal or criminal acts, threats of violence.",
|
||||||
|
"rule_type": "content"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"text": "Intellectual property infringement",
|
||||||
|
"subtext": "Impersonating another account or business, infringing on intellectual property rights.",
|
||||||
|
"rule_type": "content"
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,119 @@
|
|||||||
|
{
|
||||||
|
"allowedEmoji": [
|
||||||
|
"👍",
|
||||||
|
"❤️",
|
||||||
|
"😆",
|
||||||
|
"😮",
|
||||||
|
"😢",
|
||||||
|
"😡",
|
||||||
|
"😩"
|
||||||
|
],
|
||||||
|
"brandColor": "#990099",
|
||||||
|
"copyright": "♡2021. Copying is an act of love. Please copy and share.",
|
||||||
|
"cryptoAddresses": [
|
||||||
|
{
|
||||||
|
"address": "bc1qv7lk3algpfg4zpyuhvxfm0uza9ck4parz3y3l5",
|
||||||
|
"note": "",
|
||||||
|
"ticker": "btc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "0xadc66B63bFee7677CD27CFb81b16a8860f1A1226",
|
||||||
|
"note": "",
|
||||||
|
"ticker": "eth"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "DSf7UmRf7DGGsjh4QYhzQaqtjJMTXZ8k79",
|
||||||
|
"note": "",
|
||||||
|
"ticker": "doge"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "ltc1q642pnkuvw0gpuuvddw6vafvl9hhp3efyl9mnqz",
|
||||||
|
"note": "",
|
||||||
|
"ticker": "ltc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "t1faHDsoa4bd3pGaLjaU7DiuUtBPzbnEEse",
|
||||||
|
"note": "",
|
||||||
|
"ticker": "zec"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "XchTLkcSMsDoZGESwr4tqtxSU5dideAZVQ",
|
||||||
|
"note": "",
|
||||||
|
"ticker": "dash"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "bitcoincash:qp8f80z27294phmhdk55yf05p3f0tkxl4v9r2aavw5",
|
||||||
|
"note": "",
|
||||||
|
"ticker": "bch"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cryptoDonatePanel": {
|
||||||
|
"limit": 1
|
||||||
|
},
|
||||||
|
"customCss": [
|
||||||
|
"/instance/spinster.css"
|
||||||
|
],
|
||||||
|
"defaultSettings": {
|
||||||
|
"autoPlayGif": false,
|
||||||
|
"themeMode": "light"
|
||||||
|
},
|
||||||
|
"extensions": {
|
||||||
|
"patron": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"logo": "https://spinster.xyz/instance/images/spinster-logo.svg",
|
||||||
|
"navlinks": {
|
||||||
|
"homeFooter": [
|
||||||
|
{
|
||||||
|
"title": "About",
|
||||||
|
"url": "/about"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Terms of Service",
|
||||||
|
"url": "/about/tos"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Privacy Policy",
|
||||||
|
"url": "/about/privacy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "DMCA",
|
||||||
|
"url": "/about/dmca"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Source Code",
|
||||||
|
"url": "/about#opensource"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"promoPanel": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"icon": "shopping-basket",
|
||||||
|
"text": "Buy Spinster Merch",
|
||||||
|
"url": "https://shop.4w.pub/collections/spinster"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "eye-slash",
|
||||||
|
"text": "Privacy Guide",
|
||||||
|
"url": "https://4w.pub/your-guide-to-spinster-privacy-options/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "question-circle",
|
||||||
|
"text": "Spinster FAQs",
|
||||||
|
"url": "https://spinster.xyz/about#faqs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "bug",
|
||||||
|
"text": "Report a Bug",
|
||||||
|
"url": "https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "fediverse",
|
||||||
|
"text": "About the Fediverse",
|
||||||
|
"url": "https://jointhefedi.com/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,101 @@
|
|||||||
|
import { mockStore, mockWindowProperty } from 'soapbox/jest/test-helpers';
|
||||||
|
import rootReducer from 'soapbox/reducers';
|
||||||
|
|
||||||
|
import { checkOnboardingStatus, startOnboarding, endOnboarding } from '../onboarding';
|
||||||
|
|
||||||
|
describe('checkOnboarding()', () => {
|
||||||
|
let mockGetItem: any;
|
||||||
|
|
||||||
|
mockWindowProperty('localStorage', {
|
||||||
|
getItem: (key: string) => mockGetItem(key),
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGetItem = jest.fn().mockReturnValue(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing if localStorage item is not set', async() => {
|
||||||
|
mockGetItem = jest.fn().mockReturnValue(null);
|
||||||
|
|
||||||
|
const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } });
|
||||||
|
const store = mockStore(state);
|
||||||
|
|
||||||
|
await store.dispatch(checkOnboardingStatus());
|
||||||
|
const actions = store.getActions();
|
||||||
|
|
||||||
|
expect(actions).toEqual([]);
|
||||||
|
expect(mockGetItem.mock.calls.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing if localStorage item is invalid', async() => {
|
||||||
|
mockGetItem = jest.fn().mockReturnValue('invalid');
|
||||||
|
|
||||||
|
const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } });
|
||||||
|
const store = mockStore(state);
|
||||||
|
|
||||||
|
await store.dispatch(checkOnboardingStatus());
|
||||||
|
const actions = store.getActions();
|
||||||
|
|
||||||
|
expect(actions).toEqual([]);
|
||||||
|
expect(mockGetItem.mock.calls.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches the correct action', async() => {
|
||||||
|
mockGetItem = jest.fn().mockReturnValue('1');
|
||||||
|
|
||||||
|
const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } });
|
||||||
|
const store = mockStore(state);
|
||||||
|
|
||||||
|
await store.dispatch(checkOnboardingStatus());
|
||||||
|
const actions = store.getActions();
|
||||||
|
|
||||||
|
expect(actions).toEqual([{ type: 'ONBOARDING_START' }]);
|
||||||
|
expect(mockGetItem.mock.calls.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('startOnboarding()', () => {
|
||||||
|
let mockSetItem: any;
|
||||||
|
|
||||||
|
mockWindowProperty('localStorage', {
|
||||||
|
setItem: (key: string, value: string) => mockSetItem(key, value),
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockSetItem = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches the correct action', async() => {
|
||||||
|
const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } });
|
||||||
|
const store = mockStore(state);
|
||||||
|
|
||||||
|
await store.dispatch(startOnboarding());
|
||||||
|
const actions = store.getActions();
|
||||||
|
|
||||||
|
expect(actions).toEqual([{ type: 'ONBOARDING_START' }]);
|
||||||
|
expect(mockSetItem.mock.calls.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('endOnboarding()', () => {
|
||||||
|
let mockRemoveItem: any;
|
||||||
|
|
||||||
|
mockWindowProperty('localStorage', {
|
||||||
|
removeItem: (key: string) => mockRemoveItem(key),
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockRemoveItem = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches the correct action', async() => {
|
||||||
|
const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } });
|
||||||
|
const store = mockStore(state);
|
||||||
|
|
||||||
|
await store.dispatch(endOnboarding());
|
||||||
|
const actions = store.getActions();
|
||||||
|
|
||||||
|
expect(actions).toEqual([{ type: 'ONBOARDING_END' }]);
|
||||||
|
expect(mockRemoveItem.mock.calls.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,26 @@
|
|||||||
|
import { __stub } from 'soapbox/api';
|
||||||
|
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
||||||
|
|
||||||
|
import { fetchRules, RULES_FETCH_REQUEST, RULES_FETCH_SUCCESS } from '../rules';
|
||||||
|
|
||||||
|
describe('fetchRules()', () => {
|
||||||
|
it('sets the rules', (done) => {
|
||||||
|
const rules = require('soapbox/__fixtures__/rules.json');
|
||||||
|
|
||||||
|
__stub((mock) => {
|
||||||
|
mock.onGet('/api/v1/instance/rules').reply(200, rules);
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = mockStore(rootState);
|
||||||
|
|
||||||
|
store.dispatch(fetchRules()).then((context) => {
|
||||||
|
const actions = store.getActions();
|
||||||
|
|
||||||
|
expect(actions[0].type).toEqual(RULES_FETCH_REQUEST);
|
||||||
|
expect(actions[1].type).toEqual(RULES_FETCH_SUCCESS);
|
||||||
|
expect(actions[1].payload[0].id).toEqual('1');
|
||||||
|
|
||||||
|
done();
|
||||||
|
}).catch(console.error);
|
||||||
|
});
|
||||||
|
});
|
@ -1,17 +0,0 @@
|
|||||||
export const HEIGHT_CACHE_SET = 'HEIGHT_CACHE_SET';
|
|
||||||
export const HEIGHT_CACHE_CLEAR = 'HEIGHT_CACHE_CLEAR';
|
|
||||||
|
|
||||||
export function setHeight(key, id, height) {
|
|
||||||
return {
|
|
||||||
type: HEIGHT_CACHE_SET,
|
|
||||||
key,
|
|
||||||
id,
|
|
||||||
height,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearHeight() {
|
|
||||||
return {
|
|
||||||
type: HEIGHT_CACHE_CLEAR,
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
import { changeSetting, saveSettings } from './settings';
|
|
||||||
|
|
||||||
export const INTRODUCTION_VERSION = 20181216044202;
|
|
||||||
|
|
||||||
export const closeOnboarding = () => dispatch => {
|
|
||||||
dispatch(changeSetting(['introductionVersion'], INTRODUCTION_VERSION));
|
|
||||||
dispatch(saveSettings());
|
|
||||||
};
|
|
@ -0,0 +1,40 @@
|
|||||||
|
const ONBOARDING_START = 'ONBOARDING_START';
|
||||||
|
const ONBOARDING_END = 'ONBOARDING_END';
|
||||||
|
|
||||||
|
const ONBOARDING_LOCAL_STORAGE_KEY = 'soapbox:onboarding';
|
||||||
|
|
||||||
|
type OnboardingStartAction = {
|
||||||
|
type: typeof ONBOARDING_START
|
||||||
|
}
|
||||||
|
|
||||||
|
type OnboardingEndAction = {
|
||||||
|
type: typeof ONBOARDING_END
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OnboardingActions = OnboardingStartAction | OnboardingEndAction
|
||||||
|
|
||||||
|
const checkOnboardingStatus = () => (dispatch: React.Dispatch<OnboardingActions>) => {
|
||||||
|
const needsOnboarding = localStorage.getItem(ONBOARDING_LOCAL_STORAGE_KEY) === '1';
|
||||||
|
|
||||||
|
if (needsOnboarding) {
|
||||||
|
dispatch({ type: ONBOARDING_START });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startOnboarding = () => (dispatch: React.Dispatch<OnboardingActions>) => {
|
||||||
|
localStorage.setItem(ONBOARDING_LOCAL_STORAGE_KEY, '1');
|
||||||
|
dispatch({ type: ONBOARDING_START });
|
||||||
|
};
|
||||||
|
|
||||||
|
const endOnboarding = () => (dispatch: React.Dispatch<OnboardingActions>) => {
|
||||||
|
localStorage.removeItem(ONBOARDING_LOCAL_STORAGE_KEY);
|
||||||
|
dispatch({ type: ONBOARDING_END });
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
ONBOARDING_END,
|
||||||
|
ONBOARDING_START,
|
||||||
|
checkOnboardingStatus,
|
||||||
|
endOnboarding,
|
||||||
|
startOnboarding,
|
||||||
|
};
|
@ -0,0 +1,31 @@
|
|||||||
|
import api from '../api';
|
||||||
|
|
||||||
|
import type { Rule } from 'soapbox/reducers/rules';
|
||||||
|
|
||||||
|
const RULES_FETCH_REQUEST = 'RULES_FETCH_REQUEST';
|
||||||
|
const RULES_FETCH_SUCCESS = 'RULES_FETCH_SUCCESS';
|
||||||
|
|
||||||
|
type RulesFetchRequestAction = {
|
||||||
|
type: typeof RULES_FETCH_REQUEST
|
||||||
|
}
|
||||||
|
|
||||||
|
type RulesFetchRequestSuccessAction = {
|
||||||
|
type: typeof RULES_FETCH_SUCCESS
|
||||||
|
payload: Rule[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RulesActions = RulesFetchRequestAction | RulesFetchRequestSuccessAction
|
||||||
|
|
||||||
|
const fetchRules = () => (dispatch: React.Dispatch<RulesActions>, getState: any) => {
|
||||||
|
dispatch({ type: RULES_FETCH_REQUEST });
|
||||||
|
|
||||||
|
return api(getState)
|
||||||
|
.get('/api/v1/instance/rules')
|
||||||
|
.then((response) => dispatch({ type: RULES_FETCH_SUCCESS, payload: response.data }));
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
fetchRules,
|
||||||
|
RULES_FETCH_REQUEST,
|
||||||
|
RULES_FETCH_SUCCESS,
|
||||||
|
};
|
@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
export default function compareId(id1, id2) {
|
export default function compareId(id1: string, id2: string) {
|
||||||
if (id1 === id2) {
|
if (id1 === id2) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
@ -1,13 +1,26 @@
|
|||||||
import PropTypes from 'prop-types';
|
import classNames from 'classnames';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const Badge = (props: any) => (
|
interface IBadge {
|
||||||
<span data-testid='badge' className={'badge badge--' + props.slug}>{props.title}</span>
|
title: string,
|
||||||
);
|
slug: 'patron' | 'donor' | 'admin' | 'moderator' | 'bot' | 'opaque',
|
||||||
|
}
|
||||||
|
|
||||||
Badge.propTypes = {
|
/** Badge to display on a user's profile. */
|
||||||
title: PropTypes.string.isRequired,
|
const Badge: React.FC<IBadge> = ({ title, slug }) => (
|
||||||
slug: PropTypes.string.isRequired,
|
<span
|
||||||
};
|
data-testid='badge'
|
||||||
|
className={classNames('inline-flex items-center px-2 py-0.5 rounded text-xs font-medium text-white', {
|
||||||
|
'bg-fuchsia-700': slug === 'patron',
|
||||||
|
'bg-yellow-500': slug === 'donor',
|
||||||
|
'bg-black': slug === 'admin',
|
||||||
|
'bg-cyan-600': slug === 'moderator',
|
||||||
|
'bg-gray-100 text-gray-800': slug === 'bot',
|
||||||
|
'bg-white bg-opacity-75 text-gray-900': slug === 'opaque',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
export default Badge;
|
export default Badge;
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
import { fetchBirthdayReminders } from 'soapbox/actions/accounts';
|
||||||
|
import { Widget } from 'soapbox/components/ui';
|
||||||
|
import AccountContainer from 'soapbox/containers/account_container';
|
||||||
|
import { useAppSelector } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
interface IBirthdayPanel {
|
||||||
|
limit: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const BirthdayPanel = ({ limit }: IBirthdayPanel) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const birthdays: ImmutableOrderedSet<string> = useAppSelector(state => state.user_lists.getIn(['birthday_reminders', state.me], ImmutableOrderedSet()));
|
||||||
|
const birthdaysToRender = birthdays.slice(0, limit);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const date = new Date();
|
||||||
|
|
||||||
|
const day = date.getDate();
|
||||||
|
const month = date.getMonth() + 1;
|
||||||
|
|
||||||
|
dispatch(fetchBirthdayReminders(month, day));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (birthdaysToRender.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Widget title={<FormattedMessage id='birthday_panel.title' defaultMessage='Birthdays' />}>
|
||||||
|
{birthdaysToRender.map(accountId => (
|
||||||
|
<AccountContainer
|
||||||
|
key={accountId}
|
||||||
|
// @ts-ignore: TS thinks `id` is passed to <Account>, but it isn't
|
||||||
|
id={accountId}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Widget>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BirthdayPanel;
|
@ -1,161 +0,0 @@
|
|||||||
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { HotKeys } from 'react-hotkeys';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { fetchBirthdayReminders } from 'soapbox/actions/accounts';
|
|
||||||
import { openModal } from 'soapbox/actions/modals';
|
|
||||||
import Icon from 'soapbox/components/icon';
|
|
||||||
import { HStack, Text } from 'soapbox/components/ui';
|
|
||||||
import { makeGetAccount } from 'soapbox/selectors';
|
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => {
|
|
||||||
const me = state.get('me');
|
|
||||||
const getAccount = makeGetAccount();
|
|
||||||
|
|
||||||
const birthdays = state.getIn(['user_lists', 'birthday_reminders', me]);
|
|
||||||
|
|
||||||
if (birthdays?.size > 0) {
|
|
||||||
return {
|
|
||||||
birthdays,
|
|
||||||
account: getAccount(state, birthdays.first()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
birthdays,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
|
||||||
@injectIntl
|
|
||||||
class BirthdayReminders extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
birthdays: ImmutablePropTypes.orderedSet,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
dispatch: PropTypes.func.isRequired,
|
|
||||||
onMoveDown: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
|
|
||||||
const date = new Date();
|
|
||||||
|
|
||||||
const day = date.getDate();
|
|
||||||
const month = date.getMonth() + 1;
|
|
||||||
|
|
||||||
dispatch(fetchBirthdayReminders(day, month));
|
|
||||||
}
|
|
||||||
|
|
||||||
getHandlers() {
|
|
||||||
return {
|
|
||||||
open: this.handleOpenBirthdaysModal,
|
|
||||||
moveDown: this.props.onMoveDown,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
handleOpenBirthdaysModal = () => {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
|
|
||||||
dispatch(openModal('BIRTHDAYS'));
|
|
||||||
}
|
|
||||||
|
|
||||||
renderMessage() {
|
|
||||||
const { birthdays, account } = this.props;
|
|
||||||
|
|
||||||
const link = (
|
|
||||||
<bdi>
|
|
||||||
<Link
|
|
||||||
className='text-gray-800 dark:text-gray-200 font-bold hover:underline'
|
|
||||||
title={account.get('acct')}
|
|
||||||
to={`/@${account.get('acct')}`}
|
|
||||||
dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }}
|
|
||||||
/>
|
|
||||||
</bdi>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (birthdays.size === 1) {
|
|
||||||
return <FormattedMessage id='notification.birthday' defaultMessage='{name} has a birthday today' values={{ name: link }} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormattedMessage
|
|
||||||
id='notification.birthday_plural'
|
|
||||||
defaultMessage='{name} and {more} have birthday today'
|
|
||||||
values={{
|
|
||||||
name: link,
|
|
||||||
more: (
|
|
||||||
<span type='button' role='presentation' onClick={this.handleOpenBirthdaysModal}>
|
|
||||||
<FormattedMessage
|
|
||||||
id='notification.birthday.more'
|
|
||||||
defaultMessage='{count} more {count, plural, one {friend} other {friends}}'
|
|
||||||
values={{ count: birthdays.size - 1 }}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderMessageForScreenReader = () => {
|
|
||||||
const { intl, birthdays, account } = this.props;
|
|
||||||
|
|
||||||
if (birthdays.size === 1) {
|
|
||||||
return intl.formatMessage({ id: 'notification.birthday', defaultMessage: '{name} has a birthday today' }, { name: account.get('display_name') });
|
|
||||||
}
|
|
||||||
|
|
||||||
return intl.formatMessage(
|
|
||||||
{
|
|
||||||
id: 'notification.birthday_plural',
|
|
||||||
defaultMessage: '{name} and {more} have birthday today',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: account.get('display_name'),
|
|
||||||
more: intl.formatMessage(
|
|
||||||
{
|
|
||||||
id: 'notification.birthday.more',
|
|
||||||
defaultMessage: '{count} more {count, plural, one {friend} other {friends}}',
|
|
||||||
},
|
|
||||||
{ count: birthdays.size - 1 },
|
|
||||||
),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { birthdays } = this.props;
|
|
||||||
|
|
||||||
if (!birthdays || birthdays.size === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<HotKeys handlers={this.getHandlers()}>
|
|
||||||
<div className='notification notification-birthday focusable' tabIndex='0' title={this.renderMessageForScreenReader()}>
|
|
||||||
<div className='p-4 focusable'>
|
|
||||||
<HStack alignItems='center' space={1.5}>
|
|
||||||
<Icon
|
|
||||||
src={require('@tabler/icons/icons/ballon.svg')}
|
|
||||||
className='text-primary-600'
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Text
|
|
||||||
theme='muted'
|
|
||||||
size='sm'
|
|
||||||
>
|
|
||||||
{this.renderMessage()}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</HotKeys>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,68 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { decode } from 'blurhash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { useRef, useEffect } from 'react';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef BlurhashPropsBase
|
|
||||||
* @property {string?} hash Hash to render
|
|
||||||
* @property {number} width
|
|
||||||
* Width of the blurred region in pixels. Defaults to 32
|
|
||||||
* @property {number} [height]
|
|
||||||
* Height of the blurred region in pixels. Defaults to width
|
|
||||||
* @property {boolean} [dummy]
|
|
||||||
* Whether dummy mode is enabled. If enabled, nothing is rendered
|
|
||||||
* and canvas left untouched
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** @typedef {JSX.IntrinsicElements['canvas'] & BlurhashPropsBase} BlurhashProps */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component that is used to render blurred of blurhash string
|
|
||||||
*
|
|
||||||
* @param {BlurhashProps} param1 Props of the component
|
|
||||||
* @returns Canvas which will render blurred region element to embed
|
|
||||||
*/
|
|
||||||
function Blurhash({
|
|
||||||
hash,
|
|
||||||
width = 32,
|
|
||||||
height = width,
|
|
||||||
dummy = false,
|
|
||||||
...canvasProps
|
|
||||||
}) {
|
|
||||||
const canvasRef = /** @type {import('react').MutableRefObject<HTMLCanvasElement>} */ (useRef());
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const { current: canvas } = canvasRef;
|
|
||||||
|
|
||||||
// resets canvas
|
|
||||||
canvas.width = canvas.width; // eslint-disable-line no-self-assign
|
|
||||||
|
|
||||||
if (dummy || !hash) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const pixels = decode(hash, width, height);
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
const imageData = new ImageData(pixels, width, height);
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
ctx.putImageData(imageData, 0, 0);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Blurhash decoding failure', { err, hash });
|
|
||||||
}
|
|
||||||
}, [dummy, hash, width, height]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<canvas {...canvasProps} ref={canvasRef} width={width} height={height} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Blurhash.propTypes = {
|
|
||||||
hash: PropTypes.string,
|
|
||||||
width: PropTypes.number,
|
|
||||||
height: PropTypes.number,
|
|
||||||
dummy: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default React.memo(Blurhash);
|
|
@ -0,0 +1,59 @@
|
|||||||
|
import { decode } from 'blurhash';
|
||||||
|
import React, { useRef, useEffect } from 'react';
|
||||||
|
|
||||||
|
interface IBlurhash {
|
||||||
|
/** Hash to render */
|
||||||
|
hash: string | null | undefined,
|
||||||
|
/** Width of the blurred region in pixels. Defaults to 32. */
|
||||||
|
width?: number,
|
||||||
|
/** Height of the blurred region in pixels. Defaults to width. */
|
||||||
|
height?: number,
|
||||||
|
/**
|
||||||
|
* Whether dummy mode is enabled. If enabled, nothing is rendered
|
||||||
|
* and canvas left untouched.
|
||||||
|
*/
|
||||||
|
dummy?: boolean,
|
||||||
|
/** className of the canvas element. */
|
||||||
|
className?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a blurhash in a canvas element.
|
||||||
|
* @see {@link https://blurha.sh/}
|
||||||
|
*/
|
||||||
|
const Blurhash: React.FC<IBlurhash> = ({
|
||||||
|
hash,
|
||||||
|
width = 32,
|
||||||
|
height = width,
|
||||||
|
dummy = false,
|
||||||
|
...canvasProps
|
||||||
|
}) => {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { current: canvas } = canvasRef;
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
// resets canvas
|
||||||
|
canvas.width = canvas.width; // eslint-disable-line no-self-assign
|
||||||
|
|
||||||
|
if (dummy || !hash) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pixels = decode(hash, width, height);
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const imageData = new ImageData(pixels, width, height);
|
||||||
|
|
||||||
|
if (!ctx) return;
|
||||||
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Blurhash decoding failure', { err, hash });
|
||||||
|
}
|
||||||
|
}, [dummy, hash, width, height]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas {...canvasProps} ref={canvasRef} width={width} height={height} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(Blurhash);
|
@ -1,43 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import IconButton from './icon_button';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
|
|
||||||
});
|
|
||||||
|
|
||||||
export default @injectIntl
|
|
||||||
class Account extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
domain: PropTypes.string,
|
|
||||||
onUnblockDomain: PropTypes.func.isRequired,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleDomainUnblock = () => {
|
|
||||||
this.props.onUnblockDomain(this.props.domain);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { domain, intl } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='domain'>
|
|
||||||
<div className='domain__wrapper'>
|
|
||||||
<span className='domain__domain-name'>
|
|
||||||
<strong>{domain}</strong>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div className='domain__buttons'>
|
|
||||||
<IconButton active src={require('@tabler/icons/icons/lock-open.svg')} title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
import { unblockDomain } from 'soapbox/actions/domain_blocks';
|
||||||
|
|
||||||
|
import IconButton from './icon_button';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
|
||||||
|
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IDomain {
|
||||||
|
domain: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const Domain: React.FC<IDomain> = ({ domain }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
// const onBlockDomain = () => {
|
||||||
|
// dispatch(openModal('CONFIRM', {
|
||||||
|
// icon: require('@tabler/icons/icons/ban.svg'),
|
||||||
|
// heading: <FormattedMessage id='confirmations.domain_block.heading' defaultMessage='Block {domain}' values={{ domain }} />,
|
||||||
|
// message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.' values={{ domain: <strong>{domain}</strong> }} />,
|
||||||
|
// confirm: intl.formatMessage(messages.blockDomainConfirm),
|
||||||
|
// onConfirm: () => dispatch(blockDomain(domain)),
|
||||||
|
// }));
|
||||||
|
// }
|
||||||
|
|
||||||
|
const handleDomainUnblock = () => {
|
||||||
|
dispatch(unblockDomain(domain));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='domain'>
|
||||||
|
<div className='domain__wrapper'>
|
||||||
|
<span className='domain__domain-name'>
|
||||||
|
<strong>{domain}</strong>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className='domain__buttons'>
|
||||||
|
<IconButton active src={require('@tabler/icons/icons/lock-open.svg')} title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={handleDomainUnblock} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Domain;
|
@ -0,0 +1,146 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { usePopper } from 'react-popper';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
import { simpleEmojiReact } from 'soapbox/actions/emoji_reacts';
|
||||||
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
|
import EmojiSelector from 'soapbox/components/ui/emoji-selector/emoji-selector';
|
||||||
|
import { useAppSelector, useOwnAccount, useSoapboxConfig } from 'soapbox/hooks';
|
||||||
|
import { isUserTouching } from 'soapbox/is_mobile';
|
||||||
|
import { getReactForStatus } from 'soapbox/utils/emoji_reacts';
|
||||||
|
|
||||||
|
interface IEmojiButtonWrapper {
|
||||||
|
statusId: string,
|
||||||
|
children: JSX.Element,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Provides emoji reaction functionality to the underlying button component */
|
||||||
|
const EmojiButtonWrapper: React.FC<IEmojiButtonWrapper> = ({ statusId, children }): JSX.Element | null => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const ownAccount = useOwnAccount();
|
||||||
|
const status = useAppSelector(state => state.statuses.get(statusId));
|
||||||
|
const soapboxConfig = useSoapboxConfig();
|
||||||
|
|
||||||
|
const timeout = useRef<NodeJS.Timeout>();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
// const [focused, setFocused] = useState(false);
|
||||||
|
|
||||||
|
// `useRef` won't trigger a re-render, while `useState` does.
|
||||||
|
// https://popper.js.org/react-popper/v2/
|
||||||
|
const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||||
|
placement: 'top-start',
|
||||||
|
modifiers: [
|
||||||
|
{
|
||||||
|
name: 'offset',
|
||||||
|
options: {
|
||||||
|
offset: [-10, 0],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (timeout.current) {
|
||||||
|
clearTimeout(timeout.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!status) return null;
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
if (timeout.current) {
|
||||||
|
clearTimeout(timeout.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isUserTouching()) {
|
||||||
|
setVisible(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
if (timeout.current) {
|
||||||
|
clearTimeout(timeout.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unless the user is touching, delay closing the emoji selector briefly
|
||||||
|
// so the user can move the mouse diagonally to make a selection.
|
||||||
|
if (isUserTouching()) {
|
||||||
|
setVisible(false);
|
||||||
|
} else {
|
||||||
|
timeout.current = setTimeout(() => {
|
||||||
|
setVisible(false);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReact = (emoji: string): void => {
|
||||||
|
if (ownAccount) {
|
||||||
|
dispatch(simpleEmojiReact(status, emoji));
|
||||||
|
} else {
|
||||||
|
dispatch(openModal('UNAUTHORIZED', {
|
||||||
|
action: 'FAVOURITE',
|
||||||
|
ap_id: status.url,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
setVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick: React.EventHandler<React.MouseEvent> = e => {
|
||||||
|
const meEmojiReact = getReactForStatus(status, soapboxConfig.allowedEmoji) || '👍';
|
||||||
|
|
||||||
|
if (isUserTouching()) {
|
||||||
|
if (visible) {
|
||||||
|
handleReact(meEmojiReact);
|
||||||
|
} else {
|
||||||
|
setVisible(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleReact(meEmojiReact);
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
// const handleUnfocus: React.EventHandler<React.KeyboardEvent> = () => {
|
||||||
|
// setFocused(false);
|
||||||
|
// };
|
||||||
|
|
||||||
|
const selector = (
|
||||||
|
<div
|
||||||
|
className={classNames('z-50 transition-opacity duration-100', {
|
||||||
|
'opacity-0 pointer-events-none': !visible,
|
||||||
|
})}
|
||||||
|
ref={setPopperElement}
|
||||||
|
style={styles.popper}
|
||||||
|
{...attributes.popper}
|
||||||
|
>
|
||||||
|
<EmojiSelector
|
||||||
|
emojis={soapboxConfig.allowedEmoji}
|
||||||
|
onReact={handleReact}
|
||||||
|
// focused={focused}
|
||||||
|
// onUnfocus={handleUnfocus}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='relative' onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||||
|
{React.cloneElement(children, {
|
||||||
|
onClick: handleClick,
|
||||||
|
ref: setReferenceElement,
|
||||||
|
})}
|
||||||
|
|
||||||
|
{selector}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmojiButtonWrapper;
|
@ -1,63 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import React, { useState, useRef } from 'react';
|
|
||||||
import { usePopper } from 'react-popper';
|
|
||||||
|
|
||||||
interface IHoverable {
|
|
||||||
component: JSX.Element,
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Wrapper to render a given component when hovered */
|
|
||||||
const Hoverable: React.FC<IHoverable> = ({
|
|
||||||
component,
|
|
||||||
children,
|
|
||||||
}): JSX.Element => {
|
|
||||||
|
|
||||||
const [portalActive, setPortalActive] = useState(false);
|
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
const popperRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
|
||||||
setPortalActive(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
|
||||||
setPortalActive(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { styles, attributes } = usePopper(ref.current, popperRef.current, {
|
|
||||||
placement: 'top-start',
|
|
||||||
strategy: 'fixed',
|
|
||||||
modifiers: [
|
|
||||||
{
|
|
||||||
name: 'offset',
|
|
||||||
options: {
|
|
||||||
offset: [-10, 0],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
onMouseEnter={handleMouseEnter}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
ref={ref}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={classNames('fixed z-50 transition-opacity duration-100', {
|
|
||||||
'opacity-0 pointer-events-none': !portalActive,
|
|
||||||
})}
|
|
||||||
ref={popperRef}
|
|
||||||
style={styles.popper}
|
|
||||||
{...attributes.popper}
|
|
||||||
>
|
|
||||||
{component}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Hoverable;
|
|
@ -1,131 +0,0 @@
|
|||||||
import { is } from 'immutable';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
|
|
||||||
import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
|
|
||||||
|
|
||||||
// Diff these props in the "rendered" state
|
|
||||||
const updateOnPropsForRendered = ['id', 'index', 'listLength'];
|
|
||||||
// Diff these props in the "unrendered" state
|
|
||||||
const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight'];
|
|
||||||
|
|
||||||
export default class IntersectionObserverArticle extends React.Component {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
intersectionObserverWrapper: PropTypes.object.isRequired,
|
|
||||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
||||||
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
||||||
listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
||||||
saveHeightKey: PropTypes.string,
|
|
||||||
cachedHeight: PropTypes.number,
|
|
||||||
onHeightChange: PropTypes.func,
|
|
||||||
children: PropTypes.node,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
isHidden: false, // set to true in requestIdleCallback to trigger un-render
|
|
||||||
isIntersecting: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
|
||||||
const isUnrendered = !this.state.isIntersecting && (this.state.isHidden || this.props.cachedHeight);
|
|
||||||
const willBeUnrendered = !nextState.isIntersecting && (nextState.isHidden || nextProps.cachedHeight);
|
|
||||||
if (!!isUnrendered !== !!willBeUnrendered) {
|
|
||||||
// If we're going from rendered to unrendered (or vice versa) then update
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Otherwise, diff based on props
|
|
||||||
const propsToDiff = isUnrendered ? updateOnPropsForUnrendered : updateOnPropsForRendered;
|
|
||||||
return !propsToDiff.every(prop => is(nextProps[prop], this.props[prop]));
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const { intersectionObserverWrapper, id } = this.props;
|
|
||||||
|
|
||||||
intersectionObserverWrapper.observe(
|
|
||||||
id,
|
|
||||||
this.node,
|
|
||||||
this.handleIntersection,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.componentMounted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
const { intersectionObserverWrapper, id } = this.props;
|
|
||||||
intersectionObserverWrapper.unobserve(id, this.node);
|
|
||||||
|
|
||||||
this.componentMounted = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleIntersection = (entry) => {
|
|
||||||
this.entry = entry;
|
|
||||||
|
|
||||||
scheduleIdleTask(this.calculateHeight);
|
|
||||||
this.setState(this.updateStateAfterIntersection);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStateAfterIntersection = (prevState) => {
|
|
||||||
if (prevState.isIntersecting !== false && !this.entry.isIntersecting) {
|
|
||||||
scheduleIdleTask(this.hideIfNotIntersecting);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
isIntersecting: this.entry.isIntersecting,
|
|
||||||
isHidden: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateHeight = () => {
|
|
||||||
const { onHeightChange, saveHeightKey, id } = this.props;
|
|
||||||
// save the height of the fully-rendered element (this is expensive
|
|
||||||
// on Chrome, where we need to fall back to getBoundingClientRect)
|
|
||||||
this.height = getRectFromEntry(this.entry).height;
|
|
||||||
|
|
||||||
if (onHeightChange && saveHeightKey) {
|
|
||||||
onHeightChange(saveHeightKey, id, this.height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hideIfNotIntersecting = () => {
|
|
||||||
if (!this.componentMounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// When the browser gets a chance, test if we're still not intersecting,
|
|
||||||
// and if so, set our isHidden to true to trigger an unrender. The point of
|
|
||||||
// this is to save DOM nodes and avoid using up too much memory.
|
|
||||||
this.setState((prevState) => ({ isHidden: !prevState.isIntersecting }));
|
|
||||||
}
|
|
||||||
|
|
||||||
handleRef = (node) => {
|
|
||||||
this.node = node;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { children, id, index, listLength, cachedHeight } = this.props;
|
|
||||||
const { isIntersecting, isHidden } = this.state;
|
|
||||||
|
|
||||||
if (!isIntersecting && (isHidden || cachedHeight)) {
|
|
||||||
return (
|
|
||||||
<article
|
|
||||||
ref={this.handleRef}
|
|
||||||
aria-posinset={index + 1}
|
|
||||||
aria-setsize={listLength}
|
|
||||||
style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
|
|
||||||
data-id={id}
|
|
||||||
tabIndex='0'
|
|
||||||
>
|
|
||||||
{children && React.cloneElement(children, { hidden: true })}
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<article ref={this.handleRef} aria-posinset={index + 1} aria-setsize={listLength} data-id={id} tabIndex='0'>
|
|
||||||
{children && React.cloneElement(children, { hidden: false })}
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import { Button } from 'soapbox/components/ui';
|
|
||||||
|
|
||||||
export default class LoadMore extends React.PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
disabled: PropTypes.bool,
|
|
||||||
visible: PropTypes.bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
visible: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { disabled, visible } = this.props;
|
|
||||||
|
|
||||||
if (!visible) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button theme='secondary' block disabled={disabled || !visible} onClick={this.props.onClick}>
|
|
||||||
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Button } from 'soapbox/components/ui';
|
||||||
|
|
||||||
|
interface ILoadMore {
|
||||||
|
onClick: () => void,
|
||||||
|
disabled?: boolean,
|
||||||
|
visible?: Boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoadMore: React.FC<ILoadMore> = ({ onClick, disabled, visible = true }) => {
|
||||||
|
if (!visible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button theme='primary' block disabled={disabled || !visible} onClick={onClick}>
|
||||||
|
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoadMore;
|
@ -1,68 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { getFeatures } from 'soapbox/utils/features';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
following: {
|
|
||||||
id: 'morefollows.following_label',
|
|
||||||
defaultMessage: '…and {count} more {count, plural, one {follow} other {follows}} on remote sites.',
|
|
||||||
},
|
|
||||||
followers: {
|
|
||||||
id: 'morefollows.followers_label',
|
|
||||||
defaultMessage: '…and {count} more {count, plural, one {follower} other {followers}} on remote sites.',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
|
||||||
const instance = state.get('instance');
|
|
||||||
|
|
||||||
return {
|
|
||||||
features: getFeatures(instance),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
|
||||||
@injectIntl
|
|
||||||
class MoreFollows extends React.PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
visible: PropTypes.bool,
|
|
||||||
count: PropTypes.number,
|
|
||||||
type: PropTypes.string,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
features: PropTypes.object.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
visible: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
getMessage = () => {
|
|
||||||
const { type, count, intl } = this.props;
|
|
||||||
return intl.formatMessage(messages[type], { count });
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { features } = this.props;
|
|
||||||
|
|
||||||
// If the instance isn't federating, there are no remote followers
|
|
||||||
if (!features.federating) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='morefollows-indicator'>
|
|
||||||
<div>
|
|
||||||
<div className='morefollows-indicator__label' style={{ visibility: this.props.visible ? 'visible' : 'hidden' }}>
|
|
||||||
{this.getMessage()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,49 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { useAppSelector } from 'soapbox/hooks';
|
||||||
|
import { getFeatures } from 'soapbox/utils/features';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
following: {
|
||||||
|
id: 'morefollows.following_label',
|
||||||
|
defaultMessage: '…and {count} more {count, plural, one {follow} other {follows}} on remote sites.',
|
||||||
|
},
|
||||||
|
followers: {
|
||||||
|
id: 'morefollows.followers_label',
|
||||||
|
defaultMessage: '…and {count} more {count, plural, one {follower} other {followers}} on remote sites.',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IMoreFollows {
|
||||||
|
visible?: Boolean,
|
||||||
|
count?: number,
|
||||||
|
type: 'following' | 'followers',
|
||||||
|
}
|
||||||
|
|
||||||
|
const MoreFollows: React.FC<IMoreFollows> = ({ visible = true, count, type }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const features = useAppSelector((state) => getFeatures(state.instance));
|
||||||
|
|
||||||
|
const getMessage = () => {
|
||||||
|
return intl.formatMessage(messages[type], { count });
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// If the instance isn't federating, there are no remote followers
|
||||||
|
if (!features.federating) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='morefollows-indicator'>
|
||||||
|
<div>
|
||||||
|
<div className='morefollows-indicator__label' style={{ visibility: visible ? 'visible' : 'hidden' }}>
|
||||||
|
{getMessage()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MoreFollows;
|
@ -1,17 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
|
|
||||||
export default class ProgressBar extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { progress } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='progress-bar'>
|
|
||||||
<div className='progress-bar__progress' style={{ width: `${Math.floor(progress*100)}%` }} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,62 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default class ProgressCircle extends React.PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
progress: PropTypes.number.isRequired,
|
|
||||||
radius: PropTypes.number,
|
|
||||||
stroke: PropTypes.number,
|
|
||||||
title: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
radius: 12,
|
|
||||||
stroke: 4,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { progress, radius, stroke, title } = this.props;
|
|
||||||
|
|
||||||
const progressStroke = stroke + 0.5;
|
|
||||||
const actualRadius = radius + progressStroke;
|
|
||||||
const circumference = 2 * Math.PI * radius;
|
|
||||||
const dashoffset = circumference * (1 - Math.min(progress, 1));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div title={title}>
|
|
||||||
<svg
|
|
||||||
width={actualRadius * 2}
|
|
||||||
height={actualRadius * 2}
|
|
||||||
viewBox={`0 0 ${actualRadius * 2} ${actualRadius * 2}`}
|
|
||||||
>
|
|
||||||
<circle
|
|
||||||
className='stroke-gray-400'
|
|
||||||
cx={actualRadius}
|
|
||||||
cy={actualRadius}
|
|
||||||
r={radius}
|
|
||||||
fill='none'
|
|
||||||
strokeWidth={stroke}
|
|
||||||
/>
|
|
||||||
<circle
|
|
||||||
className={classNames('stroke-primary-800', {
|
|
||||||
'stroke-danger-600': progress > 1,
|
|
||||||
})}
|
|
||||||
style={{
|
|
||||||
strokeDashoffset: dashoffset,
|
|
||||||
strokeDasharray: circumference,
|
|
||||||
}}
|
|
||||||
cx={actualRadius}
|
|
||||||
cy={actualRadius}
|
|
||||||
r={radius}
|
|
||||||
fill='none'
|
|
||||||
strokeWidth={progressStroke}
|
|
||||||
strokeLinecap='round'
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,52 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface IProgressCircle {
|
||||||
|
progress: number,
|
||||||
|
radius?: number,
|
||||||
|
stroke?: number,
|
||||||
|
title?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProgressCircle: React.FC<IProgressCircle> = ({ progress, radius = 12, stroke = 4, title }) => {
|
||||||
|
const progressStroke = stroke + 0.5;
|
||||||
|
const actualRadius = radius + progressStroke;
|
||||||
|
const circumference = 2 * Math.PI * radius;
|
||||||
|
const dashoffset = circumference * (1 - Math.min(progress, 1));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div title={title}>
|
||||||
|
<svg
|
||||||
|
width={actualRadius * 2}
|
||||||
|
height={actualRadius * 2}
|
||||||
|
viewBox={`0 0 ${actualRadius * 2} ${actualRadius * 2}`}
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
className='stroke-gray-400'
|
||||||
|
cx={actualRadius}
|
||||||
|
cy={actualRadius}
|
||||||
|
r={radius}
|
||||||
|
fill='none'
|
||||||
|
strokeWidth={stroke}
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
className={classNames('stroke-primary-800', {
|
||||||
|
'stroke-danger-600': progress > 1,
|
||||||
|
})}
|
||||||
|
style={{
|
||||||
|
strokeDashoffset: dashoffset,
|
||||||
|
strokeDasharray: circumference,
|
||||||
|
}}
|
||||||
|
cx={actualRadius}
|
||||||
|
cy={actualRadius}
|
||||||
|
r={radius}
|
||||||
|
fill='none'
|
||||||
|
strokeWidth={progressStroke}
|
||||||
|
strokeLinecap='round'
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProgressCircle;
|
@ -1,347 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import { List as ImmutableList } from 'immutable';
|
|
||||||
import { throttle } from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { PureComponent } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { withRouter } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { getSettings } from 'soapbox/actions/settings';
|
|
||||||
import PullToRefresh from 'soapbox/components/pull-to-refresh';
|
|
||||||
|
|
||||||
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
|
|
||||||
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
|
|
||||||
|
|
||||||
import LoadMore from './load_more';
|
|
||||||
import MoreFollows from './more_follows';
|
|
||||||
import { Spinner, Text } from './ui';
|
|
||||||
|
|
||||||
const MOUSE_IDLE_DELAY = 300;
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
|
||||||
const settings = getSettings(state);
|
|
||||||
|
|
||||||
return {
|
|
||||||
autoload: settings.get('autoloadMore'),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default @connect(mapStateToProps, null, null, { forwardRef: true })
|
|
||||||
@withRouter
|
|
||||||
class ScrollableList extends PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
scrollKey: PropTypes.string.isRequired,
|
|
||||||
onLoadMore: PropTypes.func,
|
|
||||||
isLoading: PropTypes.bool,
|
|
||||||
showLoading: PropTypes.bool,
|
|
||||||
hasMore: PropTypes.bool,
|
|
||||||
diffCount: PropTypes.number,
|
|
||||||
prepend: PropTypes.node,
|
|
||||||
alwaysPrepend: PropTypes.bool,
|
|
||||||
emptyMessage: PropTypes.node,
|
|
||||||
children: PropTypes.node,
|
|
||||||
onScrollToTop: PropTypes.func,
|
|
||||||
onScroll: PropTypes.func,
|
|
||||||
placeholderComponent: PropTypes.object,
|
|
||||||
placeholderCount: PropTypes.number,
|
|
||||||
autoload: PropTypes.bool,
|
|
||||||
onRefresh: PropTypes.func,
|
|
||||||
className: PropTypes.string,
|
|
||||||
location: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
cachedMediaWidth: 250, // Default media/card width using default theme
|
|
||||||
};
|
|
||||||
|
|
||||||
intersectionObserverWrapper = new IntersectionObserverWrapper();
|
|
||||||
|
|
||||||
mouseIdleTimer = null;
|
|
||||||
mouseMovedRecently = false;
|
|
||||||
lastScrollWasSynthetic = false;
|
|
||||||
scrollToTopOnMouseIdle = false;
|
|
||||||
|
|
||||||
setScrollTop = newScrollTop => {
|
|
||||||
if (this.documentElement.scrollTop !== newScrollTop) {
|
|
||||||
this.lastScrollWasSynthetic = true;
|
|
||||||
this.documentElement.scrollTop = newScrollTop;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
clearMouseIdleTimer = () => {
|
|
||||||
if (this.mouseIdleTimer === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearTimeout(this.mouseIdleTimer);
|
|
||||||
this.mouseIdleTimer = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMouseMove = throttle(() => {
|
|
||||||
// As long as the mouse keeps moving, clear and restart the idle timer.
|
|
||||||
this.clearMouseIdleTimer();
|
|
||||||
this.mouseIdleTimer = setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY);
|
|
||||||
|
|
||||||
if (!this.mouseMovedRecently && this.documentElement.scrollTop === 0) {
|
|
||||||
// Only set if we just started moving and are scrolled to the top.
|
|
||||||
this.scrollToTopOnMouseIdle = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save setting this flag for last, so we can do the comparison above.
|
|
||||||
this.mouseMovedRecently = true;
|
|
||||||
}, MOUSE_IDLE_DELAY / 2);
|
|
||||||
|
|
||||||
handleMouseIdle = () => {
|
|
||||||
if (this.scrollToTopOnMouseIdle) {
|
|
||||||
this.setScrollTop(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.mouseMovedRecently = false;
|
|
||||||
this.scrollToTopOnMouseIdle = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.window = window;
|
|
||||||
this.documentElement = document.scrollingElement || document.documentElement;
|
|
||||||
|
|
||||||
this.attachScrollListener();
|
|
||||||
this.attachIntersectionObserver();
|
|
||||||
// Handle initial scroll position
|
|
||||||
this.handleScroll();
|
|
||||||
}
|
|
||||||
|
|
||||||
getScrollPosition = () => {
|
|
||||||
if (this.documentElement && (this.documentElement.scrollTop > 0 || this.mouseMovedRecently)) {
|
|
||||||
return { height: this.documentElement.scrollHeight, top: this.documentElement.scrollTop };
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateScrollBottom = (snapshot) => {
|
|
||||||
const newScrollTop = this.documentElement.scrollHeight - snapshot;
|
|
||||||
|
|
||||||
this.setScrollTop(newScrollTop);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState, snapshot) {
|
|
||||||
// Reset the scroll position when a new child comes in in order not to
|
|
||||||
// jerk the scrollbar around if you're already scrolled down the page.
|
|
||||||
if (snapshot !== null) {
|
|
||||||
this.setScrollTop(this.documentElement.scrollHeight - snapshot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
attachScrollListener() {
|
|
||||||
this.window.addEventListener('scroll', this.handleScroll);
|
|
||||||
this.window.addEventListener('wheel', this.handleWheel);
|
|
||||||
}
|
|
||||||
|
|
||||||
detachScrollListener() {
|
|
||||||
this.window.removeEventListener('scroll', this.handleScroll);
|
|
||||||
this.window.removeEventListener('wheel', this.handleWheel);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleScroll = throttle(() => {
|
|
||||||
const { autoload } = this.props;
|
|
||||||
|
|
||||||
if (this.window) {
|
|
||||||
const { scrollTop, scrollHeight } = this.documentElement;
|
|
||||||
const { innerHeight } = this.window;
|
|
||||||
const offset = scrollHeight - scrollTop - innerHeight;
|
|
||||||
|
|
||||||
if (autoload && 400 > offset && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) {
|
|
||||||
this.props.onLoadMore();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scrollTop < 100 && this.props.onScrollToTop) {
|
|
||||||
this.props.onScrollToTop();
|
|
||||||
} else if (this.props.onScroll) {
|
|
||||||
this.props.onScroll();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.lastScrollWasSynthetic) {
|
|
||||||
// If the last scroll wasn't caused by setScrollTop(), assume it was
|
|
||||||
// intentional and cancel any pending scroll reset on mouse idle
|
|
||||||
this.scrollToTopOnMouseIdle = false;
|
|
||||||
}
|
|
||||||
this.lastScrollWasSynthetic = false;
|
|
||||||
}
|
|
||||||
}, 150, {
|
|
||||||
trailing: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
handleWheel = throttle(() => {
|
|
||||||
this.scrollToTopOnMouseIdle = false;
|
|
||||||
}, 150, {
|
|
||||||
trailing: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
getSnapshotBeforeUpdate(prevProps) {
|
|
||||||
const someItemInserted = React.Children.count(prevProps.children) > 0 &&
|
|
||||||
React.Children.count(prevProps.children) < React.Children.count(this.props.children) &&
|
|
||||||
this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props);
|
|
||||||
|
|
||||||
if (someItemInserted && (this.documentElement.scrollTop > 0 || this.mouseMovedRecently)) {
|
|
||||||
return this.documentElement.scrollHeight - this.documentElement.scrollTop;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cacheMediaWidth = (width) => {
|
|
||||||
if (width && this.state.cachedMediaWidth !== width) {
|
|
||||||
this.setState({ cachedMediaWidth: width });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.clearMouseIdleTimer();
|
|
||||||
this.detachScrollListener();
|
|
||||||
this.detachIntersectionObserver();
|
|
||||||
}
|
|
||||||
|
|
||||||
attachIntersectionObserver() {
|
|
||||||
this.intersectionObserverWrapper.connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
detachIntersectionObserver() {
|
|
||||||
this.intersectionObserverWrapper.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
getFirstChildKey(props) {
|
|
||||||
const { children } = props;
|
|
||||||
let firstChild = children;
|
|
||||||
|
|
||||||
if (children instanceof ImmutableList) {
|
|
||||||
firstChild = children.get(0);
|
|
||||||
} else if (Array.isArray(children)) {
|
|
||||||
firstChild = children[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return firstChild && firstChild.key;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleLoadMore = e => {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.onLoadMore();
|
|
||||||
}
|
|
||||||
|
|
||||||
getMoreFollows = () => {
|
|
||||||
const { scrollKey, isLoading, diffCount, hasMore } = this.props;
|
|
||||||
const isMoreFollows = ['followers', 'following'].some(k => k === scrollKey);
|
|
||||||
if (!(diffCount && isMoreFollows)) return null;
|
|
||||||
if (hasMore) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MoreFollows visible={!isLoading} count={diffCount} type={scrollKey} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setRef = c => {
|
|
||||||
this.node = c;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderLoading = () => {
|
|
||||||
const { className, prepend, placeholderComponent: Placeholder, placeholderCount } = this.props;
|
|
||||||
|
|
||||||
if (Placeholder && placeholderCount > 0) {
|
|
||||||
return (
|
|
||||||
<div role='feed' className={className}>
|
|
||||||
{Array(placeholderCount).fill().map((_, i) => (
|
|
||||||
<Placeholder key={i} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames('slist slist--flex', className)}>
|
|
||||||
<div role='feed' className='item-list'>
|
|
||||||
{prepend}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='slist__append'>
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderEmptyMessage = () => {
|
|
||||||
const { className, prepend, alwaysPrepend, emptyMessage } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames('mt-2', className)} ref={this.setRef}>
|
|
||||||
{alwaysPrepend && prepend}
|
|
||||||
|
|
||||||
<div className='bg-primary-50 dark:bg-slate-700 mt-2 rounded-lg text-center p-8'>
|
|
||||||
<Text>{emptyMessage}</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderFeed = () => {
|
|
||||||
const { className, children, scrollKey, isLoading, hasMore, prepend, onLoadMore, onRefresh, placeholderComponent: Placeholder } = this.props;
|
|
||||||
const childrenCount = React.Children.count(children);
|
|
||||||
const trackScroll = true; //placeholder
|
|
||||||
const loadMore = (hasMore && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
|
|
||||||
|
|
||||||
const feed = (
|
|
||||||
<div ref={this.setRef} onMouseMove={this.handleMouseMove}>
|
|
||||||
<div role='feed' className={className}>
|
|
||||||
{prepend}
|
|
||||||
|
|
||||||
{React.Children.map(children, (child, index) => (
|
|
||||||
<IntersectionObserverArticleContainer
|
|
||||||
key={child.key}
|
|
||||||
id={child.key}
|
|
||||||
index={index}
|
|
||||||
listLength={childrenCount}
|
|
||||||
intersectionObserverWrapper={this.intersectionObserverWrapper}
|
|
||||||
saveHeightKey={trackScroll ? `${this.props.location.key}:${scrollKey}` : null}
|
|
||||||
>
|
|
||||||
{React.cloneElement(child, {
|
|
||||||
getScrollPosition: this.getScrollPosition,
|
|
||||||
updateScrollBottom: this.updateScrollBottom,
|
|
||||||
cachedMediaWidth: this.state.cachedMediaWidth,
|
|
||||||
cacheMediaWidth: this.cacheMediaWidth,
|
|
||||||
})}
|
|
||||||
</IntersectionObserverArticleContainer>
|
|
||||||
))}
|
|
||||||
{(isLoading && Placeholder) && (
|
|
||||||
<Placeholder />
|
|
||||||
)}
|
|
||||||
{this.getMoreFollows()}
|
|
||||||
{loadMore}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (onRefresh) {
|
|
||||||
return (
|
|
||||||
<PullToRefresh onRefresh={onRefresh}>
|
|
||||||
{feed}
|
|
||||||
</PullToRefresh>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return feed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { children, showLoading, isLoading, hasMore, emptyMessage } = this.props;
|
|
||||||
const childrenCount = React.Children.count(children);
|
|
||||||
|
|
||||||
if (showLoading) {
|
|
||||||
return this.renderLoading();
|
|
||||||
} else if (isLoading || childrenCount > 0 || hasMore || !emptyMessage) {
|
|
||||||
return this.renderFeed();
|
|
||||||
} else {
|
|
||||||
return this.renderEmptyMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,167 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Virtuoso, Components } from 'react-virtuoso';
|
||||||
|
|
||||||
|
import PullToRefresh from 'soapbox/components/pull-to-refresh';
|
||||||
|
import { useSettings } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
import LoadMore from './load_more';
|
||||||
|
import { Spinner, Text } from './ui';
|
||||||
|
|
||||||
|
type Context = {
|
||||||
|
itemClassName?: string,
|
||||||
|
listClassName?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: It's crucial to space lists with **padding** instead of margin!
|
||||||
|
// Pass an `itemClassName` like `pb-3`, NOT a `space-y-3` className
|
||||||
|
// https://virtuoso.dev/troubleshooting#list-does-not-scroll-to-the-bottom--items-jump-around
|
||||||
|
const Item: Components<Context>['Item'] = ({ context, ...rest }) => (
|
||||||
|
<div className={context?.itemClassName} {...rest} />
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ensure the className winds up here
|
||||||
|
const List: Components<Context>['List'] = React.forwardRef((props, ref) => {
|
||||||
|
const { context, ...rest } = props;
|
||||||
|
return <div ref={ref} className={context?.listClassName} {...rest} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IScrollableList {
|
||||||
|
scrollKey?: string,
|
||||||
|
onLoadMore?: () => void,
|
||||||
|
isLoading?: boolean,
|
||||||
|
showLoading?: boolean,
|
||||||
|
hasMore?: boolean,
|
||||||
|
prepend?: React.ReactElement,
|
||||||
|
alwaysPrepend?: boolean,
|
||||||
|
emptyMessage?: React.ReactNode,
|
||||||
|
children: Iterable<React.ReactNode>,
|
||||||
|
onScrollToTop?: () => void,
|
||||||
|
onScroll?: () => void,
|
||||||
|
placeholderComponent?: React.ComponentType,
|
||||||
|
placeholderCount?: number,
|
||||||
|
onRefresh?: () => Promise<any>,
|
||||||
|
className?: string,
|
||||||
|
itemClassName?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Legacy ScrollableList with Virtuoso for backwards-compatibility */
|
||||||
|
const ScrollableList: React.FC<IScrollableList> = ({
|
||||||
|
prepend = null,
|
||||||
|
alwaysPrepend,
|
||||||
|
children,
|
||||||
|
isLoading,
|
||||||
|
emptyMessage,
|
||||||
|
showLoading,
|
||||||
|
onRefresh,
|
||||||
|
onScroll,
|
||||||
|
onScrollToTop,
|
||||||
|
onLoadMore,
|
||||||
|
className,
|
||||||
|
itemClassName,
|
||||||
|
hasMore,
|
||||||
|
placeholderComponent: Placeholder,
|
||||||
|
placeholderCount = 0,
|
||||||
|
}) => {
|
||||||
|
const settings = useSettings();
|
||||||
|
const autoloadMore = settings.get('autoloadMore');
|
||||||
|
|
||||||
|
/** Normalized children */
|
||||||
|
const elements = Array.from(children || []);
|
||||||
|
|
||||||
|
const showPlaceholder = showLoading && Placeholder && placeholderCount > 0;
|
||||||
|
|
||||||
|
// NOTE: We are doing some trickery to load a feed of placeholders
|
||||||
|
// Virtuoso's `EmptyPlaceholder` unfortunately doesn't work for our use-case
|
||||||
|
const data = showPlaceholder ? Array(placeholderCount).fill('') : elements;
|
||||||
|
const isEmpty = data.length === 0; // Yes, if it has placeholders it isn't "empty"
|
||||||
|
|
||||||
|
// Add a placeholder at the bottom for loading
|
||||||
|
// (Don't use Virtuoso's `Footer` component because it doesn't preserve its height)
|
||||||
|
if (hasMore && (autoloadMore || isLoading) && Placeholder) {
|
||||||
|
data.push(<Placeholder />);
|
||||||
|
} else if (hasMore && (autoloadMore || isLoading)) {
|
||||||
|
data.push(<Spinner />);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Render an empty state instead of the scrollable list */
|
||||||
|
const renderEmpty = (): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<div className='mt-2'>
|
||||||
|
{alwaysPrepend && prepend}
|
||||||
|
|
||||||
|
<div className='bg-primary-50 dark:bg-slate-700 mt-2 rounded-lg text-center p-8'>
|
||||||
|
{isLoading ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
<Text>{emptyMessage}</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Render a single item */
|
||||||
|
const renderItem = (_i: number, element: JSX.Element): JSX.Element => {
|
||||||
|
if (showPlaceholder) {
|
||||||
|
return <Placeholder />;
|
||||||
|
} else {
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEndReached = () => {
|
||||||
|
if (autoloadMore && hasMore && onLoadMore) {
|
||||||
|
onLoadMore();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadMore = () => {
|
||||||
|
if (autoloadMore || !hasMore || !onLoadMore) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return <LoadMore visible={!isLoading} onClick={onLoadMore} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Render the actual Virtuoso list */
|
||||||
|
const renderFeed = (): JSX.Element => (
|
||||||
|
<Virtuoso
|
||||||
|
useWindowScroll
|
||||||
|
className={className}
|
||||||
|
data={data}
|
||||||
|
startReached={onScrollToTop}
|
||||||
|
endReached={handleEndReached}
|
||||||
|
isScrolling={isScrolling => isScrolling && onScroll && onScroll()}
|
||||||
|
itemContent={renderItem}
|
||||||
|
context={{
|
||||||
|
listClassName: className,
|
||||||
|
itemClassName,
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
Header: () => prepend,
|
||||||
|
ScrollSeekPlaceholder: Placeholder as any,
|
||||||
|
EmptyPlaceholder: () => renderEmpty(),
|
||||||
|
List,
|
||||||
|
Item,
|
||||||
|
Footer: loadMore,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Conditionally render inner elements */
|
||||||
|
const renderBody = (): JSX.Element => {
|
||||||
|
if (isEmpty) {
|
||||||
|
return renderEmpty();
|
||||||
|
} else {
|
||||||
|
return renderFeed();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PullToRefresh onRefresh={onRefresh}>
|
||||||
|
{renderBody()}
|
||||||
|
</PullToRefresh>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScrollableList;
|
@ -1,65 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import IconButton from 'soapbox/components/icon_button';
|
|
||||||
import { FormPropTypes, InputContainer, LabelInputContainer } from 'soapbox/features/forms';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
showPassword: { id: 'forms.show_password', defaultMessage: 'Show password' },
|
|
||||||
hidePassword: { id: 'forms.hide_password', defaultMessage: 'Hide password' },
|
|
||||||
});
|
|
||||||
|
|
||||||
export default @injectIntl
|
|
||||||
class ShowablePassword extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
label: FormPropTypes.label,
|
|
||||||
className: PropTypes.string,
|
|
||||||
hint: PropTypes.node,
|
|
||||||
error: PropTypes.bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
|
||||||
revealed: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleReveal = () => {
|
|
||||||
if (this.props.onToggleVisibility) {
|
|
||||||
this.props.onToggleVisibility();
|
|
||||||
} else {
|
|
||||||
this.setState({ revealed: !this.state.revealed });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { intl, hint, error, label, className, ...props } = this.props;
|
|
||||||
const { revealed } = this.state;
|
|
||||||
|
|
||||||
const revealButton = (
|
|
||||||
<IconButton
|
|
||||||
src={revealed ? require('@tabler/icons/icons/eye-off.svg') : require('@tabler/icons/icons/eye.svg')}
|
|
||||||
onClick={this.toggleReveal}
|
|
||||||
title={intl.formatMessage(revealed ? messages.hidePassword : messages.showPassword)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<InputContainer {...this.props} extraClass={classNames('showable-password', className)}>
|
|
||||||
{label ? (
|
|
||||||
<LabelInputContainer label={label}>
|
|
||||||
<input {...props} type={revealed ? 'text' : 'password'} />
|
|
||||||
{revealButton}
|
|
||||||
</LabelInputContainer>
|
|
||||||
) : (<>
|
|
||||||
<input {...props} type={revealed ? 'text' : 'password'} />
|
|
||||||
{revealButton}
|
|
||||||
</>)}
|
|
||||||
</InputContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,58 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import IconButton from 'soapbox/components/icon_button';
|
||||||
|
import { InputContainer, LabelInputContainer } from 'soapbox/features/forms';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
showPassword: { id: 'forms.show_password', defaultMessage: 'Show password' },
|
||||||
|
hidePassword: { id: 'forms.hide_password', defaultMessage: 'Hide password' },
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IShowablePassword {
|
||||||
|
label?: React.ReactNode,
|
||||||
|
className?: string,
|
||||||
|
hint?: React.ReactNode,
|
||||||
|
error?: boolean,
|
||||||
|
onToggleVisibility?: () => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShowablePassword: React.FC<IShowablePassword> = (props) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const [revealed, setRevealed] = useState(false);
|
||||||
|
|
||||||
|
const { hint, error, label, className, ...rest } = props;
|
||||||
|
|
||||||
|
const toggleReveal = () => {
|
||||||
|
if (props.onToggleVisibility) {
|
||||||
|
props.onToggleVisibility();
|
||||||
|
} else {
|
||||||
|
setRevealed(!revealed);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const revealButton = (
|
||||||
|
<IconButton
|
||||||
|
src={revealed ? require('@tabler/icons/icons/eye-off.svg') : require('@tabler/icons/icons/eye.svg')}
|
||||||
|
onClick={toggleReveal}
|
||||||
|
title={intl.formatMessage(revealed ? messages.hidePassword : messages.showPassword)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputContainer {...props} extraClass={classNames('showable-password', className)}>
|
||||||
|
{label ? (
|
||||||
|
<LabelInputContainer label={label}>
|
||||||
|
<input {...rest} type={revealed ? 'text' : 'password'} />
|
||||||
|
{revealButton}
|
||||||
|
</LabelInputContainer>
|
||||||
|
) : (<>
|
||||||
|
<input {...rest} type={revealed ? 'text' : 'password'} />
|
||||||
|
{revealButton}
|
||||||
|
</>)}
|
||||||
|
</InputContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShowablePassword;
|
@ -1,297 +0,0 @@
|
|||||||
import classnames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { withRouter } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
|
||||||
import Icon from 'soapbox/components/icon';
|
|
||||||
import Poll from 'soapbox/components/poll';
|
|
||||||
import { addGreentext } from 'soapbox/utils/greentext';
|
|
||||||
import { onlyEmoji } from 'soapbox/utils/rich_content';
|
|
||||||
|
|
||||||
import { isRtl } from '../rtl';
|
|
||||||
|
|
||||||
import Permalink from './permalink';
|
|
||||||
|
|
||||||
const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
|
|
||||||
const BIG_EMOJI_LIMIT = 10;
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
greentext: getSoapboxConfig(state).get('greentext'),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
|
||||||
@withRouter
|
|
||||||
class StatusContent extends React.PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
status: ImmutablePropTypes.record.isRequired,
|
|
||||||
reblogContent: PropTypes.string,
|
|
||||||
expanded: PropTypes.bool,
|
|
||||||
onExpandedToggle: PropTypes.func,
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
collapsable: PropTypes.bool,
|
|
||||||
greentext: PropTypes.bool,
|
|
||||||
history: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
hidden: true,
|
|
||||||
collapsed: null, // `collapsed: null` indicates that an element doesn't need collapsing, while `true` or `false` indicates that it does (and is/isn't).
|
|
||||||
};
|
|
||||||
|
|
||||||
_updateStatusLinks() {
|
|
||||||
const node = this.node;
|
|
||||||
|
|
||||||
if (!node) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const links = node.querySelectorAll('a');
|
|
||||||
|
|
||||||
for (let i = 0; i < links.length; ++i) {
|
|
||||||
const link = links[i];
|
|
||||||
if (link.classList.contains('status-link')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
link.classList.add('status-link');
|
|
||||||
link.setAttribute('rel', 'nofollow noopener');
|
|
||||||
link.setAttribute('target', '_blank');
|
|
||||||
|
|
||||||
const mention = this.props.status.get('mentions').find(item => link.href === `${item.get('url')}`);
|
|
||||||
|
|
||||||
if (mention) {
|
|
||||||
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
|
|
||||||
link.setAttribute('title', mention.get('acct'));
|
|
||||||
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
|
|
||||||
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
|
|
||||||
} else {
|
|
||||||
link.setAttribute('title', link.href);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setCollapse() {
|
|
||||||
const node = this.node;
|
|
||||||
|
|
||||||
if (!node) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.props.collapsable
|
|
||||||
&& this.props.onClick
|
|
||||||
&& this.state.collapsed === null
|
|
||||||
&& this.props.status.get('spoiler_text').length === 0
|
|
||||||
) {
|
|
||||||
if (node.clientHeight > MAX_HEIGHT){
|
|
||||||
this.setState({ collapsed: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setOnlyEmoji = () => {
|
|
||||||
if (!this.node) return;
|
|
||||||
const only = onlyEmoji(this.node, BIG_EMOJI_LIMIT, true);
|
|
||||||
|
|
||||||
if (only !== this.state.onlyEmoji) {
|
|
||||||
this.setState({ onlyEmoji: only });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh = () => {
|
|
||||||
this.setCollapse();
|
|
||||||
this._updateStatusLinks();
|
|
||||||
this.setOnlyEmoji();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
onMentionClick = (mention, e) => {
|
|
||||||
if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.history.push(`/@${mention.get('acct')}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onHashtagClick = (hashtag, e) => {
|
|
||||||
hashtag = hashtag.replace(/^#/, '').toLowerCase();
|
|
||||||
|
|
||||||
if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.history.push(`/tags/${hashtag}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseDown = (e) => {
|
|
||||||
this.startXY = [e.clientX, e.clientY];
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseUp = (e) => {
|
|
||||||
if (!this.startXY) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [ startX, startY ] = this.startXY;
|
|
||||||
const [ deltaX, deltaY ] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)];
|
|
||||||
|
|
||||||
if (e.target.localName === 'button' || e.target.localName === 'a' || (e.target.parentNode && (e.target.parentNode.localName === 'button' || e.target.parentNode.localName === 'a'))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deltaX + deltaY < 5 && e.button === 0 && this.props.onClick) {
|
|
||||||
this.props.onClick();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.startXY = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSpoilerClick = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (this.props.onExpandedToggle) {
|
|
||||||
// The parent manages the state
|
|
||||||
this.props.onExpandedToggle();
|
|
||||||
} else {
|
|
||||||
this.setState({ hidden: !this.state.hidden });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCollapsedClick = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
this.setState({ collapsed: !this.state.collapsed });
|
|
||||||
}
|
|
||||||
|
|
||||||
setRef = (c) => {
|
|
||||||
this.node = c;
|
|
||||||
}
|
|
||||||
|
|
||||||
parseHtml = html => {
|
|
||||||
const { greentext } = this.props;
|
|
||||||
if (greentext) return addGreentext(html);
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
getHtmlContent = () => {
|
|
||||||
const { status } = this.props;
|
|
||||||
const html = status.get('contentHtml');
|
|
||||||
return this.parseHtml(html);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { status } = this.props;
|
|
||||||
const { onlyEmoji } = this.state;
|
|
||||||
|
|
||||||
if (status.get('content').length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
|
|
||||||
|
|
||||||
const content = { __html: this.getHtmlContent() };
|
|
||||||
const spoilerContent = { __html: status.get('spoilerHtml') };
|
|
||||||
const directionStyle = { direction: 'ltr' };
|
|
||||||
const classNames = classnames('status__content', {
|
|
||||||
'status__content--with-action': this.props.onClick && this.props.history,
|
|
||||||
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
|
|
||||||
'status__content--collapsed': this.state.collapsed === true,
|
|
||||||
'status__content--big': onlyEmoji,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isRtl(status.get('search_index'))) {
|
|
||||||
directionStyle.direction = 'rtl';
|
|
||||||
}
|
|
||||||
|
|
||||||
const readMoreButton = (
|
|
||||||
<button className='status__content__read-more-button' onClick={this.props.onClick} key='read-more'>
|
|
||||||
<FormattedMessage id='status.read_more' defaultMessage='Read more' /><Icon id='angle-right' fixedWidth />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (status.get('spoiler_text').length > 0) {
|
|
||||||
let mentionsPlaceholder = '';
|
|
||||||
|
|
||||||
const mentionLinks = status.get('mentions').map(item => (
|
|
||||||
<Permalink to={`/@${item.get('acct')}`} href={`/@${item.get('acct')}`} key={item.get('id')} className='mention'>
|
|
||||||
@<span>{item.get('username')}</span>
|
|
||||||
</Permalink>
|
|
||||||
)).reduce((aggregate, item) => [...aggregate, item, ' '], []);
|
|
||||||
|
|
||||||
const toggleText = hidden ? <FormattedMessage id='status.show_more' defaultMessage='Show more' /> : <FormattedMessage id='status.show_less' defaultMessage='Show less' />;
|
|
||||||
|
|
||||||
if (hidden) {
|
|
||||||
mentionsPlaceholder = <div>{mentionLinks}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
|
|
||||||
<p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
|
|
||||||
<span dangerouslySetInnerHTML={spoilerContent} lang={status.get('language')} />
|
|
||||||
{' '}
|
|
||||||
<button tabIndex='0' className={`status__content__spoiler-link ${hidden ? 'status__content__spoiler-link--show-more' : 'status__content__spoiler-link--show-less'}`} onClick={this.handleSpoilerClick}>{toggleText}</button>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{mentionsPlaceholder}
|
|
||||||
|
|
||||||
<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} lang={status.get('language')} />
|
|
||||||
|
|
||||||
{!hidden && !!status.get('poll') && <Poll id={status.get('poll')} status={status.get('url')} />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (this.props.onClick) {
|
|
||||||
const output = [
|
|
||||||
<div
|
|
||||||
ref={this.setRef}
|
|
||||||
tabIndex='0'
|
|
||||||
key='content'
|
|
||||||
className={classNames}
|
|
||||||
style={directionStyle}
|
|
||||||
dangerouslySetInnerHTML={content}
|
|
||||||
lang={status.get('language')}
|
|
||||||
onMouseDown={this.handleMouseDown}
|
|
||||||
onMouseUp={this.handleMouseUp}
|
|
||||||
/>,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (this.state.collapsed) {
|
|
||||||
output.push(readMoreButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.get('poll')) {
|
|
||||||
output.push(<Poll id={status.get('poll')} key='poll' status={status.get('url')} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
} else {
|
|
||||||
const output = [
|
|
||||||
<div
|
|
||||||
ref={this.setRef}
|
|
||||||
tabIndex='0'
|
|
||||||
key='content'
|
|
||||||
className={classnames('status__content', {
|
|
||||||
'status__content--big': onlyEmoji,
|
|
||||||
})}
|
|
||||||
style={directionStyle}
|
|
||||||
dangerouslySetInnerHTML={content}
|
|
||||||
lang={status.get('language')}
|
|
||||||
/>,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (status.get('poll')) {
|
|
||||||
output.push(<Poll id={status.get('poll')} key='poll' status={status.get('url')} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,304 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
import Icon from 'soapbox/components/icon';
|
||||||
|
import Poll from 'soapbox/components/poll';
|
||||||
|
import { useSoapboxConfig } from 'soapbox/hooks';
|
||||||
|
import { addGreentext } from 'soapbox/utils/greentext';
|
||||||
|
import { onlyEmoji as isOnlyEmoji } from 'soapbox/utils/rich_content';
|
||||||
|
|
||||||
|
import { isRtl } from '../rtl';
|
||||||
|
|
||||||
|
import type { Status, Mention } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
|
||||||
|
const BIG_EMOJI_LIMIT = 10;
|
||||||
|
|
||||||
|
type Point = [
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
]
|
||||||
|
|
||||||
|
interface IReadMoreButton {
|
||||||
|
onClick: React.MouseEventHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Button to expand a truncated status (due to too much content) */
|
||||||
|
const ReadMoreButton: React.FC<IReadMoreButton> = ({ onClick }) => (
|
||||||
|
<button className='status__content__read-more-button' onClick={onClick}>
|
||||||
|
<FormattedMessage id='status.read_more' defaultMessage='Read more' />
|
||||||
|
<Icon id='angle-right' fixedWidth />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface ISpoilerButton {
|
||||||
|
onClick: React.MouseEventHandler,
|
||||||
|
hidden: boolean,
|
||||||
|
tabIndex?: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Button to expand status text behind a content warning */
|
||||||
|
const SpoilerButton: React.FC<ISpoilerButton> = ({ onClick, hidden, tabIndex }) => (
|
||||||
|
<button
|
||||||
|
tabIndex={tabIndex}
|
||||||
|
className={classNames(
|
||||||
|
'inline-block rounded-md px-1.5 py-0.5 ml-[0.5em]',
|
||||||
|
'text-black dark:text-white',
|
||||||
|
'font-bold text-[11px] uppercase',
|
||||||
|
'bg-primary-100 dark:bg-primary-900',
|
||||||
|
'hover:bg-primary-300 dark:hover:bg-primary-600',
|
||||||
|
'focus:bg-primary-200 dark:focus:bg-primary-600',
|
||||||
|
'hover:no-underline',
|
||||||
|
'duration-100',
|
||||||
|
)}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{hidden ? (
|
||||||
|
<FormattedMessage id='status.show_more' defaultMessage='Show more' />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage id='status.show_less' defaultMessage='Show less' />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface IStatusContent {
|
||||||
|
status: Status,
|
||||||
|
expanded?: boolean,
|
||||||
|
onExpandedToggle?: () => void,
|
||||||
|
onClick?: () => void,
|
||||||
|
collapsable?: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Renders the text content of a status */
|
||||||
|
const StatusContent: React.FC<IStatusContent> = ({ status, expanded = false, onExpandedToggle, onClick, collapsable = false }) => {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const [hidden, setHidden] = useState(true);
|
||||||
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
const [onlyEmoji, setOnlyEmoji] = useState(false);
|
||||||
|
|
||||||
|
const startXY = useRef<Point>();
|
||||||
|
const node = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const { greentext } = useSoapboxConfig();
|
||||||
|
|
||||||
|
const onMentionClick = (mention: Mention, e: MouseEvent) => {
|
||||||
|
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
history.push(`/@${mention.acct}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onHashtagClick = (hashtag: string, e: MouseEvent) => {
|
||||||
|
hashtag = hashtag.replace(/^#/, '').toLowerCase();
|
||||||
|
|
||||||
|
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
history.push(`/tags/${hashtag}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** For regular links, just stop propogation */
|
||||||
|
const onLinkClick = (e: MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateStatusLinks = () => {
|
||||||
|
if (!node.current) return;
|
||||||
|
|
||||||
|
const links = node.current.querySelectorAll('a');
|
||||||
|
|
||||||
|
links.forEach(link => {
|
||||||
|
// Skip already processed
|
||||||
|
if (link.classList.contains('status-link')) return;
|
||||||
|
|
||||||
|
// Add attributes
|
||||||
|
link.classList.add('status-link');
|
||||||
|
link.setAttribute('rel', 'nofollow noopener');
|
||||||
|
link.setAttribute('target', '_blank');
|
||||||
|
|
||||||
|
const mention = status.mentions.find(mention => link.href === `${mention.url}`);
|
||||||
|
|
||||||
|
// Add event listeners on mentions and hashtags
|
||||||
|
if (mention) {
|
||||||
|
link.addEventListener('click', onMentionClick.bind(link, mention), false);
|
||||||
|
link.setAttribute('title', mention.acct);
|
||||||
|
} else if (link.textContent?.charAt(0) === '#' || (link.previousSibling?.textContent?.charAt(link.previousSibling.textContent.length - 1) === '#')) {
|
||||||
|
link.addEventListener('click', onHashtagClick.bind(link, link.text), false);
|
||||||
|
} else {
|
||||||
|
link.setAttribute('title', link.href);
|
||||||
|
link.addEventListener('click', onLinkClick.bind(link), false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const maybeSetCollapsed = (): void => {
|
||||||
|
if (!node.current) return;
|
||||||
|
|
||||||
|
if (collapsable && onClick && !collapsed && status.spoiler_text.length === 0) {
|
||||||
|
if (node.current.clientHeight > MAX_HEIGHT) {
|
||||||
|
setCollapsed(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const maybeSetOnlyEmoji = (): void => {
|
||||||
|
if (!node.current) return;
|
||||||
|
const only = isOnlyEmoji(node.current, BIG_EMOJI_LIMIT, true);
|
||||||
|
|
||||||
|
if (only !== onlyEmoji) {
|
||||||
|
setOnlyEmoji(only);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const refresh = (): void => {
|
||||||
|
maybeSetCollapsed();
|
||||||
|
maybeSetOnlyEmoji();
|
||||||
|
updateStatusLinks();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleMouseDown: React.EventHandler<React.MouseEvent> = (e) => {
|
||||||
|
startXY.current = [e.clientX, e.clientY];
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp: React.EventHandler<React.MouseEvent> = (e) => {
|
||||||
|
if (!startXY.current) return;
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
const parentNode = target.parentNode as HTMLElement;
|
||||||
|
|
||||||
|
const [ startX, startY ] = startXY.current;
|
||||||
|
const [ deltaX, deltaY ] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)];
|
||||||
|
|
||||||
|
if (target.localName === 'button' || target.localName === 'a' || (parentNode && (parentNode.localName === 'button' || parentNode.localName === 'a'))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deltaX + deltaY < 5 && e.button === 0 && onClick) {
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
|
|
||||||
|
startXY.current = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSpoilerClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (onExpandedToggle) {
|
||||||
|
// The parent manages the state
|
||||||
|
onExpandedToggle();
|
||||||
|
} else {
|
||||||
|
setHidden(!hidden);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHtmlContent = (): string => {
|
||||||
|
const { contentHtml: html } = status;
|
||||||
|
if (greentext) return addGreentext(html);
|
||||||
|
return html;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (status.content.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isHidden = onExpandedToggle ? !expanded : hidden;
|
||||||
|
|
||||||
|
const content = { __html: getHtmlContent() };
|
||||||
|
const spoilerContent = { __html: status.spoilerHtml };
|
||||||
|
const directionStyle: React.CSSProperties = { direction: 'ltr' };
|
||||||
|
const className = classNames('status__content', {
|
||||||
|
'status__content--with-action': onClick,
|
||||||
|
'status__content--with-spoiler': status.spoiler_text.length > 0,
|
||||||
|
'status__content--collapsed': collapsed,
|
||||||
|
'status__content--big': onlyEmoji,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isRtl(status.search_index)) {
|
||||||
|
directionStyle.direction = 'rtl';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.spoiler_text.length > 0) {
|
||||||
|
return (
|
||||||
|
<div className={className} ref={node} tabIndex={0} style={directionStyle} onMouseDown={handleMouseDown} onMouseUp={handleMouseUp}>
|
||||||
|
<p style={{ marginBottom: isHidden && status.mentions.isEmpty() ? 0 : undefined }}>
|
||||||
|
<span dangerouslySetInnerHTML={spoilerContent} lang={status.language || undefined} />
|
||||||
|
|
||||||
|
<SpoilerButton
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={handleSpoilerClick}
|
||||||
|
hidden={isHidden}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
tabIndex={!isHidden ? 0 : undefined}
|
||||||
|
className={classNames('status__content__text', {
|
||||||
|
'status__content__text--visible': !isHidden,
|
||||||
|
})}
|
||||||
|
style={directionStyle}
|
||||||
|
dangerouslySetInnerHTML={content}
|
||||||
|
lang={status.language || undefined}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!isHidden && status.poll && typeof status.poll === 'string' && (
|
||||||
|
<Poll id={status.poll} status={status.url} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (onClick) {
|
||||||
|
const output = [
|
||||||
|
<div
|
||||||
|
ref={node}
|
||||||
|
tabIndex={0}
|
||||||
|
key='content'
|
||||||
|
className={className}
|
||||||
|
style={directionStyle}
|
||||||
|
dangerouslySetInnerHTML={content}
|
||||||
|
lang={status.language || undefined}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
|
/>,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (collapsed) {
|
||||||
|
output.push(<ReadMoreButton onClick={onClick} key='read-more' />);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.poll && typeof status.poll === 'string') {
|
||||||
|
output.push(<Poll id={status.poll} key='poll' status={status.url} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{output}</>;
|
||||||
|
} else {
|
||||||
|
const output = [
|
||||||
|
<div
|
||||||
|
ref={node}
|
||||||
|
tabIndex={0}
|
||||||
|
key='content'
|
||||||
|
className={classNames('status__content', {
|
||||||
|
'status__content--big': onlyEmoji,
|
||||||
|
})}
|
||||||
|
style={directionStyle}
|
||||||
|
dangerouslySetInnerHTML={content}
|
||||||
|
lang={status.language || undefined}
|
||||||
|
/>,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (status.poll && typeof status.poll === 'string') {
|
||||||
|
output.push(<Poll id={status.poll} key='poll' status={status.url} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{output}</>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatusContent;
|
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
||||||
|
|
||||||
|
interface ICounter {
|
||||||
|
/** Number this counter should display. */
|
||||||
|
count: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A simple counter for notifications, etc. */
|
||||||
|
const Counter: React.FC<ICounter> = ({ count }) => {
|
||||||
|
return (
|
||||||
|
<span className='block px-1.5 py-0.5 bg-accent-500 text-xs text-white rounded-full ring-2 ring-white dark:ring-slate-800'>
|
||||||
|
{shortNumberFormat(count)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Counter;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue