1
0
Fork 0
forked from gitea/nas

Change reactiondeck to draggable, add-removable

This commit is contained in:
KMY 2023-05-26 20:42:36 +09:00
parent c4efa4d986
commit b9dab37eac
8 changed files with 200 additions and 29 deletions

View file

@ -18,6 +18,35 @@ class Api::V1::ReactionDeckController < Api::BaseController
end
def create
deck = []
(deck_params['emojis'] || []).each do |shortcode|
shortcode = shortcode.delete(':')
custom_emoji = CustomEmoji.find_by(shortcode: shortcode, domain: nil)
emoji_data = {}
if custom_emoji
emoji_data['name'] = custom_emoji.shortcode
emoji_data['url'] = full_asset_url(custom_emoji.image.url)
emoji_data['static_url'] = full_asset_url(custom_emoji.image.url(:static))
emoji_data['width'] = custom_emoji.image_width
emoji_data['height'] = custom_emoji.image_height
emoji_data['custom_emoji_id'] = custom_emoji.id
else
emoji_data['name'] = shortcode
end
deck << emoji_data
end
current_user.settings['reaction_deck'] = deck.to_json
current_user.save!
render json: remove_metas(deck)
end
def legacy_create
deck = @deck
(deck_params['emojis'] || []).each do |data|
@ -86,6 +115,7 @@ class Api::V1::ReactionDeckController < Api::BaseController
deck.tap do |d|
d.each do |item|
item.delete('custom_emoji_id')
# item.delete('id') if item.key?('id')
end
end
end

View file

