Test: 新規投稿時のみ引用設定
This commit is contained in:
parent
c60b6aa65d
commit
2fddfbf0c2
7 changed files with 67 additions and 281 deletions
|
@ -1,174 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import { Helmet } from 'react-helmet';
|
|
||||||
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { Map as ImmutableMap } from 'immutable';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
|
|
||||||
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd';
|
|
||||||
|
|
||||||
import MenuIcon from '@/material-icons/400-24px/menu.svg?react';
|
|
||||||
import EmojiReactionIcon from '@/material-icons/400-24px/mood.svg?react';
|
|
||||||
import { updateReactionDeck } from 'mastodon/actions/reaction_deck';
|
|
||||||
import { Button } from 'mastodon/components/button';
|
|
||||||
import ColumnHeader from 'mastodon/components/column_header';
|
|
||||||
import { Icon } from 'mastodon/components/icon';
|
|
||||||
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';
|
|
||||||
|
|
||||||
|
|
||||||
// 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()));
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
reaction_deck_add: { id: 'reaction_deck.add', defaultMessage: 'Add' },
|
|
||||||
heading: { id: 'column.reaction_deck', defaultMessage: 'Reaction deck' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
|
||||||
accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]),
|
|
||||||
deck: state.get('reaction_deck'),
|
|
||||||
emojiMap: customEmojiMap(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
onChange: (emojis) => dispatch(updateReactionDeck(emojis)),
|
|
||||||
});
|
|
||||||
|
|
||||||
class ReactionDeck extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
params: PropTypes.object.isRequired,
|
|
||||||
deck: ImmutablePropTypes.list,
|
|
||||||
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;
|
|
||||||
|
|
||||||
if (!deck) {
|
|
||||||
return (
|
|
||||||
<Column>
|
|
||||||
<LoadingIndicator />
|
|
||||||
</Column>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Column bindToDocument={!multiColumn}>
|
|
||||||
<ColumnHeader
|
|
||||||
icon='smile-o'
|
|
||||||
iconComponent={EmojiReactionIcon}
|
|
||||||
title={intl.formatMessage(messages.heading)}
|
|
||||||
multiColumn={multiColumn}
|
|
||||||
showBackButton
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ScrollableList
|
|
||||||
scrollKey='reaction_deck'
|
|
||||||
bindToDocument={!multiColumn}
|
|
||||||
>
|
|
||||||
<DragDropContext onDragEnd={this.handleReorder}>
|
|
||||||
<StrictModeDroppable droppableId='deckitems'>
|
|
||||||
{(provided) => (
|
|
||||||
<div className='deckitems reaction_deck_container' {...provided.droppableProps} ref={provided.innerRef}>
|
|
||||||
{deck.map((emoji, index) => (
|
|
||||||
<Draggable key={index} draggableId={'' + index} index={index}>
|
|
||||||
{(provided2) => (
|
|
||||||
<div className='reaction_deck_container__row' ref={provided2.innerRef} {...provided2.draggableProps}>
|
|
||||||
<span {...provided2.dragHandleProps}>
|
|
||||||
<Icon id='bars' icon={MenuIcon} className='handle' />
|
|
||||||
</span>
|
|
||||||
<ReactionEmoji emojiMap={emojiMap}
|
|
||||||
emoji={emoji.get('name')}
|
|
||||||
index={index}
|
|
||||||
onChange={this.handleChange}
|
|
||||||
onRemove={this.handleRemove}
|
|
||||||
className='reaction_emoji'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Draggable>
|
|
||||||
))}
|
|
||||||
{provided.placeholder}
|
|
||||||
|
|
||||||
<Button text={intl.formatMessage(messages.reaction_deck_add)} onClick={this.handleAdd} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</StrictModeDroppable>
|
|
||||||
</DragDropContext>
|
|
||||||
</ScrollableList>
|
|
||||||
|
|
||||||
<Helmet>
|
|
||||||
<meta name='robots' content='noindex' />
|
|
||||||
</Helmet>
|
|
||||||
</Column>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ReactionDeck));
|
|
|
@ -1,84 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { Button } from 'mastodon/components/button';
|
|
||||||
import EmojiPickerDropdownContainer from 'mastodon/features/compose/containers/emoji_picker_dropdown_container';
|
|
||||||
import emojify from 'mastodon/features/emoji/emoji';
|
|
||||||
import { autoPlayGif } from 'mastodon/initial_state';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
remove: { id: 'reaction_deck.remove', defaultMessage: 'Remove' },
|
|
||||||
});
|
|
||||||
|
|
||||||
class ReactionEmoji extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
index: PropTypes.number,
|
|
||||||
emoji: PropTypes.string,
|
|
||||||
emojiMap: ImmutablePropTypes.map.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
onRemove: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
emoji: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
handleChange = (emoji) => {
|
|
||||||
this.props.onChange(this.props.index, emoji);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleRemove = () => {
|
|
||||||
this.props.onRemove(this.props.index);
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { intl, emojiMap, emoji } = this.props;
|
|
||||||
|
|
||||||
let content = null;
|
|
||||||
|
|
||||||
if (emojiMap.get(emoji)) {
|
|
||||||
const filename = autoPlayGif ? emojiMap.getIn([emoji, 'url']) : emojiMap.getIn([emoji, 'static_url']);
|
|
||||||
const shortCode = `:${emoji}:`;
|
|
||||||
|
|
||||||
content = (
|
|
||||||
<img
|
|
||||||
draggable='false'
|
|
||||||
className='emojione custom-emoji'
|
|
||||||
alt={shortCode}
|
|
||||||
title={shortCode}
|
|
||||||
src={filename}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const html = { __html: emojify(emoji) };
|
|
||||||
content = (
|
|
||||||
<span dangerouslySetInnerHTML={html} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='reaction_deck__emoji'>
|
|
||||||
<div className='reaction_deck__emoji__wrapper'>
|
|
||||||
<div className='reaction_deck__emoji__wrapper__content'>
|
|
||||||
<EmojiPickerDropdownContainer onPickEmoji={this.handleChange} />
|
|
||||||
<div>
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='reaction_deck__emoji__wrapper__options'>
|
|
||||||
<Button secondary text={intl.formatMessage(messages.remove)} onClick={this.handleRemove} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect()(injectIntl(ReactionEmoji));
|
|
|
@ -2,7 +2,8 @@
|
||||||
@typescript-eslint/no-explicit-any,
|
@typescript-eslint/no-explicit-any,
|
||||||
@typescript-eslint/no-unsafe-call,
|
@typescript-eslint/no-unsafe-call,
|
||||||
@typescript-eslint/no-unsafe-member-access,
|
@typescript-eslint/no-unsafe-member-access,
|
||||||
@typescript-eslint/no-unsafe-assignment */
|
@typescript-eslint/no-unsafe-assignment,
|
||||||
|
@typescript-eslint/no-confusing-void-expression */
|
||||||
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
|
@ -58,18 +59,19 @@ const ReactionEmoji: React.FC<{
|
||||||
index: number;
|
index: number;
|
||||||
emoji: string;
|
emoji: string;
|
||||||
emojiMap: any;
|
emojiMap: any;
|
||||||
onChange: (index: number, emoji: any) => void;
|
overlay?: boolean;
|
||||||
onRemove: (index: number) => void;
|
onChange?: (index: number, emoji: any) => void;
|
||||||
}> = ({ index, emoji, emojiMap, onChange, onRemove }) => {
|
onRemove?: (index: number) => void;
|
||||||
|
}> = ({ index, emoji, emojiMap, overlay, onChange, onRemove }) => {
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(emoji: any) => {
|
(emoji: any) => {
|
||||||
onChange(index, emoji);
|
if (onChange) onChange(index, emoji);
|
||||||
},
|
},
|
||||||
[index, onChange],
|
[index, onChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleRemove = useCallback(() => {
|
const handleRemove = useCallback(() => {
|
||||||
onRemove(index);
|
if (onRemove) onRemove(index);
|
||||||
}, [index, onRemove]);
|
}, [index, onRemove]);
|
||||||
|
|
||||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||||
|
@ -103,6 +105,10 @@ const ReactionEmoji: React.FC<{
|
||||||
content = <span dangerouslySetInnerHTML={html} />;
|
content = <span dangerouslySetInnerHTML={html} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (overlay) {
|
||||||
|
return <div>{content}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='reaction_deck_container__row'
|
className='reaction_deck_container__row'
|
||||||
|
@ -148,15 +154,6 @@ export const ReactionDeck: React.FC<{
|
||||||
const deckToArray = (deckData: any) =>
|
const deckToArray = (deckData: any) =>
|
||||||
deckData.map((item: any) => item.get('name')).toArray();
|
deckData.map((item: any) => item.get('name')).toArray();
|
||||||
|
|
||||||
/*
|
|
||||||
const handleReorder = useCallback((result: any) => {
|
|
||||||
const newDeck = deckToArray(deck);
|
|
||||||
const deleted = newDeck.splice(result.source.index, 1);
|
|
||||||
newDeck.splice(result.destination.index, 0, deleted[0]);
|
|
||||||
onChange(newDeck);
|
|
||||||
}, [onChange, deck]);
|
|
||||||
*/
|
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(index: number, emoji: any) => {
|
(index: number, emoji: any) => {
|
||||||
const newDeck = deckToArray(deck);
|
const newDeck = deckToArray(deck);
|
||||||
|
@ -186,6 +183,7 @@ export const ReactionDeck: React.FC<{
|
||||||
);
|
);
|
||||||
|
|
||||||
const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
|
const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
|
||||||
|
const [activeEmoji, setActiveEmoji] = useState<any>(null);
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(PointerSensor, {
|
||||||
|
@ -203,21 +201,38 @@ export const ReactionDeck: React.FC<{
|
||||||
const { active } = e;
|
const { active } = e;
|
||||||
|
|
||||||
setActiveId(active.id);
|
setActiveId(active.id);
|
||||||
|
setActiveEmoji(deck.get(idToNumber(active.id)));
|
||||||
},
|
},
|
||||||
[setActiveId],
|
[setActiveId, setActiveEmoji, deck],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const idToNumber = (id: UniqueIdentifier): number => {
|
||||||
|
if (typeof id === 'string') {
|
||||||
|
return parseInt(id);
|
||||||
|
}
|
||||||
|
if (typeof id === 'number') {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
const handleDragEnd = useCallback(
|
const handleDragEnd = useCallback(
|
||||||
(e: DragEndEvent) => {
|
(e: DragEndEvent) => {
|
||||||
const { active, over } = e;
|
const { active, over } = e;
|
||||||
|
|
||||||
if (over && active.id !== over.id) {
|
if (over && active.id !== over.id) {
|
||||||
//onChange(deck);
|
const newDeck = deckToArray(deck);
|
||||||
|
const old = newDeck[idToNumber(active.id)];
|
||||||
|
newDeck[idToNumber(active.id)] = newDeck[idToNumber(over.id)];
|
||||||
|
newDeck[idToNumber(over.id)] = old;
|
||||||
|
|
||||||
|
onChange(newDeck);
|
||||||
}
|
}
|
||||||
|
|
||||||
setActiveId(null);
|
setActiveId(null);
|
||||||
|
setActiveEmoji(null);
|
||||||
},
|
},
|
||||||
[setActiveId],
|
[setActiveId, setActiveEmoji, deck],
|
||||||
);
|
);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
@ -259,7 +274,16 @@ export const ReactionDeck: React.FC<{
|
||||||
))}
|
))}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
|
|
||||||
<DragOverlay>{activeId ? <span>Test</span> : null}</DragOverlay>
|
<DragOverlay>
|
||||||
|
{activeId ? (
|
||||||
|
<ReactionEmoji
|
||||||
|
emojiMap={emojiMap}
|
||||||
|
emoji={activeEmoji.get('name')}
|
||||||
|
index={-1}
|
||||||
|
overlay
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</DragOverlay>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -918,7 +918,6 @@
|
||||||
"privacy_policy.last_updated": "Last updated {date}",
|
"privacy_policy.last_updated": "Last updated {date}",
|
||||||
"privacy_policy.title": "Privacy Policy",
|
"privacy_policy.title": "Privacy Policy",
|
||||||
"reaction_deck.add": "Add",
|
"reaction_deck.add": "Add",
|
||||||
"reaction_deck.remove": "Remove",
|
|
||||||
"recommended": "Recommended",
|
"recommended": "Recommended",
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
"regeneration_indicator.please_stand_by": "Please stand by.",
|
"regeneration_indicator.please_stand_by": "Please stand by.",
|
||||||
|
|
|
@ -42,7 +42,7 @@ class PostStatusService < BaseService
|
||||||
@options = options
|
@options = options
|
||||||
@text = @options[:text] || ''
|
@text = @options[:text] || ''
|
||||||
@in_reply_to = @options[:thread]
|
@in_reply_to = @options[:thread]
|
||||||
@quoted_status = @options[:quoted_status]
|
@quoted_status = @options[:quoted_status] || quoted_status_from_text
|
||||||
|
|
||||||
@antispam = Antispam.new
|
@antispam = Antispam.new
|
||||||
|
|
||||||
|
@ -296,7 +296,13 @@ class PostStatusService < BaseService
|
||||||
|
|
||||||
def quote_url
|
def quote_url
|
||||||
ProcessReferencesService.extract_quote(@text)
|
ProcessReferencesService.extract_quote(@text)
|
||||||
# TODO: quote
|
end
|
||||||
|
|
||||||
|
def quoted_status_from_text
|
||||||
|
url = quote_url
|
||||||
|
return unless url
|
||||||
|
|
||||||
|
ActivityPub::TagManager.instance.uri_to_resource(url, Status, url: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
def reference_urls
|
def reference_urls
|
||||||
|
|
|
@ -138,8 +138,11 @@ class UpdateStatusService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def quote_url
|
def quote_url
|
||||||
ProcessReferencesService.extract_quote(text)
|
# ProcessReferencesService.extract_quote(text)
|
||||||
# TODO: quote
|
# TODO: quote
|
||||||
|
return unless @status.quote&.quoted_status
|
||||||
|
|
||||||
|
ActivityPub::TagManager.instance.uri_for(@status.quote.quoted_status)
|
||||||
end
|
end
|
||||||
|
|
||||||
def reference_urls
|
def reference_urls
|
||||||
|
|
|
@ -548,6 +548,18 @@ RSpec.describe PostStatusService do
|
||||||
expect(hashtags_service).to have_received(:call).with(status)
|
expect(hashtags_service).to have_received(:call).with(status)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'creates a quote by kmyblue format' do
|
||||||
|
target_status = Fabricate(:status)
|
||||||
|
|
||||||
|
account = Fabricate(:account)
|
||||||
|
|
||||||
|
status = subject.call(account, text: "test status QT #{ActivityPub::TagManager.instance.uri_for(target_status)}")
|
||||||
|
|
||||||
|
expect(status).to be_persisted
|
||||||
|
expect(status.quote).to be_persisted
|
||||||
|
expect(status.quote.quoted_status_id).to eq target_status.id
|
||||||
|
end
|
||||||
|
|
||||||
it 'gets distributed' do
|
it 'gets distributed' do
|
||||||
allow(DistributionWorker).to receive(:perform_async)
|
allow(DistributionWorker).to receive(:perform_async)
|
||||||
allow(ActivityPub::DistributionWorker).to receive(:perform_async)
|
allow(ActivityPub::DistributionWorker).to receive(:perform_async)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue