From 26778823b9727c1de77ff63836d5f165b83a2921 Mon Sep 17 00:00:00 2001 From: Essem Date: Tue, 7 Nov 2023 18:43:47 -0600 Subject: [PATCH 1/4] Add support for emoji reactions Squashed, modified, and rebased from glitch-soc/mastodon#2221. Co-authored-by: fef Co-authored-by: Jeremy Kescher Co-authored-by: neatchee Co-authored-by: Ivan Rodriguez <104603218+IRod22@users.noreply.github.com> Co-authored-by: Plastikmensch --- .env.production.sample | 3 + .../api/v1/statuses/reactions_controller.rb | 31 ++++ .../flavours/glitch/actions/interactions.js | 82 ++++++++ .../flavours/glitch/actions/notifications.js | 1 + .../flavours/glitch/components/status.jsx | 23 ++- .../glitch/components/status_action_bar.jsx | 27 ++- .../glitch/components/status_prepend.jsx | 11 ++ .../glitch/components/status_reactions.jsx | 175 ++++++++++++++++++ .../glitch/containers/status_container.js | 10 + .../components/emoji_picker_dropdown.jsx | 5 +- .../notifications/components/filter_bar.jsx | 8 + .../notifications/components/notification.jsx | 22 +++ .../features/status/components/action_bar.jsx | 29 ++- .../status/components/detailed_status.jsx | 20 ++ .../flavours/glitch/features/status/index.jsx | 18 ++ .../flavours/glitch/initial_state.js | 5 + .../flavours/glitch/locales/de.json | 4 + .../flavours/glitch/locales/en.json | 5 + .../flavours/glitch/locales/fr.json | 4 + .../flavours/glitch/reducers/settings.js | 3 + .../flavours/glitch/reducers/statuses.js | 50 +++++ .../glitch/styles/components/accounts.scss | 4 + .../glitch/styles/components/status.scss | 19 +- .../mastodon/actions/interactions.js | 82 ++++++++ .../mastodon/actions/notifications.js | 1 + app/javascript/mastodon/components/status.jsx | 19 +- .../mastodon/components/status_action_bar.jsx | 28 ++- .../mastodon/components/status_reactions.jsx | 175 ++++++++++++++++++ .../mastodon/containers/status_container.jsx | 10 + .../components/emoji_picker_dropdown.jsx | 3 +- .../components/column_settings.jsx | 11 ++ .../notifications/components/filter_bar.jsx | 8 + .../notifications/components/notification.jsx | 35 ++++ .../features/status/components/action_bar.jsx | 31 +++- .../status/components/detailed_status.jsx | 15 ++ .../mastodon/features/status/index.jsx | 18 ++ app/javascript/mastodon/initial_state.js | 5 + app/javascript/mastodon/locales/de.json | 4 + app/javascript/mastodon/locales/en.json | 4 + app/javascript/mastodon/locales/fr.json | 4 + app/javascript/mastodon/reducers/settings.js | 3 + app/javascript/mastodon/reducers/statuses.js | 50 +++++ .../styles/mastodon/components.scss | 19 ++ app/lib/activitypub/activity.rb | 30 +++ app/lib/activitypub/activity/emoji_react.rb | 26 +++ app/lib/activitypub/activity/like.rb | 28 ++- app/lib/activitypub/activity/undo.rb | 27 +++ app/models/concerns/account_associations.rb | 1 + app/models/concerns/account_interactions.rb | 4 + app/models/concerns/has_user_settings.rb | 14 ++ app/models/notification.rb | 10 +- app/models/status.rb | 16 ++ app/models/status_reaction.rb | 33 ++++ app/models/user_settings.rb | 1 + app/policies/status_policy.rb | 4 + .../activitypub/emoji_reaction_serializer.rb | 39 ++++ .../undo_emoji_reaction_serializer.rb | 19 ++ app/serializers/initial_state_serializer.rb | 7 +- app/serializers/rest/instance_serializer.rb | 4 + .../rest/notification_serializer.rb | 2 +- app/serializers/rest/reaction_serializer.rb | 14 ++ app/serializers/rest/status_serializer.rb | 5 + .../rest/v1/instance_serializer.rb | 4 + app/services/react_service.rb | 31 ++++ app/services/unreact_service.rb | 23 +++ app/validators/status_reaction_validator.rb | 28 +++ .../preferences/appearance/show.html.haml | 3 + app/workers/unreact_worker.rb | 11 ++ config/locales-glitch/de.yml | 5 + config/locales-glitch/en.yml | 5 + config/locales-glitch/fr.yml | 5 + config/locales-glitch/simple_form.de.yml | 1 + config/locales-glitch/simple_form.en.yml | 1 + config/locales-glitch/simple_form.fr.yml | 1 + config/routes/api.rb | 5 + config/settings.yml | 1 + .../20221124114030_create_status_reactions.rb | 16 ++ ...15350_fix_foreign_keys_status_reactions.rb | 18 ++ ...0215074425_move_emoji_reaction_settings.rb | 49 +++++ db/schema.rb | 15 ++ .../fabricators/status_reaction_fabricator.rb | 8 + spec/models/status_reaction_spec.rb | 3 + 82 files changed, 1587 insertions(+), 14 deletions(-) create mode 100644 app/controllers/api/v1/statuses/reactions_controller.rb create mode 100644 app/javascript/flavours/glitch/components/status_reactions.jsx create mode 100644 app/javascript/mastodon/components/status_reactions.jsx create mode 100644 app/lib/activitypub/activity/emoji_react.rb create mode 100644 app/models/status_reaction.rb create mode 100644 app/serializers/activitypub/emoji_reaction_serializer.rb create mode 100644 app/serializers/activitypub/undo_emoji_reaction_serializer.rb create mode 100644 app/services/react_service.rb create mode 100644 app/services/unreact_service.rb create mode 100644 app/validators/status_reaction_validator.rb create mode 100644 app/workers/unreact_worker.rb create mode 100644 db/migrate/20221124114030_create_status_reactions.rb create mode 100644 db/migrate/20221218015350_fix_foreign_keys_status_reactions.rb create mode 100644 db/migrate/20230215074425_move_emoji_reaction_settings.rb create mode 100644 spec/fabricators/status_reaction_fabricator.rb create mode 100644 spec/models/status_reaction_spec.rb diff --git a/.env.production.sample b/.env.production.sample index 7bcce0f7e5..b604c4b04d 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -269,6 +269,9 @@ MAX_POLL_OPTIONS=5 # Maximum allowed poll option characters MAX_POLL_OPTION_CHARS=100 +# Maximum number of emoji reactions per toot and user (minimum 1) +MAX_REACTIONS=1 + # Maximum image and video/audio upload sizes # Units are in bytes # 1048576 bytes equals 1 megabyte diff --git a/app/controllers/api/v1/statuses/reactions_controller.rb b/app/controllers/api/v1/statuses/reactions_controller.rb new file mode 100644 index 0000000000..2d7e4f5984 --- /dev/null +++ b/app/controllers/api/v1/statuses/reactions_controller.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class Api::V1::Statuses::ReactionsController < Api::BaseController + include Authorization + + before_action -> { doorkeeper_authorize! :write, :'write:favourites' } + before_action :require_user! + before_action :set_status + + def create + ReactService.new.call(current_account, @status, params[:id]) + render json: @status, serializer: REST::StatusSerializer + end + + def destroy + UnreactWorker.perform_async(current_account.id, @status.id, params[:id]) + + render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, reactions_map: { @status.id => false }) + rescue Mastodon::NotPermittedError + not_found + end + + private + + def set_status + @status = Status.find(params[:status_id]) + authorize @status, :show? + rescue Mastodon::NotPermittedError + not_found + end +end diff --git a/app/javascript/flavours/glitch/actions/interactions.js b/app/javascript/flavours/glitch/actions/interactions.js index 095fb3155e..eb61b5bc1a 100644 --- a/app/javascript/flavours/glitch/actions/interactions.js +++ b/app/javascript/flavours/glitch/actions/interactions.js @@ -51,6 +51,16 @@ export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST'; export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS'; export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL'; +export const REACTION_UPDATE = 'REACTION_UPDATE'; + +export const REACTION_ADD_REQUEST = 'REACTION_ADD_REQUEST'; +export const REACTION_ADD_SUCCESS = 'REACTION_ADD_SUCCESS'; +export const REACTION_ADD_FAIL = 'REACTION_ADD_FAIL'; + +export const REACTION_REMOVE_REQUEST = 'REACTION_REMOVE_REQUEST'; +export const REACTION_REMOVE_SUCCESS = 'REACTION_REMOVE_SUCCESS'; +export const REACTION_REMOVE_FAIL = 'REACTION_REMOVE_FAIL'; + export function reblog(status, visibility) { return function (dispatch, getState) { dispatch(reblogRequest(status)); @@ -496,3 +506,75 @@ export function unpinFail(status, error) { error, }; } + +export const addReaction = (statusId, name, url) => (dispatch, getState) => { + const status = getState().get('statuses').get(statusId); + let alreadyAdded = false; + if (status) { + const reaction = status.get('reactions').find(x => x.get('name') === name); + if (reaction && reaction.get('me')) { + alreadyAdded = true; + } + } + if (!alreadyAdded) { + dispatch(addReactionRequest(statusId, name, url)); + } + + // encodeURIComponent is required for the Keycap Number Sign emoji, see: + // + api(getState).post(`/api/v1/statuses/${statusId}/react/${encodeURIComponent(name)}`).then(() => { + dispatch(addReactionSuccess(statusId, name)); + }).catch(err => { + if (!alreadyAdded) { + dispatch(addReactionFail(statusId, name, err)); + } + }); +}; + +export const addReactionRequest = (statusId, name, url) => ({ + type: REACTION_ADD_REQUEST, + id: statusId, + name, + url, +}); + +export const addReactionSuccess = (statusId, name) => ({ + type: REACTION_ADD_SUCCESS, + id: statusId, + name, +}); + +export const addReactionFail = (statusId, name, error) => ({ + type: REACTION_ADD_FAIL, + id: statusId, + name, + error, +}); + +export const removeReaction = (statusId, name) => (dispatch, getState) => { + dispatch(removeReactionRequest(statusId, name)); + + api(getState).post(`/api/v1/statuses/${statusId}/unreact/${encodeURIComponent(name)}`).then(() => { + dispatch(removeReactionSuccess(statusId, name)); + }).catch(err => { + dispatch(removeReactionFail(statusId, name, err)); + }); +}; + +export const removeReactionRequest = (statusId, name) => ({ + type: REACTION_REMOVE_REQUEST, + id: statusId, + name, +}); + +export const removeReactionSuccess = (statusId, name) => ({ + type: REACTION_REMOVE_SUCCESS, + id: statusId, + name, +}); + +export const removeReactionFail = (statusId, name) => ({ + type: REACTION_REMOVE_FAIL, + id: statusId, + name, +}); diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index 81b8045d70..4b4b48b7da 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -147,6 +147,7 @@ const excludeTypesFromFilter = filter => { 'follow', 'follow_request', 'favourite', + 'reaction', 'reblog', 'mention', 'poll', diff --git a/app/javascript/flavours/glitch/components/status.jsx b/app/javascript/flavours/glitch/components/status.jsx index 54ccbbd9c8..a818ece3d4 100644 --- a/app/javascript/flavours/glitch/components/status.jsx +++ b/app/javascript/flavours/glitch/components/status.jsx @@ -12,7 +12,7 @@ import { HotKeys } from 'react-hotkeys'; import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder'; import PollContainer from 'flavours/glitch/containers/poll_container'; import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container'; -import { displayMedia } from 'flavours/glitch/initial_state'; +import { displayMedia, visibleReactions } from 'flavours/glitch/initial_state'; import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning'; import { withOptionalRouter, WithOptionalRouterPropTypes } from 'flavours/glitch/utils/react_router'; @@ -26,6 +26,7 @@ import StatusContent from './status_content'; import StatusHeader from './status_header'; import StatusIcons from './status_icons'; import StatusPrepend from './status_prepend'; +import StatusReactions from './status_reactions'; const domParser = new DOMParser(); @@ -68,6 +69,10 @@ export const defaultMediaVisibility = (status, settings) => { class Status extends ImmutablePureComponent { + static contextTypes = { + identity: PropTypes.object, + }; + static propTypes = { containerId: PropTypes.string, id: PropTypes.string, @@ -83,6 +88,8 @@ class Status extends ImmutablePureComponent { onDelete: PropTypes.func, onDirect: PropTypes.func, onMention: PropTypes.func, + onReactionAdd: PropTypes.func, + onReactionRemove: PropTypes.func, onPin: PropTypes.func, onOpenMedia: PropTypes.func, onOpenVideo: PropTypes.func, @@ -748,6 +755,7 @@ class Status extends ImmutablePureComponent { if (this.props.prepend && account) { const notifKind = { favourite: 'favourited', + reaction: 'reacted', reblog: 'boosted', reblogged_by: 'boosted', status: 'posted', @@ -779,6 +787,9 @@ class Status extends ImmutablePureComponent { muted, }, 'focusable'); + const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status); + contentMedia.push(hashtagBar); + return (
+ + {!isCollapsed || !(muted || !settings.getIn(['collapsed', 'show_action_bar'])) ? ( diff --git a/app/javascript/flavours/glitch/components/status_action_bar.jsx b/app/javascript/flavours/glitch/components/status_action_bar.jsx index b8dd63b270..3afe7a4cbb 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.jsx +++ b/app/javascript/flavours/glitch/components/status_action_bar.jsx @@ -9,7 +9,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; -import { me } from 'flavours/glitch/initial_state'; +import EmojiPickerDropdown from 'flavours/glitch/features/compose/containers/emoji_picker_dropdown_container'; +import { me, maxReactions } from 'flavours/glitch/initial_state'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions'; import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links'; import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; @@ -34,6 +35,7 @@ const messages = defineMessages({ cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, favourite: { id: 'status.favourite', defaultMessage: 'Favorite' }, + react: { id: 'status.react', defaultMessage: 'React' }, bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' }, open: { id: 'status.open', defaultMessage: 'Expand this status' }, report: { id: 'status.report', defaultMessage: 'Report @{name}' }, @@ -62,6 +64,7 @@ class StatusActionBar extends ImmutablePureComponent { status: ImmutablePropTypes.map.isRequired, onReply: PropTypes.func, onFavourite: PropTypes.func, + onReactionAdd: PropTypes.func, onReblog: PropTypes.func, onDelete: PropTypes.func, onDirect: PropTypes.func, @@ -119,6 +122,10 @@ class StatusActionBar extends ImmutablePureComponent { } }; + handleEmojiPick = data => { + this.props.onReactionAdd(this.props.status.get('id'), data.native.replace(/:/g, ''), data.imageUrl); + }; + handleReblogClick = e => { const { signedIn } = this.context.identity; @@ -194,6 +201,8 @@ class StatusActionBar extends ImmutablePureComponent { this.props.onAddFilter(this.props.status); }; + handleNoOp = () => {}; // hack for reaction add button + render () { const { status, intl, withDismiss, withCounters, showReplyCount, scrollKey } = this.props; const { permissions, signedIn } = this.context.identity; @@ -299,6 +308,17 @@ class StatusActionBar extends ImmutablePureComponent { ); + const canReact = permissions && status.get('reactions').filter(r => r.get('count') > 0 && r.get('me')).size < maxReactions; + const reactButton = ( + + ); + return (
+ { + permissions + ? + : reactButton + } {filterButton} diff --git a/app/javascript/flavours/glitch/components/status_prepend.jsx b/app/javascript/flavours/glitch/components/status_prepend.jsx index 31e84c6e11..c6c3741404 100644 --- a/app/javascript/flavours/glitch/components/status_prepend.jsx +++ b/app/javascript/flavours/glitch/components/status_prepend.jsx @@ -59,6 +59,14 @@ export default class StatusPrepend extends PureComponent { values={{ name : link }} /> ); + case 'reaction': + return ( + + ); case 'reblog': return ( x.get('count') > 0) + .sort((a, b) => b.get('count') - a.get('count')); + + if (numVisible >= 0) { + visibleReactions = visibleReactions.filter((_, i) => i < numVisible); + } + + const styles = visibleReactions.map(reaction => ({ + key: reaction.get('name'), + data: reaction, + style: { scale: reduceMotion ? 1 : spring(1, { stiffness: 150, damping: 13 }) }, + })).toArray(); + + return ( + + {items => ( +
+ {items.map(({ key, data, style }) => ( + + ))} +
+ )} +
+ ); + } + +} + +class Reaction extends ImmutablePureComponent { + + static propTypes = { + statusId: PropTypes.string, + reaction: ImmutablePropTypes.map.isRequired, + addReaction: PropTypes.func.isRequired, + removeReaction: PropTypes.func.isRequired, + canReact: PropTypes.bool.isRequired, + style: PropTypes.object, + }; + + state = { + hovered: false, + }; + + handleClick = () => { + const { reaction, statusId, addReaction, removeReaction } = this.props; + + if (reaction.get('me')) { + removeReaction(statusId, reaction.get('name')); + } else { + addReaction(statusId, reaction.get('name')); + } + } + + handleMouseEnter = () => this.setState({ hovered: true }) + + handleMouseLeave = () => this.setState({ hovered: false }) + + render() { + const { reaction } = this.props; + + return ( + + ); + } + +} + +class Emoji extends React.PureComponent { + + static propTypes = { + emoji: PropTypes.string.isRequired, + hovered: PropTypes.bool.isRequired, + url: PropTypes.string, + staticUrl: PropTypes.string, + }; + + render() { + const { emoji, hovered, url, staticUrl } = this.props; + + if (unicodeMapping[emoji]) { + const { filename, shortCode } = unicodeMapping[this.props.emoji]; + const title = shortCode ? `:${shortCode}:` : ''; + + return ( + {emoji} + ); + } else { + const filename = (autoPlayGif || hovered) ? url : staticUrl; + const shortCode = `:${emoji}:`; + + return ( + {shortCode} + ); + } + } + +} diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index f6bdf9400d..50bb4dfc2e 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -21,6 +21,8 @@ import { unbookmark, pin, unpin, + addReaction, + removeReaction, } from 'flavours/glitch/actions/interactions'; import { changeLocalSetting } from 'flavours/glitch/actions/local_settings'; import { openModal } from 'flavours/glitch/actions/modal'; @@ -173,6 +175,14 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ } }, + onReactionAdd (statusId, name, url) { + dispatch(addReaction(statusId, name, url)); + }, + + onReactionRemove (statusId, name) { + dispatch(removeReaction(statusId, name)); + }, + onEmbed (status) { dispatch(openModal({ modalType: 'EMBED', diff --git a/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx index c2c8030615..01cd3d5dd2 100644 --- a/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx @@ -324,6 +324,7 @@ class EmojiPickerDropdown extends PureComponent { onSkinTone: PropTypes.func.isRequired, skinTone: PropTypes.number.isRequired, button: PropTypes.node, + disabled: PropTypes.bool, }; state = { @@ -357,7 +358,7 @@ class EmojiPickerDropdown extends PureComponent { }; onToggle = (e) => { - if (!this.state.loading && (!e.key || e.key === 'Enter')) { + if (!this.state.disabled && !this.state.loading && (!e.key || e.key === 'Enter')) { if (this.state.active) { this.onHideDropdown(); } else { @@ -395,7 +396,7 @@ class EmojiPickerDropdown extends PureComponent { />}
- + {({ props, placement })=> (
diff --git a/app/javascript/flavours/glitch/features/notifications/components/filter_bar.jsx b/app/javascript/flavours/glitch/features/notifications/components/filter_bar.jsx index 7f4df1d92d..0e8127e362 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/filter_bar.jsx +++ b/app/javascript/flavours/glitch/features/notifications/components/filter_bar.jsx @@ -8,6 +8,7 @@ import { Icon } from 'flavours/glitch/components/icon'; const tooltips = defineMessages({ mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' }, favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Favorites' }, + reactions: { id: 'notifications.filter.reactions', defaultMessage: 'Reactions' }, boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' }, polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' }, follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' }, @@ -75,6 +76,13 @@ class FilterBar extends PureComponent { > + + ); + } + +} + +class Emoji extends React.PureComponent { + + static propTypes = { + emoji: PropTypes.string.isRequired, + hovered: PropTypes.bool.isRequired, + url: PropTypes.string, + staticUrl: PropTypes.string, + }; + + render() { + const { emoji, hovered, url, staticUrl } = this.props; + + if (unicodeMapping[emoji]) { + const { filename, shortCode } = unicodeMapping[this.props.emoji]; + const title = shortCode ? `:${shortCode}:` : ''; + + return ( + {emoji} + ); + } else { + const filename = (autoPlayGif || hovered) ? url : staticUrl; + const shortCode = `:${emoji}:`; + + return ( + {shortCode} + ); + } + } + +} diff --git a/app/javascript/mastodon/containers/status_container.jsx b/app/javascript/mastodon/containers/status_container.jsx index 7a7cd9880f..722a35568d 100644 --- a/app/javascript/mastodon/containers/status_container.jsx +++ b/app/javascript/mastodon/containers/status_container.jsx @@ -30,6 +30,8 @@ import { unbookmark, pin, unpin, + addReaction, + removeReaction, } from '../actions/interactions'; import { openModal } from '../actions/modal'; import { initMuteModal } from '../actions/mutes'; @@ -135,6 +137,14 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ } }, + onReactionAdd (statusId, name, url) { + dispatch(addReaction(statusId, name, url)); + }, + + onReactionRemove (statusId, name) { + dispatch(removeReaction(statusId, name)); + }, + onEmbed (status) { dispatch(openModal({ modalType: 'EMBED', diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx index 494b8d8624..d3254f0ef1 100644 --- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx +++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx @@ -322,6 +322,7 @@ class EmojiPickerDropdown extends PureComponent { onSkinTone: PropTypes.func.isRequired, skinTone: PropTypes.number.isRequired, button: PropTypes.node, + disabled: PropTypes.bool, }; state = { @@ -355,7 +356,7 @@ class EmojiPickerDropdown extends PureComponent { }; onToggle = (e) => { - if (!this.state.loading && (!e.key || e.key === 'Enter')) { + if (!this.state.disabled && !this.state.loading && (!e.key || e.key === 'Enter')) { if (this.state.active) { this.onHideDropdown(); } else { diff --git a/app/javascript/mastodon/features/notifications/components/column_settings.jsx b/app/javascript/mastodon/features/notifications/components/column_settings.jsx index 09154f257a..a08fa6c948 100644 --- a/app/javascript/mastodon/features/notifications/components/column_settings.jsx +++ b/app/javascript/mastodon/features/notifications/components/column_settings.jsx @@ -119,6 +119,17 @@ export default class ColumnSettings extends PureComponent {
+
+ + +
+ + {showPushSettings && } + + +
+
+
diff --git a/app/javascript/mastodon/features/notifications/components/filter_bar.jsx b/app/javascript/mastodon/features/notifications/components/filter_bar.jsx index 84bd4791ca..f6b68c34dc 100644 --- a/app/javascript/mastodon/features/notifications/components/filter_bar.jsx +++ b/app/javascript/mastodon/features/notifications/components/filter_bar.jsx @@ -15,6 +15,7 @@ import { Icon } from 'mastodon/components/icon'; const tooltips = defineMessages({ mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' }, favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Favorites' }, + reactions: { id: 'notifications.filter.reactions', defaultMessage: 'Reactions' }, boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' }, polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' }, follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' }, @@ -82,6 +83,13 @@ class FilterBar extends PureComponent { > +