Add reaction deck
This commit is contained in:
parent
db5c358f4f
commit
f1625fe101
24 changed files with 404 additions and 13 deletions
45
app/controllers/api/v1/reaction_deck_controller.rb
Normal file
45
app/controllers/api/v1/reaction_deck_controller.rb
Normal file
|
@ -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
|
79
app/javascript/mastodon/actions/reaction_deck.js
Normal file
79
app/javascript/mastodon/actions/reaction_deck.js
Normal file
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import { Provider as ReduxProvider } from 'react-redux';
|
||||||
import { ScrollContext } from 'react-router-scroll-4';
|
import { ScrollContext } from 'react-router-scroll-4';
|
||||||
|
|
||||||
import { fetchCustomEmojis } from 'mastodon/actions/custom_emojis';
|
import { fetchCustomEmojis } from 'mastodon/actions/custom_emojis';
|
||||||
|
import { fetchReactionDeck } from 'mastodon/actions/reaction_deck';
|
||||||
import { hydrateStore } from 'mastodon/actions/store';
|
import { hydrateStore } from 'mastodon/actions/store';
|
||||||
import { connectUserStream } from 'mastodon/actions/streaming';
|
import { connectUserStream } from 'mastodon/actions/streaming';
|
||||||
import ErrorBoundary from 'mastodon/components/error_boundary';
|
import ErrorBoundary from 'mastodon/components/error_boundary';
|
||||||
|
@ -29,6 +30,7 @@ const hydrateAction = hydrateStore(initialState);
|
||||||
store.dispatch(hydrateAction);
|
store.dispatch(hydrateAction);
|
||||||
if (initialState.meta.me) {
|
if (initialState.meta.me) {
|
||||||
store.dispatch(fetchCustomEmojis());
|
store.dispatch(fetchCustomEmojis());
|
||||||
|
store.dispatch(fetchReactionDeck());
|
||||||
}
|
}
|
||||||
|
|
||||||
const createIdentityContext = state => ({
|
const createIdentityContext = state => ({
|
||||||
|
|
|
@ -11,6 +11,7 @@ const messages = defineMessages({
|
||||||
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||||
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
|
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
|
||||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
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' },
|
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||||
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
||||||
emoji_reactions: { id: 'navigation_bar.emoji_reactions', defaultMessage: 'Stamps' },
|
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.edit_profile), href: '/settings/profile' });
|
||||||
menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' });
|
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({ text: intl.formatMessage(messages.pins), to: '/pinned' });
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
|
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
|
||||||
|
|
|
@ -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 { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
import { hideRecentEmojis } from 'mastodon/initial_state';
|
||||||
|
|
||||||
import { useEmoji } from '../../../actions/emojis';
|
import { useEmoji } from '../../../actions/emojis';
|
||||||
import { changeSetting } from '../../../actions/settings';
|
import { changeSetting } from '../../../actions/settings';
|
||||||
|
import { shortCodes } from '../../emoji/emoji_mart_data_light';
|
||||||
import EmojiPickerDropdown from '../components/emoji_picker_dropdown';
|
import EmojiPickerDropdown from '../components/emoji_picker_dropdown';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const perLine = 8;
|
const perLine = 8;
|
||||||
const lines = 2;
|
const lines = 2;
|
||||||
|
|
||||||
|
@ -28,21 +33,43 @@ const DEFAULTS = [
|
||||||
'ok_hand',
|
'ok_hand',
|
||||||
];
|
];
|
||||||
|
|
||||||
const getFrequentlyUsedEmojis = createSelector([
|
const RECENT_SIZE = DEFAULTS.length;
|
||||||
state => state.getIn(['settings', 'frequentlyUsedEmojis'], ImmutableMap()),
|
const DECK_SIZE = 16;
|
||||||
], emojiCounters => {
|
|
||||||
let emojis = emojiCounters
|
|
||||||
.keySeq()
|
|
||||||
.sort((a, b) => emojiCounters.get(a) - emojiCounters.get(b))
|
|
||||||
.reverse()
|
|
||||||
.slice(0, perLine * lines)
|
|
||||||
.toArray();
|
|
||||||
|
|
||||||
if (emojis.length < DEFAULTS.length) {
|
const getFrequentlyUsedEmojis = createSelector([
|
||||||
let uniqueDefaults = DEFAULTS.filter(emoji => !emojis.includes(emoji));
|
state => { return {
|
||||||
emojis = emojis.concat(uniqueDefaults.slice(0, DEFAULTS.length - emojis.length));
|
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;
|
return emojis;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { unicodeToUnifiedName } from './unicode_to_unified_name';
|
||||||
const [ shortCodesToEmojiData, skins, categories, short_names ] = emojiCompressed;
|
const [ shortCodesToEmojiData, skins, categories, short_names ] = emojiCompressed;
|
||||||
|
|
||||||
const emojis = {};
|
const emojis = {};
|
||||||
|
const shortCodes = {};
|
||||||
|
|
||||||
// decompress
|
// decompress
|
||||||
Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
|
Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
|
||||||
|
@ -33,10 +34,12 @@ Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
|
||||||
short_names,
|
short_names,
|
||||||
unified,
|
unified,
|
||||||
};
|
};
|
||||||
|
shortCodes[native] = shortCode;
|
||||||
});
|
});
|
||||||
|
|
||||||
export {
|
export {
|
||||||
emojis,
|
emojis,
|
||||||
|
shortCodes,
|
||||||
skins,
|
skins,
|
||||||
categories,
|
categories,
|
||||||
short_names,
|
short_names,
|
||||||
|
|
|
@ -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 = (
|
||||||
|
<img
|
||||||
|
draggable='false'
|
||||||
|
className='emojione custom-emoji'
|
||||||
|
alt={shortCode}
|
||||||
|
title={shortCode}
|
||||||
|
src={filename}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const html = { __html: emojify(emoji) };
|
||||||
|
content = (
|
||||||
|
<span dangerouslySetInnerHTML={html} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='reaction_deck__emoji'>
|
||||||
|
<div className='reaction_deck__emoji__wrapper'>
|
||||||
|
<EmojiPickerDropdownContainer onPickEmoji={onChange} />
|
||||||
|
<div>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(MapStateToProps, mapDispatchToProps)(injectIntl(ReactionEmoji));
|
86
app/javascript/mastodon/features/reaction_deck/index.jsx
Normal file
86
app/javascript/mastodon/features/reaction_deck/index.jsx
Normal file
|
@ -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 (
|
||||||
|
<Column>
|
||||||
|
<LoadingIndicator />
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column bindToDocument={!multiColumn}>
|
||||||
|
<ColumnHeader
|
||||||
|
icon='smile-o'
|
||||||
|
title={intl.formatMessage(messages.heading)}
|
||||||
|
multiColumn={multiColumn}
|
||||||
|
showBackButton
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ScrollableList
|
||||||
|
scrollKey='reaction_deck'
|
||||||
|
bindToDocument={!multiColumn}
|
||||||
|
>
|
||||||
|
{[...Array(DECK_SIZE).keys()].map(emojiId =>
|
||||||
|
<ReactionEmoji key={emojiId} emojiMap={emojiMap} emojiId={emojiId} />
|
||||||
|
)}
|
||||||
|
</ScrollableList>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<meta name='robots' content='noindex' />
|
||||||
|
</Helmet>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(injectIntl(ReactionDeck));
|
|
@ -62,6 +62,7 @@ import {
|
||||||
Lists,
|
Lists,
|
||||||
Directory,
|
Directory,
|
||||||
Explore,
|
Explore,
|
||||||
|
ReactionDeck,
|
||||||
Onboarding,
|
Onboarding,
|
||||||
About,
|
About,
|
||||||
PrivacyPolicy,
|
PrivacyPolicy,
|
||||||
|
@ -203,6 +204,8 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
|
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
|
||||||
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
|
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
|
||||||
|
|
||||||
|
<WrappedRoute path='/reaction_deck' component={ReactionDeck} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path='/start' exact component={Onboarding} content={children} />
|
<WrappedRoute path='/start' exact component={Onboarding} content={children} />
|
||||||
<WrappedRoute path='/directory' component={Directory} content={children} />
|
<WrappedRoute path='/directory' component={Directory} content={children} />
|
||||||
<WrappedRoute path={['/explore', '/search']} component={Explore} content={children} />
|
<WrappedRoute path={['/explore', '/search']} component={Explore} content={children} />
|
||||||
|
|
|
@ -166,6 +166,10 @@ export function Onboarding () {
|
||||||
return import(/* webpackChunkName: "features/onboarding" */'../../onboarding');
|
return import(/* webpackChunkName: "features/onboarding" */'../../onboarding');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ReactionDeck () {
|
||||||
|
return import(/* webpackChunkName: "features/reaction_deck" */'../../reaction_deck');
|
||||||
|
}
|
||||||
|
|
||||||
export function CompareHistoryModal () {
|
export function CompareHistoryModal () {
|
||||||
return import(/*webpackChunkName: "modals/compare_history_modal" */'../components/compare_history_modal');
|
return import(/*webpackChunkName: "modals/compare_history_modal" */'../components/compare_history_modal');
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,7 @@
|
||||||
* @property {string} domain
|
* @property {string} domain
|
||||||
* @property {boolean} enable_login_privacy
|
* @property {boolean} enable_login_privacy
|
||||||
* @property {boolean=} expand_spoilers
|
* @property {boolean=} expand_spoilers
|
||||||
|
* @property {boolean} hide_recent_emojis
|
||||||
* @property {boolean} limited_federation_mode
|
* @property {boolean} limited_federation_mode
|
||||||
* @property {string} locale
|
* @property {string} locale
|
||||||
* @property {string | null} mascot
|
* @property {string | null} mascot
|
||||||
|
@ -115,6 +116,7 @@ export const domain = getMeta('domain');
|
||||||
export const enableLoginPrivacy = getMeta('enable_login_privacy');
|
export const enableLoginPrivacy = getMeta('enable_login_privacy');
|
||||||
export const expandSpoilers = getMeta('expand_spoilers');
|
export const expandSpoilers = getMeta('expand_spoilers');
|
||||||
export const forceSingleColumn = !getMeta('advanced_layout');
|
export const forceSingleColumn = !getMeta('advanced_layout');
|
||||||
|
export const hideRecentEmojis = getMeta('hide_recent_emojis');
|
||||||
export const limitedFederationMode = getMeta('limited_federation_mode');
|
export const limitedFederationMode = getMeta('limited_federation_mode');
|
||||||
export const mascot = getMeta('mascot');
|
export const mascot = getMeta('mascot');
|
||||||
export const me = getMeta('me');
|
export const me = getMeta('me');
|
||||||
|
|
|
@ -34,6 +34,7 @@ import notifications from './notifications';
|
||||||
import picture_in_picture from './picture_in_picture';
|
import picture_in_picture from './picture_in_picture';
|
||||||
import polls from './polls';
|
import polls from './polls';
|
||||||
import push_notifications from './push_notifications';
|
import push_notifications from './push_notifications';
|
||||||
|
import reaction_deck from './reaction_deck';
|
||||||
import relationships from './relationships';
|
import relationships from './relationships';
|
||||||
import search from './search';
|
import search from './search';
|
||||||
import server from './server';
|
import server from './server';
|
||||||
|
@ -92,6 +93,7 @@ const reducers = {
|
||||||
history,
|
history,
|
||||||
tags,
|
tags,
|
||||||
followed_tags,
|
followed_tags,
|
||||||
|
reaction_deck,
|
||||||
};
|
};
|
||||||
|
|
||||||
const rootReducer = combineReducers(reducers);
|
const rootReducer = combineReducers(reducers);
|
||||||
|
|
13
app/javascript/mastodon/reducers/reaction_deck.js
Normal file
13
app/javascript/mastodon/reducers/reaction_deck.js
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -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 {
|
.focal-point {
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: move;
|
cursor: move;
|
||||||
|
|
|
@ -23,10 +23,18 @@ module HasUserSettings
|
||||||
settings['web.auto_play']
|
settings['web.auto_play']
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def setting_reaction_deck
|
||||||
|
settings['reaction_deck']
|
||||||
|
end
|
||||||
|
|
||||||
def setting_enable_login_privacy
|
def setting_enable_login_privacy
|
||||||
settings['web.enable_login_privacy']
|
settings['web.enable_login_privacy']
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def setting_hide_recent_emojis
|
||||||
|
settings['web.hide_recent_emojis']
|
||||||
|
end
|
||||||
|
|
||||||
def setting_default_sensitive
|
def setting_default_sensitive
|
||||||
settings['default_sensitive']
|
settings['default_sensitive']
|
||||||
end
|
end
|
||||||
|
|
|
@ -25,6 +25,7 @@ class UserSettings
|
||||||
setting :reject_public_unlisted_subscription, default: false
|
setting :reject_public_unlisted_subscription, default: false
|
||||||
setting :reject_unlisted_subscription, default: false
|
setting :reject_unlisted_subscription, default: false
|
||||||
setting :send_without_domain_blocks, default: false
|
setting :send_without_domain_blocks, default: false
|
||||||
|
setting :reaction_deck, default: nil
|
||||||
setting :stop_emoji_reaction_streaming, default: false
|
setting :stop_emoji_reaction_streaming, default: false
|
||||||
setting :emoji_reaction_streaming_notify_impl2, default: false
|
setting :emoji_reaction_streaming_notify_impl2, default: false
|
||||||
|
|
||||||
|
@ -38,6 +39,7 @@ class UserSettings
|
||||||
setting :disable_swiping, default: false
|
setting :disable_swiping, default: false
|
||||||
setting :delete_modal, default: true
|
setting :delete_modal, default: true
|
||||||
setting :enable_login_privacy, default: false
|
setting :enable_login_privacy, default: false
|
||||||
|
setting :hide_recent_emojis, default: false
|
||||||
setting :reblog_modal, default: false
|
setting :reblog_modal, default: false
|
||||||
setting :unfollow_modal, default: true
|
setting :unfollow_modal, default: true
|
||||||
setting :reduce_motion, default: false
|
setting :reduce_motion, default: false
|
||||||
|
|
|
@ -44,6 +44,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
||||||
store[:display_media_expand] = object.current_account.user.setting_display_media_expand
|
store[:display_media_expand] = object.current_account.user.setting_display_media_expand
|
||||||
store[:expand_spoilers] = object.current_account.user.setting_expand_spoilers
|
store[:expand_spoilers] = object.current_account.user.setting_expand_spoilers
|
||||||
store[:enable_login_privacy] = object.current_account.user.setting_enable_login_privacy
|
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[:reduce_motion] = object.current_account.user.setting_reduce_motion
|
||||||
store[:disable_swiping] = object.current_account.user.setting_disable_swiping
|
store[:disable_swiping] = object.current_account.user.setting_disable_swiping
|
||||||
store[:advanced_layout] = object.current_account.user.setting_advanced_layout
|
store[:advanced_layout] = object.current_account.user.setting_advanced_layout
|
||||||
|
|
|
@ -85,6 +85,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
||||||
max_reactions_per_account: EmojiReaction::EMOJI_REACTION_PER_ACCOUNT_LIMIT,
|
max_reactions_per_account: EmojiReaction::EMOJI_REACTION_PER_ACCOUNT_LIMIT,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
reaction_deck: {
|
||||||
|
max_items: 16,
|
||||||
|
},
|
||||||
|
|
||||||
reactions: {
|
reactions: {
|
||||||
max_reactions: EmojiReaction::EMOJI_REACTION_PER_ACCOUNT_LIMIT,
|
max_reactions: EmojiReaction::EMOJI_REACTION_PER_ACCOUNT_LIMIT,
|
||||||
},
|
},
|
||||||
|
@ -110,6 +114,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
||||||
:kmyblue_searchability,
|
:kmyblue_searchability,
|
||||||
:searchability,
|
:searchability,
|
||||||
:kmyblue_markdown,
|
:kmyblue_markdown,
|
||||||
|
:kmyblue_reaction_deck,
|
||||||
]
|
]
|
||||||
|
|
||||||
capabilities << :profile_search unless Chewy.enabled?
|
capabilities << :profile_search unless Chewy.enabled?
|
||||||
|
|
|
@ -91,6 +91,10 @@ class REST::V1::InstanceSerializer < ActiveModel::Serializer
|
||||||
max_reactions_per_account: EmojiReaction::EMOJI_REACTION_PER_ACCOUNT_LIMIT,
|
max_reactions_per_account: EmojiReaction::EMOJI_REACTION_PER_ACCOUNT_LIMIT,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
reaction_deck: {
|
||||||
|
max_items: 16,
|
||||||
|
},
|
||||||
|
|
||||||
reactions: {
|
reactions: {
|
||||||
max_reactions: EmojiReaction::EMOJI_REACTION_PER_ACCOUNT_LIMIT,
|
max_reactions: EmojiReaction::EMOJI_REACTION_PER_ACCOUNT_LIMIT,
|
||||||
},
|
},
|
||||||
|
@ -119,6 +123,7 @@ class REST::V1::InstanceSerializer < ActiveModel::Serializer
|
||||||
:kmyblue_searchability,
|
:kmyblue_searchability,
|
||||||
:searchability,
|
:searchability,
|
||||||
:kmyblue_markdown,
|
:kmyblue_markdown,
|
||||||
|
:kmyblue_reaction_deck,
|
||||||
]
|
]
|
||||||
|
|
||||||
capabilities << :profile_search unless Chewy.enabled?
|
capabilities << :profile_search unless Chewy.enabled?
|
||||||
|
|
|
@ -39,6 +39,9 @@
|
||||||
.fields-group
|
.fields-group
|
||||||
= ff.input :'web.crop_images', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_crop_images')
|
= 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
|
.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')
|
= 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')
|
||||||
|
|
||||||
|
|
|
@ -225,6 +225,7 @@ en:
|
||||||
setting_hide_followers_count: Hide followers count
|
setting_hide_followers_count: Hide followers count
|
||||||
setting_hide_following_count: Hide following count
|
setting_hide_following_count: Hide following count
|
||||||
setting_hide_network: Hide your social graph
|
setting_hide_network: Hide your social graph
|
||||||
|
setting_hide_recent_emojis: Hide recent emojis
|
||||||
setting_hide_statuses_count: Hide statuses count
|
setting_hide_statuses_count: Hide statuses count
|
||||||
setting_noai: Set noai meta tags
|
setting_noai: Set noai meta tags
|
||||||
setting_noindex: Opt-out of search engine indexing
|
setting_noindex: Opt-out of search engine indexing
|
||||||
|
|
|
@ -233,6 +233,7 @@ ja:
|
||||||
setting_hide_followers_count: フォロワー数を隠す
|
setting_hide_followers_count: フォロワー数を隠す
|
||||||
setting_hide_following_count: フォロー数を隠す
|
setting_hide_following_count: フォロー数を隠す
|
||||||
setting_hide_network: 繋がりを隠す
|
setting_hide_network: 繋がりを隠す
|
||||||
|
setting_hide_recent_emojis: 絵文字ピッカーで最近使用した絵文字を隠す(リアクションデッキのみを表示する)
|
||||||
setting_hide_statuses_count: 投稿数を隠す
|
setting_hide_statuses_count: 投稿数を隠す
|
||||||
setting_noai: 自分のコンテンツのAI学習利用に対して不快感を表明する
|
setting_noai: 自分のコンテンツのAI学習利用に対して不快感を表明する
|
||||||
setting_noindex: 検索エンジンによるインデックスを拒否する
|
setting_noindex: 検索エンジンによるインデックスを拒否する
|
||||||
|
|
|
@ -19,6 +19,7 @@ Rails.application.routes.draw do
|
||||||
/emoji_reactions
|
/emoji_reactions
|
||||||
/bookmarks
|
/bookmarks
|
||||||
/pinned
|
/pinned
|
||||||
|
/reaction_deck
|
||||||
/start
|
/start
|
||||||
/directory
|
/directory
|
||||||
/explore/(*any)
|
/explore/(*any)
|
||||||
|
|
|
@ -54,6 +54,7 @@ namespace :api, format: false do
|
||||||
get '/streaming/(*any)', to: 'streaming#index'
|
get '/streaming/(*any)', to: 'streaming#index'
|
||||||
|
|
||||||
resources :custom_emojis, only: [:index]
|
resources :custom_emojis, only: [:index]
|
||||||
|
resources :reaction_deck, only: [:index, :create]
|
||||||
resources :suggestions, only: [:index, :destroy]
|
resources :suggestions, only: [:index, :destroy]
|
||||||
resources :scheduled_statuses, only: [:index, :show, :update, :destroy]
|
resources :scheduled_statuses, only: [:index, :show, :update, :destroy]
|
||||||
resources :preferences, only: [:index]
|
resources :preferences, only: [:index]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue