From a414faa98544ef15885347feb5ebd12eae2ace7c Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Tue, 21 Jan 2025 18:20:18 +0100 Subject: [PATCH] Mini Apps: Request for rights (#5238) --- src/api/gramjs/apiBuilders/users.ts | 2 + src/api/gramjs/methods/bots.ts | 9 + src/api/types/users.ts | 2 + src/assets/localization/fallback.strings | 3 + src/bundles/extra.ts | 2 + .../common/FullNameTitle.module.scss | 7 +- src/components/common/FullNameTitle.tsx | 27 ++- src/components/common/PeerChip.module.scss | 4 + src/components/common/PeerChip.tsx | 5 +- src/components/common/profile/ChatExtra.tsx | 82 ++++--- src/components/modals/ModalContainer.tsx | 6 + .../EmojiStatusAccessModal.async.tsx | 18 ++ .../EmojiStatusAccessModal.module.scss | 9 + .../EmojiStatusAccessModal.tsx | 204 ++++++++++++++++++ .../LocationAccessModal.async.tsx | 18 ++ .../LocationAccessModal.module.scss | 11 + .../locationAccess/LocationAccessModal.tsx | 168 +++++++++++++++ .../modals/webApp/WebAppModalTabContent.tsx | 125 +++++++++-- src/components/ui/ConfirmDialog.tsx | 2 +- src/components/ui/Transition.tsx | 4 +- src/global/actions/api/bots.ts | 44 +++- src/global/actions/ui/bots.ts | 50 +++++ src/global/cache.ts | 24 ++- src/global/initialState.ts | 1 + src/global/reducers/users.ts | 29 ++- src/global/selectors/users.ts | 10 +- src/global/types/actions.ts | 29 ++- src/global/types/globalState.ts | 2 + src/global/types/tabState.ts | 11 + src/lib/gramjs/tl/apiTl.js | 3 +- src/lib/gramjs/tl/static/api.json | 1 + src/types/index.ts | 4 + src/types/language.d.ts | 6 + src/types/webapp.ts | 32 ++- src/util/windowEnvironment.ts | 25 +++ 35 files changed, 905 insertions(+), 74 deletions(-) create mode 100644 src/components/modals/emojiStatusAccess/EmojiStatusAccessModal.async.tsx create mode 100644 src/components/modals/emojiStatusAccess/EmojiStatusAccessModal.module.scss create mode 100644 src/components/modals/emojiStatusAccess/EmojiStatusAccessModal.tsx create mode 100644 src/components/modals/locationAccess/LocationAccessModal.async.tsx create mode 100644 src/components/modals/locationAccess/LocationAccessModal.module.scss create mode 100644 src/components/modals/locationAccess/LocationAccessModal.tsx diff --git a/src/api/gramjs/apiBuilders/users.ts b/src/api/gramjs/apiBuilders/users.ts index 99b4edca4..d4519c766 100644 --- a/src/api/gramjs/apiBuilders/users.ts +++ b/src/api/gramjs/apiBuilders/users.ts @@ -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, }; } diff --git a/src/api/gramjs/methods/bots.ts b/src/api/gramjs/methods/bots.ts index 8a9e2ebe1..ce1c28788 100644 --- a/src/api/gramjs/methods/bots.ts +++ b/src/api/gramjs/methods/bots.ts @@ -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) { diff --git a/src/api/types/users.ts b/src/api/types/users.ts index 4589d4281..687332434 100644 --- a/src/api/types/users.ts +++ b/src/api/types/users.ts @@ -62,6 +62,8 @@ export interface ApiUserFullInfo { businessWorkHours?: ApiBusinessWorkHours; businessIntro?: ApiBusinessIntro; starGiftCount?: number; + isBotCanManageEmojiStatus?: boolean; + isBotAccessEmojiGranted?: boolean; hasScheduledMessages?: boolean; botVerification?: ApiBotVerification; } diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 6017bf032..cb558c087 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -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}**."; + diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index debfcd513..283be8671 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -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'; diff --git a/src/components/common/FullNameTitle.module.scss b/src/components/common/FullNameTitle.module.scss index dcfbf9595..6245cfe9b 100644 --- a/src/components/common/FullNameTitle.module.scss +++ b/src/components/common/FullNameTitle.module.scss @@ -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; } diff --git a/src/components/common/FullNameTitle.tsx b/src/components/common/FullNameTitle.tsx index 09f374103..28ddc00ad 100644 --- a/src/components/common/FullNameTitle.tsx +++ b/src/components/common/FullNameTitle.tsx @@ -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 = ({ noLoopLimit, canCopyTitle, iconElement, - allowMultiLine, onEmojiStatusClick, observeIntersection, }) => { @@ -125,7 +124,6 @@ const FullNameTitle: FC = ({ className={buildClassName( 'fullName', styles.fullName, - !allowMultiLine && styles.ellipsis, canCopyTitle && styles.canCopy, )} onClick={handleTitleClick} @@ -137,13 +135,22 @@ const FullNameTitle: FC = ({ {!noVerified && peer?.isVerified && } {!noFake && peer?.fakeType && } {canShowEmojiStatus && realPeer.emojiStatus && ( - + + + )} {canShowEmojiStatus && !realPeer.emojiStatus && isPremium && } diff --git a/src/components/common/PeerChip.module.scss b/src/components/common/PeerChip.module.scss index cbe689b99..3d8aa4bc8 100644 --- a/src/components/common/PeerChip.module.scss +++ b/src/components/common/PeerChip.module.scss @@ -45,6 +45,10 @@ max-width: unset; } + &.notClickable { + cursor: default; + } + .avatar, .iconWrapper { width: 2rem; diff --git a/src/components/common/PeerChip.tsx b/src/components/common/PeerChip.tsx index 6c55ccbab..2fb9e8efe 100644 --- a/src/components/common/PeerChip.tsx +++ b/src/components/common/PeerChip.tsx @@ -38,6 +38,7 @@ type OwnProps = { withEmojiStatus?: boolean; clickArg?: T; onClick?: (arg: T) => void; + itemClassName?: string; }; type StateProps = { @@ -62,6 +63,7 @@ const PeerChip = ({ withPeerColors, withEmojiStatus, onClick, + itemClassName, }: OwnProps & StateProps) => { const lang = useOldLang(); @@ -105,6 +107,7 @@ const PeerChip = ({ canClose && styles.closeable, isCloseNonDestructive && styles.nonDestructive, fluid && styles.fluid, + !onClick && styles.notClickable, withPeerColors && getPeerColorClass(customPeer || peer), className, ); @@ -118,7 +121,7 @@ const PeerChip = ({ > {iconElement} {!isMinimized && ( -
+
{titleElement}
)} diff --git a/src/components/common/profile/ChatExtra.tsx b/src/components/common/profile/ChatExtra.tsx index 83cdb5888..10aac92b3 100644 --- a/src/components/common/profile/ChatExtra.tsx +++ b/src/components/common/profile/ChatExtra.tsx @@ -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 = ({ @@ -105,6 +111,8 @@ const ChatExtra: FC = ({ hasSavedMessages, personalChannel, hasMainMiniApp, + isBotCanManageEmojiStatus, + botAppPermissions, botVerification, }) => { const { @@ -116,6 +124,8 @@ const ChatExtra: FC = ({ openMapModal, requestCollectibleInfo, requestMainWebView, + toggleUserEmojiStatusPermission, + toggleUserLocationPermission, } = getActions(); const { @@ -135,12 +145,6 @@ const ChatExtra: FC = ({ 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 = ({ }); 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 = ({ @@ -442,6 +448,26 @@ const ChatExtra: FC = ({ {oldLang('SavedMessagesTab')} )} + {userFullInfo && 'isBotAccessEmojiGranted' in userFullInfo && ( + + {oldLang('BotProfilePermissionEmojiStatus')} + + + )} + {botAppPermissions?.geolocation !== undefined && ( + + {oldLang('BotProfilePermissionLocation')} + + + )} {botVerification && (
( 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( 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( user, userFullInfo, canInviteUsers, + botAppPermissions, isMuted, topicId, chatInviteLink, @@ -505,6 +532,7 @@ export default memo(withGlobal( hasSavedMessages, personalChannel, hasMainMiniApp, + isBotCanManageEmojiStatus: userFullInfo?.isBotCanManageEmojiStatus, botVerification, }; }, diff --git a/src/components/modals/ModalContainer.tsx b/src/components/modals/ModalContainer.tsx index 03204c493..6f5d9116f 100644 --- a/src/components/modals/ModalContainer.tsx +++ b/src/components/modals/ModalContainer.tsx @@ -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; @@ -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[]; diff --git a/src/components/modals/emojiStatusAccess/EmojiStatusAccessModal.async.tsx b/src/components/modals/emojiStatusAccess/EmojiStatusAccessModal.async.tsx new file mode 100644 index 000000000..d180d9284 --- /dev/null +++ b/src/components/modals/emojiStatusAccess/EmojiStatusAccessModal.async.tsx @@ -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 = (props) => { + const { modal } = props; + const EmojiStatusAccessModal = useModuleLoader(Bundles.Extra, 'EmojiStatusAccessModal', !modal); + + // eslint-disable-next-line react/jsx-props-no-spreading + return EmojiStatusAccessModal ? : undefined; +}; + +export default EmojiStatusAccessModalAsync; diff --git a/src/components/modals/emojiStatusAccess/EmojiStatusAccessModal.module.scss b/src/components/modals/emojiStatusAccess/EmojiStatusAccessModal.module.scss new file mode 100644 index 000000000..af99ac25f --- /dev/null +++ b/src/components/modals/emojiStatusAccess/EmojiStatusAccessModal.module.scss @@ -0,0 +1,9 @@ +.content { + display: flex; + flex-direction: column; + align-items: center; +} + +.chatItem { + padding-right: 0.5rem; +} diff --git a/src/components/modals/emojiStatusAccess/EmojiStatusAccessModal.tsx b/src/components/modals/emojiStatusAccess/EmojiStatusAccessModal.tsx new file mode 100644 index 000000000..799ee7d82 --- /dev/null +++ b/src/components/modals/emojiStatusAccess/EmojiStatusAccessModal.tsx @@ -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 = ({ + 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(null); + + const [currentStatusIndex, setCurrentStatusIndex] = useState(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 ( + + ); + }); + + 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 ( + + {renderPickerItem()} +
+ {renderStatusText()} +
+ + +
+
+
+ ); +}; + +export default memo(withGlobal( + (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)); diff --git a/src/components/modals/locationAccess/LocationAccessModal.async.tsx b/src/components/modals/locationAccess/LocationAccessModal.async.tsx new file mode 100644 index 000000000..8fc514a16 --- /dev/null +++ b/src/components/modals/locationAccess/LocationAccessModal.async.tsx @@ -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 = (props) => { + const { modal } = props; + const LocationAccessModal = useModuleLoader(Bundles.Extra, 'LocationAccessModal', !modal); + + // eslint-disable-next-line react/jsx-props-no-spreading + return LocationAccessModal ? : undefined; +}; + +export default LocationAccessModalAsync; diff --git a/src/components/modals/locationAccess/LocationAccessModal.module.scss b/src/components/modals/locationAccess/LocationAccessModal.module.scss new file mode 100644 index 000000000..c5b6b51b3 --- /dev/null +++ b/src/components/modals/locationAccess/LocationAccessModal.module.scss @@ -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); +} diff --git a/src/components/modals/locationAccess/LocationAccessModal.tsx b/src/components/modals/locationAccess/LocationAccessModal.tsx new file mode 100644 index 000000000..25448f4dd --- /dev/null +++ b/src/components/modals/locationAccess/LocationAccessModal.tsx @@ -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 = ({ + 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(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 ( +
+ + + +
+ ); + }); + + const renderStatusText = useLastCallback(() => { + if (!modal?.bot) return undefined; + return lang('LocationPermissionText', { + name: getUserFullName(modal?.bot!), + }, { + withNodes: true, + withMarkdown: true, + }); + }); + + return ( + + {renderInfo()} +
+ {renderStatusText()} +
+ + +
+
+
+ ); +}; + +export default memo(withGlobal( + (global): StateProps => { + const currentUser = selectUser(global, global.currentUserId!); + + return { + currentUser, + }; + }, +)(LocationAccessModal)); diff --git a/src/components/modals/webApp/WebAppModalTabContent.tsx b/src/components/modals/webApp/WebAppModalTabContent.tsx index c3886f16e..2c5371392 100644 --- a/src/components/modals/webApp/WebAppModalTabContent.tsx +++ b/src/components/modals/webApp/WebAppModalTabContent.tsx @@ -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 = ({ modalState, isMultiTabSupported, onContextMenuButtonClick, + botAppPermissions, botAppSettings, modalHeight, }) => { @@ -130,6 +142,10 @@ const WebAppModalTabContent: FC = ({ sharePhoneWithBot, updateWebApp, resetPaymentStatus, + openChatWithInfo, + showNotification, + openEmojiStatusAccessModal, + openLocationAccessModal, changeWebAppModalState, closeWebAppModal, } = getActions(); @@ -190,6 +206,10 @@ const WebAppModalTabContent: FC = ({ 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) => { if (!webAppKey) return; updateWebApp({ key: webAppKey, update: updatedPartialWebApp }); @@ -425,7 +445,8 @@ const WebAppModalTabContent: FC = ({ }); 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 = ({ }); async function handleRequestWriteAccess() { + if (!bot) return; const canWrite = await callApi('fetchBotCanSendMessage', { - bot: bot!, + bot, }); if (canWrite) { @@ -454,7 +476,6 @@ const WebAppModalTabContent: FC = ({ }, }); } - setIsRequestingWriteAccess(!canWrite); } @@ -527,6 +548,10 @@ const WebAppModalTabContent: FC = ({ } }, [isOpen]); + const handleOpenChat = useLastCallback(() => { + openChatWithInfo({ id: bot!.id }); + }); + function handleEvent(event: WebAppInboundEvent) { const { eventType, eventData } = event; @@ -616,8 +641,8 @@ const WebAppModalTabContent: FC = ({ 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 = ({ } 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 = ({ > {!secondaryButton?.isProgressVisible && secondaryButtonCurrentText} {secondaryButton?.isProgressVisible - && } + && }