1
0

Use new PR 2462 for reactions

This commit is contained in:
Noa Himesaka 2023-11-11 22:32:45 +09:00
commit 10ad23fdb4
28 changed files with 286 additions and 22 deletions

View File

@ -70,7 +70,6 @@ export const defaultMediaVisibility = (status, settings) => {
class Status extends ImmutablePureComponent { class Status extends ImmutablePureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object,
identity: PropTypes.object, identity: PropTypes.object,
}; };
@ -788,6 +787,9 @@ class Status extends ImmutablePureComponent {
muted, muted,
}, 'focusable'); }, 'focusable');
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
contentMedia.push(hashtagBar);
return ( return (
<HotKeys handlers={handlers}> <HotKeys handlers={handlers}>
<div <div
@ -837,6 +839,16 @@ class Status extends ImmutablePureComponent {
disabled={!history} disabled={!history}
tagLinks={settings.get('tag_misleading_links')} tagLinks={settings.get('tag_misleading_links')}
rewriteMentions={settings.get('rewrite_mentions')} rewriteMentions={settings.get('rewrite_mentions')}
{...statusContentProps}
/>
<StatusReactions
statusId={status.get('id')}
reactions={status.get('reactions')}
numVisible={visibleReactions}
addReaction={this.props.onReactionAdd}
removeReaction={this.props.onReactionRemove}
canReact={this.context.identity.signedIn}
/> />
<StatusReactions <StatusReactions

View File

@ -9,6 +9,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
import EmojiPickerDropdown from 'flavours/glitch/features/compose/containers/emoji_picker_dropdown_container';
import { me, maxReactions } from 'flavours/glitch/initial_state'; import { me, maxReactions } from 'flavours/glitch/initial_state';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions';
import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links'; import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links';
@ -341,7 +342,7 @@ class StatusActionBar extends ImmutablePureComponent {
? <EmojiPickerDropdown className='status__action-bar-button' onPickEmoji={this.handleEmojiPick} button={reactButton} disabled={!canReact} /> ? <EmojiPickerDropdown className='status__action-bar-button' onPickEmoji={this.handleEmojiPick} button={reactButton} disabled={!canReact} />
: reactButton : reactButton
} }
<IconButton className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /> <IconButton className='status__action-bar-button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
{filterButton} {filterButton}

View File

@ -1,15 +1,20 @@
import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types'; 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 React from 'react';
import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import TransitionMotion from 'react-motion/lib/TransitionMotion';
import spring from 'react-motion/lib/spring';
import { unicodeMapping } from '../features/emoji/emoji_unicode_mapping_light'; import { unicodeMapping } from '../features/emoji/emoji_unicode_mapping_light';
import { AnimatedNumber } from './animated_number'; import { autoPlayGif, reduceMotion } from '../initial_state';
import { assetHost } from '../utils/config'; import { assetHost } from '../utils/config';
import { AnimatedNumber } from './animated_number';
export default class StatusReactions extends ImmutablePureComponent { export default class StatusReactions extends ImmutablePureComponent {
static propTypes = { static propTypes = {

View File

@ -10,6 +10,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { IconButton } from 'flavours/glitch/components/icon_button'; import { IconButton } from 'flavours/glitch/components/icon_button';
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
import EmojiPickerDropdown from 'flavours/glitch/features/compose/containers/emoji_picker_dropdown_container';
import { me, maxReactions } from 'flavours/glitch/initial_state'; import { me, maxReactions } from 'flavours/glitch/initial_state';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions';
import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links'; import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links';
@ -88,7 +89,7 @@ class ActionBar extends PureComponent {
handleEmojiPick = data => { handleEmojiPick = data => {
this.props.onReactionAdd(this.props.status.get('id'), data.native.replace(/:/g, ''), data.imageUrl); this.props.onReactionAdd(this.props.status.get('id'), data.native.replace(/:/g, ''), data.imageUrl);
} };
handleBookmarkClick = (e) => { handleBookmarkClick = (e) => {
this.props.onBookmark(this.props.status, e); this.props.onBookmark(this.props.status, e);
@ -149,7 +150,7 @@ class ActionBar extends PureComponent {
navigator.clipboard.writeText(url); navigator.clipboard.writeText(url);
}; };
handleNoOp = () => {} // hack for reaction add button handleNoOp = () => {}; // hack for reaction add button
render () { render () {
const { status, intl } = this.props; const { status, intl } = this.props;

View File

@ -13,6 +13,7 @@ import AttachmentList from 'flavours/glitch/components/attachment_list';
import { Avatar } from 'flavours/glitch/components/avatar'; import { Avatar } from 'flavours/glitch/components/avatar';
import { DisplayName } from 'flavours/glitch/components/display_name'; import { DisplayName } from 'flavours/glitch/components/display_name';
import EditedTimestamp from 'flavours/glitch/components/edited_timestamp'; import EditedTimestamp from 'flavours/glitch/components/edited_timestamp';
import { getHashtagBarForStatus } from 'flavours/glitch/components/hashtag_bar';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
import MediaGallery from 'flavours/glitch/components/media_gallery'; import MediaGallery from 'flavours/glitch/components/media_gallery';
import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder'; import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
@ -31,7 +32,6 @@ import Card from './card';
class DetailedStatus extends ImmutablePureComponent { class DetailedStatus extends ImmutablePureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object,
identity: PropTypes.object, identity: PropTypes.object,
}; };
@ -311,6 +311,9 @@ class DetailedStatus extends ImmutablePureComponent {
); );
} }
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
contentMedia.push(hashtagBar);
return ( return (
<div style={outerStyle}> <div style={outerStyle}>
<div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact })} data-status-by={status.getIn(['account', 'acct'])}> <div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact })} data-status-by={status.getIn(['account', 'acct'])}>
@ -333,6 +336,15 @@ class DetailedStatus extends ImmutablePureComponent {
tagLinks={settings.get('tag_misleading_links')} tagLinks={settings.get('tag_misleading_links')}
rewriteMentions={settings.get('rewrite_mentions')} rewriteMentions={settings.get('rewrite_mentions')}
disabled disabled
{...statusContentProps}
/>
<StatusReactions
statusId={status.get('id')}
reactions={status.get('reactions')}
addReaction={this.props.onReactionAdd}
removeReaction={this.props.onReactionRemove}
canReact={this.context.identity.signedIn}
/> />
<StatusReactions <StatusReactions

View File

@ -62,6 +62,7 @@
* @property {boolean} limited_federation_mode * @property {boolean} limited_federation_mode
* @property {string} locale * @property {string} locale
* @property {string | null} mascot * @property {string | null} mascot
* @property {number} max_reactions
* @property {string=} me * @property {string=} me
* @property {string=} moved_to_account_id * @property {string=} moved_to_account_id
* @property {string=} owner * @property {string=} owner

View File

@ -191,6 +191,7 @@
"status.react": "React", "status.react": "React",
"status.sensitive_toggle": "Click to view", "status.sensitive_toggle": "Click to view",
"status.uncollapse": "Uncollapse", "status.uncollapse": "Uncollapse",
"tooltips.reactions": "Reactions",
"web_app_crash.change_your_settings": "Change your {settings}", "web_app_crash.change_your_settings": "Change your {settings}",
"web_app_crash.content": "You could try any of the following:", "web_app_crash.content": "You could try any of the following:",
"web_app_crash.debug_info": "Debug information", "web_app_crash.debug_info": "Debug information",

View File

@ -62,6 +62,7 @@ const initialState = ImmutableMap({
follow: true, follow: true,
follow_request: false, follow_request: false,
favourite: true, favourite: true,
reaction: true,
reblog: true, reblog: true,
reaction: true, reaction: true,
mention: true, mention: true,
@ -76,6 +77,7 @@ const initialState = ImmutableMap({
follow: true, follow: true,
follow_request: false, follow_request: false,
favourite: true, favourite: true,
reaction: true,
reblog: true, reblog: true,
reaction: true, reaction: true,
mention: true, mention: true,

View File

@ -18,6 +18,11 @@ import {
REACTION_REMOVE_REQUEST, REACTION_REMOVE_REQUEST,
UNBOOKMARK_REQUEST, UNBOOKMARK_REQUEST,
UNBOOKMARK_FAIL, UNBOOKMARK_FAIL,
REACTION_UPDATE,
REACTION_ADD_FAIL,
REACTION_REMOVE_FAIL,
REACTION_ADD_REQUEST,
REACTION_REMOVE_REQUEST,
} from 'flavours/glitch/actions/interactions'; } from 'flavours/glitch/actions/interactions';
import { import {
STATUS_MUTE_SUCCESS, STATUS_MUTE_SUCCESS,
@ -50,6 +55,43 @@ const deleteStatus = (state, id, references) => {
return state.delete(id); return state.delete(id);
}; };
const updateReaction = (state, id, name, updater) => state.update(
id,
status => status.update(
'reactions',
reactions => {
const index = reactions.findIndex(reaction => reaction.get('name') === name);
if (index > -1) {
return reactions.update(index, reaction => updater(reaction));
} else {
return reactions.push(updater(fromJS({ name, count: 0 })));
}
},
),
);
const updateReactionCount = (state, reaction) => updateReaction(state, reaction.status_id, reaction.name, x => x.set('count', reaction.count));
// The url parameter is only used when adding a new custom emoji reaction
// (one that wasn't in the reactions list before) because we don't have its
// URL yet. In all other cases, it's undefined.
const addReaction = (state, id, name, url) => updateReaction(
state,
id,
name,
x => x.set('me', true)
.update('count', n => n + 1)
.update('url', old => old ? old : url)
.update('static_url', old => old ? old : url),
);
const removeReaction = (state, id, name) => updateReaction(
state,
id,
name,
x => x.set('me', false).update('count', n => n - 1),
);
const statusTranslateSuccess = (state, id, translation) => { const statusTranslateSuccess = (state, id, translation) => {
return state.withMutations(map => { return state.withMutations(map => {
map.setIn([id, 'translation'], fromJS(normalizeStatusTranslation(translation, map.get(id)))); map.setIn([id, 'translation'], fromJS(normalizeStatusTranslation(translation, map.get(id))));

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -32,6 +32,7 @@ import { RelativeTimestamp } from './relative_timestamp';
import StatusActionBar from './status_action_bar'; import StatusActionBar from './status_action_bar';
import StatusReactions from './status_reactions'; import StatusReactions from './status_reactions';
import StatusContent from './status_content'; import StatusContent from './status_content';
import StatusReactions from './status_reactions';
import { VisibilityIcon } from './visibility_icon'; import { VisibilityIcon } from './visibility_icon';
const domParser = new DOMParser(); const domParser = new DOMParser();
@ -80,7 +81,6 @@ const messages = defineMessages({
class Status extends ImmutablePureComponent { class Status extends ImmutablePureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object,
identity: PropTypes.object, identity: PropTypes.object,
}; };

View File

@ -9,6 +9,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { ReactComponent as AddIcon } from '@material-symbols/svg-600/outlined/add.svg';
import { ReactComponent as BookmarkIcon } from '@material-symbols/svg-600/outlined/bookmark-fill.svg'; import { ReactComponent as BookmarkIcon } from '@material-symbols/svg-600/outlined/bookmark-fill.svg';
import { ReactComponent as BookmarkBorderIcon } from '@material-symbols/svg-600/outlined/bookmark.svg'; import { ReactComponent as BookmarkBorderIcon } from '@material-symbols/svg-600/outlined/bookmark.svg';
import { ReactComponent as MoreHorizIcon } from '@material-symbols/svg-600/outlined/more_horiz.svg'; import { ReactComponent as MoreHorizIcon } from '@material-symbols/svg-600/outlined/more_horiz.svg';
@ -24,6 +25,7 @@ import EmojiPickerDropdown from '../features/compose/containers/emoji_picker_dro
import { WithRouterPropTypes } from 'mastodon/utils/react_router'; import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import DropdownMenuContainer from '../containers/dropdown_menu_container'; import DropdownMenuContainer from '../containers/dropdown_menu_container';
import EmojiPickerDropdown from '../features/compose/containers/emoji_picker_dropdown_container';
import { me, maxReactions } from '../initial_state'; import { me, maxReactions } from '../initial_state';
import { IconButton } from './icon_button'; import { IconButton } from './icon_button';
@ -147,7 +149,7 @@ class StatusActionBar extends ImmutablePureComponent {
handleEmojiPick = data => { handleEmojiPick = data => {
this.props.onReactionAdd(this.props.status.get('id'), data.native.replace(/:/g, ''), data.imageUrl); this.props.onReactionAdd(this.props.status.get('id'), data.native.replace(/:/g, ''), data.imageUrl);
} };
handleReblogClick = e => { handleReblogClick = e => {
const { signedIn } = this.context.identity; const { signedIn } = this.context.identity;
@ -252,7 +254,7 @@ class StatusActionBar extends ImmutablePureComponent {
this.props.onFilter(); this.props.onFilter();
}; };
handleNoOp = () => {} // hack for reaction add button handleNoOp = () => {}; // hack for reaction add button
render () { render () {
const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props; const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props;
@ -396,6 +398,17 @@ class StatusActionBar extends ImmutablePureComponent {
); );
const isReply = status.get('in_reply_to_account_id') === status.getIn(['account', 'id']); const isReply = status.get('in_reply_to_account_id') === status.getIn(['account', 'id']);
const canReact = signedIn && 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'
iconComponent={AddIcon}
/>
);
return ( return (
<div className='status__action-bar'> <div className='status__action-bar'>

View File

@ -1,3 +1,4 @@
<<<<<<< HEAD
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
@ -10,6 +11,25 @@ import { unicodeMapping } from '../features/emoji/emoji_unicode_mapping_light';
import { AnimatedNumber } from './animated_number'; import { AnimatedNumber } from './animated_number';
import { assetHost } from '../utils/config'; import { assetHost } from '../utils/config';
=======
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import TransitionMotion from 'react-motion/lib/TransitionMotion';
import spring from 'react-motion/lib/spring';
import { unicodeMapping } from '../features/emoji/emoji_unicode_mapping_light';
import { autoPlayGif, reduceMotion } from '../initial_state';
import { assetHost } from '../utils/config';
import { AnimatedNumber } from './animated_number';
>>>>>>> pr2462
export default class StatusReactions extends ImmutablePureComponent { export default class StatusReactions extends ImmutablePureComponent {
static propTypes = { static propTypes = {

View File

@ -9,6 +9,7 @@ import { withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { ReactComponent as AddIcon } from '@material-symbols/svg-600/outlined/add.svg';
import { ReactComponent as BookmarkIcon } from '@material-symbols/svg-600/outlined/bookmark-fill.svg'; import { ReactComponent as BookmarkIcon } from '@material-symbols/svg-600/outlined/bookmark-fill.svg';
import { ReactComponent as BookmarkBorderIcon } from '@material-symbols/svg-600/outlined/bookmark.svg'; import { ReactComponent as BookmarkBorderIcon } from '@material-symbols/svg-600/outlined/bookmark.svg';
import { ReactComponent as MoreHorizIcon } from '@material-symbols/svg-600/outlined/more_horiz.svg'; import { ReactComponent as MoreHorizIcon } from '@material-symbols/svg-600/outlined/more_horiz.svg';
@ -23,8 +24,13 @@ import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { IconButton } from '../../../components/icon_button'; import { IconButton } from '../../../components/icon_button';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
<<<<<<< HEAD
import EmojiPickerDropdown from '../../compose/containers/emoji_picker_dropdown_container'; import EmojiPickerDropdown from '../../compose/containers/emoji_picker_dropdown_container';
import { me, maxReactions } from '../../../initial_state'; import { me, maxReactions } from '../../../initial_state';
=======
import { me, maxReactions } from '../../../initial_state';
import EmojiPickerDropdown from '../../compose/containers/emoji_picker_dropdown_container';
>>>>>>> pr2462
const messages = defineMessages({ const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' }, delete: { id: 'status.delete', defaultMessage: 'Delete' },
@ -111,7 +117,11 @@ class ActionBar extends PureComponent {
handleEmojiPick = data => { handleEmojiPick = data => {
this.props.onReactionAdd(this.props.status.get('id'), data.native.replace(/:/g, '')); this.props.onReactionAdd(this.props.status.get('id'), data.native.replace(/:/g, ''));
<<<<<<< HEAD
} }
=======
};
>>>>>>> pr2462
handleBookmarkClick = (e) => { handleBookmarkClick = (e) => {
this.props.onBookmark(this.props.status, e); this.props.onBookmark(this.props.status, e);
@ -200,7 +210,11 @@ class ActionBar extends PureComponent {
navigator.clipboard.writeText(url); navigator.clipboard.writeText(url);
}; };
<<<<<<< HEAD
handleNoOp = () => {} // hack for reaction add button handleNoOp = () => {} // hack for reaction add button
=======
handleNoOp = () => {}; // hack for reaction add button
>>>>>>> pr2462
render () { render () {
const { status, relationship, intl } = this.props; const { status, relationship, intl } = this.props;
@ -295,6 +309,10 @@ class ActionBar extends PureComponent {
title={intl.formatMessage(messages.react)} title={intl.formatMessage(messages.react)}
disabled={!canReact} disabled={!canReact}
icon='plus' icon='plus'
<<<<<<< HEAD
=======
iconComponent={AddIcon}
>>>>>>> pr2462
/> />
); );

View File

@ -34,7 +34,6 @@ import Card from './card';
class DetailedStatus extends ImmutablePureComponent { class DetailedStatus extends ImmutablePureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object,
identity: PropTypes.object, identity: PropTypes.object,
}; };
@ -326,6 +325,14 @@ class DetailedStatus extends ImmutablePureComponent {
{expanded && hashtagBar} {expanded && hashtagBar}
<StatusReactions
statusId={status.get('id')}
reactions={status.get('reactions')}
addReaction={this.props.onReactionAdd}
removeReaction={this.props.onReactionRemove}
canReact={this.context.identity.signedIn}
/>
<div className='detailed-status__meta'> <div className='detailed-status__meta'>
<a className='detailed-status__datetime' href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} target='_blank' rel='noopener noreferrer'> <a className='detailed-status__datetime' href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} target='_blank' rel='noopener noreferrer'>
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' /> <FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />

View File

@ -431,6 +431,7 @@
"notification.mention": "{name} mentioned you", "notification.mention": "{name} mentioned you",
"notification.own_poll": "Your poll has ended", "notification.own_poll": "Your poll has ended",
"notification.poll": "A poll you have voted in has ended", "notification.poll": "A poll you have voted in has ended",
"notification.reaction": "{name} reacted to your post",
"notification.reblog": "{name} boosted your post", "notification.reblog": "{name} boosted your post",
"notification.status": "{name} just posted", "notification.status": "{name} just posted",
"notification.update": "{name} edited a post", "notification.update": "{name} edited a post",
@ -449,6 +450,7 @@
"notifications.column_settings.mention": "Mentions:", "notifications.column_settings.mention": "Mentions:",
"notifications.column_settings.poll": "Poll results:", "notifications.column_settings.poll": "Poll results:",
"notifications.column_settings.push": "Push notifications", "notifications.column_settings.push": "Push notifications",
"notifications.column_settings.reaction": "Reactions:",
"notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.reblog": "Boosts:",
"notifications.column_settings.show": "Show in column", "notifications.column_settings.show": "Show in column",
"notifications.column_settings.sound": "Play sound", "notifications.column_settings.sound": "Play sound",
@ -651,6 +653,7 @@
"status.pin": "Pin on profile", "status.pin": "Pin on profile",
"status.pinned": "Pinned post", "status.pinned": "Pinned post",
"status.read_more": "Read more", "status.read_more": "Read more",
"status.react": "React",
"status.reblog": "Boost", "status.reblog": "Boost",
"status.reblog_private": "Boost with original visibility", "status.reblog_private": "Boost with original visibility",
"status.reblogged_by": "{name} boosted", "status.reblogged_by": "{name} boosted",
@ -689,6 +692,7 @@
"timeline_hint.resources.followers": "Followers", "timeline_hint.resources.followers": "Followers",
"timeline_hint.resources.follows": "Follows", "timeline_hint.resources.follows": "Follows",
"timeline_hint.resources.statuses": "Older posts", "timeline_hint.resources.statuses": "Older posts",
"tooltips.reactions": "Reactions",
"trends.counter_by_accounts": "{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {{days} days}}", "trends.counter_by_accounts": "{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {{days} days}}",
"trends.trending_now": "Trending now", "trends.trending_now": "Trending now",
"tooltips.reactions": "Reactions", "tooltips.reactions": "Reactions",

View File

@ -57,6 +57,7 @@ const initialState = ImmutableMap({
follow: true, follow: true,
follow_request: false, follow_request: false,
favourite: true, favourite: true,
reaction: true,
reblog: true, reblog: true,
reaction: true, reaction: true,
mention: true, mention: true,
@ -71,6 +72,7 @@ const initialState = ImmutableMap({
follow: true, follow: true,
follow_request: false, follow_request: false,
favourite: true, favourite: true,
reaction: true,
reblog: true, reblog: true,
reaction: true, reaction: true,
mention: true, mention: true,

View File

@ -20,6 +20,11 @@ import {
REACTION_REMOVE_REQUEST, REACTION_REMOVE_REQUEST,
UNBOOKMARK_REQUEST, UNBOOKMARK_REQUEST,
UNBOOKMARK_FAIL, UNBOOKMARK_FAIL,
REACTION_UPDATE,
REACTION_ADD_FAIL,
REACTION_REMOVE_FAIL,
REACTION_ADD_REQUEST,
REACTION_REMOVE_REQUEST,
} from '../actions/interactions'; } from '../actions/interactions';
import { import {
STATUS_MUTE_SUCCESS, STATUS_MUTE_SUCCESS,
@ -47,6 +52,43 @@ const deleteStatus = (state, id, references) => {
return state.delete(id); return state.delete(id);
}; };
const updateReaction = (state, id, name, updater) => state.update(
id,
status => status.update(
'reactions',
reactions => {
const index = reactions.findIndex(reaction => reaction.get('name') === name);
if (index > -1) {
return reactions.update(index, reaction => updater(reaction));
} else {
return reactions.push(updater(fromJS({ name, count: 0 })));
}
},
),
);
const updateReactionCount = (state, reaction) => updateReaction(state, reaction.status_id, reaction.name, x => x.set('count', reaction.count));
// The url parameter is only used when adding a new custom emoji reaction
// (one that wasn't in the reactions list before) because we don't have its
// URL yet. In all other cases, it's undefined.
const addReaction = (state, id, name, url) => updateReaction(
state,
id,
name,
x => x.set('me', true)
.update('count', n => n + 1)
.update('url', old => old ? old : url)
.update('static_url', old => old ? old : url),
);
const removeReaction = (state, id, name) => updateReaction(
state,
id,
name,
x => x.set('me', false).update('count', n => n - 1),
);
const statusTranslateSuccess = (state, id, translation) => { const statusTranslateSuccess = (state, id, translation) => {
return state.withMutations(map => { return state.withMutations(map => {
map.setIn([id, 'translation'], fromJS(normalizeStatusTranslation(translation, map.get(id)))); map.setIn([id, 'translation'], fromJS(normalizeStatusTranslation(translation, map.get(id))));

View File

@ -6,8 +6,8 @@ class NotificationMailer < ApplicationMailer
:routing :routing
before_action :process_params before_action :process_params
before_action :set_status, only: [:mention, :favourite, :reblog] before_action :set_status, only: [:mention, :favourite, :reaction, :reblog]
before_action :set_account, only: [:follow, :favourite, :reblog, :follow_request] before_action :set_account, only: [:follow, :favourite, :reaction, :reblog, :follow_request]
after_action :set_list_headers! after_action :set_list_headers!
default to: -> { email_address_with_name(@user.email, @me.username) } default to: -> { email_address_with_name(@user.email, @me.username) }
@ -38,6 +38,15 @@ class NotificationMailer < ApplicationMailer
end end
end end
def reaction
return unless @user.functional? && @status.present?
locale_for_account(@me) do
thread_by_conversation(@status.conversation)
mail subject: default_i18n_subject(name: @account.acct)
end
end
def reblog def reblog
return unless @user.functional? && @status.present? return unless @user.functional? && @status.present?

View File

@ -22,6 +22,10 @@ class StatusReaction < ApplicationRecord
validates :name, presence: true validates :name, presence: true
validates_with StatusReactionValidator validates_with StatusReactionValidator
before_validation do
self.status = status.reblog if status&.reblog?
end
before_validation :set_custom_emoji before_validation :set_custom_emoji
private private

View File

@ -45,6 +45,7 @@ class UserSettings
setting :follow, default: true setting :follow, default: true
setting :reblog, default: false setting :reblog, default: false
setting :favourite, default: false setting :favourite, default: false
setting :reaction, default: false
setting :mention, default: true setting :mention, default: true
setting :follow_request, default: true setting :follow_request, default: true
setting :report, default: true setting :report, default: true

View File

@ -0,0 +1,45 @@
%table.email-table{ cellspacing: 0, cellpadding: 0 }
%tbody
%tr
%td.email-body
.email-container
%table.content-section{ cellspacing: 0, cellpadding: 0 }
%tbody
%tr
%td.content-cell.hero
.email-row
.col-6
%table.column{ cellspacing: 0, cellpadding: 0 }
%tbody
%tr
%td.column-cell.text-center.padded
%table.hero-icon{ align: 'center', cellspacing: 0, cellpadding: 0 }
%tbody
%tr
%td
= image_tag full_pack_url('media/images/mailer/icon_add.png'), alt: ''
%h1= t 'notification_mailer.reaction.title'
%p.lead= t('notification_mailer.reaction.body', name: @account.pretty_acct)
= render 'status', status: @status, time_zone: @me.user_time_zone
%table.email-table{ cellspacing: 0, cellpadding: 0 }
%tbody
%tr
%td.email-body
.email-container
%table.content-section{ cellspacing: 0, cellpadding: 0 }
%tbody
%tr
%td.content-cell.content-start.border-top
%table.column{ cellspacing: 0, cellpadding: 0 }
%tbody
%tr
%td.column-cell.button-cell
%table.button{ align: 'center', cellspacing: 0, cellpadding: 0 }
%tbody
%tr
%td.button-primary
= link_to web_url("@#{@status.account.pretty_acct}/#{@status.id}") do
%span= t 'application_mailer.view_status'

View File

@ -0,0 +1,5 @@
<%= raw t('application_mailer.salutation', name: display_name(@me)) %>
<%= raw t('notification_mailer.reaction.body', name: @account.pretty_acct) %>
<%= render 'status', status: @status %>

View File

@ -36,8 +36,6 @@
= ff.input :'web.use_system_font', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_system_font_ui') = ff.input :'web.use_system_font', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_system_font_ui')
= ff.input :'web.use_system_emoji_font', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_system_emoji_font'), glitch_only: true = ff.input :'web.use_system_emoji_font', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_system_emoji_font'), glitch_only: true
%h4= t 'appearance.toot_layout'
.fields-group.fields-row__column.fields-row__column-6 .fields-group.fields-row__column.fields-row__column-6
= ff.input :'visible_reactions', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_visible_reactions'), input_html: { type: 'number', min: '0', data: { default: '6' } }, hint: false = ff.input :'visible_reactions', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_visible_reactions'), input_html: { type: 'number', min: '0', data: { default: '6' } }, hint: false

View File

@ -17,6 +17,7 @@
= ff.input :'notification_emails.follow_request', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.follow_request') = ff.input :'notification_emails.follow_request', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.follow_request')
= ff.input :'notification_emails.reblog', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.reblog') = ff.input :'notification_emails.reblog', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.reblog')
= ff.input :'notification_emails.favourite', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.favourite') = ff.input :'notification_emails.favourite', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.favourite')
= ff.input :'notification_emails.reaction', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.reaction')
= ff.input :'notification_emails.mention', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.mention') = ff.input :'notification_emails.mention', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.mention')
.fields-group .fields-group

View File

@ -22,6 +22,7 @@ en:
setting_system_emoji_font: Use system's default font for emojis (applies to Glitch flavour only) setting_system_emoji_font: Use system's default font for emojis (applies to Glitch flavour only)
setting_visible_reactions: Number of visible emoji reactions setting_visible_reactions: Number of visible emoji reactions
notification_emails: notification_emails:
reaction: Someone reacted to your post
trending_link: New trending link requires review trending_link: New trending link requires review
trending_status: New trending post requires review trending_status: New trending post requires review
trending_tag: New trending tag requires review trending_tag: New trending tag requires review

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
class FixForeignKeysStatusReactions < ActiveRecord::Migration[6.1]
disable_ddl_transaction!
def change
# Fixes an oversight in a previous version of the CreateStatusReactions migration
remove_foreign_key :status_reactions, :accounts
add_foreign_key :status_reactions, :accounts, on_delete: :cascade, validate: false
validate_foreign_key :status_reactions, :accounts
remove_foreign_key :status_reactions, :statuses
add_foreign_key :status_reactions, :statuses, on_delete: :cascade, validate: false
validate_foreign_key :status_reactions, :statuses
remove_foreign_key :status_reactions, :custom_emojis
add_foreign_key :status_reactions, :custom_emojis, on_delete: :cascade, validate: false
validate_foreign_key :status_reactions, :custom_emojis
end
end

View File

@ -947,7 +947,6 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_07_150100) do
t.datetime "created_at", precision: 6, null: false t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false
t.index ["account_id", "status_id", "name"], name: "index_status_reactions_on_account_id_and_status_id", unique: true t.index ["account_id", "status_id", "name"], name: "index_status_reactions_on_account_id_and_status_id", unique: true
t.index ["account_id"], name: "index_status_reactions_on_account_id"
t.index ["custom_emoji_id"], name: "index_status_reactions_on_custom_emoji_id" t.index ["custom_emoji_id"], name: "index_status_reactions_on_custom_emoji_id"
t.index ["status_id"], name: "index_status_reactions_on_status_id" t.index ["status_id"], name: "index_status_reactions_on_status_id"
end end