Merge pull request #1193 from ThibG/glitch-soc/merge-upstream

Merge upstream changes
pull/1194/head
ThibG 2019-08-07 20:04:22 +02:00 committed by GitHub
commit 86cfa2ea6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 329 additions and 71 deletions

View File

@ -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'

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
)

View File

@ -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

View File

@ -45,7 +45,6 @@ class DropdownMenu extends React.PureComponent {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('keydown', this.handleKeyDown, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
this.activeElement = document.activeElement;
if (this.focusedItem && this.props.openedViaKeyboard) {
this.focusedItem.focus();
}
@ -56,9 +55,6 @@ class DropdownMenu extends React.PureComponent {
document.removeEventListener('click', this.handleDocumentClick, false);
document.removeEventListener('keydown', this.handleKeyDown, false);
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
if (this.activeElement) {
this.activeElement.focus();
}
}
setRef = c => {
@ -117,7 +113,7 @@ class DropdownMenu extends React.PureComponent {
}
}
handleItemKeyUp = e => {
handleItemKeyPress = e => {
if (e.key === 'Enter' || e.key === ' ') {
this.handleClick(e);
}
@ -147,7 +143,7 @@ class DropdownMenu extends React.PureComponent {
return (
<li className='dropdown-menu__item' key={`${text}-${i}`}>
<a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyUp={this.handleItemKeyUp} data-index={i}>
<a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
{text}
</a>
</li>
@ -214,15 +210,44 @@ export default class Dropdown extends React.PureComponent {
} else {
const { top } = target.getBoundingClientRect();
const placement = top * 2 < innerHeight ? 'bottom' : 'top';
this.props.onOpen(this.state.id, this.handleItemClick, placement, type !== 'click');
}
}
handleClose = () => {
if (this.activeElement) {
this.activeElement.focus();
this.activeElement = null;
}
this.props.onClose(this.state.id);
}
handleMouseDown = () => {
if (!this.state.open) {
this.activeElement = document.activeElement;
}
}
handleButtonKeyDown = (e) => {
switch(e.key) {
case ' ':
case 'Enter':
this.handleMouseDown();
break;
}
}
handleKeyPress = (e) => {
switch(e.key) {
case ' ':
case 'Enter':
this.handleClick(e);
e.stopPropagation();
e.preventDefault();
break;
}
}
handleItemClick = (i, e) => {
const { action, to } = this.props.items[i];
@ -265,6 +290,9 @@ export default class Dropdown extends React.PureComponent {
size={size}
ref={this.setTargetRef}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleButtonKeyDown}
onKeyPress={this.handleKeyPress}
/>
<Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>

View File

@ -172,7 +172,7 @@ export default class StatusContent extends React.PureComponent {
}
onHashtagClick = (hashtag, e) => {
hashtag = hashtag.replace(/^#/, '').toLowerCase();
hashtag = hashtag.replace(/^#/, '');
if (this.props.parseClick) {
this.props.parseClick(e, `/timelines/tag/${hashtag}`);

View File

@ -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,
});

View File

@ -45,7 +45,6 @@ class DropdownMenu extends React.PureComponent {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('keydown', this.handleKeyDown, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
this.activeElement = document.activeElement;
if (this.focusedItem && this.props.openedViaKeyboard) {
this.focusedItem.focus();
}
@ -56,9 +55,6 @@ class DropdownMenu extends React.PureComponent {
document.removeEventListener('click', this.handleDocumentClick, false);
document.removeEventListener('keydown', this.handleKeyDown, false);
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
if (this.activeElement) {
this.activeElement.focus();
}
}
setRef = c => {
@ -117,7 +113,7 @@ class DropdownMenu extends React.PureComponent {
}
}
handleItemKeyUp = e => {
handleItemKeyPress = e => {
if (e.key === 'Enter' || e.key === ' ') {
this.handleClick(e);
}
@ -147,7 +143,7 @@ class DropdownMenu extends React.PureComponent {
return (
<li className='dropdown-menu__item' key={`${text}-${i}`}>
<a href={href} target={target} data-method={method} rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyUp={this.handleItemKeyUp} data-index={i}>
<a href={href} target={target} data-method={method} rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
{text}
</a>
</li>
@ -214,15 +210,44 @@ export default class Dropdown extends React.PureComponent {
} else {
const { top } = target.getBoundingClientRect();
const placement = top * 2 < innerHeight ? 'bottom' : 'top';
this.props.onOpen(this.state.id, this.handleItemClick, placement, type !== 'click');
}
}
handleClose = () => {
if (this.activeElement) {
this.activeElement.focus();
this.activeElement = null;
}
this.props.onClose(this.state.id);
}
handleMouseDown = () => {
if (!this.state.open) {
this.activeElement = document.activeElement;
}
}
handleButtonKeyDown = (e) => {
switch(e.key) {
case ' ':
case 'Enter':
this.handleMouseDown();
break;
}
}
handleKeyPress = (e) => {
switch(e.key) {
case ' ':
case 'Enter':
this.handleClick(e);
e.stopPropagation();
e.preventDefault();
break;
}
}
handleItemClick = e => {
const i = Number(e.currentTarget.getAttribute('data-index'));
const { action, to } = this.props.items[i];
@ -266,6 +291,9 @@ export default class Dropdown extends React.PureComponent {
size={size}
ref={this.setTargetRef}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleButtonKeyDown}
onKeyPress={this.handleKeyPress}
/>
<Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>

View File

@ -14,6 +14,7 @@ export default class IconButton extends React.PureComponent {
onClick: PropTypes.func,
onMouseDown: PropTypes.func,
onKeyDown: PropTypes.func,
onKeyPress: PropTypes.func,
size: PropTypes.number,
active: PropTypes.bool,
pressed: PropTypes.bool,
@ -44,6 +45,12 @@ export default class IconButton extends React.PureComponent {
}
}
handleKeyPress = (e) => {
if (this.props.onKeyPress && !this.props.disabled) {
this.props.onKeyPress(e);
}
}
handleMouseDown = (e) => {
if (!this.props.disabled && this.props.onMouseDown) {
this.props.onMouseDown(e);
@ -100,6 +107,7 @@ export default class IconButton extends React.PureComponent {
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleKeyDown}
onKeyPress={this.handleKeyPress}
style={style}
tabIndex={tabIndex}
disabled={disabled}
@ -121,6 +129,7 @@ export default class IconButton extends React.PureComponent {
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleKeyDown}
onKeyPress={this.handleKeyPress}
style={style}
tabIndex={tabIndex}
disabled={disabled}

View File

@ -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();

View File

@ -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 (
<div className='getting-started__trends'>
{trends.take(3).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
</div>
);
}
}

View File

@ -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);

View File

@ -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 {
<LinkFooter withHotkeys={multiColumn} />
</div>
{multiColumn && showTrends && <TrendsContainer />}
</Column>
);
}

View File

@ -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 = () => (
<div className='navigation-panel'>
@ -25,6 +26,9 @@ const NavigationPanel = () => (
<a className='column-link column-link--transparent' href='/settings/preferences'><Icon className='column-link__icon' id='cog' fixedWidth /><FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' /></a>
<a className='column-link column-link--transparent' href='/relationships'><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='navigation_bar.follows_and_followers' defaultMessage='Follows and followers' /></a>
{!!profile_directory && <a className='column-link column-link--transparent' href='/explore'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='navigation_bar.profile_directory' defaultMessage='Profile directory' /></a>}
{showTrends && <div className='flex-spacer' />}
{showTrends && <TrendsContainer />}
</div>
);

View File

@ -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;

View File

@ -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);

View File

@ -12,6 +12,10 @@ const initialState = ImmutableMap({
skinTone: 1,
trends: ImmutableMap({
show: true,
}),
home: ImmutableMap({
shows: ImmutableMap({
reblog: true,

View File

@ -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;
}
};

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -380,7 +380,7 @@ class Formatter
end
def hashtag_html(tag)
"<a href=\"#{encode(tag_url(tag.downcase))}\" class=\"mention hashtag\" rel=\"tag\">#<span>#{encode(tag)}</span></a>"
"<a href=\"#{encode(tag_url(tag))}\" class=\"mention hashtag\" rel=\"tag\">#<span>#{encode(tag)}</span></a>"
end
def mention_html(account)

View File

@ -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

View File

@ -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|

View File

@ -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)

View File

@ -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(

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -38,6 +38,7 @@ defaults: &defaults
advanced_layout: false
use_blurhash: true
use_pending_items: false
trends: true
notification_emails:
follow: false
reblog: false

View File

@ -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

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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