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..ce1cab0f6 --- /dev/null +++ b/app/soapbox/actions/admin.js @@ -0,0 +1,18 @@ +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 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 }); + }); + }; +} 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/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/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/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..1eb680c94 100644 --- a/app/soapbox/components/sidebar_menu.js +++ b/app/soapbox/components/sidebar_menu.js @@ -29,6 +29,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' }, @@ -172,10 +173,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/containers/soapbox.js b/app/soapbox/containers/soapbox.js index 473e592ff..2c7d6f85c 100644 --- a/app/soapbox/containers/soapbox.js +++ b/app/soapbox/containers/soapbox.js @@ -23,6 +23,7 @@ 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'; @@ -42,6 +43,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 +54,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_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/compose/components/action_bar.js b/app/soapbox/features/compose/components/action_bar.js index d24cf8032..9db322093 100644 --- a/app/soapbox/features/compose/components/action_bar.js +++ b/app/soapbox/features/compose/components/action_bar.js @@ -19,6 +19,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' }, keyboard_shortcuts: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Hotkeys' }, @@ -80,7 +81,8 @@ class ActionBar extends React.PureComponent { menu.push(null); menu.push({ text: intl.formatMessage(messages.keyboard_shortcuts), action: this.handleHotkeyClick }); if (isStaff) { - menu.push({ text: intl.formatMessage(messages.admin_settings), href: '/pleroma/admin/', newTab: true }); + menu.push({ text: intl.formatMessage(messages.admin_settings), href: '/pleroma/admin', newTab: true }); + menu.push({ text: intl.formatMessage(messages.soapbox_config), to: '/soapbox/config' }); } menu.push({ text: intl.formatMessage(messages.preferences), to: '/settings/preferences' }); menu.push({ text: intl.formatMessage(messages.security), to: '/auth/edit' }); diff --git a/app/soapbox/features/forms/index.js b/app/soapbox/features/forms/index.js index e4f7a955a..9faf30e59 100644 --- a/app/soapbox/features/forms/index.js +++ b/app/soapbox/features/forms/index.js @@ -3,6 +3,10 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { v4 as uuidv4 } from 'uuid'; +import { SketchPicker } from 'react-color'; +import Overlay from 'react-overlays/lib/Overlay'; +import { isMobile } from '../../is_mobile'; +import detectPassiveEvents from 'detect-passive-events'; const FormPropTypes = { label: PropTypes.oneOfType([ @@ -12,6 +16,8 @@ const FormPropTypes = { ]), }; +const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; + export const InputContainer = (props) => { const containerClass = classNames('input', { 'with_label': props.label, @@ -186,6 +192,98 @@ export class RadioGroup extends ImmutablePureComponent { } +export class ColorPicker extends React.PureComponent { + + static propTypes = { + style: PropTypes.object, + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + onClose: PropTypes.func, + } + + handleDocumentClick = e => { + if (this.node && !this.node.contains(e.target)) { + this.props.onClose(); + } + } + + componentDidMount() { + document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); + } + + componentWillUnmount() { + document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); + } + + setRef = c => { + this.node = c; + } + + render() { + const { style, value, onChange } = this.props; + let margin_left_picker = isMobile(window.innerWidth) ? '20px' : '12px'; + + return ( + + + + ); + } + +} + +export class ColorWithPicker extends ImmutablePureComponent { + + static propTypes = { + buttonId: PropTypes.string.isRequired, + label: FormPropTypes.label, + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + } + + onToggle = (e) => { + if (!e.key || e.key === 'Enter') { + if (this.state.active) { + this.onHidePicker(); + } else { + this.onShowPicker(e); + } + } + } + + state = { + active: false, + placement: null, + } + + onHidePicker = () => { + this.setState({ active: false }); + } + + onShowPicker = ({ target }) => { + this.setState({ active: true }); + this.setState({ placement: isMobile(window.innerWidth) ? 'bottom' : 'right' }); + } + + render() { + const { buttonId, label, value, onChange } = this.props; + const { active, placement } = this.state; + + return ( + + {label} + + + + + + ); + } + +} + export class RadioItem extends ImmutablePureComponent { static propTypes = { @@ -256,3 +354,11 @@ export const FileChooser = props => ( FileChooser.defaultProps = { accept: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], }; + +export const FileChooserLogo = props => ( + +); + +FileChooserLogo.defaultProps = { + accept: ['image/svg', 'image/png'], +}; diff --git a/app/soapbox/features/preferences/index.js b/app/soapbox/features/preferences/index.js index 80a49a7b7..beaab5c25 100644 --- a/app/soapbox/features/preferences/index.js +++ b/app/soapbox/features/preferences/index.js @@ -13,7 +13,7 @@ import { RadioItem, SelectDropdown, } from 'soapbox/features/forms'; -import SettingsCheckbox from './components/settings_checkbox'; +import SettingsCheckbox from 'soapbox/components/settings_checkbox'; const languages = { en: 'English', diff --git a/app/soapbox/features/public_layout/components/footer.js b/app/soapbox/features/public_layout/components/footer.js index e7a071b19..d34248bc4 100644 --- a/app/soapbox/features/public_layout/components/footer.js +++ b/app/soapbox/features/public_layout/components/footer.js @@ -5,11 +5,16 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { Link } from 'react-router-dom'; import { List as ImmutableList } from 'immutable'; +import { getSoapboxConfig } from 'soapbox/actions/soapbox'; -const mapStateToProps = (state, props) => ({ - copyright: state.getIn(['soapbox', 'copyright']), - navlinks: state.getIn(['soapbox', 'navlinks', 'homeFooter'], ImmutableList()), -}); +const mapStateToProps = (state, props) => { + const soapboxConfig = getSoapboxConfig(state); + + return { + copyright: soapboxConfig.get('copyright'), + navlinks: soapboxConfig.getIn(['navlinks', 'homeFooter'], ImmutableList()), + }; +}; export default @connect(mapStateToProps) class Footer extends ImmutablePureComponent { diff --git a/app/soapbox/features/public_layout/components/site_banner.js b/app/soapbox/features/public_layout/components/site_banner.js index eb17c7980..2f0775bed 100644 --- a/app/soapbox/features/public_layout/components/site_banner.js +++ b/app/soapbox/features/public_layout/components/site_banner.js @@ -1,10 +1,11 @@ import React from 'react'; import { connect } from 'react-redux'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import { getSoapboxConfig } from 'soapbox/actions/soapbox'; const mapStateToProps = (state, props) => ({ instance: state.get('instance'), - soapbox: state.get('soapbox'), + soapbox: getSoapboxConfig(state), }); class SiteBanner extends ImmutablePureComponent { @@ -15,7 +16,7 @@ class SiteBanner extends ImmutablePureComponent { imgLogo: (), textLogo: ({instance.get('title')}), }; - return soapbox.has('banner') ? logos.imgLogo : logos.textLogo; + return soapbox.getIn(['banner']) ? logos.imgLogo : logos.textLogo; } } diff --git a/app/soapbox/features/public_layout/components/site_logo.js b/app/soapbox/features/public_layout/components/site_logo.js index a19ab5074..14a24f691 100644 --- a/app/soapbox/features/public_layout/components/site_logo.js +++ b/app/soapbox/features/public_layout/components/site_logo.js @@ -1,10 +1,11 @@ import React from 'react'; import { connect } from 'react-redux'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import { getSoapboxConfig } from 'soapbox/actions/soapbox'; const mapStateToProps = (state, props) => ({ instance: state.get('instance'), - soapbox: state.get('soapbox'), + soapbox: getSoapboxConfig(state), }); class SiteLogo extends ImmutablePureComponent { @@ -15,7 +16,7 @@ class SiteLogo extends ImmutablePureComponent { imgLogo: (), textLogo: ({instance.get('title')}), }; - return soapbox.has('logo') ? logos.imgLogo : logos.textLogo; + return soapbox.getIn(['logo']) ? logos.imgLogo : logos.textLogo; } } diff --git a/app/soapbox/features/public_layout/index.js b/app/soapbox/features/public_layout/index.js index e475d0ac6..3148dd478 100644 --- a/app/soapbox/features/public_layout/index.js +++ b/app/soapbox/features/public_layout/index.js @@ -7,10 +7,11 @@ import Header from './components/header'; import Footer from './components/footer'; import LandingPage from '../landing_page'; import AboutPage from '../about'; +import { getSoapboxConfig } from 'soapbox/actions/soapbox'; const mapStateToProps = (state, props) => ({ instance: state.get('instance'), - soapbox: state.get('soapbox'), + soapbox: getSoapboxConfig(state), }); const wave = ( diff --git a/app/soapbox/features/soapbox_config/index.js b/app/soapbox/features/soapbox_config/index.js new file mode 100644 index 000000000..b20d47a2f --- /dev/null +++ b/app/soapbox/features/soapbox_config/index.js @@ -0,0 +1,365 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import Column from '../ui/components/column'; +import { + SimpleForm, + FieldsGroup, + TextInput, + Checkbox, + FileChooser, + SimpleTextarea, + ColorWithPicker, + FileChooserLogo, +} from 'soapbox/features/forms'; +import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; +import { updateAdminConfig } from 'soapbox/actions/admin'; +import Icon from 'soapbox/components/icon'; +import { defaultConfig } from 'soapbox/actions/soapbox'; +import { uploadMedia } from 'soapbox/actions/media'; + +const messages = defineMessages({ + heading: { id: 'column.soapbox_config', defaultMessage: 'Soapbox config' }, + copyrightFooterLabel: { id: 'soapbox_config.copyright_footer.meta_fields.label_placeholder', defaultMessage: 'Copyright footer' }, + promoItemIcon: { id: 'soapbox_config.promo_panel.meta_fields.icon_placeholder', defaultMessage: 'Icon' }, + promoItemLabel: { id: 'soapbox_config.promo_panel.meta_fields.label_placeholder', defaultMessage: 'Label' }, + promoItemURL: { id: 'soapbox_config.promo_panel.meta_fields.url_placeholder', defaultMessage: 'URL' }, + homeFooterItemLabel: { id: 'soapbox_config.home_footer.meta_fields.label_placeholder', defaultMessage: 'Label' }, + homeFooterItemURL: { id: 'soapbox_config.home_footer.meta_fields.url_placeholder', defaultMessage: 'URL' }, + customCssLabel: { id: 'soapbox_config.custom_css.meta_fields.url_placeholder', defaultMessage: 'URL' }, + rawJSONLabel: { id: 'soapbox_config.raw_json_label', defaultMessage: 'Raw JSON data' }, + rawJSONHint: { id: 'soapbox_config.raw_json_hint', defaultMessage: 'Advanced: Edit the settings data directly.' }, +}); + +const templates = { + promoPanelItem: ImmutableMap({ icon: '', text: '', url: '' }), + footerItem: ImmutableMap({ title: '', url: '' }), +}; + +const mapStateToProps = state => ({ + soapbox: state.get('soapbox'), +}); + +export default @connect(mapStateToProps) +@injectIntl +class SoapboxConfig extends ImmutablePureComponent { + + static propTypes = { + soapbox: ImmutablePropTypes.map.isRequired, + dispatch: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + state = { + isLoading: false, + soapbox: this.props.soapbox, + rawJSON: JSON.stringify(this.props.soapbox, null, 2), + jsonValid: true, + } + + setConfig = (path, value) => { + const { soapbox } = this.state; + const config = soapbox.setIn(path, value); + this.setState({ soapbox: config, jsonValid: true }); + }; + + putConfig = config => { + this.setState({ soapbox: config, jsonValid: true }); + }; + + getParams = () => { + const { soapbox } = this.state; + return { + configs: [{ + group: ':pleroma', + key: ':frontend_configurations', + value: [{ + tuple: [':soapbox_fe', soapbox.toJSON()], + }], + }], + }; + } + + handleSubmit = (event) => { + const { dispatch } = this.props; + dispatch(updateAdminConfig(this.getParams())).then(() => { + this.setState({ isLoading: false }); + }).catch((error) => { + this.setState({ isLoading: false }); + }); + this.setState({ isLoading: true }); + event.preventDefault(); + } + + handleChange = (path, getValue) => { + return e => { + this.setConfig(path, getValue(e)); + }; + }; + + handleFileChange = path => { + return e => { + const data = new FormData(); + data.append('file', e.target.files[0]); + this.props.dispatch(uploadMedia(data)).then(({ data }) => { + this.handleChange(path, e => data.url)(e); + }).catch(() => {}); + }; + }; + + handleAddItem = (path, template) => { + return e => { + this.setConfig( + path, + this.getSoapboxConfig().getIn(path, ImmutableList()).push(template), + ); + }; + }; + + handleDeleteItem = path => { + return e => { + const soapbox = this.state.soapbox.deleteIn(path); + this.setState({ soapbox }); + }; + }; + + handleItemChange = (path, key, field, template) => { + return this.handleChange( + path, (e) => + template + .merge(field) + .set(key, e.target.value) + ); + }; + + handlePromoItemChange = (index, key, field) => { + return this.handleItemChange( + ['promoPanel', 'items', index], key, field, templates.promoPanelItem + ); + }; + + handleHomeFooterItemChange = (index, key, field) => { + return this.handleItemChange( + ['navlinks', 'homeFooter', index], key, field, templates.footerItem + ); + }; + + handleEditJSON = e => { + this.setState({ rawJSON: e.target.value }); + } + + getSoapboxConfig = () => { + return defaultConfig.mergeDeep(this.state.soapbox); + } + + componentDidUpdate(prevProps, prevState) { + if (prevProps.soapbox !== this.props.soapbox) { + this.putConfig(this.props.soapbox); + } + + if (prevState.soapbox !== this.state.soapbox) { + this.setState({ rawJSON: JSON.stringify(this.state.soapbox, null, 2) }); + } + + if (prevState.rawJSON !== this.state.rawJSON) { + try { + const data = fromJS(JSON.parse(this.state.rawJSON)); + this.putConfig(data); + } catch { + this.setState({ jsonValid: false }); + } + } + } + + render() { + const { intl } = this.props; + const soapbox = this.getSoapboxConfig(); + + return ( + + + + + + + + + + } + name='logo' + hint={} + onChange={this.handleFileChange(['logo'])} + /> + + + + + + + + } + name='banner' + hint={} + onChange={this.handleFileChange(['banner'])} + /> + + + + + + } + value={soapbox.get('brandColor')} + onChange={this.handleChange(['brandColor'], (e) => e.hex)} + /> + + + + } + hint={} + name='patron' + checked={soapbox.getIn(['extensions', 'patron', 'enabled'])} + onChange={this.handleChange( + ['extensions', 'patron', 'enabled'], (e) => e.checked, + )} + /> + + + e.target.value)} + /> + + + + + + + + + + Soapbox Icons List }} /> + + { + soapbox.getIn(['promoPanel', 'items']).map((field, i) => ( + + + + + + + )) + } + + + + + + + + + + + + + { + soapbox.getIn(['navlinks', 'homeFooter']).map((field, i) => ( + + + + + + )) + } + + + + + + + + + + + + + + { + soapbox.get('customCss').map((field, i) => ( + + e.target.value)} + /> + + + )) + } + + + + + + + + + + + + + + + + + + + + + + ); + } + +} diff --git a/app/soapbox/features/ui/components/promo_panel.js b/app/soapbox/features/ui/components/promo_panel.js index 06f686f2f..b91380bbc 100644 --- a/app/soapbox/features/ui/components/promo_panel.js +++ b/app/soapbox/features/ui/components/promo_panel.js @@ -2,9 +2,10 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Icon from 'soapbox/components/icon'; import { connect } from 'react-redux'; +import { getSoapboxConfig } from 'soapbox/actions/soapbox'; const mapStateToProps = state => ({ - promoItems: state.getIn(['soapbox', 'promoPanel', 'items']), + promoItems: getSoapboxConfig(state).getIn(['promoPanel', 'items']), }); export default @connect(mapStateToProps) diff --git a/app/soapbox/features/ui/components/tabs_bar.js b/app/soapbox/features/ui/components/tabs_bar.js index d26f294fe..1772b363f 100644 --- a/app/soapbox/features/ui/components/tabs_bar.js +++ b/app/soapbox/features/ui/components/tabs_bar.js @@ -13,6 +13,7 @@ import { openModal } from '../../../actions/modal'; import { openSidebar } from '../../../actions/sidebar'; import Icon from '../../../components/icon'; import ThemeToggle from '../../ui/components/theme_toggle'; +import { getSoapboxConfig } from 'soapbox/actions/soapbox'; const messages = defineMessages({ post: { id: 'tabs_bar.post', defaultMessage: 'Post' }, @@ -133,7 +134,7 @@ const mapStateToProps = state => { const me = state.get('me'); return { account: state.getIn(['accounts', me]), - logo: state.getIn(['soapbox', 'logo']), + logo: getSoapboxConfig(state).get('logo'), }; }; diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index e0cc6c599..dbc4ac464 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -72,6 +72,7 @@ import { LoginPage, Preferences, EditProfile, + SoapboxConfig, PasswordReset, SecurityForm, MfaForm, @@ -254,6 +255,7 @@ class SwitchingColumnsArea extends React.PureComponent { + diff --git a/app/soapbox/features/ui/util/async-components.js b/app/soapbox/features/ui/util/async-components.js index e6c468ea0..c93ad2f8e 100644 --- a/app/soapbox/features/ui/util/async-components.js +++ b/app/soapbox/features/ui/util/async-components.js @@ -182,6 +182,10 @@ export function EditProfile() { return import(/* webpackChunkName: "features/edit_profile" */'../../edit_profile'); } +export function SoapboxConfig() { + return import(/* webpackChunkName: "features/soapbox_config" */'../../soapbox_config'); +} + export function PasswordReset() { return import(/* webpackChunkName: "features/auth_login" */'../../auth_login/components/password_reset'); } diff --git a/app/soapbox/pages/home_page.js b/app/soapbox/pages/home_page.js index 81f2c93e3..01f299845 100644 --- a/app/soapbox/pages/home_page.js +++ b/app/soapbox/pages/home_page.js @@ -12,13 +12,14 @@ import ComposeFormContainer from '../features/compose/containers/compose_form_co import Avatar from '../components/avatar'; import { getFeatures } from 'soapbox/utils/features'; // import GroupSidebarPanel from '../features/groups/sidebar_panel'; +import { getSoapboxConfig } from 'soapbox/actions/soapbox'; const mapStateToProps = state => { const me = state.get('me'); return { me, account: state.getIn(['accounts', me]), - hasPatron: state.getIn(['soapbox', 'extensions', 'patron', 'enabled']), + hasPatron: getSoapboxConfig(state).getIn(['extensions', 'patron', 'enabled']), features: getFeatures(state.get('instance')), }; }; diff --git a/app/soapbox/reducers/__tests__/soapbox-test.js b/app/soapbox/reducers/__tests__/soapbox-test.js index f0829d66d..25106faac 100644 --- a/app/soapbox/reducers/__tests__/soapbox-test.js +++ b/app/soapbox/reducers/__tests__/soapbox-test.js @@ -1,8 +1,47 @@ import reducer from '../soapbox'; import { Map as ImmutableMap } from 'immutable'; +import * as actions from 'soapbox/actions/soapbox'; +import { ADMIN_CONFIG_UPDATE_SUCCESS } from 'soapbox/actions/admin'; +import soapbox from 'soapbox/__fixtures__/soapbox.json'; +import soapboxConfig from 'soapbox/__fixtures__/admin_api_frontend_config.json'; describe('soapbox reducer', () => { it('should return the initial state', () => { expect(reducer(undefined, {})).toEqual(ImmutableMap()); }); + + it('should handle SOAPBOX_CONFIG_REQUEST_SUCCESS', () => { + const state = ImmutableMap({ brandColor: '#354e91' }); + const action = { + type: actions.SOAPBOX_CONFIG_REQUEST_SUCCESS, + soapboxConfig: soapbox, + }; + expect(reducer(state, action).toJS()).toMatchObject({ + brandColor: '#254f92', + }); + }); + + // it('should handle SOAPBOX_CONFIG_REQUEST_FAIL', () => { + // const state = ImmutableMap({ skipAlert: false, brandColor: '#354e91' }); + // const action = { + // type: actions.SOAPBOX_CONFIG_REQUEST_FAIL, + // skipAlert: true, + // }; + // expect(reducer(state, action).toJS()).toMatchObject({ + // skipAlert: true, + // brandColor: '#354e91', + // }); + // }); + + it('should handle ADMIN_CONFIG_UPDATE_SUCCESS', () => { + const state = ImmutableMap({ brandColor: '#354e91' }); + const action = { + type: ADMIN_CONFIG_UPDATE_SUCCESS, + config: soapboxConfig, + }; + expect(reducer(state, action).toJS()).toMatchObject({ + brandColor: '#254f92', + }); + }); + }); diff --git a/app/soapbox/reducers/soapbox.js b/app/soapbox/reducers/soapbox.js index 147bd6c49..dd7b0bea3 100644 --- a/app/soapbox/reducers/soapbox.js +++ b/app/soapbox/reducers/soapbox.js @@ -1,21 +1,29 @@ -import { - SOAPBOX_CONFIG_REQUEST_SUCCESS, - SOAPBOX_CONFIG_REQUEST_FAIL, -} from '../actions/soapbox'; -import { Map as ImmutableMap, fromJS } from 'immutable'; +import { ADMIN_CONFIG_UPDATE_SUCCESS } from '../actions/admin'; +import { SOAPBOX_CONFIG_REQUEST_SUCCESS } from '../actions/soapbox'; +import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; +import { ConfigDB } from 'soapbox/utils/config_db'; const initialState = ImmutableMap(); -const defaultState = ImmutableMap({ - brandColor: '#0482d8', // Azure -}); +const updateFromAdmin = (state, config) => { + const configs = config.get('configs', ImmutableList()); + + try { + return ConfigDB.find(configs, ':pleroma', ':frontend_configurations') + .get('value') + .find(value => value.getIn(['tuple', 0]) === ':soapbox_fe') + .getIn(['tuple', 1]); + } catch { + return state; + } +}; export default function soapbox(state = initialState, action) { switch(action.type) { case SOAPBOX_CONFIG_REQUEST_SUCCESS: - return defaultState.merge(ImmutableMap(fromJS(action.soapboxConfig))); - case SOAPBOX_CONFIG_REQUEST_FAIL: - return defaultState; + return fromJS(action.soapboxConfig); + case ADMIN_CONFIG_UPDATE_SUCCESS: + return updateFromAdmin(state, fromJS(action.config)); default: return state; } diff --git a/app/soapbox/utils/__tests__/config_db-test.js b/app/soapbox/utils/__tests__/config_db-test.js new file mode 100644 index 000000000..2a185fbf4 --- /dev/null +++ b/app/soapbox/utils/__tests__/config_db-test.js @@ -0,0 +1,12 @@ +import { ConfigDB } from '../config_db'; +import config_db from 'soapbox/__fixtures__/config_db.json'; +import { fromJS } from 'immutable'; + +test('find', () => { + const configs = fromJS(config_db).get('configs'); + expect(ConfigDB.find(configs, ':phoenix', ':json_library')).toEqual(fromJS({ + group: ':phoenix', + key: ':json_library', + value: 'Jason', + })); +}); diff --git a/app/soapbox/utils/config_db.js b/app/soapbox/utils/config_db.js new file mode 100644 index 000000000..c4661ad00 --- /dev/null +++ b/app/soapbox/utils/config_db.js @@ -0,0 +1,9 @@ +export const ConfigDB = { + find: (configs, group, key) => { + return configs.find(config => + config.isSuperset({ group, key }) + ); + }, +}; + +export default ConfigDB; diff --git a/app/styles/components/buttons.scss b/app/styles/components/buttons.scss index 49070ad6c..5eeda468e 100644 --- a/app/styles/components/buttons.scss +++ b/app/styles/components/buttons.scss @@ -108,6 +108,10 @@ button { &:disabled { opacity: 0.5; } + + i.fa { + margin-right: 0.5em; + } } &.button--block { diff --git a/app/styles/forms.scss b/app/styles/forms.scss index 1ec3ceb7d..5471f9778 100644 --- a/app/styles/forms.scss +++ b/app/styles/forms.scss @@ -389,7 +389,8 @@ code { button, .button, - .block-button { + .block-button, + .color-swatch { display: block; width: 100%; border: 0; @@ -453,6 +454,18 @@ code { } .label_input { + + &__color { + display: inline-flex; + font-size: 14px; + + .color-swatch { + width: 32px; + height: 16px; + margin-left: 12px; + } + } + &__wrapper { position: relative; } @@ -971,3 +984,25 @@ code { margin-top: 10px; } } + +.file-picker img { + max-width: 100px; + max-height: 100px; +} + +.code-editor textarea { + font-family: monospace; + white-space: pre; +} + +.code-editor--invalid textarea { + border-color: $error-red !important; + color: $error-red; +} + +.input .row .fa-times-circle { + position: absolute; + right: 7px; + cursor: pointer; + color: $error-red; +} diff --git a/package.json b/package.json index ad53a512d..e79c4356c 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ "qrcode.react": "^1.0.0", "rails-ujs": "^5.2.3", "react": "^16.13.1", + "react-color": "^2.18.1", "react-dom": "^16.13.1", "react-helmet": "^6.0.0", "react-hotkeys": "^1.1.4", diff --git a/yarn.lock b/yarn.lock index 496871844..561257434 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1229,6 +1229,11 @@ dependencies: emojis-list "^3.0.0" +"@icons/material@^0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8" + integrity sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -7411,6 +7416,11 @@ lodash@^4.0.0, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.1 resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== +lodash@^4.0.1: + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== + lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.5, lodash@^4.7.11: version "4.17.11" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" @@ -7510,6 +7520,11 @@ marky@^1.2.1: resolved "https://registry.yarnpkg.com/marky/-/marky-1.2.1.tgz#a3fcf82ffd357756b8b8affec9fdbf3a30dc1b02" integrity sha512-md9k+Gxa3qLH6sUKpeC2CNkJK/Ld+bEz5X96nYwloqphQE0CKCVEKco/6jxEZixinqNdz5RFi/KaCyfbMDMAXQ== +material-colors@^1.2.1: + version "1.2.6" + resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46" + integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg== + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -9437,6 +9452,18 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-color@^2.18.1: + version "2.18.1" + resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.18.1.tgz#2cda8cc8e06a9e2c52ad391a30ddad31972472f4" + integrity sha512-X5XpyJS6ncplZs74ak0JJoqPi+33Nzpv5RYWWxn17bslih+X7OlgmfpmGC1fNvdkK7/SGWYf1JJdn7D2n5gSuQ== + dependencies: + "@icons/material" "^0.2.4" + lodash "^4.17.11" + material-colors "^1.2.1" + prop-types "^15.5.10" + reactcss "^1.2.0" + tinycolor2 "^1.4.1" + react-dom@^16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f" @@ -9752,6 +9779,13 @@ react@^16.13.1: object-assign "^4.1.1" prop-types "^15.6.2" +reactcss@^1.2.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/reactcss/-/reactcss-1.2.3.tgz#c00013875e557b1cf0dfd9a368a1c3dab3b548dd" + integrity sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A== + dependencies: + lodash "^4.0.1" + read-pkg-up@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" @@ -11344,6 +11378,11 @@ tiny-warning@^1.0.0: resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== +tinycolor2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8" + integrity sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g= + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"