From 5dbb3ddea9a582bf4a618f1a58d6ba02a848fb50 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 3 Jan 2025 17:15:34 +0100 Subject: [PATCH] Reactions: Support service messages (#5387) --- src/api/gramjs/apiBuilders/messages.ts | 3 + src/api/types/messages.ts | 2 + src/components/middle/ActionMessage.tsx | 26 +++++++- .../middle/ActionMessageUpdatedAvatar.tsx | 60 +++++++++++++++++++ src/components/middle/MessageList.scss | 7 +++ .../middle/message/MessageContextMenu.tsx | 3 +- .../middle/message/reactions/Reactions.scss | 6 ++ .../middle/message/reactions/Reactions.tsx | 7 ++- 8 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 src/components/middle/ActionMessageUpdatedAvatar.tsx diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 8402cec0e..3dcb832e0 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -216,6 +216,7 @@ export function buildApiMessageWithChatId( const senderBoosts = mtpMessage.fromBoostsApplied; const factCheck = mtpMessage.factcheck && buildApiFactCheck(mtpMessage.factcheck); const isVideoProcessingPending = mtpMessage.videoProcessingPending; + const areReactionsPossible = mtpMessage.reactionsArePossible; const isInvertedMedia = mtpMessage.invertMedia; @@ -242,6 +243,7 @@ export function buildApiMessageWithChatId( editDate: mtpMessage.editDate, isMediaUnread, hasUnreadMention: mtpMessage.mentioned && isMediaUnread, + areReactionsPossible, isMentioned: mtpMessage.mentioned, ...(groupedId && { groupedId, @@ -439,6 +441,7 @@ function buildAction( text = 'Notification.ChangedGroupPhoto'; translationValues.push('%action_origin%'); } + type = 'updateProfilePhoto'; } else if (action instanceof GramJs.MessageActionChatDeletePhoto) { if (isChannelPost) { text = 'Channel.MessagePhotoRemoved'; diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 692b3f93f..0ac2f7c51 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -478,6 +478,7 @@ export interface ApiAction { | 'chatCreate' | 'topicCreate' | 'suggestProfilePhoto' + | 'updateProfilePhoto' | 'joinedChannel' | 'chatBoost' | 'receipt' @@ -755,6 +756,7 @@ export interface ApiMessage { effectId?: string; isInvertedMedia?: true; isVideoProcessingPending?: true; + areReactionsPossible?: true; } export interface ApiReactions { diff --git a/src/components/middle/ActionMessage.tsx b/src/components/middle/ActionMessage.tsx index d6dfe9b5d..8f83a0d82 100644 --- a/src/components/middle/ActionMessage.tsx +++ b/src/components/middle/ActionMessage.tsx @@ -19,6 +19,7 @@ import { selectChatMessage, selectGiftStickerForDuration, selectGiftStickerForStars, + selectIsCurrentUserPremium, selectIsMessageFocused, selectStarGiftSticker, selectTabState, @@ -47,7 +48,9 @@ import Avatar from '../common/Avatar'; import GiftRibbon from '../common/gift/GiftRibbon'; import Sparkles from '../common/Sparkles'; import ActionMessageSuggestedAvatar from './ActionMessageSuggestedAvatar'; +import ActionMessageUpdatedAvatar from './ActionMessageUpdatedAvatar'; import ContextMenuContainer from './message/ContextMenuContainer.async'; +import Reactions from './message/reactions/Reactions'; import SimilarChannels from './message/SimilarChannels'; type OwnProps = { @@ -82,6 +85,7 @@ type StateProps = { starsGiftSticker?: ApiSticker; canPlayAnimatedEmojis?: boolean; patternColor?: string; + isCurrentUserPremium?: boolean; }; const APPEARANCE_DELAY = 10; @@ -89,6 +93,7 @@ const STAR_GIFT_STICKER_SIZE = 120; const ActionMessage: FC = ({ message, + threadId, isEmbedded, appearanceOrder = 0, isJustAdded, @@ -114,6 +119,7 @@ const ActionMessage: FC = ({ observeIntersectionForLoading, observeIntersectionForPlaying, onIntersectPinnedMessage, + isCurrentUserPremium, }) => { const { openPremiumModal, @@ -156,11 +162,14 @@ const ActionMessage: FC = ({ const isPremiumGift = message.content.action?.type === 'giftPremium'; const isGiftCode = message.content.action?.type === 'giftCode'; const isSuggestedAvatar = message.content.action?.type === 'suggestProfilePhoto' && message.content.action!.photo; + const isUpdatedAvatar = message.content.action?.type === 'updateProfilePhoto' && message.content.action!.photo; const isJoinedMessage = isJoinedChannelMessage(message); const isStarsGift = message.content.action?.type === 'giftStars'; const isStarGift = message.content.action?.type === 'starGift'; const isPrizeStars = message.content.action?.type === 'prizeStars'; + const withServiceReactions = Boolean(message.areReactionsPossible && message?.reactions); + useEffect(() => { if (noAppearanceAnimation) { return; @@ -548,7 +557,7 @@ const ActionMessage: FC = ({ const className = buildClassName( 'ActionMessage message-list-item', isFocused && !noFocusHighlight && 'focused', - (isPremiumGift || isSuggestedAvatar) && 'centered-action', + (isPremiumGift || isSuggestedAvatar || isUpdatedAvatar) && 'centered-action', isContextMenuShown && 'has-menu-open', isLastInList && 'last-in-list', transitionClassNames, @@ -564,7 +573,7 @@ const ActionMessage: FC = ({ onMouseDown={handleMouseDown} onContextMenu={handleContextMenu} > - {!isSuggestedAvatar && !isGiftCode && !isJoinedMessage && ( + {!isSuggestedAvatar && !isGiftCode && !isJoinedMessage && !isUpdatedAvatar && ( {renderContent()} )} {isPremiumGift && renderGift()} @@ -575,6 +584,9 @@ const ActionMessage: FC = ({ {isSuggestedAvatar && ( )} + {isUpdatedAvatar && ( + + )} {isJoinedMessage && } {contextMenuAnchor && ( = ({ onCloseAnimationEnd={handleContextMenuHide} /> )} + {withServiceReactions && ( + + )} ); }; @@ -646,6 +667,7 @@ export default memo(withGlobal( focusDirection, noFocusHighlight, }), + isCurrentUserPremium: selectIsCurrentUserPremium(global), }; }, )(ActionMessage)); diff --git a/src/components/middle/ActionMessageUpdatedAvatar.tsx b/src/components/middle/ActionMessageUpdatedAvatar.tsx new file mode 100644 index 000000000..4544e94ff --- /dev/null +++ b/src/components/middle/ActionMessageUpdatedAvatar.tsx @@ -0,0 +1,60 @@ +import type { FC } from '../../lib/teact/teact'; +import React, { memo } from '../../lib/teact/teact'; +import { getActions } from '../../global'; + +import type { ApiMessage } from '../../api/types'; +import type { TextPart } from '../../types'; +import { MAIN_THREAD_ID } from '../../api/types'; +import { MediaViewerOrigin } from '../../types'; + +import useOldLang from '../../hooks/useOldLang'; + +import Avatar from '../common/Avatar'; + +type OwnProps = { + message: ApiMessage; + renderContent: () => TextPart | undefined; +}; + +const ActionMessageUpdatedAvatar: FC = ({ + message, + renderContent, +}) => { + const { + openMediaViewer, + } = getActions(); + + const lang = useOldLang(); + const isVideo = message.content.action!.photo?.isVideo; + + const handleViewUpdatedAvatar = () => { + openMediaViewer({ + chatId: message.chatId, + messageId: message.id, + threadId: MAIN_THREAD_ID, + origin: MediaViewerOrigin.SuggestedAvatar, + }); + }; + + return ( + <> + {renderContent()} + + + + + ); +}; + +export default memo(ActionMessageUpdatedAvatar); diff --git a/src/components/middle/MessageList.scss b/src/components/middle/MessageList.scss index 53fc3bcc0..d5c7719b4 100644 --- a/src/components/middle/MessageList.scss +++ b/src/components/middle/MessageList.scss @@ -339,6 +339,13 @@ } } + .action-message-updated-avatar { + background: transparent !important; + margin-top: 0.5rem; + cursor: var(--custom-cursor, pointer); + outline: none; + } + .action-message-button { position: relative; display: inline-block; diff --git a/src/components/middle/message/MessageContextMenu.tsx b/src/components/middle/message/MessageContextMenu.tsx index f12426739..4684ff0d6 100644 --- a/src/components/middle/message/MessageContextMenu.tsx +++ b/src/components/middle/message/MessageContextMenu.tsx @@ -232,7 +232,8 @@ const MessageContextMenu: FC = ({ const scrollableRef = useRef(null); const lang = useOldLang(); const noReactions = !isPrivate && !enabledReactions; - const withReactions = canShowReactionList && !noReactions; + const areReactionsPossible = message.areReactionsPossible; + const withReactions = (canShowReactionList && !noReactions) || areReactionsPossible; const isEdited = ('isEdited' in message) && message.isEdited; const seenByDates = message.seenByDates; const isPremiumGift = message.content.action?.type === 'giftPremium'; diff --git a/src/components/middle/message/reactions/Reactions.scss b/src/components/middle/message/reactions/Reactions.scss index ee2175e28..aac421100 100644 --- a/src/components/middle/message/reactions/Reactions.scss +++ b/src/components/middle/message/reactions/Reactions.scss @@ -13,6 +13,12 @@ margin-top: 0.25rem; } + &.with-service-reactions { + justify-content: center; + max-width: 19rem; + margin: 0.3rem auto; + } + .own &.is-outside { flex-direction: row-reverse; } diff --git a/src/components/middle/message/reactions/Reactions.tsx b/src/components/middle/message/reactions/Reactions.tsx index 1dc0ff2b7..2616dcbd1 100644 --- a/src/components/middle/message/reactions/Reactions.tsx +++ b/src/components/middle/message/reactions/Reactions.tsx @@ -64,6 +64,7 @@ const Reactions: FC = ({ const lang = useOldLang(); const { results, areTags, recentReactions } = message.reactions!; + const withServiceReactions = Boolean(message.areReactionsPossible && message.reactions); const totalCount = useMemo(() => ( results.reduce((acc, reaction) => acc + reaction.count, 0) @@ -178,7 +179,11 @@ const Reactions: FC = ({ return (