@ -47,11 +47,11 @@ export function fetchReactionDeckFail(error) {
};
}
export function updateReactionDeck(id, emoji) {
export function updateReactionDeck(emojis) {
return (dispatch, getState) => {
dispatch(updateReactionDeckRequest());
api(getState).post('/api/v1/reaction_deck', { emojis: [{ id, emoji: emoji.native || emoji.id }] }).then(response => {
api(getState).post('/api/v1/reaction_deck', { emojis }).then(response => {
dispatch(updateReactionDeckSuccess(response.data));
}).catch(error => {
dispatch(updateReactionDeckFail(error));

View file

@ -2,12 +2,10 @@ import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { updateReactionDeck, removeReactionDeck } from 'mastodon/actions/reaction_deck';
import Button from 'mastodon/components/button';
import EmojiPickerDropdownContainer from 'mastodon/features/compose/containers/emoji_picker_dropdown_container';
import emojify from 'mastodon/features/emoji/emoji';
@ -17,19 +15,10 @@ const messages = defineMessages({
remove: { id: 'reaction_deck.remove', defaultMessage: 'Remove' },
});
const MapStateToProps = (state, { emojiId, emojiMap }) => ({
emoji: (state.get('reaction_deck', ImmutableList()).toArray().find(em => em.get('id') === emojiId) || ImmutableMap({ emoji: { shortcode: '' } })).get('name'),
emojiMap,
});
const mapDispatchToProps = (dispatch, { emojiId }) => ({
onChange: (emoji) => dispatch(updateReactionDeck(emojiId, emoji)),
onRemove: () => dispatch(removeReactionDeck(emojiId)),
});
class ReactionEmoji extends ImmutablePureComponent {
static propTypes = {
index: PropTypes.number,
emoji: PropTypes.string,
emojiMap: ImmutablePropTypes.map.isRequired,
onChange: PropTypes.func.isRequired,
@ -40,8 +29,16 @@ class ReactionEmoji extends ImmutablePureComponent {
emoji: '',
};
handleChange = (emoji) => {
this.props.onChange(this.props.index, emoji);
};
handleRemove = () => {
this.props.onRemove(this.props.index);
};
render () {
const { intl, emojiMap, emoji, onChange, onRemove } = this.props;
const { intl, emojiMap, emoji } = this.props;
let content = null;
@ -69,13 +66,13 @@ class ReactionEmoji extends ImmutablePureComponent {
<div className='reaction_deck__emoji'>
<div className='reaction_deck__emoji__wrapper'>
<div className='reaction_deck__emoji__wrapper__content'>
<EmojiPickerDropdownContainer onPickEmoji={onChange} />
<EmojiPickerDropdownContainer onPickEmoji={this.handleChange} />
<div>
{content}
</div>
</div>
<div className='reaction_deck__emoji__wrapper__options'>
<Button secondary text={intl.formatMessage(messages.remove)} onClick={onRemove} />
<Button secondary text={intl.formatMessage(messages.remove)} onClick={this.handleRemove} />
</div>
</div>
</div>
@ -84,4 +81,4 @@ class ReactionEmoji extends ImmutablePureComponent {
}
export default connect(MapStateToProps, mapDispatchToProps)(injectIntl(ReactionEmoji));
export default connect(injectIntl(ReactionEmoji));

View file

@ -1,4 +1,5 @@
import PropTypes from 'prop-types';
import { useEffect, useState } from "react";
import { defineMessages, injectIntl } from 'react-intl';
@ -10,15 +11,36 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
import { updateReactionDeck } from 'mastodon/actions/reaction_deck';
import Button from 'mastodon/components/button';
import ColumnHeader from 'mastodon/components/column_header';
import LoadingIndicator from 'mastodon/components/loading_indicator';
import ScrollableList from 'mastodon/components/scrollable_list';
import Column from 'mastodon/features/ui/components/column';
import ReactionEmoji from './components/reaction_emoji';
const DECK_SIZE = 16;
// https://medium.com/@wbern/getting-react-18s-strict-mode-to-work-with-react-beautiful-dnd-47bc909348e4
/* eslint react/prop-types: 0 */
const StrictModeDroppable = ({ children, ...props }) => {
const [enabled, setEnabled] = useState(false);
useEffect(() => {
const animation = requestAnimationFrame(() => setEnabled(true));
return () => {
cancelAnimationFrame(animation);
setEnabled(false);
};
}, []);
if (!enabled) {
return null;
}
return <Droppable {...props}>{children}</Droppable>;
};
/* eslint react/prop-types: 0 */
const customEmojiMap = createSelector([state => state.get('custom_emojis')], items => items.reduce((map, emoji) => map.set(emoji.get('shortcode'), emoji), ImmutableMap()));
@ -33,6 +55,10 @@ const mapStateToProps = (state, props) => ({
emojiMap: customEmojiMap(state),
});
const mapDispatchToProps = (dispatch) => ({
onChange: (emojis) => dispatch(updateReactionDeck(emojis)),
});
class ReactionDeck extends ImmutablePureComponent {
static propTypes = {
@ -42,8 +68,40 @@ class ReactionDeck extends ImmutablePureComponent {
emojiMap: ImmutablePropTypes.map,
multiColumn: PropTypes.bool,
intl: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
};
deckToArray = () => {
const { deck } = this.props;
return deck.map((item) => item.get('name')).toArray();
};
handleReorder = (result) => {
const newDeck = this.deckToArray();
const deleted = newDeck.splice(result.source.index, 1);
newDeck.splice(result.destination.index, 0, deleted[0]);
this.props.onChange(newDeck);
};
handleChange = (index, emoji) => {
const newDeck = this.deckToArray();
newDeck[index] = emoji.native || emoji.id.replace(':', '');
this.props.onChange(newDeck);
};
handleRemove = (index) => {
const newDeck = this.deckToArray();
newDeck.splice(index, 1);
this.props.onChange(newDeck);
};
handleAdd = () => {
const newDeck = this.deckToArray();
newDeck.push('👍');
this.props.onChange(newDeck);
}
render () {
const { intl, deck, emojiMap, multiColumn } = this.props;
@ -69,9 +127,32 @@ class ReactionDeck extends ImmutablePureComponent {
scrollKey='reaction_deck'
bindToDocument={!multiColumn}
>
{[...Array(DECK_SIZE).keys()].map(emojiId =>
<ReactionEmoji key={emojiId} emojiMap={emojiMap} emojiId={emojiId} />
)}
<DragDropContext onDragEnd={this.handleReorder}>
<StrictModeDroppable droppableId='deckitems'>
{(provided) => (
<div className='deckitems' {...provided.droppableProps} ref={provided.innerRef}>
{deck.map((emoji, index) => (
<Draggable key={index} draggableId={'' + index} index={index}>
{(provided2) => (
<div ref={provided2.innerRef} {...provided2.draggableProps} {...provided2.dragHandleProps}>
<ReactionEmoji emojiMap={emojiMap}
emojiId={emoji.get('id')}
emoji={emoji.get('name')}
index={index}
onChange={this.handleChange}
onRemove={this.handleRemove}
/>
</div>
)}
</Draggable>
))}
{provided.placeholder}
<Button text='Add' onClick={this.handleAdd} />
</div>
)}
</StrictModeDroppable>
</DragDropContext>
</ScrollableList>
<Helmet>
@ -83,4 +164,4 @@ class ReactionDeck extends ImmutablePureComponent {
}
export default connect(mapStateToProps)(injectIntl(ReactionDeck));
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ReactionDeck));

View file

@ -86,7 +86,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
},
reaction_deck: {
max_emojis: 16,
max_emojis: 32_767,
},
reactions: {

View file

@ -92,7 +92,7 @@ class REST::V1::InstanceSerializer < ActiveModel::Serializer
},
reaction_deck: {
max_emojis: 16,
max_emojis: 32_767,
},
reactions: {

View file

@ -88,6 +88,7 @@
"prop-types": "^15.8.1",
"punycode": "^2.3.0",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-dom": "^18.2.0",
"react-helmet": "^6.1.0",
"react-hotkeys": "^1.1.4",

View file

@ -1062,7 +1062,7 @@
dependencies:
regenerator-runtime "^0.12.0"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.13.8", "@babel/runtime@^7.2.0", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.13.8", "@babel/runtime@^7.15.4", "@babel/runtime@^7.2.0", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
version "7.21.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.5.tgz#8492dddda9644ae3bda3b45eabe87382caee7200"
integrity sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==
@ -2006,7 +2006,7 @@
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64"
integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==
"@types/hoist-non-react-statics@^3.3.1":
"@types/hoist-non-react-statics@^3.3.0", "@types/hoist-non-react-statics@^3.3.1":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
@ -2222,6 +2222,16 @@
dependencies:
react-overlays "*"
"@types/react-redux@^7.1.20":
version "7.1.25"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.25.tgz#de841631205b24f9dfb4967dd4a7901e048f9a88"
integrity sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==
dependencies:
"@types/hoist-non-react-statics" "^3.3.0"
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
redux "^4.0.0"
"@types/react-router-dom@^5.3.3":
version "5.3.3"
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83"
@ -4222,6 +4232,13 @@ crypto-random-string@^2.0.0:
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
css-box-model@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1"
integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==
dependencies:
tiny-invariant "^1.0.6"
css-declaration-sorter@^6.3.1:
version "6.3.1"
resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz#be5e1d71b7a992433fb1c542c7a1b835e45682ec"
@ -7940,6 +7957,11 @@ media-typer@0.3.0:
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
memoize-one@^5.1.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
memoize-one@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045"
@ -9445,6 +9467,11 @@ quick-lru@^4.0.1:
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
raf-schd@^4.0.2:
version "4.0.3"
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a"
integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==
raf@^3.1.0:
version "3.4.1"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
@ -9482,6 +9509,19 @@ raw-body@2.5.1:
iconv-lite "0.4.24"
unpipe "1.0.0"
react-beautiful-dnd@^13.1.1:
version "13.1.1"
resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz#b0f3087a5840920abf8bb2325f1ffa46d8c4d0a2"
integrity sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==
dependencies:
"@babel/runtime" "^7.9.2"
css-box-model "^1.2.0"
memoize-one "^5.1.1"
raf-schd "^4.0.2"
react-redux "^7.2.0"
redux "^4.0.4"
use-memo-one "^1.1.1"
react-dom@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
@ -9568,7 +9608,7 @@ react-is@^16.13.1, react-is@^16.7.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-is@^17.0.1:
react-is@^17.0.1, react-is@^17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
@ -9621,6 +9661,18 @@ react-redux-loading-bar@^5.0.4:
prop-types "^15.7.2"
react-lifecycles-compat "^3.0.4"
react-redux@^7.2.0:
version "7.2.9"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.9.tgz#09488fbb9416a4efe3735b7235055442b042481d"
integrity sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==
dependencies:
"@babel/runtime" "^7.15.4"
"@types/react-redux" "^7.1.20"
hoist-non-react-statics "^3.3.2"
loose-envify "^1.4.0"
prop-types "^15.7.2"
react-is "^17.0.2"
react-redux@^8.0.4:
version "8.0.5"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.0.5.tgz#e5fb8331993a019b8aaf2e167a93d10af469c7bd"
@ -9871,7 +9923,7 @@ redux-thunk@^2.4.2:
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.2.tgz#b9d05d11994b99f7a91ea223e8b04cf0afa5ef3b"
integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==
redux@^4.0.0, redux@^4.2.1:
redux@^4.0.0, redux@^4.0.4, redux@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197"
integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==
@ -11281,6 +11333,11 @@ tiny-invariant@^1.0.2:
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
tiny-invariant@^1.0.6:
version "1.3.1"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642"
integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==
tiny-queue@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/tiny-queue/-/tiny-queue-0.2.1.tgz#25a67f2c6e253b2ca941977b5ef7442ef97a6046"
@ -11685,6 +11742,11 @@ use-latest@^1.2.1:
dependencies:
use-isomorphic-layout-effect "^1.1.1"
use-memo-one@^1.1.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99"
integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==
use-sync-external-store@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"