Use an emoji picker modal on mobile

fix-error-messages
Alex Gleason 2024-11-13 18:35:49 -06:00
rodzic 5380922d4e
commit ad24779343
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 7211D1F99744FBB7
7 zmienionych plików z 75 dodań i 35 usunięć

Wyświetl plik

@ -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 (

Wyświetl plik

@ -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'>

Wyświetl plik

@ -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
); );
}; };

Wyświetl plik

@ -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;

Wyświetl plik

@ -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,

Wyświetl plik

@ -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;

Wyświetl plik

@ -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'));