diff --git a/Gemfile b/Gemfile
index 70295bc8d13..43d8d2fd08c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -115,7 +115,7 @@ end
group :test do
gem 'capybara', '~> 3.28'
gem 'climate_control', '~> 0.2'
- gem 'faker', '~> 1.9'
+ gem 'faker', '~> 2.1'
gem 'microformats', '~> 4.1'
gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 5a096ba0619..6979c7a0f8a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -229,7 +229,7 @@ GEM
tzinfo
excon (0.62.0)
fabrication (2.20.2)
- faker (1.9.6)
+ faker (2.1.0)
i18n (>= 0.7)
faraday (0.15.0)
multipart-post (>= 1.2, < 3)
@@ -698,7 +698,7 @@ DEPENDENCIES
doorkeeper (~> 5.1)
dotenv-rails (~> 2.7)
fabrication (~> 2.20)
- faker (~> 1.9)
+ faker (~> 2.1)
fast_blank (~> 1.0)
fastimage
fog-core (<= 2.1.0)
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index 4f4341918bb..92bf7fbb90b 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -28,10 +28,13 @@ module Admin
@pam_enabled = ENV['PAM_ENABLED'] == 'true'
@hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true'
@trending_hashtags = TrendingTags.get(10, filtered: false)
+ @authorized_fetch = authorized_fetch_mode?
+ @whitelist_enabled = whitelist_mode?
@profile_directory = Setting.profile_directory
@timeline_preview = Setting.timeline_preview
@keybase_integration = Setting.enable_keybase
@spam_check_enabled = Setting.spam_check_enabled
+ @trends_enabled = Setting.trends
end
private
@@ -41,7 +44,13 @@ module Admin
end
def redis_info
- @redis_info ||= Redis.current.info
+ @redis_info ||= begin
+ if Redis.current.is_a?(Redis::Namespace)
+ Redis.current.redis.info
+ else
+ Redis.current.info
+ end
+ end
end
end
end
diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb
index 0e9dda30221..ed271aedcb0 100644
--- a/app/controllers/admin/tags_controller.rb
+++ b/app/controllers/admin/tags_controller.rb
@@ -17,7 +17,7 @@ module Admin
authorize @tag, :update?
if @tag.update(tag_params.merge(reviewed_at: Time.now.utc))
- redirect_to admin_tag_path(@tag.id)
+ redirect_to admin_tag_path(@tag.id), notice: I18n.t('admin.tags.updated_msg')
else
render :show
end
diff --git a/app/controllers/directories_controller.rb b/app/controllers/directories_controller.rb
index 0702ebc04a6..f2d1f56619d 100644
--- a/app/controllers/directories_controller.rb
+++ b/app/controllers/directories_controller.rb
@@ -30,7 +30,7 @@ class DirectoriesController < ApplicationController
end
def set_tag
- @tag = Tag.discoverable.find_by!(name: params[:id].downcase)
+ @tag = Tag.discoverable.find_normalized!(params[:id])
end
def set_tags
diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb
index ea4491d1e2d..418ea5d7ac6 100644
--- a/app/controllers/settings/preferences_controller.rb
+++ b/app/controllers/settings/preferences_controller.rb
@@ -58,6 +58,7 @@ class Settings::PreferencesController < Settings::BaseController
:setting_default_content_type,
: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/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index ec494bb2dc3..d6bb28eb513 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -48,7 +48,7 @@ class TagsController < ApplicationController
private
def set_tag
- @tag = Tag.find_normalized!(params[:id])
+ @tag = Tag.usable.find_normalized!(params[:id])
end
def set_body_classes
diff --git a/app/javascript/mastodon/actions/trends.js b/app/javascript/mastodon/actions/trends.js
new file mode 100644
index 00000000000..853e4f60aec
--- /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/components/status_content.js b/app/javascript/mastodon/components/status_content.js
index 549de95fc13..76117f1d92f 100644
--- a/app/javascript/mastodon/components/status_content.js
+++ b/app/javascript/mastodon/components/status_content.js
@@ -112,7 +112,7 @@ export default class StatusContent extends React.PureComponent {
}
onHashtagClick = (hashtag, e) => {
- hashtag = hashtag.replace(/^#/, '').toLowerCase();
+ hashtag = hashtag.replace(/^#/, '');
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
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 00000000000..1dcacc8b392
--- /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 00000000000..1df3fb4fe20
--- /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 791f22d4714..6a122a750b1 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 ef3ad2e092f..64a40a9da84 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 3c3c80e9953..8db5f59aff4 100644
--- a/app/javascript/mastodon/initial_state.js
+++ b/app/javascript/mastodon/initial_state.js
@@ -23,5 +23,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 981ad8e64cf..3b60878eb79 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 033bfc999a0..793a99f8f56 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 00000000000..5cecc8fcab5
--- /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 97ef06efebc..2d04aeca78d 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/javascript/styles/mastodon/widgets.scss b/app/javascript/styles/mastodon/widgets.scss
index acaf5b02404..8c30bc57c94 100644
--- a/app/javascript/styles/mastodon/widgets.scss
+++ b/app/javascript/styles/mastodon/widgets.scss
@@ -324,7 +324,8 @@
&.active h4 {
&,
.fa,
- small {
+ small,
+ .trends__item__current {
color: $primary-text-color;
}
}
@@ -337,6 +338,10 @@
&.active .avatar-stack .account__avatar {
border-color: $ui-highlight-color;
}
+
+ .trends__item__current {
+ padding-right: 0;
+ }
}
}
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb
index c9f78cd31af..d85a333b399 100644
--- a/app/lib/formatter.rb
+++ b/app/lib/formatter.rb
@@ -380,7 +380,7 @@ class Formatter
end
def hashtag_html(tag)
- "##{encode(tag)}"
+ "##{encode(tag)}"
end
def mention_html(account)
diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb
index 51d8c0970f0..a52172707b7 100644
--- a/app/lib/user_settings_decorator.rb
+++ b/app/lib/user_settings_decorator.rb
@@ -40,6 +40,7 @@ class UserSettingsDecorator
user.settings['default_content_type']= default_content_type_preference if change?('setting_default_content_type')
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
@@ -142,6 +143,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/account.rb b/app/models/account.rb
index 3370fbc5e6f..92e60f74702 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -231,17 +231,7 @@ class Account < ApplicationRecord
end
def tags_as_strings=(tag_names)
- tag_names.map! { |name| name.mb_chars.downcase.to_s }
- tag_names.uniq!
-
- # Existing hashtags
- hashtags_map = Tag.where(name: tag_names).each_with_object({}) { |tag, h| h[tag.name] = tag }
-
- # Initialize not yet existing hashtags
- tag_names.each do |name|
- next if hashtags_map.key?(name)
- hashtags_map[name] = Tag.new(name: name)
- end
+ hashtags_map = Tag.find_or_create_by_names(tag_names).each_with_object({}) { |tag, h| h[tag.name] = tag }
# Remove hashtags that are to be deleted
tags.each do |tag|
diff --git a/app/models/featured_tag.rb b/app/models/featured_tag.rb
index d06ae26a890..e02ae0705a2 100644
--- a/app/models/featured_tag.rb
+++ b/app/models/featured_tag.rb
@@ -23,7 +23,7 @@ class FeaturedTag < ApplicationRecord
validate :validate_featured_tags_limit, on: :create
def name=(str)
- self.tag = Tag.find_or_initialize_by(name: str.strip.delete('#').mb_chars.downcase.to_s)
+ self.tag = Tag.find_or_create_by_names(str.strip)&.first
end
def increment(timestamp)
diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb
index ecaed44f60b..2c3a7f13b95 100644
--- a/app/models/form/admin_settings.rb
+++ b/app/models/form/admin_settings.rb
@@ -35,6 +35,7 @@ class Form::AdminSettings
show_reblogs_in_public_timelines
show_replies_in_public_timelines
spam_check_enabled
+ trends
).freeze
BOOLEAN_KEYS = %i(
@@ -51,6 +52,7 @@ class Form::AdminSettings
show_reblogs_in_public_timelines
show_replies_in_public_timelines
spam_check_enabled
+ trends
).freeze
UPLOAD_KEYS = %i(
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 6a02581fad3..e2fe91da1b4 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -31,7 +31,8 @@ class Tag < ApplicationRecord
scope :reviewed, -> { where.not(reviewed_at: nil) }
scope :pending_review, -> { where(reviewed_at: nil).where.not(requested_review_at: nil) }
- scope :discoverable, -> { where.not(listable: false).joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).order(Arel.sql('account_tag_stats.accounts_count desc')) }
+ scope :usable, -> { where(usable: [true, nil]) }
+ scope :discoverable, -> { where(listable: [true, nil]).joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).order(Arel.sql('account_tag_stats.accounts_count desc')) }
scope :most_used, ->(account) { joins(:statuses).where(statuses: { account: account }).group(:id).order(Arel.sql('count(*) desc')) }
delegate :accounts_count,
diff --git a/app/models/trending_tags.rb b/app/models/trending_tags.rb
index e9b9b25e31f..0a7e2feac4e 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 67cf92307e8..45a4b89890b 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -107,7 +107,9 @@ class User < ApplicationRecord
delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :favourite_modal, :delete_modal,
:reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_media, :hide_network, :hide_followers_count,
:expand_spoilers, :default_language, :aggregate_reblogs, :show_application,
- :advanced_layout, :default_content_type, :use_blurhash, :use_pending_items, :use_pending_items, to: :settings, prefix: :setting, allow_nil: false
+ :advanced_layout, :use_blurhash, :use_pending_items, :trends,
+ :default_content_type,
+ 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 e2205918231..c8da6e725cd 100644
--- a/app/serializers/initial_state_serializer.rb
+++ b/app/serializers/initial_state_serializer.rb
@@ -34,6 +34,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
@@ -50,6 +51,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
store[:default_content_type] = object.current_account.user.setting_default_content_type
end
diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb
index bbee47cb701..c9a9a5a6e05 100644
--- a/app/services/batched_remove_status_service.rb
+++ b/app/services/batched_remove_status_service.rb
@@ -81,8 +81,8 @@ class BatchedRemoveStatusService < BaseService
end
@tags[status.id].each do |hashtag|
- redis.publish("timeline:hashtag:#{hashtag}", payload)
- redis.publish("timeline:hashtag:#{hashtag}:local", payload) if status.local?
+ redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", payload)
+ redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", payload) if status.local?
end
end
end
diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb
index cf433d8a69d..72f716dc511 100644
--- a/app/services/fan_out_on_write_service.rb
+++ b/app/services/fan_out_on_write_service.rb
@@ -77,8 +77,8 @@ class FanOutOnWriteService < BaseService
Rails.logger.debug "Delivering status #{status.id} to hashtags"
status.tags.pluck(:name).each do |hashtag|
- Redis.current.publish("timeline:hashtag:#{hashtag}", @payload)
- Redis.current.publish("timeline:hashtag:#{hashtag}:local", @payload) if status.local?
+ Redis.current.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", @payload)
+ Redis.current.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", @payload) if status.local?
end
end
diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb
index 958a67e8f14..c19fa2126fb 100644
--- a/app/services/remove_status_service.rb
+++ b/app/services/remove_status_service.rb
@@ -126,8 +126,8 @@ class RemoveStatusService < BaseService
return unless @status.public_visibility?
@tags.each do |hashtag|
- redis.publish("timeline:hashtag:#{hashtag}", @payload)
- redis.publish("timeline:hashtag:#{hashtag}:local", @payload) if @status.local?
+ redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", @payload)
+ redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", @payload) if @status.local?
end
end
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index d3ac3ff4250..3c98da35f34 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -51,6 +51,8 @@
= feature_hint(link_to(t('admin.dashboard.feature_timeline_preview'), edit_admin_settings_path), @timeline_preview)
%li
= feature_hint(link_to(t('admin.dashboard.keybase'), edit_admin_settings_path), @keybase_integration)
+ %li
+ = feature_hint(link_to(t('admin.dashboard.trends'), edit_admin_settings_path), @trends_enabled)
%li
= feature_hint(link_to(t('admin.dashboard.feature_relay'), admin_relays_path), @relay_enabled)
%li
@@ -92,6 +94,10 @@
= feature_hint(t('admin.dashboard.search'), @search_enabled)
%li
= feature_hint(t('admin.dashboard.single_user_mode'), @single_user_mode)
+ %li
+ = feature_hint(t('admin.dashboard.authorized_fetch_mode'), @authorized_fetch)
+ %li
+ = feature_hint(t('admin.dashboard.whitelist_mode'), @whitelist_mode)
%li
= feature_hint('LDAP', @ldap_enabled)
%li
diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml
index efe6ea56bda..b0ab394d6bb 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 :hide_followers_count, as: :boolean, wrapper: :with_label, label: t('admin.settings.hide_followers_count.title'), hint: t('admin.settings.hide_followers_count.desc_html')
diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml
index 4479582531e..0bda49f44ac 100644
--- a/app/views/settings/preferences/appearance/show.html.haml
+++ b/app/views/settings/preferences/appearance/show.html.haml
@@ -22,6 +22,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 d4e4a0c9a3f..6b6b996836a 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -247,6 +247,7 @@ en:
updated_msg: Emoji successfully updated!
upload: Upload
dashboard:
+ authorized_fetch_mode: Authorized fetch mode
backlog: backlogged jobs
config: Configuration
feature_deletions: Account deletions
@@ -271,6 +272,7 @@ en:
week_interactions: interactions this week
week_users_active: active this week
week_users_new: users this week
+ whitelist_mode: Whitelist mode
domain_allows:
add_new: Whitelist domain
created_msg: Domain has been successfully whitelisted
@@ -473,8 +475,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
@@ -482,6 +484,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:
@@ -504,6 +509,7 @@ en:
title: Hashtags
trending_right_now: Trending right now
unique_uses_today: "%{count} posting today"
+ updated_msg: Hashtag settings updated successfully
title: Administration
warning_presets:
add_new: Add new
@@ -527,6 +533,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
@@ -574,6 +581,7 @@ en:
status:
account_status: Account status
confirming: Waiting for e-mail confirmation to be completed.
+ functional: Your account is fully operational.
pending: Your application is pending review by our staff. This may take some time. You will receive an e-mail if your application is approved.
trouble_logging_in: Trouble logging in?
authorize_follow:
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index 82e12958154..5da0cc45df3 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -125,6 +125,8 @@ en:
setting_show_application: Disclose application used to send toots
setting_skin: Skin
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 2abb87c43d0..10836db3f5f 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -38,6 +38,7 @@ defaults: &defaults
advanced_layout: false
use_blurhash: true
use_pending_items: false
+ trends: true
notification_emails:
follow: false
reblog: false
diff --git a/spec/controllers/settings/identity_proofs_controller_spec.rb b/spec/controllers/settings/identity_proofs_controller_spec.rb
index 2a0f91088a9..261e980d4be 100644
--- a/spec/controllers/settings/identity_proofs_controller_spec.rb
+++ b/spec/controllers/settings/identity_proofs_controller_spec.rb
@@ -8,8 +8,8 @@ describe Settings::IdentityProofsController do
let(:valid_token) { '1'*66 }
let(:kbname) { 'kbuser' }
let(:provider) { 'keybase' }
- let(:findable_id) { Faker::Number.number(5) }
- let(:unfindable_id) { Faker::Number.number(5) }
+ let(:findable_id) { Faker::Number.number(digits: 5) }
+ let(:unfindable_id) { Faker::Number.number(digits: 5) }
let(:new_proof_params) do
{ provider: provider, provider_username: kbname, token: valid_token, username: user.account.username }
end
diff --git a/spec/fabricators/account_fabricator.rb b/spec/fabricators/account_fabricator.rb
index f12464ef3e6..ab900c5fa03 100644
--- a/spec/fabricators/account_fabricator.rb
+++ b/spec/fabricators/account_fabricator.rb
@@ -4,7 +4,7 @@ private_key = keypair.to_pem
Fabricator(:account) do
transient :suspended, :silenced
- username { sequence(:username) { |i| "#{Faker::Internet.user_name(nil, %w(_))}#{i}" } }
+ username { sequence(:username) { |i| "#{Faker::Internet.user_name(separators: %w(_))}#{i}" } }
last_webfingered_at { Time.now.utc }
public_key { public_key }
private_key { private_key }
diff --git a/spec/fabricators/account_identity_proof_fabricator.rb b/spec/fabricators/account_identity_proof_fabricator.rb
index 94f40dfd6b7..7b932fa9687 100644
--- a/spec/fabricators/account_identity_proof_fabricator.rb
+++ b/spec/fabricators/account_identity_proof_fabricator.rb
@@ -1,7 +1,7 @@
Fabricator(:account_identity_proof) do
account
provider 'keybase'
- provider_username { sequence(:provider_username) { |i| "#{Faker::Lorem.characters(15)}" } }
+ provider_username { sequence(:provider_username) { |i| "#{Faker::Lorem.characters(number: 15)}" } }
token { sequence(:token) { |i| "#{i}#{Faker::Crypto.sha1()*2}"[0..65] } }
verified false
live false
diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb
index 3a17d540ac9..3eec464bd3b 100644
--- a/spec/models/account_spec.rb
+++ b/spec/models/account_spec.rb
@@ -607,19 +607,19 @@ RSpec.describe Account, type: :model do
end
it 'is invalid if the username is longer then 30 characters' do
- account = Fabricate.build(:account, username: Faker::Lorem.characters(31))
+ account = Fabricate.build(:account, username: Faker::Lorem.characters(number: 31))
account.valid?
expect(account).to model_have_error_on_field(:username)
end
it 'is invalid if the display name is longer than 30 characters' do
- account = Fabricate.build(:account, display_name: Faker::Lorem.characters(31))
+ account = Fabricate.build(:account, display_name: Faker::Lorem.characters(number: 31))
account.valid?
expect(account).to model_have_error_on_field(:display_name)
end
it 'is invalid if the note is longer than 500 characters' do
- account = Fabricate.build(:account, note: Faker::Lorem.characters(501))
+ account = Fabricate.build(:account, note: Faker::Lorem.characters(number: 501))
account.valid?
expect(account).to model_have_error_on_field(:note)
end
@@ -653,19 +653,19 @@ RSpec.describe Account, type: :model do
end
it 'is valid even if the username is longer then 30 characters' do
- account = Fabricate.build(:account, domain: 'domain', username: Faker::Lorem.characters(31))
+ account = Fabricate.build(:account, domain: 'domain', username: Faker::Lorem.characters(number: 31))
account.valid?
expect(account).not_to model_have_error_on_field(:username)
end
it 'is valid even if the display name is longer than 30 characters' do
- account = Fabricate.build(:account, domain: 'domain', display_name: Faker::Lorem.characters(31))
+ account = Fabricate.build(:account, domain: 'domain', display_name: Faker::Lorem.characters(number: 31))
account.valid?
expect(account).not_to model_have_error_on_field(:display_name)
end
it 'is valid even if the note is longer than 500 characters' do
- account = Fabricate.build(:account, domain: 'domain', note: Faker::Lorem.characters(501))
+ account = Fabricate.build(:account, domain: 'domain', note: Faker::Lorem.characters(number: 501))
account.valid?
expect(account).not_to model_have_error_on_field(:note)
end
@@ -804,7 +804,7 @@ RSpec.describe Account, type: :model do
context 'when is local' do
# Test disabled because test environment omits autogenerating keys for performance
xit 'generates keys' do
- account = Account.create!(domain: nil, username: Faker::Internet.user_name(nil, ['_']))
+ account = Account.create!(domain: nil, username: Faker::Internet.user_name(separators: ['_']))
expect(account.keypair.private?).to eq true
end
end
@@ -812,12 +812,12 @@ RSpec.describe Account, type: :model do
context 'when is remote' do
it 'does not generate keys' do
key = OpenSSL::PKey::RSA.new(1024).public_key
- account = Account.create!(domain: 'remote', username: Faker::Internet.user_name(nil, ['_']), public_key: key.to_pem)
+ account = Account.create!(domain: 'remote', username: Faker::Internet.user_name(separators: ['_']), public_key: key.to_pem)
expect(account.keypair.params).to eq key.params
end
it 'normalizes domain' do
- account = Account.create!(domain: 'にゃん', username: Faker::Internet.user_name(nil, ['_']))
+ account = Account.create!(domain: 'にゃん', username: Faker::Internet.user_name(separators: ['_']))
expect(account.domain).to eq 'xn--r9j5b5b'
end
end
diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb
index a0cd0800da5..312954c9dc9 100644
--- a/spec/models/report_spec.rb
+++ b/spec/models/report_spec.rb
@@ -125,7 +125,7 @@ describe Report do
end
it 'is invalid if comment is longer than 1000 characters' do
- report = Fabricate.build(:report, comment: Faker::Lorem.characters(1001))
+ report = Fabricate.build(:report, comment: Faker::Lorem.characters(number: 1001))
report.valid?
expect(report).to model_have_error_on_field(:comment)
end