Add trends UI with admin and user settings (#11502)
This commit is contained in:
		| @ -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) | ||||
|     ) | ||||
|  | ||||
							
								
								
									
										32
									
								
								app/javascript/mastodon/actions/trends.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								app/javascript/mastodon/actions/trends.js
									
									
									
									
									
										Normal 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, | ||||
| }); | ||||
| @ -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, | ||||
|  | ||||
							
								
								
									
										23
									
								
								app/javascript/mastodon/reducers/trends.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								app/javascript/mastodon/reducers/trends.js
									
									
									
									
									
										Normal 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; | ||||
|   } | ||||
| }; | ||||
| @ -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 | ||||
|  | ||||
		Reference in New Issue
	
	Block a user