From d2528b26b6da34f34b5d7a392e263428d3c09d69 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 5 Oct 2022 03:47:56 +0200 Subject: Add server banner to web app, add `GET /api/v2/instance` to REST API (#19294) --- app/views/shared/_og.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app/views/shared') diff --git a/app/views/shared/_og.html.haml b/app/views/shared/_og.html.haml index 7feae1b8b..b54ab2429 100644 --- a/app/views/shared/_og.html.haml +++ b/app/views/shared/_og.html.haml @@ -1,12 +1,12 @@ - thumbnail = @instance_presenter.thumbnail -- description ||= strip_tags(@instance_presenter.site_short_description.presence || t('about.about_mastodon_html')) +- description ||= strip_tags(@instance_presenter.description.presence || t('about.about_mastodon_html')) %meta{ name: 'description', content: description }/ = opengraph 'og:site_name', t('about.hosted_on', domain: site_hostname) = opengraph 'og:url', url_for(only_path: false) = opengraph 'og:type', 'website' -= opengraph 'og:title', @instance_presenter.site_title += opengraph 'og:title', @instance_presenter.title = opengraph 'og:description', description = opengraph 'og:image', full_asset_url(thumbnail&.file&.url || asset_pack_path('media/images/preview.png', protocol: :request)) = opengraph 'og:image:width', thumbnail ? thumbnail.meta['width'] : '1200' -- cgit From 62782babd08bc2385a604e275bf88af925d137c1 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 6 Oct 2022 02:26:34 +0200 Subject: Change public statuses pages to mount the web UI (#19301) --- .../concerns/web_app_controller_concern.rb | 18 ++++++++ app/controllers/home_controller.rb | 11 ++--- app/controllers/statuses_controller.rb | 12 +----- app/views/home/index.html.haml | 18 +------- app/views/shared/_web_app.html.haml | 17 ++++++++ app/views/statuses/show.html.haml | 7 +--- spec/views/statuses/show.html.haml_spec.rb | 48 ---------------------- 7 files changed, 42 insertions(+), 89 deletions(-) create mode 100644 app/controllers/concerns/web_app_controller_concern.rb create mode 100644 app/views/shared/_web_app.html.haml (limited to 'app/views/shared') diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb new file mode 100644 index 000000000..8a6c73af3 --- /dev/null +++ b/app/controllers/concerns/web_app_controller_concern.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module WebAppControllerConcern + extend ActiveSupport::Concern + + included do + before_action :set_body_classes + before_action :set_referrer_policy_header + end + + def set_body_classes + @body_classes = 'app-body' + end + + def set_referrer_policy_header + response.headers['Referrer-Policy'] = 'origin' + end +end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 29478209d..b4d6578b9 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -1,13 +1,12 @@ # frozen_string_literal: true class HomeController < ApplicationController + include WebAppControllerConcern + before_action :redirect_unauthenticated_to_permalinks! - before_action :set_referrer_policy_header before_action :set_instance_presenter - def index - @body_classes = 'app-body' - end + def index; end private @@ -19,10 +18,6 @@ class HomeController < ApplicationController redirect_to(redirect_path) if redirect_path.present? end - def set_referrer_policy_header - response.headers['Referrer-Policy'] = 'origin' - end - def set_instance_presenter @instance_presenter = InstancePresenter.new end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 7d9db4d5b..181c76c9a 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -5,17 +5,15 @@ class StatusesController < ApplicationController include SignatureAuthentication include Authorization include AccountOwnedConcern - - layout 'public' + include WebAppControllerConcern before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_status before_action :set_instance_presenter before_action :set_link_headers before_action :redirect_to_original, only: :show - before_action :set_referrer_policy_header, only: :show before_action :set_cache_headers - before_action :set_body_classes + before_action :set_body_classes, only: :embed skip_around_action :set_locale, if: -> { request.format == :json } skip_before_action :require_functional!, only: [:show, :embed], unless: :whitelist_mode? @@ -28,8 +26,6 @@ class StatusesController < ApplicationController respond_to do |format| format.html do expires_in 10.seconds, public: true if current_account.nil? - set_ancestors - set_descendants end format.json do @@ -77,8 +73,4 @@ class StatusesController < ApplicationController def redirect_to_original redirect_to ActivityPub::TagManager.instance.url_for(@status.reblog) if @status.reblog? end - - def set_referrer_policy_header - response.headers['Referrer-Policy'] = 'origin' unless @status.distributable? - end end diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index 19c5191d8..76a02e0f0 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -1,20 +1,4 @@ - content_for :header_tags do - - if user_signed_in? - = preload_pack_asset 'features/getting_started.js', crossorigin: 'anonymous' - = preload_pack_asset 'features/compose.js', crossorigin: 'anonymous' - = preload_pack_asset 'features/home_timeline.js', crossorigin: 'anonymous' - = preload_pack_asset 'features/notifications.js', crossorigin: 'anonymous' - = render partial: 'shared/og' - %meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key} - - = render_initial_state - = javascript_pack_tag 'application', crossorigin: 'anonymous' - -.notranslate.app-holder#mastodon{ data: { props: Oj.dump(default_props) } } - %noscript - = image_pack_tag 'logo.svg', alt: 'Mastodon' - - %div - = t('errors.noscript_html', apps_path: 'https://joinmastodon.org/apps') += render 'shared/web_app' diff --git a/app/views/shared/_web_app.html.haml b/app/views/shared/_web_app.html.haml new file mode 100644 index 000000000..998cee9fa --- /dev/null +++ b/app/views/shared/_web_app.html.haml @@ -0,0 +1,17 @@ +- content_for :header_tags do + - if user_signed_in? + = preload_pack_asset 'features/compose.js', crossorigin: 'anonymous' + = preload_pack_asset 'features/home_timeline.js', crossorigin: 'anonymous' + = preload_pack_asset 'features/notifications.js', crossorigin: 'anonymous' + + %meta{ name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key } + + = render_initial_state + = javascript_pack_tag 'application', crossorigin: 'anonymous' + +.notranslate.app-holder#mastodon{ data: { props: Oj.dump(default_props) } } + %noscript + = image_pack_tag 'logo.svg', alt: 'Mastodon' + + %div + = t('errors.noscript_html', apps_path: 'https://joinmastodon.org/apps') diff --git a/app/views/statuses/show.html.haml b/app/views/statuses/show.html.haml index 7ef7b09a2..5a3c94b84 100644 --- a/app/views/statuses/show.html.haml +++ b/app/views/statuses/show.html.haml @@ -17,9 +17,4 @@ = render 'og_description', activity: @status = render 'og_image', activity: @status, account: @account -.grid - .column-0 - .activity-stream.h-entry - = render partial: 'status', locals: { status: @status, include_threads: true } - .column-1 - = render 'application/sidebar' += render 'shared/web_app' diff --git a/spec/views/statuses/show.html.haml_spec.rb b/spec/views/statuses/show.html.haml_spec.rb index 879a26959..a69843216 100644 --- a/spec/views/statuses/show.html.haml_spec.rb +++ b/spec/views/statuses/show.html.haml_spec.rb @@ -15,54 +15,6 @@ describe 'statuses/show.html.haml', without_verify_partial_doubles: true do assign(:instance_presenter, InstancePresenter.new) end - it 'has valid author h-card and basic data for a detailed_status' do - alice = Fabricate(:account, username: 'alice', display_name: 'Alice') - bob = Fabricate(:account, username: 'bob', display_name: 'Bob') - status = Fabricate(:status, account: alice, text: 'Hello World') - media = Fabricate(:media_attachment, account: alice, status: status, type: :video) - reply = Fabricate(:status, account: bob, thread: status, text: 'Hello Alice') - - assign(:status, status) - assign(:account, alice) - assign(:descendant_threads, []) - - render - - mf2 = Microformats.parse(rendered) - - expect(mf2.entry.url.to_s).not_to be_empty - expect(mf2.entry.author.name.to_s).to eq alice.display_name - expect(mf2.entry.author.url.to_s).not_to be_empty - end - - it 'has valid h-cites for p-in-reply-to and p-comment' do - alice = Fabricate(:account, username: 'alice', display_name: 'Alice') - bob = Fabricate(:account, username: 'bob', display_name: 'Bob') - carl = Fabricate(:account, username: 'carl', display_name: 'Carl') - status = Fabricate(:status, account: alice, text: 'Hello World') - media = Fabricate(:media_attachment, account: alice, status: status, type: :video) - reply = Fabricate(:status, account: bob, thread: status, text: 'Hello Alice') - comment = Fabricate(:status, account: carl, thread: reply, text: 'Hello Bob') - - assign(:status, reply) - assign(:account, alice) - assign(:ancestors, reply.ancestors(1, bob)) - assign(:descendant_threads, [{ statuses: reply.descendants(1) }]) - - render - - mf2 = Microformats.parse(rendered) - - expect(mf2.entry.url.to_s).not_to be_empty - expect(mf2.entry.comment.url.to_s).not_to be_empty - expect(mf2.entry.comment.author.name.to_s).to eq carl.display_name - expect(mf2.entry.comment.author.url.to_s).not_to be_empty - - expect(mf2.entry.in_reply_to.url.to_s).not_to be_empty - expect(mf2.entry.in_reply_to.author.name.to_s).to eq alice.display_name - expect(mf2.entry.in_reply_to.author.url.to_s).not_to be_empty - end - it 'has valid opengraph tags' do alice = Fabricate(:account, username: 'alice', display_name: 'Alice') status = Fabricate(:status, account: alice, text: 'Hello World') -- cgit From bf3cb42da78a3f31a9e07167a764aa942ec73731 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 5 Oct 2022 03:47:56 +0200 Subject: Add server banner to web app, add `GET /api/v2/instance` to REST API (#19294) Cherry-picked d2528b26b6da34f34b5d7a392e263428d3c09d69 Conflicts: - `app/serializers/initial_state_serializer.rb`: Upstream changed stuff, we had extra attributes. Applied upstream changes while keeping our extra attributes. - `app/serializers/rest/instance_serializer.rb`: Upstream actually moved that to `app/serializers/rest/v1/instance_serializer.rb`, so updated that file by keeping our extra attributes, and took upstream's version of `app/serializers/rest/instance_serializer.rb`. - `spec/views/about/show.html.haml_spec.rb`: Took upstream's version. --- app/controllers/about_controller.rb | 2 +- app/controllers/api/v1/instances_controller.rb | 2 +- app/controllers/api/v2/instances_controller.rb | 8 ++ app/javascript/mastodon/actions/rules.js | 27 ----- app/javascript/mastodon/actions/server.js | 30 ++++++ app/javascript/mastodon/components/account.js | 14 ++- app/javascript/mastodon/components/display_name.js | 8 +- .../mastodon/components/server_banner.js | 91 ++++++++++++++++ app/javascript/mastodon/features/report/rules.js | 2 +- .../features/ui/components/compose_panel.js | 2 + .../features/ui/components/report_modal.js | 4 +- app/javascript/mastodon/features/ui/index.js | 4 +- app/javascript/mastodon/initial_state.js | 1 - app/javascript/mastodon/reducers/index.js | 4 +- app/javascript/mastodon/reducers/rules.js | 13 --- app/javascript/mastodon/reducers/server.js | 19 ++++ app/javascript/styles/mastodon/components.scss | 82 +++++++++++++++ app/presenters/instance_presenter.rb | 64 +++++++++--- app/serializers/initial_state_serializer.rb | 19 ++-- app/serializers/manifest_serializer.rb | 4 +- app/serializers/rest/instance_serializer.rb | 94 ++++------------- app/serializers/rest/v1/instance_serializer.rb | 115 +++++++++++++++++++++ app/views/about/more.html.haml | 8 +- app/views/about/show.html.haml | 6 +- app/views/application/_sidebar.html.haml | 4 +- app/views/privacy/show.html.haml | 2 +- app/views/shared/_og.html.haml | 4 +- config/routes.rb | 4 +- spec/presenters/instance_presenter_spec.rb | 38 +++---- spec/views/about/show.html.haml_spec.rb | 21 +--- 30 files changed, 486 insertions(+), 210 deletions(-) create mode 100644 app/controllers/api/v2/instances_controller.rb delete mode 100644 app/javascript/mastodon/actions/rules.js create mode 100644 app/javascript/mastodon/actions/server.js create mode 100644 app/javascript/mastodon/components/server_banner.js delete mode 100644 app/javascript/mastodon/reducers/rules.js create mode 100644 app/javascript/mastodon/reducers/server.js create mode 100644 app/serializers/rest/v1/instance_serializer.rb (limited to 'app/views/shared') diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 654f2d92c..d3f03374f 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -20,7 +20,7 @@ class AboutController < ApplicationController def more flash.now[:notice] = I18n.t('about.instance_actor_flash') if params[:instance_actor] - toc_generator = TOCGenerator.new(@instance_presenter.site_extended_description) + toc_generator = TOCGenerator.new(@instance_presenter.extended_description) @rules = Rule.ordered @contents = toc_generator.html diff --git a/app/controllers/api/v1/instances_controller.rb b/app/controllers/api/v1/instances_controller.rb index 5b5058a7b..913319a86 100644 --- a/app/controllers/api/v1/instances_controller.rb +++ b/app/controllers/api/v1/instances_controller.rb @@ -6,6 +6,6 @@ class Api::V1::InstancesController < Api::BaseController def show expires_in 3.minutes, public: true - render_with_cache json: {}, serializer: REST::InstanceSerializer, root: 'instance' + render_with_cache json: InstancePresenter.new, serializer: REST::V1::InstanceSerializer, root: 'instance' end end diff --git a/app/controllers/api/v2/instances_controller.rb b/app/controllers/api/v2/instances_controller.rb new file mode 100644 index 000000000..bcd90cff2 --- /dev/null +++ b/app/controllers/api/v2/instances_controller.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class Api::V2::InstancesController < Api::V1::InstancesController + def show + expires_in 3.minutes, public: true + render_with_cache json: InstancePresenter.new, serializer: REST::InstanceSerializer, root: 'instance' + end +end diff --git a/app/javascript/mastodon/actions/rules.js b/app/javascript/mastodon/actions/rules.js deleted file mode 100644 index 34e60a121..000000000 --- a/app/javascript/mastodon/actions/rules.js +++ /dev/null @@ -1,27 +0,0 @@ -import api from '../api'; - -export const RULES_FETCH_REQUEST = 'RULES_FETCH_REQUEST'; -export const RULES_FETCH_SUCCESS = 'RULES_FETCH_SUCCESS'; -export const RULES_FETCH_FAIL = 'RULES_FETCH_FAIL'; - -export const fetchRules = () => (dispatch, getState) => { - dispatch(fetchRulesRequest()); - - api(getState) - .get('/api/v1/instance').then(({ data }) => dispatch(fetchRulesSuccess(data.rules))) - .catch(err => dispatch(fetchRulesFail(err))); -}; - -const fetchRulesRequest = () => ({ - type: RULES_FETCH_REQUEST, -}); - -const fetchRulesSuccess = rules => ({ - type: RULES_FETCH_SUCCESS, - rules, -}); - -const fetchRulesFail = error => ({ - type: RULES_FETCH_FAIL, - error, -}); diff --git a/app/javascript/mastodon/actions/server.js b/app/javascript/mastodon/actions/server.js new file mode 100644 index 000000000..af8fef780 --- /dev/null +++ b/app/javascript/mastodon/actions/server.js @@ -0,0 +1,30 @@ +import api from '../api'; +import { importFetchedAccount } from './importer'; + +export const SERVER_FETCH_REQUEST = 'Server_FETCH_REQUEST'; +export const SERVER_FETCH_SUCCESS = 'Server_FETCH_SUCCESS'; +export const SERVER_FETCH_FAIL = 'Server_FETCH_FAIL'; + +export const fetchServer = () => (dispatch, getState) => { + dispatch(fetchServerRequest()); + + api(getState) + .get('/api/v2/instance').then(({ data }) => { + if (data.contact.account) dispatch(importFetchedAccount(data.contact.account)); + dispatch(fetchServerSuccess(data)); + }).catch(err => dispatch(fetchServerFail(err))); +}; + +const fetchServerRequest = () => ({ + type: SERVER_FETCH_REQUEST, +}); + +const fetchServerSuccess = server => ({ + type: SERVER_FETCH_SUCCESS, + server, +}); + +const fetchServerFail = error => ({ + type: SERVER_FETCH_FAIL, + error, +}); diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js index af9f119c8..36429e647 100644 --- a/app/javascript/mastodon/components/account.js +++ b/app/javascript/mastodon/components/account.js @@ -9,6 +9,7 @@ import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { me } from '../initial_state'; import RelativeTimestamp from './relative_timestamp'; +import Skeleton from 'mastodon/components/skeleton'; const messages = defineMessages({ follow: { id: 'account.follow', defaultMessage: 'Follow' }, @@ -26,7 +27,7 @@ export default @injectIntl class Account extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.map, onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired, @@ -67,7 +68,16 @@ class Account extends ImmutablePureComponent { const { account, intl, hidden, onActionClick, actionIcon, actionTitle, defaultAction } = this.props; if (!account) { - return
; + return ( +
+
+
+
+ +
+
+
+ ); } if (hidden) { diff --git a/app/javascript/mastodon/components/display_name.js b/app/javascript/mastodon/components/display_name.js index 7ccfbd0cd..e9139ab0f 100644 --- a/app/javascript/mastodon/components/display_name.js +++ b/app/javascript/mastodon/components/display_name.js @@ -2,11 +2,12 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { autoPlayGif } from 'mastodon/initial_state'; +import Skeleton from 'mastodon/components/skeleton'; export default class DisplayName extends React.PureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.map, others: ImmutablePropTypes.list, localDomain: PropTypes.string, }; @@ -48,7 +49,7 @@ export default class DisplayName extends React.PureComponent { if (others.size - 2 > 0) { suffix = `+${others.size - 2}`; } - } else { + } else if ((others && others.size > 0) || this.props.account) { if (others && others.size > 0) { account = others.first(); } else { @@ -63,6 +64,9 @@ export default class DisplayName extends React.PureComponent { displayName = ; suffix = @{acct}; + } else { + displayName = ; + suffix = ; } return ( diff --git a/app/javascript/mastodon/components/server_banner.js b/app/javascript/mastodon/components/server_banner.js new file mode 100644 index 000000000..bdd7f7380 --- /dev/null +++ b/app/javascript/mastodon/components/server_banner.js @@ -0,0 +1,91 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { domain } from 'mastodon/initial_state'; +import { fetchServer } from 'mastodon/actions/server'; +import { connect } from 'react-redux'; +import Account from 'mastodon/containers/account_container'; +import ShortNumber from 'mastodon/components/short_number'; +import Skeleton from 'mastodon/components/skeleton'; +import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; + +const messages = defineMessages({ + aboutActiveUsers: { id: 'server_banner.about_active_users', defaultMessage: 'People using this server during the last 30 days (Monthly Active Users)' }, +}); + +const mapStateToProps = state => ({ + server: state.get('server'), +}); + +export default @connect(mapStateToProps) +@injectIntl +class ServerBanner extends React.PureComponent { + + static propTypes = { + server: PropTypes.object, + dispatch: PropTypes.func, + intl: PropTypes.object, + }; + + componentDidMount () { + const { dispatch } = this.props; + dispatch(fetchServer()); + } + + render () { + const { server, intl } = this.props; + const isLoading = server.get('isLoading'); + + return ( +
+
+ {domain}, mastodon: Mastodon }} /> +
+ + {server.get('title')} + +
+ {isLoading ? ( + <> + +
+ +
+ + + ) : server.get('description')} +
+ +
+
+

+ + +
+ +
+

+ + {isLoading ? ( + <> + +
+ + + ) : ( + <> + +
+ + + )} +
+
+ +
+ + +
+ ); + } + +} diff --git a/app/javascript/mastodon/features/report/rules.js b/app/javascript/mastodon/features/report/rules.js index f2db0d9e4..2cb4a95b5 100644 --- a/app/javascript/mastodon/features/report/rules.js +++ b/app/javascript/mastodon/features/report/rules.js @@ -7,7 +7,7 @@ import Button from 'mastodon/components/button'; import Option from './components/option'; const mapStateToProps = state => ({ - rules: state.get('rules'), + rules: state.getIn(['server', 'rules']), }); export default @connect(mapStateToProps) diff --git a/app/javascript/mastodon/features/ui/components/compose_panel.js b/app/javascript/mastodon/features/ui/components/compose_panel.js index 1c128188f..c8bc79a67 100644 --- a/app/javascript/mastodon/features/ui/components/compose_panel.js +++ b/app/javascript/mastodon/features/ui/components/compose_panel.js @@ -5,6 +5,7 @@ import SearchContainer from 'mastodon/features/compose/containers/search_contain import ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container'; import NavigationContainer from 'mastodon/features/compose/containers/navigation_container'; import LinkFooter from './link_footer'; +import ServerBanner from 'mastodon/components/server_banner'; import { changeComposing } from 'mastodon/actions/compose'; export default @connect() @@ -35,6 +36,7 @@ class ComposePanel extends React.PureComponent { {!signedIn && ( +
)} diff --git a/app/javascript/mastodon/features/ui/components/report_modal.js b/app/javascript/mastodon/features/ui/components/report_modal.js index 744dd248b..264da07ce 100644 --- a/app/javascript/mastodon/features/ui/components/report_modal.js +++ b/app/javascript/mastodon/features/ui/components/report_modal.js @@ -2,7 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { submitReport } from 'mastodon/actions/reports'; import { expandAccountTimeline } from 'mastodon/actions/timelines'; -import { fetchRules } from 'mastodon/actions/rules'; +import { fetchServer } from 'mastodon/actions/server'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { makeGetAccount } from 'mastodon/selectors'; @@ -117,7 +117,7 @@ class ReportModal extends ImmutablePureComponent { const { dispatch, accountId } = this.props; dispatch(expandAccountTimeline(accountId, { withReplies: true })); - dispatch(fetchRules()); + dispatch(fetchServer()); } render () { diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 5825db1e4..efe460fab 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -13,7 +13,7 @@ import { debounce } from 'lodash'; import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose'; import { expandHomeTimeline } from '../../actions/timelines'; import { expandNotifications } from '../../actions/notifications'; -import { fetchRules } from '../../actions/rules'; +import { fetchServer } from '../../actions/server'; import { clearHeight } from '../../actions/height_cache'; import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app'; import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers'; @@ -389,7 +389,7 @@ class UI extends React.PureComponent { this.props.dispatch(expandHomeTimeline()); this.props.dispatch(expandNotifications()); - setTimeout(() => this.props.dispatch(fetchRules()), 3000); + setTimeout(() => this.props.dispatch(fetchServer()), 3000); } this.hotkeys.__mousetrap__.stopCallback = (e, element) => { diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index 3587bb32a..08121005a 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -29,6 +29,5 @@ export const title = getMeta('title'); export const cropImages = getMeta('crop_images'); export const disableSwiping = getMeta('disable_swiping'); export const languages = initialState && initialState.languages; -export const server = initialState && initialState.server; export default initialState; diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index d3d0303df..bccdc1865 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -17,7 +17,7 @@ import status_lists from './status_lists'; import mutes from './mutes'; import blocks from './blocks'; import boosts from './boosts'; -import rules from './rules'; +import server from './server'; import contexts from './contexts'; import compose from './compose'; import search from './search'; @@ -62,7 +62,7 @@ const reducers = { mutes, blocks, boosts, - rules, + server, contexts, compose, search, diff --git a/app/javascript/mastodon/reducers/rules.js b/app/javascript/mastodon/reducers/rules.js deleted file mode 100644 index c1180b520..000000000 --- a/app/javascript/mastodon/reducers/rules.js +++ /dev/null @@ -1,13 +0,0 @@ -import { RULES_FETCH_SUCCESS } from 'mastodon/actions/rules'; -import { List as ImmutableList, fromJS } from 'immutable'; - -const initialState = ImmutableList(); - -export default function rules(state = initialState, action) { - switch (action.type) { - case RULES_FETCH_SUCCESS: - return fromJS(action.rules); - default: - return state; - } -} diff --git a/app/javascript/mastodon/reducers/server.js b/app/javascript/mastodon/reducers/server.js new file mode 100644 index 000000000..68131c6dd --- /dev/null +++ b/app/javascript/mastodon/reducers/server.js @@ -0,0 +1,19 @@ +import { SERVER_FETCH_REQUEST, SERVER_FETCH_SUCCESS, SERVER_FETCH_FAIL } from 'mastodon/actions/server'; +import { Map as ImmutableMap, fromJS } from 'immutable'; + +const initialState = ImmutableMap({ + isLoading: true, +}); + +export default function server(state = initialState, action) { + switch (action.type) { + case SERVER_FETCH_REQUEST: + return state.set('isLoading', true); + case SERVER_FETCH_SUCCESS: + return fromJS(action.server).set('isLoading', false); + case SERVER_FETCH_FAIL: + return state.set('isLoading', false); + default: + return state; + } +} diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index a75437b1c..b906117db 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -7949,3 +7949,85 @@ noscript { } } } + +.server-banner { + padding: 20px 0; + + &__introduction { + color: $darker-text-color; + margin-bottom: 20px; + + strong { + font-weight: 600; + } + + a { + color: inherit; + text-decoration: underline; + + &:hover, + &:active, + &:focus { + text-decoration: none; + } + } + } + + &__hero { + display: block; + border-radius: 4px; + width: 100%; + height: auto; + margin-bottom: 20px; + aspect-ratio: 1.9; + border: 0; + background: $ui-base-color; + object-fit: cover; + } + + &__description { + margin-bottom: 20px; + } + + &__meta { + display: flex; + gap: 10px; + max-width: 100%; + + &__column { + flex: 0 0 auto; + width: calc(50% - 5px); + overflow: hidden; + } + } + + &__number { + font-weight: 600; + color: $primary-text-color; + } + + &__number-label { + color: $darker-text-color; + font-weight: 500; + } + + h4 { + text-transform: uppercase; + color: $darker-text-color; + margin-bottom: 10px; + font-weight: 600; + } + + .account { + padding: 0; + border: 0; + } + + .account__avatar-wrapper { + margin-left: 0; + } + + .spacer { + margin: 10px 0; + } +} diff --git a/app/presenters/instance_presenter.rb b/app/presenters/instance_presenter.rb index 3e85faa92..c461ac55f 100644 --- a/app/presenters/instance_presenter.rb +++ b/app/presenters/instance_presenter.rb @@ -1,19 +1,51 @@ # frozen_string_literal: true -class InstancePresenter - delegate( - :site_contact_email, - :site_title, - :site_short_description, - :site_description, - :site_extended_description, - :site_terms, - :closed_registrations_message, - to: Setting - ) - - def contact_account - Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')) +class InstancePresenter < ActiveModelSerializers::Model + attributes :domain, :title, :version, :source_url, + :description, :languages, :rules, :contact + + class ContactPresenter < ActiveModelSerializers::Model + attributes :email, :account + + def email + Setting.site_contact_email + end + + def account + Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')) + end + end + + def contact + ContactPresenter.new + end + + def closed_registrations_message + Setting.closed_registrations_message + end + + def description + Setting.site_short_description + end + + def extended_description + Setting.site_extended_description + end + + def privacy_policy + Setting.site_terms + end + + def domain + Rails.configuration.x.local_domain + end + + def title + Setting.site_title + end + + def languages + [I18n.default_locale] end def rules @@ -40,8 +72,8 @@ class InstancePresenter Rails.cache.fetch('sample_accounts', expires_in: 12.hours) { Account.local.discoverable.popular.limit(3) } end - def version_number - Mastodon::Version + def version + Mastodon::Version.to_s end def source_url diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 45a5cda6f..aa36f82a1 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -6,7 +6,7 @@ class InitialStateSerializer < ActiveModel::Serializer attributes :meta, :compose, :accounts, :media_attachments, :settings, :max_toot_chars, :poll_limits, - :languages, :server + :languages has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer has_one :role, serializer: REST::RoleSerializer @@ -24,18 +24,19 @@ class InitialStateSerializer < ActiveModel::Serializer } end + # rubocop:disable Metrics/AbcSize def meta store = { streaming_api_base_url: Rails.configuration.x.streaming_api_base_url, access_token: object.token, locale: I18n.locale, - domain: Rails.configuration.x.local_domain, - title: instance_presenter.site_title, + domain: instance_presenter.domain, + title: instance_presenter.title, admin: object.admin&.id&.to_s, search_enabled: Chewy.enabled?, repository: Mastodon::Version.repository, - source_url: Mastodon::Version.source_url, - version: Mastodon::Version.to_s, + source_url: instance_presenter.source_url, + version: instance_presenter.version, limited_federation_mode: Rails.configuration.x.whitelist_mode, mascot: instance_presenter.mascot&.file&.url, profile_directory: Setting.profile_directory, @@ -71,6 +72,7 @@ class InitialStateSerializer < ActiveModel::Serializer store end + # rubocop:enable Metrics/AbcSize def compose store = {} @@ -102,13 +104,6 @@ class InitialStateSerializer < ActiveModel::Serializer LanguagesHelper::SUPPORTED_LOCALES.map { |(key, value)| [key, value[0], value[1]] } end - def server - { - hero: instance_presenter.hero&.file&.url || instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.png'), - description: instance_presenter.site_short_description.presence || I18n.t('about.about_mastodon_html'), - } - end - private def instance_presenter diff --git a/app/serializers/manifest_serializer.rb b/app/serializers/manifest_serializer.rb index 9827323a8..6b5296480 100644 --- a/app/serializers/manifest_serializer.rb +++ b/app/serializers/manifest_serializer.rb @@ -22,11 +22,11 @@ class ManifestSerializer < ActiveModel::Serializer :share_target, :shortcuts def name - object.site_title + object.title end def short_name - object.site_title + object.title end def icons diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index 575c6214e..f4ea49427 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -1,74 +1,39 @@ # frozen_string_literal: true class REST::InstanceSerializer < ActiveModel::Serializer - include RoutingHelper - - attributes :uri, :title, :short_description, :description, :email, - :version, :urls, :stats, :thumbnail, :max_toot_chars, :poll_limits, - :languages, :registrations, :approval_required, :invites_enabled, - :configuration - - has_one :contact_account, serializer: REST::AccountSerializer - - has_many :rules, serializer: REST::RuleSerializer - - delegate :contact_account, :rules, to: :instance_presenter - - def uri - Rails.configuration.x.local_domain - end - - def title - Setting.site_title - end + class ContactSerializer < ActiveModel::Serializer + attributes :email - def short_description - Setting.site_short_description + has_one :account, serializer: REST::AccountSerializer end - def description - Setting.site_description - end + include RoutingHelper - def email - Setting.site_contact_email - end + attributes :domain, :title, :version, :source_url, :description, + :usage, :thumbnail, :languages, :configuration, + :registrations - def version - Mastodon::Version.to_s - end + has_one :contact, serializer: ContactSerializer + has_many :rules, serializer: REST::RuleSerializer def thumbnail - instance_presenter.thumbnail ? full_asset_url(instance_presenter.thumbnail.file.url) : full_pack_url('media/images/preview.png') + object.thumbnail ? full_asset_url(object.thumbnail.file.url) : full_pack_url('media/images/preview.png') end - def max_toot_chars - StatusLengthValidator::MAX_CHARS - end - - def poll_limits + def usage { - max_options: PollValidator::MAX_OPTIONS, - max_option_chars: PollValidator::MAX_OPTION_CHARS, - min_expiration: PollValidator::MIN_EXPIRATION, - max_expiration: PollValidator::MAX_EXPIRATION, - } - end - - def stats - { - user_count: instance_presenter.user_count, - status_count: instance_presenter.status_count, - domain_count: instance_presenter.domain_count, + users: { + active_month: object.active_user_count(4), + }, } end - def urls - { streaming_api: Rails.configuration.x.streaming_api_base_url } - end - def configuration { + urls: { + streaming: Rails.configuration.x.streaming_api_base_url, + }, + statuses: { max_characters: StatusLengthValidator::MAX_CHARS, max_media_attachments: 4, @@ -93,25 +58,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer } end - def languages - [I18n.default_locale] - end - def registrations - Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode - end - - def approval_required - Setting.registrations_mode == 'approved' - end - - def invites_enabled - UserRole.everyone.can?(:invite_users) - end - - private - - def instance_presenter - @instance_presenter ||= InstancePresenter.new + { + enabled: Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode, + approval_required: Setting.registrations_mode == 'approved', + } end end diff --git a/app/serializers/rest/v1/instance_serializer.rb b/app/serializers/rest/v1/instance_serializer.rb new file mode 100644 index 000000000..fefbed8ee --- /dev/null +++ b/app/serializers/rest/v1/instance_serializer.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +class REST::V1::InstanceSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :uri, :title, :short_description, :description, :email, + :version, :urls, :stats, :thumbnail, :max_toot_chars, :poll_limits, + :languages, :registrations, :approval_required, :invites_enabled, + :configuration + + has_one :contact_account, serializer: REST::AccountSerializer + + has_many :rules, serializer: REST::RuleSerializer + + def uri + object.domain + end + + def short_description + object.description + end + + def description + Setting.site_description # Legacy + end + + def email + object.contact.email + end + + def contact_account + object.contact.account + end + + def thumbnail + instance_presenter.thumbnail ? full_asset_url(instance_presenter.thumbnail.file.url) : full_pack_url('media/images/preview.png') + end + + def max_toot_chars + StatusLengthValidator::MAX_CHARS + end + + def poll_limits + { + max_options: PollValidator::MAX_OPTIONS, + max_option_chars: PollValidator::MAX_OPTION_CHARS, + min_expiration: PollValidator::MIN_EXPIRATION, + max_expiration: PollValidator::MAX_EXPIRATION, + } + end + + def stats + { + user_count: instance_presenter.user_count, + status_count: instance_presenter.status_count, + domain_count: instance_presenter.domain_count, + } + end + + def urls + { streaming_api: Rails.configuration.x.streaming_api_base_url } + end + + def usage + { + users: { + active_month: instance_presenter.active_user_count(4), + }, + } + end + + def configuration + { + statuses: { + max_characters: StatusLengthValidator::MAX_CHARS, + max_media_attachments: 4, + characters_reserved_per_url: StatusLengthValidator::URL_PLACEHOLDER_CHARS, + }, + + media_attachments: { + supported_mime_types: MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES + MediaAttachment::AUDIO_MIME_TYPES, + image_size_limit: MediaAttachment::IMAGE_LIMIT, + image_matrix_limit: Attachmentable::MAX_MATRIX_LIMIT, + video_size_limit: MediaAttachment::VIDEO_LIMIT, + video_frame_rate_limit: MediaAttachment::MAX_VIDEO_FRAME_RATE, + video_matrix_limit: MediaAttachment::MAX_VIDEO_MATRIX_LIMIT, + }, + + polls: { + max_options: PollValidator::MAX_OPTIONS, + max_characters_per_option: PollValidator::MAX_OPTION_CHARS, + min_expiration: PollValidator::MIN_EXPIRATION, + max_expiration: PollValidator::MAX_EXPIRATION, + }, + } + end + + def registrations + Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode + end + + def approval_required + Setting.registrations_mode == 'approved' + end + + def invites_enabled + UserRole.everyone.can?(:invite_users) + end + + private + + def instance_presenter + @instance_presenter ||= InstancePresenter.new + end +end diff --git a/app/views/about/more.html.haml b/app/views/about/more.html.haml index 0b75f159a..a75549120 100644 --- a/app/views/about/more.html.haml +++ b/app/views/about/more.html.haml @@ -8,7 +8,7 @@ .column-0 .public-account-header.public-account-header--no-bar .public-account-header__image - = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.png'), alt: @instance_presenter.site_title, class: 'parallax' + = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.png'), alt: @instance_presenter.title, class: 'parallax' .column-1 .landing-page__call-to-action{ dir: 'ltr' } @@ -30,14 +30,14 @@ .contact-widget %h4= t 'about.administered_by' - = account_link_to(@instance_presenter.contact_account) + = account_link_to(@instance_presenter.contact.account) - - if @instance_presenter.site_contact_email.present? + - if @instance_presenter.contact.email.present? %h4 = succeed ':' do = t 'about.contact' - = mail_to @instance_presenter.site_contact_email, nil, title: @instance_presenter.site_contact_email + = mail_to @instance_presenter.contact.email, nil, title: @instance_presenter.contact.email .column-3 = render 'application/flashes' diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml index fb292941b..8d09a2938 100644 --- a/app/views/about/show.html.haml +++ b/app/views/about/show.html.haml @@ -53,11 +53,11 @@ .hero-widget .hero-widget__img - = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.png'), alt: @instance_presenter.site_title + = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.png'), alt: @instance_presenter.title .hero-widget__text %p - = @instance_presenter.site_short_description.html_safe.presence || t('about.about_mastodon_html') + = @instance_presenter.description.html_safe.presence || t('about.about_mastodon_html') = link_to about_more_path do = t('about.learn_more') = fa_icon 'angle-double-right' @@ -66,7 +66,7 @@ .hero-widget__footer__column %h4= t 'about.administered_by' - = account_link_to @instance_presenter.contact_account + = account_link_to @instance_presenter.contact.account .hero-widget__footer__column %h4= t 'about.server_stats' diff --git a/app/views/application/_sidebar.html.haml b/app/views/application/_sidebar.html.haml index cc157bf47..eb2813dd0 100644 --- a/app/views/application/_sidebar.html.haml +++ b/app/views/application/_sidebar.html.haml @@ -1,9 +1,9 @@ .hero-widget .hero-widget__img - = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.png'), alt: @instance_presenter.site_title + = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.png'), alt: @instance_presenter.title .hero-widget__text - %p= @instance_presenter.site_short_description.html_safe.presence || t('about.about_mastodon_html') + %p= @instance_presenter.description.html_safe.presence || t('about.about_mastodon_html') - if Setting.trends && !(user_signed_in? && !current_user.setting_trends) - trends = Trends.tags.query.allowed.limit(3) diff --git a/app/views/privacy/show.html.haml b/app/views/privacy/show.html.haml index 9d076a91b..cdd38a595 100644 --- a/app/views/privacy/show.html.haml +++ b/app/views/privacy/show.html.haml @@ -4,6 +4,6 @@ .grid .column-0 .box-widget - .rich-formatting= @instance_presenter.site_terms.html_safe.presence || t('terms.body_html') + .rich-formatting= @instance_presenter.privacy_policy.html_safe.presence || t('terms.body_html') .column-1 = render 'application/sidebar' diff --git a/app/views/shared/_og.html.haml b/app/views/shared/_og.html.haml index 7feae1b8b..b54ab2429 100644 --- a/app/views/shared/_og.html.haml +++ b/app/views/shared/_og.html.haml @@ -1,12 +1,12 @@ - thumbnail = @instance_presenter.thumbnail -- description ||= strip_tags(@instance_presenter.site_short_description.presence || t('about.about_mastodon_html')) +- description ||= strip_tags(@instance_presenter.description.presence || t('about.about_mastodon_html')) %meta{ name: 'description', content: description }/ = opengraph 'og:site_name', t('about.hosted_on', domain: site_hostname) = opengraph 'og:url', url_for(only_path: false) = opengraph 'og:type', 'website' -= opengraph 'og:title', @instance_presenter.site_title += opengraph 'og:title', @instance_presenter.title = opengraph 'og:description', description = opengraph 'og:image', full_asset_url(thumbnail&.file&.url || asset_pack_path('media/images/preview.png', protocol: :request)) = opengraph 'og:image:width', thumbnail ? thumbnail.meta['width'] : '1200' diff --git a/config/routes.rb b/config/routes.rb index 7330bb8df..8df1e45b7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -639,10 +639,12 @@ Rails.application.routes.draw do end namespace :v2 do - resources :media, only: [:create] get '/search', to: 'search#index', as: :search + + resources :media, only: [:create] resources :suggestions, only: [:index] resources :filters, only: [:index, :create, :show, :update, :destroy] + resource :instance, only: [:show] namespace :admin do resources :accounts, only: [:index] diff --git a/spec/presenters/instance_presenter_spec.rb b/spec/presenters/instance_presenter_spec.rb index 81d8d0e98..4baad03f4 100644 --- a/spec/presenters/instance_presenter_spec.rb +++ b/spec/presenters/instance_presenter_spec.rb @@ -3,21 +3,20 @@ require 'rails_helper' describe InstancePresenter do let(:instance_presenter) { InstancePresenter.new } - context do + describe '#description' do around do |example| - site_description = Setting.site_description + site_description = Setting.site_short_description example.run - Setting.site_description = site_description + Setting.site_short_description = site_description end it "delegates site_description to Setting" do - Setting.site_description = "Site desc" - - expect(instance_presenter.site_description).to eq "Site desc" + Setting.site_short_description = "Site desc" + expect(instance_presenter.description).to eq "Site desc" end end - context do + describe '#extended_description' do around do |example| site_extended_description = Setting.site_extended_description example.run @@ -26,12 +25,11 @@ describe InstancePresenter do it "delegates site_extended_description to Setting" do Setting.site_extended_description = "Extended desc" - - expect(instance_presenter.site_extended_description).to eq "Extended desc" + expect(instance_presenter.extended_description).to eq "Extended desc" end end - context do + describe '#email' do around do |example| site_contact_email = Setting.site_contact_email example.run @@ -40,12 +38,11 @@ describe InstancePresenter do it "delegates contact_email to Setting" do Setting.site_contact_email = "admin@example.com" - - expect(instance_presenter.site_contact_email).to eq "admin@example.com" + expect(instance_presenter.contact.email).to eq "admin@example.com" end end - describe "contact_account" do + describe '#account' do around do |example| site_contact_username = Setting.site_contact_username example.run @@ -55,12 +52,11 @@ describe InstancePresenter do it "returns the account for the site contact username" do Setting.site_contact_username = "aaa" account = Fabricate(:account, username: "aaa") - - expect(instance_presenter.contact_account).to eq(account) + expect(instance_presenter.contact.account).to eq(account) end end - describe "user_count" do + describe '#user_count' do it "returns the number of site users" do Rails.cache.write 'user_count', 123 @@ -68,7 +64,7 @@ describe InstancePresenter do end end - describe "status_count" do + describe '#status_count' do it "returns the number of local statuses" do Rails.cache.write 'local_status_count', 234 @@ -76,7 +72,7 @@ describe InstancePresenter do end end - describe "domain_count" do + describe '#domain_count' do it "returns the number of known domains" do Rails.cache.write 'distinct_domain_count', 345 @@ -84,9 +80,9 @@ describe InstancePresenter do end end - describe '#version_number' do - it 'returns Mastodon::Version' do - expect(instance_presenter.version_number).to be(Mastodon::Version) + describe '#version' do + it 'returns string' do + expect(instance_presenter.version).to be_a String end end diff --git a/spec/views/about/show.html.haml_spec.rb b/spec/views/about/show.html.haml_spec.rb index 140f3fd41..15802799a 100644 --- a/spec/views/about/show.html.haml_spec.rb +++ b/spec/views/about/show.html.haml_spec.rb @@ -14,26 +14,7 @@ describe 'about/show.html.haml', without_verify_partial_doubles: true do end it 'has valid open graph tags' do - instance_presenter = double( - :instance_presenter, - site_title: 'something', - site_short_description: 'something', - site_description: 'something', - version_number: '1.0', - source_url: 'https://github.com/mastodon/mastodon', - open_registrations: false, - thumbnail: nil, - hero: nil, - mascot: nil, - user_count: 420, - status_count: 69, - active_user_count: 420, - commit_hash: commit_hash, - contact_account: nil, - sample_accounts: [] - ) - - assign(:instance_presenter, instance_presenter) + assign(:instance_presenter, InstancePresenter.new) render header_tags = view.content_for(:header_tags) -- cgit From b04633a9614609f18b39ba0f0015df301a04ab64 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 13 Oct 2022 11:29:19 +0200 Subject: Add image processing and generate blurhash for server thumbnail (#19348) Remove separate server hero setting --- app/models/form/admin_settings.rb | 2 -- app/models/site_upload.rb | 27 +++++++++++++++++++++- app/presenters/instance_presenter.rb | 4 ---- app/serializers/rest/instance_serializer.rb | 15 +++++++++++- app/serializers/rest/v1/instance_serializer.rb | 2 +- app/views/admin/settings/edit.html.haml | 2 -- app/views/application/_sidebar.html.haml | 2 +- app/views/shared/_og.html.haml | 2 +- config/locales/en.yml | 3 --- .../20221012181003_add_blurhash_to_site_uploads.rb | 5 ++++ db/schema.rb | 3 ++- spec/presenters/instance_presenter_spec.rb | 7 ------ 12 files changed, 50 insertions(+), 24 deletions(-) create mode 100644 db/migrate/20221012181003_add_blurhash_to_site_uploads.rb (limited to 'app/views/shared') diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index 7bd9e3743..b6bb3d795 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -22,7 +22,6 @@ class Form::AdminSettings custom_css profile_directory thumbnail - hero mascot trends trendable_by_default @@ -49,7 +48,6 @@ class Form::AdminSettings UPLOAD_KEYS = %i( thumbnail - hero mascot ).freeze diff --git a/app/models/site_upload.rb b/app/models/site_upload.rb index cf10b30fc..d3b81d4d5 100644 --- a/app/models/site_upload.rb +++ b/app/models/site_upload.rb @@ -12,10 +12,35 @@ # meta :json # created_at :datetime not null # updated_at :datetime not null +# blurhash :string # class SiteUpload < ApplicationRecord - has_attached_file :file + include Attachmentable + + STYLES = { + thumbnail: { + '@1x': { + format: 'png', + geometry: '1200x630#', + file_geometry_parser: FastGeometryParser, + blurhash: { + x_comp: 4, + y_comp: 4, + }.freeze, + }, + + '@2x': { + format: 'png', + geometry: '2400x1260#', + file_geometry_parser: FastGeometryParser, + }.freeze, + }.freeze, + + mascot: {}.freeze, + }.freeze + + has_attached_file :file, styles: ->(file) { STYLES[file.instance.var.to_sym] }, convert_options: { all: '-coalesce -strip' }, processors: [:lazy_thumbnail, :blurhash_transcoder, :type_corrector] validates_attachment_content_type :file, content_type: /\Aimage\/.*\z/ validates :file, presence: true diff --git a/app/presenters/instance_presenter.rb b/app/presenters/instance_presenter.rb index c461ac55f..43594a280 100644 --- a/app/presenters/instance_presenter.rb +++ b/app/presenters/instance_presenter.rb @@ -84,10 +84,6 @@ class InstancePresenter < ActiveModelSerializers::Model @thumbnail ||= Rails.cache.fetch('site_uploads/thumbnail') { SiteUpload.find_by(var: 'thumbnail') } end - def hero - @hero ||= Rails.cache.fetch('site_uploads/hero') { SiteUpload.find_by(var: 'hero') } - end - def mascot @mascot ||= Rails.cache.fetch('site_uploads/mascot') { SiteUpload.find_by(var: 'mascot') } end diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index f4ea49427..dfa8ce40a 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -17,7 +17,20 @@ class REST::InstanceSerializer < ActiveModel::Serializer has_many :rules, serializer: REST::RuleSerializer def thumbnail - object.thumbnail ? full_asset_url(object.thumbnail.file.url) : full_pack_url('media/images/preview.png') + if object.thumbnail + { + url: full_asset_url(object.thumbnail.file.url(:'@1x')), + blurhash: object.thumbnail.blurhash, + versions: { + '@1x': full_asset_url(object.thumbnail.file.url(:'@1x')), + '@2x': full_asset_url(object.thumbnail.file.url(:'@2x')), + }, + } + else + { + url: full_pack_url('media/images/preview.png'), + } + end end def usage diff --git a/app/serializers/rest/v1/instance_serializer.rb b/app/serializers/rest/v1/instance_serializer.rb index 47fa086fc..872175451 100644 --- a/app/serializers/rest/v1/instance_serializer.rb +++ b/app/serializers/rest/v1/instance_serializer.rb @@ -33,7 +33,7 @@ class REST::V1::InstanceSerializer < ActiveModel::Serializer end def thumbnail - instance_presenter.thumbnail ? full_asset_url(instance_presenter.thumbnail.file.url) : full_pack_url('media/images/preview.png') + instance_presenter.thumbnail ? full_asset_url(instance_presenter.thumbnail.file.url(:'@1x')) : full_pack_url('media/images/preview.png') end def stats diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml index 79f73a60f..15b1a2498 100644 --- a/app/views/admin/settings/edit.html.haml +++ b/app/views/admin/settings/edit.html.haml @@ -34,8 +34,6 @@ .fields-row .fields-row__column.fields-row__column-6.fields-group = f.input :thumbnail, as: :file, wrapper: :with_block_label, label: t('admin.settings.thumbnail.title'), hint: site_upload_delete_hint(t('admin.settings.thumbnail.desc_html'), :thumbnail) - .fields-row__column.fields-row__column-6.fields-group - = f.input :hero, as: :file, wrapper: :with_block_label, label: t('admin.settings.hero.title'), hint: site_upload_delete_hint(t('admin.settings.hero.desc_html'), :hero) .fields-row .fields-row__column.fields-row__column-6.fields-group diff --git a/app/views/application/_sidebar.html.haml b/app/views/application/_sidebar.html.haml index eb2813dd0..6d18668b0 100644 --- a/app/views/application/_sidebar.html.haml +++ b/app/views/application/_sidebar.html.haml @@ -1,6 +1,6 @@ .hero-widget .hero-widget__img - = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.png'), alt: @instance_presenter.title + = image_tag @instance_presenter.thumbnail&.file&.url(:'@1x') || asset_pack_path('media/images/preview.png'), alt: @instance_presenter.title .hero-widget__text %p= @instance_presenter.description.html_safe.presence || t('about.about_mastodon_html') diff --git a/app/views/shared/_og.html.haml b/app/views/shared/_og.html.haml index b54ab2429..2941b566e 100644 --- a/app/views/shared/_og.html.haml +++ b/app/views/shared/_og.html.haml @@ -8,7 +8,7 @@ = opengraph 'og:type', 'website' = opengraph 'og:title', @instance_presenter.title = opengraph 'og:description', description -= opengraph 'og:image', full_asset_url(thumbnail&.file&.url || asset_pack_path('media/images/preview.png', protocol: :request)) += opengraph 'og:image', full_asset_url(thumbnail&.file&.url(:'@1x') || asset_pack_path('media/images/preview.png', protocol: :request)) = opengraph 'og:image:width', thumbnail ? thumbnail.meta['width'] : '1200' = opengraph 'og:image:height', thumbnail ? thumbnail.meta['height'] : '630' = opengraph 'twitter:card', 'summary_large_image' diff --git a/config/locales/en.yml b/config/locales/en.yml index bd913a5ca..8a70bd8ca 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -735,9 +735,6 @@ en: users: To logged-in local users domain_blocks_rationale: title: Show rationale - hero: - desc_html: Displayed on the frontpage. At least 600x100px recommended. When not set, falls back to server thumbnail - title: Hero image mascot: desc_html: Displayed on multiple pages. At least 293×205px recommended. When not set, falls back to default mascot title: Mascot image diff --git a/db/migrate/20221012181003_add_blurhash_to_site_uploads.rb b/db/migrate/20221012181003_add_blurhash_to_site_uploads.rb new file mode 100644 index 000000000..e1c87712b --- /dev/null +++ b/db/migrate/20221012181003_add_blurhash_to_site_uploads.rb @@ -0,0 +1,5 @@ +class AddBlurhashToSiteUploads < ActiveRecord::Migration[6.1] + def change + add_column :site_uploads, :blurhash, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 2f9058509..3972f777a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_10_06_061337) do +ActiveRecord::Schema.define(version: 2022_10_12_181003) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -866,6 +866,7 @@ ActiveRecord::Schema.define(version: 2022_10_06_061337) do t.json "meta" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "blurhash" t.index ["var"], name: "index_site_uploads_on_var", unique: true end diff --git a/spec/presenters/instance_presenter_spec.rb b/spec/presenters/instance_presenter_spec.rb index a0a8628e8..659c2cc2f 100644 --- a/spec/presenters/instance_presenter_spec.rb +++ b/spec/presenters/instance_presenter_spec.rb @@ -99,13 +99,6 @@ describe InstancePresenter do end end - describe '#hero' do - it 'returns SiteUpload' do - hero = Fabricate(:site_upload, var: 'hero') - expect(instance_presenter.hero).to eq(hero) - end - end - describe '#mascot' do it 'returns SiteUpload' do mascot = Fabricate(:site_upload, var: 'mascot') -- cgit