Composer: Support guest bot mentions (#6961)
This commit is contained in:
parent
37c3156493
commit
fb5a7cfb5a
@ -211,7 +211,7 @@ export function buildApiUrlAuthResult(result: GramJs.TypeUrlAuthResult): ApiUrlA
|
|||||||
export function buildApiConfig(config: GramJs.Config): ApiConfig {
|
export function buildApiConfig(config: GramJs.Config): ApiConfig {
|
||||||
const {
|
const {
|
||||||
testMode, expires, gifSearchUsername, chatSizeMax, autologinToken, reactionsDefault,
|
testMode, expires, gifSearchUsername, chatSizeMax, autologinToken, reactionsDefault,
|
||||||
messageLengthMax, editTimeLimit, forwardedCountMax,
|
messageLengthMax, editTimeLimit, forwardedCountMax, ratingEDecay,
|
||||||
} = config;
|
} = config;
|
||||||
const defaultReaction = reactionsDefault && buildApiReaction(reactionsDefault);
|
const defaultReaction = reactionsDefault && buildApiReaction(reactionsDefault);
|
||||||
return {
|
return {
|
||||||
@ -224,6 +224,7 @@ export function buildApiConfig(config: GramJs.Config): ApiConfig {
|
|||||||
maxMessageLength: messageLengthMax,
|
maxMessageLength: messageLengthMax,
|
||||||
editTimeLimit,
|
editTimeLimit,
|
||||||
maxForwardedCount: forwardedCountMax,
|
maxForwardedCount: forwardedCountMax,
|
||||||
|
ratingEDecay,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,6 @@ import {
|
|||||||
buildBotSwitchPm,
|
buildBotSwitchPm,
|
||||||
buildBotSwitchWebview,
|
buildBotSwitchWebview,
|
||||||
} from '../apiBuilders/bots';
|
} from '../apiBuilders/bots';
|
||||||
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
|
||||||
import { omitVirtualClassFields } from '../apiBuilders/helpers';
|
import { omitVirtualClassFields } from '../apiBuilders/helpers';
|
||||||
import { buildMessageMediaContent } from '../apiBuilders/messageContent';
|
import { buildMessageMediaContent } from '../apiBuilders/messageContent';
|
||||||
import { buildApiUrlAuthResult } from '../apiBuilders/misc';
|
import { buildApiUrlAuthResult } from '../apiBuilders/misc';
|
||||||
@ -39,7 +38,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
addDocumentToLocalDb,
|
addDocumentToLocalDb,
|
||||||
addPhotoToLocalDb,
|
addPhotoToLocalDb,
|
||||||
addUserToLocalDb,
|
|
||||||
addWebDocumentToLocalDb,
|
addWebDocumentToLocalDb,
|
||||||
} from '../helpers/localDb';
|
} from '../helpers/localDb';
|
||||||
import { deserializeBytes } from '../helpers/misc';
|
import { deserializeBytes } from '../helpers/misc';
|
||||||
@ -61,68 +59,6 @@ export async function answerCallbackButton({
|
|||||||
return result ? omitVirtualClassFields(result) : undefined;
|
return result ? omitVirtualClassFields(result) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchTopInlineBots() {
|
|
||||||
const topPeers = await invokeRequest(new GramJs.contacts.GetTopPeers({
|
|
||||||
botsInline: true,
|
|
||||||
limit: DEFAULT_PRIMITIVES.INT,
|
|
||||||
offset: DEFAULT_PRIMITIVES.INT,
|
|
||||||
hash: DEFAULT_PRIMITIVES.BIGINT,
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (!(topPeers instanceof GramJs.contacts.TopPeers)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const users = topPeers.users.map(buildApiUser).filter(Boolean);
|
|
||||||
const ids = users.map(({ id }) => id);
|
|
||||||
|
|
||||||
return {
|
|
||||||
ids,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchTopBotApps() {
|
|
||||||
const topPeers = await invokeRequest(new GramJs.contacts.GetTopPeers({
|
|
||||||
botsApp: true,
|
|
||||||
limit: DEFAULT_PRIMITIVES.INT,
|
|
||||||
offset: DEFAULT_PRIMITIVES.INT,
|
|
||||||
hash: DEFAULT_PRIMITIVES.BIGINT,
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (!(topPeers instanceof GramJs.contacts.TopPeers)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const users = topPeers.users.map(buildApiUser).filter(Boolean);
|
|
||||||
const ids = users.map(({ id }) => id);
|
|
||||||
|
|
||||||
return {
|
|
||||||
ids,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchInlineBot({ username }: { username: string }) {
|
|
||||||
const resolvedPeer = await invokeRequest(new GramJs.contacts.ResolveUsername({ username }));
|
|
||||||
|
|
||||||
if (
|
|
||||||
!resolvedPeer
|
|
||||||
|| !(
|
|
||||||
resolvedPeer.users[0] instanceof GramJs.User
|
|
||||||
&& resolvedPeer.users[0].bot
|
|
||||||
&& resolvedPeer.users[0].botInlinePlaceholder
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
addUserToLocalDb(resolvedPeer.users[0]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
user: buildApiUser(resolvedPeer.users[0]),
|
|
||||||
chat: buildApiChatFromPreview(resolvedPeer.users[0]),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchInlineBotResults({
|
export async function fetchInlineBotResults({
|
||||||
bot, chat, query, offset = DEFAULT_PRIMITIVES.STRING,
|
bot, chat, query, offset = DEFAULT_PRIMITIVES.STRING,
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@ -21,6 +21,8 @@ export * from './messages';
|
|||||||
|
|
||||||
export * from './users';
|
export * from './users';
|
||||||
|
|
||||||
|
export * from './topPeers';
|
||||||
|
|
||||||
export * from './symbols';
|
export * from './symbols';
|
||||||
|
|
||||||
export * from './management';
|
export * from './management';
|
||||||
|
|||||||
103
src/api/gramjs/methods/topPeers.ts
Normal file
103
src/api/gramjs/methods/topPeers.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ApiPeer,
|
||||||
|
ApiTopPeer,
|
||||||
|
ApiTopPeerCategory,
|
||||||
|
ApiTopPeersResult,
|
||||||
|
} from '../../types';
|
||||||
|
|
||||||
|
import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
|
||||||
|
import { buildInputPeer, DEFAULT_PRIMITIVES } from '../gramjsBuilders';
|
||||||
|
import { addChatToLocalDb, addUserToLocalDb } from '../helpers/localDb';
|
||||||
|
import { invokeRequest } from './client';
|
||||||
|
|
||||||
|
const TOP_PEER_LIMIT = 50;
|
||||||
|
|
||||||
|
export async function fetchTopPeers({
|
||||||
|
category,
|
||||||
|
}: {
|
||||||
|
category: ApiTopPeerCategory;
|
||||||
|
}): Promise<ApiTopPeersResult | undefined> {
|
||||||
|
const result = await invokeRequest(new GramJs.contacts.GetTopPeers({
|
||||||
|
correspondents: category === 'correspondents' || undefined,
|
||||||
|
botsInline: category === 'botsInline' || undefined,
|
||||||
|
botsApp: category === 'botsApp' || undefined,
|
||||||
|
botsGuestchat: category === 'botsGuestChat' || undefined,
|
||||||
|
offset: DEFAULT_PRIMITIVES.INT,
|
||||||
|
limit: TOP_PEER_LIMIT,
|
||||||
|
hash: DEFAULT_PRIMITIVES.BIGINT,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (result instanceof GramJs.contacts.TopPeersNotModified) {
|
||||||
|
return { type: 'unchanged' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result instanceof GramJs.contacts.TopPeersDisabled) {
|
||||||
|
return { type: 'disabled' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(result instanceof GramJs.contacts.TopPeers)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.users.forEach(addUserToLocalDb);
|
||||||
|
result.chats.forEach((chat) => {
|
||||||
|
if (chat instanceof GramJs.Chat || chat instanceof GramJs.Channel) {
|
||||||
|
addChatToLocalDb(chat);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const topPeerCategory = result.categories.find(({ category: mtpCategory }) => {
|
||||||
|
return getTopPeerCategory(mtpCategory) === category;
|
||||||
|
});
|
||||||
|
|
||||||
|
const topPeers: ApiTopPeer[] = topPeerCategory
|
||||||
|
? topPeerCategory.peers.map(({ peer, rating }) => ({
|
||||||
|
peerId: getApiChatIdFromMtpPeer(peer),
|
||||||
|
rating,
|
||||||
|
})) : [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'topPeers',
|
||||||
|
category,
|
||||||
|
topPeers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetTopPeerRating({ category, peer }: { category: ApiTopPeerCategory; peer: ApiPeer }) {
|
||||||
|
return invokeRequest(new GramJs.contacts.ResetTopPeerRating({
|
||||||
|
category: buildTopPeerCategory(category),
|
||||||
|
peer: buildInputPeer(peer.id, peer.accessHash),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTopPeerCategory(category: GramJs.TypeTopPeerCategory): ApiTopPeerCategory | undefined {
|
||||||
|
if (category instanceof GramJs.TopPeerCategoryCorrespondents) {
|
||||||
|
return 'correspondents';
|
||||||
|
}
|
||||||
|
if (category instanceof GramJs.TopPeerCategoryBotsInline) {
|
||||||
|
return 'botsInline';
|
||||||
|
}
|
||||||
|
if (category instanceof GramJs.TopPeerCategoryBotsApp) {
|
||||||
|
return 'botsApp';
|
||||||
|
}
|
||||||
|
if (category instanceof GramJs.TopPeerCategoryBotsGuestChat) {
|
||||||
|
return 'botsGuestChat';
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTopPeerCategory(category: ApiTopPeerCategory): GramJs.TypeTopPeerCategory {
|
||||||
|
switch (category) {
|
||||||
|
case 'correspondents':
|
||||||
|
return new GramJs.TopPeerCategoryCorrespondents();
|
||||||
|
case 'botsInline':
|
||||||
|
return new GramJs.TopPeerCategoryBotsInline();
|
||||||
|
case 'botsApp':
|
||||||
|
return new GramJs.TopPeerCategoryBotsApp();
|
||||||
|
case 'botsGuestChat':
|
||||||
|
return new GramJs.TopPeerCategoryBotsGuestChat();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -132,25 +132,6 @@ export async function fetchNearestCountry() {
|
|||||||
return dcInfo?.country;
|
return dcInfo?.country;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchTopUsers() {
|
|
||||||
const topPeers = await invokeRequest(new GramJs.contacts.GetTopPeers({
|
|
||||||
correspondents: true,
|
|
||||||
offset: DEFAULT_PRIMITIVES.INT,
|
|
||||||
limit: DEFAULT_PRIMITIVES.INT,
|
|
||||||
hash: DEFAULT_PRIMITIVES.BIGINT,
|
|
||||||
}));
|
|
||||||
if (!(topPeers instanceof GramJs.contacts.TopPeers)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const users = topPeers.users.map(buildApiUser).filter((user): user is ApiUser => Boolean(user) && !user.isSelf);
|
|
||||||
const ids = users.map(({ id }) => id);
|
|
||||||
|
|
||||||
return {
|
|
||||||
ids,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchContactList() {
|
export async function fetchContactList() {
|
||||||
const result = await invokeRequest(new GramJs.contacts.GetContacts({ hash: DEFAULT_PRIMITIVES.BIGINT }));
|
const result = await invokeRequest(new GramJs.contacts.GetContacts({ hash: DEFAULT_PRIMITIVES.BIGINT }));
|
||||||
if (!result || result instanceof GramJs.contacts.ContactsNotModified) {
|
if (!result || result instanceof GramJs.contacts.ContactsNotModified) {
|
||||||
|
|||||||
@ -351,8 +351,26 @@ export interface ApiConfig {
|
|||||||
maxMessageLength: number;
|
maxMessageLength: number;
|
||||||
editTimeLimit: number;
|
editTimeLimit: number;
|
||||||
maxForwardedCount: number;
|
maxForwardedCount: number;
|
||||||
|
ratingEDecay: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ApiTopPeerCategory = 'correspondents' | 'botsInline' | 'botsApp' | 'botsGuestChat';
|
||||||
|
|
||||||
|
export type ApiTopPeer = {
|
||||||
|
peerId: string;
|
||||||
|
rating: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiTopPeersResult = {
|
||||||
|
type: 'topPeers';
|
||||||
|
category: ApiTopPeerCategory;
|
||||||
|
topPeers: ApiTopPeer[];
|
||||||
|
} | {
|
||||||
|
type: 'unchanged';
|
||||||
|
} | {
|
||||||
|
type: 'disabled';
|
||||||
|
};
|
||||||
|
|
||||||
export interface ApiPromoData {
|
export interface ApiPromoData {
|
||||||
expires: number;
|
expires: number;
|
||||||
pendingSuggestions: string[];
|
pendingSuggestions: string[];
|
||||||
|
|||||||
@ -67,6 +67,7 @@ import {
|
|||||||
isChatSuperGroup,
|
isChatSuperGroup,
|
||||||
isSameReaction,
|
isSameReaction,
|
||||||
isSystemBot,
|
isSystemBot,
|
||||||
|
isUserRightBanned,
|
||||||
} from '../../global/helpers';
|
} from '../../global/helpers';
|
||||||
import { getChatNotifySettings } from '../../global/helpers/notifications';
|
import { getChatNotifySettings } from '../../global/helpers/notifications';
|
||||||
import { getPeerTitle } from '../../global/helpers/peers';
|
import { getPeerTitle } from '../../global/helpers/peers';
|
||||||
@ -269,6 +270,7 @@ type StateProps = {
|
|||||||
baseEmojiKeywords?: Record<string, string[]>;
|
baseEmojiKeywords?: Record<string, string[]>;
|
||||||
emojiKeywords?: Record<string, string[]>;
|
emojiKeywords?: Record<string, string[]>;
|
||||||
topInlineBotIds?: string[];
|
topInlineBotIds?: string[];
|
||||||
|
topGuestBotIds?: string[];
|
||||||
isInlineBotLoading: boolean;
|
isInlineBotLoading: boolean;
|
||||||
inlineBots?: Record<string, false | InlineBotSettings>;
|
inlineBots?: Record<string, false | InlineBotSettings>;
|
||||||
botCommands?: ApiBotCommand[] | false;
|
botCommands?: ApiBotCommand[] | false;
|
||||||
@ -387,6 +389,7 @@ const Composer = ({
|
|||||||
stickersForEmoji,
|
stickersForEmoji,
|
||||||
customEmojiForEmoji,
|
customEmojiForEmoji,
|
||||||
topInlineBotIds,
|
topInlineBotIds,
|
||||||
|
topGuestBotIds,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
currentUser,
|
currentUser,
|
||||||
captionLimit,
|
captionLimit,
|
||||||
@ -591,6 +594,7 @@ const Composer = ({
|
|||||||
),
|
),
|
||||||
[chat, chatFullInfo, isChatWithBot, isChatWithSelf, isInStoryViewer, paidMessagesStars, isInScheduledList],
|
[chat, chatFullInfo, isChatWithBot, isChatWithSelf, isInStoryViewer, paidMessagesStars, isInScheduledList],
|
||||||
);
|
);
|
||||||
|
const canUseInlineBots = !chat || isChatAdmin(chat) || !isUserRightBanned(chat, 'sendInline', chatFullInfo);
|
||||||
|
|
||||||
const isNeedPremium = isContactRequirePremium && isInStoryViewer;
|
const isNeedPremium = isContactRequirePremium && isInStoryViewer;
|
||||||
const isSendTextBlocked = isNeedPremium || !canSendPlainText;
|
const isSendTextBlocked = isNeedPremium || !canSendPlainText;
|
||||||
@ -844,7 +848,8 @@ const Composer = ({
|
|||||||
getSelectionRange,
|
getSelectionRange,
|
||||||
inputRef,
|
inputRef,
|
||||||
groupChatMembers,
|
groupChatMembers,
|
||||||
topInlineBotIds,
|
canUseInlineBots ? topInlineBotIds : undefined,
|
||||||
|
topGuestBotIds,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -887,7 +892,7 @@ const Composer = ({
|
|||||||
help: inlineBotHelp,
|
help: inlineBotHelp,
|
||||||
loadMore: loadMoreForInlineBot,
|
loadMore: loadMoreForInlineBot,
|
||||||
} = useInlineBotTooltip(
|
} = useInlineBotTooltip(
|
||||||
Boolean(isInMessageList && isReady && isForCurrentMessageList && !hasAttachments),
|
Boolean(canUseInlineBots && isInMessageList && isReady && isForCurrentMessageList && !hasAttachments),
|
||||||
chatId,
|
chatId,
|
||||||
getHtml,
|
getHtml,
|
||||||
inlineBots,
|
inlineBots,
|
||||||
@ -1595,7 +1600,7 @@ const Composer = ({
|
|||||||
const handleInlineBotSelect = useLastCallback((
|
const handleInlineBotSelect = useLastCallback((
|
||||||
inlineResult: ApiBotInlineResult | ApiBotInlineMediaResult, isSilent?: boolean, isScheduleRequested?: boolean,
|
inlineResult: ApiBotInlineResult | ApiBotInlineMediaResult, isSilent?: boolean, isScheduleRequested?: boolean,
|
||||||
) => {
|
) => {
|
||||||
if (!currentMessageList && !storyId) {
|
if ((!currentMessageList && !storyId) || !inlineBotId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2786,7 +2791,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
stickersForEmoji: global.stickers.forEmoji.stickers,
|
stickersForEmoji: global.stickers.forEmoji.stickers,
|
||||||
customEmojiForEmoji: global.customEmojis.forEmoji.stickers,
|
customEmojiForEmoji: global.customEmojis.forEmoji.stickers,
|
||||||
chatFullInfo,
|
chatFullInfo,
|
||||||
topInlineBotIds: global.topInlineBots?.userIds,
|
topInlineBotIds: global.topPeerCategories.botsInline?.peerIds,
|
||||||
|
topGuestBotIds: global.topPeerCategories.botsGuestChat?.peerIds,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
currentUser,
|
currentUser,
|
||||||
contentToBeScheduled: tabState.contentToBeScheduled,
|
contentToBeScheduled: tabState.contentToBeScheduled,
|
||||||
|
|||||||
@ -152,6 +152,6 @@ export default memo(withGlobal<OwnProps>((global): Complete<StateProps> => {
|
|||||||
return {
|
return {
|
||||||
isLoading: !foundIds && globalSearch.fetchingStatus?.botApps,
|
isLoading: !foundIds && globalSearch.fetchingStatus?.botApps,
|
||||||
foundIds,
|
foundIds,
|
||||||
recentBotIds: global.topBotApps.userIds,
|
recentBotIds: global.topPeerCategories.botsApp?.peerIds,
|
||||||
};
|
};
|
||||||
})(BotAppResults));
|
})(BotAppResults));
|
||||||
|
|||||||
@ -1,19 +1,20 @@
|
|||||||
import type { FC } from '../../../lib/teact/teact';
|
|
||||||
import {
|
import {
|
||||||
memo,
|
memo,
|
||||||
useCallback, useEffect, useRef,
|
useCallback, useEffect, useRef,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { ApiUser } from '../../../api/types';
|
import type { GlobalState } from '../../../global/types';
|
||||||
|
|
||||||
import { getUserFirstOrLastName } from '../../../global/helpers';
|
import { getPeerTitle } from '../../../global/helpers/peers';
|
||||||
|
import { selectPeer } from '../../../global/selectors';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { throttle } from '../../../util/schedulers';
|
import { throttle } from '../../../util/schedulers';
|
||||||
import renderText from '../../common/helpers/renderText';
|
import renderText from '../../common/helpers/renderText';
|
||||||
|
|
||||||
|
import { useShallowSelector } from '../../../hooks/data/useSelector';
|
||||||
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
||||||
import useOldLang from '../../../hooks/useOldLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
|
|
||||||
import Avatar from '../../common/Avatar';
|
import Avatar from '../../common/Avatar';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
@ -26,8 +27,7 @@ type OwnProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
topUserIds?: string[];
|
topPeerIds?: string[];
|
||||||
usersById: Record<string, ApiUser>;
|
|
||||||
recentlyFoundChatIds?: string[];
|
recentlyFoundChatIds?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -36,28 +36,33 @@ const NBSP = '\u00A0';
|
|||||||
|
|
||||||
const runThrottled = throttle((cb) => cb(), 60000, true);
|
const runThrottled = throttle((cb) => cb(), 60000, true);
|
||||||
|
|
||||||
const RecentContacts: FC<OwnProps & StateProps> = ({
|
const RecentContacts = ({
|
||||||
topUserIds,
|
topPeerIds,
|
||||||
usersById,
|
|
||||||
recentlyFoundChatIds,
|
recentlyFoundChatIds,
|
||||||
onReset,
|
onReset,
|
||||||
}) => {
|
}: OwnProps & StateProps) => {
|
||||||
const {
|
const {
|
||||||
loadTopUsers, openChat,
|
loadTopPeers, openChat,
|
||||||
addRecentlyFoundChatId, clearRecentlyFoundChats,
|
addRecentlyFoundChatId, clearRecentlyFoundChats,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const topUsersRef = useRef<HTMLDivElement>();
|
const topPeersRef = useRef<HTMLDivElement>();
|
||||||
|
|
||||||
// Due to the parent Transition, this component never gets unmounted,
|
// Due to the parent Transition, this component never gets unmounted,
|
||||||
// that's why we use throttled API call on every update.
|
// that's why we use throttled API call on every update.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
runThrottled(() => {
|
runThrottled(() => {
|
||||||
loadTopUsers();
|
loadTopPeers({ category: 'correspondents' });
|
||||||
});
|
});
|
||||||
}, [loadTopUsers]);
|
}, [loadTopPeers]);
|
||||||
|
|
||||||
useHorizontalScroll(topUsersRef, !topUserIds);
|
const topPeersSelector = useCallback((global: GlobalState) => {
|
||||||
|
return topPeerIds?.map((peerId) => selectPeer(global, peerId)).filter(Boolean);
|
||||||
|
}, [topPeerIds]);
|
||||||
|
const topPeers = useShallowSelector(topPeersSelector);
|
||||||
|
const shouldRenderTopPeers = Boolean(topPeers?.length);
|
||||||
|
|
||||||
|
useHorizontalScroll(topPeersRef, !shouldRenderTopPeers);
|
||||||
|
|
||||||
const handleClick = useCallback((id: string) => {
|
const handleClick = useCallback((id: string) => {
|
||||||
openChat({ id, shouldReplaceHistory: true });
|
openChat({ id, shouldReplaceHistory: true });
|
||||||
@ -71,22 +76,22 @@ const RecentContacts: FC<OwnProps & StateProps> = ({
|
|||||||
clearRecentlyFoundChats();
|
clearRecentlyFoundChats();
|
||||||
}, [clearRecentlyFoundChats]);
|
}, [clearRecentlyFoundChats]);
|
||||||
|
|
||||||
const lang = useOldLang();
|
const lang = useLang();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="RecentContacts custom-scroll">
|
<div className="RecentContacts custom-scroll">
|
||||||
{topUserIds && (
|
{shouldRenderTopPeers && (
|
||||||
<div className="top-peers-section" dir={lang.isRtl ? 'rtl' : undefined}>
|
<div className="top-peers-section" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
<div ref={topUsersRef} className="top-peers">
|
<div ref={topPeersRef} className="top-peers">
|
||||||
{topUserIds.map((userId) => (
|
{topPeers?.map((peer) => (
|
||||||
<div
|
<div
|
||||||
key={userId}
|
key={peer.id}
|
||||||
className="top-peer-item"
|
className="top-peer-item"
|
||||||
onClick={() => handleClick(userId)}
|
onClick={() => handleClick(peer.id)}
|
||||||
dir={lang.isRtl ? 'rtl' : undefined}
|
dir={lang.isRtl ? 'rtl' : undefined}
|
||||||
>
|
>
|
||||||
<Avatar peer={usersById[userId]} />
|
<Avatar peer={peer} />
|
||||||
<div className="top-peer-name">{renderText(getUserFirstOrLastName(usersById[userId]) || NBSP)}</div>
|
<div className="top-peer-name">{renderText(getPeerTitle(lang, peer) || NBSP)}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -97,7 +102,7 @@ const RecentContacts: FC<OwnProps & StateProps> = ({
|
|||||||
<h3
|
<h3
|
||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
'section-heading mt-0 recent-chats-header',
|
'section-heading mt-0 recent-chats-header',
|
||||||
!topUserIds && 'without-border',
|
!shouldRenderTopPeers && 'without-border',
|
||||||
)}
|
)}
|
||||||
dir={lang.isRtl ? 'rtl' : undefined}
|
dir={lang.isRtl ? 'rtl' : undefined}
|
||||||
>
|
>
|
||||||
@ -129,13 +134,11 @@ const RecentContacts: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global): Complete<StateProps> => {
|
(global): Complete<StateProps> => {
|
||||||
const { userIds: topUserIds } = global.topPeers;
|
const topPeerIds = global.topPeerCategories.correspondents?.peerIds;
|
||||||
const usersById = global.users.byId;
|
|
||||||
const { recentlyFoundChatIds } = global;
|
const { recentlyFoundChatIds } = global;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
topUserIds,
|
topPeerIds,
|
||||||
usersById,
|
|
||||||
recentlyFoundChatIds,
|
recentlyFoundChatIds,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -213,7 +213,7 @@ const Main = ({
|
|||||||
loadNotificationExceptions,
|
loadNotificationExceptions,
|
||||||
updateIsOnline,
|
updateIsOnline,
|
||||||
onTabFocusChange,
|
onTabFocusChange,
|
||||||
loadTopInlineBots,
|
loadTopPeers,
|
||||||
loadEmojiKeywords,
|
loadEmojiKeywords,
|
||||||
loadCountryList,
|
loadCountryList,
|
||||||
loadAvailableReactions,
|
loadAvailableReactions,
|
||||||
@ -257,7 +257,6 @@ const Main = ({
|
|||||||
loadQuickReplies,
|
loadQuickReplies,
|
||||||
loadStarStatus,
|
loadStarStatus,
|
||||||
loadAvailableEffects,
|
loadAvailableEffects,
|
||||||
loadTopBotApps,
|
|
||||||
loadPaidReactionPrivacy,
|
loadPaidReactionPrivacy,
|
||||||
loadPasswordInfo,
|
loadPasswordInfo,
|
||||||
loadBotFreezeAppeal,
|
loadBotFreezeAppeal,
|
||||||
@ -327,13 +326,14 @@ const Main = ({
|
|||||||
loadAttachBots();
|
loadAttachBots();
|
||||||
loadNotificationSettings();
|
loadNotificationSettings();
|
||||||
loadNotificationExceptions();
|
loadNotificationExceptions();
|
||||||
loadTopInlineBots();
|
loadTopPeers({ category: 'botsInline' });
|
||||||
loadTopReactions();
|
loadTopReactions();
|
||||||
loadStarStatus();
|
loadStarStatus();
|
||||||
loadEmojiKeywords({ language: BASE_EMOJI_KEYWORD_LANG });
|
loadEmojiKeywords({ language: BASE_EMOJI_KEYWORD_LANG });
|
||||||
loadFeaturedEmojiStickers();
|
loadFeaturedEmojiStickers();
|
||||||
loadSavedReactionTags();
|
loadSavedReactionTags();
|
||||||
loadTopBotApps();
|
loadTopPeers({ category: 'botsApp' });
|
||||||
|
loadTopPeers({ category: 'botsGuestChat' });
|
||||||
loadPaidReactionPrivacy();
|
loadPaidReactionPrivacy();
|
||||||
loadDefaultTopicIcons();
|
loadDefaultTopicIcons();
|
||||||
loadAnimatedEmojis();
|
loadAnimatedEmojis();
|
||||||
|
|||||||
@ -103,6 +103,7 @@ type StateProps = {
|
|||||||
emojiKeywords?: Record<string, string[]>;
|
emojiKeywords?: Record<string, string[]>;
|
||||||
shouldSuggestCustomEmoji?: boolean;
|
shouldSuggestCustomEmoji?: boolean;
|
||||||
customEmojiForEmoji?: ApiSticker[];
|
customEmojiForEmoji?: ApiSticker[];
|
||||||
|
topGuestBotIds?: string[];
|
||||||
captionLimit: number;
|
captionLimit: number;
|
||||||
attachmentSettings: GlobalState['attachmentSettings'];
|
attachmentSettings: GlobalState['attachmentSettings'];
|
||||||
shouldSaveAttachmentsCompression?: boolean;
|
shouldSaveAttachmentsCompression?: boolean;
|
||||||
@ -127,6 +128,7 @@ const AttachmentModal = ({
|
|||||||
isChatWithSelf,
|
isChatWithSelf,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
groupChatMembers,
|
groupChatMembers,
|
||||||
|
topGuestBotIds,
|
||||||
recentEmojis,
|
recentEmojis,
|
||||||
baseEmojiKeywords,
|
baseEmojiKeywords,
|
||||||
emojiKeywords,
|
emojiKeywords,
|
||||||
@ -283,6 +285,7 @@ const AttachmentModal = ({
|
|||||||
inputRef,
|
inputRef,
|
||||||
groupChatMembers,
|
groupChatMembers,
|
||||||
undefined,
|
undefined,
|
||||||
|
topGuestBotIds,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -944,6 +947,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
isChatWithSelf,
|
isChatWithSelf,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
groupChatMembers: chatFullInfo?.members,
|
groupChatMembers: chatFullInfo?.members,
|
||||||
|
topGuestBotIds: global.topPeerCategories.botsGuestChat?.peerIds,
|
||||||
recentEmojis,
|
recentEmojis,
|
||||||
baseEmojiKeywords: baseEmojiKeywords?.keywords,
|
baseEmojiKeywords: baseEmojiKeywords?.keywords,
|
||||||
emojiKeywords: emojiKeywords?.keywords,
|
emojiKeywords: emojiKeywords?.keywords,
|
||||||
|
|||||||
@ -39,6 +39,7 @@ export default function useMentionTooltip(
|
|||||||
inputRef: ElementRef<HTMLDivElement>,
|
inputRef: ElementRef<HTMLDivElement>,
|
||||||
groupChatMembers?: ApiChatMember[],
|
groupChatMembers?: ApiChatMember[],
|
||||||
topInlineBotIds?: string[],
|
topInlineBotIds?: string[],
|
||||||
|
topGuestBotIds?: string[],
|
||||||
currentUserId?: string,
|
currentUserId?: string,
|
||||||
) {
|
) {
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
@ -65,7 +66,7 @@ export default function useMentionTooltip(
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const usernameTag = getUsernameTag();
|
const usernameTag = getUsernameTag();
|
||||||
|
|
||||||
if (!usernameTag || !(groupChatMembers || topInlineBotIds)) {
|
if (!usernameTag || !(groupChatMembers || topInlineBotIds || topGuestBotIds)) {
|
||||||
setFilteredUsers(undefined);
|
setFilteredUsers(undefined);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -90,13 +91,14 @@ export default function useMentionTooltip(
|
|||||||
ids: unique([
|
ids: unique([
|
||||||
...((getWithInlineBots() && topInlineBotIds) || []),
|
...((getWithInlineBots() && topInlineBotIds) || []),
|
||||||
...(memberIds || []),
|
...(memberIds || []),
|
||||||
|
...(topGuestBotIds || []),
|
||||||
]),
|
]),
|
||||||
query: filter,
|
query: filter,
|
||||||
type: 'user',
|
type: 'user',
|
||||||
});
|
});
|
||||||
|
|
||||||
setFilteredUsers(Object.values(pickTruthy(usersById, filteredIds)));
|
setFilteredUsers(Object.values(pickTruthy(usersById, filteredIds)));
|
||||||
}, [currentUserId, groupChatMembers, topInlineBotIds, getUsernameTag, getWithInlineBots]);
|
}, [currentUserId, groupChatMembers, topInlineBotIds, topGuestBotIds, getUsernameTag, getWithInlineBots]);
|
||||||
|
|
||||||
const insertMention = useLastCallback((
|
const insertMention = useLastCallback((
|
||||||
peer: ApiPeer,
|
peer: ApiPeer,
|
||||||
|
|||||||
@ -133,6 +133,6 @@ export default memo(withGlobal((global): Complete<StateProps> => {
|
|||||||
return {
|
return {
|
||||||
isLoading: !foundIds && globalSearch.fetchingStatus?.botApps,
|
isLoading: !foundIds && globalSearch.fetchingStatus?.botApps,
|
||||||
foundIds,
|
foundIds,
|
||||||
recentBotIds: global.topBotApps.userIds,
|
recentBotIds: global.topPeerCategories.botsApp?.peerIds,
|
||||||
};
|
};
|
||||||
})(MoreAppsTabContent));
|
})(MoreAppsTabContent));
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import './api/sync';
|
|||||||
import './api/accounts';
|
import './api/accounts';
|
||||||
import './api/ai';
|
import './api/ai';
|
||||||
import './api/users';
|
import './api/users';
|
||||||
|
import './api/topPeers';
|
||||||
import './api/bots';
|
import './api/bots';
|
||||||
import './api/settings';
|
import './api/settings';
|
||||||
import './api/twoFaSettings';
|
import './api/twoFaSettings';
|
||||||
|
|||||||
@ -23,12 +23,14 @@ import { formatStarsAsText } from '../../../util/localization/format';
|
|||||||
import { oldTranslate } from '../../../util/oldLangProvider';
|
import { oldTranslate } from '../../../util/oldLangProvider';
|
||||||
import requestActionTimeout from '../../../util/requestActionTimeout';
|
import requestActionTimeout from '../../../util/requestActionTimeout';
|
||||||
import { debounce } from '../../../util/schedulers';
|
import { debounce } from '../../../util/schedulers';
|
||||||
import { getServerTime } from '../../../util/serverTime';
|
|
||||||
import { extractCurrentThemeParams } from '../../../util/themeStyle';
|
import { extractCurrentThemeParams } from '../../../util/themeStyle';
|
||||||
import { callApi } from '../../../api/gramjs';
|
import { callApi } from '../../../api/gramjs';
|
||||||
import {
|
import {
|
||||||
getMainUsername,
|
getMainUsername,
|
||||||
getWebAppKey,
|
getWebAppKey,
|
||||||
|
isChatAdmin,
|
||||||
|
isUserBot,
|
||||||
|
isUserRightBanned,
|
||||||
prepareMessageReplyInfo,
|
prepareMessageReplyInfo,
|
||||||
} from '../../helpers';
|
} from '../../helpers';
|
||||||
import {
|
import {
|
||||||
@ -52,6 +54,7 @@ import { updateTabState } from '../../reducers/tabs';
|
|||||||
import {
|
import {
|
||||||
selectBot,
|
selectBot,
|
||||||
selectChat,
|
selectChat,
|
||||||
|
selectChatFullInfo,
|
||||||
selectChatLastMessageId,
|
selectChatLastMessageId,
|
||||||
selectChatMessage,
|
selectChatMessage,
|
||||||
selectCurrentChat,
|
selectCurrentChat,
|
||||||
@ -73,10 +76,13 @@ import { getPeerStarsForMessage } from './messages';
|
|||||||
|
|
||||||
import { getIsWebAppsFullscreenSupported } from '../../../hooks/useAppLayout';
|
import { getIsWebAppsFullscreenSupported } from '../../../hooks/useAppLayout';
|
||||||
|
|
||||||
const TOP_PEERS_REQUEST_COOLDOWN = 60; // 1 min
|
|
||||||
const runDebouncedForSearch = debounce((cb) => cb(), 500, false);
|
const runDebouncedForSearch = debounce((cb) => cb(), 500, false);
|
||||||
let botFatherId: string | null;
|
let botFatherId: string | null;
|
||||||
|
|
||||||
|
function canUseInlineBots<T extends GlobalState>(global: T, chat: ApiChat) {
|
||||||
|
return isChatAdmin(chat) || !isUserRightBanned(chat, 'sendInline', selectChatFullInfo(global, chat.id));
|
||||||
|
}
|
||||||
|
|
||||||
addActionHandler('clickSuggestedMessageButton', (global, actions, payload): ActionReturnType => {
|
addActionHandler('clickSuggestedMessageButton', (global, actions, payload): ActionReturnType => {
|
||||||
const {
|
const {
|
||||||
chatId, messageId, button, tabId = getCurrentTabId(),
|
chatId, messageId, button, tabId = getCurrentTabId(),
|
||||||
@ -285,71 +291,26 @@ addActionHandler('restartBot', async (global, actions, payload): Promise<void> =
|
|||||||
void sendBotCommand(chat, MAIN_THREAD_ID, '/start', undefined, selectSendAs(global, chatId), lastMessageId);
|
void sendBotCommand(chat, MAIN_THREAD_ID, '/start', undefined, selectSendAs(global, chatId), lastMessageId);
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('loadTopInlineBots', async (global): Promise<void> => {
|
|
||||||
const { lastRequestedAt } = global.topInlineBots;
|
|
||||||
if (lastRequestedAt && getServerTime() - lastRequestedAt < TOP_PEERS_REQUEST_COOLDOWN) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await callApi('fetchTopInlineBots');
|
|
||||||
if (!result) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { ids } = result;
|
|
||||||
|
|
||||||
global = getGlobal();
|
|
||||||
global = {
|
|
||||||
...global,
|
|
||||||
topInlineBots: {
|
|
||||||
...global.topInlineBots,
|
|
||||||
userIds: ids,
|
|
||||||
lastRequestedAt: getServerTime(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
setGlobal(global);
|
|
||||||
});
|
|
||||||
|
|
||||||
addActionHandler('loadTopBotApps', async (global): Promise<void> => {
|
|
||||||
const { lastRequestedAt } = global.topBotApps;
|
|
||||||
if (lastRequestedAt && getServerTime() - lastRequestedAt < TOP_PEERS_REQUEST_COOLDOWN) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await callApi('fetchTopBotApps');
|
|
||||||
if (!result) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { ids } = result;
|
|
||||||
|
|
||||||
global = getGlobal();
|
|
||||||
global = {
|
|
||||||
...global,
|
|
||||||
topBotApps: {
|
|
||||||
...global.topBotApps,
|
|
||||||
userIds: ids,
|
|
||||||
lastRequestedAt: getServerTime(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
setGlobal(global);
|
|
||||||
});
|
|
||||||
|
|
||||||
addActionHandler('queryInlineBot', async (global, actions, payload): Promise<void> => {
|
addActionHandler('queryInlineBot', async (global, actions, payload): Promise<void> => {
|
||||||
const {
|
const {
|
||||||
chatId, username, query, offset,
|
chatId, username, query, offset,
|
||||||
tabId = getCurrentTabId(),
|
tabId = getCurrentTabId(),
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
|
const chat = selectChat(global, chatId);
|
||||||
|
if (!chat || !canUseInlineBots(global, chat)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let inlineBotData = selectTabState(global, tabId).inlineBots.byUsername[username];
|
let inlineBotData = selectTabState(global, tabId).inlineBots.byUsername[username];
|
||||||
if (inlineBotData === false) {
|
if (inlineBotData === false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inlineBotData === undefined) {
|
if (inlineBotData === undefined) {
|
||||||
const { user: inlineBot, chat } = await callApi('fetchInlineBot', { username }) || {};
|
const { user: inlineBot } = await callApi('getChatByUsername', username) || {};
|
||||||
global = getGlobal();
|
global = getGlobal();
|
||||||
if (!inlineBot || !chat) {
|
if (!inlineBot || !isUserBot(inlineBot) || !inlineBot.botPlaceholder) {
|
||||||
global = replaceInlineBotSettings(global, username, false, tabId);
|
global = replaceInlineBotSettings(global, username, false, tabId);
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
return;
|
return;
|
||||||
@ -818,6 +779,7 @@ addActionHandler('requestMainWebView', async (global, actions, payload): Promise
|
|||||||
};
|
};
|
||||||
global = addWebAppToOpenList(global, newActiveApp, true, true, tabId);
|
global = addWebAppToOpenList(global, newActiveApp, true, true, tabId);
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
|
actions.bumpTopPeerRating({ category: 'botsApp', peerId: botId });
|
||||||
|
|
||||||
if (isFullscreen && getIsWebAppsFullscreenSupported()) {
|
if (isFullscreen && getIsWebAppsFullscreenSupported()) {
|
||||||
actions.changeWebAppModalState({ state: 'fullScreen', tabId });
|
actions.changeWebAppModalState({ state: 'fullScreen', tabId });
|
||||||
|
|||||||
127
src/global/actions/api/topPeers.ts
Normal file
127
src/global/actions/api/topPeers.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import type { ApiTopPeerCategory } from '../../../api/types';
|
||||||
|
import type { ActionReturnType, GlobalState } from '../../types';
|
||||||
|
|
||||||
|
import { unique } from '../../../util/iteratees';
|
||||||
|
import { getServerTime } from '../../../util/serverTime';
|
||||||
|
import { callApi } from '../../../api/gramjs';
|
||||||
|
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||||
|
import { selectPeer } from '../../selectors';
|
||||||
|
|
||||||
|
const TOP_PEERS_CACHE_TTL = 24 * 60 * 60; // 24 hours
|
||||||
|
|
||||||
|
addActionHandler('loadTopPeers', async (global, actions, payload): Promise<void> => {
|
||||||
|
const { category, force } = payload;
|
||||||
|
const current = global.topPeerCategories[category];
|
||||||
|
const now = getServerTime();
|
||||||
|
|
||||||
|
if (!force && current?.lastRequestedAt && now - current.lastRequestedAt < TOP_PEERS_CACHE_TTL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await callApi('fetchTopPeers', { category });
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global = getGlobal();
|
||||||
|
const nextNow = getServerTime();
|
||||||
|
const nextCurrent = global.topPeerCategories[category];
|
||||||
|
|
||||||
|
if (result.type === 'unchanged') {
|
||||||
|
global = updateTopPeerCategory(global, category, {
|
||||||
|
...nextCurrent,
|
||||||
|
peerIds: nextCurrent?.peerIds || [],
|
||||||
|
ratingsByPeerId: nextCurrent?.ratingsByPeerId || {},
|
||||||
|
lastRequestedAt: nextNow,
|
||||||
|
});
|
||||||
|
setGlobal(global);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.type === 'disabled') {
|
||||||
|
global = updateTopPeerCategory(global, category, {
|
||||||
|
peerIds: [],
|
||||||
|
ratingsByPeerId: {},
|
||||||
|
lastRequestedAt: nextNow,
|
||||||
|
isDisabled: true,
|
||||||
|
});
|
||||||
|
setGlobal(global);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ratingsByPeerId = result.topPeers.reduce((acc, { peerId, rating }) => {
|
||||||
|
acc[peerId] = rating;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, number>);
|
||||||
|
const peerIds = result.topPeers.map(({ peerId }) => peerId);
|
||||||
|
|
||||||
|
global = updateTopPeerCategory(global, category, {
|
||||||
|
peerIds,
|
||||||
|
ratingsByPeerId,
|
||||||
|
lastRequestedAt: nextNow,
|
||||||
|
isDisabled: undefined,
|
||||||
|
});
|
||||||
|
setGlobal(global);
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('removeTopPeer', async (global, actions, payload): Promise<void> => {
|
||||||
|
const { category, peerId } = payload;
|
||||||
|
const current = global.topPeerCategories[category];
|
||||||
|
if (!current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const peerIds = current.peerIds.filter((id) => id !== peerId);
|
||||||
|
const { [peerId]: removedRating, ...ratingsByPeerId } = current.ratingsByPeerId;
|
||||||
|
|
||||||
|
global = updateTopPeerCategory(global, category, {
|
||||||
|
...current,
|
||||||
|
peerIds,
|
||||||
|
ratingsByPeerId,
|
||||||
|
});
|
||||||
|
setGlobal(global);
|
||||||
|
|
||||||
|
const peer = selectPeer(global, peerId);
|
||||||
|
if (peer) {
|
||||||
|
await callApi('resetTopPeerRating', { category, peer });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('bumpTopPeerRating', (global, actions, payload): ActionReturnType => {
|
||||||
|
const { category, peerId, date } = payload;
|
||||||
|
const current = global.topPeerCategories[category];
|
||||||
|
const ratingEDecay = global.config?.ratingEDecay;
|
||||||
|
if (!ratingEDecay || current?.isDisabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ratingDate = date || getServerTime();
|
||||||
|
const basePeerIds = current?.peerIds || [];
|
||||||
|
const peerIds = unique([...basePeerIds, peerId]);
|
||||||
|
const ratingsByPeerId = { ...current?.ratingsByPeerId };
|
||||||
|
const normalizeRate = current?.lastRequestedAt || ratingDate;
|
||||||
|
|
||||||
|
ratingsByPeerId[peerId] = (ratingsByPeerId[peerId] || 0) + Math.exp((ratingDate - normalizeRate) / ratingEDecay);
|
||||||
|
peerIds.sort((firstId, secondId) => (ratingsByPeerId[secondId] || 0) - (ratingsByPeerId[firstId] || 0));
|
||||||
|
|
||||||
|
return updateTopPeerCategory(global, category, {
|
||||||
|
peerIds,
|
||||||
|
ratingsByPeerId,
|
||||||
|
lastRequestedAt: normalizeRate,
|
||||||
|
isDisabled: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateTopPeerCategory<T extends GlobalState>(
|
||||||
|
global: T,
|
||||||
|
category: ApiTopPeerCategory,
|
||||||
|
categoryState: NonNullable<GlobalState['topPeerCategories'][ApiTopPeerCategory]>,
|
||||||
|
): T {
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
topPeerCategories: {
|
||||||
|
...global.topPeerCategories,
|
||||||
|
[category]: categoryState,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -8,7 +8,6 @@ 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';
|
||||||
import { throttle } from '../../../util/schedulers';
|
import { throttle } from '../../../util/schedulers';
|
||||||
import { getServerTime } from '../../../util/serverTime';
|
|
||||||
import { callApi } from '../../../api/gramjs';
|
import { callApi } from '../../../api/gramjs';
|
||||||
import { isUserBot } from '../../helpers';
|
import { isUserBot } from '../../helpers';
|
||||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||||
@ -43,7 +42,6 @@ import {
|
|||||||
} from '../../selectors';
|
} from '../../selectors';
|
||||||
|
|
||||||
const PROFILE_PHOTOS_FIRST_LOAD_LIMIT = 10;
|
const PROFILE_PHOTOS_FIRST_LOAD_LIMIT = 10;
|
||||||
const TOP_PEERS_REQUEST_COOLDOWN = 60; // 1 min
|
|
||||||
const runThrottledForSearch = throttle((cb) => cb(), 500, false);
|
const runThrottledForSearch = throttle((cb) => cb(), 500, false);
|
||||||
|
|
||||||
addActionHandler('loadFullUser', async (global, actions, payload): Promise<void> => {
|
addActionHandler('loadFullUser', async (global, actions, payload): Promise<void> => {
|
||||||
@ -105,32 +103,6 @@ addActionHandler('loadUser', async (global, actions, payload): Promise<void> =>
|
|||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('loadTopUsers', async (global): Promise<void> => {
|
|
||||||
const { topPeers: { lastRequestedAt } } = global;
|
|
||||||
|
|
||||||
if (!(!lastRequestedAt || getServerTime() - lastRequestedAt > TOP_PEERS_REQUEST_COOLDOWN)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await callApi('fetchTopUsers');
|
|
||||||
if (!result) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { ids } = result;
|
|
||||||
|
|
||||||
global = getGlobal();
|
|
||||||
global = {
|
|
||||||
...global,
|
|
||||||
topPeers: {
|
|
||||||
...global.topPeers,
|
|
||||||
userIds: ids,
|
|
||||||
lastRequestedAt: getServerTime(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
setGlobal(global);
|
|
||||||
});
|
|
||||||
|
|
||||||
addActionHandler('loadContactList', async (global): Promise<void> => {
|
addActionHandler('loadContactList', async (global): Promise<void> => {
|
||||||
const contactList = await callApi('fetchContactList');
|
const contactList = await callApi('fetchContactList');
|
||||||
if (!contactList) {
|
if (!contactList) {
|
||||||
|
|||||||
@ -30,7 +30,9 @@ import {
|
|||||||
getMessageText,
|
getMessageText,
|
||||||
groupMessageIdsByThreadId,
|
groupMessageIdsByThreadId,
|
||||||
isActionMessage,
|
isActionMessage,
|
||||||
|
isDeletedUser,
|
||||||
isMessageLocal,
|
isMessageLocal,
|
||||||
|
isUserBot,
|
||||||
pickMatchingTypingDraftMessage,
|
pickMatchingTypingDraftMessage,
|
||||||
} from '../../helpers';
|
} from '../../helpers';
|
||||||
import { getMessageReplyInfo, getStoryReplyInfo } from '../../helpers/replies';
|
import { getMessageReplyInfo, getStoryReplyInfo } from '../../helpers/replies';
|
||||||
@ -99,6 +101,7 @@ import {
|
|||||||
selectTabState,
|
selectTabState,
|
||||||
selectTopic,
|
selectTopic,
|
||||||
selectTopicFromMessage,
|
selectTopicFromMessage,
|
||||||
|
selectUser,
|
||||||
selectViewportIds,
|
selectViewportIds,
|
||||||
} from '../../selectors';
|
} from '../../selectors';
|
||||||
import {
|
import {
|
||||||
@ -179,6 +182,25 @@ function removeTypingDraftEntries<T extends GlobalState>(
|
|||||||
return global;
|
return global;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldBumpGuestBotTopPeer<T extends GlobalState>(global: T, message: ApiMessage) {
|
||||||
|
const { guestChatViaId, senderId } = message;
|
||||||
|
if (message.isOutgoing || message.content.action || guestChatViaId !== global.currentUserId || !senderId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sender = selectUser(global, senderId);
|
||||||
|
return Boolean(sender?.isGuestChatBot);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldBumpInlineBotTopPeer(message: ApiMessage) {
|
||||||
|
return Boolean(message.isOutgoing && !message.content.action && message.viaBotId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldBumpCorrespondentTopPeer<T extends GlobalState>(global: T, chatId: string) {
|
||||||
|
const user = selectUser(global, chatId);
|
||||||
|
return Boolean(user && !user.isSelf && !isUserBot(user) && !isDeletedUser(user));
|
||||||
|
}
|
||||||
|
|
||||||
addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||||
switch (update['@type']) {
|
switch (update['@type']) {
|
||||||
case 'newMessage': {
|
case 'newMessage': {
|
||||||
@ -345,6 +367,22 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
|||||||
|
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
|
|
||||||
|
if (shouldBumpGuestBotTopPeer(global, newMessage)) {
|
||||||
|
actions.bumpTopPeerRating({
|
||||||
|
category: 'botsGuestChat',
|
||||||
|
peerId: newMessage.senderId!,
|
||||||
|
date: newMessage.date,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldBumpInlineBotTopPeer(newMessage)) {
|
||||||
|
actions.bumpTopPeerRating({
|
||||||
|
category: 'botsInline',
|
||||||
|
peerId: newMessage.viaBotId!,
|
||||||
|
date: newMessage.date,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Reload dialogs if chat is not present in the list
|
// Reload dialogs if chat is not present in the list
|
||||||
if (!isLocal && !chat?.isNotJoined && !selectIsChatListed(global, chatId)) {
|
if (!isLocal && !chat?.isNotJoined && !selectIsChatListed(global, chatId)) {
|
||||||
actions.loadTopChats();
|
actions.loadTopChats();
|
||||||
@ -690,6 +728,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
|||||||
|
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
|
|
||||||
|
if (shouldBumpCorrespondentTopPeer(global, chatId)) {
|
||||||
|
actions.bumpTopPeerRating({ category: 'correspondents', peerId: chatId });
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -725,6 +767,9 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
|
if (shouldBumpCorrespondentTopPeer(global, chatId)) {
|
||||||
|
actions.bumpTopPeerRating({ category: 'correspondents', peerId: chatId });
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -252,8 +252,8 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
|
|||||||
if (!cached.chats.loadingParameters) {
|
if (!cached.chats.loadingParameters) {
|
||||||
cached.chats.loadingParameters = initialState.chats.loadingParameters;
|
cached.chats.loadingParameters = initialState.chats.loadingParameters;
|
||||||
}
|
}
|
||||||
if (!cached.topBotApps) {
|
if (!cached.topPeerCategories) {
|
||||||
cached.topBotApps = initialState.topBotApps;
|
cached.topPeerCategories = initialState.topPeerCategories;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cached.reactions.defaultTags?.[0]?.type) {
|
if (!cached.reactions.defaultTags?.[0]?.type) {
|
||||||
@ -443,9 +443,7 @@ function reduceGlobal<T extends GlobalState>(global: T) {
|
|||||||
'attachMenu',
|
'attachMenu',
|
||||||
'currentUserId',
|
'currentUserId',
|
||||||
'contactList',
|
'contactList',
|
||||||
'topPeers',
|
'topPeerCategories',
|
||||||
'topInlineBots',
|
|
||||||
'topBotApps',
|
|
||||||
'recentEmojis',
|
'recentEmojis',
|
||||||
'recentCustomEmojis',
|
'recentCustomEmojis',
|
||||||
'push',
|
'push',
|
||||||
@ -560,6 +558,7 @@ function reduceUsers<T extends GlobalState>(global: T): GlobalState['users'] {
|
|||||||
.filter((id): id is string => Boolean(id) && isUserId(id));
|
.filter((id): id is string => Boolean(id) && isUserId(id));
|
||||||
|
|
||||||
const attachBotIds = Object.keys(global.attachMenu?.bots || {});
|
const attachBotIds = Object.keys(global.attachMenu?.bots || {});
|
||||||
|
const topPeerIds = getTopPeerIds(global);
|
||||||
|
|
||||||
const idsToSave = unique([
|
const idsToSave = unique([
|
||||||
...currentUserId ? [currentUserId] : [],
|
...currentUserId ? [currentUserId] : [],
|
||||||
@ -567,7 +566,7 @@ function reduceUsers<T extends GlobalState>(global: T): GlobalState['users'] {
|
|||||||
...chatStoriesUserIds,
|
...chatStoriesUserIds,
|
||||||
...visibleUserIds || [],
|
...visibleUserIds || [],
|
||||||
...attachBotIds,
|
...attachBotIds,
|
||||||
...global.topPeers.userIds || [],
|
...topPeerIds.filter(isUserId),
|
||||||
...global.recentlyFoundChatIds?.filter(isUserId) || [],
|
...global.recentlyFoundChatIds?.filter(isUserId) || [],
|
||||||
...getOrderedIds(ARCHIVED_FOLDER_ID)?.slice(0, GLOBAL_STATE_CACHE_ARCHIVED_CHAT_LIST_LIMIT).filter(isUserId) || [],
|
...getOrderedIds(ARCHIVED_FOLDER_ID)?.slice(0, GLOBAL_STATE_CACHE_ARCHIVED_CHAT_LIST_LIMIT).filter(isUserId) || [],
|
||||||
...getOrderedIds(ALL_FOLDER_ID)?.filter(isUserId) || [],
|
...getOrderedIds(ALL_FOLDER_ID)?.filter(isUserId) || [],
|
||||||
@ -608,11 +607,13 @@ function reduceChats<T extends GlobalState>(global: T): GlobalState['chats'] {
|
|||||||
return content.storyData?.peerId || webPage?.story?.peerId || replyPeer;
|
return content.storyData?.peerId || webPage?.story?.peerId || replyPeer;
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
const topPeerIds = getTopPeerIds(global);
|
||||||
|
|
||||||
const unlinkedIdsToSave = [
|
const unlinkedIdsToSave = [
|
||||||
...currentUserId ? [currentUserId] : [],
|
...currentUserId ? [currentUserId] : [],
|
||||||
...currentChatIds,
|
...currentChatIds,
|
||||||
...messagesChatIds,
|
...messagesChatIds,
|
||||||
|
...topPeerIds,
|
||||||
...global.recentlyFoundChatIds || [],
|
...global.recentlyFoundChatIds || [],
|
||||||
...getOrderedIds(ARCHIVED_FOLDER_ID)?.slice(0, GLOBAL_STATE_CACHE_ARCHIVED_CHAT_LIST_LIMIT) || [],
|
...getOrderedIds(ARCHIVED_FOLDER_ID)?.slice(0, GLOBAL_STATE_CACHE_ARCHIVED_CHAT_LIST_LIMIT) || [],
|
||||||
...getOrderedIds(ALL_FOLDER_ID) || [],
|
...getOrderedIds(ALL_FOLDER_ID) || [],
|
||||||
@ -652,6 +653,10 @@ function reduceChats<T extends GlobalState>(global: T): GlobalState['chats'] {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTopPeerIds<T extends GlobalState>(global: T) {
|
||||||
|
return unique(Object.values(global.topPeerCategories).flatMap((category) => category?.peerIds || []));
|
||||||
|
}
|
||||||
|
|
||||||
function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages'] {
|
function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages'] {
|
||||||
const { currentUserId } = global;
|
const { currentUserId } = global;
|
||||||
const byChatId: GlobalState['messages']['byChatId'] = {};
|
const byChatId: GlobalState['messages']['byChatId'] = {};
|
||||||
|
|||||||
@ -268,10 +268,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
|||||||
saved: {},
|
saved: {},
|
||||||
},
|
},
|
||||||
|
|
||||||
topPeers: {},
|
topPeerCategories: {},
|
||||||
|
|
||||||
topInlineBots: {},
|
|
||||||
topBotApps: {},
|
|
||||||
|
|
||||||
activeSessions: {
|
activeSessions: {
|
||||||
byHash: {},
|
byHash: {},
|
||||||
|
|||||||
@ -58,6 +58,7 @@ import type {
|
|||||||
ApiStickerSetInfo,
|
ApiStickerSetInfo,
|
||||||
ApiThemeParameters,
|
ApiThemeParameters,
|
||||||
ApiTodoItem,
|
ApiTodoItem,
|
||||||
|
ApiTopPeerCategory,
|
||||||
ApiTypeCurrencyAmount,
|
ApiTypeCurrencyAmount,
|
||||||
ApiTypePrepaidGiveaway,
|
ApiTypePrepaidGiveaway,
|
||||||
ApiUpdate,
|
ApiUpdate,
|
||||||
@ -1914,7 +1915,6 @@ export interface ActionPayloads {
|
|||||||
|
|
||||||
// Users
|
// Users
|
||||||
loadNearestCountry: undefined;
|
loadNearestCountry: undefined;
|
||||||
loadTopUsers: undefined;
|
|
||||||
loadContactList: undefined;
|
loadContactList: undefined;
|
||||||
|
|
||||||
loadCurrentUser: undefined;
|
loadCurrentUser: undefined;
|
||||||
@ -2164,8 +2164,19 @@ export interface ActionPayloads {
|
|||||||
command: string;
|
command: string;
|
||||||
chatId?: string;
|
chatId?: string;
|
||||||
} & WithTabId;
|
} & WithTabId;
|
||||||
loadTopInlineBots: undefined;
|
loadTopPeers: {
|
||||||
loadTopBotApps: undefined;
|
category: ApiTopPeerCategory;
|
||||||
|
force?: boolean;
|
||||||
|
};
|
||||||
|
removeTopPeer: {
|
||||||
|
category: ApiTopPeerCategory;
|
||||||
|
peerId: string;
|
||||||
|
};
|
||||||
|
bumpTopPeerRating: {
|
||||||
|
category: ApiTopPeerCategory;
|
||||||
|
peerId: string;
|
||||||
|
date?: number;
|
||||||
|
};
|
||||||
queryInlineBot: {
|
queryInlineBot: {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
username: string;
|
username: string;
|
||||||
@ -2175,6 +2186,7 @@ export interface ActionPayloads {
|
|||||||
sendInlineBotResult: {
|
sendInlineBotResult: {
|
||||||
id: string;
|
id: string;
|
||||||
queryId: string;
|
queryId: string;
|
||||||
|
botId?: string;
|
||||||
chatId: string;
|
chatId: string;
|
||||||
threadId: ThreadId;
|
threadId: ThreadId;
|
||||||
isSilent?: boolean;
|
isSilent?: boolean;
|
||||||
@ -2185,6 +2197,7 @@ export interface ActionPayloads {
|
|||||||
chat: ApiChat;
|
chat: ApiChat;
|
||||||
id: string;
|
id: string;
|
||||||
queryId: string;
|
queryId: string;
|
||||||
|
botId?: string;
|
||||||
replyInfo?: ApiInputMessageReplyInfo;
|
replyInfo?: ApiInputMessageReplyInfo;
|
||||||
sendAs?: ApiPeer;
|
sendAs?: ApiPeer;
|
||||||
isSilent?: boolean;
|
isSilent?: boolean;
|
||||||
|
|||||||
@ -46,6 +46,7 @@ import type {
|
|||||||
ApiStoryAlbum,
|
ApiStoryAlbum,
|
||||||
ApiTimezone,
|
ApiTimezone,
|
||||||
ApiTonAmount,
|
ApiTonAmount,
|
||||||
|
ApiTopPeerCategory,
|
||||||
ApiTranscription,
|
ApiTranscription,
|
||||||
ApiUpdateAuthorizationStateType,
|
ApiUpdateAuthorizationStateType,
|
||||||
ApiUpdateConnectionStateType,
|
ApiUpdateConnectionStateType,
|
||||||
@ -418,20 +419,12 @@ export type GlobalState = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
topPeers: {
|
topPeerCategories: Partial<Record<ApiTopPeerCategory, {
|
||||||
userIds?: string[];
|
peerIds: string[];
|
||||||
|
ratingsByPeerId: Record<string, number>;
|
||||||
lastRequestedAt?: number;
|
lastRequestedAt?: number;
|
||||||
};
|
isDisabled?: boolean;
|
||||||
|
}>>;
|
||||||
topInlineBots: {
|
|
||||||
userIds?: string[];
|
|
||||||
lastRequestedAt?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
topBotApps: {
|
|
||||||
userIds?: string[];
|
|
||||||
lastRequestedAt?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
activeSessions: {
|
activeSessions: {
|
||||||
byHash: Record<string, ApiSession>;
|
byHash: Record<string, ApiSession>;
|
||||||
|
|||||||
@ -145,7 +145,13 @@ class TelegramClient {
|
|||||||
if (request instanceof Api.contacts.GetTopPeers) {
|
if (request instanceof Api.contacts.GetTopPeers) {
|
||||||
return new Api.contacts.TopPeers({
|
return new Api.contacts.TopPeers({
|
||||||
categories: [new Api.TopPeerCategoryPeers({
|
categories: [new Api.TopPeerCategoryPeers({
|
||||||
category: new Api.TopPeerCategoryCorrespondents(),
|
category: request.botsInline
|
||||||
|
? new Api.TopPeerCategoryBotsInline()
|
||||||
|
: request.botsApp
|
||||||
|
? new Api.TopPeerCategoryBotsApp()
|
||||||
|
: request.botsGuestchat
|
||||||
|
? new Api.TopPeerCategoryBotsGuestChat()
|
||||||
|
: new Api.TopPeerCategoryCorrespondents(),
|
||||||
count: this.mockData.topPeers.length,
|
count: this.mockData.topPeers.length,
|
||||||
peers: this.mockData.topPeers.map((id) => {
|
peers: this.mockData.topPeers.map((id) => {
|
||||||
return new Api.TopPeer({
|
return new Api.TopPeer({
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user