Mini apps main app button and previews (#4866)
Co-authored-by: Ponama <anastasiiadmm@gmail.com>
This commit is contained in:
parent
8433012a88
commit
ff7c6dca5c
@ -110,7 +110,7 @@ function buildApiAttachMenuIcon(icon: GramJs.AttachMenuBotIcon): ApiAttachBotIco
|
||||
|
||||
export function buildApiBotInfo(botInfo: GramJs.BotInfo, chatId: string): ApiBotInfo {
|
||||
const {
|
||||
description, descriptionPhoto, descriptionDocument, userId, commands, menuButton,
|
||||
description, descriptionPhoto, descriptionDocument, userId, commands, menuButton, hasPreviewMedias,
|
||||
} = botInfo;
|
||||
|
||||
const botId = userId && buildApiPeerId(userId, 'user');
|
||||
@ -126,6 +126,7 @@ export function buildApiBotInfo(botInfo: GramJs.BotInfo, chatId: string): ApiBot
|
||||
photo,
|
||||
menuButton: buildApiBotMenuButton(menuButton),
|
||||
commands: commandsArray?.length ? commandsArray : undefined,
|
||||
hasPreviewMedia: hasPreviewMedias,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -81,6 +81,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
|
||||
type: userType,
|
||||
firstName,
|
||||
lastName,
|
||||
hasMainMiniApp: Boolean(mtpUser.botHasMainApp),
|
||||
canEditBot: botCanEdit,
|
||||
...(userType === 'userTypeBot' && { canBeInvitedToGroup: !mtpUser.botNochats }),
|
||||
...(usernames && { usernames }),
|
||||
|
||||
@ -3,6 +3,7 @@ import { Api as GramJs } from '../../../lib/gramjs';
|
||||
|
||||
import type {
|
||||
ApiBotApp,
|
||||
ApiBotPreviewMedia,
|
||||
ApiChat,
|
||||
ApiInputMessageReplyInfo,
|
||||
ApiPeer,
|
||||
@ -23,6 +24,7 @@ import {
|
||||
} from '../apiBuilders/bots';
|
||||
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
||||
import { omitVirtualClassFields } from '../apiBuilders/helpers';
|
||||
import { buildMessageMediaContent } from '../apiBuilders/messageContent';
|
||||
import { buildApiUrlAuthResult } from '../apiBuilders/misc';
|
||||
import { buildApiUser } from '../apiBuilders/users';
|
||||
import {
|
||||
@ -238,6 +240,35 @@ export async function requestWebView({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function requestMainWebView({
|
||||
peer,
|
||||
bot,
|
||||
startParam,
|
||||
theme,
|
||||
}: {
|
||||
peer: ApiPeer;
|
||||
bot: ApiUser;
|
||||
startParam?: string;
|
||||
theme?: ApiThemeParameters;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.messages.RequestMainWebView({
|
||||
peer: buildInputPeer(peer.id, peer.accessHash),
|
||||
bot: buildInputPeer(bot.id, bot.accessHash),
|
||||
startParam,
|
||||
themeParams: theme ? buildInputThemeParams(theme) : undefined,
|
||||
platform: WEB_APP_PLATFORM,
|
||||
}));
|
||||
|
||||
if (!(result instanceof GramJs.WebViewResultUrl)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
url: result.url,
|
||||
queryId: result.queryId?.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
export async function requestSimpleWebView({
|
||||
bot,
|
||||
url,
|
||||
@ -551,6 +582,22 @@ export async function invokeWebViewCustomMethod({
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchPreviewMedias({ bot } : { bot: ApiUser }) {
|
||||
const result = await invokeRequest(new GramJs.bots.GetPreviewMedias({
|
||||
bot: buildInputPeer(bot.id, bot.accessHash),
|
||||
}));
|
||||
|
||||
if (!result) return undefined;
|
||||
|
||||
const previews: ApiBotPreviewMedia[] = result.map((preview) => {
|
||||
return {
|
||||
content: buildMessageMediaContent(preview.media)!,
|
||||
date: preview.date,
|
||||
};
|
||||
});
|
||||
return previews;
|
||||
}
|
||||
|
||||
function processInlineBotResult(queryId: string, results: GramJs.TypeBotInlineResult[]) {
|
||||
return results.map((result) => {
|
||||
if (result instanceof GramJs.BotInlineMediaResult) {
|
||||
|
||||
@ -65,8 +65,8 @@ export {
|
||||
} from './twoFaSettings';
|
||||
|
||||
export {
|
||||
answerCallbackButton, setBotInfo, fetchTopInlineBots, fetchInlineBot, fetchInlineBotResults,
|
||||
sendInlineBotResult, startBot, fetchPopularAppBots, fetchTopBotApps,
|
||||
answerCallbackButton, setBotInfo, fetchTopInlineBots, fetchPreviewMedias, fetchInlineBot, fetchInlineBotResults,
|
||||
sendInlineBotResult, startBot, requestMainWebView, fetchPopularAppBots, fetchTopBotApps,
|
||||
requestWebView, requestSimpleWebView, sendWebViewData, prolongWebView, loadAttachBots, toggleAttachBot, fetchBotApp,
|
||||
requestBotUrlAuth, requestLinkUrlAuth, acceptBotUrlAuth, acceptLinkUrlAuth, loadAttachBot, requestAppWebView,
|
||||
allowBotSendMessages, fetchBotCanSendMessage, invokeWebViewCustomMethod,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type {
|
||||
ApiDimensions,
|
||||
ApiPhoto, ApiSticker, ApiThumbnail, ApiVideo,
|
||||
ApiPhoto, ApiSticker, ApiThumbnail, ApiVideo, MediaContainer,
|
||||
} from './messages';
|
||||
|
||||
export type ApiInlineResultType = (
|
||||
@ -74,4 +74,9 @@ export interface ApiBotInfo {
|
||||
photo?: ApiPhoto;
|
||||
gif?: ApiVideo;
|
||||
menuButton: ApiBotMenuButton;
|
||||
hasPreviewMedia?: true;
|
||||
}
|
||||
|
||||
export interface ApiBotPreviewMedia extends MediaContainer {
|
||||
date: number;
|
||||
}
|
||||
|
||||
@ -40,6 +40,7 @@ export interface ApiUser {
|
||||
maxStoryId?: number;
|
||||
color?: ApiPeerColor;
|
||||
canEditBot?: boolean;
|
||||
hasMainMiniApp?: boolean;
|
||||
botActiveUsers?: number;
|
||||
}
|
||||
|
||||
|
||||
@ -1271,6 +1271,9 @@
|
||||
"MenuInstallApp" = "Install App";
|
||||
"RemoveEffect" = "Remove effect";
|
||||
"ReplyInPrivateMessage" = "Reply In Private Message";
|
||||
"ProfileOpenAppAbout" = "By launching this mini app, you agree to the {terms}.";
|
||||
"ProfileOpenAppTerms" = "Terms of Service for Mini Apps";
|
||||
"ProfileBotOpenAppInfoLink" = "https://telegram.org/tos/mini-apps";
|
||||
"MonetizationInfoTONTitle" = "What is 💎 TON?";
|
||||
"ChannelEarnLearnCoinAbout" = "TON is a blockchain platform and cryptocurrency that Telegram uses for its high speed and low commissions on transactions. {link}";
|
||||
"MonetizationBalanceZeroInfo" = "You will be able to collect rewards using Fragment, a third-party platform used by advertisers to pay for ads. {link}";
|
||||
|
||||
82
src/components/common/PreviewMedia.tsx
Normal file
82
src/components/common/PreviewMedia.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { memo, useRef } from '../../lib/teact/teact';
|
||||
|
||||
import type { ApiBotPreviewMedia } from '../../api/types';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
|
||||
import {
|
||||
getMessageMediaHash, getMessageMediaThumbDataUri,
|
||||
} from '../../global/helpers';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { formatMediaDuration } from '../../util/dates/dateFormat';
|
||||
import stopEvent from '../../util/stopEvent';
|
||||
|
||||
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import useMediaTransition from '../../hooks/useMediaTransition';
|
||||
|
||||
import './Media.scss';
|
||||
|
||||
type OwnProps = {
|
||||
media: ApiBotPreviewMedia;
|
||||
idPrefix?: string;
|
||||
isProtected?: boolean;
|
||||
observeIntersection?: ObserveFn;
|
||||
onClick: (index: number) => void;
|
||||
index: number;
|
||||
};
|
||||
|
||||
const PreviewMedia: FC<OwnProps> = ({
|
||||
media,
|
||||
idPrefix = 'preview-media',
|
||||
isProtected,
|
||||
observeIntersection,
|
||||
onClick,
|
||||
index,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||
const thumbDataUri = getMessageMediaThumbDataUri(media);
|
||||
|
||||
const mediaBlobUrl = useMedia(getMessageMediaHash(media, 'preview'), !isIntersecting);
|
||||
const transitionClassNames = useMediaTransition(mediaBlobUrl);
|
||||
|
||||
const video = media.content.video;
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
onClick(index);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
id={`${idPrefix}${index}`}
|
||||
className="Media scroll-item"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<img
|
||||
src={thumbDataUri}
|
||||
className="media-miniature"
|
||||
alt=""
|
||||
draggable={!isProtected}
|
||||
decoding="async"
|
||||
onContextMenu={isProtected ? stopEvent : undefined}
|
||||
/>
|
||||
<img
|
||||
src={mediaBlobUrl}
|
||||
className={buildClassName('full-media', 'media-miniature', transitionClassNames)}
|
||||
alt=""
|
||||
draggable={!isProtected}
|
||||
decoding="async"
|
||||
onContextMenu={isProtected ? stopEvent : undefined}
|
||||
/>
|
||||
{video && <span className="video-duration">{video.isGif ? 'GIF' : formatMediaDuration(video.duration)}</span>}
|
||||
{isProtected && <span className="protector" />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PreviewMedia);
|
||||
@ -24,6 +24,11 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.sectionInfo {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.personalChannelSubscribers {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
@ -36,3 +41,8 @@
|
||||
grid-column: 1 / span 2;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.openAppButton {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
@ -32,20 +32,24 @@ import { copyTextToClipboard } from '../../../util/clipboard';
|
||||
import { formatPhoneNumberWithCode } from '../../../util/phoneNumber';
|
||||
import { debounce } from '../../../util/schedulers';
|
||||
import stopEvent from '../../../util/stopEvent';
|
||||
import { extractCurrentThemeParams } from '../../../util/themeStyle';
|
||||
import { ChatAnimationTypes } from '../../left/main/hooks';
|
||||
import formatUsername from '../helpers/formatUsername';
|
||||
import renderText from '../helpers/renderText';
|
||||
|
||||
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
import useDevicePixelRatio from '../../../hooks/window/useDevicePixelRatio';
|
||||
|
||||
import Chat from '../../left/main/Chat';
|
||||
import Button from '../../ui/Button';
|
||||
import ListItem from '../../ui/ListItem';
|
||||
import Skeleton from '../../ui/placeholder/Skeleton';
|
||||
import Switcher from '../../ui/Switcher';
|
||||
import SafeLink from '../SafeLink';
|
||||
import BusinessHours from './BusinessHours';
|
||||
import UserBirthday from './UserBirthday';
|
||||
|
||||
@ -70,6 +74,7 @@ type StateProps = {
|
||||
topicLink?: string;
|
||||
hasSavedMessages?: boolean;
|
||||
personalChannel?: ApiChat;
|
||||
hasMainMiniApp?: boolean;
|
||||
};
|
||||
|
||||
const DEFAULT_MAP_CONFIG = {
|
||||
@ -95,6 +100,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
||||
topicLink,
|
||||
hasSavedMessages,
|
||||
personalChannel,
|
||||
hasMainMiniApp,
|
||||
}) => {
|
||||
const {
|
||||
showNotification,
|
||||
@ -104,6 +110,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
||||
openSavedDialog,
|
||||
openMapModal,
|
||||
requestCollectibleInfo,
|
||||
requestMainWebView,
|
||||
} = getActions();
|
||||
|
||||
const {
|
||||
@ -120,7 +127,8 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
||||
personalChannelMessageId,
|
||||
birthday,
|
||||
} = userFullInfo || {};
|
||||
const lang = useOldLang();
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
|
||||
const [areNotificationsEnabled, setAreNotificationsEnabled] = useState(!isMuted);
|
||||
|
||||
@ -177,7 +185,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
||||
const { address, geo } = businessLocation!;
|
||||
if (!geo) {
|
||||
copyTextToClipboard(address);
|
||||
showNotification({ message: lang('BusinessLocationCopied') });
|
||||
showNotification({ message: oldLang('BusinessLocationCopied') });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -220,7 +228,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
||||
requestCollectibleInfo({ collectible: phoneNumber, peerId: peerId!, type: 'phone' });
|
||||
return;
|
||||
}
|
||||
copy(formattedNumber!, lang('Phone'));
|
||||
copy(formattedNumber!, oldLang('Phone'));
|
||||
});
|
||||
|
||||
const handleUsernameClick = useLastCallback((username: ApiUsername, isChat?: boolean) => {
|
||||
@ -228,9 +236,35 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
||||
requestCollectibleInfo({ collectible: username.username, peerId: peerId!, type: 'username' });
|
||||
return;
|
||||
}
|
||||
copy(formatUsername(username.username, isChat), lang(isChat ? 'Link' : 'Username'));
|
||||
copy(formatUsername(username.username, isChat), oldLang(isChat ? 'Link' : 'Username'));
|
||||
});
|
||||
|
||||
const handleOpenApp = useLastCallback(() => {
|
||||
if (!chat) {
|
||||
return;
|
||||
}
|
||||
const botId = user?.id;
|
||||
if (!botId) {
|
||||
return;
|
||||
}
|
||||
const theme = extractCurrentThemeParams();
|
||||
requestMainWebView({
|
||||
botId,
|
||||
peerId: botId,
|
||||
theme,
|
||||
shouldMarkBotTrusted: true,
|
||||
});
|
||||
});
|
||||
|
||||
const appTermsInfo = lang('ProfileOpenAppAbout', {
|
||||
terms: (
|
||||
<SafeLink
|
||||
text={lang('ProfileOpenAppTerms')}
|
||||
url={lang('ProfileBotOpenAppInfoLink')}
|
||||
/>
|
||||
),
|
||||
}, { withNodes: true });
|
||||
|
||||
if (!chat || chat.isRestricted || (isSelf && !isInSettings)) {
|
||||
return undefined;
|
||||
}
|
||||
@ -239,7 +273,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
||||
const [mainUsername, ...otherUsernames] = usernameList;
|
||||
|
||||
const usernameLinks = otherUsernames.length
|
||||
? (lang('UsernameAlso', '%USERNAMES%') as string)
|
||||
? (oldLang('UsernameAlso', '%USERNAMES%') as string)
|
||||
.split('%')
|
||||
.map((s) => {
|
||||
return (s === 'USERNAMES' ? (
|
||||
@ -282,7 +316,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
||||
<span className="title" dir="auto">{formatUsername(mainUsername.username, isChat)}</span>
|
||||
<span className="subtitle">
|
||||
{usernameLinks && <span className="other-usernames">{usernameLinks}</span>}
|
||||
{lang(isChat ? 'Link' : 'Username')}
|
||||
{oldLang(isChat ? 'Link' : 'Username')}
|
||||
</span>
|
||||
</ListItem>
|
||||
);
|
||||
@ -292,9 +326,9 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
||||
<div className="ChatExtra">
|
||||
{personalChannel && (
|
||||
<div className={styles.personalChannel}>
|
||||
<h3 className={styles.personalChannelTitle}>{lang('ProfileChannel')}</h3>
|
||||
<h3 className={styles.personalChannelTitle}>{oldLang('ProfileChannel')}</h3>
|
||||
<span className={styles.personalChannelSubscribers}>
|
||||
{lang('Subscribers', personalChannel.membersCount, 'i')}
|
||||
{oldLang('Subscribers', personalChannel.membersCount, 'i')}
|
||||
</span>
|
||||
<Chat
|
||||
chatId={personalChannel.id}
|
||||
@ -310,7 +344,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
<ListItem icon="phone" multiline narrow ripple onClick={handlePhoneClick}>
|
||||
<span className="title" dir="auto">{formattedNumber}</span>
|
||||
<span className="subtitle">{lang('Phone')}</span>
|
||||
<span className="subtitle">{oldLang('Phone')}</span>
|
||||
</ListItem>
|
||||
)}
|
||||
{activeUsernames && renderUsernames(activeUsernames)}
|
||||
@ -331,7 +365,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
||||
])
|
||||
}
|
||||
</span>
|
||||
<span className="subtitle">{lang(userId ? 'UserBio' : 'Info')}</span>
|
||||
<span className="subtitle">{oldLang(userId ? 'UserBio' : 'Info')}</span>
|
||||
</ListItem>
|
||||
)}
|
||||
{activeChatUsernames && !isTopicInfo && renderUsernames(activeChatUsernames, true)}
|
||||
@ -342,18 +376,36 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
||||
narrow
|
||||
ripple
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => copy(link, lang('SetUrlPlaceholder'))}
|
||||
onClick={() => copy(link, oldLang('SetUrlPlaceholder'))}
|
||||
>
|
||||
<div className="title">{link}</div>
|
||||
<span className="subtitle">{lang('SetUrlPlaceholder')}</span>
|
||||
<span className="subtitle">{oldLang('SetUrlPlaceholder')}</span>
|
||||
</ListItem>
|
||||
)}
|
||||
{birthday && (
|
||||
<UserBirthday key={peerId} birthday={birthday} user={user!} isInSettings={isInSettings} />
|
||||
)}
|
||||
{ hasMainMiniApp && (
|
||||
<ListItem
|
||||
multiline
|
||||
isStatic
|
||||
narrow
|
||||
>
|
||||
<Button
|
||||
className={styles.openAppButton}
|
||||
size="smaller"
|
||||
onClick={handleOpenApp}
|
||||
>
|
||||
{oldLang('ProfileBotOpenApp')}
|
||||
</Button>
|
||||
<div className={styles.sectionInfo}>
|
||||
{appTermsInfo}
|
||||
</div>
|
||||
</ListItem>
|
||||
)}
|
||||
{!isInSettings && (
|
||||
<ListItem icon="unmute" narrow ripple onClick={handleNotificationChange}>
|
||||
<span>{lang('Notifications')}</span>
|
||||
<span>{oldLang('Notifications')}</span>
|
||||
<Switcher
|
||||
id="group-notifications"
|
||||
label={userId ? 'Toggle User Notifications' : 'Toggle Chat Notifications'}
|
||||
@ -375,12 +427,12 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
||||
onClick={handleClickLocation}
|
||||
>
|
||||
<div className="title">{businessLocation.address}</div>
|
||||
<span className="subtitle">{lang('BusinessProfileLocation')}</span>
|
||||
<span className="subtitle">{oldLang('BusinessProfileLocation')}</span>
|
||||
</ListItem>
|
||||
)}
|
||||
{hasSavedMessages && !isInSettings && (
|
||||
<ListItem icon="saved-messages" narrow ripple onClick={handleOpenSavedDialog}>
|
||||
<span>{lang('SavedMessagesTab')}</span>
|
||||
<span>{oldLang('SavedMessagesTab')}</span>
|
||||
</ListItem>
|
||||
)}
|
||||
</div>
|
||||
@ -418,6 +470,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
? selectChat(global, userFullInfo.personalChannelId)
|
||||
: undefined;
|
||||
|
||||
const hasMainMiniApp = user?.hasMainMiniApp;
|
||||
|
||||
return {
|
||||
phoneCodeList,
|
||||
chat,
|
||||
@ -431,6 +485,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
topicLink,
|
||||
hasSavedMessages,
|
||||
personalChannel,
|
||||
hasMainMiniApp,
|
||||
};
|
||||
},
|
||||
)(ChatExtra));
|
||||
|
||||
@ -299,6 +299,11 @@ function getNodes(origin: MediaViewerOrigin, message?: ApiMessage, index?: numbe
|
||||
mediaSelector = '.full-media';
|
||||
break;
|
||||
|
||||
case MediaViewerOrigin.PreviewMedia:
|
||||
containerSelector = `#preview-media${index}`;
|
||||
mediaSelector = 'img';
|
||||
break;
|
||||
|
||||
case MediaViewerOrigin.SharedMedia:
|
||||
containerSelector = `#shared-media${getMessageHtmlId(message!.id, index)}`;
|
||||
mediaSelector = 'img';
|
||||
@ -358,6 +363,7 @@ function applyShape(ghost: HTMLDivElement, origin: MediaViewerOrigin) {
|
||||
case MediaViewerOrigin.Inline:
|
||||
case MediaViewerOrigin.ScheduledInline:
|
||||
case MediaViewerOrigin.StarsTransaction:
|
||||
case MediaViewerOrigin.PreviewMedia:
|
||||
ghost.classList.add('rounded-corners');
|
||||
break;
|
||||
|
||||
|
||||
@ -71,7 +71,8 @@
|
||||
|
||||
&.storiesArchive-list,
|
||||
&.stories-list,
|
||||
&.media-list {
|
||||
&.media-list,
|
||||
&.previewMedia-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-auto-rows: 1fr;
|
||||
|
||||
@ -6,6 +6,7 @@ import React, {
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type {
|
||||
ApiBotPreviewMedia,
|
||||
ApiChat,
|
||||
ApiChatMember,
|
||||
ApiMessage,
|
||||
@ -52,6 +53,7 @@ import {
|
||||
selectTabState,
|
||||
selectTheme,
|
||||
selectUser,
|
||||
selectUserFullInfo,
|
||||
} from '../../global/selectors';
|
||||
import { selectPremiumLimit } from '../../global/selectors/limits';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
@ -77,6 +79,7 @@ import Document from '../common/Document';
|
||||
import GroupChatInfo from '../common/GroupChatInfo';
|
||||
import Media from '../common/Media';
|
||||
import NothingFound from '../common/NothingFound';
|
||||
import PreviewMedia from '../common/PreviewMedia';
|
||||
import PrivateChatInfo from '../common/PrivateChatInfo';
|
||||
import ChatExtra from '../common/profile/ChatExtra';
|
||||
import ProfileInfo from '../common/ProfileInfo';
|
||||
@ -113,6 +116,7 @@ type StateProps = {
|
||||
hasCommonChatsTab?: boolean;
|
||||
hasStoriesTab?: boolean;
|
||||
hasMembersTab?: boolean;
|
||||
hasPreviewMediaTab?: boolean;
|
||||
areMembersHidden?: boolean;
|
||||
canAddMembers?: boolean;
|
||||
canDeleteMembers?: boolean;
|
||||
@ -133,6 +137,7 @@ type StateProps = {
|
||||
nextProfileTab?: ProfileTabType;
|
||||
shouldWarnAboutSvg?: boolean;
|
||||
similarChannels?: string[];
|
||||
botPreviewMedia? : ApiBotPreviewMedia[];
|
||||
isCurrentUserPremium?: boolean;
|
||||
limitSimilarChannels: number;
|
||||
isTopicInfo?: boolean;
|
||||
@ -174,6 +179,8 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
hasCommonChatsTab,
|
||||
hasStoriesTab,
|
||||
hasMembersTab,
|
||||
hasPreviewMediaTab,
|
||||
botPreviewMedia,
|
||||
areMembersHidden,
|
||||
canAddMembers,
|
||||
canDeleteMembers,
|
||||
@ -210,6 +217,7 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
loadStoriesArchive,
|
||||
openPremiumModal,
|
||||
loadChannelRecommendations,
|
||||
loadPreviewMedias,
|
||||
} = getActions();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
@ -229,6 +237,9 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
...(hasMembersTab ? [{
|
||||
type: 'members' as const, title: isChannel ? 'ChannelSubscribers' : 'GroupMembers',
|
||||
}] : []),
|
||||
...(hasPreviewMediaTab ? [{
|
||||
type: 'previewMedia' as const, title: 'ProfileBotPreviewTab',
|
||||
}] : []),
|
||||
...TABS,
|
||||
// TODO The filter for voice messages currently does not work
|
||||
// in forum topics. Return it when it's fixed on the server side.
|
||||
@ -240,6 +251,7 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
]), [
|
||||
hasCommonChatsTab,
|
||||
hasMembersTab,
|
||||
hasPreviewMediaTab,
|
||||
hasStoriesTab,
|
||||
isChannel,
|
||||
isTopicInfo,
|
||||
@ -274,6 +286,12 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
setActiveTab(index);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasPreviewMediaTab && !botPreviewMedia) {
|
||||
loadPreviewMedias({ botId: chatId });
|
||||
}
|
||||
}, [chatId, botPreviewMedia, hasPreviewMediaTab]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isChannel && !similarChannels) {
|
||||
loadChannelRecommendations({ chatId });
|
||||
@ -364,6 +382,15 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
});
|
||||
|
||||
const handleSelectPreviewMedia = useLastCallback((index: number) => {
|
||||
openMediaViewer({
|
||||
standaloneMedia: botPreviewMedia?.flatMap((item) => item?.content.photo
|
||||
|| item?.content.video).filter(Boolean),
|
||||
origin: MediaViewerOrigin.PreviewMedia,
|
||||
mediaIndex: index,
|
||||
});
|
||||
});
|
||||
|
||||
const handlePlayAudio = useLastCallback((messageId: number) => {
|
||||
openAudioPlayer({ chatId: profileId, messageId });
|
||||
});
|
||||
@ -416,7 +443,7 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
if (isFirstTab) {
|
||||
renderingDelay = !isRightColumnShown ? HIDDEN_RENDER_DELAY : 0;
|
||||
// @optimization Used to delay first render of secondary tabs while animating
|
||||
} else if (!viewportIds) {
|
||||
} else if (!viewportIds && !botPreviewMedia) {
|
||||
renderingDelay = SLIDE_TRANSITION_DURATION;
|
||||
}
|
||||
const canRenderContent = useAsyncRendering([chatId, threadId, resultType, renderingActiveTab], renderingDelay);
|
||||
@ -438,7 +465,7 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (!viewportIds || !canRenderContent || !messagesById) {
|
||||
if ((!viewportIds && !botPreviewMedia) || !canRenderContent || !messagesById) {
|
||||
const noSpinner = isFirstTab && !canRenderContent;
|
||||
const forceRenderHiddenMembers = Boolean(resultType === 'members' && areMembersHidden);
|
||||
|
||||
@ -450,7 +477,7 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (!viewportIds.length) {
|
||||
if (viewportIds && !viewportIds?.length) {
|
||||
let text: string;
|
||||
|
||||
switch (resultType) {
|
||||
@ -595,6 +622,17 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
<GroupChatInfo chatId={id} />
|
||||
</ListItem>
|
||||
))
|
||||
) : resultType === 'previewMedia' ? (
|
||||
botPreviewMedia!.map((media, i) => (
|
||||
<PreviewMedia
|
||||
key={media.date}
|
||||
media={media}
|
||||
isProtected={isChatProtected}
|
||||
observeIntersection={observeIntersectionForMedia}
|
||||
onClick={handleSelectPreviewMedia}
|
||||
index={i}
|
||||
/>
|
||||
))
|
||||
) : resultType === 'similarChannels' ? (
|
||||
<div key={resultType}>
|
||||
{(viewportIds as string[])!.map((channelId, i) => (
|
||||
@ -739,6 +777,11 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
const peer = user || chat;
|
||||
const peerFullInfo = selectPeerFullInfo(global, chatId);
|
||||
|
||||
const userFullInfo = selectUserFullInfo(global, chatId);
|
||||
const hasPreviewMediaTab = userFullInfo?.botInfo?.hasPreviewMedia;
|
||||
const botPreviewMedia = global.users.previewMediaByBotId[chatId];
|
||||
|
||||
const hasStoriesTab = peer && (user?.isSelf || (!peer.areStoriesHidden && peerFullInfo?.hasPinnedStories))
|
||||
&& !isSavedDialog;
|
||||
const peerStories = hasStoriesTab ? selectPeerStories(global, peer.id) : undefined;
|
||||
@ -757,6 +800,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
hasCommonChatsTab,
|
||||
hasStoriesTab,
|
||||
hasMembersTab,
|
||||
hasPreviewMediaTab,
|
||||
areMembersHidden,
|
||||
canAddMembers,
|
||||
canDeleteMembers,
|
||||
@ -776,6 +820,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
forceScrollProfileTab: selectTabState(global).forceScrollProfileTab,
|
||||
shouldWarnAboutSvg: global.settings.byKey.shouldWarnAboutSvg,
|
||||
similarChannels: similarChannelIds,
|
||||
botPreviewMedia,
|
||||
isCurrentUserPremium,
|
||||
isTopicInfo,
|
||||
isSavedDialog,
|
||||
|
||||
@ -612,6 +612,88 @@ addActionHandler('requestWebView', async (global, actions, payload): Promise<voi
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('requestMainWebView', async (global, actions, payload): Promise<void> => {
|
||||
const {
|
||||
botId, peerId, theme, startParam, shouldMarkBotTrusted,
|
||||
tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
const bot = selectUser(global, botId);
|
||||
if (!bot) return;
|
||||
const peer = selectPeer(global, peerId);
|
||||
if (!peer) return;
|
||||
|
||||
if (!selectIsTrustedBot(global, botId)) {
|
||||
if (shouldMarkBotTrusted) {
|
||||
actions.markBotTrusted({ botId, isWriteAllowed: true, tabId });
|
||||
} else {
|
||||
global = updateTabState(global, {
|
||||
botTrustRequest: {
|
||||
botId,
|
||||
type: 'webApp',
|
||||
onConfirm: {
|
||||
action: 'requestMainWebView',
|
||||
payload,
|
||||
},
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await callApi('requestMainWebView', {
|
||||
bot,
|
||||
peer,
|
||||
theme,
|
||||
startParam,
|
||||
});
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { url: webViewUrl, queryId } = result;
|
||||
|
||||
global = getGlobal();
|
||||
global = updateTabState(global, {
|
||||
webApp: {
|
||||
url: webViewUrl,
|
||||
botId,
|
||||
queryId,
|
||||
buttonText: '',
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('loadPreviewMedias', async (global, actions, payload): Promise<void> => {
|
||||
const {
|
||||
botId,
|
||||
} = payload;
|
||||
const bot = selectUser(global, botId);
|
||||
if (!bot) return;
|
||||
|
||||
const medias = await callApi('fetchPreviewMedias', {
|
||||
bot,
|
||||
});
|
||||
|
||||
global = getGlobal();
|
||||
if (medias) {
|
||||
global = {
|
||||
...global,
|
||||
users: {
|
||||
...global.users,
|
||||
previewMediaByBotId: {
|
||||
...global.users.previewMediaByBotId,
|
||||
[botId]: medias,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setGlobal(global);
|
||||
}
|
||||
});
|
||||
|
||||
addActionHandler('requestAppWebView', async (global, actions, payload): Promise<void> => {
|
||||
const {
|
||||
botId, appName, startApp, theme, isWriteAllowed, isFromConfirm,
|
||||
|
||||
@ -1485,6 +1485,20 @@ addActionHandler('openChatByUsername', async (global, actions, payload): Promise
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (startApp !== undefined && !webAppName) {
|
||||
const theme = extractCurrentThemeParams();
|
||||
const chatByUsername = await fetchChatByUsername(global, username);
|
||||
global = getGlobal();
|
||||
const user = chatByUsername && selectUser(global, chatByUsername.id);
|
||||
if (!chatByUsername || !chat || !user?.hasMainMiniApp) return;
|
||||
actions.requestMainWebView({
|
||||
botId: chatByUsername.id,
|
||||
peerId: chat.id,
|
||||
theme,
|
||||
tabId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!isWebApp) {
|
||||
await openChatByUsername(
|
||||
global, actions, {
|
||||
|
||||
@ -253,6 +253,9 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
|
||||
cached.quickReplies = initialState.quickReplies;
|
||||
}
|
||||
|
||||
if (!cached.users.previewMediaByBotId) {
|
||||
cached.users.previewMediaByBotId = initialState.users.previewMediaByBotId;
|
||||
}
|
||||
if (!cached.chats.loadingParameters) {
|
||||
cached.chats.loadingParameters = initialState.chats.loadingParameters;
|
||||
}
|
||||
@ -366,7 +369,11 @@ function reduceCustomEmojis<T extends GlobalState>(global: T): GlobalState['cust
|
||||
}
|
||||
|
||||
function reduceUsers<T extends GlobalState>(global: T): GlobalState['users'] {
|
||||
const { users: { byId, statusesById, fullInfoById }, currentUserId } = global;
|
||||
const {
|
||||
users: {
|
||||
byId, statusesById, fullInfoById,
|
||||
}, currentUserId,
|
||||
} = global;
|
||||
const currentChatIds = compact(
|
||||
Object.values(global.byTabId)
|
||||
.map(({ id: tabId }) => selectCurrentMessageList(global, tabId)),
|
||||
@ -400,6 +407,7 @@ function reduceUsers<T extends GlobalState>(global: T): GlobalState['users'] {
|
||||
byId: pick(byId, idsToSave),
|
||||
statusesById: pick(statusesById, idsToSave),
|
||||
fullInfoById: pick(fullInfoById, idsToSave),
|
||||
previewMediaByBotId: {},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -97,6 +97,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
||||
byId: {},
|
||||
statusesById: {},
|
||||
fullInfoById: {},
|
||||
previewMediaByBotId: {},
|
||||
},
|
||||
|
||||
chats: {
|
||||
|
||||
@ -6,6 +6,7 @@ import type {
|
||||
ApiAvailableReaction,
|
||||
ApiBoost,
|
||||
ApiBoostsStatus,
|
||||
ApiBotPreviewMedia,
|
||||
ApiChannelMonetizationStatistics,
|
||||
ApiChannelStatistics,
|
||||
ApiChat,
|
||||
@ -925,6 +926,7 @@ export type GlobalState = {
|
||||
statusesById: Record<string, ApiUserStatus>;
|
||||
// Obtained from GetFullUser / UserFullInfo
|
||||
fullInfoById: Record<string, ApiUserFullInfo>;
|
||||
previewMediaByBotId: Record<string, ApiBotPreviewMedia[]>;
|
||||
};
|
||||
|
||||
chats: {
|
||||
@ -2873,6 +2875,13 @@ export interface ActionPayloads {
|
||||
isFromBotMenu?: boolean;
|
||||
startParam?: string;
|
||||
} & WithTabId;
|
||||
requestMainWebView: {
|
||||
botId: string;
|
||||
peerId: string;
|
||||
theme?: ApiThemeParameters;
|
||||
startParam?: string;
|
||||
shouldMarkBotTrusted?: boolean;
|
||||
} & WithTabId;
|
||||
prolongWebView: {
|
||||
botId: string;
|
||||
peerId: string;
|
||||
@ -2898,6 +2907,9 @@ export interface ActionPayloads {
|
||||
isWriteAllowed?: boolean;
|
||||
isFromConfirm?: boolean;
|
||||
} & WithTabId;
|
||||
loadPreviewMedias: {
|
||||
botId: string;
|
||||
};
|
||||
setWebAppPaymentSlug: {
|
||||
slug?: string;
|
||||
} & WithTabId;
|
||||
|
||||
@ -1547,6 +1547,7 @@ messages.getQuickReplyMessages#94a495c3 flags:# shortcut_id:int id:flags.0?Vecto
|
||||
messages.sendQuickReplyMessages#6c750de1 peer:InputPeer shortcut_id:int id:Vector<int> random_id:Vector<long> = Updates;
|
||||
messages.getAvailableEffects#dea20a39 hash:int = messages.AvailableEffects;
|
||||
messages.getFactCheck#b9cdc5ee peer:InputPeer msg_id:Vector<int> = Vector<FactCheck>;
|
||||
messages.requestMainWebView#c9e01e7b flags:# compact:flags.7?true peer:InputPeer bot:InputUser start_param:flags.1?string theme_params:flags.0?DataJSON platform:string = WebViewResult;
|
||||
updates.getState#edd4882a = updates.State;
|
||||
updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference;
|
||||
updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference;
|
||||
@ -1617,6 +1618,7 @@ bots.setBotInfo#10cf3123 flags:# bot:flags.2?InputUser lang_code:string name:fla
|
||||
bots.canSendMessage#1359f4e6 bot:InputUser = Bool;
|
||||
bots.allowSendMessage#f132e3ef bot:InputUser = Updates;
|
||||
bots.invokeWebViewCustomMethod#87fc5e7 bot:InputUser custom_method:string params:DataJSON = DataJSON;
|
||||
bots.getPreviewMedias#a2a5594d bot:InputUser = Vector<BotPreviewMedia>;
|
||||
bots.getPopularAppBots#c2510192 offset:string limit:int = bots.PopularAppBots;
|
||||
payments.getPaymentForm#37148dbb flags:# invoice:InputInvoice theme_params:flags.0?DataJSON = payments.PaymentForm;
|
||||
payments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.PaymentReceipt;
|
||||
|
||||
@ -175,6 +175,7 @@
|
||||
"messages.getQuickReplyMessages",
|
||||
"messages.sendQuickReplyMessages",
|
||||
"messages.getFactCheck",
|
||||
"messages.requestMainWebView",
|
||||
"updates.getState",
|
||||
"updates.getDifference",
|
||||
"updates.getChannelDifference",
|
||||
@ -230,6 +231,7 @@
|
||||
"bots.invokeWebViewCustomMethod",
|
||||
"bots.getPopularAppBots",
|
||||
"bots.setBotInfo",
|
||||
"bots.getPreviewMedias",
|
||||
"payments.getPaymentForm",
|
||||
"payments.getPaymentReceipt",
|
||||
"payments.validateRequestedInfo",
|
||||
|
||||
@ -329,6 +329,7 @@ export enum MediaViewerOrigin {
|
||||
SearchResult,
|
||||
SuggestedAvatar,
|
||||
StarsTransaction,
|
||||
PreviewMedia,
|
||||
}
|
||||
|
||||
export enum StoryViewerOrigin {
|
||||
@ -392,6 +393,7 @@ export type ProfileTabType =
|
||||
| 'members'
|
||||
| 'commonChats'
|
||||
| 'media'
|
||||
| 'previewMedia'
|
||||
| 'documents'
|
||||
| 'links'
|
||||
| 'audio'
|
||||
|
||||
5
src/types/language.d.ts
vendored
5
src/types/language.d.ts
vendored
@ -1512,6 +1512,11 @@ export interface LangPair {
|
||||
'MenuInstallApp': undefined;
|
||||
'RemoveEffect': undefined;
|
||||
'ReplyInPrivateMessage': undefined;
|
||||
'ProfileOpenAppAbout': {
|
||||
'terms': string;
|
||||
};
|
||||
'ProfileOpenAppTerms': undefined;
|
||||
'ProfileBotOpenAppInfoLink': undefined;
|
||||
'MonetizationInfoTONTitle': undefined;
|
||||
'ChannelEarnLearnCoinAbout': {
|
||||
'link': string | number;
|
||||
|
||||
@ -511,4 +511,6 @@ export default {
|
||||
SlowModeWait: 'Slow Mode — %d',
|
||||
OpenMapWith: 'Open map with...',
|
||||
FullDateTimeFormat: '%@, %@',
|
||||
ProfileOpenAppTerms: 'Terms of Service for Mini Apps',
|
||||
ProfileBotOpenAppInfoLink: 'https://telegram.org/tos/mini-apps',
|
||||
} as ApiOldLangPack;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user