Story: Display reaction effect (#3829)
This commit is contained in:
parent
6e43795216
commit
31403702fe
@ -110,7 +110,8 @@
|
||||
|
||||
&.story-reaction-button {
|
||||
--custom-emoji-size: 1.5rem;
|
||||
--custom-emoji-border-radius: 0.25rem;
|
||||
|
||||
overflow: visible !important;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background) !important;
|
||||
|
||||
@ -26,7 +26,8 @@ import type {
|
||||
ApiVideo,
|
||||
} from '../../api/types';
|
||||
import type {
|
||||
ApiDraft, GlobalState, MessageList, MessageListType, TabState,
|
||||
ApiDraft, GlobalState, MessageList,
|
||||
MessageListType, TabState,
|
||||
} from '../../global/types';
|
||||
import type { IAnchorPosition, InlineBotSettings, ISettings } from '../../types';
|
||||
|
||||
@ -41,6 +42,7 @@ import {
|
||||
import { requestMeasure, requestNextMutation } from '../../lib/fasterdom/fasterdom';
|
||||
import {
|
||||
getAllowedAttachmentOptions,
|
||||
getStoryKey,
|
||||
isChatAdmin,
|
||||
isChatChannel,
|
||||
isChatSuperGroup,
|
||||
@ -92,7 +94,6 @@ import applyIosAutoCapitalizationFix from '../middle/composer/helpers/applyIosAu
|
||||
import buildAttachment, { prepareAttachmentsToSend } from '../middle/composer/helpers/buildAttachment';
|
||||
import { buildCustomEmojiHtml } from '../middle/composer/helpers/customEmoji';
|
||||
import { isSelectionInsideInput } from '../middle/composer/helpers/selection';
|
||||
import { REM } from './helpers/mediaDimensions';
|
||||
import renderText from './helpers/renderText';
|
||||
import { getTextWithEntitiesAsHtml } from './helpers/renderTextWithEntities';
|
||||
|
||||
@ -149,7 +150,7 @@ import ResponsiveHoverButton from '../ui/ResponsiveHoverButton';
|
||||
import Spinner from '../ui/Spinner';
|
||||
import Avatar from './Avatar';
|
||||
import DeleteMessageModal from './DeleteMessageModal.async';
|
||||
import ReactionStaticEmoji from './ReactionStaticEmoji';
|
||||
import ReactionAnimatedEmoji from './reactions/ReactionAnimatedEmoji';
|
||||
|
||||
import './Composer.scss';
|
||||
|
||||
@ -260,7 +261,6 @@ const MESSAGE_MAX_LENGTH = 4096;
|
||||
const SENDING_ANIMATION_DURATION = 350;
|
||||
const MOUNT_ANIMATION_DURATION = 430;
|
||||
|
||||
const REACTION_SIZE = 1.5 * REM;
|
||||
const HEART_REACTION: ApiReaction = {
|
||||
emoticon: '❤',
|
||||
};
|
||||
@ -1293,8 +1293,8 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
|| isMentionTooltipOpen || isInlineBotTooltipOpen || isDeleteModalOpen || isBotCommandMenuOpen || isAttachMenuOpen
|
||||
|| isStickerTooltipOpen || isBotCommandTooltipOpen || isCustomEmojiTooltipOpen || isBotMenuButtonOpen
|
||||
|| isCustomSendMenuOpen || Boolean(activeVoiceRecording) || attachments.length > 0 || isInputHasFocus;
|
||||
const isReactionSelectorOpen = (isComposerHasFocus || isReactionPickerOpen)
|
||||
&& isInStoryViewer && !isAttachMenuOpen && !isSymbolMenuOpen;
|
||||
const isReactionSelectorOpen = isComposerHasFocus && !isReactionPickerOpen && isInStoryViewer && !isAttachMenuOpen
|
||||
&& !isSymbolMenuOpen;
|
||||
|
||||
useEffect(() => {
|
||||
if (isComposerHasFocus) {
|
||||
@ -1814,13 +1814,14 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
ariaLabel={lang('AccDescrLike')}
|
||||
ref={storyReactionRef}
|
||||
>
|
||||
{sentStoryReaction && !isSentStoryReactionHeart ? (
|
||||
<ReactionStaticEmoji
|
||||
{sentStoryReaction && (
|
||||
<ReactionAnimatedEmoji
|
||||
containerId={getStoryKey(chatId, storyId!)}
|
||||
reaction={sentStoryReaction}
|
||||
availableReactions={availableReactions}
|
||||
size={REACTION_SIZE}
|
||||
withEffectOnly={isSentStoryReactionHeart}
|
||||
/>
|
||||
) : (
|
||||
)}
|
||||
{(!sentStoryReaction || isSentStoryReactionHeart) && (
|
||||
<i
|
||||
className={buildClassName(
|
||||
'icon',
|
||||
|
||||
@ -99,6 +99,7 @@ const ReactionEmoji: FC<OwnProps> = ({
|
||||
className={transitionClassNames}
|
||||
sharedCanvas={sharedCanvasRef!.current || undefined}
|
||||
sharedCanvasCoords={coords}
|
||||
forceAlways={forcePlayback}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -9,7 +9,7 @@ import { IS_OFFSET_PATH_SUPPORTED } from '../../../util/windowEnvironment';
|
||||
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
|
||||
import CustomEmoji from '../../common/CustomEmoji';
|
||||
import CustomEmoji from '../CustomEmoji';
|
||||
|
||||
import styles from './CustomEmojiEffect.module.scss';
|
||||
|
||||
@ -6,23 +6,27 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
margin-inline-end: 0.125rem;
|
||||
width: var(--custom-emoji-size);
|
||||
height: var(--custom-emoji-size);
|
||||
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.animated-icon, .effect {
|
||||
position: fixed;
|
||||
top: -0.375rem;
|
||||
left: -0.375rem;
|
||||
pointer-events: none;
|
||||
.animated-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
&.effect {
|
||||
top: -2.25rem;
|
||||
left: -2.25rem;
|
||||
}
|
||||
.effect {
|
||||
position: fixed;
|
||||
top: -2.25rem;
|
||||
left: -2.25rem;
|
||||
}
|
||||
|
||||
.animated-icon, .effect {
|
||||
pointer-events: none;
|
||||
|
||||
&:not(:global(.open)) {
|
||||
opacity: 1 !important;
|
||||
@ -39,3 +43,10 @@
|
||||
// Fix for redundant scroll in Firefox
|
||||
contain: layout;
|
||||
}
|
||||
|
||||
.withEffectOnly {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
@ -1,51 +1,65 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useMemo, useRef,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions } from '../../../global';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiAvailableReaction, ApiReaction, ApiStickerSet } from '../../../api/types';
|
||||
import type { ActiveReaction } from '../../../global/types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
|
||||
import { isSameReaction } from '../../../global/helpers';
|
||||
import { selectPerformanceSettingsValue, selectTabState } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { REM } from '../../common/helpers/mediaDimensions';
|
||||
import { roundToNearestEven } from '../../../util/math';
|
||||
import { REM } from '../helpers/mediaDimensions';
|
||||
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
import useShowTransition from '../../../hooks/useShowTransition';
|
||||
import useCustomEmoji from '../../common/hooks/useCustomEmoji';
|
||||
import useCustomEmoji from '../hooks/useCustomEmoji';
|
||||
|
||||
import AnimatedSticker from '../../common/AnimatedSticker';
|
||||
import CustomEmoji from '../../common/CustomEmoji';
|
||||
import ReactionStaticEmoji from '../../common/ReactionStaticEmoji';
|
||||
import AnimatedSticker from '../AnimatedSticker';
|
||||
import CustomEmoji from '../CustomEmoji';
|
||||
import ReactionStaticEmoji from '../ReactionStaticEmoji';
|
||||
import CustomEmojiEffect from './CustomEmojiEffect';
|
||||
|
||||
import styles from './ReactionAnimatedEmoji.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
containerId: string;
|
||||
reaction: ApiReaction;
|
||||
activeReactions?: ActiveReaction[];
|
||||
className?: string;
|
||||
size?: number;
|
||||
effectSize?: number;
|
||||
withEffectOnly?: boolean;
|
||||
observeIntersection?: ObserveFn;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
activeReactions?: ApiReaction[];
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
genericEffects?: ApiStickerSet;
|
||||
observeIntersection?: ObserveFn;
|
||||
withEffects?: boolean;
|
||||
};
|
||||
|
||||
const CENTER_ICON_SIZE = 2.5 * REM;
|
||||
const ICON_SIZE = 1.5 * REM;
|
||||
const CENTER_ICON_MULTIPLIER = 1.9;
|
||||
const EFFECT_SIZE = 6.5 * REM;
|
||||
|
||||
const ReactionAnimatedEmoji: FC<OwnProps> = ({
|
||||
const ReactionAnimatedEmoji = ({
|
||||
containerId,
|
||||
reaction,
|
||||
genericEffects,
|
||||
className,
|
||||
size = ICON_SIZE,
|
||||
effectSize = EFFECT_SIZE,
|
||||
activeReactions,
|
||||
availableReactions,
|
||||
observeIntersection,
|
||||
genericEffects,
|
||||
withEffects,
|
||||
}) => {
|
||||
withEffectOnly,
|
||||
observeIntersection,
|
||||
}: OwnProps & StateProps) => {
|
||||
const { stopActiveReaction } = getActions();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
@ -93,7 +107,7 @@ const ReactionAnimatedEmoji: FC<OwnProps> = ({
|
||||
const mediaDataEffect = useMedia(mediaHashEffect, !effectId);
|
||||
|
||||
const activeReaction = useMemo(() => (
|
||||
activeReactions?.find((active) => isSameReaction(active.reaction, reaction))
|
||||
activeReactions?.find((active) => isSameReaction(active, reaction))
|
||||
), [activeReactions, reaction]);
|
||||
|
||||
const shouldPlay = Boolean(withEffects && activeReaction && (isCustom || mediaDataCenterIcon) && mediaDataEffect);
|
||||
@ -103,26 +117,39 @@ const ReactionAnimatedEmoji: FC<OwnProps> = ({
|
||||
} = useShowTransition(shouldPlay, undefined, true, 'slow');
|
||||
|
||||
const handleEnded = useLastCallback(() => {
|
||||
if (!activeReaction?.messageId) return;
|
||||
stopActiveReaction({ messageId: activeReaction.messageId, reaction });
|
||||
stopActiveReaction({ containerId, reaction });
|
||||
});
|
||||
|
||||
const [isAnimationLoaded, markAnimationLoaded, unmarkAnimationLoaded] = useFlag();
|
||||
const shouldRenderStatic = !isCustom && (!shouldPlay || !isAnimationLoaded);
|
||||
const shouldShowStatic = !isCustom && (!shouldPlay || !isAnimationLoaded);
|
||||
const {
|
||||
shouldRender: shouldRenderStatic,
|
||||
transitionClassNames: staticClassNames,
|
||||
} = useShowTransition(shouldShowStatic, undefined, true);
|
||||
|
||||
const className = buildClassName(
|
||||
const rootClassName = buildClassName(
|
||||
styles.root,
|
||||
shouldRenderAnimation && styles.animating,
|
||||
isCustom && styles.isCustomEmoji,
|
||||
withEffectOnly && styles.withEffectOnly,
|
||||
className,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={className} ref={ref}>
|
||||
{shouldRenderStatic && <ReactionStaticEmoji reaction={reaction} availableReactions={availableReactions} />}
|
||||
{isCustom && (
|
||||
<div className={rootClassName} ref={ref}>
|
||||
{!withEffectOnly && shouldRenderStatic && (
|
||||
<ReactionStaticEmoji
|
||||
className={staticClassNames}
|
||||
reaction={reaction}
|
||||
availableReactions={availableReactions}
|
||||
size={size}
|
||||
observeIntersection={observeIntersection}
|
||||
/>
|
||||
)}
|
||||
{!withEffectOnly && isCustom && (
|
||||
<CustomEmoji
|
||||
documentId={reaction.documentId}
|
||||
className={styles.customEmoji}
|
||||
size={size}
|
||||
observeIntersectionForPlaying={observeIntersection}
|
||||
/>
|
||||
)}
|
||||
@ -131,20 +158,19 @@ const ReactionAnimatedEmoji: FC<OwnProps> = ({
|
||||
<AnimatedSticker
|
||||
key={effectId}
|
||||
className={buildClassName(styles.effect, animationClassNames)}
|
||||
size={EFFECT_SIZE}
|
||||
size={effectSize}
|
||||
tgsUrl={mediaDataEffect}
|
||||
play={isIntersecting}
|
||||
noLoop
|
||||
forceAlways
|
||||
onEnded={handleEnded}
|
||||
/>
|
||||
{isCustom ? (
|
||||
!assignedEffectId && isIntersecting && <CustomEmojiEffect reaction={reaction} />
|
||||
) : (
|
||||
{isCustom && !assignedEffectId && isIntersecting && <CustomEmojiEffect reaction={reaction} />}
|
||||
{!isCustom && !withEffectOnly && (
|
||||
<AnimatedSticker
|
||||
key={centerIconId}
|
||||
className={buildClassName(styles.animatedIcon, animationClassNames)}
|
||||
size={CENTER_ICON_SIZE}
|
||||
size={roundToNearestEven(size * CENTER_ICON_MULTIPLIER)}
|
||||
tgsUrl={mediaDataCenterIcon}
|
||||
play={isIntersecting}
|
||||
noLoop
|
||||
@ -159,4 +185,18 @@ const ReactionAnimatedEmoji: FC<OwnProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ReactionAnimatedEmoji);
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { containerId }) => {
|
||||
const { availableReactions, genericEmojiEffects } = global;
|
||||
const { activeReactions } = selectTabState(global);
|
||||
|
||||
const withEffects = selectPerformanceSettingsValue(global, 'reactionEffects');
|
||||
|
||||
return {
|
||||
activeReactions: activeReactions?.[containerId],
|
||||
availableReactions,
|
||||
genericEffects: genericEmojiEffects,
|
||||
withEffects,
|
||||
};
|
||||
},
|
||||
)(ReactionAnimatedEmoji));
|
||||
@ -15,7 +15,7 @@ import useTimeout from '../../../hooks/useTimeout';
|
||||
|
||||
import CustomEmoji from '../../common/CustomEmoji';
|
||||
import PremiumIcon from '../../common/PremiumIcon';
|
||||
import CustomEmojiEffect from '../../middle/message/CustomEmojiEffect';
|
||||
import CustomEmojiEffect from '../../common/reactions/CustomEmojiEffect';
|
||||
import Button from '../../ui/Button';
|
||||
import StatusPickerMenu from './StatusPickerMenu.async';
|
||||
|
||||
|
||||
@ -11,7 +11,6 @@ import type {
|
||||
ApiMessage,
|
||||
ApiMessageOutgoingStatus,
|
||||
ApiReaction,
|
||||
ApiStickerSet,
|
||||
ApiThreadInfo,
|
||||
ApiTopic,
|
||||
ApiTypeStory,
|
||||
@ -20,7 +19,6 @@ import type {
|
||||
} from '../../../api/types';
|
||||
import type {
|
||||
ActiveEmojiInteraction,
|
||||
ActiveReaction,
|
||||
ChatTranslatedMessages,
|
||||
MessageListType,
|
||||
} from '../../../global/types';
|
||||
@ -37,6 +35,7 @@ import {
|
||||
getMessageContent,
|
||||
getMessageCustomShape,
|
||||
getMessageHtmlId,
|
||||
getMessageKey,
|
||||
getMessageLocation,
|
||||
getMessageSingleCustomEmoji,
|
||||
getMessageSingleRegularEmoji,
|
||||
@ -230,7 +229,7 @@ type StateProps = {
|
||||
highlight?: string;
|
||||
animatedEmoji?: string;
|
||||
animatedCustomEmoji?: string;
|
||||
genericEffects?: ApiStickerSet;
|
||||
hasActiveReactions?: boolean;
|
||||
isInSelectMode?: boolean;
|
||||
isSelected?: boolean;
|
||||
isGroupSelected?: boolean;
|
||||
@ -247,7 +246,6 @@ type StateProps = {
|
||||
reactionMessage?: ApiMessage;
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
defaultReaction?: ApiReaction;
|
||||
activeReactions?: ActiveReaction[];
|
||||
activeEmojiInteractions?: ActiveEmojiInteraction[];
|
||||
hasUnreadReaction?: boolean;
|
||||
isTranscribing?: boolean;
|
||||
@ -262,7 +260,6 @@ type StateProps = {
|
||||
shouldDetectChatLanguage?: boolean;
|
||||
requestedTranslationLanguage?: string;
|
||||
requestedChatTranslationLanguage?: string;
|
||||
withReactionEffects?: boolean;
|
||||
withStickerEffects?: boolean;
|
||||
webPageStory?: ApiTypeStory;
|
||||
isConnected: boolean;
|
||||
@ -341,7 +338,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
highlight,
|
||||
animatedEmoji,
|
||||
animatedCustomEmoji,
|
||||
genericEffects,
|
||||
hasActiveReactions,
|
||||
hasLinkedChat,
|
||||
isInSelectMode,
|
||||
isSelected,
|
||||
@ -350,7 +347,6 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
reactionMessage,
|
||||
availableReactions,
|
||||
defaultReaction,
|
||||
activeReactions,
|
||||
activeEmojiInteractions,
|
||||
messageListType,
|
||||
isPinnedList,
|
||||
@ -371,7 +367,6 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
shouldDetectChatLanguage,
|
||||
requestedTranslationLanguage,
|
||||
requestedChatTranslationLanguage,
|
||||
withReactionEffects,
|
||||
withStickerEffects,
|
||||
webPageStory,
|
||||
isConnected,
|
||||
@ -611,7 +606,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
isSwiped && 'is-swiped',
|
||||
transitionClassNames,
|
||||
isJustAdded && 'is-just-added',
|
||||
(Boolean(activeReactions) || hasActiveStickerEffect) && 'has-active-reaction',
|
||||
(hasActiveReactions || hasActiveStickerEffect) && 'has-active-reaction',
|
||||
isStoryMention && 'is-story-mention',
|
||||
);
|
||||
|
||||
@ -864,7 +859,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={buildClassName('quick-reaction', isQuickReactionVisible && !activeReactions && 'visible')}
|
||||
className={buildClassName('quick-reaction', isQuickReactionVisible && !hasActiveReactions && 'visible')}
|
||||
onClick={handleSendQuickReaction}
|
||||
ref={quickReactionRef}
|
||||
>
|
||||
@ -877,7 +872,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
</div>
|
||||
);
|
||||
}, [
|
||||
activeReactions, availableReactions, defaultReaction, handleSendQuickReaction, isQuickReactionVisible,
|
||||
hasActiveReactions, availableReactions, defaultReaction, handleSendQuickReaction, isQuickReactionVisible,
|
||||
observeIntersectionForPlaying,
|
||||
]);
|
||||
|
||||
@ -908,14 +903,10 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
|
||||
return (
|
||||
<Reactions
|
||||
activeReactions={activeReactions}
|
||||
message={reactionMessage!}
|
||||
metaChildren={meta}
|
||||
availableReactions={availableReactions}
|
||||
genericEffects={genericEffects}
|
||||
observeIntersection={observeIntersectionForPlaying}
|
||||
noRecentReactors={isChannel}
|
||||
withEffects={withReactionEffects}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1381,12 +1372,8 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
message={reactionMessage!}
|
||||
isOutside
|
||||
maxWidth={reactionsMaxWidth}
|
||||
activeReactions={activeReactions}
|
||||
availableReactions={availableReactions}
|
||||
genericEffects={genericEffects}
|
||||
observeIntersection={observeIntersectionForPlaying}
|
||||
noRecentReactors={isChannel}
|
||||
withEffects={withReactionEffects}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -1437,7 +1424,7 @@ function MessageAppendix({ isOwn } : { isOwn: boolean }) {
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, ownProps): StateProps => {
|
||||
const {
|
||||
focusedMessage, forwardMessages, activeReactions, activeEmojiInteractions,
|
||||
focusedMessage, forwardMessages, activeEmojiInteractions, activeReactions,
|
||||
} = selectTabState(global);
|
||||
const {
|
||||
message, album, withSenderName, withAvatar, threadId, messageListType, isLastInDocumentGroup, isFirstInGroup,
|
||||
@ -1538,6 +1525,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
const isConnected = global.connectionState === 'connectionStateReady';
|
||||
|
||||
const hasActiveReactions = Boolean(reactionMessage && activeReactions[getMessageKey(reactionMessage)]?.length);
|
||||
|
||||
return {
|
||||
theme: selectTheme(global),
|
||||
chatUsernames,
|
||||
@ -1584,7 +1573,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
availableReactions: global.availableReactions,
|
||||
defaultReaction: isMessageLocal(message) || messageListType === 'scheduled'
|
||||
? undefined : selectDefaultReaction(global, chatId),
|
||||
activeReactions: reactionMessage && activeReactions[reactionMessage.id],
|
||||
hasActiveReactions,
|
||||
activeEmojiInteractions,
|
||||
hasUnreadReaction,
|
||||
isTranscribing: transcriptionId !== undefined && global.transcriptions[transcriptionId]?.isPending,
|
||||
@ -1592,7 +1581,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
isPremium: selectIsCurrentUserPremium(global),
|
||||
senderAdminMember,
|
||||
messageTopic,
|
||||
genericEffects: global.genericEmojiEffects,
|
||||
hasTopicChip,
|
||||
chatTranslations,
|
||||
areTranslationsEnabled,
|
||||
@ -1600,7 +1588,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
requestedTranslationLanguage,
|
||||
requestedChatTranslationLanguage,
|
||||
hasLinkedChat: Boolean(chatFullInfo?.linkedChatId),
|
||||
withReactionEffects: selectPerformanceSettingsValue(global, 'reactionEffects'),
|
||||
withStickerEffects: selectPerformanceSettingsValue(global, 'stickerEffects'),
|
||||
webPageStory,
|
||||
isConnected,
|
||||
|
||||
@ -3,41 +3,35 @@ import React, { memo, useMemo } from '../../../lib/teact/teact';
|
||||
import { getActions, getGlobal } from '../../../global';
|
||||
|
||||
import type {
|
||||
ApiAvailableReaction, ApiChat, ApiMessage, ApiReactionCount, ApiStickerSet, ApiUser,
|
||||
ApiChat, ApiMessage, ApiReactionCount, ApiUser,
|
||||
} from '../../../api/types';
|
||||
import type { ActiveReaction } from '../../../global/types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
|
||||
import { isReactionChosen, isSameReaction } from '../../../global/helpers';
|
||||
import { getMessageKey, isReactionChosen, isSameReaction } from '../../../global/helpers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatIntegerCompact } from '../../../util/textFormat';
|
||||
import { REM } from '../../common/helpers/mediaDimensions';
|
||||
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import AnimatedCounter from '../../common/AnimatedCounter';
|
||||
import AvatarList from '../../common/AvatarList';
|
||||
import ReactionAnimatedEmoji from '../../common/reactions/ReactionAnimatedEmoji';
|
||||
import Button from '../../ui/Button';
|
||||
import ReactionAnimatedEmoji from './ReactionAnimatedEmoji';
|
||||
|
||||
import './Reactions.scss';
|
||||
|
||||
const REACTION_SIZE = 1.25 * REM;
|
||||
|
||||
const ReactionButton: FC<{
|
||||
reaction: ApiReactionCount;
|
||||
message: ApiMessage;
|
||||
activeReactions?: ActiveReaction[];
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
withRecentReactors?: boolean;
|
||||
withEffects?: boolean;
|
||||
genericEffects?: ApiStickerSet;
|
||||
observeIntersection?: ObserveFn;
|
||||
}> = ({
|
||||
reaction,
|
||||
message,
|
||||
activeReactions,
|
||||
availableReactions,
|
||||
withRecentReactors,
|
||||
withEffects,
|
||||
genericEffects,
|
||||
observeIntersection,
|
||||
}) => {
|
||||
const { toggleReaction } = getActions();
|
||||
@ -68,21 +62,22 @@ const ReactionButton: FC<{
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={buildClassName(isReactionChosen(reaction) && 'chosen')}
|
||||
className={buildClassName(isReactionChosen(reaction) && 'chosen', 'message-reaction')}
|
||||
size="tiny"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<ReactionAnimatedEmoji
|
||||
activeReactions={activeReactions}
|
||||
className="reaction-animated-emoji"
|
||||
containerId={getMessageKey(message)}
|
||||
reaction={reaction.reaction}
|
||||
availableReactions={availableReactions}
|
||||
genericEffects={genericEffects}
|
||||
size={REACTION_SIZE}
|
||||
observeIntersection={observeIntersection}
|
||||
withEffects={withEffects}
|
||||
/>
|
||||
{recentReactors?.length ? (
|
||||
<AvatarList size="mini" peers={recentReactors} />
|
||||
) : <AnimatedCounter text={formatIntegerCompact(reaction.count)} className="counter" />}
|
||||
) : (
|
||||
<AnimatedCounter text={formatIntegerCompact(reaction.count)} className="counter" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
max-width: calc(var(--max-width) + 2.25rem);
|
||||
|
||||
.Button {
|
||||
--custom-emoji-size: 1.25rem;
|
||||
--reaction-background: var(--color-reaction);
|
||||
--reaction-background-hover: var(--hover-color-reaction);
|
||||
--reaction-text-color: var(--text-color-reaction);
|
||||
@ -36,10 +37,6 @@
|
||||
|
||||
transition: background-color 150ms, color 150ms, backdrop-filter 150ms, filter 150ms;
|
||||
|
||||
.ReactionStaticEmoji {
|
||||
width: 1.25rem;
|
||||
}
|
||||
|
||||
.avatars {
|
||||
display: flex;
|
||||
|
||||
@ -56,6 +53,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.message-reaction {
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.reaction-animated-emoji {
|
||||
margin: 0.25rem;
|
||||
}
|
||||
|
||||
&.chosen {
|
||||
--reaction-background: var(--color-reaction-chosen);
|
||||
--reaction-background-hover: var(--hover-color-reaction-chosen);
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo, useMemo } from '../../../lib/teact/teact';
|
||||
|
||||
import type { ApiAvailableReaction, ApiMessage, ApiStickerSet } from '../../../api/types';
|
||||
import type { ActiveReaction } from '../../../global/types';
|
||||
import type { ApiMessage } from '../../../api/types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
|
||||
import { getReactionUniqueKey } from '../../../global/helpers';
|
||||
@ -18,13 +17,9 @@ type OwnProps = {
|
||||
message: ApiMessage;
|
||||
isOutside?: boolean;
|
||||
maxWidth?: number;
|
||||
activeReactions?: ActiveReaction[];
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
metaChildren?: React.ReactNode;
|
||||
genericEffects?: ApiStickerSet;
|
||||
observeIntersection?: ObserveFn;
|
||||
noRecentReactors?: boolean;
|
||||
withEffects?: boolean;
|
||||
};
|
||||
|
||||
const MAX_RECENT_AVATARS = 3;
|
||||
@ -33,13 +28,9 @@ const Reactions: FC<OwnProps> = ({
|
||||
message,
|
||||
isOutside,
|
||||
maxWidth,
|
||||
activeReactions,
|
||||
availableReactions,
|
||||
metaChildren,
|
||||
genericEffects,
|
||||
observeIntersection,
|
||||
noRecentReactors,
|
||||
withEffects,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
|
||||
@ -58,12 +49,8 @@ const Reactions: FC<OwnProps> = ({
|
||||
key={getReactionUniqueKey(reaction.reaction)}
|
||||
reaction={reaction}
|
||||
message={message}
|
||||
activeReactions={activeReactions}
|
||||
availableReactions={availableReactions}
|
||||
withRecentReactors={totalCount <= MAX_RECENT_AVATARS && !noRecentReactors}
|
||||
genericEffects={genericEffects}
|
||||
observeIntersection={observeIntersection}
|
||||
withEffects={withEffects}
|
||||
/>
|
||||
))}
|
||||
{metaChildren}
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import React, {
|
||||
memo, useEffect, useLayoutEffect, useMemo, useRef, useState,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getGlobal, withGlobal } from '../../global';
|
||||
import { getActions, getGlobal, withGlobal } from '../../global';
|
||||
|
||||
import type { ApiUserStories } from '../../api/types';
|
||||
|
||||
import { ANIMATION_END_DELAY } from '../../config';
|
||||
import { getStoryKey } from '../../global/helpers';
|
||||
import { selectIsStoryViewerOpen, selectTabState, selectUser } from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import buildStyle from '../../util/buildStyle';
|
||||
@ -66,6 +67,7 @@ function StorySlides({
|
||||
onClose,
|
||||
onReport,
|
||||
}: OwnProps & StateProps) {
|
||||
const { stopActiveReaction } = getActions();
|
||||
const [renderingUserId, setRenderingUserId] = useState(currentUserId);
|
||||
const [renderingStoryId, setRenderingStoryId] = useState(currentStoryId);
|
||||
const prevUserId = usePrevious(currentUserId);
|
||||
@ -168,6 +170,15 @@ function StorySlides({
|
||||
};
|
||||
}, [prevUserId, currentUserId, setIsAnimating]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (!currentStoryId || !currentUserId) return;
|
||||
stopActiveReaction({
|
||||
containerId: getStoryKey(currentUserId, currentStoryId),
|
||||
});
|
||||
};
|
||||
}, [currentStoryId, currentUserId]);
|
||||
|
||||
const slideAmount = currentUserPosition - renderingUserPosition;
|
||||
const isBackward = renderingUserPosition > currentUserPosition;
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import type { IDimensions } from '../../../global/types';
|
||||
|
||||
import { roundToNearestEven } from '../../../util/math';
|
||||
|
||||
const BASE_SCREEN_WIDTH = 1200;
|
||||
const BASE_SCREEN_HEIGHT = 800;
|
||||
const BASE_ACTIVE_SLIDE_WIDTH = 405;
|
||||
@ -56,8 +58,3 @@ function calculateScale(baseWidth: number, baseHeight: number, newWidth: number,
|
||||
|
||||
return Math.min(widthScale, heightScale);
|
||||
}
|
||||
|
||||
// Fractional values cause blurry text. Round to even to keep whole numbers while centering
|
||||
function roundToNearestEven(value: number) {
|
||||
return Math.round(value / 2) * 2;
|
||||
}
|
||||
|
||||
@ -7,7 +7,10 @@ import * as mediaLoader from '../../../util/mediaLoader';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import {
|
||||
getDocumentMediaHash,
|
||||
getUserReactions, isMessageLocal, isSameReaction,
|
||||
getMessageKey,
|
||||
getUserReactions,
|
||||
isMessageLocal,
|
||||
isSameReaction,
|
||||
} from '../../helpers';
|
||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||
import {
|
||||
@ -150,22 +153,14 @@ addActionHandler('toggleReaction', async (global, actions, payload): Promise<voi
|
||||
|
||||
const limit = selectMaxUserReactions(global);
|
||||
const reactions = newUserReactions.slice(-limit);
|
||||
const tabState = selectTabState(global, tabId);
|
||||
const messageKey = getMessageKey(message);
|
||||
|
||||
if (selectPerformanceSettingsValue(global, 'reactionEffects')) {
|
||||
const newActiveReactions = hasReaction ? omit(tabState.activeReactions, [messageId]) : {
|
||||
...tabState.activeReactions,
|
||||
[messageId]: [
|
||||
...(tabState.activeReactions[messageId] || []),
|
||||
{
|
||||
messageId,
|
||||
reaction,
|
||||
},
|
||||
],
|
||||
};
|
||||
global = updateTabState(global, {
|
||||
activeReactions: newActiveReactions,
|
||||
}, tabId);
|
||||
if (hasReaction) {
|
||||
actions.stopActiveReaction({ containerId: messageKey, reaction, tabId });
|
||||
} else {
|
||||
actions.startActiveReaction({ containerId: messageKey, reaction, tabId });
|
||||
}
|
||||
}
|
||||
|
||||
global = addMessageReaction(global, message, reactions);
|
||||
@ -185,21 +180,41 @@ addActionHandler('toggleReaction', async (global, actions, payload): Promise<voi
|
||||
}
|
||||
});
|
||||
|
||||
addActionHandler('stopActiveReaction', (global, actions, payload): ActionReturnType => {
|
||||
const { messageId, reaction, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
addActionHandler('startActiveReaction', (global, actions, payload): ActionReturnType => {
|
||||
const { containerId, reaction, tabId = getCurrentTabId() } = payload;
|
||||
const tabState = selectTabState(global, tabId);
|
||||
if (!tabState.activeReactions[messageId]?.some((active) => isSameReaction(active.reaction, reaction))) {
|
||||
return global;
|
||||
|
||||
if (!selectPerformanceSettingsValue(global, 'reactionEffects')) return undefined;
|
||||
|
||||
const currentActiveReactions = tabState.activeReactions[containerId] || [];
|
||||
if (currentActiveReactions.some((active) => isSameReaction(active, reaction))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const newMessageActiveReactions = tabState.activeReactions[messageId]
|
||||
.filter((active) => !isSameReaction(active.reaction, reaction));
|
||||
const newActiveReactions = currentActiveReactions.concat(reaction);
|
||||
|
||||
return updateTabState(global, {
|
||||
activeReactions: {
|
||||
...tabState.activeReactions,
|
||||
[containerId]: newActiveReactions,
|
||||
},
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('stopActiveReaction', (global, actions, payload): ActionReturnType => {
|
||||
const { containerId, reaction, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
const tabState = selectTabState(global, tabId);
|
||||
|
||||
const currentActiveReactions = tabState.activeReactions[containerId] || [];
|
||||
// Remove all reactions if reaction is not specified
|
||||
const newMessageActiveReactions = reaction
|
||||
? currentActiveReactions.filter((active) => !isSameReaction(active, reaction)) : [];
|
||||
|
||||
const newActiveReactions = newMessageActiveReactions.length ? {
|
||||
...tabState.activeReactions,
|
||||
[messageId]: newMessageActiveReactions,
|
||||
} : omit(tabState.activeReactions, [messageId]);
|
||||
[containerId]: newMessageActiveReactions,
|
||||
} : omit(tabState.activeReactions, [containerId]);
|
||||
|
||||
return updateTabState(global, {
|
||||
activeReactions: newActiveReactions,
|
||||
|
||||
@ -6,6 +6,7 @@ import { buildCollectionByKey } from '../../../util/iteratees';
|
||||
import { translate } from '../../../util/langProvider';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import { getStoryKey } from '../../helpers';
|
||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||
import {
|
||||
addStories,
|
||||
@ -428,7 +429,7 @@ addActionHandler('loadStoriesMaxIds', async (global, actions, payload): Promise<
|
||||
|
||||
addActionHandler('sendStoryReaction', async (global, actions, payload): Promise<void> => {
|
||||
const {
|
||||
userId, storyId, reaction, shouldAddToRecent,
|
||||
userId, storyId, reaction, shouldAddToRecent, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
const user = selectUser(global, userId);
|
||||
if (!user) return;
|
||||
@ -442,6 +443,13 @@ addActionHandler('sendStoryReaction', async (global, actions, payload): Promise<
|
||||
});
|
||||
setGlobal(global);
|
||||
|
||||
const containerId = getStoryKey(userId, storyId);
|
||||
if (reaction) {
|
||||
actions.startActiveReaction({ containerId, reaction, tabId });
|
||||
} else {
|
||||
actions.stopActiveReaction({ containerId, tabId });
|
||||
}
|
||||
|
||||
const result = await callApi('sendStoryReaction', {
|
||||
user, storyId, reaction, shouldAddToRecent,
|
||||
});
|
||||
|
||||
@ -54,3 +54,7 @@ function getSizeParameter(size: StorySize) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function getStoryKey(chatId: string, storyId: number) {
|
||||
return `story${chatId}-${storyId}`;
|
||||
}
|
||||
|
||||
@ -132,11 +132,6 @@ export type IDimensions = {
|
||||
|
||||
export type ApiPaymentStatus = 'paid' | 'failed' | 'pending' | 'cancelled';
|
||||
|
||||
export interface ActiveReaction {
|
||||
messageId?: number;
|
||||
reaction?: ApiReaction;
|
||||
}
|
||||
|
||||
export interface TabThread {
|
||||
scrollOffset?: number;
|
||||
replyStack?: number[];
|
||||
@ -321,7 +316,7 @@ export type TabState = {
|
||||
};
|
||||
|
||||
activeEmojiInteractions?: ActiveEmojiInteraction[];
|
||||
activeReactions: Record<number, ActiveReaction[]>;
|
||||
activeReactions: Record<string, ApiReaction[]>;
|
||||
|
||||
localTextSearch: {
|
||||
byChatThreadKey: Record<string, {
|
||||
@ -1927,10 +1922,14 @@ export interface ActionPayloads {
|
||||
enabledReactions?: ApiChatReactions;
|
||||
} & WithTabId;
|
||||
|
||||
stopActiveReaction: {
|
||||
messageId: number;
|
||||
startActiveReaction: {
|
||||
containerId: string;
|
||||
reaction: ApiReaction;
|
||||
} & WithTabId;
|
||||
stopActiveReaction: {
|
||||
containerId: string;
|
||||
reaction?: ApiReaction;
|
||||
} & WithTabId;
|
||||
|
||||
openMessageReactionPicker: {
|
||||
chatId: string;
|
||||
@ -2051,7 +2050,7 @@ export interface ActionPayloads {
|
||||
storyId: number;
|
||||
reaction?: ApiReaction;
|
||||
shouldAddToRecent?: boolean;
|
||||
};
|
||||
} & WithTabId;
|
||||
toggleStealthModal: {
|
||||
isOpen: boolean;
|
||||
} & WithTabId;
|
||||
|
||||
@ -4,3 +4,8 @@ export const round = (num: number, decimals: number = 0) => Math.round(num * 10
|
||||
export const lerp = (start: number, end: number, interpolationRatio: number) => {
|
||||
return (1 - interpolationRatio) * start + interpolationRatio * end;
|
||||
};
|
||||
|
||||
// Fractional values cause blurry text & canvas. Round to even to keep whole numbers while centering
|
||||
export function roundToNearestEven(value: number) {
|
||||
return Math.round(value / 2) * 2;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user