Change: リアクションデッキのTS化 (WIP)

This commit is contained in:
KMY 2025-05-27 12:11:17 +09:00
parent 7c65b6f9df
commit 0c27b62a25
6 changed files with 138 additions and 44 deletions

View file

@ -589,7 +589,7 @@ class Status extends ImmutablePureComponent {
} }
data-id={status.get('id')} data-id={status.get('id')}
> >
{(connectReply || connectUp || connectToRoot) && <div className={classNames('status__line', { 'status__line--full': connectReply, 'status__line--first': !status.get('in_reply_to_id') && !connectToRoot })} />} {(connectUp || connectToRoot) && <div className={classNames('status__line', { 'status__line--first': !status.get('in_reply_to_id') && !connectToRoot })} />}
<div onClick={this.handleHeaderClick} onAuxClick={this.handleHeaderClick} className='status__info'> <div onClick={this.handleHeaderClick} onAuxClick={this.handleHeaderClick} className='status__info'>
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} className='status__relative-time'> <Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} className='status__relative-time'>
@ -600,15 +600,14 @@ class Status extends ImmutablePureComponent {
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>} <RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
</Link> </Link>
<Link to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} data-hover-card-account={status.getIn(['account', 'id'])} className='status__display-name'> <Link to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} data-hover-card-account={status.getIn(['account', 'id'])} className='status__display-name'>
<div className='status__avatar'> <div className='status__avatar'>
{statusAvatar} {statusAvatar}
</div> </div>
<DisplayName account={status.get('account')} /> <DisplayName account={status.get('account')} />
</Link> </Link>
</div> </div>
)}
{matchedFilters && <FilterWarning title={matchedFilters.join(', ')} expanded={this.state.showDespiteFilter} onClick={this.handleFilterToggle} />} {matchedFilters && <FilterWarning title={matchedFilters.join(', ')} expanded={this.state.showDespiteFilter} onClick={this.handleFilterToggle} />}

View file

