kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Use an emoji picker modal on mobile
rodzic
5380922d4e
commit
ad24779343
|
@ -3,13 +3,16 @@ import dotsIcon from '@tabler/icons/outline/dots.svg';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { closeModal, openModal } from 'soapbox/actions/modals.ts';
|
||||||
import EmojiComponent from 'soapbox/components/ui/emoji.tsx';
|
import EmojiComponent from 'soapbox/components/ui/emoji.tsx';
|
||||||
import HStack from 'soapbox/components/ui/hstack.tsx';
|
import HStack from 'soapbox/components/ui/hstack.tsx';
|
||||||
import IconButton from 'soapbox/components/ui/icon-button.tsx';
|
import IconButton from 'soapbox/components/ui/icon-button.tsx';
|
||||||
import EmojiPickerDropdown from 'soapbox/features/emoji/components/emoji-picker-dropdown.tsx';
|
import EmojiPickerDropdown from 'soapbox/features/emoji/components/emoji-picker-dropdown.tsx';
|
||||||
|
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||||
import { useClickOutside } from 'soapbox/hooks/useClickOutside.ts';
|
import { useClickOutside } from 'soapbox/hooks/useClickOutside.ts';
|
||||||
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
||||||
import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts';
|
import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts';
|
||||||
|
import { userTouching } from 'soapbox/is-mobile.ts';
|
||||||
|
|
||||||
import type { Emoji } from 'soapbox/features/emoji/index.ts';
|
import type { Emoji } from 'soapbox/features/emoji/index.ts';
|
||||||
|
|
||||||
|
@ -67,6 +70,7 @@ const EmojiSelector: React.FC<IEmojiSelector> = ({
|
||||||
const soapboxConfig = useSoapboxConfig();
|
const soapboxConfig = useSoapboxConfig();
|
||||||
const { customEmojiReacts } = useFeatures();
|
const { customEmojiReacts } = useFeatures();
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
const { x, y, strategy, refs, update } = useFloating<HTMLElement>({
|
const { x, y, strategy, refs, update } = useFloating<HTMLElement>({
|
||||||
|
@ -75,7 +79,18 @@ const EmojiSelector: React.FC<IEmojiSelector> = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleExpand: React.MouseEventHandler = () => {
|
const handleExpand: React.MouseEventHandler = () => {
|
||||||
setExpanded(true);
|
if (userTouching.matches) {
|
||||||
|
dispatch(openModal('EMOJI_PICKER', {
|
||||||
|
onPickEmoji: (emoji: Emoji) => {
|
||||||
|
handlePickEmoji(emoji);
|
||||||
|
dispatch(closeModal('EMOJI_PICKER'));
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
onClose?.();
|
||||||
|
} else {
|
||||||
|
setExpanded(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePickEmoji = (emoji: Emoji) => {
|
const handlePickEmoji = (emoji: Emoji) => {
|
||||||
|
@ -95,9 +110,7 @@ const EmojiSelector: React.FC<IEmojiSelector> = ({
|
||||||
}, [visible]);
|
}, [visible]);
|
||||||
|
|
||||||
useClickOutside(refs, () => {
|
useClickOutside(refs, () => {
|
||||||
if (onClose) {
|
onClose?.();
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -16,7 +16,7 @@ const messages = defineMessages({
|
||||||
});
|
});
|
||||||
|
|
||||||
const themes = {
|
const themes = {
|
||||||
normal: 'bg-white p-6 shadow-xl',
|
normal: 'bg-white black:bg-black dark:bg-primary-900 p-6 shadow-xl text-gray-900 dark:text-gray-100',
|
||||||
transparent: 'bg-transparent p-0 shadow-none',
|
transparent: 'bg-transparent p-0 shadow-none',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -105,7 +105,7 @@ const Modal = forwardRef<HTMLDivElement, IModal>(({
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
data-testid='modal'
|
data-testid='modal'
|
||||||
className={clsx(className, 'pointer-events-auto mx-auto block w-full rounded-2xl text-start align-middle text-gray-900 transition-all black:bg-black dark:bg-primary-900 dark:text-gray-100', widths[width], themes[theme])}
|
className={clsx(className, 'pointer-events-auto mx-auto block w-full rounded-2xl text-start align-middle transition-all', widths[width], themes[theme])}
|
||||||
>
|
>
|
||||||
<div className='w-full justify-between sm:flex sm:items-start'>
|
<div className='w-full justify-between sm:flex sm:items-start'>
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Map as ImmutableMap } from 'immutable';
|
import { Map as ImmutableMap } from 'immutable';
|
||||||
import { useEffect, useState, useLayoutEffect, Suspense } from 'react';
|
import React, { useEffect, useState, useLayoutEffect, Suspense } from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
@ -45,9 +45,9 @@ export interface IEmojiPickerDropdown {
|
||||||
onPickEmoji?: (emoji: Emoji) => void;
|
onPickEmoji?: (emoji: Emoji) => void;
|
||||||
condensed?: boolean;
|
condensed?: boolean;
|
||||||
withCustom?: boolean;
|
withCustom?: boolean;
|
||||||
visible: boolean;
|
visible?: boolean;
|
||||||
setVisible: (value: boolean) => void;
|
setVisible?: (value: boolean) => void;
|
||||||
update: (() => any) | null;
|
update?: (() => any) | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const perLine = 8;
|
const perLine = 8;
|
||||||
|
@ -105,8 +105,13 @@ const getCustomEmojis = createSelector([
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
interface IRenderAfter {
|
||||||
|
children: React.ReactNode;
|
||||||
|
update: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
// Fixes render bug where popover has a delayed position update
|
// Fixes render bug where popover has a delayed position update
|
||||||
const RenderAfter = ({ children, update }: any) => {
|
const RenderAfter: React.FC<IRenderAfter> = ({ children, update }) => {
|
||||||
const [nextTick, setNextTick] = useState(false);
|
const [nextTick, setNextTick] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -125,7 +130,7 @@ const RenderAfter = ({ children, update }: any) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const EmojiPickerDropdown: React.FC<IEmojiPickerDropdown> = ({
|
const EmojiPickerDropdown: React.FC<IEmojiPickerDropdown> = ({
|
||||||
onPickEmoji, visible, setVisible, update, withCustom = true,
|
onPickEmoji, visible = true, setVisible, update, withCustom = true,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
@ -136,7 +141,7 @@ const EmojiPickerDropdown: React.FC<IEmojiPickerDropdown> = ({
|
||||||
const frequentlyUsedEmojis = useAppSelector((state) => getFrequentlyUsedEmojis(state));
|
const frequentlyUsedEmojis = useAppSelector((state) => getFrequentlyUsedEmojis(state));
|
||||||
|
|
||||||
const handlePick = (emoji: any) => {
|
const handlePick = (emoji: any) => {
|
||||||
setVisible(false);
|
setVisible?.(false);
|
||||||
|
|
||||||
let pickedEmoji: Emoji;
|
let pickedEmoji: Emoji;
|
||||||
|
|
||||||
|
@ -213,28 +218,30 @@ const EmojiPickerDropdown: React.FC<IEmojiPickerDropdown> = ({
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
if (!visible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
visible ? (
|
<Suspense>
|
||||||
<Suspense>
|
<RenderAfter update={update ?? (() => {})}>
|
||||||
<RenderAfter update={update}>
|
<EmojiPicker
|
||||||
<EmojiPicker
|
custom={withCustom ? [{ emojis: buildCustomEmojis(customEmojis) }] : undefined}
|
||||||
custom={withCustom ? [{ emojis: buildCustomEmojis(customEmojis) }] : undefined}
|
title={title}
|
||||||
title={title}
|
onEmojiSelect={handlePick}
|
||||||
onEmojiSelect={handlePick}
|
recent={frequentlyUsedEmojis}
|
||||||
recent={frequentlyUsedEmojis}
|
perLine={8}
|
||||||
perLine={8}
|
skin={handleSkinTone}
|
||||||
skin={handleSkinTone}
|
emojiSize={22}
|
||||||
emojiSize={22}
|
emojiButtonSize={34}
|
||||||
emojiButtonSize={34}
|
set='twitter'
|
||||||
set='twitter'
|
theme={theme}
|
||||||
theme={theme}
|
i18n={getI18n()}
|
||||||
i18n={getI18n()}
|
skinTonePosition='search'
|
||||||
skinTonePosition='search'
|
previewPosition='none'
|
||||||
previewPosition='none'
|
/>
|
||||||
/>
|
</RenderAfter>
|
||||||
</RenderAfter>
|
</Suspense>
|
||||||
</Suspense>
|
|
||||||
) : null
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ const Picker: React.FC<any> = (props) => {
|
||||||
new EmojiPicker(input);
|
new EmojiPicker(input);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <div ref={ref} />;
|
return <div className='flex justify-center' ref={ref} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Picker;
|
export default Picker;
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
EditDomainModal,
|
EditDomainModal,
|
||||||
EditFederationModal,
|
EditFederationModal,
|
||||||
EmbedModal,
|
EmbedModal,
|
||||||
|
EmojiPickerModal,
|
||||||
EventMapModal,
|
EventMapModal,
|
||||||
EventParticipantsModal,
|
EventParticipantsModal,
|
||||||
FamiliarFollowersModal,
|
FamiliarFollowersModal,
|
||||||
|
@ -72,6 +73,7 @@ const MODAL_COMPONENTS: Record<string, React.ExoticComponent<any>> = {
|
||||||
'EDIT_FEDERATION': EditFederationModal,
|
'EDIT_FEDERATION': EditFederationModal,
|
||||||
'EDIT_RULE': EditRuleModal,
|
'EDIT_RULE': EditRuleModal,
|
||||||
'EMBED': EmbedModal,
|
'EMBED': EmbedModal,
|
||||||
|
'EMOJI_PICKER': EmojiPickerModal,
|
||||||
'EVENT_MAP': EventMapModal,
|
'EVENT_MAP': EventMapModal,
|
||||||
'EVENT_PARTICIPANTS': EventParticipantsModal,
|
'EVENT_PARTICIPANTS': EventParticipantsModal,
|
||||||
'FAMILIAR_FOLLOWERS': FamiliarFollowersModal,
|
'FAMILIAR_FOLLOWERS': FamiliarFollowersModal,
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import Modal from 'soapbox/components/ui/modal.tsx';
|
||||||
|
import EmojiPickerDropdown from 'soapbox/features/emoji/components/emoji-picker-dropdown.tsx';
|
||||||
|
import { Emoji } from 'soapbox/features/emoji/index.ts';
|
||||||
|
|
||||||
|
interface IEmojiPickerModal {
|
||||||
|
onPickEmoji?: (emoji: Emoji) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmojiPickerModal: React.FC<IEmojiPickerModal> = (props) => {
|
||||||
|
return (
|
||||||
|
<Modal className='flex' theme='transparent'>
|
||||||
|
<EmojiPickerDropdown {...props} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmojiPickerModal;
|
|
@ -2,6 +2,7 @@ import { lazy } from 'react';
|
||||||
|
|
||||||
export const AboutPage = lazy(() => import('soapbox/features/about/index.tsx'));
|
export const AboutPage = lazy(() => import('soapbox/features/about/index.tsx'));
|
||||||
export const EmojiPicker = lazy(() => import('soapbox/features/emoji/components/emoji-picker.tsx'));
|
export const EmojiPicker = lazy(() => import('soapbox/features/emoji/components/emoji-picker.tsx'));
|
||||||
|
export const EmojiPickerModal = lazy(() => import('soapbox/features/ui/components/modals/emoji-picker-modal.tsx'));
|
||||||
export const Notifications = lazy(() => import('soapbox/features/notifications/index.tsx'));
|
export const Notifications = lazy(() => import('soapbox/features/notifications/index.tsx'));
|
||||||
export const LandingTimeline = lazy(() => import('soapbox/features/landing-timeline/index.tsx'));
|
export const LandingTimeline = lazy(() => import('soapbox/features/landing-timeline/index.tsx'));
|
||||||
export const HomeTimeline = lazy(() => import('soapbox/features/home-timeline/index.tsx'));
|
export const HomeTimeline = lazy(() => import('soapbox/features/home-timeline/index.tsx'));
|
||||||
|
|
Ładowanie…
Reference in New Issue