Support sending custom emoji (#2000)
This commit is contained in:
parent
76c1816eba
commit
c365881d57
@ -110,6 +110,7 @@ export function buildStickerSet(set: GramJs.StickerSet): ApiStickerSet {
|
|||||||
count,
|
count,
|
||||||
shortName,
|
shortName,
|
||||||
emojis,
|
emojis,
|
||||||
|
thumbDocumentId,
|
||||||
} = set;
|
} = set;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -121,7 +122,7 @@ export function buildStickerSet(set: GramJs.StickerSet): ApiStickerSet {
|
|||||||
id: String(id),
|
id: String(id),
|
||||||
accessHash: String(accessHash),
|
accessHash: String(accessHash),
|
||||||
title,
|
title,
|
||||||
hasThumbnail: Boolean(thumbs?.length),
|
hasThumbnail: Boolean(thumbs?.length || thumbDocumentId),
|
||||||
count,
|
count,
|
||||||
shortName,
|
shortName,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -42,6 +42,7 @@ export {
|
|||||||
faveSticker, fetchStickers, fetchSavedGifs, saveGif, searchStickers, installStickerSet, uninstallStickerSet,
|
faveSticker, fetchStickers, fetchSavedGifs, saveGif, searchStickers, installStickerSet, uninstallStickerSet,
|
||||||
searchGifs, fetchAnimatedEmojis, fetchStickersForEmoji, fetchEmojiKeywords, fetchAnimatedEmojiEffects,
|
searchGifs, fetchAnimatedEmojis, fetchStickersForEmoji, fetchEmojiKeywords, fetchAnimatedEmojiEffects,
|
||||||
removeRecentSticker, clearRecentStickers, fetchCustomEmoji, fetchPremiumGifts, fetchCustomEmojiSets,
|
removeRecentSticker, clearRecentStickers, fetchCustomEmoji, fetchPremiumGifts, fetchCustomEmojiSets,
|
||||||
|
fetchFeaturedEmojiStickers,
|
||||||
} from './symbols';
|
} from './symbols';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export async function fetchCustomEmojiSets({ hash = '0' }: { hash?: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
allStickers.sets.forEach((stickerSet) => {
|
allStickers.sets.forEach((stickerSet) => {
|
||||||
if (stickerSet.thumbs?.length) {
|
if (stickerSet.thumbs?.length || stickerSet.thumbDocumentId) {
|
||||||
localDb.stickerSets[String(stickerSet.id)] = stickerSet;
|
localDb.stickerSets[String(stickerSet.id)] = stickerSet;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -98,6 +98,25 @@ export async function fetchFeaturedStickers({ hash = '0' }: { hash?: string }) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchFeaturedEmojiStickers() {
|
||||||
|
const result = await invokeRequest(new GramJs.messages.GetFeaturedEmojiStickers({ hash: BigInt(0) }));
|
||||||
|
|
||||||
|
if (!result || result instanceof GramJs.messages.FeaturedStickersNotModified) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.sets.forEach(({ set }) => {
|
||||||
|
if (set.thumbDocumentId) {
|
||||||
|
localDb.stickerSets[String(set.id)] = set;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
isPremium: Boolean(result.premium),
|
||||||
|
sets: result.sets.map(buildStickerSetCovered),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function faveSticker({
|
export async function faveSticker({
|
||||||
sticker,
|
sticker,
|
||||||
unfave,
|
unfave,
|
||||||
|
|||||||
@ -57,6 +57,7 @@ export { default as BotCommandTooltip } from '../components/middle/composer/BotC
|
|||||||
export { default as BotCommandMenu } from '../components/middle/composer/BotCommandMenu';
|
export { default as BotCommandMenu } from '../components/middle/composer/BotCommandMenu';
|
||||||
export { default as MentionTooltip } from '../components/middle/composer/MentionTooltip';
|
export { default as MentionTooltip } from '../components/middle/composer/MentionTooltip';
|
||||||
export { default as StickerTooltip } from '../components/middle/composer/StickerTooltip';
|
export { default as StickerTooltip } from '../components/middle/composer/StickerTooltip';
|
||||||
|
export { default as CustomEmojiTooltip } from '../components/middle/composer/CustomEmojiTooltip';
|
||||||
export { default as CustomSendMenu } from '../components/middle/composer/CustomSendMenu';
|
export { default as CustomSendMenu } from '../components/middle/composer/CustomSendMenu';
|
||||||
export { default as DropArea } from '../components/middle/composer/DropArea';
|
export { default as DropArea } from '../components/middle/composer/DropArea';
|
||||||
export { default as TextFormatter } from '../components/middle/composer/TextFormatter';
|
export { default as TextFormatter } from '../components/middle/composer/TextFormatter';
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import type { OwnProps as AnimatedIconProps } from './AnimatedIcon';
|
|||||||
import type { ApiSticker } from '../../api/types';
|
import type { ApiSticker } from '../../api/types';
|
||||||
import { ApiMediaFormat } from '../../api/types';
|
import { ApiMediaFormat } from '../../api/types';
|
||||||
|
|
||||||
|
import { getStickerPreviewHash } from '../../global/helpers';
|
||||||
|
|
||||||
import useMedia from '../../hooks/useMedia';
|
import useMedia from '../../hooks/useMedia';
|
||||||
|
|
||||||
import AnimatedIconWithPreview from './AnimatedIconWithPreview';
|
import AnimatedIconWithPreview from './AnimatedIconWithPreview';
|
||||||
@ -20,7 +22,7 @@ function AnimatedIconFromSticker(props: OwnProps) {
|
|||||||
const thumbDataUri = sticker?.thumbnail?.dataUri;
|
const thumbDataUri = sticker?.thumbnail?.dataUri;
|
||||||
const localMediaHash = `sticker${sticker?.id}`;
|
const localMediaHash = `sticker${sticker?.id}`;
|
||||||
const previewBlobUrl = useMedia(
|
const previewBlobUrl = useMedia(
|
||||||
sticker ? `${localMediaHash}?size=m` : undefined,
|
sticker ? getStickerPreviewHash(sticker.id) : undefined,
|
||||||
noLoad && !forcePreview,
|
noLoad && !forcePreview,
|
||||||
ApiMediaFormat.BlobUrl,
|
ApiMediaFormat.BlobUrl,
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
|
|||||||
@ -11,14 +11,16 @@ import renderText from './helpers/renderText';
|
|||||||
import { getPropertyHexColor } from '../../util/themeStyle';
|
import { getPropertyHexColor } from '../../util/themeStyle';
|
||||||
import { hexToRgb } from '../../util/switchTheme';
|
import { hexToRgb } from '../../util/switchTheme';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import { getStickerPreviewHash } from '../../global/helpers';
|
||||||
import { selectIsAlwaysHighPriorityEmoji, selectIsDefaultEmojiStatusPack } from '../../global/selectors';
|
import { selectIsAlwaysHighPriorityEmoji, selectIsDefaultEmojiStatusPack } from '../../global/selectors';
|
||||||
|
import safePlay from '../../util/safePlay';
|
||||||
|
|
||||||
import useMedia from '../../hooks/useMedia';
|
import useMedia from '../../hooks/useMedia';
|
||||||
import useEnsureCustomEmoji from '../../hooks/useEnsureCustomEmoji';
|
import useEnsureCustomEmoji from '../../hooks/useEnsureCustomEmoji';
|
||||||
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||||
import useThumbnail from '../../hooks/useThumbnail';
|
import useThumbnail from '../../hooks/useThumbnail';
|
||||||
import useCustomEmoji from './hooks/useCustomEmoji';
|
import useCustomEmoji from './hooks/useCustomEmoji';
|
||||||
import safePlay from '../../util/safePlay';
|
import useMediaTransition from '../../hooks/useMediaTransition';
|
||||||
|
|
||||||
import AnimatedSticker from './AnimatedSticker';
|
import AnimatedSticker from './AnimatedSticker';
|
||||||
import OptimizedVideo from '../ui/OptimizedVideo';
|
import OptimizedVideo from '../ui/OptimizedVideo';
|
||||||
@ -28,9 +30,11 @@ import styles from './CustomEmoji.module.scss';
|
|||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
documentId: string;
|
documentId: string;
|
||||||
children?: TeactNode;
|
children?: TeactNode;
|
||||||
|
size?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
loopLimit?: number;
|
loopLimit?: number;
|
||||||
withGridFix?: boolean;
|
withGridFix?: boolean;
|
||||||
|
withPreview?: boolean;
|
||||||
observeIntersection?: ObserveFn;
|
observeIntersection?: ObserveFn;
|
||||||
onClick?: NoneToVoidFunction;
|
onClick?: NoneToVoidFunction;
|
||||||
};
|
};
|
||||||
@ -40,9 +44,11 @@ const STICKER_SIZE = 24;
|
|||||||
const CustomEmoji: FC<OwnProps> = ({
|
const CustomEmoji: FC<OwnProps> = ({
|
||||||
documentId,
|
documentId,
|
||||||
children,
|
children,
|
||||||
|
size = STICKER_SIZE,
|
||||||
className,
|
className,
|
||||||
loopLimit,
|
loopLimit,
|
||||||
withGridFix,
|
withGridFix,
|
||||||
|
withPreview,
|
||||||
observeIntersection,
|
observeIntersection,
|
||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
@ -51,9 +57,17 @@ const CustomEmoji: FC<OwnProps> = ({
|
|||||||
// An alternative to `withGlobal` to avoid adding numerous global containers
|
// An alternative to `withGlobal` to avoid adding numerous global containers
|
||||||
const customEmoji = useCustomEmoji(documentId);
|
const customEmoji = useCustomEmoji(documentId);
|
||||||
const isUnsupportedVideo = customEmoji?.isVideo && !IS_WEBM_SUPPORTED;
|
const isUnsupportedVideo = customEmoji?.isVideo && !IS_WEBM_SUPPORTED;
|
||||||
const mediaHash = customEmoji && `sticker${customEmoji.id}${isUnsupportedVideo ? '?size=m' : ''}`;
|
const mediaHash = customEmoji && `sticker${customEmoji.id}`;
|
||||||
const mediaData = useMedia(mediaHash);
|
const mediaData = useMedia(mediaHash);
|
||||||
|
|
||||||
|
const shouldLoadPreview = !mediaData && (withPreview || isUnsupportedVideo);
|
||||||
|
const previewMediaHash = shouldLoadPreview && customEmoji && getStickerPreviewHash(customEmoji.id);
|
||||||
|
const previewMediaData = useMedia(previewMediaHash);
|
||||||
const thumbDataUri = useThumbnail(customEmoji);
|
const thumbDataUri = useThumbnail(customEmoji);
|
||||||
|
|
||||||
|
const shouldDisplayPreview = Boolean(mediaData ? isUnsupportedVideo : previewMediaData);
|
||||||
|
const transitionClassNames = useMediaTransition(shouldDisplayPreview ? previewMediaData : mediaData);
|
||||||
|
|
||||||
const loopCountRef = useRef(0);
|
const loopCountRef = useRef(0);
|
||||||
const [shouldLoop, setShouldLoop] = useState(true);
|
const [shouldLoop, setShouldLoop] = useState(true);
|
||||||
const [customColor, setCustomColor] = useState<[number, number, number] | undefined>();
|
const [customColor, setCustomColor] = useState<[number, number, number] | undefined>();
|
||||||
@ -108,15 +122,15 @@ const CustomEmoji: FC<OwnProps> = ({
|
|||||||
return (children && renderText(children, ['emoji']));
|
return (children && renderText(children, ['emoji']));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mediaData) {
|
if (!mediaData && !previewMediaData) {
|
||||||
return (
|
return (
|
||||||
<img className={styles.media} src={thumbDataUri} alt={customEmoji.emoji} />
|
<img className={styles.media} src={thumbDataUri} alt={customEmoji.emoji} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isUnsupportedVideo || (!customEmoji.isVideo && !customEmoji.isLottie)) {
|
if (shouldDisplayPreview || isUnsupportedVideo || (!customEmoji.isVideo && !customEmoji.isLottie)) {
|
||||||
return (
|
return (
|
||||||
<img className={styles.media} src={mediaData} alt={customEmoji.emoji} />
|
<img className={styles.media} src={previewMediaData || mediaData} alt={customEmoji.emoji} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,9 +151,9 @@ const CustomEmoji: FC<OwnProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedSticker
|
<AnimatedSticker
|
||||||
|
size={size}
|
||||||
key={mediaData}
|
key={mediaData}
|
||||||
className={styles.sticker}
|
className={styles.sticker}
|
||||||
size={STICKER_SIZE}
|
|
||||||
tgsUrl={mediaData}
|
tgsUrl={mediaData}
|
||||||
play={isIntersecting}
|
play={isIntersecting}
|
||||||
color={customColor}
|
color={customColor}
|
||||||
@ -160,6 +174,7 @@ const CustomEmoji: FC<OwnProps> = ({
|
|||||||
'emoji',
|
'emoji',
|
||||||
hasCustomColor && 'custom-color',
|
hasCustomColor && 'custom-color',
|
||||||
withGridFix && styles.withGridFix,
|
withGridFix && styles.withGridFix,
|
||||||
|
...transitionClassNames,
|
||||||
)}
|
)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import type { MouseEvent as ReactMouseEvent, ReactNode } from 'react';
|
|||||||
import React, {
|
import React, {
|
||||||
memo, useCallback, useEffect, useMemo, useRef,
|
memo, useCallback, useEffect, useMemo, useRef,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
import { getActions } from '../../global';
|
import { getActions, getGlobal } from '../../global';
|
||||||
|
|
||||||
import type { ApiBotInlineMediaResult, ApiSticker } from '../../api/types';
|
import type { ApiBotInlineMediaResult, ApiSticker } from '../../api/types';
|
||||||
import { ApiMediaFormat } from '../../api/types';
|
import { ApiMediaFormat } from '../../api/types';
|
||||||
@ -11,6 +11,8 @@ import buildClassName from '../../util/buildClassName';
|
|||||||
import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur';
|
import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur';
|
||||||
import safePlay from '../../util/safePlay';
|
import safePlay from '../../util/safePlay';
|
||||||
import { IS_TOUCH_ENV, IS_WEBM_SUPPORTED } from '../../util/environment';
|
import { IS_TOUCH_ENV, IS_WEBM_SUPPORTED } from '../../util/environment';
|
||||||
|
import { selectIsAlwaysHighPriorityEmoji } from '../../global/selectors';
|
||||||
|
import { getStickerPreviewHash } from '../../global/helpers';
|
||||||
|
|
||||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||||
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||||
@ -76,7 +78,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||||
|
|
||||||
const thumbDataUri = useThumbnail(sticker);
|
const thumbDataUri = useThumbnail(sticker);
|
||||||
const previewBlobUrl = useMedia(`${localMediaHash}?size=m`, !isIntersecting, ApiMediaFormat.BlobUrl);
|
const previewBlobUrl = useMedia(getStickerPreviewHash(sticker.id), !isIntersecting, ApiMediaFormat.BlobUrl);
|
||||||
|
|
||||||
const shouldPlay = isIntersecting && !noAnimate;
|
const shouldPlay = isIntersecting && !noAnimate;
|
||||||
const lottieData = useMedia(sticker.isLottie && localMediaHash, !shouldPlay);
|
const lottieData = useMedia(sticker.isLottie && localMediaHash, !shouldPlay);
|
||||||
@ -285,7 +287,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
tgsUrl={lottieData}
|
tgsUrl={lottieData}
|
||||||
play
|
play
|
||||||
size={size}
|
size={size}
|
||||||
isLowPriority
|
isLowPriority={!selectIsAlwaysHighPriorityEmoji(getGlobal(), sticker.stickerSetInfo)}
|
||||||
onLoad={markLoaded}
|
onLoad={markLoaded}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import buildClassName from '../../../util/buildClassName';
|
|||||||
import renderText from './renderText';
|
import renderText from './renderText';
|
||||||
import { copyTextToClipboard } from '../../../util/clipboard';
|
import { copyTextToClipboard } from '../../../util/clipboard';
|
||||||
import { getTranslation } from '../../../util/langProvider';
|
import { getTranslation } from '../../../util/langProvider';
|
||||||
|
import { buildCustomEmojiHtmlFromEntity } from '../../middle/composer/helpers/customEmoji';
|
||||||
|
|
||||||
import MentionLink from '../../middle/message/MentionLink';
|
import MentionLink from '../../middle/message/MentionLink';
|
||||||
import SafeLink from '../SafeLink';
|
import SafeLink from '../SafeLink';
|
||||||
@ -480,6 +481,8 @@ function processEntityAsHtml(
|
|||||||
class="spoiler"
|
class="spoiler"
|
||||||
data-entity-type="${ApiMessageEntityTypes.Spoiler}"
|
data-entity-type="${ApiMessageEntityTypes.Spoiler}"
|
||||||
>${renderedContent}</span>`;
|
>${renderedContent}</span>`;
|
||||||
|
case ApiMessageEntityTypes.CustomEmoji:
|
||||||
|
return buildCustomEmojiHtmlFromEntity(rawEntityText, entity);
|
||||||
default:
|
default:
|
||||||
return renderedContent;
|
return renderedContent;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import {
|
|||||||
} from '../../../global/selectors';
|
} from '../../../global/selectors';
|
||||||
import { renderActionMessageText } from '../../common/helpers/renderActionMessageText';
|
import { renderActionMessageText } from '../../common/helpers/renderActionMessageText';
|
||||||
import renderText from '../../common/helpers/renderText';
|
import renderText from '../../common/helpers/renderText';
|
||||||
|
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
|
||||||
import { fastRaf } from '../../../util/schedulers';
|
import { fastRaf } from '../../../util/schedulers';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { renderMessageSummary } from '../../common/helpers/renderMessageText';
|
import { renderMessageSummary } from '../../common/helpers/renderMessageText';
|
||||||
@ -249,7 +250,7 @@ const Chat: FC<OwnProps & StateProps> = ({
|
|||||||
return (
|
return (
|
||||||
<p className="last-message" dir={lang.isRtl ? 'auto' : 'ltr'}>
|
<p className="last-message" dir={lang.isRtl ? 'auto' : 'ltr'}>
|
||||||
<span className="draft">{lang('Draft')}</span>
|
<span className="draft">{lang('Draft')}</span>
|
||||||
{renderText(draft.text)}
|
{renderTextWithEntities(draft.text, draft.entities)}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { getActions, withGlobal } from '../../../global';
|
|||||||
|
|
||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import type { ApiSticker, ApiStickerSet } from '../../../api/types';
|
import type { ApiSticker, ApiStickerSet } from '../../../api/types';
|
||||||
|
import type { ISettings } from '../../../types';
|
||||||
|
|
||||||
import renderText from '../../common/helpers/renderText';
|
import renderText from '../../common/helpers/renderText';
|
||||||
import { pick } from '../../../util/iteratees';
|
import { pick } from '../../../util/iteratees';
|
||||||
@ -14,13 +15,16 @@ import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver'
|
|||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
|
|
||||||
import StickerSetCard from '../../common/StickerSetCard';
|
import StickerSetCard from '../../common/StickerSetCard';
|
||||||
|
import Checkbox from '../../ui/Checkbox';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = Pick<ISettings, (
|
||||||
|
'shouldSuggestCustomEmoji'
|
||||||
|
)> & {
|
||||||
customEmojiSetIds?: string[];
|
customEmojiSetIds?: string[];
|
||||||
stickerSetsById: Record<string, ApiStickerSet>;
|
stickerSetsById: Record<string, ApiStickerSet>;
|
||||||
};
|
};
|
||||||
@ -29,9 +33,10 @@ const SettingsCustomEmoji: FC<OwnProps & StateProps> = ({
|
|||||||
isActive,
|
isActive,
|
||||||
customEmojiSetIds,
|
customEmojiSetIds,
|
||||||
stickerSetsById,
|
stickerSetsById,
|
||||||
|
shouldSuggestCustomEmoji,
|
||||||
onReset,
|
onReset,
|
||||||
}) => {
|
}) => {
|
||||||
const { openStickerSet } = getActions();
|
const { openStickerSet, setSettingOption } = getActions();
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
@ -49,6 +54,10 @@ const SettingsCustomEmoji: FC<OwnProps & StateProps> = ({
|
|||||||
});
|
});
|
||||||
}, [openStickerSet]);
|
}, [openStickerSet]);
|
||||||
|
|
||||||
|
const handleSuggestCustomEmojiChange = useCallback((newValue: boolean) => {
|
||||||
|
setSettingOption({ shouldSuggestCustomEmoji: newValue });
|
||||||
|
}, [setSettingOption]);
|
||||||
|
|
||||||
const customEmojiSets = useMemo(() => (
|
const customEmojiSets = useMemo(() => (
|
||||||
customEmojiSetIds && Object.values(pick(stickerSetsById, customEmojiSetIds))
|
customEmojiSetIds && Object.values(pick(stickerSetsById, customEmojiSetIds))
|
||||||
), [customEmojiSetIds, stickerSetsById]);
|
), [customEmojiSetIds, stickerSetsById]);
|
||||||
@ -57,7 +66,12 @@ const SettingsCustomEmoji: FC<OwnProps & StateProps> = ({
|
|||||||
<div className="settings-content custom-scroll">
|
<div className="settings-content custom-scroll">
|
||||||
{customEmojiSets && (
|
{customEmojiSets && (
|
||||||
<div className="settings-item">
|
<div className="settings-item">
|
||||||
<div ref={stickerSettingsRef}>
|
<Checkbox
|
||||||
|
label={lang('SuggestAnimatedEmoji')}
|
||||||
|
checked={shouldSuggestCustomEmoji}
|
||||||
|
onCheck={handleSuggestCustomEmojiChange}
|
||||||
|
/>
|
||||||
|
<div className="mt-4" ref={stickerSettingsRef}>
|
||||||
{customEmojiSets.map((stickerSet: ApiStickerSet) => (
|
{customEmojiSets.map((stickerSet: ApiStickerSet) => (
|
||||||
<StickerSetCard
|
<StickerSetCard
|
||||||
key={stickerSet.id}
|
key={stickerSet.id}
|
||||||
@ -79,6 +93,9 @@ const SettingsCustomEmoji: FC<OwnProps & StateProps> = ({
|
|||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global) => {
|
(global) => {
|
||||||
return {
|
return {
|
||||||
|
...pick(global.settings.byKey, [
|
||||||
|
'shouldSuggestCustomEmoji',
|
||||||
|
]),
|
||||||
customEmojiSetIds: global.customEmojis.added.setIds,
|
customEmojiSetIds: global.customEmojis.added.setIds,
|
||||||
stickerSetsById: global.stickers.setsById,
|
stickerSetsById: global.stickers.setsById,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import React, {
|
import React, {
|
||||||
memo, useCallback, useEffect, useRef,
|
memo, useCallback, useEffect, useRef,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
|
import { getActions } from '../../../global';
|
||||||
|
|
||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import type { ApiAttachment, ApiChatMember } from '../../../api/types';
|
import type { ApiAttachment, ApiChatMember, ApiSticker } from '../../../api/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CONTENT_TYPES_WITH_PREVIEW,
|
CONTENT_TYPES_WITH_PREVIEW,
|
||||||
@ -23,6 +24,7 @@ import useLang from '../../../hooks/useLang';
|
|||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
||||||
import { useStateRef } from '../../../hooks/useStateRef';
|
import { useStateRef } from '../../../hooks/useStateRef';
|
||||||
|
import useCustomEmojiTooltip from './hooks/useCustomEmojiTooltip';
|
||||||
|
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import Modal from '../../ui/Modal';
|
import Modal from '../../ui/Modal';
|
||||||
@ -31,6 +33,7 @@ import MessageInput from './MessageInput';
|
|||||||
import MentionTooltip from './MentionTooltip';
|
import MentionTooltip from './MentionTooltip';
|
||||||
import EmojiTooltip from './EmojiTooltip.async';
|
import EmojiTooltip from './EmojiTooltip.async';
|
||||||
import CustomSendMenu from './CustomSendMenu.async';
|
import CustomSendMenu from './CustomSendMenu.async';
|
||||||
|
import CustomEmojiTooltip from './CustomEmojiTooltip.async';
|
||||||
|
|
||||||
import './AttachmentModal.scss';
|
import './AttachmentModal.scss';
|
||||||
|
|
||||||
@ -48,8 +51,9 @@ export type OwnProps = {
|
|||||||
baseEmojiKeywords?: Record<string, string[]>;
|
baseEmojiKeywords?: Record<string, string[]>;
|
||||||
emojiKeywords?: Record<string, string[]>;
|
emojiKeywords?: Record<string, string[]>;
|
||||||
shouldSchedule?: boolean;
|
shouldSchedule?: boolean;
|
||||||
|
shouldSuggestCustomEmoji?: boolean;
|
||||||
|
customEmojiForEmoji?: ApiSticker[];
|
||||||
captionLimit: number;
|
captionLimit: number;
|
||||||
addRecentEmoji: AnyToVoidFunction;
|
|
||||||
onCaptionUpdate: (html: string) => void;
|
onCaptionUpdate: (html: string) => void;
|
||||||
onSend: () => void;
|
onSend: () => void;
|
||||||
onFileAppend: (files: File[], isQuick: boolean) => void;
|
onFileAppend: (files: File[], isQuick: boolean) => void;
|
||||||
@ -75,7 +79,8 @@ const AttachmentModal: FC<OwnProps> = ({
|
|||||||
baseEmojiKeywords,
|
baseEmojiKeywords,
|
||||||
emojiKeywords,
|
emojiKeywords,
|
||||||
shouldSchedule,
|
shouldSchedule,
|
||||||
addRecentEmoji,
|
shouldSuggestCustomEmoji,
|
||||||
|
customEmojiForEmoji,
|
||||||
onCaptionUpdate,
|
onCaptionUpdate,
|
||||||
onSend,
|
onSend,
|
||||||
onFileAppend,
|
onFileAppend,
|
||||||
@ -83,6 +88,7 @@ const AttachmentModal: FC<OwnProps> = ({
|
|||||||
onSendSilent,
|
onSendSilent,
|
||||||
onSendScheduled,
|
onSendScheduled,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { addRecentCustomEmoji, addRecentEmoji } = getActions();
|
||||||
const captionRef = useStateRef(caption);
|
const captionRef = useStateRef(caption);
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const mainButtonRef = useStateRef<HTMLButtonElement | null>(null);
|
const mainButtonRef = useStateRef<HTMLButtonElement | null>(null);
|
||||||
@ -106,8 +112,22 @@ const AttachmentModal: FC<OwnProps> = ({
|
|||||||
currentUserId,
|
currentUserId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { isCustomEmojiTooltipOpen, insertCustomEmoji } = useCustomEmojiTooltip(
|
||||||
|
Boolean(shouldSuggestCustomEmoji) && isOpen,
|
||||||
|
`#${EDITABLE_INPUT_MODAL_ID}`,
|
||||||
|
caption,
|
||||||
|
onCaptionUpdate,
|
||||||
|
customEmojiForEmoji,
|
||||||
|
!isReady,
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isEmojiTooltipOpen, closeEmojiTooltip, filteredEmojis, insertEmoji,
|
isEmojiTooltipOpen,
|
||||||
|
filteredEmojis,
|
||||||
|
filteredCustomEmojis,
|
||||||
|
insertEmoji,
|
||||||
|
insertCustomEmoji: insertCustomEmojiFromEmojiTooltip,
|
||||||
|
closeEmojiTooltip,
|
||||||
} = useEmojiTooltip(
|
} = useEmojiTooltip(
|
||||||
isOpen,
|
isOpen,
|
||||||
captionRef,
|
captionRef,
|
||||||
@ -292,9 +312,18 @@ const AttachmentModal: FC<OwnProps> = ({
|
|||||||
<EmojiTooltip
|
<EmojiTooltip
|
||||||
isOpen={isEmojiTooltipOpen}
|
isOpen={isEmojiTooltipOpen}
|
||||||
emojis={filteredEmojis}
|
emojis={filteredEmojis}
|
||||||
|
customEmojis={filteredCustomEmojis}
|
||||||
onClose={closeEmojiTooltip}
|
onClose={closeEmojiTooltip}
|
||||||
onEmojiSelect={insertEmoji}
|
onEmojiSelect={insertEmoji}
|
||||||
|
onCustomEmojiSelect={insertCustomEmojiFromEmojiTooltip}
|
||||||
addRecentEmoji={addRecentEmoji}
|
addRecentEmoji={addRecentEmoji}
|
||||||
|
addRecentCustomEmoji={addRecentCustomEmoji}
|
||||||
|
/>
|
||||||
|
<CustomEmojiTooltip
|
||||||
|
chatId={chatId}
|
||||||
|
isOpen={isCustomEmojiTooltipOpen}
|
||||||
|
onCustomEmojiSelect={insertCustomEmoji}
|
||||||
|
addRecentCustomEmoji={addRecentCustomEmoji}
|
||||||
/>
|
/>
|
||||||
<MessageInput
|
<MessageInput
|
||||||
id="caption-input-text"
|
id="caption-input-text"
|
||||||
|
|||||||
@ -547,6 +547,11 @@
|
|||||||
vertical-align: 0;
|
vertical-align: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.custom-emoji {
|
||||||
|
--custom-emoji-size: 1.25rem;
|
||||||
|
vertical-align: text-top;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#caption-input-text {
|
#caption-input-text {
|
||||||
|
|||||||
@ -76,6 +76,8 @@ import { isSelectionInsideInput } from './helpers/selection';
|
|||||||
import applyIosAutoCapitalizationFix from './helpers/applyIosAutoCapitalizationFix';
|
import applyIosAutoCapitalizationFix from './helpers/applyIosAutoCapitalizationFix';
|
||||||
import { getServerTime } from '../../../util/serverTime';
|
import { getServerTime } from '../../../util/serverTime';
|
||||||
import { selectCurrentLimit } from '../../../global/selectors/limits';
|
import { selectCurrentLimit } from '../../../global/selectors/limits';
|
||||||
|
import { buildCustomEmojiHtml } from './helpers/customEmoji';
|
||||||
|
import { processMessageInputForCustomEmoji } from '../../../util/customEmojiManager';
|
||||||
|
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
import usePrevious from '../../../hooks/usePrevious';
|
import usePrevious from '../../../hooks/usePrevious';
|
||||||
@ -95,6 +97,7 @@ import useMentionTooltip from './hooks/useMentionTooltip';
|
|||||||
import useInlineBotTooltip from './hooks/useInlineBotTooltip';
|
import useInlineBotTooltip from './hooks/useInlineBotTooltip';
|
||||||
import useBotCommandTooltip from './hooks/useBotCommandTooltip';
|
import useBotCommandTooltip from './hooks/useBotCommandTooltip';
|
||||||
import useSchedule from '../../../hooks/useSchedule';
|
import useSchedule from '../../../hooks/useSchedule';
|
||||||
|
import useCustomEmojiTooltip from './hooks/useCustomEmojiTooltip';
|
||||||
|
|
||||||
import DeleteMessageModal from '../../common/DeleteMessageModal.async';
|
import DeleteMessageModal from '../../common/DeleteMessageModal.async';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
@ -107,6 +110,7 @@ import InlineBotTooltip from './InlineBotTooltip.async';
|
|||||||
import MentionTooltip from './MentionTooltip.async';
|
import MentionTooltip from './MentionTooltip.async';
|
||||||
import CustomSendMenu from './CustomSendMenu.async';
|
import CustomSendMenu from './CustomSendMenu.async';
|
||||||
import StickerTooltip from './StickerTooltip.async';
|
import StickerTooltip from './StickerTooltip.async';
|
||||||
|
import CustomEmojiTooltip from './CustomEmojiTooltip.async';
|
||||||
import EmojiTooltip from './EmojiTooltip.async';
|
import EmojiTooltip from './EmojiTooltip.async';
|
||||||
import BotCommandTooltip from './BotCommandTooltip.async';
|
import BotCommandTooltip from './BotCommandTooltip.async';
|
||||||
import BotKeyboardMenu from './BotKeyboardMenu';
|
import BotKeyboardMenu from './BotKeyboardMenu';
|
||||||
@ -150,12 +154,14 @@ type StateProps =
|
|||||||
shouldSchedule?: boolean;
|
shouldSchedule?: boolean;
|
||||||
canScheduleUntilOnline?: boolean;
|
canScheduleUntilOnline?: boolean;
|
||||||
stickersForEmoji?: ApiSticker[];
|
stickersForEmoji?: ApiSticker[];
|
||||||
|
customEmojiForEmoji?: ApiSticker[];
|
||||||
groupChatMembers?: ApiChatMember[];
|
groupChatMembers?: ApiChatMember[];
|
||||||
currentUserId?: string;
|
currentUserId?: string;
|
||||||
recentEmojis: string[];
|
recentEmojis: string[];
|
||||||
lastSyncTime?: number;
|
lastSyncTime?: number;
|
||||||
contentToBeScheduled?: GlobalState['messages']['contentToBeScheduled'];
|
contentToBeScheduled?: GlobalState['messages']['contentToBeScheduled'];
|
||||||
shouldSuggestStickers?: boolean;
|
shouldSuggestStickers?: boolean;
|
||||||
|
shouldSuggestCustomEmoji?: boolean;
|
||||||
baseEmojiKeywords?: Record<string, string[]>;
|
baseEmojiKeywords?: Record<string, string[]>;
|
||||||
emojiKeywords?: Record<string, string[]>;
|
emojiKeywords?: Record<string, string[]>;
|
||||||
topInlineBotIds?: string[];
|
topInlineBotIds?: string[];
|
||||||
@ -229,6 +235,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
botKeyboardPlaceholder,
|
botKeyboardPlaceholder,
|
||||||
withScheduledButton,
|
withScheduledButton,
|
||||||
stickersForEmoji,
|
stickersForEmoji,
|
||||||
|
customEmojiForEmoji,
|
||||||
groupChatMembers,
|
groupChatMembers,
|
||||||
topInlineBotIds,
|
topInlineBotIds,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
@ -236,6 +243,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
contentToBeScheduled,
|
contentToBeScheduled,
|
||||||
shouldSuggestStickers,
|
shouldSuggestStickers,
|
||||||
|
shouldSuggestCustomEmoji,
|
||||||
baseEmojiKeywords,
|
baseEmojiKeywords,
|
||||||
emojiKeywords,
|
emojiKeywords,
|
||||||
recentEmojis,
|
recentEmojis,
|
||||||
@ -271,13 +279,15 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
resetOpenChatWithDraft,
|
resetOpenChatWithDraft,
|
||||||
callAttachBot,
|
callAttachBot,
|
||||||
openLimitReachedModal,
|
openLimitReachedModal,
|
||||||
|
openPremiumModal,
|
||||||
|
addRecentCustomEmoji,
|
||||||
showNotification,
|
showNotification,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const appendixRef = useRef<HTMLDivElement>(null);
|
const appendixRef = useRef<HTMLDivElement>(null);
|
||||||
const [html, setHtml] = useState<string>('');
|
const [html, setInnerHtml] = useState<string>('');
|
||||||
const htmlRef = useStateRef(html);
|
const htmlRef = useStateRef(html);
|
||||||
const lastMessageSendTimeSeconds = useRef<number>();
|
const lastMessageSendTimeSeconds = useRef<number>();
|
||||||
const prevDropAreaState = usePrevious(dropAreaState);
|
const prevDropAreaState = usePrevious(dropAreaState);
|
||||||
@ -289,6 +299,15 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
const [isSymbolMenuForced, forceShowSymbolMenu, cancelForceShowSymbolMenu] = useFlag();
|
const [isSymbolMenuForced, forceShowSymbolMenu, cancelForceShowSymbolMenu] = useFlag();
|
||||||
const sendMessageAction = useSendMessageAction(chatId, threadId);
|
const sendMessageAction = useSendMessageAction(chatId, threadId);
|
||||||
|
|
||||||
|
const setHtml = useCallback((newHtml: string) => {
|
||||||
|
setInnerHtml(newHtml);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
processMessageInputForCustomEmoji();
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const customEmojiNotificationNumber = useRef(0);
|
||||||
|
|
||||||
const handleScheduleCancel = useCallback(() => {
|
const handleScheduleCancel = useCallback(() => {
|
||||||
cancelForceShowSymbolMenu();
|
cancelForceShowSymbolMenu();
|
||||||
}, [cancelForceShowSymbolMenu]);
|
}, [cancelForceShowSymbolMenu]);
|
||||||
@ -441,8 +460,21 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
stickersForEmoji,
|
stickersForEmoji,
|
||||||
!isReady,
|
!isReady,
|
||||||
);
|
);
|
||||||
|
const { isCustomEmojiTooltipOpen, closeCustomEmojiTooltip, insertCustomEmoji } = useCustomEmojiTooltip(
|
||||||
|
Boolean(shouldSuggestCustomEmoji && !attachments.length),
|
||||||
|
EDITABLE_INPUT_CSS_SELECTOR,
|
||||||
|
html,
|
||||||
|
setHtml,
|
||||||
|
customEmojiForEmoji,
|
||||||
|
!isReady,
|
||||||
|
);
|
||||||
const {
|
const {
|
||||||
isEmojiTooltipOpen, closeEmojiTooltip, filteredEmojis, insertEmoji,
|
isEmojiTooltipOpen,
|
||||||
|
closeEmojiTooltip,
|
||||||
|
filteredEmojis,
|
||||||
|
filteredCustomEmojis,
|
||||||
|
insertEmoji,
|
||||||
|
insertCustomEmoji: insertCustomEmojiFromEmojiTooltip,
|
||||||
} = useEmojiTooltip(
|
} = useEmojiTooltip(
|
||||||
Boolean(shouldSuggestStickers && canSendStickers && !attachments.length),
|
Boolean(shouldSuggestStickers && canSendStickers && !attachments.length),
|
||||||
htmlRef,
|
htmlRef,
|
||||||
@ -478,7 +510,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
focusEditableElement(messageInput);
|
focusEditableElement(messageInput);
|
||||||
});
|
});
|
||||||
}, [htmlRef]);
|
}, [htmlRef, setHtml]);
|
||||||
|
|
||||||
const insertTextAndUpdateCursor = useCallback((text: string, inputId: string = EDITABLE_INPUT_ID) => {
|
const insertTextAndUpdateCursor = useCallback((text: string, inputId: string = EDITABLE_INPUT_ID) => {
|
||||||
const newHtml = renderText(text, ['escape_html', 'emoji_html', 'br_html'])
|
const newHtml = renderText(text, ['escape_html', 'emoji_html', 'br_html'])
|
||||||
@ -487,6 +519,10 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
insertHtmlAndUpdateCursor(newHtml, inputId);
|
insertHtmlAndUpdateCursor(newHtml, inputId);
|
||||||
}, [insertHtmlAndUpdateCursor]);
|
}, [insertHtmlAndUpdateCursor]);
|
||||||
|
|
||||||
|
const insertCustomEmojiAndUpdateCursor = useCallback((emoji: ApiSticker, inputId: string = EDITABLE_INPUT_ID) => {
|
||||||
|
insertHtmlAndUpdateCursor(buildCustomEmojiHtml(emoji), inputId);
|
||||||
|
}, [insertHtmlAndUpdateCursor]);
|
||||||
|
|
||||||
const removeSymbol = useCallback(() => {
|
const removeSymbol = useCallback(() => {
|
||||||
const selection = window.getSelection()!;
|
const selection = window.getSelection()!;
|
||||||
|
|
||||||
@ -499,7 +535,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setHtml(deleteLastCharacterOutsideSelection(htmlRef.current!));
|
setHtml(deleteLastCharacterOutsideSelection(htmlRef.current!));
|
||||||
}, [htmlRef]);
|
}, [htmlRef, setHtml]);
|
||||||
|
|
||||||
const resetComposer = useCallback((shouldPreserveInput = false) => {
|
const resetComposer = useCallback((shouldPreserveInput = false) => {
|
||||||
if (!shouldPreserveInput) {
|
if (!shouldPreserveInput) {
|
||||||
@ -507,6 +543,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
setAttachments(MEMO_EMPTY_ARRAY);
|
setAttachments(MEMO_EMPTY_ARRAY);
|
||||||
closeStickerTooltip();
|
closeStickerTooltip();
|
||||||
|
closeCustomEmojiTooltip();
|
||||||
closeMentionTooltip();
|
closeMentionTooltip();
|
||||||
closeEmojiTooltip();
|
closeEmojiTooltip();
|
||||||
|
|
||||||
@ -516,7 +553,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
} else {
|
} else {
|
||||||
closeSymbolMenu();
|
closeSymbolMenu();
|
||||||
}
|
}
|
||||||
}, [closeStickerTooltip, closeMentionTooltip, closeEmojiTooltip, closeSymbolMenu]);
|
}, [closeStickerTooltip, closeCustomEmojiTooltip, closeMentionTooltip, closeEmojiTooltip, closeSymbolMenu, setHtml]);
|
||||||
|
|
||||||
// Handle chat change (ref is used to avoid redundant effect calls)
|
// Handle chat change (ref is used to avoid redundant effect calls)
|
||||||
const stopRecordingVoiceRef = useRef<typeof stopRecordingVoice>();
|
const stopRecordingVoiceRef = useRef<typeof stopRecordingVoice>();
|
||||||
@ -734,7 +771,33 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
focusEditableElement(messageInput, true);
|
focusEditableElement(messageInput, true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [requestedText, resetOpenChatWithDraft]);
|
}, [requestedText, resetOpenChatWithDraft, setHtml]);
|
||||||
|
|
||||||
|
const handleCustomEmojiSelect = useCallback((emoji: ApiSticker) => {
|
||||||
|
if (!emoji.isFree && !isCurrentUserPremium && !isChatWithSelf) {
|
||||||
|
const notificationNumber = customEmojiNotificationNumber.current;
|
||||||
|
if (!notificationNumber) {
|
||||||
|
showNotification({
|
||||||
|
message: lang('UnlockPremiumEmojiHint'),
|
||||||
|
action: () => openPremiumModal({ initialSection: 'animated_emoji' }),
|
||||||
|
actionText: lang('PremiumMore'),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
showNotification({
|
||||||
|
message: lang('UnlockPremiumEmojiHint2'),
|
||||||
|
action: () => openChat({ id: currentUserId, shouldReplaceHistory: true }),
|
||||||
|
actionText: lang('Open'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
customEmojiNotificationNumber.current = Number(!notificationNumber);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
insertCustomEmojiAndUpdateCursor(emoji);
|
||||||
|
}, [
|
||||||
|
currentUserId, insertCustomEmojiAndUpdateCursor, isChatWithSelf, isCurrentUserPremium, lang,
|
||||||
|
openChat, openPremiumModal, showNotification,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleStickerSelect = useCallback((
|
const handleStickerSelect = useCallback((
|
||||||
sticker: ApiSticker, isSilent?: boolean, isScheduleRequested?: boolean, shouldPreserveInput = false,
|
sticker: ApiSticker, isSilent?: boolean, isScheduleRequested?: boolean, shouldPreserveInput = false,
|
||||||
@ -1029,13 +1092,14 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
onCaptionUpdate={setHtml}
|
onCaptionUpdate={setHtml}
|
||||||
baseEmojiKeywords={baseEmojiKeywords}
|
baseEmojiKeywords={baseEmojiKeywords}
|
||||||
emojiKeywords={emojiKeywords}
|
emojiKeywords={emojiKeywords}
|
||||||
addRecentEmoji={addRecentEmoji}
|
|
||||||
shouldSchedule={shouldSchedule}
|
shouldSchedule={shouldSchedule}
|
||||||
onSendSilent={handleSendSilent}
|
onSendSilent={handleSendSilent}
|
||||||
onSend={handleSend}
|
onSend={handleSend}
|
||||||
onSendScheduled={handleSendScheduled}
|
onSendScheduled={handleSendScheduled}
|
||||||
onFileAppend={handleAppendFiles}
|
onFileAppend={handleAppendFiles}
|
||||||
onClear={handleClearAttachment}
|
onClear={handleClearAttachment}
|
||||||
|
shouldSuggestCustomEmoji={shouldSuggestCustomEmoji}
|
||||||
|
customEmojiForEmoji={customEmojiForEmoji}
|
||||||
/>
|
/>
|
||||||
<PollModal
|
<PollModal
|
||||||
isOpen={pollModal.isOpen}
|
isOpen={pollModal.isOpen}
|
||||||
@ -1237,12 +1301,21 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
isOpen={isStickerTooltipOpen}
|
isOpen={isStickerTooltipOpen}
|
||||||
onStickerSelect={handleStickerSelect}
|
onStickerSelect={handleStickerSelect}
|
||||||
/>
|
/>
|
||||||
|
<CustomEmojiTooltip
|
||||||
|
chatId={chatId}
|
||||||
|
isOpen={isCustomEmojiTooltipOpen}
|
||||||
|
onCustomEmojiSelect={insertCustomEmoji}
|
||||||
|
addRecentCustomEmoji={addRecentCustomEmoji}
|
||||||
|
/>
|
||||||
<EmojiTooltip
|
<EmojiTooltip
|
||||||
isOpen={isEmojiTooltipOpen}
|
isOpen={isEmojiTooltipOpen}
|
||||||
emojis={filteredEmojis}
|
emojis={filteredEmojis}
|
||||||
|
customEmojis={filteredCustomEmojis}
|
||||||
onClose={closeEmojiTooltip}
|
onClose={closeEmojiTooltip}
|
||||||
onEmojiSelect={insertEmoji}
|
onEmojiSelect={insertEmoji}
|
||||||
addRecentEmoji={addRecentEmoji}
|
addRecentEmoji={addRecentEmoji}
|
||||||
|
onCustomEmojiSelect={insertCustomEmojiFromEmojiTooltip}
|
||||||
|
addRecentCustomEmoji={addRecentCustomEmoji}
|
||||||
/>
|
/>
|
||||||
<SymbolMenu
|
<SymbolMenu
|
||||||
chatId={chatId}
|
chatId={chatId}
|
||||||
@ -1254,10 +1327,12 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
onClose={closeSymbolMenu}
|
onClose={closeSymbolMenu}
|
||||||
onEmojiSelect={insertTextAndUpdateCursor}
|
onEmojiSelect={insertTextAndUpdateCursor}
|
||||||
onStickerSelect={handleStickerSelect}
|
onStickerSelect={handleStickerSelect}
|
||||||
|
onCustomEmojiSelect={handleCustomEmojiSelect}
|
||||||
onGifSelect={handleGifSelect}
|
onGifSelect={handleGifSelect}
|
||||||
onRemoveSymbol={removeSymbol}
|
onRemoveSymbol={removeSymbol}
|
||||||
onSearchOpen={handleSearchOpen}
|
onSearchOpen={handleSearchOpen}
|
||||||
addRecentEmoji={addRecentEmoji}
|
addRecentEmoji={addRecentEmoji}
|
||||||
|
addRecentCustomEmoji={addRecentCustomEmoji}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1313,7 +1388,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const isChatWithSelf = selectIsChatWithSelf(global, chatId);
|
const isChatWithSelf = selectIsChatWithSelf(global, chatId);
|
||||||
const messageWithActualBotKeyboard = isChatWithBot && selectNewestMessageWithBotKeyboardButtons(global, chatId);
|
const messageWithActualBotKeyboard = isChatWithBot && selectNewestMessageWithBotKeyboardButtons(global, chatId);
|
||||||
const scheduledIds = selectScheduledIds(global, chatId);
|
const scheduledIds = selectScheduledIds(global, chatId);
|
||||||
const { language, shouldSuggestStickers } = global.settings.byKey;
|
const { language, shouldSuggestStickers, shouldSuggestCustomEmoji } = global.settings.byKey;
|
||||||
const baseEmojiKeywords = global.emojiKeywords[BASE_EMOJI_KEYWORD_LANG];
|
const baseEmojiKeywords = global.emojiKeywords[BASE_EMOJI_KEYWORD_LANG];
|
||||||
const emojiKeywords = language !== BASE_EMOJI_KEYWORD_LANG ? global.emojiKeywords[language] : undefined;
|
const emojiKeywords = language !== BASE_EMOJI_KEYWORD_LANG ? global.emojiKeywords[language] : undefined;
|
||||||
const botKeyboardMessageId = messageWithActualBotKeyboard ? messageWithActualBotKeyboard.id : undefined;
|
const botKeyboardMessageId = messageWithActualBotKeyboard ? messageWithActualBotKeyboard.id : undefined;
|
||||||
@ -1360,12 +1435,14 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
isForwarding: chatId === global.forwardMessages.toChatId,
|
isForwarding: chatId === global.forwardMessages.toChatId,
|
||||||
pollModal: global.pollModal,
|
pollModal: global.pollModal,
|
||||||
stickersForEmoji: global.stickers.forEmoji.stickers,
|
stickersForEmoji: global.stickers.forEmoji.stickers,
|
||||||
|
customEmojiForEmoji: global.customEmojis.forEmoji.stickers,
|
||||||
groupChatMembers: chat?.fullInfo?.members,
|
groupChatMembers: chat?.fullInfo?.members,
|
||||||
topInlineBotIds: global.topInlineBots?.userIds,
|
topInlineBotIds: global.topInlineBots?.userIds,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
lastSyncTime: global.lastSyncTime,
|
lastSyncTime: global.lastSyncTime,
|
||||||
contentToBeScheduled: global.messages.contentToBeScheduled,
|
contentToBeScheduled: global.messages.contentToBeScheduled,
|
||||||
shouldSuggestStickers,
|
shouldSuggestStickers,
|
||||||
|
shouldSuggestCustomEmoji,
|
||||||
recentEmojis: global.recentEmojis,
|
recentEmojis: global.recentEmojis,
|
||||||
baseEmojiKeywords: baseEmojiKeywords?.keywords,
|
baseEmojiKeywords: baseEmojiKeywords?.keywords,
|
||||||
emojiKeywords: emojiKeywords?.keywords,
|
emojiKeywords: emojiKeywords?.keywords,
|
||||||
|
|||||||
46
src/components/middle/composer/CustomEmojiButton.tsx
Normal file
46
src/components/middle/composer/CustomEmojiButton.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React, { memo, useCallback } from '../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import type { ApiSticker } from '../../../api/types';
|
||||||
|
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
|
||||||
|
import CustomEmoji from '../../common/CustomEmoji';
|
||||||
|
|
||||||
|
import './EmojiButton.scss';
|
||||||
|
|
||||||
|
const CUSTOM_EMOJI_SIZE = 32;
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
emoji: ApiSticker;
|
||||||
|
focus?: boolean;
|
||||||
|
onClick?: (emoji: ApiSticker) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CustomEmojiButton: FC<OwnProps> = ({
|
||||||
|
emoji, focus, onClick,
|
||||||
|
}) => {
|
||||||
|
const handleClick = useCallback((e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||||
|
// Preventing safari from losing focus on Composer MessageInput
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
onClick?.(emoji);
|
||||||
|
}, [emoji, onClick]);
|
||||||
|
|
||||||
|
const className = buildClassName(
|
||||||
|
'EmojiButton',
|
||||||
|
focus && 'focus',
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={className}
|
||||||
|
onMouseDown={handleClick}
|
||||||
|
title={emoji.emoji}
|
||||||
|
>
|
||||||
|
<CustomEmoji documentId={emoji.id} size={CUSTOM_EMOJI_SIZE} withPreview />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(CustomEmojiButton);
|
||||||
298
src/components/middle/composer/CustomEmojiPicker.tsx
Normal file
298
src/components/middle/composer/CustomEmojiPicker.tsx
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import React, {
|
||||||
|
useState, useEffect, memo, useRef, useMemo, useCallback,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
import { withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type { ApiStickerSet, ApiSticker, ApiChat } from '../../../api/types';
|
||||||
|
import type { StickerSetOrRecent } from '../../../types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CHAT_STICKER_SET_ID,
|
||||||
|
FAVORITE_SYMBOL_SET_ID,
|
||||||
|
PREMIUM_STICKER_SET_ID,
|
||||||
|
RECENT_SYMBOL_SET_ID,
|
||||||
|
SLIDE_TRANSITION_DURATION,
|
||||||
|
STICKER_SIZE_PICKER_HEADER,
|
||||||
|
} from '../../../config';
|
||||||
|
import { IS_TOUCH_ENV } from '../../../util/environment';
|
||||||
|
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||||
|
import fastSmoothScroll from '../../../util/fastSmoothScroll';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal';
|
||||||
|
import { pickTruthy } from '../../../util/iteratees';
|
||||||
|
import { selectIsChatWithSelf, selectIsCurrentUserPremium } from '../../../global/selectors';
|
||||||
|
|
||||||
|
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
||||||
|
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
||||||
|
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
|
||||||
|
import Loading from '../../ui/Loading';
|
||||||
|
import Button from '../../ui/Button';
|
||||||
|
import StickerButton from '../../common/StickerButton';
|
||||||
|
import StickerSet from './StickerSet';
|
||||||
|
import StickerSetCover from './StickerSetCover';
|
||||||
|
import StickerSetCoverAnimated from './StickerSetCoverAnimated';
|
||||||
|
|
||||||
|
import './StickerPicker.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
chatId: string;
|
||||||
|
className: string;
|
||||||
|
loadAndPlay: boolean;
|
||||||
|
onCustomEmojiSelect: (sticker: ApiSticker) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
chat?: ApiChat;
|
||||||
|
stickerSetsById: Record<string, ApiStickerSet>;
|
||||||
|
addedCustomEmojiIds?: string[];
|
||||||
|
recentCustomEmoji: ApiSticker[];
|
||||||
|
featuredCustomEmojiIds?: string[];
|
||||||
|
shouldPlay?: boolean;
|
||||||
|
isSavedMessages?: boolean;
|
||||||
|
isCurrentUserPremium?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SMOOTH_SCROLL_DISTANCE = 500;
|
||||||
|
const HEADER_BUTTON_WIDTH = 52; // px (including margin)
|
||||||
|
const STICKER_INTERSECTION_THROTTLE = 200;
|
||||||
|
|
||||||
|
const stickerSetIntersections: boolean[] = [];
|
||||||
|
|
||||||
|
const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||||
|
className,
|
||||||
|
loadAndPlay,
|
||||||
|
addedCustomEmojiIds,
|
||||||
|
recentCustomEmoji,
|
||||||
|
stickerSetsById,
|
||||||
|
featuredCustomEmojiIds,
|
||||||
|
shouldPlay,
|
||||||
|
isSavedMessages,
|
||||||
|
isCurrentUserPremium,
|
||||||
|
onCustomEmojiSelect,
|
||||||
|
}) => {
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const headerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [activeSetIndex, setActiveSetIndex] = useState<number>(0);
|
||||||
|
|
||||||
|
const { observe: observeIntersection } = useIntersectionObserver({
|
||||||
|
rootRef: containerRef,
|
||||||
|
throttleMs: STICKER_INTERSECTION_THROTTLE,
|
||||||
|
}, (entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
const { id } = entry.target as HTMLDivElement;
|
||||||
|
if (!id || !id.startsWith('custom-emoji-set-')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = Number(id.replace('custom-emoji-set-', ''));
|
||||||
|
stickerSetIntersections[index] = entry.isIntersecting;
|
||||||
|
});
|
||||||
|
|
||||||
|
const intersectingWithIndexes = stickerSetIntersections
|
||||||
|
.map((isIntersecting, index) => ({ index, isIntersecting }))
|
||||||
|
.filter(({ isIntersecting }) => isIntersecting);
|
||||||
|
|
||||||
|
if (!intersectingWithIndexes.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveSetIndex(intersectingWithIndexes[Math.floor(intersectingWithIndexes.length / 2)].index);
|
||||||
|
});
|
||||||
|
const { observe: observeIntersectionForCovers } = useIntersectionObserver({ rootRef: headerRef });
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const areAddedLoaded = Boolean(addedCustomEmojiIds);
|
||||||
|
|
||||||
|
const allSets = useMemo(() => {
|
||||||
|
if (!addedCustomEmojiIds) {
|
||||||
|
return MEMO_EMPTY_ARRAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultSets = [];
|
||||||
|
|
||||||
|
if (recentCustomEmoji.length) {
|
||||||
|
defaultSets.push({
|
||||||
|
id: RECENT_SYMBOL_SET_ID,
|
||||||
|
title: lang('RecentStickers'),
|
||||||
|
stickers: recentCustomEmoji,
|
||||||
|
count: recentCustomEmoji.length,
|
||||||
|
isEmoji: true as true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingAddedSetIds = Object.values(pickTruthy(stickerSetsById, addedCustomEmojiIds));
|
||||||
|
|
||||||
|
const filteredFeaturedIds = featuredCustomEmojiIds?.filter((id) => !addedCustomEmojiIds.includes(id)) || [];
|
||||||
|
const featuredSetIds = Object.values(pickTruthy(stickerSetsById, filteredFeaturedIds));
|
||||||
|
|
||||||
|
return [
|
||||||
|
...defaultSets,
|
||||||
|
...existingAddedSetIds,
|
||||||
|
...featuredSetIds,
|
||||||
|
];
|
||||||
|
}, [addedCustomEmojiIds, featuredCustomEmojiIds, lang, recentCustomEmoji, stickerSetsById]);
|
||||||
|
|
||||||
|
const noPopulatedSets = useMemo(() => (
|
||||||
|
areAddedLoaded
|
||||||
|
&& allSets.filter((set) => set.stickers?.length).length === 0
|
||||||
|
), [allSets, areAddedLoaded]);
|
||||||
|
|
||||||
|
useHorizontalScroll(headerRef.current);
|
||||||
|
|
||||||
|
// Scroll container and header when active set changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!areAddedLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const header = headerRef.current;
|
||||||
|
if (!header) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newLeft = activeSetIndex * HEADER_BUTTON_WIDTH - (header.offsetWidth / 2 - HEADER_BUTTON_WIDTH / 2);
|
||||||
|
|
||||||
|
fastSmoothScrollHorizontal(header, newLeft);
|
||||||
|
}, [areAddedLoaded, activeSetIndex]);
|
||||||
|
|
||||||
|
const selectStickerSet = useCallback((index: number) => {
|
||||||
|
setActiveSetIndex(index);
|
||||||
|
const stickerSetEl = document.getElementById(`custom-emoji-set-${index}`)!;
|
||||||
|
fastSmoothScroll(containerRef.current!, stickerSetEl, 'start', undefined, SMOOTH_SCROLL_DISTANCE);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleEmojiSelect = useCallback((emoji: ApiSticker) => {
|
||||||
|
onCustomEmojiSelect(emoji);
|
||||||
|
}, [onCustomEmojiSelect]);
|
||||||
|
|
||||||
|
const canRenderContents = useAsyncRendering([], SLIDE_TRANSITION_DURATION);
|
||||||
|
|
||||||
|
function renderCover(stickerSet: StickerSetOrRecent, index: number) {
|
||||||
|
const firstSticker = stickerSet.stickers?.[0];
|
||||||
|
const buttonClassName = buildClassName(
|
||||||
|
'symbol-set-button sticker-set-button',
|
||||||
|
index === activeSetIndex && 'activated',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (stickerSet.id === RECENT_SYMBOL_SET_ID
|
||||||
|
|| stickerSet.id === FAVORITE_SYMBOL_SET_ID
|
||||||
|
|| stickerSet.id === CHAT_STICKER_SET_ID
|
||||||
|
|| stickerSet.id === PREMIUM_STICKER_SET_ID
|
||||||
|
|| stickerSet.hasThumbnail
|
||||||
|
|| !firstSticker) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={stickerSet.id}
|
||||||
|
className={buttonClassName}
|
||||||
|
ariaLabel={stickerSet.title}
|
||||||
|
round
|
||||||
|
faded={stickerSet.id === RECENT_SYMBOL_SET_ID || stickerSet.id === FAVORITE_SYMBOL_SET_ID}
|
||||||
|
color="translucent"
|
||||||
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
|
onClick={() => selectStickerSet(index)}
|
||||||
|
>
|
||||||
|
{stickerSet.id === RECENT_SYMBOL_SET_ID ? (
|
||||||
|
<i className="icon-recent" />
|
||||||
|
) : stickerSet.isLottie ? (
|
||||||
|
<StickerSetCoverAnimated
|
||||||
|
stickerSet={stickerSet as ApiStickerSet}
|
||||||
|
observeIntersection={observeIntersectionForCovers}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<StickerSetCover
|
||||||
|
stickerSet={stickerSet as ApiStickerSet}
|
||||||
|
observeIntersection={observeIntersectionForCovers}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<StickerButton
|
||||||
|
key={stickerSet.id}
|
||||||
|
sticker={firstSticker}
|
||||||
|
size={STICKER_SIZE_PICKER_HEADER}
|
||||||
|
title={stickerSet.title}
|
||||||
|
className={buttonClassName}
|
||||||
|
observeIntersection={observeIntersectionForCovers}
|
||||||
|
onClick={selectStickerSet}
|
||||||
|
clickArg={index}
|
||||||
|
noContextMenu
|
||||||
|
isCurrentUserPremium
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullClassName = buildClassName('StickerPicker', 'CustomEmojiPicker', className);
|
||||||
|
|
||||||
|
if (!areAddedLoaded || !canRenderContents || noPopulatedSets) {
|
||||||
|
return (
|
||||||
|
<div className={fullClassName}>
|
||||||
|
{noPopulatedSets ? (
|
||||||
|
<div className="picker-disabled">{lang('NoStickers')}</div>
|
||||||
|
) : (
|
||||||
|
<Loading />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={fullClassName}>
|
||||||
|
<div
|
||||||
|
ref={headerRef}
|
||||||
|
className="StickerPicker-header no-selection no-scrollbar"
|
||||||
|
>
|
||||||
|
{allSets.map(renderCover)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={buildClassName('StickerPicker-main no-selection', IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll')}
|
||||||
|
>
|
||||||
|
{allSets.map((stickerSet, i) => (
|
||||||
|
<StickerSet
|
||||||
|
key={stickerSet.id}
|
||||||
|
stickerSet={stickerSet}
|
||||||
|
loadAndPlay={Boolean(shouldPlay && loadAndPlay)}
|
||||||
|
index={i}
|
||||||
|
observeIntersection={observeIntersection}
|
||||||
|
shouldRender={activeSetIndex >= i - 1 && activeSetIndex <= i + 1}
|
||||||
|
onStickerSelect={handleEmojiSelect}
|
||||||
|
isSavedMessages={isSavedMessages}
|
||||||
|
isCustomEmojiPicker
|
||||||
|
isCurrentUserPremium={isCurrentUserPremium}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global, { chatId }): StateProps => {
|
||||||
|
const {
|
||||||
|
setsById,
|
||||||
|
} = global.stickers;
|
||||||
|
|
||||||
|
const isSavedMessages = selectIsChatWithSelf(global, chatId);
|
||||||
|
|
||||||
|
const recentCustomEmoji = Object.values(pickTruthy(global.customEmojis.byId, global.recentCustomEmojis));
|
||||||
|
|
||||||
|
return {
|
||||||
|
stickerSetsById: setsById,
|
||||||
|
addedCustomEmojiIds: global.customEmojis.added.setIds,
|
||||||
|
shouldPlay: global.settings.byKey.shouldLoopStickers,
|
||||||
|
isSavedMessages,
|
||||||
|
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||||
|
recentCustomEmoji,
|
||||||
|
featuredCustomEmojiIds: global.customEmojis.featuredIds,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(CustomEmojiPicker));
|
||||||
16
src/components/middle/composer/CustomEmojiTooltip.async.tsx
Normal file
16
src/components/middle/composer/CustomEmojiTooltip.async.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import React, { memo } from '../../../lib/teact/teact';
|
||||||
|
import type { OwnProps } from './CustomEmojiTooltip';
|
||||||
|
import { Bundles } from '../../../util/moduleLoader';
|
||||||
|
|
||||||
|
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||||
|
|
||||||
|
const CustomEmojiTooltipAsync: FC<OwnProps> = (props) => {
|
||||||
|
const { isOpen } = props;
|
||||||
|
const CustomEmojiTooltip = useModuleLoader(Bundles.Extra, 'CustomEmojiTooltip', !isOpen);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
return CustomEmojiTooltip ? <CustomEmojiTooltip {...props} /> : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(CustomEmojiTooltipAsync);
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
.root:global(.composer-tooltip) {
|
||||||
|
display: flex;
|
||||||
|
padding-left: 0.25rem;
|
||||||
|
|
||||||
|
overflow-x: auto;
|
||||||
|
@supports (overflow-x: overlay) {
|
||||||
|
overflow-x: overlay;
|
||||||
|
}
|
||||||
|
overflow-y: hidden;
|
||||||
|
|
||||||
|
.emojiButton {
|
||||||
|
flex: 0 0 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
116
src/components/middle/composer/CustomEmojiTooltip.tsx
Normal file
116
src/components/middle/composer/CustomEmojiTooltip.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import React, {
|
||||||
|
memo, useCallback, useEffect, useRef,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import type { ApiSticker } from '../../../api/types';
|
||||||
|
import type { GlobalActions } from '../../../global/types';
|
||||||
|
|
||||||
|
import { EMOJI_SIZE_PICKER } from '../../../config';
|
||||||
|
import { selectIsChatWithSelf, selectIsCurrentUserPremium } from '../../../global/selectors';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||||
|
|
||||||
|
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
||||||
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
|
import usePrevious from '../../../hooks/usePrevious';
|
||||||
|
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
||||||
|
|
||||||
|
import Loading from '../../ui/Loading';
|
||||||
|
import StickerButton from '../../common/StickerButton';
|
||||||
|
|
||||||
|
import styles from './CustomEmojiTooltip.module.scss';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
chatId: string;
|
||||||
|
isOpen: boolean;
|
||||||
|
onCustomEmojiSelect: (customEmoji: ApiSticker) => void;
|
||||||
|
addRecentCustomEmoji: GlobalActions['addRecentCustomEmoji'];
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
customEmoji?: ApiSticker[];
|
||||||
|
isSavedMessages?: boolean;
|
||||||
|
isCurrentUserPremium?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const INTERSECTION_THROTTLE = 200;
|
||||||
|
|
||||||
|
const CustomEmojiTooltip: FC<OwnProps & StateProps> = ({
|
||||||
|
isOpen,
|
||||||
|
customEmoji,
|
||||||
|
isSavedMessages,
|
||||||
|
isCurrentUserPremium,
|
||||||
|
onCustomEmojiSelect,
|
||||||
|
addRecentCustomEmoji,
|
||||||
|
}) => {
|
||||||
|
const { clearCustomEmojiForEmoji } = getActions();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false);
|
||||||
|
const prevStickers = usePrevious(customEmoji, true);
|
||||||
|
const displayedStickers = customEmoji || prevStickers;
|
||||||
|
|
||||||
|
useHorizontalScroll(containerRef.current);
|
||||||
|
|
||||||
|
const {
|
||||||
|
observe: observeIntersection,
|
||||||
|
} = useIntersectionObserver({ rootRef: containerRef, throttleMs: INTERSECTION_THROTTLE });
|
||||||
|
|
||||||
|
useEffect(() => (
|
||||||
|
isOpen ? captureEscKeyListener(clearCustomEmojiForEmoji) : undefined
|
||||||
|
), [isOpen, clearCustomEmojiForEmoji]);
|
||||||
|
|
||||||
|
const handleCustomEmojiSelect = useCallback((ce: ApiSticker) => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
onCustomEmojiSelect(ce);
|
||||||
|
addRecentCustomEmoji({
|
||||||
|
documentId: ce.id,
|
||||||
|
});
|
||||||
|
clearCustomEmojiForEmoji();
|
||||||
|
}, [addRecentCustomEmoji, clearCustomEmojiForEmoji, isOpen, onCustomEmojiSelect]);
|
||||||
|
|
||||||
|
const className = buildClassName(
|
||||||
|
styles.root,
|
||||||
|
'composer-tooltip custom-scroll-x',
|
||||||
|
transitionClassNames,
|
||||||
|
!displayedStickers?.length && styles.hidden,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{shouldRender && displayedStickers ? (
|
||||||
|
displayedStickers.map((sticker) => (
|
||||||
|
<StickerButton
|
||||||
|
key={sticker.id}
|
||||||
|
sticker={sticker}
|
||||||
|
className={styles.emojiButton}
|
||||||
|
size={EMOJI_SIZE_PICKER}
|
||||||
|
observeIntersection={observeIntersection}
|
||||||
|
onClick={handleCustomEmojiSelect}
|
||||||
|
clickArg={sticker}
|
||||||
|
isSavedMessages={isSavedMessages}
|
||||||
|
canViewSet
|
||||||
|
isCurrentUserPremium={isCurrentUserPremium}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : shouldRender ? (
|
||||||
|
<Loading />
|
||||||
|
) : undefined}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global, { chatId }): StateProps => {
|
||||||
|
const { stickers: customEmoji } = global.customEmojis.forEmoji;
|
||||||
|
const isSavedMessages = selectIsChatWithSelf(global, chatId);
|
||||||
|
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
|
||||||
|
return { customEmoji, isSavedMessages, isCurrentUserPremium };
|
||||||
|
},
|
||||||
|
)(CustomEmojiTooltip));
|
||||||
@ -30,4 +30,8 @@
|
|||||||
width: 2rem;
|
width: 2rem;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& > .custom-emoji {
|
||||||
|
--custom-emoji-size: 2rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import type { FC } from '../../../lib/teact/teact';
|
|
||||||
import React, { memo, useCallback } from '../../../lib/teact/teact';
|
import React, { memo, useCallback } from '../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
|
||||||
import { IS_EMOJI_SUPPORTED } from '../../../util/environment';
|
import { IS_EMOJI_SUPPORTED } from '../../../util/environment';
|
||||||
import { handleEmojiLoad, LOADED_EMOJIS } from '../../../util/emoji';
|
import { handleEmojiLoad, LOADED_EMOJIS } from '../../../util/emoji';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
@ -13,7 +14,9 @@ type OwnProps = {
|
|||||||
onClick: (emoji: string, name: string) => void;
|
onClick: (emoji: string, name: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EmojiButton: FC<OwnProps> = ({ emoji, focus, onClick }) => {
|
const EmojiButton: FC<OwnProps> = ({
|
||||||
|
emoji, focus, onClick,
|
||||||
|
}) => {
|
||||||
const handleClick = useCallback((e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
const handleClick = useCallback((e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||||
// Preventing safari from losing focus on Composer MessageInput
|
// Preventing safari from losing focus on Composer MessageInput
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import type { FC } from '../../../lib/teact/teact';
|
|
||||||
import React, {
|
import React, {
|
||||||
memo, useCallback, useEffect, useRef,
|
memo, useCallback, useEffect, useRef,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { ApiSticker } from '../../../api/types';
|
||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import findInViewport from '../../../util/findInViewport';
|
import findInViewport from '../../../util/findInViewport';
|
||||||
import isFullyVisible from '../../../util/isFullyVisible';
|
import isFullyVisible from '../../../util/isFullyVisible';
|
||||||
@ -15,6 +17,7 @@ import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
|||||||
|
|
||||||
import Loading from '../../ui/Loading';
|
import Loading from '../../ui/Loading';
|
||||||
import EmojiButton from './EmojiButton';
|
import EmojiButton from './EmojiButton';
|
||||||
|
import CustomEmojiButton from './CustomEmojiButton';
|
||||||
|
|
||||||
import './EmojiTooltip.scss';
|
import './EmojiTooltip.scss';
|
||||||
|
|
||||||
@ -52,23 +55,31 @@ function setItemVisible(index: number, containerRef: Record<string, any>) {
|
|||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onEmojiSelect: (text: string) => void;
|
|
||||||
onClose: NoneToVoidFunction;
|
|
||||||
addRecentEmoji: AnyToVoidFunction;
|
|
||||||
emojis: Emoji[];
|
emojis: Emoji[];
|
||||||
|
customEmojis: ApiSticker[];
|
||||||
|
onEmojiSelect: (text: string) => void;
|
||||||
|
onCustomEmojiSelect: (emoji: ApiSticker) => void;
|
||||||
|
onClose: NoneToVoidFunction;
|
||||||
|
addRecentEmoji: ({ emoji }: { emoji: string }) => void;
|
||||||
|
addRecentCustomEmoji: ({ documentId }: { documentId: string }) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EmojiTooltip: FC<OwnProps> = ({
|
const EmojiTooltip: FC<OwnProps> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
emojis,
|
emojis,
|
||||||
|
customEmojis,
|
||||||
onClose,
|
onClose,
|
||||||
onEmojiSelect,
|
onEmojiSelect,
|
||||||
|
onCustomEmojiSelect,
|
||||||
addRecentEmoji,
|
addRecentEmoji,
|
||||||
|
addRecentCustomEmoji,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false);
|
const { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false);
|
||||||
const listEmojis: Emoji[] = usePrevDuringAnimation(emojis.length ? emojis : undefined, CLOSE_DURATION) || [];
|
const listEmojis: (Emoji | ApiSticker)[] = usePrevDuringAnimation(
|
||||||
|
emojis.length ? [...customEmojis, ...emojis] : undefined, CLOSE_DURATION,
|
||||||
|
) || [];
|
||||||
|
|
||||||
useHorizontalScroll(containerRef.current);
|
useHorizontalScroll(containerRef.current);
|
||||||
|
|
||||||
@ -77,16 +88,34 @@ const EmojiTooltip: FC<OwnProps> = ({
|
|||||||
addRecentEmoji({ emoji: emoji.id });
|
addRecentEmoji({ emoji: emoji.id });
|
||||||
}, [addRecentEmoji, onEmojiSelect]);
|
}, [addRecentEmoji, onEmojiSelect]);
|
||||||
|
|
||||||
|
const handleCustomEmojiSelect = useCallback((emoji: ApiSticker) => {
|
||||||
|
onCustomEmojiSelect(emoji);
|
||||||
|
addRecentCustomEmoji({ documentId: emoji.id });
|
||||||
|
}, [addRecentCustomEmoji, onCustomEmojiSelect]);
|
||||||
|
|
||||||
|
const handleSelect = useCallback((emoji: Emoji | ApiSticker) => {
|
||||||
|
if ('native' in emoji) {
|
||||||
|
handleSelectEmoji(emoji);
|
||||||
|
} else {
|
||||||
|
handleCustomEmojiSelect(emoji);
|
||||||
|
}
|
||||||
|
}, [handleCustomEmojiSelect, handleSelectEmoji]);
|
||||||
|
|
||||||
const handleClick = useCallback((native: string, id: string) => {
|
const handleClick = useCallback((native: string, id: string) => {
|
||||||
onEmojiSelect(native);
|
onEmojiSelect(native);
|
||||||
addRecentEmoji({ emoji: id });
|
addRecentEmoji({ emoji: id });
|
||||||
}, [addRecentEmoji, onEmojiSelect]);
|
}, [addRecentEmoji, onEmojiSelect]);
|
||||||
|
|
||||||
|
const handleCustomEmojiClick = useCallback((emoji: ApiSticker) => {
|
||||||
|
onCustomEmojiSelect(emoji);
|
||||||
|
addRecentCustomEmoji({ documentId: emoji.id });
|
||||||
|
}, [addRecentCustomEmoji, onCustomEmojiSelect]);
|
||||||
|
|
||||||
const selectedIndex = useKeyboardNavigation({
|
const selectedIndex = useKeyboardNavigation({
|
||||||
isActive: isOpen,
|
isActive: isOpen,
|
||||||
isHorizontal: true,
|
isHorizontal: true,
|
||||||
items: emojis,
|
items: listEmojis,
|
||||||
onSelect: handleSelectEmoji,
|
onSelect: handleSelect,
|
||||||
onClose,
|
onClose,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -106,12 +135,21 @@ const EmojiTooltip: FC<OwnProps> = ({
|
|||||||
>
|
>
|
||||||
{shouldRender && listEmojis ? (
|
{shouldRender && listEmojis ? (
|
||||||
listEmojis.map((emoji, index) => (
|
listEmojis.map((emoji, index) => (
|
||||||
<EmojiButton
|
'native' in emoji ? (
|
||||||
key={emoji.id}
|
<EmojiButton
|
||||||
emoji={emoji}
|
key={emoji.id}
|
||||||
focus={selectedIndex === index}
|
emoji={emoji}
|
||||||
onClick={handleClick}
|
focus={selectedIndex === index}
|
||||||
/>
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CustomEmojiButton
|
||||||
|
key={emoji.id}
|
||||||
|
emoji={emoji}
|
||||||
|
focus={selectedIndex === index}
|
||||||
|
onClick={handleCustomEmojiClick}
|
||||||
|
/>
|
||||||
|
)
|
||||||
))
|
))
|
||||||
) : shouldRender ? (
|
) : shouldRender ? (
|
||||||
<Loading />
|
<Loading />
|
||||||
|
|||||||
@ -297,6 +297,7 @@ const MessageInput: FC<OwnProps & StateProps> = ({
|
|||||||
&& (!textContent || !textContent.length)
|
&& (!textContent || !textContent.length)
|
||||||
// When emojis are not supported, innerHTML contains an emoji img tag that doesn't exist in the textContext
|
// When emojis are not supported, innerHTML contains an emoji img tag that doesn't exist in the textContext
|
||||||
&& !(!IS_EMOJI_SUPPORTED && innerHTML.includes('emoji-small'))
|
&& !(!IS_EMOJI_SUPPORTED && innerHTML.includes('emoji-small'))
|
||||||
|
&& !(innerHTML.includes('custom-emoji'))
|
||||||
) {
|
) {
|
||||||
const selection = window.getSelection()!;
|
const selection = window.getSelection()!;
|
||||||
if (selection) {
|
if (selection) {
|
||||||
|
|||||||
@ -75,6 +75,10 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.activated {
|
||||||
|
background-color: var(--color-background-selected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import {
|
|||||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
|
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
|
||||||
import windowSize from '../../../util/windowSize';
|
import windowSize from '../../../util/windowSize';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import { selectIsSetPremium } from '../../../global/selectors';
|
||||||
|
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
@ -31,12 +32,13 @@ type OwnProps = {
|
|||||||
shouldRender: boolean;
|
shouldRender: boolean;
|
||||||
favoriteStickers?: ApiSticker[];
|
favoriteStickers?: ApiSticker[];
|
||||||
isSavedMessages?: boolean;
|
isSavedMessages?: boolean;
|
||||||
|
isCurrentUserPremium?: boolean;
|
||||||
|
isCustomEmojiPicker?: boolean;
|
||||||
observeIntersection: ObserveFn;
|
observeIntersection: ObserveFn;
|
||||||
onStickerSelect?: (sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean) => void;
|
onStickerSelect?: (sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean) => void;
|
||||||
onStickerUnfave?: (sticker: ApiSticker) => void;
|
onStickerUnfave?: (sticker: ApiSticker) => void;
|
||||||
onStickerFave?: (sticker: ApiSticker) => void;
|
onStickerFave?: (sticker: ApiSticker) => void;
|
||||||
onStickerRemoveRecent?: (sticker: ApiSticker) => void;
|
onStickerRemoveRecent?: (sticker: ApiSticker) => void;
|
||||||
isCurrentUserPremium?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const STICKERS_PER_ROW_ON_DESKTOP = 5;
|
const STICKERS_PER_ROW_ON_DESKTOP = 5;
|
||||||
@ -52,14 +54,20 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
shouldRender,
|
shouldRender,
|
||||||
favoriteStickers,
|
favoriteStickers,
|
||||||
isSavedMessages,
|
isSavedMessages,
|
||||||
|
isCurrentUserPremium,
|
||||||
|
isCustomEmojiPicker,
|
||||||
observeIntersection,
|
observeIntersection,
|
||||||
onStickerSelect,
|
onStickerSelect,
|
||||||
onStickerUnfave,
|
onStickerUnfave,
|
||||||
onStickerFave,
|
onStickerFave,
|
||||||
onStickerRemoveRecent,
|
onStickerRemoveRecent,
|
||||||
isCurrentUserPremium,
|
|
||||||
}) => {
|
}) => {
|
||||||
const { clearRecentStickers } = getActions();
|
const {
|
||||||
|
clearRecentStickers,
|
||||||
|
clearRecentCustomEmoji,
|
||||||
|
openPremiumModal,
|
||||||
|
toggleStickerSet,
|
||||||
|
} = getActions();
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useFlag();
|
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useFlag();
|
||||||
@ -70,14 +78,32 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
|
|
||||||
const transitionClassNames = useMediaTransition(shouldRender);
|
const transitionClassNames = useMediaTransition(shouldRender);
|
||||||
|
|
||||||
|
const isRecent = stickerSet.id === RECENT_SYMBOL_SET_ID;
|
||||||
const isEmoji = stickerSet.isEmoji;
|
const isEmoji = stickerSet.isEmoji;
|
||||||
|
const isPremiumSet = !isRecent && selectIsSetPremium(stickerSet);
|
||||||
|
|
||||||
const handleClearRecent = useCallback(() => {
|
const handleClearRecent = useCallback(() => {
|
||||||
clearRecentStickers();
|
if (isEmoji) {
|
||||||
|
clearRecentCustomEmoji();
|
||||||
|
} else {
|
||||||
|
clearRecentStickers();
|
||||||
|
}
|
||||||
closeConfirmModal();
|
closeConfirmModal();
|
||||||
}, [clearRecentStickers, closeConfirmModal]);
|
}, [clearRecentCustomEmoji, clearRecentStickers, closeConfirmModal, isEmoji]);
|
||||||
|
|
||||||
const isLocked = !isSavedMessages && isEmoji && !isCurrentUserPremium
|
const handleAddClick = useCallback(() => {
|
||||||
|
if (isPremiumSet && !isCurrentUserPremium) {
|
||||||
|
openPremiumModal({
|
||||||
|
initialSection: 'animated_emoji',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toggleStickerSet({
|
||||||
|
stickerSetId: stickerSet.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isCurrentUserPremium, isPremiumSet, openPremiumModal, stickerSet, toggleStickerSet]);
|
||||||
|
|
||||||
|
const isLocked = !isSavedMessages && !isRecent && isEmoji && !isCurrentUserPremium
|
||||||
&& stickerSet.stickers?.some((l) => !l.isFree);
|
&& stickerSet.stickers?.some((l) => !l.isFree);
|
||||||
const itemSize = isEmoji ? EMOJI_SIZE_PICKER : STICKER_SIZE_PICKER;
|
const itemSize = isEmoji ? EMOJI_SIZE_PICKER : STICKER_SIZE_PICKER;
|
||||||
const itemsPerRow = isEmoji ? EMOJI_PER_ROW_ON_DESKTOP : STICKERS_PER_ROW_ON_DESKTOP;
|
const itemsPerRow = isEmoji ? EMOJI_PER_ROW_ON_DESKTOP : STICKERS_PER_ROW_ON_DESKTOP;
|
||||||
@ -97,13 +123,11 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
favoriteStickers ? new Set(favoriteStickers.map(({ id }) => id)) : undefined
|
favoriteStickers ? new Set(favoriteStickers.map(({ id }) => id)) : undefined
|
||||||
), [favoriteStickers]);
|
), [favoriteStickers]);
|
||||||
|
|
||||||
const isRecent = stickerSet.id === RECENT_SYMBOL_SET_ID;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
key={stickerSet.id}
|
key={stickerSet.id}
|
||||||
id={`sticker-set-${index}`}
|
id={`${isCustomEmojiPicker ? 'custom-emoji-set' : 'sticker-set'}-${index}`}
|
||||||
className={
|
className={
|
||||||
buildClassName('symbol-set', isLocked && 'symbol-set-locked')
|
buildClassName('symbol-set', isLocked && 'symbol-set-locked')
|
||||||
}
|
}
|
||||||
@ -116,6 +140,18 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
{isRecent && (
|
{isRecent && (
|
||||||
<i className="symbol-set-remove icon-close" onClick={openConfirmModal} />
|
<i className="symbol-set-remove icon-close" onClick={openConfirmModal} />
|
||||||
)}
|
)}
|
||||||
|
{!isRecent && isEmoji && !stickerSet.installedDate && (
|
||||||
|
<Button
|
||||||
|
className="symbol-set-add-button"
|
||||||
|
withPremiumGradient={isPremiumSet && !isCurrentUserPremium}
|
||||||
|
onClick={handleAddClick}
|
||||||
|
pill
|
||||||
|
size="tiny"
|
||||||
|
fluid
|
||||||
|
>
|
||||||
|
{isPremiumSet && isLocked ? lang('Unlock') : lang('Add')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={buildClassName('symbol-set-container', transitionClassNames)}
|
className={buildClassName('symbol-set-container', transitionClassNames)}
|
||||||
@ -141,7 +177,7 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
isCurrentUserPremium={isCurrentUserPremium}
|
isCurrentUserPremium={isCurrentUserPremium}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{!isExpanded && stickerSet.count > itemsBeforeCutout - 1 && (
|
{!isExpanded && stickerSet.count > itemsBeforeCutout && (
|
||||||
<Button className="StickerButton custom-emoji set-expand" round color="translucent" onClick={expand}>
|
<Button className="StickerButton custom-emoji set-expand" round color="translucent" onClick={expand}>
|
||||||
+{stickerSet.count - itemsBeforeCutout + 1}
|
+{stickerSet.count - itemsBeforeCutout + 1}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -4,11 +4,12 @@ import React, { memo, useMemo, useRef } from '../../../lib/teact/teact';
|
|||||||
import type { ApiStickerSet } from '../../../api/types';
|
import type { ApiStickerSet } from '../../../api/types';
|
||||||
|
|
||||||
import { STICKER_SIZE_PICKER_HEADER } from '../../../config';
|
import { STICKER_SIZE_PICKER_HEADER } from '../../../config';
|
||||||
|
import { getFirstLetters } from '../../../util/textFormat';
|
||||||
|
|
||||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||||
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
||||||
import useMedia from '../../../hooks/useMedia';
|
import useMedia from '../../../hooks/useMedia';
|
||||||
import useMediaTransition from '../../../hooks/useMediaTransition';
|
import useMediaTransition from '../../../hooks/useMediaTransition';
|
||||||
import { getFirstLetters } from '../../../util/textFormat';
|
|
||||||
|
|
||||||
import AnimatedSticker from '../../common/AnimatedSticker';
|
import AnimatedSticker from '../../common/AnimatedSticker';
|
||||||
|
|
||||||
@ -46,6 +47,7 @@ const StickerSetCoverAnimated: FC<OwnProps> = ({
|
|||||||
size={size}
|
size={size}
|
||||||
tgsUrl={lottieData}
|
tgsUrl={lottieData}
|
||||||
className={transitionClassNames}
|
className={transitionClassNames}
|
||||||
|
play={isIntersecting}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -181,8 +181,8 @@
|
|||||||
&-header {
|
&-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
color: rgba(var(--color-text-secondary-rgb), 0.75);
|
color: rgba(var(--color-text-secondary-rgb), 0.75);
|
||||||
align-self: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&-name {
|
&-name {
|
||||||
@ -196,7 +196,6 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
unicode-bidi: plaintext;
|
unicode-bidi: plaintext;
|
||||||
flex-grow: 1;
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
background-color: var(--color-background);
|
background-color: var(--color-background);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import Button from '../../ui/Button';
|
|||||||
import Menu from '../../ui/Menu';
|
import Menu from '../../ui/Menu';
|
||||||
import Transition from '../../ui/Transition';
|
import Transition from '../../ui/Transition';
|
||||||
import EmojiPicker from './EmojiPicker';
|
import EmojiPicker from './EmojiPicker';
|
||||||
|
import CustomEmojiPicker from './CustomEmojiPicker';
|
||||||
import StickerPicker from './StickerPicker';
|
import StickerPicker from './StickerPicker';
|
||||||
import GifPicker from './GifPicker';
|
import GifPicker from './GifPicker';
|
||||||
import SymbolMenuFooter, { SYMBOL_MENU_TAB_TITLES, SymbolMenuTabs } from './SymbolMenuFooter';
|
import SymbolMenuFooter, { SYMBOL_MENU_TAB_TITLES, SymbolMenuTabs } from './SymbolMenuFooter';
|
||||||
@ -38,6 +39,7 @@ export type OwnProps = {
|
|||||||
onLoad: () => void;
|
onLoad: () => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onEmojiSelect: (emoji: string) => void;
|
onEmojiSelect: (emoji: string) => void;
|
||||||
|
onCustomEmojiSelect: (emoji: ApiSticker) => void;
|
||||||
onStickerSelect: (
|
onStickerSelect: (
|
||||||
sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean, shouldPreserveInput?: boolean
|
sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean, shouldPreserveInput?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
@ -45,11 +47,13 @@ export type OwnProps = {
|
|||||||
onRemoveSymbol: () => void;
|
onRemoveSymbol: () => void;
|
||||||
onSearchOpen: (type: 'stickers' | 'gifs') => void;
|
onSearchOpen: (type: 'stickers' | 'gifs') => void;
|
||||||
addRecentEmoji: GlobalActions['addRecentEmoji'];
|
addRecentEmoji: GlobalActions['addRecentEmoji'];
|
||||||
|
addRecentCustomEmoji: GlobalActions['addRecentCustomEmoji'];
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
isLeftColumnShown: boolean;
|
isLeftColumnShown: boolean;
|
||||||
isCurrentUserPremium?: boolean;
|
isCurrentUserPremium?: boolean;
|
||||||
|
lastSyncTime?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
let isActivated = false;
|
let isActivated = false;
|
||||||
@ -62,18 +66,22 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
|||||||
canSendGifs,
|
canSendGifs,
|
||||||
isLeftColumnShown,
|
isLeftColumnShown,
|
||||||
isCurrentUserPremium,
|
isCurrentUserPremium,
|
||||||
|
lastSyncTime,
|
||||||
onLoad,
|
onLoad,
|
||||||
onClose,
|
onClose,
|
||||||
onEmojiSelect,
|
onEmojiSelect,
|
||||||
|
onCustomEmojiSelect,
|
||||||
onStickerSelect,
|
onStickerSelect,
|
||||||
onGifSelect,
|
onGifSelect,
|
||||||
onRemoveSymbol,
|
onRemoveSymbol,
|
||||||
onSearchOpen,
|
onSearchOpen,
|
||||||
addRecentEmoji,
|
addRecentEmoji,
|
||||||
|
addRecentCustomEmoji,
|
||||||
}) => {
|
}) => {
|
||||||
const { loadPremiumSetStickers } = getActions();
|
const { loadPremiumSetStickers, loadFeaturedEmojiStickers } = getActions();
|
||||||
const [activeTab, setActiveTab] = useState<number>(0);
|
const [activeTab, setActiveTab] = useState<number>(0);
|
||||||
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
|
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
|
||||||
|
const [recentCustomEmojis, setRecentCustomEmojis] = useState<string[]>([]);
|
||||||
|
|
||||||
const [handleMouseEnter, handleMouseLeave] = useMouseInside(isOpen, onClose, undefined, IS_SINGLE_COLUMN_LAYOUT);
|
const [handleMouseEnter, handleMouseLeave] = useMouseInside(isOpen, onClose, undefined, IS_SINGLE_COLUMN_LAYOUT);
|
||||||
const { shouldRender, transitionClassNames } = useShowTransition(isOpen, onClose, false, false);
|
const { shouldRender, transitionClassNames } = useShowTransition(isOpen, onClose, false, false);
|
||||||
@ -87,10 +95,12 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
|||||||
}, [onLoad]);
|
}, [onLoad]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!lastSyncTime) return;
|
||||||
if (isCurrentUserPremium) {
|
if (isCurrentUserPremium) {
|
||||||
loadPremiumSetStickers();
|
loadPremiumSetStickers();
|
||||||
}
|
}
|
||||||
}, [isCurrentUserPremium, loadPremiumSetStickers]);
|
loadFeaturedEmojiStickers();
|
||||||
|
}, [isCurrentUserPremium, lastSyncTime, loadFeaturedEmojiStickers, loadPremiumSetStickers]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!IS_SINGLE_COLUMN_LAYOUT) {
|
if (!IS_SINGLE_COLUMN_LAYOUT) {
|
||||||
@ -134,6 +144,28 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
|||||||
onEmojiSelect(emoji);
|
onEmojiSelect(emoji);
|
||||||
}, [onEmojiSelect]);
|
}, [onEmojiSelect]);
|
||||||
|
|
||||||
|
const recentCustomEmojisRef = useRef(recentCustomEmojis);
|
||||||
|
recentCustomEmojisRef.current = recentCustomEmojis;
|
||||||
|
useEffect(() => {
|
||||||
|
if (!recentCustomEmojisRef.current.length || isOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
recentCustomEmojisRef.current.forEach((documentId) => {
|
||||||
|
addRecentCustomEmoji({
|
||||||
|
documentId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setRecentEmojis([]);
|
||||||
|
}, [isOpen, addRecentCustomEmoji]);
|
||||||
|
|
||||||
|
const handleCustomEmojiSelect = useCallback((emoji: ApiSticker) => {
|
||||||
|
setRecentCustomEmojis((ids) => [...ids, emoji.id]);
|
||||||
|
|
||||||
|
onCustomEmojiSelect(emoji);
|
||||||
|
}, [onCustomEmojiSelect]);
|
||||||
|
|
||||||
const handleSearch = useCallback((type: 'stickers' | 'gifs') => {
|
const handleSearch = useCallback((type: 'stickers' | 'gifs') => {
|
||||||
onClose();
|
onClose();
|
||||||
onSearchOpen(type);
|
onSearchOpen(type);
|
||||||
@ -154,6 +186,15 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
|||||||
onEmojiSelect={handleEmojiSelect}
|
onEmojiSelect={handleEmojiSelect}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case SymbolMenuTabs.CustomEmoji:
|
||||||
|
return (
|
||||||
|
<CustomEmojiPicker
|
||||||
|
className="picker-tab"
|
||||||
|
loadAndPlay={isOpen && (isActive || isFrom)}
|
||||||
|
onCustomEmojiSelect={handleCustomEmojiSelect}
|
||||||
|
chatId={chatId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case SymbolMenuTabs.Stickers:
|
case SymbolMenuTabs.Stickers:
|
||||||
return (
|
return (
|
||||||
<StickerPicker
|
<StickerPicker
|
||||||
@ -257,6 +298,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
return {
|
return {
|
||||||
isLeftColumnShown: global.isLeftColumnShown,
|
isLeftColumnShown: global.isLeftColumnShown,
|
||||||
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||||
|
lastSyncTime: global.lastSyncTime,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(SymbolMenu));
|
)(SymbolMenu));
|
||||||
|
|||||||
@ -14,18 +14,21 @@ type OwnProps = {
|
|||||||
|
|
||||||
export enum SymbolMenuTabs {
|
export enum SymbolMenuTabs {
|
||||||
'Emoji',
|
'Emoji',
|
||||||
|
'CustomEmoji',
|
||||||
'Stickers',
|
'Stickers',
|
||||||
'GIFs',
|
'GIFs',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SYMBOL_MENU_TAB_TITLES: Record<SymbolMenuTabs, string> = {
|
export const SYMBOL_MENU_TAB_TITLES: Record<SymbolMenuTabs, string> = {
|
||||||
[SymbolMenuTabs.Emoji]: 'Emoji',
|
[SymbolMenuTabs.Emoji]: 'Emoji',
|
||||||
|
[SymbolMenuTabs.CustomEmoji]: 'StickersList.EmojiItem',
|
||||||
[SymbolMenuTabs.Stickers]: 'AccDescrStickers',
|
[SymbolMenuTabs.Stickers]: 'AccDescrStickers',
|
||||||
[SymbolMenuTabs.GIFs]: 'GifsTab',
|
[SymbolMenuTabs.GIFs]: 'GifsTab',
|
||||||
};
|
};
|
||||||
|
|
||||||
const SYMBOL_MENU_TAB_ICONS = {
|
const SYMBOL_MENU_TAB_ICONS = {
|
||||||
[SymbolMenuTabs.Emoji]: 'icon-smile',
|
[SymbolMenuTabs.Emoji]: 'icon-smile',
|
||||||
|
[SymbolMenuTabs.CustomEmoji]: 'icon-favorite',
|
||||||
[SymbolMenuTabs.Stickers]: 'icon-stickers',
|
[SymbolMenuTabs.Stickers]: 'icon-stickers',
|
||||||
[SymbolMenuTabs.GIFs]: 'icon-gifs',
|
[SymbolMenuTabs.GIFs]: 'icon-gifs',
|
||||||
};
|
};
|
||||||
@ -61,7 +64,7 @@ const SymbolMenuFooter: FC<OwnProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="SymbolMenu-footer" onClick={stopPropagation} dir={lang.isRtl ? 'rtl' : undefined}>
|
<div className="SymbolMenu-footer" onClick={stopPropagation} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
{activeTab !== SymbolMenuTabs.Emoji && (
|
{activeTab !== SymbolMenuTabs.Emoji && activeTab !== SymbolMenuTabs.CustomEmoji && (
|
||||||
<Button
|
<Button
|
||||||
className="symbol-search-button"
|
className="symbol-search-button"
|
||||||
ariaLabel={activeTab === SymbolMenuTabs.Stickers ? 'Search Stickers' : 'Search GIFs'}
|
ariaLabel={activeTab === SymbolMenuTabs.Stickers ? 'Search Stickers' : 'Search GIFs'}
|
||||||
@ -75,6 +78,7 @@ const SymbolMenuFooter: FC<OwnProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{renderTabButton(SymbolMenuTabs.Emoji)}
|
{renderTabButton(SymbolMenuTabs.Emoji)}
|
||||||
|
{renderTabButton(SymbolMenuTabs.CustomEmoji)}
|
||||||
{renderTabButton(SymbolMenuTabs.Stickers)}
|
{renderTabButton(SymbolMenuTabs.Stickers)}
|
||||||
{renderTabButton(SymbolMenuTabs.GIFs)}
|
{renderTabButton(SymbolMenuTabs.GIFs)}
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import buildClassName from '../../../util/buildClassName';
|
|||||||
import { ensureProtocol } from '../../../util/ensureProtocol';
|
import { ensureProtocol } from '../../../util/ensureProtocol';
|
||||||
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||||
import getKeyFromEvent from '../../../util/getKeyFromEvent';
|
import getKeyFromEvent from '../../../util/getKeyFromEvent';
|
||||||
|
import { INPUT_CUSTOM_EMOJI_SELECTOR } from './helpers/customEmoji';
|
||||||
|
|
||||||
import useShowTransition from '../../../hooks/useShowTransition';
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
import useVirtualBackdrop from '../../../hooks/useVirtualBackdrop';
|
import useVirtualBackdrop from '../../../hooks/useVirtualBackdrop';
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
@ -129,11 +131,16 @@ const TextFormatter: FC<OwnProps> = ({
|
|||||||
}
|
}
|
||||||
}, [setSelectedRange]);
|
}, [setSelectedRange]);
|
||||||
|
|
||||||
const getSelectedText = useCallback(() => {
|
const getSelectedText = useCallback((shouldDropCustomEmoji?: boolean) => {
|
||||||
if (!selectedRange) {
|
if (!selectedRange) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
fragmentEl.replaceChildren(selectedRange.cloneContents());
|
fragmentEl.replaceChildren(selectedRange.cloneContents());
|
||||||
|
if (shouldDropCustomEmoji) {
|
||||||
|
fragmentEl.querySelectorAll(INPUT_CUSTOM_EMOJI_SELECTOR).forEach((el) => {
|
||||||
|
el.replaceWith(el.getAttribute('alt')!);
|
||||||
|
});
|
||||||
|
}
|
||||||
return fragmentEl.innerHTML;
|
return fragmentEl.innerHTML;
|
||||||
}, [selectedRange]);
|
}, [selectedRange]);
|
||||||
|
|
||||||
@ -304,7 +311,7 @@ const TextFormatter: FC<OwnProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = getSelectedText();
|
const text = getSelectedText(true);
|
||||||
document.execCommand('insertHTML', false, `<code class="text-entity-code" dir="auto">${text}</code>`);
|
document.execCommand('insertHTML', false, `<code class="text-entity-code" dir="auto">${text}</code>`);
|
||||||
onClose();
|
onClose();
|
||||||
}, [
|
}, [
|
||||||
@ -327,7 +334,7 @@ const TextFormatter: FC<OwnProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = getSelectedText();
|
const text = getSelectedText(true);
|
||||||
restoreSelection();
|
restoreSelection();
|
||||||
document.execCommand(
|
document.execCommand(
|
||||||
'insertHTML',
|
'insertHTML',
|
||||||
|
|||||||
26
src/components/middle/composer/helpers/customEmoji.ts
Normal file
26
src/components/middle/composer/helpers/customEmoji.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import type { ApiMessageEntityCustomEmoji, ApiSticker } from '../../../../api/types';
|
||||||
|
import { getCustomEmojiPreviewMediaData } from '../../../../util/customEmojiManager';
|
||||||
|
|
||||||
|
export const INPUT_CUSTOM_EMOJI_SELECTOR = 'img[data-document-id]';
|
||||||
|
|
||||||
|
export function buildCustomEmojiHtml(emoji: ApiSticker) {
|
||||||
|
const mediaData = getCustomEmojiPreviewMediaData(emoji.id);
|
||||||
|
const src = mediaData && `src="${mediaData}"`;
|
||||||
|
return `<img
|
||||||
|
class="custom-emoji emoji emoji-small"
|
||||||
|
draggable="false"
|
||||||
|
alt="${emoji.emoji}"
|
||||||
|
data-document-id="${emoji.id}"
|
||||||
|
${src} />`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCustomEmojiHtmlFromEntity(rawText: string, entity: ApiMessageEntityCustomEmoji) {
|
||||||
|
const mediaData = getCustomEmojiPreviewMediaData(entity.documentId);
|
||||||
|
const src = mediaData && `src="${mediaData}"`;
|
||||||
|
return `<img
|
||||||
|
class="custom-emoji emoji emoji-small"
|
||||||
|
draggable="false"
|
||||||
|
alt="${rawText}"
|
||||||
|
data-document-id="${entity.documentId}"
|
||||||
|
${src} />`;
|
||||||
|
}
|
||||||
@ -0,0 +1,90 @@
|
|||||||
|
import { useCallback, useEffect, useState } from '../../../../lib/teact/teact';
|
||||||
|
import { getActions } from '../../../../global';
|
||||||
|
|
||||||
|
import type { ApiSticker } from '../../../../api/types';
|
||||||
|
|
||||||
|
import { EMOJI_IMG_REGEX } from '../../../../config';
|
||||||
|
import { IS_EMOJI_SUPPORTED } from '../../../../util/environment';
|
||||||
|
import { getHtmlBeforeSelection } from '../../../../util/selection';
|
||||||
|
import focusEditableElement from '../../../../util/focusEditableElement';
|
||||||
|
import twemojiRegex from '../../../../lib/twemojiRegex';
|
||||||
|
import { buildCustomEmojiHtml } from '../helpers/customEmoji';
|
||||||
|
|
||||||
|
import useOnSelectionChange from '../../../../hooks/useOnSelectionChange';
|
||||||
|
import useCacheBuster from '../../../../hooks/useCacheBuster';
|
||||||
|
|
||||||
|
const RE_ENDS_ON_EMOJI = new RegExp(`(${twemojiRegex.source})$`, 'g');
|
||||||
|
const ENDS_ON_EMOJI_IMG_REGEX = new RegExp(`${EMOJI_IMG_REGEX.source}$`, 'g');
|
||||||
|
|
||||||
|
export default function useCustomEmojiTooltip(
|
||||||
|
isAllowed: boolean,
|
||||||
|
inputSelector: string,
|
||||||
|
html: string,
|
||||||
|
onUpdateHtml: (html: string) => void,
|
||||||
|
stickers?: ApiSticker[],
|
||||||
|
isDisabled = false,
|
||||||
|
) {
|
||||||
|
const { loadCustomEmojiForEmoji, clearCustomEmojiForEmoji } = getActions();
|
||||||
|
|
||||||
|
const [htmlBeforeSelection, setHtmlBeforeSelection] = useState('');
|
||||||
|
|
||||||
|
const [cacheBuster, updateCacheBuster] = useCacheBuster();
|
||||||
|
|
||||||
|
const handleSelectionChange = useCallback((range: Range) => {
|
||||||
|
if (range.collapsed) {
|
||||||
|
updateCacheBuster(); // Update tooltip on cursor move
|
||||||
|
}
|
||||||
|
}, [updateCacheBuster]);
|
||||||
|
|
||||||
|
useOnSelectionChange(document.querySelector<HTMLDivElement>(inputSelector), handleSelectionChange);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!html) {
|
||||||
|
setHtmlBeforeSelection('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setHtmlBeforeSelection(getHtmlBeforeSelection(document.querySelector<HTMLDivElement>(inputSelector)!));
|
||||||
|
}, [html, inputSelector, cacheBuster]);
|
||||||
|
|
||||||
|
const lastEmojiText = htmlBeforeSelection.match(IS_EMOJI_SUPPORTED ? RE_ENDS_ON_EMOJI : ENDS_ON_EMOJI_IMG_REGEX)?.[0];
|
||||||
|
const hasStickers = Boolean(stickers?.length && lastEmojiText);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDisabled) return;
|
||||||
|
|
||||||
|
if (isAllowed && lastEmojiText) {
|
||||||
|
loadCustomEmojiForEmoji({
|
||||||
|
emoji: IS_EMOJI_SUPPORTED ? lastEmojiText : lastEmojiText.match(/.+alt="(.+)"/)?.[1]!,
|
||||||
|
});
|
||||||
|
} else if (hasStickers || !lastEmojiText) {
|
||||||
|
clearCustomEmojiForEmoji();
|
||||||
|
}
|
||||||
|
// We omit `hasStickers` here to prevent re-fetching after manually closing tooltip (via <Esc>).
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [lastEmojiText, clearCustomEmojiForEmoji, loadCustomEmojiForEmoji, isAllowed, isDisabled]);
|
||||||
|
|
||||||
|
const insertCustomEmoji = useCallback((emoji: ApiSticker) => {
|
||||||
|
if (!lastEmojiText) return;
|
||||||
|
const containerEl = document.querySelector<HTMLDivElement>(inputSelector)!;
|
||||||
|
const regexText = IS_EMOJI_SUPPORTED ? lastEmojiText
|
||||||
|
// Escape regexp special chars
|
||||||
|
: lastEmojiText.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||||
|
const regex = new RegExp(`(${regexText})\\1*$`, '');
|
||||||
|
const matched = htmlBeforeSelection.match(regex)![0];
|
||||||
|
const count = matched.length / lastEmojiText.length;
|
||||||
|
|
||||||
|
const newHtml = htmlBeforeSelection.replace(regex, buildCustomEmojiHtml(emoji).repeat(count));
|
||||||
|
const htmlAfterSelection = containerEl.innerHTML.substring(htmlBeforeSelection.length);
|
||||||
|
onUpdateHtml(`${newHtml}${htmlAfterSelection}`);
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
focusEditableElement(containerEl, true, true);
|
||||||
|
});
|
||||||
|
}, [htmlBeforeSelection, inputSelector, lastEmojiText, onUpdateHtml]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isCustomEmojiTooltipOpen: hasStickers,
|
||||||
|
closeCustomEmojiTooltip: clearCustomEmojiForEmoji,
|
||||||
|
insertCustomEmoji,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo } from '../../../../lib/teact/teact';
|
|||||||
import { getActions } from '../../../../global';
|
import { getActions } from '../../../../global';
|
||||||
|
|
||||||
import type { ApiFormattedText, ApiMessage } from '../../../../api/types';
|
import type { ApiFormattedText, ApiMessage } from '../../../../api/types';
|
||||||
|
import { ApiMessageEntityTypes } from '../../../../api/types';
|
||||||
|
|
||||||
import { DRAFT_DEBOUNCE, EDITABLE_INPUT_CSS_SELECTOR } from '../../../../config';
|
import { DRAFT_DEBOUNCE, EDITABLE_INPUT_CSS_SELECTOR } from '../../../../config';
|
||||||
import usePrevious from '../../../../hooks/usePrevious';
|
import usePrevious from '../../../../hooks/usePrevious';
|
||||||
@ -26,7 +27,7 @@ const useDraft = (
|
|||||||
editedMessage: ApiMessage | undefined,
|
editedMessage: ApiMessage | undefined,
|
||||||
lastSyncTime?: number,
|
lastSyncTime?: number,
|
||||||
) => {
|
) => {
|
||||||
const { saveDraft, clearDraft } = getActions();
|
const { saveDraft, clearDraft, loadCustomEmojis } = getActions();
|
||||||
const prevDraft = usePrevious(draft);
|
const prevDraft = usePrevious(draft);
|
||||||
|
|
||||||
const updateDraft = useCallback((draftChatId: string, draftThreadId: number) => {
|
const updateDraft = useCallback((draftChatId: string, draftThreadId: number) => {
|
||||||
@ -73,6 +74,11 @@ const useDraft = (
|
|||||||
|
|
||||||
setHtml(getTextWithEntitiesAsHtml(draft));
|
setHtml(getTextWithEntitiesAsHtml(draft));
|
||||||
|
|
||||||
|
const customEmojiIds = draft.entities
|
||||||
|
?.map((entity) => entity.type === ApiMessageEntityTypes.CustomEmoji && entity.documentId)
|
||||||
|
.filter(Boolean) || [];
|
||||||
|
if (customEmojiIds.length) loadCustomEmojis({ ids: customEmojiIds });
|
||||||
|
|
||||||
if (!IS_TOUCH_ENV) {
|
if (!IS_TOUCH_ENV) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const messageInput = document.querySelector<HTMLDivElement>(EDITABLE_INPUT_CSS_SELECTOR);
|
const messageInput = document.querySelector<HTMLDivElement>(EDITABLE_INPUT_CSS_SELECTOR);
|
||||||
@ -81,7 +87,9 @@ const useDraft = (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [chatId, threadId, draft, setHtml, updateDraft, prevChatId, prevThreadId, editedMessage, prevDraft]);
|
}, [
|
||||||
|
chatId, threadId, draft, setHtml, updateDraft, prevChatId, prevThreadId, editedMessage, prevDraft, loadCustomEmojis,
|
||||||
|
]);
|
||||||
|
|
||||||
const html = htmlRef.current;
|
const html = htmlRef.current;
|
||||||
// Update draft when input changes
|
// Update draft when input changes
|
||||||
|
|||||||
@ -1,19 +1,26 @@
|
|||||||
import {
|
import {
|
||||||
useCallback, useEffect, useState,
|
useCallback, useEffect, useState,
|
||||||
} from '../../../../lib/teact/teact';
|
} from '../../../../lib/teact/teact';
|
||||||
|
import { getGlobal } from '../../../../global';
|
||||||
|
|
||||||
|
import type { ApiSticker } from '../../../../api/types';
|
||||||
|
import type { EmojiData, EmojiModule, EmojiRawData } from '../../../../util/emoji';
|
||||||
|
|
||||||
import { EDITABLE_INPUT_CSS_SELECTOR, EDITABLE_INPUT_ID } from '../../../../config';
|
import { EDITABLE_INPUT_CSS_SELECTOR, EDITABLE_INPUT_ID } from '../../../../config';
|
||||||
|
import {
|
||||||
|
buildCollectionByKey, mapValues, pickTruthy, unique, uniqueByField,
|
||||||
|
} from '../../../../util/iteratees';
|
||||||
import { MEMO_EMPTY_ARRAY } from '../../../../util/memo';
|
import { MEMO_EMPTY_ARRAY } from '../../../../util/memo';
|
||||||
import { prepareForRegExp } from '../helpers/prepareForRegExp';
|
import { prepareForRegExp } from '../helpers/prepareForRegExp';
|
||||||
import type { EmojiData, EmojiModule, EmojiRawData } from '../../../../util/emoji';
|
|
||||||
import { uncompressEmoji } from '../../../../util/emoji';
|
import { uncompressEmoji } from '../../../../util/emoji';
|
||||||
import focusEditableElement from '../../../../util/focusEditableElement';
|
import focusEditableElement from '../../../../util/focusEditableElement';
|
||||||
import {
|
|
||||||
buildCollectionByKey, mapValues, pickTruthy, unique,
|
|
||||||
} from '../../../../util/iteratees';
|
|
||||||
import memoized from '../../../../util/memoized';
|
import memoized from '../../../../util/memoized';
|
||||||
import useFlag from '../../../../hooks/useFlag';
|
|
||||||
import renderText from '../../../common/helpers/renderText';
|
import renderText from '../../../common/helpers/renderText';
|
||||||
|
import { selectCustomEmojiForEmojis } from '../../../../global/selectors';
|
||||||
|
import { buildCustomEmojiHtml } from '../helpers/customEmoji';
|
||||||
|
|
||||||
|
import useFlag from '../../../../hooks/useFlag';
|
||||||
|
import useDebouncedCallback from '../../../../hooks/useDebouncedCallback';
|
||||||
|
|
||||||
interface Library {
|
interface Library {
|
||||||
keywords: string[];
|
keywords: string[];
|
||||||
@ -30,6 +37,8 @@ let RE_EMOJI_SEARCH: RegExp;
|
|||||||
const EMOJIS_LIMIT = 36;
|
const EMOJIS_LIMIT = 36;
|
||||||
const FILTER_MIN_LENGTH = 2;
|
const FILTER_MIN_LENGTH = 2;
|
||||||
|
|
||||||
|
const DEBOUNCE = 300;
|
||||||
|
|
||||||
const prepareRecentEmojisMemo = memoized(prepareRecentEmojis);
|
const prepareRecentEmojisMemo = memoized(prepareRecentEmojis);
|
||||||
const prepareLibraryMemo = memoized(prepareLibrary);
|
const prepareLibraryMemo = memoized(prepareLibrary);
|
||||||
const searchInLibraryMemo = memoized(searchInLibrary);
|
const searchInLibraryMemo = memoized(searchInLibrary);
|
||||||
@ -54,7 +63,12 @@ export default function useEmojiTooltip(
|
|||||||
const [isOpen, markIsOpen, unmarkIsOpen] = useFlag();
|
const [isOpen, markIsOpen, unmarkIsOpen] = useFlag();
|
||||||
const [byId, setById] = useState<Record<string, Emoji> | undefined>();
|
const [byId, setById] = useState<Record<string, Emoji> | undefined>();
|
||||||
const [shouldForceInsertEmoji, setShouldForceInsertEmoji] = useState(false);
|
const [shouldForceInsertEmoji, setShouldForceInsertEmoji] = useState(false);
|
||||||
const [filteredEmojis, setFilteredEmojis] = useState<Emoji[]>(MEMO_EMPTY_ARRAY);
|
const [filteredEmojis, setFilteredEmojisInner] = useState<Emoji[]>(MEMO_EMPTY_ARRAY);
|
||||||
|
const [filteredCustomEmojis, setFilteredCustomEmojis] = useState<ApiSticker[]>(MEMO_EMPTY_ARRAY);
|
||||||
|
|
||||||
|
const setFilteredEmojis = useDebouncedCallback((emojis: Emoji[]) => {
|
||||||
|
setFilteredEmojisInner(emojis);
|
||||||
|
}, [], DEBOUNCE);
|
||||||
|
|
||||||
// Initialize data on first render.
|
// Initialize data on first render.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -72,6 +86,15 @@ export default function useEmojiTooltip(
|
|||||||
}, [isDisabled]);
|
}, [isDisabled]);
|
||||||
|
|
||||||
const html = htmlRef.current;
|
const html = htmlRef.current;
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDisabled) return;
|
||||||
|
const customEmojis = uniqueByField(
|
||||||
|
selectCustomEmojiForEmojis(getGlobal(), filteredEmojis.map((emoji) => emoji.native)),
|
||||||
|
'id',
|
||||||
|
);
|
||||||
|
setFilteredCustomEmojis(customEmojis);
|
||||||
|
}, [filteredEmojis, isDisabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAllowed || !html || !byId || isDisabled) {
|
if (!isAllowed || !html || !byId || isDisabled) {
|
||||||
unmarkIsOpen();
|
unmarkIsOpen();
|
||||||
@ -108,7 +131,7 @@ export default function useEmojiTooltip(
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
byId, html, isAllowed, markIsOpen, recentEmojiIds, unmarkIsOpen, setShouldForceInsertEmoji,
|
byId, html, isAllowed, markIsOpen, recentEmojiIds, unmarkIsOpen, setShouldForceInsertEmoji,
|
||||||
isDisabled, baseEmojiKeywords, emojiKeywords,
|
isDisabled, baseEmojiKeywords, emojiKeywords, setFilteredEmojis,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const insertEmoji = useCallback((textEmoji: string, isForce?: boolean) => {
|
const insertEmoji = useCallback((textEmoji: string, isForce?: boolean) => {
|
||||||
@ -130,6 +153,25 @@ export default function useEmojiTooltip(
|
|||||||
unmarkIsOpen();
|
unmarkIsOpen();
|
||||||
}, [htmlRef, inputId, onUpdateHtml, unmarkIsOpen]);
|
}, [htmlRef, inputId, onUpdateHtml, unmarkIsOpen]);
|
||||||
|
|
||||||
|
const insertCustomEmoji = useCallback((emoji: ApiSticker, isForce?: boolean) => {
|
||||||
|
const currentHtml = htmlRef.current;
|
||||||
|
const atIndex = currentHtml.lastIndexOf(':', isForce ? currentHtml.lastIndexOf(':') - 1 : undefined);
|
||||||
|
if (atIndex !== -1) {
|
||||||
|
onUpdateHtml(`${currentHtml.substr(0, atIndex)}${buildCustomEmojiHtml(emoji)}`);
|
||||||
|
let messageInput: HTMLDivElement;
|
||||||
|
if (inputId === EDITABLE_INPUT_ID) {
|
||||||
|
messageInput = document.querySelector<HTMLDivElement>(EDITABLE_INPUT_CSS_SELECTOR)!;
|
||||||
|
} else {
|
||||||
|
messageInput = document.getElementById(inputId) as HTMLDivElement;
|
||||||
|
}
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
focusEditableElement(messageInput, true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
unmarkIsOpen();
|
||||||
|
}, [htmlRef, inputId, onUpdateHtml, unmarkIsOpen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen && shouldForceInsertEmoji && filteredEmojis.length) {
|
if (isOpen && shouldForceInsertEmoji && filteredEmojis.length) {
|
||||||
insertEmoji(filteredEmojis[0].native, true);
|
insertEmoji(filteredEmojis[0].native, true);
|
||||||
@ -140,7 +182,9 @@ export default function useEmojiTooltip(
|
|||||||
isEmojiTooltipOpen: isOpen,
|
isEmojiTooltipOpen: isOpen,
|
||||||
closeEmojiTooltip: unmarkIsOpen,
|
closeEmojiTooltip: unmarkIsOpen,
|
||||||
filteredEmojis,
|
filteredEmojis,
|
||||||
|
filteredCustomEmojis,
|
||||||
insertEmoji,
|
insertEmoji,
|
||||||
|
insertCustomEmoji,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,10 +9,11 @@ import { EDITABLE_INPUT_ID } from '../../../../config';
|
|||||||
import { filterUsersByName, getUserFirstOrLastName } from '../../../../global/helpers';
|
import { filterUsersByName, getUserFirstOrLastName } from '../../../../global/helpers';
|
||||||
import { prepareForRegExp } from '../helpers/prepareForRegExp';
|
import { prepareForRegExp } from '../helpers/prepareForRegExp';
|
||||||
import focusEditableElement from '../../../../util/focusEditableElement';
|
import focusEditableElement from '../../../../util/focusEditableElement';
|
||||||
import useFlag from '../../../../hooks/useFlag';
|
|
||||||
import { pickTruthy, unique } from '../../../../util/iteratees';
|
import { pickTruthy, unique } from '../../../../util/iteratees';
|
||||||
import { throttle } from '../../../../util/schedulers';
|
import { throttle } from '../../../../util/schedulers';
|
||||||
|
|
||||||
|
import useFlag from '../../../../hooks/useFlag';
|
||||||
|
|
||||||
const runThrottled = throttle((cb) => cb(), 500, true);
|
const runThrottled = throttle((cb) => cb(), 500, true);
|
||||||
let RE_USERNAME_SEARCH: RegExp;
|
let RE_USERNAME_SEARCH: RegExp;
|
||||||
|
|
||||||
|
|||||||
@ -3,11 +3,13 @@ import { getActions } from '../../../../global';
|
|||||||
|
|
||||||
import type { ApiSticker } from '../../../../api/types';
|
import type { ApiSticker } from '../../../../api/types';
|
||||||
|
|
||||||
|
import { EMOJI_IMG_REGEX } from '../../../../config';
|
||||||
import { IS_EMOJI_SUPPORTED } from '../../../../util/environment';
|
import { IS_EMOJI_SUPPORTED } from '../../../../util/environment';
|
||||||
|
|
||||||
import parseEmojiOnlyString from '../../../common/helpers/parseEmojiOnlyString';
|
import parseEmojiOnlyString from '../../../common/helpers/parseEmojiOnlyString';
|
||||||
import { prepareForRegExp } from '../helpers/prepareForRegExp';
|
import { prepareForRegExp } from '../helpers/prepareForRegExp';
|
||||||
|
|
||||||
|
const STARTS_ENDS_ON_EMOJI_IMG_REGEX = new RegExp(`^${EMOJI_IMG_REGEX.source}$`, 'g');
|
||||||
|
|
||||||
export default function useStickerTooltip(
|
export default function useStickerTooltip(
|
||||||
isAllowed: boolean,
|
isAllowed: boolean,
|
||||||
html: string,
|
html: string,
|
||||||
@ -18,7 +20,7 @@ export default function useStickerTooltip(
|
|||||||
const { loadStickersForEmoji, clearStickersForEmoji } = getActions();
|
const { loadStickersForEmoji, clearStickersForEmoji } = getActions();
|
||||||
const isSingleEmoji = (
|
const isSingleEmoji = (
|
||||||
(IS_EMOJI_SUPPORTED && parseEmojiOnlyString(cleanHtml) === 1)
|
(IS_EMOJI_SUPPORTED && parseEmojiOnlyString(cleanHtml) === 1)
|
||||||
|| (!IS_EMOJI_SUPPORTED && Boolean(html.match(/^<img.[^>]*?>$/g)))
|
|| (!IS_EMOJI_SUPPORTED && Boolean(html.match(STARTS_ENDS_ON_EMOJI_IMG_REGEX)))
|
||||||
);
|
);
|
||||||
const hasStickers = Boolean(stickers?.length) && isSingleEmoji;
|
const hasStickers = Boolean(stickers?.length) && isSingleEmoji;
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import type { ApiMessage } from '../../../api/types';
|
|||||||
import { ApiMediaFormat } from '../../../api/types';
|
import { ApiMediaFormat } from '../../../api/types';
|
||||||
|
|
||||||
import { getStickerDimensions } from '../../common/helpers/mediaDimensions';
|
import { getStickerDimensions } from '../../common/helpers/mediaDimensions';
|
||||||
import { getMessageMediaFormat, getMessageMediaHash } from '../../../global/helpers';
|
import { getMessageMediaFormat, getMessageMediaHash, getStickerPreviewHash } from '../../../global/helpers';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { IS_WEBM_SUPPORTED } from '../../../util/environment';
|
import { IS_WEBM_SUPPORTED } from '../../../util/environment';
|
||||||
import { getActions } from '../../../global';
|
import { getActions } from '../../../global';
|
||||||
@ -63,7 +63,7 @@ const Sticker: FC<OwnProps> = ({
|
|||||||
const mediaHashEffect = `sticker${sticker.id}?size=f`;
|
const mediaHashEffect = `sticker${sticker.id}?size=f`;
|
||||||
|
|
||||||
const previewMediaHash = isVideo && !canDisplayVideo && (
|
const previewMediaHash = isVideo && !canDisplayVideo && (
|
||||||
sticker.isPreloadedGlobally ? `sticker${sticker.id}?size=m` : getMessageMediaHash(message, 'pictogram'));
|
sticker.isPreloadedGlobally ? getStickerPreviewHash(sticker.id) : getMessageMediaHash(message, 'pictogram'));
|
||||||
const previewBlobUrl = useMedia(previewMediaHash);
|
const previewBlobUrl = useMedia(previewMediaHash);
|
||||||
const thumbDataUri = useThumbnail(sticker);
|
const thumbDataUri = useThumbnail(sticker);
|
||||||
const previewUrl = previewBlobUrl || thumbDataUri;
|
const previewUrl = previewBlobUrl || thumbDataUri;
|
||||||
|
|||||||
@ -40,6 +40,8 @@ export const MEDIA_CACHE_NAME = 'tt-media';
|
|||||||
export const MEDIA_CACHE_NAME_AVATARS = 'tt-media-avatars';
|
export const MEDIA_CACHE_NAME_AVATARS = 'tt-media-avatars';
|
||||||
export const MEDIA_PROGRESSIVE_CACHE_DISABLED = false;
|
export const MEDIA_PROGRESSIVE_CACHE_DISABLED = false;
|
||||||
export const MEDIA_PROGRESSIVE_CACHE_NAME = 'tt-media-progressive';
|
export const MEDIA_PROGRESSIVE_CACHE_NAME = 'tt-media-progressive';
|
||||||
|
export const CUSTOM_EMOJI_PREVIEW_CACHE_DISABLED = false;
|
||||||
|
export const CUSTOM_EMOJI_PREVIEW_CACHE_NAME = 'tt-custom-emoji-preview';
|
||||||
export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB
|
export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB
|
||||||
export const CUSTOM_BG_CACHE_NAME = 'tt-custom-bg';
|
export const CUSTOM_BG_CACHE_NAME = 'tt-custom-bg';
|
||||||
export const LANG_CACHE_NAME = 'tt-lang-packs-v14';
|
export const LANG_CACHE_NAME = 'tt-lang-packs-v14';
|
||||||
@ -155,6 +157,7 @@ export const RECENT_SYMBOL_SET_ID = 'recent';
|
|||||||
export const FAVORITE_SYMBOL_SET_ID = 'favorite';
|
export const FAVORITE_SYMBOL_SET_ID = 'favorite';
|
||||||
export const CHAT_STICKER_SET_ID = 'chatStickers';
|
export const CHAT_STICKER_SET_ID = 'chatStickers';
|
||||||
export const PREMIUM_STICKER_SET_ID = 'premium';
|
export const PREMIUM_STICKER_SET_ID = 'premium';
|
||||||
|
export const EMOJI_IMG_REGEX = /<img[^>]+alt="([^"]+)"(?![^>]*data-document-id)[^>]*>/gm;
|
||||||
|
|
||||||
export const BASE_EMOJI_KEYWORD_LANG = 'en';
|
export const BASE_EMOJI_KEYWORD_LANG = 'en';
|
||||||
|
|
||||||
|
|||||||
@ -13,12 +13,14 @@ import {
|
|||||||
updateGifSearch,
|
updateGifSearch,
|
||||||
updateStickersForEmoji,
|
updateStickersForEmoji,
|
||||||
rebuildStickersForEmoji,
|
rebuildStickersForEmoji,
|
||||||
|
updateCustomEmojiForEmoji,
|
||||||
} from '../../reducers';
|
} from '../../reducers';
|
||||||
import searchWords from '../../../util/searchWords';
|
import searchWords from '../../../util/searchWords';
|
||||||
import { selectIsCurrentUserPremium, selectStickerSet } from '../../selectors';
|
import { selectIsCurrentUserPremium, selectStickerSet } from '../../selectors';
|
||||||
import { getTranslation } from '../../../util/langProvider';
|
import { getTranslation } from '../../../util/langProvider';
|
||||||
import { selectCurrentLimit, selectPremiumLimit } from '../../selectors/limits';
|
import { selectCurrentLimit, selectPremiumLimit } from '../../selectors/limits';
|
||||||
import * as langProvider from '../../../util/langProvider';
|
import * as langProvider from '../../../util/langProvider';
|
||||||
|
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||||
|
|
||||||
const ADDED_SETS_THROTTLE = 200;
|
const ADDED_SETS_THROTTLE = 200;
|
||||||
const ADDED_SETS_THROTTLE_CHUNK = 10;
|
const ADDED_SETS_THROTTLE_CHUNK = 10;
|
||||||
@ -548,6 +550,49 @@ addActionHandler('clearStickersForEmoji', (global) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('loadCustomEmojiForEmoji', (global, actions, payload) => {
|
||||||
|
const { emoji } = payload;
|
||||||
|
|
||||||
|
return updateCustomEmojiForEmoji(global, emoji);
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('clearCustomEmojiForEmoji', (global) => {
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
customEmojis: {
|
||||||
|
...global.customEmojis,
|
||||||
|
forEmoji: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('loadFeaturedEmojiStickers', async (global) => {
|
||||||
|
const featuredStickers = await callApi('fetchFeaturedEmojiStickers');
|
||||||
|
if (!featuredStickers) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global = getGlobal();
|
||||||
|
setGlobal({
|
||||||
|
...global,
|
||||||
|
customEmojis: {
|
||||||
|
...global.customEmojis,
|
||||||
|
featuredIds: featuredStickers.sets.map(({ id }) => id),
|
||||||
|
byId: {
|
||||||
|
...global.customEmojis.byId,
|
||||||
|
...buildCollectionByKey(featuredStickers.sets.flatMap((set) => set.stickers || []), 'id'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stickers: {
|
||||||
|
...global.stickers,
|
||||||
|
setsById: {
|
||||||
|
...global.stickers.setsById,
|
||||||
|
...buildCollectionByKey(featuredStickers.sets, 'id'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
addActionHandler('openStickerSet', async (global, actions, payload) => {
|
addActionHandler('openStickerSet', async (global, actions, payload) => {
|
||||||
const { stickerSetInfo } = payload;
|
const { stickerSetInfo } = payload;
|
||||||
if (!selectStickerSet(global, stickerSetInfo)) {
|
if (!selectStickerSet(global, stickerSetInfo)) {
|
||||||
|
|||||||
@ -192,6 +192,35 @@ addActionHandler('addRecentSticker', (global, action, payload) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('addRecentCustomEmoji', (global, action, payload) => {
|
||||||
|
const { documentId } = payload;
|
||||||
|
const { recentCustomEmojis } = global;
|
||||||
|
if (!recentCustomEmojis) {
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
recentCustomEmojis: [documentId],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const newEmojis = recentCustomEmojis.filter((id) => id !== documentId);
|
||||||
|
newEmojis.unshift(documentId);
|
||||||
|
if (newEmojis.length > MAX_STORED_EMOJIS) {
|
||||||
|
newEmojis.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
recentCustomEmojis: newEmojis,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('clearRecentCustomEmoji', (global) => {
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
recentCustomEmojis: [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
addActionHandler('reorderStickerSets', (global, action, payload) => {
|
addActionHandler('reorderStickerSets', (global, action, payload) => {
|
||||||
const { order, isCustomEmoji } = payload;
|
const { order, isCustomEmoji } = payload;
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -280,15 +280,28 @@ export function migrateCache(cached: GlobalState, initialState: GlobalState) {
|
|||||||
added: {},
|
added: {},
|
||||||
byId: {},
|
byId: {},
|
||||||
lastRendered: [],
|
lastRendered: [],
|
||||||
|
forEmoji: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!cached.recentCustomEmojis) {
|
||||||
|
cached.recentCustomEmojis = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cached.settings.byKey.shouldSuggestCustomEmoji === undefined) {
|
||||||
|
cached.settings.byKey.shouldSuggestCustomEmoji = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!cached.stickers.premiumSet) {
|
if (!cached.stickers.premiumSet) {
|
||||||
cached.stickers.premiumSet = {
|
cached.stickers.premiumSet = {
|
||||||
stickers: [],
|
stickers: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!cached.customEmojis.forEmoji) {
|
||||||
|
cached.customEmojis.forEmoji = {};
|
||||||
|
}
|
||||||
|
|
||||||
// TODO Remove in Jan 2023 (this was re-designed but can be hardcoded in cache)
|
// TODO Remove in Jan 2023 (this was re-designed but can be hardcoded in cache)
|
||||||
const { light: lightTheme } = cached.settings.themes;
|
const { light: lightTheme } = cached.settings.themes;
|
||||||
if (lightTheme?.patternColor === 'rgba(90, 110, 70, 0.6)' || !lightTheme?.patternColor) {
|
if (lightTheme?.patternColor === 'rgba(90, 110, 70, 0.6)' || !lightTheme?.patternColor) {
|
||||||
@ -396,6 +409,7 @@ function reduceCustomEmojis(global: GlobalState): GlobalState['customEmojis'] {
|
|||||||
return {
|
return {
|
||||||
byId: byIdToSave,
|
byId: byIdToSave,
|
||||||
lastRendered: idsToSave,
|
lastRendered: idsToSave,
|
||||||
|
forEmoji: {},
|
||||||
added: {},
|
added: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,3 +8,4 @@ export * from './payments';
|
|||||||
export * from './reactions';
|
export * from './reactions';
|
||||||
export * from './bots';
|
export * from './bots';
|
||||||
export * from './media';
|
export * from './media';
|
||||||
|
export * from './symbols';
|
||||||
|
|||||||
3
src/global/helpers/symbols.ts
Normal file
3
src/global/helpers/symbols.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function getStickerPreviewHash(stickerId: string) {
|
||||||
|
return `sticker${stickerId}?size=m`;
|
||||||
|
}
|
||||||
@ -96,6 +96,7 @@ export const INITIAL_STATE: GlobalState = {
|
|||||||
lastRendered: [],
|
lastRendered: [],
|
||||||
byId: {},
|
byId: {},
|
||||||
added: {},
|
added: {},
|
||||||
|
forEmoji: {},
|
||||||
},
|
},
|
||||||
|
|
||||||
emojiKeywords: {},
|
emojiKeywords: {},
|
||||||
@ -190,6 +191,7 @@ export const INITIAL_STATE: GlobalState = {
|
|||||||
canAutoPlayGifs: true,
|
canAutoPlayGifs: true,
|
||||||
canAutoPlayVideos: true,
|
canAutoPlayVideos: true,
|
||||||
shouldSuggestStickers: true,
|
shouldSuggestStickers: true,
|
||||||
|
shouldSuggestCustomEmoji: true,
|
||||||
shouldLoopStickers: true,
|
shouldLoopStickers: true,
|
||||||
language: 'en',
|
language: 'en',
|
||||||
timeFormat: '24h',
|
timeFormat: '24h',
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { GlobalState } from '../types';
|
import type { GlobalState } from '../types';
|
||||||
import type { ApiSticker, ApiStickerSet, ApiVideo } from '../../api/types';
|
import type { ApiSticker, ApiStickerSet, ApiVideo } from '../../api/types';
|
||||||
import { buildCollectionByKey, unique } from '../../util/iteratees';
|
import { buildCollectionByKey, unique } from '../../util/iteratees';
|
||||||
import { selectStickersForEmoji } from '../selectors';
|
import { selectCustomEmojiForEmoji, selectStickersForEmoji } from '../selectors';
|
||||||
|
|
||||||
export function updateStickerSets(
|
export function updateStickerSets(
|
||||||
global: GlobalState,
|
global: GlobalState,
|
||||||
@ -176,6 +176,26 @@ export function updateStickersForEmoji(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateCustomEmojiForEmoji(
|
||||||
|
global: GlobalState, emoji: string,
|
||||||
|
): GlobalState {
|
||||||
|
const localStickers = selectCustomEmojiForEmoji(global, emoji);
|
||||||
|
const uniqueIds = unique(localStickers.map(({ id }) => id));
|
||||||
|
const byId = buildCollectionByKey(localStickers, 'id');
|
||||||
|
const stickers = uniqueIds.map((id) => byId[id]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
customEmojis: {
|
||||||
|
...global.customEmojis,
|
||||||
|
forEmoji: {
|
||||||
|
emoji,
|
||||||
|
stickers,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function rebuildStickersForEmoji(global: GlobalState): GlobalState {
|
export function rebuildStickersForEmoji(global: GlobalState): GlobalState {
|
||||||
if (global.stickers.forEmoji) {
|
if (global.stickers.forEmoji) {
|
||||||
const { emoji, stickers, hash } = global.stickers.forEmoji;
|
const { emoji, stickers, hash } = global.stickers.forEmoji;
|
||||||
@ -186,5 +206,14 @@ export function rebuildStickersForEmoji(global: GlobalState): GlobalState {
|
|||||||
return updateStickersForEmoji(global, emoji, stickers, hash);
|
return updateStickersForEmoji(global, emoji, stickers, hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (global.customEmojis.forEmoji) {
|
||||||
|
const { emoji } = global.customEmojis.forEmoji;
|
||||||
|
if (!emoji) {
|
||||||
|
return global;
|
||||||
|
}
|
||||||
|
|
||||||
|
return updateCustomEmojiForEmoji(global, emoji);
|
||||||
|
}
|
||||||
|
|
||||||
return global;
|
return global;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -70,7 +70,27 @@ export function selectCustomEmojiForEmoji(global: GlobalState, emoji: string) {
|
|||||||
return isCurrentUserPremium ? customEmojiForEmoji : customEmojiForEmoji.filter(({ isFree }) => isFree);
|
return isCurrentUserPremium ? customEmojiForEmoji : customEmojiForEmoji.filter(({ isFree }) => isFree);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectIsSetPremium(stickerSet: ApiStickerSet) {
|
// Slow, not to be used in `withGlobal`
|
||||||
|
export function selectCustomEmojiForEmojis(global: GlobalState, emojis: string[]) {
|
||||||
|
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
|
||||||
|
const addedCustomSets = global.customEmojis.added.setIds;
|
||||||
|
let customEmojiForEmoji: ApiSticker[] = [];
|
||||||
|
|
||||||
|
// Added sets
|
||||||
|
addedCustomSets?.forEach((id) => {
|
||||||
|
const packs = global.stickers.setsById[id].packs;
|
||||||
|
if (!packs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const customEmojis = Object.entries(packs).filter(([emoji]) => (
|
||||||
|
emojis.includes(emoji) || emojis.includes(cleanEmoji(emoji))
|
||||||
|
)).flatMap(([, stickers]) => stickers);
|
||||||
|
customEmojiForEmoji = customEmojiForEmoji.concat(customEmojis);
|
||||||
|
});
|
||||||
|
return isCurrentUserPremium ? customEmojiForEmoji : customEmojiForEmoji.filter(({ isFree }) => isFree);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectIsSetPremium(stickerSet: Pick<ApiStickerSet, 'stickers' | 'isEmoji'>) {
|
||||||
return stickerSet.isEmoji && stickerSet.stickers?.some((sticker) => !sticker.isFree);
|
return stickerSet.isEmoji && stickerSet.stickers?.some((sticker) => !sticker.isFree);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -327,6 +327,11 @@ export type GlobalState = {
|
|||||||
};
|
};
|
||||||
lastRendered: string[];
|
lastRendered: string[];
|
||||||
byId: Record<string, ApiSticker>;
|
byId: Record<string, ApiSticker>;
|
||||||
|
forEmoji: {
|
||||||
|
emoji?: string;
|
||||||
|
stickers?: ApiSticker[];
|
||||||
|
};
|
||||||
|
featuredIds?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
animatedEmojis?: ApiStickerSet;
|
animatedEmojis?: ApiStickerSet;
|
||||||
@ -926,6 +931,11 @@ export interface ActionPayloads {
|
|||||||
};
|
};
|
||||||
clearStickersForEmoji: never;
|
clearStickersForEmoji: never;
|
||||||
|
|
||||||
|
loadCustomEmojiForEmoji: {
|
||||||
|
emoji: string;
|
||||||
|
};
|
||||||
|
clearCustomEmojiForEmoji: never;
|
||||||
|
|
||||||
addRecentEmoji: {
|
addRecentEmoji: {
|
||||||
emoji: string;
|
emoji: string;
|
||||||
};
|
};
|
||||||
@ -941,6 +951,11 @@ export interface ActionPayloads {
|
|||||||
setIds: string[];
|
setIds: string[];
|
||||||
};
|
};
|
||||||
closeCustomEmojiSets: never;
|
closeCustomEmojiSets: never;
|
||||||
|
addRecentCustomEmoji: {
|
||||||
|
documentId: string;
|
||||||
|
};
|
||||||
|
clearRecentCustomEmoji: never;
|
||||||
|
loadFeaturedEmojiStickers: never;
|
||||||
|
|
||||||
// Bots
|
// Bots
|
||||||
startBot: {
|
startBot: {
|
||||||
|
|||||||
23
src/hooks/useOnSelectionChange.ts
Normal file
23
src/hooks/useOnSelectionChange.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { useEffect } from '../lib/teact/teact';
|
||||||
|
|
||||||
|
export default function useOnSelectionChange(container: HTMLElement | null, callback: (range: Range) => void) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!container) return undefined;
|
||||||
|
|
||||||
|
const onSelectionChange = () => {
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (!selection) return;
|
||||||
|
|
||||||
|
for (let i = 0; i < selection.rangeCount; i++) {
|
||||||
|
const range = selection.getRangeAt(i);
|
||||||
|
const ancestor = range.commonAncestorContainer;
|
||||||
|
if (container.contains(ancestor)) {
|
||||||
|
callback(range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('selectionchange', onSelectionChange);
|
||||||
|
return () => document.removeEventListener('selectionchange', onSelectionChange);
|
||||||
|
}, [callback, container]);
|
||||||
|
}
|
||||||
@ -598,21 +598,40 @@ class TelegramClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
downloadStickerSetThumb(stickerSet) {
|
downloadStickerSetThumb(stickerSet) {
|
||||||
if (!stickerSet.thumbs || !stickerSet.thumbs.length) {
|
if (!stickerSet.thumbs?.length && !stickerSet.thumbDocumentId) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { thumbVersion } = stickerSet;
|
const { thumbVersion } = stickerSet;
|
||||||
return this.downloadFile(
|
|
||||||
new constructors.InputStickerSetThumb({
|
if (!stickerSet.thumbDocumentId) {
|
||||||
stickerset: new constructors.InputStickerSetID({
|
return this.downloadFile(
|
||||||
id: stickerSet.id,
|
new constructors.InputStickerSetThumb({
|
||||||
accessHash: stickerSet.accessHash,
|
stickerset: new constructors.InputStickerSetID({
|
||||||
|
id: stickerSet.id,
|
||||||
|
accessHash: stickerSet.accessHash,
|
||||||
|
}),
|
||||||
|
thumbVersion,
|
||||||
}),
|
}),
|
||||||
thumbVersion,
|
{ dcId: stickerSet.thumbDcId },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.invoke(new constructors.messages.GetCustomEmojiDocuments({
|
||||||
|
documentId: [stickerSet.thumbDocumentId],
|
||||||
|
})).then((docs) => {
|
||||||
|
const doc = docs[0];
|
||||||
|
return this.downloadFile(new constructors.InputDocumentFileLocation({
|
||||||
|
id: doc.id,
|
||||||
|
accessHash: doc.accessHash,
|
||||||
|
fileReference: doc.fileReference,
|
||||||
|
thumbSize: '',
|
||||||
}),
|
}),
|
||||||
{ dcId: stickerSet.thumbDcId },
|
{
|
||||||
);
|
fileSize: doc.size.toJSNumber(),
|
||||||
|
dcId: doc.dcId,
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_pickFileSize(sizes, sizeType) {
|
_pickFileSize(sizes, sizeType) {
|
||||||
|
|||||||
@ -51,7 +51,7 @@ export async function respondForDownload(e: FetchEvent) {
|
|||||||
const filenameHeader = matchedFilename ? `filename="${decodeURIComponent(matchedFilename[1])}"` : '';
|
const filenameHeader = matchedFilename ? `filename="${decodeURIComponent(matchedFilename[1])}"` : '';
|
||||||
const { fullSize, mimeType } = partInfo;
|
const { fullSize, mimeType } = partInfo;
|
||||||
|
|
||||||
const headers = [
|
const headers: [string, string][] = [
|
||||||
['Content-Length', String(fullSize)],
|
['Content-Length', String(fullSize)],
|
||||||
['Content-Type', mimeType],
|
['Content-Type', mimeType],
|
||||||
['Content-Disposition', `attachment; ${filenameHeader}`],
|
['Content-Disposition', `attachment; ${filenameHeader}`],
|
||||||
|
|||||||
@ -100,7 +100,7 @@ export async function respondForProgressive(e: FetchEvent) {
|
|||||||
const partSize = Math.min(end - start + 1, arrayBuffer.byteLength);
|
const partSize = Math.min(end - start + 1, arrayBuffer.byteLength);
|
||||||
end = start + partSize - 1;
|
end = start + partSize - 1;
|
||||||
const arrayBufferPart = arrayBuffer.slice(0, partSize);
|
const arrayBufferPart = arrayBuffer.slice(0, partSize);
|
||||||
const headers = [
|
const headers: [string, string][] = [
|
||||||
['Content-Range', `bytes ${start}-${end}/${fullSize}`],
|
['Content-Range', `bytes ${start}-${end}/${fullSize}`],
|
||||||
['Accept-Ranges', 'bytes'],
|
['Accept-Ranges', 'bytes'],
|
||||||
['Content-Length', String(partSize)],
|
['Content-Length', String(partSize)],
|
||||||
|
|||||||
@ -78,6 +78,7 @@ export interface ISettings extends NotifySettings, Record<string, any> {
|
|||||||
canAutoPlayGifs: boolean;
|
canAutoPlayGifs: boolean;
|
||||||
canAutoPlayVideos: boolean;
|
canAutoPlayVideos: boolean;
|
||||||
shouldSuggestStickers: boolean;
|
shouldSuggestStickers: boolean;
|
||||||
|
shouldSuggestCustomEmoji: boolean;
|
||||||
shouldLoopStickers: boolean;
|
shouldLoopStickers: boolean;
|
||||||
hasPassword?: boolean;
|
hasPassword?: boolean;
|
||||||
languages?: ApiLanguage[];
|
languages?: ApiLanguage[];
|
||||||
|
|||||||
31
src/util/customEmojiManager.ts
Normal file
31
src/util/customEmojiManager.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { ApiMediaFormat } from '../api/types';
|
||||||
|
|
||||||
|
import { getStickerPreviewHash } from '../global/helpers';
|
||||||
|
import * as mediaLoader from './mediaLoader';
|
||||||
|
import { throttle } from './schedulers';
|
||||||
|
|
||||||
|
const DOM_PROCESS_THROTTLE = 500;
|
||||||
|
|
||||||
|
function processDomForCustomEmoji() {
|
||||||
|
const emojis = document.querySelectorAll<HTMLImageElement>('img[data-document-id]:not([src])');
|
||||||
|
emojis.forEach((emoji) => {
|
||||||
|
const mediaHash = getStickerPreviewHash(emoji.dataset.documentId!);
|
||||||
|
const mediaData = mediaLoader.getFromMemory(mediaHash);
|
||||||
|
if (mediaData) {
|
||||||
|
emoji.src = mediaData;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const processMessageInputForCustomEmoji = throttle(processDomForCustomEmoji, DOM_PROCESS_THROTTLE);
|
||||||
|
|
||||||
|
export function getCustomEmojiPreviewMediaData(emojiId: string) {
|
||||||
|
const mediaHash = getStickerPreviewHash(emojiId);
|
||||||
|
const data = mediaLoader.getFromMemory(mediaHash);
|
||||||
|
if (data) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaLoader.fetch(mediaHash, ApiMediaFormat.BlobUrl).then(() => processMessageInputForCustomEmoji());
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import type { ApiMessageEntity, ApiFormattedText } from '../api/types';
|
import type { ApiMessageEntity, ApiFormattedText } from '../api/types';
|
||||||
import { ApiMessageEntityTypes } from '../api/types';
|
import { ApiMessageEntityTypes } from '../api/types';
|
||||||
import { IS_EMOJI_SUPPORTED } from './environment';
|
|
||||||
import { RE_LINK_TEMPLATE } from '../config';
|
import { RE_LINK_TEMPLATE } from '../config';
|
||||||
|
|
||||||
const ENTITY_CLASS_BY_NODE_NAME: Record<string, ApiMessageEntityTypes> = {
|
const ENTITY_CLASS_BY_NODE_NAME: Record<string, ApiMessageEntityTypes> = {
|
||||||
@ -15,7 +14,6 @@ const ENTITY_CLASS_BY_NODE_NAME: Record<string, ApiMessageEntityTypes> = {
|
|||||||
CODE: ApiMessageEntityTypes.Code,
|
CODE: ApiMessageEntityTypes.Code,
|
||||||
PRE: ApiMessageEntityTypes.Pre,
|
PRE: ApiMessageEntityTypes.Pre,
|
||||||
BLOCKQUOTE: ApiMessageEntityTypes.Blockquote,
|
BLOCKQUOTE: ApiMessageEntityTypes.Blockquote,
|
||||||
'CUSTOM-EMOJI': ApiMessageEntityTypes.CustomEmoji,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_TAG_DEEPNESS = 3;
|
const MAX_TAG_DEEPNESS = 3;
|
||||||
@ -23,6 +21,7 @@ const MAX_TAG_DEEPNESS = 3;
|
|||||||
export default function parseMessageInput(html: string, withMarkdownLinks = false): ApiFormattedText {
|
export default function parseMessageInput(html: string, withMarkdownLinks = false): ApiFormattedText {
|
||||||
const fragment = document.createElement('div');
|
const fragment = document.createElement('div');
|
||||||
fragment.innerHTML = withMarkdownLinks ? parseMarkdown(parseMarkdownLinks(html)) : parseMarkdown(html);
|
fragment.innerHTML = withMarkdownLinks ? parseMarkdown(parseMarkdownLinks(html)) : parseMarkdown(html);
|
||||||
|
fixImageContent(fragment);
|
||||||
const text = fragment.innerText.trim().replace(/\u200b+/g, '');
|
const text = fragment.innerText.trim().replace(/\u200b+/g, '');
|
||||||
let textIndex = 0;
|
let textIndex = 0;
|
||||||
let recursionDeepness = 0;
|
let recursionDeepness = 0;
|
||||||
@ -59,14 +58,19 @@ export default function parseMessageInput(html: string, withMarkdownLinks = fals
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fixImageContent(fragment: HTMLDivElement) {
|
||||||
|
fragment.querySelectorAll('img').forEach((node) => {
|
||||||
|
if (node.dataset.documentId) { // Custom Emoji
|
||||||
|
node.textContent = (node as HTMLImageElement).alt || '';
|
||||||
|
} else { // Regular emoji with image fallback
|
||||||
|
node.replaceWith(node.alt || '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function parseMarkdown(html: string) {
|
function parseMarkdown(html: string) {
|
||||||
let parsedHtml = html.slice(0);
|
let parsedHtml = html.slice(0);
|
||||||
|
|
||||||
if (!IS_EMOJI_SUPPORTED) {
|
|
||||||
// Emojis
|
|
||||||
parsedHtml = parsedHtml.replace(/<img[^>]+alt="([^"]+)"[^>]*>/gm, '$1');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strip redundant nbsp's
|
// Strip redundant nbsp's
|
||||||
parsedHtml = parsedHtml.replace(/ /g, ' ');
|
parsedHtml = parsedHtml.replace(/ /g, ' ');
|
||||||
|
|
||||||
@ -94,7 +98,7 @@ function parseMarkdown(html: string) {
|
|||||||
// Custom Emoji markdown tag
|
// Custom Emoji markdown tag
|
||||||
parsedHtml = parsedHtml.replace(
|
parsedHtml = parsedHtml.replace(
|
||||||
/(^|\s)(?!<(?:code|pre)[^<]*|<\/)\[([^\]\n]+)\]\(customEmoji:(\d+)\)(?![^<]*<\/(?:code|pre)>)(\s|$)/g,
|
/(^|\s)(?!<(?:code|pre)[^<]*|<\/)\[([^\]\n]+)\]\(customEmoji:(\d+)\)(?![^<]*<\/(?:code|pre)>)(\s|$)/g,
|
||||||
'$1<custom-emoji document-id="$3" alt="$2">$2</custom-emoji>$4',
|
'$1<img alt="$2" data-document-id="$3">$4',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Other simple markdown
|
// Other simple markdown
|
||||||
@ -131,6 +135,7 @@ function getEntityDataFromNode(
|
|||||||
textIndex: number,
|
textIndex: number,
|
||||||
): { index: number; entity?: ApiMessageEntity } {
|
): { index: number; entity?: ApiMessageEntity } {
|
||||||
const type = getEntityTypeFromNode(node);
|
const type = getEntityTypeFromNode(node);
|
||||||
|
|
||||||
if (!type || !node.textContent) {
|
if (!type || !node.textContent) {
|
||||||
return {
|
return {
|
||||||
index: textIndex,
|
index: textIndex,
|
||||||
@ -187,7 +192,7 @@ function getEntityDataFromNode(
|
|||||||
type,
|
type,
|
||||||
offset,
|
offset,
|
||||||
length,
|
length,
|
||||||
documentId: (node as HTMLElement).getAttribute('document-id')!,
|
documentId: (node as HTMLImageElement).dataset.documentId!,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -232,5 +237,11 @@ function getEntityTypeFromNode(node: ChildNode): ApiMessageEntityTypes | undefin
|
|||||||
return (node as HTMLElement).dataset.entityType as any;
|
return (node as HTMLElement).dataset.entityType as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (node.nodeName === 'IMG') {
|
||||||
|
if ((node as HTMLImageElement).dataset.documentId) {
|
||||||
|
return ApiMessageEntityTypes.CustomEmoji;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
const fragmentEl = document.createElement('div');
|
||||||
|
|
||||||
export function insertHtmlInSelection(html: string) {
|
export function insertHtmlInSelection(html: string) {
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
|
|
||||||
@ -18,3 +20,16 @@ export function insertHtmlInSelection(html: string) {
|
|||||||
selection.addRange(range);
|
selection.addRange(range);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getHtmlBeforeSelection(container?: HTMLElement, useCommonAncestor?: boolean) {
|
||||||
|
if (!container) return '';
|
||||||
|
const sel = window.getSelection();
|
||||||
|
if (!sel || !sel.rangeCount) return container.innerHTML;
|
||||||
|
const range = sel.getRangeAt(0).cloneRange();
|
||||||
|
if (!range.intersectsNode(container)) return container.innerHTML;
|
||||||
|
if (!useCommonAncestor && !container.contains(range.commonAncestorContainer)) return '';
|
||||||
|
range.collapse(true);
|
||||||
|
range.setStart(container, 0);
|
||||||
|
fragmentEl.replaceChildren(range.cloneContents());
|
||||||
|
return fragmentEl.innerHTML;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user