Story: Display reaction effect (#3829)

This commit is contained in:
Alexander Zinchuk 2023-09-15 16:44:29 +02:00
parent 6e43795216
commit 31403702fe
19 changed files with 222 additions and 155 deletions

View File

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

View File

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

View File

@ -99,6 +99,7 @@ const ReactionEmoji: FC<OwnProps> = ({
className={transitionClassNames}
sharedCanvas={sharedCanvasRef!.current || undefined}
sharedCanvasCoords={coords}
forceAlways={forcePlayback}
/>
)}
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -54,3 +54,7 @@ function getSizeParameter(size: StorySize) {
return '';
}
}
export function getStoryKey(chatId: string, storyId: number) {
return `story${chatId}-${storyId}`;
}

View File

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

View File

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