@ -71,6 +71,10 @@ export const ActionBar: React.FC = () => {
text: intl.formatMessage(messages.emoji_reactions), text: intl.formatMessage(messages.emoji_reactions),
to: '/emoji_reactions', to: '/emoji_reactions',
}, },
{
text: intl.formatMessage(messages.reaction_deck),
to: '/reaction_deck',
},
{ text: intl.formatMessage(messages.lists), to: '/lists' }, { text: intl.formatMessage(messages.lists), to: '/lists' },
{ {
text: intl.formatMessage(messages.followed_tags), text: intl.formatMessage(messages.followed_tags),

View file

@ -10,10 +10,10 @@ import { navigateToStatus } from 'mastodon/actions/statuses';
import { Avatar } from 'mastodon/components/avatar'; import { Avatar } from 'mastodon/components/avatar';
import { AvatarGroup } from 'mastodon/components/avatar_group'; import { AvatarGroup } from 'mastodon/components/avatar_group';
import EmojiView from 'mastodon/components/emoji_view'; import EmojiView from 'mastodon/components/emoji_view';
import type { EmojiReactionGroup } from 'mastodon/models/notification_group';
import type { IconProp } from 'mastodon/components/icon'; import type { IconProp } from 'mastodon/components/icon';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp'; import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import type { EmojiReactionGroup } from 'mastodon/models/notification_group';
import { NOTIFICATIONS_GROUP_MAX_AVATARS } from 'mastodon/models/notification_group'; import { NOTIFICATIONS_GROUP_MAX_AVATARS } from 'mastodon/models/notification_group';
import { useAppSelector, useAppDispatch } from 'mastodon/store'; import { useAppSelector, useAppDispatch } from 'mastodon/store';

View file

@ -5,12 +5,36 @@
@typescript-eslint/no-unsafe-assignment */ @typescript-eslint/no-unsafe-assignment */
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { useCallback } from 'react'; import { useState, useCallback } from 'react';
import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
import { Helmet } from 'react-helmet'; 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 MenuIcon from '@/material-icons/400-24px/menu.svg?react';
import EmojiReactionIcon from '@/material-icons/400-24px/mood.svg?react'; import EmojiReactionIcon from '@/material-icons/400-24px/mood.svg?react';
import { updateReactionDeck } from 'mastodon/actions/reaction_deck'; import { updateReactionDeck } from 'mastodon/actions/reaction_deck';
@ -37,14 +61,25 @@ const ReactionEmoji: React.FC<{
onChange: (index: number, emoji: any) => void; onChange: (index: number, emoji: any) => void;
onRemove: (index: number) => void; onRemove: (index: number) => void;
}> = ({ index, emoji, emojiMap, onChange, onRemove }) => { }> = ({ index, emoji, emojiMap, onChange, onRemove }) => {
const handleChange = useCallback((emoji: any) => { const handleChange = useCallback(
onChange(index, emoji); (emoji: any) => {
}, [index, onChange]); onChange(index, emoji);
},
[index, onChange],
);
const handleRemove = useCallback(() => { const handleRemove = useCallback(() => {
onRemove(index); onRemove(index);
}, [index, onRemove]); }, [index, onRemove]);
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: index.toString() });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
let content: ReactNode; let content: ReactNode;
const mapEmoji = emojiMap.find((e: any) => e.get('shortcode') === emoji); const mapEmoji = emojiMap.find((e: any) => e.get('shortcode') === emoji);
@ -69,16 +104,25 @@ const ReactionEmoji: React.FC<{
} }
return ( return (
<div className='reaction_deck__emoji'> <div
<div className='reaction_deck__emoji__wrapper'> className='reaction_deck_container__row'
<div className='reaction_deck__emoji__wrapper__content'> ref={setNodeRef}
<EmojiPickerDropdown onPickEmoji={handleChange} /> style={style}
<div> {...attributes}
{content} {...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 className='reaction_deck__emoji__wrapper__options'>
<Button secondary text={'Remove'} onClick={handleRemove} />
</div> </div>
</div> </div>
</div> </div>
@ -119,7 +163,7 @@ export const ReactionDeck: React.FC<{
newDeck[index] = emoji.native || emoji.id.replace(':', ''); newDeck[index] = emoji.native || emoji.id.replace(':', '');
onChange(newDeck); onChange(newDeck);
}, },
[onChange, deck] [onChange, deck],
); );
const handleRemove = useCallback( const handleRemove = useCallback(
@ -134,12 +178,48 @@ export const ReactionDeck: React.FC<{
const handleAdd = useCallback( const handleAdd = useCallback(
(emoji: any) => { (emoji: any) => {
const newDeck = deckToArray(deck); const newDeck = deckToArray(deck);
newDeck.push('👍'); const newEmoji = emoji.native || emoji.id.replace(':', '');
newDeck.push(newEmoji);
onChange(newDeck); onChange(newDeck);
}, },
[onChange, deck], [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 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!deck) { if (!deck) {
return ( return (
@ -159,16 +239,28 @@ export const ReactionDeck: React.FC<{
showBackButton showBackButton
/> />
{deck.map((emoji: any, index) => ( <DndContext
<ReactionEmoji sensors={sensors}
emojiMap={emojiMap} collisionDetection={closestCenter}
key={index} onDragStart={handleDragStart}
emoji={emoji.get('name')} onDragEnd={handleDragEnd}
index={index} >
onChange={handleChange} <SortableContext items={deck.toArray()} strategy={rectSortingStrategy}>
onRemove={handleRemove} {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> <div>
<EmojiPickerDropdown <EmojiPickerDropdown

View file

@ -1,10 +1,10 @@
const config = { const config = {
// '*': 'prettier --ignore-unknown --write', '*': 'prettier --ignore-unknown --write',
'Gemfile|*.{rb,ruby,ru,rake}': 'bin/rubocop --force-exclusion -a', 'Gemfile|*.{rb,ruby,ru,rake}': 'bin/rubocop --force-exclusion -a',
// '*.{js,jsx,ts,tsx}': 'eslint --fix', '*.{js,jsx,ts,tsx}': 'eslint --fix',
// '*.{css,scss}': 'stylelint --fix', '*.{css,scss}': 'stylelint --fix',
// '*.haml': 'bin/haml-lint -a', '*.haml': 'bin/haml-lint -a',
// '**/*.ts?(x)': () => 'tsc -p tsconfig.json --noEmit', '**/*.ts?(x)': () => 'tsc -p tsconfig.json --noEmit',
}; };
module.exports = config; module.exports = config;

View file

@ -5,13 +5,12 @@ import { optimizeLodashImports } from '@optimize-lodash/rollup-plugin';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import { PluginOption } from 'vite'; import { PluginOption } from 'vite';
import svgr from 'vite-plugin-svgr'; import svgr from 'vite-plugin-svgr';
import { visualizer } from 'rollup-plugin-visualizer'; // import { visualizer } from 'rollup-plugin-visualizer';
import RailsPlugin from 'vite-plugin-rails'; import RailsPlugin from 'vite-plugin-rails';
import { VitePWA } from 'vite-plugin-pwa'; import { VitePWA } from 'vite-plugin-pwa';
import tsconfigPaths from 'vite-tsconfig-paths'; import tsconfigPaths from 'vite-tsconfig-paths';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import legacy from '@vitejs/plugin-legacy'; import legacy from '@vitejs/plugin-legacy';
import vitePluginRequire from "vite-plugin-require";
import { defineConfig, UserConfigFnPromise, UserConfig } from 'vite'; import { defineConfig, UserConfigFnPromise, UserConfig } from 'vite';
import postcssPresetEnv from 'postcss-preset-env'; import postcssPresetEnv from 'postcss-preset-env';
@ -107,7 +106,6 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => {
}, },
}, },
plugins: [ plugins: [
vitePluginRequire(),
tsconfigPaths(), tsconfigPaths(),
RailsPlugin({ RailsPlugin({
compress: mode === 'production' && command === 'build', compress: mode === 'production' && command === 'build',
@ -153,7 +151,8 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => {
svgr(), svgr(),
// Old library types need to be converted // Old library types need to be converted
optimizeLodashImports() as PluginOption, optimizeLodashImports() as PluginOption,
!!process.env.ANALYZE_BUNDLE_SIZE && (visualizer() as PluginOption), // Disable in knyblue because kmyblue-developer cannot launch foreman
// !!process.env.ANALYZE_BUNDLE_SIZE && (visualizer() as PluginOption),
], ],
} satisfies UserConfig; } satisfies UserConfig;
}; };