From 162dc845a04741f15d0d8ca70e17f1ee9ed38841 Mon Sep 17 00:00:00 2001 From: ThibG Date: Sun, 29 Dec 2019 05:41:26 +0100 Subject: Fix batch actions on non-pending tags in admin interface (#12537) - Allow batch actions on unreviewed tags - Hide checkboxes when batch actions aren't available --- app/views/admin/tags/_tag.html.haml | 5 +++-- app/views/admin/tags/index.html.haml | 15 ++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) (limited to 'app/views') diff --git a/app/views/admin/tags/_tag.html.haml b/app/views/admin/tags/_tag.html.haml index 670f3bc05..287d28e53 100644 --- a/app/views/admin/tags/_tag.html.haml +++ b/app/views/admin/tags/_tag.html.haml @@ -1,6 +1,7 @@ .batch-table__row - %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox - = f.check_box :tag_ids, { multiple: true, include_hidden: false }, tag.id + - if batch_available + %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox + = f.check_box :tag_ids, { multiple: true, include_hidden: false }, tag.id .directory__tag = link_to admin_tag_path(tag.id) do diff --git a/app/views/admin/tags/index.html.haml b/app/views/admin/tags/index.html.haml index b29991328..7f2c53190 100644 --- a/app/views/admin/tags/index.html.haml +++ b/app/views/admin/tags/index.html.haml @@ -50,25 +50,26 @@ .batch-table.optional .batch-table__toolbar - %label.batch-table__toolbar__select.batch-checkbox-all - = check_box_tag :batch_checkbox_all, nil, false - .batch-table__toolbar__actions - - if params[:pending_review] == '1' + - if params[:pending_review] == '1' || params[:unreviewed] == '1' + %label.batch-table__toolbar__select.batch-checkbox-all + = check_box_tag :batch_checkbox_all, nil, false + .batch-table__toolbar__actions = f.button safe_join([fa_icon('check'), t('admin.accounts.approve')]), name: :approve, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } = f.button safe_join([fa_icon('times'), t('admin.accounts.reject')]), name: :reject, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } - - else + - else + .batch-table__toolbar__actions %span.neutral-hint= t('generic.no_batch_actions_available') .batch-table__body - if @tags.empty? = nothing_here 'nothing-here--under-tabs' - else - = render partial: 'tag', collection: @tags, locals: { f: f } + = render partial: 'tag', collection: @tags, locals: { f: f, batch_available: params[:pending_review] == '1' || params[:unreviewed] == '1' } = paginate @tags -- if params[:pending_review] == '1' +- if params[:pending_review] == '1' || params[:unreviewed] == '1' %hr.spacer/ %div{ style: 'overflow: hidden' } -- cgit From 9edab7afafd6f6db9338ada83a84b2ef14f397a9 Mon Sep 17 00:00:00 2001 From: Sasha Sorokin Date: Wed, 1 Jan 2020 04:15:05 +0700 Subject: Add translation project promotion link (#12736) This commit adds promotional notice on appearance settings about translation project if any other locale than English is used. It allows users to learn and contribute translations to Mastodon. Step ahead, in this commit one unusual string is added - link to a guide. By default it refers to Crowdin project itself, but if any of Mastodon localization teams established their own guide, they can refer it. Or, if Crowdin supports localized domain for language, it can also be put there (e.g. https://fr.crowdin.com/...). --- app/views/settings/preferences/appearance/show.html.haml | 4 ++++ config/locales/en.yml | 4 ++++ 2 files changed, 8 insertions(+) (limited to 'app/views') diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml index 9ed83fb93..d2b05513e 100644 --- a/app/views/settings/preferences/appearance/show.html.haml +++ b/app/views/settings/preferences/appearance/show.html.haml @@ -8,6 +8,10 @@ .fields-group.fields-row__column.fields-row__column-6 = f.input :setting_theme, collection: Themes.instance.names, label_method: lambda { |theme| I18n.t("themes.#{theme}", default: theme) }, wrapper: :with_label, include_blank: false, hint: false + - unless I18n.locale == :en + .flash-message{ style: "text-align: unset; color: unset" } + #{t 'appearance.localization.body'} #{content_tag(:a, t('appearance.localization.guide_link_text'), href: t('appearance.localization.guide_link'), target: "_blank", rel: "noopener", style: "text-decoration: underline")} + %h4= t 'appearance.advanced_web_interface' %p.hint= t 'appearance.advanced_web_interface_hint' diff --git a/config/locales/en.yml b/config/locales/en.yml index c7179d304..42bd81e40 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -581,6 +581,10 @@ en: animations_and_accessibility: Animations and accessibility confirmation_dialogs: Confirmation dialogs discovery: Discovery + localization: + body: Mastodon is translated by volunteers. + guide_link: https://crowdin.com/project/mastodon + guide_link_text: Everyone can contribute. sensitive_content: Sensitive content toot_layout: Toot layout application_mailer: -- cgit From 6a8c8dc6fb67f58f5aadc082fe49d0fcdc5e47ed Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 3 Jan 2020 05:00:17 +0100 Subject: Fix cache digesting log noise on status embeds (#12750) --- app/views/statuses/embed.html.haml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'app/views') diff --git a/app/views/statuses/embed.html.haml b/app/views/statuses/embed.html.haml index 6f2ec646f..2f111f53f 100644 --- a/app/views/statuses/embed.html.haml +++ b/app/views/statuses/embed.html.haml @@ -1,3 +1,2 @@ -- cache @status do - .activity-stream.activity-stream--headless - = render 'status', status: @status, centered: true, autoplay: @autoplay +.activity-stream.activity-stream--headless + = render 'status', status: @status, centered: true, autoplay: @autoplay -- cgit From 345dd93310a6ce830444f6f875204d30c43c5a1e Mon Sep 17 00:00:00 2001 From: Sasha Sorokin Date: Tue, 7 Jan 2020 16:41:19 +0700 Subject: Little improvements to filters settings page (#12793) When you have many filters, it may be hard for you to reach the button to create yet another one. This commit moves creation button to the heading, leaving the page just for the list. On the other hand, when there are no filters, page looks kind of strange with the empty table. So text stating obvious fact that user has no filters was added in this commit too. Closes #11020 Closes #12790 --- app/views/filters/index.html.haml | 37 +++++++++++++++++++++---------------- config/locales/en.yml | 1 + 2 files changed, 22 insertions(+), 16 deletions(-) (limited to 'app/views') diff --git a/app/views/filters/index.html.haml b/app/views/filters/index.html.haml index 18ebee570..530ea6f59 100644 --- a/app/views/filters/index.html.haml +++ b/app/views/filters/index.html.haml @@ -1,20 +1,25 @@ - content_for :page_title do = t('filters.index.title') -.table-wrapper - %table.table - %thead - %tr - %th= t('simple_form.labels.defaults.phrase') - %th= t('simple_form.labels.defaults.context') - %th - %tbody - - @filters.each do |filter| - %tr - %td= filter.phrase - %td= filter.context.map { |context| I18n.t("filters.contexts.#{context}") }.join(', ') - %td - = table_link_to 'pencil', t('filters.edit.title'), edit_filter_path(filter) - = table_link_to 'times', t('filters.index.delete'), filter_path(filter), method: :delete +- content_for :page_heading_actions do + = link_to t('filters.new.title'), new_filter_path, class: 'button' -= link_to t('filters.new.title'), new_filter_path, class: 'button' +- if @filters.count == 0 + %div{ style: 'display: flex; justify-content: center' } + %div.muted-hint= t 'filters.index.empty' +- else + .table-wrapper + %table.table + %thead + %tr + %th= t('simple_form.labels.defaults.phrase') + %th= t('simple_form.labels.defaults.context') + %th + %tbody + - @filters.each do |filter| + %tr + %td= filter.phrase + %td= filter.context.map { |context| I18n.t("filters.contexts.#{context}") }.join(', ') + %td + = table_link_to 'pencil', t('filters.edit.title'), edit_filter_path(filter) + = table_link_to 'times', t('filters.index.delete'), filter_path(filter), method: :delete diff --git a/config/locales/en.yml b/config/locales/en.yml index 42bd81e40..6b52f4cad 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -744,6 +744,7 @@ en: invalid_irreversible: Irreversible filtering only works with home or notifications context index: delete: Delete + empty: You have no filters. title: Filters new: title: Add new filter -- cgit From 139f7646a7e3af491d5d6c6a99e82d46f9551400 Mon Sep 17 00:00:00 2001 From: Sasha Sorokin Date: Fri, 10 Jan 2020 07:10:17 +0700 Subject: Rename page_heading_actions variable (#12799) Current name - page_heading_actions is long and hard to remember, this commit shortens it to just heading_actions, which is way more easier to remember and write, it's logical too. --- app/views/admin/reports/show.html.haml | 2 +- app/views/filters/index.html.haml | 2 +- app/views/layouts/admin.html.haml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) (limited to 'app/views') diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml index 7f3ec35a4..af8fce2a7 100644 --- a/app/views/admin/reports/show.html.haml +++ b/app/views/admin/reports/show.html.haml @@ -4,7 +4,7 @@ - content_for :page_title do = t('admin.reports.report', id: @report.id) -- content_for :page_heading_actions do +- content_for :heading_actions do - if @report.unresolved? = link_to t('admin.reports.mark_as_resolved'), resolve_admin_report_path(@report), method: :post, class: 'button' - else diff --git a/app/views/filters/index.html.haml b/app/views/filters/index.html.haml index 530ea6f59..2f7d96f96 100644 --- a/app/views/filters/index.html.haml +++ b/app/views/filters/index.html.haml @@ -1,7 +1,7 @@ - content_for :page_title do = t('filters.index.title') -- content_for :page_heading_actions do +- content_for :heading_actions do = link_to t('filters.new.title'), new_filter_path, class: 'button' - if @filters.count == 0 diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml index 059bf8579..d1de2e80f 100644 --- a/app/views/layouts/admin.html.haml +++ b/app/views/layouts/admin.html.haml @@ -24,9 +24,9 @@ .content-heading %h2= yield :page_title - - if :page_heading_actions + - if :heading_actions .content-heading-actions - = yield :page_heading_actions + = yield :heading_actions = render 'application/flashes' -- cgit From 36426ed4ad450320f4a4e598c41355a6d8faad66 Mon Sep 17 00:00:00 2001 From: Sasha Sorokin Date: Sat, 11 Jan 2020 08:14:45 +0700 Subject: Use heading actions and placeholders in settings (#12801) This commit: - Refactors centered text blocks currently used for placeholders for empty tables and puts styles for it in separate class - .centered-text, simply aliasing text-align: center. Which is furtherly used in this commit. - Improves applications settings page to use heading actions, moving "New application" button there, and displaying placeholder "You have no applications" in place of empty table. - Improves custom emoji settings page to use heading action for "Upload" button, making it more easily accessible without need to scroll through all of the emojis. - Improves email domain blocks settings page, moving "Add new" to the heading actions and using placeholder "No e-mail domains currently blacklisted" instead of showing empty table. --- app/javascript/styles/mastodon/admin.scss | 4 +++ app/views/admin/custom_emojis/index.html.haml | 6 ++-- .../admin/email_domain_blocks/index.html.haml | 23 +++++++++------ app/views/filters/index.html.haml | 3 +- app/views/settings/applications/index.html.haml | 33 +++++++++++++--------- config/locales/doorkeeper.en.yml | 1 + config/locales/en.yml | 1 + 7 files changed, 43 insertions(+), 28 deletions(-) (limited to 'app/views') diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 203365f5e..9bd030964 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -878,3 +878,7 @@ a.name-tag, color: $primary-text-color; } } + +.center-text { + text-align: center; +} diff --git a/app/views/admin/custom_emojis/index.html.haml b/app/views/admin/custom_emojis/index.html.haml index 7320ce1bb..389e9dd71 100644 --- a/app/views/admin/custom_emojis/index.html.haml +++ b/app/views/admin/custom_emojis/index.html.haml @@ -4,6 +4,9 @@ - content_for :header_tags do = javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous' +- content_for :heading_actions do + = link_to t('admin.custom_emojis.upload'), new_admin_custom_emoji_path, class: 'button' + .filters .filter-subset %strong= t('admin.accounts.location.title') @@ -81,6 +84,3 @@ = paginate @custom_emojis -%hr.spacer/ - -= link_to t('admin.custom_emojis.upload'), new_admin_custom_emoji_path, class: 'button' diff --git a/app/views/admin/email_domain_blocks/index.html.haml b/app/views/admin/email_domain_blocks/index.html.haml index 7bb204e52..c1cc470b6 100644 --- a/app/views/admin/email_domain_blocks/index.html.haml +++ b/app/views/admin/email_domain_blocks/index.html.haml @@ -1,14 +1,19 @@ - content_for :page_title do = t('admin.email_domain_blocks.title') -.table-wrapper - %table.table - %thead - %tr - %th= t('admin.email_domain_blocks.domain') - %th - %tbody - = render @email_domain_blocks +- content_for :heading_actions do + = link_to t('admin.email_domain_blocks.add_new'), new_admin_email_domain_block_path, class: 'button' + +- if @email_domain_blocks.count == 0 + %div.muted-hint.center-text=t 'admin.email_domain_blocks.empty' +- else + .table-wrapper + %table.table + %thead + %tr + %th= t('admin.email_domain_blocks.domain') + %th + %tbody + = render @email_domain_blocks = paginate @email_domain_blocks -= link_to t('admin.email_domain_blocks.add_new'), new_admin_email_domain_block_path, class: 'button' diff --git a/app/views/filters/index.html.haml b/app/views/filters/index.html.haml index 2f7d96f96..8ace638ca 100644 --- a/app/views/filters/index.html.haml +++ b/app/views/filters/index.html.haml @@ -5,8 +5,7 @@ = link_to t('filters.new.title'), new_filter_path, class: 'button' - if @filters.count == 0 - %div{ style: 'display: flex; justify-content: center' } - %div.muted-hint= t 'filters.index.empty' + %div.muted-hint.center-text= t 'filters.index.empty' - else .table-wrapper %table.table diff --git a/app/views/settings/applications/index.html.haml b/app/views/settings/applications/index.html.haml index 919472c2e..1cb94760f 100644 --- a/app/views/settings/applications/index.html.haml +++ b/app/views/settings/applications/index.html.haml @@ -1,20 +1,25 @@ - content_for :page_title do = t('doorkeeper.applications.index.title') -.table-wrapper - %table.table - %thead - %tr - %th= t('doorkeeper.applications.index.application') - %th= t('doorkeeper.applications.index.scopes') - %th - %tbody - - @applications.each do |application| +- content_for :heading_actions do + = link_to t('doorkeeper.applications.index.new'), new_settings_application_path, class: 'button' + +- if @applications.count == 0 + %div.muted-hint.center-text=t 'doorkeeper.applications.index.empty' +- else + .table-wrapper + %table.table + %thead %tr - %td= link_to application.name, settings_application_path(application) - %th= application.scopes - %td - = table_link_to 'times', t('doorkeeper.applications.index.delete'), settings_application_path(application), method: :delete, data: { confirm: t('doorkeeper.applications.confirmations.destroy') } + %th= t('doorkeeper.applications.index.application') + %th= t('doorkeeper.applications.index.scopes') + %th + %tbody + - @applications.each do |application| + %tr + %td= link_to application.name, settings_application_path(application) + %th= application.scopes + %td + = table_link_to 'times', t('doorkeeper.applications.index.delete'), settings_application_path(application), method: :delete, data: { confirm: t('doorkeeper.applications.confirmations.destroy') } = paginate @applications -= link_to t('doorkeeper.applications.index.new'), new_settings_application_path, class: 'button' diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index 4e9c83a8f..2be2ef036 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -38,6 +38,7 @@ en: application: Application callback_url: Callback URL delete: Delete + empty: You have no applications. name: Name new: New application scopes: Scopes diff --git a/config/locales/en.yml b/config/locales/en.yml index 6b52f4cad..660d0a4e1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -339,6 +339,7 @@ en: delete: Delete destroyed_msg: Successfully deleted e-mail domain from blacklist domain: Domain + empty: No e-mail domains currenly blacklisted. new: create: Add domain title: New e-mail blacklist entry -- cgit From 1ded3bb752916fe87755d80a832af39431a42438 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 18 Jan 2020 19:50:43 +0100 Subject: Change reported media attachments to always be hidden in admin UI (#12879) Also: - Fix Mastodon logo not showing up in status embeds - Fix blurhash not being used in status embeds - Fix blurhash not being used in admin UI - Fix autoplay param not working correctly on status embeds --- app/javascript/mastodon/components/media_gallery.js | 16 +++++++++++----- app/views/admin/reports/_status.html.haml | 7 +++++-- app/views/layouts/admin.html.haml | 1 + app/views/layouts/embedded.html.haml | 4 ++++ app/views/statuses/_detailed_status.html.haml | 4 ++-- app/views/statuses/_simple_status.html.haml | 4 ++-- 6 files changed, 25 insertions(+), 11 deletions(-) (limited to 'app/views') diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index 12b7e5b66..cfe164a50 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -23,6 +23,7 @@ class Item extends React.PureComponent { onClick: PropTypes.func.isRequired, displayWidth: PropTypes.number, visible: PropTypes.bool.isRequired, + autoplay: PropTypes.bool, }; static defaultProps = { @@ -48,9 +49,13 @@ class Item extends React.PureComponent { } } + getAutoPlay() { + return this.props.autoplay || autoPlayGif; + } + hoverToPlay () { const { attachment } = this.props; - return !autoPlayGif && attachment.get('type') === 'gifv'; + return !this.getAutoPlay() && attachment.get('type') === 'gifv'; } handleClick = (e) => { @@ -201,7 +206,7 @@ class Item extends React.PureComponent { ); } else if (attachment.get('type') === 'gifv') { - const autoPlay = !isIOS() && autoPlayGif; + const autoPlay = !isIOS() && this.getAutoPlay(); thumbnail = (
@@ -248,6 +253,7 @@ class MediaGallery extends React.PureComponent { defaultWidth: PropTypes.number, cacheWidth: PropTypes.func, visible: PropTypes.bool, + autoplay: PropTypes.bool, onToggleVisibility: PropTypes.func, }; @@ -297,7 +303,7 @@ class MediaGallery extends React.PureComponent { } render () { - const { media, intl, sensitive, height, defaultWidth, standalone } = this.props; + const { media, intl, sensitive, height, defaultWidth, standalone, autoplay } = this.props; const { visible } = this.state; const width = this.state.width || defaultWidth; @@ -320,9 +326,9 @@ class MediaGallery extends React.PureComponent { const uncached = media.every(attachment => attachment.get('type') === 'unknown'); if (standalone && this.isFullSizeEligible()) { - children = ; + children = ; } else { - children = media.take(4).map((attachment, i) => ); + children = media.take(4).map((attachment, i) => ); } if (uncached) { diff --git a/app/views/admin/reports/_status.html.haml b/app/views/admin/reports/_status.html.haml index 425d315e1..105352e46 100644 --- a/app/views/admin/reports/_status.html.haml +++ b/app/views/admin/reports/_status.html.haml @@ -14,9 +14,12 @@ - unless status.proper.media_attachments.empty? - if status.proper.media_attachments.first.video? - video = status.proper.media_attachments.first - = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), sensitive: !current_account&.user&.show_all_media? && status.proper.sensitive? || current_account&.user&.hide_all_media?, width: 610, height: 343, inline: true, alt: video.description + = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: status.proper.sensitive?, visible: false, width: 610, height: 343, inline: true, alt: video.description + - elsif status.media_attachments.first.audio? + - audio = status.proper.media_attachments.first + = react_component :audio, src: audio.file.url(:original), height: 110, alt: audio.description, duration: audio.file.meta.dig(:original, :duration) - else - = react_component :media_gallery, height: 343, sensitive: !current_account&.user&.show_all_media? && status.proper.sensitive? || current_account&.user&.hide_all_media?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.proper.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } + = react_component :media_gallery, height: 343, sensitive: status.proper.sensitive?, visible: false, media: status.proper.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } .detailed-status__meta = link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener noreferrer' do diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml index d1de2e80f..b1a2d0617 100644 --- a/app/views/layouts/admin.html.haml +++ b/app/views/layouts/admin.html.haml @@ -1,4 +1,5 @@ - content_for :header_tags do + = render_initial_state = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous' - content_for :content do diff --git a/app/views/layouts/embedded.html.haml b/app/views/layouts/embedded.html.haml index 0503dcdc1..d7e74ade5 100644 --- a/app/views/layouts/embedded.html.haml +++ b/app/views/layouts/embedded.html.haml @@ -14,6 +14,10 @@ = stylesheet_pack_tag Setting.default_settings['theme'], media: 'all' = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous' = javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous' + = render_initial_state = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous' %body.embed = yield + + %div{ style: 'display: none'} + = render file: Rails.root.join('app', 'javascript', 'images', 'logo_transparent.svg') diff --git a/app/views/statuses/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml index 3fa52d3f2..021390e47 100644 --- a/app/views/statuses/_detailed_status.html.haml +++ b/app/views/statuses/_detailed_status.html.haml @@ -29,14 +29,14 @@ - if !status.media_attachments.empty? - if status.media_attachments.first.video? - video = status.media_attachments.first - = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 670, height: 380, detailed: true, inline: true, alt: video.description do + = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: status.sensitive?, width: 670, height: 380, detailed: true, inline: true, alt: video.description do = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments } - elsif status.media_attachments.first.audio? - audio = status.media_attachments.first = react_component :audio, src: audio.file.url(:original), height: 130, alt: audio.description, preload: true, duration: audio.file.meta.dig(:original, :duration) do = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments } - else - = react_component :media_gallery, height: 380, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif || autoplay, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do + = react_component :media_gallery, height: 380, sensitive: status.sensitive?, standalone: true, autoplay: autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments } - elsif status.preview_card = react_component :card, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json diff --git a/app/views/statuses/_simple_status.html.haml b/app/views/statuses/_simple_status.html.haml index edcfbba2b..66c9412af 100644 --- a/app/views/statuses/_simple_status.html.haml +++ b/app/views/statuses/_simple_status.html.haml @@ -33,14 +33,14 @@ - if !status.media_attachments.empty? - if status.media_attachments.first.video? - video = status.media_attachments.first - = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 610, height: 343, inline: true, alt: video.description do + = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: status.sensitive?, width: 610, height: 343, inline: true, alt: video.description do = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments } - elsif status.media_attachments.first.audio? - audio = status.media_attachments.first = react_component :audio, src: audio.file.url(:original), height: 110, alt: audio.description, duration: audio.file.meta.dig(:original, :duration) do = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments } - else - = react_component :media_gallery, height: 343, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif || autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do + = react_component :media_gallery, height: 343, sensitive: status.sensitive?, autoplay: autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments } - elsif status.preview_card = react_component :card, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json -- cgit From 6feafb8802b2759eb33968a70b6c1cb100bf3926 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 20 Jan 2020 15:55:03 +0100 Subject: Various fixes and improvements (#12878) * Fix unused role routes being generated * Remove unused JavaScript code * Refactor filters code to be DRYer * Fix `.count == 0` comparisons to `.empty?` in views * Fix filters in views --- app/controllers/admin/accounts_controller.rb | 16 +--------------- app/controllers/admin/custom_emojis_controller.rb | 2 +- app/controllers/admin/instances_controller.rb | 2 +- app/controllers/admin/invites_controller.rb | 2 +- app/controllers/admin/reports_controller.rb | 7 +------ app/controllers/admin/tags_controller.rb | 2 +- app/controllers/relationships_controller.rb | 2 +- app/helpers/admin/filter_helper.rb | 18 +++++++++--------- app/javascript/packs/public.js | 9 --------- app/models/account_filter.rb | 15 +++++++++++++++ app/models/custom_emoji_filter.rb | 7 +++++++ app/models/instance_filter.rb | 5 +++++ app/models/invite_filter.rb | 5 +++++ app/models/relationship_filter.rb | 11 +++++++++++ app/models/report_filter.rb | 7 +++++++ app/models/tag_filter.rb | 10 ++++++++++ app/views/admin/accounts/index.html.haml | 2 +- app/views/admin/custom_emojis/index.html.haml | 4 ++-- app/views/admin/email_domain_blocks/index.html.haml | 2 +- app/views/admin/instances/index.html.haml | 5 ++--- app/views/admin/reports/index.html.haml | 5 ++--- app/views/admin/tags/index.html.haml | 5 ++--- app/views/filters/index.html.haml | 2 +- app/views/relationships/show.html.haml | 7 +++---- app/views/settings/applications/index.html.haml | 2 +- config/routes.rb | 2 +- 26 files changed, 92 insertions(+), 64 deletions(-) create mode 100644 app/models/relationship_filter.rb (limited to 'app/views') diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index 68b6352f8..7b1783542 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -109,21 +109,7 @@ module Admin end def filter_params - params.permit( - :local, - :remote, - :by_domain, - :active, - :pending, - :disabled, - :silenced, - :suspended, - :username, - :display_name, - :email, - :ip, - :staff - ) + params.slice(*AccountFilter::KEYS).permit(*AccountFilter::KEYS) end end end diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb index a446465c9..efa8f2950 100644 --- a/app/controllers/admin/custom_emojis_controller.rb +++ b/app/controllers/admin/custom_emojis_controller.rb @@ -48,7 +48,7 @@ module Admin end def filter_params - params.slice(:local, :remote, :by_domain, :shortcode, :page).permit(:local, :remote, :by_domain, :shortcode, :page) + params.slice(:page, *CustomEmojiFilter::KEYS).permit(:page, *CustomEmojiFilter::KEYS) end def action_from_button diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb index b47b18f8e..2fc041207 100644 --- a/app/controllers/admin/instances_controller.rb +++ b/app/controllers/admin/instances_controller.rb @@ -62,7 +62,7 @@ module Admin end def filter_params - params.permit(:limited, :by_domain) + params.slice(*InstanceFilter::KEYS).permit(*InstanceFilter::KEYS) end end end diff --git a/app/controllers/admin/invites_controller.rb b/app/controllers/admin/invites_controller.rb index 44a8eec77..dabfe9765 100644 --- a/app/controllers/admin/invites_controller.rb +++ b/app/controllers/admin/invites_controller.rb @@ -47,7 +47,7 @@ module Admin end def filter_params - params.permit(:available, :expired) + params.slice(*InviteFilter::KEYS).permit(*InviteFilter::KEYS) end end end diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index 09ce1761c..7c831b3d4 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -52,12 +52,7 @@ module Admin end def filter_params - params.permit( - :account_id, - :resolved, - :target_account_id, - :by_target_domain - ) + params.slice(*ReportFilter::KEYS).permit(*ReportFilter::KEYS) end def set_report diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb index 65341bbfb..59df4470e 100644 --- a/app/controllers/admin/tags_controller.rb +++ b/app/controllers/admin/tags_controller.rb @@ -73,7 +73,7 @@ module Admin end def filter_params - params.slice(:directory, :reviewed, :unreviewed, :pending_review, :page, :popular, :active, :name).permit(:directory, :reviewed, :unreviewed, :pending_review, :page, :popular, :active, :name) + params.slice(:page, *TagFilter::KEYS).permit(:page, *TagFilter::KEYS) end def tag_params diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb index e6705c327..9d0be4a00 100644 --- a/app/controllers/relationships_controller.rb +++ b/app/controllers/relationships_controller.rb @@ -85,7 +85,7 @@ class RelationshipsController < ApplicationController end def current_params - params.slice(:page, :status, :relationship, :by_domain, :activity, :order).permit(:page, :status, :relationship, :by_domain, :activity, :order) + params.slice(:page, *RelationshipFilter::KEYS).permit(:page, *RelationshipFilter::KEYS) end def action_from_button diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb index fc4f15985..130686a02 100644 --- a/app/helpers/admin/filter_helper.rb +++ b/app/helpers/admin/filter_helper.rb @@ -1,15 +1,15 @@ # frozen_string_literal: true module Admin::FilterHelper - ACCOUNT_FILTERS = %i(local remote by_domain active pending silenced suspended username display_name email ip staff).freeze - REPORT_FILTERS = %i(resolved account_id target_account_id by_target_domain).freeze - INVITE_FILTER = %i(available expired).freeze - CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze - TAGS_FILTERS = %i(directory reviewed unreviewed pending_review popular active name).freeze - INSTANCES_FILTERS = %i(limited by_domain).freeze - FOLLOWERS_FILTERS = %i(relationship status by_domain activity order).freeze - - FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS + TAGS_FILTERS + INSTANCES_FILTERS + FOLLOWERS_FILTERS + FILTERS = [ + AccountFilter::KEYS, + CustomEmojiFilter::KEYS, + ReportFilter::KEYS, + TagFilter::KEYS, + InstanceFilter::KEYS, + InviteFilter::KEYS, + RelationshipFilter::KEYS, + ].flatten.freeze def filter_link_to(text, link_to_params, link_class_params = link_to_params) new_url = filtered_url_for(link_to_params) diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index 6a7f8831d..9bc6214af 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -142,15 +142,6 @@ function main() { return false; }); - delegate(document, '.blocks-table button.icon-button', 'click', function(e) { - e.preventDefault(); - - const classList = this.firstElementChild.classList; - classList.toggle('fa-chevron-down'); - classList.toggle('fa-chevron-up'); - this.parentElement.parentElement.nextElementSibling.classList.toggle('hidden'); - }); - delegate(document, '.modal-button', 'click', e => { e.preventDefault(); diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb index c3b1fe08d..c1e6b0deb 100644 --- a/app/models/account_filter.rb +++ b/app/models/account_filter.rb @@ -1,6 +1,21 @@ # frozen_string_literal: true class AccountFilter + KEYS = %i( + local + remote + by_domain + active + pending + silenced + suspended + username + display_name + email + ip + staff + ).freeze + attr_reader :params def initialize(params) diff --git a/app/models/custom_emoji_filter.rb b/app/models/custom_emoji_filter.rb index 15b8da1d1..414e1fcdd 100644 --- a/app/models/custom_emoji_filter.rb +++ b/app/models/custom_emoji_filter.rb @@ -1,6 +1,13 @@ # frozen_string_literal: true class CustomEmojiFilter + KEYS = %i( + local + remote + by_domain + shortcode + ).freeze + attr_reader :params def initialize(params) diff --git a/app/models/instance_filter.rb b/app/models/instance_filter.rb index 8bfab826d..9c467bc27 100644 --- a/app/models/instance_filter.rb +++ b/app/models/instance_filter.rb @@ -1,6 +1,11 @@ # frozen_string_literal: true class InstanceFilter + KEYS = %i( + limited + by_domain + ).freeze + attr_reader :params def initialize(params) diff --git a/app/models/invite_filter.rb b/app/models/invite_filter.rb index 7d89bad4a..9685d4abb 100644 --- a/app/models/invite_filter.rb +++ b/app/models/invite_filter.rb @@ -1,6 +1,11 @@ # frozen_string_literal: true class InviteFilter + KEYS = %i( + available + expired + ).freeze + attr_reader :params def initialize(params) diff --git a/app/models/relationship_filter.rb b/app/models/relationship_filter.rb new file mode 100644 index 000000000..51640f494 --- /dev/null +++ b/app/models/relationship_filter.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class RelationshipFilter + KEYS = %i( + relationship + status + by_domain + activity + order + ).freeze +end diff --git a/app/models/report_filter.rb b/app/models/report_filter.rb index abf53cbab..c32d4359e 100644 --- a/app/models/report_filter.rb +++ b/app/models/report_filter.rb @@ -1,6 +1,13 @@ # frozen_string_literal: true class ReportFilter + KEYS = %i( + resolved + account_id + target_account_id + by_target_domain + ).freeze + attr_reader :params def initialize(params) diff --git a/app/models/tag_filter.rb b/app/models/tag_filter.rb index 8921e186b..a9ff5b703 100644 --- a/app/models/tag_filter.rb +++ b/app/models/tag_filter.rb @@ -1,6 +1,16 @@ # frozen_string_literal: true class TagFilter + KEYS = %i( + directory + reviewed + unreviewed + pending_review + popular + active + name + ).freeze + attr_reader :params def initialize(params) diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml index 7e9adb3ff..3a85324c9 100644 --- a/app/views/admin/accounts/index.html.haml +++ b/app/views/admin/accounts/index.html.haml @@ -22,7 +22,7 @@ = form_tag admin_accounts_url, method: 'GET', class: 'simple_form' do .fields-group - - Admin::FilterHelper::ACCOUNT_FILTERS.each do |key| + - AccountFilter::KEYS.each do |key| - if params[key].present? = hidden_field_tag key, params[key] diff --git a/app/views/admin/custom_emojis/index.html.haml b/app/views/admin/custom_emojis/index.html.haml index 389e9dd71..69aa5ae41 100644 --- a/app/views/admin/custom_emojis/index.html.haml +++ b/app/views/admin/custom_emojis/index.html.haml @@ -25,7 +25,7 @@ = form_tag admin_custom_emojis_url, method: 'GET', class: 'simple_form' do .fields-group - - Admin::FilterHelper::CUSTOM_EMOJI_FILTERS.each do |key| + - CustomEmojiFilter::KEYS.each do |key| = hidden_field_tag key, params[key] if params[key].present? - %i(shortcode by_domain).each do |key| @@ -39,7 +39,7 @@ = form_for(@form, url: batch_admin_custom_emojis_path) do |f| = hidden_field_tag :page, params[:page] || 1 - - Admin::FilterHelper::CUSTOM_EMOJI_FILTERS.each do |key| + - CustomEmojiFilter::KEYS.each do |key| = hidden_field_tag key, params[key] if params[key].present? .batch-table diff --git a/app/views/admin/email_domain_blocks/index.html.haml b/app/views/admin/email_domain_blocks/index.html.haml index c1cc470b6..6015cfac0 100644 --- a/app/views/admin/email_domain_blocks/index.html.haml +++ b/app/views/admin/email_domain_blocks/index.html.haml @@ -4,7 +4,7 @@ - content_for :heading_actions do = link_to t('admin.email_domain_blocks.add_new'), new_admin_email_domain_block_path, class: 'button' -- if @email_domain_blocks.count == 0 +- if @email_domain_blocks.empty? %div.muted-hint.center-text=t 'admin.email_domain_blocks.empty' - else .table-wrapper diff --git a/app/views/admin/instances/index.html.haml b/app/views/admin/instances/index.html.haml index 1d85aa75e..0b299acc5 100644 --- a/app/views/admin/instances/index.html.haml +++ b/app/views/admin/instances/index.html.haml @@ -19,9 +19,8 @@ - unless whitelist_mode? = form_tag admin_instances_url, method: 'GET', class: 'simple_form' do .fields-group - - Admin::FilterHelper::INSTANCES_FILTERS.each do |key| - - if params[key].present? - = hidden_field_tag key, params[key] + - InstanceFilter::KEYS.each do |key| + = hidden_field_tag key, params[key] if params[key].present? - %i(by_domain).each do |key| .input.string.optional diff --git a/app/views/admin/reports/index.html.haml b/app/views/admin/reports/index.html.haml index 30c7549b0..0263b80fb 100644 --- a/app/views/admin/reports/index.html.haml +++ b/app/views/admin/reports/index.html.haml @@ -10,9 +10,8 @@ = form_tag admin_reports_url, method: 'GET', class: 'simple_form' do .fields-group - - Admin::FilterHelper::REPORT_FILTERS.each do |key| - - if params[key].present? - = hidden_field_tag key, params[key] + - ReportFilter::KEYS.each do |key| + = hidden_field_tag key, params[key] if params[key].present? - %i(by_target_domain).each do |key| .input.string.optional diff --git a/app/views/admin/tags/index.html.haml b/app/views/admin/tags/index.html.haml index 7f2c53190..1ff538ba3 100644 --- a/app/views/admin/tags/index.html.haml +++ b/app/views/admin/tags/index.html.haml @@ -28,7 +28,7 @@ = form_tag admin_tags_url, method: 'GET', class: 'simple_form' do .fields-group - - Admin::FilterHelper::TAGS_FILTERS.each do |key| + - TagFilter::KEYS.each do |key| = hidden_field_tag key, params[key] if params[key].present? - %i(name).each do |key| @@ -43,9 +43,8 @@ = form_for(@form, url: batch_admin_tags_path) do |f| = hidden_field_tag :page, params[:page] || 1 - = hidden_field_tag :name, params[:name] if params[:name].present? - - Admin::FilterHelper::TAGS_FILTERS.each do |key| + - TagFilter::KEYS.each do |key| = hidden_field_tag key, params[key] if params[key].present? .batch-table.optional diff --git a/app/views/filters/index.html.haml b/app/views/filters/index.html.haml index 8ace638ca..b4d5333aa 100644 --- a/app/views/filters/index.html.haml +++ b/app/views/filters/index.html.haml @@ -4,7 +4,7 @@ - content_for :heading_actions do = link_to t('filters.new.title'), new_filter_path, class: 'button' -- if @filters.count == 0 +- if @filters.empty? %div.muted-hint.center-text= t 'filters.index.empty' - else .table-wrapper diff --git a/app/views/relationships/show.html.haml b/app/views/relationships/show.html.haml index 0da1596ce..099bb3202 100644 --- a/app/views/relationships/show.html.haml +++ b/app/views/relationships/show.html.haml @@ -33,10 +33,9 @@ = form_for(@form, url: relationships_path, method: :patch) do |f| = hidden_field_tag :page, params[:page] || 1 - = hidden_field_tag :relationship, params[:relationship] - = hidden_field_tag :status, params[:status] - = hidden_field_tag :activity, params[:activity] - = hidden_field_tag :order, params[:order] + + - RelationshipFilter::KEYS.each do |key| + = hidden_field_tag key, params[key] if params[key].present? .batch-table .batch-table__toolbar diff --git a/app/views/settings/applications/index.html.haml b/app/views/settings/applications/index.html.haml index 1cb94760f..a1f904a3a 100644 --- a/app/views/settings/applications/index.html.haml +++ b/app/views/settings/applications/index.html.haml @@ -4,7 +4,7 @@ - content_for :heading_actions do = link_to t('doorkeeper.applications.index.new'), new_settings_application_path, class: 'button' -- if @applications.count == 0 +- if @applications.empty? %div.muted-hint.center-text=t 'doorkeeper.applications.index.empty' - else .table-wrapper diff --git a/config/routes.rb b/config/routes.rb index 5411cff58..ff308699d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -231,7 +231,7 @@ Rails.application.routes.draw do end end - resource :role do + resource :role, only: [] do member do post :promote post :demote -- cgit From 91e4955e08506abc5fa70dadc9ffa6956b1fb0bc Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 20 Jan 2020 17:39:14 +0100 Subject: Fix admin interface crashing when listing boosts with non-video media attachments (#12907) Fix regression introduced by #12879 --- app/views/admin/reports/_status.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/views') diff --git a/app/views/admin/reports/_status.html.haml b/app/views/admin/reports/_status.html.haml index 105352e46..fa15796d2 100644 --- a/app/views/admin/reports/_status.html.haml +++ b/app/views/admin/reports/_status.html.haml @@ -15,7 +15,7 @@ - if status.proper.media_attachments.first.video? - video = status.proper.media_attachments.first = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: status.proper.sensitive?, visible: false, width: 610, height: 343, inline: true, alt: video.description - - elsif status.media_attachments.first.audio? + - elsif status.proper.media_attachments.first.audio? - audio = status.proper.media_attachments.first = react_component :audio, src: audio.file.url(:original), height: 110, alt: audio.description, duration: audio.file.meta.dig(:original, :duration) - else -- cgit From 02063c245c8a2a09e741cbce8302edf3ec14feef Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 20 Jan 2020 18:00:54 +0100 Subject: Fix not all of account's active IPs showing up in admin UI (#12909) --- app/models/user.rb | 15 +++++++++++++++ app/views/admin/accounts/show.html.haml | 12 ++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) (limited to 'app/views') diff --git a/app/models/user.rb b/app/models/user.rb index a1753784d..a43e63b2e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -290,6 +290,21 @@ class User < ApplicationRecord setting_display_media == 'hide_all' end + def recent_ips + @recent_ips ||= begin + arr = [] + + session_activations.each do |session_activation| + arr << [session_activation.updated_at, session_activation.ip] + end + + arr << [current_sign_in_at, current_sign_in_ip] if current_sign_in_ip.present? + arr << [last_sign_in_at, last_sign_in_ip] if last_sign_in_ip.present? + + arr.sort_by(&:first).uniq(&:last).reverse! + end + end + protected def send_devise_notification(notification, *args) diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index 9f1e3816b..1429f56d5 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -139,12 +139,12 @@ %time.formatted{ datetime: @account.created_at.iso8601, title: l(@account.created_at) }= l @account.created_at %td - %tr - %th= t('admin.accounts.most_recent_ip') - %td= @account.user_current_sign_in_ip - %td - - if @account.user_current_sign_in_ip - = table_link_to 'search', t('admin.accounts.search_same_ip'), admin_accounts_path(ip: @account.user_current_sign_in_ip) + - @account.user.recent_ips.each_with_index do |(_, ip), i| + %tr + - if i.zero? + %th{ rowspan: @account.user.recent_ips.size }= t('admin.accounts.most_recent_ip') + %td= ip + %td= table_link_to 'search', t('admin.accounts.search_same_ip'), admin_accounts_path(ip: ip) %tr %th= t('admin.accounts.most_recent_activity') -- cgit From 40852b84bd896fd2f81e8f7dd45f70b121ba70fd Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 22 Jan 2020 16:26:23 +0100 Subject: Fix duplicate β€œdescription” meta tag on accounts public pages (#12923) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #12920 --- app/views/accounts/show.html.haml | 2 -- 1 file changed, 2 deletions(-) (limited to 'app/views') diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index 9c26dbabc..c312fe2bd 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -2,8 +2,6 @@ = "#{display_name(@account)} (@#{@account.local_username_and_domain})" - content_for :header_tags do - %meta{ name: 'description', content: account_description(@account) }/ - - if @account.user&.setting_noindex %meta{ name: 'robots', content: 'noindex, noarchive' }/ -- cgit From c0006a004d0e58bb3ad356759c17e60f28975b61 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 23 Jan 2020 20:33:20 +0100 Subject: Change followers page to relationships page in admin UI (#12927) Allow browsing and filtering all relationships instead of just followers, unify the codebase with the user-facing relationship manager, add ability to see who the user invited --- app/controllers/admin/followers_controller.rb | 18 ---- app/controllers/admin/relationships_controller.rb | 25 +++++ app/controllers/relationships_controller.rb | 46 +-------- app/models/relationship_filter.rb | 109 ++++++++++++++++++++++ app/views/admin/accounts/show.html.haml | 2 +- app/views/admin/followers/index.html.haml | 28 ------ app/views/admin/relationships/index.html.haml | 39 ++++++++ config/locales/en.yml | 6 +- config/routes.rb | 2 +- 9 files changed, 181 insertions(+), 94 deletions(-) delete mode 100644 app/controllers/admin/followers_controller.rb create mode 100644 app/controllers/admin/relationships_controller.rb delete mode 100644 app/views/admin/followers/index.html.haml create mode 100644 app/views/admin/relationships/index.html.haml (limited to 'app/views') diff --git a/app/controllers/admin/followers_controller.rb b/app/controllers/admin/followers_controller.rb deleted file mode 100644 index d826f47c5..000000000 --- a/app/controllers/admin/followers_controller.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module Admin - class FollowersController < BaseController - before_action :set_account - - PER_PAGE = 40 - - def index - authorize :account, :index? - @followers = @account.followers.local.recent.page(params[:page]).per(PER_PAGE) - end - - def set_account - @account = Account.find(params[:account_id]) - end - end -end diff --git a/app/controllers/admin/relationships_controller.rb b/app/controllers/admin/relationships_controller.rb new file mode 100644 index 000000000..07d121340 --- /dev/null +++ b/app/controllers/admin/relationships_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Admin + class RelationshipsController < BaseController + before_action :set_account + + PER_PAGE = 40 + + def index + authorize :account, :index? + + @accounts = RelationshipFilter.new(@account, filter_params).results.page(params[:page]).per(PER_PAGE) + end + + private + + def set_account + @account = Account.find(params[:account_id]) + end + + def filter_params + params.slice(RelationshipFilter::KEYS).permit(RelationshipFilter::KEYS) + end + end +end diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb index 9d0be4a00..0835758f2 100644 --- a/app/controllers/relationships_controller.rb +++ b/app/controllers/relationships_controller.rb @@ -19,53 +19,13 @@ class RelationshipsController < ApplicationController rescue ActionController::ParameterMissing # Do nothing ensure - redirect_to relationships_path(current_params) + redirect_to relationships_path(filter_params) end private def set_accounts - @accounts = relationships_scope.page(params[:page]).per(40) - end - - def relationships_scope - scope = begin - if following_relationship? - current_account.following.eager_load(:account_stat).reorder(nil) - else - current_account.followers.eager_load(:account_stat).reorder(nil) - end - end - - scope.merge!(Follow.recent) if params[:order].blank? || params[:order] == 'recent' - scope.merge!(Account.by_recent_status) if params[:order] == 'active' - scope.merge!(mutual_relationship_scope) if mutual_relationship? - scope.merge!(moved_account_scope) if params[:status] == 'moved' - scope.merge!(primary_account_scope) if params[:status] == 'primary' - scope.merge!(by_domain_scope) if params[:by_domain].present? - scope.merge!(dormant_account_scope) if params[:activity] == 'dormant' - - scope - end - - def mutual_relationship_scope - Account.where(id: current_account.following) - end - - def moved_account_scope - Account.where.not(moved_to_account_id: nil) - end - - def primary_account_scope - Account.where(moved_to_account_id: nil) - end - - def dormant_account_scope - AccountStat.where(last_status_at: nil).or(AccountStat.where(AccountStat.arel_table[:last_status_at].lt(1.month.ago))) - end - - def by_domain_scope - Account.where(domain: params[:by_domain]) + @accounts = RelationshipFilter.new(current_account, filter_params).results.page(params[:page]).per(40) end def form_account_batch_params @@ -84,7 +44,7 @@ class RelationshipsController < ApplicationController params[:relationship] == 'followed_by' end - def current_params + def filter_params params.slice(:page, *RelationshipFilter::KEYS).permit(:page, *RelationshipFilter::KEYS) end diff --git a/app/models/relationship_filter.rb b/app/models/relationship_filter.rb index 51640f494..fcb3a8dc5 100644 --- a/app/models/relationship_filter.rb +++ b/app/models/relationship_filter.rb @@ -7,5 +7,114 @@ class RelationshipFilter by_domain activity order + location ).freeze + + attr_reader :params, :account + + def initialize(account, params) + @account = account + @params = params + + set_defaults! + end + + def results + scope = scope_for('relationship', params['relationship']) + + params.each do |key, value| + next if key.to_s == 'page' + + scope.merge!(scope_for(key, value)) if value.present? + end + + scope + end + + private + + def set_defaults! + params['relationship'] = 'following' if params['relationship'].blank? + params['order'] = 'recent' if params['order'].blank? + end + + def scope_for(key, value) + case key.to_s + when 'relationship' + relationship_scope(value) + when 'by_domain' + by_domain_scope(value) + when 'location' + location_scope(value) + when 'status' + status_scope(value) + when 'order' + order_scope(value) + when 'activity' + activity_scope(value) + else + raise "Unknown filter: #{key}" + end + end + + def relationship_scope(value) + case value.to_s + when 'following' + account.following.eager_load(:account_stat).reorder(nil) + when 'followed_by' + account.followers.eager_load(:account_stat).reorder(nil) + when 'mutual' + account.followers.eager_load(:account_stat).reorder(nil).merge(Account.where(id: account.following)) + when 'invited' + Account.joins(user: :invite).merge(Invite.where(user: account.user)).eager_load(:account_stat).reorder(nil) + else + raise "Unknown relationship: #{value}" + end + end + + def by_domain_scope(value) + Account.where(domain: value.to_s) + end + + def location_scope(value) + case value.to_s + when 'local' + Account.local + when 'remote' + Account.remote + else + raise "Unknown location: #{value}" + end + end + + def status_scope(value) + case value.to_s + when 'moved' + Account.where.not(moved_to_account_id: nil) + when 'primary' + Account.where(moved_to_account_id: nil) + else + raise "Unknown status: #{value}" + end + end + + def order_scope(value) + case value.to_s + when 'active' + Account.by_recent_status + when 'recent' + Follow.recent + else + raise "Unknown order: #{value}" + end + end + + def activity_scope(value) + case value.to_s + when 'dormant' + AccountStat.where(last_status_at: nil).or(AccountStat.where(AccountStat.arel_table[:last_status_at].lt(1.month.ago))) + else + raise "Unknown activity: #{value}" + end + end end diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index 1429f56d5..f191d8f25 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -41,7 +41,7 @@ .dashboard__counters__num= number_to_human_size @account.media_attachments.sum('file_file_size') .dashboard__counters__label= t 'admin.accounts.media_attachments' %div - = link_to admin_account_followers_path(@account.id) do + = link_to admin_account_relationships_path(@account.id, location: 'local') do .dashboard__counters__num= number_with_delimiter @account.local_followers_count .dashboard__counters__label= t 'admin.accounts.followers' %div diff --git a/app/views/admin/followers/index.html.haml b/app/views/admin/followers/index.html.haml deleted file mode 100644 index 25f1f290f..000000000 --- a/app/views/admin/followers/index.html.haml +++ /dev/null @@ -1,28 +0,0 @@ -- content_for :page_title do - = t('admin.followers.title', acct: @account.acct) - -.filters - .filter-subset - %strong= t('admin.accounts.location.title') - %ul - %li= link_to t('admin.accounts.location.local'), admin_account_followers_path(@account.id), class: 'selected' - .back-link{ style: 'flex: 1 1 auto; text-align: right' } - = link_to admin_account_path(@account.id) do - = fa_icon 'chevron-left fw' - = t('admin.followers.back_to_account') - -%hr.spacer/ - -.table-wrapper - %table.table - %thead - %tr - %th= t('admin.accounts.username') - %th= t('admin.accounts.role') - %th= t('admin.accounts.most_recent_ip') - %th= t('admin.accounts.most_recent_activity') - %th - %tbody - = render partial: 'admin/accounts/account', collection: @followers - -= paginate @followers diff --git a/app/views/admin/relationships/index.html.haml b/app/views/admin/relationships/index.html.haml new file mode 100644 index 000000000..3afaff615 --- /dev/null +++ b/app/views/admin/relationships/index.html.haml @@ -0,0 +1,39 @@ +- content_for :page_title do + = t('admin.relationships.title', acct: @account.acct) + +.filters + .filter-subset + %strong= t 'relationships.relationship' + %ul + %li= filter_link_to t('relationships.following'), relationship: nil + %li= filter_link_to t('relationships.followers'), relationship: 'followed_by' + %li= filter_link_to t('relationships.mutual'), relationship: 'mutual' + %li= filter_link_to t('relationships.invited'), relationship: 'invited' + + .filter-subset + %strong= t('admin.accounts.location.title') + %ul + %li= filter_link_to t('admin.accounts.moderation.all'), location: nil + %li= filter_link_to t('admin.accounts.location.local'), location: 'local' + %li= filter_link_to t('admin.accounts.location.remote'), location: 'remote' + + .back-link{ style: 'flex: 1 1 auto; text-align: right' } + = link_to admin_account_path(@account.id) do + = fa_icon 'chevron-left fw' + = t('admin.statuses.back_to_account') + +%hr.spacer/ + +.table-wrapper + %table.table + %thead + %tr + %th= t('admin.accounts.username') + %th= t('admin.accounts.role') + %th= t('admin.accounts.most_recent_ip') + %th= t('admin.accounts.most_recent_activity') + %th + %tbody + = render partial: 'admin/accounts/account', collection: @accounts + += paginate @accounts diff --git a/config/locales/en.yml b/config/locales/en.yml index d768cef33..8a86f8c7a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -344,9 +344,6 @@ en: create: Add domain title: New e-mail blacklist entry title: E-mail blacklist - followers: - back_to_account: Back To Account - title: "%{acct}'s Followers" instances: by_domain: Domain delivery_available: Delivery is available @@ -375,6 +372,8 @@ en: title: Invites pending_accounts: title: Pending accounts (%{count}) + relationships: + title: "%{acct}'s relationships" relays: add_new: Add new relay delete: Delete @@ -935,6 +934,7 @@ en: dormant: Dormant followers: Followers following: Following + invited: Invited last_active: Last active most_recent: Most recent moved: Moved diff --git a/config/routes.rb b/config/routes.rb index ff308699d..f79af192d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -223,7 +223,7 @@ Rails.application.routes.draw do resource :reset, only: [:create] resource :action, only: [:new, :create], controller: 'account_actions' resources :statuses, only: [:index, :show, :create, :update, :destroy] - resources :followers, only: [:index] + resources :relationships, only: [:index] resource :confirmation, only: [:create] do collection do -- cgit From f52c988e12e464e7baefc2fdb48ddf4a95584664 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 23 Jan 2020 22:00:13 +0100 Subject: Add announcements (#12662) * Add announcements Fix #11006 * Add reactions to announcements * Add admin UI for announcements * Add unit tests * Fix issues - Add `with_dismissed` param to announcements API - Fix end date not being formatted when time range is given - Fix announcement delete causing reactions to send streaming updates - Fix announcements container growing too wide and mascot too small - Fix `all_day` being settable when no time range is given - Change text "Update" to "Announcement" * Fix scheduler unpublishing announcements before they are due * Fix filter params not being passed to announcements filter --- app/controllers/admin/announcements_controller.rb | 69 ++++ app/controllers/api/base_controller.rb | 2 +- .../api/v1/announcements/reactions_controller.rb | 29 ++ app/controllers/api/v1/announcements_controller.rb | 33 ++ app/helpers/admin/action_logs_helper.rb | 8 + app/helpers/admin/announcements_helper.rb | 11 + app/helpers/admin/filter_helper.rb | 1 + app/javascript/images/elephant_ui_plane.svg | 2 +- app/javascript/mastodon/actions/announcements.js | 133 +++++++ .../mastodon/actions/importer/normalizer.js | 10 +- app/javascript/mastodon/actions/notifications.js | 3 +- app/javascript/mastodon/actions/streaming.js | 11 +- app/javascript/mastodon/actions/timelines.js | 2 +- .../mastodon/components/error_boundary.js | 2 +- .../compose/components/emoji_picker_dropdown.js | 7 +- .../getting_started/components/announcements.js | 395 +++++++++++++++++++++ .../containers/announcements_container.js | 21 ++ .../getting_started/containers/trends_container.js | 2 +- .../mastodon/features/home_timeline/index.js | 3 + .../mastodon/features/ui/components/media_modal.js | 1 - app/javascript/mastodon/reducers/announcements.js | 72 ++++ app/javascript/mastodon/reducers/index.js | 2 + app/javascript/styles/mastodon/components.scss | 213 +++++++++++ app/javascript/styles/mastodon/forms.scss | 6 + app/lib/entity_cache.rb | 2 +- app/lib/inline_renderer.rb | 4 + app/models/account.rb | 6 + app/models/announcement.rb | 85 +++++ app/models/announcement_filter.rb | 39 ++ app/models/announcement_mute.rb | 19 + app/models/announcement_reaction.rb | 37 ++ app/models/backup.rb | 2 +- app/models/bookmark.rb | 6 +- app/models/concerns/account_interactions.rb | 1 + app/models/custom_emoji.rb | 2 +- app/policies/announcement_policy.rb | 19 + app/serializers/rest/announcement_serializer.rb | 34 ++ app/serializers/rest/reaction_serializer.rb | 31 ++ app/validators/reaction_validator.rb | 17 + .../admin/announcements/_announcement.html.haml | 14 + app/views/admin/announcements/edit.html.haml | 22 ++ app/views/admin/announcements/index.html.haml | 30 ++ app/views/admin/announcements/new.html.haml | 21 ++ .../publish_announcement_reaction_worker.rb | 22 ++ .../publish_scheduled_announcement_worker.rb | 18 + .../scheduler/scheduled_statuses_scheduler.rb | 28 +- config/initializers/simple_form.rb | 2 +- config/locales/en.yml | 22 ++ config/locales/simple_form.en.yml | 12 + config/navigation.rb | 1 + config/routes.rb | 13 + db/migrate/20191218153258_create_announcements.rb | 16 + .../20200113125135_create_announcement_mutes.rb | 12 + ...20200114113335_create_announcement_reactions.rb | 15 + db/schema.rb | 41 ++- lib/tasks/auto_annotate_models.rake | 1 + .../v1/announcements/reactions_controller_spec.rb | 65 ++++ .../api/v1/announcements_controller_spec.rb | 59 +++ spec/controllers/api/v1/trends_controller_spec.rb | 18 + spec/fabricators/announcement_fabricator.rb | 6 + spec/fabricators/announcement_mute_fabricator.rb | 4 + .../announcement_reaction_fabricator.rb | 5 + spec/models/announcement_mute_spec.rb | 4 + spec/models/announcement_reaction_spec.rb | 4 + spec/models/announcement_spec.rb | 4 + 65 files changed, 1779 insertions(+), 22 deletions(-) create mode 100644 app/controllers/admin/announcements_controller.rb create mode 100644 app/controllers/api/v1/announcements/reactions_controller.rb create mode 100644 app/controllers/api/v1/announcements_controller.rb create mode 100644 app/helpers/admin/announcements_helper.rb create mode 100644 app/javascript/mastodon/actions/announcements.js create mode 100644 app/javascript/mastodon/features/getting_started/components/announcements.js create mode 100644 app/javascript/mastodon/features/getting_started/containers/announcements_container.js create mode 100644 app/javascript/mastodon/reducers/announcements.js create mode 100644 app/models/announcement.rb create mode 100644 app/models/announcement_filter.rb create mode 100644 app/models/announcement_mute.rb create mode 100644 app/models/announcement_reaction.rb create mode 100644 app/policies/announcement_policy.rb create mode 100644 app/serializers/rest/announcement_serializer.rb create mode 100644 app/serializers/rest/reaction_serializer.rb create mode 100644 app/validators/reaction_validator.rb create mode 100644 app/views/admin/announcements/_announcement.html.haml create mode 100644 app/views/admin/announcements/edit.html.haml create mode 100644 app/views/admin/announcements/index.html.haml create mode 100644 app/views/admin/announcements/new.html.haml create mode 100644 app/workers/publish_announcement_reaction_worker.rb create mode 100644 app/workers/publish_scheduled_announcement_worker.rb create mode 100644 db/migrate/20191218153258_create_announcements.rb create mode 100644 db/migrate/20200113125135_create_announcement_mutes.rb create mode 100644 db/migrate/20200114113335_create_announcement_reactions.rb create mode 100644 spec/controllers/api/v1/announcements/reactions_controller_spec.rb create mode 100644 spec/controllers/api/v1/announcements_controller_spec.rb create mode 100644 spec/controllers/api/v1/trends_controller_spec.rb create mode 100644 spec/fabricators/announcement_fabricator.rb create mode 100644 spec/fabricators/announcement_mute_fabricator.rb create mode 100644 spec/fabricators/announcement_reaction_fabricator.rb create mode 100644 spec/models/announcement_mute_spec.rb create mode 100644 spec/models/announcement_reaction_spec.rb create mode 100644 spec/models/announcement_spec.rb (limited to 'app/views') diff --git a/app/controllers/admin/announcements_controller.rb b/app/controllers/admin/announcements_controller.rb new file mode 100644 index 000000000..02198f0b5 --- /dev/null +++ b/app/controllers/admin/announcements_controller.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +class Admin::AnnouncementsController < Admin::BaseController + before_action :set_announcements, only: :index + before_action :set_announcement, except: [:index, :new, :create] + + def index + authorize :announcement, :index? + end + + def new + authorize :announcement, :create? + + @announcement = Announcement.new + end + + def create + authorize :announcement, :create? + + @announcement = Announcement.new(resource_params) + + if @announcement.save + log_action :create, @announcement + redirect_to admin_announcements_path + else + render :new + end + end + + def edit + authorize :announcement, :update? + end + + def update + authorize :announcement, :update? + + if @announcement.update(resource_params) + log_action :update, @announcement + redirect_to admin_announcements_path + else + render :edit + end + end + + def destroy + authorize :announcement, :destroy? + @announcement.destroy! + log_action :destroy, @announcement + redirect_to admin_announcements_path + end + + private + + def set_announcements + @announcements = AnnouncementFilter.new(filter_params).results.page(params[:page]) + end + + def set_announcement + @announcement = Announcement.find(params[:id]) + end + + def filter_params + params.slice(*AnnouncementFilter::KEYS).permit(*AnnouncementFilter::KEYS) + end + + def resource_params + params.require(:announcement).permit(:text, :scheduled_at, :starts_at, :ends_at, :all_day) + end +end diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 144fdd6ac..68bf425f4 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -85,7 +85,7 @@ class Api::BaseController < ApplicationController end def require_authenticated_user! - render json: { error: 'This API requires an authenticated user' }, status: 401 unless current_user + render json: { error: 'This method requires an authenticated user' }, status: 401 unless current_user end def require_user! diff --git a/app/controllers/api/v1/announcements/reactions_controller.rb b/app/controllers/api/v1/announcements/reactions_controller.rb new file mode 100644 index 000000000..e4a72e595 --- /dev/null +++ b/app/controllers/api/v1/announcements/reactions_controller.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class Api::V1::Announcements::ReactionsController < Api::BaseController + before_action -> { doorkeeper_authorize! :write, :'write:favourites' } + before_action :require_user! + + before_action :set_announcement + before_action :set_reaction, except: :update + + def update + @announcement.announcement_reactions.create!(account: current_account, name: params[:id]) + render_empty + end + + def destroy + @reaction.destroy! + render_empty + end + + private + + def set_reaction + @reaction = @announcement.announcement_reactions.where(account: current_account).find_by!(name: params[:id]) + end + + def set_announcement + @announcement = Announcement.published.find(params[:announcement_id]) + end +end diff --git a/app/controllers/api/v1/announcements_controller.rb b/app/controllers/api/v1/announcements_controller.rb new file mode 100644 index 000000000..6724fac2e --- /dev/null +++ b/app/controllers/api/v1/announcements_controller.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class Api::V1::AnnouncementsController < Api::BaseController + before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: :dismiss + before_action :require_user! + before_action :set_announcements, only: :index + before_action :set_announcement, except: :index + + def index + render json: @announcements, each_serializer: REST::AnnouncementSerializer + end + + def dismiss + AnnouncementMute.create!(account: current_account, announcement: @announcement) + render_empty + end + + private + + def set_announcements + @announcements = begin + scope = Announcement.published + + scope.merge!(Announcement.without_muted(current_account)) unless truthy_param?(:with_dismissed) + + scope.chronological + end + end + + def set_announcement + @announcement = Announcement.published.find(params[:id]) + end +end diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb index 608a99dd5..6bc75aa56 100644 --- a/app/helpers/admin/action_logs_helper.rb +++ b/app/helpers/admin/action_logs_helper.rb @@ -22,6 +22,8 @@ module Admin::ActionLogsHelper log.recorded_changes.slice('severity', 'reject_media') elsif log.target_type == 'Status' && log.action == :update log.recorded_changes.slice('sensitive') + elsif log.target_type == 'Announcement' && log.action == :update + log.recorded_changes.slice('text', 'starts_at', 'ends_at', 'all_day') end end @@ -52,6 +54,8 @@ module Admin::ActionLogsHelper 'pencil' when 'AccountWarning' 'warning' + when 'Announcement' + 'bullhorn' end end @@ -94,6 +98,8 @@ module Admin::ActionLogsHelper link_to record.account.acct, ActivityPub::TagManager.instance.url_for(record) when 'AccountWarning' link_to record.target_account.acct, admin_account_path(record.target_account_id) + when 'Announcement' + link_to "##{record.id}", edit_admin_announcement_path(record.id) end end @@ -111,6 +117,8 @@ module Admin::ActionLogsHelper else I18n.t('admin.action_logs.deleted_status') end + when 'Announcement' + "##{attributes['id']}" end end end diff --git a/app/helpers/admin/announcements_helper.rb b/app/helpers/admin/announcements_helper.rb new file mode 100644 index 000000000..0c053ddec --- /dev/null +++ b/app/helpers/admin/announcements_helper.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Admin::AnnouncementsHelper + def time_range(announcement) + if announcement.all_day? + safe_join([l(announcement.starts_at.to_date), ' - ', l(announcement.ends_at.to_date)]) + else + safe_join([l(announcement.starts_at), ' - ', l(announcement.ends_at)]) + end + end +end diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb index 130686a02..6ab92939d 100644 --- a/app/helpers/admin/filter_helper.rb +++ b/app/helpers/admin/filter_helper.rb @@ -9,6 +9,7 @@ module Admin::FilterHelper InstanceFilter::KEYS, InviteFilter::KEYS, RelationshipFilter::KEYS, + AnnouncementFilter::KEYS, ].flatten.freeze def filter_link_to(text, link_to_params, link_class_params = link_to_params) diff --git a/app/javascript/images/elephant_ui_plane.svg b/app/javascript/images/elephant_ui_plane.svg index a2624d170..ca675c9eb 100644 --- a/app/javascript/images/elephant_ui_plane.svg +++ b/app/javascript/images/elephant_ui_plane.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/app/javascript/mastodon/actions/announcements.js b/app/javascript/mastodon/actions/announcements.js new file mode 100644 index 000000000..c65bc052e --- /dev/null +++ b/app/javascript/mastodon/actions/announcements.js @@ -0,0 +1,133 @@ +import api from '../api'; +import { normalizeAnnouncement } from './importer/normalizer'; + +export const ANNOUNCEMENTS_FETCH_REQUEST = 'ANNOUNCEMENTS_FETCH_REQUEST'; +export const ANNOUNCEMENTS_FETCH_SUCCESS = 'ANNOUNCEMENTS_FETCH_SUCCESS'; +export const ANNOUNCEMENTS_FETCH_FAIL = 'ANNOUNCEMENTS_FETCH_FAIL'; +export const ANNOUNCEMENTS_UPDATE = 'ANNOUNCEMENTS_UPDATE'; +export const ANNOUNCEMENTS_DISMISS = 'ANNOUNCEMENTS_DISMISS'; + +export const ANNOUNCEMENTS_REACTION_ADD_REQUEST = 'ANNOUNCEMENTS_REACTION_ADD_REQUEST'; +export const ANNOUNCEMENTS_REACTION_ADD_SUCCESS = 'ANNOUNCEMENTS_REACTION_ADD_SUCCESS'; +export const ANNOUNCEMENTS_REACTION_ADD_FAIL = 'ANNOUNCEMENTS_REACTION_ADD_FAIL'; + +export const ANNOUNCEMENTS_REACTION_REMOVE_REQUEST = 'ANNOUNCEMENTS_REACTION_REMOVE_REQUEST'; +export const ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS = 'ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS'; +export const ANNOUNCEMENTS_REACTION_REMOVE_FAIL = 'ANNOUNCEMENTS_REACTION_REMOVE_FAIL'; + +export const ANNOUNCEMENTS_REACTION_UPDATE = 'ANNOUNCEMENTS_REACTION_UPDATE'; + +const noOp = () => {}; + +export const fetchAnnouncements = (done = noOp) => (dispatch, getState) => { + dispatch(fetchAnnouncementsRequest()); + + api(getState).get('/api/v1/announcements').then(response => { + dispatch(fetchAnnouncementsSuccess(response.data.map(x => normalizeAnnouncement(x)))); + }).catch(error => { + dispatch(fetchAnnouncementsFail(error)); + }).finally(() => { + done(); + }); +}; + +export const fetchAnnouncementsRequest = () => ({ + type: ANNOUNCEMENTS_FETCH_REQUEST, + skipLoading: true, +}); + +export const fetchAnnouncementsSuccess = announcements => ({ + type: ANNOUNCEMENTS_FETCH_SUCCESS, + announcements, + skipLoading: true, +}); + +export const fetchAnnouncementsFail= error => ({ + type: ANNOUNCEMENTS_FETCH_FAIL, + error, + skipLoading: true, + skipAlert: true, +}); + +export const updateAnnouncements = announcement => ({ + type: ANNOUNCEMENTS_UPDATE, + announcement: normalizeAnnouncement(announcement), +}); + +export const dismissAnnouncement = announcementId => (dispatch, getState) => { + dispatch({ + type: ANNOUNCEMENTS_DISMISS, + id: announcementId, + }); + + api(getState).post(`/api/v1/announcements/${announcementId}/dismiss`); +}; + +export const addReaction = (announcementId, name) => (dispatch, getState) => { + dispatch(addReactionRequest(announcementId, name)); + + api(getState).put(`/api/v1/announcements/${announcementId}/reactions/${name}`).then(() => { + dispatch(addReactionSuccess(announcementId, name)); + }).catch(err => { + dispatch(addReactionFail(announcementId, name, err)); + }); +}; + +export const addReactionRequest = (announcementId, name) => ({ + type: ANNOUNCEMENTS_REACTION_ADD_REQUEST, + id: announcementId, + name, + skipLoading: true, +}); + +export const addReactionSuccess = (announcementId, name) => ({ + type: ANNOUNCEMENTS_REACTION_ADD_SUCCESS, + id: announcementId, + name, + skipLoading: true, +}); + +export const addReactionFail = (announcementId, name, error) => ({ + type: ANNOUNCEMENTS_REACTION_ADD_FAIL, + id: announcementId, + name, + error, + skipLoading: true, +}); + +export const removeReaction = (announcementId, name) => (dispatch, getState) => { + dispatch(removeReactionRequest(announcementId, name)); + + api(getState).delete(`/api/v1/announcements/${announcementId}/reactions/${name}`).then(() => { + dispatch(removeReactionSuccess(announcementId, name)); + }).catch(err => { + dispatch(removeReactionFail(announcementId, name, err)); + }); +}; + +export const removeReactionRequest = (announcementId, name) => ({ + type: ANNOUNCEMENTS_REACTION_REMOVE_REQUEST, + id: announcementId, + name, + skipLoading: true, +}); + +export const removeReactionSuccess = (announcementId, name) => ({ + type: ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS, + id: announcementId, + name, + skipLoading: true, +}); + +export const removeReactionFail = (announcementId, name, error) => ({ + type: ANNOUNCEMENTS_REACTION_REMOVE_FAIL, + id: announcementId, + name, + error, + skipLoading: true, +}); + +export const updateReaction = reaction => ({ + type: ANNOUNCEMENTS_REACTION_UPDATE, + reaction, +}); diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index 78f321da4..f7cbe4c1c 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -76,7 +76,6 @@ export function normalizeStatus(status, normalOldStatus) { export function normalizePoll(poll) { const normalPoll = { ...poll }; - const emojiMap = makeEmojiMap(normalPoll); normalPoll.options = poll.options.map((option, index) => ({ @@ -87,3 +86,12 @@ export function normalizePoll(poll) { return normalPoll; } + +export function normalizeAnnouncement(announcement) { + const normalAnnouncement = { ...announcement }; + const emojiMap = makeEmojiMap(normalAnnouncement); + + normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap); + + return normalAnnouncement; +} diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 798f9b37e..8a066b896 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -157,9 +157,9 @@ export function expandNotifications({ maxId } = {}, done = noOp) { dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems)); fetchRelatedRelationships(dispatch, response.data); - done(); }).catch(error => { dispatch(expandNotificationsFail(error, isLoadingMore)); + }).finally(() => { done(); }); }; @@ -188,6 +188,7 @@ export function expandNotificationsFail(error, isLoadingMore) { type: NOTIFICATIONS_EXPAND_FAIL, error, skipLoading: !isLoadingMore, + skipAlert: !isLoadingMore, }; }; diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index c678e9393..ac325f74c 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -8,6 +8,7 @@ import { } from './timelines'; import { updateNotifications, expandNotifications } from './notifications'; import { updateConversations } from './conversations'; +import { fetchAnnouncements, updateAnnouncements, updateReaction as updateAnnouncementsReaction } from './announcements'; import { fetchFilters } from './filters'; import { getLocale } from '../locales'; @@ -44,6 +45,12 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null, case 'filters_changed': dispatch(fetchFilters()); break; + case 'announcement': + dispatch(updateAnnouncements(JSON.parse(data.payload))); + break; + case 'announcement.reaction': + dispatch(updateAnnouncementsReaction(JSON.parse(data.payload))); + break; } }, }; @@ -51,7 +58,9 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null, } const refreshHomeTimelineAndNotification = (dispatch, done) => { - dispatch(expandHomeTimeline({}, () => dispatch(expandNotifications({}, done)))); + dispatch(expandHomeTimeline({}, () => + dispatch(expandNotifications({}, () => + dispatch(fetchAnnouncements(done)))))); }; export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification); diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index bc2ac5e82..054668655 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -98,9 +98,9 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedStatuses(response.data)); dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems)); - done(); }).catch(error => { dispatch(expandTimelineFail(timelineId, error, isLoadingMore)); + }).finally(() => { done(); }); }; diff --git a/app/javascript/mastodon/components/error_boundary.js b/app/javascript/mastodon/components/error_boundary.js index 800b1c270..4e1c882e2 100644 --- a/app/javascript/mastodon/components/error_boundary.js +++ b/app/javascript/mastodon/components/error_boundary.js @@ -58,7 +58,7 @@ export default class ErrorBoundary extends React.PureComponent {

