Skip to content

Commit

Permalink
Add support for emoji reactions
Browse files Browse the repository at this point in the history
Squashed from glitch-soc#2462

Co-authored-by: Essem <[email protected]>
Co-authored-by: fef <[email protected]>
Co-authored-by: Jeremy Kescher <[email protected]>
Co-authored-by: neatchee <[email protected]>
Co-authored-by: Ivan Rodriguez <[email protected]>
Co-authored-by: Plastikmensch <[email protected]>
  • Loading branch information
7 people authored Oct 12, 2024
1 parent f919362 commit a9bffa5
Show file tree
Hide file tree
Showing 71 changed files with 1,207 additions and 24 deletions.
3 changes: 3 additions & 0 deletions .env.production.sample
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,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
Expand Down
19 changes: 19 additions & 0 deletions app/controllers/api/v1/statuses/reactions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

class Api::V1::Statuses::ReactionsController < Api::V1::Statuses::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:favourites' }
before_action :require_user!

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
end
82 changes: 82 additions & 0 deletions app/javascript/flavours/glitch/actions/interactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,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 * from "./interactions_typed";

export function favourite(status) {
Expand Down Expand Up @@ -494,3 +504,75 @@ export function toggleFavourite(statusId, skipModal = false) {
}
};
}

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:
// <https://github.com/glitch-soc/mastodon/pull/1980#issuecomment-1345538932>
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,
});
1 change: 1 addition & 0 deletions app/javascript/flavours/glitch/actions/notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ const excludeTypesFromFilter = filter => {
'follow',
'follow_request',
'favourite',
'reaction',
'reblog',
'mention',
'poll',
Expand Down
2 changes: 2 additions & 0 deletions app/javascript/flavours/glitch/api_types/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const allNotificationTypes = [
'follow',
'follow_request',
'favourite',
'reaction',
'reblog',
'mention',
'poll',
Expand All @@ -24,6 +25,7 @@ export const allNotificationTypes = [

export type NotificationWithStatusType =
| 'favourite'
| 'reaction'
| 'reblog'
| 'status'
| 'mention'
Expand Down
20 changes: 18 additions & 2 deletions app/javascript/flavours/glitch/components/status.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +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 { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning';
import { withOptionalRouter, WithOptionalRouterPropTypes } from 'flavours/glitch/utils/react_router';

Expand All @@ -21,7 +22,7 @@ import Card from '../features/status/components/card';
import Bundle from '../features/ui/components/bundle';
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
import { SensitiveMediaContext } from '../features/ui/util/sensitive_media_context';
import { displayMedia } from '../initial_state';
import { displayMedia, visibleReactions } from '../initial_state';

import AttachmentList from './attachment_list';
import { CollapseButton } from './collapse_button';
Expand All @@ -31,6 +32,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();

Expand Down Expand Up @@ -76,6 +78,7 @@ class Status extends ImmutablePureComponent {
static contextType = SensitiveMediaContext;

static propTypes = {
identity: identityContextPropShape,
containerId: PropTypes.string,
id: PropTypes.string,
status: ImmutablePropTypes.map,
Expand All @@ -91,6 +94,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,
Expand Down Expand Up @@ -538,6 +543,7 @@ class Status extends ImmutablePureComponent {
onOpenMedia,
notification,
history,
identity,
...other
} = this.props;
const { isCollapsed } = this.state;
Expand Down Expand Up @@ -755,6 +761,7 @@ class Status extends ImmutablePureComponent {
if (this.props.prepend && account) {
const notifKind = {
favourite: 'favourited',
reaction: 'reacted',
reblog: 'boosted',
reblogged_by: 'boosted',
status: 'posted',
Expand Down Expand Up @@ -839,6 +846,15 @@ class Status extends ImmutablePureComponent {
{...statusContentProps}
/>

<StatusReactions
statusId={status.get('id')}
reactions={status.get('reactions')}
numVisible={visibleReactions}
addReaction={this.props.onReactionAdd}
removeReaction={this.props.onReactionRemove}
canReact={this.props.identity.signedIn}
/>

{(!isCollapsed || !(muted || !settings.getIn(['collapsed', 'show_action_bar']))) && (
<StatusActionBar
status={status}
Expand All @@ -861,4 +877,4 @@ class Status extends ImmutablePureComponent {

}

export default withOptionalRouter(injectIntl(Status));
export default withOptionalRouter(injectIntl((withIdentity(Status))));
15 changes: 14 additions & 1 deletion app/javascript/flavours/glitch/components/status_action_bar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';

import AddReactionIcon from '@/material-icons/400-24px/add_reaction.svg?react';
import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg?react';
import BookmarkBorderIcon from '@/material-icons/400-24px/bookmark.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
Expand All @@ -27,7 +28,8 @@ import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';

import DropdownMenuContainer from '../containers/dropdown_menu_container';
import { me } from '../initial_state';
import EmojiPickerDropdown from '../features/compose/containers/emoji_picker_dropdown_container';
import { me, maxReactions } from '../initial_state';

import { IconButton } from './icon_button';
import { RelativeTimestamp } from './relative_timestamp';
Expand All @@ -49,6 +51,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}' },
Expand All @@ -73,6 +76,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,
Expand Down Expand Up @@ -130,6 +134,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.props.identity;

Expand Down Expand Up @@ -320,6 +328,8 @@ class StatusActionBar extends ImmutablePureComponent {
</div>
);

const canReact = permissions && status.get('reactions').filter(r => r.get('count') > 0 && r.get('me')).size < maxReactions;

return (
<div className='status__action-bar'>
<div className='status__action-bar__button-wrapper'>
Expand All @@ -339,6 +349,9 @@ class StatusActionBar extends ImmutablePureComponent {
<div className='status__action-bar__button-wrapper'>
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
</div>
<div className='status__action-bar__button-wrapper'>
<EmojiPickerDropdown className='status__action-bar-button' onPickEmoji={this.handleEmojiPick} title={intl.formatMessage(messages.react)} icon={AddReactionIcon} disabled={!canReact} />
</div>
<div className='status__action-bar__button-wrapper'>
<IconButton className='status__action-bar-button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} />
</div>
Expand Down
13 changes: 13 additions & 0 deletions app/javascript/flavours/glitch/components/status_prepend.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react';
import MoodIcon from '@/material-icons/400-24px/mood.svg?react';
import PushPinIcon from '@/material-icons/400-24px/push_pin.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
Expand Down Expand Up @@ -70,6 +71,14 @@ export default class StatusPrepend extends PureComponent {
values={{ name : link }}
/>
);
case 'reaction':
return (
<FormattedMessage
id='notification.reaction'
defaultMessage='{name} reacted to your status'
values={{ name: link }}
/>
);
case 'reblog':
return (
<FormattedMessage
Expand Down Expand Up @@ -125,6 +134,10 @@ export default class StatusPrepend extends PureComponent {
iconId = 'star';
iconComponent = StarIcon;
break;
case 'reaction':
iconId = 'mood';
iconComponent = MoodIcon;
break;
case 'featured':
iconId = 'thumb-tack';
iconComponent = PushPinIcon;
Expand Down
Loading

0 comments on commit a9bffa5

Please sign in to comment.