Implement Games (#1797)
This commit is contained in:
parent
cd4e9077d5
commit
d7226fded9
@ -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;
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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%',
|
||||
|
||||
@ -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));
|
||||
|
||||
48
src/components/main/GameModal.scss
Normal file
48
src/components/main/GameModal.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
95
src/components/main/GameModal.tsx
Normal file
95
src/components/main/GameModal.tsx
Normal 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);
|
||||
@ -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));
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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 && (
|
||||
|
||||
34
src/components/middle/message/Game.scss
Normal file
34
src/components/middle/message/Game.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
84
src/components/middle/message/Game.tsx
Normal file
84
src/components/middle/message/Game.tsx
Normal 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);
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
.Skeleton {
|
||||
position: relative;
|
||||
background-color: var(--color-skeleton-background);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&.round {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
@ -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;` : '');
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
@ -219,6 +219,12 @@ function migrateCache(cached: GlobalState, initialState: GlobalState) {
|
||||
if (!cached.activeReactions) {
|
||||
cached.activeReactions = {};
|
||||
}
|
||||
|
||||
if (!cached.pollModal) {
|
||||
cached.pollModal = {
|
||||
isOpen: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function updateCache() {
|
||||
|
||||
@ -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}`;
|
||||
|
||||
@ -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}"`;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -202,4 +202,8 @@ export const INITIAL_STATE: GlobalState = {
|
||||
statistics: {
|
||||
byChatId: {},
|
||||
},
|
||||
|
||||
pollModal: {
|
||||
isOpen: false,
|
||||
},
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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' |
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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
45
src/util/PopupManager.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user