-

Mastodon v{version} Β· Β·

+

Mastodon v{version} Β· Β·

); diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js index e57c3c20c..582bb0d39 100644 --- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js @@ -290,6 +290,7 @@ class EmojiPickerDropdown extends React.PureComponent { onPickEmoji: PropTypes.func.isRequired, onSkinTone: PropTypes.func.isRequired, skinTone: PropTypes.number.isRequired, + button: PropTypes.node, }; state = { @@ -350,18 +351,18 @@ class EmojiPickerDropdown extends React.PureComponent { } render () { - const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props; + const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis, button } = this.props; const title = intl.formatMessage(messages.emoji); const { active, loading, placement } = this.state; return (
- πŸ™‚ + />}
diff --git a/app/javascript/mastodon/features/getting_started/components/announcements.js b/app/javascript/mastodon/features/getting_started/components/announcements.js new file mode 100644 index 000000000..ee444e3f0 --- /dev/null +++ b/app/javascript/mastodon/features/getting_started/components/announcements.js @@ -0,0 +1,395 @@ +import React from 'react'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import ReactSwipeableViews from 'react-swipeable-views'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import IconButton from 'mastodon/components/icon_button'; +import Icon from 'mastodon/components/icon'; +import { defineMessages, injectIntl, FormattedMessage, FormattedDate, FormattedNumber } from 'react-intl'; +import { autoPlayGif } from 'mastodon/initial_state'; +import elephantUIPlane from 'mastodon/../images/elephant_ui_plane.svg'; +import { mascot } from 'mastodon/initial_state'; +import unicodeMapping from 'mastodon/features/emoji/emoji_unicode_mapping_light'; +import classNames from 'classnames'; +import EmojiPickerDropdown from 'mastodon/features/compose/containers/emoji_picker_dropdown_container'; + +const messages = defineMessages({ + close: { id: 'lightbox.close', defaultMessage: 'Close' }, + previous: { id: 'lightbox.previous', defaultMessage: 'Previous' }, + next: { id: 'lightbox.next', defaultMessage: 'Next' }, +}); + +class Content extends ImmutablePureComponent { + + static contextTypes = { + router: PropTypes.object, + }; + + static propTypes = { + announcement: ImmutablePropTypes.map.isRequired, + }; + + setRef = c => { + this.node = c; + } + + componentDidMount () { + this._updateLinks(); + this._updateEmojis(); + } + + componentDidUpdate () { + this._updateLinks(); + this._updateEmojis(); + } + + _updateEmojis () { + const node = this.node; + + if (!node || autoPlayGif) { + return; + } + + const emojis = node.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + + if (emoji.classList.contains('status-emoji')) { + continue; + } + + emoji.classList.add('status-emoji'); + + emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); + emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); + } + } + + _updateLinks () { + const node = this.node; + + if (!node) { + return; + } + + const links = node.querySelectorAll('a'); + + for (var i = 0; i < links.length; ++i) { + let link = links[i]; + + if (link.classList.contains('status-link')) { + continue; + } + + link.classList.add('status-link'); + + let mention = this.props.announcement.get('mentions').find(item => link.href === item.get('url')); + + if (mention) { + link.addEventListener('click', this.onMentionClick.bind(this, mention), false); + link.setAttribute('title', mention.get('acct')); + } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) { + link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false); + } else { + link.setAttribute('title', link.href); + link.classList.add('unhandled-link'); + } + + link.setAttribute('target', '_blank'); + link.setAttribute('rel', 'noopener noreferrer'); + } + } + + onMentionClick = (mention, e) => { + if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { + e.preventDefault(); + this.context.router.history.push(`/accounts/${mention.get('id')}`); + } + } + + onHashtagClick = (hashtag, e) => { + hashtag = hashtag.replace(/^#/, ''); + + if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { + e.preventDefault(); + this.context.router.history.push(`/timelines/tag/${hashtag}`); + } + } + + handleEmojiMouseEnter = ({ target }) => { + target.src = target.getAttribute('data-original'); + } + + handleEmojiMouseLeave = ({ target }) => { + target.src = target.getAttribute('data-static'); + } + + render () { + const { announcement } = this.props; + + return ( +
+ ); + } + +} + +const assetHost = process.env.CDN_HOST || ''; + +class Emoji extends React.PureComponent { + + static propTypes = { + emoji: PropTypes.string.isRequired, + emojiMap: ImmutablePropTypes.map.isRequired, + hovered: PropTypes.bool.isRequired, + }; + + render () { + const { emoji, emojiMap, hovered } = this.props; + + if (unicodeMapping[emoji]) { + const { filename, shortCode } = unicodeMapping[this.props.emoji]; + const title = shortCode ? `:${shortCode}:` : ''; + + return ( + {emoji} + ); + } else if (emojiMap.get(emoji)) { + const filename = (autoPlayGif || hovered) ? emojiMap.getIn([emoji, 'url']) : emojiMap.getIn([emoji, 'static_url']); + const shortCode = `:${emoji}:`; + + return ( + {shortCode} + ); + } else { + return null; + } + } + +} + +class Reaction extends ImmutablePureComponent { + + static propTypes = { + announcementId: PropTypes.string.isRequired, + reaction: ImmutablePropTypes.map.isRequired, + addReaction: PropTypes.func.isRequired, + removeReaction: PropTypes.func.isRequired, + emojiMap: ImmutablePropTypes.map.isRequired, + }; + + state = { + hovered: false, + }; + + handleClick = () => { + const { reaction, announcementId, addReaction, removeReaction } = this.props; + + if (reaction.get('me')) { + removeReaction(announcementId, reaction.get('name')); + } else { + addReaction(announcementId, reaction.get('name')); + } + } + + handleMouseEnter = () => this.setState({ hovered: true }) + + handleMouseLeave = () => this.setState({ hovered: false }) + + render () { + const { reaction } = this.props; + + let shortCode = reaction.get('name'); + + if (unicodeMapping[shortCode]) { + shortCode = unicodeMapping[shortCode].shortCode; + } + + return ( + + ); + } + +} + +class ReactionsBar extends ImmutablePureComponent { + + static propTypes = { + announcementId: PropTypes.string.isRequired, + reactions: ImmutablePropTypes.list.isRequired, + addReaction: PropTypes.func.isRequired, + removeReaction: PropTypes.func.isRequired, + emojiMap: ImmutablePropTypes.map.isRequired, + }; + + handleEmojiPick = data => { + const { addReaction, announcementId } = this.props; + addReaction(announcementId, data.native.replace(/:/g, '')); + } + + render () { + const { reactions } = this.props; + const visibleReactions = reactions.filter(x => x.get('count') > 0); + + return ( +
+ {visibleReactions.map(reaction => ( + + ))} + + } /> +
+ ); + } + +} + +class Announcement extends ImmutablePureComponent { + + static propTypes = { + announcement: ImmutablePropTypes.map.isRequired, + emojiMap: ImmutablePropTypes.map.isRequired, + dismissAnnouncement: PropTypes.func.isRequired, + addReaction: PropTypes.func.isRequired, + removeReaction: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + handleDismissClick = () => { + const { dismissAnnouncement, announcement } = this.props; + dismissAnnouncement(announcement.get('id')); + } + + render () { + const { announcement, intl } = this.props; + const startsAt = announcement.get('starts_at') && new Date(announcement.get('starts_at')); + const endsAt = announcement.get('ends_at') && new Date(announcement.get('ends_at')); + const now = new Date(); + const hasTimeRange = startsAt && endsAt; + const skipYear = hasTimeRange && startsAt.getFullYear() === endsAt.getFullYear() && endsAt.getFullYear() === now.getFullYear(); + const skipEndDate = hasTimeRange && startsAt.getDate() === endsAt.getDate() && startsAt.getMonth() === endsAt.getMonth() && startsAt.getFullYear() === endsAt.getFullYear(); + const skipTime = announcement.get('all_day'); + + return ( +
+ + + {hasTimeRange && Β· - } + + + + + + + +
+ ); + } + +} + +export default @injectIntl +class Announcements extends ImmutablePureComponent { + + static propTypes = { + announcements: ImmutablePropTypes.list, + emojiMap: ImmutablePropTypes.map.isRequired, + fetchAnnouncements: PropTypes.func.isRequired, + dismissAnnouncement: PropTypes.func.isRequired, + addReaction: PropTypes.func.isRequired, + removeReaction: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + state = { + index: 0, + }; + + componentDidMount () { + const { fetchAnnouncements } = this.props; + fetchAnnouncements(); + } + + handleChangeIndex = index => { + this.setState({ index: index % this.props.announcements.size }); + } + + handleNextClick = () => { + this.setState({ index: (this.state.index + 1) % this.props.announcements.size }); + } + + handlePrevClick = () => { + this.setState({ index: (this.props.announcements.size + this.state.index - 1) % this.props.announcements.size }); + } + + render () { + const { announcements, intl } = this.props; + const { index } = this.state; + + if (announcements.isEmpty()) { + return null; + } + + return ( +
+ + +
+ + {announcements.map(announcement => ( + + ))} + + +
+ + {index + 1} / {announcements.size} + +
+
+
+ ); + } + +} diff --git a/app/javascript/mastodon/features/getting_started/containers/announcements_container.js b/app/javascript/mastodon/features/getting_started/containers/announcements_container.js new file mode 100644 index 000000000..b10d1d4ce --- /dev/null +++ b/app/javascript/mastodon/features/getting_started/containers/announcements_container.js @@ -0,0 +1,21 @@ +import { connect } from 'react-redux'; +import { fetchAnnouncements, dismissAnnouncement, addReaction, removeReaction } from 'mastodon/actions/announcements'; +import Announcements from '../components/announcements'; +import { createSelector } from 'reselect'; +import { Map as ImmutableMap } from 'immutable'; + +const customEmojiMap = createSelector([state => state.get('custom_emojis')], items => items.reduce((map, emoji) => map.set(emoji.get('shortcode'), emoji), ImmutableMap())); + +const mapStateToProps = state => ({ + announcements: state.getIn(['announcements', 'items']), + emojiMap: customEmojiMap(state), +}); + +const mapDispatchToProps = dispatch => ({ + fetchAnnouncements: () => dispatch(fetchAnnouncements()), + dismissAnnouncement: id => dispatch(dismissAnnouncement(id)), + addReaction: (id, name) => dispatch(addReaction(id, name)), + removeReaction: (id, name) => dispatch(removeReaction(id, name)), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(Announcements); diff --git a/app/javascript/mastodon/features/getting_started/containers/trends_container.js b/app/javascript/mastodon/features/getting_started/containers/trends_container.js index 1df3fb4fe..7a5268780 100644 --- a/app/javascript/mastodon/features/getting_started/containers/trends_container.js +++ b/app/javascript/mastodon/features/getting_started/containers/trends_container.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { fetchTrends } from '../../../actions/trends'; +import { fetchTrends } from 'mastodon/actions/trends'; import Trends from '../components/trends'; const mapStateToProps = state => ({ diff --git a/app/javascript/mastodon/features/home_timeline/index.js b/app/javascript/mastodon/features/home_timeline/index.js index 1cafb88ed..b7f9d5095 100644 --- a/app/javascript/mastodon/features/home_timeline/index.js +++ b/app/javascript/mastodon/features/home_timeline/index.js @@ -9,6 +9,7 @@ import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ColumnSettingsContainer from './containers/column_settings_container'; import { Link } from 'react-router-dom'; +import AnnouncementsContainer from 'mastodon/features/getting_started/containers/announcements_container'; const messages = defineMessages({ title: { id: 'column.home', defaultMessage: 'Home' }, @@ -113,6 +114,8 @@ class HomeTimeline extends React.PureComponent { } + alwaysPrepend trackScroll={!pinned} scrollKey={`home_timeline-${columnId}`} onLoadMore={this.handleLoadMore} diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js index a785551c0..d7f97f210 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.js +++ b/app/javascript/mastodon/features/ui/components/media_modal.js @@ -211,7 +211,6 @@ class MediaModal extends ImmutablePureComponent { style={swipeableViewsStyle} containerStyle={containerStyle} onChangeIndex={this.handleSwipe} - onSwitching={this.handleSwitching} index={index} > {content} diff --git a/app/javascript/mastodon/reducers/announcements.js b/app/javascript/mastodon/reducers/announcements.js new file mode 100644 index 000000000..aa674e516 --- /dev/null +++ b/app/javascript/mastodon/reducers/announcements.js @@ -0,0 +1,72 @@ +import { + ANNOUNCEMENTS_FETCH_REQUEST, + ANNOUNCEMENTS_FETCH_SUCCESS, + ANNOUNCEMENTS_FETCH_FAIL, + ANNOUNCEMENTS_UPDATE, + ANNOUNCEMENTS_DISMISS, + ANNOUNCEMENTS_REACTION_UPDATE, + ANNOUNCEMENTS_REACTION_ADD_REQUEST, + ANNOUNCEMENTS_REACTION_ADD_FAIL, + ANNOUNCEMENTS_REACTION_REMOVE_REQUEST, + ANNOUNCEMENTS_REACTION_REMOVE_FAIL, +} from '../actions/announcements'; +import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; + +const initialState = ImmutableMap({ + items: ImmutableList(), + isLoading: false, +}); + +const updateReaction = (state, id, name, updater) => state.update('items', list => list.map(announcement => { + if (announcement.get('id') === id) { + return announcement.update('reactions', reactions => { + if (reactions.find(reaction => reaction.get('name') === name)) { + return reactions.map(reaction => { + if (reaction.get('name') === name) { + return updater(reaction); + } + + return reaction; + }); + } + + return reactions.push(updater(fromJS({ name, count: 0 }))); + }); + } + + return announcement; +})); + +const updateReactionCount = (state, reaction) => updateReaction(state, reaction.announcement_id, reaction.name, x => x.set('count', reaction.count)); + +const addReaction = (state, id, name) => updateReaction(state, id, name, x => x.set('me', true).update('count', y => y + 1)); + +const removeReaction = (state, id, name) => updateReaction(state, id, name, x => x.set('me', false).update('count', y => y - 1)); + +export default function announcementsReducer(state = initialState, action) { + switch(action.type) { + case ANNOUNCEMENTS_FETCH_REQUEST: + return state.set('isLoading', true); + case ANNOUNCEMENTS_FETCH_SUCCESS: + return state.withMutations(map => { + map.set('items', fromJS(action.announcements)); + map.set('isLoading', false); + }); + case ANNOUNCEMENTS_FETCH_FAIL: + return state.set('isLoading', false); + case ANNOUNCEMENTS_UPDATE: + return state.update('items', list => list.unshift(fromJS(action.announcement)).sortBy(announcement => announcement.get('starts_at'))); + case ANNOUNCEMENTS_DISMISS: + return state.update('items', list => list.filterNot(announcement => announcement.get('id') === action.id)); + case ANNOUNCEMENTS_REACTION_UPDATE: + return updateReactionCount(state, action.reaction); + case ANNOUNCEMENTS_REACTION_ADD_REQUEST: + case ANNOUNCEMENTS_REACTION_REMOVE_FAIL: + return addReaction(state, action.id, action.name); + case ANNOUNCEMENTS_REACTION_REMOVE_REQUEST: + case ANNOUNCEMENTS_REACTION_ADD_FAIL: + return removeReaction(state, action.id, action.name); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index b8d608888..b9817cd38 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -34,8 +34,10 @@ import polls from './polls'; import identity_proofs from './identity_proofs'; import trends from './trends'; import missed_updates from './missed_updates'; +import announcements from './announcements'; const reducers = { + announcements, dropdown_menu, timelines, meta, diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 94671c350..922d48ad7 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -859,6 +859,44 @@ } } +.announcements__item__content { + word-wrap: break-word; + + .emojione { + width: 20px; + height: 20px; + margin: -3px 0 0; + } + + p { + margin-bottom: 10px; + white-space: pre-wrap; + + &:last-child { + margin-bottom: 0; + } + } + + a { + color: $highlight-text-color; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + + &.mention { + &:hover { + text-decoration: none; + + span { + text-decoration: underline; + } + } + } + } +} + .status__content.status__content--collapsed { max-height: 20px * 15; // 15 lines is roughly above 500 characters } @@ -6581,3 +6619,178 @@ noscript { } } } + +.announcements { + background: lighten($ui-base-color, 4%); + border-top: 1px solid $ui-base-color; + font-size: 13px; + display: flex; + align-items: flex-end; + + &__mastodon { + width: 124px; + flex: 0 0 auto; + + @media screen and (max-width: 124px + 300px) { + display: none; + } + } + + &__container { + width: calc(100% - 124px); + flex: 0 0 auto; + position: relative; + + @media screen and (max-width: 124px + 300px) { + width: 100%; + } + } + + &__item { + box-sizing: border-box; + width: 100%; + padding: 15px; + padding-right: 15px + 18px; + position: relative; + + &__range { + display: block; + font-weight: 500; + margin-bottom: 10px; + } + + &__dismiss-icon { + position: absolute; + top: 12px; + right: 12px; + } + } + + &__pagination { + padding: 15px; + color: $darker-text-color; + position: absolute; + bottom: 3px; + right: 0; + } +} + +.layout-multiple-columns .announcements__mastodon { + display: none; +} + +.layout-multiple-columns .announcements__container { + width: 100%; +} + +.reactions-bar { + display: flex; + flex-wrap: wrap; + align-items: center; + margin-top: 15px; + margin-left: -2px; + width: calc(100% - (90px - 33px)); + + &__item { + flex-shrink: 0; + background: lighten($ui-base-color, 12%); + border: 0; + border-radius: 3px; + margin: 2px; + cursor: pointer; + user-select: none; + padding: 0 6px; + display: flex; + align-items: center; + transition: all 100ms ease-in; + transition-property: background-color, color; + + &__emoji { + display: block; + margin: 3px 0; + width: 16px; + height: 16px; + + img { + display: block; + margin: 0; + width: 100%; + height: 100%; + min-width: auto; + min-height: auto; + vertical-align: bottom; + object-fit: contain; + } + } + + &__count { + display: block; + min-width: 9px; + font-size: 13px; + font-weight: 500; + text-align: center; + margin-left: 6px; + color: $darker-text-color; + } + + &:hover, + &:focus, + &:active { + background: lighten($ui-base-color, 16%); + transition: all 200ms ease-out; + transition-property: background-color, color; + + &__count { + color: lighten($darker-text-color, 4%); + } + } + + &.active { + transition: all 100ms ease-in; + transition-property: background-color, color; + background-color: mix(lighten($ui-base-color, 12%), $ui-highlight-color, 90%); + + .reactions-bar__item__count { + color: $highlight-text-color; + } + } + } + + .emoji-picker-dropdown { + margin: 2px; + } + + &:hover .emoji-button { + opacity: 0.85; + } + + .emoji-button { + color: $darker-text-color; + margin: 0; + font-size: 16px; + width: auto; + flex-shrink: 0; + padding: 0 6px; + height: 22px; + display: flex; + align-items: center; + opacity: 0.5; + transition: all 100ms ease-in; + transition-property: background-color, color; + + &:hover, + &:active, + &:focus { + opacity: 1; + color: lighten($darker-text-color, 4%); + transition: all 200ms ease-out; + transition-property: background-color, color; + } + } + + &--empty { + .emoji-button { + padding: 0; + } + } +} diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index 8965ce675..65cefbd7c 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -222,6 +222,12 @@ code { } } + .input.datetime .label_input select { + display: inline-block; + width: auto; + flex: 0; + } + .required abbr { text-decoration: none; color: lighten($error-value-color, 12%); diff --git a/app/lib/entity_cache.rb b/app/lib/entity_cache.rb index 8fff544a0..35a3773d2 100644 --- a/app/lib/entity_cache.rb +++ b/app/lib/entity_cache.rb @@ -8,7 +8,7 @@ class EntityCache MAX_EXPIRATION = 7.days.freeze def mention(username, domain) - Rails.cache.fetch(to_key(:mention, username, domain), expires_in: MAX_EXPIRATION) { Account.select(:username, :domain, :url).find_remote(username, domain) } + Rails.cache.fetch(to_key(:mention, username, domain), expires_in: MAX_EXPIRATION) { Account.select(:id, :username, :domain, :url).find_remote(username, domain) } end def emoji(shortcodes, domain) diff --git a/app/lib/inline_renderer.rb b/app/lib/inline_renderer.rb index 761a8822d..27e334a4d 100644 --- a/app/lib/inline_renderer.rb +++ b/app/lib/inline_renderer.rb @@ -15,6 +15,10 @@ class InlineRenderer serializer = REST::NotificationSerializer when :conversation serializer = REST::ConversationSerializer + when :announcement + serializer = REST::AnnouncementSerializer + when :reaction + serializer = REST::ReactionSerializer else return end diff --git a/app/models/account.rb b/app/models/account.rb index 1e8abe6ec..da6f51a9c 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -476,6 +476,12 @@ class Account < ApplicationRecord records end + def from_text(text) + return [] if text.blank? + + text.scan(MENTION_RE).map { |match| match.first.split('@', 2) }.uniq.map { |(username, domain)| EntityCache.instance.mention(username, domain) } + end + private def generate_query_for_search(terms) diff --git a/app/models/announcement.rb b/app/models/announcement.rb new file mode 100644 index 000000000..4da9f94d6 --- /dev/null +++ b/app/models/announcement.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: announcements +# +# id :bigint(8) not null, primary key +# text :text default(""), not null +# published :boolean default(FALSE), not null +# all_day :boolean default(FALSE), not null +# scheduled_at :datetime +# starts_at :datetime +# ends_at :datetime +# created_at :datetime not null +# updated_at :datetime not null +# + +class Announcement < ApplicationRecord + after_commit :queue_publish, on: :create + + scope :unpublished, -> { where(published: false) } + scope :published, -> { where(published: true) } + scope :without_muted, ->(account) { joins("LEFT OUTER JOIN announcement_mutes ON announcement_mutes.announcement_id = announcements.id AND announcement_mutes.account_id = #{account.id}").where('announcement_mutes.id IS NULL') } + scope :chronological, -> { order(Arel.sql('COALESCE(announcements.starts_at, announcements.scheduled_at, announcements.created_at) ASC')) } + + has_many :announcement_mutes, dependent: :destroy + has_many :announcement_reactions, dependent: :destroy + + validates :text, presence: true + validates :starts_at, presence: true, if: -> { ends_at.present? } + validates :ends_at, presence: true, if: -> { starts_at.present? } + + before_validation :set_all_day + before_validation :set_starts_at, on: :create + before_validation :set_ends_at, on: :create + + def time_range? + starts_at.present? && ends_at.present? + end + + def mentions + @mentions ||= Account.from_text(text) + end + + def tags + @tags ||= Tag.find_or_create_by_names(Extractor.extract_hashtags(text)) + end + + def emojis + @emojis ||= CustomEmoji.from_text(text) + end + + def reactions(account = nil) + records = begin + scope = announcement_reactions.group(:announcement_id, :name, :custom_emoji_id).order(Arel.sql('MIN(created_at) ASC')) + + if account.nil? + scope.select('name, custom_emoji_id, count(*) as count, false as me') + else + scope.select("name, custom_emoji_id, count(*) as count, exists(select 1 from announcement_reactions r where r.account_id = #{account.id} and r.announcement_id = announcement_reactions.announcement_id and r.name = announcement_reactions.name) as me") + end + end + + ActiveRecord::Associations::Preloader.new.preload(records, :custom_emoji) + records + end + + private + + def set_all_day + self.all_day = false if starts_at.blank? || ends_at.blank? + end + + def set_starts_at + self.starts_at = starts_at.change(hour: 0, min: 0, sec: 0) if all_day? && starts_at.present? + end + + def set_ends_at + self.ends_at = ends_at.change(hour: 23, min: 59, sec: 59) if all_day? && ends_at.present? + end + + def queue_publish + PublishScheduledAnnouncementWorker.perform_async(id) if scheduled_at.blank? + end +end diff --git a/app/models/announcement_filter.rb b/app/models/announcement_filter.rb new file mode 100644 index 000000000..950852460 --- /dev/null +++ b/app/models/announcement_filter.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class AnnouncementFilter + KEYS = %i( + published + unpublished + ).freeze + + attr_reader :params + + def initialize(params) + @params = params + end + + def results + scope = Announcement.unscoped + + params.each do |key, value| + next if key.to_s == 'page' + + scope.merge!(scope_for(key, value.to_s.strip)) if value.present? + end + + scope.chronological + end + + private + + def scope_for(key, _value) + case key.to_s + when 'published' + Announcement.published + when 'unpublished' + Announcement.unpublished + else + raise "Unknown filter: #{key}" + end + end +end diff --git a/app/models/announcement_mute.rb b/app/models/announcement_mute.rb new file mode 100644 index 000000000..46fda2f5d --- /dev/null +++ b/app/models/announcement_mute.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: announcement_mutes +# +# id :bigint(8) not null, primary key +# account_id :bigint(8) +# announcement_id :bigint(8) +# created_at :datetime not null +# updated_at :datetime not null +# + +class AnnouncementMute < ApplicationRecord + belongs_to :account + belongs_to :announcement, inverse_of: :announcement_mutes + + validates :account_id, uniqueness: { scope: :announcement_id } +end diff --git a/app/models/announcement_reaction.rb b/app/models/announcement_reaction.rb new file mode 100644 index 000000000..d22771034 --- /dev/null +++ b/app/models/announcement_reaction.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: announcement_reactions +# +# id :bigint(8) not null, primary key +# account_id :bigint(8) +# announcement_id :bigint(8) +# name :string default(""), not null +# custom_emoji_id :bigint(8) +# created_at :datetime not null +# updated_at :datetime not null +# + +class AnnouncementReaction < ApplicationRecord + after_commit :queue_publish + + belongs_to :account + belongs_to :announcement, inverse_of: :announcement_reactions + belongs_to :custom_emoji, optional: true + + validates :name, presence: true + validates_with ReactionValidator + + before_validation :set_custom_emoji + + private + + def set_custom_emoji + self.custom_emoji = CustomEmoji.local.find_by(disabled: false, shortcode: name) if name.present? + end + + def queue_publish + PublishAnnouncementReactionWorker.perform_async(announcement_id, name) unless announcement.destroyed? + end +end diff --git a/app/models/backup.rb b/app/models/backup.rb index 8eeb1748a..d242fd62c 100644 --- a/app/models/backup.rb +++ b/app/models/backup.rb @@ -7,11 +7,11 @@ # user_id :bigint(8) # dump_file_name :string # dump_content_type :string -# dump_file_size :bigint # dump_updated_at :datetime # processed :boolean default(FALSE), not null # created_at :datetime not null # updated_at :datetime not null +# dump_file_size :bigint(8) # class Backup < ApplicationRecord diff --git a/app/models/bookmark.rb b/app/models/bookmark.rb index 01dc48ee7..916261a17 100644 --- a/app/models/bookmark.rb +++ b/app/models/bookmark.rb @@ -3,11 +3,11 @@ # # Table name: bookmarks # -# id :integer not null, primary key +# id :bigint(8) not null, primary key +# account_id :bigint(8) not null +# status_id :bigint(8) not null # created_at :datetime not null # updated_at :datetime not null -# account_id :integer not null -# status_id :integer not null # class Bookmark < ApplicationRecord diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index f27d39483..14bcf7bb1 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -84,6 +84,7 @@ module AccountInteractions has_many :muted_by, -> { order('mutes.id desc') }, through: :muted_by_relationships, source: :account has_many :conversation_mutes, dependent: :destroy has_many :domain_blocks, class_name: 'AccountDomainBlock', dependent: :destroy + has_many :announcement_mutes, dependent: :destroy end def follow!(other_account, reblogs: nil, uri: nil) diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index 0dacaf654..d177cf281 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -67,7 +67,7 @@ class CustomEmoji < ApplicationRecord end class << self - def from_text(text, domain) + def from_text(text, domain = nil) return [] if text.blank? shortcodes = text.scan(SCAN_RE).map(&:first).uniq diff --git a/app/policies/announcement_policy.rb b/app/policies/announcement_policy.rb new file mode 100644 index 000000000..0a4e4575c --- /dev/null +++ b/app/policies/announcement_policy.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AnnouncementPolicy < ApplicationPolicy + def index? + staff? + end + + def create? + admin? + end + + def update? + admin? + end + + def destroy? + admin? + end +end diff --git a/app/serializers/rest/announcement_serializer.rb b/app/serializers/rest/announcement_serializer.rb new file mode 100644 index 000000000..924d87b34 --- /dev/null +++ b/app/serializers/rest/announcement_serializer.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class REST::AnnouncementSerializer < ActiveModel::Serializer + attributes :id, :content, :starts_at, :ends_at, :all_day + + has_many :mentions + has_many :tags, serializer: REST::StatusSerializer::TagSerializer + has_many :emojis, serializer: REST::CustomEmojiSerializer + has_many :reactions, serializer: REST::ReactionSerializer + + def id + object.id.to_s + end + + def content + Formatter.instance.linkify(object.text) + end + + def reactions + object.reactions(current_user&.account) + end + + class AccountSerializer < ActiveModel::Serializer + attributes :id, :username, :url, :acct + + def id + object.id.to_s + end + + def url + ActivityPub::TagManager.instance.url_for(object) + end + end +end diff --git a/app/serializers/rest/reaction_serializer.rb b/app/serializers/rest/reaction_serializer.rb new file mode 100644 index 000000000..1a5dca018 --- /dev/null +++ b/app/serializers/rest/reaction_serializer.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class REST::ReactionSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :name, :count + + attribute :me, if: :current_user? + attribute :url, if: :custom_emoji? + attribute :static_url, if: :custom_emoji? + + def count + object.respond_to?(:count) ? object.count : 0 + end + + def current_user? + !current_user.nil? + end + + def custom_emoji? + object.custom_emoji.present? + end + + def url + full_asset_url(object.custom_emoji.image.url) + end + + def static_url + full_asset_url(object.custom_emoji.image.url(:static)) + end +end diff --git a/app/validators/reaction_validator.rb b/app/validators/reaction_validator.rb new file mode 100644 index 000000000..de0f2c94b --- /dev/null +++ b/app/validators/reaction_validator.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class ReactionValidator < ActiveModel::Validator + SUPPORTED_EMOJIS = Oj.load(File.read(Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_map.json'))).keys.freeze + + def validate(reaction) + return if reaction.name.blank? || reaction.custom_emoji_id.present? + + reaction.errors.add(:name, I18n.t('reactions.errors.unrecognized_emoji')) unless unicode_emoji?(reaction.name) + end + + private + + def unicode_emoji?(name) + SUPPORTED_EMOJIS.include?(name) + end +end diff --git a/app/views/admin/announcements/_announcement.html.haml b/app/views/admin/announcements/_announcement.html.haml new file mode 100644 index 000000000..75768c7ba --- /dev/null +++ b/app/views/admin/announcements/_announcement.html.haml @@ -0,0 +1,14 @@ +%tr + %td + = truncate(announcement.text) + %td + = time_range(announcement) if announcement.time_range? + %td + - if announcement.scheduled_at.present? + = fa_icon('clock-o') if announcement.scheduled_at > Time.now.utc + = l(announcement.scheduled_at) + - else + = l(announcement.created_at) + %td + = table_link_to 'pencil', t('generic.edit'), edit_admin_announcement_path(announcement) if can?(:update, announcement) + = table_link_to 'trash', t('generic.delete'), admin_announcement_path(announcement), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:destroy, announcement) diff --git a/app/views/admin/announcements/edit.html.haml b/app/views/admin/announcements/edit.html.haml new file mode 100644 index 000000000..c5c605e93 --- /dev/null +++ b/app/views/admin/announcements/edit.html.haml @@ -0,0 +1,22 @@ +- content_for :page_title do + = t('.title') + += simple_form_for @announcement, url: admin_announcement_path(@announcement) do |f| + = render 'shared/error_messages', object: @announcement + + .fields-group + = f.input :starts_at, include_blank: true, wrapper: :with_block_label + = f.input :ends_at, include_blank: true, wrapper: :with_block_label + + .fields-group + = f.input :all_day, as: :boolean, wrapper: :with_label + + .fields-group + = f.input :text, wrapper: :with_block_label + + - if @announcement.scheduled_at.present? && !@announcement.published? + .fields-group + = f.input :scheduled_at, include_blank: true, wrapper: :with_block_label + + .actions + = f.button :button, t('generic.save_changes'), type: :submit diff --git a/app/views/admin/announcements/index.html.haml b/app/views/admin/announcements/index.html.haml new file mode 100644 index 000000000..634f586fb --- /dev/null +++ b/app/views/admin/announcements/index.html.haml @@ -0,0 +1,30 @@ +- content_for :page_title do + = t('admin.announcements.title') + +- content_for :heading_actions do + = link_to t('admin.announcements.new.title'), new_admin_announcement_path, class: 'button' + +.filters + .filter-subset + %strong= t('admin.relays.status') + %ul + %li= filter_link_to t('generic.all'), published: nil, unpublished: nil + %li= filter_link_to safe_join([t('admin.announcements.live'), "(#{number_with_delimiter(Announcement.published.count)})"], ' '), published: '1', unpublished: nil + +- if @announcements.empty? + %div.muted-hint.center-text + = t 'admin.announcements.empty' +- else + .table-wrapper + %table.table + %thead + %tr + %th= t('simple_form.labels.announcement.text') + %th= t('admin.announcements.time_range') + %th= t('admin.announcements.published') + %th + %tbody + = render partial: 'announcement', collection: @announcements + += paginate @announcements + diff --git a/app/views/admin/announcements/new.html.haml b/app/views/admin/announcements/new.html.haml new file mode 100644 index 000000000..a5298c5f6 --- /dev/null +++ b/app/views/admin/announcements/new.html.haml @@ -0,0 +1,21 @@ +- content_for :page_title do + = t('.title') + += simple_form_for @announcement, url: admin_announcements_path do |f| + = render 'shared/error_messages', object: @announcement + + .fields-group + = f.input :starts_at, include_blank: true, wrapper: :with_block_label + = f.input :ends_at, include_blank: true, wrapper: :with_block_label + + .fields-group + = f.input :all_day, as: :boolean, wrapper: :with_label + + .fields-group + = f.input :text, wrapper: :with_block_label + + .fields-group + = f.input :scheduled_at, include_blank: true, wrapper: :with_block_label + + .actions + = f.button :button, t('.create'), type: :submit diff --git a/app/workers/publish_announcement_reaction_worker.rb b/app/workers/publish_announcement_reaction_worker.rb new file mode 100644 index 000000000..6f3b6dc5b --- /dev/null +++ b/app/workers/publish_announcement_reaction_worker.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class PublishAnnouncementReactionWorker + include Sidekiq::Worker + include Redisable + + def perform(announcement_id, name) + announcement = Announcement.find(announcement_id) + + reaction, = announcement.announcement_reactions.where(name: name).group(:announcement_id, :name, :custom_emoji_id).select('name, custom_emoji_id, count(*) as count, false as me') + reaction ||= announcement.announcement_reactions.new(name: name) + + payload = InlineRenderer.render(reaction, nil, :reaction).tap { |h| h[:announcement_id] = announcement_id } + payload = Oj.dump(event: :'announcement.reaction', payload: payload) + + Account.joins(:user).where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago).find_each do |account| + redis.publish("timeline:#{account.id}", payload) if redis.exists("subscribed:timeline:#{account.id}") + end + rescue ActiveRecord::RecordNotFound + true + end +end diff --git a/app/workers/publish_scheduled_announcement_worker.rb b/app/workers/publish_scheduled_announcement_worker.rb new file mode 100644 index 000000000..4b2014e34 --- /dev/null +++ b/app/workers/publish_scheduled_announcement_worker.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class PublishScheduledAnnouncementWorker + include Sidekiq::Worker + include Redisable + + def perform(announcement_id) + announcement = Announcement.find(announcement_id) + announcement.update(published: true) + + payload = InlineRenderer.render(announcement, nil, :announcement) + payload = Oj.dump(event: :announcement, payload: payload) + + Account.joins(:user).where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago).find_each do |account| + redis.publish("timeline:#{account.id}", payload) if redis.exists("subscribed:timeline:#{account.id}") + end + end +end diff --git a/app/workers/scheduler/scheduled_statuses_scheduler.rb b/app/workers/scheduler/scheduled_statuses_scheduler.rb index 1772a246b..4262f1d01 100644 --- a/app/workers/scheduler/scheduled_statuses_scheduler.rb +++ b/app/workers/scheduler/scheduled_statuses_scheduler.rb @@ -6,14 +6,38 @@ class Scheduler::ScheduledStatusesScheduler sidekiq_options unique: :until_executed, retry: 0 def perform + publish_scheduled_statuses! + publish_scheduled_announcements! + unpublish_expired_announcements! + end + + private + + def publish_scheduled_statuses! due_statuses.find_each do |scheduled_status| PublishScheduledStatusWorker.perform_at(scheduled_status.scheduled_at, scheduled_status.id) end end - private - def due_statuses ScheduledStatus.where('scheduled_at <= ?', Time.now.utc + PostStatusService::MIN_SCHEDULE_OFFSET) end + + def publish_scheduled_announcements! + due_announcements.find_each do |announcement| + PublishScheduledAnnouncementWorker.perform_at(announcement.scheduled_at, announcement.id) + end + end + + def due_announcements + Announcement.unpublished.where('scheduled_at IS NOT NULL AND scheduled_at <= ?', Time.now.utc + PostStatusService::MIN_SCHEDULE_OFFSET) + end + + def unpublish_expired_announcements! + expired_announcements.in_batches.update_all(published: false) + end + + def expired_announcements + Announcement.published.where('ends_at IS NOT NULL AND ends_at <= ?', Time.now.utc) + end end diff --git a/config/initializers/simple_form.rb b/config/initializers/simple_form.rb index 964526819..3dc48ef08 100644 --- a/config/initializers/simple_form.rb +++ b/config/initializers/simple_form.rb @@ -98,7 +98,7 @@ SimpleForm.setup do |config| b.use :html5 b.use :label b.use :hint, wrap_with: { tag: :span, class: :hint } - b.use :input + b.use :input, wrap_with: { tag: :div, class: :label_input } b.use :error, wrap_with: { tag: :span, class: :error } end diff --git a/config/locales/en.yml b/config/locales/en.yml index 2bd84c264..c4e846354 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -198,11 +198,13 @@ en: change_email_user: "%{name} changed the e-mail address of user %{target}" confirm_user: "%{name} confirmed e-mail address of user %{target}" create_account_warning: "%{name} sent a warning to %{target}" + create_announcement: "%{name} created new announcement %{target}" create_custom_emoji: "%{name} uploaded new emoji %{target}" create_domain_allow: "%{name} whitelisted domain %{target}" create_domain_block: "%{name} blocked domain %{target}" create_email_domain_block: "%{name} blacklisted e-mail domain %{target}" demote_user: "%{name} demoted user %{target}" + destroy_announcement: "%{name} deleted announcement %{target}" destroy_custom_emoji: "%{name} destroyed emoji %{target}" destroy_domain_allow: "%{name} removed domain %{target} from whitelist" destroy_domain_block: "%{name} unblocked domain %{target}" @@ -224,10 +226,22 @@ en: unassigned_report: "%{name} unassigned report %{target}" unsilence_account: "%{name} unsilenced %{target}'s account" unsuspend_account: "%{name} unsuspended %{target}'s account" + update_announcement: "%{name} updated announcement %{target}" update_custom_emoji: "%{name} updated emoji %{target}" update_status: "%{name} updated status by %{target}" deleted_status: "(deleted status)" title: Audit log + announcements: + edit: + title: Edit announcement + empty: No announcements found. + live: Live + new: + create: Create announcement + title: New announcement + published: Published + time_range: Time range + title: Announcements custom_emojis: assign_category: Assign category by_domain: Domain @@ -657,6 +671,9 @@ en: hint_html: "Tip: We won't ask you for your password again for the next hour." invalid_password: Invalid password prompt: Confirm password to continue + date: + formats: + default: "%b %d, %Y" datetime: distance_in_words: about_x_hours: "%{count}h" @@ -758,6 +775,8 @@ en: all: All changes_saved_msg: Changes successfully saved! copy: Copy + delete: Delete + edit: Edit no_batch_actions_available: No batch actions available on this page order_by: Order by save_changes: Save changes @@ -930,6 +949,9 @@ en: other: Other posting_defaults: Posting defaults public_timelines: Public timelines + reactions: + errors: + unrecognized_emoji: is not a recognized emoji relationships: activity: Account activity dormant: Dormant diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 66f518c1b..f050ec8a3 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -14,6 +14,12 @@ en: text_html: Optional. You can use toot syntax. You can add warning presets to save time type_html: Choose what to do with %{acct} warning_preset_id: Optional. You can still add custom text to end of the preset + announcement: + all_day: When checked, only the dates of the time range will be displayed + ends_at: Optional. Announcement will be automatically unpublished at this time + scheduled_at: Leave blank to publish the announcement immediately + starts_at: Optional. In case your announcement is bound to a specific time range + text: You can use toot syntax. Please be mindful of the space the announcement will take up on the user's screen defaults: autofollow: People who sign up through the invite will automatically follow you avatar: PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px @@ -83,6 +89,12 @@ en: silence: Silence suspend: Suspend and irreversibly delete account data warning_preset_id: Use a warning preset + announcement: + all_day: All-day event + ends_at: End of event + scheduled_at: Schedule publication + starts_at: Begin of event + text: Announcement defaults: autofollow: Invite to follow your account avatar: Avatar diff --git a/config/navigation.rb b/config/navigation.rb index eebd4f75e..8fd296d5a 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -46,6 +46,7 @@ SimpleNavigation::Configuration.run do |navigation| n.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), admin_dashboard_url, if: proc { current_user.staff? } do |s| s.item :dashboard, safe_join([fa_icon('tachometer fw'), t('admin.dashboard.title')]), admin_dashboard_url s.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), edit_admin_settings_url, if: -> { current_user.admin? }, highlights_on: %r{/admin/settings} + s.item :announcements, safe_join([fa_icon('bullhorn fw'), t('admin.announcements.title')]), admin_announcements_path, highlights_on: %r{/admin/announcements} s.item :custom_emojis, safe_join([fa_icon('smile-o fw'), t('admin.custom_emojis.title')]), admin_custom_emojis_url, highlights_on: %r{/admin/custom_emojis} s.item :relays, safe_join([fa_icon('exchange fw'), t('admin.relays.title')]), admin_relays_url, if: -> { current_user.admin? && !whitelist_mode? }, highlights_on: %r{/admin/relays} s.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url, link_html: { target: 'sidekiq' }, if: -> { current_user.admin? } diff --git a/config/routes.rb b/config/routes.rb index f79af192d..da7bf6f88 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -173,9 +173,12 @@ Rails.application.routes.draw do get :edit end end + resources :email_domain_blocks, only: [:index, :new, :create, :destroy] resources :action_logs, only: [:index] resources :warning_presets, except: [:new] + resources :announcements, except: [:show] + resource :settings, only: [:edit, :update] resources :invites, only: [:index, :create, :destroy] do @@ -317,6 +320,16 @@ Rails.application.routes.draw do resources :scheduled_statuses, only: [:index, :show, :update, :destroy] resources :preferences, only: [:index] + resources :announcements, only: [:index] do + scope module: :announcements do + resources :reactions, only: [:update, :destroy] + end + + member do + post :dismiss + end + end + resources :conversations, only: [:index, :destroy] do member do post :read diff --git a/db/migrate/20191218153258_create_announcements.rb b/db/migrate/20191218153258_create_announcements.rb new file mode 100644 index 000000000..58e143c92 --- /dev/null +++ b/db/migrate/20191218153258_create_announcements.rb @@ -0,0 +1,16 @@ +class CreateAnnouncements < ActiveRecord::Migration[5.2] + def change + create_table :announcements do |t| + t.text :text, null: false, default: '' + + t.boolean :published, null: false, default: false + t.boolean :all_day, null: false, default: false + + t.datetime :scheduled_at + t.datetime :starts_at + t.datetime :ends_at + + t.timestamps + end + end +end diff --git a/db/migrate/20200113125135_create_announcement_mutes.rb b/db/migrate/20200113125135_create_announcement_mutes.rb new file mode 100644 index 000000000..c588e7fcd --- /dev/null +++ b/db/migrate/20200113125135_create_announcement_mutes.rb @@ -0,0 +1,12 @@ +class CreateAnnouncementMutes < ActiveRecord::Migration[5.2] + def change + create_table :announcement_mutes do |t| + t.belongs_to :account, foreign_key: { on_delete: :cascade, index: false } + t.belongs_to :announcement, foreign_key: { on_delete: :cascade } + + t.timestamps + end + + add_index :announcement_mutes, [:account_id, :announcement_id], unique: true + end +end diff --git a/db/migrate/20200114113335_create_announcement_reactions.rb b/db/migrate/20200114113335_create_announcement_reactions.rb new file mode 100644 index 000000000..226c81a18 --- /dev/null +++ b/db/migrate/20200114113335_create_announcement_reactions.rb @@ -0,0 +1,15 @@ +class CreateAnnouncementReactions < ActiveRecord::Migration[5.2] + def change + create_table :announcement_reactions do |t| + t.belongs_to :account, foreign_key: { on_delete: :cascade, index: false } + t.belongs_to :announcement, foreign_key: { on_delete: :cascade } + + t.string :name, null: false, default: '' + t.belongs_to :custom_emoji, foreign_key: { on_delete: :cascade } + + t.timestamps + end + + add_index :announcement_reactions, [:account_id, :announcement_id, :name], unique: true, name: :index_announcement_reactions_on_account_id_and_announcement_id + end +end diff --git a/db/schema.rb b/db/schema.rb index fc2d3a511..d3a2c05b3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -196,15 +196,49 @@ ActiveRecord::Schema.define(version: 2020_01_19_112504) do t.index ["target_type", "target_id"], name: "index_admin_action_logs_on_target_type_and_target_id" end + create_table "announcement_mutes", force: :cascade do |t| + t.bigint "account_id" + t.bigint "announcement_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id", "announcement_id"], name: "index_announcement_mutes_on_account_id_and_announcement_id", unique: true + t.index ["account_id"], name: "index_announcement_mutes_on_account_id" + t.index ["announcement_id"], name: "index_announcement_mutes_on_announcement_id" + end + + create_table "announcement_reactions", force: :cascade do |t| + t.bigint "account_id" + t.bigint "announcement_id" + t.string "name", default: "", null: false + t.bigint "custom_emoji_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id", "announcement_id", "name"], name: "index_announcement_reactions_on_account_id_and_announcement_id", unique: true + t.index ["account_id"], name: "index_announcement_reactions_on_account_id" + t.index ["announcement_id"], name: "index_announcement_reactions_on_announcement_id" + t.index ["custom_emoji_id"], name: "index_announcement_reactions_on_custom_emoji_id" + end + + create_table "announcements", force: :cascade do |t| + t.text "text", default: "", null: false + t.boolean "published", default: false, null: false + t.boolean "all_day", default: false, null: false + t.datetime "scheduled_at" + t.datetime "starts_at" + t.datetime "ends_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "backups", force: :cascade do |t| t.bigint "user_id" t.string "dump_file_name" t.string "dump_content_type" - t.bigint "dump_file_size" t.datetime "dump_updated_at" t.boolean "processed", default: false, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.bigint "dump_file_size" end create_table "blocks", force: :cascade do |t| @@ -818,6 +852,11 @@ ActiveRecord::Schema.define(version: 2020_01_19_112504) do add_foreign_key "account_warnings", "accounts", on_delete: :nullify add_foreign_key "accounts", "accounts", column: "moved_to_account_id", on_delete: :nullify add_foreign_key "admin_action_logs", "accounts", on_delete: :cascade + add_foreign_key "announcement_mutes", "accounts", on_delete: :cascade + add_foreign_key "announcement_mutes", "announcements", on_delete: :cascade + add_foreign_key "announcement_reactions", "accounts", on_delete: :cascade + add_foreign_key "announcement_reactions", "announcements", on_delete: :cascade + add_foreign_key "announcement_reactions", "custom_emojis", on_delete: :cascade add_foreign_key "backups", "users", on_delete: :nullify add_foreign_key "blocks", "accounts", column: "target_account_id", name: "fk_9571bfabc1", on_delete: :cascade add_foreign_key "blocks", "accounts", name: "fk_4269e03e65", on_delete: :cascade diff --git a/lib/tasks/auto_annotate_models.rake b/lib/tasks/auto_annotate_models.rake index fb9c89aa4..a374e33ad 100644 --- a/lib/tasks/auto_annotate_models.rake +++ b/lib/tasks/auto_annotate_models.rake @@ -4,6 +4,7 @@ if Rails.env.development? task :set_annotation_options do Annotate.set_defaults( 'routes' => 'false', + 'models' => 'true', 'position_in_routes' => 'before', 'position_in_class' => 'before', 'position_in_test' => 'before', diff --git a/spec/controllers/api/v1/announcements/reactions_controller_spec.rb b/spec/controllers/api/v1/announcements/reactions_controller_spec.rb new file mode 100644 index 000000000..72620e242 --- /dev/null +++ b/spec/controllers/api/v1/announcements/reactions_controller_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Api::V1::Announcements::ReactionsController, type: :controller do + render_views + + let(:user) { Fabricate(:user) } + let(:scopes) { 'write:favourites' } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + + let!(:announcement) { Fabricate(:announcement) } + + describe 'PUT #update' do + context 'without token' do + it 'returns http unauthorized' do + put :update, params: { announcement_id: announcement.id, id: 'πŸ˜‚' } + expect(response).to have_http_status :unauthorized + end + end + + context 'with token' do + before do + allow(controller).to receive(:doorkeeper_token) { token } + put :update, params: { announcement_id: announcement.id, id: 'πŸ˜‚' } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'creates reaction' do + expect(announcement.announcement_reactions.find_by(name: 'πŸ˜‚', account: user.account)).to_not be_nil + end + end + end + + describe 'DELETE #destroy' do + before do + announcement.announcement_reactions.create!(account: user.account, name: 'πŸ˜‚') + end + + context 'without token' do + it 'returns http unauthorized' do + delete :destroy, params: { announcement_id: announcement.id, id: 'πŸ˜‚' } + expect(response).to have_http_status :unauthorized + end + end + + context 'with token' do + before do + allow(controller).to receive(:doorkeeper_token) { token } + delete :destroy, params: { announcement_id: announcement.id, id: 'πŸ˜‚' } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'creates reaction' do + expect(announcement.announcement_reactions.find_by(name: 'πŸ˜‚', account: user.account)).to be_nil + end + end + end +end diff --git a/spec/controllers/api/v1/announcements_controller_spec.rb b/spec/controllers/api/v1/announcements_controller_spec.rb new file mode 100644 index 000000000..6ee46b60e --- /dev/null +++ b/spec/controllers/api/v1/announcements_controller_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Api::V1::AnnouncementsController, type: :controller do + render_views + + let(:user) { Fabricate(:user) } + let(:scopes) { 'read' } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + + let!(:announcement) { Fabricate(:announcement) } + + describe 'GET #index' do + context 'without token' do + it 'returns http unprocessable entity' do + get :index + expect(response).to have_http_status :unprocessable_entity + end + end + + context 'with token' do + before do + allow(controller).to receive(:doorkeeper_token) { token } + get :index + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + end + + describe 'POST #dismiss' do + context 'without token' do + it 'returns http unauthorized' do + post :dismiss, params: { id: announcement.id } + expect(response).to have_http_status :unauthorized + end + end + + context 'with token' do + let(:scopes) { 'write:accounts' } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + post :dismiss, params: { id: announcement.id } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'dismisses announcement' do + expect(announcement.announcement_mutes.find_by(account: user.account)).to_not be_nil + end + end + end +end diff --git a/spec/controllers/api/v1/trends_controller_spec.rb b/spec/controllers/api/v1/trends_controller_spec.rb new file mode 100644 index 000000000..91e0d18fe --- /dev/null +++ b/spec/controllers/api/v1/trends_controller_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Api::V1::TrendsController, type: :controller do + render_views + + describe 'GET #index' do + before do + allow(TrendingTags).to receive(:get).and_return(Fabricate.times(10, :tag)) + get :index + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/fabricators/announcement_fabricator.rb b/spec/fabricators/announcement_fabricator.rb new file mode 100644 index 000000000..5a3871d90 --- /dev/null +++ b/spec/fabricators/announcement_fabricator.rb @@ -0,0 +1,6 @@ +Fabricator(:announcement) do + text { Faker::Lorem.paragraph(sentence_count: 2) } + published true + starts_at nil + ends_at nil +end diff --git a/spec/fabricators/announcement_mute_fabricator.rb b/spec/fabricators/announcement_mute_fabricator.rb new file mode 100644 index 000000000..c4eafe8f4 --- /dev/null +++ b/spec/fabricators/announcement_mute_fabricator.rb @@ -0,0 +1,4 @@ +Fabricator(:announcement_mute) do + account + announcement +end diff --git a/spec/fabricators/announcement_reaction_fabricator.rb b/spec/fabricators/announcement_reaction_fabricator.rb new file mode 100644 index 000000000..f923c59c6 --- /dev/null +++ b/spec/fabricators/announcement_reaction_fabricator.rb @@ -0,0 +1,5 @@ +Fabricator(:announcement_reaction) do + account + announcement + name '🌿' +end diff --git a/spec/models/announcement_mute_spec.rb b/spec/models/announcement_mute_spec.rb new file mode 100644 index 000000000..9d0e4c903 --- /dev/null +++ b/spec/models/announcement_mute_spec.rb @@ -0,0 +1,4 @@ +require 'rails_helper' + +RSpec.describe AnnouncementMute, type: :model do +end diff --git a/spec/models/announcement_reaction_spec.rb b/spec/models/announcement_reaction_spec.rb new file mode 100644 index 000000000..f6e151584 --- /dev/null +++ b/spec/models/announcement_reaction_spec.rb @@ -0,0 +1,4 @@ +require 'rails_helper' + +RSpec.describe AnnouncementReaction, type: :model do +end diff --git a/spec/models/announcement_spec.rb b/spec/models/announcement_spec.rb new file mode 100644 index 000000000..7f7b647a9 --- /dev/null +++ b/spec/models/announcement_spec.rb @@ -0,0 +1,4 @@ +require 'rails_helper' + +RSpec.describe Announcement, type: :model do +end -- cgit From ce1dee85b58fb7ab41af7ea1d276853630ce59a0 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 24 Jan 2020 00:20:23 +0100 Subject: Fix relationships page not showing results in admin UI (#12934) Follow-up to #12927 --- app/controllers/admin/relationships_controller.rb | 2 +- app/models/relationship_filter.rb | 20 ++++++++++---------- app/views/admin/accounts/show.html.haml | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) (limited to 'app/views') diff --git a/app/controllers/admin/relationships_controller.rb b/app/controllers/admin/relationships_controller.rb index 07d121340..f8a95cfc8 100644 --- a/app/controllers/admin/relationships_controller.rb +++ b/app/controllers/admin/relationships_controller.rb @@ -19,7 +19,7 @@ module Admin end def filter_params - params.slice(RelationshipFilter::KEYS).permit(RelationshipFilter::KEYS) + params.slice(*RelationshipFilter::KEYS).permit(*RelationshipFilter::KEYS) end end end diff --git a/app/models/relationship_filter.rb b/app/models/relationship_filter.rb index fcb3a8dc5..e6859bf3d 100644 --- a/app/models/relationship_filter.rb +++ b/app/models/relationship_filter.rb @@ -20,12 +20,12 @@ class RelationshipFilter end def results - scope = scope_for('relationship', params['relationship']) + scope = scope_for('relationship', params['relationship'].to_s.strip) params.each do |key, value| next if key.to_s == 'page' - scope.merge!(scope_for(key, value)) if value.present? + scope.merge!(scope_for(key.to_s, value.to_s.strip)) if value.present? end scope @@ -39,7 +39,7 @@ class RelationshipFilter end def scope_for(key, value) - case key.to_s + case key when 'relationship' relationship_scope(value) when 'by_domain' @@ -58,7 +58,7 @@ class RelationshipFilter end def relationship_scope(value) - case value.to_s + case value when 'following' account.following.eager_load(:account_stat).reorder(nil) when 'followed_by' @@ -73,11 +73,11 @@ class RelationshipFilter end def by_domain_scope(value) - Account.where(domain: value.to_s) + Account.where(domain: value) end def location_scope(value) - case value.to_s + case value when 'local' Account.local when 'remote' @@ -88,7 +88,7 @@ class RelationshipFilter end def status_scope(value) - case value.to_s + case value when 'moved' Account.where.not(moved_to_account_id: nil) when 'primary' @@ -99,18 +99,18 @@ class RelationshipFilter end def order_scope(value) - case value.to_s + case value when 'active' Account.by_recent_status when 'recent' - Follow.recent + params[:relationship] == 'invited' ? Account.recent : Follow.recent else raise "Unknown order: #{value}" end end def activity_scope(value) - case value.to_s + case value when 'dormant' AccountStat.where(last_status_at: nil).or(AccountStat.where(AccountStat.arel_table[:last_status_at].lt(1.month.ago))) else diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index f191d8f25..a83f77134 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -41,7 +41,7 @@ .dashboard__counters__num= number_to_human_size @account.media_attachments.sum('file_file_size') .dashboard__counters__label= t 'admin.accounts.media_attachments' %div - = link_to admin_account_relationships_path(@account.id, location: 'local') do + = link_to admin_account_relationships_path(@account.id, location: 'local', relationship: 'followed_by') do .dashboard__counters__num= number_with_delimiter @account.local_followers_count .dashboard__counters__label= t 'admin.accounts.followers' %div -- cgit From 42d2a915e4aa31533032e37aaa46354cee2386da Mon Sep 17 00:00:00 2001 From: ThibG Date: Sun, 26 Jan 2020 23:13:48 +0100 Subject: Change last_status_at to be a date, not datetime (#12966) * Return last_status_at as date, not datetime * Fix relative timestamp for dates when delay is inferior to 1 day * Also fix public directory * Fix error when last_status_at isn't set --- app/javascript/mastodon/components/relative_timestamp.js | 16 +++++++++++----- app/serializers/rest/account_serializer.rb | 4 ++++ app/views/directories/index.html.haml | 2 +- app/views/relationships/_account.html.haml | 2 +- 4 files changed, 17 insertions(+), 7 deletions(-) (limited to 'app/views') diff --git a/app/javascript/mastodon/components/relative_timestamp.js b/app/javascript/mastodon/components/relative_timestamp.js index aa4b73cfe..711181dcd 100644 --- a/app/javascript/mastodon/components/relative_timestamp.js +++ b/app/javascript/mastodon/components/relative_timestamp.js @@ -3,6 +3,7 @@ import { injectIntl, defineMessages } from 'react-intl'; import PropTypes from 'prop-types'; const messages = defineMessages({ + today: { id: 'relative_time.today', defaultMessage: 'today' }, just_now: { id: 'relative_time.just_now', defaultMessage: 'now' }, seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' }, minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' }, @@ -65,12 +66,14 @@ const getUnitDelay = units => { } }; -export const timeAgoString = (intl, date, now, year) => { +export const timeAgoString = (intl, date, now, year, timeGiven = true) => { const delta = now - date.getTime(); let relativeTime; - if (delta < 10 * SECOND) { + if (delta < DAY && !timeGiven) { + relativeTime = intl.formatMessage(messages.today); + } else if (delta < 10 * SECOND) { relativeTime = intl.formatMessage(messages.just_now); } else if (delta < 7 * DAY) { if (delta < MINUTE) { @@ -91,12 +94,14 @@ export const timeAgoString = (intl, date, now, year) => { return relativeTime; }; -const timeRemainingString = (intl, date, now) => { +const timeRemainingString = (intl, date, now, timeGiven = true) => { const delta = date.getTime() - now; let relativeTime; - if (delta < 10 * SECOND) { + if (delta < DAY && !timeGiven) { + relativeTime = intl.formatMessage(messages.today); + } else if (delta < 10 * SECOND) { relativeTime = intl.formatMessage(messages.moments_remaining); } else if (delta < MINUTE) { relativeTime = intl.formatMessage(messages.seconds_remaining, { number: Math.floor(delta / SECOND) }); @@ -173,8 +178,9 @@ class RelativeTimestamp extends React.Component { render () { const { timestamp, intl, year, futureDate } = this.props; + const timeGiven = timestamp.includes('T'); const date = new Date(timestamp); - const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now) : timeAgoString(intl, date, this.state.now, year); + const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven); return (