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

Add Support for Emoji Reactions (Replaces #1980) #2127

Closed
wants to merge 66 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
9fccc8c
add backend support for status emoji reactions
fef1312 Nov 24, 2022
671fb4c
add frontend for emoji reactions
fef1312 Nov 24, 2022
56cb7ff
show reactions in detailed status view
fef1312 Nov 25, 2022
a1685c4
federate emoji reactions
fef1312 Nov 28, 2022
6646d36
remove accidentally created file
fef1312 Nov 28, 2022
4f4237c
make status reaction count limit configurable
fef1312 Nov 28, 2022
6bd0646
make frontend fetch reaction limit
fef1312 Nov 29, 2022
f61c649
cherry-pick emoji reaction changes
fef1312 Nov 29, 2022
c96110a
move react button to action bar
fef1312 Nov 29, 2022
256a07e
handle misskey reactions properly
fef1312 Nov 29, 2022
0ae702a
fix padding for reaction button
fef1312 Nov 29, 2022
9868231
cleanup backend emoji reaction code
fef1312 Nov 29, 2022
299b501
cleanup frontend emoji reaction code
fef1312 Nov 29, 2022
4951abe
fix reaction margins and paddings
fef1312 Nov 29, 2022
78e0e74
limit number of reactions displayed
fef1312 Nov 30, 2022
b6f8093
change default reaction limit to 1
fef1312 Nov 30, 2022
c3a18c6
make number of displayed reactions a setting
fef1312 Nov 30, 2022
573f12e
make number of visible reactions a vanilla setting
fef1312 Nov 30, 2022
7403e91
rebase with upstream
fef1312 Nov 30, 2022
49c8048
clean up new imports in vanilla flavour
fef1312 Nov 30, 2022
a8afee5
remove outdated comments
fef1312 Nov 30, 2022
946debe
fix reaction deletion bug and clean up controller
fef1312 Dec 1, 2022
be710b6
change reaction api to match other interactions
fef1312 Dec 1, 2022
490c1a2
remove unnecessary parameter
fef1312 Dec 1, 2022
4452ddb
cleanup JS imports and other minor stuff
fef1312 Dec 1, 2022
9f6a880
rename nop handler to handleNoOp
fef1312 Dec 2, 2022
8aadf99
fix padding on posts without reactions
fef1312 Dec 2, 2022
d5ccf3b
Add reaction limit to instance serializer
kescherCode Dec 2, 2022
d820510
sanitize setting for number of visible reactions
fef1312 Dec 2, 2022
16e08e1
download remote custom emojis from reactions
fef1312 Dec 2, 2022
7349a5a
support Undo action for EmojiReaction
fef1312 Dec 3, 2022
dbbfea1
handle incoming custom emoji reactions properly
fef1312 Dec 3, 2022
d0f3f64
display external custom emoji reactions properly
fef1312 Dec 3, 2022
eac272d
run i18n-tasks normalize
fef1312 Dec 3, 2022
799fb28
fix image for new custom emoji reactions
fef1312 Dec 3, 2022
6a1c438
disable reaction button when not signed in
fef1312 Dec 3, 2022
588315a
also disable reaction buttons in vanilla flavour
fef1312 Dec 4, 2022
13b5375
serialize custom emoji reactions properly for AP
fef1312 Dec 4, 2022
f6b1ec0
properly disable reactions when not logged in
fef1312 Dec 4, 2022
3eab0d0
support reacting with foreign custom emojis
fef1312 Dec 7, 2022
215a76c
delete reaction notifications when deleting status
fef1312 Dec 7, 2022
ed60e5b
fix schema after rebase
fef1312 Dec 7, 2022
4761bd3
fix status action bar after upstream changes
fef1312 Dec 8, 2022
79ff467
fix 404 when reacting with Keycap Number Sign
fef1312 Dec 11, 2022
275a67b
bypass reaction limit for foreign accounts
fef1312 Dec 15, 2022
b753263
Fix status reactions preventing an on_cascade delete
kescherCode Dec 18, 2022
49b25c5
move emoji reaction strings to locales-glitch
fef1312 Dec 20, 2022
2664621
Per PR suggestion, split name and domain, and look for emoji ID, for …
neatchee Jan 26, 2023
98eab31
Fix rebase issues
neatchee Jan 26, 2023
b78ea8b
Keep emoji picker within screen bounds
Mar 8, 2023
f718c02
Remove old .js locale files accidentally restored during rebase
neatchee Mar 8, 2023
0c23022
Migrate emoji reactions
kescherCode Apr 3, 2023
34f9e54
Fix appearance/show.html.haml
kescherCode Apr 3, 2023
759a277
Fix placement of reactions bar for new threading UI
neatchee May 7, 2023
7d7eff8
Restoring missing db migrate for reactions. How did this even go miss…
neatchee May 7, 2023
e0db7dc
Update emoji reaction patches
kescherCode May 7, 2023
fa0a5a7
Restore loc files for non-English languages; CrowdIn should handle this
neatchee May 7, 2023
b0b593d
Add missing authorization to ReactService
kescherCode May 7, 2023
ec687d7
Remove failing skip_before_action from v1/custom_emojis_controller.rb
kescherCode May 7, 2023
4bd3c80
Reactions: Return 404 when status should not be visible, asynchronous…
kescherCode May 7, 2023
2a8b66a
Fix missed merge conflict text in version.rb
neatchee May 7, 2023
730878a
Remove stale/missed references to makeCustomEmojiMap / EmojiMap
neatchee May 7, 2023
eb68f62
Revert "Fix missed merge conflict text in version.rb"
neatchee May 7, 2023
5bd0c35
Revert "Fix appearance/show.html.haml"
neatchee May 7, 2023
f560ee4
Fix appearance/show.html.haml
neatchee May 7, 2023
924e858
Merge remote-tracking branch 'glitch-soc/main' into feat/emoji_reactions
neatchee May 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.production.sample
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions app/controllers/api/v1/custom_emojis_controller.rb
Expand Up @@ -2,6 +2,7 @@

class Api::V1::CustomEmojisController < Api::BaseController
vary_by '', unless: :disallow_unauthenticated_api_access?
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this also a catstodon specific thing to allow anyone to copy custom emojis? (@kescherCode )

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is and should not be in here.


def index
cache_even_if_authenticated! unless disallow_unauthenticated_api_access?
Expand Down
39 changes: 39 additions & 0 deletions app/controllers/api/v1/statuses/reactions_controller.rb
@@ -0,0 +1,39 @@
# 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, only: [:create]

def create
ReactService.new.call(current_account, @status, params[:id])
render json: @status, serializer: REST::StatusSerializer
end

def destroy
react = current_account.status_reactions.find_by(status_id: params[:status_id], name: params[:id])

if react
@status = react.status
UnreactWorker.perform_async(current_account.id, @status.id, params[:id])
else
@status = Status.find(params[:status_id])
authorize @status, :show?
end

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
84 changes: 83 additions & 1 deletion app/javascript/flavours/glitch/actions/interactions.js
Expand Up @@ -41,6 +41,16 @@
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));
Expand Down Expand Up @@ -391,4 +401,76 @@
status,
error,
};
}
};

Check failure on line 404 in app/javascript/flavours/glitch/actions/interactions.js

View workflow job for this annotation

GitHub Actions / lint

Unnecessary semicolon

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
Expand Up @@ -139,6 +139,7 @@ const excludeTypesFromFilter = filter => {
'follow',
'follow_request',
'favourite',
'reaction',
'reblog',
'mention',
'poll',
Expand Down
16 changes: 15 additions & 1 deletion app/javascript/flavours/glitch/components/status.jsx
Expand Up @@ -6,6 +6,7 @@
import StatusIcons from './status_icons';
import StatusContent from './status_content';
import StatusActionBar from './status_action_bar';
import StatusReactions from './status_reactions';
import AttachmentList from './attachment_list';
import Card from '../features/status/components/card';
import { injectIntl, FormattedMessage } from 'react-intl';
Expand All @@ -16,7 +17,7 @@
import classNames from 'classnames';
import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning';
import PollContainer from 'flavours/glitch/containers/poll_container';
import { displayMedia } from 'flavours/glitch/initial_state';
import { displayMedia, visibleReactions } from 'flavours/glitch/initial_state';

Check failure on line 20 in app/javascript/flavours/glitch/components/status.jsx

View workflow job for this annotation

GitHub Actions / lint

visibleReactions not found in 'flavours/glitch/initial_state'
import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';

// We use the component (and not the container) since we do not want
Expand Down Expand Up @@ -60,6 +61,7 @@

static contextTypes = {
router: PropTypes.object,
identity: PropTypes.object,
};

static propTypes = {
Expand All @@ -77,6 +79,8 @@
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 @@ -743,6 +747,7 @@
if (this.props.prepend && account) {
const notifKind = {
favourite: 'favourited',
reaction: 'reacted',
reblog: 'boosted',
reblogged_by: 'boosted',
status: 'posted',
Expand Down Expand Up @@ -824,6 +829,15 @@
rewriteMentions={settings.get('rewrite_mentions')}
/>

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

{!isCollapsed || !(muted || !settings.getIn(['collapsed', 'show_action_bar'])) ? (
<StatusActionBar
status={status}
Expand Down
28 changes: 26 additions & 2 deletions app/javascript/flavours/glitch/components/status_action_bar.jsx
Expand Up @@ -5,11 +5,12 @@ import IconButton from './icon_button';
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { me } from 'flavours/glitch/initial_state';
import { me, maxReactions } from 'flavours/glitch/initial_state';
import RelativeTimestamp from './relative_timestamp';
import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links';
import classNames from 'classnames';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions';
import EmojiPickerDropdown from '../features/compose/containers/emoji_picker_dropdown_container';

const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
Expand All @@ -28,6 +29,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: 'Favourite' },
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 Down Expand Up @@ -57,6 +59,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 @@ -114,6 +117,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;

Expand Down Expand Up @@ -195,10 +202,11 @@ 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 } = this.context.identity;

const anonymousAccess = !me;
const mutingConversation = status.get('muted');
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
Expand Down Expand Up @@ -299,6 +307,17 @@ class StatusActionBar extends ImmutablePureComponent {
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.hide)} icon='eye' onClick={this.handleHideClick} />
);

const canReact = permissions && status.get('reactions').filter(r => r.get('count') > 0 && r.get('me')).size < maxReactions;
const reactButton = (
<IconButton
className='status__action-bar-button'
onClick={this.handleNoOp} // EmojiPickerDropdown handles that
title={intl.formatMessage(messages.react)}
disabled={!canReact}
icon='plus'
/>
);

return (
<div className='status__action-bar'>
<IconButton
Expand All @@ -311,6 +330,11 @@ class StatusActionBar extends ImmutablePureComponent {
/>
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon={reblogIcon} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
{
permissions
? <EmojiPickerDropdown className='status__action-bar-button' onPickEmoji={this.handleEmojiPick} button={reactButton} disabled={!canReact} />
: reactButton
}
{shareButton}
<IconButton className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />

Expand Down
11 changes: 11 additions & 0 deletions app/javascript/flavours/glitch/components/status_prepend.jsx
Expand Up @@ -56,6 +56,14 @@ export default class StatusPrepend extends React.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 @@ -110,6 +118,9 @@ export default class StatusPrepend extends React.PureComponent {
case 'favourite':
iconId = 'star';
break;
case 'reaction':
iconId = 'plus';
break;
case 'featured':
iconId = 'thumb-tack';
break;
Expand Down