diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb
index d548072a80..edf29947bb 100644
--- a/app/controllers/settings/preferences_controller.rb
+++ b/app/controllers/settings/preferences_controller.rb
@@ -56,6 +56,7 @@ class Settings::PreferencesController < Settings::BaseController
:setting_advanced_layout,
:setting_use_blurhash,
:setting_use_pending_items,
+ :setting_trends,
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag),
interactions: %i(must_be_follower must_be_following must_be_following_dm)
)
diff --git a/app/javascript/mastodon/actions/trends.js b/app/javascript/mastodon/actions/trends.js
new file mode 100644
index 0000000000..853e4f60ae
--- /dev/null
+++ b/app/javascript/mastodon/actions/trends.js
@@ -0,0 +1,32 @@
+import api from '../api';
+
+export const TRENDS_FETCH_REQUEST = 'TRENDS_FETCH_REQUEST';
+export const TRENDS_FETCH_SUCCESS = 'TRENDS_FETCH_SUCCESS';
+export const TRENDS_FETCH_FAIL = 'TRENDS_FETCH_FAIL';
+
+export const fetchTrends = () => (dispatch, getState) => {
+ dispatch(fetchTrendsRequest());
+
+ api(getState)
+ .get('/api/v1/trends')
+ .then(({ data }) => dispatch(fetchTrendsSuccess(data)))
+ .catch(err => dispatch(fetchTrendsFail(err)));
+};
+
+export const fetchTrendsRequest = () => ({
+ type: TRENDS_FETCH_REQUEST,
+ skipLoading: true,
+});
+
+export const fetchTrendsSuccess = trends => ({
+ type: TRENDS_FETCH_SUCCESS,
+ trends,
+ skipLoading: true,
+});
+
+export const fetchTrendsFail = error => ({
+ type: TRENDS_FETCH_FAIL,
+ error,
+ skipLoading: true,
+ skipAlert: true,
+});
diff --git a/app/javascript/mastodon/features/getting_started/components/trends.js b/app/javascript/mastodon/features/getting_started/components/trends.js
new file mode 100644
index 0000000000..1dcacc8b39
--- /dev/null
+++ b/app/javascript/mastodon/features/getting_started/components/trends.js
@@ -0,0 +1,43 @@
+import React from 'react';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import Hashtag from 'mastodon/components/hashtag';
+
+export default class Trends extends ImmutablePureComponent {
+
+ static defaultProps = {
+ loading: false,
+ };
+
+ static propTypes = {
+ trends: ImmutablePropTypes.list,
+ fetchTrends: PropTypes.func.isRequired,
+ };
+
+ componentDidMount () {
+ this.props.fetchTrends();
+ this.refreshInterval = setInterval(() => this.props.fetchTrends(), 36000);
+ }
+
+ componentWillUnmount () {
+ if (this.refreshInterval) {
+ clearInterval(this.refreshInterval);
+ }
+ }
+
+ render () {
+ const { trends } = this.props;
+
+ if (!trends || trends.isEmpty()) {
+ return null;
+ }
+
+ return (
+
+ {trends.take(3).map(hashtag => )}
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/getting_started/containers/trends_container.js b/app/javascript/mastodon/features/getting_started/containers/trends_container.js
new file mode 100644
index 0000000000..1df3fb4fe2
--- /dev/null
+++ b/app/javascript/mastodon/features/getting_started/containers/trends_container.js
@@ -0,0 +1,13 @@
+import { connect } from 'react-redux';
+import { fetchTrends } from '../../../actions/trends';
+import Trends from '../components/trends';
+
+const mapStateToProps = state => ({
+ trends: state.getIn(['trends', 'items']),
+});
+
+const mapDispatchToProps = dispatch => ({
+ fetchTrends: () => dispatch(fetchTrends()),
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(Trends);
diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js
index 791f22d471..6a122a750b 100644
--- a/app/javascript/mastodon/features/getting_started/index.js
+++ b/app/javascript/mastodon/features/getting_started/index.js
@@ -7,12 +7,13 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import { me, profile_directory } from '../../initial_state';
+import { me, profile_directory, showTrends } from '../../initial_state';
import { fetchFollowRequests } from 'mastodon/actions/accounts';
import { List as ImmutableList } from 'immutable';
import NavigationBar from '../compose/components/navigation_bar';
import Icon from 'mastodon/components/icon';
import LinkFooter from 'mastodon/features/ui/components/link_footer';
+import TrendsContainer from './containers/trends_container';
const messages = defineMessages({
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
@@ -168,6 +169,8 @@ class GettingStarted extends ImmutablePureComponent {
+
+ {multiColumn && showTrends && }
);
}
diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.js b/app/javascript/mastodon/features/ui/components/navigation_panel.js
index ef3ad2e092..64a40a9da8 100644
--- a/app/javascript/mastodon/features/ui/components/navigation_panel.js
+++ b/app/javascript/mastodon/features/ui/components/navigation_panel.js
@@ -2,10 +2,11 @@ import React from 'react';
import { NavLink, withRouter } from 'react-router-dom';
import { FormattedMessage } from 'react-intl';
import Icon from 'mastodon/components/icon';
-import { profile_directory } from 'mastodon/initial_state';
+import { profile_directory, showTrends } from 'mastodon/initial_state';
import NotificationsCounterIcon from './notifications_counter_icon';
import FollowRequestsNavLink from './follow_requests_nav_link';
import ListPanel from './list_panel';
+import TrendsContainer from 'mastodon/features/getting_started/containers/trends_container';
const NavigationPanel = () => (
@@ -25,6 +26,9 @@ const NavigationPanel = () => (
{!!profile_directory &&
}
+
+ {showTrends &&
}
+ {showTrends &&
}
);
diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js
index cb2ccc7c41..38e7b0595a 100644
--- a/app/javascript/mastodon/initial_state.js
+++ b/app/javascript/mastodon/initial_state.js
@@ -22,5 +22,6 @@ export const isStaff = getMeta('is_staff');
export const forceSingleColumn = !getMeta('advanced_layout');
export const useBlurhash = getMeta('use_blurhash');
export const usePendingItems = getMeta('use_pending_items');
+export const showTrends = getMeta('trends');
export default initialState;
diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js
index 981ad8e64c..3b60878eb7 100644
--- a/app/javascript/mastodon/reducers/index.js
+++ b/app/javascript/mastodon/reducers/index.js
@@ -31,6 +31,7 @@ import conversations from './conversations';
import suggestions from './suggestions';
import polls from './polls';
import identity_proofs from './identity_proofs';
+import trends from './trends';
const reducers = {
dropdown_menu,
@@ -65,6 +66,7 @@ const reducers = {
conversations,
suggestions,
polls,
+ trends,
};
export default combineReducers(reducers);
diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js
index 033bfc999a..793a99f8f5 100644
--- a/app/javascript/mastodon/reducers/settings.js
+++ b/app/javascript/mastodon/reducers/settings.js
@@ -12,6 +12,10 @@ const initialState = ImmutableMap({
skinTone: 1,
+ trends: ImmutableMap({
+ show: true,
+ }),
+
home: ImmutableMap({
shows: ImmutableMap({
reblog: true,
diff --git a/app/javascript/mastodon/reducers/trends.js b/app/javascript/mastodon/reducers/trends.js
new file mode 100644
index 0000000000..5cecc8fcab
--- /dev/null
+++ b/app/javascript/mastodon/reducers/trends.js
@@ -0,0 +1,23 @@
+import { TRENDS_FETCH_REQUEST, TRENDS_FETCH_SUCCESS, TRENDS_FETCH_FAIL } from '../actions/trends';
+import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
+
+const initialState = ImmutableMap({
+ items: ImmutableList(),
+ isLoading: false,
+});
+
+export default function trendsReducer(state = initialState, action) {
+ switch(action.type) {
+ case TRENDS_FETCH_REQUEST:
+ return state.set('isLoading', true);
+ case TRENDS_FETCH_SUCCESS:
+ return state.withMutations(map => {
+ map.set('items', fromJS(action.trends));
+ map.set('isLoading', false);
+ });
+ case TRENDS_FETCH_FAIL:
+ return state.set('isLoading', false);
+ default:
+ return state;
+ }
+};
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index f02458ded8..8de72d72ee 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -2212,7 +2212,6 @@ a.account__display-name {
}
.getting-started__wrapper,
- .getting-started__trends,
.search {
margin-bottom: 10px;
}
@@ -2319,13 +2318,24 @@ a.account__display-name {
margin-bottom: 10px;
height: calc(100% - 20px);
overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+
+ & > a {
+ flex: 0 0 auto;
+ }
hr {
+ flex: 0 0 auto;
border: 0;
background: transparent;
border-top: 1px solid lighten($ui-base-color, 4%);
margin: 10px 0;
}
+
+ .flex-spacer {
+ background: transparent;
+ }
}
.drawer__pager {
@@ -2717,8 +2727,10 @@ a.account__display-name {
}
&__trends {
- background: $ui-base-color;
flex: 0 1 auto;
+ opacity: 1;
+ animation: fade 150ms linear;
+ margin-top: 10px;
@media screen and (max-height: 810px) {
.trends__item:nth-child(3) {
@@ -2735,11 +2747,15 @@ a.account__display-name {
@media screen and (max-height: 670px) {
display: none;
}
- }
- &__scrollable {
- max-height: 100%;
- overflow-y: auto;
+ .trends__item {
+ border-bottom: 0;
+ padding: 10px;
+
+ &__current {
+ color: $darker-text-color;
+ }
+ }
}
}
@@ -5968,7 +5984,8 @@ noscript {
font-size: 24px;
line-height: 36px;
font-weight: 500;
- text-align: center;
+ text-align: right;
+ padding-right: 15px;
color: $secondary-text-color;
}
@@ -5976,7 +5993,12 @@ noscript {
flex: 0 0 auto;
width: 50px;
- path {
+ path:first-child {
+ fill: rgba($highlight-text-color, 0.25) !important;
+ fill-opacity: 1 !important;
+ }
+
+ path:last-child {
stroke: lighten($highlight-text-color, 6%) !important;
}
}
diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb
index 9ae9986c28..3568a3e116 100644
--- a/app/lib/user_settings_decorator.rb
+++ b/app/lib/user_settings_decorator.rb
@@ -36,6 +36,7 @@ class UserSettingsDecorator
user.settings['advanced_layout'] = advanced_layout_preference if change?('setting_advanced_layout')
user.settings['use_blurhash'] = use_blurhash_preference if change?('setting_use_blurhash')
user.settings['use_pending_items'] = use_pending_items_preference if change?('setting_use_pending_items')
+ user.settings['trends'] = trends_preference if change?('setting_trends')
end
def merged_notification_emails
@@ -122,6 +123,10 @@ class UserSettingsDecorator
boolean_cast_setting 'setting_use_pending_items'
end
+ def trends_preference
+ boolean_cast_setting 'setting_trends'
+ end
+
def boolean_cast_setting(key)
ActiveModel::Type::Boolean.new.cast(settings[key])
end
diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb
index 2c03c88a8c..051268375e 100644
--- a/app/models/form/admin_settings.rb
+++ b/app/models/form/admin_settings.rb
@@ -29,6 +29,7 @@ class Form::AdminSettings
hero
mascot
spam_check_enabled
+ trends
).freeze
BOOLEAN_KEYS = %i(
@@ -41,6 +42,7 @@ class Form::AdminSettings
preview_sensitive_media
profile_directory
spam_check_enabled
+ trends
).freeze
UPLOAD_KEYS = %i(
diff --git a/app/models/trending_tags.rb b/app/models/trending_tags.rb
index e9b9b25e31..0a7e2feac4 100644
--- a/app/models/trending_tags.rb
+++ b/app/models/trending_tags.rb
@@ -66,6 +66,10 @@ class TrendingTags
end
def request_review!(tag)
+ return unless Setting.trends
+
+ tag.touch(:requested_review_at)
+
User.staff.includes(:account).find_each { |u| AdminMailer.new_trending_tag(u.account, tag).deliver_later! if u.allows_trending_tag_emails? }
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index b83e26af3f..a4a20d9751 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -107,7 +107,8 @@ class User < ApplicationRecord
delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal,
:reduce_motion, :system_font_ui, :noindex, :theme, :display_media, :hide_network,
:expand_spoilers, :default_language, :aggregate_reblogs, :show_application,
- :advanced_layout, :use_blurhash, :use_pending_items, to: :settings, prefix: :setting, allow_nil: false
+ :advanced_layout, :use_blurhash, :use_pending_items, :trends,
+ to: :settings, prefix: :setting, allow_nil: false
attr_reader :invite_code
attr_writer :external
diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb
index 7e5d3eda90..c92c5e606b 100644
--- a/app/serializers/initial_state_serializer.rb
+++ b/app/serializers/initial_state_serializer.rb
@@ -20,6 +20,7 @@ class InitialStateSerializer < ActiveModel::Serializer
invites_enabled: Setting.min_invite_role == 'user',
mascot: instance_presenter.mascot&.file&.url,
profile_directory: Setting.profile_directory,
+ trends: Setting.trends,
}
if object.current_account
@@ -35,6 +36,7 @@ class InitialStateSerializer < ActiveModel::Serializer
store[:use_blurhash] = object.current_account.user.setting_use_blurhash
store[:use_pending_items] = object.current_account.user.setting_use_pending_items
store[:is_staff] = object.current_account.user.staff?
+ store[:trends] = Setting.trends && object.current_account.user.setting_trends
end
store
diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml
index 1e2ed3f77f..28c0ece15b 100644
--- a/app/views/admin/settings/edit.html.haml
+++ b/app/views/admin/settings/edit.html.haml
@@ -68,6 +68,9 @@
.fields-group
= f.input :profile_directory, as: :boolean, wrapper: :with_label, label: t('admin.settings.profile_directory.title'), hint: t('admin.settings.profile_directory.desc_html')
+ .fields-group
+ = f.input :trends, as: :boolean, wrapper: :with_label, label: t('admin.settings.trends.title'), hint: t('admin.settings.trends.desc_html')
+
.fields-group
= f.input :spam_check_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.spam_check_enabled.title'), hint: t('admin.settings.spam_check_enabled.desc_html')
diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml
index e279a61c48..d6ee1933f8 100644
--- a/app/views/settings/preferences/appearance/show.html.haml
+++ b/app/views/settings/preferences/appearance/show.html.haml
@@ -25,6 +25,11 @@
= f.input :setting_reduce_motion, as: :boolean, wrapper: :with_label
= f.input :setting_system_font_ui, as: :boolean, wrapper: :with_label
+ %h4= t 'appearance.discovery'
+
+ .fields-group
+ = f.input :setting_trends, as: :boolean, wrapper: :with_label
+
%h4= t 'appearance.confirmation_dialogs'
.fields-group
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 9b62aac3af..67c3926628 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -460,8 +460,8 @@ en:
title: Custom terms of service
site_title: Server name
spam_check_enabled:
- desc_html: Mastodon can auto-silence and auto-report accounts based on measures such as detecting accounts who send repeated unsolicited messages. There may be false positives.
- title: Anti-spam
+ desc_html: Mastodon can auto-silence and auto-report accounts that send repeated unsolicited messages. There may be false positives.
+ title: Anti-spam automation
thumbnail:
desc_html: Used for previews via OpenGraph and API. 1200x630px recommended
title: Server thumbnail
@@ -469,6 +469,9 @@ en:
desc_html: Display public timeline on landing page
title: Timeline preview
title: Site settings
+ trends:
+ desc_html: Publicly display previously reviewed hashtags that are currently trending
+ title: Trending hashtags
statuses:
back_to_account: Back to account page
batch:
@@ -514,6 +517,7 @@ en:
advanced_web_interface_hint: 'If you want to make use of your entire screen width, the advanced web interface allows you to configure many different columns to see as much information at the same time as you want: Home, notifications, federated timeline, any number of lists and hashtags.'
animations_and_accessibility: Animations and accessibility
confirmation_dialogs: Confirmation dialogs
+ discovery: Discovery
sensitive_content: Sensitive content
application_mailer:
notification_preferences: Change e-mail preferences
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index 6fdfc9d7bc..e15d5904fd 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -114,6 +114,7 @@ en:
setting_show_application: Disclose application used to send toots
setting_system_font_ui: Use system's default font
setting_theme: Site theme
+ setting_trends: Show today's trends
setting_unfollow_modal: Show confirmation dialog before unfollowing someone
setting_use_blurhash: Show colorful gradients for hidden media
setting_use_pending_items: Slow mode
diff --git a/config/settings.yml b/config/settings.yml
index 10180201f3..4e5eefb593 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -34,6 +34,7 @@ defaults: &defaults
advanced_layout: false
use_blurhash: true
use_pending_items: false
+ trends: true
notification_emails:
follow: false
reblog: false