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,
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);

View File

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

View File

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

View File

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

View File

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

View File

@ -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 */

View File

@ -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<OwnProps & StateProps> = ({
onForward,
webPagePreview,
noWebPage,
effect,
effectReactions,
areEffectsSupported,
canPlayEffect,
shouldPlayEffect,
}) => {
const {
sendMessage,
@ -384,6 +398,9 @@ const Composer: FC<OwnProps & StateProps> = ({
sendStoryReaction,
editMessage,
updateAttachmentSettings,
saveEffectInDraft,
setReactionEffect,
hideEffectInComposer,
} = getActions();
const lang = useOldLang();
@ -1023,11 +1040,15 @@ const Composer: FC<OwnProps & StateProps> = ({
const messageInput = document.querySelector<HTMLDivElement>(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<OwnProps & StateProps> = ({
isSilent,
shouldUpdateStickerSetOrder,
isInvertedMedia,
effectId,
});
}
@ -1077,7 +1099,7 @@ const Composer: FC<OwnProps & StateProps> = ({
});
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<OwnProps & StateProps> = ({
...args,
messageList,
scheduledAt,
effectId,
});
}
});
@ -1429,7 +1452,7 @@ const Composer: FC<OwnProps & StateProps> = ({
return;
}
requestCalendar((scheduledAt) => {
handleMessageSchedule({}, scheduledAt, currentMessageList);
handleMessageSchedule({}, scheduledAt, currentMessageList, effect?.id);
});
break;
default:
@ -1494,6 +1517,12 @@ const Composer: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
});
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<OwnProps & StateProps> = ({
},
);
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<OwnProps & StateProps> = ({
const withBotCommands = isChatWithBot && botMenuButton?.type === 'commands' && !editingMessage
&& botCommands !== false && !activeVoiceRecording;
const effectEmoji = areEffectsSupported && effect?.emoticon;
return (
<div className={fullClassName}>
{isInMessageList && canAttachMedia && isReady && (
@ -1984,6 +2019,18 @@ const Composer: FC<OwnProps & StateProps> = ({
{isInMessageList && <i className="icon icon-schedule" />}
{isInMessageList && <i className="icon icon-check" />}
</Button>
{effectEmoji && (
<span className="effect-icon">
{renderText(effectEmoji)}
</span>
)}
{effect && canPlayEffect && (
<MessageEffect
shouldPlay={shouldPlayEffect}
effect={effect}
onStop={handleStopEffect}
/>
)}
{canShowCustomSendMenu && (
<CustomSendMenu
isOpen={isCustomSendMenuOpen}
@ -1992,9 +2039,20 @@ const Composer: FC<OwnProps & StateProps> = ({
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<OwnProps>(
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<OwnProps>(
canSendQuickReplies,
noWebPage,
webPagePreview: selectTabState(global).webPagePreview,
effect,
effectReactions,
areEffectsSupported,
canPlayEffect,
shouldPlayEffect,
};
},
)(Composer));

View File

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

View File

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

View File

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

View File

@ -11,6 +11,8 @@ import type { StickerSetOrReactionsSetOrRecent } from '../../types';
import {
DEFAULT_STATUS_ICON_ID,
DEFAULT_TOPIC_ICON_STICKER_ID,
EFFECT_EMOJIS_SET_ID,
EFFECT_STICKERS_SET_ID,
EMOJI_SIZE_PICKER,
FAVORITE_SYMBOL_SET_ID,
POPULAR_SYMBOL_SET_ID,
@ -232,8 +234,11 @@ const StickerSet: FC<OwnProps> = ({
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<OwnProps> = ({
</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;`}
>
<canvas
@ -382,6 +391,9 @@ const StickerSet: FC<OwnProps> = ({
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)}
/>
);
})}

View File

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

View File

@ -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<OwnProps> = ({
isOpen,
isOpenToBottom = false,
@ -34,45 +62,117 @@ const CustomSendMenu: FC<OwnProps> = ({
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 (
<Menu
isOpen={isOpen}
autoClose
positionX="right"
positionY={isOpenToBottom ? 'top' : 'bottom'}
className="CustomSendMenu with-menu-transitions"
className={buildClassName(
'CustomSendMenu', 'fluid', 'with-menu-transitions', withEffects && 'with-effects',
)}
onClose={onClose}
onCloseAnimationEnd={onCloseAnimationEnd}
onMouseEnter={!IS_TOUCH_ENV ? handleMouseEnter : undefined}
onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined}
noCloseOnBackdrop={!IS_TOUCH_ENV}
>
{onSendSilent && <MenuItem icon="mute" onClick={onSendSilent}>{lang('SendWithoutSound')}</MenuItem>}
{canSchedule && onSendSchedule && (
<MenuItem icon="schedule" onClick={onSendSchedule}>
{lang(isSavedMessages ? 'SetReminder' : 'ScheduleMessage')}
</MenuItem>
)}
{canSchedule && onSendSchedule && displayScheduleUntilOnline && (
<MenuItem icon="user-online" onClick={onSendWhenOnline}>
{lang('SendWhenOnline')}
</MenuItem>
{withEffects && !isInStoryViewer && (
<ReactionSelector
allAvailableReactions={allAvailableReactions}
effectReactions={effectReactions}
currentReactions={undefined}
onToggleReaction={onToggleReaction!}
isPrivate
isReady={isReady}
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>
);
};

View File

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

View File

@ -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<string, ApiStickerSet>;
chatStickerSetId?: string;
addedSetIds?: string[];
@ -83,6 +88,8 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
canSendStickers,
recentStickers,
favoriteStickers,
effectStickers,
effectEmojis,
addedSetIds,
stickerSetsById,
chatStickerSetId,
@ -92,6 +99,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
noContextMenus,
idPrefix,
onStickerSelect,
isForEffects,
}) => {
const {
loadRecentStickers,
@ -113,7 +121,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
...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<OwnProps & StateProps> = ({
}, [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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
if (!shouldRenderContents) {
return (
<div className={fullClassName}>
{!canSendStickers ? (
{!canSendStickers && !isForEffects ? (
<div className={styles.pickerDisabled}>{lang('ErrorSendRestrictedStickersAll')}</div>
) : noPopulatedSets ? (
<div className={styles.pickerDisabled}>{lang('NoStickers')}</div>
@ -309,17 +354,25 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
return (
<div className={fullClassName}>
<div ref={headerRef} className={headerClassName}>
<div className="shared-canvas-container">
<canvas ref={sharedCanvasRef} className="shared-canvas" />
{allSets.map(renderCover)}
{ !isForEffects && (
<div ref={headerRef} className={headerClassName}>
<div className="shared-canvas-container">
<canvas ref={sharedCanvasRef} className="shared-canvas" />
{allSets.map(renderCover)}
</div>
</div>
</div>
) }
<div
ref={containerRef}
onMouseMove={handleMouseMove}
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) => (
<StickerSet
@ -343,6 +396,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
onStickerFave={handleStickerFave}
onStickerRemoveRecent={handleRemoveRecentSticker}
forcePlayback
shouldHideHeader={stickerSet.id === EFFECT_EMOJIS_SET_ID}
/>
))}
</div>
@ -357,6 +411,7 @@ export default memo(withGlobal<OwnProps>(
added,
recent,
favorite,
effect,
} = global.stickers;
const isSavedMessages = selectIsChatWithSelf(global, chatId);
@ -365,6 +420,8 @@ export default memo(withGlobal<OwnProps>(
return {
chat,
effectStickers: effect?.stickers,
effectEmojis: effect?.emojis,
recentStickers: recent.stickers,
favoriteStickers: favorite.stickers,
stickerSetsById: setsById,

View File

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

View File

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

View File

@ -1,6 +1,6 @@
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';
@ -16,18 +16,20 @@ import Portal from '../../ui/Portal';
import styles from './MessageEffect.module.scss';
type OwnProps = {
message: ApiMessage;
messageId?: number;
isMirrored?: boolean;
effect: ApiAvailableEffect;
shouldPlay?: boolean;
observeIntersectionForLoading: ObserveFn;
observeIntersectionForPlaying: ObserveFn;
observeIntersectionForLoading?: ObserveFn;
observeIntersectionForPlaying?: ObserveFn;
onStop?: VoidFunction;
};
const EFFECT_SIZE = 256;
const MessageEffect = ({
message,
messageId,
isMirrored,
effect,
shouldPlay,
observeIntersectionForLoading,
@ -42,31 +44,37 @@ const MessageEffect = ({
const canPlay = useIsIntersecting(anchorRef, observeIntersectionForPlaying);
const [isPlaying, startPlaying, stopPlaying] = useFlag();
const [isPositionUpdateRequired, requirePositionUpdate, resetPositionUpdate] = useFlag();
const effectHash = getEffectHash(effect);
const effectBlob = useMedia(effectHash, !canLoad);
const isMirrored = !message.isOutgoing;
const handleEnded = useLastCallback(() => {
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 = ({
<Portal>
<AnimatedSticker
ref={ref}
key={`effect-${message.id}`}
key={`effect-${messageId ?? effect.id}`}
className={effectClassName}
tgsUrl={effectBlob}
size={EFFECT_SIZE}

View File

@ -14,12 +14,14 @@ export default function useOverlayPosition({
isMirrored,
isForMessageEffect,
isDisabled,
id,
} : {
anchorRef: RefObject<HTMLDivElement>;
overlayRef: RefObject<HTMLDivElement>;
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<HTMLDivElement>('.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;
}

View File

@ -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<string, ApiAvailableEffect>;
}
const FULL_PICKER_SHIFT_DELTA = { x: -23, y: -64 };
@ -57,9 +63,13 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
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<string[]>((acc, { chosenOrder, reaction }) => {
if (chosenOrder !== undefined) {
@ -201,25 +221,42 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
backdropExcludedSelector=".Modal.confirm"
onClose={closeReactionPicker}
>
<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}
{isForEffects && chatId ? (
<StickerPicker
className=""
isHidden={!isOpen}
loadAndPlay={Boolean(isOpen && withCustomReactions)}
idPrefix="message-effect"
canSendStickers={false}
noContextMenus={false}
chatId={chatId}
isTranslucent={isTranslucent}
onStickerSelect={handleStickerSelect}
isForEffects={isForEffects}
/>
) : (
<>
<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>
);
@ -227,8 +264,9 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>((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<OwnProps>((global): StateProps => {
isTranslucent: selectIsContextMenuTranslucent(global),
isCurrentUserPremium: selectIsCurrentUserPremium(global),
sendAsMessage,
isForEffects,
chatId,
availableEffectById,
};
})(ReactionPicker));

View File

@ -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<OwnProps> = ({
isCurrentUserPremium,
isInSavedMessages,
isInStoryViewer,
isForEffects,
effectReactions,
onClose,
onToggleReaction,
onShowMore,
@ -72,12 +76,15 @@ const ReactionSelector: FC<OwnProps> = ({
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<OwnProps> = ({
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<OwnProps> = ({
return lang('StoryReactionsHint');
}
if (isForEffects) {
return lang('AddEffectMessageHint');
}
return undefined;
}, [isCurrentUserPremium, isInSavedMessages, isInStoryViewer, lang]);
}, [isCurrentUserPremium, isInSavedMessages, isInStoryViewer, lang, isForEffects]);
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 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';

View File

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

View File

@ -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<T extends GlobalState>({
global, chatId, threadId, draft, isLocalOnly, noLocalTimeUpdate,
} : {
@ -1410,6 +1428,7 @@ async function sendMessage<T extends GlobalState>(global: T, params: {
wasDrafted?: boolean;
lastMessageId?: number;
isInvertedMedia?: true;
effectId?: string;
}) {
let currentMessageKey: MessageKey | undefined;
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 { ApiMediaFormat } from '../../../api/types';
@ -83,12 +84,35 @@ addActionHandler('loadAvailableEffects', async (global): Promise<void> => {
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);
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: {},
},

View File

@ -343,8 +343,11 @@ export type TabState = {
storyId?: number;
position?: IAnchorPosition;
sendAsMessage?: boolean;
isForEffects?: boolean;
};
shouldPlayEffectInComposer?: true;
inlineBots: {
isLoading: boolean;
byUsername: Record<string, false | InlineBotSettings>;
@ -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;

View File

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