From 4768a8103c699e18b6abc63b1848a85745dea488 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Mon, 15 Jul 2024 15:50:56 +0200 Subject: [PATCH] Message: Send effects (#4727) --- src/api/gramjs/apiBuilders/messages.ts | 2 + src/api/gramjs/apiBuilders/symbols.ts | 5 +- src/api/gramjs/methods/messages.ts | 5 + src/api/gramjs/methods/reactions.ts | 26 +++- src/assets/localization/fallback.strings | 1 + src/components/common/Composer.scss | 11 ++ src/components/common/Composer.tsx | 79 ++++++++++- src/components/common/CustomEmojiPicker.tsx | 1 + src/components/common/StickerButton.scss | 11 +- src/components/common/StickerButton.tsx | 8 +- src/components/common/StickerSet.tsx | 18 ++- .../middle/composer/CustomSendMenu.scss | 41 ++++++ .../middle/composer/CustomSendMenu.tsx | 126 ++++++++++++++++-- .../middle/composer/StickerPicker.module.scss | 6 +- .../middle/composer/StickerPicker.tsx | 77 +++++++++-- .../middle/composer/SymbolMenu.scss | 4 + src/components/middle/message/Message.tsx | 5 +- .../middle/message/MessageEffect.tsx | 38 +++--- .../message/hooks/useOverlayPosition.ts | 7 +- .../message/reactions/ReactionPicker.tsx | 89 +++++++++---- .../message/reactions/ReactionSelector.tsx | 17 ++- src/config.ts | 2 + src/global/actions/api/chats.ts | 16 ++- src/global/actions/api/messages.ts | 19 +++ src/global/actions/api/reactions.ts | 26 +++- src/global/actions/apiUpdaters/chats.ts | 11 +- src/global/actions/ui/chats.ts | 1 + src/global/actions/ui/misc.ts | 46 +++++++ src/global/actions/ui/reactions.ts | 17 +++ src/global/cache.ts | 1 + src/global/initialState.ts | 5 + src/global/types.ts | 29 ++++ src/types/language.d.ts | 2 +- 33 files changed, 663 insertions(+), 89 deletions(-) diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index c46e29f63..28cb4f61a 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -803,6 +803,7 @@ export function buildLocalMessage( sendAs?: ApiPeer, story?: ApiStory | ApiStorySkipped, isInvertedMedia?: true, + effectId?: string, ): ApiMessage { const localId = getNextLocalMessageId(lastMessageId); const media = attachment && buildUploadingMedia(attachment); @@ -838,6 +839,7 @@ export function buildLocalMessage( ...(scheduledAt && { isScheduled: true }), isForwardingAllowed: true, isInvertedMedia, + effectId, } satisfies ApiMessage; const emojiOnlyCount = getEmojiOnlyCountForMessage(message.content, message.groupedId); diff --git a/src/api/gramjs/apiBuilders/symbols.ts b/src/api/gramjs/apiBuilders/symbols.ts index 337281262..8a4abd80e 100644 --- a/src/api/gramjs/apiBuilders/symbols.ts +++ b/src/api/gramjs/apiBuilders/symbols.ts @@ -9,7 +9,8 @@ import { compact } from '../../../util/iteratees'; import localDb from '../localDb'; 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) { return undefined; } @@ -46,7 +47,7 @@ export function buildStickerFromDocument(document: GramJs.TypeDocument, isNoPrem const stickerOrEmojiAttribute = (stickerAttribute || customEmojiAttribute)!; const stickerSetInfo = buildApiStickerSetInfo(stickerOrEmojiAttribute?.stickerset); const emoji = stickerOrEmojiAttribute?.alt; - const isFree = Boolean(customEmojiAttribute?.free ?? true); + const isFree = Boolean(customEmojiAttribute?.free ?? true) && !isPremium; const cachedThumb = document.thumbs && document.thumbs.find( (s): s is GramJs.PhotoCachedSize => s instanceof GramJs.PhotoCachedSize, diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 5730273f1..2751dc7a6 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -1,3 +1,4 @@ +import BigInt from 'big-integer'; import { Api as GramJs } from '../../../lib/gramjs'; import type { ThreadId } from '../../../types'; @@ -270,6 +271,7 @@ export function sendMessage( shouldUpdateStickerSetOrder, wasDrafted, isInvertedMedia, + effectId, }: { chat: ApiChat; lastMessageId?: number; @@ -290,6 +292,7 @@ export function sendMessage( shouldUpdateStickerSetOrder?: boolean; wasDrafted?: boolean; isInvertedMedia?: true; + effectId?: string; }, onProgress?: ApiOnProgress, ) { @@ -309,6 +312,7 @@ export function sendMessage( sendAs, story, isInvertedMedia, + effectId, ); onUpdate({ @@ -396,6 +400,7 @@ export function sendMessage( ...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }), ...(shouldUpdateStickerSetOrder && { updateStickersetsOrder: shouldUpdateStickerSetOrder }), ...(isInvertedMedia && { invertMedia: isInvertedMedia }), + ...(effectId && { effect: BigInt(effectId) }), }), { shouldThrow: true, shouldIgnoreUpdates: true, diff --git a/src/api/gramjs/methods/reactions.ts b/src/api/gramjs/methods/reactions.ts index c595e3f2b..6d2743ca8 100644 --- a/src/api/gramjs/methods/reactions.ts +++ b/src/api/gramjs/methods/reactions.ts @@ -1,7 +1,9 @@ import BigInt from 'big-integer'; import { Api as GramJs } from '../../../lib/gramjs'; -import type { ApiChat, ApiReaction } from '../../types'; +import type { + ApiChat, ApiReaction, ApiSticker, +} from '../../types'; import { API_GENERAL_ID_LIMIT, @@ -18,6 +20,7 @@ import { buildApiSavedReactionTag, buildMessagePeerReaction, } from '../apiBuilders/reactions'; +import { buildStickerFromDocument } from '../apiBuilders/symbols'; import { buildApiUser } from '../apiBuilders/users'; import { buildInputPeer, buildInputReaction } from '../gramjsBuilders'; import { addEntitiesToLocalDb } from '../helpers'; @@ -103,13 +106,32 @@ export async function fetchAvailableEffects() { return undefined; } + const documentsMap = new Map(result.documents.map((doc) => [String(doc.id), doc])); + result.documents.forEach((document) => { if (document instanceof GramJs.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({ diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 763cf52d5..cca1c1a6e 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -1269,3 +1269,4 @@ "MenuBetaChangelog" = "Beta Changelog"; "MenuSwitchToK" = "Switch to K Version"; "MenuInstallApp" = "Install App"; +"RemoveEffect" = "Remove effect" diff --git a/src/components/common/Composer.scss b/src/components/common/Composer.scss index 8a7ab30d0..e1d787a96 100644 --- a/src/components/common/Composer.scss +++ b/src/components/common/Composer.scss @@ -30,6 +30,17 @@ --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 { from { /* stylelint-disable-next-line plugin/no-low-performance-animation-properties */ diff --git a/src/components/common/Composer.tsx b/src/components/common/Composer.tsx index 50ad384d4..36824c493 100644 --- a/src/components/common/Composer.tsx +++ b/src/components/common/Composer.tsx @@ -7,6 +7,7 @@ import { getActions, getGlobal, withGlobal } from '../../global'; import type { ApiAttachment, ApiAttachMenuPeerType, + ApiAvailableEffect, ApiAvailableReaction, ApiBotCommand, ApiBotInlineMediaResult, @@ -45,6 +46,7 @@ import { REPLIES_USER_ID, SCHEDULED_WHEN_ONLINE, SEND_MESSAGE_ACTION_INTERVAL, + SERVICE_NOTIFICATIONS_USER_ID, } from '../../config'; import { requestMeasure, requestNextMutation } from '../../lib/fasterdom/fasterdom'; import { @@ -78,6 +80,7 @@ import { selectNewestMessageWithBotKeyboardButtons, selectNoWebPage, selectPeerStory, + selectPerformanceSettingsValue, selectRequestedDraft, selectRequestedDraftFiles, selectScheduledIds, @@ -153,6 +156,7 @@ import SendAsMenu from '../middle/composer/SendAsMenu.async'; import StickerTooltip from '../middle/composer/StickerTooltip.async'; import SymbolMenuButton from '../middle/composer/SymbolMenuButton'; import WebPagePreview from '../middle/composer/WebPagePreview'; +import MessageEffect from '../middle/message/MessageEffect'; import ReactionSelector from '../middle/message/reactions/ReactionSelector'; import Button from '../ui/Button'; import ResponsiveHoverButton from '../ui/ResponsiveHoverButton'; @@ -253,6 +257,11 @@ type StateProps = canSendQuickReplies?: boolean; webPagePreview?: ApiWebPage; noWebPage?: boolean; + effect?: ApiAvailableEffect; + effectReactions?: ApiReaction[]; + areEffectsSupported?: boolean; + canPlayEffect?: boolean; + shouldPlayEffect?: boolean; }; enum MainButtonState { @@ -361,6 +370,11 @@ const Composer: FC = ({ onForward, webPagePreview, noWebPage, + effect, + effectReactions, + areEffectsSupported, + canPlayEffect, + shouldPlayEffect, }) => { const { sendMessage, @@ -384,6 +398,9 @@ const Composer: FC = ({ sendStoryReaction, editMessage, updateAttachmentSettings, + saveEffectInDraft, + setReactionEffect, + hideEffectInComposer, } = getActions(); const lang = useOldLang(); @@ -1023,11 +1040,15 @@ const Composer: FC = ({ const messageInput = document.querySelector(editableInputCssSelector); + const effectId = effect?.id; + if (text) { if (!checkSlowMode()) return; const isInvertedMedia = hasWebPagePreview ? attachmentSettings.isInvertedMedia : undefined; + if (areEffectsSupported) saveEffectInDraft({ chatId, threadId, effectId: undefined }); + sendMessage({ messageList: currentMessageList, text, @@ -1036,6 +1057,7 @@ const Composer: FC = ({ isSilent, shouldUpdateStickerSetOrder, isInvertedMedia, + effectId, }); } @@ -1077,7 +1099,7 @@ const Composer: FC = ({ }); const handleMessageSchedule = useLastCallback(( - args: ScheduledMessageArgs, scheduledAt: number, messageList: MessageList, + args: ScheduledMessageArgs, scheduledAt: number, messageList: MessageList, effectId?: string, ) => { if (args && 'queryId' in args) { const { id, queryId, isSilent } = args; @@ -1103,6 +1125,7 @@ const Composer: FC = ({ ...args, messageList, scheduledAt, + effectId, }); } }); @@ -1429,7 +1452,7 @@ const Composer: FC = ({ return; } requestCalendar((scheduledAt) => { - handleMessageSchedule({}, scheduledAt, currentMessageList); + handleMessageSchedule({}, scheduledAt, currentMessageList, effect?.id); }); break; default: @@ -1494,6 +1517,12 @@ const Composer: FC = ({ closeReactionPicker(); }); + const handleToggleEffectReaction = useLastCallback((reaction: ApiReaction) => { + setReactionEffect({ chatId, threadId, reaction }); + + closeReactionPicker(); + }); + const handleReactionPickerOpen = useLastCallback((position: IAnchorPosition) => { openStoryReactionPicker({ peerId: chatId, @@ -1524,7 +1553,7 @@ const Composer: FC = ({ }); const handleSendWhenOnline = useLastCallback(() => { - handleMessageSchedule({}, SCHEDULED_WHEN_ONLINE, currentMessageList!); + handleMessageSchedule({}, SCHEDULED_WHEN_ONLINE, currentMessageList!, effect?.id); }); const handleSendScheduledAttachments = useLastCallback( @@ -1541,6 +1570,10 @@ const Composer: FC = ({ }, ); + const handleRemoveEffect = useLastCallback(() => { saveEffectInDraft({ chatId, threadId, effectId: undefined }); }); + + const handleStopEffect = useLastCallback(() => { hideEffectInComposer({ }); }); + const onSend = useMemo(() => { switch (mainButtonState) { case MainButtonState.Edit: @@ -1555,6 +1588,8 @@ const Composer: FC = ({ const withBotCommands = isChatWithBot && botMenuButton?.type === 'commands' && !editingMessage && botCommands !== false && !activeVoiceRecording; + const effectEmoji = areEffectsSupported && effect?.emoticon; + return (
{isInMessageList && canAttachMedia && isReady && ( @@ -1984,6 +2019,18 @@ const Composer: FC = ({ {isInMessageList && } {isInMessageList && } + {effectEmoji && ( + + {renderText(effectEmoji)} + + )} + {effect && canPlayEffect && ( + + )} {canShowCustomSendMenu && ( = ({ onSendSilent={!isChatWithSelf ? handleSendSilent : undefined} onSendSchedule={!isInScheduledList ? handleSendScheduled : undefined} onSendWhenOnline={handleSendWhenOnline} + onRemoveEffect={handleRemoveEffect} onClose={handleContextMenuClose} onCloseAnimationEnd={handleContextMenuHide} isSavedMessages={isChatWithSelf} + chatId={chatId} + withEffects={areEffectsSupported} + hasCurrentEffect={Boolean(effect)} + effectReactions={effectReactions} + allAvailableReactions={availableReactions} + onToggleReaction={handleToggleEffectReaction} + isCurrentUserPremium={isCurrentUserPremium} + isInSavedMessages={isChatWithSelf} + isInStoryViewer={isInStoryViewer} + canPlayAnimatedEmojis={canPlayAnimatedEmojis} /> )} {calendar} @@ -2068,8 +2126,16 @@ export default memo(withGlobal( 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 { - availableReactions: type === 'story' ? global.reactions.availableReactions : undefined, + availableReactions: global.reactions.availableReactions, topReactions: type === 'story' ? global.reactions.topReactions : undefined, isOnActiveTab: !tabState.isBlurred, editingMessage: selectEditingMessage(global, chatId, threadId, messageListType), @@ -2137,6 +2203,11 @@ export default memo(withGlobal( canSendQuickReplies, noWebPage, webPagePreview: selectTabState(global).webPagePreview, + effect, + effectReactions, + areEffectsSupported, + canPlayEffect, + shouldPlayEffect, }; }, )(Composer)); diff --git a/src/components/common/CustomEmojiPicker.tsx b/src/components/common/CustomEmojiPicker.tsx index 2674835fe..bd05dca64 100644 --- a/src/components/common/CustomEmojiPicker.tsx +++ b/src/components/common/CustomEmojiPicker.tsx @@ -390,6 +390,7 @@ const CustomEmojiPicker: FC = ({ pickerStyles.main_customEmoji, IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll', pickerListClassName, + pickerStyles.hasHeader, ); return ( diff --git a/src/components/common/StickerButton.scss b/src/components/common/StickerButton.scss index 4a7894492..bcb630397 100644 --- a/src/components/common/StickerButton.scss +++ b/src/components/common/StickerButton.scss @@ -27,6 +27,12 @@ } } + &.effect-emoji .sticker-locked { + font-size: 0.75rem; + width: 0.875rem; + height: 0.875rem; + } + &.set-expand { padding: 0; vertical-align: bottom; @@ -38,8 +44,7 @@ .sticker-locked { position: absolute; bottom: 0; - left: 50%; - transform: translate(-50%, 50%); + right: 0; width: 1.25rem; height: 1.25rem; display: flex; @@ -47,6 +52,8 @@ align-items: center; border-radius: 50%; color: white; + z-index: 2; + opacity: 0.75; background: var(--premium-gradient); } diff --git a/src/components/common/StickerButton.tsx b/src/components/common/StickerButton.tsx index 0df89b3e3..c5408d928 100644 --- a/src/components/common/StickerButton.tsx +++ b/src/components/common/StickerButton.tsx @@ -54,6 +54,7 @@ type OwnProps = { onContextMenuOpen?: NoneToVoidFunction; onContextMenuClose?: NoneToVoidFunction; onContextMenuClick?: NoneToVoidFunction; + isEffectEmoji?: boolean; }; const contentForStatusMenuContext = [ @@ -91,6 +92,7 @@ const StickerButton = ) => { const { openStickerSet, openPremiumModal, setEmojiStatus } = getActions(); // eslint-disable-next-line no-null/no-null @@ -102,8 +104,11 @@ const StickerButton = = ({ const isLocked = !isSavedMessages && !isCurrentUserPremium && isPremiumSet && !isChatEmojiSet; 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 itemsBeforeCutout = itemsPerRow * 3 - 1; const totalItemsCount = withDefaultTopicIcon ? stickerSet.count + 1 : stickerSet.count; @@ -298,7 +303,11 @@ const StickerSet: FC = ({
)}
= ({ onContextMenuClose={onContextMenuClose} onContextMenuClick={onContextMenuClick} forcePlayback={forcePlayback} + isEffectEmoji={stickerSet.id === EFFECT_EMOJIS_SET_ID} + noShowPremium={isCurrentUserPremium + && (stickerSet.id === EFFECT_STICKERS_SET_ID || stickerSet.id === EFFECT_EMOJIS_SET_ID)} /> ); })} diff --git a/src/components/middle/composer/CustomSendMenu.scss b/src/components/middle/composer/CustomSendMenu.scss index ccd72ae89..8e780975e 100644 --- a/src/components/middle/composer/CustomSendMenu.scss +++ b/src/components/middle/composer/CustomSendMenu.scss @@ -19,4 +19,45 @@ .bubble { 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; } diff --git a/src/components/middle/composer/CustomSendMenu.tsx b/src/components/middle/composer/CustomSendMenu.tsx index 5ff6c49a5..5a02675a5 100644 --- a/src/components/middle/composer/CustomSendMenu.tsx +++ b/src/components/middle/composer/CustomSendMenu.tsx @@ -1,14 +1,28 @@ 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 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 useOldLang from '../../../hooks/useOldLang'; import Menu from '../../ui/Menu'; import MenuItem from '../../ui/MenuItem'; +import ReactionSelector from '../message/reactions/ReactionSelector'; import './CustomSendMenu.scss'; @@ -21,10 +35,24 @@ export type OwnProps = { onSendSilent?: NoneToVoidFunction; onSendSchedule?: NoneToVoidFunction; onSendWhenOnline?: NoneToVoidFunction; + onRemoveEffect?: NoneToVoidFunction; onClose: 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 = ({ isOpen, isOpenToBottom = false, @@ -34,45 +62,117 @@ const CustomSendMenu: FC = ({ onSendSilent, onSendSchedule, onSendWhenOnline, + onRemoveEffect, onClose, onCloseAnimationEnd, + chatId, + withEffects, + hasCurrentEffect, + effectReactions, + allAvailableReactions, + onToggleReaction, + canBuyPremium, + isCurrentUserPremium, + isInSavedMessages, + isInStoryViewer, + canPlayAnimatedEmojis, }) => { + const { + openEffectPicker, + } = getActions(); + const [handleMouseEnter, handleMouseLeave] = useMouseInside(isOpen, onClose); const [displayScheduleUntilOnline, setDisplayScheduleUntilOnline] = useState(false); - const lang = useOldLang(); + const oldLang = useOldLang(); + const lang = useLang(); + const [areItemsHidden, hideItems, showItems] = useFlag(); useEffectWithPrevDeps(([prevIsOpen]) => { // Avoid context menu item shuffling when opened if (isOpen && !prevIsOpen) { + showItems(); setDisplayScheduleUntilOnline(Boolean(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 ( - {onSendSilent && {lang('SendWithoutSound')}} - {canSchedule && onSendSchedule && ( - - {lang(isSavedMessages ? 'SetReminder' : 'ScheduleMessage')} - - )} - {canSchedule && onSendSchedule && displayScheduleUntilOnline && ( - - {lang('SendWhenOnline')} - + + {withEffects && !isInStoryViewer && ( + )} + +
+ {onSendSilent && {oldLang('SendWithoutSound')}} + {canSchedule && onSendSchedule && ( + + {oldLang(isSavedMessages ? 'SetReminder' : 'ScheduleMessage')} + + )} + {canSchedule && onSendSchedule && displayScheduleUntilOnline && ( + + {oldLang('SendWhenOnline')} + + )} + {withEffects && hasCurrentEffect && ( + + {lang('RemoveEffect')} + + )} +
); }; diff --git a/src/components/middle/composer/StickerPicker.module.scss b/src/components/middle/composer/StickerPicker.module.scss index da64ef8cd..a0b110818 100644 --- a/src/components/middle/composer/StickerPicker.module.scss +++ b/src/components/middle/composer/StickerPicker.module.scss @@ -11,7 +11,11 @@ --symbol-set-gap-size: 0.25rem; position: relative; - height: calc(100% - 3rem); + height: 100%; + + &.hasHeader { + height: calc(100% - 3rem); + } overflow-y: auto; overflow-x: hidden; diff --git a/src/components/middle/composer/StickerPicker.tsx b/src/components/middle/composer/StickerPicker.tsx index e50e56e46..36eae847f 100644 --- a/src/components/middle/composer/StickerPicker.tsx +++ b/src/components/middle/composer/StickerPicker.tsx @@ -10,6 +10,8 @@ import type { StickerSetOrReactionsSetOrRecent, ThreadId } from '../../../types' import { CHAT_STICKER_SET_ID, + EFFECT_EMOJIS_SET_ID, + EFFECT_STICKERS_SET_ID, FAVORITE_SYMBOL_SET_ID, RECENT_SYMBOL_SET_ID, SLIDE_TRANSITION_DURATION, @@ -57,12 +59,15 @@ type OwnProps = { onStickerSelect: ( sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean, canUpdateStickerSetsOrder?: boolean, ) => void; + isForEffects?: boolean; }; type StateProps = { chat?: ApiChat; recentStickers: ApiSticker[]; favoriteStickers: ApiSticker[]; + effectStickers?: ApiSticker[]; + effectEmojis?: ApiSticker[]; stickerSetsById: Record; chatStickerSetId?: string; addedSetIds?: string[]; @@ -83,6 +88,8 @@ const StickerPicker: FC = ({ canSendStickers, recentStickers, favoriteStickers, + effectStickers, + effectEmojis, addedSetIds, stickerSetsById, chatStickerSetId, @@ -92,6 +99,7 @@ const StickerPicker: FC = ({ noContextMenus, idPrefix, onStickerSelect, + isForEffects, }) => { const { loadRecentStickers, @@ -113,7 +121,7 @@ const StickerPicker: FC = ({ isAtBeginning: shouldHideTopBorder, } = useScrolledState(); - const sendMessageAction = useSendMessageAction(chat!.id, threadId); + const sendMessageAction = useSendMessageAction(chat?.id, threadId); const prefix = `${idPrefix}-sticker-set`; const { @@ -130,6 +138,30 @@ const StickerPicker: FC = ({ const areAddedLoaded = Boolean(addedSetIds); 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) { return MEMO_EMPTY_ARRAY; } @@ -167,7 +199,17 @@ const StickerPicker: FC = ({ ...defaultSets, ...existingAddedSetIds, ]; - }, [addedSetIds, stickerSetsById, favoriteStickers, recentStickers, chatStickerSetId, lang]); + }, [ + addedSetIds, + stickerSetsById, + favoriteStickers, + recentStickers, + chatStickerSetId, + lang, + effectStickers, + isForEffects, + effectEmojis, + ]); const noPopulatedSets = useMemo(() => ( areAddedLoaded @@ -182,7 +224,8 @@ const StickerPicker: FC = ({ }, [canSendStickers, loadAndPlay, loadRecentStickers, sendMessageAction]); const canRenderContents = useAsyncRendering([], SLIDE_TRANSITION_DURATION); - const shouldRenderContents = areAddedLoaded && canRenderContents && !noPopulatedSets && canSendStickers; + const shouldRenderContents = areAddedLoaded && canRenderContents + && !noPopulatedSets && (canSendStickers || isForEffects); useHorizontalScroll(headerRef, !shouldRenderContents || !headerRef.current); @@ -224,6 +267,8 @@ const StickerPicker: FC = ({ removeRecentSticker({ sticker }); }); + if (!chat) return undefined; + function renderCover(stickerSet: StickerSetOrReactionsSetOrRecent, index: number) { const firstSticker = stickerSet.stickers?.[0]; const buttonClassName = buildClassName(styles.stickerCover, index === activeSetIndex && styles.activated); @@ -290,7 +335,7 @@ const StickerPicker: FC = ({ if (!shouldRenderContents) { return (
- {!canSendStickers ? ( + {!canSendStickers && !isForEffects ? (
{lang('ErrorSendRestrictedStickersAll')}
) : noPopulatedSets ? (
{lang('NoStickers')}
@@ -309,17 +354,25 @@ const StickerPicker: FC = ({ return (
-
-
- - {allSets.map(renderCover)} + { !isForEffects && ( +
+
+ + {allSets.map(renderCover)} +
-
+ ) }
{allSets.map((stickerSet, i) => ( = ({ onStickerFave={handleStickerFave} onStickerRemoveRecent={handleRemoveRecentSticker} forcePlayback + shouldHideHeader={stickerSet.id === EFFECT_EMOJIS_SET_ID} /> ))}
@@ -357,6 +411,7 @@ export default memo(withGlobal( added, recent, favorite, + effect, } = global.stickers; const isSavedMessages = selectIsChatWithSelf(global, chatId); @@ -365,6 +420,8 @@ export default memo(withGlobal( return { chat, + effectStickers: effect?.stickers, + effectEmojis: effect?.emojis, recentStickers: recent.stickers, favoriteStickers: favorite.stickers, stickerSetsById: setsById, diff --git a/src/components/middle/composer/SymbolMenu.scss b/src/components/middle/composer/SymbolMenu.scss index 714d1697a..dd7814750 100644 --- a/src/components/middle/composer/SymbolMenu.scss +++ b/src/components/middle/composer/SymbolMenu.scss @@ -270,6 +270,10 @@ } } +.effect-emojis.symbol-set-container { + --emoji-size: 2.25rem; +} + .symbol-set-container { display: grid !important; justify-content: space-between; diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index ecf1c5abc..fbda2c6c9 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -1115,10 +1115,11 @@ const Message: FC = ({ activeEmojiInteractions={activeEmojiInteractions} /> )} - {withAnimatedEffects && effect && ( + {withAnimatedEffects && effect && !isLocal && ( { stopPlaying(); onStop?.(); }); - useEffect(() => { - if (canPlay && shouldPlay && effectBlob) { - startPlaying(); - } - }, [canPlay, effectBlob, shouldPlay]); - - useOverlayPosition({ + const updatePosition = useOverlayPosition({ anchorRef, overlayRef: ref, isMirrored, isDisabled: !isPlaying, 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( styles.root, isMirrored && styles.mirror, @@ -78,7 +86,7 @@ const MessageEffect = ({ ; overlayRef: RefObject; isMirrored?: boolean; isForMessageEffect?: boolean; isDisabled?: boolean; + id?: string; }) { const updatePosition = useLastCallback(() => { const element = overlayRef.current; @@ -49,12 +51,13 @@ export default function useOverlayPosition({ useEffect(() => { if (isDisabled) return; updatePosition(); - }, [isDisabled]); + }, [isDisabled, id]); useEffect(() => { if (isDisabled) return undefined; const messagesContainer = anchorRef.current!.closest('.MessageList')!; + if (!messagesContainer) return undefined; messagesContainer.addEventListener('scroll', updatePosition, { passive: true }); @@ -62,4 +65,6 @@ export default function useOverlayPosition({ messagesContainer.removeEventListener('scroll', updatePosition); }; }, [isDisabled, anchorRef]); + + return updatePosition; } diff --git a/src/components/middle/message/reactions/ReactionPicker.tsx b/src/components/middle/message/reactions/ReactionPicker.tsx index b553ae6e4..3ab0b83b0 100644 --- a/src/components/middle/message/reactions/ReactionPicker.tsx +++ b/src/components/middle/message/reactions/ReactionPicker.tsx @@ -2,11 +2,13 @@ import type { FC } from '../../../../lib/teact/teact'; import React, { memo, useMemo, useRef } from '../../../../lib/teact/teact'; 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 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 { @@ -26,6 +28,7 @@ import useOldLang from '../../../../hooks/useOldLang'; import CustomEmojiPicker from '../../../common/CustomEmojiPicker'; import Menu from '../../../ui/Menu'; +import StickerPicker from '../../composer/StickerPicker'; import ReactionPickerLimited from './ReactionPickerLimited'; import styles from './ReactionPicker.module.scss'; @@ -42,6 +45,9 @@ interface StateProps { position?: IAnchorPosition; isTranslucent?: boolean; sendAsMessage?: boolean; + chatId?: string; + isForEffects?: boolean; + availableEffectById: Record; } const FULL_PICKER_SHIFT_DELTA = { x: -23, y: -64 }; @@ -57,9 +63,13 @@ const ReactionPicker: FC = ({ isCurrentUserPremium, withCustomReactions, sendAsMessage, + chatId, + isForEffects, + availableEffectById, }) => { const { - toggleReaction, closeReactionPicker, sendMessage, showNotification, sendStoryReaction, + toggleReaction, closeReactionPicker, sendMessage, showNotification, sendStoryReaction, saveEffectInDraft, + requestEffectInComposer, } = getActions(); const lang = useOldLang(); @@ -171,6 +181,16 @@ const ReactionPicker: FC = ({ 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(() => { return (message?.reactions?.results || []).reduce((acc, { chosenOrder, reaction }) => { if (chosenOrder !== undefined) { @@ -201,25 +221,42 @@ const ReactionPicker: FC = ({ backdropExcludedSelector=".Modal.confirm" onClose={closeReactionPicker} > - - {!withCustomReactions && Boolean(renderedChatId) && ( - + ) : ( + <> + + {!withCustomReactions && Boolean(renderedChatId) && ( + + )} + )} ); @@ -227,8 +264,9 @@ const ReactionPicker: FC = ({ export default memo(withGlobal((global): StateProps => { const state = selectTabState(global); + const availableEffectById = global.availableEffectById; const { - chatId, messageId, storyPeerId, storyId, position, sendAsMessage, + chatId, messageId, storyPeerId, storyId, position, sendAsMessage, isForEffects, } = state.reactionPicker || {}; const story = storyPeerId && storyId ? selectPeerStory(global, storyPeerId, storyId) as ApiStory | ApiStorySkipped @@ -251,6 +289,9 @@ export default memo(withGlobal((global): StateProps => { isTranslucent: selectIsContextMenuTranslucent(global), isCurrentUserPremium: selectIsCurrentUserPremium(global), sendAsMessage, + isForEffects, + chatId, + availableEffectById, }; })(ReactionPicker)); diff --git a/src/components/middle/message/reactions/ReactionSelector.tsx b/src/components/middle/message/reactions/ReactionSelector.tsx index 04ee8edd8..c0c539fb1 100644 --- a/src/components/middle/message/reactions/ReactionSelector.tsx +++ b/src/components/middle/message/reactions/ReactionSelector.tsx @@ -27,6 +27,7 @@ type OwnProps = { isPrivate?: boolean; topReactions?: ApiReaction[]; defaultTagReactions?: ApiReaction[]; + effectReactions?: ApiReaction[]; allAvailableReactions?: ApiAvailableReaction[]; currentReactions?: ApiReactionCount[]; maxUniqueReactions?: number; @@ -37,6 +38,7 @@ type OwnProps = { className?: string; isInSavedMessages?: boolean; isInStoryViewer?: boolean; + isForEffects?: boolean; onClose?: NoneToVoidFunction; onToggleReaction: (reaction: ApiReaction) => void; onShowMore: (position: IAnchorPosition) => void; @@ -60,6 +62,8 @@ const ReactionSelector: FC = ({ isCurrentUserPremium, isInSavedMessages, isInStoryViewer, + isForEffects, + effectReactions, onClose, onToggleReaction, onShowMore, @@ -72,12 +76,15 @@ const ReactionSelector: FC = ({ const areReactionsLocked = isInSavedMessages && !isCurrentUserPremium && !isInStoryViewer; const availableReactions = useMemo(() => { - const reactions = isInSavedMessages ? defaultTagReactions + const reactions = isForEffects ? effectReactions : isInSavedMessages ? defaultTagReactions : (enabledReactions?.type === 'some' ? enabledReactions.allowed : allAvailableReactions?.map((reaction) => reaction.reaction)); const filteredReactions = reactions?.map((reaction) => { const isCustomReaction = 'documentId' in reaction; const availableReaction = allAvailableReactions?.find((r) => isSameReaction(r.reaction, reaction)); + + if (isForEffects) return availableReaction; + if ((!isCustomReaction && !availableReaction) || availableReaction?.isInactive) return undefined; if (!isPrivate && (!enabledReactions || !canSendReaction(reaction, enabledReactions))) { @@ -95,7 +102,7 @@ const ReactionSelector: FC = ({ return sortReactions(filteredReactions, topReactions); }, [ allAvailableReactions, currentReactions, defaultTagReactions, enabledReactions, isInSavedMessages, isPrivate, - maxUniqueReactions, topReactions, + maxUniqueReactions, topReactions, isForEffects, effectReactions, ]); const reactionsToRender = useMemo(() => { @@ -151,8 +158,12 @@ const ReactionSelector: FC = ({ return lang('StoryReactionsHint'); } + if (isForEffects) { + return lang('AddEffectMessageHint'); + } + return undefined; - }, [isCurrentUserPremium, isInSavedMessages, isInStoryViewer, lang]); + }, [isCurrentUserPremium, isInSavedMessages, isInStoryViewer, lang, isForEffects]); if (!reactionsToRender.length) return undefined; diff --git a/src/config.ts b/src/config.ts index d153d4bcb..37f8865ac 100644 --- a/src/config.ts +++ b/src/config.ts @@ -206,6 +206,8 @@ export const TOP_SYMBOL_SET_ID = 'top'; export const POPULAR_SYMBOL_SET_ID = 'popular'; export const RECENT_SYMBOL_SET_ID = 'recent'; 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 DEFAULT_TOPIC_ICON_STICKER_ID = 'topic-default-icon'; export const DEFAULT_STATUS_ICON_ID = 'status-default-icon'; diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index 4078800e2..f94fab484 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -4,7 +4,8 @@ import type { } from '../../../api/types'; import type { RequiredGlobalActions } from '../../index'; import type { - ActionReturnType, ChatListType, GlobalState, TabArgs, + ActionReturnType, ApiDraft, + ChatListType, GlobalState, TabArgs, } from '../../types'; import { MAIN_THREAD_ID } from '../../../api/types'; import { @@ -2824,9 +2825,18 @@ async function loadChats( 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, chatId, MAIN_THREAD_ID, 'draft', draft, + global, chatId, MAIN_THREAD_ID, 'draft', newDraft, ); } }); diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index 9ac10129b..4cb8d13e1 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -525,6 +525,7 @@ addActionHandler('saveDraft', (global, actions, payload): ActionReturnType => { const newDraft: ApiDraft = { text, replyInfo: currentDraft?.replyInfo, + effectId: currentDraft?.effectId, }; 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({ global, chatId, threadId, draft, isLocalOnly, noLocalTimeUpdate, } : { @@ -1410,6 +1428,7 @@ async function sendMessage(global: T, params: { wasDrafted?: boolean; lastMessageId?: number; isInvertedMedia?: true; + effectId?: string; }) { let currentMessageKey: MessageKey | undefined; const progressCallback = params.attachment ? (progress: number, messageKey: MessageKey) => { diff --git a/src/global/actions/api/reactions.ts b/src/global/actions/api/reactions.ts index 0a5c39ced..700565df3 100644 --- a/src/global/actions/api/reactions.ts +++ b/src/global/actions/api/reactions.ts @@ -1,3 +1,4 @@ +import type { ApiReactionEmoji } from '../../../api/types'; import type { ActionReturnType } from '../../types'; import { ApiMediaFormat } from '../../../api/types'; @@ -83,12 +84,35 @@ addActionHandler('loadAvailableEffects', async (global): Promise => { 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 = { ...global, availableEffectById: effectById, + stickers: { + ...global.stickers, + effect: { + stickers, + emojis, + }, + }, + reactions: { + ...global.reactions, + effectReactions: reactions, + }, }; setGlobal(global); }); diff --git a/src/global/actions/apiUpdaters/chats.ts b/src/global/actions/apiUpdaters/chats.ts index acaab23d6..60ff96e60 100644 --- a/src/global/actions/apiUpdaters/chats.ts +++ b/src/global/actions/apiUpdaters/chats.ts @@ -34,6 +34,7 @@ import { selectChatMessages, selectCommonBoxChatId, selectCurrentMessageList, + selectDraft, selectIsChatListed, selectTabState, selectThreadParam, @@ -452,7 +453,15 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { 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 }); return global; } diff --git a/src/global/actions/ui/chats.ts b/src/global/actions/ui/chats.ts index c5f931302..7ce642751 100644 --- a/src/global/actions/ui/chats.ts +++ b/src/global/actions/ui/chats.ts @@ -36,6 +36,7 @@ addActionHandler('processOpenChatOrThread', (global, actions, payload): ActionRe }, }, tabId); } + actions.hideEffectInComposer({ tabId }); if (!currentMessageList || ( currentMessageList.chatId !== chatId diff --git a/src/global/actions/ui/misc.ts b/src/global/actions/ui/misc.ts index 636ba1ecd..eaea4fb10 100644 --- a/src/global/actions/ui/misc.ts +++ b/src/global/actions/ui/misc.ts @@ -29,6 +29,7 @@ import { selectChatMessage, selectCurrentChat, selectCurrentMessageList, + selectIsCurrentUserPremium, selectIsTrustedBot, selectTabState, } 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 => { const { limit, tabId = getCurrentTabId() } = payload; diff --git a/src/global/actions/ui/reactions.ts b/src/global/actions/ui/reactions.ts index a851b36ed..f041b6bd7 100644 --- a/src/global/actions/ui/reactions.ts +++ b/src/global/actions/ui/reactions.ts @@ -62,6 +62,22 @@ addActionHandler('openStoryReactionPicker', (global, actions, payload): ActionRe }, 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 => { const { tabId = getCurrentTabId() } = payload || {}; const tabState = selectTabState(global, tabId); @@ -73,6 +89,7 @@ addActionHandler('closeReactionPicker', (global, actions, payload): ActionReturn position: undefined, storyId: undefined, storyPeerId: undefined, + isForEffects: undefined, }, }, tabId); }); diff --git a/src/global/cache.ts b/src/global/cache.ts index b2e09bbc9..3eb63bc04 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -301,6 +301,7 @@ export function serializeGlobal(global: T) { 'defaultTags', 'recentReactions', 'topReactions', + 'effectReactions', 'hash', ]), availableReactions: reduceAvailableReactions(global.reactions.availableReactions), diff --git a/src/global/initialState.ts b/src/global/initialState.ts index 6431e95aa..337c71c0b 100644 --- a/src/global/initialState.ts +++ b/src/global/initialState.ts @@ -160,6 +160,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = { defaultTags: [], topReactions: [], recentReactions: [], + effectReactions: [], hash: {}, }, availableEffectById: {}, @@ -182,6 +183,10 @@ export const INITIAL_GLOBAL_STATE: GlobalState = { featured: { setIds: [], }, + effect: { + stickers: [], + emojis: [], + }, forEmoji: {}, }, diff --git a/src/global/types.ts b/src/global/types.ts index c2640dc2f..75f5edb90 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -343,8 +343,11 @@ export type TabState = { storyId?: number; position?: IAnchorPosition; sendAsMessage?: boolean; + isForEffects?: boolean; }; + shouldPlayEffectInComposer?: true; + inlineBots: { isLoading: boolean; byUsername: Record; @@ -980,6 +983,7 @@ export type GlobalState = { topReactions: ApiReaction[]; recentReactions: ApiReaction[]; defaultTags: ApiReaction[]; + effectReactions: ApiReaction[]; availableReactions?: ApiAvailableReaction[]; hash: { topReactions?: string; @@ -1020,6 +1024,10 @@ export type GlobalState = { stickers?: ApiSticker[]; hash?: string; }; + effect: { + stickers: ApiSticker[]; + emojis: ApiSticker[]; + }; }; customEmojis: { @@ -1140,6 +1148,7 @@ export type ApiDraft = { text?: ApiFormattedText; replyInfo?: ApiInputMessageReplyInfo; date?: number; + effectId?: string; isLocal?: boolean; }; @@ -1482,6 +1491,7 @@ export interface ActionPayloads { messageList?: MessageList; isReaction?: true; // Reaction to the story are sent in the form of a message isInvertedMedia?: true; + effectId?: string; } & WithTabId; sendInviteMessages: { chatId: string; @@ -2318,6 +2328,10 @@ export interface ActionPayloads { reaction?: ApiReaction; } & WithTabId; + openEffectPicker: { + chatId: string; + position: IAnchorPosition; + } & WithTabId; openMessageReactionPicker: { chatId: string; messageId: number; @@ -2918,6 +2932,21 @@ export interface ActionPayloads { isInvertedMedia?: true; }; + saveEffectInDraft: { + chatId: string; + threadId: ThreadId; + effectId?: string; + }; + + setReactionEffect: { + chatId: string; + threadId: ThreadId; + reaction?: ApiReaction; + } & WithTabId; + + requestEffectInComposer: WithTabId; + hideEffectInComposer: WithTabId; + updateArchiveSettings: { isMinimized?: boolean; isHidden?: boolean; diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 52d741c78..db3e98940 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -1511,7 +1511,7 @@ export interface LangPair { 'MenuBetaChangelog': undefined; 'MenuSwitchToK': undefined; 'MenuInstallApp': undefined; - + 'RemoveEffect' : undefined; } export type LangKey = keyof LangPair;