Implement Games (#1797)

This commit is contained in:
Alexander Zinchuk 2022-04-08 20:59:24 +02:00
parent cd4e9077d5
commit d7226fded9
37 changed files with 914 additions and 105 deletions

View File

@ -29,6 +29,7 @@ import {
ApiSponsoredMessage,
ApiUser,
ApiLocation,
ApiGame,
} from '../../types';
import {
@ -327,6 +328,9 @@ export function buildMessageMediaContent(media: GramJs.TypeMessageMedia): ApiMes
const location = buildLocationFromMedia(media);
if (location) return { location };
const game = buildGameFromMedia(media);
if (game) return { game };
return undefined;
}
@ -532,6 +536,16 @@ export function buildApiDocument(document: GramJs.TypeDocument): ApiDocument | u
}
} else if (SUPPORTED_VIDEO_CONTENT_TYPES.has(mimeType)) {
mediaType = 'video';
const videoAttribute = attributes
.find((a: any): a is GramJs.DocumentAttributeVideo => a instanceof GramJs.DocumentAttributeVideo);
if (videoAttribute) {
const { w: width, h: height } = videoAttribute;
mediaSize = {
width,
height,
};
}
}
}
@ -638,6 +652,33 @@ function buildGeoPoint(geo: GramJs.TypeGeoPoint): ApiLocation['geo'] | undefined
};
}
function buildGameFromMedia(media: GramJs.TypeMessageMedia): ApiGame | undefined {
if (!(media instanceof GramJs.MessageMediaGame)) {
return undefined;
}
return buildGame(media);
}
function buildGame(media: GramJs.MessageMediaGame): ApiGame | undefined {
const {
id, accessHash, shortName, title, description, photo: apiPhoto, document: apiDocument,
} = media.game;
const photo = apiPhoto instanceof GramJs.Photo ? buildApiPhoto(apiPhoto) : undefined;
const document = apiDocument instanceof GramJs.Document ? buildApiDocument(apiDocument) : undefined;
return {
id: id.toString(),
accessHash: accessHash.toString(),
shortName,
title,
description,
photo,
document,
};
}
export function buildPoll(poll: GramJs.Poll, pollResults: GramJs.PollResults): ApiPoll {
const { id, answers: rawAnswers } = poll;
const answers = rawAnswers.map((answer) => ({
@ -751,6 +792,7 @@ function buildAction(
const translationValues = [];
let type: ApiAction['type'] = 'other';
let photo: ApiPhoto | undefined;
let score: number | undefined;
const targetUserIds = 'users' in action
? action.users && action.users.map((id) => buildApiPeerId(id, 'user'))
@ -868,6 +910,10 @@ function buildAction(
} else if (action instanceof GramJs.MessageActionChatJoinedByRequest) {
text = 'ChatService.UserJoinedGroupByRequest';
translationValues.push('%action_origin%');
} else if (action instanceof GramJs.MessageActionGameScore) {
text = senderId === currentUserId ? 'ActionYouScoredInGame' : 'ActionUserScoredInGame';
translationValues.push('%score%');
score = action.score;
} else {
text = 'ChatList.UnsupportedMessage';
}
@ -887,21 +933,22 @@ function buildAction(
currency,
translationValues,
call,
score,
};
}
function buildReplyButtons(message: UniversalMessage): ApiReplyKeyboard | undefined {
const { id: messageId, replyMarkup, media } = message;
const { replyMarkup, media } = message;
// TODO Move to the proper button inside preview
if (!replyMarkup) {
if (media instanceof GramJs.MessageMediaWebPage && media.webpage instanceof GramJs.WebPage) {
if (media.webpage.type === 'telegram_message') {
return {
inlineButtons: [[{
type: 'url' as const,
type: 'url',
text: 'Show Message',
messageId,
value: media.webpage.url,
url: media.webpage.url,
}]],
};
}
@ -916,40 +963,103 @@ function buildReplyButtons(message: UniversalMessage): ApiReplyKeyboard | undefi
}
const markup = replyMarkup.rows.map(({ buttons }) => {
return buttons.map((button) => {
let { text } = button;
return buttons.map((button): ApiKeyboardButton => {
const { text } = button;
let type;
let value;
if (button instanceof GramJs.KeyboardButton) {
type = 'command';
value = text;
} else if (button instanceof GramJs.KeyboardButtonUrl) {
type = 'url';
value = button.url;
} else if (button instanceof GramJs.KeyboardButtonCallback) {
type = 'callback';
value = serializeBytes(button.data);
} else if (button instanceof GramJs.KeyboardButtonRequestPoll) {
type = 'requestPoll';
} else if (button instanceof GramJs.KeyboardButtonRequestPhone) {
type = 'requestSelfContact';
} else if (button instanceof GramJs.KeyboardButtonBuy) {
if (media instanceof GramJs.MessageMediaInvoice && media.receiptMsgId) {
text = 'PaymentReceipt';
value = media.receiptMsgId;
return {
type: 'command',
text,
};
}
if (button instanceof GramJs.KeyboardButtonUrl) {
if (button.url.includes('?startgroup=')) {
return {
type: 'unsupported',
text,
};
}
type = 'buy';
} else {
type = 'NOT_SUPPORTED';
return {
type: 'url',
text,
url: button.url,
};
}
if (button instanceof GramJs.KeyboardButtonCallback) {
if (button.requiresPassword) {
return {
type: 'unsupported',
text,
};
}
return {
type: 'callback',
text,
data: serializeBytes(button.data),
};
}
if (button instanceof GramJs.KeyboardButtonRequestPoll) {
return {
type: 'requestPoll',
text,
isQuiz: button.quiz,
};
}
if (button instanceof GramJs.KeyboardButtonRequestPhone) {
return {
type: 'requestPhone',
text,
};
}
if (button instanceof GramJs.KeyboardButtonBuy) {
if (media instanceof GramJs.MessageMediaInvoice && media.receiptMsgId) {
return {
type: 'receipt',
text: 'PaymentReceipt',
receiptMessageId: media.receiptMsgId,
};
}
return {
type: 'buy',
text,
};
}
if (button instanceof GramJs.KeyboardButtonGame) {
return {
type: 'game',
text,
};
}
if (button instanceof GramJs.KeyboardButtonSwitchInline) {
return {
type: 'switchBotInline',
text,
query: button.query,
isSamePeer: button.samePeer,
};
}
if (button instanceof GramJs.KeyboardButtonUserProfile) {
return {
type: 'userProfile',
text,
userId: button.userId.toString(),
};
}
return {
type,
type: 'unsupported',
text,
messageId,
value,
} as ApiKeyboardButton;
};
});
});

View File

@ -319,6 +319,9 @@ export function isMessageWithMedia(message: GramJs.Message | GramJs.UpdateServic
&& media.webpage.document.mimeType.startsWith('video')
)
)
) || (
media instanceof GramJs.MessageMediaGame
&& (media.game.document instanceof GramJs.Document || media.game.photo instanceof GramJs.Photo)
)
);
}
@ -441,6 +444,8 @@ export function buildSendMessageAction(action: ApiSendMessageAction) {
return new GramJs.SendMessageRecordAudioAction();
case 'chooseSticker':
return new GramJs.SendMessageChooseStickerAction();
case 'playingGame':
return new GramJs.SendMessageGamePlayAction();
}
return undefined;
}

View File

@ -31,6 +31,15 @@ export function addMessageToLocalDb(message: GramJs.Message | GramJs.MessageServ
localDb.documents[String(message.media.webpage.document.id)] = message.media.webpage.document;
}
if (message instanceof GramJs.Message
&& message.media instanceof GramJs.MessageMediaGame
) {
if (message.media.game.document instanceof GramJs.Document) {
localDb.documents[String(message.media.game.document.id)] = message.media.game.document;
}
addPhotoToLocalDb(message.media.game.photo);
}
if (message instanceof GramJs.MessageService && 'photo' in message.action) {
addPhotoToLocalDb(message.action.photo);
}

View File

@ -16,14 +16,15 @@ export function init() {
}
export async function answerCallbackButton({
chatId, accessHash, messageId, data,
chatId, accessHash, messageId, data, isGame,
}: {
chatId: string; accessHash?: string; messageId: number; data: string;
chatId: string; accessHash?: string; messageId: number; data?: string; isGame?: boolean;
}) {
const result = await invokeRequest(new GramJs.messages.GetBotCallbackAnswer({
peer: buildInputPeer(chatId, accessHash),
msgId: messageId,
data: deserializeBytes(data),
data: data ? deserializeBytes(data) : undefined,
game: isGame || undefined,
}));
return result ? omitVirtualClassFields(result) : undefined;

View File

@ -1102,6 +1102,7 @@ export async function forwardMessages({
isSilent,
scheduledAt,
sendAs,
withMyScore,
}: {
fromChat: ApiChat;
toChat: ApiChat;
@ -1110,6 +1111,7 @@ export async function forwardMessages({
isSilent?: boolean;
scheduledAt?: number;
sendAs?: ApiUser | ApiChat;
withMyScore?: boolean;
}) {
const messageIds = messages.map(({ id }) => id);
const randomIds = messages.map(generateRandomBigInt);
@ -1131,7 +1133,8 @@ export async function forwardMessages({
toPeer: buildInputPeer(toChat.id, toChat.accessHash),
randomId: randomIds,
id: messageIds,
...(isSilent && { sil2ent: isSilent }),
withMyScore: withMyScore || undefined,
silent: isSilent || undefined,
...(scheduledAt && { scheduleDate: scheduledAt }),
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
}), true);

View File

@ -174,6 +174,16 @@ interface ApiGeoLive {
export type ApiLocation = ApiGeo | ApiVenue | ApiGeoLive;
export type ApiGame = {
title: string;
description: string;
photo?: ApiPhoto;
shortName: string;
id: string;
accessHash: string;
document?: ApiDocument;
};
export type ApiNewPoll = {
summary: ApiPoll['summary'];
quiz?: {
@ -193,6 +203,7 @@ export interface ApiAction {
currency?: string;
translationValues: string[];
call?: Partial<ApiGroupCall>;
score?: number;
}
export interface ApiWebPage {
@ -272,6 +283,7 @@ export interface ApiMessage {
voice?: ApiVoice;
invoice?: ApiInvoice;
location?: ApiLocation;
game?: ApiGame;
};
date: number;
isOutgoing: boolean;
@ -363,13 +375,60 @@ export type ApiSponsoredMessage = {
expiresAt: number;
};
export interface ApiKeyboardButton {
type: 'command' | 'url' | 'callback' | 'requestPoll' | 'requestSelfContact' | 'buy' | 'NOT_SUPPORTED';
// KeyboardButtons
interface ApiKeyboardButtonSimple {
type: 'unsupported' | 'buy' | 'command' | 'requestPhone' | 'game';
text: string;
messageId: number;
value?: string;
}
interface ApiKeyboardButtonReceipt {
type: 'receipt';
text: string;
receiptMessageId: number;
}
interface ApiKeyboardButtonUrl {
type: 'url';
text: string;
url: string;
}
interface ApiKeyboardButtonCallback {
type: 'callback';
text: string;
data: string;
}
interface ApiKeyboardButtonRequestPoll {
type: 'requestPoll';
text: string;
isQuiz?: boolean;
}
interface ApiKeyboardButtonSwitchInline {
type: 'switchBotInline';
text: string;
query: string;
isSamePeer?: boolean;
}
interface ApiKeyboardButtonUserProfile {
type: 'userProfile';
text: string;
userId: string;
}
export type ApiKeyboardButton = (
ApiKeyboardButtonSimple
| ApiKeyboardButtonReceipt
| ApiKeyboardButtonUrl
| ApiKeyboardButtonCallback
| ApiKeyboardButtonRequestPoll
| ApiKeyboardButtonSwitchInline
| ApiKeyboardButtonUserProfile
);
export type ApiKeyboardButtons = ApiKeyboardButton[][];
export type ApiReplyKeyboard = {
keyboardPlaceholder?: string;
@ -385,7 +444,7 @@ export type ApiReportReason = 'spam' | 'violence' | 'pornography' | 'childAbuse'
| 'copyright' | 'geoIrrelevant' | 'fake' | 'illegalDrugs' | 'personalDetails' | 'other';
export type ApiSendMessageAction = {
type: 'cancel' | 'typing' | 'recordAudio' | 'chooseSticker';
type: 'cancel' | 'typing' | 'recordAudio' | 'chooseSticker' | 'playingGame';
};
export const MAIN_THREAD_ID = -1;

View File

@ -42,15 +42,17 @@ export function renderActionMessageText(
}
const {
text, translationValues, amount, currency, call,
text, translationValues, amount, currency, call, score,
} = message.content.action;
const content: TextPart[] = [];
const noLinks = options.asPlainText || options.asTextWithSpoilers;
const translationKey = text === 'Chat.Service.Group.UpdatedPinnedMessage1' && !targetMessage
? 'Message.PinnedGenericMessage'
: text;
let unprocessed = lang(translationKey, translationValues?.length ? translationValues : undefined);
if (translationKey.includes('ScoredInGame')) { // Translation hack for games
unprocessed = unprocessed.replace('un1', '%action_origin%').replace('un2', '%message%');
}
let processed: TextPart[];
if (unprocessed.includes('%payment_amount%')) {
@ -76,6 +78,16 @@ export function renderActionMessageText(
unprocessed = processed.pop() as string;
content.push(...processed);
if (unprocessed.includes('%score%')) {
processed = processPlaceholder(
unprocessed,
'%score%',
score!.toString(),
);
unprocessed = processed.pop() as string;
content.push(...processed);
}
processed = processPlaceholder(
unprocessed,
'%target_user%',

View File

@ -4,6 +4,7 @@ import React, {
import { getActions, getGlobal, withGlobal } from '../../global';
import { ApiChat, MAIN_THREAD_ID } from '../../api/types';
import { GlobalState } from '../../global/types';
import {
filterChatsByName,
@ -29,6 +30,7 @@ type StateProps = {
pinnedIds?: string[];
contactIds?: string[];
currentUserId?: string;
switchBotInline?: GlobalState['switchBotInline'];
};
const ForwardPicker: FC<OwnProps & StateProps> = ({
@ -39,10 +41,13 @@ const ForwardPicker: FC<OwnProps & StateProps> = ({
contactIds,
currentUserId,
isOpen,
switchBotInline,
}) => {
const {
setForwardChatId,
exitForwardMode,
openChatWithText,
resetSwitchBotInline,
} = getActions();
const lang = useLang();
@ -86,8 +91,14 @@ const ForwardPicker: FC<OwnProps & StateProps> = ({
}, [activeListIds, archivedListIds, chatsById, contactIds, currentUserId, filter, isOpen, lang, pinnedIds]);
const handleSelectUser = useCallback((userId: string) => {
setForwardChatId({ id: userId });
}, [setForwardChatId]);
if (switchBotInline) {
const text = `@${switchBotInline.botUsername} ${switchBotInline.query}`;
openChatWithText({ chatId: userId, text });
resetSwitchBotInline();
} else {
setForwardChatId({ id: userId });
}
}, [openChatWithText, resetSwitchBotInline, setForwardChatId, switchBotInline]);
const renderingChatAndContactIds = useCurrentOrPrev(chatAndContactIds, true)!;
@ -120,6 +131,7 @@ export default memo(withGlobal<OwnProps>(
orderedPinnedIds,
},
currentUserId,
switchBotInline,
} = global;
return {
@ -129,6 +141,7 @@ export default memo(withGlobal<OwnProps>(
pinnedIds: orderedPinnedIds.active,
contactIds: global.contactList?.userIds,
currentUserId,
switchBotInline,
};
},
)(ForwardPicker));

View File

@ -0,0 +1,48 @@
.GameModal {
.modal-dialog {
max-width: 80%;
height: 100%;
justify-content: center;
background-color: transparent;
border: none;
box-shadow: none;
margin: 0;
}
.modal-header {
display: none;
}
.modal-content {
overflow: hidden;
}
.game-frame {
width: 100%;
height: 100%;
border: 0;
border-radius: var(--border-radius-default);
}
@media (max-width: 600px) {
.modal-dialog {
background-color: var(--color-background);
max-width: 100% !important;
border-radius: 0;
}
.modal-header {
display: flex;
padding: 0.5rem;
}
.modal-content {
max-height: none;
padding: 0;
}
.game-frame {
border-radius: 0;
}
}
}

View File

@ -0,0 +1,95 @@
import React, {
FC, memo, useCallback, useEffect,
} from '../../lib/teact/teact';
import { getActions } from '../../lib/teact/teactn';
import { GlobalState } from '../../global/types';
import windowSize from '../../util/windowSize';
import useLang from '../../hooks/useLang';
import useSendMessageAction from '../../hooks/useSendMessageAction';
import useInterval from '../../hooks/useInterval';
import Modal from '../ui/Modal';
import './GameModal.scss';
type GameEvents = { eventType: 'share_score' | 'share_game' };
const PLAY_GAME_ACTION_INTERVAL = 5000;
type OwnProps = {
openedGame?: GlobalState['openedGame'];
gameTitle?: string;
};
const GameModal: FC<OwnProps> = ({ openedGame, gameTitle }) => {
const { closeGame, showNotification, openForwardMenu } = getActions();
const lang = useLang();
const { url, chatId, messageId } = openedGame || {};
const isOpen = Boolean(url);
const sendMessageAction = useSendMessageAction(chatId);
useInterval(() => {
sendMessageAction({ type: 'playingGame' });
}, isOpen ? PLAY_GAME_ACTION_INTERVAL : undefined);
const handleMessage = useCallback((event: MessageEvent<string>) => {
try {
const data = JSON.parse(event.data) as GameEvents;
if (data.eventType === 'share_score') {
openForwardMenu({ fromChatId: chatId, messageIds: [messageId], withMyScore: true });
closeGame();
}
if (data.eventType === 'share_game') {
showNotification({ message: 'Unsupported game action' });
}
} catch (e) {
// Ignore messages from other origins
}
}, [chatId, closeGame, messageId, openForwardMenu, showNotification]);
const handleLoad = useCallback((event: React.SyntheticEvent<HTMLIFrameElement>) => {
event.currentTarget.focus();
}, []);
useEffect(() => {
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [handleMessage]);
// Prevent refresh when rotating device
useEffect(() => {
if (!isOpen) return undefined;
windowSize.disableRefresh();
return () => {
windowSize.enableRefresh();
};
}, [isOpen]);
return (
<Modal
className="GameModal"
isOpen={isOpen}
onClose={closeGame}
title={gameTitle}
hasCloseButton
>
{isOpen && (
<iframe
className="game-frame"
onLoad={handleLoad}
src={url}
title={lang('AttachGame')}
sandbox="allow-scripts allow-same-origin allow-orientation-lock"
allow="fullscreen"
/>
)}
</Modal>
);
};
export default memo(GameModal);

View File

@ -5,6 +5,7 @@ import { getActions, withGlobal } from '../../global';
import { LangCode } from '../../types';
import { ApiMessage, ApiUpdateAuthorizationStateType, ApiUpdateConnectionStateType } from '../../api/types';
import { GlobalState } from '../../global/types';
import '../../global/actions/all';
import {
@ -40,6 +41,7 @@ import RightColumn from '../right/RightColumn';
import MediaViewer from '../mediaViewer/MediaViewer.async';
import AudioPlayer from '../middle/AudioPlayer';
import DownloadManager from './DownloadManager';
import GameModal from './GameModal';
import Notifications from './Notifications.async';
import Dialogs from './Dialogs.async';
import ForwardPicker from './ForwardPicker.async';
@ -76,6 +78,8 @@ type StateProps = {
addedSetIds?: string[];
newContactUserId?: string;
newContactByPhoneNumber?: boolean;
openedGame?: GlobalState['openedGame'];
gameTitle?: string;
};
const NOTIFICATION_INTERVAL = 1000;
@ -109,6 +113,8 @@ const Main: FC<StateProps> = ({
addedSetIds,
newContactUserId,
newContactByPhoneNumber,
openedGame,
gameTitle,
}) => {
const {
sync,
@ -340,6 +346,7 @@ const Main: FC<StateProps> = ({
userId={newContactUserId}
isByPhoneNumber={newContactByPhoneNumber}
/>
<GameModal openedGame={openedGame} gameTitle={gameTitle} />
<DownloadManager />
<CallFallbackConfirm isOpen={isCallFallbackConfirmOpen} />
<UnreadCount isForAppBadge />
@ -375,6 +382,9 @@ export default memo(withGlobal(
const audioMessage = audioChatId && audioMessageId
? selectChatMessage(global, audioChatId, audioMessageId)
: undefined;
const openedGame = global.openedGame;
const gameMessage = openedGame && selectChatMessage(global, openedGame.chatId, openedGame.messageId);
const gameTitle = gameMessage?.content.game?.title;
return {
connectionState: global.connectionState,
@ -400,6 +410,8 @@ export default memo(withGlobal(
addedSetIds: global.stickers.added.setIds,
newContactUserId: global.newContact?.userId,
newContactByPhoneNumber: global.newContact?.isByPhoneNumber,
openedGame,
gameTitle,
};
},
)(Main));

View File

@ -28,7 +28,7 @@ type StateProps = {
const BotKeyboardMenu: FC<OwnProps & StateProps> = ({
isOpen, message, onClose,
}) => {
const { clickInlineButton } = getActions();
const { clickBotInlineButton } = getActions();
const [handleMouseEnter, handleMouseLeave] = useMouseInside(isOpen, onClose);
const { isKeyboardSingleUse } = message || {};
@ -66,9 +66,9 @@ const BotKeyboardMenu: FC<OwnProps & StateProps> = ({
{row.map((button) => (
<Button
ripple
disabled={button.type === 'NOT_SUPPORTED'}
disabled={button.type === 'unsupported'}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => clickInlineButton({ button })}
onClick={() => clickBotInlineButton({ messageId: message.id, button })}
>
{button.text}
</Button>

View File

@ -42,6 +42,7 @@ import {
selectCanScheduleUntilOnline,
selectEditingScheduledDraft,
selectEditingDraft,
selectRequestedText,
} from '../../../global/selectors';
import {
getAllowedAttachmentOptions,
@ -127,7 +128,7 @@ type StateProps =
isRightColumnShown?: boolean;
isSelectModeActive?: boolean;
isForwarding?: boolean;
isPollModalOpen?: boolean;
pollModal: GlobalState['pollModal'];
botKeyboardMessageId?: number;
botKeyboardPlaceholder?: string;
withScheduledButton?: boolean;
@ -151,6 +152,7 @@ type StateProps =
sendAsChat?: ApiChat;
sendAsId?: string;
editingDraft?: ApiFormattedText;
requestedText?: string;
}
& Pick<GlobalState, 'connectionState'>;
@ -196,7 +198,7 @@ const Composer: FC<OwnProps & StateProps> = ({
isRightColumnShown,
isSelectModeActive,
isForwarding,
isPollModalOpen,
pollModal,
botKeyboardMessageId,
botKeyboardPlaceholder,
withScheduledButton,
@ -218,6 +220,7 @@ const Composer: FC<OwnProps & StateProps> = ({
sendAsChat,
sendAsId,
editingDraft,
requestedText,
}) => {
const {
sendMessage,
@ -234,6 +237,7 @@ const Composer: FC<OwnProps & StateProps> = ({
sendInlineBotResult,
loadSendAs,
loadFullChat,
resetOpenChatWithText,
} = getActions();
const lang = useLang();
@ -657,6 +661,17 @@ const Composer: FC<OwnProps & StateProps> = ({
}
}, [contentToBeScheduled, handleMessageSchedule, requestCalendar]);
useEffect(() => {
if (requestedText) {
setHtml(requestedText);
resetOpenChatWithText();
requestAnimationFrame(() => {
const messageInput = document.getElementById(EDITABLE_INPUT_ID)!;
focusEditableElement(messageInput, true);
});
}
}, [requestedText, resetOpenChatWithText]);
const handleStickerSelect = useCallback((
sticker: ApiSticker, isSilent?: boolean, isScheduleRequested?: boolean, shouldPreserveInput = false,
) => {
@ -945,7 +960,8 @@ const Composer: FC<OwnProps & StateProps> = ({
onClear={handleClearAttachment}
/>
<PollModal
isOpen={Boolean(isPollModalOpen)}
isOpen={pollModal.isOpen}
isQuiz={pollModal.isQuiz}
shouldBeAnonimous={isChannel}
onClear={closePollModal}
onSend={handlePollSend}
@ -1213,6 +1229,7 @@ export default memo(withGlobal<OwnProps>(
: (chat?.adminRights?.anonymous ? chat?.id : undefined);
const sendAsUser = sendAsId ? selectUser(global, sendAsId) : undefined;
const sendAsChat = !sendAsUser && sendAsId ? selectChat(global, sendAsId) : undefined;
const requestedText = selectRequestedText(global, chatId);
const editingDraft = messageListType === 'scheduled'
? selectEditingScheduledDraft(global, chatId)
@ -1238,7 +1255,7 @@ export default memo(withGlobal<OwnProps>(
botKeyboardMessageId,
botKeyboardPlaceholder: keyboardMessage?.keyboardPlaceholder,
isForwarding: chatId === global.forwardMessages.toChatId,
isPollModalOpen: global.isPollModalOpen,
pollModal: global.pollModal,
stickersForEmoji: global.stickers.forEmoji.stickers,
groupChatMembers: chat?.fullInfo?.members,
topInlineBotIds: global.topInlineBots?.userIds,
@ -1257,6 +1274,7 @@ export default memo(withGlobal<OwnProps>(
sendAsChat,
sendAsId,
editingDraft,
requestedText,
};
},
)(Composer));

View File

@ -425,7 +425,7 @@ export default memo(withGlobal<OwnProps>(
return {
messageSendKeyCombo,
replyingToId: chatId && threadId ? selectReplyingToId(global, chatId, threadId) : undefined,
noTabCapture: global.isPollModalOpen || global.payment.isPaymentModalOpen,
noTabCapture: global.pollModal.isOpen || global.payment.isPaymentModalOpen,
};
},
)(MessageInput));

View File

@ -20,6 +20,7 @@ import './PollModal.scss';
export type OwnProps = {
isOpen: boolean;
shouldBeAnonimous?: boolean;
isQuiz?: boolean;
onSend: (pollSummary: ApiNewPoll) => void;
onClear: () => void;
};
@ -31,7 +32,7 @@ const MAX_QUESTION_LENGTH = 255;
const MAX_SOLUTION_LENGTH = 200;
const PollModal: FC<OwnProps> = ({
isOpen, shouldBeAnonimous, onSend, onClear,
isOpen, isQuiz, shouldBeAnonimous, onSend, onClear,
}) => {
// eslint-disable-next-line no-null/no-null
const questionInputRef = useRef<HTMLInputElement>(null);
@ -44,8 +45,8 @@ const PollModal: FC<OwnProps> = ({
const [options, setOptions] = useState<string[]>(['']);
const [isAnonymous, setIsAnonymous] = useState(true);
const [isMultipleAnswers, setIsMultipleAnswers] = useState(false);
const [isQuizMode, setIsQuizMode] = useState(false);
const [solution, setSolution] = useState<string>();
const [isQuizMode, setIsQuizMode] = useState(isQuiz || false);
const [solution, setSolution] = useState<string>('');
const [correctOption, setCorrectOption] = useState<string>();
const [hasErrors, setHasErrors] = useState<boolean>(false);
@ -64,12 +65,12 @@ const PollModal: FC<OwnProps> = ({
setOptions(['']);
setIsAnonymous(true);
setIsMultipleAnswers(false);
setIsQuizMode(false);
setIsQuizMode(isQuiz || false);
setSolution('');
setCorrectOption('');
setHasErrors(false);
}
}, [isOpen]);
}, [isQuiz, isOpen]);
useEffect(() => focusInput(questionInputRef), [focusInput, isOpen]);
@ -338,7 +339,7 @@ const PollModal: FC<OwnProps> = ({
<Checkbox
label={lang('PollQuiz')}
checked={isQuizMode}
disabled={isMultipleAnswers}
disabled={isMultipleAnswers || isQuiz !== undefined}
onChange={handleQuizModeChange}
/>
{isQuizMode && (

View File

@ -0,0 +1,34 @@
.Game {
.title {
font-weight: 500;
}
.description {
line-height: 1.2;
margin-bottom: 0.75rem;
}
.preview {
position: relative;
max-width: 100%;
margin-bottom: 0.25rem;
margin-top: 0.25rem;
cursor: pointer;
}
.preview-content {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 0.5rem;
overflow: hidden;
}
.skeleton {
aspect-ratio: 16/9;
}
}

View File

@ -0,0 +1,84 @@
import React, {
FC, memo,
} from '../../../lib/teact/teact';
import { ApiMessage } from '../../../api/types';
import { getActions } from '../../../global';
import { getGamePreviewPhotoHash, getGamePreviewVideoHash, getMessageText } from '../../../global/helpers';
import useMedia from '../../../hooks/useMedia';
import Skeleton from '../../ui/Skeleton';
import './Game.scss';
const DEFAULT_PREVIEW_DIMENSIONS = {
width: 480,
height: 270,
};
type OwnProps = {
message: ApiMessage;
canAutoLoadMedia?: boolean;
lastSyncTime?: number;
};
const Game: FC<OwnProps> = ({
message,
canAutoLoadMedia,
lastSyncTime,
}) => {
const { clickBotInlineButton } = getActions();
const game = message.content.game!;
const {
title, description,
} = game;
const photoHash = Boolean(lastSyncTime) && getGamePreviewPhotoHash(game);
const videoHash = Boolean(lastSyncTime) && getGamePreviewVideoHash(game);
const photoBlobUrl = useMedia(photoHash, !canAutoLoadMedia);
const videoBlobUrl = useMedia(videoHash, !canAutoLoadMedia);
const handleGameClick = () => {
clickBotInlineButton({
messageId: message.id,
button: message.inlineButtons![0][0],
});
};
return (
<div className="Game">
<div
className="preview"
style={`width: ${DEFAULT_PREVIEW_DIMENSIONS.width}px; height: ${DEFAULT_PREVIEW_DIMENSIONS.height}px`}
onClick={handleGameClick}
>
{!photoBlobUrl && !videoBlobUrl && (
<Skeleton className="skeleton preview-content" />
)}
{photoBlobUrl && (
<img
className="preview-content"
src={photoBlobUrl}
alt={title}
/>
)}
{videoBlobUrl && (
<video
className="preview-content"
playsInline
muted
autoPlay
loop
src={videoBlobUrl}
/>
)}
</div>
<div className="title">{title}</div>
{!getMessageText(message) && <div className="description">{description}</div>}
</div>
);
};
export default memo(Game);

View File

@ -12,12 +12,11 @@ import './InlineButtons.scss';
type OwnProps = {
message: ApiMessage;
onClick: ({ button }: { button: ApiKeyboardButton }) => void;
onClick: ({ messageId, button }: { messageId: number; button: ApiKeyboardButton }) => void;
};
const InlineButtons: FC<OwnProps> = ({ message, onClick }) => {
const lang = useLang();
return (
<div className="InlineButtons">
{message.inlineButtons!.map((row) => (
@ -26,13 +25,14 @@ const InlineButtons: FC<OwnProps> = ({ message, onClick }) => {
<Button
size="tiny"
ripple
disabled={button.type === 'NOT_SUPPORTED'}
disabled={button.type === 'unsupported'}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => onClick({ button })}
onClick={() => onClick({ messageId: message.id, button })}
>
{renderText(lang(button.text))}
{button.type === 'buy' && <i className="icon-card" />}
{button.type === 'url' && !button.value!.match(RE_TME_LINK) && <i className="icon-arrow-right" />}
{['buy', 'receipt'].includes(button.type) && <i className="icon-card" />}
{button.type === 'url' && !RE_TME_LINK.test(button.url) && <i className="icon-arrow-right" />}
{button.type === 'switchBotInline' && <i className="icon-share-filled" />}
</Button>
))}
</div>

View File

@ -106,6 +106,7 @@ import Poll from './Poll';
import WebPage from './WebPage';
import Invoice from './Invoice';
import Location from './Location';
import Game from './Game';
import Album from './Album';
import RoundVideo from './RoundVideo';
import InlineButtons from './InlineButtons';
@ -281,7 +282,7 @@ const Message: FC<OwnProps & StateProps> = ({
}) => {
const {
toggleMessageSelection,
clickInlineButton,
clickBotInlineButton,
disableContextMenuHint,
} = getActions();
@ -461,7 +462,7 @@ const Message: FC<OwnProps & StateProps> = ({
);
const {
text, photo, video, audio, voice, document, sticker, contact, poll, webPage, invoice, location,
text, photo, video, audio, voice, document, sticker, contact, poll, webPage, invoice, location, game,
} = getMessageContent(message);
const contentClassName = buildContentClassName(message, {
@ -753,6 +754,13 @@ const Message: FC<OwnProps & StateProps> = ({
{poll && (
<Poll message={message} poll={poll} onSendVote={handleVoteSend} />
)}
{game && (
<Game
message={message}
canAutoLoadMedia={canAutoLoadMedia}
lastSyncTime={lastSyncTime}
/>
)}
{!hasAnimatedEmoji && textParts && (
<p className={textContentClass} dir="auto">
{textParts}
@ -935,7 +943,7 @@ const Message: FC<OwnProps & StateProps> = ({
)}
</div>
{message.inlineButtons && (
<InlineButtons message={message} onClick={clickInlineButton} />
<InlineButtons message={message} onClick={clickBotInlineButton} />
)}
{reactionsPosition === 'outside' && (
<Reactions

View File

@ -1,6 +1,9 @@
.Skeleton {
position: relative;
background-color: var(--color-skeleton-background);
width: 100%;
height: 100%;
&.round {
border-radius: 50%;
}

View File

@ -13,7 +13,11 @@ type OwnProps = {
};
const Skeleton: FC<OwnProps> = ({
variant = 'rectangular', animation = 'wave', width, height, className,
variant = 'rectangular',
animation = 'wave',
width,
height,
className,
}) => {
const classNames = buildClassName('Skeleton', variant, animation, className);
const style = (width ? `width: ${width}px;` : '') + (height ? `height: ${height}px;` : '');

View File

@ -18,37 +18,41 @@ import { buildCollectionByKey } from '../../../util/iteratees';
import { debounce } from '../../../util/schedulers';
import { replaceInlineBotSettings, replaceInlineBotsIsLoading } from '../../reducers/bots';
import { getServerTime } from '../../../util/serverTime';
import PopupManager from '../../../util/PopupManager';
const GAMEE_URL = 'https://prizes.gamee.com/';
const TOP_PEERS_REQUEST_COOLDOWN = 60; // 1 min
const runDebouncedForSearch = debounce((cb) => cb(), 500, false);
addActionHandler('clickInlineButton', (global, actions, payload) => {
const { button } = payload;
addActionHandler('clickBotInlineButton', (global, actions, payload) => {
const { messageId, button } = payload;
switch (button.type) {
case 'command':
actions.sendBotCommand({ command: button.value });
actions.sendBotCommand({ command: button.text });
break;
case 'url':
if (button.value.match(RE_TME_LINK) || button.value.match(RE_TG_LINK)) {
actions.openTelegramLink({ url: button.value });
case 'url': {
const { url } = button;
if (url.match(RE_TME_LINK) || url.match(RE_TG_LINK)) {
actions.openTelegramLink({ url });
} else {
actions.toggleSafeLinkModal({ url: button.value });
actions.toggleSafeLinkModal({ url });
}
break;
}
case 'callback': {
const chat = selectCurrentChat(global);
if (!chat) {
return;
}
void answerCallbackButton(chat, button.messageId, button.value);
void answerCallbackButton(chat, messageId, button.data);
break;
}
case 'requestPoll':
actions.openPollModal();
actions.openPollModal({ isQuiz: button.isQuiz });
break;
case 'requestSelfContact': {
case 'requestPhone': {
const user = global.currentUserId ? selectUser(global, global.currentUserId) : undefined;
if (!user) {
return;
@ -63,20 +67,44 @@ addActionHandler('clickInlineButton', (global, actions, payload) => {
});
break;
}
case 'receipt': {
const chat = selectCurrentChat(global);
if (!chat) {
return;
}
const { receiptMessageId } = button;
actions.getReceipt({ receiptMessageId, chatId: chat.id, messageId });
break;
}
case 'buy': {
const chat = selectCurrentChat(global);
const { messageId, value } = button;
if (!chat) {
return;
}
if (value) {
actions.getReceipt({ receiptMessageId: value, chatId: chat.id, messageId });
} else {
actions.getPaymentForm({ chat, messageId });
actions.setInvoiceMessageInfo(selectChatMessage(global, chat.id, messageId));
actions.openPaymentModal({ chatId: chat.id, messageId });
actions.getPaymentForm({ chat, messageId });
actions.setInvoiceMessageInfo(selectChatMessage(global, chat.id, messageId));
actions.openPaymentModal({ chatId: chat.id, messageId });
break;
}
case 'game': {
const chat = selectCurrentChat(global);
if (!chat) {
return;
}
void answerCallbackButton(chat, messageId, undefined, true);
break;
}
case 'switchBotInline': {
const { query, isSamePeer } = button;
actions.switchBotInline({ query, isSamePeer, messageId });
break;
}
case 'userProfile': {
const { userId } = button;
actions.openChatWithInfo({ id: userId });
break;
}
}
@ -193,6 +221,45 @@ addActionHandler('queryInlineBot', async (global, actions, payload) => {
});
});
addActionHandler('switchBotInline', (global, actions, payload) => {
const { query, isSamePeer, messageId } = payload;
const chat = selectCurrentChat(global);
if (!chat) {
return undefined;
}
const message = selectChatMessage(global, chat.id, messageId);
if (!message) {
return undefined;
}
const botSender = selectChatBot(global, message.senderId!);
if (!botSender) {
return undefined;
}
const text = `@${botSender.username} ${query}`;
if (isSamePeer) {
actions.openChatWithText({ chatId: chat.id, text });
return undefined;
}
return {
...global,
switchBotInline: {
query,
botUsername: botSender.username,
},
};
});
addActionHandler('resetSwitchBotInline', (global) => {
return {
...global,
switchBotInline: undefined,
};
});
addActionHandler('sendInlineBotResult', (global, actions, payload) => {
const {
id, queryId, isSilent, scheduledAt,
@ -327,19 +394,34 @@ async function sendBotCommand(
});
}
async function answerCallbackButton(chat: ApiChat, messageId: number, data: string) {
let gameePopups: PopupManager | undefined;
async function answerCallbackButton(chat: ApiChat, messageId: number, data?: string, isGame = false) {
const {
showDialog, showNotification, toggleSafeLinkModal, openGame,
} = getActions();
if (isGame) {
if (!gameePopups) {
gameePopups = new PopupManager('popup,width=800,height=600', () => {
showNotification({ message: 'Allow browser to open popup window' });
});
}
gameePopups.preOpenIfNeeded();
}
const result = await callApi('answerCallbackButton', {
chatId: chat.id,
accessHash: chat.accessHash,
messageId,
data,
isGame,
});
if (!result) {
return;
}
const { showDialog, showNotification, toggleSafeLinkModal } = getActions();
const { message, alert: isError, url } = result;
if (isError) {
@ -347,6 +429,16 @@ async function answerCallbackButton(chat: ApiChat, messageId: number, data: stri
} else if (message) {
showNotification({ message });
} else if (url) {
toggleSafeLinkModal({ url });
if (isGame) {
// Workaround for Gamee embedding bug
if (url.includes(GAMEE_URL)) {
gameePopups!.open(url);
} else {
gameePopups!.cancelPreOpen();
openGame({ url, chatId: chat.id, messageId });
}
} else {
toggleSafeLinkModal({ url });
}
}
}

View File

@ -905,6 +905,30 @@ addActionHandler('setActiveChatFolder', (global, actions, payload) => {
};
});
addActionHandler('openChatWithText', (global, actions, payload) => {
const { chatId, text } = payload;
actions.openChat({ id: chatId });
actions.exitMessageSelectMode();
global = getGlobal();
return {
...global,
openChatWithText: {
chatId,
text,
},
};
});
addActionHandler('resetOpenChatWithText', (global) => {
return {
...global,
openChatWithText: undefined,
};
});
addActionHandler('loadMoreMembers', async (global) => {
const { chatId } = selectCurrentMessageList(global) || {};
const chat = chatId ? selectChat(global, chatId) : undefined;

View File

@ -574,7 +574,9 @@ addActionHandler('loadPollOptionResults', (global, actions, payload) => {
});
addActionHandler('forwardMessages', (global, action, payload) => {
const { fromChatId, messageIds, toChatId } = global.forwardMessages;
const {
fromChatId, messageIds, toChatId, withMyScore,
} = global.forwardMessages;
const fromChat = fromChatId ? selectChat(global, fromChatId) : undefined;
const toChat = toChatId ? selectChat(global, toChatId) : undefined;
const messages = fromChatId && messageIds
@ -600,6 +602,7 @@ addActionHandler('forwardMessages', (global, action, payload) => {
isSilent,
scheduledAt,
sendAs,
withMyScore,
});
}

View File

@ -410,7 +410,9 @@ addActionHandler('focusMessage', (global, actions, payload) => {
});
addActionHandler('openForwardMenu', (global, actions, payload) => {
const { fromChatId, messageIds, groupedId } = payload!;
const {
fromChatId, messageIds, groupedId, withMyScore,
} = payload!;
let groupedMessageIds;
if (groupedId) {
groupedMessageIds = selectMessageIdsByGroupId(global, fromChatId, groupedId);
@ -421,6 +423,7 @@ addActionHandler('openForwardMenu', (global, actions, payload) => {
fromChatId,
messageIds: groupedMessageIds || messageIds,
isModalShown: true,
withMyScore,
},
};
});
@ -573,17 +576,23 @@ addActionHandler('disableContextMenuHint', (global) => {
addActionHandler('exitMessageSelectMode', exitMessageSelectMode);
addActionHandler('openPollModal', (global) => {
addActionHandler('openPollModal', (global, actions, payload) => {
const { isQuiz } = payload || {};
return {
...global,
isPollModalOpen: true,
pollModal: {
isOpen: true,
isQuiz,
},
};
});
addActionHandler('closePollModal', (global) => {
return {
...global,
isPollModalOpen: false,
pollModal: {
isOpen: false,
},
};
});

View File

@ -261,3 +261,22 @@ addActionHandler('closeHistoryCalendar', (global) => {
historyCalendarSelectedAt: undefined,
};
});
addActionHandler('openGame', (global, actions, payload) => {
const { url, chatId, messageId } = payload;
return {
...global,
openedGame: {
url,
chatId,
messageId,
},
};
});
addActionHandler('closeGame', (global) => {
return {
...global,
openedGame: undefined,
};
});

View File

@ -219,6 +219,12 @@ function migrateCache(cached: GlobalState, initialState: GlobalState) {
if (!cached.activeReactions) {
cached.activeReactions = {};
}
if (!cached.pollModal) {
cached.pollModal = {
isOpen: false,
};
}
}
function updateCache() {

View File

@ -1,5 +1,5 @@
import {
ApiAudio, ApiMediaFormat, ApiMessage, ApiMessageSearchType, ApiPhoto, ApiVideo, ApiDimensions, ApiLocation,
ApiAudio, ApiMediaFormat, ApiMessage, ApiMessageSearchType, ApiPhoto, ApiVideo, ApiDimensions, ApiLocation, ApiGame,
} from '../../api/types';
import { IS_OPUS_SUPPORTED, IS_PROGRESSIVE_SUPPORTED, IS_SAFARI } from '../../util/environment';
@ -252,6 +252,26 @@ export function getMessageMediaHash(
return undefined;
}
export function getGamePreviewPhotoHash(game: ApiGame) {
const { photo } = game;
if (photo) {
return `photo${photo.id}?size=x`;
}
return undefined;
}
export function getGamePreviewVideoHash(game: ApiGame) {
const { document } = game;
if (document) {
return `document${document.id}`;
}
return undefined;
}
function getVideoOrAudioBaseHash(media: ApiAudio | ApiVideo, base: string) {
if (IS_PROGRESSIVE_SUPPORTED && IS_SAFARI) {
return `${base}?fileSize=${media.size}&mimeType=${media.mimeType}`;

View File

@ -118,6 +118,7 @@ export function getMessageSummaryDescription(
poll,
invoice,
location,
game,
} = message.content;
let summary: string | TextPart[] | undefined;
@ -178,6 +179,10 @@ export function getMessageSummaryDescription(
summary = lang('Message.LiveLocation');
}
if (game) {
summary = `🎮 ${game.title}`;
}
const reaction = !noReactions && getMessageRecentReaction(message);
if (summary && reaction) {
summary = `to your "${summary}"`;

View File

@ -44,7 +44,7 @@ export function getMessageOriginalId(message: ApiMessage) {
export function getMessageText(message: ApiMessage) {
const {
text, sticker, photo, video, audio, voice, document, poll, webPage, contact, invoice, location,
text, sticker, photo, video, audio, voice, document, poll, webPage, contact, invoice, location, game,
} = message.content;
if (text) {
@ -52,7 +52,7 @@ export function getMessageText(message: ApiMessage) {
}
if (sticker || photo || video || audio || voice || document
|| contact || poll || webPage || invoice || location) {
|| contact || poll || webPage || invoice || location || game) {
return undefined;
}

View File

@ -202,4 +202,8 @@ export const INITIAL_STATE: GlobalState = {
statistics: {
byChatId: {},
},
pollModal: {
isOpen: false,
},
};

View File

@ -165,3 +165,10 @@ export function selectSendAs(global: GlobalState, chatId: string) {
return selectUser(global, id) || selectChat(global, id);
}
export function selectRequestedText(global: GlobalState, chatId: string) {
if (global.openChatWithText?.chatId === chatId) {
return global.openChatWithText.text;
}
return undefined;
}

View File

@ -642,8 +642,8 @@ export function selectIsPollResultsOpen(global: GlobalState) {
}
export function selectIsForwardModalOpen(global: GlobalState) {
const { forwardMessages } = global;
return Boolean(forwardMessages.isModalShown);
const { forwardMessages, switchBotInline } = global;
return Boolean(switchBotInline || forwardMessages.isModalShown);
}
export function selectCommonBoxChatId(global: GlobalState, messageId: number) {

View File

@ -28,7 +28,9 @@ import {
ApiSponsoredMessage,
ApiChannelStatistics,
ApiGroupStatistics,
ApiPaymentFormNativeParams, ApiUpdate,
ApiPaymentFormNativeParams,
ApiUpdate,
ApiKeyboardButton,
} from '../api/types';
import {
FocusDirection,
@ -114,7 +116,6 @@ export type GlobalState = {
isChatInfoShown: boolean;
isStatisticsShown?: boolean;
isLeftColumnShown: boolean;
isPollModalOpen?: boolean;
newChatMembersProgress?: NewChatMembersProgress;
uiReadyState: 0 | 1 | 2;
shouldSkipHistoryAnimations?: boolean;
@ -407,6 +408,7 @@ export type GlobalState = {
fromChatId?: string;
messageIds?: number[];
toChatId?: string;
withMyScore?: boolean;
};
pollResults: {
@ -516,6 +518,27 @@ export type GlobalState = {
userId?: string;
isByPhoneNumber?: boolean;
};
openedGame?: {
url: string;
chatId: string;
messageId: number;
};
switchBotInline?: {
query: string;
botUsername: string;
};
openChatWithText?: {
chatId: string;
text: string;
};
pollModal: {
isOpen: boolean;
isQuiz?: boolean;
};
};
export interface ActionPayloads {
@ -531,6 +554,13 @@ export interface ActionPayloads {
shouldReplaceHistory?: boolean;
};
openChatWithText: {
chatId: string;
text: string;
};
resetOpenChatWithText: {};
// Messages
setEditingDraft: {
text?: ApiFormattedText;
@ -615,6 +645,34 @@ export interface ActionPayloads {
isMuted?: boolean;
shouldSharePhoneNumber?: boolean;
};
// Bots
clickBotInlineButton: {
messageId: number;
button: ApiKeyboardButton;
};
switchBotInline: {
messageId: number;
query: string;
isSamePeer?: boolean;
};
resetSwitchBotInline: {};
// Misc
openGame: {
url: string;
chatId: string;
messageId: number;
};
closeGame: {};
openPollModal: {
isQuiz?: boolean;
};
closePollModal: {};
}
export type NonTypedActionNames = (
@ -697,10 +755,9 @@ export type NonTypedActionNames = (
'loadStickersForEmoji' | 'clearStickersForEmoji' | 'loadEmojiKeywords' | 'loadGreetingStickers' |
'openStickerSetShortName' |
// bots
'clickInlineButton' | 'sendBotCommand' | 'loadTopInlineBots' | 'queryInlineBot' | 'sendInlineBotResult' |
'sendBotCommand' | 'loadTopInlineBots' | 'queryInlineBot' | 'sendInlineBotResult' |
'resetInlineBot' | 'restartBot' | 'startBot' |
// misc
'openPollModal' | 'closePollModal' |
'loadWebPagePreview' | 'clearWebPagePreview' | 'loadWallpapers' | 'uploadWallpaper' |
'setDeviceToken' | 'deleteDeviceToken' |
'checkVersionNotification' | 'createServiceNotification' |

View File

@ -6,7 +6,7 @@ import { ApiSendMessageAction } from '../api/types';
import { SEND_MESSAGE_ACTION_INTERVAL } from '../config';
import { throttle } from '../util/schedulers';
const useSendMessageAction = (chatId: string, threadId?: number) => {
const useSendMessageAction = (chatId?: string, threadId?: number) => {
return useMemo(() => {
return throttle((action: ApiSendMessageAction) => {
getActions().sendMessageAction({ chatId, threadId, action });

View File

@ -1,4 +1,3 @@
// eslint-disable-next-line import/no-named-default
import { default as Api } from '../tl/api';
import TelegramClient from './TelegramClient';
import utils from '../Utils';

45
src/util/PopupManager.ts Normal file
View File

@ -0,0 +1,45 @@
import { IS_ANDROID, IS_IOS } from './environment';
const SHOULD_PRE_OPEN = IS_IOS || IS_ANDROID;
export default class PopupManager {
private preOpened?: WindowProxy | null;
constructor(private features?: string, private onFail?: NoneToVoidFunction) {
}
preOpenIfNeeded() {
if (!SHOULD_PRE_OPEN) return;
this.preOpened = window.open('about:blank', undefined, this.features);
if (this.preOpened) {
this.preOpened.blur();
} else {
this.onFail?.();
}
}
open(url: string) {
if (this.preOpened) {
this.preOpened!.location.href = url;
this.preOpened!.focus();
this.preOpened = undefined;
return;
}
if (!SHOULD_PRE_OPEN) {
const popup = window.open(url, undefined, this.features);
if (popup) {
popup.focus();
} else {
this.onFail?.();
}
}
}
cancelPreOpen() {
this.preOpened?.close();
this.preOpened = undefined;
}
}