Mini Apps: Request for rights (#5238)
This commit is contained in:
parent
cdbb5e15ed
commit
a414faa985
@ -25,6 +25,7 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
|
||||
fallbackPhoto, personalPhoto, translationsDisabled, storiesPinnedAvailable,
|
||||
contactRequirePremium, businessWorkHours, businessLocation, businessIntro,
|
||||
birthday, personalChannelId, personalChannelMessage, sponsoredEnabled, stargiftsCount, botVerification,
|
||||
botCanManageEmojiStatus,
|
||||
},
|
||||
users,
|
||||
} = mtpUserFull;
|
||||
@ -54,6 +55,7 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
|
||||
botVerification: botVerification && buildApiBotVerification(botVerification),
|
||||
areAdsEnabled: sponsoredEnabled,
|
||||
starGiftCount: stargiftsCount,
|
||||
isBotCanManageEmojiStatus: botCanManageEmojiStatus,
|
||||
hasScheduledMessages: hasScheduled,
|
||||
};
|
||||
}
|
||||
|
||||
@ -611,6 +611,15 @@ export function checkBotDownloadFileParams({
|
||||
});
|
||||
}
|
||||
|
||||
export function toggleUserEmojiStatusPermission({ bot, isEnabled } : { bot: ApiUser; isEnabled: boolean }) {
|
||||
return invokeRequest(new GramJs.bots.ToggleUserEmojiStatusPermission({
|
||||
bot: buildInputPeer(bot.id, bot.accessHash),
|
||||
enabled: isEnabled,
|
||||
}), {
|
||||
shouldReturnTrue: true,
|
||||
});
|
||||
}
|
||||
|
||||
function processInlineBotResult(queryId: string, results: GramJs.TypeBotInlineResult[]) {
|
||||
return results.map((result) => {
|
||||
if (result instanceof GramJs.BotInlineMediaResult) {
|
||||
|
||||
@ -62,6 +62,8 @@ export interface ApiUserFullInfo {
|
||||
businessWorkHours?: ApiBusinessWorkHours;
|
||||
businessIntro?: ApiBusinessIntro;
|
||||
starGiftCount?: number;
|
||||
isBotCanManageEmojiStatus?: boolean;
|
||||
isBotAccessEmojiGranted?: boolean;
|
||||
hasScheduledMessages?: boolean;
|
||||
botVerification?: ApiBotVerification;
|
||||
}
|
||||
|
||||
@ -1405,6 +1405,7 @@
|
||||
"CloseMiniApps" = "Close Mini Apps";
|
||||
"DoNotAskAgain" = "Don't ask again";
|
||||
"PaymentInfoDone" = "Proceed to checkout";
|
||||
"EmojiStatusAccessText" = "**{name}** requests access to set your **emoji status**. You will be able to revoke this access in the profile page of **{name}**.";
|
||||
"VideoConversionTitle" = "Improving Video...";
|
||||
"VideoConversionText" = "The video will be published after it's optimized for the best viewing experience.";
|
||||
"VideoConversionDone" = "Video published.";
|
||||
@ -1473,3 +1474,5 @@
|
||||
"ProfileTabVoice" = "Voice";
|
||||
"ProfileTabSharedGroups" = "Groups";
|
||||
"ProfileTabSimilarChannels" = "Similar Channels";
|
||||
"LocationPermissionText" = "**{name}** requests access to set your **location**. You will be able to revoke this access in the profile page of **{name}**.";
|
||||
|
||||
|
||||
@ -29,6 +29,8 @@ export { default as ChatInviteModal } from '../components/modals/chatInvite/Chat
|
||||
export { default as AboutAdsModal } from '../components/modals/aboutAds/AboutAdsModal';
|
||||
export { default as AboutMonetizationModal } from '../components/common/AboutMonetizationModal';
|
||||
export { default as VerificationMonetizationModal } from '../components/common/VerificationMonetizationModal';
|
||||
export { default as EmojiStatusAccessModal } from '../components/modals/emojiStatusAccess/EmojiStatusAccessModal';
|
||||
export { default as LocationAccessModal } from '../components/modals/locationAccess/LocationAccessModal';
|
||||
export { default as ReportAdModal } from '../components/modals/reportAd/ReportAdModal';
|
||||
export { default as ReportModal } from '../components/modals/reportModal/ReportModal';
|
||||
export { default as CalendarModal } from '../components/common/CalendarModal';
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
.root {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.25rem;
|
||||
|
||||
:global(.custom-emoji) {
|
||||
@ -17,8 +18,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
.transition {
|
||||
width: 1.5rem;
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ import renderText from './helpers/renderText';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
|
||||
import Transition from '../ui/Transition';
|
||||
import CustomEmoji from './CustomEmoji';
|
||||
import FakeIcon from './FakeIcon';
|
||||
import StarIcon from './icons/StarIcon';
|
||||
@ -44,7 +45,6 @@ type OwnProps = {
|
||||
noLoopLimit?: boolean;
|
||||
canCopyTitle?: boolean;
|
||||
iconElement?: React.ReactNode;
|
||||
allowMultiLine?: boolean;
|
||||
onEmojiStatusClick?: NoneToVoidFunction;
|
||||
observeIntersection?: ObserveFn;
|
||||
};
|
||||
@ -61,7 +61,6 @@ const FullNameTitle: FC<OwnProps> = ({
|
||||
noLoopLimit,
|
||||
canCopyTitle,
|
||||
iconElement,
|
||||
allowMultiLine,
|
||||
onEmojiStatusClick,
|
||||
observeIntersection,
|
||||
}) => {
|
||||
@ -125,7 +124,6 @@ const FullNameTitle: FC<OwnProps> = ({
|
||||
className={buildClassName(
|
||||
'fullName',
|
||||
styles.fullName,
|
||||
!allowMultiLine && styles.ellipsis,
|
||||
canCopyTitle && styles.canCopy,
|
||||
)}
|
||||
onClick={handleTitleClick}
|
||||
@ -137,13 +135,22 @@ const FullNameTitle: FC<OwnProps> = ({
|
||||
{!noVerified && peer?.isVerified && <VerifiedIcon />}
|
||||
{!noFake && peer?.fakeType && <FakeIcon fakeType={peer.fakeType} />}
|
||||
{canShowEmojiStatus && realPeer.emojiStatus && (
|
||||
<CustomEmoji
|
||||
documentId={realPeer.emojiStatus.documentId}
|
||||
size={emojiStatusSize}
|
||||
loopLimit={!noLoopLimit ? EMOJI_STATUS_LOOP_LIMIT : undefined}
|
||||
observeIntersectionForLoading={observeIntersection}
|
||||
onClick={onEmojiStatusClick}
|
||||
/>
|
||||
<Transition
|
||||
className={styles.transition}
|
||||
activeKey={Number(realPeer.emojiStatus.documentId)}
|
||||
name="fade"
|
||||
shouldCleanup
|
||||
shouldRestoreHeight
|
||||
>
|
||||
<CustomEmoji
|
||||
forceAlways
|
||||
documentId={realPeer.emojiStatus.documentId}
|
||||
size={emojiStatusSize}
|
||||
loopLimit={!noLoopLimit ? EMOJI_STATUS_LOOP_LIMIT : undefined}
|
||||
observeIntersectionForLoading={observeIntersection}
|
||||
onClick={onEmojiStatusClick}
|
||||
/>
|
||||
</Transition>
|
||||
)}
|
||||
{canShowEmojiStatus && !realPeer.emojiStatus && isPremium && <StarIcon />}
|
||||
</>
|
||||
|
||||
@ -45,6 +45,10 @@
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
&.notClickable {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.avatar,
|
||||
.iconWrapper {
|
||||
width: 2rem;
|
||||
|
||||
@ -38,6 +38,7 @@ type OwnProps<T = undefined> = {
|
||||
withEmojiStatus?: boolean;
|
||||
clickArg?: T;
|
||||
onClick?: (arg: T) => void;
|
||||
itemClassName?: string;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -62,6 +63,7 @@ const PeerChip = <T,>({
|
||||
withPeerColors,
|
||||
withEmojiStatus,
|
||||
onClick,
|
||||
itemClassName,
|
||||
}: OwnProps<T> & StateProps) => {
|
||||
const lang = useOldLang();
|
||||
|
||||
@ -105,6 +107,7 @@ const PeerChip = <T,>({
|
||||
canClose && styles.closeable,
|
||||
isCloseNonDestructive && styles.nonDestructive,
|
||||
fluid && styles.fluid,
|
||||
!onClick && styles.notClickable,
|
||||
withPeerColors && getPeerColorClass(customPeer || peer),
|
||||
className,
|
||||
);
|
||||
@ -118,7 +121,7 @@ const PeerChip = <T,>({
|
||||
>
|
||||
{iconElement}
|
||||
{!isMinimized && (
|
||||
<div className={styles.name} dir="auto">
|
||||
<div className={buildClassName(styles.name, itemClassName)} dir="auto">
|
||||
{titleElement}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -1,13 +1,18 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useEffect, useMemo, useState,
|
||||
memo, useMemo,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type {
|
||||
ApiChat,
|
||||
ApiCountryCode,
|
||||
ApiUser,
|
||||
ApiUserFullInfo,
|
||||
ApiUsername,
|
||||
ApiBotVerification,
|
||||
ApiChat, ApiCountryCode, ApiUser, ApiUserFullInfo, ApiUsername,
|
||||
} from '../../../api/types';
|
||||
import type { BotAppPermissions } from '../../../types';
|
||||
import { MAIN_THREAD_ID } from '../../../api/types';
|
||||
|
||||
import { FRAGMENT_PHONE_CODE, FRAGMENT_PHONE_LENGTH } from '../../../config';
|
||||
@ -20,6 +25,7 @@ import {
|
||||
selectIsChatMuted,
|
||||
} from '../../../global/helpers';
|
||||
import {
|
||||
selectBotAppPermissions,
|
||||
selectChat,
|
||||
selectChatFullInfo,
|
||||
selectCurrentMessageList,
|
||||
@ -31,7 +37,6 @@ import {
|
||||
} from '../../../global/selectors';
|
||||
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';
|
||||
@ -77,6 +82,8 @@ type StateProps = {
|
||||
hasSavedMessages?: boolean;
|
||||
personalChannel?: ApiChat;
|
||||
hasMainMiniApp?: boolean;
|
||||
isBotCanManageEmojiStatus?: boolean;
|
||||
botAppPermissions?: BotAppPermissions;
|
||||
botVerification?: ApiBotVerification;
|
||||
};
|
||||
|
||||
@ -86,7 +93,6 @@ const DEFAULT_MAP_CONFIG = {
|
||||
zoom: 15,
|
||||
};
|
||||
|
||||
const runDebounced = debounce((cb) => cb(), 500, false);
|
||||
const BOT_VERIFICATION_ICON_SIZE = 16;
|
||||
|
||||
const ChatExtra: FC<OwnProps & StateProps> = ({
|
||||
@ -105,6 +111,8 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
||||
hasSavedMessages,
|
||||
personalChannel,
|
||||
hasMainMiniApp,
|
||||
isBotCanManageEmojiStatus,
|
||||
botAppPermissions,
|
||||
botVerification,
|
||||
}) => {
|
||||
const {
|
||||
@ -116,6 +124,8 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
||||
openMapModal,
|
||||
requestCollectibleInfo,
|
||||
requestMainWebView,
|
||||
toggleUserEmojiStatusPermission,
|
||||
toggleUserLocationPermission,
|
||||
} = getActions();
|
||||
|
||||
const {
|
||||
@ -135,12 +145,6 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
|
||||
const [areNotificationsEnabled, setAreNotificationsEnabled] = useState(!isMuted);
|
||||
|
||||
useEffect(() => {
|
||||
setAreNotificationsEnabled(!isMuted);
|
||||
}, [isMuted]);
|
||||
|
||||
useEffectWithPrevDeps(([prevPeerId]) => {
|
||||
if (!peerId || prevPeerId === peerId) return;
|
||||
if (user || (chat && isChatChannel(chat))) {
|
||||
@ -198,23 +202,25 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
|
||||
const handleNotificationChange = useLastCallback(() => {
|
||||
setAreNotificationsEnabled((current) => {
|
||||
const newAreNotificationsEnabled = !current;
|
||||
|
||||
runDebounced(() => {
|
||||
if (isTopicInfo) {
|
||||
updateTopicMutedState({
|
||||
chatId: chatId!,
|
||||
topicId: topicId!,
|
||||
isMuted: !newAreNotificationsEnabled,
|
||||
});
|
||||
} else {
|
||||
updateChatMutedState({ chatId: chatId!, isMuted: !newAreNotificationsEnabled });
|
||||
}
|
||||
if (isTopicInfo) {
|
||||
updateTopicMutedState({
|
||||
chatId: chatId!,
|
||||
topicId: topicId!,
|
||||
isMuted: !isMuted,
|
||||
});
|
||||
} else {
|
||||
updateChatMutedState({ chatId: chatId!, isMuted: !isMuted });
|
||||
}
|
||||
});
|
||||
|
||||
return newAreNotificationsEnabled;
|
||||
});
|
||||
const manageEmojiStatusChange = useLastCallback(() => {
|
||||
if (!user) return;
|
||||
toggleUserEmojiStatusPermission({ botId: user.id, isEnabled: !isBotCanManageEmojiStatus });
|
||||
});
|
||||
|
||||
const handleLocationPermissionChange = useLastCallback(() => {
|
||||
if (!user) return;
|
||||
toggleUserLocationPermission({ botId: user.id, isAccessGranted: !botAppPermissions?.geolocation });
|
||||
});
|
||||
|
||||
const handleOpenSavedDialog = useLastCallback(() => {
|
||||
@ -416,7 +422,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
||||
<Switcher
|
||||
id="group-notifications"
|
||||
label={userId ? 'Toggle User Notifications' : 'Toggle Chat Notifications'}
|
||||
checked={areNotificationsEnabled}
|
||||
checked={isMuted}
|
||||
inactive
|
||||
/>
|
||||
</ListItem>
|
||||
@ -442,6 +448,26 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
||||
<span>{oldLang('SavedMessagesTab')}</span>
|
||||
</ListItem>
|
||||
)}
|
||||
{userFullInfo && 'isBotAccessEmojiGranted' in userFullInfo && (
|
||||
<ListItem icon="user" narrow ripple onClick={manageEmojiStatusChange}>
|
||||
<span>{oldLang('BotProfilePermissionEmojiStatus')}</span>
|
||||
<Switcher
|
||||
label={oldLang('BotProfilePermissionEmojiStatus')}
|
||||
checked={isBotCanManageEmojiStatus}
|
||||
inactive
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
{botAppPermissions?.geolocation !== undefined && (
|
||||
<ListItem icon="location" narrow ripple onClick={handleLocationPermissionChange}>
|
||||
<span>{oldLang('BotProfilePermissionLocation')}</span>
|
||||
<Switcher
|
||||
label={oldLang('BotProfilePermissionLocation')}
|
||||
checked={botAppPermissions?.geolocation}
|
||||
inactive
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
{botVerification && (
|
||||
<div className={styles.botVerificationSection}>
|
||||
<CustomEmoji
|
||||
@ -462,6 +488,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
const chat = chatOrUserId ? selectChat(global, chatOrUserId) : undefined;
|
||||
const user = chatOrUserId ? selectUser(global, chatOrUserId) : undefined;
|
||||
const botAppPermissions = chatOrUserId ? selectBotAppPermissions(global, chatOrUserId) : undefined;
|
||||
const isForum = chat?.isForum;
|
||||
const isMuted = chat && selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global));
|
||||
const { threadId } = selectCurrentMessageList(global) || {};
|
||||
@ -473,7 +500,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
const botVerification = userFullInfo?.botVerification || chatFullInfo?.botVerification;
|
||||
|
||||
const chatInviteLink = chatFullInfo?.inviteLink;
|
||||
|
||||
const description = userFullInfo?.bio || chatFullInfo?.about;
|
||||
|
||||
const canInviteUsers = chat && !user && (
|
||||
@ -497,6 +523,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
user,
|
||||
userFullInfo,
|
||||
canInviteUsers,
|
||||
botAppPermissions,
|
||||
isMuted,
|
||||
topicId,
|
||||
chatInviteLink,
|
||||
@ -505,6 +532,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
hasSavedMessages,
|
||||
personalChannel,
|
||||
hasMainMiniApp,
|
||||
isBotCanManageEmojiStatus: userFullInfo?.isBotCanManageEmojiStatus,
|
||||
botVerification,
|
||||
};
|
||||
},
|
||||
|
||||
@ -13,11 +13,13 @@ import BoostModal from './boost/BoostModal.async';
|
||||
import ChatInviteModal from './chatInvite/ChatInviteModal.async';
|
||||
import ChatlistModal from './chatlist/ChatlistModal.async';
|
||||
import CollectibleInfoModal from './collectible/CollectibleInfoModal.async';
|
||||
import EmojiStatusAccessModal from './emojiStatusAccess/EmojiStatusAccessModal.async';
|
||||
import PremiumGiftModal from './gift/GiftModal.async';
|
||||
import GiftInfoModal from './gift/info/GiftInfoModal.async';
|
||||
import GiftRecipientPicker from './gift/recipient/GiftRecipientPicker.async';
|
||||
import GiftCodeModal from './giftcode/GiftCodeModal.async';
|
||||
import InviteViaLinkModal from './inviteViaLink/InviteViaLinkModal.async';
|
||||
import LocationAccessModal from './locationAccess/LocationAccessModal.async';
|
||||
import MapModal from './map/MapModal.async';
|
||||
import OneTimeMediaModal from './oneTimeMedia/OneTimeMediaModal.async';
|
||||
import PaidReactionModal from './paidReaction/PaidReactionModal.async';
|
||||
@ -59,6 +61,8 @@ type ModalKey = keyof Pick<TabState,
|
||||
'isWebAppsCloseConfirmationModalOpen' |
|
||||
'giftInfoModal' |
|
||||
'suggestedStatusModal' |
|
||||
'emojiStatusAccessModal' |
|
||||
'locationAccessModal' |
|
||||
'aboutAdsModal'
|
||||
>;
|
||||
|
||||
@ -99,6 +103,8 @@ const MODALS: ModalRegistry = {
|
||||
isWebAppsCloseConfirmationModalOpen: WebAppsCloseConfirmationModal,
|
||||
giftInfoModal: GiftInfoModal,
|
||||
suggestedStatusModal: SuggestedStatusModal,
|
||||
emojiStatusAccessModal: EmojiStatusAccessModal,
|
||||
locationAccessModal: LocationAccessModal,
|
||||
aboutAdsModal: AboutAdsModal,
|
||||
};
|
||||
const MODAL_KEYS = Object.keys(MODALS) as ModalKey[];
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React from '../../../lib/teact/teact';
|
||||
|
||||
import type { OwnProps } from './EmojiStatusAccessModal';
|
||||
|
||||
import { Bundles } from '../../../util/moduleLoader';
|
||||
|
||||
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||
|
||||
const EmojiStatusAccessModalAsync: FC<OwnProps> = (props) => {
|
||||
const { modal } = props;
|
||||
const EmojiStatusAccessModal = useModuleLoader(Bundles.Extra, 'EmojiStatusAccessModal', !modal);
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return EmojiStatusAccessModal ? <EmojiStatusAccessModal {...props} /> : undefined;
|
||||
};
|
||||
|
||||
export default EmojiStatusAccessModalAsync;
|
||||
@ -0,0 +1,9 @@
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chatItem {
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
@ -0,0 +1,204 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useEffect,
|
||||
useMemo, useRef, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiStickerSet, ApiUser } from '../../../api/types';
|
||||
import type { TabState } from '../../../global/types';
|
||||
|
||||
import { getUserFullName } from '../../../global/helpers';
|
||||
import { selectIsCurrentUserPremium, selectStickerSet, selectUser } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
|
||||
import useInterval from '../../../hooks/schedulers/useInterval';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import PeerChip from '../../common/PeerChip';
|
||||
import Button from '../../ui/Button';
|
||||
import Modal from '../../ui/Modal';
|
||||
|
||||
import styles from './EmojiStatusAccessModal.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
modal: TabState['emojiStatusAccessModal'];
|
||||
};
|
||||
|
||||
export type StateProps = {
|
||||
currentUser?: ApiUser;
|
||||
stickerSet?: ApiStickerSet;
|
||||
isPremium?: boolean;
|
||||
};
|
||||
|
||||
const INTERVAL = 3000;
|
||||
|
||||
const EmojiStatusAccessModal: FC<OwnProps & StateProps> = ({
|
||||
modal,
|
||||
currentUser,
|
||||
stickerSet,
|
||||
isPremium,
|
||||
}) => {
|
||||
const {
|
||||
closeEmojiStatusAccessModal,
|
||||
toggleUserEmojiStatusPermission,
|
||||
sendWebAppEvent,
|
||||
openPremiumModal,
|
||||
loadDefaultStatusIcons,
|
||||
} = getActions();
|
||||
|
||||
const isOpen = Boolean(modal);
|
||||
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [currentStatusIndex, setCurrentStatusIndex] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && !stickerSet?.stickers) {
|
||||
loadDefaultStatusIcons();
|
||||
}
|
||||
}, [isOpen, stickerSet]);
|
||||
|
||||
const mockPeerWithStatus = useMemo(() => {
|
||||
if (!currentUser || !stickerSet?.stickers) return undefined;
|
||||
return {
|
||||
...currentUser,
|
||||
emojiStatus: {
|
||||
documentId: stickerSet.stickers[currentStatusIndex].id,
|
||||
},
|
||||
} satisfies ApiUser;
|
||||
}, [currentUser, stickerSet, currentStatusIndex]);
|
||||
|
||||
const totalCount = stickerSet?.stickers?.length;
|
||||
useInterval(
|
||||
() => {
|
||||
if (!totalCount) return;
|
||||
setCurrentStatusIndex((prevIndex) => (prevIndex + 1) % totalCount);
|
||||
},
|
||||
totalCount ? INTERVAL : undefined,
|
||||
);
|
||||
|
||||
const renderPickerItem = useLastCallback(() => {
|
||||
return (
|
||||
<PeerChip
|
||||
withEmojiStatus
|
||||
className={styles.chatItem}
|
||||
itemClassName={styles.itemName}
|
||||
mockPeer={mockPeerWithStatus}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const confirmHandler = useLastCallback(() => {
|
||||
if (!modal?.bot?.id) return;
|
||||
closeEmojiStatusAccessModal();
|
||||
if (modal?.webAppKey) {
|
||||
if (isPremium) {
|
||||
sendWebAppEvent({
|
||||
webAppKey: modal.webAppKey,
|
||||
event: {
|
||||
eventType: 'emoji_status_access_requested',
|
||||
eventData: {
|
||||
status: 'allowed',
|
||||
},
|
||||
},
|
||||
});
|
||||
toggleUserEmojiStatusPermission({ botId: modal.bot.id, isEnabled: true, isBotAccessEmojiGranted: true });
|
||||
} else {
|
||||
openPremiumModal();
|
||||
sendWebAppEvent({
|
||||
webAppKey: modal.webAppKey,
|
||||
event: {
|
||||
eventType: 'emoji_status_access_requested',
|
||||
eventData: {
|
||||
status: 'cancelled',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const onCloseHandler = useLastCallback(() => {
|
||||
if (!modal?.bot?.id) return;
|
||||
closeEmojiStatusAccessModal();
|
||||
if (modal?.webAppKey) {
|
||||
sendWebAppEvent({
|
||||
webAppKey: modal.webAppKey,
|
||||
event: {
|
||||
eventType: 'emoji_status_access_requested',
|
||||
eventData: {
|
||||
status: 'cancelled',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (isPremium) {
|
||||
toggleUserEmojiStatusPermission({ botId: modal.bot.id, isEnabled: false });
|
||||
}
|
||||
});
|
||||
|
||||
const renderStatusText = useLastCallback(() => {
|
||||
if (!modal?.bot) return undefined;
|
||||
return lang('EmojiStatusAccessText', {
|
||||
name: getUserFullName(modal?.bot!),
|
||||
}, {
|
||||
withNodes: true,
|
||||
withMarkdown: true,
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={buildClassName('confirm')}
|
||||
contentClassName={styles.content}
|
||||
isOpen={isOpen}
|
||||
onClose={onCloseHandler}
|
||||
>
|
||||
{renderPickerItem()}
|
||||
<div>
|
||||
{renderStatusText()}
|
||||
<div
|
||||
className="dialog-buttons mt-2"
|
||||
ref={containerRef}
|
||||
>
|
||||
<Button
|
||||
className="confirm-dialog-button"
|
||||
isText
|
||||
onClick={confirmHandler}
|
||||
color="primary"
|
||||
>
|
||||
{oldLang('lng_bot_allow_write_confirm')}
|
||||
</Button>
|
||||
<Button
|
||||
className="confirm-dialog-button"
|
||||
isText
|
||||
onClick={onCloseHandler}
|
||||
>
|
||||
{lang('Cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): StateProps => {
|
||||
const currentUser = selectUser(global, global.currentUserId!);
|
||||
const isPremium = selectIsCurrentUserPremium(global);
|
||||
const stickerSet = global.defaultStatusIconsId ? selectStickerSet(global, global.defaultStatusIconsId) : undefined;
|
||||
|
||||
return {
|
||||
currentUser,
|
||||
stickerSet,
|
||||
isPremium,
|
||||
};
|
||||
},
|
||||
)(EmojiStatusAccessModal));
|
||||
@ -0,0 +1,18 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React from '../../../lib/teact/teact';
|
||||
|
||||
import type { OwnProps } from './LocationAccessModal';
|
||||
|
||||
import { Bundles } from '../../../util/moduleLoader';
|
||||
|
||||
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||
|
||||
const LocationAccessModalAsync: FC<OwnProps> = (props) => {
|
||||
const { modal } = props;
|
||||
const LocationAccessModal = useModuleLoader(Bundles.Extra, 'LocationAccessModal', !modal);
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return LocationAccessModal ? <LocationAccessModal {...props} /> : undefined;
|
||||
};
|
||||
|
||||
export default LocationAccessModalAsync;
|
||||
@ -0,0 +1,11 @@
|
||||
.avatars {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 2rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
168
src/components/modals/locationAccess/LocationAccessModal.tsx
Normal file
168
src/components/modals/locationAccess/LocationAccessModal.tsx
Normal file
@ -0,0 +1,168 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useRef,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiUser } from '../../../api/types';
|
||||
import type { TabState } from '../../../global/types';
|
||||
|
||||
import { getUserFullName } from '../../../global/helpers';
|
||||
import { selectUser } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { getGeolocationStatus } from '../../../util/windowEnvironment';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import Avatar from '../../common/Avatar';
|
||||
import Icon from '../../common/icons/Icon';
|
||||
import Button from '../../ui/Button';
|
||||
import Modal from '../../ui/Modal';
|
||||
|
||||
import styles from './LocationAccessModal.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
modal: TabState['locationAccessModal'];
|
||||
};
|
||||
|
||||
export type StateProps = {
|
||||
currentUser?: ApiUser;
|
||||
};
|
||||
|
||||
const LocationAccessModal: FC<OwnProps & StateProps> = ({
|
||||
modal,
|
||||
currentUser,
|
||||
}) => {
|
||||
const {
|
||||
closeLocationAccessModal, toggleUserLocationPermission, sendWebAppEvent,
|
||||
} = getActions();
|
||||
|
||||
const isOpen = Boolean(modal);
|
||||
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const confirmHandler = useLastCallback(async () => {
|
||||
const geolocationData = await getGeolocationStatus();
|
||||
const { geolocation } = geolocationData;
|
||||
if (!modal?.bot?.id) return;
|
||||
closeLocationAccessModal();
|
||||
if (modal?.webAppKey) {
|
||||
toggleUserLocationPermission({
|
||||
botId: modal.bot.id,
|
||||
isAccessGranted: true,
|
||||
});
|
||||
sendWebAppEvent({
|
||||
webAppKey: modal.webAppKey,
|
||||
event: {
|
||||
eventType: 'location_requested',
|
||||
eventData: {
|
||||
available: true,
|
||||
latitude: geolocation?.latitude,
|
||||
longitude: geolocation?.longitude,
|
||||
altitude: geolocation?.altitude,
|
||||
course: geolocation?.heading,
|
||||
speed: geolocation?.speed,
|
||||
horizontal_accuracy: geolocation?.accuracy,
|
||||
vertical_accuracy: geolocation?.accuracy,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const onCloseHandler = useLastCallback(() => {
|
||||
if (!modal?.bot?.id) return;
|
||||
closeLocationAccessModal();
|
||||
if (modal?.webAppKey) {
|
||||
toggleUserLocationPermission({
|
||||
botId: modal.bot.id,
|
||||
isAccessGranted: false,
|
||||
});
|
||||
sendWebAppEvent({
|
||||
webAppKey: modal.webAppKey,
|
||||
event: {
|
||||
eventType: 'location_requested',
|
||||
eventData: {
|
||||
available: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const renderInfo = useLastCallback(() => {
|
||||
if (!modal?.bot) return undefined;
|
||||
return (
|
||||
<div className={styles.avatars}>
|
||||
<Avatar
|
||||
size="large"
|
||||
peer={currentUser}
|
||||
/>
|
||||
<Icon name="next" className={styles.arrow} />
|
||||
<Avatar
|
||||
size="large"
|
||||
peer={modal.bot}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const renderStatusText = useLastCallback(() => {
|
||||
if (!modal?.bot) return undefined;
|
||||
return lang('LocationPermissionText', {
|
||||
name: getUserFullName(modal?.bot!),
|
||||
}, {
|
||||
withNodes: true,
|
||||
withMarkdown: true,
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={buildClassName('confirm')}
|
||||
isOpen={isOpen}
|
||||
onClose={onCloseHandler}
|
||||
>
|
||||
{renderInfo()}
|
||||
<div>
|
||||
{renderStatusText()}
|
||||
<div
|
||||
className="dialog-buttons mt-2"
|
||||
ref={containerRef}
|
||||
>
|
||||
<Button
|
||||
className="confirm-dialog-button"
|
||||
isText
|
||||
onClick={confirmHandler}
|
||||
color="primary"
|
||||
>
|
||||
{oldLang('lng_bot_allow_write_confirm')}
|
||||
</Button>
|
||||
<Button
|
||||
className="confirm-dialog-button"
|
||||
isText
|
||||
onClick={onCloseHandler}
|
||||
>
|
||||
{lang('Cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): StateProps => {
|
||||
const currentUser = selectUser(global, global.currentUserId!);
|
||||
|
||||
return {
|
||||
currentUser,
|
||||
};
|
||||
},
|
||||
)(LocationAccessModal));
|
||||
@ -7,24 +7,33 @@ import React, {
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type {
|
||||
ApiAttachBot, ApiBotAppSettings, ApiChat, ApiUser,
|
||||
ApiAttachBot, ApiBotAppSettings, ApiUser,
|
||||
} from '../../../api/types';
|
||||
import type { TabState } from '../../../global/types';
|
||||
import type { ThemeKey } from '../../../types';
|
||||
import type { BotAppPermissions, ThemeKey } from '../../../types';
|
||||
import type {
|
||||
PopupOptions, WebApp, WebAppInboundEvent, WebAppModalStateType, WebAppOutboundEvent,
|
||||
PopupOptions,
|
||||
WebApp,
|
||||
WebAppInboundEvent,
|
||||
WebAppModalStateType,
|
||||
WebAppOutboundEvent,
|
||||
} from '../../../types/webapp';
|
||||
|
||||
import { TME_LINK_PREFIX } from '../../../config';
|
||||
import { convertToApiChatType } from '../../../global/helpers';
|
||||
import { getWebAppKey } from '../../../global/helpers/bots';
|
||||
import {
|
||||
selectCurrentChat, selectTabState, selectTheme, selectUser, selectUserFullInfo,
|
||||
selectBotAppPermissions,
|
||||
selectTabState,
|
||||
selectTheme,
|
||||
selectUser,
|
||||
selectUserFullInfo,
|
||||
selectWebApp,
|
||||
} from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import download from '../../../util/download';
|
||||
import { extractCurrentThemeParams, validateHexColor } from '../../../util/themeStyle';
|
||||
import { getGeolocationStatus, IS_GEOLOCATION_SUPPORTED } from '../../../util/windowEnvironment';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
|
||||
@ -71,14 +80,16 @@ export type OwnProps = {
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
chat?: ApiChat;
|
||||
bot?: ApiUser;
|
||||
currentUser?: ApiUser;
|
||||
botAppSettings?: ApiBotAppSettings;
|
||||
attachBot?: ApiAttachBot;
|
||||
theme?: ThemeKey;
|
||||
isPaymentModalOpen?: boolean;
|
||||
paymentStatus?: TabState['payment']['status'];
|
||||
isPremium?: boolean;
|
||||
modalState?: WebAppModalStateType;
|
||||
botAppPermissions?: BotAppPermissions;
|
||||
};
|
||||
|
||||
const NBSP = '\u00A0';
|
||||
@ -117,6 +128,7 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
||||
modalState,
|
||||
isMultiTabSupported,
|
||||
onContextMenuButtonClick,
|
||||
botAppPermissions,
|
||||
botAppSettings,
|
||||
modalHeight,
|
||||
}) => {
|
||||
@ -130,6 +142,10 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
||||
sharePhoneWithBot,
|
||||
updateWebApp,
|
||||
resetPaymentStatus,
|
||||
openChatWithInfo,
|
||||
showNotification,
|
||||
openEmojiStatusAccessModal,
|
||||
openLocationAccessModal,
|
||||
changeWebAppModalState,
|
||||
closeWebAppModal,
|
||||
} = getActions();
|
||||
@ -190,6 +206,10 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const isActive = (activeWebApp && webApp) && activeWebAppKey === webAppKey;
|
||||
|
||||
const isAvailable = IS_GEOLOCATION_SUPPORTED;
|
||||
const isAccessRequested = botAppPermissions?.geolocation !== undefined;
|
||||
const isAccessGranted = Boolean(botAppPermissions?.geolocation);
|
||||
|
||||
const updateCurrentWebApp = useLastCallback((updatedPartialWebApp: Partial<WebApp>) => {
|
||||
if (!webAppKey) return;
|
||||
updateWebApp({ key: webAppKey, update: updatedPartialWebApp });
|
||||
@ -425,7 +445,8 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
|
||||
const handleAcceptWriteAccess = useLastCallback(async () => {
|
||||
const result = await callApi('allowBotSendMessages', { bot: bot! });
|
||||
if (!bot) return;
|
||||
const result = await callApi('allowBotSendMessages', { bot });
|
||||
if (!result) {
|
||||
handleRejectWriteAccess();
|
||||
return;
|
||||
@ -442,8 +463,9 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
|
||||
async function handleRequestWriteAccess() {
|
||||
if (!bot) return;
|
||||
const canWrite = await callApi('fetchBotCanSendMessage', {
|
||||
bot: bot!,
|
||||
bot,
|
||||
});
|
||||
|
||||
if (canWrite) {
|
||||
@ -454,7 +476,6 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setIsRequestingWriteAccess(!canWrite);
|
||||
}
|
||||
|
||||
@ -527,6 +548,10 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handleOpenChat = useLastCallback(() => {
|
||||
openChatWithInfo({ id: bot!.id });
|
||||
});
|
||||
|
||||
function handleEvent(event: WebAppInboundEvent) {
|
||||
const { eventType, eventData } = event;
|
||||
|
||||
@ -616,8 +641,8 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
||||
|
||||
if (eventType === 'web_app_open_popup') {
|
||||
if (popupParameters || !eventData.message.trim().length || !eventData.buttons?.length
|
||||
|| eventData.buttons.length > 3 || isRequestingPhone || isRequestingWriteAccess
|
||||
|| unlockPopupsAt > Date.now()) {
|
||||
|| eventData.buttons.length > 3 || isRequestingPhone || isRequestingWriteAccess
|
||||
|| unlockPopupsAt > Date.now()) {
|
||||
handleAppPopupClose(undefined);
|
||||
return;
|
||||
}
|
||||
@ -672,6 +697,74 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
handleCheckDownloadFile(eventData.url, eventData.file_name);
|
||||
}
|
||||
|
||||
if (eventType === 'web_app_request_emoji_status_access') {
|
||||
if (!bot) return;
|
||||
openEmojiStatusAccessModal({ bot, webAppKey });
|
||||
}
|
||||
|
||||
if (eventType === 'web_app_check_location') {
|
||||
const handleGeolocationCheck = () => {
|
||||
sendEvent({
|
||||
eventType: 'location_checked',
|
||||
eventData: {
|
||||
available: isAvailable,
|
||||
access_requested: isAccessRequested,
|
||||
access_granted: isAccessGranted,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
handleGeolocationCheck();
|
||||
}
|
||||
|
||||
if (eventType === 'web_app_request_location') {
|
||||
const handleRequestLocation = async () => {
|
||||
const geolocationData = await getGeolocationStatus();
|
||||
const { accessRequested, accessGranted, geolocation } = geolocationData;
|
||||
|
||||
if (!accessGranted || !accessRequested) {
|
||||
sendEvent({
|
||||
eventType: 'location_requested',
|
||||
eventData: {
|
||||
available: false,
|
||||
},
|
||||
});
|
||||
showNotification({ message: oldLang('PermissionNoLocationPosition') });
|
||||
handleAppPopupClose(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAvailable) {
|
||||
if (isAccessRequested) {
|
||||
sendEvent({
|
||||
eventType: 'location_requested',
|
||||
eventData: {
|
||||
available: botAppPermissions?.geolocation!,
|
||||
latitude: geolocation?.latitude,
|
||||
longitude: geolocation?.longitude,
|
||||
altitude: geolocation?.altitude,
|
||||
course: geolocation?.heading,
|
||||
speed: geolocation?.speed,
|
||||
horizontal_accuracy: geolocation?.accuracy,
|
||||
vertical_accuracy: geolocation?.accuracy,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
openLocationAccessModal({ bot, webAppKey });
|
||||
}
|
||||
} else {
|
||||
showNotification({ message: oldLang('PermissionNoLocationPosition') });
|
||||
handleAppPopupClose(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
handleRequestLocation();
|
||||
}
|
||||
|
||||
if (eventType === 'web_app_open_location_settings') {
|
||||
handleOpenChat();
|
||||
}
|
||||
}
|
||||
|
||||
const mainButtonCurrentColor = useCurrentOrPrev(mainButton?.color, true);
|
||||
@ -989,7 +1082,7 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
||||
>
|
||||
{!secondaryButton?.isProgressVisible && secondaryButtonCurrentText}
|
||||
{secondaryButton?.isProgressVisible
|
||||
&& <Spinner className={styles.mainButtonSpinner} color="blue" />}
|
||||
&& <Spinner className={styles.mainButtonSpinner} color="blue" />}
|
||||
</Button>
|
||||
<Button
|
||||
className={buildClassName(
|
||||
@ -1056,7 +1149,7 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
||||
/>
|
||||
<ConfirmDialog
|
||||
isOpen={Boolean(requestedFileDownload)}
|
||||
title={lang('BotDownloadFileTitle')}
|
||||
title={oldLang('BotDownloadFileTitle')}
|
||||
textParts={lang('BotDownloadFileDescription', {
|
||||
bot: bot?.firstName,
|
||||
filename: requestedFileDownload?.fileName,
|
||||
@ -1064,7 +1157,7 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
||||
withNodes: true,
|
||||
withMarkdown: true,
|
||||
})}
|
||||
confirmLabel={lang('BotDownloadFileButton')}
|
||||
confirmLabel={oldLang('BotDownloadFileButton')}
|
||||
onClose={handleRejectFileDownload}
|
||||
confirmHandler={handleDownloadFile}
|
||||
/>
|
||||
@ -1100,21 +1193,23 @@ export default memo(withGlobal<OwnProps>(
|
||||
const bot = activeBotId ? selectUser(global, activeBotId) : undefined;
|
||||
const userFullInfo = activeBotId ? selectUserFullInfo(global, activeBotId) : undefined;
|
||||
const botAppSettings = userFullInfo?.botInfo?.appSettings;
|
||||
const chat = selectCurrentChat(global);
|
||||
const currentUser = global.currentUserId ? selectUser(global, global.currentUserId) : undefined;
|
||||
const theme = selectTheme(global);
|
||||
const { isPaymentModalOpen, status: regularPaymentStatus } = selectTabState(global).payment;
|
||||
const { status: starsPaymentStatus, inputInvoice: starsInputInvoice } = selectTabState(global).starsPayment;
|
||||
const botAppPermissions = bot ? selectBotAppPermissions(global, bot.id) : undefined;
|
||||
|
||||
const paymentStatus = starsPaymentStatus || regularPaymentStatus;
|
||||
|
||||
return {
|
||||
attachBot,
|
||||
bot,
|
||||
chat,
|
||||
currentUser,
|
||||
theme,
|
||||
isPaymentModalOpen: isPaymentModalOpen || Boolean(starsInputInvoice),
|
||||
paymentStatus,
|
||||
modalState,
|
||||
botAppPermissions,
|
||||
botAppSettings,
|
||||
};
|
||||
},
|
||||
|
||||
@ -60,7 +60,7 @@ const ConfirmDialog: FC<OwnProps> = ({
|
||||
return (
|
||||
<Modal
|
||||
className={buildClassName('confirm', className)}
|
||||
title={title || lang('Telegram')}
|
||||
title={(title || lang('Telegram'))}
|
||||
header={header}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
|
||||
@ -340,8 +340,8 @@ function Transition({
|
||||
return;
|
||||
}
|
||||
|
||||
const { clientHeight } = activeElement || {};
|
||||
if (!clientHeight) {
|
||||
const { clientHeight, clientWidth } = activeElement || {};
|
||||
if (!clientHeight || !clientWidth) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -31,7 +31,11 @@ import {
|
||||
addActionHandler, getGlobal, setGlobal,
|
||||
} from '../../index';
|
||||
import {
|
||||
removeBlockedUser, updateManagementProgress, updateUser, updateUserFullInfo,
|
||||
removeBlockedUser,
|
||||
updateBotAppPermissions,
|
||||
updateManagementProgress,
|
||||
updateUser,
|
||||
updateUserFullInfo,
|
||||
} from '../../reducers';
|
||||
import {
|
||||
activateWebAppIfOpen,
|
||||
@ -1318,6 +1322,44 @@ addActionHandler('setBotInfo', async (global, actions, payload): Promise<void> =
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('toggleUserEmojiStatusPermission', async (global, actions, payload): Promise<void> => {
|
||||
const {
|
||||
botId, isEnabled, isBotAccessEmojiGranted,
|
||||
} = payload;
|
||||
|
||||
const bot = selectBot(global, botId);
|
||||
|
||||
if (!botId || !bot) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await callApi('toggleUserEmojiStatusPermission', {
|
||||
bot, isEnabled,
|
||||
});
|
||||
|
||||
if (!result) return;
|
||||
|
||||
global = getGlobal();
|
||||
global = updateUserFullInfo(global, botId, {
|
||||
isBotCanManageEmojiStatus: isEnabled,
|
||||
isBotAccessEmojiGranted,
|
||||
});
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('toggleUserLocationPermission', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
botId, isAccessGranted,
|
||||
} = payload;
|
||||
|
||||
const bot = selectUser(global, botId);
|
||||
if (!bot) return;
|
||||
|
||||
global = getGlobal();
|
||||
global = updateBotAppPermissions(global, bot.id, { geolocation: isAccessGranted });
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('startBotFatherConversation', async (global, actions, payload): Promise<void> => {
|
||||
const {
|
||||
param,
|
||||
|
||||
@ -224,3 +224,53 @@ addActionHandler('cancelAttachBotInChat', (global, actions, payload): ActionRetu
|
||||
requestedAttachBotInChat: undefined,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('openEmojiStatusAccessModal', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
bot, webAppKey, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
if (!bot || !webAppKey) return;
|
||||
|
||||
global = getGlobal();
|
||||
global = updateTabState(global, {
|
||||
emojiStatusAccessModal: {
|
||||
bot,
|
||||
webAppKey,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('closeEmojiStatusAccessModal', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
return updateTabState(global, {
|
||||
emojiStatusAccessModal: undefined,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('openLocationAccessModal', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
bot, webAppKey, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
if (!bot || !webAppKey) return;
|
||||
|
||||
global = getGlobal();
|
||||
global = updateTabState(global, {
|
||||
locationAccessModal: {
|
||||
bot,
|
||||
webAppKey,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('closeLocationAccessModal', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
return updateTabState(global, {
|
||||
locationAccessModal: undefined,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
@ -2,7 +2,13 @@
|
||||
import { getIsHeavyAnimating, onFullyIdle } from '../lib/teact/teact';
|
||||
import { addCallback, removeCallback } from '../lib/teact/teactn';
|
||||
|
||||
import type { ApiAvailableReaction, ApiMessage } from '../api/types';
|
||||
import type {
|
||||
ApiAvailableReaction,
|
||||
ApiBotPreviewMedia,
|
||||
ApiMessage,
|
||||
ApiUserCommonChats,
|
||||
ApiUserGifts,
|
||||
} from '../api/types';
|
||||
import type { MessageList, ThreadId } from '../types';
|
||||
import type { ActionReturnType, GlobalState } from './types';
|
||||
import { MAIN_THREAD_ID } from '../api/types';
|
||||
@ -253,6 +259,9 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
|
||||
if (!cached.users.commonChatsById) {
|
||||
cached.users.commonChatsById = initialState.users.commonChatsById;
|
||||
}
|
||||
if (!cached.users.botAppPermissionsById) {
|
||||
cached.users.botAppPermissionsById = initialState.users.botAppPermissionsById;
|
||||
}
|
||||
if (!cached.chats.topicsInfoById) {
|
||||
cached.chats.topicsInfoById = initialState.chats.topicsInfoById;
|
||||
}
|
||||
@ -373,10 +382,18 @@ function reduceCustomEmojis<T extends GlobalState>(global: T): GlobalState['cust
|
||||
};
|
||||
}
|
||||
|
||||
function reduceUsers<T extends GlobalState>(global: T): GlobalState['users'] {
|
||||
function reduceUsers<T extends GlobalState>(global: T): {
|
||||
commonChatsById: Record<string, ApiUserCommonChats>;
|
||||
giftsById: Record<string, ApiUserGifts>;
|
||||
botAppPermissionsById: any;
|
||||
statusesById: any;
|
||||
fullInfoById: any;
|
||||
byId: any;
|
||||
previewMediaByBotId: Record<string, ApiBotPreviewMedia[]>;
|
||||
} {
|
||||
const {
|
||||
users: {
|
||||
byId, statusesById, fullInfoById,
|
||||
byId, statusesById, fullInfoById, botAppPermissionsById,
|
||||
}, currentUserId,
|
||||
} = global;
|
||||
const currentChatIds = compact(
|
||||
@ -413,6 +430,7 @@ function reduceUsers<T extends GlobalState>(global: T): GlobalState['users'] {
|
||||
byId: pickTruthy(byId, idsToSave),
|
||||
statusesById: pickTruthy(statusesById, idsToSave),
|
||||
fullInfoById: pickTruthy(fullInfoById, idsToSave),
|
||||
botAppPermissionsById,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -103,6 +103,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
||||
previewMediaByBotId: {},
|
||||
commonChatsById: {},
|
||||
giftsById: {},
|
||||
botAppPermissionsById: {},
|
||||
},
|
||||
|
||||
chats: {
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
import type {
|
||||
ApiMissingInvitedUser, ApiUser, ApiUserCommonChats, ApiUserFullInfo, ApiUserStatus,
|
||||
ApiMissingInvitedUser,
|
||||
ApiUser,
|
||||
ApiUserCommonChats,
|
||||
ApiUserFullInfo,
|
||||
ApiUserStatus,
|
||||
} from '../../api/types';
|
||||
import type { BotAppPermissions } from '../../types';
|
||||
import type { GlobalState, TabArgs, TabState } from '../types';
|
||||
|
||||
import { areDeepEqual } from '../../util/areDeepEqual';
|
||||
@ -295,3 +300,25 @@ export function updateMissingInvitedUsers<T extends GlobalState>(
|
||||
},
|
||||
}, tabId);
|
||||
}
|
||||
|
||||
export function updateBotAppPermissions<T extends GlobalState>(
|
||||
global: T,
|
||||
botId: string,
|
||||
permissions: BotAppPermissions,
|
||||
): T {
|
||||
const { botAppPermissionsById } = global.users;
|
||||
|
||||
return {
|
||||
...global,
|
||||
users: {
|
||||
...global.users,
|
||||
botAppPermissionsById: {
|
||||
...botAppPermissionsById,
|
||||
[botId]: {
|
||||
...botAppPermissionsById[botId],
|
||||
...permissions,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import type {
|
||||
ApiUser, ApiUserCommonChats, ApiUserFullInfo, ApiUserStatus,
|
||||
ApiUser, ApiUserCommonChats,
|
||||
ApiUserFullInfo, ApiUserStatus,
|
||||
} from '../../api/types';
|
||||
import type { BotAppPermissions } from '../../types';
|
||||
import type { GlobalState } from '../types';
|
||||
|
||||
import { SERVICE_NOTIFICATIONS_USER_ID } from '../../config';
|
||||
@ -67,3 +69,9 @@ export function selectCanGift<T extends GlobalState>(global: T, userId: string)
|
||||
return !selectIsPremiumPurchaseBlocked(global) && user && !bot
|
||||
&& !user.isSelf && userId !== SERVICE_NOTIFICATIONS_USER_ID;
|
||||
}
|
||||
|
||||
export function selectBotAppPermissions<T extends GlobalState>(
|
||||
global: T, userId: string,
|
||||
): BotAppPermissions | undefined {
|
||||
return global.users.botAppPermissionsById[userId];
|
||||
}
|
||||
|
||||
@ -84,11 +84,7 @@ import type {
|
||||
ThreadId,
|
||||
WebPageMediaSize,
|
||||
} from '../../types';
|
||||
import type {
|
||||
WebApp,
|
||||
WebAppModalStateType,
|
||||
WebAppOutboundEvent,
|
||||
} from '../../types/webapp';
|
||||
import type { WebApp, WebAppModalStateType, WebAppOutboundEvent } from '../../types/webapp';
|
||||
import type { DownloadableMedia } from '../helpers';
|
||||
import type { TabState } from './tabState';
|
||||
|
||||
@ -2424,6 +2420,29 @@ export interface ActionPayloads {
|
||||
file?: File;
|
||||
isSuggest?: boolean;
|
||||
} & WithTabId;
|
||||
|
||||
openEmojiStatusAccessModal: {
|
||||
bot?: ApiUser;
|
||||
webAppKey?: string;
|
||||
} & WithTabId;
|
||||
closeEmojiStatusAccessModal: WithTabId | undefined;
|
||||
|
||||
openLocationAccessModal: {
|
||||
bot?: ApiUser;
|
||||
webAppKey?: string;
|
||||
} & WithTabId;
|
||||
closeLocationAccessModal: WithTabId | undefined;
|
||||
|
||||
toggleUserEmojiStatusPermission: {
|
||||
botId: string;
|
||||
isEnabled: boolean;
|
||||
isBotAccessEmojiGranted?: boolean;
|
||||
};
|
||||
|
||||
toggleUserLocationPermission: {
|
||||
botId: string;
|
||||
isAccessGranted: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RequiredActionPayloads {
|
||||
|
||||
@ -47,6 +47,7 @@ import type {
|
||||
ApiWebSession,
|
||||
} from '../../api/types';
|
||||
import type {
|
||||
BotAppPermissions,
|
||||
ChatListType,
|
||||
ChatTranslatedMessages,
|
||||
EmojiKeywords,
|
||||
@ -172,6 +173,7 @@ export type GlobalState = {
|
||||
previewMediaByBotId: Record<string, ApiBotPreviewMedia[]>;
|
||||
commonChatsById: Record<string, ApiUserCommonChats>;
|
||||
giftsById: Record<string, ApiUserGifts>;
|
||||
botAppPermissionsById: Record<string, BotAppPermissions>;
|
||||
};
|
||||
profilePhotosById: Record<string, ApiPeerPhotos>;
|
||||
|
||||
|
||||
@ -40,6 +40,7 @@ import type {
|
||||
ApiSticker,
|
||||
ApiTypePrepaidGiveaway,
|
||||
ApiTypeStoryView,
|
||||
ApiUser,
|
||||
ApiUserStarGift,
|
||||
ApiVideo,
|
||||
ApiWebPage,
|
||||
@ -510,6 +511,16 @@ export type TabState = {
|
||||
startParam?: string;
|
||||
};
|
||||
|
||||
emojiStatusAccessModal?: {
|
||||
bot: ApiUser;
|
||||
webAppKey: string;
|
||||
};
|
||||
|
||||
locationAccessModal?: {
|
||||
bot: ApiUser;
|
||||
webAppKey: string;
|
||||
};
|
||||
|
||||
confetti?: {
|
||||
lastConfettiTime?: number;
|
||||
top?: number;
|
||||
|
||||
@ -1686,6 +1686,7 @@ bots.invokeWebViewCustomMethod#87fc5e7 bot:InputUser custom_method:string params
|
||||
bots.getPopularAppBots#c2510192 offset:string limit:int = bots.PopularAppBots;
|
||||
bots.getPreviewMedias#a2a5594d bot:InputUser = Vector<BotPreviewMedia>;
|
||||
bots.checkDownloadFileParams#50077589 bot:InputUser file_name:string url:string = Bool;
|
||||
bots.toggleUserEmojiStatusPermission#6de6392 bot:InputUser enabled:Bool = Bool;
|
||||
payments.getPaymentForm#37148dbb flags:# invoice:InputInvoice theme_params:flags.0?DataJSON = payments.PaymentForm;
|
||||
payments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.PaymentReceipt;
|
||||
payments.validateRequestedInfo#b6c8f12b flags:# save:flags.0?true invoice:InputInvoice info:PaymentRequestedInfo = payments.ValidatedRequestedInfo;
|
||||
@ -1776,4 +1777,4 @@ premium.getBoostsList#60f67660 flags:# gifts:flags.0?true peer:InputPeer offset:
|
||||
premium.getMyBoosts#be77b4a = premium.MyBoosts;
|
||||
premium.applyBoost#6b7da746 flags:# slots:flags.0?Vector<int> peer:InputPeer = premium.MyBoosts;
|
||||
premium.getBoostsStatus#42f1f61 peer:InputPeer = premium.BoostsStatus;
|
||||
fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo;`;
|
||||
fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo;`;
|
||||
|
||||
@ -277,6 +277,7 @@
|
||||
"bots.setBotInfo",
|
||||
"bots.getPreviewMedias",
|
||||
"bots.checkDownloadFileParams",
|
||||
"bots.toggleUserEmojiStatusPermission",
|
||||
"payments.getPaymentForm",
|
||||
"payments.getPaymentReceipt",
|
||||
"payments.validateRequestedInfo",
|
||||
|
||||
@ -636,3 +636,7 @@ export type StarGiftCategory = number | 'all' | 'limited' | 'stock';
|
||||
export type CallSound = (
|
||||
'join' | 'allowTalk' | 'leave' | 'connecting' | 'incoming' | 'end' | 'connect' | 'busy' | 'ringing'
|
||||
);
|
||||
|
||||
export type BotAppPermissions = {
|
||||
geolocation?: boolean;
|
||||
};
|
||||
|
||||
6
src/types/language.d.ts
vendored
6
src/types/language.d.ts
vendored
@ -1631,6 +1631,12 @@ export interface LangPairWithVariables<V extends unknown = LangVariable> {
|
||||
'StarsPerMonth': {
|
||||
'amount': V;
|
||||
};
|
||||
'EmojiStatusAccessText': {
|
||||
'name': V;
|
||||
};
|
||||
'LocationPermissionText': {
|
||||
'name': V;
|
||||
};
|
||||
'BotSuggestedStatusFor': {
|
||||
'bot': V;
|
||||
'duration': V;
|
||||
|
||||
@ -139,8 +139,9 @@ export type WebAppInboundEvent =
|
||||
}> |
|
||||
WebAppEvent<'web_app_request_viewport' | 'web_app_request_theme' | 'web_app_ready' | 'web_app_expand'
|
||||
| 'web_app_request_phone' | 'web_app_close' | 'web_app_close_scan_qr_popup'
|
||||
| 'web_app_request_write_access' | 'web_app_request_phone' | 'iframe_will_reload'
|
||||
| 'web_app_biometry_get_info' | 'web_app_biometry_open_settings'
|
||||
| 'web_app_request_write_access' | 'iframe_will_reload'
|
||||
| 'web_app_biometry_get_info' | 'web_app_biometry_open_settings' | 'web_app_request_emoji_status_access'
|
||||
| 'web_app_check_location' | 'web_app_request_location' | 'web_app_open_location_settings'
|
||||
| 'web_app_request_fullscreen' | 'web_app_exit_fullscreen'
|
||||
| 'web_app_request_safe_area' | 'web_app_request_content_safe_area',
|
||||
null>;
|
||||
@ -224,6 +225,33 @@ export type WebAppOutboundEvent =
|
||||
WebAppEvent<'biometry_token_updated', {
|
||||
status: 'updated' | 'removed' | 'failed';
|
||||
}> |
|
||||
WebAppEvent<'location_checked', {
|
||||
available: false;
|
||||
} | {
|
||||
available: boolean;
|
||||
access_requested: boolean;
|
||||
access_granted?: boolean;
|
||||
}> |
|
||||
WebAppEvent<'location_requested', {
|
||||
available: boolean;
|
||||
} | {
|
||||
available: boolean;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
altitude: number | null;
|
||||
course: number | null;
|
||||
speed: number | null;
|
||||
horizontal_accuracy: number | null;
|
||||
vertical_accuracy: number | null;
|
||||
course_accuracy: number | null;
|
||||
speed_accuracy: number | null;
|
||||
}> |
|
||||
WebAppEvent<'emoji_status_access_requested', {
|
||||
status: 'allowed' | 'cancelled';
|
||||
}> |
|
||||
WebAppEvent<'access_requested', {
|
||||
available: true;
|
||||
}> |
|
||||
WebAppEvent<'emoji_status_failed', {
|
||||
error: 'UNSUPPORTED' | 'USER_DECLINED' | 'SUGGESTED_EMOJI_INVALID'
|
||||
| 'DURATION_INVALID' | 'SERVER_ERROR' | 'UNKNOWN_ERROR';
|
||||
|
||||
@ -140,3 +140,28 @@ function isLastEmojiVersionSupported() {
|
||||
|
||||
return Math.abs(newEmojiWidth - legacyEmojiWidth) < ALLOWABLE_CALCULATION_ERROR_SIZE;
|
||||
}
|
||||
|
||||
export const IS_GEOLOCATION_SUPPORTED = 'geolocation' in navigator;
|
||||
|
||||
export const getGeolocationStatus = async () => {
|
||||
try {
|
||||
const permissionStatus = await navigator.permissions.query({ name: 'geolocation' });
|
||||
|
||||
if (permissionStatus.state === 'granted' || permissionStatus.state === 'prompt') {
|
||||
const geolocation = await new Promise<GeolocationCoordinates>((resolve, reject) => {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => resolve(position.coords),
|
||||
(error) => reject(error),
|
||||
);
|
||||
});
|
||||
return { accessRequested: true, accessGranted: true, geolocation };
|
||||
}
|
||||
if (permissionStatus.state === 'denied') {
|
||||
return { accessRequested: true, accessGranted: false };
|
||||
}
|
||||
} catch (error) {
|
||||
return { accessRequested: false, accessGranted: false };
|
||||
}
|
||||
|
||||
return { accessRequested: false, accessGranted: false };
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user