diff --git a/src/api/gramjs/apiBuilders/chats.ts b/src/api/gramjs/apiBuilders/chats.ts index be9b71fa1..80ef41162 100644 --- a/src/api/gramjs/apiBuilders/chats.ts +++ b/src/api/gramjs/apiBuilders/chats.ts @@ -27,7 +27,7 @@ import { pick, pickTruthy } from '../../../util/iteratees'; import { getServerTime, getServerTimeOffset } from '../../../util/serverTime'; import { addPhotoToLocalDb, addUserToLocalDb, serializeBytes } from '../helpers'; import { - buildApiFormattedText, buildApiPhoto, buildApiUsernames, buildAvatarPhotoId, + buildApiBotVerification, buildApiFormattedText, buildApiPhoto, buildApiUsernames, buildAvatarPhotoId, } from './common'; import { omitVirtualClassFields } from './helpers'; import { @@ -65,6 +65,8 @@ function buildApiChatFieldsFromPeerEntity( const isForum = Boolean('forum' in peerEntity && peerEntity.forum); const areStoriesHidden = Boolean('storiesHidden' in peerEntity && peerEntity.storiesHidden); const maxStoryId = 'storiesMaxId' in peerEntity ? peerEntity.storiesMaxId : undefined; + const botVerificationIconId = 'botVerificationIconId' in peerEntity + ? peerEntity.botVerificationIconId?.toString() : undefined; const storiesUnavailable = Boolean('storiesUnavailable' in peerEntity && peerEntity.storiesUnavailable); const color = ('color' in peerEntity && peerEntity.color) ? buildApiPeerColor(peerEntity.color) : undefined; const emojiStatus = ('emojiStatus' in peerEntity && peerEntity.emojiStatus) @@ -105,6 +107,7 @@ function buildApiChatFieldsFromPeerEntity( hasStories: Boolean(maxStoryId) && !storiesUnavailable, emojiStatus, boostLevel, + botVerificationIconId, subscriptionUntil, }; } @@ -685,7 +688,7 @@ export function buildApiSponsoredMessageReportResult( export function buildApiChatInviteInfo(invite: GramJs.ChatInvite): ApiChatInviteInfo { const { color, participants, participantsCount, photo, title, about, scam, fake, verified, megagroup, channel, broadcast, - requestNeeded, subscriptionFormId, subscriptionPricing, canRefulfillSubscription, + requestNeeded, subscriptionFormId, subscriptionPricing, canRefulfillSubscription, botVerification, } = invite; let apiPhoto; @@ -714,6 +717,7 @@ export function buildApiChatInviteInfo(invite: GramJs.ChatInvite): ApiChatInvite subscriptionPricing: subscriptionPricing && buildApiStarsSubscriptionPricing(subscriptionPricing), canRefulfillSubscription, participantIds: participants?.map((participant) => buildApiPeerId(participant.id, 'user')).filter(Boolean), + botVerification: botVerification && buildApiBotVerification(botVerification), }; } diff --git a/src/api/gramjs/apiBuilders/common.ts b/src/api/gramjs/apiBuilders/common.ts index e6168e0f8..1d420cfb1 100644 --- a/src/api/gramjs/apiBuilders/common.ts +++ b/src/api/gramjs/apiBuilders/common.ts @@ -2,6 +2,7 @@ import { Api as GramJs } from '../../../lib/gramjs'; import { strippedPhotoToJpg } from '../../../lib/gramjs/Utils'; import type { + ApiBotVerification, ApiFormattedText, ApiMessageEntity, ApiMessageEntityDefault, @@ -299,3 +300,11 @@ export function buildAvatarPhotoId(photo: GramJs.TypeUserProfilePhoto | GramJs.T return undefined; } + +export function buildApiBotVerification(botVerification: GramJs.BotVerification): ApiBotVerification { + return { + botId: buildApiPeerId(botVerification.botId, 'user'), + iconId: botVerification.icon.toString(), + description: botVerification.description, + }; +} diff --git a/src/api/gramjs/apiBuilders/users.ts b/src/api/gramjs/apiBuilders/users.ts index 935a74089..99b4edca4 100644 --- a/src/api/gramjs/apiBuilders/users.ts +++ b/src/api/gramjs/apiBuilders/users.ts @@ -11,7 +11,9 @@ import type { import { buildApiBotInfo } from './bots'; import { buildApiBusinessIntro, buildApiBusinessLocation, buildApiBusinessWorkHours } from './business'; -import { buildApiPhoto, buildApiUsernames, buildAvatarPhotoId } from './common'; +import { + buildApiBotVerification, buildApiPhoto, buildApiUsernames, buildAvatarPhotoId, +} from './common'; import { omitVirtualClassFields } from './helpers'; import { buildApiEmojiStatus, buildApiPeerColor, buildApiPeerId } from './peers'; @@ -22,7 +24,7 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse profilePhoto, voiceMessagesForbidden, premiumGifts, hasScheduled, fallbackPhoto, personalPhoto, translationsDisabled, storiesPinnedAvailable, contactRequirePremium, businessWorkHours, businessLocation, businessIntro, - birthday, personalChannelId, personalChannelMessage, sponsoredEnabled, stargiftsCount, + birthday, personalChannelId, personalChannelMessage, sponsoredEnabled, stargiftsCount, botVerification, }, users, } = mtpUserFull; @@ -49,6 +51,7 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse businessIntro: businessIntro && buildApiBusinessIntro(businessIntro), personalChannelId: personalChannelId && buildApiPeerId(personalChannelId, 'channel'), personalChannelMessageId: personalChannelMessage, + botVerification: botVerification && buildApiBotVerification(botVerification), areAdsEnabled: sponsoredEnabled, starGiftCount: stargiftsCount, hasScheduledMessages: hasScheduled, @@ -62,7 +65,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined { const { id, firstName, lastName, fake, scam, support, closeFriend, storiesUnavailable, storiesMaxId, - bot, botActiveUsers, botInlinePlaceholder, botAttachMenu, botCanEdit, + bot, botActiveUsers, botVerificationIcon, botInlinePlaceholder, botAttachMenu, botCanEdit, } = mtpUser; const hasVideoAvatar = mtpUser.photo instanceof GramJs.UserProfilePhoto ? Boolean(mtpUser.photo.hasVideo) : undefined; const avatarPhotoId = mtpUser.photo && buildAvatarPhotoId(mtpUser.photo); @@ -99,6 +102,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined { ...(bot && botInlinePlaceholder && { botPlaceholder: botInlinePlaceholder }), ...(bot && botAttachMenu && { isAttachBot: botAttachMenu }), botActiveUsers, + botVerificationIconId: botVerificationIcon?.toString(), color: mtpUser.color && buildApiPeerColor(mtpUser.color), }; } diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index 5c4c7474c..cde5825f7 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -49,7 +49,7 @@ import { buildChatMembers, getPeerKey, } from '../apiBuilders/chats'; -import { buildApiPhoto } from '../apiBuilders/common'; +import { buildApiBotVerification, buildApiPhoto } from '../apiBuilders/common'; import { buildApiMessage, buildMessageDraft } from '../apiBuilders/messages'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers'; import { buildStickerSet } from '../apiBuilders/symbols'; @@ -603,6 +603,7 @@ async function getFullChannelInfo( emojiset, boostsApplied, boostsUnrestrict, + botVerification, canViewRevenue: canViewMonetization, paidReactionsAvailable, hasScheduled, @@ -695,6 +696,7 @@ async function getFullChannelInfo( hasPinnedStories: Boolean(storiesPinnedAvailable), boostsApplied, boostsToUnrestrict: boostsUnrestrict, + botVerification: botVerification && buildApiBotVerification(botVerification), isPaidReactionAvailable: paidReactionsAvailable, hasScheduledMessages: hasScheduled, }, diff --git a/src/api/types/chats.ts b/src/api/types/chats.ts index fb7624d49..66ff41434 100644 --- a/src/api/types/chats.ts +++ b/src/api/types/chats.ts @@ -2,7 +2,7 @@ import type { ApiBotCommand } from './bots'; import type { ApiChatReactions, ApiFormattedText, ApiInputMessageReplyInfo, ApiPhoto, ApiStickerSet, } from './messages'; -import type { ApiChatInviteImporter } from './misc'; +import type { ApiBotVerification, ApiChatInviteImporter } from './misc'; import type { ApiEmojiStatus, ApiFakeType, ApiUser, ApiUsername, } from './users'; @@ -48,6 +48,7 @@ export interface ApiChat { isForum?: boolean; isForumAsMessages?: true; boostLevel?: number; + botVerificationIconId?: string; // Calls isCallActive?: boolean; @@ -143,6 +144,7 @@ export interface ApiChatFullInfo { boostsApplied?: number; boostsToUnrestrict?: number; + botVerification?: ApiBotVerification; } export interface ApiChatMember { diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index 89894b82a..fb290760b 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -175,6 +175,7 @@ export type ApiChatInviteInfo = { subscriptionFormId?: string; canRefulfillSubscription?: boolean; subscriptionPricing?: ApiStarsSubscriptionPricing; + botVerification?: ApiBotVerification; }; export type ApiChatInviteImporter = { @@ -321,6 +322,12 @@ export interface ApiPeerPhotos { isLoading?: boolean; } +export interface ApiBotVerification { + botId: string; + iconId: string; + description: string; +} + export type ApiLimitType = | 'uploadMaxFileparts' | 'stickersFaved' diff --git a/src/api/types/users.ts b/src/api/types/users.ts index d430f4d1e..4589d4281 100644 --- a/src/api/types/users.ts +++ b/src/api/types/users.ts @@ -3,6 +3,7 @@ import type { ApiBotInfo } from './bots'; import type { ApiBusinessIntro, ApiBusinessLocation, ApiBusinessWorkHours } from './business'; import type { ApiPeerColor } from './chats'; import type { ApiDocument, ApiPhoto } from './messages'; +import type { ApiBotVerification } from './misc'; import type { ApiUserStarGift } from './payments'; export interface ApiUser { @@ -36,6 +37,7 @@ export interface ApiUser { canEditBot?: boolean; hasMainMiniApp?: boolean; botActiveUsers?: number; + botVerificationIconId?: string; } export interface ApiUserFullInfo { @@ -61,6 +63,7 @@ export interface ApiUserFullInfo { businessIntro?: ApiBusinessIntro; starGiftCount?: number; hasScheduledMessages?: boolean; + botVerification?: ApiBotVerification; } export type ApiFakeType = 'fake' | 'scam'; diff --git a/src/components/common/FullNameTitle.tsx b/src/components/common/FullNameTitle.tsx index a06f5f4e1..09f374103 100644 --- a/src/components/common/FullNameTitle.tsx +++ b/src/components/common/FullNameTitle.tsx @@ -107,9 +107,18 @@ const FullNameTitle: FC = ({ return undefined; }, [customPeer, isSavedDialog, isSavedMessages, lang, realPeer]); + const botVerificationIconId = realPeer?.botVerificationIconId; return (
+ {botVerificationIconId && ( + + )}

cb(), 500, false); +const BOT_VERIFICATION_ICON_SIZE = 16; const ChatExtra: FC = ({ chatOrUserId, @@ -101,6 +105,7 @@ const ChatExtra: FC = ({ hasSavedMessages, personalChannel, hasMainMiniApp, + botVerification, }) => { const { showNotification, @@ -437,6 +442,16 @@ const ChatExtra: FC = ({ {oldLang('SavedMessagesTab')} )} + {botVerification && ( +
+ + {botVerification.description} +
+ )}

); }; @@ -455,6 +470,8 @@ export default memo(withGlobal( const chatFullInfo = chat && selectChatFullInfo(global, chat.id); const userFullInfo = user && selectUserFullInfo(global, user.id); + const botVerification = userFullInfo?.botVerification || chatFullInfo?.botVerification; + const chatInviteLink = chatFullInfo?.inviteLink; const description = userFullInfo?.bio || chatFullInfo?.about; @@ -488,6 +505,7 @@ export default memo(withGlobal( hasSavedMessages, personalChannel, hasMainMiniApp, + botVerification, }; }, )(ChatExtra)); diff --git a/src/components/middle/MiddleHeaderPanes.tsx b/src/components/middle/MiddleHeaderPanes.tsx index 6e198baaa..17a35406f 100644 --- a/src/components/middle/MiddleHeaderPanes.tsx +++ b/src/components/middle/MiddleHeaderPanes.tsx @@ -22,6 +22,7 @@ import { applyAnimationState, type PaneState } from './hooks/useHeaderPane'; import GroupCallTopPane from '../calls/group/GroupCallTopPane'; import AudioPlayer from './panes/AudioPlayer'; import BotAdPane from './panes/BotAdPane'; +import BotVerificationPane from './panes/BotVerificationPane'; import ChatReportPane from './panes/ChatReportPane'; import HeaderPinnedMessage from './panes/HeaderPinnedMessage'; @@ -65,6 +66,7 @@ const MiddleHeaderPanes = ({ const [getGroupCallState, setGroupCallState] = useSignal(FALLBACK_PANE_STATE); const [getChatReportState, setChatReportState] = useSignal(FALLBACK_PANE_STATE); const [getBotAdState, setBotAdState] = useSignal(FALLBACK_PANE_STATE); + const [getBotVerificationState, setBotVerificationState] = useSignal(FALLBACK_PANE_STATE); const isPinnedMessagesFullWidth = isAudioPlayerRendered || !isDesktop; @@ -84,13 +86,15 @@ const MiddleHeaderPanes = ({ useSignalEffect(() => { const audioPlayerState = getAudioPlayerState(); + const botVerificationState = getBotVerificationState(); const pinnedState = getPinnedState(); const groupCallState = getGroupCallState(); const chatReportState = getChatReportState(); const botAdState = getBotAdState(); // Keep in sync with the order of the panes in the DOM - const stateArray = [audioPlayerState, groupCallState, chatReportState, pinnedState, botAdState]; + const stateArray = [audioPlayerState, groupCallState, + chatReportState, botVerificationState, pinnedState, botAdState]; const isFirstRender = isFirstRenderRef.current; const totalHeight = stateArray.reduce((acc, state) => acc + state.height, 0); @@ -103,7 +107,8 @@ const MiddleHeaderPanes = ({ setExtraStyles(middleColumn, { '--middle-header-panes-height': `${totalHeight}px`, }); - }, [getAudioPlayerState, getGroupCallState, getPinnedState, getChatReportState, getBotAdState]); + }, [getAudioPlayerState, getGroupCallState, getPinnedState, + getChatReportState, getBotAdState, getBotVerificationState]); if (!shouldRender) return undefined; @@ -128,6 +133,10 @@ const MiddleHeaderPanes = ({ isAutoArchived={settings?.isAutoArchived} onPaneStateChange={setChatReportState} /> + void; +}; + +type StateProps = { + wasShown: boolean; + botVerification?: ApiBotVerification; +}; +const BOT_VERIFICATION_ICON_SIZE = 16; +const DISPLAY_DURATION_MS = 5000; // 5 sec + +const BotVerificationPane: FC = ({ + peerId, + wasShown, + botVerification, + onPaneStateChange, +}) => { + const isOpen = Boolean(!wasShown && botVerification); + + const { + markBotVerificationInfoShown, + } = getActions(); + + const { ref, shouldRender } = useHeaderPane({ + isOpen, + onStateChange: onPaneStateChange, + }); + + const markAsShowed = useLastCallback(() => { + markBotVerificationInfoShown({ peerId }); + }); + useTimeout(markAsShowed, !wasShown ? DISPLAY_DURATION_MS : undefined); + + if (!shouldRender || !botVerification) return undefined; + + return ( +
+ + + + {botVerification.description} +
+ ); +}; + +export default memo(withGlobal( + (global, { peerId }): StateProps => { + const peerFullInfo = selectPeerFullInfo(global, peerId); + + const botVerification = peerFullInfo?.botVerification; + const wasShown = global.settings.botVerificationShownPeerIds.includes(peerId); + + return { + botVerification, + wasShown, + }; + }, +)(BotVerificationPane)); diff --git a/src/config.ts b/src/config.ts index 48c246a2b..306a54007 100644 --- a/src/config.ts +++ b/src/config.ts @@ -21,6 +21,7 @@ export const DEBUG = process.env.APP_ENV !== 'production'; export const DEBUG_MORE = false; export const DEBUG_LOG_FILENAME = 'tt-log.json'; export const STRICTERDOM_ENABLED = DEBUG; +export const BOT_VERIFICATION_PEERS_LIMIT = 20; export const BETA_CHANGELOG_URL = 'https://telegra.ph/WebA-Beta-03-20'; export const ELECTRON_HOST_URL = process.env.ELECTRON_HOST_URL!; diff --git a/src/global/actions/api/users.ts b/src/global/actions/api/users.ts index f686a344a..9408b35d2 100644 --- a/src/global/actions/api/users.ts +++ b/src/global/actions/api/users.ts @@ -2,6 +2,7 @@ import type { ApiUser } from '../../../api/types'; import type { ActionReturnType } from '../../types'; import { ManagementProgress } from '../../../types'; +import { BOT_VERIFICATION_PEERS_LIMIT } from '../../../config'; import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { buildCollectionByKey, unique } from '../../../util/iteratees'; import * as langProvider from '../../../util/oldLangProvider'; @@ -9,11 +10,7 @@ import { throttle } from '../../../util/schedulers'; import { getServerTime } from '../../../util/serverTime'; import { callApi } from '../../../api/gramjs'; import { isUserBot, isUserId } from '../../helpers'; -import { - addActionHandler, - getGlobal, - setGlobal, -} from '../../index'; +import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { addUserStatuses, closeNewContactDialog, @@ -508,3 +505,20 @@ addActionHandler('openSuggestedStatusModal', async (global, actions, payload): P }, tabId); setGlobal(global); }); + +addActionHandler('markBotVerificationInfoShown', (global, actions, payload): ActionReturnType => { + const { peerId } = payload; + + const currentPeerIds = global.settings.botVerificationShownPeerIds; + const newPeerIds = unique([peerId, ...currentPeerIds]).slice(0, BOT_VERIFICATION_PEERS_LIMIT); + + global = { + ...global, + settings: { + ...global.settings, + botVerificationShownPeerIds: newPeerIds, + }, + }; + + setGlobal(global); +}); diff --git a/src/global/cache.ts b/src/global/cache.ts index 49b46dcc5..f4c3a6252 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -260,6 +260,9 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) { if (!cached.messages.pollById) { cached.messages.pollById = initialState.messages.pollById; } + if (!cached.settings.botVerificationShownPeerIds) { + cached.settings.botVerificationShownPeerIds = initialState.settings.botVerificationShownPeerIds; + } } function updateCache(force?: boolean) { @@ -610,7 +613,9 @@ function omitLocalMedia(message: ApiMessage): ApiMessage { } function reduceSettings(global: T): GlobalState['settings'] { - const { byKey, themes, performance } = global.settings; + const { + byKey, themes, performance, botVerificationShownPeerIds, + } = global.settings; return { byKey, @@ -618,6 +623,7 @@ function reduceSettings(global: T): GlobalState['settings performance, privacy: {}, notifyExceptions: {}, + botVerificationShownPeerIds, }; } diff --git a/src/global/initialState.ts b/src/global/initialState.ts index 9698ecf09..bdf12a53c 100644 --- a/src/global/initialState.ts +++ b/src/global/initialState.ts @@ -296,6 +296,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = { performance: INITIAL_PERFORMANCE_STATE_MAX, privacy: {}, notifyExceptions: {}, + botVerificationShownPeerIds: [], }, serviceNotifications: [], diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index 3d47eb330..cb7d82870 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -269,6 +269,9 @@ export interface ActionPayloads { saveCloseFriends: { userIds: string[]; }; + markBotVerificationInfoShown: { + peerId: string; + }; // Message search openMiddleSearch: WithTabId | undefined; diff --git a/src/global/types/globalState.ts b/src/global/types/globalState.ts index bb4c56934..663a4e9ba 100644 --- a/src/global/types/globalState.ts +++ b/src/global/types/globalState.ts @@ -401,6 +401,7 @@ export type GlobalState = { lastPremiumBandwithNotificationDate?: number; paidReactionPrivacy?: boolean; languages?: ApiLanguage[]; + botVerificationShownPeerIds: string[]; }; push?: { diff --git a/src/hooks/stickers/useDynamicColorListener.ts b/src/hooks/stickers/useDynamicColorListener.ts index d055256f7..073ebe665 100644 --- a/src/hooks/stickers/useDynamicColorListener.ts +++ b/src/hooks/stickers/useDynamicColorListener.ts @@ -10,7 +10,8 @@ import useResizeObserver from '../useResizeObserver'; const TRANSITION_PROPERTY = 'color'; const TRANSITION_STYLE = `50ms ${TRANSITION_PROPERTY} linear`; -export default function useDynamicColorListener(ref: React.RefObject, isDisabled?: boolean) { +export default function useDynamicColorListener(ref: React.RefObject, + isDisabled?: boolean) { const [hexColor, setHexColor] = useState(); const updateColor = useLastCallback(() => {