Message: Display effects (#4697)
This commit is contained in:
parent
5fb1283884
commit
bbb5ef0a86
@ -235,6 +235,7 @@ export function buildApiMessageWithChatId(
|
|||||||
senderBoosts,
|
senderBoosts,
|
||||||
viaBusinessBotId: mtpMessage.viaBusinessBotId?.toString(),
|
viaBusinessBotId: mtpMessage.viaBusinessBotId?.toString(),
|
||||||
factCheck,
|
factCheck,
|
||||||
|
effectId: mtpMessage.effect?.toString(),
|
||||||
isInvertedMedia,
|
isInvertedMedia,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Api as GramJs } from '../../../lib/gramjs';
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
ApiAvailableEffect,
|
||||||
ApiAvailableReaction,
|
ApiAvailableReaction,
|
||||||
ApiPeerReaction,
|
ApiPeerReaction,
|
||||||
ApiReaction,
|
ApiReaction,
|
||||||
@ -117,3 +118,18 @@ export function buildApiAvailableReaction(availableReaction: GramJs.AvailableRea
|
|||||||
isPremium: premium,
|
isPremium: premium,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildApiAvailableEffect(availableEffect: GramJs.AvailableEffect): ApiAvailableEffect {
|
||||||
|
const {
|
||||||
|
id, emoticon, premiumRequired, staticIconId, effectStickerId, effectAnimationId,
|
||||||
|
} = availableEffect;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: id.toString(),
|
||||||
|
emoticon,
|
||||||
|
isPremium: premiumRequired,
|
||||||
|
staticIconId: staticIconId?.toString(),
|
||||||
|
effectStickerId: effectStickerId.toString(),
|
||||||
|
effectAnimationId: effectAnimationId?.toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
import { split } from '../../../util/iteratees';
|
import { split } from '../../../util/iteratees';
|
||||||
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
||||||
import {
|
import {
|
||||||
|
buildApiAvailableEffect,
|
||||||
buildApiAvailableReaction,
|
buildApiAvailableReaction,
|
||||||
buildApiReaction,
|
buildApiReaction,
|
||||||
buildApiSavedReactionTag,
|
buildApiSavedReactionTag,
|
||||||
@ -95,6 +96,22 @@ export async function fetchAvailableReactions() {
|
|||||||
return result.reactions.map(buildApiAvailableReaction);
|
return result.reactions.map(buildApiAvailableReaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchAvailableEffects() {
|
||||||
|
const result = await invokeRequest(new GramJs.messages.GetAvailableEffects({}));
|
||||||
|
|
||||||
|
if (!result || result instanceof GramJs.messages.AvailableEffectsNotModified) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.documents.forEach((document) => {
|
||||||
|
if (document instanceof GramJs.Document) {
|
||||||
|
localDb.documents[String(document.id)] = document;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.effects.map(buildApiAvailableEffect);
|
||||||
|
}
|
||||||
|
|
||||||
export function sendReaction({
|
export function sendReaction({
|
||||||
chat, messageId, reactions, shouldAddToRecent,
|
chat, messageId, reactions, shouldAddToRecent,
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@ -606,6 +606,7 @@ export interface ApiMessage {
|
|||||||
savedPeerId?: string;
|
savedPeerId?: string;
|
||||||
senderBoosts?: number;
|
senderBoosts?: number;
|
||||||
factCheck?: ApiFactCheck;
|
factCheck?: ApiFactCheck;
|
||||||
|
effectId?: string;
|
||||||
isInvertedMedia?: true;
|
isInvertedMedia?: true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -645,6 +646,15 @@ export interface ApiAvailableReaction {
|
|||||||
isPremium?: boolean;
|
isPremium?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiAvailableEffect {
|
||||||
|
id: string;
|
||||||
|
emoticon: string;
|
||||||
|
staticIconId?: string;
|
||||||
|
effectAnimationId?: string;
|
||||||
|
effectStickerId: string;
|
||||||
|
isPremium?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
type ApiChatReactionsAll = {
|
type ApiChatReactionsAll = {
|
||||||
type: 'all';
|
type: 'all';
|
||||||
areCustomAllowed?: true;
|
areCustomAllowed?: true;
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import useOldLang from '../../hooks/useOldLang';
|
|||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
import Menu from '../ui/Menu';
|
import Menu from '../ui/Menu';
|
||||||
import MenuItem from '../ui/MenuItem';
|
import MenuItem from '../ui/MenuItem';
|
||||||
|
import Icon from './icons/Icon';
|
||||||
import StickerView from './StickerView';
|
import StickerView from './StickerView';
|
||||||
|
|
||||||
import './StickerButton.scss';
|
import './StickerButton.scss';
|
||||||
@ -311,12 +312,12 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
<div
|
<div
|
||||||
className="sticker-locked"
|
className="sticker-locked"
|
||||||
>
|
>
|
||||||
<i className="icon icon-lock-badge" />
|
<Icon name="lock-badge" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!noShowPremium && isPremium && !isLocked && (
|
{!noShowPremium && isPremium && !isLocked && (
|
||||||
<div className="sticker-premium">
|
<div className="sticker-premium">
|
||||||
<i className="icon icon-premium" />
|
<Icon name="star" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{shouldShowCloseButton && (
|
{shouldShowCloseButton && (
|
||||||
@ -327,7 +328,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
noFastClick
|
noFastClick
|
||||||
onClick={handleRemoveClick}
|
onClick={handleRemoveClick}
|
||||||
>
|
>
|
||||||
<i className="icon icon-close" />
|
<Icon name="close" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{Boolean(contextMenuItems.length) && (
|
{Boolean(contextMenuItems.length) && (
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import useWindowSize from '../../hooks/window/useWindowSize';
|
|||||||
|
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
import ConfirmDialog from '../ui/ConfirmDialog';
|
import ConfirmDialog from '../ui/ConfirmDialog';
|
||||||
|
import Icon from './icons/Icon';
|
||||||
import ReactionEmoji from './ReactionEmoji';
|
import ReactionEmoji from './ReactionEmoji';
|
||||||
import StickerButton from './StickerButton';
|
import StickerButton from './StickerButton';
|
||||||
|
|
||||||
@ -268,7 +269,7 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
{!shouldHideHeader && (
|
{!shouldHideHeader && (
|
||||||
<div className="symbol-set-header">
|
<div className="symbol-set-header">
|
||||||
<p className={buildClassName('symbol-set-title', withAddSetButton && 'symbol-set-title-external')}>
|
<p className={buildClassName('symbol-set-title', withAddSetButton && 'symbol-set-title-external')}>
|
||||||
{isLocked && <i className="symbol-set-locked-icon icon icon-lock-badge" />}
|
{isLocked && <Icon name="lock-badge" className="symbol-set-locked-icon" />}
|
||||||
<span className="symbol-set-name">{stickerSet.title}</span>
|
<span className="symbol-set-name">{stickerSet.title}</span>
|
||||||
{(isChatEmojiSet || isChatStickerSet) && (
|
{(isChatEmojiSet || isChatStickerSet) && (
|
||||||
<span className="symbol-set-chat">{lang(isChatEmojiSet ? 'GroupEmoji' : 'GroupStickers')}</span>
|
<span className="symbol-set-chat">{lang(isChatEmojiSet ? 'GroupEmoji' : 'GroupStickers')}</span>
|
||||||
@ -280,7 +281,7 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
{isRecent && (
|
{isRecent && (
|
||||||
<i className="symbol-set-remove icon icon-close" onClick={openConfirmModal} />
|
<Icon className="symbol-set-remove" name="close" onClick={openConfirmModal} />
|
||||||
)}
|
)}
|
||||||
{withAddSetButton && (
|
{withAddSetButton && (
|
||||||
<Button
|
<Button
|
||||||
@ -323,7 +324,7 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
onClick={handleDefaultStatusIconClick}
|
onClick={handleDefaultStatusIconClick}
|
||||||
key="default-status-icon"
|
key="default-status-icon"
|
||||||
>
|
>
|
||||||
<i className="icon icon-premium" />
|
<Icon name="star" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{shouldRender && stickerSet.reactions?.map((reaction) => {
|
{shouldRender && stickerSet.reactions?.map((reaction) => {
|
||||||
|
|||||||
@ -11,6 +11,7 @@ type OwnProps = {
|
|||||||
style?: string;
|
style?: string;
|
||||||
role?: AriaRole;
|
role?: AriaRole;
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
|
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Icon = ({
|
const Icon = ({
|
||||||
@ -19,6 +20,7 @@ const Icon = ({
|
|||||||
style,
|
style,
|
||||||
role,
|
role,
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
|
onClick,
|
||||||
}: OwnProps) => {
|
}: OwnProps) => {
|
||||||
return (
|
return (
|
||||||
<i
|
<i
|
||||||
@ -27,6 +29,7 @@ const Icon = ({
|
|||||||
aria-hidden={!ariaLabel}
|
aria-hidden={!ariaLabel}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
role={role}
|
role={role}
|
||||||
|
onClick={onClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -249,6 +249,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
loadTimezones,
|
loadTimezones,
|
||||||
loadQuickReplies,
|
loadQuickReplies,
|
||||||
loadStarStatus,
|
loadStarStatus,
|
||||||
|
loadAvailableEffects,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
if (DEBUG && !DEBUG_isLogged) {
|
if (DEBUG && !DEBUG_isLogged) {
|
||||||
@ -310,26 +311,27 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
initMain();
|
initMain();
|
||||||
loadAvailableReactions();
|
loadAvailableReactions();
|
||||||
loadAnimatedEmojis();
|
loadAnimatedEmojis();
|
||||||
loadBirthdayNumbersStickers();
|
|
||||||
loadGenericEmojiEffects();
|
|
||||||
loadNotificationSettings();
|
loadNotificationSettings();
|
||||||
loadNotificationExceptions();
|
loadNotificationExceptions();
|
||||||
loadTopInlineBots();
|
|
||||||
loadEmojiKeywords({ language: BASE_EMOJI_KEYWORD_LANG });
|
|
||||||
loadAttachBots();
|
loadAttachBots();
|
||||||
loadContactList();
|
loadContactList();
|
||||||
loadPremiumGifts();
|
|
||||||
loadDefaultTopicIcons();
|
loadDefaultTopicIcons();
|
||||||
checkAppVersion();
|
checkAppVersion();
|
||||||
loadTopReactions();
|
loadTopReactions();
|
||||||
loadRecentReactions();
|
loadRecentReactions();
|
||||||
loadDefaultTagReactions();
|
loadDefaultTagReactions();
|
||||||
loadFeaturedEmojiStickers();
|
loadFeaturedEmojiStickers();
|
||||||
loadAuthorizations();
|
loadTopInlineBots();
|
||||||
loadSavedReactionTags();
|
loadEmojiKeywords({ language: BASE_EMOJI_KEYWORD_LANG });
|
||||||
loadTimezones();
|
loadTimezones();
|
||||||
loadQuickReplies();
|
loadQuickReplies();
|
||||||
loadStarStatus();
|
loadStarStatus();
|
||||||
|
loadPremiumGifts();
|
||||||
|
loadAvailableEffects();
|
||||||
|
loadBirthdayNumbersStickers();
|
||||||
|
loadGenericEmojiEffects();
|
||||||
|
loadSavedReactionTags();
|
||||||
|
loadAuthorizations();
|
||||||
}
|
}
|
||||||
}, [isMasterTab, isSynced]);
|
}, [isMasterTab, isSynced]);
|
||||||
|
|
||||||
|
|||||||
@ -140,7 +140,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.has-active-reaction {
|
&.has-active-effect {
|
||||||
.message-content-wrapper {
|
.message-content-wrapper {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import React, {
|
|||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
ApiAvailableEffect,
|
||||||
ApiAvailableReaction,
|
ApiAvailableReaction,
|
||||||
ApiChat,
|
ApiChat,
|
||||||
ApiChatMember,
|
ApiChatMember,
|
||||||
@ -166,6 +167,7 @@ import Invoice from './Invoice';
|
|||||||
import InvoiceMediaPreview from './InvoiceMediaPreview';
|
import InvoiceMediaPreview from './InvoiceMediaPreview';
|
||||||
import Location from './Location';
|
import Location from './Location';
|
||||||
import MessageAppendix from './MessageAppendix';
|
import MessageAppendix from './MessageAppendix';
|
||||||
|
import MessageEffect from './MessageEffect';
|
||||||
import MessageMeta from './MessageMeta';
|
import MessageMeta from './MessageMeta';
|
||||||
import MessagePhoneCall from './MessagePhoneCall';
|
import MessagePhoneCall from './MessagePhoneCall';
|
||||||
import Photo from './Photo';
|
import Photo from './Photo';
|
||||||
@ -278,7 +280,7 @@ type StateProps = {
|
|||||||
shouldDetectChatLanguage?: boolean;
|
shouldDetectChatLanguage?: boolean;
|
||||||
requestedTranslationLanguage?: string;
|
requestedTranslationLanguage?: string;
|
||||||
requestedChatTranslationLanguage?: string;
|
requestedChatTranslationLanguage?: string;
|
||||||
withStickerEffects?: boolean;
|
withAnimatedEffects?: boolean;
|
||||||
webPageStory?: ApiTypeStory;
|
webPageStory?: ApiTypeStory;
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
isLoadingComments?: boolean;
|
isLoadingComments?: boolean;
|
||||||
@ -287,6 +289,7 @@ type StateProps = {
|
|||||||
tags?: Record<ApiReactionKey, ApiSavedReactionTag>;
|
tags?: Record<ApiReactionKey, ApiSavedReactionTag>;
|
||||||
canTranscribeVoice?: boolean;
|
canTranscribeVoice?: boolean;
|
||||||
viaBusinessBot?: ApiUser;
|
viaBusinessBot?: ApiUser;
|
||||||
|
effect?: ApiAvailableEffect;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MetaPosition =
|
type MetaPosition =
|
||||||
@ -397,7 +400,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
shouldDetectChatLanguage,
|
shouldDetectChatLanguage,
|
||||||
requestedTranslationLanguage,
|
requestedTranslationLanguage,
|
||||||
requestedChatTranslationLanguage,
|
requestedChatTranslationLanguage,
|
||||||
withStickerEffects,
|
withAnimatedEffects,
|
||||||
webPageStory,
|
webPageStory,
|
||||||
isConnected,
|
isConnected,
|
||||||
getIsMessageListReady,
|
getIsMessageListReady,
|
||||||
@ -406,6 +409,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
tags,
|
tags,
|
||||||
canTranscribeVoice,
|
canTranscribeVoice,
|
||||||
viaBusinessBot,
|
viaBusinessBot,
|
||||||
|
effect,
|
||||||
onPinnedIntersectionChange,
|
onPinnedIntersectionChange,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
@ -429,7 +433,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
const lang = useOldLang();
|
const lang = useOldLang();
|
||||||
|
|
||||||
const [isTranscriptionHidden, setTranscriptionHidden] = useState(false);
|
const [isTranscriptionHidden, setTranscriptionHidden] = useState(false);
|
||||||
const [hasActiveStickerEffect, startStickerEffect, stopStickerEffect] = useFlag();
|
const [shouldPlayEffect, requestEffect, hideEffect] = useFlag();
|
||||||
const { isMobile, isTouchScreen } = useAppLayout();
|
const { isMobile, isTouchScreen } = useAppLayout();
|
||||||
|
|
||||||
useOnIntersect(bottomMarkerRef, observeIntersectionForBottom);
|
useOnIntersect(bottomMarkerRef, observeIntersectionForBottom);
|
||||||
@ -622,6 +626,12 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
isRepliesChat,
|
isRepliesChat,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleEffectClick = useLastCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
requestEffect();
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLastInList) {
|
if (!isLastInList) {
|
||||||
return;
|
return;
|
||||||
@ -662,7 +672,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
isSwiped && 'is-swiped',
|
isSwiped && 'is-swiped',
|
||||||
transitionClassNames,
|
transitionClassNames,
|
||||||
isJustAdded && 'is-just-added',
|
isJustAdded && 'is-just-added',
|
||||||
(hasActiveReactions || hasActiveStickerEffect) && 'has-active-reaction',
|
(hasActiveReactions || shouldPlayEffect) && 'has-active-effect',
|
||||||
isStoryMention && 'is-story-mention',
|
isStoryMention && 'is-story-mention',
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -679,6 +689,14 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
const { replyToMsgId, replyToPeerId, isQuote } = messageReplyInfo || {};
|
const { replyToMsgId, replyToPeerId, isQuote } = messageReplyInfo || {};
|
||||||
const { peerId: storyReplyPeerId, storyId: storyReplyId } = storyReplyInfo || {};
|
const { peerId: storyReplyPeerId, storyId: storyReplyId } = storyReplyInfo || {};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if ((sticker?.hasEffect || effect) && ((
|
||||||
|
memoFirstUnreadIdRef.current && messageId >= memoFirstUnreadIdRef.current
|
||||||
|
) || isLocal)) {
|
||||||
|
requestEffect();
|
||||||
|
}
|
||||||
|
}, [effect, isLocal, memoFirstUnreadIdRef, messageId, sticker?.hasEffect]);
|
||||||
|
|
||||||
const detectedLanguage = useTextLanguage(
|
const detectedLanguage = useTextLanguage(
|
||||||
text?.text,
|
text?.text,
|
||||||
!(areTranslationsEnabled || shouldDetectChatLanguage),
|
!(areTranslationsEnabled || shouldDetectChatLanguage),
|
||||||
@ -980,7 +998,9 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
availableReactions={availableReactions}
|
availableReactions={availableReactions}
|
||||||
isTranslated={Boolean(requestedTranslationLanguage ? currentTranslatedText : undefined)}
|
isTranslated={Boolean(requestedTranslationLanguage ? currentTranslatedText : undefined)}
|
||||||
|
effectEmoji={effect?.emoticon}
|
||||||
onClick={handleMetaClick}
|
onClick={handleMetaClick}
|
||||||
|
onEffectClick={handleEffectClick}
|
||||||
onTranslationClick={handleTranslationClick}
|
onTranslationClick={handleTranslationClick}
|
||||||
onOpenThread={handleOpenThread}
|
onOpenThread={handleOpenThread}
|
||||||
/>
|
/>
|
||||||
@ -1066,20 +1086,15 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
observeIntersection={observeIntersectionForLoading}
|
observeIntersection={observeIntersectionForLoading}
|
||||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||||
shouldLoop={shouldLoopStickers}
|
shouldLoop={shouldLoopStickers}
|
||||||
shouldPlayEffect={(
|
shouldPlayEffect={shouldPlayEffect}
|
||||||
sticker.hasEffect && ((
|
withEffect={withAnimatedEffects}
|
||||||
memoFirstUnreadIdRef.current && messageId >= memoFirstUnreadIdRef.current
|
onStopEffect={hideEffect}
|
||||||
) || isLocal)
|
|
||||||
) || undefined}
|
|
||||||
withEffect={withStickerEffects}
|
|
||||||
onPlayEffect={startStickerEffect}
|
|
||||||
onStopEffect={stopStickerEffect}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{hasAnimatedEmoji && animatedCustomEmoji && (
|
{hasAnimatedEmoji && animatedCustomEmoji && (
|
||||||
<AnimatedCustomEmoji
|
<AnimatedCustomEmoji
|
||||||
customEmojiId={animatedCustomEmoji}
|
customEmojiId={animatedCustomEmoji}
|
||||||
withEffects={withStickerEffects && isUserId(chatId)}
|
withEffects={withAnimatedEffects && isUserId(chatId) && !effect}
|
||||||
isOwn={isOwn}
|
isOwn={isOwn}
|
||||||
observeIntersection={observeIntersectionForLoading}
|
observeIntersection={observeIntersectionForLoading}
|
||||||
forceLoadPreview={isLocal}
|
forceLoadPreview={isLocal}
|
||||||
@ -1091,7 +1106,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
{hasAnimatedEmoji && animatedEmoji && (
|
{hasAnimatedEmoji && animatedEmoji && (
|
||||||
<AnimatedEmoji
|
<AnimatedEmoji
|
||||||
emoji={animatedEmoji}
|
emoji={animatedEmoji}
|
||||||
withEffects={withStickerEffects && isUserId(chatId)}
|
withEffects={withAnimatedEffects && isUserId(chatId) && !effect}
|
||||||
isOwn={isOwn}
|
isOwn={isOwn}
|
||||||
observeIntersection={observeIntersectionForLoading}
|
observeIntersection={observeIntersectionForLoading}
|
||||||
forceLoadPreview={isLocal}
|
forceLoadPreview={isLocal}
|
||||||
@ -1100,6 +1115,16 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
activeEmojiInteractions={activeEmojiInteractions}
|
activeEmojiInteractions={activeEmojiInteractions}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{withAnimatedEffects && effect && (
|
||||||
|
<MessageEffect
|
||||||
|
shouldPlay={shouldPlayEffect}
|
||||||
|
message={message}
|
||||||
|
effect={effect}
|
||||||
|
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||||
|
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||||
|
onStop={hideEffect}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{phoneCall && (
|
{phoneCall && (
|
||||||
<MessagePhoneCall
|
<MessagePhoneCall
|
||||||
message={message}
|
message={message}
|
||||||
@ -1616,7 +1641,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
message, album, withSenderName, withAvatar, threadId, messageListType, isLastInDocumentGroup, isFirstInGroup,
|
message, album, withSenderName, withAvatar, threadId, messageListType, isLastInDocumentGroup, isFirstInGroup,
|
||||||
} = ownProps;
|
} = ownProps;
|
||||||
const {
|
const {
|
||||||
id, chatId, viaBotId, isOutgoing, forwardInfo, transcriptionId, isPinned, viaBusinessBotId,
|
id, chatId, viaBotId, isOutgoing, forwardInfo, transcriptionId, isPinned, viaBusinessBotId, effectId,
|
||||||
} = message;
|
} = message;
|
||||||
|
|
||||||
const chat = selectChat(global, chatId);
|
const chat = selectChat(global, chatId);
|
||||||
@ -1728,6 +1753,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
|
|
||||||
const viaBusinessBot = viaBusinessBotId ? selectUser(global, viaBusinessBotId) : undefined;
|
const viaBusinessBot = viaBusinessBotId ? selectUser(global, viaBusinessBotId) : undefined;
|
||||||
|
|
||||||
|
const effect = effectId ? global.availableEffectById[effectId] : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
theme: selectTheme(global),
|
theme: selectTheme(global),
|
||||||
forceSenderName,
|
forceSenderName,
|
||||||
@ -1793,7 +1820,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
requestedTranslationLanguage,
|
requestedTranslationLanguage,
|
||||||
requestedChatTranslationLanguage,
|
requestedChatTranslationLanguage,
|
||||||
hasLinkedChat: Boolean(chatFullInfo?.linkedChatId),
|
hasLinkedChat: Boolean(chatFullInfo?.linkedChatId),
|
||||||
withStickerEffects: selectPerformanceSettingsValue(global, 'stickerEffects'),
|
withAnimatedEffects: selectPerformanceSettingsValue(global, 'stickerEffects'),
|
||||||
webPageStory,
|
webPageStory,
|
||||||
isConnected,
|
isConnected,
|
||||||
isLoadingComments: repliesThreadInfo?.isCommentsInfo
|
isLoadingComments: repliesThreadInfo?.isCommentsInfo
|
||||||
@ -1813,6 +1840,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
tags: global.savedReactionTags?.byKey,
|
tags: global.savedReactionTags?.byKey,
|
||||||
canTranscribeVoice,
|
canTranscribeVoice,
|
||||||
viaBusinessBot,
|
viaBusinessBot,
|
||||||
|
effect,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(Message));
|
)(Message));
|
||||||
|
|||||||
21
src/components/middle/message/MessageEffect.module.scss
Normal file
21
src/components/middle/message/MessageEffect.module.scss
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
.anchor {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mirrorAnchor {
|
||||||
|
right: auto;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root {
|
||||||
|
position: fixed;
|
||||||
|
z-index: var(--z-message-effect);
|
||||||
|
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mirror {
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
105
src/components/middle/message/MessageEffect.tsx
Normal file
105
src/components/middle/message/MessageEffect.tsx
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import React, { memo, useEffect, useRef } from '../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { ApiAvailableEffect, ApiMessage } from '../../../api/types';
|
||||||
|
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
|
||||||
|
import useFlag from '../../../hooks/useFlag';
|
||||||
|
import { type ObserveFn, useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
import useMedia from '../../../hooks/useMedia';
|
||||||
|
import useOverlayPosition from './hooks/useOverlayPosition';
|
||||||
|
|
||||||
|
import AnimatedSticker from '../../common/AnimatedSticker';
|
||||||
|
import Portal from '../../ui/Portal';
|
||||||
|
|
||||||
|
import styles from './MessageEffect.module.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
message: ApiMessage;
|
||||||
|
effect: ApiAvailableEffect;
|
||||||
|
shouldPlay?: boolean;
|
||||||
|
observeIntersectionForLoading: ObserveFn;
|
||||||
|
observeIntersectionForPlaying: ObserveFn;
|
||||||
|
onStop?: VoidFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EFFECT_SIZE = 256;
|
||||||
|
|
||||||
|
const MessageEffect = ({
|
||||||
|
message,
|
||||||
|
effect,
|
||||||
|
shouldPlay,
|
||||||
|
observeIntersectionForLoading,
|
||||||
|
observeIntersectionForPlaying,
|
||||||
|
onStop,
|
||||||
|
}: OwnProps) => {
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const anchorRef = useRef<HTMLDivElement>(null);
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const canLoad = useIsIntersecting(anchorRef, observeIntersectionForLoading);
|
||||||
|
const canPlay = useIsIntersecting(anchorRef, observeIntersectionForPlaying);
|
||||||
|
|
||||||
|
const [isPlaying, startPlaying, stopPlaying] = 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({
|
||||||
|
anchorRef,
|
||||||
|
overlayRef: ref,
|
||||||
|
isMirrored,
|
||||||
|
isDisabled: !isPlaying,
|
||||||
|
isForMessageEffect: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const effectClassName = buildClassName(
|
||||||
|
styles.root,
|
||||||
|
isMirrored && styles.mirror,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={buildClassName(styles.anchor, isMirrored && styles.mirrorAnchor)} ref={anchorRef}>
|
||||||
|
{isPlaying && (
|
||||||
|
<Portal>
|
||||||
|
<AnimatedSticker
|
||||||
|
ref={ref}
|
||||||
|
key={`effect-${message.id}`}
|
||||||
|
className={effectClassName}
|
||||||
|
tgsUrl={effectBlob}
|
||||||
|
size={EFFECT_SIZE}
|
||||||
|
play
|
||||||
|
isLowPriority
|
||||||
|
noLoop
|
||||||
|
forceAlways
|
||||||
|
onEnded={handleEnded}
|
||||||
|
/>
|
||||||
|
</Portal>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function getEffectHash(effect: ApiAvailableEffect) {
|
||||||
|
if (effect.effectAnimationId) {
|
||||||
|
return `sticker${effect.effectAnimationId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `sticker${effect.effectStickerId}?size=f`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(MessageEffect);
|
||||||
@ -19,7 +19,8 @@
|
|||||||
.message-views,
|
.message-views,
|
||||||
.message-replies,
|
.message-replies,
|
||||||
.message-translated,
|
.message-translated,
|
||||||
.message-pinned {
|
.message-pinned,
|
||||||
|
.message-effect-icon {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@ -47,6 +48,16 @@
|
|||||||
margin-inline-end: 0.25rem;
|
margin-inline-end: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-effect-icon {
|
||||||
|
margin-inline-end: 0.25rem;
|
||||||
|
|
||||||
|
color: var(--color-text);
|
||||||
|
& > .emoji {
|
||||||
|
width: 1rem !important;
|
||||||
|
height: 1rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.message-pinned {
|
.message-pinned {
|
||||||
margin-inline-end: 0.1875rem;
|
margin-inline-end: 0.1875rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import useFlag from '../../../hooks/useFlag';
|
|||||||
import useOldLang from '../../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
|
|
||||||
import AnimatedCounter from '../../common/AnimatedCounter';
|
import AnimatedCounter from '../../common/AnimatedCounter';
|
||||||
|
import Icon from '../../common/icons/Icon';
|
||||||
import MessageOutgoingStatus from '../../common/MessageOutgoingStatus';
|
import MessageOutgoingStatus from '../../common/MessageOutgoingStatus';
|
||||||
|
|
||||||
import './MessageMeta.scss';
|
import './MessageMeta.scss';
|
||||||
@ -30,8 +31,10 @@ type OwnProps = {
|
|||||||
isTranslated?: boolean;
|
isTranslated?: boolean;
|
||||||
isPinned?: boolean;
|
isPinned?: boolean;
|
||||||
withFullDate?: boolean;
|
withFullDate?: boolean;
|
||||||
|
effectEmoji?: string;
|
||||||
onClick: (e: React.MouseEvent<HTMLDivElement>) => void;
|
onClick: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
onTranslationClick: (e: React.MouseEvent<HTMLDivElement>) => void;
|
onTranslationClick: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
|
onEffectClick: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
renderQuickReactionButton?: () => TeactNode | undefined;
|
renderQuickReactionButton?: () => TeactNode | undefined;
|
||||||
onOpenThread: NoneToVoidFunction;
|
onOpenThread: NoneToVoidFunction;
|
||||||
};
|
};
|
||||||
@ -47,8 +50,10 @@ const MessageMeta: FC<OwnProps> = ({
|
|||||||
isTranslated,
|
isTranslated,
|
||||||
isPinned,
|
isPinned,
|
||||||
withFullDate,
|
withFullDate,
|
||||||
|
effectEmoji,
|
||||||
onClick,
|
onClick,
|
||||||
onTranslationClick,
|
onTranslationClick,
|
||||||
|
onEffectClick,
|
||||||
onOpenThread,
|
onOpenThread,
|
||||||
}) => {
|
}) => {
|
||||||
const { showNotification } = getActions();
|
const { showNotification } = getActions();
|
||||||
@ -118,15 +123,20 @@ const MessageMeta: FC<OwnProps> = ({
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
data-ignore-on-paste
|
data-ignore-on-paste
|
||||||
>
|
>
|
||||||
|
{effectEmoji && (
|
||||||
|
<span className="message-effect-icon" onClick={onEffectClick}>
|
||||||
|
{renderText(effectEmoji)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{isTranslated && (
|
{isTranslated && (
|
||||||
<i className="icon icon-language message-translated" onClick={onTranslationClick} />
|
<Icon name="language" className="message-translated" onClick={onTranslationClick} />
|
||||||
)}
|
)}
|
||||||
{Boolean(message.viewsCount) && (
|
{Boolean(message.viewsCount) && (
|
||||||
<>
|
<>
|
||||||
<span className="message-views">
|
<span className="message-views">
|
||||||
{formatIntegerCompact(message.viewsCount!)}
|
{formatIntegerCompact(message.viewsCount!)}
|
||||||
</span>
|
</span>
|
||||||
<i className="icon icon-channelviews" />
|
<Icon name="channelviews" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!noReplies && Boolean(repliesThreadInfo?.messagesCount) && (
|
{!noReplies && Boolean(repliesThreadInfo?.messagesCount) && (
|
||||||
@ -134,11 +144,11 @@ const MessageMeta: FC<OwnProps> = ({
|
|||||||
<span className="message-replies">
|
<span className="message-replies">
|
||||||
<AnimatedCounter text={formatIntegerCompact(repliesThreadInfo!.messagesCount!)} />
|
<AnimatedCounter text={formatIntegerCompact(repliesThreadInfo!.messagesCount!)} />
|
||||||
</span>
|
</span>
|
||||||
<i className="icon icon-reply-filled" />
|
<Icon name="reply-filled" />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{isPinned && (
|
{isPinned && (
|
||||||
<i className="icon icon-pinned-message message-pinned" />
|
<Icon name="pinned-message" className="message-pinned" />
|
||||||
)}
|
)}
|
||||||
{signature && (
|
{signature && (
|
||||||
<span className="message-signature">{renderText(signature)}</span>
|
<span className="message-signature">{renderText(signature)}</span>
|
||||||
|
|||||||
23
src/components/middle/message/Sticker.module.scss
Normal file
23
src/components/middle/message/Sticker.module.scss
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
.root {
|
||||||
|
overflow: visible !important;
|
||||||
|
contain: layout;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:not(.inactive) {
|
||||||
|
cursor: var(--custom-cursor, pointer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mirrored {
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inactive {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect {
|
||||||
|
position: fixed;
|
||||||
|
z-index: var(--z-message-effect);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
@ -1,24 +0,0 @@
|
|||||||
.Sticker {
|
|
||||||
overflow: visible !important;
|
|
||||||
contain: layout;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&.reversed {
|
|
||||||
transform: scaleX(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.inactive) {
|
|
||||||
cursor: var(--custom-cursor, pointer);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.inactive {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.effect-sticker {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
right: -1rem;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -17,12 +17,13 @@ import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
|||||||
import useLastCallback from '../../../hooks/useLastCallback';
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
import useMedia from '../../../hooks/useMedia';
|
import useMedia from '../../../hooks/useMedia';
|
||||||
import useOldLang from '../../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
import usePrevious from '../../../hooks/usePrevious';
|
import useOverlayPosition from './hooks/useOverlayPosition';
|
||||||
|
|
||||||
import AnimatedSticker from '../../common/AnimatedSticker';
|
import AnimatedSticker from '../../common/AnimatedSticker';
|
||||||
import StickerView from '../../common/StickerView';
|
import StickerView from '../../common/StickerView';
|
||||||
|
import Portal from '../../ui/Portal';
|
||||||
|
|
||||||
import './Sticker.scss';
|
import styles from './Sticker.module.scss';
|
||||||
|
|
||||||
// https://github.com/telegramdesktop/tdesktop/blob/master/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp#L42
|
// https://github.com/telegramdesktop/tdesktop/blob/master/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp#L42
|
||||||
const EFFECT_SIZE_MULTIPLIER = 1 + 0.245 * 2;
|
const EFFECT_SIZE_MULTIPLIER = 1 + 0.245 * 2;
|
||||||
@ -34,13 +35,12 @@ type OwnProps = {
|
|||||||
shouldLoop?: boolean;
|
shouldLoop?: boolean;
|
||||||
shouldPlayEffect?: boolean;
|
shouldPlayEffect?: boolean;
|
||||||
withEffect?: boolean;
|
withEffect?: boolean;
|
||||||
onPlayEffect?: VoidFunction;
|
|
||||||
onStopEffect?: VoidFunction;
|
onStopEffect?: VoidFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Sticker: FC<OwnProps> = ({
|
const Sticker: FC<OwnProps> = ({
|
||||||
message, observeIntersection, observeIntersectionForPlaying, shouldLoop,
|
message, observeIntersection, observeIntersectionForPlaying, shouldLoop,
|
||||||
shouldPlayEffect, withEffect, onPlayEffect, onStopEffect,
|
shouldPlayEffect, withEffect, onStopEffect,
|
||||||
}) => {
|
}) => {
|
||||||
const { showNotification, openStickerSet } = getActions();
|
const { showNotification, openStickerSet } = getActions();
|
||||||
|
|
||||||
@ -50,8 +50,12 @@ const Sticker: FC<OwnProps> = ({
|
|||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const effectRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const sticker = message.content.sticker!;
|
const sticker = message.content.sticker!;
|
||||||
const { stickerSetInfo, isVideo, hasEffect } = sticker;
|
const { stickerSetInfo, isVideo, hasEffect } = sticker;
|
||||||
|
const isMirrored = !message.isOutgoing;
|
||||||
|
|
||||||
const mediaHash = sticker.isPreloadedGlobally ? undefined : (
|
const mediaHash = sticker.isPreloadedGlobally ? undefined : (
|
||||||
getMessageMediaHash(message, isVideo && !IS_WEBM_SUPPORTED ? 'pictogram' : 'inline')!
|
getMessageMediaHash(message, isVideo && !IS_WEBM_SUPPORTED ? 'pictogram' : 'inline')!
|
||||||
@ -62,7 +66,7 @@ const Sticker: FC<OwnProps> = ({
|
|||||||
const mediaHashEffect = `sticker${sticker.id}?size=f`;
|
const mediaHashEffect = `sticker${sticker.id}?size=f`;
|
||||||
const effectBlobUrl = useMedia(
|
const effectBlobUrl = useMedia(
|
||||||
mediaHashEffect,
|
mediaHashEffect,
|
||||||
!canLoad || !hasEffect,
|
!canLoad || !hasEffect || !withEffect,
|
||||||
ApiMediaFormat.BlobUrl,
|
ApiMediaFormat.BlobUrl,
|
||||||
);
|
);
|
||||||
const [isPlayingEffect, startPlayingEffect, stopPlayingEffect] = useFlag();
|
const [isPlayingEffect, startPlayingEffect, stopPlayingEffect] = useFlag();
|
||||||
@ -72,14 +76,19 @@ const Sticker: FC<OwnProps> = ({
|
|||||||
onStopEffect?.();
|
onStopEffect?.();
|
||||||
});
|
});
|
||||||
|
|
||||||
const previousShouldPlayEffect = usePrevious(shouldPlayEffect);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasEffect && withEffect && canPlay && (shouldPlayEffect || previousShouldPlayEffect)) {
|
if (hasEffect && withEffect && canPlay && shouldPlayEffect) {
|
||||||
startPlayingEffect();
|
startPlayingEffect();
|
||||||
onPlayEffect?.();
|
|
||||||
}
|
}
|
||||||
}, [hasEffect, canPlay, onPlayEffect, shouldPlayEffect, previousShouldPlayEffect, startPlayingEffect, withEffect]);
|
}, [hasEffect, canPlay, shouldPlayEffect, startPlayingEffect, withEffect]);
|
||||||
|
|
||||||
|
const shouldRenderEffect = hasEffect && withEffect && effectBlobUrl && isPlayingEffect;
|
||||||
|
useOverlayPosition({
|
||||||
|
anchorRef: ref,
|
||||||
|
overlayRef: effectRef,
|
||||||
|
isMirrored,
|
||||||
|
isDisabled: !shouldRenderEffect,
|
||||||
|
});
|
||||||
|
|
||||||
const openModal = useLastCallback(() => {
|
const openModal = useLastCallback(() => {
|
||||||
openStickerSet({
|
openStickerSet({
|
||||||
@ -103,7 +112,6 @@ const Sticker: FC<OwnProps> = ({
|
|||||||
return;
|
return;
|
||||||
} else if (withEffect) {
|
} else if (withEffect) {
|
||||||
startPlayingEffect();
|
startPlayingEffect();
|
||||||
onPlayEffect?.();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -113,9 +121,10 @@ const Sticker: FC<OwnProps> = ({
|
|||||||
const isMemojiSticker = 'isMissing' in stickerSetInfo;
|
const isMemojiSticker = 'isMissing' in stickerSetInfo;
|
||||||
const { width, height } = getStickerDimensions(sticker, isMobile);
|
const { width, height } = getStickerDimensions(sticker, isMobile);
|
||||||
const className = buildClassName(
|
const className = buildClassName(
|
||||||
'Sticker media-inner',
|
'media-inner',
|
||||||
isMemojiSticker && 'inactive',
|
styles.root,
|
||||||
hasEffect && !message.isOutgoing && 'reversed',
|
isMemojiSticker && styles.inactive,
|
||||||
|
hasEffect && isMirrored && styles.mirrored,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -136,17 +145,20 @@ const Sticker: FC<OwnProps> = ({
|
|||||||
noPlay={!canPlay}
|
noPlay={!canPlay}
|
||||||
withSharedAnimation
|
withSharedAnimation
|
||||||
/>
|
/>
|
||||||
{hasEffect && withEffect && canLoad && isPlayingEffect && (
|
{shouldRenderEffect && (
|
||||||
<AnimatedSticker
|
<Portal>
|
||||||
key={mediaHashEffect}
|
<AnimatedSticker
|
||||||
className="effect-sticker"
|
ref={effectRef}
|
||||||
tgsUrl={effectBlobUrl}
|
key={mediaHashEffect}
|
||||||
size={width * EFFECT_SIZE_MULTIPLIER}
|
className={buildClassName(styles.effect, isMirrored && styles.mirrored)}
|
||||||
play
|
tgsUrl={effectBlobUrl}
|
||||||
isLowPriority
|
size={width * EFFECT_SIZE_MULTIPLIER}
|
||||||
noLoop
|
play
|
||||||
onEnded={handleEffectEnded}
|
isLowPriority
|
||||||
/>
|
noLoop
|
||||||
|
onEnded={handleEffectEnded}
|
||||||
|
/>
|
||||||
|
</Portal>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
65
src/components/middle/message/hooks/useOverlayPosition.ts
Normal file
65
src/components/middle/message/hooks/useOverlayPosition.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { type RefObject } from 'react';
|
||||||
|
import { useEffect } from '../../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import { requestMutation } from '../../../../lib/fasterdom/fasterdom';
|
||||||
|
import { REM } from '../../../common/helpers/mediaDimensions';
|
||||||
|
|
||||||
|
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||||
|
|
||||||
|
const OFFSET_X = REM;
|
||||||
|
|
||||||
|
export default function useOverlayPosition({
|
||||||
|
anchorRef,
|
||||||
|
overlayRef,
|
||||||
|
isMirrored,
|
||||||
|
isForMessageEffect,
|
||||||
|
isDisabled,
|
||||||
|
} : {
|
||||||
|
anchorRef: RefObject<HTMLDivElement>;
|
||||||
|
overlayRef: RefObject<HTMLDivElement>;
|
||||||
|
isMirrored?: boolean;
|
||||||
|
isForMessageEffect?: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
}) {
|
||||||
|
const updatePosition = useLastCallback(() => {
|
||||||
|
const element = overlayRef.current;
|
||||||
|
const anchor = anchorRef.current;
|
||||||
|
if (!element || !anchor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const anchorRect = anchor.getBoundingClientRect();
|
||||||
|
const elementRect = element.getBoundingClientRect();
|
||||||
|
const windowWidth = window.innerWidth;
|
||||||
|
|
||||||
|
requestMutation(() => {
|
||||||
|
const anchorCenterY = anchorRect.top + anchorRect.height / 2;
|
||||||
|
const anchorBottomY = anchorRect.bottom;
|
||||||
|
const y = isForMessageEffect ? anchorBottomY : anchorCenterY;
|
||||||
|
element.style.top = `${y - elementRect.height / 2}px`;
|
||||||
|
|
||||||
|
if (isMirrored) {
|
||||||
|
element.style.left = `${anchorRect.left - OFFSET_X}px`;
|
||||||
|
} else {
|
||||||
|
element.style.right = `${windowWidth - anchorRect.right - OFFSET_X}px`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDisabled) return;
|
||||||
|
updatePosition();
|
||||||
|
}, [isDisabled]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDisabled) return undefined;
|
||||||
|
|
||||||
|
const messagesContainer = anchorRef.current!.closest<HTMLDivElement>('.MessageList')!;
|
||||||
|
|
||||||
|
messagesContainer.addEventListener('scroll', updatePosition, { passive: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
messagesContainer.removeEventListener('scroll', updatePosition);
|
||||||
|
};
|
||||||
|
}, [isDisabled, anchorRef]);
|
||||||
|
}
|
||||||
@ -77,6 +77,22 @@ addActionHandler('loadAvailableReactions', async (global): Promise<void> => {
|
|||||||
}, GENERAL_REFETCH_INTERVAL);
|
}, GENERAL_REFETCH_INTERVAL);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('loadAvailableEffects', async (global): Promise<void> => {
|
||||||
|
const result = await callApi('fetchAvailableEffects');
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const effectById = buildCollectionByKey(result, 'id');
|
||||||
|
|
||||||
|
global = getGlobal();
|
||||||
|
global = {
|
||||||
|
...global,
|
||||||
|
availableEffectById: effectById,
|
||||||
|
};
|
||||||
|
setGlobal(global);
|
||||||
|
});
|
||||||
|
|
||||||
addActionHandler('interactWithAnimatedEmoji', (global, actions, payload): ActionReturnType => {
|
addActionHandler('interactWithAnimatedEmoji', (global, actions, payload): ActionReturnType => {
|
||||||
const {
|
const {
|
||||||
emoji, x, y, startSize, isReversed, tabId = getCurrentTabId(),
|
emoji, x, y, startSize, isReversed, tabId = getCurrentTabId(),
|
||||||
|
|||||||
@ -286,6 +286,7 @@ export function serializeGlobal<T extends GlobalState>(global: T) {
|
|||||||
'peerColors',
|
'peerColors',
|
||||||
'savedReactionTags',
|
'savedReactionTags',
|
||||||
'timezones',
|
'timezones',
|
||||||
|
'availableEffectById',
|
||||||
]),
|
]),
|
||||||
lastIsChatInfoShown: !getIsMobile() ? global.lastIsChatInfoShown : undefined,
|
lastIsChatInfoShown: !getIsMobile() ? global.lastIsChatInfoShown : undefined,
|
||||||
customEmojis: reduceCustomEmojis(global),
|
customEmojis: reduceCustomEmojis(global),
|
||||||
|
|||||||
@ -162,6 +162,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
|||||||
recentReactions: [],
|
recentReactions: [],
|
||||||
hash: {},
|
hash: {},
|
||||||
},
|
},
|
||||||
|
availableEffectById: {},
|
||||||
|
|
||||||
stickers: {
|
stickers: {
|
||||||
setsById: {},
|
setsById: {},
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import type {
|
|||||||
ApiAppConfig,
|
ApiAppConfig,
|
||||||
ApiAttachBot,
|
ApiAttachBot,
|
||||||
ApiAttachment,
|
ApiAttachment,
|
||||||
|
ApiAvailableEffect,
|
||||||
ApiAvailableReaction,
|
ApiAvailableReaction,
|
||||||
ApiBoost,
|
ApiBoost,
|
||||||
ApiBoostsStatus,
|
ApiBoostsStatus,
|
||||||
@ -986,6 +987,7 @@ export type GlobalState = {
|
|||||||
defaultTags?: string;
|
defaultTags?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
availableEffectById: Record<string, ApiAvailableEffect>;
|
||||||
|
|
||||||
stickers: {
|
stickers: {
|
||||||
setsById: Record<string, ApiStickerSet>;
|
setsById: Record<string, ApiStickerSet>;
|
||||||
@ -2646,6 +2648,8 @@ export interface ActionPayloads {
|
|||||||
loadGenericEmojiEffects: undefined;
|
loadGenericEmojiEffects: undefined;
|
||||||
loadBirthdayNumbersStickers: undefined;
|
loadBirthdayNumbersStickers: undefined;
|
||||||
|
|
||||||
|
loadAvailableEffects: undefined;
|
||||||
|
|
||||||
addRecentSticker: {
|
addRecentSticker: {
|
||||||
sticker: ApiSticker;
|
sticker: ApiSticker;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1523,6 +1523,7 @@ messages.getOutboxReadDate#8c4bfe5d peer:InputPeer msg_id:int = OutboxReadDate;
|
|||||||
messages.getQuickReplies#d483f2a8 hash:long = messages.QuickReplies;
|
messages.getQuickReplies#d483f2a8 hash:long = messages.QuickReplies;
|
||||||
messages.getQuickReplyMessages#94a495c3 flags:# shortcut_id:int id:flags.0?Vector<int> hash:long = messages.Messages;
|
messages.getQuickReplyMessages#94a495c3 flags:# shortcut_id:int id:flags.0?Vector<int> hash:long = messages.Messages;
|
||||||
messages.sendQuickReplyMessages#6c750de1 peer:InputPeer shortcut_id:int id:Vector<int> random_id:Vector<long> = Updates;
|
messages.sendQuickReplyMessages#6c750de1 peer:InputPeer shortcut_id:int id:Vector<int> random_id:Vector<long> = Updates;
|
||||||
|
messages.getAvailableEffects#dea20a39 hash:int = messages.AvailableEffects;
|
||||||
messages.getFactCheck#b9cdc5ee peer:InputPeer msg_id:Vector<int> = Vector<FactCheck>;
|
messages.getFactCheck#b9cdc5ee peer:InputPeer msg_id:Vector<int> = Vector<FactCheck>;
|
||||||
updates.getState#edd4882a = updates.State;
|
updates.getState#edd4882a = updates.State;
|
||||||
updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference;
|
updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference;
|
||||||
|
|||||||
@ -263,6 +263,7 @@
|
|||||||
"messages.getMessageReactionsList",
|
"messages.getMessageReactionsList",
|
||||||
"messages.setChatAvailableReactions",
|
"messages.setChatAvailableReactions",
|
||||||
"messages.getAvailableReactions",
|
"messages.getAvailableReactions",
|
||||||
|
"messages.getAvailableEffects",
|
||||||
"messages.setDefaultReaction",
|
"messages.setDefaultReaction",
|
||||||
"messages.translateText",
|
"messages.translateText",
|
||||||
"help.getAppConfig",
|
"help.getAppConfig",
|
||||||
|
|||||||
@ -250,6 +250,7 @@ $color-message-story-mention-to: #74bcff;
|
|||||||
--z-animation-fade: 50;
|
--z-animation-fade: 50;
|
||||||
--z-menu-bubble: 21;
|
--z-menu-bubble: 21;
|
||||||
--z-menu-backdrop: 20;
|
--z-menu-backdrop: 20;
|
||||||
|
--z-message-effect: 15;
|
||||||
--z-message-highlighted: 14;
|
--z-message-highlighted: 14;
|
||||||
--z-forum-panel: 13;
|
--z-forum-panel: 13;
|
||||||
--z-message-context-menu: 13;
|
--z-message-context-menu: 13;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user