Add trends UI with admin and user settings (#11502)
parent
82d2069c75
commit
9072fe5ab6
|
@ -56,6 +56,7 @@ class Settings::PreferencesController < Settings::BaseController
|
|||
:setting_advanced_layout,
|
||||
:setting_use_blurhash,
|
||||
:setting_use_pending_items,
|
||||
:setting_trends,
|
||||
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag),
|
||||
interactions: %i(must_be_follower must_be_following must_be_following_dm)
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
});
|
|
@ -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 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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
@ -22,5 +22,6 @@ export const isStaff = getMeta('is_staff');
|
|||
export const forceSingleColumn = !getMeta('advanced_layout');
|
||||
export const useBlurhash = getMeta('use_blurhash');
|
||||
export const usePendingItems = getMeta('use_pending_items');
|
||||
export const showTrends = getMeta('trends');
|
||||
|
||||
export default initialState;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -12,6 +12,10 @@ const initialState = ImmutableMap({
|
|||
|
||||
skinTone: 1,
|
||||
|
||||
trends: ImmutableMap({
|
||||
show: true,
|
||||
}),
|
||||
|
||||
home: ImmutableMap({
|
||||
shows: ImmutableMap({
|
||||
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__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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ class UserSettingsDecorator
|
|||
user.settings['advanced_layout'] = advanced_layout_preference if change?('setting_advanced_layout')
|
||||
user.settings['use_blurhash'] = use_blurhash_preference if change?('setting_use_blurhash')
|
||||
user.settings['use_pending_items'] = use_pending_items_preference if change?('setting_use_pending_items')
|
||||
user.settings['trends'] = trends_preference if change?('setting_trends')
|
||||
end
|
||||
|
||||
def merged_notification_emails
|
||||
|
@ -122,6 +123,10 @@ class UserSettingsDecorator
|
|||
boolean_cast_setting 'setting_use_pending_items'
|
||||
end
|
||||
|
||||
def trends_preference
|
||||
boolean_cast_setting 'setting_trends'
|
||||
end
|
||||
|
||||
def boolean_cast_setting(key)
|
||||
ActiveModel::Type::Boolean.new.cast(settings[key])
|
||||
end
|
||||
|
|
|
@ -29,6 +29,7 @@ class Form::AdminSettings
|
|||
hero
|
||||
mascot
|
||||
spam_check_enabled
|
||||
trends
|
||||
).freeze
|
||||
|
||||
BOOLEAN_KEYS = %i(
|
||||
|
@ -41,6 +42,7 @@ class Form::AdminSettings
|
|||
preview_sensitive_media
|
||||
profile_directory
|
||||
spam_check_enabled
|
||||
trends
|
||||
).freeze
|
||||
|
||||
UPLOAD_KEYS = %i(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -107,7 +107,8 @@ class User < ApplicationRecord
|
|||
delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal,
|
||||
:reduce_motion, :system_font_ui, :noindex, :theme, :display_media, :hide_network,
|
||||
:expand_spoilers, :default_language, :aggregate_reblogs, :show_application,
|
||||
:advanced_layout, :use_blurhash, :use_pending_items, to: :settings, prefix: :setting, allow_nil: false
|
||||
:advanced_layout, :use_blurhash, :use_pending_items, :trends,
|
||||
to: :settings, prefix: :setting, allow_nil: false
|
||||
|
||||
attr_reader :invite_code
|
||||
attr_writer :external
|
||||
|
|
|
@ -20,6 +20,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
|||
invites_enabled: Setting.min_invite_role == 'user',
|
||||
mascot: instance_presenter.mascot&.file&.url,
|
||||
profile_directory: Setting.profile_directory,
|
||||
trends: Setting.trends,
|
||||
}
|
||||
|
||||
if object.current_account
|
||||
|
@ -35,6 +36,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
|||
store[:use_blurhash] = object.current_account.user.setting_use_blurhash
|
||||
store[:use_pending_items] = object.current_account.user.setting_use_pending_items
|
||||
store[:is_staff] = object.current_account.user.staff?
|
||||
store[:trends] = Setting.trends && object.current_account.user.setting_trends
|
||||
end
|
||||
|
||||
store
|
||||
|
|
|
@ -68,6 +68,9 @@
|
|||
.fields-group
|
||||
= f.input :profile_directory, as: :boolean, wrapper: :with_label, label: t('admin.settings.profile_directory.title'), hint: t('admin.settings.profile_directory.desc_html')
|
||||
|
||||
.fields-group
|
||||
= f.input :trends, as: :boolean, wrapper: :with_label, label: t('admin.settings.trends.title'), hint: t('admin.settings.trends.desc_html')
|
||||
|
||||
.fields-group
|
||||
= f.input :spam_check_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.spam_check_enabled.title'), hint: t('admin.settings.spam_check_enabled.desc_html')
|
||||
|
||||
|
|
|
@ -25,6 +25,11 @@
|
|||
= f.input :setting_reduce_motion, as: :boolean, wrapper: :with_label
|
||||
= f.input :setting_system_font_ui, as: :boolean, wrapper: :with_label
|
||||
|
||||
%h4= t 'appearance.discovery'
|
||||
|
||||
.fields-group
|
||||
= f.input :setting_trends, as: :boolean, wrapper: :with_label
|
||||
|
||||
%h4= t 'appearance.confirmation_dialogs'
|
||||
|
||||
.fields-group
|
||||
|
|
|
@ -460,8 +460,8 @@ en:
|
|||
title: Custom terms of service
|
||||
site_title: Server name
|
||||
spam_check_enabled:
|
||||
desc_html: Mastodon can auto-silence and auto-report accounts based on measures such as detecting accounts who send repeated unsolicited messages. There may be false positives.
|
||||
title: Anti-spam
|
||||
desc_html: Mastodon can auto-silence and auto-report accounts that send repeated unsolicited messages. There may be false positives.
|
||||
title: Anti-spam automation
|
||||
thumbnail:
|
||||
desc_html: Used for previews via OpenGraph and API. 1200x630px recommended
|
||||
title: Server thumbnail
|
||||
|
@ -469,6 +469,9 @@ en:
|
|||
desc_html: Display public timeline on landing page
|
||||
title: Timeline preview
|
||||
title: Site settings
|
||||
trends:
|
||||
desc_html: Publicly display previously reviewed hashtags that are currently trending
|
||||
title: Trending hashtags
|
||||
statuses:
|
||||
back_to_account: Back to account page
|
||||
batch:
|
||||
|
@ -514,6 +517,7 @@ en:
|
|||
advanced_web_interface_hint: 'If you want to make use of your entire screen width, the advanced web interface allows you to configure many different columns to see as much information at the same time as you want: Home, notifications, federated timeline, any number of lists and hashtags.'
|
||||
animations_and_accessibility: Animations and accessibility
|
||||
confirmation_dialogs: Confirmation dialogs
|
||||
discovery: Discovery
|
||||
sensitive_content: Sensitive content
|
||||
application_mailer:
|
||||
notification_preferences: Change e-mail preferences
|
||||
|
|
|
@ -114,6 +114,7 @@ en:
|
|||
setting_show_application: Disclose application used to send toots
|
||||
setting_system_font_ui: Use system's default font
|
||||
setting_theme: Site theme
|
||||
setting_trends: Show today's trends
|
||||
setting_unfollow_modal: Show confirmation dialog before unfollowing someone
|
||||
setting_use_blurhash: Show colorful gradients for hidden media
|
||||
setting_use_pending_items: Slow mode
|
||||
|
|
|
@ -34,6 +34,7 @@ defaults: &defaults
|
|||
advanced_layout: false
|
||||
use_blurhash: true
|
||||
use_pending_items: false
|
||||
trends: true
|
||||
notification_emails:
|
||||
follow: false
|
||||
reblog: false
|
||||
|
|
Reference in New Issue