Reactions: Support service messages (#5387)

This commit is contained in:
Alexander Zinchuk 2025-01-03 17:15:34 +01:00
parent d0ad84217d
commit 5dbb3ddea9
8 changed files with 110 additions and 4 deletions

View File

@ -216,6 +216,7 @@ export function buildApiMessageWithChatId(
const senderBoosts = mtpMessage.fromBoostsApplied; const senderBoosts = mtpMessage.fromBoostsApplied;
const factCheck = mtpMessage.factcheck && buildApiFactCheck(mtpMessage.factcheck); const factCheck = mtpMessage.factcheck && buildApiFactCheck(mtpMessage.factcheck);
const isVideoProcessingPending = mtpMessage.videoProcessingPending; const isVideoProcessingPending = mtpMessage.videoProcessingPending;
const areReactionsPossible = mtpMessage.reactionsArePossible;
const isInvertedMedia = mtpMessage.invertMedia; const isInvertedMedia = mtpMessage.invertMedia;
@ -242,6 +243,7 @@ export function buildApiMessageWithChatId(
editDate: mtpMessage.editDate, editDate: mtpMessage.editDate,
isMediaUnread, isMediaUnread,
hasUnreadMention: mtpMessage.mentioned && isMediaUnread, hasUnreadMention: mtpMessage.mentioned && isMediaUnread,
areReactionsPossible,
isMentioned: mtpMessage.mentioned, isMentioned: mtpMessage.mentioned,
...(groupedId && { ...(groupedId && {
groupedId, groupedId,
@ -439,6 +441,7 @@ function buildAction(
text = 'Notification.ChangedGroupPhoto'; text = 'Notification.ChangedGroupPhoto';
translationValues.push('%action_origin%'); translationValues.push('%action_origin%');
} }
type = 'updateProfilePhoto';
} else if (action instanceof GramJs.MessageActionChatDeletePhoto) { } else if (action instanceof GramJs.MessageActionChatDeletePhoto) {
if (isChannelPost) { if (isChannelPost) {
text = 'Channel.MessagePhotoRemoved'; text = 'Channel.MessagePhotoRemoved';

View File

@ -478,6 +478,7 @@ export interface ApiAction {
| 'chatCreate' | 'chatCreate'
| 'topicCreate' | 'topicCreate'
| 'suggestProfilePhoto' | 'suggestProfilePhoto'
| 'updateProfilePhoto'
| 'joinedChannel' | 'joinedChannel'
| 'chatBoost' | 'chatBoost'
| 'receipt' | 'receipt'
@ -755,6 +756,7 @@ export interface ApiMessage {
effectId?: string; effectId?: string;
isInvertedMedia?: true; isInvertedMedia?: true;
isVideoProcessingPending?: true; isVideoProcessingPending?: true;
areReactionsPossible?: true;
} }
export interface ApiReactions { export interface ApiReactions {

View File

@ -19,6 +19,7 @@ import {
selectChatMessage, selectChatMessage,
selectGiftStickerForDuration, selectGiftStickerForDuration,
selectGiftStickerForStars, selectGiftStickerForStars,
selectIsCurrentUserPremium,
selectIsMessageFocused, selectIsMessageFocused,
selectStarGiftSticker, selectStarGiftSticker,
selectTabState, selectTabState,
@ -47,7 +48,9 @@ import Avatar from '../common/Avatar';
import GiftRibbon from '../common/gift/GiftRibbon'; import GiftRibbon from '../common/gift/GiftRibbon';
import Sparkles from '../common/Sparkles'; import Sparkles from '../common/Sparkles';
import ActionMessageSuggestedAvatar from './ActionMessageSuggestedAvatar'; import ActionMessageSuggestedAvatar from './ActionMessageSuggestedAvatar';
import ActionMessageUpdatedAvatar from './ActionMessageUpdatedAvatar';
import ContextMenuContainer from './message/ContextMenuContainer.async'; import ContextMenuContainer from './message/ContextMenuContainer.async';
import Reactions from './message/reactions/Reactions';
import SimilarChannels from './message/SimilarChannels'; import SimilarChannels from './message/SimilarChannels';
type OwnProps = { type OwnProps = {
@ -82,6 +85,7 @@ type StateProps = {
starsGiftSticker?: ApiSticker; starsGiftSticker?: ApiSticker;
canPlayAnimatedEmojis?: boolean; canPlayAnimatedEmojis?: boolean;
patternColor?: string; patternColor?: string;
isCurrentUserPremium?: boolean;
}; };
const APPEARANCE_DELAY = 10; const APPEARANCE_DELAY = 10;
@ -89,6 +93,7 @@ const STAR_GIFT_STICKER_SIZE = 120;
const ActionMessage: FC<OwnProps & StateProps> = ({ const ActionMessage: FC<OwnProps & StateProps> = ({
message, message,
threadId,
isEmbedded, isEmbedded,
appearanceOrder = 0, appearanceOrder = 0,
isJustAdded, isJustAdded,
@ -114,6 +119,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
observeIntersectionForLoading, observeIntersectionForLoading,
observeIntersectionForPlaying, observeIntersectionForPlaying,
onIntersectPinnedMessage, onIntersectPinnedMessage,
isCurrentUserPremium,
}) => { }) => {
const { const {
openPremiumModal, openPremiumModal,
@ -156,11 +162,14 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
const isPremiumGift = message.content.action?.type === 'giftPremium'; const isPremiumGift = message.content.action?.type === 'giftPremium';
const isGiftCode = message.content.action?.type === 'giftCode'; const isGiftCode = message.content.action?.type === 'giftCode';
const isSuggestedAvatar = message.content.action?.type === 'suggestProfilePhoto' && message.content.action!.photo; 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 isJoinedMessage = isJoinedChannelMessage(message);
const isStarsGift = message.content.action?.type === 'giftStars'; const isStarsGift = message.content.action?.type === 'giftStars';
const isStarGift = message.content.action?.type === 'starGift'; const isStarGift = message.content.action?.type === 'starGift';
const isPrizeStars = message.content.action?.type === 'prizeStars'; const isPrizeStars = message.content.action?.type === 'prizeStars';
const withServiceReactions = Boolean(message.areReactionsPossible && message?.reactions);
useEffect(() => { useEffect(() => {
if (noAppearanceAnimation) { if (noAppearanceAnimation) {
return; return;
@ -548,7 +557,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
const className = buildClassName( const className = buildClassName(
'ActionMessage message-list-item', 'ActionMessage message-list-item',
isFocused && !noFocusHighlight && 'focused', isFocused && !noFocusHighlight && 'focused',
(isPremiumGift || isSuggestedAvatar) && 'centered-action', (isPremiumGift || isSuggestedAvatar || isUpdatedAvatar) && 'centered-action',
isContextMenuShown && 'has-menu-open', isContextMenuShown && 'has-menu-open',
isLastInList && 'last-in-list', isLastInList && 'last-in-list',
transitionClassNames, transitionClassNames,
@ -564,7 +573,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
> >
{!isSuggestedAvatar && !isGiftCode && !isJoinedMessage && ( {!isSuggestedAvatar && !isGiftCode && !isJoinedMessage && !isUpdatedAvatar && (
<span className="action-message-content" onClick={handleClick}>{renderContent()}</span> <span className="action-message-content" onClick={handleClick}>{renderContent()}</span>
)} )}
{isPremiumGift && renderGift()} {isPremiumGift && renderGift()}
@ -575,6 +584,9 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
{isSuggestedAvatar && ( {isSuggestedAvatar && (
<ActionMessageSuggestedAvatar message={message} renderContent={renderContent} /> <ActionMessageSuggestedAvatar message={message} renderContent={renderContent} />
)} )}
{isUpdatedAvatar && (
<ActionMessageUpdatedAvatar message={message} renderContent={renderContent} />
)}
{isJoinedMessage && <SimilarChannels chatId={targetChatId!} />} {isJoinedMessage && <SimilarChannels chatId={targetChatId!} />}
{contextMenuAnchor && ( {contextMenuAnchor && (
<ContextMenuContainer <ContextMenuContainer
@ -586,6 +598,15 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
onCloseAnimationEnd={handleContextMenuHide} onCloseAnimationEnd={handleContextMenuHide}
/> />
)} )}
{withServiceReactions && (
<Reactions
isOutside
message={message!}
threadId={threadId}
observeIntersection={observeIntersectionForPlaying}
isCurrentUserPremium={isCurrentUserPremium}
/>
)}
</div> </div>
); );
}; };
@ -646,6 +667,7 @@ export default memo(withGlobal<OwnProps>(
focusDirection, focusDirection,
noFocusHighlight, noFocusHighlight,
}), }),
isCurrentUserPremium: selectIsCurrentUserPremium(global),
}; };
}, },
)(ActionMessage)); )(ActionMessage));

View File

@ -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<OwnProps> = ({
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 (
<>
<span>{renderContent()}</span>
<span
className="action-message-updated-avatar"
tabIndex={0}
role="button"
onClick={handleViewUpdatedAvatar}
aria-label={lang('ViewPhotoAction')}
>
<Avatar
photo={message.content.action!.photo}
loopIndefinitely
withVideo={isVideo}
size="jumbo"
/>
</span>
</>
);
};
export default memo(ActionMessageUpdatedAvatar);

View File

@ -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 { .action-message-button {
position: relative; position: relative;
display: inline-block; display: inline-block;

View File

@ -232,7 +232,8 @@ const MessageContextMenu: FC<OwnProps> = ({
const scrollableRef = useRef<HTMLDivElement>(null); const scrollableRef = useRef<HTMLDivElement>(null);
const lang = useOldLang(); const lang = useOldLang();
const noReactions = !isPrivate && !enabledReactions; 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 isEdited = ('isEdited' in message) && message.isEdited;
const seenByDates = message.seenByDates; const seenByDates = message.seenByDates;
const isPremiumGift = message.content.action?.type === 'giftPremium'; const isPremiumGift = message.content.action?.type === 'giftPremium';

View File

@ -13,6 +13,12 @@
margin-top: 0.25rem; margin-top: 0.25rem;
} }
&.with-service-reactions {
justify-content: center;
max-width: 19rem;
margin: 0.3rem auto;
}
.own &.is-outside { .own &.is-outside {
flex-direction: row-reverse; flex-direction: row-reverse;
} }

View File

@ -64,6 +64,7 @@ const Reactions: FC<OwnProps> = ({
const lang = useOldLang(); const lang = useOldLang();
const { results, areTags, recentReactions } = message.reactions!; const { results, areTags, recentReactions } = message.reactions!;
const withServiceReactions = Boolean(message.areReactionsPossible && message.reactions);
const totalCount = useMemo(() => ( const totalCount = useMemo(() => (
results.reduce((acc, reaction) => acc + reaction.count, 0) results.reduce((acc, reaction) => acc + reaction.count, 0)
@ -178,7 +179,11 @@ const Reactions: FC<OwnProps> = ({
return ( return (
<div <div
className={buildClassName('Reactions', isOutside && 'is-outside')} className={buildClassName(
'Reactions',
isOutside && 'is-outside',
withServiceReactions && 'with-service-reactions',
)}
style={maxWidth ? `max-width: ${maxWidth}px` : undefined} style={maxWidth ? `max-width: ${maxWidth}px` : undefined}
dir={lang.isRtl ? 'rtl' : 'ltr'} dir={lang.isRtl ? 'rtl' : 'ltr'}
> >