diff --git a/app/controllers/api/v1/reaction_deck_controller.rb b/app/controllers/api/v1/reaction_deck_controller.rb new file mode 100644 index 0000000000..d3e06f3b09 --- /dev/null +++ b/app/controllers/api/v1/reaction_deck_controller.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +class Api::V1::ReactionDeckController < Api::BaseController + before_action -> { doorkeeper_authorize! :read, :'read:lists' }, only: [:index] + before_action -> { doorkeeper_authorize! :write, :'write:lists' }, only: [:create] + + before_action :require_user! + before_action :set_deck, only: [:index, :create] + + rescue_from ArgumentError do |e| + render json: { error: e.to_s }, status: 422 + end + + def index + render json: @deck + end + + def create + (deck_params['emojis'] || []).each do |data| + raise ArgumentError if data['id'].to_i >= 16 || data['id'].to_i.negative? + + exists = @deck.find { |dd| dd['id'] == data['id'] } + if exists + exists['emoji'] = data['emoji'].delete(':') + else + @deck << { id: data['id'], emoji: data['emoji'].delete(':') } + end + end + @deck = @deck.sort_by { |a| a['id'].to_i } + current_user.settings['reaction_deck'] = @deck.to_json + current_user.save! + + render json: @deck + end + + private + + def set_deck + @deck = current_user.setting_reaction_deck ? JSON.parse(current_user.setting_reaction_deck) : [] + end + + def deck_params + params + end +end diff --git a/app/javascript/mastodon/actions/reaction_deck.js b/app/javascript/mastodon/actions/reaction_deck.js new file mode 100644 index 0000000000..2914e8ceef --- /dev/null +++ b/app/javascript/mastodon/actions/reaction_deck.js @@ -0,0 +1,79 @@ +import api from '../api'; + +export const REACTION_DECK_FETCH_REQUEST = 'REACTION_DECK_FETCH_REQUEST'; +export const REACTION_DECK_FETCH_SUCCESS = 'REACTION_DECK_FETCH_SUCCESS'; +export const REACTION_DECK_FETCH_FAIL = 'REACTION_DECK_FETCH_FAIL'; + +export const REACTION_DECK_UPDATE_REQUEST = 'REACTION_DECK_UPDATE_REQUEST'; +export const REACTION_DECK_UPDATE_SUCCESS = 'REACTION_DECK_UPDATE_SUCCESS'; +export const REACTION_DECK_UPDATE_FAIL = 'REACTION_DECK_UPDATE_FAIL'; + +export function fetchReactionDeck() { + return (dispatch, getState) => { + dispatch(fetchReactionDeckRequest()); + + api(getState).get('/api/v1/reaction_deck').then(response => { + dispatch(fetchReactionDeckSuccess(response.data)); + }).catch(error => { + dispatch(fetchReactionDeckFail(error)); + }); + }; +} + +export function fetchReactionDeckRequest() { + return { + type: REACTION_DECK_FETCH_REQUEST, + skipLoading: true, + }; +} + +export function fetchReactionDeckSuccess(emojis) { + return { + type: REACTION_DECK_FETCH_SUCCESS, + emojis, + skipLoading: true, + }; +} + +export function fetchReactionDeckFail(error) { + return { + type: REACTION_DECK_FETCH_FAIL, + error, + skipLoading: true, + }; +} + +export function updateReactionDeck(id, emoji) { + return (dispatch, getState) => { + dispatch(updateReactionDeckRequest()); + + api(getState).post('/api/v1/reaction_deck', { emojis: [{ id, emoji: emoji.native || emoji.id }] }).then(response => { + dispatch(updateReactionDeckSuccess(response.data)); + }).catch(error => { + dispatch(updateReactionDeckFail(error)); + }); + }; +} + +export function updateReactionDeckRequest() { + return { + type: REACTION_DECK_UPDATE_REQUEST, + skipLoading: true, + }; +} + +export function updateReactionDeckSuccess(emojis) { + return { + type: REACTION_DECK_UPDATE_SUCCESS, + emojis, + skipLoading: true, + }; +} + +export function updateReactionDeckFail(error) { + return { + type: REACTION_DECK_UPDATE_FAIL, + error, + skipLoading: true, + }; +} diff --git a/app/javascript/mastodon/containers/mastodon.jsx b/app/javascript/mastodon/containers/mastodon.jsx index 5be163f5a4..4626968af9 100644 --- a/app/javascript/mastodon/containers/mastodon.jsx +++ b/app/javascript/mastodon/containers/mastodon.jsx @@ -11,6 +11,7 @@ import { Provider as ReduxProvider } from 'react-redux'; import { ScrollContext } from 'react-router-scroll-4'; import { fetchCustomEmojis } from 'mastodon/actions/custom_emojis'; +import { fetchReactionDeck } from 'mastodon/actions/reaction_deck'; import { hydrateStore } from 'mastodon/actions/store'; import { connectUserStream } from 'mastodon/actions/streaming'; import ErrorBoundary from 'mastodon/components/error_boundary'; @@ -29,6 +30,7 @@ const hydrateAction = hydrateStore(initialState); store.dispatch(hydrateAction); if (initialState.meta.me) { store.dispatch(fetchCustomEmojis()); + store.dispatch(fetchReactionDeck()); } const createIdentityContext = state => ({ diff --git a/app/javascript/mastodon/features/compose/components/action_bar.jsx b/app/javascript/mastodon/features/compose/components/action_bar.jsx index 08d98b6c4d..3c4d33683f 100644 --- a/app/javascript/mastodon/features/compose/components/action_bar.jsx +++ b/app/javascript/mastodon/features/compose/components/action_bar.jsx @@ -11,6 +11,7 @@ const messages = defineMessages({ edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' }, preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, + reaction_deck: { id: 'navigation_bar.reaction_deck', defaultMessage: 'Reaction deck' }, follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, emoji_reactions: { id: 'navigation_bar.emoji_reactions', defaultMessage: 'Stamps' }, @@ -44,6 +45,7 @@ class ActionBar extends PureComponent { menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' }); menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' }); + menu.push({ text: intl.formatMessage(messages.reaction_deck), to: '/reaction_deck' }); menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' }); menu.push(null); menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' }); diff --git a/app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js b/app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js index a0e50029df..2fc8a6c59f 100644 --- a/app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js +++ b/app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js @@ -1,11 +1,16 @@ -import { Map as ImmutableMap } from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; +import { hideRecentEmojis } from 'mastodon/initial_state'; + import { useEmoji } from '../../../actions/emojis'; import { changeSetting } from '../../../actions/settings'; +import { shortCodes } from '../../emoji/emoji_mart_data_light'; import EmojiPickerDropdown from '../components/emoji_picker_dropdown'; + + const perLine = 8; const lines = 2; @@ -28,21 +33,43 @@ const DEFAULTS = [ 'ok_hand', ]; -const getFrequentlyUsedEmojis = createSelector([ - state => state.getIn(['settings', 'frequentlyUsedEmojis'], ImmutableMap()), -], emojiCounters => { - let emojis = emojiCounters - .keySeq() - .sort((a, b) => emojiCounters.get(a) - emojiCounters.get(b)) - .reverse() - .slice(0, perLine * lines) - .toArray(); +const RECENT_SIZE = DEFAULTS.length; +const DECK_SIZE = 16; - if (emojis.length < DEFAULTS.length) { - let uniqueDefaults = DEFAULTS.filter(emoji => !emojis.includes(emoji)); - emojis = emojis.concat(uniqueDefaults.slice(0, DEFAULTS.length - emojis.length)); +const getFrequentlyUsedEmojis = createSelector([ + state => { return { + emojiCounters: state.getIn(['settings', 'frequentlyUsedEmojis'], ImmutableMap()), + reactionDeck: state.get('reaction_deck', ImmutableList()), + }; }, +], data => { + const { emojiCounters, reactionDeck } = data; + let deckEmojis = reactionDeck + .toArray() + .map((e) => e.get('emoji')) + .filter((e) => e) + .map((e) => shortCodes[e] || e); + deckEmojis = [...new Set(deckEmojis)]; + + let emojis; + if (!hideRecentEmojis) { + emojis = emojiCounters + .keySeq() + .filter((ee) => deckEmojis.indexOf(ee) < 0) + .sort((a, b) => emojiCounters.get(a) - emojiCounters.get(b)) + .reverse() + .slice(0, perLine * lines) + .toArray(); + + if (emojis.length < RECENT_SIZE) { + let uniqueDefaults = DEFAULTS.filter(emoji => !emojis.includes(emoji)); + emojis = emojis.concat(uniqueDefaults.slice(0, RECENT_SIZE - emojis.length)); + } + } else { + emojis = []; } + emojis = deckEmojis.slice(0, DECK_SIZE).concat(emojis); + return emojis; }); diff --git a/app/javascript/mastodon/features/emoji/emoji_mart_data_light.js b/app/javascript/mastodon/features/emoji/emoji_mart_data_light.js index 11698937c0..32565b450d 100644 --- a/app/javascript/mastodon/features/emoji/emoji_mart_data_light.js +++ b/app/javascript/mastodon/features/emoji/emoji_mart_data_light.js @@ -7,6 +7,7 @@ import { unicodeToUnifiedName } from './unicode_to_unified_name'; const [ shortCodesToEmojiData, skins, categories, short_names ] = emojiCompressed; const emojis = {}; +const shortCodes = {}; // decompress Object.keys(shortCodesToEmojiData).forEach((shortCode) => { @@ -33,10 +34,12 @@ Object.keys(shortCodesToEmojiData).forEach((shortCode) => { short_names, unified, }; + shortCodes[native] = shortCode; }); export { emojis, + shortCodes, skins, categories, short_names, diff --git a/app/javascript/mastodon/features/reaction_deck/components/reaction_emoji.jsx b/app/javascript/mastodon/features/reaction_deck/components/reaction_emoji.jsx new file mode 100644 index 0000000000..26036c4901 --- /dev/null +++ b/app/javascript/mastodon/features/reaction_deck/components/reaction_emoji.jsx @@ -0,0 +1,75 @@ +import PropTypes from 'prop-types'; + +import { injectIntl } from 'react-intl'; + +import { List as ImmutableList, Map as ImmutableMap } from 'immutable'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; + +import { updateReactionDeck } from 'mastodon/actions/reaction_deck'; +import EmojiPickerDropdownContainer from 'mastodon/features/compose/containers/emoji_picker_dropdown_container'; +import emojify from 'mastodon/features/emoji/emoji'; +import { autoPlayGif } from 'mastodon/initial_state'; + +const MapStateToProps = (state, { emojiId, emojiMap }) => ({ + emoji: (state.get('reaction_deck', ImmutableList()).toArray().find(em => em.get('id') === emojiId) || ImmutableMap({ emoji: '' })).get('emoji'), + emojiMap, +}); + +const mapDispatchToProps = (dispatch, { emojiId }) => ({ + onChange: (emoji) => dispatch(updateReactionDeck(emojiId, emoji)), +}); + +class ReactionEmoji extends ImmutablePureComponent { + + static propTypes = { + emoji: PropTypes.string, + emojiMap: ImmutablePropTypes.map.isRequired, + onChange: PropTypes.func.isRequired, + }; + + static defaultProps = { + emoji: '', + }; + + render () { + const { emojiMap, emoji, onChange } = this.props; + + let content = null; + + if (emojiMap.get(emoji)) { + const filename = autoPlayGif ? emojiMap.getIn([emoji, 'url']) : emojiMap.getIn([emoji, 'static_url']); + const shortCode = `:${emoji}:`; + + content = ( + {shortCode} + ); + } else { + const html = { __html: emojify(emoji) }; + content = ( + + ) + } + + return ( +
+
+ +
+ {content} +
+
+
+ ); + } + +} + +export default connect(MapStateToProps, mapDispatchToProps)(injectIntl(ReactionEmoji)); diff --git a/app/javascript/mastodon/features/reaction_deck/index.jsx b/app/javascript/mastodon/features/reaction_deck/index.jsx new file mode 100644 index 0000000000..a457272da5 --- /dev/null +++ b/app/javascript/mastodon/features/reaction_deck/index.jsx @@ -0,0 +1,86 @@ +import PropTypes from 'prop-types'; + +import { defineMessages, injectIntl } from 'react-intl'; + +import { Helmet } from 'react-helmet'; + +import { Map as ImmutableMap } from 'immutable'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; + +import ColumnHeader from 'mastodon/components/column_header'; +import LoadingIndicator from 'mastodon/components/loading_indicator'; +import ScrollableList from 'mastodon/components/scrollable_list'; +import Column from 'mastodon/features/ui/components/column'; + +import ReactionEmoji from './components/reaction_emoji'; + + +const DECK_SIZE = 16; + +const customEmojiMap = createSelector([state => state.get('custom_emojis')], items => items.reduce((map, emoji) => map.set(emoji.get('shortcode'), emoji), ImmutableMap())); + +const messages = defineMessages({ + refresh: { id: 'refresh', defaultMessage: 'Refresh' }, + heading: { id: 'column.reaction_deck', defaultMessage: 'Reaction deck' }, +}); + +const mapStateToProps = (state, props) => ({ + accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]), + deck: state.get('reaction_deck'), + emojiMap: customEmojiMap(state), +}); + +class ReactionDeck extends ImmutablePureComponent { + + static propTypes = { + params: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + deck: ImmutablePropTypes.list, + emojiMap: ImmutablePropTypes.map, + multiColumn: PropTypes.bool, + intl: PropTypes.object.isRequired, + }; + + render () { + const { intl, deck, emojiMap, multiColumn } = this.props; + + if (!deck) { + return ( + + + + ); + } + + + return ( + + + + + {[...Array(DECK_SIZE).keys()].map(emojiId => + + )} + + + + + + + ); + } + +} + +export default connect(mapStateToProps)(injectIntl(ReactionDeck)); diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index 8528bca4ae..b765ee568b 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -62,6 +62,7 @@ import { Lists, Directory, Explore, + ReactionDeck, Onboarding, About, PrivacyPolicy, @@ -203,6 +204,8 @@ class SwitchingColumnsArea extends PureComponent { + + diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index d7705a3069..d05b8b88e4 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -166,6 +166,10 @@ export function Onboarding () { return import(/* webpackChunkName: "features/onboarding" */'../../onboarding'); } +export function ReactionDeck () { + return import(/* webpackChunkName: "features/reaction_deck" */'../../reaction_deck'); +} + export function CompareHistoryModal () { return import(/*webpackChunkName: "modals/compare_history_modal" */'../components/compare_history_modal'); } diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index 991af7eb24..ed070ddb74 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -60,6 +60,7 @@ * @property {string} domain * @property {boolean} enable_login_privacy * @property {boolean=} expand_spoilers + * @property {boolean} hide_recent_emojis * @property {boolean} limited_federation_mode * @property {string} locale * @property {string | null} mascot @@ -115,6 +116,7 @@ export const domain = getMeta('domain'); export const enableLoginPrivacy = getMeta('enable_login_privacy'); export const expandSpoilers = getMeta('expand_spoilers'); export const forceSingleColumn = !getMeta('advanced_layout'); +export const hideRecentEmojis = getMeta('hide_recent_emojis'); export const limitedFederationMode = getMeta('limited_federation_mode'); export const mascot = getMeta('mascot'); export const me = getMeta('me'); diff --git a/app/javascript/mastodon/reducers/index.ts b/app/javascript/mastodon/reducers/index.ts index 6e3648b392..728a66ef6e 100644 --- a/app/javascript/mastodon/reducers/index.ts +++ b/app/javascript/mastodon/reducers/index.ts @@ -34,6 +34,7 @@ import notifications from './notifications'; import picture_in_picture from './picture_in_picture'; import polls from './polls'; import push_notifications from './push_notifications'; +import reaction_deck from './reaction_deck'; import relationships from './relationships'; import search from './search'; import server from './server'; @@ -92,6 +93,7 @@ const reducers = { history, tags, followed_tags, + reaction_deck, }; const rootReducer = combineReducers(reducers); diff --git a/app/javascript/mastodon/reducers/reaction_deck.js b/app/javascript/mastodon/reducers/reaction_deck.js new file mode 100644 index 0000000000..12b4e8ef21 --- /dev/null +++ b/app/javascript/mastodon/reducers/reaction_deck.js @@ -0,0 +1,13 @@ +import { List as ImmutableList, fromJS as ConvertToImmutable } from 'immutable'; + +import { REACTION_DECK_FETCH_SUCCESS, REACTION_DECK_UPDATE_SUCCESS } from '../actions/reaction_deck'; + +const initialState = ImmutableList([]); + +export default function reaction_deck(state = initialState, action) { + if(action.type === REACTION_DECK_FETCH_SUCCESS || action.type === REACTION_DECK_UPDATE_SUCCESS) { + state = ConvertToImmutable(action.emojis); + } + + return state; +} diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index befdbf4031..7b5eff3f5c 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -7544,6 +7544,26 @@ noscript { } } +.reaction_deck__emoji { + &__wrapper { + display: flex; + + margin: 8px 4px; + height: 32px; + + .emojione { + min-width: 24px; + height: 24px; + } + + .emoji-button { + margin-left: 20px; + margin-right: 24px; + padding: 0; + } + } +} + .focal-point { position: relative; cursor: move; diff --git a/app/models/concerns/has_user_settings.rb b/app/models/concerns/has_user_settings.rb index 0c56e724b9..8f8657fcd7 100644 --- a/app/models/concerns/has_user_settings.rb +++ b/app/models/concerns/has_user_settings.rb @@ -23,10 +23,18 @@ module HasUserSettings settings['web.auto_play'] end + def setting_reaction_deck + settings['reaction_deck'] + end + def setting_enable_login_privacy settings['web.enable_login_privacy'] end + def setting_hide_recent_emojis + settings['web.hide_recent_emojis'] + end + def setting_default_sensitive settings['default_sensitive'] end diff --git a/app/models/user_settings.rb b/app/models/user_settings.rb index 2634fc48c2..918c7b0747 100644 --- a/app/models/user_settings.rb +++ b/app/models/user_settings.rb @@ -25,6 +25,7 @@ class UserSettings setting :reject_public_unlisted_subscription, default: false setting :reject_unlisted_subscription, default: false setting :send_without_domain_blocks, default: false + setting :reaction_deck, default: nil setting :stop_emoji_reaction_streaming, default: false setting :emoji_reaction_streaming_notify_impl2, default: false @@ -38,6 +39,7 @@ class UserSettings setting :disable_swiping, default: false setting :delete_modal, default: true setting :enable_login_privacy, default: false + setting :hide_recent_emojis, default: false setting :reblog_modal, default: false setting :unfollow_modal, default: true setting :reduce_motion, default: false diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index c2206eef05..f87e3c4aea 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -44,6 +44,7 @@ class InitialStateSerializer < ActiveModel::Serializer store[:display_media_expand] = object.current_account.user.setting_display_media_expand store[:expand_spoilers] = object.current_account.user.setting_expand_spoilers store[:enable_login_privacy] = object.current_account.user.setting_enable_login_privacy + store[:hide_recent_emojis] = object.current_account.user.setting_hide_recent_emojis store[:reduce_motion] = object.current_account.user.setting_reduce_motion store[:disable_swiping] = object.current_account.user.setting_disable_swiping store[:advanced_layout] = object.current_account.user.setting_advanced_layout diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index b25fba1475..833fc93d07 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -85,6 +85,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer max_reactions_per_account: EmojiReaction::EMOJI_REACTION_PER_ACCOUNT_LIMIT, }, + reaction_deck: { + max_items: 16, + }, + reactions: { max_reactions: EmojiReaction::EMOJI_REACTION_PER_ACCOUNT_LIMIT, }, @@ -110,6 +114,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer :kmyblue_searchability, :searchability, :kmyblue_markdown, + :kmyblue_reaction_deck, ] capabilities << :profile_search unless Chewy.enabled? diff --git a/app/serializers/rest/v1/instance_serializer.rb b/app/serializers/rest/v1/instance_serializer.rb index c10520a606..b5a1d3abe6 100644 --- a/app/serializers/rest/v1/instance_serializer.rb +++ b/app/serializers/rest/v1/instance_serializer.rb @@ -91,6 +91,10 @@ class REST::V1::InstanceSerializer < ActiveModel::Serializer max_reactions_per_account: EmojiReaction::EMOJI_REACTION_PER_ACCOUNT_LIMIT, }, + reaction_deck: { + max_items: 16, + }, + reactions: { max_reactions: EmojiReaction::EMOJI_REACTION_PER_ACCOUNT_LIMIT, }, @@ -119,6 +123,7 @@ class REST::V1::InstanceSerializer < ActiveModel::Serializer :kmyblue_searchability, :searchability, :kmyblue_markdown, + :kmyblue_reaction_deck, ] capabilities << :profile_search unless Chewy.enabled? diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml index 0ca5df5eac..43c3be19df 100644 --- a/app/views/settings/preferences/appearance/show.html.haml +++ b/app/views/settings/preferences/appearance/show.html.haml @@ -39,6 +39,9 @@ .fields-group = ff.input :'web.crop_images', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_crop_images') + .fields-group + = ff.input :'web.hide_recent_emojis', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_hide_recent_emojis'), hint: false + .fields-group = ff.input :emoji_reaction_streaming_notify_impl2, as: :boolean, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_emoji_reaction_streaming_notify_impl2'), hint: I18n.t('simple_form.hints.defaults.setting_emoji_reaction_streaming_notify_impl2') diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index bfae64a85c..8e4b4dd45f 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -225,6 +225,7 @@ en: setting_hide_followers_count: Hide followers count setting_hide_following_count: Hide following count setting_hide_network: Hide your social graph + setting_hide_recent_emojis: Hide recent emojis setting_hide_statuses_count: Hide statuses count setting_noai: Set noai meta tags setting_noindex: Opt-out of search engine indexing diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index 817ca05d6f..5e0a8c9076 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -233,6 +233,7 @@ ja: setting_hide_followers_count: フォロワー数を隠す setting_hide_following_count: フォロー数を隠す setting_hide_network: 繋がりを隠す + setting_hide_recent_emojis: 絵文字ピッカーで最近使用した絵文字を隠す(リアクションデッキのみを表示する) setting_hide_statuses_count: 投稿数を隠す setting_noai: 自分のコンテンツのAI学習利用に対して不快感を表明する setting_noindex: 検索エンジンによるインデックスを拒否する diff --git a/config/routes.rb b/config/routes.rb index d57c32505d..b3b37ff591 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -19,6 +19,7 @@ Rails.application.routes.draw do /emoji_reactions /bookmarks /pinned + /reaction_deck /start /directory /explore/(*any) diff --git a/config/routes/api.rb b/config/routes/api.rb index 5590b5e4e5..57543be8fb 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -54,6 +54,7 @@ namespace :api, format: false do get '/streaming/(*any)', to: 'streaming#index' resources :custom_emojis, only: [:index] + resources :reaction_deck, only: [:index, :create] resources :suggestions, only: [:index, :destroy] resources :scheduled_statuses, only: [:index, :show, :update, :destroy] resources :preferences, only: [:index]