diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 583cacf70..9cbe0c543 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -199,7 +199,7 @@ export function buildApiMessageWithChatId( const isPrivateChat = getEntityTypeById(chatId) === 'user'; // Server can return `fromId` for our own messages in private chats, but not for incoming ones // This can break grouping logic, as we do not fill `fromId` for `UpdateShortMessage` case - const fromId = mtpMessage.fromId && !isPrivateChat + const fromId = mtpMessage.fromId && (!isPrivateChat || mtpMessage.guestchatViaFrom) ? getApiChatIdFromMtpPeer(mtpMessage.fromId) : undefined; const isChatWithSelf = !fromId && chatId === currentUserId; @@ -299,6 +299,7 @@ export function buildApiMessageWithChatId( restrictionReasons, summaryLanguageCode: mtpMessage.summaryFromLanguage, fromRank: mtpMessage.fromRank, + guestChatViaId: mtpMessage.guestchatViaFrom && getApiChatIdFromMtpPeer(mtpMessage.guestchatViaFrom), }; } diff --git a/src/api/gramjs/apiBuilders/users.ts b/src/api/gramjs/apiBuilders/users.ts index c2a6514c8..336da50a7 100644 --- a/src/api/gramjs/apiBuilders/users.ts +++ b/src/api/gramjs/apiBuilders/users.ts @@ -116,7 +116,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined { const { id, firstName, lastName, fake, scam, support, closeFriend, storiesUnavailable, bot, botActiveUsers, botVerificationIcon, botInlinePlaceholder, botAttachMenu, botCanEdit, - sendPaidMessagesStars, profileColor, botForumView, botForumCanManageTopics, + sendPaidMessagesStars, profileColor, botForumView, botForumCanManageTopics, botGuestchat, } = mtpUser; const storiesMaxId = mtpUser.storiesMaxId?.maxId; const hasVideoAvatar = mtpUser.photo instanceof GramJs.UserProfilePhoto ? Boolean(mtpUser.photo.hasVideo) : undefined; @@ -162,6 +162,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined { paidMessagesStars: toJSNumber(sendPaidMessagesStars), isBotForum: botForumView, canManageBotForumTopics: botForumCanManageTopics, + isGuestChatBot: botGuestchat, }; } diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index ec2fb8f4e..3f2a06d4c 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -764,6 +764,7 @@ export interface ApiMessage { isKeyboardSelective?: boolean; viaBotId?: string; viaBusinessBotId?: string; + guestChatViaId?: string; postAuthorTitle?: string; isScheduled?: boolean; scheduleRepeatPeriod?: number; diff --git a/src/api/types/users.ts b/src/api/types/users.ts index 0f3a9fbe8..3bd65de99 100644 --- a/src/api/types/users.ts +++ b/src/api/types/users.ts @@ -49,6 +49,7 @@ export interface ApiUser { paidMessagesStars?: number; isBotForum?: boolean; canManageBotForumTopics?: boolean; + isGuestChatBot?: boolean; } export interface ApiUserFullInfo { diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 01cd6b338..a9cc37329 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -816,6 +816,7 @@ "PaymentInvoiceNotFound" = "Invoice not found"; "NoWordsRecognized" = "No words recognized."; "ViaBot" = "via"; +"ForBot" = "for"; "DiscussChannel" = "channel"; "ForwardedMessage" = "Forwarded message"; "ContextForwardMsg" = "Forward"; diff --git a/src/components/middle/MessageListContent.tsx b/src/components/middle/MessageListContent.tsx index aa2281a5f..952db32c8 100644 --- a/src/components/middle/MessageListContent.tsx +++ b/src/components/middle/MessageListContent.tsx @@ -23,6 +23,7 @@ import { getPeerTitle } from '../../global/helpers/peers'; import { selectChatMessage, selectSender } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import { formatHumanDate, formatScheduledDateTime } from '../../util/dates/oldDateFormat'; +import { isUserId } from '../../util/entities/ids'; import { convertTonFromNanos } from '../../util/formatCurrency'; import { compact } from '../../util/iteratees'; import { formatMessageListDate } from '../../util/localization/dateFormat'; @@ -134,6 +135,7 @@ const MessageListContent = ({ const getIsReady = useDerivedSignal(() => isReady && !getIsHeavyAnimating2(), [isReady, getIsHeavyAnimating2]); const areDatesClickable = !isSavedDialog && !isSchedule; + const isPrivate = isUserId(chatId); const shouldRenderSponsoredMessage = canShowAds && isViewportNewest; const shouldHideComments = hasLinkedChat === false || !isChannelChat || Boolean(isChatMonoforum); @@ -352,6 +354,7 @@ const MessageListContent = ({ const originalId = getMessageOriginalId(message); const key = isServiceNotificationMessage(message) ? `${message.date}_${originalId}` : originalId; + const shouldShowGuestAvatar = isPrivate && !withUsers && Boolean(message.guestChatViaId); return compact([ message.id === memoUnreadDividerBeforeIdRef.current && unreadDivider, @@ -365,9 +368,10 @@ const MessageListContent = ({ observeIntersectionForPlaying={observeIntersectionForPlaying} album={album} documentGroup={documentGroup} - noAvatars={noAvatars} - withAvatar={position.isLastInGroup && withUsers && !isOwn && (!isThreadTopMessage || !isComments)} - withSenderName={position.isFirstInGroup && withUsers && !isOwn} + noAvatars={noAvatars && !shouldShowGuestAvatar} + withAvatar={position.isLastInGroup && (withUsers || shouldShowGuestAvatar) + && !isOwn && (!isThreadTopMessage || !isComments)} + withSenderName={position.isFirstInGroup && (withUsers || shouldShowGuestAvatar) && !isOwn} threadId={threadId} messageListType={type} noComments={shouldHideComments} @@ -450,8 +454,6 @@ const MessageListContent = ({ return renderMessageElement(message, position, isThreadTopMessage, album); }).flat(); - if (!withUsers) return senderGroupElements; - const lastItem = senderGroup[senderGroup.length - 1]; const lastMessage = isAlbum(lastItem) ? lastItem.mainMessage @@ -474,11 +476,14 @@ const MessageListContent = ({ const isThreadTopMessage = lastMessage.id === threadId || (firstMessage.id === threadId && Boolean(firstMessage.groupedId)); + const shouldShowGuestAvatar = isPrivate && !withUsers && Boolean(lastMessage.guestChatViaId); + if (!withUsers && !shouldShowGuestAvatar) return senderGroupElements; + const key = `${firstMessageId}-${lastMessageId}`; const id = (firstMessageId === lastMessageId) ? `message-group-${firstMessageId}` : `message-group-${firstMessageId}-${lastMessageId}`; - const withAvatar = withUsers && !isOwn && (!isThreadTopMessage || !isComments); + const withAvatar = (withUsers || shouldShowGuestAvatar) && !isOwn && (!isThreadTopMessage || !isComments); return compact([ ; canTranscribeVoice?: boolean; viaBusinessBot?: ApiUser; + guestFromSender?: ApiPeer; effect?: ApiAvailableEffect; poll?: ApiMessagePoll; webPage?: ApiWebPage; @@ -459,6 +460,7 @@ const Message = ({ tags, canTranscribeVoice, viaBusinessBot, + guestFromSender, effect, poll, maxTimestamp, @@ -503,7 +505,7 @@ const Message = ({ const oldLang = useOldLang(); const lang = useLang(); const { - id: messageId, chatId, forwardInfo, viaBotId, isTranscriptionError, factCheck, + id: messageId, chatId, forwardInfo, viaBotId, guestChatViaId, isTranscriptionError, factCheck, isTypingDraft, previousLocalId, fromRank, } = message; @@ -600,13 +602,15 @@ const Message = ({ const isCustomShape = !withVoiceTranscription && getMessageCustomShape(message); const hasAnimatedEmoji = isCustomShape && (animatedEmoji || animatedCustomEmoji); const hasReactions = reactionMessage?.reactions && !areReactionsEmpty(reactionMessage.reactions); + const hasViaSender = Boolean(viaBotId || guestChatViaId); + const asForwarded = ( forwardInfo && (!isChatWithSelf || isScheduled) && !isRepliesChat && !forwardInfo.isLinkedChannelPost && !isAnonymousForwards - && !botSender + && !hasViaSender ) || Boolean(storyData && !storyData.isMention); const canShowSenderBoosts = Boolean(senderBoosts) && !asForwarded && isFirstInGroup; const isStoryMention = storyData?.isMention; @@ -691,6 +695,7 @@ const Message = ({ const { handleSenderClick, handleViaBotClick, + handleGuestForClick, handleReplyClick, handleMediaClick, handleDocumentClick, @@ -724,6 +729,7 @@ const Message = ({ avatarPeer, senderPeer, botSender, + guestFromSender, messageTopic, isTranslatingChat: Boolean(requestedChatTranslationLanguage), story: replyStory && 'content' in replyStory ? replyStory : undefined, @@ -815,6 +821,7 @@ const Message = ({ isJustAdded && 'is-just-added', (hasActiveReactions || shouldPlayEffect) && 'has-active-effect', isStoryMention && 'is-story-mention', + guestChatViaId && 'has-guest-avatar', ); const text = textMessage && getMessageContent(textMessage).text; @@ -1655,8 +1662,9 @@ const Message = ({ function shouldRenderSenderName() { const media = photo || video || location || paidMedia; - return !(isCustomShape && !viaBotId) && ( - (withSenderName && (!media || hasTopicChip)) || asForwarded || viaBotId || forceSenderName + return !(isCustomShape && !hasViaSender) && ( + (withSenderName && (!media || hasTopicChip)) || asForwarded || viaBotId + || (guestChatViaId && isFirstInGroup) || forceSenderName ) && !isInDocumentGroupNotFirst && !(hasMessageReply && isCustomShape); } @@ -1734,7 +1742,7 @@ const Message = ({ shouldSkipRenderForwardTitle: boolean = false, shouldSkipRenderAdminTitle: boolean = false, ) { let senderTitle; - if (senderPeer && !(isCustomShape && viaBotId)) { + if (senderPeer && !(isCustomShape && hasViaSender)) { senderTitle = getPeerFullTitle(oldLang, senderPeer); } else if (forwardInfo?.hiddenUserName) { senderTitle = forwardInfo.hiddenUserName; @@ -1743,6 +1751,7 @@ const Message = ({ } const senderEmojiStatus = senderPeer && 'emojiStatus' in senderPeer && senderPeer.emojiStatus; const senderIsPremium = senderPeer && 'isPremium' in senderPeer && senderPeer.isPremium; + const guestFromSenderTitle = guestFromSender ? getPeerTitle(oldLang, guestFromSender) : undefined; const shouldRenderForwardAvatar = asForwarded && senderPeer; return ( @@ -1784,12 +1793,12 @@ const Message = ({ {senderPeer?.fakeType && } - ) : !botSender ? ( + ) : (!botSender && !guestFromSender) ? ( NBSP ) : undefined} {botSender?.hasUsername && ( - - {oldLang('ViaBot')} + + {lang('ViaBot')} )} + {guestFromSenderTitle && ( + + {lang('ForBot')} + + {renderText(guestFromSenderTitle)} + + + )}
{((!shouldSkipRenderAdminTitle && !signature) || canShowSenderBoosts) && ( @@ -2083,8 +2103,8 @@ export default memo(withGlobal( isLastInDocumentGroup, isFirstInGroup, } = ownProps; const { - id, chatId, viaBotId, isOutgoing, forwardInfo, transcriptionId, isPinned, viaBusinessBotId, effectId, - paidMessageStars, + id, chatId, viaBotId, guestChatViaId, isOutgoing, forwardInfo, transcriptionId, isPinned, + viaBusinessBotId, effectId, paidMessageStars, } = message; const webPage = selectFullWebPageFromMessage(global, message); @@ -2112,6 +2132,7 @@ export default memo(withGlobal( const sender = selectSender(global, message); const originSender = selectForwardedSender(global, message); const botSender = viaBotId ? selectUser(global, viaBotId) : undefined; + const guestFromSender = guestChatViaId ? selectPeer(global, guestChatViaId) : undefined; const senderChatMember = sender?.id ? (adminMembersById?.[sender?.id] || members?.find((member) => member.userId === sender?.id)) : undefined; @@ -2234,6 +2255,7 @@ export default memo(withGlobal( canShowSender, originSender, botSender, + guestFromSender, shouldHideReply: shouldHideReply || isReplyToTopicStart, replyMessage, replyMessageSender, diff --git a/src/components/middle/message/_message-content.scss b/src/components/middle/message/_message-content.scss index 2c60bd7da..8883725af 100644 --- a/src/components/middle/message/_message-content.scss +++ b/src/components/middle/message/_message-content.scss @@ -354,6 +354,12 @@ text-overflow: ellipsis; } + .via-sender { + display: flex; + align-items: center; + line-height: normal; + } + .sender-hidden { font-weight: normal; } @@ -639,6 +645,9 @@ .MessageList.no-avatars & { max-width: min(29rem, calc(100vw - 3.75rem)); } + .has-guest-avatar & { + max-width: min(29rem, calc(100vw - 6.25rem)); + } .Message.own & { max-width: min(30rem, calc(100vw - 3.75rem)); } @@ -649,6 +658,9 @@ .MessageList.no-avatars & { max-width: min(29rem, calc(100vw - 4.5rem)); } + .has-guest-avatar & { + max-width: min(29rem, calc(100vw - 7rem)); + } .Message.own & { max-width: min(30rem, calc(100vw - 4.5rem)); diff --git a/src/components/middle/message/helpers/buildContentClassName.ts b/src/components/middle/message/helpers/buildContentClassName.ts index b37875d6f..218d2f0f4 100644 --- a/src/components/middle/message/helpers/buildContentClassName.ts +++ b/src/components/middle/message/helpers/buildContentClassName.ts @@ -67,7 +67,7 @@ export function buildContentClassName( const hasText = text || location?.mediaType === 'venue' || isGeoLiveActive || hasFactCheck || poll; const isMediaWithNoText = isMedia && !hasText; const hasInlineKeyboard = Boolean(message.inlineButtons); - const isViaBot = Boolean(message.viaBotId); + const isViaBot = Boolean(message.viaBotId || message.guestChatViaId); const hasFooter = (() => { if (isInvertedMedia && isInvertibleMedia) { diff --git a/src/components/middle/message/hooks/useInnerHandlers.ts b/src/components/middle/message/hooks/useInnerHandlers.ts index 7f216a809..a0435282a 100644 --- a/src/components/middle/message/hooks/useInnerHandlers.ts +++ b/src/components/middle/message/hooks/useInnerHandlers.ts @@ -1,6 +1,6 @@ import { getActions } from '../../../../global'; -import type { ApiMessage, ApiPeer, ApiStory, ApiTopic, ApiUser, ApiWebPage } from '../../../../api/types'; +import type { ApiMessage, ApiPeer, ApiStory, ApiTopic, ApiWebPage } from '../../../../api/types'; import type { OldLangFn } from '../../../../hooks/useOldLang'; import type { IAlbum, ThreadId } from '../../../../types'; import { MAIN_THREAD_ID } from '../../../../api/types'; @@ -25,6 +25,7 @@ export default function useInnerHandlers({ album, senderPeer, botSender, + guestFromSender, messageTopic, isTranslatingChat, story, @@ -45,7 +46,8 @@ export default function useInnerHandlers({ album?: IAlbum; avatarPeer?: ApiPeer; senderPeer?: ApiPeer; - botSender?: ApiUser; + botSender?: ApiPeer; + guestFromSender?: ApiPeer; messageTopic?: ApiTopic; isTranslatingChat?: boolean; story?: ApiStory; @@ -83,7 +85,8 @@ export default function useInnerHandlers({ }); const handleViaBotClick = useLastCallback(() => { - if (!botSender) { + const username = botSender && getMainUsername(botSender); + if (!username) { return; } @@ -91,11 +94,19 @@ export default function useInnerHandlers({ chatId, threadId, text: { - text: `@${getMainUsername(botSender)} `, + text: `@${username} `, }, }); }); + const handleGuestForClick = useLastCallback(() => { + if (!guestFromSender) { + return; + } + + openChat({ id: guestFromSender.id }); + }); + const handleReplyClick = useLastCallback((): void => { if (!replyToMsgId || isReplyPrivate) { showNotification({ @@ -289,6 +300,7 @@ export default function useInnerHandlers({ return { handleSenderClick, handleViaBotClick, + handleGuestForClick, handleReplyClick, handleDocumentClick, handleMediaClick, diff --git a/src/global/helpers/peers.ts b/src/global/helpers/peers.ts index c43af4227..c72484a5d 100644 --- a/src/global/helpers/peers.ts +++ b/src/global/helpers/peers.ts @@ -112,8 +112,10 @@ export function getPeerFullTitle(lang: OldLangFn | LangFn, peer: ApiPeer | Custo } export function getMessageSenderName(lang: LangFn, chatId: string, sender: ApiPeer) { + const isSenderUser = isApiPeerUser(sender); + const isSenderGuestBot = isSenderUser && sender.isGuestChatBot; // Hide sender name for private chats - if (isUserId(chatId)) return undefined; + if (isUserId(chatId) && !isSenderGuestBot) return undefined; if (isApiPeerChat(sender)) { if (chatId === sender.id) return undefined; diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 02e159318..c3850be7d 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -709,6 +709,7 @@ export interface LangPair { 'PaymentInvoiceNotFound': undefined; 'NoWordsRecognized': undefined; 'ViaBot': undefined; + 'ForBot': undefined; 'DiscussChannel': undefined; 'ForwardedMessage': undefined; 'ContextForwardMsg': undefined;