Verification: Support bot verification (#5396)

Co-authored-by: Alexander Zinchuk <alx.zinchuk@gmail.com>
This commit is contained in:
Alexander Zinchuk 2025-01-05 20:18:49 +01:00
parent 683384074a
commit 1e68ce4da7
20 changed files with 218 additions and 16 deletions

View File

@ -27,7 +27,7 @@ import { pick, pickTruthy } from '../../../util/iteratees';
import { getServerTime, getServerTimeOffset } from '../../../util/serverTime'; import { getServerTime, getServerTimeOffset } from '../../../util/serverTime';
import { addPhotoToLocalDb, addUserToLocalDb, serializeBytes } from '../helpers'; import { addPhotoToLocalDb, addUserToLocalDb, serializeBytes } from '../helpers';
import { import {
buildApiFormattedText, buildApiPhoto, buildApiUsernames, buildAvatarPhotoId, buildApiBotVerification, buildApiFormattedText, buildApiPhoto, buildApiUsernames, buildAvatarPhotoId,
} from './common'; } from './common';
import { omitVirtualClassFields } from './helpers'; import { omitVirtualClassFields } from './helpers';
import { import {
@ -65,6 +65,8 @@ function buildApiChatFieldsFromPeerEntity(
const isForum = Boolean('forum' in peerEntity && peerEntity.forum); const isForum = Boolean('forum' in peerEntity && peerEntity.forum);
const areStoriesHidden = Boolean('storiesHidden' in peerEntity && peerEntity.storiesHidden); const areStoriesHidden = Boolean('storiesHidden' in peerEntity && peerEntity.storiesHidden);
const maxStoryId = 'storiesMaxId' in peerEntity ? peerEntity.storiesMaxId : undefined; 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 storiesUnavailable = Boolean('storiesUnavailable' in peerEntity && peerEntity.storiesUnavailable);
const color = ('color' in peerEntity && peerEntity.color) ? buildApiPeerColor(peerEntity.color) : undefined; const color = ('color' in peerEntity && peerEntity.color) ? buildApiPeerColor(peerEntity.color) : undefined;
const emojiStatus = ('emojiStatus' in peerEntity && peerEntity.emojiStatus) const emojiStatus = ('emojiStatus' in peerEntity && peerEntity.emojiStatus)
@ -105,6 +107,7 @@ function buildApiChatFieldsFromPeerEntity(
hasStories: Boolean(maxStoryId) && !storiesUnavailable, hasStories: Boolean(maxStoryId) && !storiesUnavailable,
emojiStatus, emojiStatus,
boostLevel, boostLevel,
botVerificationIconId,
subscriptionUntil, subscriptionUntil,
}; };
} }
@ -685,7 +688,7 @@ export function buildApiSponsoredMessageReportResult(
export function buildApiChatInviteInfo(invite: GramJs.ChatInvite): ApiChatInviteInfo { export function buildApiChatInviteInfo(invite: GramJs.ChatInvite): ApiChatInviteInfo {
const { const {
color, participants, participantsCount, photo, title, about, scam, fake, verified, megagroup, channel, broadcast, color, participants, participantsCount, photo, title, about, scam, fake, verified, megagroup, channel, broadcast,
requestNeeded, subscriptionFormId, subscriptionPricing, canRefulfillSubscription, requestNeeded, subscriptionFormId, subscriptionPricing, canRefulfillSubscription, botVerification,
} = invite; } = invite;
let apiPhoto; let apiPhoto;
@ -714,6 +717,7 @@ export function buildApiChatInviteInfo(invite: GramJs.ChatInvite): ApiChatInvite
subscriptionPricing: subscriptionPricing && buildApiStarsSubscriptionPricing(subscriptionPricing), subscriptionPricing: subscriptionPricing && buildApiStarsSubscriptionPricing(subscriptionPricing),
canRefulfillSubscription, canRefulfillSubscription,
participantIds: participants?.map((participant) => buildApiPeerId(participant.id, 'user')).filter(Boolean), participantIds: participants?.map((participant) => buildApiPeerId(participant.id, 'user')).filter(Boolean),
botVerification: botVerification && buildApiBotVerification(botVerification),
}; };
} }

View File

@ -2,6 +2,7 @@ import { Api as GramJs } from '../../../lib/gramjs';
import { strippedPhotoToJpg } from '../../../lib/gramjs/Utils'; import { strippedPhotoToJpg } from '../../../lib/gramjs/Utils';
import type { import type {
ApiBotVerification,
ApiFormattedText, ApiFormattedText,
ApiMessageEntity, ApiMessageEntity,
ApiMessageEntityDefault, ApiMessageEntityDefault,
@ -299,3 +300,11 @@ export function buildAvatarPhotoId(photo: GramJs.TypeUserProfilePhoto | GramJs.T
return undefined; return undefined;
} }
export function buildApiBotVerification(botVerification: GramJs.BotVerification): ApiBotVerification {
return {
botId: buildApiPeerId(botVerification.botId, 'user'),
iconId: botVerification.icon.toString(),
description: botVerification.description,
};
}

View File

@ -11,7 +11,9 @@ import type {
import { buildApiBotInfo } from './bots'; import { buildApiBotInfo } from './bots';
import { buildApiBusinessIntro, buildApiBusinessLocation, buildApiBusinessWorkHours } from './business'; import { buildApiBusinessIntro, buildApiBusinessLocation, buildApiBusinessWorkHours } from './business';
import { buildApiPhoto, buildApiUsernames, buildAvatarPhotoId } from './common'; import {
buildApiBotVerification, buildApiPhoto, buildApiUsernames, buildAvatarPhotoId,
} from './common';
import { omitVirtualClassFields } from './helpers'; import { omitVirtualClassFields } from './helpers';
import { buildApiEmojiStatus, buildApiPeerColor, buildApiPeerId } from './peers'; import { buildApiEmojiStatus, buildApiPeerColor, buildApiPeerId } from './peers';
@ -22,7 +24,7 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
profilePhoto, voiceMessagesForbidden, premiumGifts, hasScheduled, profilePhoto, voiceMessagesForbidden, premiumGifts, hasScheduled,
fallbackPhoto, personalPhoto, translationsDisabled, storiesPinnedAvailable, fallbackPhoto, personalPhoto, translationsDisabled, storiesPinnedAvailable,
contactRequirePremium, businessWorkHours, businessLocation, businessIntro, contactRequirePremium, businessWorkHours, businessLocation, businessIntro,
birthday, personalChannelId, personalChannelMessage, sponsoredEnabled, stargiftsCount, birthday, personalChannelId, personalChannelMessage, sponsoredEnabled, stargiftsCount, botVerification,
}, },
users, users,
} = mtpUserFull; } = mtpUserFull;
@ -49,6 +51,7 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
businessIntro: businessIntro && buildApiBusinessIntro(businessIntro), businessIntro: businessIntro && buildApiBusinessIntro(businessIntro),
personalChannelId: personalChannelId && buildApiPeerId(personalChannelId, 'channel'), personalChannelId: personalChannelId && buildApiPeerId(personalChannelId, 'channel'),
personalChannelMessageId: personalChannelMessage, personalChannelMessageId: personalChannelMessage,
botVerification: botVerification && buildApiBotVerification(botVerification),
areAdsEnabled: sponsoredEnabled, areAdsEnabled: sponsoredEnabled,
starGiftCount: stargiftsCount, starGiftCount: stargiftsCount,
hasScheduledMessages: hasScheduled, hasScheduledMessages: hasScheduled,
@ -62,7 +65,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
const { const {
id, firstName, lastName, fake, scam, support, closeFriend, storiesUnavailable, storiesMaxId, id, firstName, lastName, fake, scam, support, closeFriend, storiesUnavailable, storiesMaxId,
bot, botActiveUsers, botInlinePlaceholder, botAttachMenu, botCanEdit, bot, botActiveUsers, botVerificationIcon, botInlinePlaceholder, botAttachMenu, botCanEdit,
} = mtpUser; } = mtpUser;
const hasVideoAvatar = mtpUser.photo instanceof GramJs.UserProfilePhoto ? Boolean(mtpUser.photo.hasVideo) : undefined; const hasVideoAvatar = mtpUser.photo instanceof GramJs.UserProfilePhoto ? Boolean(mtpUser.photo.hasVideo) : undefined;
const avatarPhotoId = mtpUser.photo && buildAvatarPhotoId(mtpUser.photo); const avatarPhotoId = mtpUser.photo && buildAvatarPhotoId(mtpUser.photo);
@ -99,6 +102,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
...(bot && botInlinePlaceholder && { botPlaceholder: botInlinePlaceholder }), ...(bot && botInlinePlaceholder && { botPlaceholder: botInlinePlaceholder }),
...(bot && botAttachMenu && { isAttachBot: botAttachMenu }), ...(bot && botAttachMenu && { isAttachBot: botAttachMenu }),
botActiveUsers, botActiveUsers,
botVerificationIconId: botVerificationIcon?.toString(),
color: mtpUser.color && buildApiPeerColor(mtpUser.color), color: mtpUser.color && buildApiPeerColor(mtpUser.color),
}; };
} }

View File

@ -49,7 +49,7 @@ import {
buildChatMembers, buildChatMembers,
getPeerKey, getPeerKey,
} from '../apiBuilders/chats'; } from '../apiBuilders/chats';
import { buildApiPhoto } from '../apiBuilders/common'; import { buildApiBotVerification, buildApiPhoto } from '../apiBuilders/common';
import { buildApiMessage, buildMessageDraft } from '../apiBuilders/messages'; import { buildApiMessage, buildMessageDraft } from '../apiBuilders/messages';
import { buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
import { buildStickerSet } from '../apiBuilders/symbols'; import { buildStickerSet } from '../apiBuilders/symbols';
@ -603,6 +603,7 @@ async function getFullChannelInfo(
emojiset, emojiset,
boostsApplied, boostsApplied,
boostsUnrestrict, boostsUnrestrict,
botVerification,
canViewRevenue: canViewMonetization, canViewRevenue: canViewMonetization,
paidReactionsAvailable, paidReactionsAvailable,
hasScheduled, hasScheduled,
@ -695,6 +696,7 @@ async function getFullChannelInfo(
hasPinnedStories: Boolean(storiesPinnedAvailable), hasPinnedStories: Boolean(storiesPinnedAvailable),
boostsApplied, boostsApplied,
boostsToUnrestrict: boostsUnrestrict, boostsToUnrestrict: boostsUnrestrict,
botVerification: botVerification && buildApiBotVerification(botVerification),
isPaidReactionAvailable: paidReactionsAvailable, isPaidReactionAvailable: paidReactionsAvailable,
hasScheduledMessages: hasScheduled, hasScheduledMessages: hasScheduled,
}, },

View File

@ -2,7 +2,7 @@ import type { ApiBotCommand } from './bots';
import type { import type {
ApiChatReactions, ApiFormattedText, ApiInputMessageReplyInfo, ApiPhoto, ApiStickerSet, ApiChatReactions, ApiFormattedText, ApiInputMessageReplyInfo, ApiPhoto, ApiStickerSet,
} from './messages'; } from './messages';
import type { ApiChatInviteImporter } from './misc'; import type { ApiBotVerification, ApiChatInviteImporter } from './misc';
import type { import type {
ApiEmojiStatus, ApiFakeType, ApiUser, ApiUsername, ApiEmojiStatus, ApiFakeType, ApiUser, ApiUsername,
} from './users'; } from './users';
@ -48,6 +48,7 @@ export interface ApiChat {
isForum?: boolean; isForum?: boolean;
isForumAsMessages?: true; isForumAsMessages?: true;
boostLevel?: number; boostLevel?: number;
botVerificationIconId?: string;
// Calls // Calls
isCallActive?: boolean; isCallActive?: boolean;
@ -143,6 +144,7 @@ export interface ApiChatFullInfo {
boostsApplied?: number; boostsApplied?: number;
boostsToUnrestrict?: number; boostsToUnrestrict?: number;
botVerification?: ApiBotVerification;
} }
export interface ApiChatMember { export interface ApiChatMember {

View File

@ -175,6 +175,7 @@ export type ApiChatInviteInfo = {
subscriptionFormId?: string; subscriptionFormId?: string;
canRefulfillSubscription?: boolean; canRefulfillSubscription?: boolean;
subscriptionPricing?: ApiStarsSubscriptionPricing; subscriptionPricing?: ApiStarsSubscriptionPricing;
botVerification?: ApiBotVerification;
}; };
export type ApiChatInviteImporter = { export type ApiChatInviteImporter = {
@ -321,6 +322,12 @@ export interface ApiPeerPhotos {
isLoading?: boolean; isLoading?: boolean;
} }
export interface ApiBotVerification {
botId: string;
iconId: string;
description: string;
}
export type ApiLimitType = export type ApiLimitType =
| 'uploadMaxFileparts' | 'uploadMaxFileparts'
| 'stickersFaved' | 'stickersFaved'

View File

@ -3,6 +3,7 @@ import type { ApiBotInfo } from './bots';
import type { ApiBusinessIntro, ApiBusinessLocation, ApiBusinessWorkHours } from './business'; import type { ApiBusinessIntro, ApiBusinessLocation, ApiBusinessWorkHours } from './business';
import type { ApiPeerColor } from './chats'; import type { ApiPeerColor } from './chats';
import type { ApiDocument, ApiPhoto } from './messages'; import type { ApiDocument, ApiPhoto } from './messages';
import type { ApiBotVerification } from './misc';
import type { ApiUserStarGift } from './payments'; import type { ApiUserStarGift } from './payments';
export interface ApiUser { export interface ApiUser {
@ -36,6 +37,7 @@ export interface ApiUser {
canEditBot?: boolean; canEditBot?: boolean;
hasMainMiniApp?: boolean; hasMainMiniApp?: boolean;
botActiveUsers?: number; botActiveUsers?: number;
botVerificationIconId?: string;
} }
export interface ApiUserFullInfo { export interface ApiUserFullInfo {
@ -61,6 +63,7 @@ export interface ApiUserFullInfo {
businessIntro?: ApiBusinessIntro; businessIntro?: ApiBusinessIntro;
starGiftCount?: number; starGiftCount?: number;
hasScheduledMessages?: boolean; hasScheduledMessages?: boolean;
botVerification?: ApiBotVerification;
} }
export type ApiFakeType = 'fake' | 'scam'; export type ApiFakeType = 'fake' | 'scam';

View File

@ -107,9 +107,18 @@ const FullNameTitle: FC<OwnProps> = ({
return undefined; return undefined;
}, [customPeer, isSavedDialog, isSavedMessages, lang, realPeer]); }, [customPeer, isSavedDialog, isSavedMessages, lang, realPeer]);
const botVerificationIconId = realPeer?.botVerificationIconId;
return ( return (
<div className={buildClassName('title', styles.root, className)}> <div className={buildClassName('title', styles.root, className)}>
{botVerificationIconId && (
<CustomEmoji
documentId={botVerificationIconId}
size={emojiStatusSize}
loopLimit={!noLoopLimit ? EMOJI_STATUS_LOOP_LIMIT : undefined}
observeIntersectionForLoading={observeIntersection}
/>
)}
<h3 <h3
dir="auto" dir="auto"
role="button" role="button"

View File

@ -24,11 +24,21 @@
margin-bottom: 0; margin-bottom: 0;
} }
.botVerificationSection,
.sectionInfo { .sectionInfo {
color: var(--color-text-secondary); color: var(--color-text-secondary);
font-size: 0.875rem; font-size: 0.875rem;
} }
.botVerificationSection {
padding-inline: 1.25rem;
}
.botVerificationIcon {
--custom-emoji-size: 1rem;
padding-inline-end: 0.125rem;
}
.personalChannelSubscribers { .personalChannelSubscribers {
grid-column: 2; grid-column: 2;
grid-row: 1; grid-row: 1;

View File

@ -5,6 +5,7 @@ import React, {
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { import type {
ApiBotVerification,
ApiChat, ApiCountryCode, ApiUser, ApiUserFullInfo, ApiUsername, ApiChat, ApiCountryCode, ApiUser, ApiUserFullInfo, ApiUsername,
} from '../../../api/types'; } from '../../../api/types';
import { MAIN_THREAD_ID } from '../../../api/types'; import { MAIN_THREAD_ID } from '../../../api/types';
@ -49,6 +50,7 @@ import Button from '../../ui/Button';
import ListItem from '../../ui/ListItem'; import ListItem from '../../ui/ListItem';
import Skeleton from '../../ui/placeholder/Skeleton'; import Skeleton from '../../ui/placeholder/Skeleton';
import Switcher from '../../ui/Switcher'; import Switcher from '../../ui/Switcher';
import CustomEmoji from '../CustomEmoji';
import SafeLink from '../SafeLink'; import SafeLink from '../SafeLink';
import BusinessHours from './BusinessHours'; import BusinessHours from './BusinessHours';
import UserBirthday from './UserBirthday'; import UserBirthday from './UserBirthday';
@ -75,6 +77,7 @@ type StateProps = {
hasSavedMessages?: boolean; hasSavedMessages?: boolean;
personalChannel?: ApiChat; personalChannel?: ApiChat;
hasMainMiniApp?: boolean; hasMainMiniApp?: boolean;
botVerification?: ApiBotVerification;
}; };
const DEFAULT_MAP_CONFIG = { const DEFAULT_MAP_CONFIG = {
@ -84,6 +87,7 @@ const DEFAULT_MAP_CONFIG = {
}; };
const runDebounced = debounce((cb) => cb(), 500, false); const runDebounced = debounce((cb) => cb(), 500, false);
const BOT_VERIFICATION_ICON_SIZE = 16;
const ChatExtra: FC<OwnProps & StateProps> = ({ const ChatExtra: FC<OwnProps & StateProps> = ({
chatOrUserId, chatOrUserId,
@ -101,6 +105,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
hasSavedMessages, hasSavedMessages,
personalChannel, personalChannel,
hasMainMiniApp, hasMainMiniApp,
botVerification,
}) => { }) => {
const { const {
showNotification, showNotification,
@ -437,6 +442,16 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
<span>{oldLang('SavedMessagesTab')}</span> <span>{oldLang('SavedMessagesTab')}</span>
</ListItem> </ListItem>
)} )}
{botVerification && (
<div className={styles.botVerificationSection}>
<CustomEmoji
className={styles.botVerificationIcon}
documentId={botVerification.iconId}
size={BOT_VERIFICATION_ICON_SIZE}
/>
{botVerification.description}
</div>
)}
</div> </div>
); );
}; };
@ -455,6 +470,8 @@ export default memo(withGlobal<OwnProps>(
const chatFullInfo = chat && selectChatFullInfo(global, chat.id); const chatFullInfo = chat && selectChatFullInfo(global, chat.id);
const userFullInfo = user && selectUserFullInfo(global, user.id); const userFullInfo = user && selectUserFullInfo(global, user.id);
const botVerification = userFullInfo?.botVerification || chatFullInfo?.botVerification;
const chatInviteLink = chatFullInfo?.inviteLink; const chatInviteLink = chatFullInfo?.inviteLink;
const description = userFullInfo?.bio || chatFullInfo?.about; const description = userFullInfo?.bio || chatFullInfo?.about;
@ -488,6 +505,7 @@ export default memo(withGlobal<OwnProps>(
hasSavedMessages, hasSavedMessages,
personalChannel, personalChannel,
hasMainMiniApp, hasMainMiniApp,
botVerification,
}; };
}, },
)(ChatExtra)); )(ChatExtra));

View File

@ -22,6 +22,7 @@ import { applyAnimationState, type PaneState } from './hooks/useHeaderPane';
import GroupCallTopPane from '../calls/group/GroupCallTopPane'; import GroupCallTopPane from '../calls/group/GroupCallTopPane';
import AudioPlayer from './panes/AudioPlayer'; import AudioPlayer from './panes/AudioPlayer';
import BotAdPane from './panes/BotAdPane'; import BotAdPane from './panes/BotAdPane';
import BotVerificationPane from './panes/BotVerificationPane';
import ChatReportPane from './panes/ChatReportPane'; import ChatReportPane from './panes/ChatReportPane';
import HeaderPinnedMessage from './panes/HeaderPinnedMessage'; import HeaderPinnedMessage from './panes/HeaderPinnedMessage';
@ -65,6 +66,7 @@ const MiddleHeaderPanes = ({
const [getGroupCallState, setGroupCallState] = useSignal<PaneState>(FALLBACK_PANE_STATE); const [getGroupCallState, setGroupCallState] = useSignal<PaneState>(FALLBACK_PANE_STATE);
const [getChatReportState, setChatReportState] = useSignal<PaneState>(FALLBACK_PANE_STATE); const [getChatReportState, setChatReportState] = useSignal<PaneState>(FALLBACK_PANE_STATE);
const [getBotAdState, setBotAdState] = useSignal<PaneState>(FALLBACK_PANE_STATE); const [getBotAdState, setBotAdState] = useSignal<PaneState>(FALLBACK_PANE_STATE);
const [getBotVerificationState, setBotVerificationState] = useSignal<PaneState>(FALLBACK_PANE_STATE);
const isPinnedMessagesFullWidth = isAudioPlayerRendered || !isDesktop; const isPinnedMessagesFullWidth = isAudioPlayerRendered || !isDesktop;
@ -84,13 +86,15 @@ const MiddleHeaderPanes = ({
useSignalEffect(() => { useSignalEffect(() => {
const audioPlayerState = getAudioPlayerState(); const audioPlayerState = getAudioPlayerState();
const botVerificationState = getBotVerificationState();
const pinnedState = getPinnedState(); const pinnedState = getPinnedState();
const groupCallState = getGroupCallState(); const groupCallState = getGroupCallState();
const chatReportState = getChatReportState(); const chatReportState = getChatReportState();
const botAdState = getBotAdState(); const botAdState = getBotAdState();
// Keep in sync with the order of the panes in the DOM // 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 isFirstRender = isFirstRenderRef.current;
const totalHeight = stateArray.reduce((acc, state) => acc + state.height, 0); const totalHeight = stateArray.reduce((acc, state) => acc + state.height, 0);
@ -103,7 +107,8 @@ const MiddleHeaderPanes = ({
setExtraStyles(middleColumn, { setExtraStyles(middleColumn, {
'--middle-header-panes-height': `${totalHeight}px`, '--middle-header-panes-height': `${totalHeight}px`,
}); });
}, [getAudioPlayerState, getGroupCallState, getPinnedState, getChatReportState, getBotAdState]); }, [getAudioPlayerState, getGroupCallState, getPinnedState,
getChatReportState, getBotAdState, getBotVerificationState]);
if (!shouldRender) return undefined; if (!shouldRender) return undefined;
@ -128,6 +133,10 @@ const MiddleHeaderPanes = ({
isAutoArchived={settings?.isAutoArchived} isAutoArchived={settings?.isAutoArchived}
onPaneStateChange={setChatReportState} onPaneStateChange={setChatReportState}
/> />
<BotVerificationPane
peerId={chatId}
onPaneStateChange={setBotVerificationState}
/>
<HeaderPinnedMessage <HeaderPinnedMessage
chatId={chatId} chatId={chatId}
threadId={threadId} threadId={threadId}

View File

@ -0,0 +1,18 @@
@use "../../../styles/mixins";
.root {
@include mixins.header-pane;
display: flex;
height: auto;
justify-content: center;
padding-inline: 1rem;
color: var(--color-text-secondary);
font-size: 0.875rem;
}
.icon {
--custom-emoji-size: 1rem;
padding-inline-end: 0.125rem;
}

View File

@ -0,0 +1,80 @@
import type { FC } from '../../../lib/teact/teact';
import React, { memo } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { ApiBotVerification } from '../../../api/types';
import {
selectPeerFullInfo,
} from '../../../global/selectors';
import useTimeout from '../../../hooks/schedulers/useTimeout';
import useLastCallback from '../../../hooks/useLastCallback';
import useHeaderPane, { type PaneState } from '../hooks/useHeaderPane';
import CustomEmoji from '../../common/CustomEmoji';
import styles from './BotVerificationPane.module.scss';
type OwnProps = {
peerId: string;
onPaneStateChange?: (state: PaneState) => void;
};
type StateProps = {
wasShown: boolean;
botVerification?: ApiBotVerification;
};
const BOT_VERIFICATION_ICON_SIZE = 16;
const DISPLAY_DURATION_MS = 5000; // 5 sec
const BotVerificationPane: FC<OwnProps & StateProps> = ({
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 (
<div ref={ref} className={styles.root}>
<span className={styles.icon}>
<CustomEmoji
documentId={botVerification.iconId}
size={BOT_VERIFICATION_ICON_SIZE}
/>
</span>
{botVerification.description}
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global, { peerId }): StateProps => {
const peerFullInfo = selectPeerFullInfo(global, peerId);
const botVerification = peerFullInfo?.botVerification;
const wasShown = global.settings.botVerificationShownPeerIds.includes(peerId);
return {
botVerification,
wasShown,
};
},
)(BotVerificationPane));

View File

@ -21,6 +21,7 @@ export const DEBUG = process.env.APP_ENV !== 'production';
export const DEBUG_MORE = false; export const DEBUG_MORE = false;
export const DEBUG_LOG_FILENAME = 'tt-log.json'; export const DEBUG_LOG_FILENAME = 'tt-log.json';
export const STRICTERDOM_ENABLED = DEBUG; 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 BETA_CHANGELOG_URL = 'https://telegra.ph/WebA-Beta-03-20';
export const ELECTRON_HOST_URL = process.env.ELECTRON_HOST_URL!; export const ELECTRON_HOST_URL = process.env.ELECTRON_HOST_URL!;

View File

@ -2,6 +2,7 @@ import type { ApiUser } from '../../../api/types';
import type { ActionReturnType } from '../../types'; import type { ActionReturnType } from '../../types';
import { ManagementProgress } from '../../../types'; import { ManagementProgress } from '../../../types';
import { BOT_VERIFICATION_PEERS_LIMIT } from '../../../config';
import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { buildCollectionByKey, unique } from '../../../util/iteratees'; import { buildCollectionByKey, unique } from '../../../util/iteratees';
import * as langProvider from '../../../util/oldLangProvider'; import * as langProvider from '../../../util/oldLangProvider';
@ -9,11 +10,7 @@ import { throttle } from '../../../util/schedulers';
import { getServerTime } from '../../../util/serverTime'; import { getServerTime } from '../../../util/serverTime';
import { callApi } from '../../../api/gramjs'; import { callApi } from '../../../api/gramjs';
import { isUserBot, isUserId } from '../../helpers'; import { isUserBot, isUserId } from '../../helpers';
import { import { addActionHandler, getGlobal, setGlobal } from '../../index';
addActionHandler,
getGlobal,
setGlobal,
} from '../../index';
import { import {
addUserStatuses, addUserStatuses,
closeNewContactDialog, closeNewContactDialog,
@ -508,3 +505,20 @@ addActionHandler('openSuggestedStatusModal', async (global, actions, payload): P
}, tabId); }, tabId);
setGlobal(global); 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);
});

View File

@ -260,6 +260,9 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
if (!cached.messages.pollById) { if (!cached.messages.pollById) {
cached.messages.pollById = initialState.messages.pollById; cached.messages.pollById = initialState.messages.pollById;
} }
if (!cached.settings.botVerificationShownPeerIds) {
cached.settings.botVerificationShownPeerIds = initialState.settings.botVerificationShownPeerIds;
}
} }
function updateCache(force?: boolean) { function updateCache(force?: boolean) {
@ -610,7 +613,9 @@ function omitLocalMedia(message: ApiMessage): ApiMessage {
} }
function reduceSettings<T extends GlobalState>(global: T): GlobalState['settings'] { function reduceSettings<T extends GlobalState>(global: T): GlobalState['settings'] {
const { byKey, themes, performance } = global.settings; const {
byKey, themes, performance, botVerificationShownPeerIds,
} = global.settings;
return { return {
byKey, byKey,
@ -618,6 +623,7 @@ function reduceSettings<T extends GlobalState>(global: T): GlobalState['settings
performance, performance,
privacy: {}, privacy: {},
notifyExceptions: {}, notifyExceptions: {},
botVerificationShownPeerIds,
}; };
} }

View File

@ -296,6 +296,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
performance: INITIAL_PERFORMANCE_STATE_MAX, performance: INITIAL_PERFORMANCE_STATE_MAX,
privacy: {}, privacy: {},
notifyExceptions: {}, notifyExceptions: {},
botVerificationShownPeerIds: [],
}, },
serviceNotifications: [], serviceNotifications: [],

View File

@ -269,6 +269,9 @@ export interface ActionPayloads {
saveCloseFriends: { saveCloseFriends: {
userIds: string[]; userIds: string[];
}; };
markBotVerificationInfoShown: {
peerId: string;
};
// Message search // Message search
openMiddleSearch: WithTabId | undefined; openMiddleSearch: WithTabId | undefined;

View File

@ -401,6 +401,7 @@ export type GlobalState = {
lastPremiumBandwithNotificationDate?: number; lastPremiumBandwithNotificationDate?: number;
paidReactionPrivacy?: boolean; paidReactionPrivacy?: boolean;
languages?: ApiLanguage[]; languages?: ApiLanguage[];
botVerificationShownPeerIds: string[];
}; };
push?: { push?: {

View File

@ -10,7 +10,8 @@ import useResizeObserver from '../useResizeObserver';
const TRANSITION_PROPERTY = 'color'; const TRANSITION_PROPERTY = 'color';
const TRANSITION_STYLE = `50ms ${TRANSITION_PROPERTY} linear`; const TRANSITION_STYLE = `50ms ${TRANSITION_PROPERTY} linear`;
export default function useDynamicColorListener(ref: React.RefObject<HTMLElement>, isDisabled?: boolean) { export default function useDynamicColorListener(ref: React.RefObject<HTMLElement>,
isDisabled?: boolean) {
const [hexColor, setHexColor] = useState<string | undefined>(); const [hexColor, setHexColor] = useState<string | undefined>();
const updateColor = useLastCallback(() => { const updateColor = useLastCallback(() => {