284 lines
7.4 KiB
TypeScript
284 lines
7.4 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-unsafe-return,
|
|
@typescript-eslint/no-explicit-any,
|
|
@typescript-eslint/no-unsafe-call,
|
|
@typescript-eslint/no-unsafe-member-access,
|
|
@typescript-eslint/no-unsafe-assignment */
|
|
|
|
import type { ReactNode } from 'react';
|
|
import { useState, useCallback } from 'react';
|
|
|
|
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
|
|
|
import { Helmet } from 'react-helmet';
|
|
|
|
import type {
|
|
DragStartEvent,
|
|
DragEndEvent,
|
|
UniqueIdentifier,
|
|
// Announcements,
|
|
// ScreenReaderInstructions,
|
|
} from '@dnd-kit/core';
|
|
import {
|
|
DndContext,
|
|
closestCenter,
|
|
KeyboardSensor,
|
|
PointerSensor,
|
|
useSensor,
|
|
useSensors,
|
|
DragOverlay,
|
|
} from '@dnd-kit/core';
|
|
import {
|
|
SortableContext,
|
|
sortableKeyboardCoordinates,
|
|
rectSortingStrategy,
|
|
useSortable,
|
|
} from '@dnd-kit/sortable';
|
|
import { CSS } from '@dnd-kit/utilities';
|
|
|
|
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 { Column } from 'mastodon/components/column';
|
|
import { ColumnHeader } from 'mastodon/components/column_header';
|
|
import { Icon } from 'mastodon/components/icon';
|
|
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
|
import EmojiPickerDropdown from 'mastodon/features/compose/containers/emoji_picker_dropdown_container';
|
|
import { autoPlayGif } from 'mastodon/initial_state';
|
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
|
|
|
import emojify from '../emoji/emoji';
|
|
|
|
const messages = defineMessages({
|
|
reaction_deck_add: { id: 'reaction_deck.add', defaultMessage: 'Add' },
|
|
heading: { id: 'column.reaction_deck', defaultMessage: 'Reaction deck' },
|
|
});
|
|
|
|
const ReactionEmoji: React.FC<{
|
|
index: number;
|
|
emoji: string;
|
|
emojiMap: any;
|
|
onChange: (index: number, emoji: any) => void;
|
|
onRemove: (index: number) => void;
|
|
}> = ({ index, emoji, emojiMap, onChange, onRemove }) => {
|
|
const handleChange = useCallback(
|
|
(emoji: any) => {
|
|
onChange(index, emoji);
|
|
},
|
|
[index, onChange],
|
|
);
|
|
|
|
const handleRemove = useCallback(() => {
|
|
onRemove(index);
|
|
}, [index, onRemove]);
|
|
|
|
const { attributes, listeners, setNodeRef, transform, transition } =
|
|
useSortable({ id: index.toString() });
|
|
|
|
const style = {
|
|
transform: CSS.Transform.toString(transform),
|
|
transition,
|
|
};
|
|
|
|
let content: ReactNode;
|
|
const mapEmoji = emojiMap.find((e: any) => e.get('shortcode') === emoji);
|
|
|
|
if (mapEmoji) {
|
|
const filename = autoPlayGif
|
|
? mapEmoji.get('url')
|
|
: mapEmoji.get('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_container__row'
|
|
ref={setNodeRef}
|
|
style={style}
|
|
{...attributes}
|
|
{...listeners}
|
|
>
|
|
<span>
|
|
<Icon id='bars' icon={MenuIcon} className='handle' />
|
|
</span>
|
|
<div className='reaction_deck__emoji'>
|
|
<div className='reaction_deck__emoji__wrapper'>
|
|
<div className='reaction_deck__emoji__wrapper__content'>
|
|
<EmojiPickerDropdown onPickEmoji={handleChange} />
|
|
<div>{content}</div>
|
|
</div>
|
|
<div className='reaction_deck__emoji__wrapper__options'>
|
|
<Button secondary text={'Remove'} onClick={handleRemove} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const ReactionDeck: React.FC<{
|
|
multiColumn?: boolean;
|
|
}> = ({ multiColumn }) => {
|
|
const dispatch = useAppDispatch();
|
|
const intl = useIntl();
|
|
|
|
const emojiMap = useAppSelector((state) => state.custom_emojis);
|
|
const deck = useAppSelector((state) => state.reaction_deck);
|
|
|
|
const onChange = useCallback(
|
|
(emojis: any) => {
|
|
dispatch(updateReactionDeck(emojis));
|
|
},
|
|
[dispatch],
|
|
);
|
|
|
|
const deckToArray = (deckData: any) =>
|
|
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(
|
|
(index: number, emoji: any) => {
|
|
const newDeck = deckToArray(deck);
|
|
newDeck[index] = emoji.native || emoji.id.replace(':', '');
|
|
onChange(newDeck);
|
|
},
|
|
[onChange, deck],
|
|
);
|
|
|
|
const handleRemove = useCallback(
|
|
(index: number) => {
|
|
const newDeck = deckToArray(deck);
|
|
newDeck.splice(index, 1);
|
|
onChange(newDeck);
|
|
},
|
|
[onChange, deck],
|
|
);
|
|
|
|
const handleAdd = useCallback(
|
|
(emoji: any) => {
|
|
const newDeck = deckToArray(deck);
|
|
const newEmoji = emoji.native || emoji.id.replace(':', '');
|
|
newDeck.push(newEmoji);
|
|
onChange(newDeck);
|
|
},
|
|
[onChange, deck],
|
|
);
|
|
|
|
const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
|
|
|
|
const sensors = useSensors(
|
|
useSensor(PointerSensor, {
|
|
activationConstraint: {
|
|
distance: 5,
|
|
},
|
|
}),
|
|
useSensor(KeyboardSensor, {
|
|
coordinateGetter: sortableKeyboardCoordinates,
|
|
}),
|
|
);
|
|
|
|
const handleDragStart = useCallback(
|
|
(e: DragStartEvent) => {
|
|
const { active } = e;
|
|
|
|
setActiveId(active.id);
|
|
},
|
|
[setActiveId],
|
|
);
|
|
|
|
const handleDragEnd = useCallback(
|
|
(e: DragEndEvent) => {
|
|
const { active, over } = e;
|
|
|
|
if (over && active.id !== over.id) {
|
|
//onChange(deck);
|
|
}
|
|
|
|
setActiveId(null);
|
|
},
|
|
[dispatch, setActiveId],
|
|
);
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
if (!deck) {
|
|
return (
|
|
<Column>
|
|
<LoadingIndicator />
|
|
</Column>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Column bindToDocument={!multiColumn}>
|
|
<ColumnHeader
|
|
icon='smile-o'
|
|
iconComponent={EmojiReactionIcon}
|
|
title={intl.formatMessage(messages.heading)}
|
|
multiColumn={multiColumn}
|
|
showBackButton
|
|
/>
|
|
|
|
<DndContext
|
|
sensors={sensors}
|
|
collisionDetection={closestCenter}
|
|
onDragStart={handleDragStart}
|
|
onDragEnd={handleDragEnd}
|
|
>
|
|
<SortableContext items={deck.toArray()} strategy={rectSortingStrategy}>
|
|
{deck.map((emoji: any, index) => (
|
|
<div key={index} id={index.toString()}>
|
|
<ReactionEmoji
|
|
emojiMap={emojiMap}
|
|
emoji={emoji.get('name')}
|
|
index={index}
|
|
onChange={handleChange}
|
|
onRemove={handleRemove}
|
|
/>
|
|
</div>
|
|
))}
|
|
</SortableContext>
|
|
|
|
<DragOverlay>{activeId ? <span>Test</span> : null}</DragOverlay>
|
|
</DndContext>
|
|
|
|
<div>
|
|
<EmojiPickerDropdown
|
|
onPickEmoji={handleAdd}
|
|
button={
|
|
<Button secondary>
|
|
<FormattedMessage id='reaction_deck.add' defaultMessage='Add' />
|
|
</Button>
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
<Helmet>
|
|
<meta name='robots' content='noindex' />
|
|
</Helmet>
|
|
</Column>
|
|
);
|
|
};
|
|
|
|
// eslint-disable-next-line import/no-default-export
|
|
export default ReactionDeck;
|