Merge remote-tracking branch 'catstodon/feature/emoji_reactions'
This commit is contained in:
commit
b763a47623
15 changed files with 497 additions and 34 deletions
170
app/javascript/flavours/glitch/components/status_reactions.jsx
Normal file
170
app/javascript/flavours/glitch/components/status_reactions.jsx
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { autoPlayGif, reduceMotion } from '../initial_state';
|
||||||
|
import spring from 'react-motion/lib/spring';
|
||||||
|
import TransitionMotion from 'react-motion/lib/TransitionMotion';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import unicodeMapping from '../features/emoji/emoji_unicode_mapping_light';
|
||||||
|
import { AnimatedNumber } from './animated_number';
|
||||||
|
import { assetHost } from '../utils/config';
|
||||||
|
|
||||||
|
export default class StatusReactions extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
statusId: PropTypes.string.isRequired,
|
||||||
|
reactions: ImmutablePropTypes.list.isRequired,
|
||||||
|
numVisible: PropTypes.number,
|
||||||
|
addReaction: PropTypes.func.isRequired,
|
||||||
|
canReact: PropTypes.bool.isRequired,
|
||||||
|
removeReaction: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
willEnter() {
|
||||||
|
return { scale: reduceMotion ? 1 : 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
willLeave() {
|
||||||
|
return { scale: reduceMotion ? 0 : spring(0, { stiffness: 170, damping: 26 }) };
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { reactions, numVisible } = this.props;
|
||||||
|
let visibleReactions = reactions
|
||||||
|
.filter(x => 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 (
|
||||||
|
<TransitionMotion styles={styles} willEnter={this.willEnter} willLeave={this.willLeave}>
|
||||||
|
{items => (
|
||||||
|
<div className={classNames('reactions-bar', { 'reactions-bar--empty': visibleReactions.isEmpty() })}>
|
||||||
|
{items.map(({ key, data, style }) => (
|
||||||
|
<Reaction
|
||||||
|
key={key}
|
||||||
|
statusId={this.props.statusId}
|
||||||
|
reaction={data}
|
||||||
|
style={{ transform: `scale(${style.scale})`, position: style.scale < 0.5 ? 'absolute' : 'static' }}
|
||||||
|
addReaction={this.props.addReaction}
|
||||||
|
removeReaction={this.props.removeReaction}
|
||||||
|
canReact={this.props.canReact}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TransitionMotion>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<button
|
||||||
|
className={classNames('reactions-bar__item', { active: reaction.get('me') })}
|
||||||
|
onClick={this.handleClick}
|
||||||
|
onMouseEnter={this.handleMouseEnter}
|
||||||
|
onMouseLeave={this.handleMouseLeave}
|
||||||
|
disabled={!this.props.canReact}
|
||||||
|
style={this.props.style}
|
||||||
|
>
|
||||||
|
<span className='reactions-bar__item__emoji'>
|
||||||
|
<Emoji
|
||||||
|
hovered={this.state.hovered}
|
||||||
|
emoji={reaction.get('name')}
|
||||||
|
url={reaction.get('url')}
|
||||||
|
staticUrl={reaction.get('static_url')}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className='reactions-bar__item__count'>
|
||||||
|
<AnimatedNumber value={reaction.get('count')} />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<img
|
||||||
|
draggable='false'
|
||||||
|
className='emojione'
|
||||||
|
alt={emoji}
|
||||||
|
title={title}
|
||||||
|
src={`${assetHost}/emoji/${filename}.svg`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const filename = (autoPlayGif || hovered) ? url : staticUrl;
|
||||||
|
const shortCode = `:${emoji}:`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
draggable='false'
|
||||||
|
className='emojione custom-emoji'
|
||||||
|
alt={shortCode}
|
||||||
|
title={shortCode}
|
||||||
|
src={filename}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -78,6 +78,15 @@
|
||||||
],
|
],
|
||||||
"path": "app/javascript/flavours/glitch/components/notification_purge_buttons.json"
|
"path": "app/javascript/flavours/glitch/components/notification_purge_buttons.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"descriptors": [
|
||||||
|
{
|
||||||
|
"defaultMessage": "React",
|
||||||
|
"id": "status.react"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"path": "app/javascript/flavours/glitch/components/status_action_bar.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"descriptors": [
|
"descriptors": [
|
||||||
{
|
{
|
||||||
|
@ -119,6 +128,15 @@
|
||||||
],
|
],
|
||||||
"path": "app/javascript/flavours/glitch/components/status_icons.json"
|
"path": "app/javascript/flavours/glitch/components/status_icons.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"descriptors": [
|
||||||
|
{
|
||||||
|
"defaultMessage": "{name} reacted to your status",
|
||||||
|
"id": "notification.reaction"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"path": "app/javascript/flavours/glitch/components/status_prepend.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"descriptors": [
|
"descriptors": [
|
||||||
{
|
{
|
||||||
|
@ -903,6 +921,24 @@
|
||||||
],
|
],
|
||||||
"path": "app/javascript/flavours/glitch/features/local_settings/page/index.json"
|
"path": "app/javascript/flavours/glitch/features/local_settings/page/index.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"descriptors": [
|
||||||
|
{
|
||||||
|
"defaultMessage": "Reactions:",
|
||||||
|
"id": "notifications.column_settings.reaction"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"path": "app/javascript/flavours/glitch/features/notifications/components/column_settings.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"descriptors": [
|
||||||
|
{
|
||||||
|
"defaultMessage": "Reactions",
|
||||||
|
"id": "notifications.filter.reactions"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"path": "app/javascript/flavours/glitch/features/notifications/components/filter_bar.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"descriptors": [
|
"descriptors": [
|
||||||
{
|
{
|
||||||
|
@ -956,6 +992,15 @@
|
||||||
],
|
],
|
||||||
"path": "app/javascript/flavours/glitch/features/reblogs/index.json"
|
"path": "app/javascript/flavours/glitch/features/reblogs/index.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"descriptors": [
|
||||||
|
{
|
||||||
|
"defaultMessage": "React",
|
||||||
|
"id": "status.react"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"path": "app/javascript/flavours/glitch/features/status/components/action_bar.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"descriptors": [
|
"descriptors": [
|
||||||
{
|
{
|
||||||
|
@ -1110,5 +1155,50 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"path": "app/javascript/flavours/glitch/features/ui/index.json"
|
"path": "app/javascript/flavours/glitch/features/ui/index.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"descriptors": [
|
||||||
|
{
|
||||||
|
"defaultMessage": "React",
|
||||||
|
"id": "status.react"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"path": "app/javascript/mastodon/components/status_action_bar.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"descriptors": [
|
||||||
|
{
|
||||||
|
"defaultMessage": "Reactions:",
|
||||||
|
"id": "notifications.column_settings.reaction"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"path": "app/javascript/mastodon/features/notifications/components/column_settings.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"descriptors": [
|
||||||
|
{
|
||||||
|
"defaultMessage": "Reactions",
|
||||||
|
"id": "notifications.filter.reactions"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"path": "app/javascript/mastodon/features/notifications/components/filter_bar.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"descriptors": [
|
||||||
|
{
|
||||||
|
"defaultMessage": "{name} reacted to your status",
|
||||||
|
"id": "notification.reaction"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"path": "app/javascript/mastodon/features/notifications/components/notification.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"descriptors": [
|
||||||
|
{
|
||||||
|
"defaultMessage": "React",
|
||||||
|
"id": "status.react"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"path": "app/javascript/mastodon/features/status/components/action_bar.json"
|
||||||
}
|
}
|
||||||
]
|
]
|
|
@ -76,12 +76,13 @@
|
||||||
"navigation_bar.misc": "Misc",
|
"navigation_bar.misc": "Misc",
|
||||||
"notification.markForDeletion": "Mark for deletion",
|
"notification.markForDeletion": "Mark for deletion",
|
||||||
"notification.reaction": "{name} reacted to your post",
|
"notification.reaction": "{name} reacted to your post",
|
||||||
"notifications.column_settings.reaction": "Reactions:",
|
|
||||||
"notification_purge.btn_all": "Select\nall",
|
"notification_purge.btn_all": "Select\nall",
|
||||||
"notification_purge.btn_apply": "Clear\nselected",
|
"notification_purge.btn_apply": "Clear\nselected",
|
||||||
"notification_purge.btn_invert": "Invert\nselection",
|
"notification_purge.btn_invert": "Invert\nselection",
|
||||||
"notification_purge.btn_none": "Select\nnone",
|
"notification_purge.btn_none": "Select\nnone",
|
||||||
"notification_purge.start": "Enter notification cleaning mode",
|
"notification_purge.start": "Enter notification cleaning mode",
|
||||||
|
"notifications.column_settings.reaction": "Reactions:",
|
||||||
|
"notifications.filter.reactions": "Reactions",
|
||||||
"notifications.marked_clear": "Clear selected notifications",
|
"notifications.marked_clear": "Clear selected notifications",
|
||||||
"notifications.marked_clear_confirmation": "Are you sure you want to permanently clear all selected notifications?",
|
"notifications.marked_clear_confirmation": "Are you sure you want to permanently clear all selected notifications?",
|
||||||
"onboarding.done": "Done",
|
"onboarding.done": "Done",
|
||||||
|
@ -201,6 +202,7 @@
|
||||||
"status.in_reply_to": "This toot is a reply",
|
"status.in_reply_to": "This toot is a reply",
|
||||||
"status.is_poll": "This toot is a poll",
|
"status.is_poll": "This toot is a poll",
|
||||||
"status.local_only": "Only visible from your instance",
|
"status.local_only": "Only visible from your instance",
|
||||||
|
"status.react": "React",
|
||||||
"status.sensitive_toggle": "Click to view",
|
"status.sensitive_toggle": "Click to view",
|
||||||
"status.uncollapse": "Uncollapse",
|
"status.uncollapse": "Uncollapse",
|
||||||
"web_app_crash.change_your_settings": "Change your {settings}",
|
"web_app_crash.change_your_settings": "Change your {settings}",
|
||||||
|
|
170
app/javascript/mastodon/components/status_reactions.jsx
Normal file
170
app/javascript/mastodon/components/status_reactions.jsx
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { autoPlayGif, reduceMotion } from '../initial_state';
|
||||||
|
import spring from 'react-motion/lib/spring';
|
||||||
|
import TransitionMotion from 'react-motion/lib/TransitionMotion';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import unicodeMapping from '../features/emoji/emoji_unicode_mapping_light';
|
||||||
|
import { AnimatedNumber } from './animated_number';
|
||||||
|
import { assetHost } from '../utils/config';
|
||||||
|
|
||||||
|
export default class StatusReactions extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
statusId: PropTypes.string.isRequired,
|
||||||
|
reactions: ImmutablePropTypes.list.isRequired,
|
||||||
|
numVisible: PropTypes.number,
|
||||||
|
addReaction: PropTypes.func.isRequired,
|
||||||
|
canReact: PropTypes.bool.isRequired,
|
||||||
|
removeReaction: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
willEnter() {
|
||||||
|
return { scale: reduceMotion ? 1 : 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
willLeave() {
|
||||||
|
return { scale: reduceMotion ? 0 : spring(0, { stiffness: 170, damping: 26 }) };
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { reactions, numVisible } = this.props;
|
||||||
|
let visibleReactions = reactions
|
||||||
|
.filter(x => 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 (
|
||||||
|
<TransitionMotion styles={styles} willEnter={this.willEnter} willLeave={this.willLeave}>
|
||||||
|
{items => (
|
||||||
|
<div className={classNames('reactions-bar', { 'reactions-bar--empty': visibleReactions.isEmpty() })}>
|
||||||
|
{items.map(({ key, data, style }) => (
|
||||||
|
<Reaction
|
||||||
|
key={key}
|
||||||
|
statusId={this.props.statusId}
|
||||||
|
reaction={data}
|
||||||
|
style={{ transform: `scale(${style.scale})`, position: style.scale < 0.5 ? 'absolute' : 'static' }}
|
||||||
|
addReaction={this.props.addReaction}
|
||||||
|
removeReaction={this.props.removeReaction}
|
||||||
|
canReact={this.props.canReact}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TransitionMotion>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<button
|
||||||
|
className={classNames('reactions-bar__item', { active: reaction.get('me') })}
|
||||||
|
onClick={this.handleClick}
|
||||||
|
onMouseEnter={this.handleMouseEnter}
|
||||||
|
onMouseLeave={this.handleMouseLeave}
|
||||||
|
disabled={!this.props.canReact}
|
||||||
|
style={this.props.style}
|
||||||
|
>
|
||||||
|
<span className='reactions-bar__item__emoji'>
|
||||||
|
<Emoji
|
||||||
|
hovered={this.state.hovered}
|
||||||
|
emoji={reaction.get('name')}
|
||||||
|
url={reaction.get('url')}
|
||||||
|
staticUrl={reaction.get('static_url')}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className='reactions-bar__item__count'>
|
||||||
|
<AnimatedNumber value={reaction.get('count')} />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<img
|
||||||
|
draggable='false'
|
||||||
|
className='emojione'
|
||||||
|
alt={emoji}
|
||||||
|
title={title}
|
||||||
|
src={`${assetHost}/emoji/${filename}.svg`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const filename = (autoPlayGif || hovered) ? url : staticUrl;
|
||||||
|
const shortCode = `:${emoji}:`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
draggable='false'
|
||||||
|
className='emojione custom-emoji'
|
||||||
|
alt={shortCode}
|
||||||
|
title={shortCode}
|
||||||
|
src={filename}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -179,23 +179,20 @@ class ActivityPub::Activity
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
# Ensure all emojis declared in the activity's tags are
|
# Ensure emoji declared in the activity's tags are
|
||||||
# present in the database and downloaded to the local cache.
|
# present in the database and downloaded to the local cache.
|
||||||
# Required by EmojiReact and Like for emoji reactions.
|
# Required by EmojiReact and Like for emoji reactions.
|
||||||
def process_emoji_tags(tags)
|
def process_emoji_tags(name, tags)
|
||||||
as_array(tags).each do |tag|
|
tag = as_array(tags).find { |item| item['type'] == 'Emoji' }
|
||||||
process_single_emoji tag if tag['type'] == 'Emoji'
|
return if tag.nil?
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_single_emoji(tag)
|
|
||||||
custom_emoji_parser = ActivityPub::Parser::CustomEmojiParser.new(tag)
|
custom_emoji_parser = ActivityPub::Parser::CustomEmojiParser.new(tag)
|
||||||
return if custom_emoji_parser.shortcode.blank? || custom_emoji_parser.image_remote_url.blank?
|
return if custom_emoji_parser.shortcode.blank? || custom_emoji_parser.image_remote_url.blank? || !name.eql?(custom_emoji_parser.shortcode)
|
||||||
|
|
||||||
emoji = CustomEmoji.find_by(shortcode: custom_emoji_parser.shortcode, domain: @account.domain)
|
emoji = CustomEmoji.find_by(shortcode: custom_emoji_parser.shortcode, domain: @account.domain)
|
||||||
return unless emoji.nil? ||
|
return emoji unless emoji.nil? ||
|
||||||
custom_emoji_parser.image_remote_url != emoji.image_remote_url ||
|
custom_emoji_parser.image_remote_url != emoji.image_remote_url ||
|
||||||
(custom_emoji_parser.updated_at && custom_emoji_parser.updated_at >= emoji.updated_at)
|
(custom_emoji_parser.updated_at && custom_emoji_parser.updated_at >= emoji.updated_at)
|
||||||
|
|
||||||
begin
|
begin
|
||||||
emoji ||= CustomEmoji.new(domain: @account.domain,
|
emoji ||= CustomEmoji.new(domain: @account.domain,
|
||||||
|
@ -205,6 +202,8 @@ class ActivityPub::Activity
|
||||||
emoji.save
|
emoji.save
|
||||||
rescue Seahorse::Client::NetworkingError => e
|
rescue Seahorse::Client::NetworkingError => e
|
||||||
Rails.logger.warn "Error fetching emoji: #{e}"
|
Rails.logger.warn "Error fetching emoji: #{e}"
|
||||||
|
return
|
||||||
end
|
end
|
||||||
|
emoji
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,20 +6,21 @@ class ActivityPub::Activity::EmojiReact < ActivityPub::Activity
|
||||||
name = @json['content']
|
name = @json['content']
|
||||||
return if original_status.nil? ||
|
return if original_status.nil? ||
|
||||||
!original_status.account.local? ||
|
!original_status.account.local? ||
|
||||||
delete_arrived_first?(@json['id']) ||
|
delete_arrived_first?(@json['id'])
|
||||||
@account.reacted?(original_status, name)
|
|
||||||
|
|
||||||
custom_emoji = nil
|
|
||||||
if /^:.*:$/.match?(name)
|
if /^:.*:$/.match?(name)
|
||||||
process_emoji_tags(@json['tag'])
|
|
||||||
|
|
||||||
name.delete! ':'
|
name.delete! ':'
|
||||||
custom_emoji = CustomEmoji.find_by(shortcode: name, domain: @account.domain)
|
custom_emoji = process_emoji_tags(name, @json['tag'])
|
||||||
|
|
||||||
return if custom_emoji.nil?
|
return if custom_emoji.nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
return if @account.reacted?(original_status, name, custom_emoji)
|
||||||
|
|
||||||
reaction = original_status.status_reactions.create!(account: @account, name: name, custom_emoji: custom_emoji)
|
reaction = original_status.status_reactions.create!(account: @account, name: name, custom_emoji: custom_emoji)
|
||||||
|
|
||||||
LocalNotificationWorker.perform_async(original_status.account_id, reaction.id, 'StatusReaction', 'reaction')
|
LocalNotificationWorker.perform_async(original_status.account_id, reaction.id, 'StatusReaction', 'reaction')
|
||||||
|
rescue ActiveRecord::RecordInvalid
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,7 @@ class ActivityPub::Activity::Like < ActivityPub::Activity
|
||||||
original_status = status_from_uri(object_uri)
|
original_status = status_from_uri(object_uri)
|
||||||
return if original_status.nil? || !original_status.account.local? || delete_arrived_first?(@json['id'])
|
return if original_status.nil? || !original_status.account.local? || delete_arrived_first?(@json['id'])
|
||||||
|
|
||||||
return if maybe_process_misskey_reaction(original_status)
|
return if maybe_process_misskey_reaction
|
||||||
|
|
||||||
return if @account.favourited?(original_status)
|
return if @account.favourited?(original_status)
|
||||||
|
|
||||||
|
@ -17,22 +17,24 @@ class ActivityPub::Activity::Like < ActivityPub::Activity
|
||||||
|
|
||||||
# Misskey delivers reactions as likes with the emoji in _misskey_reaction
|
# Misskey delivers reactions as likes with the emoji in _misskey_reaction
|
||||||
# see https://misskey-hub.net/ns.html#misskey-reaction for details
|
# see https://misskey-hub.net/ns.html#misskey-reaction for details
|
||||||
def maybe_process_misskey_reaction(original_status)
|
def maybe_process_misskey_reaction
|
||||||
|
original_status = status_from_uri(object_uri)
|
||||||
name = @json['_misskey_reaction']
|
name = @json['_misskey_reaction']
|
||||||
return false if name.nil?
|
return false if name.nil?
|
||||||
|
|
||||||
custom_emoji = nil
|
|
||||||
if /^:.*:$/.match?(name)
|
if /^:.*:$/.match?(name)
|
||||||
process_emoji_tags(@json['tag'])
|
|
||||||
|
|
||||||
name.delete! ':'
|
name.delete! ':'
|
||||||
custom_emoji = CustomEmoji.find_by(shortcode: name, domain: @account.domain)
|
custom_emoji = process_emoji_tags(name, @json['tag'])
|
||||||
|
|
||||||
return false if custom_emoji.nil? # invalid custom emoji, treat it as a regular like
|
return false if custom_emoji.nil? # invalid custom emoji, treat it as a regular like
|
||||||
end
|
end
|
||||||
return true if @account.reacted?(original_status, name)
|
return true if @account.reacted?(original_status, name, custom_emoji)
|
||||||
|
|
||||||
reaction = original_status.status_reactions.create!(account: @account, name: name, custom_emoji: custom_emoji)
|
reaction = original_status.status_reactions.create!(account: @account, name: name, custom_emoji: custom_emoji)
|
||||||
LocalNotificationWorker.perform_async(original_status.account_id, reaction.id, 'StatusReaction', 'reaction')
|
LocalNotificationWorker.perform_async(original_status.account_id, reaction.id, 'StatusReaction', 'reaction')
|
||||||
true
|
true
|
||||||
|
# account tried to react with disabled custom emoji. Returning true to discard activity.
|
||||||
|
rescue ActiveRecord::RecordInvalid
|
||||||
|
true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -110,6 +110,31 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity
|
||||||
if @account.favourited?(status)
|
if @account.favourited?(status)
|
||||||
favourite = status.favourites.where(account: @account).first
|
favourite = status.favourites.where(account: @account).first
|
||||||
favourite&.destroy
|
favourite&.destroy
|
||||||
|
elsif @object['_misskey_reaction'].present?
|
||||||
|
undo_emoji_react
|
||||||
|
else
|
||||||
|
delete_later!(object_uri)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def undo_emoji_react
|
||||||
|
name = @object['content'] || @object['_misskey_reaction']
|
||||||
|
return if name.nil?
|
||||||
|
|
||||||
|
status = status_from_uri(target_uri)
|
||||||
|
|
||||||
|
return if status.nil? || !status.account.local?
|
||||||
|
|
||||||
|
if /^:.*:$/.match?(name)
|
||||||
|
name.delete! ':'
|
||||||
|
custom_emoji = process_emoji_tags(name, @object['tag'])
|
||||||
|
|
||||||
|
return if custom_emoji.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
if @account.reacted?(status, name, custom_emoji)
|
||||||
|
reaction = status.status_reactions.where(account: @account, name: name).first
|
||||||
|
reaction&.destroy
|
||||||
else
|
else
|
||||||
delete_later!(object_uri)
|
delete_later!(object_uri)
|
||||||
end
|
end
|
||||||
|
|
|
@ -243,8 +243,8 @@ module AccountInteractions
|
||||||
status.proper.favourites.where(account: self).exists?
|
status.proper.favourites.where(account: self).exists?
|
||||||
end
|
end
|
||||||
|
|
||||||
def reacted?(status, name)
|
def reacted?(status, name, custom_emoji = nil)
|
||||||
status.proper.status_reactions.where(account: self, name: name).exists?
|
status.proper.status_reactions.where(account: self, name: name, custom_emoji: custom_emoji).exists?
|
||||||
end
|
end
|
||||||
|
|
||||||
def bookmarked?(status)
|
def bookmarked?(status)
|
||||||
|
|
|
@ -300,7 +300,7 @@ class Status < ApplicationRecord
|
||||||
if account.nil?
|
if account.nil?
|
||||||
scope.select('name, custom_emoji_id, count(*) as count, false as me')
|
scope.select('name, custom_emoji_id, count(*) as count, false as me')
|
||||||
else
|
else
|
||||||
scope.select("name, custom_emoji_id, count(*) as count, exists(select 1 from status_reactions r where r.account_id = #{account.id} and r.status_id = status_reactions.status_id and r.name = status_reactions.name) as me")
|
scope.select("name, custom_emoji_id, count(*) as count, exists(select 1 from status_reactions r where r.account_id = #{account.id} and r.status_id = status_reactions.status_id and r.name = status_reactions.name and (r.custom_emoji_id = status_reactions.custom_emoji_id or r.custom_emoji_id is null and status_reactions.custom_emoji_id is null)) as me")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,8 @@ class StatusReaction < ApplicationRecord
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
# Sets custom_emoji to nil when disabled
|
||||||
def set_custom_emoji
|
def set_custom_emoji
|
||||||
self.custom_emoji = CustomEmoji.find_by(shortcode: name, domain: account.domain) if name.blank?
|
self.custom_emoji = CustomEmoji.find_by(disabled: false, shortcode: name, domain: custom_emoji.domain) if name.present? && custom_emoji.present?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,6 +8,8 @@ class ReactService < BaseService
|
||||||
authorize_with account, status, :react?
|
authorize_with account, status, :react?
|
||||||
|
|
||||||
name, domain = emoji.split('@')
|
name, domain = emoji.split('@')
|
||||||
|
return unless domain.nil? || status.local?
|
||||||
|
|
||||||
custom_emoji = CustomEmoji.find_by(shortcode: name, domain: domain)
|
custom_emoji = CustomEmoji.find_by(shortcode: name, domain: domain)
|
||||||
reaction = StatusReaction.find_by(account: account, status: status, name: name, custom_emoji: custom_emoji)
|
reaction = StatusReaction.find_by(account: account, status: status, name: name, custom_emoji: custom_emoji)
|
||||||
return reaction unless reaction.nil?
|
return reaction unless reaction.nil?
|
||||||
|
|
|
@ -19,7 +19,7 @@ class StatusReactionValidator < ActiveModel::Validator
|
||||||
end
|
end
|
||||||
|
|
||||||
def new_reaction?(reaction)
|
def new_reaction?(reaction)
|
||||||
!reaction.status.status_reactions.exists?(status: reaction.status, account: reaction.account, name: reaction.name)
|
!reaction.status.status_reactions.exists?(status: reaction.status, account: reaction.account, name: reaction.name, custom_emoji: reaction.custom_emoji)
|
||||||
end
|
end
|
||||||
|
|
||||||
def limit_reached?(reaction)
|
def limit_reached?(reaction)
|
||||||
|
|
|
@ -38,6 +38,11 @@ en:
|
||||||
title: User verification
|
title: User verification
|
||||||
generic:
|
generic:
|
||||||
use_this: Use this
|
use_this: Use this
|
||||||
|
notification_mailer:
|
||||||
|
reaction:
|
||||||
|
body: "%{name} reacted to your post:"
|
||||||
|
subject: "%{name} reacted to your post"
|
||||||
|
title: New reaction
|
||||||
settings:
|
settings:
|
||||||
flavours: Flavours
|
flavours: Flavours
|
||||||
notification_mailer:
|
notification_mailer:
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe StatusReaction do
|
|
||||||
pending "add some examples to (or delete) #{__FILE__}"
|
|
||||||
end
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue