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

View File

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

View File

@ -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<OwnProps & StateProps> = ({
message,
threadId,
isEmbedded,
appearanceOrder = 0,
isJustAdded,
@ -114,6 +119,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
observeIntersectionForLoading,
observeIntersectionForPlaying,
onIntersectPinnedMessage,
isCurrentUserPremium,
}) => {
const {
openPremiumModal,
@ -156,11 +162,14 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
onMouseDown={handleMouseDown}
onContextMenu={handleContextMenu}
>
{!isSuggestedAvatar && !isGiftCode && !isJoinedMessage && (
{!isSuggestedAvatar && !isGiftCode && !isJoinedMessage && !isUpdatedAvatar && (
<span className="action-message-content" onClick={handleClick}>{renderContent()}</span>
)}
{isPremiumGift && renderGift()}
@ -575,6 +584,9 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
{isSuggestedAvatar && (
<ActionMessageSuggestedAvatar message={message} renderContent={renderContent} />
)}
{isUpdatedAvatar && (
<ActionMessageUpdatedAvatar message={message} renderContent={renderContent} />
)}
{isJoinedMessage && <SimilarChannels chatId={targetChatId!} />}
{contextMenuAnchor && (
<ContextMenuContainer
@ -586,6 +598,15 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
onCloseAnimationEnd={handleContextMenuHide}
/>
)}
{withServiceReactions && (
<Reactions
isOutside
message={message!}
threadId={threadId}
observeIntersection={observeIntersectionForPlaying}
isCurrentUserPremium={isCurrentUserPremium}
/>
)}
</div>
);
};
@ -646,6 +667,7 @@ export default memo(withGlobal<OwnProps>(
focusDirection,
noFocusHighlight,
}),
isCurrentUserPremium: selectIsCurrentUserPremium(global),
};
},
)(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 {
position: relative;
display: inline-block;

View File

@ -232,7 +232,8 @@ const MessageContextMenu: FC<OwnProps> = ({
const scrollableRef = useRef<HTMLDivElement>(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';

View File

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

View File

@ -64,6 +64,7 @@ const Reactions: FC<OwnProps> = ({
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<OwnProps> = ({
return (
<div
className={buildClassName('Reactions', isOutside && 'is-outside')}
className={buildClassName(
'Reactions',
isOutside && 'is-outside',
withServiceReactions && 'with-service-reactions',
)}
style={maxWidth ? `max-width: ${maxWidth}px` : undefined}
dir={lang.isRtl ? 'rtl' : 'ltr'}
>