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 = (
+
+ );
+ } else {
+ const html = { __html: emojify(emoji) };
+ content = (
+
+ )
+ }
+
+ return (
+
+ );
+ }
+
+}
+
+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]