diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a9c43e5ce..7d60fe637 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,6 +14,7 @@ stages: - lint - test - build + - deploy before_script: - yarn @@ -39,6 +40,16 @@ build-production: paths: - static +docs-deploy: + stage: deploy + image: alpine:latest + before_script: + - apk add curl + script: + - curl -X POST -F"token=$CI_JOB_TOKEN" -F'ref=master' https://gitlab.com/api/v4/projects/15685485/trigger/pipeline + only: + - develop + # Supposed to fail when translations are outdated, instead always passes # # i18n: diff --git a/app/soapbox/__fixtures__/admin_api_frontend_config.json b/app/soapbox/__fixtures__/admin_api_frontend_config.json new file mode 100644 index 000000000..ee37450f8 --- /dev/null +++ b/app/soapbox/__fixtures__/admin_api_frontend_config.json @@ -0,0 +1,55 @@ +{ + "configs": [ + { + "group": ":pleroma", + "key": ":frontend_configurations", + "value": [ + { + "tuple": [ + ":soapbox_fe", + { + "logo": "blob:http://localhost:3036/0cdfa863-6889-4199-b870-4942cedd364f", + "banner": "blob:http://localhost:3036/a835afed-6078-45bd-92b4-7ffd858c3eca", + "brandColor": "#254f92", + "customCss": [ + "/instance/static/custom.css" + ], + "promoPanel": { + "items": [ + { + "icon": "globe", + "text": "blog", + "url": "https://teci.world/blog" + }, + { + "icon": "globe", + "text": "book", + "url": "https://teci.world/book" + } + ] + }, + "extensions": { + "patron": false + }, + "defaultSettings": { + "autoPlayGif": false + }, + "navlinks": { + "homeFooter": [ + { + "title": "about", + "url": "/instance/about/index.html" + }, + { + "title": "tos", + "url": "/instance/about/tos.html" + } + ] + } + } + ] + } + ] + } + ] +} diff --git a/app/soapbox/__fixtures__/config_db.json b/app/soapbox/__fixtures__/config_db.json new file mode 100644 index 000000000..240164bb6 --- /dev/null +++ b/app/soapbox/__fixtures__/config_db.json @@ -0,0 +1,2735 @@ +{ + "configs": [ + { + "group": ":phoenix", + "key": ":format_encoders", + "value": [ + { + "tuple": [ + ":json", + "Jason" + ] + } + ] + }, + { + "group": ":phoenix", + "key": ":json_library", + "value": "Jason" + }, + { + "group": ":phoenix", + "key": ":filter_parameters", + "value": [ + "password", + "confirm" + ] + }, + { + "group": ":phoenix", + "key": ":stacktrace_depth", + "value": 20 + }, + { + "group": ":logger", + "key": ":ex_syslogger", + "value": [ + { + "tuple": [ + ":level", + ":debug" + ] + }, + { + "tuple": [ + ":ident", + "pleroma" + ] + }, + { + "tuple": [ + ":format", + "$metadata[$level] $message" + ] + }, + { + "tuple": [ + ":metadata", + [ + ":request_id" + ] + ] + } + ] + }, + { + "group": ":logger", + "key": ":console", + "value": [ + { + "tuple": [ + ":level", + ":debug" + ] + }, + { + "tuple": [ + ":metadata", + [ + ":request_id" + ] + ] + }, + { + "tuple": [ + ":format", + "[$level] $message\n" + ] + } + ] + }, + { + "group": ":floki", + "key": ":html_parser", + "value": "Floki.HTMLParser.FastHtml" + }, + { + "group": ":tzdata", + "key": ":http_client", + "value": "Pleroma.HTTP.Tzdata" + }, + { + "group": ":http_signatures", + "key": ":adapter", + "value": "Pleroma.Signature" + }, + { + "group": ":prometheus", + "key": "Pleroma.Web.Endpoint.MetricsExporter", + "value": [ + { + "tuple": [ + ":path", + "/api/pleroma/app_metrics" + ] + } + ] + }, + { + "group": ":ueberauth", + "key": "Ueberauth", + "value": [ + { + "tuple": [ + ":base_path", + "/oauth" + ] + }, + { + "tuple": [ + ":providers", + [] + ] + } + ] + }, + { + "group": ":esshd", + "key": ":enabled", + "value": false + }, + { + "group": ":cors_plug", + "key": ":max_age", + "value": 86400 + }, + { + "group": ":cors_plug", + "key": ":methods", + "value": [ + "POST", + "PUT", + "DELETE", + "GET", + "PATCH", + "OPTIONS" + ] + }, + { + "group": ":cors_plug", + "key": ":expose", + "value": [ + "Link", + "X-RateLimit-Reset", + "X-RateLimit-Limit", + "X-RateLimit-Remaining", + "X-Request-Id", + "Idempotency-Key" + ] + }, + { + "group": ":cors_plug", + "key": ":credentials", + "value": true + }, + { + "group": ":cors_plug", + "key": ":headers", + "value": [ + "Authorization", + "Content-Type", + "Idempotency-Key" + ] + }, + { + "group": ":mime", + "key": ":types", + "value": { + "application/activity+json": [ + "activity+json" + ], + "application/jrd+json": [ + "jrd+json" + ], + "application/ld+json": [ + "activity+json" + ], + "application/xml": [ + "xml" + ], + "application/xrd+xml": [ + "xrd+xml" + ] + } + }, + { + "group": ":quack", + "key": ":level", + "value": ":warn" + }, + { + "group": ":quack", + "key": ":meta", + "value": [ + ":all" + ] + }, + { + "group": ":quack", + "key": ":webhook_url", + "value": "https://hooks.slack.com/services/YOUR-KEY-HERE" + }, + { + "db": [ + ":subject", + ":public_key", + ":private_key" + ], + "group": ":web_push_encryption", + "key": ":vapid_details", + "value": [ + { + "tuple": [ + ":subject", + "mailto:alex@alexgleason.me" + ] + }, + { + "tuple": [ + ":public_key", + "BAlKFlwdC-9z36ObeNyiIRdGT0luMx-SDEQzrsIRLWvcspqMU7oIhT9HbgTo2gNt8lhtKoOyiQEH9IQqUxwmBp0" + ] + }, + { + "tuple": [ + ":private_key", + "o6y0A1DtjJGURKJ2RH4BLAHuqG8RcD1rDqxrUOo8wIw" + ] + } + ] + }, + { + "group": ":ex_aws", + "key": ":http_client", + "value": "Pleroma.HTTP.ExAws" + }, + { + "db": [ + ":access_key_id", + ":secret_access_key", + ":scheme", + ":host", + ":region" + ], + "group": ":ex_aws", + "key": ":s3", + "value": [ + { + "tuple": [ + ":access_key_id", + "3WJHLX5DH6LQT5NKXKU2" + ] + }, + { + "tuple": [ + ":secret_access_key", + "6Zdlw6XKtmlvvj1to1B25YlEpBAG5ahEs2ExaEqBG4k" + ] + }, + { + "tuple": [ + ":scheme", + "https://" + ] + }, + { + "tuple": [ + ":host", + "sfo2.digitaloceanspaces.com" + ] + }, + { + "tuple": [ + ":region", + "sfo2" + ] + } + ] + }, + { + "db": [ + ":default_signer" + ], + "group": ":joken", + "key": ":default_signer", + "value": "AvRdJr2XiCKeLDrU33rsKA1nTzu1aHypRDpRDCmN00oSHM8+f7Z9BkilF6nWwwv6" + }, + { + "group": ":pleroma", + "key": ":ecto_repos", + "value": [ + "Pleroma.Repo" + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Captcha", + "value": [ + { + "tuple": [ + ":enabled", + true + ] + }, + { + "tuple": [ + ":seconds_valid", + 300 + ] + }, + { + "tuple": [ + ":method", + "Pleroma.Captcha.Native" + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Captcha.Kocaptcha", + "value": [ + { + "tuple": [ + ":endpoint", + "https://captcha.kotobank.ch" + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":emoji", + "value": [ + { + "tuple": [ + ":shortcode_globs", + [ + "/emoji/custom/**/*.png" + ] + ] + }, + { + "tuple": [ + ":pack_extensions", + [ + ".png", + ".gif" + ] + ] + }, + { + "tuple": [ + ":groups", + [ + { + "tuple": [ + ":Custom", + [ + "/emoji/*.png", + "/emoji/**/*.png" + ] + ] + } + ] + ] + }, + { + "tuple": [ + ":default_manifest", + "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json" + ] + }, + { + "tuple": [ + ":shared_pack_cache_seconds_per_file", + 60 + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":uri_schemes", + "value": [ + { + "tuple": [ + ":valid_schemes", + [ + "https", + "http", + "dat", + "dweb", + "gopher", + "hyper", + "ipfs", + "ipns", + "irc", + "ircs", + "magnet", + "mailto", + "mumble", + "ssb", + "xmpp" + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":http", + "value": [ + { + "tuple": [ + ":proxy_url", + null + ] + }, + { + "tuple": [ + ":send_user_agent", + true + ] + }, + { + "tuple": [ + ":user_agent", + ":default" + ] + }, + { + "tuple": [ + ":adapter", + [] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":welcome", + "value": [ + { + "tuple": [ + ":direct_message", + [ + { + "tuple": [ + ":enabled", + false + ] + }, + { + "tuple": [ + ":sender_nickname", + null + ] + }, + { + "tuple": [ + ":message", + null + ] + } + ] + ] + }, + { + "tuple": [ + ":chat_message", + [ + { + "tuple": [ + ":enabled", + false + ] + }, + { + "tuple": [ + ":sender_nickname", + null + ] + }, + { + "tuple": [ + ":message", + null + ] + } + ] + ] + }, + { + "tuple": [ + ":email", + [ + { + "tuple": [ + ":enabled", + false + ] + }, + { + "tuple": [ + ":sender", + null + ] + }, + { + "tuple": [ + ":subject", + "Welcome to <%= instance_name %>" + ] + }, + { + "tuple": [ + ":html", + "Welcome to <%= instance_name %>" + ] + }, + { + "tuple": [ + ":text", + "Welcome to <%= instance_name %>" + ] + } + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":feed", + "value": [ + { + "tuple": [ + ":post_title", + { + ":max_length": 100, + ":omission": "..." + } + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":markup", + "value": [ + { + "tuple": [ + ":allow_inline_images", + true + ] + }, + { + "tuple": [ + ":allow_headings", + false + ] + }, + { + "tuple": [ + ":allow_tables", + false + ] + }, + { + "tuple": [ + ":allow_fonts", + false + ] + }, + { + "tuple": [ + ":scrub_policy", + [ + "Pleroma.HTML.Scrubber.Default", + "Pleroma.HTML.Transform.MediaProxy" + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":assets", + "value": [ + { + "tuple": [ + ":mascots", + [ + { + "tuple": [ + ":pleroma_fox_tan", + { + ":mime_type": "image/png", + ":url": "/images/pleroma-fox-tan-smol.png" + } + ] + }, + { + "tuple": [ + ":pleroma_fox_tan_shy", + { + ":mime_type": "image/png", + ":url": "/images/pleroma-fox-tan-shy.png" + } + ] + } + ] + ] + }, + { + "tuple": [ + ":default_mascot", + ":pleroma_fox_tan" + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":manifest", + "value": [ + { + "tuple": [ + ":icons", + [ + { + ":src": "/static/logo.png", + ":type": "image/png" + } + ] + ] + }, + { + "tuple": [ + ":theme_color", + "#282c37" + ] + }, + { + "tuple": [ + ":background_color", + "#191b22" + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":activitypub", + "value": [ + { + "tuple": [ + ":unfollow_blocked", + true + ] + }, + { + "tuple": [ + ":outgoing_blocks", + true + ] + }, + { + "tuple": [ + ":follow_handshake_timeout", + 500 + ] + }, + { + "tuple": [ + ":note_replies_output_limit", + 5 + ] + }, + { + "tuple": [ + ":sign_object_fetches", + true + ] + }, + { + "tuple": [ + ":authorized_fetch_mode", + false + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":streamer", + "value": [ + { + "tuple": [ + ":workers", + 3 + ] + }, + { + "tuple": [ + ":overflow_workers", + 2 + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":user", + "value": [ + { + "tuple": [ + ":deny_follow_blocked", + true + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_normalize_markup", + "value": [ + { + "tuple": [ + ":scrub_policy", + "Pleroma.HTML.Scrubber.Default" + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_rejectnonpublic", + "value": [ + { + "tuple": [ + ":allow_followersonly", + false + ] + }, + { + "tuple": [ + ":allow_direct", + false + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_hellthread", + "value": [ + { + "tuple": [ + ":delist_threshold", + 10 + ] + }, + { + "tuple": [ + ":reject_threshold", + 20 + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_simple", + "value": [ + { + "tuple": [ + ":media_removal", + [] + ] + }, + { + "tuple": [ + ":media_nsfw", + [] + ] + }, + { + "tuple": [ + ":federated_timeline_removal", + [] + ] + }, + { + "tuple": [ + ":report_removal", + [] + ] + }, + { + "tuple": [ + ":reject", + [] + ] + }, + { + "tuple": [ + ":followers_only", + [] + ] + }, + { + "tuple": [ + ":accept", + [] + ] + }, + { + "tuple": [ + ":avatar_removal", + [] + ] + }, + { + "tuple": [ + ":banner_removal", + [] + ] + }, + { + "tuple": [ + ":reject_deletes", + [] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_keyword", + "value": [ + { + "tuple": [ + ":reject", + [] + ] + }, + { + "tuple": [ + ":federated_timeline_removal", + [] + ] + }, + { + "tuple": [ + ":replace", + [] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_subchain", + "value": [ + { + "tuple": [ + ":match_actor", + {} + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_activity_expiration", + "value": [ + { + "tuple": [ + ":days", + 365 + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_vocabulary", + "value": [ + { + "tuple": [ + ":accept", + [] + ] + }, + { + "tuple": [ + ":reject", + [] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_object_age", + "value": [ + { + "tuple": [ + ":threshold", + 604800 + ] + }, + { + "tuple": [ + ":actions", + [ + ":delist", + ":strip_followers" + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Web.MediaProxy.Invalidation.Http", + "value": [ + { + "tuple": [ + ":method", + ":purge" + ] + }, + { + "tuple": [ + ":headers", + [] + ] + }, + { + "tuple": [ + ":options", + [] + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Web.MediaProxy.Invalidation.Script", + "value": [ + { + "tuple": [ + ":script_path", + null + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":chat", + "value": [ + { + "tuple": [ + ":enabled", + true + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":gopher", + "value": [ + { + "tuple": [ + ":enabled", + false + ] + }, + { + "tuple": [ + ":ip", + { + "tuple": [ + 0, + 0, + 0, + 0 + ] + } + ] + }, + { + "tuple": [ + ":port", + 9999 + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Web.Metadata", + "value": [ + { + "tuple": [ + ":providers", + [ + "Pleroma.Web.Metadata.Providers.OpenGraph", + "Pleroma.Web.Metadata.Providers.TwitterCard", + "Pleroma.Web.Metadata.Providers.RelMe", + "Pleroma.Web.Metadata.Providers.Feed" + ] + ] + }, + { + "tuple": [ + ":unfurl_nsfw", + false + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Web.Preload", + "value": [ + { + "tuple": [ + ":providers", + [ + "Pleroma.Web.Preload.Providers.Instance" + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":http_security", + "value": [ + { + "tuple": [ + ":enabled", + true + ] + }, + { + "tuple": [ + ":sts", + false + ] + }, + { + "tuple": [ + ":sts_max_age", + 31536000 + ] + }, + { + "tuple": [ + ":ct_max_age", + 2592000 + ] + }, + { + "tuple": [ + ":referrer_policy", + "same-origin" + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.User", + "value": [ + { + "tuple": [ + ":restricted_nicknames", + [ + ".well-known", + "~", + "about", + "activities", + "api", + "auth", + "check_password", + "dev", + "friend-requests", + "inbox", + "internal", + "main", + "media", + "nodeinfo", + "notice", + "oauth", + "objects", + "ostatus_subscribe", + "pleroma", + "proxy", + "push", + "registration", + "relay", + "settings", + "status", + "tag", + "user-search", + "user_exists", + "users", + "web", + "verify_credentials", + "update_credentials", + "relationships", + "search", + "confirmation_resend", + "mfa" + ] + ] + }, + { + "tuple": [ + ":email_blacklist", + [] + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Oban", + "value": [ + { + "tuple": [ + ":repo", + "Pleroma.Repo" + ] + }, + { + "tuple": [ + ":log", + false + ] + }, + { + "tuple": [ + ":queues", + [ + { + "tuple": [ + ":activity_expiration", + 10 + ] + }, + { + "tuple": [ + ":federator_incoming", + 50 + ] + }, + { + "tuple": [ + ":federator_outgoing", + 50 + ] + }, + { + "tuple": [ + ":web_push", + 50 + ] + }, + { + "tuple": [ + ":mailer", + 10 + ] + }, + { + "tuple": [ + ":transmogrifier", + 20 + ] + }, + { + "tuple": [ + ":scheduled_activities", + 10 + ] + }, + { + "tuple": [ + ":background", + 5 + ] + }, + { + "tuple": [ + ":remote_fetcher", + 2 + ] + }, + { + "tuple": [ + ":attachments_cleanup", + 5 + ] + }, + { + "tuple": [ + ":new_users_digest", + 1 + ] + } + ] + ] + }, + { + "tuple": [ + ":plugins", + [ + "Oban.Plugins.Pruner" + ] + ] + }, + { + "tuple": [ + ":crontab", + [ + { + "tuple": [ + "0 0 * * *", + "Pleroma.Workers.Cron.ClearOauthTokenWorker" + ] + }, + { + "tuple": [ + "0 * * * *", + "Pleroma.Workers.Cron.StatsWorker" + ] + }, + { + "tuple": [ + "* * * * *", + "Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker" + ] + }, + { + "tuple": [ + "0 0 * * 0", + "Pleroma.Workers.Cron.DigestEmailsWorker" + ] + }, + { + "tuple": [ + "0 0 * * *", + "Pleroma.Workers.Cron.NewUsersDigestWorker" + ] + } + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":workers", + "value": [ + { + "tuple": [ + ":retries", + [ + { + "tuple": [ + ":federator_incoming", + 5 + ] + }, + { + "tuple": [ + ":federator_outgoing", + 5 + ] + } + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Formatter", + "value": [ + { + "tuple": [ + ":class", + false + ] + }, + { + "tuple": [ + ":rel", + "ugc" + ] + }, + { + "tuple": [ + ":new_window", + false + ] + }, + { + "tuple": [ + ":truncate", + false + ] + }, + { + "tuple": [ + ":strip_prefix", + false + ] + }, + { + "tuple": [ + ":extra", + true + ] + }, + { + "tuple": [ + ":validate_tld", + ":no_scheme" + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":ldap", + "value": [ + { + "tuple": [ + ":enabled", + false + ] + }, + { + "tuple": [ + ":host", + "localhost" + ] + }, + { + "tuple": [ + ":port", + 389 + ] + }, + { + "tuple": [ + ":ssl", + false + ] + }, + { + "tuple": [ + ":sslopts", + [] + ] + }, + { + "tuple": [ + ":tls", + false + ] + }, + { + "tuple": [ + ":tlsopts", + [] + ] + }, + { + "tuple": [ + ":base", + "dc=example,dc=com" + ] + }, + { + "tuple": [ + ":uid", + "cn" + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":auth", + "value": [ + { + "tuple": [ + ":enforce_oauth_admin_scope_usage", + true + ] + }, + { + "tuple": [ + ":oauth_consumer_strategies", + [] + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Emails.UserEmail", + "value": [ + { + "tuple": [ + ":logo", + null + ] + }, + { + "tuple": [ + ":styling", + { + ":background_color": "#2C3645", + ":content_background_color": "#1B2635", + ":header_color": "#d8a070", + ":link_color": "#d8a070", + ":text_color": "#b9b9ba", + ":text_muted_color": "#b9b9ba" + } + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Emails.NewUsersDigestEmail", + "value": [ + { + "tuple": [ + ":enabled", + false + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.ScheduledActivity", + "value": [ + { + "tuple": [ + ":daily_user_limit", + 25 + ] + }, + { + "tuple": [ + ":total_user_limit", + 300 + ] + }, + { + "tuple": [ + ":enabled", + true + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":email_notifications", + "value": [ + { + "tuple": [ + ":digest", + { + ":active": false, + ":inactivity_threshold": 7, + ":interval": 7 + } + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":oauth2", + "value": [ + { + "tuple": [ + ":token_expires_in", + 600 + ] + }, + { + "tuple": [ + ":issue_new_refresh_token", + true + ] + }, + { + "tuple": [ + ":clean_expired_tokens", + false + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":rate_limit", + "value": [ + { + "tuple": [ + ":authentication", + { + "tuple": [ + 60000, + 15 + ] + } + ] + }, + { + "tuple": [ + ":timeline", + { + "tuple": [ + 500, + 3 + ] + } + ] + }, + { + "tuple": [ + ":search", + [ + { + "tuple": [ + 1000, + 10 + ] + }, + { + "tuple": [ + 1000, + 30 + ] + } + ] + ] + }, + { + "tuple": [ + ":app_account_creation", + { + "tuple": [ + 1800000, + 25 + ] + } + ] + }, + { + "tuple": [ + ":relations_actions", + { + "tuple": [ + 10000, + 10 + ] + } + ] + }, + { + "tuple": [ + ":relation_id_action", + { + "tuple": [ + 60000, + 2 + ] + } + ] + }, + { + "tuple": [ + ":statuses_actions", + { + "tuple": [ + 10000, + 15 + ] + } + ] + }, + { + "tuple": [ + ":status_id_action", + { + "tuple": [ + 60000, + 3 + ] + } + ] + }, + { + "tuple": [ + ":password_reset", + { + "tuple": [ + 1800000, + 5 + ] + } + ] + }, + { + "tuple": [ + ":account_confirmation_resend", + { + "tuple": [ + 8640000, + 5 + ] + } + ] + }, + { + "tuple": [ + ":ap_routes", + { + "tuple": [ + 60000, + 15 + ] + } + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.ActivityExpiration", + "value": [ + { + "tuple": [ + ":enabled", + true + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Plugs.RemoteIp", + "value": [ + { + "tuple": [ + ":enabled", + true + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":web_cache_ttl", + "value": [ + { + "tuple": [ + ":activity_pub", + null + ] + }, + { + "tuple": [ + ":activity_pub_question", + 30000 + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":modules", + "value": [ + { + "tuple": [ + ":runtime_dir", + "instance/modules" + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":connections_pool", + "value": [ + { + "tuple": [ + ":reclaim_multiplier", + 0.1 + ] + }, + { + "tuple": [ + ":connection_acquisition_wait", + 250 + ] + }, + { + "tuple": [ + ":connection_acquisition_retries", + 5 + ] + }, + { + "tuple": [ + ":max_connections", + 250 + ] + }, + { + "tuple": [ + ":max_idle_time", + 30000 + ] + }, + { + "tuple": [ + ":retry", + 0 + ] + }, + { + "tuple": [ + ":await_up_timeout", + 5000 + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":pools", + "value": [ + { + "tuple": [ + ":federation", + [ + { + "tuple": [ + ":size", + 50 + ] + }, + { + "tuple": [ + ":max_waiting", + 10 + ] + } + ] + ] + }, + { + "tuple": [ + ":media", + [ + { + "tuple": [ + ":size", + 50 + ] + }, + { + "tuple": [ + ":max_waiting", + 10 + ] + } + ] + ] + }, + { + "tuple": [ + ":upload", + [ + { + "tuple": [ + ":size", + 25 + ] + }, + { + "tuple": [ + ":max_waiting", + 5 + ] + } + ] + ] + }, + { + "tuple": [ + ":default", + [ + { + "tuple": [ + ":size", + 10 + ] + }, + { + "tuple": [ + ":max_waiting", + 2 + ] + } + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":hackney_pools", + "value": [ + { + "tuple": [ + ":federation", + [ + { + "tuple": [ + ":max_connections", + 50 + ] + }, + { + "tuple": [ + ":timeout", + 150000 + ] + } + ] + ] + }, + { + "tuple": [ + ":media", + [ + { + "tuple": [ + ":max_connections", + 50 + ] + }, + { + "tuple": [ + ":timeout", + 150000 + ] + } + ] + ] + }, + { + "tuple": [ + ":upload", + [ + { + "tuple": [ + ":max_connections", + 25 + ] + }, + { + "tuple": [ + ":timeout", + 300000 + ] + } + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":restrict_unauthenticated", + "value": [ + { + "tuple": [ + ":timelines", + { + ":federated": ":if_instance_is_private", + ":local": ":if_instance_is_private" + } + ] + }, + { + "tuple": [ + ":profiles", + { + ":local": ":if_instance_is_private", + ":remote": ":if_instance_is_private" + } + ] + }, + { + "tuple": [ + ":activities", + { + ":local": ":if_instance_is_private", + ":remote": ":if_instance_is_private" + } + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf", + "value": [ + { + "tuple": [ + ":policies", + "Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy" + ] + }, + { + "tuple": [ + ":transparency", + true + ] + }, + { + "tuple": [ + ":transparency_exclusions", + [] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":instances_favicons", + "value": [ + { + "tuple": [ + ":enabled", + false + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Web.Auth.Authenticator", + "value": "Pleroma.Web.Auth.PleromaAuthenticator" + }, + { + "group": ":pleroma", + "key": "Pleroma.Emails.Mailer", + "value": [ + { + "tuple": [ + ":enabled", + false + ] + }, + { + "tuple": [ + ":adapter", + "Swoosh.Adapters.Local" + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Web.ApiSpec.CastAndValidate", + "value": [ + { + "tuple": [ + ":strict", + true + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Uploaders.S3", + "value": [ + { + "tuple": [ + ":streaming_enabled", + true + ] + }, + { + "tuple": [ + ":public_endpoint", + "https://media.gleasonator.com" + ] + }, + { + "tuple": [ + ":bucket", + "gleasonator-media" + ] + } + ] + }, + { + "db": [ + ":enabled" + ], + "group": ":pleroma", + "key": ":static_fe", + "value": [ + { + "tuple": [ + ":enabled", + true + ] + } + ] + }, + { + "db": [ + ":enabled", + ":redirect_on_failure" + ], + "group": ":pleroma", + "key": ":media_proxy", + "value": [ + { + "tuple": [ + ":invalidation", + [ + { + "tuple": [ + ":enabled", + false + ] + }, + { + "tuple": [ + ":provider", + "Pleroma.Web.MediaProxy.Invalidation.Script" + ] + } + ] + ] + }, + { + "tuple": [ + ":proxy_opts", + [ + { + "tuple": [ + ":redirect_on_failure", + false + ] + }, + { + "tuple": [ + ":max_body_length", + 26214400 + ] + }, + { + "tuple": [ + ":http", + [ + { + "tuple": [ + ":follow_redirect", + true + ] + }, + { + "tuple": [ + ":pool", + ":media" + ] + } + ] + ] + } + ] + ] + }, + { + "tuple": [ + ":whitelist", + [] + ] + }, + { + "tuple": [ + ":enabled", + false + ] + }, + { + "tuple": [ + ":redirect_on_failure", + true + ] + } + ] + }, + { + "db": [ + ":name", + ":email", + ":notify_email", + ":limit", + ":registrations_open", + ":rewrite_policy", + ":max_pinned_statuses", + ":federating", + ":static_dir" + ], + "group": ":pleroma", + "key": ":instance", + "value": [ + { + "tuple": [ + ":description", + "Pleroma: An efficient and flexible fediverse server" + ] + }, + { + "tuple": [ + ":background_image", + "/images/city.jpg" + ] + }, + { + "tuple": [ + ":instance_thumbnail", + "/instance/thumbnail.jpeg" + ] + }, + { + "tuple": [ + ":description_limit", + 5000 + ] + }, + { + "tuple": [ + ":chat_limit", + 5000 + ] + }, + { + "tuple": [ + ":remote_limit", + 100000 + ] + }, + { + "tuple": [ + ":upload_limit", + 16000000 + ] + }, + { + "tuple": [ + ":avatar_upload_limit", + 2000000 + ] + }, + { + "tuple": [ + ":background_upload_limit", + 4000000 + ] + }, + { + "tuple": [ + ":banner_upload_limit", + 4000000 + ] + }, + { + "tuple": [ + ":poll_limits", + { + ":max_expiration": 31536000, + ":max_option_chars": 200, + ":max_options": 20, + ":min_expiration": 0 + } + ] + }, + { + "tuple": [ + ":invites_enabled", + false + ] + }, + { + "tuple": [ + ":account_activation_required", + false + ] + }, + { + "tuple": [ + ":account_approval_required", + false + ] + }, + { + "tuple": [ + ":federation_incoming_replies_max_depth", + 100 + ] + }, + { + "tuple": [ + ":federation_reachability_timeout_days", + 7 + ] + }, + { + "tuple": [ + ":federation_publisher_modules", + [ + "Pleroma.Web.ActivityPub.Publisher" + ] + ] + }, + { + "tuple": [ + ":allow_relay", + true + ] + }, + { + "tuple": [ + ":public", + true + ] + }, + { + "tuple": [ + ":quarantined_instances", + [] + ] + }, + { + "tuple": [ + ":managed_config", + true + ] + }, + { + "tuple": [ + ":allowed_post_formats", + [ + "text/plain", + "text/html", + "text/markdown", + "text/bbcode" + ] + ] + }, + { + "tuple": [ + ":autofollowed_nicknames", + [] + ] + }, + { + "tuple": [ + ":attachment_links", + false + ] + }, + { + "tuple": [ + ":max_report_comment_size", + 1000 + ] + }, + { + "tuple": [ + ":safe_dm_mentions", + false + ] + }, + { + "tuple": [ + ":healthcheck", + false + ] + }, + { + "tuple": [ + ":remote_post_retention_days", + 90 + ] + }, + { + "tuple": [ + ":skip_thread_containment", + true + ] + }, + { + "tuple": [ + ":limit_to_local_content", + ":unauthenticated" + ] + }, + { + "tuple": [ + ":user_bio_length", + 5000 + ] + }, + { + "tuple": [ + ":user_name_length", + 100 + ] + }, + { + "tuple": [ + ":max_account_fields", + 10 + ] + }, + { + "tuple": [ + ":max_remote_account_fields", + 20 + ] + }, + { + "tuple": [ + ":account_field_name_length", + 512 + ] + }, + { + "tuple": [ + ":account_field_value_length", + 2048 + ] + }, + { + "tuple": [ + ":registration_reason_length", + 500 + ] + }, + { + "tuple": [ + ":external_user_synchronization", + true + ] + }, + { + "tuple": [ + ":extended_nickname_format", + true + ] + }, + { + "tuple": [ + ":cleanup_attachments", + false + ] + }, + { + "tuple": [ + ":multi_factor_authentication", + [ + { + "tuple": [ + ":totp", + [ + { + "tuple": [ + ":digits", + 6 + ] + }, + { + "tuple": [ + ":period", + 30 + ] + } + ] + ] + }, + { + "tuple": [ + ":backup_codes", + [ + { + "tuple": [ + ":number", + 5 + ] + }, + { + "tuple": [ + ":length", + 16 + ] + } + ] + ] + } + ] + ] + }, + { + "tuple": [ + ":show_reactions", + true + ] + }, + { + "tuple": [ + ":name", + "Soapbox FE Demo" + ] + }, + { + "tuple": [ + ":email", + "alex@alexgleason.me" + ] + }, + { + "tuple": [ + ":notify_email", + "alex@alexgleason.me" + ] + }, + { + "tuple": [ + ":limit", + 5000 + ] + }, + { + "tuple": [ + ":registrations_open", + true + ] + }, + { + "tuple": [ + ":rewrite_policy", + "Pleroma.Web.ActivityPub.MRF.SimplePolicy" + ] + }, + { + "tuple": [ + ":max_pinned_statuses", + 10 + ] + }, + { + "tuple": [ + ":federating", + false + ] + }, + { + "tuple": [ + ":static_dir", + "instance/static" + ] + } + ] + }, + { + "db": [ + ":uploads" + ], + "group": ":pleroma", + "key": "Pleroma.Uploaders.Local", + "value": [ + { + "tuple": [ + ":uploads", + "uploads" + ] + } + ] + }, + { + "db": [ + ":parsers" + ], + "group": ":pleroma", + "key": ":rich_media", + "value": [ + { + "tuple": [ + ":enabled", + true + ] + }, + { + "tuple": [ + ":ignore_hosts", + [] + ] + }, + { + "tuple": [ + ":ignore_tld", + [ + "local", + "localdomain", + "lan" + ] + ] + }, + { + "tuple": [ + ":ttl_setters", + [ + "Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl" + ] + ] + }, + { + "tuple": [ + ":parsers", + [ + "Pleroma.Web.RichMedia.Parsers.OEmbed", + "Pleroma.Web.RichMedia.Parsers.TwitterCard" + ] + ] + } + ] + }, + { + "db": [ + ":uploader" + ], + "group": ":pleroma", + "key": "Pleroma.Upload", + "value": [ + { + "tuple": [ + ":filters", + [ + "Pleroma.Upload.Filter.Dedupe" + ] + ] + }, + { + "tuple": [ + ":link_name", + false + ] + }, + { + "tuple": [ + ":proxy_remote", + false + ] + }, + { + "tuple": [ + ":proxy_opts", + [ + { + "tuple": [ + ":redirect_on_failure", + false + ] + }, + { + "tuple": [ + ":max_body_length", + 26214400 + ] + }, + { + "tuple": [ + ":http", + [ + { + "tuple": [ + ":follow_redirect", + true + ] + }, + { + "tuple": [ + ":pool", + ":upload" + ] + } + ] + ] + } + ] + ] + }, + { + "tuple": [ + ":filename_display_max_length", + 30 + ] + }, + { + "tuple": [ + ":uploader", + "Pleroma.Uploaders.Local" + ] + } + ] + }, + { + "db": [ + ":soapbox_fe" + ], + "group": ":pleroma", + "key": ":frontend_configurations", + "value": [ + { + "tuple": [ + ":pleroma_fe", + { + ":alwaysShowSubjectInput": true, + ":background": "/images/city.jpg", + ":collapseMessageWithSubject": false, + ":disableChat": false, + ":greentext": false, + ":hideFilteredStatuses": false, + ":hideMutedPosts": false, + ":hidePostStats": false, + ":hideSitename": false, + ":hideUserStats": false, + ":loginMethod": "password", + ":logo": "/static/logo.png", + ":logoMargin": ".1em", + ":logoMask": true, + ":minimalScopesMode": false, + ":noAttachmentLinks": false, + ":nsfwCensorImage": "", + ":postContentType": "text/plain", + ":redirectRootLogin": "/main/friends", + ":redirectRootNoLogin": "/main/all", + ":scopeCopy": true, + ":showFeaturesPanel": true, + ":showInstanceSpecificPanel": false, + ":sidebarRight": false, + ":subjectLineBehavior": "email", + ":theme": "pleroma-dark", + ":webPushNotifications": false + } + ] + }, + { + "tuple": [ + ":masto_fe", + { + ":showInstanceSpecificPanel": true + } + ] + }, + { + "tuple": [ + ":soapbox_fe", + { + "brandColor": "#0e9066", + "copyright": "♥2020. Copying is an act of love. Please copy and share.", + "customCss": [], + "navlinks": { + "homeFooter": [] + }, + "promoPanel": { + "items": [] + } + } + ] + } + ] + } + ], + "need_reboot": false +} diff --git a/app/soapbox/__fixtures__/intlMessages.json b/app/soapbox/__fixtures__/intlMessages.json index d46a8934c..457285c3f 100644 --- a/app/soapbox/__fixtures__/intlMessages.json +++ b/app/soapbox/__fixtures__/intlMessages.json @@ -261,6 +261,7 @@ "morefollows.following_label": "…and {count} more {count, plural, one {follow} other {follows}} on remote sites.", "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.admin_settings": "Admin settings", + "navigation_bar.soapbox_config": "Soapbox config", "navigation_bar.blocks": "Blocked users", "navigation_bar.community_timeline": "Local timeline", "navigation_bar.compose": "Compose new post", @@ -738,6 +739,7 @@ "morefollows.following_label": "…and {count} more {count, plural, one {follow} other {follows}} on remote sites.", "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.admin_settings": "Admin settings", + "navigation_bar.soapbox_config": "Soapbox config", "navigation_bar.blocks": "Blocked users", "navigation_bar.community_timeline": "Local timeline", "navigation_bar.compose": "Compose new post", diff --git a/app/soapbox/__fixtures__/soapbox.json b/app/soapbox/__fixtures__/soapbox.json new file mode 100644 index 000000000..6208b855b --- /dev/null +++ b/app/soapbox/__fixtures__/soapbox.json @@ -0,0 +1,40 @@ +{ + "logo": "blob:http://localhost:3036/0cdfa863-6889-4199-b870-4942cedd364f", + "banner": "blob:http://localhost:3036/a835afed-6078-45bd-92b4-7ffd858c3eca", + "brandColor": "#254f92", + "customCss": [ + "/instance/static/custom.css" + ], + "promoPanel": { + "items": [ + { + "icon": "globe", + "text": "blog", + "url": "https://teci.world/blog" + }, + { + "icon": "globe", + "text": "book", + "url": "https://teci.world/book" + } + ] + }, + "extensions": { + "patron": false + }, + "defaultSettings": { + "autoPlayGif": false + }, + "navlinks": { + "homeFooter": [ + { + "title": "about", + "url": "/instance/about/index.html" + }, + { + "title": "tos", + "url": "/instance/about/tos.html" + } + ] + } +} diff --git a/app/soapbox/actions/admin.js b/app/soapbox/actions/admin.js new file mode 100644 index 000000000..cf3a5d0e7 --- /dev/null +++ b/app/soapbox/actions/admin.js @@ -0,0 +1,35 @@ +import api from '../api'; + +export const ADMIN_CONFIG_UPDATE_REQUEST = 'ADMIN_CONFIG_UPDATE_REQUEST'; +export const ADMIN_CONFIG_UPDATE_SUCCESS = 'ADMIN_CONFIG_UPDATE_SUCCESS'; +export const ADMIN_CONFIG_UPDATE_FAIL = 'ADMIN_CONFIG_UPDATE_FAIL'; + +export const ADMIN_REPORTS_FETCH_REQUEST = 'ADMIN_REPORTS_FETCH_REQUEST'; +export const ADMIN_REPORTS_FETCH_SUCCESS = 'ADMIN_REPORTS_FETCH_SUCCESS'; +export const ADMIN_REPORTS_FETCH_FAIL = 'ADMIN_REPORTS_FETCH_FAIL'; + +export function updateAdminConfig(params) { + return (dispatch, getState) => { + dispatch({ type: ADMIN_CONFIG_UPDATE_REQUEST }); + return api(getState) + .post('/api/pleroma/admin/config', params) + .then(response => { + dispatch({ type: ADMIN_CONFIG_UPDATE_SUCCESS, config: response.data }); + }).catch(error => { + dispatch({ type: ADMIN_CONFIG_UPDATE_FAIL, error }); + }); + }; +} + +export function fetchReports(params) { + return (dispatch, getState) => { + dispatch({ type: ADMIN_REPORTS_FETCH_REQUEST, params }); + return api(getState) + .get('/api/pleroma/admin/reports', { params }) + .then(({ data }) => { + dispatch({ type: ADMIN_REPORTS_FETCH_SUCCESS, data, params }); + }).catch(error => { + dispatch({ type: ADMIN_REPORTS_FETCH_FAIL, error, params }); + }); + }; +} diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index 8beb9c1eb..50a26cd3c 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -154,6 +154,7 @@ export function register(params) { return (dispatch, getState) => { const needsConfirmation = getState().getIn(['instance', 'pleroma', 'metadata', 'account_activation_required']); const needsApproval = getState().getIn(['instance', 'approval_required']); + params.fullname = params.username; dispatch({ type: AUTH_REGISTER_REQUEST }); return dispatch(createAppAndToken()).then(() => { return api(getState, 'app').post('/api/v1/accounts', params); diff --git a/app/soapbox/actions/chats.js b/app/soapbox/actions/chats.js new file mode 100644 index 000000000..a3e743c87 --- /dev/null +++ b/app/soapbox/actions/chats.js @@ -0,0 +1,152 @@ +import api from '../api'; +import { getSettings, changeSetting } from 'soapbox/actions/settings'; +import { v4 as uuidv4 } from 'uuid'; +import { Map as ImmutableMap } from 'immutable'; + +export const CHATS_FETCH_REQUEST = 'CHATS_FETCH_REQUEST'; +export const CHATS_FETCH_SUCCESS = 'CHATS_FETCH_SUCCESS'; +export const CHATS_FETCH_FAIL = 'CHATS_FETCH_FAIL'; + +export const CHAT_MESSAGES_FETCH_REQUEST = 'CHAT_MESSAGES_FETCH_REQUEST'; +export const CHAT_MESSAGES_FETCH_SUCCESS = 'CHAT_MESSAGES_FETCH_SUCCESS'; +export const CHAT_MESSAGES_FETCH_FAIL = 'CHAT_MESSAGES_FETCH_FAIL'; + +export const CHAT_MESSAGE_SEND_REQUEST = 'CHAT_MESSAGE_SEND_REQUEST'; +export const CHAT_MESSAGE_SEND_SUCCESS = 'CHAT_MESSAGE_SEND_SUCCESS'; +export const CHAT_MESSAGE_SEND_FAIL = 'CHAT_MESSAGE_SEND_FAIL'; + +export const CHAT_FETCH_REQUEST = 'CHAT_FETCH_REQUEST'; +export const CHAT_FETCH_SUCCESS = 'CHAT_FETCH_SUCCESS'; +export const CHAT_FETCH_FAIL = 'CHAT_FETCH_FAIL'; + +export const CHAT_READ_REQUEST = 'CHAT_READ_REQUEST'; +export const CHAT_READ_SUCCESS = 'CHAT_READ_SUCCESS'; +export const CHAT_READ_FAIL = 'CHAT_READ_FAIL'; + +export function fetchChats() { + return (dispatch, getState) => { + dispatch({ type: CHATS_FETCH_REQUEST }); + return api(getState).get('/api/v1/pleroma/chats').then(({ data }) => { + dispatch({ type: CHATS_FETCH_SUCCESS, chats: data }); + }).catch(error => { + dispatch({ type: CHATS_FETCH_FAIL, error }); + }); + }; +} + +export function fetchChatMessages(chatId) { + return (dispatch, getState) => { + dispatch({ type: CHAT_MESSAGES_FETCH_REQUEST, chatId }); + return api(getState).get(`/api/v1/pleroma/chats/${chatId}/messages`).then(({ data }) => { + dispatch({ type: CHAT_MESSAGES_FETCH_SUCCESS, chatId, chatMessages: data }); + }).catch(error => { + dispatch({ type: CHAT_MESSAGES_FETCH_FAIL, chatId, error }); + }); + }; +} + +export function sendChatMessage(chatId, params) { + return (dispatch, getState) => { + const uuid = uuidv4(); + const me = getState().get('me'); + dispatch({ type: CHAT_MESSAGE_SEND_REQUEST, chatId, params, uuid, me }); + return api(getState).post(`/api/v1/pleroma/chats/${chatId}/messages`, params).then(({ data }) => { + dispatch({ type: CHAT_MESSAGE_SEND_SUCCESS, chatId, chatMessage: data, uuid }); + }).catch(error => { + dispatch({ type: CHAT_MESSAGE_SEND_FAIL, chatId, error, uuid }); + }); + }; +} + +export function openChat(chatId) { + return (dispatch, getState) => { + const state = getState(); + const panes = getSettings(state).getIn(['chats', 'panes']); + const idx = panes.findIndex(pane => pane.get('chat_id') === chatId); + + dispatch(markChatRead(chatId)); + + if (idx > -1) { + return dispatch(changeSetting(['chats', 'panes', idx, 'state'], 'open')); + } else { + const newPane = ImmutableMap({ chat_id: chatId, state: 'open' }); + return dispatch(changeSetting(['chats', 'panes'], panes.push(newPane))); + } + }; +} + +export function closeChat(chatId) { + return (dispatch, getState) => { + const panes = getSettings(getState()).getIn(['chats', 'panes']); + const idx = panes.findIndex(pane => pane.get('chat_id') === chatId); + + if (idx > -1) { + return dispatch(changeSetting(['chats', 'panes'], panes.delete(idx))); + } else { + return false; + } + }; +} + +export function toggleChat(chatId) { + return (dispatch, getState) => { + const panes = getSettings(getState()).getIn(['chats', 'panes']); + const [idx, pane] = panes.findEntry(pane => pane.get('chat_id') === chatId); + + if (idx > -1) { + const state = pane.get('state') === 'minimized' ? 'open' : 'minimized'; + if (state === 'open') dispatch(markChatRead(chatId)); + return dispatch(changeSetting(['chats', 'panes', idx, 'state'], state)); + } else { + return false; + } + }; +} + +export function toggleMainWindow() { + return (dispatch, getState) => { + const main = getSettings(getState()).getIn(['chats', 'mainWindow']); + const state = main === 'minimized' ? 'open' : 'minimized'; + return dispatch(changeSetting(['chats', 'mainWindow'], state)); + }; +} + +export function fetchChat(chatId) { + return (dispatch, getState) => { + dispatch({ type: CHAT_FETCH_REQUEST, chatId }); + return api(getState).get(`/api/v1/pleroma/chats/${chatId}`).then(({ data }) => { + dispatch({ type: CHAT_FETCH_SUCCESS, chat: data }); + }).catch(error => { + dispatch({ type: CHAT_FETCH_FAIL, chatId, error }); + }); + }; +} + +export function startChat(accountId) { + return (dispatch, getState) => { + dispatch({ type: CHAT_FETCH_REQUEST, accountId }); + return api(getState).post(`/api/v1/pleroma/chats/by-account-id/${accountId}`).then(({ data }) => { + dispatch({ type: CHAT_FETCH_SUCCESS, chat: data }); + return data; + }).catch(error => { + dispatch({ type: CHAT_FETCH_FAIL, accountId, error }); + }); + }; +} + +export function markChatRead(chatId, lastReadId) { + return (dispatch, getState) => { + const chat = getState().getIn(['chats', chatId]); + if (!lastReadId) lastReadId = chat.get('last_message'); + + if (chat.get('unread') < 1) return; + if (!lastReadId) return; + + dispatch({ type: CHAT_READ_REQUEST, chatId, lastReadId }); + api(getState).post(`/api/v1/pleroma/chats/${chatId}/read`, { last_read_id: lastReadId }).then(({ data }) => { + dispatch({ type: CHAT_READ_SUCCESS, chat: data, lastReadId }); + }).catch(error => { + dispatch({ type: CHAT_READ_FAIL, chatId, error, lastReadId }); + }); + }; +} diff --git a/app/soapbox/actions/compose.js b/app/soapbox/actions/compose.js index a2b5c0f85..ef6b9995d 100644 --- a/app/soapbox/actions/compose.js +++ b/app/soapbox/actions/compose.js @@ -7,12 +7,12 @@ import { useEmoji } from './emojis'; import resizeImage from '../utils/resize_image'; import { importFetchedAccounts } from './importer'; import { updateTimeline, dequeueTimeline } from './timelines'; -import { showAlertForError } from './alerts'; -import { showAlert } from './alerts'; +import { showAlert, showAlertForError } from './alerts'; import { defineMessages } from 'react-intl'; import { openModal, closeModal } from './modal'; import { getSettings } from './settings'; import { getFeatures } from 'soapbox/utils/features'; +import { uploadMedia } from './media'; let cancelFetchComposeSuggestionsAccounts; @@ -239,12 +239,14 @@ export function uploadCompose(files) { // Account for disparity in size of original image and resized data total += file.size - f.size; - return api(getState).post('/api/v1/media', data, { - onUploadProgress: function({ loaded }){ - progress[i] = loaded; - dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total)); - }, - }).then(({ data }) => dispatch(uploadComposeSuccess(data))); + const onUploadProgress = function({ loaded }) { + progress[i] = loaded; + dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total)); + }; + + return dispatch(uploadMedia(data, onUploadProgress)) + .then(({ data }) => dispatch(uploadComposeSuccess(data))); + }).catch(error => dispatch(uploadComposeFail(error))); }; }; diff --git a/app/soapbox/actions/importer/index.js b/app/soapbox/actions/importer/index.js index aaf603608..0736dd7ce 100644 --- a/app/soapbox/actions/importer/index.js +++ b/app/soapbox/actions/importer/index.js @@ -1,5 +1,9 @@ import { getSettings } from '../settings'; -import { normalizeAccount, normalizeStatus, normalizePoll } from './normalizer'; +import { + normalizeAccount, + normalizeStatus, + normalizePoll, +} from './normalizer'; export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT'; export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT'; diff --git a/app/soapbox/actions/importer/normalizer.js b/app/soapbox/actions/importer/normalizer.js index 0edac3e5c..bcf6c3aea 100644 --- a/app/soapbox/actions/importer/normalizer.js +++ b/app/soapbox/actions/importer/normalizer.js @@ -80,3 +80,13 @@ export function normalizePoll(poll) { return normalPoll; } + +export function normalizeChat(chat, normalOldChat) { + const normalChat = { ...chat }; + const { account, last_message: lastMessage } = chat; + + if (account) normalChat.account = account.id; + if (lastMessage) normalChat.last_message = lastMessage.id; + + return normalChat; +} diff --git a/app/soapbox/actions/media.js b/app/soapbox/actions/media.js new file mode 100644 index 000000000..daf31c019 --- /dev/null +++ b/app/soapbox/actions/media.js @@ -0,0 +1,11 @@ +import api from '../api'; + +const noOp = () => {}; + +export function uploadMedia(data, onUploadProgress = noOp) { + return function(dispatch, getState) { + return api(getState).post('/api/v1/media', data, { + onUploadProgress: onUploadProgress, + }); + }; +} diff --git a/app/soapbox/actions/notifications.js b/app/soapbox/actions/notifications.js index 8f2b2f1c2..1346c36c0 100644 --- a/app/soapbox/actions/notifications.js +++ b/app/soapbox/actions/notifications.js @@ -13,7 +13,6 @@ import { defineMessages } from 'react-intl'; import { List as ImmutableList } from 'immutable'; import { unescapeHTML } from '../utils/html'; import { getFilters, regexFromFilters } from '../selectors'; -import { fetchMarkers } from './markers'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; @@ -71,6 +70,8 @@ export function updateNotifications(notification, intlMessages, intlLocale) { export function updateNotificationsQueue(notification, intlMessages, intlLocale, curPath) { return (dispatch, getState) => { + if (notification.type === 'pleroma:chat_mention') return; // Drop chat notifications, handle them per-chat + const showAlert = getSettings(getState()).getIn(['notifications', 'alerts', notification.type]); const filters = getFilters(getState(), { contextType: 'notifications' }); const playSound = getSettings(getState()).getIn(['notifications', 'sounds', notification.type]); @@ -173,7 +174,6 @@ export function expandNotifications({ maxId } = {}, done = noOp) { params.since_id = notifications.getIn(['items', 0, 'id']); } - dispatch(fetchMarkers(['notifications'])); dispatch(expandNotificationsRequest(isLoadingMore)); api(getState).get('/api/v1/notifications', { params }).then(response => { diff --git a/app/soapbox/actions/preload.js b/app/soapbox/actions/preload.js new file mode 100644 index 000000000..0f19ec721 --- /dev/null +++ b/app/soapbox/actions/preload.js @@ -0,0 +1,25 @@ +import { mapValues } from 'lodash'; + +export const PRELOAD_IMPORT = 'PRELOAD_IMPORT'; + +// https://git.pleroma.social/pleroma/pleroma-fe/-/merge_requests/1176/diffs +const decodeUTF8Base64 = (data) => { + const rawData = atob(data); + const array = Uint8Array.from(rawData.split('').map((char) => char.charCodeAt(0))); + const text = new TextDecoder().decode(array); + return text; +}; + +const decodeData = data => + mapValues(data, base64string => + JSON.parse(decodeUTF8Base64(base64string))); + +export function preload() { + const element = document.getElementById('initial-results'); + const data = element ? JSON.parse(element.textContent) : {}; + + return { + type: PRELOAD_IMPORT, + data: decodeData(data), + }; +} diff --git a/app/soapbox/actions/settings.js b/app/soapbox/actions/settings.js index 677556dee..4432fa6e0 100644 --- a/app/soapbox/actions/settings.js +++ b/app/soapbox/actions/settings.js @@ -1,7 +1,7 @@ import { debounce } from 'lodash'; import { showAlertForError } from './alerts'; import { patchMe } from 'soapbox/actions/me'; -import { Map as ImmutableMap } from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; export const SETTING_CHANGE = 'SETTING_CHANGE'; export const SETTING_SAVE = 'SETTING_SAVE'; @@ -29,6 +29,11 @@ const defaultSettings = ImmutableMap({ dyslexicFont: false, demetricator: false, + chats: ImmutableMap({ + panes: ImmutableList(), + mainWindow: 'minimized', + }), + home: ImmutableMap({ shows: ImmutableMap({ reblog: true, diff --git a/app/soapbox/actions/soapbox.js b/app/soapbox/actions/soapbox.js index 3faca2028..eedd1787f 100644 --- a/app/soapbox/actions/soapbox.js +++ b/app/soapbox/actions/soapbox.js @@ -1,9 +1,44 @@ import api from '../api'; +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; export const SOAPBOX_CONFIG_REQUEST_SUCCESS = 'SOAPBOX_CONFIG_REQUEST_SUCCESS'; export const SOAPBOX_CONFIG_REQUEST_FAIL = 'SOAPBOX_CONFIG_REQUEST_FAIL'; +export const defaultConfig = ImmutableMap({ + logo: '', + banner: '', + brandColor: '#0482d8', // Azure + customCss: ImmutableList(), + promoPanel: ImmutableMap({ + items: ImmutableList(), + }), + extensions: ImmutableMap(), + defaultSettings: ImmutableMap(), + copyright: '♥2020. Copying is an act of love. Please copy and share.', + navlinks: ImmutableMap({ + homeFooter: ImmutableList(), + }), +}); + +export function getSoapboxConfig(state) { + return defaultConfig.mergeDeep(state.get('soapbox')); +} + export function fetchSoapboxConfig() { + return (dispatch, getState) => { + api(getState).get('/api/pleroma/frontend_configurations').then(response => { + if (response.data.soapbox_fe) { + dispatch(importSoapboxConfig(response.data.soapbox_fe)); + } else { + dispatch(fetchSoapboxJson()); + } + }).catch(error => { + dispatch(fetchSoapboxJson()); + }); + }; +} + +export function fetchSoapboxJson() { return (dispatch, getState) => { api(getState).get('/instance/soapbox.json').then(response => { dispatch(importSoapboxConfig(response.data)); @@ -22,7 +57,7 @@ export function importSoapboxConfig(soapboxConfig) { export function soapboxConfigFail(error) { if (!error.response) { - console.error('soapbox.json parsing error: ' + error); + console.error('Unable to obtain soapbox configuration: ' + error); } return { type: SOAPBOX_CONFIG_REQUEST_FAIL, diff --git a/app/soapbox/actions/streaming.js b/app/soapbox/actions/streaming.js index 099cbab69..a581ae0fe 100644 --- a/app/soapbox/actions/streaming.js +++ b/app/soapbox/actions/streaming.js @@ -12,6 +12,8 @@ import { fetchFilters } from './filters'; import { getSettings } from 'soapbox/actions/settings'; import messages from 'soapbox/locales/messages'; +export const STREAMING_CHAT_UPDATE = 'STREAMING_CHAT_UPDATE'; + const validLocale = locale => Object.keys(messages).includes(locale); const getLocale = state => { @@ -52,6 +54,9 @@ export function connectTimelineStream(timelineId, path, pollingRefresh = null, a case 'filters_changed': dispatch(fetchFilters()); break; + case 'pleroma:chat_update': + dispatch({ type: STREAMING_CHAT_UPDATE, chat: JSON.parse(data.payload), me: getState().get('me') }); + break; } }, }; diff --git a/app/soapbox/features/preferences/components/settings_checkbox.js b/app/soapbox/components/settings_checkbox.js similarity index 94% rename from app/soapbox/features/preferences/components/settings_checkbox.js rename to app/soapbox/components/settings_checkbox.js index 99132e21a..07c6d4d72 100644 --- a/app/soapbox/features/preferences/components/settings_checkbox.js +++ b/app/soapbox/components/settings_checkbox.js @@ -4,7 +4,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { changeSetting } from 'soapbox/actions/settings'; -import { Checkbox } from '../../forms'; +import { Checkbox } from 'soapbox/features/forms'; const mapStateToProps = state => ({ settings: state.get('settings'), diff --git a/app/soapbox/components/sidebar_menu.js b/app/soapbox/components/sidebar_menu.js index a82dfe519..6cc615c65 100644 --- a/app/soapbox/components/sidebar_menu.js +++ b/app/soapbox/components/sidebar_menu.js @@ -21,7 +21,6 @@ const messages = defineMessages({ followers: { id: 'account.followers', defaultMessage: 'Followers' }, follows: { id: 'account.follows', defaultMessage: 'Follows' }, profile: { id: 'account.profile', defaultMessage: 'Profile' }, - messages: { id: 'navigation_bar.messages', defaultMessage: 'Messages' }, preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, @@ -29,6 +28,7 @@ const messages = defineMessages({ mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' }, admin_settings: { id: 'navigation_bar.admin_settings', defaultMessage: 'Admin settings' }, + soapbox_config: { id: 'navigation_bar.soapbox_config', defaultMessage: 'Soapbox config' }, security: { id: 'navigation_bar.security', defaultMessage: 'Security' }, logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, lists: { id: 'column.lists', defaultMessage: 'Lists' }, @@ -131,10 +131,6 @@ class SidebarMenu extends ImmutablePureComponent { {intl.formatMessage(messages.profile)} - - - {intl.formatMessage(messages.messages)} - {donateUrl ? @@ -172,10 +168,14 @@ class SidebarMenu extends ImmutablePureComponent { {intl.formatMessage(messages.filters)} - { isStaff && + { isStaff && {intl.formatMessage(messages.admin_settings)} } + { isStaff && + + {intl.formatMessage(messages.soapbox_config)} + } {intl.formatMessage(messages.preferences)} diff --git a/app/soapbox/components/status.js b/app/soapbox/components/status.js index d94de8fc9..4e76a622d 100644 --- a/app/soapbox/components/status.js +++ b/app/soapbox/components/status.js @@ -192,15 +192,15 @@ class Status extends ImmutablePureComponent { }; renderLoadingMediaGallery() { - return
; + return
; } renderLoadingVideoPlayer() { - return
; + return
; } renderLoadingAudioPlayer() { - return
; + return
; } handleOpenVideo = (media, startTime) => { @@ -373,7 +373,7 @@ class Status extends ImmutablePureComponent { alt={video.get('description')} aspectRatio={video.getIn(['meta', 'small', 'aspect'])} width={this.props.cachedMediaWidth} - height={110} + height={285} inline sensitive={status.get('sensitive')} onOpenVideo={this.handleOpenVideo} @@ -409,7 +409,7 @@ class Status extends ImmutablePureComponent { { - this.props.onLoadMore(this.props.statusIds.size > 0 ? this.props.statusIds.last() : undefined); + const loadMoreID = this.props.lastStatusId ? this.props.lastStatusId : this.props.statusIds.last(); + this.props.onLoadMore(loadMoreID); }, 300, { leading: true }) _selectChild(index, align_top) { diff --git a/app/soapbox/containers/soapbox.js b/app/soapbox/containers/soapbox.js index 473e592ff..b948811d3 100644 --- a/app/soapbox/containers/soapbox.js +++ b/app/soapbox/containers/soapbox.js @@ -15,23 +15,27 @@ import UI from '../features/ui'; // import Introduction from '../features/introduction'; import { fetchCustomEmojis } from '../actions/custom_emojis'; import { hydrateStore } from '../actions/store'; -import { IntlProvider } from 'react-intl'; import initialState from '../initial_state'; +import { preload } from '../actions/preload'; +import { IntlProvider } from 'react-intl'; import ErrorBoundary from '../components/error_boundary'; import { fetchInstance } from 'soapbox/actions/instance'; import { fetchSoapboxConfig } from 'soapbox/actions/soapbox'; import { fetchMe } from 'soapbox/actions/me'; import PublicLayout from 'soapbox/features/public_layout'; import { getSettings } from 'soapbox/actions/settings'; +import { getSoapboxConfig } from 'soapbox/actions/soapbox'; import { generateThemeCss } from 'soapbox/utils/theme'; import messages from 'soapbox/locales/messages'; const validLocale = locale => Object.keys(messages).includes(locale); export const store = configureStore(); + const hydrateAction = hydrateStore(initialState); store.dispatch(hydrateAction); +store.dispatch(preload()); store.dispatch(fetchMe()); store.dispatch(fetchInstance()); store.dispatch(fetchSoapboxConfig()); @@ -42,6 +46,7 @@ const mapStateToProps = (state) => { const account = state.getIn(['accounts', me]); const showIntroduction = account ? state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION : false; const settings = getSettings(state); + const soapboxConfig = getSoapboxConfig(state); const locale = settings.get('locale'); return { @@ -52,9 +57,9 @@ const mapStateToProps = (state) => { dyslexicFont: settings.get('dyslexicFont'), demetricator: settings.get('demetricator'), locale: validLocale(locale) ? locale : 'en', - themeCss: generateThemeCss(state.getIn(['soapbox', 'brandColor'])), + themeCss: generateThemeCss(soapboxConfig.get('brandColor')), themeMode: settings.get('themeMode'), - customCss: state.getIn(['soapbox', 'customCss']), + customCss: soapboxConfig.get('customCss'), }; }; diff --git a/app/soapbox/features/account/components/header.js b/app/soapbox/features/account/components/header.js index df5802b0c..444cb5272 100644 --- a/app/soapbox/features/account/components/header.js +++ b/app/soapbox/features/account/components/header.js @@ -5,6 +5,7 @@ import { connect } from 'react-redux'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import Icon from 'soapbox/components/icon'; import Button from 'soapbox/components/button'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { isStaff } from 'soapbox/utils/accounts'; @@ -226,7 +227,7 @@ class Header extends ImmutablePureComponent { const deactivated = account.getIn(['pleroma', 'deactivated'], false); return ( -
+
{info} @@ -239,48 +240,46 @@ class Header extends ImmutablePureComponent {
- { !deactivated && } +
- { !deactivated && -
- - - {shortNumberFormat(account.get('statuses_count'))} - - - - - {shortNumberFormat(account.get('following_count'))} - - - - - {shortNumberFormat(account.get('followers_count'))} - - - - { - account.get('id') === me && -
- - { /* : TODO : shortNumberFormat(account.get('favourite_count')) */ } - - - - - { /* : TODO : shortNumberFormat(account.get('pinned_count')) */ } - - - -
- } -
- } +
+ + + {shortNumberFormat(account.get('statuses_count'))} + + + + + {shortNumberFormat(account.get('following_count'))} + + + + + {shortNumberFormat(account.get('followers_count'))} + + + + { + account.get('id') === me && +
+ + { /* : TODO : shortNumberFormat(account.get('favourite_count')) */ } + + + + + { /* : TODO : shortNumberFormat(account.get('pinned_count')) */ } + + + +
+ } +
{ isSmallScreen && @@ -289,20 +288,18 @@ class Header extends ImmutablePureComponent {
} - { me && !deactivated && account.get('id') !== me && -
- - {(me && account.get('id') !== me) && - - } - { me && } -
+ { + me && +
+ + {account.get('id') !== me && account.getIn(['pleroma', 'accepts_chat_messages'], false) === true && + + } + +
}
diff --git a/app/soapbox/features/account_timeline/components/header.js b/app/soapbox/features/account_timeline/components/header.js index c7951ec66..534a12dbc 100644 --- a/app/soapbox/features/account_timeline/components/header.js +++ b/app/soapbox/features/account_timeline/components/header.js @@ -19,7 +19,7 @@ export default class Header extends ImmutablePureComponent { onMute: PropTypes.func.isRequired, onBlockDomain: PropTypes.func.isRequired, onUnblockDomain: PropTypes.func.isRequired, - onEndorseToggle: PropTypes.func.isRequired, + // onEndorseToggle: PropTypes.func.isRequired, onAddToList: PropTypes.func.isRequired, username: PropTypes.string, }; @@ -72,6 +72,10 @@ export default class Header extends ImmutablePureComponent { this.props.onUnblockDomain(domain); } + handleChat = () => { + this.props.onChat(this.props.account, this.context.router.history); + } + // handleEndorseToggle = () => { // this.props.onEndorseToggle(this.props.account); // } @@ -95,6 +99,7 @@ export default class Header extends ImmutablePureComponent { onBlock={this.handleBlock} onMention={this.handleMention} onDirect={this.handleDirect} + onChat={this.handleChat} onReblogToggle={this.handleReblogToggle} onReport={this.handleReport} onMute={this.handleMute} diff --git a/app/soapbox/features/account_timeline/containers/header_container.js b/app/soapbox/features/account_timeline/containers/header_container.js index 7049ac917..6eebe0772 100644 --- a/app/soapbox/features/account_timeline/containers/header_container.js +++ b/app/soapbox/features/account_timeline/containers/header_container.js @@ -22,6 +22,8 @@ import { blockDomain, unblockDomain } from '../../../actions/domain_blocks'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { List as ImmutableList } from 'immutable'; import { getSettings } from 'soapbox/actions/settings'; +import { startChat, openChat } from 'soapbox/actions/chats'; +import { isMobile } from 'soapbox/is_mobile'; const messages = defineMessages({ unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, @@ -127,12 +129,22 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(unblockDomain(domain)); }, - onAddToList(account){ + onAddToList(account) { dispatch(openModal('LIST_ADDER', { accountId: account.get('id'), })); }, + onChat(account, router) { + // TODO make this faster + dispatch(startChat(account.get('id'))).then(chat => { + if (isMobile(window.innerWidth)) { + router.push(`/chats/${chat.id}`); + } else { + dispatch(openChat(chat.id)); + } + }).catch(() => {}); + }, }); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header)); diff --git a/app/soapbox/features/account_timeline/index.js b/app/soapbox/features/account_timeline/index.js index 1ce040a3e..21a55181b 100644 --- a/app/soapbox/features/account_timeline/index.js +++ b/app/soapbox/features/account_timeline/index.js @@ -14,6 +14,7 @@ import { fetchAccountIdentityProofs } from '../../actions/identity_proofs'; import MissingIndicator from 'soapbox/components/missing_indicator'; import { NavLink } from 'react-router-dom'; import { fetchPatronAccount } from '../../actions/patron'; +import { getSoapboxConfig } from 'soapbox/actions/soapbox'; const emptyList = ImmutableList(); @@ -21,6 +22,7 @@ const mapStateToProps = (state, { params: { username }, withReplies = false }) = const me = state.get('me'); const accounts = state.getIn(['accounts']); const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() === username.toLowerCase()); + const soapboxConfig = getSoapboxConfig(state); let accountId = -1; let accountUsername = username; @@ -50,7 +52,7 @@ const mapStateToProps = (state, { params: { username }, withReplies = false }) = isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']), hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']), me, - patronEnabled: state.getIn(['soapbox', 'extensions', 'patron', 'enabled']), + patronEnabled: soapboxConfig.getIn(['extensions', 'patron', 'enabled']), }; }; diff --git a/app/soapbox/features/chats/chat_room.js b/app/soapbox/features/chats/chat_room.js new file mode 100644 index 000000000..e6df1953d --- /dev/null +++ b/app/soapbox/features/chats/chat_room.js @@ -0,0 +1,74 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { injectIntl } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import Avatar from 'soapbox/components/avatar'; +import { acctFull } from 'soapbox/utils/accounts'; +import { fetchChat } from 'soapbox/actions/chats'; +import ChatBox from './components/chat_box'; +import Column from 'soapbox/components/column'; +import ColumnBackButton from 'soapbox/components/column_back_button'; +import { makeGetChat } from 'soapbox/selectors'; + +const mapStateToProps = (state, { params }) => { + const getChat = makeGetChat(); + + return { + me: state.get('me'), + chat: getChat(state, { id: params.chatId }), + }; +}; + +export default @connect(mapStateToProps) +@injectIntl +class ChatRoom extends ImmutablePureComponent { + + static propTypes = { + dispatch: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + chat: ImmutablePropTypes.map, + me: PropTypes.node, + } + + handleInputRef = (el) => { + this.inputElem = el; + this.focusInput(); + }; + + focusInput = () => { + if (!this.inputElem) return; + this.inputElem.focus(); + } + + componentDidMount() { + const { dispatch, params } = this.props; + dispatch(fetchChat(params.chatId)); + } + + render() { + const { chat } = this.props; + if (!chat) return null; + const account = chat.get('account'); + + return ( + +
+ +
+ +
+ @{acctFull(account)} +
+
+
+ +
+ ); + } + +} diff --git a/app/soapbox/features/chats/components/chat.js b/app/soapbox/features/chats/components/chat.js new file mode 100644 index 000000000..1a09edd47 --- /dev/null +++ b/app/soapbox/features/chats/components/chat.js @@ -0,0 +1,49 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import Avatar from '../../../components/avatar'; +import DisplayName from '../../../components/display_name'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { shortNumberFormat } from 'soapbox/utils/numbers'; +import emojify from 'soapbox/features/emoji/emoji'; + +export default class Chat extends ImmutablePureComponent { + + static propTypes = { + chat: ImmutablePropTypes.map.isRequired, + onClick: PropTypes.func, + }; + + handleClick = () => { + this.props.onClick(this.props.chat); + } + + render() { + const { chat } = this.props; + if (!chat) return null; + const account = chat.get('account'); + const unreadCount = chat.get('unread'); + const content = chat.getIn(['last_message', 'content']); + const parsedContent = content ? emojify(content) : ''; + + return ( +
+
+ ); + } + +} diff --git a/app/soapbox/features/chats/components/chat_box.js b/app/soapbox/features/chats/components/chat_box.js new file mode 100644 index 000000000..0f1de7dbb --- /dev/null +++ b/app/soapbox/features/chats/components/chat_box.js @@ -0,0 +1,108 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { injectIntl, defineMessages } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { + fetchChatMessages, + sendChatMessage, + markChatRead, +} from 'soapbox/actions/chats'; +import { OrderedSet as ImmutableOrderedSet } from 'immutable'; +import ChatMessageList from './chat_message_list'; + +const messages = defineMessages({ + placeholder: { id: 'chat_box.input.placeholder', defaultMessage: 'Send a message…' }, +}); + +const mapStateToProps = (state, { chatId }) => ({ + me: state.get('me'), + chat: state.getIn(['chats', chatId]), + chatMessageIds: state.getIn(['chat_message_lists', chatId], ImmutableOrderedSet()), +}); + +export default @connect(mapStateToProps) +@injectIntl +class ChatBox extends ImmutablePureComponent { + + static propTypes = { + dispatch: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + chatId: PropTypes.string.isRequired, + chatMessageIds: ImmutablePropTypes.orderedSet, + chat: ImmutablePropTypes.map, + onSetInputRef: PropTypes.func, + me: PropTypes.node, + } + + state = { + content: '', + } + + handleKeyDown = (e) => { + const { chatId } = this.props; + if (e.key === 'Enter') { + this.props.dispatch(sendChatMessage(chatId, this.state)); + this.setState({ content: '' }); + e.preventDefault(); + } + } + + handleContentChange = (e) => { + this.setState({ content: e.target.value }); + } + + markRead = () => { + const { dispatch, chatId } = this.props; + dispatch(markChatRead(chatId)); + } + + handleHover = () => { + this.markRead(); + } + + setInputRef = (el) => { + const { onSetInputRef } = this.props; + this.inputElem = el; + onSetInputRef(el); + }; + + componentDidMount() { + const { dispatch, chatId } = this.props; + dispatch(fetchChatMessages(chatId)); + } + + componentDidUpdate(prevProps) { + const markReadConditions = [ + () => this.props.chat !== undefined, + () => document.activeElement === this.inputElem, + () => this.props.chat.get('unread') > 0, + ]; + + if (markReadConditions.every(c => c() === true)) + this.markRead(); + } + + render() { + const { chatMessageIds, intl } = this.props; + if (!chatMessageIds) return null; + + return ( +
+ +
+