Message: Show guest bots (#6955)

This commit is contained in:
zubiden 2026-06-01 01:15:38 +02:00 committed by Alexander Zinchuk
parent 60f3995e82
commit b7e10507e6
14 changed files with 93 additions and 25 deletions

View File

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

View File

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

View File

@ -764,6 +764,7 @@ export interface ApiMessage {
isKeyboardSelective?: boolean;
viaBotId?: string;
viaBusinessBotId?: string;
guestChatViaId?: string;
postAuthorTitle?: string;
isScheduled?: boolean;
scheduleRepeatPeriod?: number;

View File

@ -49,6 +49,7 @@ export interface ApiUser {
paidMessagesStars?: number;
isBotForum?: boolean;
canManageBotForumTopics?: boolean;
isGuestChatBot?: boolean;
}
export interface ApiUserFullInfo {

View File

@ -816,6 +816,7 @@
"PaymentInvoiceNotFound" = "Invoice not found";
"NoWordsRecognized" = "No words recognized.";
"ViaBot" = "via";
"ForBot" = "for";
"DiscussChannel" = "channel";
"ForwardedMessage" = "Forwarded message";
"ContextForwardMsg" = "Forward";

View File

@ -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([
<SenderGroupContainer
key={key}

View File

@ -121,6 +121,7 @@ export function groupMessages(
} else if (
nextMessage.id === firstUnreadId
|| message.senderId !== nextMessage.senderId
|| message.guestChatViaId !== nextMessage.guestChatViaId
|| (!withUsers && message.paidMessageStars)
|| (nextMessage.suggestedPostInfo)
|| message.isOutgoing !== nextMessage.isOutgoing

View File

@ -154,6 +154,10 @@
padding-left: 0;
}
&.has-guest-avatar {
padding-left: 2.5rem;
}
&.first-in-group:not(.last-in-group) {
--border-bottom-left-radius: var(--border-radius-messages-small);
}
@ -182,6 +186,10 @@
&.is-thread-top {
padding-left: 0.25rem;
}
&.has-guest-avatar {
padding-left: 2.875rem;
}
}
}

View File

@ -72,7 +72,7 @@ import {
isReplyToMessage,
isSystemBot,
} from '../../../global/helpers';
import { getPeerFullTitle } from '../../../global/helpers/peers';
import { getPeerFullTitle, getPeerTitle } from '../../../global/helpers/peers';
import { getMessageReplyInfo, getStoryReplyInfo } from '../../../global/helpers/replies';
import {
selectActiveDownloads,
@ -329,6 +329,7 @@ type StateProps = {
tags?: Record<ApiReactionKey, ApiSavedReactionTag>;
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 && <FakeIcon fakeType={senderPeer.fakeType} />}
</span>
</span>
) : !botSender ? (
) : (!botSender && !guestFromSender) ? (
NBSP
) : undefined}
{botSender?.hasUsername && (
<span className="interactive">
<span className="via">{oldLang('ViaBot')}</span>
<span className="interactive via-sender">
<span className="via">{lang('ViaBot')}</span>
<span
className="sender-title"
onClick={handleViaBotClick}
@ -1798,6 +1807,17 @@ const Message = ({
</span>
</span>
)}
{guestFromSenderTitle && (
<span className="interactive via-sender">
<span className="via">{lang('ForBot')}</span>
<span
className="sender-title"
onClick={handleGuestForClick}
>
{renderText(guestFromSenderTitle)}
</span>
</span>
)}
<div className="title-spacer" />
{((!shouldSkipRenderAdminTitle && !signature) || canShowSenderBoosts) && (
<span className="message-title-meta">
@ -2083,8 +2103,8 @@ export default memo(withGlobal<OwnProps>(
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<OwnProps>(
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<OwnProps>(
canShowSender,
originSender,
botSender,
guestFromSender,
shouldHideReply: shouldHideReply || isReplyToTopicStart,
replyMessage,
replyMessageSender,

View File

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

View File

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

View File

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

View File

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

View File

@ -709,6 +709,7 @@ export interface LangPair {
'PaymentInvoiceNotFound': undefined;
'NoWordsRecognized': undefined;
'ViaBot': undefined;
'ForBot': undefined;
'DiscussChannel': undefined;
'ForwardedMessage': undefined;
'ContextForwardMsg': undefined;