Message: Send effects (#4727)

This commit is contained in:
Alexander Zinchuk 2024-07-15 15:50:56 +02:00
parent e4c21c5b56
commit 4768a8103c
33 changed files with 663 additions and 89 deletions

View File

@ -803,6 +803,7 @@ export function buildLocalMessage(
sendAs?: ApiPeer, sendAs?: ApiPeer,
story?: ApiStory | ApiStorySkipped, story?: ApiStory | ApiStorySkipped,
isInvertedMedia?: true, isInvertedMedia?: true,
effectId?: string,
): ApiMessage { ): ApiMessage {
const localId = getNextLocalMessageId(lastMessageId); const localId = getNextLocalMessageId(lastMessageId);
const media = attachment && buildUploadingMedia(attachment); const media = attachment && buildUploadingMedia(attachment);
@ -838,6 +839,7 @@ export function buildLocalMessage(
...(scheduledAt && { isScheduled: true }), ...(scheduledAt && { isScheduled: true }),
isForwardingAllowed: true, isForwardingAllowed: true,
isInvertedMedia, isInvertedMedia,
effectId,
} satisfies ApiMessage; } satisfies ApiMessage;
const emojiOnlyCount = getEmojiOnlyCountForMessage(message.content, message.groupedId); const emojiOnlyCount = getEmojiOnlyCountForMessage(message.content, message.groupedId);

View File

@ -9,7 +9,8 @@ import { compact } from '../../../util/iteratees';
import localDb from '../localDb'; import localDb from '../localDb';
import { buildApiThumbnailFromCached, buildApiThumbnailFromPath } from './common'; import { buildApiThumbnailFromCached, buildApiThumbnailFromPath } from './common';
export function buildStickerFromDocument(document: GramJs.TypeDocument, isNoPremium?: boolean): ApiSticker | undefined { export function buildStickerFromDocument(document: GramJs.TypeDocument,
isNoPremium?: boolean, isPremium?: boolean): ApiSticker | undefined {
if (document instanceof GramJs.DocumentEmpty) { if (document instanceof GramJs.DocumentEmpty) {
return undefined; return undefined;
} }
@ -46,7 +47,7 @@ export function buildStickerFromDocument(document: GramJs.TypeDocument, isNoPrem
const stickerOrEmojiAttribute = (stickerAttribute || customEmojiAttribute)!; const stickerOrEmojiAttribute = (stickerAttribute || customEmojiAttribute)!;
const stickerSetInfo = buildApiStickerSetInfo(stickerOrEmojiAttribute?.stickerset); const stickerSetInfo = buildApiStickerSetInfo(stickerOrEmojiAttribute?.stickerset);
const emoji = stickerOrEmojiAttribute?.alt; const emoji = stickerOrEmojiAttribute?.alt;
const isFree = Boolean(customEmojiAttribute?.free ?? true); const isFree = Boolean(customEmojiAttribute?.free ?? true) && !isPremium;
const cachedThumb = document.thumbs && document.thumbs.find( const cachedThumb = document.thumbs && document.thumbs.find(
(s): s is GramJs.PhotoCachedSize => s instanceof GramJs.PhotoCachedSize, (s): s is GramJs.PhotoCachedSize => s instanceof GramJs.PhotoCachedSize,

View File

@ -1,3 +1,4 @@
import BigInt from 'big-integer';
import { Api as GramJs } from '../../../lib/gramjs'; import { Api as GramJs } from '../../../lib/gramjs';
import type { ThreadId } from '../../../types'; import type { ThreadId } from '../../../types';
@ -270,6 +271,7 @@ export function sendMessage(
shouldUpdateStickerSetOrder, shouldUpdateStickerSetOrder,
wasDrafted, wasDrafted,
isInvertedMedia, isInvertedMedia,
effectId,
}: { }: {
chat: ApiChat; chat: ApiChat;
lastMessageId?: number; lastMessageId?: number;
@ -290,6 +292,7 @@ export function sendMessage(
shouldUpdateStickerSetOrder?: boolean; shouldUpdateStickerSetOrder?: boolean;
wasDrafted?: boolean; wasDrafted?: boolean;
isInvertedMedia?: true; isInvertedMedia?: true;
effectId?: string;
}, },
onProgress?: ApiOnProgress, onProgress?: ApiOnProgress,
) { ) {
@ -309,6 +312,7 @@ export function sendMessage(
sendAs, sendAs,
story, story,
isInvertedMedia, isInvertedMedia,
effectId,
); );
onUpdate({ onUpdate({
@ -396,6 +400,7 @@ export function sendMessage(
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }), ...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
...(shouldUpdateStickerSetOrder && { updateStickersetsOrder: shouldUpdateStickerSetOrder }), ...(shouldUpdateStickerSetOrder && { updateStickersetsOrder: shouldUpdateStickerSetOrder }),
...(isInvertedMedia && { invertMedia: isInvertedMedia }), ...(isInvertedMedia && { invertMedia: isInvertedMedia }),
...(effectId && { effect: BigInt(effectId) }),
}), { }), {
shouldThrow: true, shouldThrow: true,
shouldIgnoreUpdates: true, shouldIgnoreUpdates: true,

View File

@ -1,7 +1,9 @@
import BigInt from 'big-integer'; import BigInt from 'big-integer';
import { Api as GramJs } from '../../../lib/gramjs'; import { Api as GramJs } from '../../../lib/gramjs';
import type { ApiChat, ApiReaction } from '../../types'; import type {
ApiChat, ApiReaction, ApiSticker,
} from '../../types';
import { import {
API_GENERAL_ID_LIMIT, API_GENERAL_ID_LIMIT,
@ -18,6 +20,7 @@ import {
buildApiSavedReactionTag, buildApiSavedReactionTag,
buildMessagePeerReaction, buildMessagePeerReaction,
} from '../apiBuilders/reactions'; } from '../apiBuilders/reactions';
import { buildStickerFromDocument } from '../apiBuilders/symbols';
import { buildApiUser } from '../apiBuilders/users'; import { buildApiUser } from '../apiBuilders/users';
import { buildInputPeer, buildInputReaction } from '../gramjsBuilders'; import { buildInputPeer, buildInputReaction } from '../gramjsBuilders';
import { addEntitiesToLocalDb } from '../helpers'; import { addEntitiesToLocalDb } from '../helpers';
@ -103,13 +106,32 @@ export async function fetchAvailableEffects() {
return undefined; return undefined;
} }
const documentsMap = new Map(result.documents.map((doc) => [String(doc.id), doc]));
result.documents.forEach((document) => { result.documents.forEach((document) => {
if (document instanceof GramJs.Document) { if (document instanceof GramJs.Document) {
localDb.documents[String(document.id)] = document; localDb.documents[String(document.id)] = document;
} }
}); });
return result.effects.map(buildApiAvailableEffect); const effects = result.effects.map(buildApiAvailableEffect);
const stickers : ApiSticker[] = [];
const emojis : ApiSticker[] = [];
for (const effect of effects) {
if (effect.effectAnimationId) {
const document = documentsMap.get(effect.effectStickerId);
const emoji = document && buildStickerFromDocument(document, false, effect.isPremium);
if (emoji) emojis.push(emoji);
} else {
const document = localDb.documents[effect.effectStickerId];
const sticker = buildStickerFromDocument(document);
if (sticker) { stickers.push(sticker); }
}
}
return { effects, emojis, stickers };
} }
export function sendReaction({ export function sendReaction({

View File

@ -1269,3 +1269,4 @@
"MenuBetaChangelog" = "Beta Changelog"; "MenuBetaChangelog" = "Beta Changelog";
"MenuSwitchToK" = "Switch to K Version"; "MenuSwitchToK" = "Switch to K Version";
"MenuInstallApp" = "Install App"; "MenuInstallApp" = "Install App";
"RemoveEffect" = "Remove effect"

View File

@ -30,6 +30,17 @@
--border-width: 0; --border-width: 0;
} }
.effect-icon {
font-size: 1.25rem;
transform: translate(-50%, 0);
color: var(--color-text);
& > .emoji {
width: 1rem !important;
height: 1rem !important;
}
}
@keyframes show-send-as-button { @keyframes show-send-as-button {
from { from {
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */ /* stylelint-disable-next-line plugin/no-low-performance-animation-properties */

View File

@ -7,6 +7,7 @@ import { getActions, getGlobal, withGlobal } from '../../global';
import type { import type {
ApiAttachment, ApiAttachment,
ApiAttachMenuPeerType, ApiAttachMenuPeerType,
ApiAvailableEffect,
ApiAvailableReaction, ApiAvailableReaction,
ApiBotCommand, ApiBotCommand,
ApiBotInlineMediaResult, ApiBotInlineMediaResult,
@ -45,6 +46,7 @@ import {
REPLIES_USER_ID, REPLIES_USER_ID,
SCHEDULED_WHEN_ONLINE, SCHEDULED_WHEN_ONLINE,
SEND_MESSAGE_ACTION_INTERVAL, SEND_MESSAGE_ACTION_INTERVAL,
SERVICE_NOTIFICATIONS_USER_ID,
} from '../../config'; } from '../../config';
import { requestMeasure, requestNextMutation } from '../../lib/fasterdom/fasterdom'; import { requestMeasure, requestNextMutation } from '../../lib/fasterdom/fasterdom';
import { import {
@ -78,6 +80,7 @@ import {
selectNewestMessageWithBotKeyboardButtons, selectNewestMessageWithBotKeyboardButtons,
selectNoWebPage, selectNoWebPage,
selectPeerStory, selectPeerStory,
selectPerformanceSettingsValue,
selectRequestedDraft, selectRequestedDraft,
selectRequestedDraftFiles, selectRequestedDraftFiles,
selectScheduledIds, selectScheduledIds,
@ -153,6 +156,7 @@ import SendAsMenu from '../middle/composer/SendAsMenu.async';
import StickerTooltip from '../middle/composer/StickerTooltip.async'; import StickerTooltip from '../middle/composer/StickerTooltip.async';
import SymbolMenuButton from '../middle/composer/SymbolMenuButton'; import SymbolMenuButton from '../middle/composer/SymbolMenuButton';
import WebPagePreview from '../middle/composer/WebPagePreview'; import WebPagePreview from '../middle/composer/WebPagePreview';
import MessageEffect from '../middle/message/MessageEffect';
import ReactionSelector from '../middle/message/reactions/ReactionSelector'; import ReactionSelector from '../middle/message/reactions/ReactionSelector';
import Button from '../ui/Button'; import Button from '../ui/Button';
import ResponsiveHoverButton from '../ui/ResponsiveHoverButton'; import ResponsiveHoverButton from '../ui/ResponsiveHoverButton';
@ -253,6 +257,11 @@ type StateProps =
canSendQuickReplies?: boolean; canSendQuickReplies?: boolean;
webPagePreview?: ApiWebPage; webPagePreview?: ApiWebPage;
noWebPage?: boolean; noWebPage?: boolean;
effect?: ApiAvailableEffect;
effectReactions?: ApiReaction[];
areEffectsSupported?: boolean;
canPlayEffect?: boolean;
shouldPlayEffect?: boolean;
}; };
enum MainButtonState { enum MainButtonState {
@ -361,6 +370,11 @@ const Composer: FC<OwnProps & StateProps> = ({
onForward, onForward,
webPagePreview, webPagePreview,
noWebPage, noWebPage,
effect,
effectReactions,
areEffectsSupported,
canPlayEffect,
shouldPlayEffect,
}) => { }) => {
const { const {
sendMessage, sendMessage,
@ -384,6 +398,9 @@ const Composer: FC<OwnProps & StateProps> = ({
sendStoryReaction, sendStoryReaction,
editMessage, editMessage,
updateAttachmentSettings, updateAttachmentSettings,
saveEffectInDraft,
setReactionEffect,
hideEffectInComposer,
} = getActions(); } = getActions();
const lang = useOldLang(); const lang = useOldLang();
@ -1023,11 +1040,15 @@ const Composer: FC<OwnProps & StateProps> = ({
const messageInput = document.querySelector<HTMLDivElement>(editableInputCssSelector); const messageInput = document.querySelector<HTMLDivElement>(editableInputCssSelector);
const effectId = effect?.id;
if (text) { if (text) {
if (!checkSlowMode()) return; if (!checkSlowMode()) return;
const isInvertedMedia = hasWebPagePreview ? attachmentSettings.isInvertedMedia : undefined; const isInvertedMedia = hasWebPagePreview ? attachmentSettings.isInvertedMedia : undefined;
if (areEffectsSupported) saveEffectInDraft({ chatId, threadId, effectId: undefined });
sendMessage({ sendMessage({
messageList: currentMessageList, messageList: currentMessageList,
text, text,
@ -1036,6 +1057,7 @@ const Composer: FC<OwnProps & StateProps> = ({
isSilent, isSilent,
shouldUpdateStickerSetOrder, shouldUpdateStickerSetOrder,
isInvertedMedia, isInvertedMedia,
effectId,
}); });
} }
@ -1077,7 +1099,7 @@ const Composer: FC<OwnProps & StateProps> = ({
}); });
const handleMessageSchedule = useLastCallback(( const handleMessageSchedule = useLastCallback((
args: ScheduledMessageArgs, scheduledAt: number, messageList: MessageList, args: ScheduledMessageArgs, scheduledAt: number, messageList: MessageList, effectId?: string,
) => { ) => {
if (args && 'queryId' in args) { if (args && 'queryId' in args) {
const { id, queryId, isSilent } = args; const { id, queryId, isSilent } = args;
@ -1103,6 +1125,7 @@ const Composer: FC<OwnProps & StateProps> = ({
...args, ...args,
messageList, messageList,
scheduledAt, scheduledAt,
effectId,
}); });
} }
}); });
@ -1429,7 +1452,7 @@ const Composer: FC<OwnProps & StateProps> = ({
return; return;
} }
requestCalendar((scheduledAt) => { requestCalendar((scheduledAt) => {
handleMessageSchedule({}, scheduledAt, currentMessageList); handleMessageSchedule({}, scheduledAt, currentMessageList, effect?.id);
}); });
break; break;
default: default:
@ -1494,6 +1517,12 @@ const Composer: FC<OwnProps & StateProps> = ({
closeReactionPicker(); closeReactionPicker();
}); });
const handleToggleEffectReaction = useLastCallback((reaction: ApiReaction) => {
setReactionEffect({ chatId, threadId, reaction });
closeReactionPicker();
});
const handleReactionPickerOpen = useLastCallback((position: IAnchorPosition) => { const handleReactionPickerOpen = useLastCallback((position: IAnchorPosition) => {
openStoryReactionPicker({ openStoryReactionPicker({
peerId: chatId, peerId: chatId,
@ -1524,7 +1553,7 @@ const Composer: FC<OwnProps & StateProps> = ({
}); });
const handleSendWhenOnline = useLastCallback(() => { const handleSendWhenOnline = useLastCallback(() => {
handleMessageSchedule({}, SCHEDULED_WHEN_ONLINE, currentMessageList!); handleMessageSchedule({}, SCHEDULED_WHEN_ONLINE, currentMessageList!, effect?.id);
}); });
const handleSendScheduledAttachments = useLastCallback( const handleSendScheduledAttachments = useLastCallback(
@ -1541,6 +1570,10 @@ const Composer: FC<OwnProps & StateProps> = ({
}, },
); );
const handleRemoveEffect = useLastCallback(() => { saveEffectInDraft({ chatId, threadId, effectId: undefined }); });
const handleStopEffect = useLastCallback(() => { hideEffectInComposer({ }); });
const onSend = useMemo(() => { const onSend = useMemo(() => {
switch (mainButtonState) { switch (mainButtonState) {
case MainButtonState.Edit: case MainButtonState.Edit:
@ -1555,6 +1588,8 @@ const Composer: FC<OwnProps & StateProps> = ({
const withBotCommands = isChatWithBot && botMenuButton?.type === 'commands' && !editingMessage const withBotCommands = isChatWithBot && botMenuButton?.type === 'commands' && !editingMessage
&& botCommands !== false && !activeVoiceRecording; && botCommands !== false && !activeVoiceRecording;
const effectEmoji = areEffectsSupported && effect?.emoticon;
return ( return (
<div className={fullClassName}> <div className={fullClassName}>
{isInMessageList && canAttachMedia && isReady && ( {isInMessageList && canAttachMedia && isReady && (
@ -1984,6 +2019,18 @@ const Composer: FC<OwnProps & StateProps> = ({
{isInMessageList && <i className="icon icon-schedule" />} {isInMessageList && <i className="icon icon-schedule" />}
{isInMessageList && <i className="icon icon-check" />} {isInMessageList && <i className="icon icon-check" />}
</Button> </Button>
{effectEmoji && (
<span className="effect-icon">
{renderText(effectEmoji)}
</span>
)}
{effect && canPlayEffect && (
<MessageEffect
shouldPlay={shouldPlayEffect}
effect={effect}
onStop={handleStopEffect}
/>
)}
{canShowCustomSendMenu && ( {canShowCustomSendMenu && (
<CustomSendMenu <CustomSendMenu
isOpen={isCustomSendMenuOpen} isOpen={isCustomSendMenuOpen}
@ -1992,9 +2039,20 @@ const Composer: FC<OwnProps & StateProps> = ({
onSendSilent={!isChatWithSelf ? handleSendSilent : undefined} onSendSilent={!isChatWithSelf ? handleSendSilent : undefined}
onSendSchedule={!isInScheduledList ? handleSendScheduled : undefined} onSendSchedule={!isInScheduledList ? handleSendScheduled : undefined}
onSendWhenOnline={handleSendWhenOnline} onSendWhenOnline={handleSendWhenOnline}
onRemoveEffect={handleRemoveEffect}
onClose={handleContextMenuClose} onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide} onCloseAnimationEnd={handleContextMenuHide}
isSavedMessages={isChatWithSelf} isSavedMessages={isChatWithSelf}
chatId={chatId}
withEffects={areEffectsSupported}
hasCurrentEffect={Boolean(effect)}
effectReactions={effectReactions}
allAvailableReactions={availableReactions}
onToggleReaction={handleToggleEffectReaction}
isCurrentUserPremium={isCurrentUserPremium}
isInSavedMessages={isChatWithSelf}
isInStoryViewer={isInStoryViewer}
canPlayAnimatedEmojis={canPlayAnimatedEmojis}
/> />
)} )}
{calendar} {calendar}
@ -2068,8 +2126,16 @@ export default memo(withGlobal<OwnProps>(
const noWebPage = selectNoWebPage(global, chatId, threadId); const noWebPage = selectNoWebPage(global, chatId, threadId);
const areEffectsSupported = isChatWithUser && !isChatWithBot
&& !isInScheduledList && !isChatWithSelf && type !== 'story' && chatId !== SERVICE_NOTIFICATIONS_USER_ID;
const canPlayEffect = selectPerformanceSettingsValue(global, 'stickerEffects');
const shouldPlayEffect = tabState.shouldPlayEffectInComposer;
const effectId = areEffectsSupported && draft?.effectId;
const effect = effectId ? global.availableEffectById[effectId] : undefined;
const effectReactions = global.reactions.effectReactions;
return { return {
availableReactions: type === 'story' ? global.reactions.availableReactions : undefined, availableReactions: global.reactions.availableReactions,
topReactions: type === 'story' ? global.reactions.topReactions : undefined, topReactions: type === 'story' ? global.reactions.topReactions : undefined,
isOnActiveTab: !tabState.isBlurred, isOnActiveTab: !tabState.isBlurred,
editingMessage: selectEditingMessage(global, chatId, threadId, messageListType), editingMessage: selectEditingMessage(global, chatId, threadId, messageListType),
@ -2137,6 +2203,11 @@ export default memo(withGlobal<OwnProps>(
canSendQuickReplies, canSendQuickReplies,
noWebPage, noWebPage,
webPagePreview: selectTabState(global).webPagePreview, webPagePreview: selectTabState(global).webPagePreview,
effect,
effectReactions,
areEffectsSupported,
canPlayEffect,
shouldPlayEffect,
}; };
}, },
)(Composer)); )(Composer));

View File

@ -390,6 +390,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
pickerStyles.main_customEmoji, pickerStyles.main_customEmoji,
IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll', IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll',
pickerListClassName, pickerListClassName,
pickerStyles.hasHeader,
); );
return ( return (

View File

@ -27,6 +27,12 @@
} }
} }
&.effect-emoji .sticker-locked {
font-size: 0.75rem;
width: 0.875rem;
height: 0.875rem;
}
&.set-expand { &.set-expand {
padding: 0; padding: 0;
vertical-align: bottom; vertical-align: bottom;
@ -38,8 +44,7 @@
.sticker-locked { .sticker-locked {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 50%; right: 0;
transform: translate(-50%, 50%);
width: 1.25rem; width: 1.25rem;
height: 1.25rem; height: 1.25rem;
display: flex; display: flex;
@ -47,6 +52,8 @@
align-items: center; align-items: center;
border-radius: 50%; border-radius: 50%;
color: white; color: white;
z-index: 2;
opacity: 0.75;
background: var(--premium-gradient); background: var(--premium-gradient);
} }

View File

@ -54,6 +54,7 @@ type OwnProps<T> = {
onContextMenuOpen?: NoneToVoidFunction; onContextMenuOpen?: NoneToVoidFunction;
onContextMenuClose?: NoneToVoidFunction; onContextMenuClose?: NoneToVoidFunction;
onContextMenuClick?: NoneToVoidFunction; onContextMenuClick?: NoneToVoidFunction;
isEffectEmoji?: boolean;
}; };
const contentForStatusMenuContext = [ const contentForStatusMenuContext = [
@ -91,6 +92,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
onContextMenuOpen, onContextMenuOpen,
onContextMenuClose, onContextMenuClose,
onContextMenuClick, onContextMenuClick,
isEffectEmoji,
}: OwnProps<T>) => { }: OwnProps<T>) => {
const { openStickerSet, openPremiumModal, setEmojiStatus } = getActions(); const { openStickerSet, openPremiumModal, setEmojiStatus } = getActions();
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
@ -102,8 +104,11 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
const customColor = useDynamicColorListener(ref, !hasCustomColor); const customColor = useDynamicColorListener(ref, !hasCustomColor);
const { const {
id, isCustomEmoji, hasEffect: isPremium, stickerSetInfo, id, stickerSetInfo,
} = sticker; } = sticker;
const isPremium = (!sticker.isFree && isEffectEmoji) || sticker.hasEffect;
const isCustomEmoji = sticker.isCustomEmoji || isEffectEmoji;
const isLocked = !isCurrentUserPremium && isPremium && !shouldIgnorePremium; const isLocked = !isCurrentUserPremium && isPremium && !shouldIgnorePremium;
const isIntersecting = useIsIntersecting(ref, observeIntersection); const isIntersecting = useIsIntersecting(ref, observeIntersection);
@ -213,6 +218,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
onClick && 'interactive', onClick && 'interactive',
isSelected && 'selected', isSelected && 'selected',
isCustomEmoji && 'custom-emoji', isCustomEmoji && 'custom-emoji',
isEffectEmoji && 'effect-emoji',
className, className,
); );

View File

@ -11,6 +11,8 @@ import type { StickerSetOrReactionsSetOrRecent } from '../../types';
import { import {
DEFAULT_STATUS_ICON_ID, DEFAULT_STATUS_ICON_ID,
DEFAULT_TOPIC_ICON_STICKER_ID, DEFAULT_TOPIC_ICON_STICKER_ID,
EFFECT_EMOJIS_SET_ID,
EFFECT_STICKERS_SET_ID,
EMOJI_SIZE_PICKER, EMOJI_SIZE_PICKER,
FAVORITE_SYMBOL_SET_ID, FAVORITE_SYMBOL_SET_ID,
POPULAR_SYMBOL_SET_ID, POPULAR_SYMBOL_SET_ID,
@ -232,8 +234,11 @@ const StickerSet: FC<OwnProps> = ({
const isLocked = !isSavedMessages && !isCurrentUserPremium && isPremiumSet && !isChatEmojiSet; const isLocked = !isSavedMessages && !isCurrentUserPremium && isPremiumSet && !isChatEmojiSet;
const isInstalled = stickerSet.installedDate && !stickerSet.isArchived; const isInstalled = stickerSet.installedDate && !stickerSet.isArchived;
const canCut = !isInstalled && stickerSet.id !== RECENT_SYMBOL_SET_ID && stickerSet.id !== POPULAR_SYMBOL_SET_ID
&& !isChatEmojiSet && !isChatStickerSet; const canCut = !isInstalled && stickerSet.id !== RECENT_SYMBOL_SET_ID
&& stickerSet.id !== POPULAR_SYMBOL_SET_ID && stickerSet.id !== EFFECT_EMOJIS_SET_ID
&& stickerSet.id !== EFFECT_STICKERS_SET_ID && !isChatEmojiSet && !isChatStickerSet;
const [isCut, , expand] = useFlag(canCut); const [isCut, , expand] = useFlag(canCut);
const itemsBeforeCutout = itemsPerRow * 3 - 1; const itemsBeforeCutout = itemsPerRow * 3 - 1;
const totalItemsCount = withDefaultTopicIcon ? stickerSet.count + 1 : stickerSet.count; const totalItemsCount = withDefaultTopicIcon ? stickerSet.count + 1 : stickerSet.count;
@ -298,7 +303,11 @@ const StickerSet: FC<OwnProps> = ({
</div> </div>
)} )}
<div <div
className={buildClassName('symbol-set-container shared-canvas-container', transitionClassNames)} className={buildClassName(
'symbol-set-container shared-canvas-container',
transitionClassNames,
stickerSet.id === EFFECT_EMOJIS_SET_ID && 'effect-emojis',
)}
style={`height: ${height}px;`} style={`height: ${height}px;`}
> >
<canvas <canvas
@ -382,6 +391,9 @@ const StickerSet: FC<OwnProps> = ({
onContextMenuClose={onContextMenuClose} onContextMenuClose={onContextMenuClose}
onContextMenuClick={onContextMenuClick} onContextMenuClick={onContextMenuClick}
forcePlayback={forcePlayback} forcePlayback={forcePlayback}
isEffectEmoji={stickerSet.id === EFFECT_EMOJIS_SET_ID}
noShowPremium={isCurrentUserPremium
&& (stickerSet.id === EFFECT_STICKERS_SET_ID || stickerSet.id === EFFECT_EMOJIS_SET_ID)}
/> />
); );
})} })}

View File

@ -19,4 +19,45 @@
.bubble { .bubble {
width: 16rem; width: 16rem;
} }
&.with-effects .bubble {
overflow: initial;
background: none !important;
backdrop-filter: none !important;
box-shadow: none;
}
&.with-effects &_items {
background: var(--color-background-compact-menu);
backdrop-filter: blur(10px);
box-shadow: 0 0.25rem 0.5rem 0.125rem var(--color-default-shadow);
border-radius: var(--border-radius-default);
padding: 0.25rem 0;
@media (min-width: 600px) {
margin-inline-end: 2.75rem;
}
body.no-menu-blur & {
background: var(--color-background);
backdrop-filter: none;
}
&-hidden {
opacity: 0;
transition: 300ms opacity;
}
}
.ReactionSelector {
position: absolute;
top: 0;
transform: translateY(calc(-100% - 0.5rem));
}
}
.ReactionSelector-hidden {
opacity: 0;
transition: 300ms opacity;
} }

View File

@ -1,14 +1,28 @@
import type { FC } from '../../../lib/teact/teact'; import type { FC } from '../../../lib/teact/teact';
import React, { memo, useState } from '../../../lib/teact/teact'; import React, {
memo, useEffect, useState,
} from '../../../lib/teact/teact';
import { getActions } from '../../../global';
import type {
ApiAvailableReaction,
ApiReaction,
} from '../../../api/types';
import type { IAnchorPosition } from '../../../types';
import buildClassName from '../../../util/buildClassName';
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment'; import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps'; import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
import useFlag from '../../../hooks/useFlag';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useMouseInside from '../../../hooks/useMouseInside'; import useMouseInside from '../../../hooks/useMouseInside';
import useOldLang from '../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
import Menu from '../../ui/Menu'; import Menu from '../../ui/Menu';
import MenuItem from '../../ui/MenuItem'; import MenuItem from '../../ui/MenuItem';
import ReactionSelector from '../message/reactions/ReactionSelector';
import './CustomSendMenu.scss'; import './CustomSendMenu.scss';
@ -21,10 +35,24 @@ export type OwnProps = {
onSendSilent?: NoneToVoidFunction; onSendSilent?: NoneToVoidFunction;
onSendSchedule?: NoneToVoidFunction; onSendSchedule?: NoneToVoidFunction;
onSendWhenOnline?: NoneToVoidFunction; onSendWhenOnline?: NoneToVoidFunction;
onRemoveEffect?: NoneToVoidFunction;
onClose: NoneToVoidFunction; onClose: NoneToVoidFunction;
onCloseAnimationEnd?: NoneToVoidFunction; onCloseAnimationEnd?: NoneToVoidFunction;
chatId?: string;
withEffects?: boolean;
hasCurrentEffect?: boolean;
effectReactions?: ApiReaction[];
allAvailableReactions?: ApiAvailableReaction[];
onToggleReaction?: (reaction: ApiReaction) => void;
canBuyPremium?: boolean;
isCurrentUserPremium?: boolean;
isInSavedMessages?: boolean;
isInStoryViewer?: boolean;
canPlayAnimatedEmojis?: boolean;
}; };
const ANIMATION_DURATION = 200;
const CustomSendMenu: FC<OwnProps> = ({ const CustomSendMenu: FC<OwnProps> = ({
isOpen, isOpen,
isOpenToBottom = false, isOpenToBottom = false,
@ -34,45 +62,117 @@ const CustomSendMenu: FC<OwnProps> = ({
onSendSilent, onSendSilent,
onSendSchedule, onSendSchedule,
onSendWhenOnline, onSendWhenOnline,
onRemoveEffect,
onClose, onClose,
onCloseAnimationEnd, onCloseAnimationEnd,
chatId,
withEffects,
hasCurrentEffect,
effectReactions,
allAvailableReactions,
onToggleReaction,
canBuyPremium,
isCurrentUserPremium,
isInSavedMessages,
isInStoryViewer,
canPlayAnimatedEmojis,
}) => { }) => {
const {
openEffectPicker,
} = getActions();
const [handleMouseEnter, handleMouseLeave] = useMouseInside(isOpen, onClose); const [handleMouseEnter, handleMouseLeave] = useMouseInside(isOpen, onClose);
const [displayScheduleUntilOnline, setDisplayScheduleUntilOnline] = useState(false); const [displayScheduleUntilOnline, setDisplayScheduleUntilOnline] = useState(false);
const lang = useOldLang(); const oldLang = useOldLang();
const lang = useLang();
const [areItemsHidden, hideItems, showItems] = useFlag();
useEffectWithPrevDeps(([prevIsOpen]) => { useEffectWithPrevDeps(([prevIsOpen]) => {
// Avoid context menu item shuffling when opened // Avoid context menu item shuffling when opened
if (isOpen && !prevIsOpen) { if (isOpen && !prevIsOpen) {
showItems();
setDisplayScheduleUntilOnline(Boolean(canScheduleUntilOnline)); setDisplayScheduleUntilOnline(Boolean(canScheduleUntilOnline));
} }
}, [isOpen, canScheduleUntilOnline]); }, [isOpen, canScheduleUntilOnline]);
const [isReady, markIsReady, unmarkIsReady] = useFlag();
const handleOpenMessageEffectsPicker = useLastCallback((position: IAnchorPosition) => {
hideItems();
if (chatId) openEffectPicker({ chatId, position });
});
useEffect(() => {
if (!isOpen) {
unmarkIsReady();
return;
}
setTimeout(() => {
markIsReady();
}, ANIMATION_DURATION);
}, [isOpen, markIsReady, unmarkIsReady]);
return ( return (
<Menu <Menu
isOpen={isOpen} isOpen={isOpen}
autoClose autoClose
positionX="right" positionX="right"
positionY={isOpenToBottom ? 'top' : 'bottom'} positionY={isOpenToBottom ? 'top' : 'bottom'}
className="CustomSendMenu with-menu-transitions" className={buildClassName(
'CustomSendMenu', 'fluid', 'with-menu-transitions', withEffects && 'with-effects',
)}
onClose={onClose} onClose={onClose}
onCloseAnimationEnd={onCloseAnimationEnd} onCloseAnimationEnd={onCloseAnimationEnd}
onMouseEnter={!IS_TOUCH_ENV ? handleMouseEnter : undefined} onMouseEnter={!IS_TOUCH_ENV ? handleMouseEnter : undefined}
onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined} onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined}
noCloseOnBackdrop={!IS_TOUCH_ENV} noCloseOnBackdrop={!IS_TOUCH_ENV}
> >
{onSendSilent && <MenuItem icon="mute" onClick={onSendSilent}>{lang('SendWithoutSound')}</MenuItem>}
{canSchedule && onSendSchedule && ( {withEffects && !isInStoryViewer && (
<MenuItem icon="schedule" onClick={onSendSchedule}> <ReactionSelector
{lang(isSavedMessages ? 'SetReminder' : 'ScheduleMessage')} allAvailableReactions={allAvailableReactions}
</MenuItem> effectReactions={effectReactions}
)} currentReactions={undefined}
{canSchedule && onSendSchedule && displayScheduleUntilOnline && ( onToggleReaction={onToggleReaction!}
<MenuItem icon="user-online" onClick={onSendWhenOnline}> isPrivate
{lang('SendWhenOnline')} isReady={isReady}
</MenuItem> canBuyPremium={canBuyPremium}
isCurrentUserPremium={isCurrentUserPremium}
isInSavedMessages={isInSavedMessages}
isForEffects
canPlayAnimatedEmojis={canPlayAnimatedEmojis}
onShowMore={handleOpenMessageEffectsPicker}
onClose={onClose}
className={buildClassName(areItemsHidden && 'ReactionSelector-hidden')}
/>
)} )}
<div
className={buildClassName(
'CustomSendMenu_items',
areItemsHidden && 'CustomSendMenu_items-hidden',
)}
dir={oldLang.isRtl ? 'rtl' : undefined}
>
{onSendSilent && <MenuItem icon="mute" onClick={onSendSilent}>{oldLang('SendWithoutSound')}</MenuItem>}
{canSchedule && onSendSchedule && (
<MenuItem icon="schedule" onClick={onSendSchedule}>
{oldLang(isSavedMessages ? 'SetReminder' : 'ScheduleMessage')}
</MenuItem>
)}
{canSchedule && onSendSchedule && displayScheduleUntilOnline && (
<MenuItem icon="user-online" onClick={onSendWhenOnline}>
{oldLang('SendWhenOnline')}
</MenuItem>
)}
{withEffects && hasCurrentEffect && (
<MenuItem icon="delete" onClick={onRemoveEffect}>
{lang('RemoveEffect')}
</MenuItem>
)}
</div>
</Menu> </Menu>
); );
}; };

View File

@ -11,7 +11,11 @@
--symbol-set-gap-size: 0.25rem; --symbol-set-gap-size: 0.25rem;
position: relative; position: relative;
height: calc(100% - 3rem); height: 100%;
&.hasHeader {
height: calc(100% - 3rem);
}
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;

View File

@ -10,6 +10,8 @@ import type { StickerSetOrReactionsSetOrRecent, ThreadId } from '../../../types'
import { import {
CHAT_STICKER_SET_ID, CHAT_STICKER_SET_ID,
EFFECT_EMOJIS_SET_ID,
EFFECT_STICKERS_SET_ID,
FAVORITE_SYMBOL_SET_ID, FAVORITE_SYMBOL_SET_ID,
RECENT_SYMBOL_SET_ID, RECENT_SYMBOL_SET_ID,
SLIDE_TRANSITION_DURATION, SLIDE_TRANSITION_DURATION,
@ -57,12 +59,15 @@ type OwnProps = {
onStickerSelect: ( onStickerSelect: (
sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean, canUpdateStickerSetsOrder?: boolean, sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean, canUpdateStickerSetsOrder?: boolean,
) => void; ) => void;
isForEffects?: boolean;
}; };
type StateProps = { type StateProps = {
chat?: ApiChat; chat?: ApiChat;
recentStickers: ApiSticker[]; recentStickers: ApiSticker[];
favoriteStickers: ApiSticker[]; favoriteStickers: ApiSticker[];
effectStickers?: ApiSticker[];
effectEmojis?: ApiSticker[];
stickerSetsById: Record<string, ApiStickerSet>; stickerSetsById: Record<string, ApiStickerSet>;
chatStickerSetId?: string; chatStickerSetId?: string;
addedSetIds?: string[]; addedSetIds?: string[];
@ -83,6 +88,8 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
canSendStickers, canSendStickers,
recentStickers, recentStickers,
favoriteStickers, favoriteStickers,
effectStickers,
effectEmojis,
addedSetIds, addedSetIds,
stickerSetsById, stickerSetsById,
chatStickerSetId, chatStickerSetId,
@ -92,6 +99,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
noContextMenus, noContextMenus,
idPrefix, idPrefix,
onStickerSelect, onStickerSelect,
isForEffects,
}) => { }) => {
const { const {
loadRecentStickers, loadRecentStickers,
@ -113,7 +121,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
isAtBeginning: shouldHideTopBorder, isAtBeginning: shouldHideTopBorder,
} = useScrolledState(); } = useScrolledState();
const sendMessageAction = useSendMessageAction(chat!.id, threadId); const sendMessageAction = useSendMessageAction(chat?.id, threadId);
const prefix = `${idPrefix}-sticker-set`; const prefix = `${idPrefix}-sticker-set`;
const { const {
@ -130,6 +138,30 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
const areAddedLoaded = Boolean(addedSetIds); const areAddedLoaded = Boolean(addedSetIds);
const allSets = useMemo(() => { const allSets = useMemo(() => {
if (isForEffects && effectStickers) {
const effectSets: StickerSetOrReactionsSetOrRecent[] = [];
if (effectEmojis?.length) {
effectSets.push({
id: EFFECT_EMOJIS_SET_ID,
accessHash: '0',
title: '',
stickers: effectEmojis,
count: effectEmojis.length,
isEmoji: true,
});
}
if (effectStickers?.length) {
effectSets.push({
id: EFFECT_STICKERS_SET_ID,
accessHash: '0',
title: lang('StickerEffects'),
stickers: effectStickers,
count: effectStickers.length,
});
}
return effectSets;
}
if (!addedSetIds) { if (!addedSetIds) {
return MEMO_EMPTY_ARRAY; return MEMO_EMPTY_ARRAY;
} }
@ -167,7 +199,17 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
...defaultSets, ...defaultSets,
...existingAddedSetIds, ...existingAddedSetIds,
]; ];
}, [addedSetIds, stickerSetsById, favoriteStickers, recentStickers, chatStickerSetId, lang]); }, [
addedSetIds,
stickerSetsById,
favoriteStickers,
recentStickers,
chatStickerSetId,
lang,
effectStickers,
isForEffects,
effectEmojis,
]);
const noPopulatedSets = useMemo(() => ( const noPopulatedSets = useMemo(() => (
areAddedLoaded areAddedLoaded
@ -182,7 +224,8 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
}, [canSendStickers, loadAndPlay, loadRecentStickers, sendMessageAction]); }, [canSendStickers, loadAndPlay, loadRecentStickers, sendMessageAction]);
const canRenderContents = useAsyncRendering([], SLIDE_TRANSITION_DURATION); const canRenderContents = useAsyncRendering([], SLIDE_TRANSITION_DURATION);
const shouldRenderContents = areAddedLoaded && canRenderContents && !noPopulatedSets && canSendStickers; const shouldRenderContents = areAddedLoaded && canRenderContents
&& !noPopulatedSets && (canSendStickers || isForEffects);
useHorizontalScroll(headerRef, !shouldRenderContents || !headerRef.current); useHorizontalScroll(headerRef, !shouldRenderContents || !headerRef.current);
@ -224,6 +267,8 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
removeRecentSticker({ sticker }); removeRecentSticker({ sticker });
}); });
if (!chat) return undefined;
function renderCover(stickerSet: StickerSetOrReactionsSetOrRecent, index: number) { function renderCover(stickerSet: StickerSetOrReactionsSetOrRecent, index: number) {
const firstSticker = stickerSet.stickers?.[0]; const firstSticker = stickerSet.stickers?.[0];
const buttonClassName = buildClassName(styles.stickerCover, index === activeSetIndex && styles.activated); const buttonClassName = buildClassName(styles.stickerCover, index === activeSetIndex && styles.activated);
@ -290,7 +335,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
if (!shouldRenderContents) { if (!shouldRenderContents) {
return ( return (
<div className={fullClassName}> <div className={fullClassName}>
{!canSendStickers ? ( {!canSendStickers && !isForEffects ? (
<div className={styles.pickerDisabled}>{lang('ErrorSendRestrictedStickersAll')}</div> <div className={styles.pickerDisabled}>{lang('ErrorSendRestrictedStickersAll')}</div>
) : noPopulatedSets ? ( ) : noPopulatedSets ? (
<div className={styles.pickerDisabled}>{lang('NoStickers')}</div> <div className={styles.pickerDisabled}>{lang('NoStickers')}</div>
@ -309,17 +354,25 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
return ( return (
<div className={fullClassName}> <div className={fullClassName}>
<div ref={headerRef} className={headerClassName}> { !isForEffects && (
<div className="shared-canvas-container"> <div ref={headerRef} className={headerClassName}>
<canvas ref={sharedCanvasRef} className="shared-canvas" /> <div className="shared-canvas-container">
{allSets.map(renderCover)} <canvas ref={sharedCanvasRef} className="shared-canvas" />
{allSets.map(renderCover)}
</div>
</div> </div>
</div> ) }
<div <div
ref={containerRef} ref={containerRef}
onMouseMove={handleMouseMove} onMouseMove={handleMouseMove}
onScroll={handleContentScroll} onScroll={handleContentScroll}
className={buildClassName(styles.main, IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll')} className={
buildClassName(
styles.main,
IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll',
!isForEffects && styles.hasHeader,
)
}
> >
{allSets.map((stickerSet, i) => ( {allSets.map((stickerSet, i) => (
<StickerSet <StickerSet
@ -343,6 +396,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
onStickerFave={handleStickerFave} onStickerFave={handleStickerFave}
onStickerRemoveRecent={handleRemoveRecentSticker} onStickerRemoveRecent={handleRemoveRecentSticker}
forcePlayback forcePlayback
shouldHideHeader={stickerSet.id === EFFECT_EMOJIS_SET_ID}
/> />
))} ))}
</div> </div>
@ -357,6 +411,7 @@ export default memo(withGlobal<OwnProps>(
added, added,
recent, recent,
favorite, favorite,
effect,
} = global.stickers; } = global.stickers;
const isSavedMessages = selectIsChatWithSelf(global, chatId); const isSavedMessages = selectIsChatWithSelf(global, chatId);
@ -365,6 +420,8 @@ export default memo(withGlobal<OwnProps>(
return { return {
chat, chat,
effectStickers: effect?.stickers,
effectEmojis: effect?.emojis,
recentStickers: recent.stickers, recentStickers: recent.stickers,
favoriteStickers: favorite.stickers, favoriteStickers: favorite.stickers,
stickerSetsById: setsById, stickerSetsById: setsById,

View File

@ -270,6 +270,10 @@
} }
} }
.effect-emojis.symbol-set-container {
--emoji-size: 2.25rem;
}
.symbol-set-container { .symbol-set-container {
display: grid !important; display: grid !important;
justify-content: space-between; justify-content: space-between;

View File

@ -1115,10 +1115,11 @@ const Message: FC<OwnProps & StateProps> = ({
activeEmojiInteractions={activeEmojiInteractions} activeEmojiInteractions={activeEmojiInteractions}
/> />
)} )}
{withAnimatedEffects && effect && ( {withAnimatedEffects && effect && !isLocal && (
<MessageEffect <MessageEffect
shouldPlay={shouldPlayEffect} shouldPlay={shouldPlayEffect}
message={message} messageId={message.id}
isMirrored={!message.isOutgoing}
effect={effect} effect={effect}
observeIntersectionForLoading={observeIntersectionForLoading} observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying} observeIntersectionForPlaying={observeIntersectionForPlaying}

View File

@ -1,6 +1,6 @@
import React, { memo, useEffect, useRef } from '../../../lib/teact/teact'; import React, { memo, useEffect, useRef } from '../../../lib/teact/teact';
import type { ApiAvailableEffect, ApiMessage } from '../../../api/types'; import type { ApiAvailableEffect } from '../../../api/types';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
@ -16,18 +16,20 @@ import Portal from '../../ui/Portal';
import styles from './MessageEffect.module.scss'; import styles from './MessageEffect.module.scss';
type OwnProps = { type OwnProps = {
message: ApiMessage; messageId?: number;
isMirrored?: boolean;
effect: ApiAvailableEffect; effect: ApiAvailableEffect;
shouldPlay?: boolean; shouldPlay?: boolean;
observeIntersectionForLoading: ObserveFn; observeIntersectionForLoading?: ObserveFn;
observeIntersectionForPlaying: ObserveFn; observeIntersectionForPlaying?: ObserveFn;
onStop?: VoidFunction; onStop?: VoidFunction;
}; };
const EFFECT_SIZE = 256; const EFFECT_SIZE = 256;
const MessageEffect = ({ const MessageEffect = ({
message, messageId,
isMirrored,
effect, effect,
shouldPlay, shouldPlay,
observeIntersectionForLoading, observeIntersectionForLoading,
@ -42,31 +44,37 @@ const MessageEffect = ({
const canPlay = useIsIntersecting(anchorRef, observeIntersectionForPlaying); const canPlay = useIsIntersecting(anchorRef, observeIntersectionForPlaying);
const [isPlaying, startPlaying, stopPlaying] = useFlag(); const [isPlaying, startPlaying, stopPlaying] = useFlag();
const [isPositionUpdateRequired, requirePositionUpdate, resetPositionUpdate] = useFlag();
const effectHash = getEffectHash(effect); const effectHash = getEffectHash(effect);
const effectBlob = useMedia(effectHash, !canLoad); const effectBlob = useMedia(effectHash, !canLoad);
const isMirrored = !message.isOutgoing;
const handleEnded = useLastCallback(() => { const handleEnded = useLastCallback(() => {
stopPlaying(); stopPlaying();
onStop?.(); onStop?.();
}); });
useEffect(() => { const updatePosition = useOverlayPosition({
if (canPlay && shouldPlay && effectBlob) {
startPlaying();
}
}, [canPlay, effectBlob, shouldPlay]);
useOverlayPosition({
anchorRef, anchorRef,
overlayRef: ref, overlayRef: ref,
isMirrored, isMirrored,
isDisabled: !isPlaying, isDisabled: !isPlaying,
isForMessageEffect: true, isForMessageEffect: true,
id: effect.id,
}); });
useEffect(() => {
if (isPositionUpdateRequired) updatePosition();
resetPositionUpdate();
}, [updatePosition, resetPositionUpdate, isPositionUpdateRequired]);
useEffect(() => {
if (canPlay && shouldPlay && effectBlob) {
startPlaying();
requirePositionUpdate();
}
}, [canPlay, effectBlob, shouldPlay, updatePosition]);
const effectClassName = buildClassName( const effectClassName = buildClassName(
styles.root, styles.root,
isMirrored && styles.mirror, isMirrored && styles.mirror,
@ -78,7 +86,7 @@ const MessageEffect = ({
<Portal> <Portal>
<AnimatedSticker <AnimatedSticker
ref={ref} ref={ref}
key={`effect-${message.id}`} key={`effect-${messageId ?? effect.id}`}
className={effectClassName} className={effectClassName}
tgsUrl={effectBlob} tgsUrl={effectBlob}
size={EFFECT_SIZE} size={EFFECT_SIZE}

View File

@ -14,12 +14,14 @@ export default function useOverlayPosition({
isMirrored, isMirrored,
isForMessageEffect, isForMessageEffect,
isDisabled, isDisabled,
id,
} : { } : {
anchorRef: RefObject<HTMLDivElement>; anchorRef: RefObject<HTMLDivElement>;
overlayRef: RefObject<HTMLDivElement>; overlayRef: RefObject<HTMLDivElement>;
isMirrored?: boolean; isMirrored?: boolean;
isForMessageEffect?: boolean; isForMessageEffect?: boolean;
isDisabled?: boolean; isDisabled?: boolean;
id?: string;
}) { }) {
const updatePosition = useLastCallback(() => { const updatePosition = useLastCallback(() => {
const element = overlayRef.current; const element = overlayRef.current;
@ -49,12 +51,13 @@ export default function useOverlayPosition({
useEffect(() => { useEffect(() => {
if (isDisabled) return; if (isDisabled) return;
updatePosition(); updatePosition();
}, [isDisabled]); }, [isDisabled, id]);
useEffect(() => { useEffect(() => {
if (isDisabled) return undefined; if (isDisabled) return undefined;
const messagesContainer = anchorRef.current!.closest<HTMLDivElement>('.MessageList')!; const messagesContainer = anchorRef.current!.closest<HTMLDivElement>('.MessageList')!;
if (!messagesContainer) return undefined;
messagesContainer.addEventListener('scroll', updatePosition, { passive: true }); messagesContainer.addEventListener('scroll', updatePosition, { passive: true });
@ -62,4 +65,6 @@ export default function useOverlayPosition({
messagesContainer.removeEventListener('scroll', updatePosition); messagesContainer.removeEventListener('scroll', updatePosition);
}; };
}, [isDisabled, anchorRef]); }, [isDisabled, anchorRef]);
return updatePosition;
} }

View File

@ -2,11 +2,13 @@ import type { FC } from '../../../../lib/teact/teact';
import React, { memo, useMemo, useRef } from '../../../../lib/teact/teact'; import React, { memo, useMemo, useRef } from '../../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../../global'; import { getActions, getGlobal, withGlobal } from '../../../../global';
import type {
ApiMessage, ApiMessageEntity,
ApiReaction, ApiReactionCustomEmoji, ApiSticker, ApiStory, ApiStorySkipped,
} from '../../../../api/types';
import type { IAnchorPosition } from '../../../../types'; import type { IAnchorPosition } from '../../../../types';
import {
type ApiAvailableEffect,
type ApiMessage, type ApiMessageEntity,
type ApiReaction, type ApiReactionCustomEmoji, type ApiSticker, type ApiStory, type ApiStorySkipped,
MAIN_THREAD_ID,
} from '../../../../api/types';
import { getReactionKey, getStoryKey, isUserId } from '../../../../global/helpers'; import { getReactionKey, getStoryKey, isUserId } from '../../../../global/helpers';
import { import {
@ -26,6 +28,7 @@ import useOldLang from '../../../../hooks/useOldLang';
import CustomEmojiPicker from '../../../common/CustomEmojiPicker'; import CustomEmojiPicker from '../../../common/CustomEmojiPicker';
import Menu from '../../../ui/Menu'; import Menu from '../../../ui/Menu';
import StickerPicker from '../../composer/StickerPicker';
import ReactionPickerLimited from './ReactionPickerLimited'; import ReactionPickerLimited from './ReactionPickerLimited';
import styles from './ReactionPicker.module.scss'; import styles from './ReactionPicker.module.scss';
@ -42,6 +45,9 @@ interface StateProps {
position?: IAnchorPosition; position?: IAnchorPosition;
isTranslucent?: boolean; isTranslucent?: boolean;
sendAsMessage?: boolean; sendAsMessage?: boolean;
chatId?: string;
isForEffects?: boolean;
availableEffectById: Record<string, ApiAvailableEffect>;
} }
const FULL_PICKER_SHIFT_DELTA = { x: -23, y: -64 }; const FULL_PICKER_SHIFT_DELTA = { x: -23, y: -64 };
@ -57,9 +63,13 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
isCurrentUserPremium, isCurrentUserPremium,
withCustomReactions, withCustomReactions,
sendAsMessage, sendAsMessage,
chatId,
isForEffects,
availableEffectById,
}) => { }) => {
const { const {
toggleReaction, closeReactionPicker, sendMessage, showNotification, sendStoryReaction, toggleReaction, closeReactionPicker, sendMessage, showNotification, sendStoryReaction, saveEffectInDraft,
requestEffectInComposer,
} = getActions(); } = getActions();
const lang = useOldLang(); const lang = useOldLang();
@ -171,6 +181,16 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
closeReactionPicker(); closeReactionPicker();
}); });
const handleStickerSelect = useLastCallback((sticker: ApiSticker) => {
const availableEffects = Object.values(availableEffectById);
const effectId = availableEffects.find((effect) => effect.effectStickerId === sticker.id)?.id;
if (chatId) saveEffectInDraft({ chatId, threadId: MAIN_THREAD_ID, effectId });
if (effectId) requestEffectInComposer({ });
closeReactionPicker();
});
const selectedReactionIds = useMemo(() => { const selectedReactionIds = useMemo(() => {
return (message?.reactions?.results || []).reduce<string[]>((acc, { chosenOrder, reaction }) => { return (message?.reactions?.results || []).reduce<string[]>((acc, { chosenOrder, reaction }) => {
if (chosenOrder !== undefined) { if (chosenOrder !== undefined) {
@ -201,25 +221,42 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
backdropExcludedSelector=".Modal.confirm" backdropExcludedSelector=".Modal.confirm"
onClose={closeReactionPicker} onClose={closeReactionPicker}
> >
<CustomEmojiPicker {isForEffects && chatId ? (
chatId={renderedChatId} <StickerPicker
idPrefix="message-emoji-set-" className=""
isHidden={!isOpen || !(withCustomReactions || renderedStoryId)} isHidden={!isOpen}
loadAndPlay={Boolean(isOpen && withCustomReactions)} loadAndPlay={Boolean(isOpen && withCustomReactions)}
isReactionPicker idPrefix="message-effect"
className={!withCustomReactions && !renderedStoryId ? styles.hidden : undefined} canSendStickers={false}
selectedReactionIds={selectedReactionIds} noContextMenus={false}
isTranslucent={isTranslucent} chatId={chatId}
onCustomEmojiSelect={renderedStoryId ? handleStoryReactionSelect : handleToggleCustomReaction} isTranslucent={isTranslucent}
onReactionSelect={renderedStoryId ? handleStoryReactionSelect : handleToggleReaction} onStickerSelect={handleStickerSelect}
/> isForEffects={isForEffects}
{!withCustomReactions && Boolean(renderedChatId) && (
<ReactionPickerLimited
chatId={renderedChatId}
loadAndPlay={isOpen}
onReactionSelect={renderedStoryId ? handleStoryReactionSelect : handleToggleReaction}
selectedReactionIds={selectedReactionIds}
/> />
) : (
<>
<CustomEmojiPicker
chatId={renderedChatId}
idPrefix="message-emoji-set-"
isHidden={!isOpen || !(withCustomReactions || renderedStoryId)}
loadAndPlay={Boolean(isOpen && withCustomReactions)}
isReactionPicker
className={!withCustomReactions && !renderedStoryId ? styles.hidden : undefined}
selectedReactionIds={selectedReactionIds}
isTranslucent={isTranslucent}
onCustomEmojiSelect={renderedStoryId ? handleStoryReactionSelect : handleToggleCustomReaction}
onReactionSelect={renderedStoryId ? handleStoryReactionSelect : handleToggleReaction}
/>
{!withCustomReactions && Boolean(renderedChatId) && (
<ReactionPickerLimited
chatId={renderedChatId}
loadAndPlay={isOpen}
onReactionSelect={renderedStoryId ? handleStoryReactionSelect : handleToggleReaction}
selectedReactionIds={selectedReactionIds}
/>
)}
</>
)} )}
</Menu> </Menu>
); );
@ -227,8 +264,9 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>((global): StateProps => { export default memo(withGlobal<OwnProps>((global): StateProps => {
const state = selectTabState(global); const state = selectTabState(global);
const availableEffectById = global.availableEffectById;
const { const {
chatId, messageId, storyPeerId, storyId, position, sendAsMessage, chatId, messageId, storyPeerId, storyId, position, sendAsMessage, isForEffects,
} = state.reactionPicker || {}; } = state.reactionPicker || {};
const story = storyPeerId && storyId const story = storyPeerId && storyId
? selectPeerStory(global, storyPeerId, storyId) as ApiStory | ApiStorySkipped ? selectPeerStory(global, storyPeerId, storyId) as ApiStory | ApiStorySkipped
@ -251,6 +289,9 @@ export default memo(withGlobal<OwnProps>((global): StateProps => {
isTranslucent: selectIsContextMenuTranslucent(global), isTranslucent: selectIsContextMenuTranslucent(global),
isCurrentUserPremium: selectIsCurrentUserPremium(global), isCurrentUserPremium: selectIsCurrentUserPremium(global),
sendAsMessage, sendAsMessage,
isForEffects,
chatId,
availableEffectById,
}; };
})(ReactionPicker)); })(ReactionPicker));

View File

@ -27,6 +27,7 @@ type OwnProps = {
isPrivate?: boolean; isPrivate?: boolean;
topReactions?: ApiReaction[]; topReactions?: ApiReaction[];
defaultTagReactions?: ApiReaction[]; defaultTagReactions?: ApiReaction[];
effectReactions?: ApiReaction[];
allAvailableReactions?: ApiAvailableReaction[]; allAvailableReactions?: ApiAvailableReaction[];
currentReactions?: ApiReactionCount[]; currentReactions?: ApiReactionCount[];
maxUniqueReactions?: number; maxUniqueReactions?: number;
@ -37,6 +38,7 @@ type OwnProps = {
className?: string; className?: string;
isInSavedMessages?: boolean; isInSavedMessages?: boolean;
isInStoryViewer?: boolean; isInStoryViewer?: boolean;
isForEffects?: boolean;
onClose?: NoneToVoidFunction; onClose?: NoneToVoidFunction;
onToggleReaction: (reaction: ApiReaction) => void; onToggleReaction: (reaction: ApiReaction) => void;
onShowMore: (position: IAnchorPosition) => void; onShowMore: (position: IAnchorPosition) => void;
@ -60,6 +62,8 @@ const ReactionSelector: FC<OwnProps> = ({
isCurrentUserPremium, isCurrentUserPremium,
isInSavedMessages, isInSavedMessages,
isInStoryViewer, isInStoryViewer,
isForEffects,
effectReactions,
onClose, onClose,
onToggleReaction, onToggleReaction,
onShowMore, onShowMore,
@ -72,12 +76,15 @@ const ReactionSelector: FC<OwnProps> = ({
const areReactionsLocked = isInSavedMessages && !isCurrentUserPremium && !isInStoryViewer; const areReactionsLocked = isInSavedMessages && !isCurrentUserPremium && !isInStoryViewer;
const availableReactions = useMemo(() => { const availableReactions = useMemo(() => {
const reactions = isInSavedMessages ? defaultTagReactions const reactions = isForEffects ? effectReactions : isInSavedMessages ? defaultTagReactions
: (enabledReactions?.type === 'some' ? enabledReactions.allowed : (enabledReactions?.type === 'some' ? enabledReactions.allowed
: allAvailableReactions?.map((reaction) => reaction.reaction)); : allAvailableReactions?.map((reaction) => reaction.reaction));
const filteredReactions = reactions?.map((reaction) => { const filteredReactions = reactions?.map((reaction) => {
const isCustomReaction = 'documentId' in reaction; const isCustomReaction = 'documentId' in reaction;
const availableReaction = allAvailableReactions?.find((r) => isSameReaction(r.reaction, reaction)); const availableReaction = allAvailableReactions?.find((r) => isSameReaction(r.reaction, reaction));
if (isForEffects) return availableReaction;
if ((!isCustomReaction && !availableReaction) || availableReaction?.isInactive) return undefined; if ((!isCustomReaction && !availableReaction) || availableReaction?.isInactive) return undefined;
if (!isPrivate && (!enabledReactions || !canSendReaction(reaction, enabledReactions))) { if (!isPrivate && (!enabledReactions || !canSendReaction(reaction, enabledReactions))) {
@ -95,7 +102,7 @@ const ReactionSelector: FC<OwnProps> = ({
return sortReactions(filteredReactions, topReactions); return sortReactions(filteredReactions, topReactions);
}, [ }, [
allAvailableReactions, currentReactions, defaultTagReactions, enabledReactions, isInSavedMessages, isPrivate, allAvailableReactions, currentReactions, defaultTagReactions, enabledReactions, isInSavedMessages, isPrivate,
maxUniqueReactions, topReactions, maxUniqueReactions, topReactions, isForEffects, effectReactions,
]); ]);
const reactionsToRender = useMemo(() => { const reactionsToRender = useMemo(() => {
@ -151,8 +158,12 @@ const ReactionSelector: FC<OwnProps> = ({
return lang('StoryReactionsHint'); return lang('StoryReactionsHint');
} }
if (isForEffects) {
return lang('AddEffectMessageHint');
}
return undefined; return undefined;
}, [isCurrentUserPremium, isInSavedMessages, isInStoryViewer, lang]); }, [isCurrentUserPremium, isInSavedMessages, isInStoryViewer, lang, isForEffects]);
if (!reactionsToRender.length) return undefined; if (!reactionsToRender.length) return undefined;

View File

@ -206,6 +206,8 @@ export const TOP_SYMBOL_SET_ID = 'top';
export const POPULAR_SYMBOL_SET_ID = 'popular'; export const POPULAR_SYMBOL_SET_ID = 'popular';
export const RECENT_SYMBOL_SET_ID = 'recent'; export const RECENT_SYMBOL_SET_ID = 'recent';
export const FAVORITE_SYMBOL_SET_ID = 'favorite'; export const FAVORITE_SYMBOL_SET_ID = 'favorite';
export const EFFECT_STICKERS_SET_ID = 'effectStickers';
export const EFFECT_EMOJIS_SET_ID = 'effectEmojis';
export const CHAT_STICKER_SET_ID = 'chatStickers'; export const CHAT_STICKER_SET_ID = 'chatStickers';
export const DEFAULT_TOPIC_ICON_STICKER_ID = 'topic-default-icon'; export const DEFAULT_TOPIC_ICON_STICKER_ID = 'topic-default-icon';
export const DEFAULT_STATUS_ICON_ID = 'status-default-icon'; export const DEFAULT_STATUS_ICON_ID = 'status-default-icon';

View File

@ -4,7 +4,8 @@ import type {
} from '../../../api/types'; } from '../../../api/types';
import type { RequiredGlobalActions } from '../../index'; import type { RequiredGlobalActions } from '../../index';
import type { import type {
ActionReturnType, ChatListType, GlobalState, TabArgs, ActionReturnType, ApiDraft,
ChatListType, GlobalState, TabArgs,
} from '../../types'; } from '../../types';
import { MAIN_THREAD_ID } from '../../../api/types'; import { MAIN_THREAD_ID } from '../../../api/types';
import { import {
@ -2824,9 +2825,18 @@ async function loadChats(
if (!draft && !thread) return; if (!draft && !thread) return;
if (!selectDraft(global, chatId, MAIN_THREAD_ID)?.isLocal) { const currentDraft = selectDraft(global, chatId, MAIN_THREAD_ID);
// Temporary workaround until the layer is updated
if (!currentDraft?.isLocal) {
const effectId = currentDraft?.effectId;
const newDraft: ApiDraft = effectId ? {
...draft,
effectId,
} : draft;
global = replaceThreadParam( global = replaceThreadParam(
global, chatId, MAIN_THREAD_ID, 'draft', draft, global, chatId, MAIN_THREAD_ID, 'draft', newDraft,
); );
} }
}); });

View File

@ -525,6 +525,7 @@ addActionHandler('saveDraft', (global, actions, payload): ActionReturnType => {
const newDraft: ApiDraft = { const newDraft: ApiDraft = {
text, text,
replyInfo: currentDraft?.replyInfo, replyInfo: currentDraft?.replyInfo,
effectId: currentDraft?.effectId,
}; };
saveDraft({ saveDraft({
@ -600,6 +601,23 @@ addActionHandler('resetDraftReplyInfo', (global, actions, payload): ActionReturn
}); });
}); });
addActionHandler('saveEffectInDraft', (global, actions, payload): ActionReturnType => {
const {
chatId, threadId, effectId,
} = payload;
const currentDraft = selectDraft(global, chatId, threadId);
const newDraft = {
...currentDraft,
effectId,
};
saveDraft({
global, chatId, threadId, draft: newDraft, isLocalOnly: true, noLocalTimeUpdate: true,
});
});
async function saveDraft<T extends GlobalState>({ async function saveDraft<T extends GlobalState>({
global, chatId, threadId, draft, isLocalOnly, noLocalTimeUpdate, global, chatId, threadId, draft, isLocalOnly, noLocalTimeUpdate,
} : { } : {
@ -1410,6 +1428,7 @@ async function sendMessage<T extends GlobalState>(global: T, params: {
wasDrafted?: boolean; wasDrafted?: boolean;
lastMessageId?: number; lastMessageId?: number;
isInvertedMedia?: true; isInvertedMedia?: true;
effectId?: string;
}) { }) {
let currentMessageKey: MessageKey | undefined; let currentMessageKey: MessageKey | undefined;
const progressCallback = params.attachment ? (progress: number, messageKey: MessageKey) => { const progressCallback = params.attachment ? (progress: number, messageKey: MessageKey) => {

View File

@ -1,3 +1,4 @@
import type { ApiReactionEmoji } from '../../../api/types';
import type { ActionReturnType } from '../../types'; import type { ActionReturnType } from '../../types';
import { ApiMediaFormat } from '../../../api/types'; import { ApiMediaFormat } from '../../../api/types';
@ -83,12 +84,35 @@ addActionHandler('loadAvailableEffects', async (global): Promise<void> => {
return; return;
} }
const effectById = buildCollectionByKey(result, 'id'); const { effects, emojis, stickers } = result;
const reactions:ApiReactionEmoji[] = [];
const effectById = buildCollectionByKey(effects, 'id');
for (const effect of effects) {
if (effect.effectAnimationId) {
const reaction: ApiReactionEmoji = {
emoticon: effect.emoticon,
};
reactions.push(reaction);
}
}
global = getGlobal(); global = getGlobal();
global = { global = {
...global, ...global,
availableEffectById: effectById, availableEffectById: effectById,
stickers: {
...global.stickers,
effect: {
stickers,
emojis,
},
},
reactions: {
...global.reactions,
effectReactions: reactions,
},
}; };
setGlobal(global); setGlobal(global);
}); });

View File

@ -34,6 +34,7 @@ import {
selectChatMessages, selectChatMessages,
selectCommonBoxChatId, selectCommonBoxChatId,
selectCurrentMessageList, selectCurrentMessageList,
selectDraft,
selectIsChatListed, selectIsChatListed,
selectTabState, selectTabState,
selectThreadParam, selectThreadParam,
@ -452,7 +453,15 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
return undefined; return undefined;
} }
global = replaceThreadParam(global, chatId, threadId || MAIN_THREAD_ID, 'draft', draft); const currentDraft = selectDraft(global, chatId, threadId ?? MAIN_THREAD_ID);
// Temporary workaround until the layer is updated
const newDraft = currentDraft?.effectId ? {
...draft,
effectId: currentDraft?.effectId,
} : draft;
global = replaceThreadParam(global, chatId, threadId || MAIN_THREAD_ID, 'draft', newDraft);
global = updateChat(global, chatId, { draftDate: draft?.date }); global = updateChat(global, chatId, { draftDate: draft?.date });
return global; return global;
} }

View File

@ -36,6 +36,7 @@ addActionHandler('processOpenChatOrThread', (global, actions, payload): ActionRe
}, },
}, tabId); }, tabId);
} }
actions.hideEffectInComposer({ tabId });
if (!currentMessageList || ( if (!currentMessageList || (
currentMessageList.chatId !== chatId currentMessageList.chatId !== chatId

View File

@ -29,6 +29,7 @@ import {
selectChatMessage, selectChatMessage,
selectCurrentChat, selectCurrentChat,
selectCurrentMessageList, selectCurrentMessageList,
selectIsCurrentUserPremium,
selectIsTrustedBot, selectIsTrustedBot,
selectTabState, selectTabState,
} from '../../selectors'; } from '../../selectors';
@ -498,6 +499,51 @@ addActionHandler('updateAttachmentSettings', (global, actions, payload): ActionR
}; };
}); });
addActionHandler('requestEffectInComposer', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload;
return updateTabState(global, {
shouldPlayEffectInComposer: true,
}, tabId);
});
addActionHandler('hideEffectInComposer', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload;
return updateTabState(global, {
shouldPlayEffectInComposer: undefined,
}, tabId);
});
addActionHandler('setReactionEffect', (global, actions, payload): ActionReturnType => {
const {
chatId, threadId, reaction, tabId = getCurrentTabId(),
} = payload;
const emoticon = reaction && 'emoticon' in reaction && reaction.emoticon;
if (!emoticon) return;
const effect = Object.values(global.availableEffectById)
.find((currentEffect) => currentEffect.effectAnimationId && currentEffect.emoticon === emoticon);
const effectId = effect?.id;
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
if (effect?.isPremium && !isCurrentUserPremium) {
actions.openPremiumModal({
initialSection: 'animated_emoji',
tabId,
});
return;
}
if (!effectId) return;
actions.requestEffectInComposer({ tabId });
actions.saveEffectInDraft({ chatId, threadId, effectId });
});
addActionHandler('openLimitReachedModal', (global, actions, payload): ActionReturnType => { addActionHandler('openLimitReachedModal', (global, actions, payload): ActionReturnType => {
const { limit, tabId = getCurrentTabId() } = payload; const { limit, tabId = getCurrentTabId() } = payload;

View File

@ -62,6 +62,22 @@ addActionHandler('openStoryReactionPicker', (global, actions, payload): ActionRe
}, tabId); }, tabId);
}); });
addActionHandler('openEffectPicker', (global, actions, payload): ActionReturnType => {
const {
position,
chatId,
tabId = getCurrentTabId(),
} = payload!;
return updateTabState(global, {
reactionPicker: {
position,
chatId,
isForEffects: true,
},
}, tabId);
});
addActionHandler('closeReactionPicker', (global, actions, payload): ActionReturnType => { addActionHandler('closeReactionPicker', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {}; const { tabId = getCurrentTabId() } = payload || {};
const tabState = selectTabState(global, tabId); const tabState = selectTabState(global, tabId);
@ -73,6 +89,7 @@ addActionHandler('closeReactionPicker', (global, actions, payload): ActionReturn
position: undefined, position: undefined,
storyId: undefined, storyId: undefined,
storyPeerId: undefined, storyPeerId: undefined,
isForEffects: undefined,
}, },
}, tabId); }, tabId);
}); });

View File

@ -301,6 +301,7 @@ export function serializeGlobal<T extends GlobalState>(global: T) {
'defaultTags', 'defaultTags',
'recentReactions', 'recentReactions',
'topReactions', 'topReactions',
'effectReactions',
'hash', 'hash',
]), ]),
availableReactions: reduceAvailableReactions(global.reactions.availableReactions), availableReactions: reduceAvailableReactions(global.reactions.availableReactions),

View File

@ -160,6 +160,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
defaultTags: [], defaultTags: [],
topReactions: [], topReactions: [],
recentReactions: [], recentReactions: [],
effectReactions: [],
hash: {}, hash: {},
}, },
availableEffectById: {}, availableEffectById: {},
@ -182,6 +183,10 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
featured: { featured: {
setIds: [], setIds: [],
}, },
effect: {
stickers: [],
emojis: [],
},
forEmoji: {}, forEmoji: {},
}, },

View File

@ -343,8 +343,11 @@ export type TabState = {
storyId?: number; storyId?: number;
position?: IAnchorPosition; position?: IAnchorPosition;
sendAsMessage?: boolean; sendAsMessage?: boolean;
isForEffects?: boolean;
}; };
shouldPlayEffectInComposer?: true;
inlineBots: { inlineBots: {
isLoading: boolean; isLoading: boolean;
byUsername: Record<string, false | InlineBotSettings>; byUsername: Record<string, false | InlineBotSettings>;
@ -980,6 +983,7 @@ export type GlobalState = {
topReactions: ApiReaction[]; topReactions: ApiReaction[];
recentReactions: ApiReaction[]; recentReactions: ApiReaction[];
defaultTags: ApiReaction[]; defaultTags: ApiReaction[];
effectReactions: ApiReaction[];
availableReactions?: ApiAvailableReaction[]; availableReactions?: ApiAvailableReaction[];
hash: { hash: {
topReactions?: string; topReactions?: string;
@ -1020,6 +1024,10 @@ export type GlobalState = {
stickers?: ApiSticker[]; stickers?: ApiSticker[];
hash?: string; hash?: string;
}; };
effect: {
stickers: ApiSticker[];
emojis: ApiSticker[];
};
}; };
customEmojis: { customEmojis: {
@ -1140,6 +1148,7 @@ export type ApiDraft = {
text?: ApiFormattedText; text?: ApiFormattedText;
replyInfo?: ApiInputMessageReplyInfo; replyInfo?: ApiInputMessageReplyInfo;
date?: number; date?: number;
effectId?: string;
isLocal?: boolean; isLocal?: boolean;
}; };
@ -1482,6 +1491,7 @@ export interface ActionPayloads {
messageList?: MessageList; messageList?: MessageList;
isReaction?: true; // Reaction to the story are sent in the form of a message isReaction?: true; // Reaction to the story are sent in the form of a message
isInvertedMedia?: true; isInvertedMedia?: true;
effectId?: string;
} & WithTabId; } & WithTabId;
sendInviteMessages: { sendInviteMessages: {
chatId: string; chatId: string;
@ -2318,6 +2328,10 @@ export interface ActionPayloads {
reaction?: ApiReaction; reaction?: ApiReaction;
} & WithTabId; } & WithTabId;
openEffectPicker: {
chatId: string;
position: IAnchorPosition;
} & WithTabId;
openMessageReactionPicker: { openMessageReactionPicker: {
chatId: string; chatId: string;
messageId: number; messageId: number;
@ -2918,6 +2932,21 @@ export interface ActionPayloads {
isInvertedMedia?: true; isInvertedMedia?: true;
}; };
saveEffectInDraft: {
chatId: string;
threadId: ThreadId;
effectId?: string;
};
setReactionEffect: {
chatId: string;
threadId: ThreadId;
reaction?: ApiReaction;
} & WithTabId;
requestEffectInComposer: WithTabId;
hideEffectInComposer: WithTabId;
updateArchiveSettings: { updateArchiveSettings: {
isMinimized?: boolean; isMinimized?: boolean;
isHidden?: boolean; isHidden?: boolean;

View File

@ -1511,7 +1511,7 @@ export interface LangPair {
'MenuBetaChangelog': undefined; 'MenuBetaChangelog': undefined;
'MenuSwitchToK': undefined; 'MenuSwitchToK': undefined;
'MenuInstallApp': undefined; 'MenuInstallApp': undefined;
'RemoveEffect' : undefined;
} }
export type LangKey = keyof LangPair; export type LangKey = keyof LangPair;