Mini Apps: Request for rights (#5238)

This commit is contained in:
Alexander Zinchuk 2025-01-21 18:20:18 +01:00
parent cdbb5e15ed
commit a414faa985
35 changed files with 905 additions and 74 deletions

View File

@ -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,
};
}

View File

@ -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) {

View File

@ -62,6 +62,8 @@ export interface ApiUserFullInfo {
businessWorkHours?: ApiBusinessWorkHours;
businessIntro?: ApiBusinessIntro;
starGiftCount?: number;
isBotCanManageEmojiStatus?: boolean;
isBotAccessEmojiGranted?: boolean;
hasScheduledMessages?: boolean;
botVerification?: ApiBotVerification;
}

View File

@ -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}**.";

View File

@ -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';

View File

@ -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;
}

View File

@ -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 />}
</>

View File

@ -45,6 +45,10 @@
max-width: unset;
}
&.notClickable {
cursor: default;
}
.avatar,
.iconWrapper {
width: 2rem;

View File

@ -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>
)}

View File

@ -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,
};
},

View File

@ -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[];

View File

@ -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;

View File

@ -0,0 +1,9 @@
.content {
display: flex;
flex-direction: column;
align-items: center;
}
.chatItem {
padding-right: 0.5rem;
}

View File

@ -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));

View File

@ -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;

View File

@ -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);
}

View 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));

View File

@ -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,
};
},

View File

@ -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}

View File

@ -340,8 +340,8 @@ function Transition({
return;
}
const { clientHeight } = activeElement || {};
if (!clientHeight) {
const { clientHeight, clientWidth } = activeElement || {};
if (!clientHeight || !clientWidth) {
return;
}

View File

@ -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,

View File

@ -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);
});

View File

@ -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,
};
}

View File

@ -103,6 +103,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
previewMediaByBotId: {},
commonChatsById: {},
giftsById: {},
botAppPermissionsById: {},
},
chats: {

View File

@ -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,
},
},
},
};
}

View File

@ -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];
}

View File

@ -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 {

View File

@ -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>;

View File

@ -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;

View File

@ -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;`;

View File

@ -277,6 +277,7 @@
"bots.setBotInfo",
"bots.getPreviewMedias",
"bots.checkDownloadFileParams",
"bots.toggleUserEmojiStatusPermission",
"payments.getPaymentForm",
"payments.getPaymentReceipt",
"payments.validateRequestedInfo",

View File

@ -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;
};

View File

@ -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;

View File

@ -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';

View File

@ -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 };
};