commit
86cfa2ea6c
2
Gemfile
2
Gemfile
|
@ -115,7 +115,7 @@ end
|
||||||
group :test do
|
group :test do
|
||||||
gem 'capybara', '~> 3.28'
|
gem 'capybara', '~> 3.28'
|
||||||
gem 'climate_control', '~> 0.2'
|
gem 'climate_control', '~> 0.2'
|
||||||
gem 'faker', '~> 1.9'
|
gem 'faker', '~> 2.1'
|
||||||
gem 'microformats', '~> 4.1'
|
gem 'microformats', '~> 4.1'
|
||||||
gem 'rails-controller-testing', '~> 1.0'
|
gem 'rails-controller-testing', '~> 1.0'
|
||||||
gem 'rspec-sidekiq', '~> 3.0'
|
gem 'rspec-sidekiq', '~> 3.0'
|
||||||
|
|
|
@ -229,7 +229,7 @@ GEM
|
||||||
tzinfo
|
tzinfo
|
||||||
excon (0.62.0)
|
excon (0.62.0)
|
||||||
fabrication (2.20.2)
|
fabrication (2.20.2)
|
||||||
faker (1.9.6)
|
faker (2.1.0)
|
||||||
i18n (>= 0.7)
|
i18n (>= 0.7)
|
||||||
faraday (0.15.0)
|
faraday (0.15.0)
|
||||||
multipart-post (>= 1.2, < 3)
|
multipart-post (>= 1.2, < 3)
|
||||||
|
@ -698,7 +698,7 @@ DEPENDENCIES
|
||||||
doorkeeper (~> 5.1)
|
doorkeeper (~> 5.1)
|
||||||
dotenv-rails (~> 2.7)
|
dotenv-rails (~> 2.7)
|
||||||
fabrication (~> 2.20)
|
fabrication (~> 2.20)
|
||||||
faker (~> 1.9)
|
faker (~> 2.1)
|
||||||
fast_blank (~> 1.0)
|
fast_blank (~> 1.0)
|
||||||
fastimage
|
fastimage
|
||||||
fog-core (<= 2.1.0)
|
fog-core (<= 2.1.0)
|
||||||
|
|
|
@ -28,10 +28,13 @@ module Admin
|
||||||
@pam_enabled = ENV['PAM_ENABLED'] == 'true'
|
@pam_enabled = ENV['PAM_ENABLED'] == 'true'
|
||||||
@hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true'
|
@hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true'
|
||||||
@trending_hashtags = TrendingTags.get(10, filtered: false)
|
@trending_hashtags = TrendingTags.get(10, filtered: false)
|
||||||
|
@authorized_fetch = authorized_fetch_mode?
|
||||||
|
@whitelist_enabled = whitelist_mode?
|
||||||
@profile_directory = Setting.profile_directory
|
@profile_directory = Setting.profile_directory
|
||||||
@timeline_preview = Setting.timeline_preview
|
@timeline_preview = Setting.timeline_preview
|
||||||
@keybase_integration = Setting.enable_keybase
|
@keybase_integration = Setting.enable_keybase
|
||||||
@spam_check_enabled = Setting.spam_check_enabled
|
@spam_check_enabled = Setting.spam_check_enabled
|
||||||
|
@trends_enabled = Setting.trends
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -41,7 +44,13 @@ module Admin
|
||||||
end
|
end
|
||||||
|
|
||||||
def redis_info
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,7 +17,7 @@ module Admin
|
||||||
authorize @tag, :update?
|
authorize @tag, :update?
|
||||||
|
|
||||||
if @tag.update(tag_params.merge(reviewed_at: Time.now.utc))
|
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
|
else
|
||||||
render :show
|
render :show
|
||||||
end
|
end
|
||||||
|
|
|
@ -30,7 +30,7 @@ class DirectoriesController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_tag
|
def set_tag
|
||||||
@tag = Tag.discoverable.find_by!(name: params[:id].downcase)
|
@tag = Tag.discoverable.find_normalized!(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_tags
|
def set_tags
|
||||||
|
|
|
@ -58,6 +58,7 @@ class Settings::PreferencesController < Settings::BaseController
|
||||||
:setting_default_content_type,
|
:setting_default_content_type,
|
||||||
:setting_use_blurhash,
|
:setting_use_blurhash,
|
||||||
:setting_use_pending_items,
|
:setting_use_pending_items,
|
||||||
|
:setting_trends,
|
||||||
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag),
|
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)
|
interactions: %i(must_be_follower must_be_following must_be_following_dm)
|
||||||
)
|
)
|
||||||
|
|
|
@ -48,7 +48,7 @@ class TagsController < ApplicationController
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_tag
|
def set_tag
|
||||||
@tag = Tag.find_normalized!(params[:id])
|
@tag = Tag.usable.find_normalized!(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_body_classes
|
def set_body_classes
|
||||||
|
|
|
@ -45,7 +45,6 @@ class DropdownMenu extends React.PureComponent {
|
||||||
document.addEventListener('click', this.handleDocumentClick, false);
|
document.addEventListener('click', this.handleDocumentClick, false);
|
||||||
document.addEventListener('keydown', this.handleKeyDown, false);
|
document.addEventListener('keydown', this.handleKeyDown, false);
|
||||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||||
this.activeElement = document.activeElement;
|
|
||||||
if (this.focusedItem && this.props.openedViaKeyboard) {
|
if (this.focusedItem && this.props.openedViaKeyboard) {
|
||||||
this.focusedItem.focus();
|
this.focusedItem.focus();
|
||||||
}
|
}
|
||||||
|
@ -56,9 +55,6 @@ class DropdownMenu extends React.PureComponent {
|
||||||
document.removeEventListener('click', this.handleDocumentClick, false);
|
document.removeEventListener('click', this.handleDocumentClick, false);
|
||||||
document.removeEventListener('keydown', this.handleKeyDown, false);
|
document.removeEventListener('keydown', this.handleKeyDown, false);
|
||||||
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||||
if (this.activeElement) {
|
|
||||||
this.activeElement.focus();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setRef = c => {
|
setRef = c => {
|
||||||
|
@ -117,7 +113,7 @@ class DropdownMenu extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleItemKeyUp = e => {
|
handleItemKeyPress = e => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
this.handleClick(e);
|
this.handleClick(e);
|
||||||
}
|
}
|
||||||
|
@ -147,7 +143,7 @@ class DropdownMenu extends React.PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className='dropdown-menu__item' key={`${text}-${i}`}>
|
<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}
|
{text}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -214,15 +210,44 @@ export default class Dropdown extends React.PureComponent {
|
||||||
} else {
|
} else {
|
||||||
const { top } = target.getBoundingClientRect();
|
const { top } = target.getBoundingClientRect();
|
||||||
const placement = top * 2 < innerHeight ? 'bottom' : 'top';
|
const placement = top * 2 < innerHeight ? 'bottom' : 'top';
|
||||||
|
|
||||||
this.props.onOpen(this.state.id, this.handleItemClick, placement, type !== 'click');
|
this.props.onOpen(this.state.id, this.handleItemClick, placement, type !== 'click');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClose = () => {
|
handleClose = () => {
|
||||||
|
if (this.activeElement) {
|
||||||
|
this.activeElement.focus();
|
||||||
|
this.activeElement = null;
|
||||||
|
}
|
||||||
this.props.onClose(this.state.id);
|
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) => {
|
handleItemClick = (i, e) => {
|
||||||
const { action, to } = this.props.items[i];
|
const { action, to } = this.props.items[i];
|
||||||
|
|
||||||
|
@ -265,6 +290,9 @@ export default class Dropdown extends React.PureComponent {
|
||||||
size={size}
|
size={size}
|
||||||
ref={this.setTargetRef}
|
ref={this.setTargetRef}
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
|
onMouseDown={this.handleMouseDown}
|
||||||
|
onKeyDown={this.handleButtonKeyDown}
|
||||||
|
onKeyPress={this.handleKeyPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
|
<Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
|
||||||
|
|
|
@ -172,7 +172,7 @@ export default class StatusContent extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
onHashtagClick = (hashtag, e) => {
|
onHashtagClick = (hashtag, e) => {
|
||||||
hashtag = hashtag.replace(/^#/, '').toLowerCase();
|
hashtag = hashtag.replace(/^#/, '');
|
||||||
|
|
||||||
if (this.props.parseClick) {
|
if (this.props.parseClick) {
|
||||||
this.props.parseClick(e, `/timelines/tag/${hashtag}`);
|
this.props.parseClick(e, `/timelines/tag/${hashtag}`);
|
||||||
|
|
|
@ -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,
|
||||||
|
});
|
|
@ -45,7 +45,6 @@ class DropdownMenu extends React.PureComponent {
|
||||||
document.addEventListener('click', this.handleDocumentClick, false);
|
document.addEventListener('click', this.handleDocumentClick, false);
|
||||||
document.addEventListener('keydown', this.handleKeyDown, false);
|
document.addEventListener('keydown', this.handleKeyDown, false);
|
||||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||||
this.activeElement = document.activeElement;
|
|
||||||
if (this.focusedItem && this.props.openedViaKeyboard) {
|
if (this.focusedItem && this.props.openedViaKeyboard) {
|
||||||
this.focusedItem.focus();
|
this.focusedItem.focus();
|
||||||
}
|
}
|
||||||
|
@ -56,9 +55,6 @@ class DropdownMenu extends React.PureComponent {
|
||||||
document.removeEventListener('click', this.handleDocumentClick, false);
|
document.removeEventListener('click', this.handleDocumentClick, false);
|
||||||
document.removeEventListener('keydown', this.handleKeyDown, false);
|
document.removeEventListener('keydown', this.handleKeyDown, false);
|
||||||
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||||
if (this.activeElement) {
|
|
||||||
this.activeElement.focus();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setRef = c => {
|
setRef = c => {
|
||||||
|
@ -117,7 +113,7 @@ class DropdownMenu extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleItemKeyUp = e => {
|
handleItemKeyPress = e => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
this.handleClick(e);
|
this.handleClick(e);
|
||||||
}
|
}
|
||||||
|
@ -147,7 +143,7 @@ class DropdownMenu extends React.PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className='dropdown-menu__item' key={`${text}-${i}`}>
|
<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}
|
{text}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -214,15 +210,44 @@ export default class Dropdown extends React.PureComponent {
|
||||||
} else {
|
} else {
|
||||||
const { top } = target.getBoundingClientRect();
|
const { top } = target.getBoundingClientRect();
|
||||||
const placement = top * 2 < innerHeight ? 'bottom' : 'top';
|
const placement = top * 2 < innerHeight ? 'bottom' : 'top';
|
||||||
|
|
||||||
this.props.onOpen(this.state.id, this.handleItemClick, placement, type !== 'click');
|
this.props.onOpen(this.state.id, this.handleItemClick, placement, type !== 'click');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClose = () => {
|
handleClose = () => {
|
||||||
|
if (this.activeElement) {
|
||||||
|
this.activeElement.focus();
|
||||||
|
this.activeElement = null;
|
||||||
|
}
|
||||||
this.props.onClose(this.state.id);
|
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 => {
|
handleItemClick = e => {
|
||||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
const i = Number(e.currentTarget.getAttribute('data-index'));
|
||||||
const { action, to } = this.props.items[i];
|
const { action, to } = this.props.items[i];
|
||||||
|
@ -266,6 +291,9 @@ export default class Dropdown extends React.PureComponent {
|
||||||
size={size}
|
size={size}
|
||||||
ref={this.setTargetRef}
|
ref={this.setTargetRef}
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
|
onMouseDown={this.handleMouseDown}
|
||||||
|
onKeyDown={this.handleButtonKeyDown}
|
||||||
|
onKeyPress={this.handleKeyPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
|
<Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
|
||||||
|
|
|
@ -14,6 +14,7 @@ export default class IconButton extends React.PureComponent {
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
onMouseDown: PropTypes.func,
|
onMouseDown: PropTypes.func,
|
||||||
onKeyDown: PropTypes.func,
|
onKeyDown: PropTypes.func,
|
||||||
|
onKeyPress: PropTypes.func,
|
||||||
size: PropTypes.number,
|
size: PropTypes.number,
|
||||||
active: PropTypes.bool,
|
active: PropTypes.bool,
|
||||||
pressed: 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) => {
|
handleMouseDown = (e) => {
|
||||||
if (!this.props.disabled && this.props.onMouseDown) {
|
if (!this.props.disabled && this.props.onMouseDown) {
|
||||||
this.props.onMouseDown(e);
|
this.props.onMouseDown(e);
|
||||||
|
@ -100,6 +107,7 @@ export default class IconButton extends React.PureComponent {
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
onMouseDown={this.handleMouseDown}
|
onMouseDown={this.handleMouseDown}
|
||||||
onKeyDown={this.handleKeyDown}
|
onKeyDown={this.handleKeyDown}
|
||||||
|
onKeyPress={this.handleKeyPress}
|
||||||
style={style}
|
style={style}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
@ -121,6 +129,7 @@ export default class IconButton extends React.PureComponent {
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
onMouseDown={this.handleMouseDown}
|
onMouseDown={this.handleMouseDown}
|
||||||
onKeyDown={this.handleKeyDown}
|
onKeyDown={this.handleKeyDown}
|
||||||
|
onKeyPress={this.handleKeyPress}
|
||||||
style={style}
|
style={style}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
|
@ -112,7 +112,7 @@ export default class StatusContent extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
onHashtagClick = (hashtag, e) => {
|
onHashtagClick = (hashtag, e) => {
|
||||||
hashtag = hashtag.replace(/^#/, '').toLowerCase();
|
hashtag = hashtag.replace(/^#/, '');
|
||||||
|
|
||||||
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
|
@ -7,12 +7,13 @@ import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
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 { fetchFollowRequests } from 'mastodon/actions/accounts';
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import NavigationBar from '../compose/components/navigation_bar';
|
import NavigationBar from '../compose/components/navigation_bar';
|
||||||
import Icon from 'mastodon/components/icon';
|
import Icon from 'mastodon/components/icon';
|
||||||
import LinkFooter from 'mastodon/features/ui/components/link_footer';
|
import LinkFooter from 'mastodon/features/ui/components/link_footer';
|
||||||
|
import TrendsContainer from './containers/trends_container';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
||||||
|
@ -168,6 +169,8 @@ class GettingStarted extends ImmutablePureComponent {
|
||||||
|
|
||||||
<LinkFooter withHotkeys={multiColumn} />
|
<LinkFooter withHotkeys={multiColumn} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{multiColumn && showTrends && <TrendsContainer />}
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,11 @@ import React from 'react';
|
||||||
import { NavLink, withRouter } from 'react-router-dom';
|
import { NavLink, withRouter } from 'react-router-dom';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import Icon from 'mastodon/components/icon';
|
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 NotificationsCounterIcon from './notifications_counter_icon';
|
||||||
import FollowRequestsNavLink from './follow_requests_nav_link';
|
import FollowRequestsNavLink from './follow_requests_nav_link';
|
||||||
import ListPanel from './list_panel';
|
import ListPanel from './list_panel';
|
||||||
|
import TrendsContainer from 'mastodon/features/getting_started/containers/trends_container';
|
||||||
|
|
||||||
const NavigationPanel = () => (
|
const NavigationPanel = () => (
|
||||||
<div className='navigation-panel'>
|
<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='/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>
|
<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>}
|
{!!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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -23,5 +23,6 @@ export const isStaff = getMeta('is_staff');
|
||||||
export const forceSingleColumn = !getMeta('advanced_layout');
|
export const forceSingleColumn = !getMeta('advanced_layout');
|
||||||
export const useBlurhash = getMeta('use_blurhash');
|
export const useBlurhash = getMeta('use_blurhash');
|
||||||
export const usePendingItems = getMeta('use_pending_items');
|
export const usePendingItems = getMeta('use_pending_items');
|
||||||
|
export const showTrends = getMeta('trends');
|
||||||
|
|
||||||
export default initialState;
|
export default initialState;
|
||||||
|
|
|
@ -31,6 +31,7 @@ import conversations from './conversations';
|
||||||
import suggestions from './suggestions';
|
import suggestions from './suggestions';
|
||||||
import polls from './polls';
|
import polls from './polls';
|
||||||
import identity_proofs from './identity_proofs';
|
import identity_proofs from './identity_proofs';
|
||||||
|
import trends from './trends';
|
||||||
|
|
||||||
const reducers = {
|
const reducers = {
|
||||||
dropdown_menu,
|
dropdown_menu,
|
||||||
|
@ -65,6 +66,7 @@ const reducers = {
|
||||||
conversations,
|
conversations,
|
||||||
suggestions,
|
suggestions,
|
||||||
polls,
|
polls,
|
||||||
|
trends,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default combineReducers(reducers);
|
export default combineReducers(reducers);
|
||||||
|
|
|
@ -12,6 +12,10 @@ const initialState = ImmutableMap({
|
||||||
|
|
||||||
skinTone: 1,
|
skinTone: 1,
|
||||||
|
|
||||||
|
trends: ImmutableMap({
|
||||||
|
show: true,
|
||||||
|
}),
|
||||||
|
|
||||||
home: ImmutableMap({
|
home: ImmutableMap({
|
||||||
shows: ImmutableMap({
|
shows: ImmutableMap({
|
||||||
reblog: true,
|
reblog: true,
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -2212,7 +2212,6 @@ a.account__display-name {
|
||||||
}
|
}
|
||||||
|
|
||||||
.getting-started__wrapper,
|
.getting-started__wrapper,
|
||||||
.getting-started__trends,
|
|
||||||
.search {
|
.search {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
@ -2319,13 +2318,24 @@ a.account__display-name {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
height: calc(100% - 20px);
|
height: calc(100% - 20px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
& > a {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
|
flex: 0 0 auto;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-top: 1px solid lighten($ui-base-color, 4%);
|
border-top: 1px solid lighten($ui-base-color, 4%);
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-spacer {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer__pager {
|
.drawer__pager {
|
||||||
|
@ -2717,8 +2727,10 @@ a.account__display-name {
|
||||||
}
|
}
|
||||||
|
|
||||||
&__trends {
|
&__trends {
|
||||||
background: $ui-base-color;
|
|
||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
|
opacity: 1;
|
||||||
|
animation: fade 150ms linear;
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
@media screen and (max-height: 810px) {
|
@media screen and (max-height: 810px) {
|
||||||
.trends__item:nth-child(3) {
|
.trends__item:nth-child(3) {
|
||||||
|
@ -2735,11 +2747,15 @@ a.account__display-name {
|
||||||
@media screen and (max-height: 670px) {
|
@media screen and (max-height: 670px) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&__scrollable {
|
.trends__item {
|
||||||
max-height: 100%;
|
border-bottom: 0;
|
||||||
overflow-y: auto;
|
padding: 10px;
|
||||||
|
|
||||||
|
&__current {
|
||||||
|
color: $darker-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5968,7 +5984,8 @@ noscript {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
line-height: 36px;
|
line-height: 36px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-align: center;
|
text-align: right;
|
||||||
|
padding-right: 15px;
|
||||||
color: $secondary-text-color;
|
color: $secondary-text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5976,7 +5993,12 @@ noscript {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
width: 50px;
|
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;
|
stroke: lighten($highlight-text-color, 6%) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -324,7 +324,8 @@
|
||||||
&.active h4 {
|
&.active h4 {
|
||||||
&,
|
&,
|
||||||
.fa,
|
.fa,
|
||||||
small {
|
small,
|
||||||
|
.trends__item__current {
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -337,6 +338,10 @@
|
||||||
&.active .avatar-stack .account__avatar {
|
&.active .avatar-stack .account__avatar {
|
||||||
border-color: $ui-highlight-color;
|
border-color: $ui-highlight-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.trends__item__current {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -380,7 +380,7 @@ class Formatter
|
||||||
end
|
end
|
||||||
|
|
||||||
def hashtag_html(tag)
|
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
|
end
|
||||||
|
|
||||||
def mention_html(account)
|
def mention_html(account)
|
||||||
|
|
|
@ -40,6 +40,7 @@ class UserSettingsDecorator
|
||||||
user.settings['default_content_type']= default_content_type_preference if change?('setting_default_content_type')
|
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_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['use_pending_items'] = use_pending_items_preference if change?('setting_use_pending_items')
|
||||||
|
user.settings['trends'] = trends_preference if change?('setting_trends')
|
||||||
end
|
end
|
||||||
|
|
||||||
def merged_notification_emails
|
def merged_notification_emails
|
||||||
|
@ -142,6 +143,10 @@ class UserSettingsDecorator
|
||||||
boolean_cast_setting 'setting_use_pending_items'
|
boolean_cast_setting 'setting_use_pending_items'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def trends_preference
|
||||||
|
boolean_cast_setting 'setting_trends'
|
||||||
|
end
|
||||||
|
|
||||||
def boolean_cast_setting(key)
|
def boolean_cast_setting(key)
|
||||||
ActiveModel::Type::Boolean.new.cast(settings[key])
|
ActiveModel::Type::Boolean.new.cast(settings[key])
|
||||||
end
|
end
|
||||||
|
|
|
@ -231,17 +231,7 @@ class Account < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def tags_as_strings=(tag_names)
|
def tags_as_strings=(tag_names)
|
||||||
tag_names.map! { |name| name.mb_chars.downcase.to_s }
|
hashtags_map = Tag.find_or_create_by_names(tag_names).each_with_object({}) { |tag, h| h[tag.name] = tag }
|
||||||
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
|
|
||||||
|
|
||||||
# Remove hashtags that are to be deleted
|
# Remove hashtags that are to be deleted
|
||||||
tags.each do |tag|
|
tags.each do |tag|
|
||||||
|
|
|
@ -23,7 +23,7 @@ class FeaturedTag < ApplicationRecord
|
||||||
validate :validate_featured_tags_limit, on: :create
|
validate :validate_featured_tags_limit, on: :create
|
||||||
|
|
||||||
def name=(str)
|
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
|
end
|
||||||
|
|
||||||
def increment(timestamp)
|
def increment(timestamp)
|
||||||
|
|
|
@ -35,6 +35,7 @@ class Form::AdminSettings
|
||||||
show_reblogs_in_public_timelines
|
show_reblogs_in_public_timelines
|
||||||
show_replies_in_public_timelines
|
show_replies_in_public_timelines
|
||||||
spam_check_enabled
|
spam_check_enabled
|
||||||
|
trends
|
||||||
).freeze
|
).freeze
|
||||||
|
|
||||||
BOOLEAN_KEYS = %i(
|
BOOLEAN_KEYS = %i(
|
||||||
|
@ -51,6 +52,7 @@ class Form::AdminSettings
|
||||||
show_reblogs_in_public_timelines
|
show_reblogs_in_public_timelines
|
||||||
show_replies_in_public_timelines
|
show_replies_in_public_timelines
|
||||||
spam_check_enabled
|
spam_check_enabled
|
||||||
|
trends
|
||||||
).freeze
|
).freeze
|
||||||
|
|
||||||
UPLOAD_KEYS = %i(
|
UPLOAD_KEYS = %i(
|
||||||
|
|
|
@ -31,7 +31,8 @@ class Tag < ApplicationRecord
|
||||||
|
|
||||||
scope :reviewed, -> { where.not(reviewed_at: nil) }
|
scope :reviewed, -> { where.not(reviewed_at: nil) }
|
||||||
scope :pending_review, -> { where(reviewed_at: nil).where.not(requested_review_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')) }
|
scope :most_used, ->(account) { joins(:statuses).where(statuses: { account: account }).group(:id).order(Arel.sql('count(*) desc')) }
|
||||||
|
|
||||||
delegate :accounts_count,
|
delegate :accounts_count,
|
||||||
|
|
|
@ -66,6 +66,10 @@ class TrendingTags
|
||||||
end
|
end
|
||||||
|
|
||||||
def request_review!(tag)
|
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? }
|
User.staff.includes(:account).find_each { |u| AdminMailer.new_trending_tag(u.account, tag).deliver_later! if u.allows_trending_tag_emails? }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -107,7 +107,9 @@ class User < ApplicationRecord
|
||||||
delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :favourite_modal, :delete_modal,
|
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,
|
:reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_media, :hide_network, :hide_followers_count,
|
||||||
:expand_spoilers, :default_language, :aggregate_reblogs, :show_application,
|
: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_reader :invite_code
|
||||||
attr_writer :external
|
attr_writer :external
|
||||||
|
|
|
@ -34,6 +34,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
||||||
invites_enabled: Setting.min_invite_role == 'user',
|
invites_enabled: Setting.min_invite_role == 'user',
|
||||||
mascot: instance_presenter.mascot&.file&.url,
|
mascot: instance_presenter.mascot&.file&.url,
|
||||||
profile_directory: Setting.profile_directory,
|
profile_directory: Setting.profile_directory,
|
||||||
|
trends: Setting.trends,
|
||||||
}
|
}
|
||||||
|
|
||||||
if object.current_account
|
if object.current_account
|
||||||
|
@ -50,6 +51,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
||||||
store[:use_blurhash] = object.current_account.user.setting_use_blurhash
|
store[:use_blurhash] = object.current_account.user.setting_use_blurhash
|
||||||
store[:use_pending_items] = object.current_account.user.setting_use_pending_items
|
store[:use_pending_items] = object.current_account.user.setting_use_pending_items
|
||||||
store[:is_staff] = object.current_account.user.staff?
|
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
|
store[:default_content_type] = object.current_account.user.setting_default_content_type
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -81,8 +81,8 @@ class BatchedRemoveStatusService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
@tags[status.id].each do |hashtag|
|
@tags[status.id].each do |hashtag|
|
||||||
redis.publish("timeline:hashtag:#{hashtag}", payload)
|
redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", payload)
|
||||||
redis.publish("timeline:hashtag:#{hashtag}:local", payload) if status.local?
|
redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", payload) if status.local?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -77,8 +77,8 @@ class FanOutOnWriteService < BaseService
|
||||||
Rails.logger.debug "Delivering status #{status.id} to hashtags"
|
Rails.logger.debug "Delivering status #{status.id} to hashtags"
|
||||||
|
|
||||||
status.tags.pluck(:name).each do |hashtag|
|
status.tags.pluck(:name).each do |hashtag|
|
||||||
Redis.current.publish("timeline:hashtag:#{hashtag}", @payload)
|
Redis.current.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", @payload)
|
||||||
Redis.current.publish("timeline:hashtag:#{hashtag}:local", @payload) if status.local?
|
Redis.current.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", @payload) if status.local?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -126,8 +126,8 @@ class RemoveStatusService < BaseService
|
||||||
return unless @status.public_visibility?
|
return unless @status.public_visibility?
|
||||||
|
|
||||||
@tags.each do |hashtag|
|
@tags.each do |hashtag|
|
||||||
redis.publish("timeline:hashtag:#{hashtag}", @payload)
|
redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", @payload)
|
||||||
redis.publish("timeline:hashtag:#{hashtag}:local", @payload) if @status.local?
|
redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", @payload) if @status.local?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,8 @@
|
||||||
= feature_hint(link_to(t('admin.dashboard.feature_timeline_preview'), edit_admin_settings_path), @timeline_preview)
|
= feature_hint(link_to(t('admin.dashboard.feature_timeline_preview'), edit_admin_settings_path), @timeline_preview)
|
||||||
%li
|
%li
|
||||||
= feature_hint(link_to(t('admin.dashboard.keybase'), edit_admin_settings_path), @keybase_integration)
|
= 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
|
%li
|
||||||
= feature_hint(link_to(t('admin.dashboard.feature_relay'), admin_relays_path), @relay_enabled)
|
= feature_hint(link_to(t('admin.dashboard.feature_relay'), admin_relays_path), @relay_enabled)
|
||||||
%li
|
%li
|
||||||
|
@ -92,6 +94,10 @@
|
||||||
= feature_hint(t('admin.dashboard.search'), @search_enabled)
|
= feature_hint(t('admin.dashboard.search'), @search_enabled)
|
||||||
%li
|
%li
|
||||||
= feature_hint(t('admin.dashboard.single_user_mode'), @single_user_mode)
|
= 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
|
%li
|
||||||
= feature_hint('LDAP', @ldap_enabled)
|
= feature_hint('LDAP', @ldap_enabled)
|
||||||
%li
|
%li
|
||||||
|
|
|
@ -68,6 +68,9 @@
|
||||||
.fields-group
|
.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')
|
= 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
|
.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')
|
= 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')
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,11 @@
|
||||||
= f.input :setting_reduce_motion, as: :boolean, wrapper: :with_label
|
= f.input :setting_reduce_motion, as: :boolean, wrapper: :with_label
|
||||||
= f.input :setting_system_font_ui, 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'
|
%h4= t 'appearance.confirmation_dialogs'
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
|
|
|
@ -247,6 +247,7 @@ en:
|
||||||
updated_msg: Emoji successfully updated!
|
updated_msg: Emoji successfully updated!
|
||||||
upload: Upload
|
upload: Upload
|
||||||
dashboard:
|
dashboard:
|
||||||
|
authorized_fetch_mode: Authorized fetch mode
|
||||||
backlog: backlogged jobs
|
backlog: backlogged jobs
|
||||||
config: Configuration
|
config: Configuration
|
||||||
feature_deletions: Account deletions
|
feature_deletions: Account deletions
|
||||||
|
@ -271,6 +272,7 @@ en:
|
||||||
week_interactions: interactions this week
|
week_interactions: interactions this week
|
||||||
week_users_active: active this week
|
week_users_active: active this week
|
||||||
week_users_new: users this week
|
week_users_new: users this week
|
||||||
|
whitelist_mode: Whitelist mode
|
||||||
domain_allows:
|
domain_allows:
|
||||||
add_new: Whitelist domain
|
add_new: Whitelist domain
|
||||||
created_msg: Domain has been successfully whitelisted
|
created_msg: Domain has been successfully whitelisted
|
||||||
|
@ -473,8 +475,8 @@ en:
|
||||||
title: Custom terms of service
|
title: Custom terms of service
|
||||||
site_title: Server name
|
site_title: Server name
|
||||||
spam_check_enabled:
|
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.
|
desc_html: Mastodon can auto-silence and auto-report accounts that send repeated unsolicited messages. There may be false positives.
|
||||||
title: Anti-spam
|
title: Anti-spam automation
|
||||||
thumbnail:
|
thumbnail:
|
||||||
desc_html: Used for previews via OpenGraph and API. 1200x630px recommended
|
desc_html: Used for previews via OpenGraph and API. 1200x630px recommended
|
||||||
title: Server thumbnail
|
title: Server thumbnail
|
||||||
|
@ -482,6 +484,9 @@ en:
|
||||||
desc_html: Display public timeline on landing page
|
desc_html: Display public timeline on landing page
|
||||||
title: Timeline preview
|
title: Timeline preview
|
||||||
title: Site settings
|
title: Site settings
|
||||||
|
trends:
|
||||||
|
desc_html: Publicly display previously reviewed hashtags that are currently trending
|
||||||
|
title: Trending hashtags
|
||||||
statuses:
|
statuses:
|
||||||
back_to_account: Back to account page
|
back_to_account: Back to account page
|
||||||
batch:
|
batch:
|
||||||
|
@ -504,6 +509,7 @@ en:
|
||||||
title: Hashtags
|
title: Hashtags
|
||||||
trending_right_now: Trending right now
|
trending_right_now: Trending right now
|
||||||
unique_uses_today: "%{count} posting today"
|
unique_uses_today: "%{count} posting today"
|
||||||
|
updated_msg: Hashtag settings updated successfully
|
||||||
title: Administration
|
title: Administration
|
||||||
warning_presets:
|
warning_presets:
|
||||||
add_new: Add new
|
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.'
|
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
|
animations_and_accessibility: Animations and accessibility
|
||||||
confirmation_dialogs: Confirmation dialogs
|
confirmation_dialogs: Confirmation dialogs
|
||||||
|
discovery: Discovery
|
||||||
sensitive_content: Sensitive content
|
sensitive_content: Sensitive content
|
||||||
application_mailer:
|
application_mailer:
|
||||||
notification_preferences: Change e-mail preferences
|
notification_preferences: Change e-mail preferences
|
||||||
|
@ -574,6 +581,7 @@ en:
|
||||||
status:
|
status:
|
||||||
account_status: Account status
|
account_status: Account status
|
||||||
confirming: Waiting for e-mail confirmation to be completed.
|
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.
|
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?
|
trouble_logging_in: Trouble logging in?
|
||||||
authorize_follow:
|
authorize_follow:
|
||||||
|
|
|
@ -125,6 +125,8 @@ en:
|
||||||
setting_show_application: Disclose application used to send toots
|
setting_show_application: Disclose application used to send toots
|
||||||
setting_skin: Skin
|
setting_skin: Skin
|
||||||
setting_system_font_ui: Use system's default font
|
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_unfollow_modal: Show confirmation dialog before unfollowing someone
|
||||||
setting_use_blurhash: Show colorful gradients for hidden media
|
setting_use_blurhash: Show colorful gradients for hidden media
|
||||||
setting_use_pending_items: Slow mode
|
setting_use_pending_items: Slow mode
|
||||||
|
|
|
@ -38,6 +38,7 @@ defaults: &defaults
|
||||||
advanced_layout: false
|
advanced_layout: false
|
||||||
use_blurhash: true
|
use_blurhash: true
|
||||||
use_pending_items: false
|
use_pending_items: false
|
||||||
|
trends: true
|
||||||
notification_emails:
|
notification_emails:
|
||||||
follow: false
|
follow: false
|
||||||
reblog: false
|
reblog: false
|
||||||
|
|
|
@ -8,8 +8,8 @@ describe Settings::IdentityProofsController do
|
||||||
let(:valid_token) { '1'*66 }
|
let(:valid_token) { '1'*66 }
|
||||||
let(:kbname) { 'kbuser' }
|
let(:kbname) { 'kbuser' }
|
||||||
let(:provider) { 'keybase' }
|
let(:provider) { 'keybase' }
|
||||||
let(:findable_id) { Faker::Number.number(5) }
|
let(:findable_id) { Faker::Number.number(digits: 5) }
|
||||||
let(:unfindable_id) { Faker::Number.number(5) }
|
let(:unfindable_id) { Faker::Number.number(digits: 5) }
|
||||||
let(:new_proof_params) do
|
let(:new_proof_params) do
|
||||||
{ provider: provider, provider_username: kbname, token: valid_token, username: user.account.username }
|
{ provider: provider, provider_username: kbname, token: valid_token, username: user.account.username }
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,7 +4,7 @@ private_key = keypair.to_pem
|
||||||
|
|
||||||
Fabricator(:account) do
|
Fabricator(:account) do
|
||||||
transient :suspended, :silenced
|
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 }
|
last_webfingered_at { Time.now.utc }
|
||||||
public_key { public_key }
|
public_key { public_key }
|
||||||
private_key { private_key }
|
private_key { private_key }
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
Fabricator(:account_identity_proof) do
|
Fabricator(:account_identity_proof) do
|
||||||
account
|
account
|
||||||
provider 'keybase'
|
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] } }
|
token { sequence(:token) { |i| "#{i}#{Faker::Crypto.sha1()*2}"[0..65] } }
|
||||||
verified false
|
verified false
|
||||||
live false
|
live false
|
||||||
|
|
|
@ -607,19 +607,19 @@ RSpec.describe Account, type: :model do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'is invalid if the username is longer then 30 characters' do
|
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?
|
account.valid?
|
||||||
expect(account).to model_have_error_on_field(:username)
|
expect(account).to model_have_error_on_field(:username)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'is invalid if the display name is longer than 30 characters' do
|
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?
|
account.valid?
|
||||||
expect(account).to model_have_error_on_field(:display_name)
|
expect(account).to model_have_error_on_field(:display_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'is invalid if the note is longer than 500 characters' do
|
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?
|
account.valid?
|
||||||
expect(account).to model_have_error_on_field(:note)
|
expect(account).to model_have_error_on_field(:note)
|
||||||
end
|
end
|
||||||
|
@ -653,19 +653,19 @@ RSpec.describe Account, type: :model do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'is valid even if the username is longer then 30 characters' do
|
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?
|
account.valid?
|
||||||
expect(account).not_to model_have_error_on_field(:username)
|
expect(account).not_to model_have_error_on_field(:username)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'is valid even if the display name is longer than 30 characters' do
|
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?
|
account.valid?
|
||||||
expect(account).not_to model_have_error_on_field(:display_name)
|
expect(account).not_to model_have_error_on_field(:display_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'is valid even if the note is longer than 500 characters' do
|
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?
|
account.valid?
|
||||||
expect(account).not_to model_have_error_on_field(:note)
|
expect(account).not_to model_have_error_on_field(:note)
|
||||||
end
|
end
|
||||||
|
@ -804,7 +804,7 @@ RSpec.describe Account, type: :model do
|
||||||
context 'when is local' do
|
context 'when is local' do
|
||||||
# Test disabled because test environment omits autogenerating keys for performance
|
# Test disabled because test environment omits autogenerating keys for performance
|
||||||
xit 'generates keys' do
|
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
|
expect(account.keypair.private?).to eq true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -812,12 +812,12 @@ RSpec.describe Account, type: :model do
|
||||||
context 'when is remote' do
|
context 'when is remote' do
|
||||||
it 'does not generate keys' do
|
it 'does not generate keys' do
|
||||||
key = OpenSSL::PKey::RSA.new(1024).public_key
|
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
|
expect(account.keypair.params).to eq key.params
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'normalizes domain' do
|
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'
|
expect(account.domain).to eq 'xn--r9j5b5b'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -125,7 +125,7 @@ describe Report do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'is invalid if comment is longer than 1000 characters' do
|
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?
|
report.valid?
|
||||||
expect(report).to model_have_error_on_field(:comment)
|
expect(report).to model_have_error_on_field(:comment)
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue