Composer: Implement Inline Bots (#1206)
This commit is contained in:
parent
b44792afab
commit
c3fd0d66e9
51
src/api/gramjs/apiBuilders/bots.ts
Normal file
51
src/api/gramjs/apiBuilders/bots.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
|
import {
|
||||||
|
ApiBotInlineMediaResult, ApiBotInlineResult, ApiInlineResultType, ApiWebDocument,
|
||||||
|
} from '../../types';
|
||||||
|
|
||||||
|
import { pick } from '../../../util/iteratees';
|
||||||
|
import { buildApiPhoto, buildApiThumbnailFromStripped } from './common';
|
||||||
|
import { buildVideoFromDocument } from './messages';
|
||||||
|
import { buildStickerFromDocument } from './symbols';
|
||||||
|
|
||||||
|
export function buildApiBotInlineResult(result: GramJs.BotInlineResult, queryId: string): ApiBotInlineResult {
|
||||||
|
const {
|
||||||
|
id, type, title, description, url, thumb,
|
||||||
|
} = result;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
queryId,
|
||||||
|
type: type as ApiInlineResultType,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
url,
|
||||||
|
webThumbnail: buildApiWebDocument(thumb),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildApiBotInlineMediaResult(
|
||||||
|
result: GramJs.BotInlineMediaResult, queryId: string,
|
||||||
|
): ApiBotInlineMediaResult {
|
||||||
|
const {
|
||||||
|
id, type, title, description, photo, document,
|
||||||
|
} = result;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
queryId,
|
||||||
|
type: type as ApiInlineResultType,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
...(type === 'sticker' && document instanceof GramJs.Document && { sticker: buildStickerFromDocument(document) }),
|
||||||
|
...(photo instanceof GramJs.Photo && { photo: buildApiPhoto(photo) }),
|
||||||
|
...(type === 'gif' && document instanceof GramJs.Document && { gif: buildVideoFromDocument(document) }),
|
||||||
|
...(type === 'video' && document instanceof GramJs.Document && {
|
||||||
|
thumbnail: buildApiThumbnailFromStripped(document.thumbs),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildApiWebDocument(document?: GramJs.TypeWebDocument): ApiWebDocument | undefined {
|
||||||
|
return document ? pick(document, ['url', 'mimeType']) : undefined;
|
||||||
|
}
|
||||||
@ -5,6 +5,8 @@ import { MEMOJI_STICKER_ID } from '../../../config';
|
|||||||
import { buildApiThumbnailFromCached, buildApiThumbnailFromPath } from './common';
|
import { buildApiThumbnailFromCached, buildApiThumbnailFromPath } from './common';
|
||||||
import localDb from '../localDb';
|
import localDb from '../localDb';
|
||||||
|
|
||||||
|
const ANIMATED_STICKER_MIME_TYPE = 'application/x-tgsticker';
|
||||||
|
|
||||||
export function buildStickerFromDocument(document: GramJs.TypeDocument): ApiSticker | undefined {
|
export function buildStickerFromDocument(document: GramJs.TypeDocument): ApiSticker | undefined {
|
||||||
if (document instanceof GramJs.DocumentEmpty) {
|
if (document instanceof GramJs.DocumentEmpty) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -15,7 +17,12 @@ export function buildStickerFromDocument(document: GramJs.TypeDocument): ApiStic
|
|||||||
attr instanceof GramJs.DocumentAttributeSticker
|
attr instanceof GramJs.DocumentAttributeSticker
|
||||||
));
|
));
|
||||||
|
|
||||||
if (!stickerAttribute) {
|
const fileAttribute = document.mimeType === ANIMATED_STICKER_MIME_TYPE && document.attributes
|
||||||
|
.find((attr: any): attr is GramJs.DocumentAttributeFilename => (
|
||||||
|
attr instanceof GramJs.DocumentAttributeFilename
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!stickerAttribute && !fileAttribute) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,9 +31,11 @@ export function buildStickerFromDocument(document: GramJs.TypeDocument): ApiStic
|
|||||||
attr instanceof GramJs.DocumentAttributeImageSize
|
attr instanceof GramJs.DocumentAttributeImageSize
|
||||||
));
|
));
|
||||||
|
|
||||||
const stickerSetInfo = stickerAttribute.stickerset as GramJs.InputStickerSetID;
|
const stickerSetInfo = stickerAttribute && stickerAttribute.stickerset instanceof GramJs.InputStickerSetID
|
||||||
const emoji = stickerAttribute.alt;
|
? stickerAttribute.stickerset
|
||||||
const isAnimated = document.mimeType === 'application/x-tgsticker';
|
: undefined;
|
||||||
|
const emoji = stickerAttribute ? stickerAttribute.alt : undefined;
|
||||||
|
const isAnimated = document.mimeType === ANIMATED_STICKER_MIME_TYPE;
|
||||||
const cachedThumb = document.thumbs && document.thumbs.find(
|
const cachedThumb = document.thumbs && document.thumbs.find(
|
||||||
(s): s is GramJs.PhotoCachedSize => s instanceof GramJs.PhotoCachedSize,
|
(s): s is GramJs.PhotoCachedSize => s instanceof GramJs.PhotoCachedSize,
|
||||||
);
|
);
|
||||||
@ -43,8 +52,8 @@ export function buildStickerFromDocument(document: GramJs.TypeDocument): ApiStic
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: String(document.id),
|
id: String(document.id),
|
||||||
stickerSetId: stickerSetInfo.id ? String(stickerSetInfo.id) : MEMOJI_STICKER_ID,
|
stickerSetId: stickerSetInfo ? String(stickerSetInfo.id) : MEMOJI_STICKER_ID,
|
||||||
stickerSetAccessHash: String(stickerSetInfo.accessHash),
|
stickerSetAccessHash: stickerSetInfo && String(stickerSetInfo.accessHash),
|
||||||
emoji,
|
emoji,
|
||||||
isAnimated,
|
isAnimated,
|
||||||
width,
|
width,
|
||||||
|
|||||||
@ -43,6 +43,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
|
|||||||
status: buildApiUserStatus(mtpUser.status),
|
status: buildApiUserStatus(mtpUser.status),
|
||||||
...(mtpUser.accessHash && { accessHash: String(mtpUser.accessHash) }),
|
...(mtpUser.accessHash && { accessHash: String(mtpUser.accessHash) }),
|
||||||
...(avatarHash && { avatarHash }),
|
...(avatarHash && { avatarHash }),
|
||||||
|
...(mtpUser.bot && mtpUser.botInlinePlaceholder && { botPlaceholder: mtpUser.botInlinePlaceholder }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ interface LocalDb {
|
|||||||
documents: Record<string, GramJs.Document>;
|
documents: Record<string, GramJs.Document>;
|
||||||
stickerSets: Record<string, GramJs.StickerSet>;
|
stickerSets: Record<string, GramJs.StickerSet>;
|
||||||
photos: Record<string, GramJs.Photo>;
|
photos: Record<string, GramJs.Photo>;
|
||||||
|
webDocuments: Record<string, GramJs.TypeWebDocument>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -20,4 +21,5 @@ export default {
|
|||||||
documents: {},
|
documents: {},
|
||||||
stickerSets: {},
|
stickerSets: {},
|
||||||
photos: {},
|
photos: {},
|
||||||
|
webDocuments: {},
|
||||||
} as LocalDb;
|
} as LocalDb;
|
||||||
|
|||||||
@ -1,6 +1,15 @@
|
|||||||
import { invokeRequest } from './client';
|
import BigInt from 'big-integer';
|
||||||
import { Api as GramJs } from '../../../lib/gramjs';
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
import { buildInputPeer } from '../gramjsBuilders';
|
|
||||||
|
import { ApiBotInlineSwitchPm, ApiChat, ApiUser } from '../../types';
|
||||||
|
|
||||||
|
import localDb from '../localDb';
|
||||||
|
import { invokeRequest } from './client';
|
||||||
|
import { buildInputPeer, calculateResultHash, generateRandomBigInt } from '../gramjsBuilders';
|
||||||
|
import { buildApiUser } from '../apiBuilders/users';
|
||||||
|
import { buildApiBotInlineMediaResult, buildApiBotInlineResult } from '../apiBuilders/bots';
|
||||||
|
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
||||||
|
import { pick } from '../../../util/iteratees';
|
||||||
|
|
||||||
export function init() {
|
export function init() {
|
||||||
}
|
}
|
||||||
@ -18,3 +27,135 @@ export function answerCallbackButton(
|
|||||||
data: Buffer.from(data),
|
data: Buffer.from(data),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchTopInlineBots({ hash = 0 }: { hash?: number }) {
|
||||||
|
const topPeers = await invokeRequest(new GramJs.contacts.GetTopPeers({
|
||||||
|
hash,
|
||||||
|
botsInline: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!(topPeers instanceof GramJs.contacts.TopPeers)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = topPeers.users.map(buildApiUser).filter<ApiUser>(Boolean as any);
|
||||||
|
const ids = users.map(({ id }) => id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hash: calculateResultHash(ids),
|
||||||
|
ids,
|
||||||
|
users,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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({
|
||||||
|
bot, chat, query, offset = '',
|
||||||
|
}: {
|
||||||
|
bot: ApiUser; chat: ApiChat; query: string; offset?: string;
|
||||||
|
}) {
|
||||||
|
const result = await invokeRequest(new GramJs.messages.GetInlineBotResults({
|
||||||
|
bot: buildInputPeer(bot.id, bot.accessHash),
|
||||||
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
|
query,
|
||||||
|
offset,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.users.map(addUserToLocalDb);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isGallery: Boolean(result.gallery),
|
||||||
|
help: bot.botPlaceholder,
|
||||||
|
nextOffset: result.nextOffset,
|
||||||
|
switchPm: buildSwitchPm(result.switchPm),
|
||||||
|
users: result.users.map(buildApiUser).filter<ApiUser>(Boolean as any),
|
||||||
|
results: processInlineBotResult(String(result.queryId), result.results),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendInlineBotResult({
|
||||||
|
chat, resultId, queryId, replyingTo,
|
||||||
|
}: {
|
||||||
|
chat: ApiChat;
|
||||||
|
resultId: string;
|
||||||
|
queryId: string;
|
||||||
|
replyingTo?: number;
|
||||||
|
}) {
|
||||||
|
const randomId = generateRandomBigInt();
|
||||||
|
|
||||||
|
await invokeRequest(new GramJs.messages.SendInlineBotResult({
|
||||||
|
clearDraft: true,
|
||||||
|
randomId,
|
||||||
|
queryId: BigInt(queryId),
|
||||||
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
|
id: resultId,
|
||||||
|
...(replyingTo && { replyToMsgId: replyingTo }),
|
||||||
|
}), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSwitchPm(switchPm?: GramJs.InlineBotSwitchPM) {
|
||||||
|
return switchPm ? pick(switchPm, ['text', 'startParam']) as ApiBotInlineSwitchPm : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function processInlineBotResult(queryId: string, results: GramJs.TypeBotInlineResult[]) {
|
||||||
|
return results.map((result) => {
|
||||||
|
if (result instanceof GramJs.BotInlineMediaResult) {
|
||||||
|
if (result.document instanceof GramJs.Document) {
|
||||||
|
addDocumentToLocalDb(result.document);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.photo instanceof GramJs.Photo) {
|
||||||
|
addPhotoToLocalDb(result.photo);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildApiBotInlineMediaResult(result, queryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.thumb) {
|
||||||
|
addWebDocumentToLocalDb(result.thumb);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildApiBotInlineResult(result, queryId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addUserToLocalDb(user: GramJs.User) {
|
||||||
|
localDb.users[user.id] = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDocumentToLocalDb(document: GramJs.Document) {
|
||||||
|
localDb.documents[String(document.id)] = document;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPhotoToLocalDb(photo: GramJs.Photo) {
|
||||||
|
localDb.photos[String(photo.id)] = photo;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addWebDocumentToLocalDb(webDocument: GramJs.TypeWebDocument) {
|
||||||
|
localDb.webDocuments[webDocument.url] = webDocument;
|
||||||
|
}
|
||||||
|
|||||||
@ -55,7 +55,7 @@ export {
|
|||||||
} from './twoFaSettings';
|
} from './twoFaSettings';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
answerCallbackButton,
|
answerCallbackButton, fetchTopInlineBots, fetchInlineBot, fetchInlineBotResults, sendInlineBotResult,
|
||||||
} from './bots';
|
} from './bots';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -17,7 +17,10 @@ import { getEntityTypeById } from '../gramjsBuilders';
|
|||||||
import { blobToDataUri } from '../../../util/files';
|
import { blobToDataUri } from '../../../util/files';
|
||||||
import * as cacheApi from '../../../util/cacheApi';
|
import * as cacheApi from '../../../util/cacheApi';
|
||||||
|
|
||||||
type EntityType = 'msg' | 'sticker' | 'wallpaper' | 'gif' | 'channel' | 'chat' | 'user' | 'photo' | 'stickerSet';
|
type EntityType = (
|
||||||
|
'msg' | 'sticker' | 'wallpaper' | 'gif' | 'channel' | 'chat' | 'user' | 'photo' | 'stickerSet' | 'webDocument'
|
||||||
|
);
|
||||||
|
const MEDIA_ENTITY_TYPES = new Set(['msg', 'sticker', 'gif', 'wallpaper', 'photo', 'webDocument']);
|
||||||
|
|
||||||
export default async function downloadMedia(
|
export default async function downloadMedia(
|
||||||
{
|
{
|
||||||
@ -70,8 +73,9 @@ async function download(
|
|||||||
end?: number,
|
end?: number,
|
||||||
mediaFormat?: ApiMediaFormat,
|
mediaFormat?: ApiMediaFormat,
|
||||||
) {
|
) {
|
||||||
// eslint-disable-next-line max-len
|
const mediaMatch = url.startsWith('webDocument')
|
||||||
const mediaMatch = url.match(/(avatar|profile|photo|msg|stickerSet|sticker|wallpaper|gif|file)([-\d\w./]+)(\?size=\w+)?/);
|
? url.match(/(webDocument):(.+)/)
|
||||||
|
: url.match(/(avatar|profile|photo|msg|stickerSet|sticker|wallpaper|gif|file)([-\d\w./]+)(\?size=\w+)?/);
|
||||||
if (!mediaMatch) {
|
if (!mediaMatch) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -91,14 +95,14 @@ async function download(
|
|||||||
const sizeType = mediaMatch[3] ? mediaMatch[3].replace('?size=', '') : undefined;
|
const sizeType = mediaMatch[3] ? mediaMatch[3].replace('?size=', '') : undefined;
|
||||||
let entity: (
|
let entity: (
|
||||||
GramJs.User | GramJs.Chat | GramJs.Channel | GramJs.Photo |
|
GramJs.User | GramJs.Chat | GramJs.Channel | GramJs.Photo |
|
||||||
GramJs.Message | GramJs.Document | GramJs.StickerSet | undefined
|
GramJs.Message | GramJs.Document | GramJs.StickerSet | GramJs.TypeWebDocument | undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
if (mediaMatch[1] === 'avatar' || mediaMatch[1] === 'profile') {
|
if (mediaMatch[1] === 'avatar' || mediaMatch[1] === 'profile') {
|
||||||
entityType = getEntityTypeById(Number(entityId));
|
entityType = getEntityTypeById(Number(entityId));
|
||||||
entityId = Math.abs(Number(entityId));
|
entityId = Math.abs(Number(entityId));
|
||||||
} else {
|
} else {
|
||||||
entityType = mediaMatch[1] as 'msg' | 'sticker' | 'wallpaper' | 'gif' | 'stickerSet' | 'photo';
|
entityType = mediaMatch[1] as 'msg' | 'sticker' | 'wallpaper' | 'gif' | 'stickerSet' | 'photo' | 'webDocument';
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (entityType) {
|
switch (entityType) {
|
||||||
@ -123,13 +127,16 @@ async function download(
|
|||||||
case 'stickerSet':
|
case 'stickerSet':
|
||||||
entity = localDb.stickerSets[entityId as string];
|
entity = localDb.stickerSets[entityId as string];
|
||||||
break;
|
break;
|
||||||
|
case 'webDocument':
|
||||||
|
entity = localDb.webDocuments[entityId as string];
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!entity) {
|
if (!entity) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['msg', 'sticker', 'gif', 'wallpaper', 'photo'].includes(entityType)) {
|
if (MEDIA_ENTITY_TYPES.has(entityType)) {
|
||||||
if (mediaFormat === ApiMediaFormat.Stream) {
|
if (mediaFormat === ApiMediaFormat.Stream) {
|
||||||
onProgress!.acceptsBuffer = true;
|
onProgress!.acceptsBuffer = true;
|
||||||
}
|
}
|
||||||
@ -154,6 +161,8 @@ async function download(
|
|||||||
mimeType = 'image/jpeg';
|
mimeType = 'image/jpeg';
|
||||||
} else if (entityType === 'sticker' && sizeType) {
|
} else if (entityType === 'sticker' && sizeType) {
|
||||||
mimeType = 'image/webp';
|
mimeType = 'image/webp';
|
||||||
|
} else if (entityType === 'webDocument') {
|
||||||
|
mimeType = (entity as GramJs.TypeWebDocument).mimeType;
|
||||||
} else {
|
} else {
|
||||||
mimeType = (entity as GramJs.Document).mimeType;
|
mimeType = (entity as GramJs.Document).mimeType;
|
||||||
fullSize = (entity as GramJs.Document).size;
|
fullSize = (entity as GramJs.Document).size;
|
||||||
@ -230,7 +239,6 @@ function prepareMedia(mediaData: ApiParsedMedia): ApiPreparedMedia {
|
|||||||
return mediaData;
|
return mediaData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getMimeType(data: Uint8Array, fallbackMimeType = 'image/jpeg') {
|
function getMimeType(data: Uint8Array, fallbackMimeType = 'image/jpeg') {
|
||||||
if (data.length < 4) {
|
if (data.length < 4) {
|
||||||
return fallbackMimeType;
|
return fallbackMimeType;
|
||||||
|
|||||||
40
src/api/types/bots.ts
Normal file
40
src/api/types/bots.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import {
|
||||||
|
ApiPhoto, ApiSticker, ApiThumbnail, ApiVideo,
|
||||||
|
} from './messages';
|
||||||
|
|
||||||
|
export type ApiInlineResultType = (
|
||||||
|
'article' | 'audio' | 'contact' | 'document' | 'game' | 'gif' | 'location' | 'mpeg4_gif' |
|
||||||
|
'photo' | 'sticker'| 'venue' | 'video' | 'voice'
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface ApiWebDocument {
|
||||||
|
url: string;
|
||||||
|
mimeType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiBotInlineResult {
|
||||||
|
id: string;
|
||||||
|
queryId: string;
|
||||||
|
type: ApiInlineResultType;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
url?: string;
|
||||||
|
webThumbnail?: ApiWebDocument;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiBotInlineMediaResult {
|
||||||
|
id: string;
|
||||||
|
queryId: string;
|
||||||
|
type: ApiInlineResultType;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
sticker?: ApiSticker;
|
||||||
|
photo?: ApiPhoto;
|
||||||
|
gif?: ApiVideo;
|
||||||
|
thumbnail?: ApiThumbnail;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiBotInlineSwitchPm {
|
||||||
|
text: string;
|
||||||
|
startParam: string;
|
||||||
|
}
|
||||||
@ -5,4 +5,5 @@ export * from './updates';
|
|||||||
export * from './media';
|
export * from './media';
|
||||||
export * from './payments';
|
export * from './payments';
|
||||||
export * from './settings';
|
export * from './settings';
|
||||||
|
export * from './bots';
|
||||||
export * from './misc';
|
export * from './misc';
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
// We cache avatars as Data URI for faster initial load
|
// We cache avatars as Data URI for faster initial load
|
||||||
// and messages media as Blob for smaller size.
|
// and messages media as Blob for smaller size.
|
||||||
|
|
||||||
export enum ApiMediaFormat {
|
export enum ApiMediaFormat {
|
||||||
DataUri,
|
DataUri,
|
||||||
BlobUrl,
|
BlobUrl,
|
||||||
|
|||||||
@ -21,8 +21,8 @@ export interface ApiPhoto {
|
|||||||
export interface ApiSticker {
|
export interface ApiSticker {
|
||||||
id: string;
|
id: string;
|
||||||
stickerSetId: string;
|
stickerSetId: string;
|
||||||
stickerSetAccessHash: string;
|
stickerSetAccessHash?: string;
|
||||||
emoji: string;
|
emoji?: string;
|
||||||
isAnimated: boolean;
|
isAnimated: boolean;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
|
|||||||
@ -15,6 +15,7 @@ export interface ApiUser {
|
|||||||
accessHash?: string;
|
accessHash?: string;
|
||||||
avatarHash?: string;
|
avatarHash?: string;
|
||||||
photos?: ApiPhoto[];
|
photos?: ApiPhoto[];
|
||||||
|
botPlaceholder?: string;
|
||||||
canBeInvitedToGroup?: boolean;
|
canBeInvitedToGroup?: boolean;
|
||||||
|
|
||||||
// Obtained from GetFullUser / UserFullInfo
|
// Obtained from GetFullUser / UserFullInfo
|
||||||
|
|||||||
@ -36,6 +36,7 @@ export { default as CustomSendMenu } from '../components/middle/composer/CustomS
|
|||||||
export { default as DropArea } from '../components/middle/composer/DropArea';
|
export { default as DropArea } from '../components/middle/composer/DropArea';
|
||||||
export { default as TextFormatter } from '../components/middle/composer/TextFormatter';
|
export { default as TextFormatter } from '../components/middle/composer/TextFormatter';
|
||||||
export { default as EmojiTooltip } from '../components/middle/composer/EmojiTooltip';
|
export { default as EmojiTooltip } from '../components/middle/composer/EmojiTooltip';
|
||||||
|
export { default as InlineBotTooltip } from '../components/middle/composer/InlineBotTooltip';
|
||||||
|
|
||||||
export { default as RightSearch } from '../components/right/RightSearch';
|
export { default as RightSearch } from '../components/right/RightSearch';
|
||||||
export { default as StickerSearch } from '../components/right/StickerSearch';
|
export { default as StickerSearch } from '../components/right/StickerSearch';
|
||||||
|
|||||||
@ -20,11 +20,12 @@ type OwnProps = {
|
|||||||
gif: ApiVideo;
|
gif: ApiVideo;
|
||||||
observeIntersection: ObserveFn;
|
observeIntersection: ObserveFn;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
|
className?: string;
|
||||||
onClick: (gif: ApiVideo) => void;
|
onClick: (gif: ApiVideo) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const GifButton: FC<OwnProps> = ({
|
const GifButton: FC<OwnProps> = ({
|
||||||
gif, observeIntersection, isDisabled, onClick,
|
gif, observeIntersection, isDisabled, className, onClick,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
@ -53,17 +54,18 @@ const GifButton: FC<OwnProps> = ({
|
|||||||
[onClick, gif, videoData],
|
[onClick, gif, videoData],
|
||||||
);
|
);
|
||||||
|
|
||||||
const className = buildClassName(
|
const fullClassName = buildClassName(
|
||||||
'GifButton',
|
'GifButton',
|
||||||
gif.width && gif.height && gif.width < gif.height ? 'vertical' : 'horizontal',
|
gif.width && gif.height && gif.width < gif.height ? 'vertical' : 'horizontal',
|
||||||
transitionClassNames,
|
transitionClassNames,
|
||||||
localMediaHash,
|
localMediaHash,
|
||||||
|
className,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={className}
|
className={fullClassName}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
{hasThumbnail && (
|
{hasThumbnail && (
|
||||||
|
|||||||
@ -18,10 +18,9 @@
|
|||||||
|
|
||||||
.Badge-wrapper {
|
.Badge-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-left: 1.5rem;
|
|
||||||
|
|
||||||
.Badge {
|
.Badge {
|
||||||
margin-left: 0.5rem;
|
margin-inline-start: .5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -53,7 +53,8 @@ type StateProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type DispatchProps = Pick<GlobalActions, (
|
type DispatchProps = Pick<GlobalActions, (
|
||||||
'loadAnimatedEmojis' | 'loadNotificationSettings' | 'loadNotificationExceptions' | 'updateIsOnline'
|
'loadAnimatedEmojis' | 'loadNotificationSettings' | 'loadNotificationExceptions' | 'updateIsOnline' |
|
||||||
|
'loadTopInlineBots'
|
||||||
)>;
|
)>;
|
||||||
|
|
||||||
const ANIMATION_DURATION = 350;
|
const ANIMATION_DURATION = 350;
|
||||||
@ -81,6 +82,7 @@ const Main: FC<StateProps & DispatchProps> = ({
|
|||||||
loadNotificationSettings,
|
loadNotificationSettings,
|
||||||
loadNotificationExceptions,
|
loadNotificationExceptions,
|
||||||
updateIsOnline,
|
updateIsOnline,
|
||||||
|
loadTopInlineBots,
|
||||||
}) => {
|
}) => {
|
||||||
if (DEBUG && !DEBUG_isLogged) {
|
if (DEBUG && !DEBUG_isLogged) {
|
||||||
DEBUG_isLogged = true;
|
DEBUG_isLogged = true;
|
||||||
@ -95,8 +97,12 @@ const Main: FC<StateProps & DispatchProps> = ({
|
|||||||
loadAnimatedEmojis();
|
loadAnimatedEmojis();
|
||||||
loadNotificationSettings();
|
loadNotificationSettings();
|
||||||
loadNotificationExceptions();
|
loadNotificationExceptions();
|
||||||
|
loadTopInlineBots();
|
||||||
}
|
}
|
||||||
}, [lastSyncTime, loadAnimatedEmojis, loadNotificationExceptions, loadNotificationSettings, updateIsOnline]);
|
}, [
|
||||||
|
lastSyncTime, loadAnimatedEmojis, loadNotificationExceptions, loadNotificationSettings, updateIsOnline,
|
||||||
|
loadTopInlineBots,
|
||||||
|
]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
transitionClassNames: middleColumnTransitionClassNames,
|
transitionClassNames: middleColumnTransitionClassNames,
|
||||||
@ -234,5 +240,6 @@ export default memo(withGlobal(
|
|||||||
},
|
},
|
||||||
(setGlobal, actions): DispatchProps => pick(actions, [
|
(setGlobal, actions): DispatchProps => pick(actions, [
|
||||||
'loadAnimatedEmojis', 'loadNotificationSettings', 'loadNotificationExceptions', 'updateIsOnline',
|
'loadAnimatedEmojis', 'loadNotificationSettings', 'loadNotificationExceptions', 'updateIsOnline',
|
||||||
|
'loadTopInlineBots',
|
||||||
]),
|
]),
|
||||||
)(Main));
|
)(Main));
|
||||||
|
|||||||
@ -25,7 +25,6 @@ import './AttachmentModal.scss';
|
|||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
attachments: ApiAttachment[];
|
attachments: ApiAttachment[];
|
||||||
caption: string;
|
caption: string;
|
||||||
canSuggestMembers?: boolean;
|
|
||||||
canSuggestEmoji?: boolean;
|
canSuggestEmoji?: boolean;
|
||||||
currentUserId?: number;
|
currentUserId?: number;
|
||||||
groupChatMembers?: ApiChatMember[];
|
groupChatMembers?: ApiChatMember[];
|
||||||
@ -45,7 +44,6 @@ const DROP_LEAVE_TIMEOUT_MS = 150;
|
|||||||
const AttachmentModal: FC<OwnProps> = ({
|
const AttachmentModal: FC<OwnProps> = ({
|
||||||
attachments,
|
attachments,
|
||||||
caption,
|
caption,
|
||||||
canSuggestMembers,
|
|
||||||
groupChatMembers,
|
groupChatMembers,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
usersById,
|
usersById,
|
||||||
@ -70,13 +68,14 @@ const AttachmentModal: FC<OwnProps> = ({
|
|||||||
const {
|
const {
|
||||||
isMentionTooltipOpen, mentionFilter,
|
isMentionTooltipOpen, mentionFilter,
|
||||||
closeMentionTooltip, insertMention,
|
closeMentionTooltip, insertMention,
|
||||||
mentionFilteredMembers,
|
mentionFilteredUsers,
|
||||||
} = useMentionTooltip(
|
} = useMentionTooltip(
|
||||||
canSuggestMembers && isOpen,
|
isOpen,
|
||||||
caption,
|
caption,
|
||||||
onCaptionUpdate,
|
onCaptionUpdate,
|
||||||
EDITABLE_INPUT_MODAL_ID,
|
EDITABLE_INPUT_MODAL_ID,
|
||||||
groupChatMembers,
|
groupChatMembers,
|
||||||
|
undefined,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
usersById,
|
usersById,
|
||||||
);
|
);
|
||||||
@ -228,7 +227,7 @@ const AttachmentModal: FC<OwnProps> = ({
|
|||||||
onClose={closeMentionTooltip}
|
onClose={closeMentionTooltip}
|
||||||
filter={mentionFilter}
|
filter={mentionFilter}
|
||||||
onInsertUserName={insertMention}
|
onInsertUserName={insertMention}
|
||||||
filteredChatMembers={mentionFilteredMembers}
|
filteredUsers={mentionFilteredUsers}
|
||||||
usersById={usersById}
|
usersById={usersById}
|
||||||
/>
|
/>
|
||||||
<EmojiTooltip
|
<EmojiTooltip
|
||||||
|
|||||||
@ -177,6 +177,11 @@
|
|||||||
.message-input-wrapper {
|
.message-input-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
> .Spinner {
|
||||||
|
align-self: center;
|
||||||
|
--spinner-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
> .Button {
|
> .Button {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background: none !important;
|
background: none !important;
|
||||||
@ -275,6 +280,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.forced-placeholder,
|
||||||
.placeholder-text {
|
.placeholder-text {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: .9375rem;
|
bottom: .9375rem;
|
||||||
@ -284,9 +290,17 @@
|
|||||||
text-align: initial;
|
text-align: initial;
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
bottom: 0.6875rem;
|
bottom: .625rem;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.forced-placeholder {
|
||||||
|
z-index: var(--z-below);
|
||||||
|
left: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 100%;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[dir=rtl] .placeholder-text {
|
&[dir=rtl] .placeholder-text {
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import { withGlobal } from '../../../lib/teact/teactn';
|
|||||||
import { GlobalActions, GlobalState, MessageListType } from '../../../global/types';
|
import { GlobalActions, GlobalState, MessageListType } from '../../../global/types';
|
||||||
import {
|
import {
|
||||||
ApiAttachment,
|
ApiAttachment,
|
||||||
|
ApiBotInlineResult,
|
||||||
|
ApiBotInlineMediaResult,
|
||||||
ApiSticker,
|
ApiSticker,
|
||||||
ApiVideo,
|
ApiVideo,
|
||||||
ApiNewPoll,
|
ApiNewPoll,
|
||||||
@ -16,7 +18,7 @@ import {
|
|||||||
ApiUser,
|
ApiUser,
|
||||||
MAIN_THREAD_ID,
|
MAIN_THREAD_ID,
|
||||||
} from '../../../api/types';
|
} from '../../../api/types';
|
||||||
import { LangCode } from '../../../types';
|
import { LangCode, InlineBotSettings } from '../../../types';
|
||||||
|
|
||||||
import { BASE_EMOJI_KEYWORD_LANG, EDITABLE_INPUT_ID, SCHEDULED_WHEN_ONLINE } from '../../../config';
|
import { BASE_EMOJI_KEYWORD_LANG, EDITABLE_INPUT_ID, SCHEDULED_WHEN_ONLINE } from '../../../config';
|
||||||
import { IS_VOICE_RECORDING_SUPPORTED, IS_SINGLE_COLUMN_LAYOUT, IS_IOS } from '../../../util/environment';
|
import { IS_VOICE_RECORDING_SUPPORTED, IS_SINGLE_COLUMN_LAYOUT, IS_IOS } from '../../../util/environment';
|
||||||
@ -35,7 +37,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
getAllowedAttachmentOptions,
|
getAllowedAttachmentOptions,
|
||||||
getChatSlowModeOptions,
|
getChatSlowModeOptions,
|
||||||
isChatGroup,
|
|
||||||
isChatPrivate,
|
isChatPrivate,
|
||||||
isChatAdmin,
|
isChatAdmin,
|
||||||
} from '../../../modules/helpers';
|
} from '../../../modules/helpers';
|
||||||
@ -62,6 +63,8 @@ import useEmojiTooltip from './hooks/useEmojiTooltip';
|
|||||||
import useMentionTooltip from './hooks/useMentionTooltip';
|
import useMentionTooltip from './hooks/useMentionTooltip';
|
||||||
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useInlineBotTooltip from './hooks/useInlineBotTooltip';
|
||||||
|
import windowSize from '../../../util/windowSize';
|
||||||
|
|
||||||
import DeleteMessageModal from '../../common/DeleteMessageModal.async';
|
import DeleteMessageModal from '../../common/DeleteMessageModal.async';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
@ -69,6 +72,7 @@ import ResponsiveHoverButton from '../../ui/ResponsiveHoverButton';
|
|||||||
import Spinner from '../../ui/Spinner';
|
import Spinner from '../../ui/Spinner';
|
||||||
import AttachMenu from './AttachMenu.async';
|
import AttachMenu from './AttachMenu.async';
|
||||||
import SymbolMenu from './SymbolMenu.async';
|
import SymbolMenu from './SymbolMenu.async';
|
||||||
|
import InlineBotTooltip from './InlineBotTooltip.async';
|
||||||
import MentionTooltip from './MentionTooltip.async';
|
import MentionTooltip from './MentionTooltip.async';
|
||||||
import CustomSendMenu from './CustomSendMenu.async';
|
import CustomSendMenu from './CustomSendMenu.async';
|
||||||
import StickerTooltip from './StickerTooltip.async';
|
import StickerTooltip from './StickerTooltip.async';
|
||||||
@ -105,7 +109,6 @@ type StateProps = {
|
|||||||
isRightColumnShown?: boolean;
|
isRightColumnShown?: boolean;
|
||||||
isSelectModeActive?: boolean;
|
isSelectModeActive?: boolean;
|
||||||
isForwarding?: boolean;
|
isForwarding?: boolean;
|
||||||
canSuggestMembers?: boolean;
|
|
||||||
isPollModalOpen?: boolean;
|
isPollModalOpen?: boolean;
|
||||||
isPaymentModalOpen?: boolean;
|
isPaymentModalOpen?: boolean;
|
||||||
isReceiptModalOpen?: boolean;
|
isReceiptModalOpen?: boolean;
|
||||||
@ -125,13 +128,16 @@ type StateProps = {
|
|||||||
baseEmojiKeywords?: Record<string, string[]>;
|
baseEmojiKeywords?: Record<string, string[]>;
|
||||||
emojiKeywords?: Record<string, string[]>;
|
emojiKeywords?: Record<string, string[]>;
|
||||||
serverTimeOffset: number;
|
serverTimeOffset: number;
|
||||||
|
topInlineBotIds?: number[];
|
||||||
|
isInlineBotLoading: boolean;
|
||||||
|
inlineBots?: Record<string, false | InlineBotSettings>;
|
||||||
} & Pick<GlobalState, 'connectionState'>;
|
} & Pick<GlobalState, 'connectionState'>;
|
||||||
|
|
||||||
type DispatchProps = Pick<GlobalActions, (
|
type DispatchProps = Pick<GlobalActions, (
|
||||||
'sendMessage' | 'editMessage' | 'saveDraft' | 'forwardMessages' |
|
'sendMessage' | 'editMessage' | 'saveDraft' | 'forwardMessages' |
|
||||||
'clearDraft' | 'showDialog' | 'setStickerSearchQuery' | 'setGifSearchQuery' |
|
'clearDraft' | 'showDialog' | 'setStickerSearchQuery' | 'setGifSearchQuery' |
|
||||||
'openPollModal' | 'closePollModal' | 'loadScheduledHistory' | 'openChat' | 'closePaymentModal' |
|
'openPollModal' | 'closePollModal' | 'loadScheduledHistory' | 'openChat' | 'closePaymentModal' |
|
||||||
'clearReceipt' | 'addRecentEmoji' | 'loadEmojiKeywords'
|
'clearReceipt' | 'addRecentEmoji' | 'loadEmojiKeywords' | 'sendInlineBotResult'
|
||||||
)>;
|
)>;
|
||||||
|
|
||||||
enum MainButtonState {
|
enum MainButtonState {
|
||||||
@ -169,7 +175,6 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
isRightColumnShown,
|
isRightColumnShown,
|
||||||
isSelectModeActive,
|
isSelectModeActive,
|
||||||
isForwarding,
|
isForwarding,
|
||||||
canSuggestMembers,
|
|
||||||
isPollModalOpen,
|
isPollModalOpen,
|
||||||
isPaymentModalOpen,
|
isPaymentModalOpen,
|
||||||
isReceiptModalOpen,
|
isReceiptModalOpen,
|
||||||
@ -177,6 +182,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
withScheduledButton,
|
withScheduledButton,
|
||||||
stickersForEmoji,
|
stickersForEmoji,
|
||||||
groupChatMembers,
|
groupChatMembers,
|
||||||
|
topInlineBotIds,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
usersById,
|
usersById,
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
@ -187,6 +193,8 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
emojiKeywords,
|
emojiKeywords,
|
||||||
serverTimeOffset,
|
serverTimeOffset,
|
||||||
recentEmojis,
|
recentEmojis,
|
||||||
|
inlineBots,
|
||||||
|
isInlineBotLoading,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
editMessage,
|
editMessage,
|
||||||
saveDraft,
|
saveDraft,
|
||||||
@ -203,7 +211,10 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
clearReceipt,
|
clearReceipt,
|
||||||
addRecentEmoji,
|
addRecentEmoji,
|
||||||
loadEmojiKeywords,
|
loadEmojiKeywords,
|
||||||
|
sendInlineBotResult,
|
||||||
}) => {
|
}) => {
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const appendixRef = useRef<HTMLDivElement>(null);
|
const appendixRef = useRef<HTMLDivElement>(null);
|
||||||
const [html, setHtml] = useState<string>('');
|
const [html, setHtml] = useState<string>('');
|
||||||
@ -213,7 +224,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
const [
|
const [
|
||||||
scheduledMessageArgs, setScheduledMessageArgs,
|
scheduledMessageArgs, setScheduledMessageArgs,
|
||||||
] = useState<GlobalState['messages']['contentToBeScheduled'] | undefined>();
|
] = useState<GlobalState['messages']['contentToBeScheduled'] | undefined>();
|
||||||
const lang = useLang();
|
const { width: windowWidth } = windowSize.get();
|
||||||
|
|
||||||
// Cache for frequently updated state
|
// Cache for frequently updated state
|
||||||
const htmlRef = useRef<string>(html);
|
const htmlRef = useRef<string>(html);
|
||||||
@ -282,17 +293,34 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
const {
|
const {
|
||||||
isMentionTooltipOpen, mentionFilter,
|
isMentionTooltipOpen, mentionFilter,
|
||||||
closeMentionTooltip, insertMention,
|
closeMentionTooltip, insertMention,
|
||||||
mentionFilteredMembers,
|
mentionFilteredUsers,
|
||||||
} = useMentionTooltip(
|
} = useMentionTooltip(
|
||||||
canSuggestMembers && !attachments.length,
|
!attachments.length,
|
||||||
html,
|
html,
|
||||||
setHtml,
|
setHtml,
|
||||||
undefined,
|
undefined,
|
||||||
groupChatMembers,
|
groupChatMembers,
|
||||||
|
topInlineBotIds,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
usersById,
|
usersById,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
isOpen: isInlineBotTooltipOpen,
|
||||||
|
id: inlineBotId,
|
||||||
|
isGallery: isInlineBotTooltipGallery,
|
||||||
|
switchPm: inlineBotSwitchPm,
|
||||||
|
results: inlineBotResults,
|
||||||
|
closeTooltip: closeInlineBotTooltip,
|
||||||
|
help: inlineBotHelp,
|
||||||
|
loadMore: loadMoreForInlineBot,
|
||||||
|
} = useInlineBotTooltip(
|
||||||
|
Boolean(!attachments.length && lastSyncTime),
|
||||||
|
chatId,
|
||||||
|
html,
|
||||||
|
inlineBots,
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isContextMenuOpen: isCustomSendMenuOpen,
|
isContextMenuOpen: isCustomSendMenuOpen,
|
||||||
handleContextMenu,
|
handleContextMenu,
|
||||||
@ -344,12 +372,10 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
|
|
||||||
setHtml(`${htmlRef.current!}${newHtml}`);
|
setHtml(`${htmlRef.current!}${newHtml}`);
|
||||||
|
|
||||||
if (!IS_SINGLE_COLUMN_LAYOUT) {
|
// If selection is outside of input, set cursor at the end of input
|
||||||
// If selection is outside of input, set cursor at the end of input
|
requestAnimationFrame(() => {
|
||||||
requestAnimationFrame(() => {
|
focusEditableElement(messageInput);
|
||||||
focusEditableElement(messageInput);
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const removeSymbol = useCallback(() => {
|
const removeSymbol = useCallback(() => {
|
||||||
@ -535,6 +561,25 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
}
|
}
|
||||||
}, [shouldSchedule, openCalendar, sendMessage, resetComposer]);
|
}, [shouldSchedule, openCalendar, sendMessage, resetComposer]);
|
||||||
|
|
||||||
|
const handleInlineBotSelect = useCallback((inlineResult: ApiBotInlineResult | ApiBotInlineMediaResult) => {
|
||||||
|
if (connectionState !== 'connectionStateReady') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendInlineBotResult({
|
||||||
|
id: inlineResult.id,
|
||||||
|
queryId: inlineResult.queryId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const messageInput = document.getElementById(EDITABLE_INPUT_ID)!;
|
||||||
|
if (IS_IOS && messageInput === document.activeElement) {
|
||||||
|
applyIosAutoCapitalizationFix(messageInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearDraft({ chatId, localOnly: true });
|
||||||
|
requestAnimationFrame(resetComposer);
|
||||||
|
}, [chatId, clearDraft, connectionState, resetComposer, sendInlineBotResult]);
|
||||||
|
|
||||||
const handlePollSend = useCallback((poll: ApiNewPoll) => {
|
const handlePollSend = useCallback((poll: ApiNewPoll) => {
|
||||||
if (shouldSchedule) {
|
if (shouldSchedule) {
|
||||||
setScheduledMessageArgs({ poll });
|
setScheduledMessageArgs({ poll });
|
||||||
@ -712,7 +757,6 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
<AttachmentModal
|
<AttachmentModal
|
||||||
attachments={attachments}
|
attachments={attachments}
|
||||||
caption={attachments.length ? html : ''}
|
caption={attachments.length ? html : ''}
|
||||||
canSuggestMembers={canSuggestMembers}
|
|
||||||
groupChatMembers={groupChatMembers}
|
groupChatMembers={groupChatMembers}
|
||||||
currentUserId={currentUserId}
|
currentUserId={currentUserId}
|
||||||
usersById={usersById}
|
usersById={usersById}
|
||||||
@ -751,7 +795,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
filter={mentionFilter}
|
filter={mentionFilter}
|
||||||
onClose={closeMentionTooltip}
|
onClose={closeMentionTooltip}
|
||||||
onInsertUserName={insertMention}
|
onInsertUserName={insertMention}
|
||||||
filteredChatMembers={mentionFilteredMembers}
|
filteredUsers={mentionFilteredUsers}
|
||||||
usersById={usersById}
|
usersById={usersById}
|
||||||
/>
|
/>
|
||||||
<div id="message-compose">
|
<div id="message-compose">
|
||||||
@ -792,15 +836,19 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
id="message-input-text"
|
id="message-input-text"
|
||||||
html={!attachments.length ? html : ''}
|
html={!attachments.length ? html : ''}
|
||||||
placeholder={
|
placeholder={
|
||||||
activeVoiceRecording && window.innerWidth <= SCREEN_WIDTH_TO_HIDE_PLACEHOLDER ? '' : lang('Message')
|
activeVoiceRecording && windowWidth <= SCREEN_WIDTH_TO_HIDE_PLACEHOLDER ? '' : lang('Message')
|
||||||
}
|
}
|
||||||
|
forcedPlaceholder={inlineBotHelp}
|
||||||
shouldSetFocus={isSymbolMenuOpen}
|
shouldSetFocus={isSymbolMenuOpen}
|
||||||
shouldSuppressFocus={IS_SINGLE_COLUMN_LAYOUT && isSymbolMenuOpen}
|
shouldSuppressFocus={IS_SINGLE_COLUMN_LAYOUT && isSymbolMenuOpen}
|
||||||
shouldSuppressTextFormatter={isEmojiTooltipOpen || isMentionTooltipOpen}
|
shouldSuppressTextFormatter={isEmojiTooltipOpen || isMentionTooltipOpen || isInlineBotTooltipOpen}
|
||||||
onUpdate={setHtml}
|
onUpdate={setHtml}
|
||||||
onSend={onSend}
|
onSend={onSend}
|
||||||
onSuppressedFocus={closeSymbolMenu}
|
onSuppressedFocus={closeSymbolMenu}
|
||||||
/>
|
/>
|
||||||
|
{isInlineBotLoading && Boolean(inlineBotId) && (
|
||||||
|
<Spinner color="gray" />
|
||||||
|
)}
|
||||||
{withScheduledButton && (
|
{withScheduledButton && (
|
||||||
<Button
|
<Button
|
||||||
round
|
round
|
||||||
@ -867,6 +915,17 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
onClose={closeBotKeyboard}
|
onClose={closeBotKeyboard}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<InlineBotTooltip
|
||||||
|
isOpen={isInlineBotTooltipOpen}
|
||||||
|
botId={inlineBotId}
|
||||||
|
allowedAttachmentOptions={allowedAttachmentOptions}
|
||||||
|
isGallery={isInlineBotTooltipGallery}
|
||||||
|
inlineBotResults={inlineBotResults}
|
||||||
|
switchPm={inlineBotSwitchPm}
|
||||||
|
onSelectResult={handleInlineBotSelect}
|
||||||
|
loadMore={loadMoreForInlineBot}
|
||||||
|
onClose={closeInlineBotTooltip}
|
||||||
|
/>
|
||||||
<SymbolMenu
|
<SymbolMenu
|
||||||
isOpen={isSymbolMenuOpen}
|
isOpen={isSymbolMenuOpen}
|
||||||
allowedAttachmentOptions={allowedAttachmentOptions}
|
allowedAttachmentOptions={allowedAttachmentOptions}
|
||||||
@ -965,10 +1024,10 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
shouldSchedule: messageListType === 'scheduled',
|
shouldSchedule: messageListType === 'scheduled',
|
||||||
botKeyboardMessageId: messageWithActualBotKeyboard ? messageWithActualBotKeyboard.id : undefined,
|
botKeyboardMessageId: messageWithActualBotKeyboard ? messageWithActualBotKeyboard.id : undefined,
|
||||||
isForwarding: chatId === global.forwardMessages.toChatId,
|
isForwarding: chatId === global.forwardMessages.toChatId,
|
||||||
canSuggestMembers: chat && isChatGroup(chat),
|
|
||||||
isPollModalOpen: global.isPollModalOpen,
|
isPollModalOpen: global.isPollModalOpen,
|
||||||
stickersForEmoji: global.stickers.forEmoji.stickers,
|
stickersForEmoji: global.stickers.forEmoji.stickers,
|
||||||
groupChatMembers: chat && chat.fullInfo && chat.fullInfo.members,
|
groupChatMembers: chat && chat.fullInfo && chat.fullInfo.members,
|
||||||
|
topInlineBotIds: global.topInlineBots && global.topInlineBots.userIds,
|
||||||
currentUserId: global.currentUserId,
|
currentUserId: global.currentUserId,
|
||||||
usersById: global.users.byId,
|
usersById: global.users.byId,
|
||||||
lastSyncTime: global.lastSyncTime,
|
lastSyncTime: global.lastSyncTime,
|
||||||
@ -981,6 +1040,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
baseEmojiKeywords: baseEmojiKeywords ? baseEmojiKeywords.keywords : undefined,
|
baseEmojiKeywords: baseEmojiKeywords ? baseEmojiKeywords.keywords : undefined,
|
||||||
emojiKeywords: emojiKeywords ? emojiKeywords.keywords : undefined,
|
emojiKeywords: emojiKeywords ? emojiKeywords.keywords : undefined,
|
||||||
serverTimeOffset: global.serverTimeOffset,
|
serverTimeOffset: global.serverTimeOffset,
|
||||||
|
inlineBots: global.inlineBots.byUsername,
|
||||||
|
isInlineBotLoading: global.inlineBots.isLoading,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
(setGlobal, actions): DispatchProps => pick(actions, [
|
(setGlobal, actions): DispatchProps => pick(actions, [
|
||||||
@ -1000,5 +1061,6 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
'openChat',
|
'openChat',
|
||||||
'addRecentEmoji',
|
'addRecentEmoji',
|
||||||
'loadEmojiKeywords',
|
'loadEmojiKeywords',
|
||||||
|
'sendInlineBotResult',
|
||||||
]),
|
]),
|
||||||
)(Composer));
|
)(Composer));
|
||||||
|
|||||||
15
src/components/middle/composer/InlineBotTooltip.async.tsx
Normal file
15
src/components/middle/composer/InlineBotTooltip.async.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import React, { FC, memo } from '../../../lib/teact/teact';
|
||||||
|
import { OwnProps } from './InlineBotTooltip';
|
||||||
|
import { Bundles } from '../../../util/moduleLoader';
|
||||||
|
|
||||||
|
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||||
|
|
||||||
|
const InlineBotTooltipAsync: FC<OwnProps> = (props) => {
|
||||||
|
const { isOpen } = props;
|
||||||
|
const InlineBotTooltip = useModuleLoader(Bundles.Extra, 'InlineBotTooltip', !isOpen);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
return InlineBotTooltip ? <InlineBotTooltip {...props} /> : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(InlineBotTooltipAsync);
|
||||||
54
src/components/middle/composer/InlineBotTooltip.scss
Normal file
54
src/components/middle/composer/InlineBotTooltip.scss
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
.InlineBotTooltip {
|
||||||
|
.switch-pm .title {
|
||||||
|
margin: 0 auto;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
--border-radius-default: 0;
|
||||||
|
|
||||||
|
&.gallery {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
grid-gap: 1px;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-pm {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
.ListItem-button {
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.GifButton {
|
||||||
|
grid-column-end: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.StickerButton {
|
||||||
|
width: initial;
|
||||||
|
height: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding-bottom: 100%;
|
||||||
|
border-radius: 0;
|
||||||
|
|
||||||
|
.AnimatedSticker, img, canvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
|
||||||
|
@media (min-width: 600px) {
|
||||||
|
top: .25rem;
|
||||||
|
left: .25rem;
|
||||||
|
width: calc(100% - .5rem) !important;
|
||||||
|
height: calc(100% - .5rem) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
225
src/components/middle/composer/InlineBotTooltip.tsx
Normal file
225
src/components/middle/composer/InlineBotTooltip.tsx
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
import React, {
|
||||||
|
FC, memo, useCallback, useEffect, useRef, useState,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
import { withGlobal } from '../../../lib/teact/teactn';
|
||||||
|
|
||||||
|
import { GlobalActions } from '../../../global/types';
|
||||||
|
import { ApiBotInlineMediaResult, ApiBotInlineResult, ApiBotInlineSwitchPm } from '../../../api/types';
|
||||||
|
import { IAllowedAttachmentOptions } from '../../../modules/helpers';
|
||||||
|
import { LoadMoreDirection } from '../../../types';
|
||||||
|
|
||||||
|
import { IS_TOUCH_ENV } from '../../../util/environment';
|
||||||
|
import setTooltipItemVisible from '../../../util/setTooltipItemVisible';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import captureKeyboardListeners from '../../../util/captureKeyboardListeners';
|
||||||
|
import cycleRestrict from '../../../util/cycleRestrict';
|
||||||
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
|
import { throttle } from '../../../util/schedulers';
|
||||||
|
import { pick } from '../../../util/iteratees';
|
||||||
|
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
||||||
|
|
||||||
|
import MediaResult from './inlineResults/MediaResult';
|
||||||
|
import ArticleResult from './inlineResults/ArticleResult';
|
||||||
|
import GifResult from './inlineResults/GifResult';
|
||||||
|
import StickerResult from './inlineResults/StickerResult';
|
||||||
|
import ListItem from '../../ui/ListItem';
|
||||||
|
import InfiniteScroll from '../../ui/InfiniteScroll';
|
||||||
|
|
||||||
|
import './InlineBotTooltip.scss';
|
||||||
|
|
||||||
|
const INTERSECTION_DEBOUNCE_MS = 200;
|
||||||
|
const runThrottled = throttle((cb) => cb(), 500, true);
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
botId?: number;
|
||||||
|
isGallery?: boolean;
|
||||||
|
allowedAttachmentOptions: IAllowedAttachmentOptions;
|
||||||
|
inlineBotResults?: (ApiBotInlineResult | ApiBotInlineMediaResult)[];
|
||||||
|
switchPm?: ApiBotInlineSwitchPm;
|
||||||
|
onSelectResult: (inlineResult: ApiBotInlineMediaResult | ApiBotInlineResult) => void;
|
||||||
|
loadMore: NoneToVoidFunction;
|
||||||
|
onClose: NoneToVoidFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DispatchProps = Pick<GlobalActions, ('sendBotCommand' | 'openChat' | 'sendInlineBotResult')>;
|
||||||
|
|
||||||
|
const InlineBotTooltip: FC<OwnProps & DispatchProps> = ({
|
||||||
|
isOpen,
|
||||||
|
botId,
|
||||||
|
isGallery,
|
||||||
|
inlineBotResults,
|
||||||
|
switchPm,
|
||||||
|
loadMore,
|
||||||
|
onClose,
|
||||||
|
openChat,
|
||||||
|
sendBotCommand,
|
||||||
|
onSelectResult,
|
||||||
|
}) => {
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false);
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState(-1);
|
||||||
|
const {
|
||||||
|
observe: observeIntersection,
|
||||||
|
} = useIntersectionObserver({
|
||||||
|
rootRef: containerRef,
|
||||||
|
debounceMs: INTERSECTION_DEBOUNCE_MS,
|
||||||
|
isDisabled: !isOpen,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedIndex(isGallery ? -1 : 0);
|
||||||
|
}, [inlineBotResults, isGallery]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTooltipItemVisible('.chat-item-clickable', selectedIndex, containerRef);
|
||||||
|
}, [selectedIndex]);
|
||||||
|
|
||||||
|
const getSelectedIndex = useCallback((newIndex: number) => {
|
||||||
|
if (!inlineBotResults || !inlineBotResults.length) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cycleRestrict(inlineBotResults.length, newIndex);
|
||||||
|
}, [inlineBotResults]);
|
||||||
|
|
||||||
|
const handleArrowKey = useCallback((value: number, e: KeyboardEvent) => {
|
||||||
|
if (isGallery) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
setSelectedIndex((index) => (getSelectedIndex(index + value)));
|
||||||
|
}, [isGallery, getSelectedIndex]);
|
||||||
|
|
||||||
|
const handleSelectInlineBotResult = useCallback((e: KeyboardEvent) => {
|
||||||
|
if (inlineBotResults && inlineBotResults.length && selectedIndex > -1) {
|
||||||
|
const inlineResult = inlineBotResults[selectedIndex];
|
||||||
|
if (inlineResult) {
|
||||||
|
e.preventDefault();
|
||||||
|
onSelectResult(inlineResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [inlineBotResults, onSelectResult, selectedIndex]);
|
||||||
|
|
||||||
|
const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => {
|
||||||
|
if (direction === LoadMoreDirection.Backwards) {
|
||||||
|
runThrottled(loadMore);
|
||||||
|
}
|
||||||
|
}, [loadMore]);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => (isOpen ? captureKeyboardListeners({
|
||||||
|
onEsc: onClose,
|
||||||
|
onUp: (e: KeyboardEvent) => handleArrowKey(-1, e),
|
||||||
|
onDown: (e: KeyboardEvent) => handleArrowKey(1, e),
|
||||||
|
onEnter: handleSelectInlineBotResult,
|
||||||
|
}) : undefined), [handleArrowKey, handleSelectInlineBotResult, isGallery, isOpen, onClose]);
|
||||||
|
|
||||||
|
const handleSendPm = useCallback(() => {
|
||||||
|
openChat({ id: botId });
|
||||||
|
sendBotCommand({ chatId: botId, command: `/start ${switchPm!.startParam}` });
|
||||||
|
}, [botId, openChat, sendBotCommand, switchPm]);
|
||||||
|
|
||||||
|
if (!shouldRender || !inlineBotResults || (!inlineBotResults.length && !switchPm)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const className = buildClassName(
|
||||||
|
'InlineBotTooltip composer-tooltip',
|
||||||
|
IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll',
|
||||||
|
isGallery && 'gallery',
|
||||||
|
transitionClassNames,
|
||||||
|
);
|
||||||
|
|
||||||
|
function renderSwitchPm() {
|
||||||
|
return (
|
||||||
|
<ListItem ripple className="switch-pm scroll-item" onClick={handleSendPm}>
|
||||||
|
<span className="title">{switchPm!.text}</span>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderContent() {
|
||||||
|
return inlineBotResults!.map((inlineBotResult, index) => {
|
||||||
|
switch (inlineBotResult.type) {
|
||||||
|
case 'gif':
|
||||||
|
return (
|
||||||
|
<GifResult
|
||||||
|
key={inlineBotResult.id}
|
||||||
|
inlineResult={inlineBotResult}
|
||||||
|
observeIntersection={observeIntersection}
|
||||||
|
onClick={onSelectResult}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'photo':
|
||||||
|
return (
|
||||||
|
<MediaResult
|
||||||
|
key={inlineBotResult.id}
|
||||||
|
isForGallery={isGallery}
|
||||||
|
inlineResult={inlineBotResult}
|
||||||
|
onClick={onSelectResult}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'sticker':
|
||||||
|
return (
|
||||||
|
<StickerResult
|
||||||
|
key={inlineBotResult.id}
|
||||||
|
inlineResult={inlineBotResult}
|
||||||
|
observeIntersection={observeIntersection}
|
||||||
|
onClick={onSelectResult}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'video':
|
||||||
|
case 'game':
|
||||||
|
return (
|
||||||
|
<MediaResult
|
||||||
|
key={inlineBotResult.id}
|
||||||
|
focus={selectedIndex === index}
|
||||||
|
inlineResult={inlineBotResult}
|
||||||
|
onClick={onSelectResult}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'article':
|
||||||
|
case 'audio':
|
||||||
|
return (
|
||||||
|
<ArticleResult
|
||||||
|
key={inlineBotResult.id}
|
||||||
|
focus={selectedIndex === index}
|
||||||
|
inlineResult={inlineBotResult}
|
||||||
|
onClick={onSelectResult}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InfiniteScroll
|
||||||
|
ref={containerRef}
|
||||||
|
className={className}
|
||||||
|
items={inlineBotResults}
|
||||||
|
itemSelector=".chat-item-clickable"
|
||||||
|
noFastList
|
||||||
|
onLoadMore={handleLoadMore}
|
||||||
|
sensitiveArea={160}
|
||||||
|
>
|
||||||
|
{switchPm && renderSwitchPm()}
|
||||||
|
{renderContent()}
|
||||||
|
</InfiniteScroll>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
undefined,
|
||||||
|
(setGlobal, actions): DispatchProps => pick(actions, [
|
||||||
|
'sendBotCommand', 'openChat', 'sendInlineBotResult',
|
||||||
|
]),
|
||||||
|
)(InlineBotTooltip));
|
||||||
@ -23,7 +23,6 @@
|
|||||||
.title {
|
.title {
|
||||||
margin-inline-end: .625rem;
|
margin-inline-end: .625rem;
|
||||||
max-width: 70%;
|
max-width: 70%;
|
||||||
flex: 1 0 auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.handle {
|
.handle {
|
||||||
|
|||||||
@ -3,14 +3,12 @@ import React, {
|
|||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import usePrevious from '../../../hooks/usePrevious';
|
import usePrevious from '../../../hooks/usePrevious';
|
||||||
|
|
||||||
import { ApiChatMember, ApiUser } from '../../../api/types';
|
import { ApiUser } from '../../../api/types';
|
||||||
|
|
||||||
import useShowTransition from '../../../hooks/useShowTransition';
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import captureKeyboardListeners from '../../../util/captureKeyboardListeners';
|
import captureKeyboardListeners from '../../../util/captureKeyboardListeners';
|
||||||
import findInViewport from '../../../util/findInViewport';
|
import setTooltipItemVisible from '../../../util/setTooltipItemVisible';
|
||||||
import isFullyVisible from '../../../util/isFullyVisible';
|
|
||||||
import fastSmoothScroll from '../../../util/fastSmoothScroll';
|
|
||||||
import cycleRestrict from '../../../util/cycleRestrict';
|
import cycleRestrict from '../../../util/cycleRestrict';
|
||||||
|
|
||||||
import ListItem from '../../ui/ListItem';
|
import ListItem from '../../ui/ListItem';
|
||||||
@ -18,38 +16,12 @@ import PrivateChatInfo from '../../common/PrivateChatInfo';
|
|||||||
|
|
||||||
import './MentionTooltip.scss';
|
import './MentionTooltip.scss';
|
||||||
|
|
||||||
const VIEWPORT_MARGIN = 8;
|
|
||||||
const SCROLL_MARGIN = 10;
|
|
||||||
|
|
||||||
function setItemVisible(index: number, containerRef: Record<string, any>) {
|
|
||||||
const container = containerRef.current!;
|
|
||||||
if (!container || index < 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { visibleIndexes, allElements } = findInViewport(
|
|
||||||
container,
|
|
||||||
'.chat-item-clickable',
|
|
||||||
VIEWPORT_MARGIN,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
if (!allElements.length || !allElements[index]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const first = visibleIndexes[0];
|
|
||||||
if (!visibleIndexes.includes(index)
|
|
||||||
|| (index === first && !isFullyVisible(container, allElements[first]))) {
|
|
||||||
const position = index > visibleIndexes[visibleIndexes.length - 1] ? 'start' : 'end';
|
|
||||||
fastSmoothScroll(container, allElements[index], position, SCROLL_MARGIN);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
filter: string;
|
filter: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onInsertUserName: (user: ApiUser, forceFocus?: boolean) => void;
|
onInsertUserName: (user: ApiUser, forceFocus?: boolean) => void;
|
||||||
filteredChatMembers?: ApiChatMember[];
|
filteredUsers?: ApiUser[];
|
||||||
usersById?: Record<number, ApiUser>;
|
usersById?: Record<number, ApiUser>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -59,19 +31,19 @@ const MentionTooltip: FC<OwnProps> = ({
|
|||||||
onClose,
|
onClose,
|
||||||
onInsertUserName,
|
onInsertUserName,
|
||||||
usersById,
|
usersById,
|
||||||
filteredChatMembers,
|
filteredUsers,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false);
|
const { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false);
|
||||||
|
|
||||||
const getSelectedIndex = useCallback((newIndex: number) => {
|
const getSelectedIndex = useCallback((newIndex: number) => {
|
||||||
if (!filteredChatMembers) {
|
if (!filteredUsers) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
const membersCount = filteredChatMembers!.length;
|
const membersCount = filteredUsers!.length;
|
||||||
return cycleRestrict(membersCount, newIndex);
|
return cycleRestrict(membersCount, newIndex);
|
||||||
}, [filteredChatMembers]);
|
}, [filteredUsers]);
|
||||||
|
|
||||||
const [selectedMentionIndex, setSelectedMentionIndex] = useState(-1);
|
const [selectedMentionIndex, setSelectedMentionIndex] = useState(-1);
|
||||||
|
|
||||||
@ -90,14 +62,14 @@ const MentionTooltip: FC<OwnProps> = ({
|
|||||||
}, [usersById, onInsertUserName]);
|
}, [usersById, onInsertUserName]);
|
||||||
|
|
||||||
const handleSelectMention = useCallback((e: KeyboardEvent) => {
|
const handleSelectMention = useCallback((e: KeyboardEvent) => {
|
||||||
if (filteredChatMembers && filteredChatMembers.length && selectedMentionIndex > -1) {
|
if (filteredUsers && filteredUsers.length && selectedMentionIndex > -1) {
|
||||||
const member = filteredChatMembers[selectedMentionIndex];
|
const member = filteredUsers[selectedMentionIndex];
|
||||||
if (member) {
|
if (member) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleUserSelect(member.userId, true);
|
handleUserSelect(member.id, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [filteredChatMembers, selectedMentionIndex, handleUserSelect]);
|
}, [filteredUsers, selectedMentionIndex, handleUserSelect]);
|
||||||
|
|
||||||
useEffect(() => (isOpen ? captureKeyboardListeners({
|
useEffect(() => (isOpen ? captureKeyboardListeners({
|
||||||
onEsc: onClose,
|
onEsc: onClose,
|
||||||
@ -108,28 +80,28 @@ const MentionTooltip: FC<OwnProps> = ({
|
|||||||
}) : undefined), [isOpen, onClose, handleArrowKey, handleSelectMention]);
|
}) : undefined), [isOpen, onClose, handleArrowKey, handleSelectMention]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (filteredChatMembers && !filteredChatMembers.length) {
|
if (filteredUsers && !filteredUsers.length) {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
}, [filteredChatMembers, onClose]);
|
}, [filteredUsers, onClose]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedMentionIndex(0);
|
setSelectedMentionIndex(0);
|
||||||
}, [filter]);
|
}, [filter]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setItemVisible(selectedMentionIndex, containerRef);
|
setTooltipItemVisible('.chat-item-clickable', selectedMentionIndex, containerRef);
|
||||||
}, [selectedMentionIndex]);
|
}, [selectedMentionIndex]);
|
||||||
|
|
||||||
const prevChatMembers = usePrevious(
|
const prevChatMembers = usePrevious(
|
||||||
filteredChatMembers && filteredChatMembers.length
|
filteredUsers && filteredUsers.length
|
||||||
? filteredChatMembers
|
? filteredUsers
|
||||||
: undefined,
|
: undefined,
|
||||||
shouldRender,
|
shouldRender,
|
||||||
);
|
);
|
||||||
const renderedChatMembers = filteredChatMembers && !filteredChatMembers.length
|
const renderedChatMembers = filteredUsers && !filteredUsers.length
|
||||||
? prevChatMembers
|
? prevChatMembers
|
||||||
: filteredChatMembers;
|
: filteredUsers;
|
||||||
|
|
||||||
if (!shouldRender || (renderedChatMembers && !renderedChatMembers.length)) {
|
if (!shouldRender || (renderedChatMembers && !renderedChatMembers.length)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -142,15 +114,15 @@ const MentionTooltip: FC<OwnProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className} ref={containerRef}>
|
<div className={className} ref={containerRef}>
|
||||||
{renderedChatMembers && renderedChatMembers.map(({ userId }, index) => (
|
{renderedChatMembers && renderedChatMembers.map(({ id }, index) => (
|
||||||
<ListItem
|
<ListItem
|
||||||
key={userId}
|
key={id}
|
||||||
className="chat-item-clickable scroll-item"
|
className="chat-item-clickable scroll-item"
|
||||||
onClick={() => handleUserSelect(userId)}
|
onClick={() => handleUserSelect(id)}
|
||||||
focus={selectedMentionIndex === index}
|
focus={selectedMentionIndex === index}
|
||||||
>
|
>
|
||||||
<PrivateChatInfo
|
<PrivateChatInfo
|
||||||
userId={userId}
|
userId={id}
|
||||||
avatarSize="small"
|
avatarSize="small"
|
||||||
withUsername
|
withUsername
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import useFlag from '../../../hooks/useFlag';
|
|||||||
import parseEmojiOnlyString from '../../common/helpers/parseEmojiOnlyString';
|
import parseEmojiOnlyString from '../../common/helpers/parseEmojiOnlyString';
|
||||||
import { isSelectionInsideInput } from './helpers/selection';
|
import { isSelectionInsideInput } from './helpers/selection';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import renderText from '../../common/helpers/renderText';
|
||||||
|
|
||||||
import TextFormatter from './TextFormatter';
|
import TextFormatter from './TextFormatter';
|
||||||
|
|
||||||
@ -36,6 +37,7 @@ type OwnProps = {
|
|||||||
editableInputId?: string;
|
editableInputId?: string;
|
||||||
html: string;
|
html: string;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
|
forcedPlaceholder?: string;
|
||||||
shouldSetFocus: boolean;
|
shouldSetFocus: boolean;
|
||||||
shouldSuppressFocus?: boolean;
|
shouldSuppressFocus?: boolean;
|
||||||
shouldSuppressTextFormatter?: boolean;
|
shouldSuppressTextFormatter?: boolean;
|
||||||
@ -78,6 +80,7 @@ const MessageInput: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
editableInputId,
|
editableInputId,
|
||||||
html,
|
html,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
forcedPlaceholder,
|
||||||
shouldSetFocus,
|
shouldSetFocus,
|
||||||
shouldSuppressFocus,
|
shouldSuppressFocus,
|
||||||
shouldSuppressTextFormatter,
|
shouldSuppressTextFormatter,
|
||||||
@ -322,10 +325,6 @@ const MessageInput: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (IS_TOUCH_ENV) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
focusInput();
|
focusInput();
|
||||||
}, [currentChatId, focusInput, replyingToId, shouldSetFocus]);
|
}, [currentChatId, focusInput, replyingToId, shouldSetFocus]);
|
||||||
|
|
||||||
@ -382,13 +381,14 @@ const MessageInput: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
onTouchCancel={handleTouchSelection}
|
onTouchCancel={handleTouchSelection}
|
||||||
/>
|
/>
|
||||||
<div ref={cloneRef} className={buildClassName(className, 'clone')} dir="auto" />
|
<div ref={cloneRef} className={buildClassName(className, 'clone')} dir="auto" />
|
||||||
<span className="placeholder-text" dir="auto">{placeholder}</span>
|
{!forcedPlaceholder && <span className="placeholder-text" dir="auto">{placeholder}</span>}
|
||||||
<TextFormatter
|
<TextFormatter
|
||||||
isOpen={isTextFormatterOpen}
|
isOpen={isTextFormatterOpen}
|
||||||
anchorPosition={textFormatterAnchorPosition}
|
anchorPosition={textFormatterAnchorPosition}
|
||||||
selectedRange={selectedRange}
|
selectedRange={selectedRange}
|
||||||
onClose={handleCloseTextFormatter}
|
onClose={handleCloseTextFormatter}
|
||||||
/>
|
/>
|
||||||
|
{forcedPlaceholder && <span className="forced-placeholder">{renderText(forcedPlaceholder!)}</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import {
|
|||||||
} from '../../../../lib/teact/teact';
|
} from '../../../../lib/teact/teact';
|
||||||
|
|
||||||
import { EDITABLE_INPUT_ID } from '../../../../config';
|
import { EDITABLE_INPUT_ID } from '../../../../config';
|
||||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../../util/environment';
|
|
||||||
import { MEMO_EMPTY_ARRAY } from '../../../../util/memo';
|
import { MEMO_EMPTY_ARRAY } from '../../../../util/memo';
|
||||||
import {
|
import {
|
||||||
EmojiData, EmojiModule, EmojiRawData, uncompressEmoji,
|
EmojiData, EmojiModule, EmojiRawData, uncompressEmoji,
|
||||||
@ -163,11 +162,9 @@ export default function useEmojiTooltip(
|
|||||||
if (atIndex !== -1) {
|
if (atIndex !== -1) {
|
||||||
onUpdateHtml(`${html.substr(0, atIndex)}${textEmoji}`);
|
onUpdateHtml(`${html.substr(0, atIndex)}${textEmoji}`);
|
||||||
const messageInput = document.getElementById(inputId)!;
|
const messageInput = document.getElementById(inputId)!;
|
||||||
if (!IS_SINGLE_COLUMN_LAYOUT) {
|
requestAnimationFrame(() => {
|
||||||
requestAnimationFrame(() => {
|
focusEditableElement(messageInput, true);
|
||||||
focusEditableElement(messageInput, true);
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unmarkIsOpen();
|
unmarkIsOpen();
|
||||||
|
|||||||
88
src/components/middle/composer/hooks/useInlineBotTooltip.ts
Normal file
88
src/components/middle/composer/hooks/useInlineBotTooltip.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { useCallback, useEffect, useState } from '../../../../lib/teact/teact';
|
||||||
|
import { getDispatch } from '../../../../lib/teact/teactn';
|
||||||
|
import { InlineBotSettings } from '../../../../types';
|
||||||
|
import useFlag from '../../../../hooks/useFlag';
|
||||||
|
import usePrevious from '../../../../hooks/usePrevious';
|
||||||
|
|
||||||
|
const tempEl = document.createElement('div');
|
||||||
|
const INLINE_BOT_QUERY_REGEXP = /^@([a-z0-9_]{1,32})[\u00A0\u0020]+(.*)/i;
|
||||||
|
const HAS_NEW_LINE = /^@([a-z0-9_]{1,32})[\u00A0\u0020]+\n{2,}/i;
|
||||||
|
|
||||||
|
export default function useInlineBotTooltip(
|
||||||
|
isAllowed: boolean,
|
||||||
|
chatId: number,
|
||||||
|
html: string,
|
||||||
|
inlineBots?: Record<string, false | InlineBotSettings>,
|
||||||
|
) {
|
||||||
|
const [isOpen, markIsOpen, unmarkIsOpen] = useFlag();
|
||||||
|
const [botSettings, setBotSettings] = useState<undefined | false | InlineBotSettings>();
|
||||||
|
const text = getPlainText(html);
|
||||||
|
const { queryInlineBot, resetInlineBot } = getDispatch();
|
||||||
|
const { username, query, canShowHelp } = parseStartWithUsernameString(text);
|
||||||
|
const usernameLowered = username.toLowerCase();
|
||||||
|
const prevUsername = usePrevious(username);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAllowed && usernameLowered && chatId) {
|
||||||
|
queryInlineBot({ chatId, username: usernameLowered, query });
|
||||||
|
}
|
||||||
|
}, [query, isAllowed, queryInlineBot, chatId, usernameLowered]);
|
||||||
|
|
||||||
|
const loadMore = useCallback(() => {
|
||||||
|
queryInlineBot({
|
||||||
|
chatId, username: usernameLowered, query, offset: botSettings && botSettings.offset,
|
||||||
|
});
|
||||||
|
}, [botSettings, chatId, query, queryInlineBot, usernameLowered]);
|
||||||
|
|
||||||
|
const inlineBotData = inlineBots && inlineBots[usernameLowered];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setBotSettings(inlineBotData);
|
||||||
|
}, [inlineBotData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
isAllowed && botSettings && botSettings.id
|
||||||
|
&& (botSettings.switchPm || (botSettings.results && botSettings.results.length))
|
||||||
|
) {
|
||||||
|
markIsOpen();
|
||||||
|
} else {
|
||||||
|
unmarkIsOpen();
|
||||||
|
}
|
||||||
|
}, [botSettings, isAllowed, markIsOpen, unmarkIsOpen]);
|
||||||
|
|
||||||
|
if (prevUsername !== username) {
|
||||||
|
resetInlineBot({ username: prevUsername });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isOpen,
|
||||||
|
closeTooltip: unmarkIsOpen,
|
||||||
|
loadMore,
|
||||||
|
username,
|
||||||
|
id: botSettings ? botSettings.id : undefined,
|
||||||
|
isGallery: botSettings ? botSettings.isGallery : undefined,
|
||||||
|
switchPm: botSettings ? botSettings.switchPm : undefined,
|
||||||
|
results: botSettings ? botSettings.results : undefined,
|
||||||
|
help: canShowHelp && botSettings && botSettings.help ? `@${username} ${botSettings.help}` : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlainText(html: string) {
|
||||||
|
tempEl.innerHTML = html.replace(/<br>/g, '\n');
|
||||||
|
|
||||||
|
return tempEl.innerText;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseStartWithUsernameString(text: string) {
|
||||||
|
const result = text.match(INLINE_BOT_QUERY_REGEXP);
|
||||||
|
if (!result) {
|
||||||
|
return { username: '', query: '', canShowHelp: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
username: result[1],
|
||||||
|
query: result[2],
|
||||||
|
canShowHelp: result[2] === '' && !text.match(HAS_NEW_LINE),
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,14 +1,19 @@
|
|||||||
import { useCallback, useEffect, useState } from '../../../../lib/teact/teact';
|
import {
|
||||||
|
useCallback, useEffect, useState, useMemo,
|
||||||
|
} from '../../../../lib/teact/teact';
|
||||||
|
|
||||||
import { ApiMessageEntityTypes, ApiChatMember, ApiUser } from '../../../../api/types';
|
import { ApiMessageEntityTypes, ApiChatMember, ApiUser } from '../../../../api/types';
|
||||||
import { EDITABLE_INPUT_ID } from '../../../../config';
|
import { EDITABLE_INPUT_ID } from '../../../../config';
|
||||||
import { getUserFirstOrLastName } from '../../../../modules/helpers';
|
import { getUserFirstOrLastName } from '../../../../modules/helpers';
|
||||||
import searchUserName from '../helpers/searchUserName';
|
import searchUserName from '../helpers/searchUserName';
|
||||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../../util/environment';
|
|
||||||
import focusEditableElement from '../../../../util/focusEditableElement';
|
import focusEditableElement from '../../../../util/focusEditableElement';
|
||||||
import useFlag from '../../../../hooks/useFlag';
|
import useFlag from '../../../../hooks/useFlag';
|
||||||
|
import { unique } from '../../../../util/iteratees';
|
||||||
|
import { throttle } from '../../../../util/schedulers';
|
||||||
|
|
||||||
|
const tempEl = document.createElement('div');
|
||||||
const RE_NOT_USERNAME_SEARCH = /[^@_\d\wа-яё]+/i;
|
const RE_NOT_USERNAME_SEARCH = /[^@_\d\wа-яё]+/i;
|
||||||
|
const runThrottled = throttle((cb) => cb(), 500, true);
|
||||||
|
|
||||||
export default function useMentionTooltip(
|
export default function useMentionTooltip(
|
||||||
canSuggestMembers: boolean | undefined,
|
canSuggestMembers: boolean | undefined,
|
||||||
@ -16,27 +21,42 @@ export default function useMentionTooltip(
|
|||||||
onUpdateHtml: (html: string) => void,
|
onUpdateHtml: (html: string) => void,
|
||||||
inputId: string = EDITABLE_INPUT_ID,
|
inputId: string = EDITABLE_INPUT_ID,
|
||||||
groupChatMembers?: ApiChatMember[],
|
groupChatMembers?: ApiChatMember[],
|
||||||
|
topInlineBotIds?: number[],
|
||||||
currentUserId?: number,
|
currentUserId?: number,
|
||||||
usersById?: Record<number, ApiUser>,
|
usersById?: Record<number, ApiUser>,
|
||||||
) {
|
) {
|
||||||
const [isOpen, markIsOpen, unmarkIsOpen] = useFlag();
|
const [isOpen, markIsOpen, unmarkIsOpen] = useFlag();
|
||||||
const [currentFilter, setCurrentFilter] = useState('');
|
const [currentFilter, setCurrentFilter] = useState('');
|
||||||
const [filteredMembers, setFilteredMembers] = useState<ApiChatMember[]>([]);
|
const [usersToMention, setUsersToMention] = useState<ApiUser[] | undefined>();
|
||||||
|
|
||||||
const getFilteredMembers = useCallback((filter) => {
|
const topInlineBots = useMemo(() => {
|
||||||
if (!groupChatMembers || !usersById) {
|
return (topInlineBotIds || []).map((id) => usersById && usersById[id]).filter<ApiUser>(Boolean as any);
|
||||||
return undefined;
|
}, [topInlineBotIds, usersById]);
|
||||||
|
|
||||||
|
const getFilteredUsers = useCallback((filter, withInlineBots: boolean) => {
|
||||||
|
if (!(groupChatMembers || topInlineBotIds) || !usersById) {
|
||||||
|
setUsersToMention(undefined);
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
runThrottled(() => {
|
||||||
|
const inlineBots = (withInlineBots ? topInlineBots : []).filter((inlineBot) => {
|
||||||
|
return !filter || searchUserName(filter, inlineBot);
|
||||||
|
});
|
||||||
|
|
||||||
return groupChatMembers.filter(({ userId }) => {
|
const chatMembers = (groupChatMembers || [])
|
||||||
const user = usersById[userId];
|
.map(({ userId }) => usersById[userId])
|
||||||
if (userId === currentUserId || !user) {
|
.filter((user) => {
|
||||||
return false;
|
if (!user || user.id === currentUserId) {
|
||||||
}
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return !filter || searchUserName(filter, user);
|
return !filter || searchUserName(filter, user);
|
||||||
|
});
|
||||||
|
|
||||||
|
setUsersToMention(unique(inlineBots.concat(chatMembers)));
|
||||||
});
|
});
|
||||||
}, [groupChatMembers, currentUserId, usersById]);
|
}, [currentUserId, groupChatMembers, topInlineBotIds, topInlineBots, usersById]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!canSuggestMembers || !html.length) {
|
if (!canSuggestMembers || !html.length) {
|
||||||
@ -44,22 +64,24 @@ export default function useMentionTooltip(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const usernameFilter = getUsernameFilter(html);
|
const usernameFilter = html.includes('@') && getUsernameFilter(html);
|
||||||
|
|
||||||
if (usernameFilter) {
|
if (usernameFilter) {
|
||||||
const filter = usernameFilter ? usernameFilter.substr(1) : '';
|
const filter = usernameFilter ? usernameFilter.substr(1) : '';
|
||||||
const membersToMention = getFilteredMembers(filter);
|
setCurrentFilter(filter);
|
||||||
if (membersToMention && membersToMention.length) {
|
getFilteredUsers(filter, canSuggestInlineBots(html));
|
||||||
markIsOpen();
|
|
||||||
setCurrentFilter(filter);
|
|
||||||
setFilteredMembers(membersToMention);
|
|
||||||
} else {
|
|
||||||
unmarkIsOpen();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
unmarkIsOpen();
|
unmarkIsOpen();
|
||||||
}
|
}
|
||||||
}, [canSuggestMembers, html, getFilteredMembers, markIsOpen, unmarkIsOpen]);
|
}, [canSuggestMembers, html, getFilteredUsers, markIsOpen, unmarkIsOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (usersToMention && usersToMention.length) {
|
||||||
|
markIsOpen();
|
||||||
|
} else {
|
||||||
|
unmarkIsOpen();
|
||||||
|
}
|
||||||
|
}, [markIsOpen, unmarkIsOpen, usersToMention]);
|
||||||
|
|
||||||
const insertMention = useCallback((user: ApiUser, forceFocus = false) => {
|
const insertMention = useCallback((user: ApiUser, forceFocus = false) => {
|
||||||
if (!user.username && !getUserFirstOrLastName(user)) {
|
if (!user.username && !getUserFirstOrLastName(user)) {
|
||||||
@ -80,11 +102,9 @@ export default function useMentionTooltip(
|
|||||||
if (atIndex !== -1) {
|
if (atIndex !== -1) {
|
||||||
onUpdateHtml(`${html.substr(0, atIndex)}${insertedHtml} `);
|
onUpdateHtml(`${html.substr(0, atIndex)}${insertedHtml} `);
|
||||||
const messageInput = document.getElementById(inputId)!;
|
const messageInput = document.getElementById(inputId)!;
|
||||||
if (!IS_SINGLE_COLUMN_LAYOUT) {
|
requestAnimationFrame(() => {
|
||||||
requestAnimationFrame(() => {
|
focusEditableElement(messageInput, forceFocus);
|
||||||
focusEditableElement(messageInput, forceFocus);
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unmarkIsOpen();
|
unmarkIsOpen();
|
||||||
@ -95,12 +115,11 @@ export default function useMentionTooltip(
|
|||||||
mentionFilter: currentFilter,
|
mentionFilter: currentFilter,
|
||||||
closeMentionTooltip: unmarkIsOpen,
|
closeMentionTooltip: unmarkIsOpen,
|
||||||
insertMention,
|
insertMention,
|
||||||
mentionFilteredMembers: filteredMembers,
|
mentionFilteredUsers: usersToMention,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUsernameFilter(html: string) {
|
function getUsernameFilter(html: string) {
|
||||||
const tempEl = document.createElement('div');
|
|
||||||
tempEl.innerHTML = html;
|
tempEl.innerHTML = html;
|
||||||
const text = tempEl.innerText.replace(/\n$/i, '');
|
const text = tempEl.innerText.replace(/\n$/i, '');
|
||||||
|
|
||||||
@ -116,3 +135,10 @@ function getUsernameFilter(html: string) {
|
|||||||
|
|
||||||
return lastWord;
|
return lastWord;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canSuggestInlineBots(html: string) {
|
||||||
|
tempEl.innerHTML = html;
|
||||||
|
const text = tempEl.innerText;
|
||||||
|
|
||||||
|
return text.startsWith('@');
|
||||||
|
}
|
||||||
|
|||||||
@ -0,0 +1,33 @@
|
|||||||
|
import React, { FC, memo, useCallback } from '../../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import { ApiBotInlineResult } from '../../../../api/types';
|
||||||
|
|
||||||
|
import BaseResult from './BaseResult';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
focus?: boolean;
|
||||||
|
inlineResult: ApiBotInlineResult;
|
||||||
|
onClick: (result: ApiBotInlineResult) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ArticleResult: FC<OwnProps> = ({ focus, inlineResult, onClick }) => {
|
||||||
|
const {
|
||||||
|
title, url, description, webThumbnail,
|
||||||
|
} = inlineResult;
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
onClick(inlineResult);
|
||||||
|
}, [inlineResult, onClick]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseResult
|
||||||
|
focus={focus}
|
||||||
|
thumbnail={webThumbnail}
|
||||||
|
title={title || url}
|
||||||
|
description={description}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ArticleResult);
|
||||||
62
src/components/middle/composer/inlineResults/BaseResult.scss
Normal file
62
src/components/middle/composer/inlineResults/BaseResult.scss
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
.BaseResult {
|
||||||
|
&.chat-item-clickable > .ListItem-button {
|
||||||
|
padding-left: 1.25rem !important;
|
||||||
|
padding-right: 1.25rem !important;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: block;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb {
|
||||||
|
background-color: var(--color-background-secondary);
|
||||||
|
flex: 0 0 3rem;
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
line-height: 3rem;
|
||||||
|
border-radius: var(--border-radius-default-tiny);
|
||||||
|
display: inline-flex;
|
||||||
|
align-content: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-inline-end: .5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
|
||||||
|
img:not(.emoji) {
|
||||||
|
width: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.emoji {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
margin: .75rem 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-inner {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: left;
|
||||||
|
unicode-bidi: plaintext;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
white-space: normal;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
text-align: left;
|
||||||
|
unicode-bidi: plaintext;
|
||||||
|
}
|
||||||
|
&[dir=rtl] .title,
|
||||||
|
&[dir=rtl] .description {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/components/middle/composer/inlineResults/BaseResult.tsx
Normal file
62
src/components/middle/composer/inlineResults/BaseResult.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import React, { FC, memo } from '../../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import { ApiWebDocument } from '../../../../api/types';
|
||||||
|
|
||||||
|
import { getFirstLetters } from '../../../../util/textFormat';
|
||||||
|
import renderText from '../../../common/helpers/renderText';
|
||||||
|
import useMedia from '../../../../hooks/useMedia';
|
||||||
|
|
||||||
|
import ListItem from '../../../ui/ListItem';
|
||||||
|
|
||||||
|
import './BaseResult.scss';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
focus?: boolean;
|
||||||
|
thumbnail?: ApiWebDocument;
|
||||||
|
thumbUrl?: string;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
transitionClassNames?: string;
|
||||||
|
onClick: NoneToVoidFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BaseResult: FC<OwnProps> = ({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
thumbnail,
|
||||||
|
thumbUrl,
|
||||||
|
focus,
|
||||||
|
transitionClassNames = '',
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
|
let content: string | undefined = '';
|
||||||
|
|
||||||
|
const thumbnailDataUrl = useMedia(thumbnail ? `webDocument:${thumbnail.url}` : undefined);
|
||||||
|
thumbUrl = thumbUrl || thumbnailDataUrl;
|
||||||
|
|
||||||
|
if (thumbUrl) {
|
||||||
|
content = (
|
||||||
|
<img src={thumbUrl} className={transitionClassNames} alt="" decoding="async" draggable="false" />
|
||||||
|
);
|
||||||
|
} else if (title) {
|
||||||
|
content = getFirstLetters(title, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
focus={focus}
|
||||||
|
className="BaseResult chat-item-clickable"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<span className="thumb">
|
||||||
|
{typeof content === 'string' ? renderText(content) : content}
|
||||||
|
</span>
|
||||||
|
<div className="content-inner">
|
||||||
|
{title && (<div className="title">{title}</div>)}
|
||||||
|
{description && (<div className="description">{description}</div>)}
|
||||||
|
</div>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(BaseResult);
|
||||||
40
src/components/middle/composer/inlineResults/GifResult.tsx
Normal file
40
src/components/middle/composer/inlineResults/GifResult.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import React, {
|
||||||
|
FC, memo, useCallback,
|
||||||
|
} from '../../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import { ApiBotInlineMediaResult, ApiBotInlineResult } from '../../../../api/types';
|
||||||
|
|
||||||
|
import { ObserveFn } from '../../../../hooks/useIntersectionObserver';
|
||||||
|
|
||||||
|
import GifButton from '../../../common/GifButton';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
inlineResult: ApiBotInlineMediaResult;
|
||||||
|
observeIntersection: ObserveFn;
|
||||||
|
onClick: (result: ApiBotInlineResult) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const GifResult: FC<OwnProps> = ({
|
||||||
|
inlineResult, observeIntersection, onClick,
|
||||||
|
}) => {
|
||||||
|
const { gif } = inlineResult;
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
onClick(inlineResult);
|
||||||
|
}, [inlineResult, onClick]);
|
||||||
|
|
||||||
|
if (!gif) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GifButton
|
||||||
|
gif={gif}
|
||||||
|
observeIntersection={observeIntersection}
|
||||||
|
className="chat-item-clickable"
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(GifResult);
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
.MediaResult {
|
||||||
|
height: 0;
|
||||||
|
padding-bottom: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
img {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/components/middle/composer/inlineResults/MediaResult.tsx
Normal file
76
src/components/middle/composer/inlineResults/MediaResult.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import React, { FC, memo, useCallback } from '../../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ApiBotInlineMediaResult, ApiBotInlineResult, ApiPhoto, ApiThumbnail, ApiWebDocument,
|
||||||
|
} from '../../../../api/types';
|
||||||
|
|
||||||
|
import useMedia from '../../../../hooks/useMedia';
|
||||||
|
import useTransitionForMedia from '../../../../hooks/useTransitionForMedia';
|
||||||
|
|
||||||
|
import BaseResult from './BaseResult';
|
||||||
|
|
||||||
|
import './MediaResult.scss';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
focus?: boolean;
|
||||||
|
isForGallery?: boolean;
|
||||||
|
inlineResult: ApiBotInlineMediaResult | ApiBotInlineResult;
|
||||||
|
onClick: (result: ApiBotInlineResult) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MediaResult: FC<OwnProps> = ({
|
||||||
|
focus, isForGallery, inlineResult, onClick,
|
||||||
|
}) => {
|
||||||
|
let photo: ApiPhoto | undefined;
|
||||||
|
let thumbnail: ApiThumbnail | undefined;
|
||||||
|
let webThumbnail: ApiWebDocument | undefined;
|
||||||
|
|
||||||
|
if ('photo' in inlineResult) {
|
||||||
|
photo = inlineResult.photo;
|
||||||
|
}
|
||||||
|
// For results with type=video (for example @stikstokbot)
|
||||||
|
if ('thumbnail' in inlineResult) {
|
||||||
|
thumbnail = inlineResult.thumbnail;
|
||||||
|
}
|
||||||
|
if ('webThumbnail' in inlineResult && isForGallery) {
|
||||||
|
webThumbnail = inlineResult.webThumbnail;
|
||||||
|
}
|
||||||
|
|
||||||
|
const thumbnailDataUrl = useMedia(webThumbnail ? `webDocument:${webThumbnail.url}` : undefined);
|
||||||
|
const mediaBlobUrl = useMedia(photo && `photo${photo.id}?size=m`);
|
||||||
|
const {
|
||||||
|
shouldRenderThumb, shouldRenderFullMedia, transitionClassNames,
|
||||||
|
} = useTransitionForMedia(mediaBlobUrl, 'slow');
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
onClick(inlineResult);
|
||||||
|
}, [inlineResult, onClick]);
|
||||||
|
|
||||||
|
if (isForGallery) {
|
||||||
|
return (
|
||||||
|
<div className="MediaResult chat-item-clickable" onClick={handleClick}>
|
||||||
|
{shouldRenderThumb && (
|
||||||
|
<img src={(photo && photo.thumbnail && photo.thumbnail.dataUri) || thumbnailDataUrl} alt="" />
|
||||||
|
)}
|
||||||
|
{shouldRenderFullMedia && (
|
||||||
|
<img src={mediaBlobUrl} className={`${transitionClassNames} full-media`} alt="" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, description } = inlineResult;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseResult
|
||||||
|
focus={focus}
|
||||||
|
thumbUrl={shouldRenderFullMedia ? mediaBlobUrl : (thumbnail && thumbnail.dataUri) || thumbnailDataUrl}
|
||||||
|
transitionClassNames={shouldRenderFullMedia ? transitionClassNames : undefined}
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(MediaResult);
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
.StickerResult {
|
||||||
|
height: 0;
|
||||||
|
padding-bottom: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background: transparent no-repeat center;
|
||||||
|
background-size: contain;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color .15s ease, opacity .3s ease !important;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.AnimatedSticker, img, canvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import React, { FC, memo } from '../../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import { ApiBotInlineMediaResult, ApiBotInlineResult } from '../../../../api/types';
|
||||||
|
|
||||||
|
import { STICKER_SIZE_INLINE_BOT_RESULT } from '../../../../config';
|
||||||
|
import { ObserveFn } from '../../../../hooks/useIntersectionObserver';
|
||||||
|
|
||||||
|
import StickerButton from '../../../common/StickerButton';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
inlineResult: ApiBotInlineMediaResult;
|
||||||
|
observeIntersection: ObserveFn;
|
||||||
|
onClick: (result: ApiBotInlineResult) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StickerResult: FC<OwnProps> = ({ inlineResult, observeIntersection, onClick }) => {
|
||||||
|
const { sticker } = inlineResult;
|
||||||
|
|
||||||
|
if (!sticker) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StickerButton
|
||||||
|
sticker={sticker}
|
||||||
|
size={STICKER_SIZE_INLINE_BOT_RESULT}
|
||||||
|
observeIntersection={observeIntersection}
|
||||||
|
title={sticker.emoji}
|
||||||
|
className="chat-item-clickable"
|
||||||
|
onClick={onClick}
|
||||||
|
clickArg={inlineResult}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(StickerResult);
|
||||||
@ -706,7 +706,7 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderSenderName() {
|
function renderSenderName() {
|
||||||
const shouldRender = !customShape && (
|
const shouldRender = !(customShape && !viaBotId) && (
|
||||||
(withSenderName && !photo && !video) || asForwarded || viaBotId || forceSenderName
|
(withSenderName && !photo && !video) || asForwarded || viaBotId || forceSenderName
|
||||||
) && (!isInDocumentGroup || isFirstInDocumentGroup);
|
) && (!isInDocumentGroup || isFirstInDocumentGroup);
|
||||||
|
|
||||||
@ -716,7 +716,7 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
|
|
||||||
let senderTitle;
|
let senderTitle;
|
||||||
let senderColor;
|
let senderColor;
|
||||||
if (senderPeer) {
|
if (senderPeer && !(customShape && viaBotId)) {
|
||||||
senderTitle = getSenderTitle(lang, senderPeer);
|
senderTitle = getSenderTitle(lang, senderPeer);
|
||||||
|
|
||||||
if (!asForwarded) {
|
if (!asForwarded) {
|
||||||
|
|||||||
@ -69,7 +69,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
& > .MessageMeta {
|
& > .MessageMeta {
|
||||||
position: relative;
|
|
||||||
top: auto;
|
top: auto;
|
||||||
bottom: -.5rem !important;
|
bottom: -.5rem !important;
|
||||||
margin-top: -.25rem;
|
margin-top: -.25rem;
|
||||||
@ -164,10 +163,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.via {
|
.via {
|
||||||
padding: 0 0.2rem;
|
padding-right: .25rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span + .via {
|
||||||
|
padding-left: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-title {
|
.admin-title {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
@ -311,6 +314,30 @@
|
|||||||
--border-top-left-radius: 0;
|
--border-top-left-radius: 0;
|
||||||
--border-top-right-radius: 0;
|
--border-top-right-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.custom-shape.is-via-bot {
|
||||||
|
font-size: inherit !important;
|
||||||
|
|
||||||
|
.message-title {
|
||||||
|
display: inline-flex;
|
||||||
|
position: relative;
|
||||||
|
top: .125rem;
|
||||||
|
max-width: calc(100% - 3rem);
|
||||||
|
margin-left: calc(100% - 3rem);
|
||||||
|
padding: 0 .5rem;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
border-radius: var(--border-radius-messages);
|
||||||
|
|
||||||
|
.Message.own & {
|
||||||
|
margin-left: -3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-inner {
|
||||||
|
margin-top: 0.375rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-content.media, .WebPage {
|
.message-content.media, .WebPage {
|
||||||
|
|||||||
@ -119,7 +119,6 @@ const GifSearch: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
>
|
>
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
</InfiniteScroll>
|
</InfiniteScroll>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -110,6 +110,7 @@ export const STICKER_SIZE_MODAL = 64;
|
|||||||
export const STICKER_SIZE_TWO_FA = 160;
|
export const STICKER_SIZE_TWO_FA = 160;
|
||||||
export const STICKER_SIZE_DISCUSSION_GROUPS = 140;
|
export const STICKER_SIZE_DISCUSSION_GROUPS = 140;
|
||||||
export const STICKER_SIZE_FOLDER_SETTINGS = 80;
|
export const STICKER_SIZE_FOLDER_SETTINGS = 80;
|
||||||
|
export const STICKER_SIZE_INLINE_BOT_RESULT = 100;
|
||||||
export const RECENT_STICKERS_LIMIT = 20;
|
export const RECENT_STICKERS_LIMIT = 20;
|
||||||
export const MEMOJI_STICKER_ID = 'MEMOJI_STICKER';
|
export const MEMOJI_STICKER_ID = 'MEMOJI_STICKER';
|
||||||
|
|
||||||
|
|||||||
@ -140,6 +140,7 @@ function updateCache() {
|
|||||||
'currentUserId',
|
'currentUserId',
|
||||||
'contactList',
|
'contactList',
|
||||||
'topPeers',
|
'topPeers',
|
||||||
|
'topInlineBots',
|
||||||
'recentEmojis',
|
'recentEmojis',
|
||||||
'push',
|
'push',
|
||||||
'shouldShowContextMenuHint',
|
'shouldShowContextMenuHint',
|
||||||
|
|||||||
@ -73,6 +73,11 @@ export const INITIAL_STATE: GlobalState = {
|
|||||||
search: {},
|
search: {},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
inlineBots: {
|
||||||
|
isLoading: false,
|
||||||
|
byUsername: {},
|
||||||
|
},
|
||||||
|
|
||||||
globalSearch: {},
|
globalSearch: {},
|
||||||
|
|
||||||
userSearch: {},
|
userSearch: {},
|
||||||
@ -91,6 +96,8 @@ export const INITIAL_STATE: GlobalState = {
|
|||||||
|
|
||||||
topPeers: {},
|
topPeers: {},
|
||||||
|
|
||||||
|
topInlineBots: {},
|
||||||
|
|
||||||
mediaViewer: {},
|
mediaViewer: {},
|
||||||
|
|
||||||
audioPlayer: {},
|
audioPlayer: {},
|
||||||
|
|||||||
@ -40,6 +40,7 @@ import {
|
|||||||
NotifyException,
|
NotifyException,
|
||||||
LangCode,
|
LangCode,
|
||||||
EmojiKeywords,
|
EmojiKeywords,
|
||||||
|
InlineBotSettings,
|
||||||
NewChatMembersProgress,
|
NewChatMembersProgress,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
@ -225,6 +226,11 @@ export type GlobalState = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
inlineBots: {
|
||||||
|
isLoading: boolean;
|
||||||
|
byUsername: Record<string, false | InlineBotSettings>;
|
||||||
|
};
|
||||||
|
|
||||||
globalSearch: {
|
globalSearch: {
|
||||||
query?: string;
|
query?: string;
|
||||||
date?: number;
|
date?: number;
|
||||||
@ -309,6 +315,12 @@ export type GlobalState = {
|
|||||||
lastRequestedAt?: number;
|
lastRequestedAt?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
topInlineBots: {
|
||||||
|
hash?: number;
|
||||||
|
userIds?: number[];
|
||||||
|
lastRequestedAt?: number;
|
||||||
|
};
|
||||||
|
|
||||||
webPagePreview?: ApiWebPage;
|
webPagePreview?: ApiWebPage;
|
||||||
|
|
||||||
forwardMessages: {
|
forwardMessages: {
|
||||||
@ -474,7 +486,8 @@ export type ActionTypes = (
|
|||||||
'faveSticker' | 'unfaveSticker' | 'toggleStickerSet' | 'loadAnimatedEmojis' |
|
'faveSticker' | 'unfaveSticker' | 'toggleStickerSet' | 'loadAnimatedEmojis' |
|
||||||
'loadStickersForEmoji' | 'clearStickersForEmoji' | 'loadEmojiKeywords' |
|
'loadStickersForEmoji' | 'clearStickersForEmoji' | 'loadEmojiKeywords' |
|
||||||
// bots
|
// bots
|
||||||
'clickInlineButton' | 'sendBotCommand' |
|
'clickInlineButton' | 'sendBotCommand' | 'loadTopInlineBots' | 'queryInlineBot' | 'sendInlineBotResult' |
|
||||||
|
'resetInlineBot' |
|
||||||
// misc
|
// misc
|
||||||
'openMediaViewer' | 'closeMediaViewer' | 'openAudioPlayer' | 'closeAudioPlayer' | 'openPollModal' | 'closePollModal' |
|
'openMediaViewer' | 'closeMediaViewer' | 'openAudioPlayer' | 'closeAudioPlayer' | 'openPollModal' | 'closePollModal' |
|
||||||
'loadWebPagePreview' | 'clearWebPagePreview' | 'loadWallpapers' | 'uploadWallpaper' | 'setDeviceToken' |
|
'loadWebPagePreview' | 'clearWebPagePreview' | 'loadWallpapers' | 'uploadWallpaper' | 'setDeviceToken' |
|
||||||
|
|||||||
@ -22,9 +22,11 @@ const { uploadFile } = require('./uploadFile');
|
|||||||
const { updateTwoFaSettings } = require('./2fa');
|
const { updateTwoFaSettings } = require('./2fa');
|
||||||
|
|
||||||
const DEFAULT_DC_ID = 2;
|
const DEFAULT_DC_ID = 2;
|
||||||
|
const WEBDOCUMENT_DC_ID = 4;
|
||||||
const DEFAULT_IPV4_IP = 'venus.web.telegram.org';
|
const DEFAULT_IPV4_IP = 'venus.web.telegram.org';
|
||||||
const DEFAULT_IPV6_IP = '[2001:67c:4e8:f002::a]';
|
const DEFAULT_IPV6_IP = '[2001:67c:4e8:f002::a]';
|
||||||
const BORROWED_SENDER_RELEASE_TIMEOUT = 30000; // 30 sec
|
const BORROWED_SENDER_RELEASE_TIMEOUT = 30000; // 30 sec
|
||||||
|
const WEBDOCUMENT_REQUEST_PART_SIZE = 131072; // 128kb
|
||||||
|
|
||||||
const PING_INTERVAL = 3000; // 3 sec
|
const PING_INTERVAL = 3000; // 3 sec
|
||||||
const PING_TIMEOUT = 5000; // 5 sec
|
const PING_TIMEOUT = 5000; // 5 sec
|
||||||
@ -571,10 +573,41 @@ class TelegramClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
_downloadWebDocument(media, args) {
|
async _downloadWebDocument(media) {
|
||||||
throw new Error('not implemented');
|
try {
|
||||||
|
const buff = [];
|
||||||
|
let offset = 0;
|
||||||
|
while (true) {
|
||||||
|
const downloaded = new requests.upload.GetWebFile({
|
||||||
|
location: new constructors.InputWebFileLocation({
|
||||||
|
url: media.url,
|
||||||
|
accessHash: media.accessHash,
|
||||||
|
}),
|
||||||
|
offset,
|
||||||
|
limit: WEBDOCUMENT_REQUEST_PART_SIZE,
|
||||||
|
});
|
||||||
|
const sender = await this._borrowExportedSender(WEBDOCUMENT_DC_ID);
|
||||||
|
const res = await sender.send(downloaded);
|
||||||
|
offset += 131072;
|
||||||
|
if (res.bytes.length) {
|
||||||
|
buff.push(res.bytes);
|
||||||
|
if (res.bytes.length < WEBDOCUMENT_REQUEST_PART_SIZE) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Buffer.concat(buff);
|
||||||
|
} catch (e) {
|
||||||
|
// the file is no longer saved in telegram's cache.
|
||||||
|
if (e.message === 'WEBFILE_NOT_AVAILABLE') {
|
||||||
|
return Buffer.alloc(0);
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// region Invoking Telegram request
|
// region Invoking Telegram request
|
||||||
/**
|
/**
|
||||||
* Invokes a MTProtoRequest (sends and receives it) and returns its result
|
* Invokes a MTProtoRequest (sends and receives it) and returns its result
|
||||||
|
|||||||
@ -1006,6 +1006,7 @@ messages.searchGlobal#4bc6589a flags:# folder_id:flags.0?int q:string filter:Mes
|
|||||||
messages.getDocumentByHash#338e2464 sha256:bytes size:int mime_type:string = Document;
|
messages.getDocumentByHash#338e2464 sha256:bytes size:int mime_type:string = Document;
|
||||||
messages.getSavedGifs#83bf3d52 hash:int = messages.SavedGifs;
|
messages.getSavedGifs#83bf3d52 hash:int = messages.SavedGifs;
|
||||||
messages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_point:flags.0?InputGeoPoint query:string offset:string = messages.BotResults;
|
messages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_point:flags.0?InputGeoPoint query:string offset:string = messages.BotResults;
|
||||||
|
messages.sendInlineBotResult#220815b0 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true hide_via:flags.11?true peer:InputPeer reply_to_msg_id:flags.0?int random_id:long query_id:long id:string schedule_date:flags.10?int = Updates;
|
||||||
messages.editMessage#48f71778 flags:# no_webpage:flags.1?true peer:InputPeer id:int message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.15?int = Updates;
|
messages.editMessage#48f71778 flags:# no_webpage:flags.1?true peer:InputPeer id:int message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.15?int = Updates;
|
||||||
messages.getBotCallbackAnswer#9342ca07 flags:# game:flags.1?true peer:InputPeer msg_id:int data:flags.0?bytes password:flags.2?InputCheckPasswordSRP = messages.BotCallbackAnswer;
|
messages.getBotCallbackAnswer#9342ca07 flags:# game:flags.1?true peer:InputPeer msg_id:int data:flags.0?bytes password:flags.2?InputCheckPasswordSRP = messages.BotCallbackAnswer;
|
||||||
messages.getPeerDialogs#e470bcfd peers:Vector<InputDialogPeer> = messages.PeerDialogs;
|
messages.getPeerDialogs#e470bcfd peers:Vector<InputDialogPeer> = messages.PeerDialogs;
|
||||||
@ -1048,6 +1049,7 @@ photos.getUserPhotos#91cd32a8 user_id:InputUser offset:int max_id:long limit:int
|
|||||||
upload.saveFilePart#b304a621 file_id:long file_part:int bytes:bytes = Bool;
|
upload.saveFilePart#b304a621 file_id:long file_part:int bytes:bytes = Bool;
|
||||||
upload.getFile#b15a9afc flags:# precise:flags.0?true cdn_supported:flags.1?true location:InputFileLocation offset:int limit:int = upload.File;
|
upload.getFile#b15a9afc flags:# precise:flags.0?true cdn_supported:flags.1?true location:InputFileLocation offset:int limit:int = upload.File;
|
||||||
upload.saveBigFilePart#de7b673d file_id:long file_part:int file_total_parts:int bytes:bytes = Bool;
|
upload.saveBigFilePart#de7b673d file_id:long file_part:int file_total_parts:int bytes:bytes = Bool;
|
||||||
|
upload.getWebFile#24e6818d location:InputWebFileLocation offset:int limit:int = upload.WebFile;
|
||||||
help.getConfig#c4f9186b = Config;
|
help.getConfig#c4f9186b = Config;
|
||||||
help.getNearestDc#1fb33026 = NearestDc;
|
help.getNearestDc#1fb33026 = NearestDc;
|
||||||
help.getSupport#9cdf08cd = help.Support;
|
help.getSupport#9cdf08cd = help.Support;
|
||||||
|
|||||||
@ -1006,6 +1006,7 @@ messages.searchGlobal#4bc6589a flags:# folder_id:flags.0?int q:string filter:Mes
|
|||||||
messages.getDocumentByHash#338e2464 sha256:bytes size:int mime_type:string = Document;
|
messages.getDocumentByHash#338e2464 sha256:bytes size:int mime_type:string = Document;
|
||||||
messages.getSavedGifs#83bf3d52 hash:int = messages.SavedGifs;
|
messages.getSavedGifs#83bf3d52 hash:int = messages.SavedGifs;
|
||||||
messages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_point:flags.0?InputGeoPoint query:string offset:string = messages.BotResults;
|
messages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_point:flags.0?InputGeoPoint query:string offset:string = messages.BotResults;
|
||||||
|
messages.sendInlineBotResult#220815b0 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true hide_via:flags.11?true peer:InputPeer reply_to_msg_id:flags.0?int random_id:long query_id:long id:string schedule_date:flags.10?int = Updates;
|
||||||
messages.editMessage#48f71778 flags:# no_webpage:flags.1?true peer:InputPeer id:int message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.15?int = Updates;
|
messages.editMessage#48f71778 flags:# no_webpage:flags.1?true peer:InputPeer id:int message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.15?int = Updates;
|
||||||
messages.getBotCallbackAnswer#9342ca07 flags:# game:flags.1?true peer:InputPeer msg_id:int data:flags.0?bytes password:flags.2?InputCheckPasswordSRP = messages.BotCallbackAnswer;
|
messages.getBotCallbackAnswer#9342ca07 flags:# game:flags.1?true peer:InputPeer msg_id:int data:flags.0?bytes password:flags.2?InputCheckPasswordSRP = messages.BotCallbackAnswer;
|
||||||
messages.getPeerDialogs#e470bcfd peers:Vector<InputDialogPeer> = messages.PeerDialogs;
|
messages.getPeerDialogs#e470bcfd peers:Vector<InputDialogPeer> = messages.PeerDialogs;
|
||||||
@ -1048,6 +1049,7 @@ photos.getUserPhotos#91cd32a8 user_id:InputUser offset:int max_id:long limit:int
|
|||||||
upload.saveFilePart#b304a621 file_id:long file_part:int bytes:bytes = Bool;
|
upload.saveFilePart#b304a621 file_id:long file_part:int bytes:bytes = Bool;
|
||||||
upload.getFile#b15a9afc flags:# precise:flags.0?true cdn_supported:flags.1?true location:InputFileLocation offset:int limit:int = upload.File;
|
upload.getFile#b15a9afc flags:# precise:flags.0?true cdn_supported:flags.1?true location:InputFileLocation offset:int limit:int = upload.File;
|
||||||
upload.saveBigFilePart#de7b673d file_id:long file_part:int file_total_parts:int bytes:bytes = Bool;
|
upload.saveBigFilePart#de7b673d file_id:long file_part:int file_total_parts:int bytes:bytes = Bool;
|
||||||
|
upload.getWebFile#24e6818d location:InputWebFileLocation offset:int limit:int = upload.WebFile;
|
||||||
help.getConfig#c4f9186b = Config;
|
help.getConfig#c4f9186b = Config;
|
||||||
help.getNearestDc#1fb33026 = NearestDc;
|
help.getNearestDc#1fb33026 = NearestDc;
|
||||||
help.getSupport#9cdf08cd = help.Support;
|
help.getSupport#9cdf08cd = help.Support;
|
||||||
|
|||||||
@ -1,10 +1,22 @@
|
|||||||
import { addReducer, getDispatch } from '../../../lib/teact/teactn';
|
import {
|
||||||
|
addReducer, getDispatch, getGlobal, setGlobal,
|
||||||
|
} from '../../../lib/teact/teactn';
|
||||||
|
|
||||||
import { ApiChat } from '../../../api/types';
|
import { ApiChat } from '../../../api/types';
|
||||||
|
import { InlineBotSettings } from '../../../types';
|
||||||
|
|
||||||
import { RE_TME_INVITE_LINK, RE_TME_LINK } from '../../../config';
|
import { RE_TME_INVITE_LINK, RE_TME_LINK } from '../../../config';
|
||||||
import { callApi } from '../../../api/gramjs';
|
import { callApi } from '../../../api/gramjs';
|
||||||
import { selectChatMessage, selectCurrentChat } from '../../selectors';
|
import {
|
||||||
|
selectChat, selectChatMessage, selectCurrentChat, selectCurrentMessageList, selectReplyingToId, selectUser,
|
||||||
|
} from '../../selectors';
|
||||||
|
import { addChats, addUsers } from '../../reducers';
|
||||||
|
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||||
|
import { throttle } from '../../../util/schedulers';
|
||||||
|
import { replaceInlineBotSettings, replaceInlineBotsIsLoading } from '../../reducers/bots';
|
||||||
|
|
||||||
|
const TOP_PEERS_REQUEST_COOLDOWN = 60000; // 1 min
|
||||||
|
const runThrottledForSearch = throttle((cb) => cb(), 500, false);
|
||||||
|
|
||||||
addReducer('clickInlineButton', (global, actions, payload) => {
|
addReducer('clickInlineButton', (global, actions, payload) => {
|
||||||
const { button } = payload;
|
const { button } = payload;
|
||||||
@ -52,9 +64,9 @@ addReducer('clickInlineButton', (global, actions, payload) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
addReducer('sendBotCommand', (global, actions, payload) => {
|
addReducer('sendBotCommand', (global, actions, payload) => {
|
||||||
const { command } = payload;
|
const { command, chatId } = payload;
|
||||||
const { currentUserId } = global;
|
const { currentUserId } = global;
|
||||||
const chat = selectCurrentChat(global);
|
const chat = chatId ? selectChat(global, chatId) : selectCurrentChat(global);
|
||||||
if (!currentUserId || !chat) {
|
if (!currentUserId || !chat) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -62,6 +74,192 @@ addReducer('sendBotCommand', (global, actions, payload) => {
|
|||||||
void sendBotCommand(chat, currentUserId, command);
|
void sendBotCommand(chat, currentUserId, command);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addReducer('loadTopInlineBots', (global) => {
|
||||||
|
const { serverTimeOffset } = global;
|
||||||
|
const { hash, lastRequestedAt } = global.topInlineBots;
|
||||||
|
|
||||||
|
if (lastRequestedAt && Date.now() + serverTimeOffset - lastRequestedAt < TOP_PEERS_REQUEST_COOLDOWN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const result = await callApi('fetchTopInlineBots', { hash });
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { hash: newHash, ids, users } = result;
|
||||||
|
|
||||||
|
let newGlobal = getGlobal();
|
||||||
|
newGlobal = addUsers(newGlobal, buildCollectionByKey(users, 'id'));
|
||||||
|
newGlobal = {
|
||||||
|
...newGlobal,
|
||||||
|
topInlineBots: {
|
||||||
|
...newGlobal.topInlineBots,
|
||||||
|
hash: newHash,
|
||||||
|
userIds: ids,
|
||||||
|
lastRequestedAt: Date.now(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setGlobal(newGlobal);
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
addReducer('queryInlineBot', ((global, actions, payload) => {
|
||||||
|
const {
|
||||||
|
chatId, username, query, offset,
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
let inlineBotData = global.inlineBots.byUsername[username];
|
||||||
|
|
||||||
|
if (inlineBotData === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inlineBotData === undefined) {
|
||||||
|
const { user: inlineBot, chat } = await callApi('fetchInlineBot', { username }) || {};
|
||||||
|
global = getGlobal();
|
||||||
|
if (!inlineBot || !chat) {
|
||||||
|
setGlobal(replaceInlineBotSettings(global, username, false));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global = addUsers(global, { [inlineBot.id]: inlineBot });
|
||||||
|
global = addChats(global, { [chat.id]: chat });
|
||||||
|
inlineBotData = {
|
||||||
|
id: inlineBot.id,
|
||||||
|
query: '',
|
||||||
|
offset: '',
|
||||||
|
switchPm: undefined,
|
||||||
|
canLoadMore: true,
|
||||||
|
results: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
global = replaceInlineBotSettings(global, username, inlineBotData);
|
||||||
|
setGlobal(global);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query === inlineBotData.query && !inlineBotData.canLoadMore) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void runThrottledForSearch(() => {
|
||||||
|
searchInlineBot({
|
||||||
|
username,
|
||||||
|
inlineBotData: inlineBotData as InlineBotSettings,
|
||||||
|
chatId,
|
||||||
|
query,
|
||||||
|
offset,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
}));
|
||||||
|
|
||||||
|
addReducer('sendInlineBotResult', (global, actions, payload) => {
|
||||||
|
const { id, queryId } = payload;
|
||||||
|
const currentMessageList = selectCurrentMessageList(global);
|
||||||
|
|
||||||
|
if (!currentMessageList || !id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { chatId, threadId } = currentMessageList;
|
||||||
|
|
||||||
|
const chat = selectChat(global, chatId)!;
|
||||||
|
|
||||||
|
actions.setReplyingToId({ messageId: undefined });
|
||||||
|
actions.clearWebPagePreview({ chatId, threadId, value: false });
|
||||||
|
|
||||||
|
void callApi('sendInlineBotResult', {
|
||||||
|
chat,
|
||||||
|
resultId: id,
|
||||||
|
queryId,
|
||||||
|
replyingTo: selectReplyingToId(global, chatId, threadId),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
addReducer('resetInlineBot', ((global, actions, payload) => {
|
||||||
|
const { username } = payload;
|
||||||
|
|
||||||
|
let inlineBotData = global.inlineBots.byUsername[username];
|
||||||
|
|
||||||
|
if (!inlineBotData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
inlineBotData = {
|
||||||
|
id: inlineBotData.id,
|
||||||
|
query: '',
|
||||||
|
offset: '',
|
||||||
|
switchPm: undefined,
|
||||||
|
canLoadMore: true,
|
||||||
|
results: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
setGlobal(replaceInlineBotSettings(global, username, inlineBotData));
|
||||||
|
}));
|
||||||
|
|
||||||
|
async function searchInlineBot({
|
||||||
|
username,
|
||||||
|
inlineBotData,
|
||||||
|
chatId,
|
||||||
|
query,
|
||||||
|
offset,
|
||||||
|
} : {
|
||||||
|
username: string;
|
||||||
|
inlineBotData: InlineBotSettings;
|
||||||
|
chatId: number;
|
||||||
|
query: string;
|
||||||
|
offset?: string;
|
||||||
|
}) {
|
||||||
|
let global = getGlobal();
|
||||||
|
const bot = selectUser(global, inlineBotData.id);
|
||||||
|
const chat = selectChat(global, chatId);
|
||||||
|
if (!bot || !chat) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global = replaceInlineBotsIsLoading(global, true);
|
||||||
|
global = replaceInlineBotSettings(global, username, {
|
||||||
|
...inlineBotData,
|
||||||
|
query,
|
||||||
|
...(inlineBotData && inlineBotData.query !== query && { offset: undefined }),
|
||||||
|
});
|
||||||
|
setGlobal(global);
|
||||||
|
|
||||||
|
const result = await callApi('fetchInlineBotResults', {
|
||||||
|
bot,
|
||||||
|
chat,
|
||||||
|
query,
|
||||||
|
offset,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newInlineBotData = global.inlineBots.byUsername[username];
|
||||||
|
global = replaceInlineBotsIsLoading(getGlobal(), false);
|
||||||
|
if (!result || !newInlineBotData || query !== newInlineBotData.query) {
|
||||||
|
setGlobal(global);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentIds = new Set((newInlineBotData.results || []).map((data) => data.id));
|
||||||
|
const newResults = result.results.filter((data) => !currentIds.has(data.id));
|
||||||
|
|
||||||
|
global = replaceInlineBotSettings(global, username, {
|
||||||
|
...newInlineBotData,
|
||||||
|
help: result.help,
|
||||||
|
isGallery: result.isGallery,
|
||||||
|
switchPm: result.switchPm,
|
||||||
|
canLoadMore: result.results.length > 0 && Boolean(result.nextOffset),
|
||||||
|
results: newInlineBotData.offset === '' || newInlineBotData.offset === result.nextOffset
|
||||||
|
? result.results
|
||||||
|
: (newInlineBotData.results || []).concat(newResults),
|
||||||
|
offset: newResults.length ? result.nextOffset : '',
|
||||||
|
});
|
||||||
|
|
||||||
|
setGlobal(global);
|
||||||
|
}
|
||||||
|
|
||||||
async function sendBotCommand(chat: ApiChat, currentUserId: number, command: string) {
|
async function sendBotCommand(chat: ApiChat, currentUserId: number, command: string) {
|
||||||
await callApi('sendMessage', {
|
await callApi('sendMessage', {
|
||||||
chat,
|
chat,
|
||||||
|
|||||||
@ -48,7 +48,7 @@ export function getMessageSummaryText(lang: LangFn, message: ApiMessage, noEmoji
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sticker) {
|
if (sticker) {
|
||||||
return `${sticker.emoji} ${lang('AttachSticker')} `;
|
return `${sticker.emoji || ''} ${lang('AttachSticker')}`.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audio) {
|
if (audio) {
|
||||||
|
|||||||
28
src/modules/reducers/bots.ts
Normal file
28
src/modules/reducers/bots.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { GlobalState } from '../../global/types';
|
||||||
|
import { InlineBotSettings } from '../../types';
|
||||||
|
|
||||||
|
|
||||||
|
export function replaceInlineBotSettings(
|
||||||
|
global: GlobalState, username: string, inlineBotSettings: InlineBotSettings | false,
|
||||||
|
): GlobalState {
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
inlineBots: {
|
||||||
|
...global.inlineBots,
|
||||||
|
byUsername: {
|
||||||
|
...global.inlineBots.byUsername,
|
||||||
|
[username]: inlineBotSettings,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replaceInlineBotsIsLoading(global: GlobalState, isLoading: boolean): GlobalState {
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
inlineBots: {
|
||||||
|
...global.inlineBots,
|
||||||
|
isLoading,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
ApiBotInlineMediaResult, ApiBotInlineResult, ApiBotInlineSwitchPm,
|
||||||
ApiLanguage, ApiMessage, ApiShippingAddress, ApiStickerSet,
|
ApiLanguage, ApiMessage, ApiShippingAddress, ApiStickerSet,
|
||||||
} from '../api/types';
|
} from '../api/types';
|
||||||
|
|
||||||
@ -305,3 +306,14 @@ export type EmojiKeywords = {
|
|||||||
version: number;
|
version: number;
|
||||||
keywords: Record<string, string[]>;
|
keywords: Record<string, string[]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type InlineBotSettings = {
|
||||||
|
id: number;
|
||||||
|
help?: string;
|
||||||
|
query?: string;
|
||||||
|
offset?: string;
|
||||||
|
canLoadMore?: boolean;
|
||||||
|
results?: (ApiBotInlineResult | ApiBotInlineMediaResult)[];
|
||||||
|
isGallery?: boolean;
|
||||||
|
switchPm?: ApiBotInlineSwitchPm;
|
||||||
|
};
|
||||||
|
|||||||
@ -13,7 +13,8 @@ export async function fetch(cacheName: string, key: string, type: Type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const request = new Request(key);
|
// To avoid the error "Request scheme 'webdocument' is unsupported"
|
||||||
|
const request = new Request(key.replace(/:/g, '_'));
|
||||||
const cache = await cacheApi.open(cacheName);
|
const cache = await cacheApi.open(cacheName);
|
||||||
const response = await cache.match(request);
|
const response = await cache.match(request);
|
||||||
if (!response) {
|
if (!response) {
|
||||||
@ -60,7 +61,8 @@ export async function save(cacheName: string, key: string, data: AnyLiteral | Bl
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const cacheData = typeof data === 'string' || data instanceof Blob ? data : JSON.stringify(data);
|
const cacheData = typeof data === 'string' || data instanceof Blob ? data : JSON.stringify(data);
|
||||||
const request = new Request(key);
|
// To avoid the error "Request scheme 'webdocument' is unsupported"
|
||||||
|
const request = new Request(key.replace(/:/g, '_'));
|
||||||
const response = new Response(cacheData);
|
const response = new Response(cacheData);
|
||||||
const cache = await cacheApi.open(cacheName);
|
const cache = await cacheApi.open(cacheName);
|
||||||
return await cache.put(request, response);
|
return await cache.put(request, response);
|
||||||
|
|||||||
@ -1,16 +1,22 @@
|
|||||||
|
import { IS_TOUCH_ENV } from './environment';
|
||||||
|
|
||||||
export default function focusEditableElement(element: HTMLElement, force?: boolean) {
|
export default function focusEditableElement(element: HTMLElement, force?: boolean) {
|
||||||
if (!force && element === document.activeElement) {
|
if (!force && element === document.activeElement) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selection = window.getSelection()!;
|
const selection = window.getSelection()!;
|
||||||
const range = document.createRange();
|
const range = document.createRange();
|
||||||
|
const lastChild = element.lastChild || element;
|
||||||
|
|
||||||
if (!element.lastChild || !element.lastChild.nodeValue) {
|
if (!IS_TOUCH_ENV && (!lastChild || !lastChild.nodeValue)) {
|
||||||
element.focus();
|
element.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
range.setStart(element.lastChild, element.lastChild.nodeValue.length);
|
range.selectNodeContents(lastChild);
|
||||||
|
// `false` means collapse to the end rather than the start
|
||||||
|
range.collapse(false);
|
||||||
selection.removeAllRanges();
|
selection.removeAllRanges();
|
||||||
selection.addRange(range);
|
selection.addRange(range);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,8 +11,10 @@ export default function insertHtmlInSelection(html: string) {
|
|||||||
if (lastInsertedNode) {
|
if (lastInsertedNode) {
|
||||||
range.setStartAfter(lastInsertedNode);
|
range.setStartAfter(lastInsertedNode);
|
||||||
range.setEndAfter(lastInsertedNode);
|
range.setEndAfter(lastInsertedNode);
|
||||||
selection.removeAllRanges();
|
} else {
|
||||||
selection.addRange(range);
|
range.collapse(false);
|
||||||
}
|
}
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
30
src/util/setTooltipItemVisible.ts
Normal file
30
src/util/setTooltipItemVisible.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import findInViewport from './findInViewport';
|
||||||
|
import isFullyVisible from './isFullyVisible';
|
||||||
|
import fastSmoothScroll from './fastSmoothScroll';
|
||||||
|
|
||||||
|
const VIEWPORT_MARGIN = 8;
|
||||||
|
const SCROLL_MARGIN = 10;
|
||||||
|
|
||||||
|
export default function setTooltipItemVisible(selector: string, index: number, containerRef: Record<string, any>) {
|
||||||
|
const container = containerRef.current!;
|
||||||
|
if (!container || index < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { visibleIndexes, allElements } = findInViewport(
|
||||||
|
container,
|
||||||
|
selector,
|
||||||
|
VIEWPORT_MARGIN,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!allElements.length || !allElements[index]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const first = visibleIndexes[0];
|
||||||
|
if (!visibleIndexes.includes(index)
|
||||||
|
|| (index === first && !isFullyVisible(container, allElements[first]))) {
|
||||||
|
const position = index > visibleIndexes[visibleIndexes.length - 1] ? 'start' : 'end';
|
||||||
|
fastSmoothScroll(container, allElements[index], position, SCROLL_MARGIN);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user