Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Emoji reactions support, take 4 #2462

Open
wants to merge 36 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
3e6d7b1
Add support for emoji reactions
TheEssem Nov 8, 2023
d2f9f3c
Add notification emails for reactions
TheEssem Nov 10, 2023
0cf4424
Fix reblog reactions
TheEssem Nov 10, 2023
1fc27dc
Refactor react services
TheEssem Nov 13, 2023
eaf2375
Linting fixes
TheEssem Dec 19, 2023
0c74d28
Add reaction notification column settings
TheEssem Dec 20, 2023
4652514
Fix rubocop complaint
TheEssem Dec 22, 2023
58b9bbb
Check for content attribute in Misskey likes
TheEssem Dec 26, 2023
f1b4dee
Normalize emojis with variant selectors
TheEssem Jan 14, 2024
c1a31a2
Make name of like content parser function more general
TheEssem Jan 14, 2024
accb750
Quick fixes
TheEssem Jan 14, 2024
918a2ed
Move reaction normalization to API controller
TheEssem Jan 14, 2024
78e1606
Revert variant selector normalization
TheEssem Jan 18, 2024
5af04c4
Update reaction emails
TheEssem Jan 19, 2024
28da921
Simplify reactions API controller
TheEssem Jan 24, 2024
dc12886
Refactor status reactions query
TheEssem Jan 24, 2024
19fcad8
Fix rubocop lint issue
TheEssem Jan 28, 2024
61e12b0
Purge status reactions on account delete
TheEssem Feb 7, 2024
0e55f7a
Hydrate reactions on streaming API
TheEssem Feb 9, 2024
340d641
Merge fixes
TheEssem Feb 24, 2024
3480c26
Fix reaction picker dropdown appearance
TheEssem Feb 24, 2024
612c258
[Glitch+Emoji reactions] Use modern React context for for identity fo…
kescherCode May 20, 2024
344de29
Disable reactions in detailed status view when visibleReactions is 0
TheEssem Jun 16, 2024
4063f67
Turn custom emoji regexps into class level constants
TheEssem Jun 18, 2024
70d0347
Add notification grouping for reactions
TheEssem Jul 19, 2024
13dbca7
Fix reactions bar alignment in grouped notifications
TheEssem Jul 25, 2024
369e33d
Fix reblog reactions being hydrated improperly
TheEssem Aug 5, 2024
1eca05c
Fix grouped reaction notification text
TheEssem Aug 22, 2024
d2c922d
Make addReaction and removeReaction optional props
TheEssem Sep 13, 2024
19c761b
Align mail status check with upstream
TheEssem Oct 3, 2024
fc494c8
Fix invisible reactions on detailed statuses when logged out
TheEssem Nov 7, 2024
e05e80c
Fix status reactions not animating on hover when logged out
TheEssem Nov 26, 2024
1314302
Update status reaction emails
TheEssem Dec 18, 2024
f2e8619
Fix old migration
TheEssem Dec 29, 2024
9323c75
Remove emoji reaction settings move migration
TheEssem Jan 3, 2025
83b1ebd
Group reaction notifications by default
TheEssem Feb 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.production.sample
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,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,
});
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ function dispatchAssociatedRecords(
}

function selectNotificationGroupedTypes(state: RootState) {
const types: NotificationType[] = ['favourite', 'reblog'];
const types: NotificationType[] = ['favourite', 'reblog', 'reaction'];

if (selectSettingsNotificationsGroupFollows(state)) types.push('follow');

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 @@ -25,6 +26,7 @@ export const allNotificationTypes = [

export type NotificationWithStatusType =
| 'favourite'
| 'reaction'
| 'reblog'
| 'status'
| 'mention'
Expand Down
22 changes: 19 additions & 3 deletions app/javascript/flavours/glitch/components/status.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { HotKeys } from 'react-hotkeys';

import { ContentWarning } from 'flavours/glitch/components/content_warning';
import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
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 @@ -20,7 +21,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 { Avatar } from './avatar';
Expand All @@ -33,6 +34,7 @@ import StatusActionBar from './status_action_bar';
import StatusContent from './status_content';
import StatusIcons from './status_icons';
import StatusPrepend from './status_prepend';
import StatusReactions from './status_reactions';

const domParser = new DOMParser();

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

static propTypes = {
identity: identityContextPropShape,
containerId: PropTypes.string,
id: PropTypes.string,
status: ImmutablePropTypes.map,
Expand All @@ -93,6 +96,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 @@ -363,7 +368,7 @@ class Status extends ImmutablePureComponent {
this.props.onClick();
return;
}

const { history } = this.props;
const status = this.props.status;

Expand Down Expand Up @@ -453,6 +458,7 @@ class Status extends ImmutablePureComponent {
onOpenMedia,
notification,
history,
identity,
...other
} = this.props;
let attachments = null;
Expand Down Expand Up @@ -642,6 +648,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 @@ -730,6 +737,15 @@ class Status extends ImmutablePureComponent {
{/* This is a glitch-soc addition to have a placeholder */}
{!expanded && <MentionsPlaceholder status={status} />}

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

<StatusActionBar
status={status}
account={status.get('account')}
Expand All @@ -745,4 +761,4 @@ class Status extends ImmutablePureComponent {

}

export default withOptionalRouter(injectIntl(Status));
export default withOptionalRouter(injectIntl((withIdentity(Status))));
14 changes: 13 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' },
removeFavourite: { id: 'status.remove_favourite', defaultMessage: 'Remove from favorites' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
removeBookmark: { id: 'status.remove_bookmark', defaultMessage: 'Remove bookmark' },
Expand All @@ -75,6 +78,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 @@ -132,6 +136,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 @@ -322,6 +330,7 @@ class StatusActionBar extends ImmutablePureComponent {
</div>
);

const canReact = permissions && status.get('reactions').filter(r => r.get('count') > 0 && r.get('me')).size < maxReactions;
const bookmarkTitle = intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark);
const favouriteTitle = intl.formatMessage(status.get('favourited') ? messages.removeFavourite : messages.favourite);

Expand All @@ -344,6 +353,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={favouriteTitle} 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={bookmarkTitle} 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 @@ -65,6 +66,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 @@ -120,6 +129,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