From 9857dc0ec709f44eed0a931566b1b18d1cfc9245 Mon Sep 17 00:00:00 2001 From: zubiden <19638254+zubiden@users.noreply.github.com> Date: Tue, 29 Jul 2025 14:33:49 +0200 Subject: [PATCH] Chat: Fix restriction validation (#6078) Co-authored-by: Dmitry Kabanov --- src/api/gramjs/apiBuilders/appConfig.ts | 10 ++ src/api/gramjs/apiBuilders/chats.ts | 22 +-- src/api/gramjs/apiBuilders/messages.ts | 4 + src/api/gramjs/apiBuilders/misc.ts | 13 ++ src/api/gramjs/methods/users.ts | 2 - src/api/types/chats.ts | 9 +- src/api/types/messages.ts | 2 + src/api/types/misc.ts | 11 ++ src/assets/localization/fallback.strings | 12 ++ src/bundles/extra.ts | 1 + src/components/common/GroupChatInfo.tsx | 6 +- .../common/MediaSpoiler.module.scss | 27 +++- src/components/common/MediaSpoiler.tsx | 13 ++ .../SensitiveContentConfirmModal.module.scss | 4 + .../common/SensitiveContentConfirmModal.tsx | 47 ++++++ .../common/embedded/EmbeddedMessage.tsx | 4 +- src/components/common/profile/ChatExtra.tsx | 6 +- .../left/settings/SettingsPrivacy.tsx | 29 +++- src/components/main/Main.tsx | 2 + src/components/middle/HeaderActions.tsx | 4 +- src/components/middle/HeaderMenuContainer.tsx | 4 +- src/components/middle/MessageList.tsx | 13 +- .../composer/ComposerEmbeddedMessage.tsx | 11 +- src/components/middle/message/Message.tsx | 25 +++- src/components/middle/message/Photo.tsx | 62 +++++++- src/components/middle/message/Sticker.tsx | 95 ++++++++++-- src/components/middle/message/Video.tsx | 67 +++++++-- src/components/modals/ModalContainer.tsx | 5 +- .../AgeVerificationModal.async.tsx | 18 +++ .../AgeVerificationModal.module.scss | 41 ++++++ .../ageVerification/AgeVerificationModal.tsx | 93 ++++++++++++ .../modals/webApp/hooks/useWebAppFrame.ts | 20 +++ src/components/right/Profile.tsx | 4 +- .../right/management/ManageChannel.tsx | 7 +- .../right/management/ManageGroup.tsx | 7 +- src/config.ts | 2 + src/global/actions/api/chats.ts | 5 +- src/global/actions/api/messages.ts | 6 +- src/global/actions/api/users.ts | 5 + src/global/actions/apiUpdaters/chats.ts | 2 +- src/global/actions/ui/account.ts | 16 ++ src/global/helpers/chats.ts | 9 +- src/global/selectors/chats.ts | 9 ++ src/global/selectors/management.ts | 5 +- src/global/selectors/messages.ts | 42 +++++- src/global/types/actions.ts | 3 + src/global/types/tabState.ts | 2 + src/hooks/polling/usePeerStoriesPolling.ts | 5 +- src/lib/gramjs/tl/AllTLObjects.ts | 2 +- src/lib/gramjs/tl/api.d.ts | 137 +++++++++++++++++- src/lib/gramjs/tl/apiTl.ts | 18 ++- src/lib/gramjs/tl/static/api.tl | 30 ++-- src/types/language.d.ts | 13 ++ src/types/webapp.ts | 1 + src/util/folderManager.ts | 4 +- 55 files changed, 888 insertions(+), 128 deletions(-) create mode 100644 src/components/common/SensitiveContentConfirmModal.module.scss create mode 100644 src/components/common/SensitiveContentConfirmModal.tsx create mode 100644 src/components/modals/ageVerification/AgeVerificationModal.async.tsx create mode 100644 src/components/modals/ageVerification/AgeVerificationModal.module.scss create mode 100644 src/components/modals/ageVerification/AgeVerificationModal.tsx diff --git a/src/api/gramjs/apiBuilders/appConfig.ts b/src/api/gramjs/apiBuilders/appConfig.ts index 88565e2e3..95492de67 100644 --- a/src/api/gramjs/apiBuilders/appConfig.ts +++ b/src/api/gramjs/apiBuilders/appConfig.ts @@ -120,6 +120,11 @@ export interface GramJsAppConfig extends LimitsConfig { todo_items_max?: number; todo_title_length_max?: number; todo_item_length_max?: number; + ignore_restriction_reasons?: string[]; + need_age_video_verification?: boolean; + verify_age_bot_username?: string; + verify_age_country?: string; + verify_age_min?: number; } function buildEmojiSounds(appConfig: GramJsAppConfig) { @@ -235,5 +240,10 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp todoItemsMax: appConfig.todo_items_max ?? TODO_ITEMS_LIMIT, todoTitleLengthMax: appConfig.todo_title_length_max ?? TODO_TITLE_LENGTH_LIMIT, todoItemLengthMax: appConfig.todo_item_length_max ?? TODO_ITEM_LENGTH_LIMIT, + ignoreRestrictionReasons: appConfig.ignore_restriction_reasons, + needAgeVideoVerification: appConfig.need_age_video_verification, + verifyAgeBotUsername: appConfig.verify_age_bot_username, + verifyAgeCountry: appConfig.verify_age_country, + verifyAgeMin: appConfig.verify_age_min, }; } diff --git a/src/api/gramjs/apiBuilders/chats.ts b/src/api/gramjs/apiBuilders/chats.ts index 93e9759d2..145cbfe19 100644 --- a/src/api/gramjs/apiBuilders/chats.ts +++ b/src/api/gramjs/apiBuilders/chats.ts @@ -23,7 +23,7 @@ import type { ApiTopic, } from '../../types'; -import { pick, pickTruthy } from '../../../util/iteratees'; +import { pickTruthy } from '../../../util/iteratees'; import { getServerTimeOffset } from '../../../util/serverTime'; import { addPhotoToLocalDb, addUserToLocalDb } from '../helpers/localDb'; import { serializeBytes } from '../helpers/misc'; @@ -31,7 +31,7 @@ import { buildApiBotVerification, buildApiFormattedText, buildApiPhoto, buildApiUsernames, buildAvatarPhotoId, } from './common'; import { omitVirtualClassFields } from './helpers'; -import { buildApiPeerNotifySettings } from './misc'; +import { buildApiPeerNotifySettings, buildApiRestrictionReasons } from './misc'; import { buildApiEmojiStatus, buildApiPeerColor, @@ -196,7 +196,7 @@ function buildApiChatRestrictions(peerEntity: Entity): { isNotJoined?: boolean; isForbidden?: boolean; isRestricted?: boolean; - restrictionReason?: ApiRestrictionReason; + restrictionReasons?: ApiRestrictionReason[]; } { if (peerEntity instanceof GramJs.ChatForbidden) { return { @@ -213,11 +213,10 @@ function buildApiChatRestrictions(peerEntity: Entity): { const restrictions = {}; if ('restricted' in peerEntity && !peerEntity.min) { - const restrictionReason = buildApiChatRestrictionReason(peerEntity.restrictionReason); + const restrictionReasons = buildApiRestrictionReasons(peerEntity.restrictionReason); Object.assign(restrictions, { - isRestricted: peerEntity.restricted, - restrictionReason, + restrictionReasons, }); } @@ -261,17 +260,6 @@ function buildApiChatMigrationInfo(peerEntity: Entity): { return {}; } -function buildApiChatRestrictionReason( - restrictionReasons?: GramJs.RestrictionReason[], -): ApiRestrictionReason | undefined { - if (!restrictionReasons) { - return undefined; - } - - const targetReason = restrictionReasons.find(({ platform }) => platform === 'all'); - return targetReason ? pick(targetReason, ['reason', 'text']) : undefined; -} - export function buildApiChatFromPreview( preview: GramJs.TypeChat | GramJs.TypeUser, isSupport = false, diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 8c55f4d76..6ca778ce3 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -70,6 +70,7 @@ import { import { type OmitVirtualFields } from './helpers'; import { buildApiMessageAction } from './messageActions'; import { buildMessageContent, buildMessageMediaContent, buildMessageTextContent } from './messageContent'; +import { buildApiRestrictionReasons } from './misc'; import { buildApiPeerColor, buildApiPeerId, getApiChatIdFromMtpPeer } from './peers'; import { buildMessageReactions } from './reactions'; @@ -230,6 +231,8 @@ export function buildApiMessageWithChatId( const savedPeerId = mtpMessage.savedPeerId && getApiChatIdFromMtpPeer(mtpMessage.savedPeerId); + const restrictionReasons = buildApiRestrictionReasons(mtpMessage.restrictionReason); + return { id: mtpMessage.id, chatId, @@ -277,6 +280,7 @@ export function buildApiMessageWithChatId( isVideoProcessingPending, reportDeliveryUntilDate: mtpMessage.reportDeliveryUntilDate, paidMessageStars: mtpMessage.paidMessageStars?.toJSNumber(), + restrictionReasons, }; } diff --git a/src/api/gramjs/apiBuilders/misc.ts b/src/api/gramjs/apiBuilders/misc.ts index eaff248ef..36443b0c7 100644 --- a/src/api/gramjs/apiBuilders/misc.ts +++ b/src/api/gramjs/apiBuilders/misc.ts @@ -10,6 +10,7 @@ import type { ApiPeerColors, ApiPeerNotifySettings, ApiPrivacyKey, + ApiRestrictionReason, ApiSession, ApiTimezone, ApiUrlAuthResult, @@ -347,3 +348,15 @@ export function buildApiCollectibleInfo(info: GramJs.fragment.TypeCollectibleInf url, }; } + +export function buildApiRestrictionReasons( + restrictionReasons?: GramJs.RestrictionReason[], +): ApiRestrictionReason[] | undefined { + if (!restrictionReasons) { + return undefined; + } + + return restrictionReasons.map(( + { reason, text, platform }) => + ({ reason, text, platform })); +} diff --git a/src/api/gramjs/methods/users.ts b/src/api/gramjs/methods/users.ts index df0f0f32d..16582265d 100644 --- a/src/api/gramjs/methods/users.ts +++ b/src/api/gramjs/methods/users.ts @@ -314,8 +314,6 @@ export async function fetchProfilePhotos({ }; } - if (chat?.isRestricted) return undefined; - const result = await searchMessagesInChat({ peer, type: 'profilePhoto', diff --git a/src/api/types/chats.ts b/src/api/types/chats.ts index e8cc18851..1cd47fc0c 100644 --- a/src/api/types/chats.ts +++ b/src/api/types/chats.ts @@ -2,7 +2,7 @@ import type { ApiBotCommand } from './bots'; import type { ApiChatReactions, ApiFormattedText, ApiInputMessageReplyInfo, ApiInputSuggestedPostInfo, ApiPhoto, ApiStickerSet, } from './messages'; -import type { ApiBotVerification, ApiChatInviteImporter, ApiPeerNotifySettings } from './misc'; +import type { ApiBotVerification, ApiChatInviteImporter, ApiPeerNotifySettings, ApiRestrictionReason } from './misc'; import type { ApiEmojiStatusType, ApiFakeType, ApiUser, ApiUsername, } from './users'; @@ -66,7 +66,7 @@ export interface ApiChat { isCreator?: boolean; isForbidden?: boolean; // Forbidden - can't send messages (user was kicked, for example) isRestricted?: boolean; // Restricted - can't access the chat (user was banned or chat is violating rules) - restrictionReason?: ApiRestrictionReason; + restrictionReasons?: ApiRestrictionReason[]; adminRights?: ApiChatAdminRights; currentUserBannedRights?: ApiChatBannedRights; defaultBannedRights?: ApiChatBannedRights; @@ -212,11 +212,6 @@ export interface ApiChatBannedRights { untilDate?: number; } -export interface ApiRestrictionReason { - reason: string; - text: string; -} - export interface ApiChatFolder { id: number; title: ApiFormattedText; diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index c1bb4e27c..df72eb111 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -6,6 +6,7 @@ import type { } from './bots'; import type { ApiPeerColor } from './chats'; import type { ApiMessageAction } from './messageActions'; +import type { ApiRestrictionReason } from './misc'; import type { ApiLabeledPrice, } from './payments'; @@ -642,6 +643,7 @@ export interface ApiMessage { areReactionsPossible?: true; reportDeliveryUntilDate?: number; paidMessageStars?: number; + restrictionReasons?: ApiRestrictionReason[]; } export interface ApiReactions { diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index 79d2dac97..9b318f338 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -267,6 +267,11 @@ export interface ApiAppConfig { todoItemsMax?: number; todoTitleLengthMax?: number; todoItemLengthMax?: number; + ignoreRestrictionReasons?: string[]; + needAgeVideoVerification?: boolean; + verifyAgeBotUsername?: string; + verifyAgeCountry?: string; + verifyAgeMin?: number; } export interface ApiConfig { @@ -385,3 +390,9 @@ export type ApiPeerNotifySettings = { }; export type ApiNotifyPeerType = 'users' | 'groups' | 'channels'; + +export interface ApiRestrictionReason { + reason: string; + text: string; + platform: string; +} diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 55e687743..996ba6592 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -2141,3 +2141,15 @@ "ButtonTopUpViaFragment" = "Top-Up Via Fragment"; "TonModalHint" = "You can top-up your TON using Fragment."; "TonGiftReceived" = "TON Top-Up"; +"MediaSpoilerSensitive" = "18+"; +"TitleSensitiveModal" = "18+ content"; +"TextSensitiveModal" = "This media may contain sensitive content suitable only for adults. Do you still want to view it?"; +"ButtonSensitiveAlways" = "Always show 18+ media"; +"ButtonSensitiveView" = "View Anyway"; +"TitleAgeVerificationModal" = "Age Verification"; +"TextAgeVerificationModal_one" = "To access such content, you must confirm that you are at least **{count}** year old as required by UK law."; +"TextAgeVerificationModal_other" = "To access such content, you must confirm that you are at least **{count}** years old as required by UK law."; +"DescriptionAgeVerificationModal" = "This is a one-time process using your phone's camera. Your selfie will not be stored by Telegram."; +"TitleAgeCheckFailed" = "Age Check Failed"; +"TitleAgeCheckSuccess" = "Age Check Success"; +"ButtonAgeVerification" = "Verify My Age"; \ No newline at end of file diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index 5c2a8d801..238b82768 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -24,6 +24,7 @@ export { default as SuggestedStatusModal } from '../components/modals/suggestedS export { default as BoostModal } from '../components/modals/boost/BoostModal'; export { default as GiftCodeModal } from '../components/modals/giftcode/GiftCodeModal'; export { default as DeleteAccountModal } from '../components/modals/deleteAccount/DeleteAccountModal'; +export { default as AgeVerificationModal } from '../components/modals/ageVerification/AgeVerificationModal'; export { default as ChatlistModal } from '../components/modals/chatlist/ChatlistModal'; export { default as ChatInviteModal } from '../components/modals/chatInvite/ChatInviteModal'; diff --git a/src/components/common/GroupChatInfo.tsx b/src/components/common/GroupChatInfo.tsx index f2f593d06..3fa9359ad 100644 --- a/src/components/common/GroupChatInfo.tsx +++ b/src/components/common/GroupChatInfo.tsx @@ -1,7 +1,7 @@ import type { FC } from '../../lib/teact/teact'; import type React from '../../lib/teact/teact'; import { memo, useEffect, useMemo } from '../../lib/teact/teact'; -import { getActions, withGlobal } from '../../global'; +import { getActions, getGlobal, withGlobal } from '../../global'; import type { ApiChat, ApiThreadInfo, ApiTopic, ApiTypingStatus, ApiUser, @@ -19,6 +19,7 @@ import { selectChat, selectChatMessages, selectChatOnlineCount, + selectIsChatRestricted, selectMonoforumChannel, selectThreadInfo, selectThreadMessagesCount, @@ -126,7 +127,8 @@ const GroupChatInfo: FC = ({ const isSuperGroup = chat && isChatSuperGroup(chat); const isTopic = Boolean(chat?.isForum && threadInfo && topic); - const { id: chatId, isMin, isRestricted } = chat || {}; + const { id: chatId, isMin } = chat || {}; + const isRestricted = selectIsChatRestricted(getGlobal(), chatId!); useEffect(() => { if (chatId && !isMin) { diff --git a/src/components/common/MediaSpoiler.module.scss b/src/components/common/MediaSpoiler.module.scss index 8553ddca3..9ba2b0ec0 100644 --- a/src/components/common/MediaSpoiler.module.scss +++ b/src/components/common/MediaSpoiler.module.scss @@ -1,11 +1,14 @@ .root { - --click-shift-x: 0px; --click-shift-y: 0px; position: absolute; + top: 0; + left: 0; + width: 100%; height: 100%; + background-color: var(--color-text-secondary); // Fallback before canvas is prepared } @@ -33,6 +36,28 @@ object-fit: cover; } +.nsfw { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + display: flex; + gap: 0.25rem; + align-items: center; + + padding: 0.125rem 0.5rem; + border-radius: 1rem; + + color: white; + + background-color: rgba(0, 0, 0, 0.25); +} + +.nsfwIcon { + font-size: 1.25rem; +} + .dots { --x-direction: var(--background-size); --y-direction: 0; diff --git a/src/components/common/MediaSpoiler.tsx b/src/components/common/MediaSpoiler.tsx index 623919188..9cd081bbe 100644 --- a/src/components/common/MediaSpoiler.tsx +++ b/src/components/common/MediaSpoiler.tsx @@ -6,15 +6,19 @@ import { requestMutation } from '../../lib/fasterdom/fasterdom'; import buildClassName from '../../util/buildClassName'; import useCanvasBlur from '../../hooks/useCanvasBlur'; +import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated'; +import Icon from './icons/Icon'; + import styles from './MediaSpoiler.module.scss'; type OwnProps = { isVisible: boolean; withAnimation?: boolean; thumbDataUri?: string; + isNsfw?: boolean; width?: number; height?: number; className?: string; @@ -27,12 +31,15 @@ const MediaSpoiler: FC = ({ isVisible, withAnimation, thumbDataUri, + isNsfw, className, width, height, }) => { const ref = useRef(); + const lang = useLang(); + const { shouldRender, transitionClassNames } = useShowTransitionDeprecated( isVisible, undefined, true, withAnimation ? false : undefined, undefined, ANIMATION_DURATION, ); @@ -68,6 +75,12 @@ const MediaSpoiler: FC = ({ height={height} />
+ {isNsfw && ( + + + {lang('MediaSpoilerSensitive')} + + )}
); }; diff --git a/src/components/common/SensitiveContentConfirmModal.module.scss b/src/components/common/SensitiveContentConfirmModal.module.scss new file mode 100644 index 000000000..c0453fbcf --- /dev/null +++ b/src/components/common/SensitiveContentConfirmModal.module.scss @@ -0,0 +1,4 @@ +.checkBox { + margin-top: 1rem; + margin-inline: -1.125rem; +} diff --git a/src/components/common/SensitiveContentConfirmModal.tsx b/src/components/common/SensitiveContentConfirmModal.tsx new file mode 100644 index 000000000..0788b0fe5 --- /dev/null +++ b/src/components/common/SensitiveContentConfirmModal.tsx @@ -0,0 +1,47 @@ +import type { FC } from '../../lib/teact/teact'; +import { memo } from '../../lib/teact/teact'; + +import useLang from '../../hooks/useLang'; + +import Checkbox from '../ui/Checkbox'; +import ConfirmDialog from '../ui/ConfirmDialog'; + +import styles from './SensitiveContentConfirmModal.module.scss'; + +type OwnProps = { + isOpen: boolean; + onClose: NoneToVoidFunction; + shouldAlwaysShow: boolean; + onAlwaysShowChanged: (value: boolean) => void; + confirmHandler: NoneToVoidFunction; +}; + +const SensitiveContentConfirmModal: FC = ({ + isOpen, + onClose, + shouldAlwaysShow, + onAlwaysShowChanged, + confirmHandler, +}) => { + const lang = useLang(); + + return ( + + {lang('TextSensitiveModal')} + + + ); +}; + +export default memo(SensitiveContentConfirmModal); diff --git a/src/components/common/embedded/EmbeddedMessage.tsx b/src/components/common/embedded/EmbeddedMessage.tsx index 80b081cc7..4c20ad22e 100644 --- a/src/components/common/embedded/EmbeddedMessage.tsx +++ b/src/components/common/embedded/EmbeddedMessage.tsx @@ -64,6 +64,7 @@ type OwnProps = { chatTranslations?: ChatTranslatedMessages; requestedChatTranslationLanguage?: string; isOpen?: boolean; + isMediaNsfw?: boolean; observeIntersectionForLoading?: ObserveFn; observeIntersectionForPlaying?: ObserveFn; onClick: ((e: React.MouseEvent) => void); @@ -88,6 +89,7 @@ const EmbeddedMessage: FC = ({ noUserColors, chatTranslations, requestedChatTranslationLanguage, + isMediaNsfw, observeIntersectionForLoading, observeIntersectionForPlaying, onClick, @@ -114,7 +116,7 @@ const EmbeddedMessage: FC = ({ const mediaThumbnail = useThumbnail(containedMedia); const isRoundVideo = Boolean(containedMedia && getMessageRoundVideo(containedMedia)); - const isSpoiler = Boolean(containedMedia && getMessageIsSpoiler(containedMedia)); + const isSpoiler = Boolean(containedMedia && getMessageIsSpoiler(containedMedia)) || isMediaNsfw; const isQuote = Boolean(replyInfo?.type === 'message' && replyInfo.isQuote); const replyForwardInfo = replyInfo?.type === 'message' ? replyInfo.replyFrom : undefined; diff --git a/src/components/common/profile/ChatExtra.tsx b/src/components/common/profile/ChatExtra.tsx index 74826b26b..ffe30706b 100644 --- a/src/components/common/profile/ChatExtra.tsx +++ b/src/components/common/profile/ChatExtra.tsx @@ -2,7 +2,7 @@ import type { FC } from '../../../lib/teact/teact'; import { memo, useMemo, } from '../../../lib/teact/teact'; -import { getActions, withGlobal } from '../../../global'; +import { getActions, getGlobal, withGlobal } from '../../../global'; import type { ApiBotVerification, @@ -29,6 +29,7 @@ import { selectChat, selectChatFullInfo, selectCurrentMessageList, + selectIsChatRestricted, selectNotifyDefaults, selectNotifyException, selectTopicLink, @@ -273,7 +274,8 @@ const ChatExtra: FC = ({ ), }, { withNodes: true }); - if (chat?.isRestricted || (isSelf && !isInSettings)) { + const isRestricted = chatId ? selectIsChatRestricted(getGlobal(), chatId) : false; + if (isRestricted || (isSelf && !isInSettings)) { return undefined; } diff --git a/src/components/left/settings/SettingsPrivacy.tsx b/src/components/left/settings/SettingsPrivacy.tsx index 31b992081..dbb876d12 100644 --- a/src/components/left/settings/SettingsPrivacy.tsx +++ b/src/components/left/settings/SettingsPrivacy.tsx @@ -20,6 +20,7 @@ import useLastCallback from '../../../hooks/useLastCallback.ts'; import useOldLang from '../../../hooks/useOldLang'; import StarIcon from '../../common/icons/StarIcon'; +import Button from '../../ui/Button'; import Checkbox from '../../ui/Checkbox'; import ListItem from '../../ui/ListItem'; @@ -43,6 +44,7 @@ type StateProps = { shouldChargeForMessages: boolean; canDisplayChatInTitle?: boolean; isCurrentUserFrozen?: boolean; + needAgeVideoVerification?: boolean; privacy: GlobalState['settings']['privacy']; accountDaysTtl?: number; }; @@ -64,6 +66,7 @@ const SettingsPrivacy: FC = ({ shouldChargeForMessages, canDisplayChatInTitle, canSetPasscode, + needAgeVideoVerification, privacy, onReset, isCurrentUserFrozen, @@ -73,7 +76,6 @@ const SettingsPrivacy: FC = ({ openDeleteAccountModal, loadPrivacySettings, loadBlockedUsers, - loadContentSettings, updateContentSettings, loadGlobalPrivacySettings, updateGlobalPrivacySettings, @@ -81,13 +83,13 @@ const SettingsPrivacy: FC = ({ setSharedSettingOption, openSettingsScreen, loadAccountDaysTtl, + openAgeVerificationModal, } = getActions(); useEffect(() => { if (!isCurrentUserFrozen) { loadBlockedUsers(); loadPrivacySettings(); - loadContentSettings(); loadWebAuthorizations(); } }, [isCurrentUserFrozen]); @@ -123,6 +125,10 @@ const SettingsPrivacy: FC = ({ updateContentSettings({ isSensitiveEnabled: isChecked }); }, [updateContentSettings]); + const handleAgeVerification = useCallback(() => { + openAgeVerificationModal(); + }, [openAgeVerificationModal]); + const handleOpenDeleteAccountModal = useLastCallback(() => { if (!accountDaysTtl) return; openDeleteAccountModal({ days: accountDaysTtl }); @@ -384,7 +390,7 @@ const SettingsPrivacy: FC = ({ {canChangeSensitive && ( -
+

{oldLang('lng_settings_sensitive_title')}

@@ -392,9 +398,23 @@ const SettingsPrivacy: FC = ({ label={oldLang('lng_settings_sensitive_disable_filtering')} subLabel={oldLang('lng_settings_sensitive_about')} checked={Boolean(isSensitiveEnabled)} - disabled={!canChangeSensitive} + disabled={!canChangeSensitive || (!isSensitiveEnabled && needAgeVideoVerification)} onCheck={handleUpdateContentSettings} /> + {!isSensitiveEnabled && needAgeVideoVerification && ( + + )}
)} @@ -475,6 +495,7 @@ export default memo(withGlobal( canChangeSensitive, shouldNewNonContactPeersRequirePremium, shouldChargeForMessages, + needAgeVideoVerification: Boolean(appConfig?.needAgeVideoVerification), privacy, canDisplayChatInTitle, canSetPasscode: selectCanSetPasscode(global), diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index a89630797..6945139aa 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -257,6 +257,7 @@ const Main = ({ loadAllChats, loadAllStories, loadAllHiddenStories, + loadContentSettings, } = getActions(); if (DEBUG && !DEBUG_isLogged) { @@ -329,6 +330,7 @@ const Main = ({ loadAllChats({ listType: 'saved' }); loadAllStories(); loadAllHiddenStories(); + loadContentSettings(); loadRecentReactions(); loadDefaultTagReactions(); loadAttachBots(); diff --git a/src/components/middle/HeaderActions.tsx b/src/components/middle/HeaderActions.tsx index acc9fd4e7..2154ec381 100644 --- a/src/components/middle/HeaderActions.tsx +++ b/src/components/middle/HeaderActions.tsx @@ -22,6 +22,7 @@ import { selectChat, selectChatFullInfo, selectIsChatBotNotStarted, + selectIsChatRestricted, selectIsChatWithSelf, selectIsCurrentUserFrozen, selectIsInSelectMode, @@ -477,7 +478,8 @@ export default memo(withGlobal( const isPrivate = isUserId(chatId); const { doNotTranslate } = global.settings.byKey; - if (!chat || chat.isRestricted || selectIsInSelectMode(global)) { + const isRestricted = selectIsChatRestricted(global, chatId); + if (!chat || isRestricted || selectIsInSelectMode(global)) { return { noMenu: true, language, diff --git a/src/components/middle/HeaderMenuContainer.tsx b/src/components/middle/HeaderMenuContainer.tsx index ae7fcc957..550ba96ed 100644 --- a/src/components/middle/HeaderMenuContainer.tsx +++ b/src/components/middle/HeaderMenuContainer.tsx @@ -32,6 +32,7 @@ import { selectChat, selectChatFullInfo, selectCurrentMessageList, + selectIsChatRestricted, selectIsChatWithSelf, selectIsCurrentUserFrozen, selectIsRightColumnShown, @@ -842,7 +843,8 @@ const HeaderMenuContainer: FC = ({ export default memo(withGlobal( (global, { chatId, threadId }): StateProps => { const chat = selectChat(global, chatId); - if (!chat || chat.isRestricted) { + const isRestricted = selectIsChatRestricted(global, chatId); + if (!chat || isRestricted) { return {}; } const isPrivate = isUserId(chat.id); diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index af4dacab4..61e6a9ce2 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -58,6 +58,8 @@ import { selectTranslationLanguage, selectUserFullInfo, } from '../../global/selectors'; +import { selectIsChatRestricted } from '../../global/selectors/chats'; +import { selectActiveRestrictionReasons } from '../../global/selectors/messages'; import animateScroll, { isAnimatingScroll, restartCurrentScrollAnimation } from '../../util/animateScroll'; import buildClassName from '../../util/buildClassName'; import { isUserId } from '../../util/entities/ids'; @@ -125,7 +127,7 @@ type StateProps = { firstUnreadId?: number; isViewportNewest?: boolean; isRestricted?: boolean; - restrictionReason?: ApiRestrictionReason; + restrictionReasons?: ApiRestrictionReason[]; focusingId?: number; isSelectModeActive?: boolean; lastMessage?: ApiMessage; @@ -189,7 +191,7 @@ const MessageList: FC = ({ isComments, isViewportNewest, isRestricted, - restrictionReason, + restrictionReasons, isEmptyThread, focusingId, isSelectModeActive, @@ -719,7 +721,7 @@ const MessageList: FC = ({ {isRestricted ? (
- {restrictionReason ? restrictionReason.text : `This is a private ${isChannelChat ? 'channel' : 'chat'}`} + {restrictionReasons?.[0]?.text || `This is a private ${isChannelChat ? 'channel' : 'chat'}`}
) : paidMessagesStars && !hasMessages && !hasCustomGreeting ? ( @@ -802,7 +804,8 @@ export default memo(withGlobal( return { currentUserId }; } - const { isRestricted, restrictionReason } = chat; + const isRestricted = selectIsChatRestricted(global, chatId); + const restrictionReasons = selectActiveRestrictionReasons(global, chat?.restrictionReasons); const lastMessage = selectChatLastMessage(global, chatId, isSavedDialog ? 'saved' : 'all'); const focusingId = selectFocusedMessageId(global, chatId); @@ -836,7 +839,7 @@ export default memo(withGlobal( areAdsEnabled, isChatLoaded: true, isRestricted, - restrictionReason, + restrictionReasons, isChannelChat: isChatChannel(chat), isChatMonoforum: isChatMonoforum(chat), isGroupChat: isChatGroup(chat), diff --git a/src/components/middle/composer/ComposerEmbeddedMessage.tsx b/src/components/middle/composer/ComposerEmbeddedMessage.tsx index a77b30868..8c30d6292 100644 --- a/src/components/middle/composer/ComposerEmbeddedMessage.tsx +++ b/src/components/middle/composer/ComposerEmbeddedMessage.tsx @@ -22,6 +22,7 @@ import { selectForwardedSender, selectIsChatWithSelf, selectIsCurrentUserPremium, + selectIsMediaNsfw, selectSender, selectTabState, } from '../../../global/selectors'; @@ -67,14 +68,15 @@ type StateProps = { currentUserId?: string; forwardMessageIds?: number[]; fromChatId?: string; + isMediaNsfw?: boolean; }; type OwnProps = { - onClear?: () => void; shouldForceShowEditing?: boolean; chatId: string; threadId: ThreadId; messageListType: MessageListType; + onClear?: () => void; }; const CLOSE_DURATION = 350; @@ -94,7 +96,6 @@ const ComposerEmbeddedMessage: FC = ({ isCurrentUserPremium, isContextMenuDisabled, isReplyToDiscussion, - onClear, isInChangingRecipientMode, shouldPreventComposerAnimation, senderChat, @@ -103,6 +104,8 @@ const ComposerEmbeddedMessage: FC = ({ isSenderChannel, forwardMessageIds, fromChatId, + isMediaNsfw, + onClear, }) => { const { resetDraftReplyInfo, @@ -301,6 +304,7 @@ const ComposerEmbeddedMessage: FC = ({ className="inside-input" replyInfo={replyInfo} suggestedPostInfo={suggestedPostInfo} + isMediaNsfw={isMediaNsfw} isInComposer message={strippedMessage} sender={!noAuthors ? sender : undefined} @@ -495,6 +499,8 @@ export default memo(withGlobal( const isReplyToDiscussion = replyInfo?.replyToMsgId === threadId && !replyInfo.replyToPeerId; + const isMediaNsfw = message && selectIsMediaNsfw(global, message); + return { replyInfo, suggestedPostInfo, @@ -516,6 +522,7 @@ export default memo(withGlobal( isSenderChannel, forwardMessageIds, fromChatId, + isMediaNsfw, }; }, )(ComposerEmbeddedMessage)); diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index 5be37224d..448f5f064 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -86,11 +86,13 @@ import { selectDefaultReaction, selectForwardedSender, selectIsChatProtected, + selectIsChatRestricted, selectIsChatWithSelf, selectIsCurrentUserFrozen, selectIsCurrentUserPremium, selectIsDocumentGroupSelected, selectIsInSelectMode, + selectIsMediaNsfw, selectIsMessageFocused, selectIsMessageProtected, selectIsMessageSelected, @@ -211,9 +213,6 @@ type MessagePositionProperties = { type OwnProps = { message: ApiMessage; - observeIntersectionForBottom?: ObserveFn; - observeIntersectionForLoading?: ObserveFn; - observeIntersectionForPlaying?: ObserveFn; album?: IAlbum; noAvatars?: boolean; withAvatar?: boolean; @@ -226,6 +225,9 @@ type OwnProps = isJustAdded: boolean; memoFirstUnreadIdRef?: { current: number | undefined }; getIsMessageListReady?: Signal; + observeIntersectionForBottom?: ObserveFn; + observeIntersectionForLoading?: ObserveFn; + observeIntersectionForPlaying?: ObserveFn; onIntersectPinnedMessage?: OnIntersectPinnedMessage; } & MessagePositionProperties; @@ -316,6 +318,8 @@ type StateProps = { isChatWithUser?: boolean; isAccountFrozen?: boolean; minFutureTime?: number; + isMediaNsfw?: boolean; + isReplyMediaNsfw?: boolean; }; type MetaPosition = @@ -437,11 +441,13 @@ const Message: FC = ({ poll, maxTimestamp, lastPlaybackTimestamp, - onIntersectPinnedMessage, + isMediaNsfw, + isReplyMediaNsfw, paidMessageStars, isChatWithUser, isAccountFrozen, minFutureTime, + onIntersectPinnedMessage, }) => { const { toggleMessageSelection, @@ -1119,6 +1125,7 @@ const Message: FC = ({ senderChat={replyMessageChat} forwardSender={replyMessageForwardSender} chatTranslations={chatTranslations} + isMediaNsfw={isReplyMediaNsfw} requestedChatTranslationLanguage={requestedChatTranslationLanguage} observeIntersectionForLoading={observeIntersectionForLoading} observeIntersectionForPlaying={observeIntersectionForPlaying} @@ -1145,6 +1152,7 @@ const Message: FC = ({ shouldLoop={shouldLoopStickers} shouldPlayEffect={shouldPlayEffect} withEffect={withAnimatedEffects} + isMediaNsfw={isMediaNsfw} onStopEffect={hideEffect} /> )} @@ -1443,6 +1451,7 @@ const Message: FC = ({ isProtected={isProtected} asForwarded={asForwarded} theme={theme} + isMediaNsfw={isMediaNsfw} forcedWidth={contentWidth} onClick={handlePhotoMediaClick} onCancelUpload={handleCancelUpload} @@ -1462,6 +1471,7 @@ const Message: FC = ({ isDownloading={isDownloading} isProtected={isProtected} asForwarded={asForwarded} + isMediaNsfw={isMediaNsfw} lastPlaybackTimestamp={lastPlaybackTimestamp} onClick={handleVideoMediaClick} onCancelUpload={handleCancelUpload} @@ -1895,7 +1905,7 @@ export default memo(withGlobal( const replyMessageChat = replyToPeerId ? selectChat(global, replyToPeerId) : undefined; const isReplyPrivate = !isSystemBotChat && !isAnonymousForwards && replyMessageChat && !isChatPublic(replyMessageChat) - && (replyMessageChat.isNotJoined || replyMessageChat.isRestricted); + && (replyMessageChat.isNotJoined || selectIsChatRestricted(global, replyMessageChat.id)); const isReplyToTopicStart = replyMessage?.content.action?.type === 'topicCreate'; const replyStory = storyReplyId && storyReplyPeerId ? selectPeerStory(global, storyReplyPeerId, storyReplyId) @@ -1982,6 +1992,9 @@ export default memo(withGlobal( const minFutureTime = global.appConfig?.starsSuggestedPostFutureMin || STARS_SUGGESTED_POST_FUTURE_MIN; + const isMediaNsfw = selectIsMediaNsfw(global, message); + const isReplyMediaNsfw = replyMessage && selectIsMediaNsfw(global, replyMessage); + return { theme: selectTheme(global), forceSenderName, @@ -2076,6 +2089,8 @@ export default memo(withGlobal( paidMessageStars, isChatWithUser, isAccountFrozen, + isMediaNsfw, + isReplyMediaNsfw, }; }, )(Message)); diff --git a/src/components/middle/message/Photo.tsx b/src/components/middle/message/Photo.tsx index cdff27bb5..350f87d74 100644 --- a/src/components/middle/message/Photo.tsx +++ b/src/components/middle/message/Photo.tsx @@ -1,5 +1,6 @@ import type React from '../../../lib/teact/teact'; -import { useEffect, useRef, useState } from '../../../lib/teact/teact'; +import { memo, useEffect, useRef, useState } from '../../../lib/teact/teact'; +import { getActions, withGlobal } from '../../../global'; import type { ApiMediaExtendedPreview, ApiPhoto } from '../../../api/types'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; @@ -28,6 +29,7 @@ import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef'; import Icon from '../../common/icons/Icon'; import MediaSpoiler from '../../common/MediaSpoiler'; +import SensitiveContentConfirmModal from '../../common/SensitiveContentConfirmModal'; import ProgressSpinner from '../../ui/ProgressSpinner'; export type OwnProps = { @@ -36,7 +38,6 @@ export type OwnProps = { isInWebPage?: boolean; messageText?: string; isOwn?: boolean; - observeIntersection?: ObserveFn; noAvatars?: boolean; canAutoLoad?: boolean; isInSelectMode?: boolean; @@ -53,16 +54,21 @@ export type OwnProps = { theme: ThemeKey; className?: string; clickArg?: T; + isMediaNsfw?: boolean; + observeIntersection?: ObserveFn; onClick?: (arg: T, e: React.MouseEvent) => void; onCancelUpload?: (arg: T) => void; }; +type StateProps = { + needsAgeVerification?: boolean; +}; + const Photo = ({ id, photo, messageText, isOwn, - observeIntersection, noAvatars, canAutoLoad, isInSelectMode, @@ -80,9 +86,12 @@ const Photo = ({ isInWebPage, clickArg, className, + isMediaNsfw, + observeIntersection, onClick, onCancelUpload, -}: OwnProps) => { + needsAgeVerification, +}: OwnProps & StateProps) => { const ref = useRef(); const isPaidPreview = photo.mediaType === 'extendedMediaPreview'; @@ -106,15 +115,29 @@ const Photo = ({ const blurredBackgroundRef = useBlurredMediaThumbRef(photo, !withBlurredBackground); const thumbDataUri = getMediaThumbUri(photo); - const [isSpoilerShown, showSpoiler, hideSpoiler] = useFlag(isPaidPreview || photo.isSpoiler); + const { updateContentSettings, openAgeVerificationModal } = getActions(); + const [isNsfwModalOpen, openNsfwModal, closeNsfwModal] = useFlag(); + const [shouldAlwaysShowNsfw, setShouldAlwaysShowNsfw] = useState(false); + + const shouldShowSpoiler = isPaidPreview || photo.isSpoiler || isMediaNsfw; + const [isSpoilerShown, showSpoiler, hideSpoiler] = useFlag(shouldShowSpoiler); useEffect(() => { - if (isPaidPreview || photo.isSpoiler) { + if (shouldShowSpoiler) { showSpoiler(); } else { hideSpoiler(); } - }, [isPaidPreview, photo]); + }, [shouldShowSpoiler]); + + const handleNsfwConfirm = useLastCallback(() => { + closeNsfwModal(); + hideSpoiler(); + + if (shouldAlwaysShowNsfw) { + updateContentSettings({ isSensitiveEnabled: true }); + } + }); const { loadProgress: downloadProgress, @@ -162,6 +185,14 @@ const Photo = ({ } if (isSpoilerShown) { + if (isMediaNsfw) { + if (needsAgeVerification) { + openAgeVerificationModal(); + return; + } + openNsfwModal(); + return; + } hideSpoiler(); return; } @@ -250,6 +281,7 @@ const Photo = ({ width={width} height={height} className="media-spoiler" + isNsfw={isMediaNsfw} /> {isTransferring && ( @@ -257,8 +289,22 @@ const Photo = ({ % )} +
); }; -export default Photo; +export default memo(withGlobal((global): StateProps => { + const appConfig = global.appConfig; + const needsAgeVerification = appConfig?.needAgeVideoVerification; + + return { + needsAgeVerification, + }; +})(Photo)); diff --git a/src/components/middle/message/Sticker.tsx b/src/components/middle/message/Sticker.tsx index fa250c1c8..b35f500f7 100644 --- a/src/components/middle/message/Sticker.tsx +++ b/src/components/middle/message/Sticker.tsx @@ -1,12 +1,12 @@ import type { FC } from '../../../lib/teact/teact'; -import { useEffect, useRef } from '../../../lib/teact/teact'; -import { getActions } from '../../../global'; +import { memo, useEffect, useRef, useState } from '../../../lib/teact/teact'; +import { getActions, withGlobal } from '../../../global'; import type { ApiMessage } from '../../../api/types'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import { ApiMediaFormat } from '../../../api/types'; -import { getStickerMediaHash } from '../../../global/helpers'; +import { getMediaThumbUri, getStickerMediaHash } from '../../../global/helpers'; import { IS_WEBM_SUPPORTED } from '../../../util/browser/windowEnvironment'; import buildClassName from '../../../util/buildClassName'; import { getStickerDimensions } from '../../common/helpers/mediaDimensions'; @@ -20,6 +20,8 @@ import useOldLang from '../../../hooks/useOldLang'; import useOverlayPosition from './hooks/useOverlayPosition'; import AnimatedSticker from '../../common/AnimatedSticker'; +import MediaSpoiler from '../../common/MediaSpoiler'; +import SensitiveContentConfirmModal from '../../common/SensitiveContentConfirmModal'; import StickerView from '../../common/StickerView'; import Portal from '../../ui/Portal'; @@ -30,19 +32,31 @@ const EFFECT_SIZE_MULTIPLIER = 1 + 0.245 * 2; type OwnProps = { message: ApiMessage; - observeIntersection: ObserveFn; - observeIntersectionForPlaying: ObserveFn; shouldLoop?: boolean; shouldPlayEffect?: boolean; withEffect?: boolean; + isMediaNsfw?: boolean; + observeIntersection: ObserveFn; + observeIntersectionForPlaying: ObserveFn; onStopEffect?: VoidFunction; }; -const Sticker: FC = ({ - message, observeIntersection, observeIntersectionForPlaying, shouldLoop, - shouldPlayEffect, withEffect, onStopEffect, +type StateProps = { + needsAgeVerification?: boolean; +}; + +const Sticker: FC = ({ + message, + shouldLoop, + shouldPlayEffect, + withEffect, + isMediaNsfw, + onStopEffect, + observeIntersection, + observeIntersectionForPlaying, + needsAgeVerification, }) => { - const { showNotification, openStickerSet } = getActions(); + const { showNotification, openStickerSet, updateContentSettings, openAgeVerificationModal } = getActions(); const lang = useOldLang(); const { isMobile } = useAppLayout(); @@ -55,6 +69,29 @@ const Sticker: FC = ({ const { stickerSetInfo, isVideo, hasEffect } = sticker; const isMirrored = !message.isOutgoing; + const [isNsfwModalOpen, openNsfwModal, closeNsfwModal] = useFlag(); + const [shouldAlwaysShowNsfw, setShouldAlwaysShowNsfw] = useState(false); + + const shouldShowSpoiler = isMediaNsfw; + const [isSpoilerShown, showSpoiler, hideSpoiler] = useFlag(shouldShowSpoiler); + + useEffect(() => { + if (shouldShowSpoiler) { + showSpoiler(); + } else { + hideSpoiler(); + } + }, [shouldShowSpoiler]); + + const handleNsfwConfirm = useLastCallback(() => { + closeNsfwModal(); + hideSpoiler(); + + if (shouldAlwaysShowNsfw) { + updateContentSettings({ isSensitiveEnabled: true }); + } + }); + const mediaHash = sticker.isPreloadedGlobally ? undefined : ( getStickerMediaHash(sticker, isVideo && !IS_WEBM_SUPPORTED ? 'pictogram' : 'inline') ); @@ -69,6 +106,8 @@ const Sticker: FC = ({ ); const [isPlayingEffect, startPlayingEffect, stopPlayingEffect] = useFlag(); + const thumbDataUri = getMediaThumbUri(sticker); + const handleEffectEnded = useLastCallback(() => { stopPlayingEffect(); onStopEffect?.(); @@ -95,6 +134,19 @@ const Sticker: FC = ({ }); const handleClick = useLastCallback(() => { + if (isSpoilerShown) { + if (isMediaNsfw) { + if (needsAgeVerification) { + openAgeVerificationModal(); + return; + } + openNsfwModal(); + return; + } + hideSpoiler(); + return; + } + if (hasEffect) { if (isPlayingEffect || !withEffect) { showNotification({ @@ -143,6 +195,15 @@ const Sticker: FC = ({ noPlay={!canPlay} withSharedAnimation /> + {shouldRenderEffect && ( = ({ /> )} + ); }; -export default Sticker; +export default memo(withGlobal((global): StateProps => { + const appConfig = global.appConfig; + const needsAgeVerification = appConfig?.needAgeVideoVerification; + + return { + needsAgeVerification, + }; +})(Sticker)); diff --git a/src/components/middle/message/Video.tsx b/src/components/middle/message/Video.tsx index 45ca64fac..2abadccbc 100644 --- a/src/components/middle/message/Video.tsx +++ b/src/components/middle/message/Video.tsx @@ -1,6 +1,6 @@ import type React from '../../../lib/teact/teact'; -import { useEffect, useRef, useState } from '../../../lib/teact/teact'; -import { getActions } from '../../../global'; +import { memo, useEffect, useRef, useState } from '../../../lib/teact/teact'; +import { getActions, withGlobal } from '../../../global'; import type { ApiMediaExtendedPreview, ApiVideo } from '../../../api/types'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; @@ -29,6 +29,7 @@ import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef'; import Icon from '../../common/icons/Icon'; import MediaSpoiler from '../../common/MediaSpoiler'; +import SensitiveContentConfirmModal from '../../common/SensitiveContentConfirmModal'; import OptimizedVideo from '../../ui/OptimizedVideo'; import ProgressSpinner from '../../ui/ProgressSpinner'; @@ -38,8 +39,6 @@ export type OwnProps = { lastPlaybackTimestamp?: number; isOwn?: boolean; isInWebPage?: boolean; - observeIntersectionForLoading?: ObserveFn; - observeIntersectionForPlaying?: ObserveFn; noAvatars?: boolean; canAutoLoad?: boolean; canAutoPlay?: boolean; @@ -51,17 +50,22 @@ export type OwnProps = { isProtected?: boolean; className?: string; clickArg?: T; + isMediaNsfw?: boolean; + observeIntersectionForLoading?: ObserveFn; + observeIntersectionForPlaying?: ObserveFn; onClick?: (arg: T, e: React.MouseEvent) => void; onCancelUpload?: (arg: T) => void; }; +type StateProps = { + needsAgeVerification?: boolean; +}; + const Video = ({ id, video, isOwn, isInWebPage, - observeIntersectionForLoading, - observeIntersectionForPlaying, noAvatars, canAutoLoad, canAutoPlay, @@ -74,26 +78,42 @@ const Video = ({ className, lastPlaybackTimestamp, clickArg, + isMediaNsfw, + observeIntersectionForLoading, + observeIntersectionForPlaying, onClick, onCancelUpload, -}: OwnProps) => { - const { cancelMediaDownload } = getActions(); + needsAgeVerification, +}: OwnProps & StateProps) => { + const { cancelMediaDownload, updateContentSettings, openAgeVerificationModal } = getActions(); const ref = useRef(); const videoRef = useRef(); + const [isNsfwModalOpen, openNsfwModal, closeNsfwModal] = useFlag(); + const [shouldAlwaysShowNsfw, setShouldAlwaysShowNsfw] = useState(false); const isPaidPreview = video.mediaType === 'extendedMediaPreview'; const localBlobUrl = !isPaidPreview ? video.blobUrl : undefined; - const [isSpoilerShown, showSpoiler, hideSpoiler] = useFlag(isPaidPreview || video.isSpoiler); + const shouldShowSpoiler = isPaidPreview || video.isSpoiler || isMediaNsfw; + const [isSpoilerShown, showSpoiler, hideSpoiler] = useFlag(shouldShowSpoiler); useEffect(() => { - if (isPaidPreview || video.isSpoiler) { + if (shouldShowSpoiler) { showSpoiler(); } else { hideSpoiler(); } - }, [isPaidPreview, video]); + }, [shouldShowSpoiler]); + + const handleNsfwConfirm = useLastCallback(() => { + closeNsfwModal(); + hideSpoiler(); + + if (shouldAlwaysShowNsfw) { + updateContentSettings({ isSensitiveEnabled: true }); + } + }); const isIntersectingForLoading = useIsIntersecting(ref, observeIntersectionForLoading); const isIntersectingForPlaying = ( @@ -204,6 +224,14 @@ const Video = ({ } if (isSpoilerShown) { + if (isMediaNsfw) { + if (needsAgeVerification) { + openAgeVerificationModal(); + return; + } + openNsfwModal(); + return; + } hideSpoiler(); return; } @@ -278,6 +306,7 @@ const Video = ({ isVisible={isSpoilerShown} withAnimation thumbDataUri={thumbDataUri} + isNsfw={isMediaNsfw} width={width} height={height} className="media-spoiler" @@ -309,8 +338,22 @@ const Video = ({ style={`--_progress: ${Math.floor((lastPlaybackTimestamp / duration) * 100)}%`} /> )} + ); }; -export default Video; +export default memo(withGlobal((global): StateProps => { + const appConfig = global.appConfig; + const needsAgeVerification = appConfig?.needAgeVideoVerification; + + return { + needsAgeVerification, + }; +})(Video)); diff --git a/src/components/modals/ModalContainer.tsx b/src/components/modals/ModalContainer.tsx index 330052338..78444cb24 100644 --- a/src/components/modals/ModalContainer.tsx +++ b/src/components/modals/ModalContainer.tsx @@ -10,6 +10,7 @@ import { pick } from '../../util/iteratees'; import VerificationMonetizationModal from '../common/VerificationMonetizationModal.async'; import WebAppsCloseConfirmationModal from '../main/WebAppsCloseConfirmationModal.async'; import AboutAdsModal from './aboutAds/AboutAdsModal.async'; +import AgeVerificationModal from './ageVerification/AgeVerificationModal.async'; import AttachBotInstallModal from './attachBotInstall/AttachBotInstallModal.async'; import BoostModal from './boost/BoostModal.async'; import ChatInviteModal from './chatInvite/ChatInviteModal.async'; @@ -89,7 +90,8 @@ type ModalKey = keyof Pick; type StateProps = { @@ -145,6 +147,7 @@ const MODALS: ModalRegistry = { chatRefundModal: ChatRefundModal, isFrozenAccountModalOpen: FrozenAccountModal, deleteAccountModal: DeleteAccountModal, + isAgeVerificationModalOpen: AgeVerificationModal, }; const MODAL_KEYS = Object.keys(MODALS) as ModalKey[]; const MODAL_ENTRIES = Object.entries(MODALS) as Entries; diff --git a/src/components/modals/ageVerification/AgeVerificationModal.async.tsx b/src/components/modals/ageVerification/AgeVerificationModal.async.tsx new file mode 100644 index 000000000..c73c9cd5a --- /dev/null +++ b/src/components/modals/ageVerification/AgeVerificationModal.async.tsx @@ -0,0 +1,18 @@ +import type { FC } from '../../../lib/teact/teact'; +import { memo } from '../../../lib/teact/teact'; + +import type { OwnProps } from './AgeVerificationModal'; + +import { Bundles } from '../../../util/moduleLoader'; + +import useModuleLoader from '../../../hooks/useModuleLoader'; + +const AgeVerificationModalAsync: FC = memo((props) => { + const { modal } = props; + + const AgeVerificationModal = useModuleLoader(Bundles.Extra, 'AgeVerificationModal', !modal); + + return AgeVerificationModal ? : undefined; +}); + +export default AgeVerificationModalAsync; diff --git a/src/components/modals/ageVerification/AgeVerificationModal.module.scss b/src/components/modals/ageVerification/AgeVerificationModal.module.scss new file mode 100644 index 000000000..b22ecb862 --- /dev/null +++ b/src/components/modals/ageVerification/AgeVerificationModal.module.scss @@ -0,0 +1,41 @@ +.root { + :global(.modal-dialog) { + max-width: 25rem; + } +} + +.content { + padding: 1rem 0; + text-align: center; +} + +.header { + display: flex; + justify-content: center; + margin-bottom: 1.5rem; +} + +.iconWrapper { + display: flex; + align-items: center; + justify-content: center; + + width: 4rem; + height: 4rem; + border-radius: 50%; + + background-color: var(--accent-color); +} + +.icon { + position: relative; + z-index: 1; + font-size: 2rem; + color: var(--color-white); +} + +.mainText, +.description { + font-size: 0.9375rem; + color: var(--color-text); +} diff --git a/src/components/modals/ageVerification/AgeVerificationModal.tsx b/src/components/modals/ageVerification/AgeVerificationModal.tsx new file mode 100644 index 000000000..0804a4659 --- /dev/null +++ b/src/components/modals/ageVerification/AgeVerificationModal.tsx @@ -0,0 +1,93 @@ +import type { FC } from '../../../lib/teact/teact'; +import { memo } from '../../../lib/teact/teact'; +import { getActions, withGlobal } from '../../../global'; + +import type { TabState } from '../../../global/types'; + +import useLang from '../../../hooks/useLang'; +import useLastCallback from '../../../hooks/useLastCallback'; + +import Icon from '../../common/icons/Icon'; +import Button from '../../ui/Button'; +import Modal from '../../ui/Modal'; + +import styles from './AgeVerificationModal.module.scss'; + +export type OwnProps = { + modal: TabState['isAgeVerificationModalOpen']; +}; + +type StateProps = { + verifyAgeBotUsername?: string; +}; + +const AGE_REQUIRED = 18; + +const AgeVerificationModal: FC = ({ + modal, + verifyAgeBotUsername, +}) => { + const { closeAgeVerificationModal, openChatByUsername } = getActions(); + const lang = useLang(); + const isOpen = Boolean(modal); + const ageRequired = AGE_REQUIRED; + + const handleVerifyAge = useLastCallback(() => { + if (verifyAgeBotUsername) { + openChatByUsername({ + shouldStartMainApp: true, + username: verifyAgeBotUsername, + }); + } + closeAgeVerificationModal(); + }); + + const handleClose = useLastCallback(() => { + closeAgeVerificationModal(); + }); + + return ( + +
+
+
+ +
+
+

+ {lang('TitleAgeVerificationModal')} +

+

+ {lang('TextAgeVerificationModal', { count: ageRequired }, { + withMarkdown: true, + withNodes: true, + pluralValue: ageRequired, + })} +

+

+ {lang('DescriptionAgeVerificationModal')} +

+
+
+ +
+
+ ); +}; + +export default memo(withGlobal((global): StateProps => { + const appConfig = global.appConfig; + const verifyAgeBotUsername = appConfig?.verifyAgeBotUsername; + + return { + verifyAgeBotUsername, + }; +})(AgeVerificationModal)); diff --git a/src/components/modals/webApp/hooks/useWebAppFrame.ts b/src/components/modals/webApp/hooks/useWebAppFrame.ts index 818f0ae99..14fee0f50 100644 --- a/src/components/modals/webApp/hooks/useWebAppFrame.ts +++ b/src/components/modals/webApp/hooks/useWebAppFrame.ts @@ -50,6 +50,7 @@ const useWebAppFrame = ( closeWebApp, openSuggestedStatusModal, updateWebApp, + updateContentSettings, } = getActions(); const isReloadSupported = useRef(false); @@ -382,6 +383,25 @@ const useWebAppFrame = ( }); } + if (eventType === 'web_app_verify_age') { + const { passed } = eventData; + + if (passed) { + showNotification({ + message: { + key: 'TitleAgeCheckSuccess', + }, + }); + updateContentSettings({ isSensitiveEnabled: true }); + } else { + showNotification({ + message: { + key: 'TitleAgeCheckFailed', + }, + }); + } + } + onEvent(data); } catch (err) { // Ignore other messages diff --git a/src/components/right/Profile.tsx b/src/components/right/Profile.tsx index e58b10eca..ce0c677f4 100644 --- a/src/components/right/Profile.tsx +++ b/src/components/right/Profile.tsx @@ -48,6 +48,7 @@ import { selectChatFullInfo, selectChatMessages, selectCurrentSharedMediaSearch, + selectIsChatRestricted, selectIsCurrentUserPremium, selectIsRightColumnShown, selectMonoforumChannel, @@ -995,6 +996,7 @@ export default memo(withGlobal( const peerGifts = selectTabState(global).savedGifts.giftsByPeerId[chatId]; const monoforumChannel = selectMonoforumChannel(global, chatId); + const isRestricted = chat && selectIsChatRestricted(global, chat.id); return { theme: selectTheme(global), @@ -1012,7 +1014,7 @@ export default memo(withGlobal( canDeleteMembers, currentUserId: global.currentUserId, isRightColumnShown: selectIsRightColumnShown(global, isMobile), - isRestricted: chat?.isRestricted, + isRestricted, activeDownloads, usersById, userStatusesById, diff --git a/src/components/right/management/ManageChannel.tsx b/src/components/right/management/ManageChannel.tsx index 20ffdd04e..ca3e4cb03 100644 --- a/src/components/right/management/ManageChannel.tsx +++ b/src/components/right/management/ManageChannel.tsx @@ -3,7 +3,7 @@ import type { FC } from '../../../lib/teact/teact'; import { memo, useEffect, useMemo, useState, } from '../../../lib/teact/teact'; -import { getActions, withGlobal } from '../../../global'; +import { getActions, getGlobal, withGlobal } from '../../../global'; import type { ApiAvailableReaction, ApiChat, ApiChatFullInfo, ApiExportedInvite, @@ -12,7 +12,7 @@ import { ApiMediaFormat } from '../../../api/types'; import { ManagementProgress, ManagementScreens } from '../../../types'; import { getChatAvatarHash, getHasAdminRight, isChatChannel, isChatPublic } from '../../../global/helpers'; -import { selectChat, selectChatFullInfo, selectTabState } from '../../../global/selectors'; +import { selectChat, selectChatFullInfo, selectIsChatRestricted, selectTabState } from '../../../global/selectors'; import { formatInteger } from '../../../util/textFormat'; import useFlag from '../../../hooks/useFlag'; @@ -217,7 +217,8 @@ const ManageChannel: FC = ({ }, [availableReactions, chatFullInfo?.enabledReactions, lang]); const isChannelPublic = useMemo(() => isChatPublic(chat), [chat]); - if (chat.isRestricted || chat.isForbidden) { + const isRestricted = selectIsChatRestricted(getGlobal(), chatId); + if (isRestricted || chat.isForbidden) { return undefined; } diff --git a/src/components/right/management/ManageGroup.tsx b/src/components/right/management/ManageGroup.tsx index 62cd09af3..61a8d8713 100644 --- a/src/components/right/management/ManageGroup.tsx +++ b/src/components/right/management/ManageGroup.tsx @@ -3,7 +3,7 @@ import type { FC } from '../../../lib/teact/teact'; import { memo, useEffect, useMemo, useRef, useState, } from '../../../lib/teact/teact'; -import { getActions, withGlobal } from '../../../global'; +import { getActions, getGlobal, withGlobal } from '../../../global'; import type { ApiAvailableReaction, ApiChat, ApiChatBannedRights, ApiChatFullInfo, ApiExportedInvite, @@ -17,7 +17,7 @@ import { isChatBasicGroup, isChatPublic, } from '../../../global/helpers'; -import { selectChat, selectChatFullInfo, selectTabState } from '../../../global/selectors'; +import { selectChat, selectChatFullInfo, selectIsChatRestricted, selectTabState } from '../../../global/selectors'; import { debounce } from '../../../util/schedulers'; import { formatInteger } from '../../../util/textFormat'; import renderText from '../../common/helpers/renderText'; @@ -312,7 +312,8 @@ const ManageGroup: FC = ({ openChat({ id: undefined }); }); - if (chat.isRestricted || chat.isForbidden) { + const isRestricted = selectIsChatRestricted(getGlobal(), chatId); + if (isRestricted || chat.isForbidden) { return undefined; } diff --git a/src/config.ts b/src/config.ts index 66e30256a..ebf751318 100644 --- a/src/config.ts +++ b/src/config.ts @@ -344,6 +344,8 @@ export const TME_WEB_DOMAINS = new Set(['t.me', 'web.t.me', 'a.t.me', 'k.t.me', export const WEB_APP_PLATFORM = 'weba'; export const LANG_PACK = 'weba'; +export const NSFW_RESTRICTION_REASON = 'sensitive'; + // eslint-disable-next-line @stylistic/max-len export const COUNTRIES_WITH_12H_TIME_FORMAT = new Set(['AU', 'BD', 'CA', 'CO', 'EG', 'HN', 'IE', 'IN', 'JO', 'MX', 'MY', 'NI', 'NZ', 'PH', 'PK', 'SA', 'SV', 'US']); diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index d334b5e2a..2f97a4334 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -1633,7 +1633,8 @@ addActionHandler('acceptChatInvite', async (global, actions, payload): Promise => { const { - username, messageId, commentId, startParam, startAttach, attach, threadId, originalParts, startApp, mode, + username, messageId, commentId, startParam, startAttach, attach, threadId, originalParts, + startApp, shouldStartMainApp, mode, text, onChatChanged, choose, ref, timestamp, tabId = getCurrentTabId(), } = payload; @@ -1661,7 +1662,7 @@ addActionHandler('openChatByUsername', async (global, actions, payload): Promise return; } - if (startApp !== undefined && !webAppName) { + if ((startApp !== undefined && !webAppName) || shouldStartMainApp) { const theme = extractCurrentThemeParams(); const chatByUsername = await fetchChatByUsername(global, username); global = getGlobal(); diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index e7691af90..7eb8cba96 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -126,6 +126,7 @@ import { selectForwardsCanBeSentToChat, selectForwardsContainVoiceMessages, selectIsChatBotNotStarted, + selectIsChatRestricted, selectIsChatWithSelf, selectIsCurrentUserFrozen, selectIsCurrentUserPremium, @@ -187,8 +188,9 @@ addActionHandler('loadViewportMessages', (global, actions, payload): ActionRetur } const chat = selectChat(global, chatId); - // TODO Revise if `chat.isRestricted` check is needed - if (!chat || chat.isRestricted) { + const isRestricted = selectIsChatRestricted(global, chatId); + // TODO Revise if `isRestricted` check is needed + if (!chat || isRestricted) { onError?.(); return; } diff --git a/src/global/actions/api/users.ts b/src/global/actions/api/users.ts index 9c9c7bcde..334dfdf8b 100644 --- a/src/global/actions/api/users.ts +++ b/src/global/actions/api/users.ts @@ -31,6 +31,7 @@ import { updateTabState } from '../../reducers/tabs'; import { selectChat, selectChatFullInfo, + selectIsChatRestricted, selectIsCurrentUserFrozen, selectIsCurrentUserPremium, selectPeer, @@ -309,6 +310,10 @@ addActionHandler('loadMoreProfilePhotos', async (global, actions, payload): Prom const user = isPrivate ? selectUser(global, peerId) : undefined; const chat = !isPrivate ? selectChat(global, peerId) : undefined; const peer = user || chat; + + if (chat && selectIsChatRestricted(global, peerId)) { + return; + } const profilePhotos = selectPeerPhotos(global, peerId); if (!peer?.avatarPhotoId) { return; diff --git a/src/global/actions/apiUpdaters/chats.ts b/src/global/actions/apiUpdaters/chats.ts index 0554ffc81..86c521677 100644 --- a/src/global/actions/apiUpdaters/chats.ts +++ b/src/global/actions/apiUpdaters/chats.ts @@ -44,7 +44,7 @@ import { const TYPING_STATUS_CLEAR_DELAY = 6000; // 6 seconds const INVALIDATE_FULL_CHAT_FIELDS = new Set([ - 'boostLevel', 'isForum', 'isLinkedInDiscussion', 'fakeType', 'restrictionReason', 'isJoinToSend', 'isJoinRequest', + 'boostLevel', 'isForum', 'isLinkedInDiscussion', 'fakeType', 'restrictionReasons', 'isJoinToSend', 'isJoinRequest', 'type', ]); diff --git a/src/global/actions/ui/account.ts b/src/global/actions/ui/account.ts index 5d6ac9072..96e04cc5a 100644 --- a/src/global/actions/ui/account.ts +++ b/src/global/actions/ui/account.ts @@ -41,3 +41,19 @@ addActionHandler('closeDeleteAccountModal', (global, actions, payload): ActionRe deleteAccountModal: undefined, }, tabId); }); + +addActionHandler('openAgeVerificationModal', (global, actions, payload): ActionReturnType => { + const { tabId = getCurrentTabId() } = payload || {}; + + return updateTabState(global, { + isAgeVerificationModalOpen: true, + }, tabId); +}); + +addActionHandler('closeAgeVerificationModal', (global, actions, payload): ActionReturnType => { + const { tabId = getCurrentTabId() } = payload || {}; + + return updateTabState(global, { + isAgeVerificationModalOpen: false, + }, tabId); +}); diff --git a/src/global/helpers/chats.ts b/src/global/helpers/chats.ts index 5cba3a24d..b80b75673 100644 --- a/src/global/helpers/chats.ts +++ b/src/global/helpers/chats.ts @@ -25,6 +25,7 @@ import { import { formatDateToString, formatTime } from '../../util/dates/dateFormat'; import { getPeerIdDividend, isUserId } from '../../util/entities/ids'; import { getServerTime } from '../../util/serverTime'; +import { selectIsChatRestricted } from '../selectors'; import { getGlobal } from '..'; import { isSystemBot } from './bots'; import { getMainUsername } from './users'; @@ -155,7 +156,9 @@ export function getCanPostInChat( } } - if (chat.isRestricted || chat.isForbidden || chat.migratedTo + const global = getGlobal(); + const isRestricted = selectIsChatRestricted(global, chat.id); + if (isRestricted || chat.isForbidden || chat.migratedTo || (chat.isNotJoined && !isChatMonoforum(chat) && !isMessageThread) || isSystemBot(chat.id) || isAnonymousForwardsChat(chat.id)) { return false; @@ -380,7 +383,9 @@ export function getGroupStatus(lang: OldLangFn, chat: ApiChat) { const chatTypeString = lang(getChatTypeString(chat)); const { membersCount } = chat; - if (chat.isRestricted) { + const global = getGlobal(); + const isRestricted = selectIsChatRestricted(global, chat.id); + if (isRestricted) { return chatTypeString === 'Channel' ? 'channel is inaccessible' : 'group is inaccessible'; } diff --git a/src/global/selectors/chats.ts b/src/global/selectors/chats.ts index 928d9a152..85252eaa8 100644 --- a/src/global/selectors/chats.ts +++ b/src/global/selectors/chats.ts @@ -20,6 +20,7 @@ import { isUserBot, isUserOnline, } from '../helpers'; +import { selectActiveRestrictionReasons } from './messages'; import { selectTabState } from './tabs'; import { selectBot, selectIsCurrentUserPremium, selectUser, selectUserFullInfo, @@ -374,3 +375,11 @@ export function selectMonoforumChannel( return chat.isMonoforum ? selectChat(global, chat.linkedMonoforumId!) : undefined; } + +export function selectIsChatRestricted(global: T, chatId: string): boolean { + const chat = selectChat(global, chatId); + if (!chat) return false; + + const activeRestrictions = selectActiveRestrictionReasons(global, chat.restrictionReasons); + return activeRestrictions.length > 0; +} diff --git a/src/global/selectors/management.ts b/src/global/selectors/management.ts index 361eb12b8..d4c86ff22 100644 --- a/src/global/selectors/management.ts +++ b/src/global/selectors/management.ts @@ -7,7 +7,7 @@ import { isAnonymousForwardsChat, isChatAdmin, isChatGroup, isUserBot, } from '../helpers'; -import { selectChat, selectIsChatWithSelf } from './chats'; +import { selectChat, selectIsChatRestricted, selectIsChatWithSelf } from './chats'; import { selectCurrentMessageList } from './messages'; import { selectTabState } from './tabs'; import { selectBot, selectUser } from './users'; @@ -72,7 +72,8 @@ export function selectCanManage( chatId: string, ) { const chat = selectChat(global, chatId); - if (!chat || chat.isRestricted || chat.isMonoforum) return false; + const isRestricted = selectIsChatRestricted(global, chatId); + if (!chat || isRestricted || chat.isMonoforum) return false; const isPrivate = isUserId(chat.id); const user = isPrivate ? selectUser(global, chatId) : undefined; diff --git a/src/global/selectors/messages.ts b/src/global/selectors/messages.ts index 0ffcf1659..1f0291858 100644 --- a/src/global/selectors/messages.ts +++ b/src/global/selectors/messages.ts @@ -5,7 +5,7 @@ import type { ApiMessageEntityCustomEmoji, ApiMessageForwardInfo, ApiMessageOutgoingStatus, - ApiPeer, ApiSponsoredMessage, + ApiPeer, ApiRestrictionReason, ApiSponsoredMessage, ApiStickerSetInfo, } from '../../api/types'; import type { @@ -22,8 +22,8 @@ import type { import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../../api/types'; import { - ANONYMOUS_USER_ID, API_GENERAL_ID_LIMIT, GENERAL_TOPIC_ID, SERVICE_NOTIFICATIONS_USER_ID, - SVG_EXTENSIONS, + ANONYMOUS_USER_ID, API_GENERAL_ID_LIMIT, GENERAL_TOPIC_ID, NSFW_RESTRICTION_REASON, SERVICE_NOTIFICATIONS_USER_ID, + SVG_EXTENSIONS, WEB_APP_PLATFORM, } from '../../config'; import { IS_TRANSLATION_SUPPORTED } from '../../util/browser/windowEnvironment'; import { isUserId } from '../../util/entities/ids'; @@ -72,11 +72,13 @@ import { selectChat, selectChatFullInfo, selectChatLastMessageId, + selectIsChatRestricted, selectIsChatWithBot, selectIsChatWithSelf, selectRequestedChatTranslationLanguage, } from './chats'; import { selectPeer, selectPeerPaidMessagesStars } from './peers'; +import { selectSettingsKeys } from './settings'; import { selectPeerStory } from './stories'; import { selectIsStickerFavorite } from './symbols'; import { selectTabState } from './tabs'; @@ -587,7 +589,8 @@ export function selectThreadIdFromMessage(global: T, mess export function selectCanReplyToMessage(global: T, message: ApiMessage, threadId: ThreadId) { const chat = selectChat(global, message.chatId); - if (!chat || chat.isRestricted || chat.isForbidden) return false; + const isRestricted = selectIsChatRestricted(global, message.chatId); + if (!chat || isRestricted || chat.isForbidden) return false; const isLocal = isMessageLocal(message); const isServiceNotification = isServiceNotificationMessage(message); @@ -632,7 +635,8 @@ export function selectAllowedMessageActionsSlow( global: T, message: ApiMessage, threadId: ThreadId, ) { const chat = selectChat(global, message.chatId); - if (!chat || chat.isRestricted) { + const isRestricted = selectIsChatRestricted(global, message.chatId); + if (!chat || isRestricted) { return {}; } @@ -1563,3 +1567,31 @@ export function selectMessageLastPlaybackTimestamp( ) { return global.messages.playbackByChatId[chatId]?.byId[messageId]; } + +export function selectActiveRestrictionReasons( + global: T, restrictionReasons?: ApiRestrictionReason[], +): ApiRestrictionReason[] { + if (!restrictionReasons) return []; + + const { ignoreRestrictionReasons } = global.appConfig || {}; + + return restrictionReasons.filter((reason) => { + const isForCurrentPlatform = reason.platform === 'all' || reason.platform === WEB_APP_PLATFORM; + if (!isForCurrentPlatform) return false; + + const shouldIgnore = ignoreRestrictionReasons?.includes(reason.reason); + return !shouldIgnore; + }); +} + +export function selectIsMediaNsfw(global: T, message: ApiMessage) { + const { isSensitiveEnabled } = selectSettingsKeys(global); + const chat = selectChat(global, message.chatId); + if (isSensitiveEnabled) return false; + + const chatActiveRestrictions = selectActiveRestrictionReasons(global, chat?.restrictionReasons); + const messageActiveRestrictions = selectActiveRestrictionReasons(global, message.restrictionReasons); + + return chatActiveRestrictions.some((reason) => reason.reason === NSFW_RESTRICTION_REASON) + || messageActiveRestrictions.some((reason) => reason.reason === NSFW_RESTRICTION_REASON); +} diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index 79a7b4e63..ef57f5137 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -656,6 +656,7 @@ export interface ActionPayloads { startAttach?: string; attach?: string; startApp?: string; + shouldStartMainApp?: boolean; mode?: string; choose?: ApiChatType[]; text?: string; @@ -1081,6 +1082,8 @@ export interface ActionPayloads { days: number; } & WithTabId | undefined; closeDeleteAccountModal: WithTabId | undefined; + openAgeVerificationModal: WithTabId | undefined; + closeAgeVerificationModal: WithTabId | undefined; setAccountTTL: { days: number; } & WithTabId | undefined; diff --git a/src/global/types/tabState.ts b/src/global/types/tabState.ts index 0f2c2c913..fe9734090 100644 --- a/src/global/types/tabState.ts +++ b/src/global/types/tabState.ts @@ -754,6 +754,8 @@ export type TabState = { selfDestructAccountDays: number; }; + isAgeVerificationModalOpen?: boolean; + paidReactionModal?: { chatId: string; messageId: number; diff --git a/src/hooks/polling/usePeerStoriesPolling.ts b/src/hooks/polling/usePeerStoriesPolling.ts index 4e14e33db..a4d16cc1e 100644 --- a/src/hooks/polling/usePeerStoriesPolling.ts +++ b/src/hooks/polling/usePeerStoriesPolling.ts @@ -4,7 +4,7 @@ import { getActions, getGlobal } from '../../global'; import type { ApiChat, ApiUser } from '../../api/types'; import { isChatChannel, isUserBot } from '../../global/helpers'; -import { selectPeer, selectUserStatus } from '../../global/selectors'; +import { selectIsChatRestricted, selectPeer, selectUserStatus } from '../../global/selectors'; import { isUserId } from '../../util/entities/ids'; import { throttle } from '../../util/schedulers'; @@ -54,7 +54,8 @@ export default function usePeerStoriesPolling(ids?: string[]) { return !user.isContact && !user.isSelf && !isUserBot(user) && !peer.isSupport && isStatusAvailable; } else { const chat = peer as ApiChat; - return isChatChannel(chat) && !chat.isRestricted; + const isRestricted = selectIsChatRestricted(global, chat.id); + return isChatChannel(chat) && !isRestricted; } }).map((user) => user.id); }, [peers]); diff --git a/src/lib/gramjs/tl/AllTLObjects.ts b/src/lib/gramjs/tl/AllTLObjects.ts index 549cd6738..2c597d0db 100644 --- a/src/lib/gramjs/tl/AllTLObjects.ts +++ b/src/lib/gramjs/tl/AllTLObjects.ts @@ -12,6 +12,6 @@ for (const tl of Object.values(Api)) { } } -export const LAYER = 207; +export const LAYER = 210; export { tlobjects }; diff --git a/src/lib/gramjs/tl/api.d.ts b/src/lib/gramjs/tl/api.d.ts index c3f9b4970..9875854d0 100644 --- a/src/lib/gramjs/tl/api.d.ts +++ b/src/lib/gramjs/tl/api.d.ts @@ -397,6 +397,8 @@ namespace Api { export type TypeTodoList = TodoList; export type TypeTodoCompletion = TodoCompletion; export type TypeSuggestedPost = SuggestedPost; + export type TypeStarsRating = StarsRating; + export type TypeStarGiftCollection = StarGiftCollection; export type TypeResPQ = ResPQ; export type TypeP_Q_inner_data = PQInnerData | PQInnerDataDc | PQInnerDataTemp | PQInnerDataTempDc; export type TypeServer_DH_Params = ServerDHParamsFail | ServerDHParamsOk; @@ -612,6 +614,7 @@ namespace Api { export type TypeSavedStarGifts = payments.SavedStarGifts; export type TypeStarGiftWithdrawalUrl = payments.StarGiftWithdrawalUrl; export type TypeResaleStarGifts = payments.ResaleStarGifts; + export type TypeStarGiftCollections = payments.StarGiftCollectionsNotModified | payments.StarGiftCollections; } export namespace phone { @@ -3918,6 +3921,7 @@ namespace Api { botVerification?: Api.TypeBotVerification; sendPaidMessagesStars?: long; disallowedGifts?: Api.TypeDisallowedGiftsSettings; + starsRating?: Api.TypeStarsRating; }> { // flags: Api.Type; blocked?: true; @@ -3969,7 +3973,8 @@ namespace Api { botVerification?: Api.TypeBotVerification; sendPaidMessagesStars?: long; disallowedGifts?: Api.TypeDisallowedGiftsSettings; - CONSTRUCTOR_ID: 2582085701; + starsRating?: Api.TypeStarsRating; + CONSTRUCTOR_ID: 702447806; SUBCLASS_OF_ID: 524706233; className: 'UserFull'; @@ -13347,6 +13352,7 @@ namespace Api { quoteText?: string; quoteEntities?: Api.TypeMessageEntity[]; quoteOffset?: int; + todoItemId?: int; } | void> { // flags: Api.Type; replyToScheduled?: true; @@ -13360,7 +13366,8 @@ namespace Api { quoteText?: string; quoteEntities?: Api.TypeMessageEntity[]; quoteOffset?: int; - CONSTRUCTOR_ID: 2948336091; + todoItemId?: int; + CONSTRUCTOR_ID: 1763137035; SUBCLASS_OF_ID: 1531810151; className: 'MessageReplyHeader'; @@ -15261,6 +15268,7 @@ namespace Api { quoteEntities?: Api.TypeMessageEntity[]; quoteOffset?: int; monoforumPeerId?: Api.TypeInputPeer; + todoItemId?: int; }> { // flags: Api.Type; replyToMsgId: int; @@ -15270,7 +15278,8 @@ namespace Api { quoteEntities?: Api.TypeMessageEntity[]; quoteOffset?: int; monoforumPeerId?: Api.TypeInputPeer; - CONSTRUCTOR_ID: 2960144560; + todoItemId?: int; + CONSTRUCTOR_ID: 2258615824; SUBCLASS_OF_ID: 2356220701; className: 'InputReplyToMessage'; @@ -16754,6 +16763,8 @@ namespace Api { limited?: true; soldOut?: true; birthday?: true; + requirePremium?: true; + limitedPerUser?: true; id: long; sticker: Api.TypeDocument; stars: long; @@ -16767,11 +16778,15 @@ namespace Api { resellMinStars?: long; title?: string; releasedBy?: Api.TypePeer; + perUserTotal?: int; + perUserRemains?: int; }> { // flags: Api.Type; limited?: true; soldOut?: true; birthday?: true; + requirePremium?: true; + limitedPerUser?: true; id: long; sticker: Api.TypeDocument; stars: long; @@ -16785,7 +16800,9 @@ namespace Api { resellMinStars?: long; title?: string; releasedBy?: Api.TypePeer; - CONSTRUCTOR_ID: 2139438098; + perUserTotal?: int; + perUserRemains?: int; + CONSTRUCTOR_ID: 12386139; SUBCLASS_OF_ID: 3273414923; className: 'StarGift'; @@ -16793,6 +16810,7 @@ namespace Api { } export class StarGiftUnique extends VirtualClass<{ // flags: Api.Type; + requirePremium?: true; id: long; title: string; slug: string; @@ -16808,6 +16826,7 @@ namespace Api { releasedBy?: Api.TypePeer; }> { // flags: Api.Type; + requirePremium?: true; id: long; title: string; slug: string; @@ -17079,6 +17098,7 @@ namespace Api { transferStars?: long; canTransferAt?: int; canResellAt?: int; + collectionId?: int[]; }> { // flags: Api.Type; nameHidden?: true; @@ -17098,7 +17118,8 @@ namespace Api { transferStars?: long; canTransferAt?: int; canResellAt?: int; - CONSTRUCTOR_ID: 3755607193; + collectionId?: int[]; + CONSTRUCTOR_ID: 514213599; SUBCLASS_OF_ID: 2385198100; className: 'SavedStarGift'; @@ -17378,6 +17399,44 @@ namespace Api { static fromReader(reader: Reader): SuggestedPost; } + export class StarsRating extends VirtualClass<{ + // flags: Api.Type; + level: int; + currentLevelStars: long; + stars: long; + nextLevelStars?: long; + }> { + // flags: Api.Type; + level: int; + currentLevelStars: long; + stars: long; + nextLevelStars?: long; + CONSTRUCTOR_ID: 453922567; + SUBCLASS_OF_ID: 1668506656; + className: 'StarsRating'; + + static fromReader(reader: Reader): StarsRating; + } + export class StarGiftCollection extends VirtualClass<{ + // flags: Api.Type; + collectionId: int; + title: string; + icon?: Api.TypeDocument; + giftsCount: int; + hash: long; + }> { + // flags: Api.Type; + collectionId: int; + title: string; + icon?: Api.TypeDocument; + giftsCount: int; + hash: long; + CONSTRUCTOR_ID: 2641040304; + SUBCLASS_OF_ID: 1138805578; + className: 'StarGiftCollection'; + + static fromReader(reader: Reader): StarGiftCollection; + } export class ResPQ extends VirtualClass<{ nonce: int128; serverNonce: int128; @@ -21385,6 +21444,23 @@ namespace Api { static fromReader(reader: Reader): ResaleStarGifts; } + export class StarGiftCollectionsNotModified extends VirtualClass { + CONSTRUCTOR_ID: 2696564503; + SUBCLASS_OF_ID: 4028047852; + className: 'StarGiftCollectionsNotModified'; + + static fromReader(reader: Reader): StarGiftCollectionsNotModified; + } + export class StarGiftCollections extends VirtualClass<{ + collections: Api.TypeStarGiftCollection[]; + }> { + collections: Api.TypeStarGiftCollection[]; + CONSTRUCTOR_ID: 2317955827; + SUBCLASS_OF_ID: 4028047852; + className: 'StarGiftCollections'; + + static fromReader(reader: Reader): StarGiftCollections; + } } export namespace phone { @@ -27128,6 +27204,7 @@ namespace Api { excludeUnique?: true; sortByValue?: true; peer: Api.TypeInputPeer; + collectionId?: int; offset: string; limit: int; }, payments.TypeSavedStarGifts> { @@ -27139,6 +27216,7 @@ namespace Api { excludeUnique?: true; sortByValue?: true; peer: Api.TypeInputPeer; + collectionId?: int; offset: string; limit: int; } @@ -27201,6 +27279,53 @@ namespace Api { stargift: Api.TypeInputSavedStarGift; resellStars: long; } + export class CreateStarGiftCollection extends Request<{ + peer: Api.TypeInputPeer; + title: string; + stargift: Api.TypeInputSavedStarGift[]; + }, Api.TypeStarGiftCollection> { + peer: Api.TypeInputPeer; + title: string; + stargift: Api.TypeInputSavedStarGift[]; + } + export class UpdateStarGiftCollection extends Request<{ + // flags: Api.Type; + peer: Api.TypeInputPeer; + collectionId: int; + title?: string; + deleteStargift?: Api.TypeInputSavedStarGift[]; + addStargift?: Api.TypeInputSavedStarGift[]; + order?: Api.TypeInputSavedStarGift[]; + }, Api.TypeStarGiftCollection> { + // flags: Api.Type; + peer: Api.TypeInputPeer; + collectionId: int; + title?: string; + deleteStargift?: Api.TypeInputSavedStarGift[]; + addStargift?: Api.TypeInputSavedStarGift[]; + order?: Api.TypeInputSavedStarGift[]; + } + export class ReorderStarGiftCollections extends Request<{ + peer: Api.TypeInputPeer; + order: int[]; + }, Bool> { + peer: Api.TypeInputPeer; + order: int[]; + } + export class DeleteStarGiftCollection extends Request<{ + peer: Api.TypeInputPeer; + collectionId: int; + }, Bool> { + peer: Api.TypeInputPeer; + collectionId: int; + } + export class GetStarGiftCollections extends Request<{ + peer: Api.TypeInputPeer; + hash: long; + }, payments.TypeStarGiftCollections> { + peer: Api.TypeInputPeer; + hash: long; + } } export namespace stickers { @@ -28196,7 +28321,7 @@ namespace Api { | help.GetConfig | help.GetNearestDc | help.GetAppUpdate | help.GetInviteText | help.GetSupport | help.SetBotUpdatesStatus | help.GetCdnConfig | help.GetRecentMeUrls | help.GetTermsOfServiceUpdate | help.AcceptTermsOfService | help.GetDeepLinkInfo | help.GetAppConfig | help.SaveAppLog | help.GetPassportConfig | help.GetSupportName | help.GetUserInfo | help.EditUserInfo | help.GetPromoData | help.HidePromoData | help.DismissSuggestion | help.GetCountriesList | help.GetPremiumPromo | help.GetPeerColors | help.GetPeerProfileColors | help.GetTimezonesList | channels.ReadHistory | channels.DeleteMessages | channels.ReportSpam | channels.GetMessages | channels.GetParticipants | channels.GetParticipant | channels.GetChannels | channels.GetFullChannel | channels.CreateChannel | channels.EditAdmin | channels.EditTitle | channels.EditPhoto | channels.CheckUsername | channels.UpdateUsername | channels.JoinChannel | channels.LeaveChannel | channels.InviteToChannel | channels.DeleteChannel | channels.ExportMessageLink | channels.ToggleSignatures | channels.GetAdminedPublicChannels | channels.EditBanned | channels.GetAdminLog | channels.SetStickers | channels.ReadMessageContents | channels.DeleteHistory | channels.TogglePreHistoryHidden | channels.GetLeftChannels | channels.GetGroupsForDiscussion | channels.SetDiscussionGroup | channels.EditCreator | channels.EditLocation | channels.ToggleSlowMode | channels.GetInactiveChannels | channels.ConvertToGigagroup | channels.GetSendAs | channels.DeleteParticipantHistory | channels.ToggleJoinToSend | channels.ToggleJoinRequest | channels.ReorderUsernames | channels.ToggleUsername | channels.DeactivateAllUsernames | channels.ToggleForum | channels.CreateForumTopic | channels.GetForumTopics | channels.GetForumTopicsByID | channels.EditForumTopic | channels.UpdatePinnedForumTopic | channels.DeleteTopicHistory | channels.ReorderPinnedForumTopics | channels.ToggleAntiSpam | channels.ReportAntiSpamFalsePositive | channels.ToggleParticipantsHidden | channels.UpdateColor | channels.ToggleViewForumAsMessages | channels.GetChannelRecommendations | channels.UpdateEmojiStatus | channels.SetBoostsToUnblockRestrictions | channels.SetEmojiStickers | channels.RestrictSponsoredMessages | channels.SearchPosts | channels.UpdatePaidMessagesPrice | channels.ToggleAutotranslation | channels.GetMessageAuthor | bots.SendCustomRequest | bots.AnswerWebhookJSONQuery | bots.SetBotCommands | bots.ResetBotCommands | bots.GetBotCommands | bots.SetBotMenuButton | bots.GetBotMenuButton | bots.SetBotBroadcastDefaultAdminRights | bots.SetBotGroupDefaultAdminRights | bots.SetBotInfo | bots.GetBotInfo | bots.ReorderUsernames | bots.ToggleUsername | bots.CanSendMessage | bots.AllowSendMessage | bots.InvokeWebViewCustomMethod | bots.GetPopularAppBots | bots.AddPreviewMedia | bots.EditPreviewMedia | bots.DeletePreviewMedia | bots.ReorderPreviewMedias | bots.GetPreviewInfo | bots.GetPreviewMedias | bots.UpdateUserEmojiStatus | bots.ToggleUserEmojiStatusPermission | bots.CheckDownloadFileParams | bots.GetAdminedBots | bots.UpdateStarRefProgram | bots.SetCustomVerification | bots.GetBotRecommendations - | payments.GetPaymentForm | payments.GetPaymentReceipt | payments.ValidateRequestedInfo | payments.SendPaymentForm | payments.GetSavedInfo | payments.ClearSavedInfo | payments.GetBankCardData | payments.ExportInvoice | payments.AssignAppStoreTransaction | payments.AssignPlayMarketTransaction | payments.GetPremiumGiftCodeOptions | payments.CheckGiftCode | payments.ApplyGiftCode | payments.GetGiveawayInfo | payments.LaunchPrepaidGiveaway | payments.GetStarsTopupOptions | payments.GetStarsStatus | payments.GetStarsTransactions | payments.SendStarsForm | payments.RefundStarsCharge | payments.GetStarsRevenueStats | payments.GetStarsRevenueWithdrawalUrl | payments.GetStarsRevenueAdsAccountUrl | payments.GetStarsTransactionsByID | payments.GetStarsGiftOptions | payments.GetStarsSubscriptions | payments.ChangeStarsSubscription | payments.FulfillStarsSubscription | payments.GetStarsGiveawayOptions | payments.GetStarGifts | payments.SaveStarGift | payments.ConvertStarGift | payments.BotCancelStarsSubscription | payments.GetConnectedStarRefBots | payments.GetConnectedStarRefBot | payments.GetSuggestedStarRefBots | payments.ConnectStarRefBot | payments.EditConnectedStarRefBot | payments.GetStarGiftUpgradePreview | payments.UpgradeStarGift | payments.TransferStarGift | payments.GetUniqueStarGift | payments.GetSavedStarGifts | payments.GetSavedStarGift | payments.GetStarGiftWithdrawalUrl | payments.ToggleChatStarGiftNotifications | payments.ToggleStarGiftsPinnedToTop | payments.CanPurchaseStore | payments.GetResaleStarGifts | payments.UpdateStarGiftPrice + | payments.GetPaymentForm | payments.GetPaymentReceipt | payments.ValidateRequestedInfo | payments.SendPaymentForm | payments.GetSavedInfo | payments.ClearSavedInfo | payments.GetBankCardData | payments.ExportInvoice | payments.AssignAppStoreTransaction | payments.AssignPlayMarketTransaction | payments.GetPremiumGiftCodeOptions | payments.CheckGiftCode | payments.ApplyGiftCode | payments.GetGiveawayInfo | payments.LaunchPrepaidGiveaway | payments.GetStarsTopupOptions | payments.GetStarsStatus | payments.GetStarsTransactions | payments.SendStarsForm | payments.RefundStarsCharge | payments.GetStarsRevenueStats | payments.GetStarsRevenueWithdrawalUrl | payments.GetStarsRevenueAdsAccountUrl | payments.GetStarsTransactionsByID | payments.GetStarsGiftOptions | payments.GetStarsSubscriptions | payments.ChangeStarsSubscription | payments.FulfillStarsSubscription | payments.GetStarsGiveawayOptions | payments.GetStarGifts | payments.SaveStarGift | payments.ConvertStarGift | payments.BotCancelStarsSubscription | payments.GetConnectedStarRefBots | payments.GetConnectedStarRefBot | payments.GetSuggestedStarRefBots | payments.ConnectStarRefBot | payments.EditConnectedStarRefBot | payments.GetStarGiftUpgradePreview | payments.UpgradeStarGift | payments.TransferStarGift | payments.GetUniqueStarGift | payments.GetSavedStarGifts | payments.GetSavedStarGift | payments.GetStarGiftWithdrawalUrl | payments.ToggleChatStarGiftNotifications | payments.ToggleStarGiftsPinnedToTop | payments.CanPurchaseStore | payments.GetResaleStarGifts | payments.UpdateStarGiftPrice | payments.CreateStarGiftCollection | payments.UpdateStarGiftCollection | payments.ReorderStarGiftCollections | payments.DeleteStarGiftCollection | payments.GetStarGiftCollections | stickers.CreateStickerSet | stickers.RemoveStickerFromSet | stickers.ChangeStickerPosition | stickers.AddStickerToSet | stickers.SetStickerSetThumb | stickers.CheckShortName | stickers.SuggestShortName | stickers.ChangeSticker | stickers.RenameStickerSet | stickers.DeleteStickerSet | stickers.ReplaceSticker | phone.GetCallConfig | phone.RequestCall | phone.AcceptCall | phone.ConfirmCall | phone.ReceivedCall | phone.DiscardCall | phone.SetCallRating | phone.SaveCallDebug | phone.SendSignalingData | phone.CreateGroupCall | phone.JoinGroupCall | phone.LeaveGroupCall | phone.InviteToGroupCall | phone.DiscardGroupCall | phone.ToggleGroupCallSettings | phone.GetGroupCall | phone.GetGroupParticipants | phone.CheckGroupCall | phone.ToggleGroupCallRecord | phone.EditGroupCallParticipant | phone.EditGroupCallTitle | phone.GetGroupCallJoinAs | phone.ExportGroupCallInvite | phone.ToggleGroupCallStartSubscription | phone.StartScheduledGroupCall | phone.SaveDefaultGroupCallJoinAs | phone.JoinGroupCallPresentation | phone.LeaveGroupCallPresentation | phone.GetGroupCallStreamChannels | phone.GetGroupCallStreamRtmpUrl | phone.SaveCallLog | phone.CreateConferenceCall | phone.DeleteConferenceCallParticipants | phone.SendConferenceCallBroadcast | phone.InviteConferenceCallParticipant | phone.DeclineConferenceCallInvite | phone.GetGroupCallChainBlocks | langpack.GetLangPack | langpack.GetStrings | langpack.GetDifference | langpack.GetLanguages | langpack.GetLanguage diff --git a/src/lib/gramjs/tl/apiTl.ts b/src/lib/gramjs/tl/apiTl.ts index 5812e7fd5..2435619aa 100644 --- a/src/lib/gramjs/tl/apiTl.ts +++ b/src/lib/gramjs/tl/apiTl.ts @@ -208,7 +208,7 @@ inputReportReasonGeoIrrelevant#dbd4feed = ReportReason; inputReportReasonFake#f5ddd6e7 = ReportReason; inputReportReasonIllegalDrugs#a8eb2be = ReportReason; inputReportReasonPersonalDetails#9ec7863d = ReportReason; -userFull#99e78045 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true wallpaper_overridden:flags.28?true contact_require_premium:flags.29?true read_dates_private:flags.30?true flags2:# sponsored_enabled:flags2.7?true can_view_revenue:flags2.9?true bot_can_manage_emoji_status:flags2.10?true display_gifts_button:flags2.16?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights wallpaper:flags.24?WallPaper stories:flags.25?PeerStories business_work_hours:flags2.0?BusinessWorkHours business_location:flags2.1?BusinessLocation business_greeting_message:flags2.2?BusinessGreetingMessage business_away_message:flags2.3?BusinessAwayMessage business_intro:flags2.4?BusinessIntro birthday:flags2.5?Birthday personal_channel_id:flags2.6?long personal_channel_message:flags2.6?int stargifts_count:flags2.8?int starref_program:flags2.11?StarRefProgram bot_verification:flags2.12?BotVerification send_paid_messages_stars:flags2.14?long disallowed_gifts:flags2.15?DisallowedGiftsSettings = UserFull; +userFull#29de80be flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true wallpaper_overridden:flags.28?true contact_require_premium:flags.29?true read_dates_private:flags.30?true flags2:# sponsored_enabled:flags2.7?true can_view_revenue:flags2.9?true bot_can_manage_emoji_status:flags2.10?true display_gifts_button:flags2.16?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights wallpaper:flags.24?WallPaper stories:flags.25?PeerStories business_work_hours:flags2.0?BusinessWorkHours business_location:flags2.1?BusinessLocation business_greeting_message:flags2.2?BusinessGreetingMessage business_away_message:flags2.3?BusinessAwayMessage business_intro:flags2.4?BusinessIntro birthday:flags2.5?Birthday personal_channel_id:flags2.6?long personal_channel_message:flags2.6?int stargifts_count:flags2.8?int starref_program:flags2.11?StarRefProgram bot_verification:flags2.12?BotVerification send_paid_messages_stars:flags2.14?long disallowed_gifts:flags2.15?DisallowedGiftsSettings stars_rating:flags2.17?StarsRating = UserFull; contact#145ade0b user_id:long mutual:Bool = Contact; importedContact#c13e3c50 user_id:long client_id:long = ImportedContact; contactStatus#16d9703b user_id:long status:UserStatus = ContactStatus; @@ -1051,7 +1051,7 @@ help.countriesList#87d0759e countries:Vector hash:int = help.Count messageViews#455b853d flags:# views:flags.0?int forwards:flags.1?int replies:flags.2?MessageReplies = MessageViews; messages.messageViews#b6c4f543 views:Vector chats:Vector users:Vector = messages.MessageViews; messages.discussionMessage#a6341782 flags:# messages:Vector max_id:flags.0?int read_inbox_max_id:flags.1?int read_outbox_max_id:flags.2?int unread_count:int chats:Vector users:Vector = messages.DiscussionMessage; -messageReplyHeader#afbc09db flags:# reply_to_scheduled:flags.2?true forum_topic:flags.3?true quote:flags.9?true reply_to_msg_id:flags.4?int reply_to_peer_id:flags.0?Peer reply_from:flags.5?MessageFwdHeader reply_media:flags.8?MessageMedia reply_to_top_id:flags.1?int quote_text:flags.6?string quote_entities:flags.7?Vector quote_offset:flags.10?int = MessageReplyHeader; +messageReplyHeader#6917560b flags:# reply_to_scheduled:flags.2?true forum_topic:flags.3?true quote:flags.9?true reply_to_msg_id:flags.4?int reply_to_peer_id:flags.0?Peer reply_from:flags.5?MessageFwdHeader reply_media:flags.8?MessageMedia reply_to_top_id:flags.1?int quote_text:flags.6?string quote_entities:flags.7?Vector quote_offset:flags.10?int todo_item_id:flags.11?int = MessageReplyHeader; messageReplyStoryHeader#e5af939 peer:Peer story_id:int = MessageReplyHeader; messageReplies#83d60fc2 flags:# comments:flags.0?true replies:int replies_pts:int recent_repliers:flags.1?Vector channel_id:flags.0?long max_id:flags.2?int read_max_id:flags.3?int = MessageReplies; peerBlocked#e8fd8014 peer_id:Peer date:int = PeerBlocked; @@ -1245,7 +1245,7 @@ storyViewPublicForward#9083670b flags:# blocked:flags.0?true blocked_my_stories_ storyViewPublicRepost#bd74cf49 flags:# blocked:flags.0?true blocked_my_stories_from:flags.1?true peer_id:Peer story:StoryItem = StoryView; stories.storyViewsList#59d78fc5 flags:# count:int views_count:int forwards_count:int reactions_count:int views:Vector chats:Vector users:Vector next_offset:flags.0?string = stories.StoryViewsList; stories.storyViews#de9eed1d views:Vector users:Vector = stories.StoryViews; -inputReplyToMessage#b07038b0 flags:# reply_to_msg_id:int top_msg_id:flags.0?int reply_to_peer_id:flags.1?InputPeer quote_text:flags.2?string quote_entities:flags.3?Vector quote_offset:flags.4?int monoforum_peer_id:flags.5?InputPeer = InputReplyTo; +inputReplyToMessage#869fbe10 flags:# reply_to_msg_id:int top_msg_id:flags.0?int reply_to_peer_id:flags.1?InputPeer quote_text:flags.2?string quote_entities:flags.3?Vector quote_offset:flags.4?int monoforum_peer_id:flags.5?InputPeer todo_item_id:flags.6?int = InputReplyTo; inputReplyToStory#5881323a peer:InputPeer story_id:int = InputReplyTo; inputReplyToMonoForum#69d66c45 monoforum_peer_id:InputPeer = InputReplyTo; exportedStoryLink#3fc9053b link:string = ExportedStoryLink; @@ -1386,8 +1386,8 @@ starsSubscription#2e6eab1a flags:# canceled:flags.0?true can_refulfill:flags.1?t messageReactor#4ba3a95a flags:# top:flags.0?true my:flags.1?true anonymous:flags.2?true peer_id:flags.3?Peer count:int = MessageReactor; starsGiveawayOption#94ce852a flags:# extended:flags.0?true default:flags.1?true stars:long yearly_boosts:int store_product:flags.2?string currency:string amount:long winners:Vector = StarsGiveawayOption; starsGiveawayWinnersOption#54236209 flags:# default:flags.0?true users:int per_user_stars:long = StarsGiveawayWinnersOption; -starGift#7f853c12 flags:# limited:flags.0?true sold_out:flags.1?true birthday:flags.2?true id:long sticker:Document stars:long availability_remains:flags.0?int availability_total:flags.0?int availability_resale:flags.4?long convert_stars:long first_sale_date:flags.1?int last_sale_date:flags.1?int upgrade_stars:flags.3?long resell_min_stars:flags.4?long title:flags.5?string released_by:flags.6?Peer = StarGift; -starGiftUnique#f63778ae flags:# id:long title:string slug:string num:int owner_id:flags.0?Peer owner_name:flags.1?string owner_address:flags.2?string attributes:Vector availability_issued:int availability_total:int gift_address:flags.3?string resell_stars:flags.4?long released_by:flags.5?Peer = StarGift; +starGift#bcff5b flags:# limited:flags.0?true sold_out:flags.1?true birthday:flags.2?true require_premium:flags.7?true limited_per_user:flags.8?true id:long sticker:Document stars:long availability_remains:flags.0?int availability_total:flags.0?int availability_resale:flags.4?long convert_stars:long first_sale_date:flags.1?int last_sale_date:flags.1?int upgrade_stars:flags.3?long resell_min_stars:flags.4?long title:flags.5?string released_by:flags.6?Peer per_user_total:flags.8?int per_user_remains:flags.8?int = StarGift; +starGiftUnique#f63778ae flags:# require_premium:flags.6?true id:long title:string slug:string num:int owner_id:flags.0?Peer owner_name:flags.1?string owner_address:flags.2?string attributes:Vector availability_issued:int availability_total:int gift_address:flags.3?string resell_stars:flags.4?long released_by:flags.5?Peer = StarGift; payments.starGiftsNotModified#a388a368 = payments.StarGifts; payments.starGifts#2ed82995 hash:int gifts:Vector chats:Vector users:Vector = payments.StarGifts; messageReportOption#7903e3d9 text:string option:bytes = MessageReportOption; @@ -1416,7 +1416,7 @@ users.users#62d706b8 users:Vector = users.Users; users.usersSlice#315a4974 count:int users:Vector = users.Users; payments.uniqueStarGift#caa2f60b gift:StarGift users:Vector = payments.UniqueStarGift; messages.webPagePreview#b53e8b21 media:MessageMedia users:Vector = messages.WebPagePreview; -savedStarGift#dfda0499 flags:# name_hidden:flags.0?true unsaved:flags.5?true refunded:flags.9?true can_upgrade:flags.10?true pinned_to_top:flags.12?true from_id:flags.1?Peer date:int gift:StarGift message:flags.2?TextWithEntities msg_id:flags.3?int saved_id:flags.11?long convert_stars:flags.4?long upgrade_stars:flags.6?long can_export_at:flags.7?int transfer_stars:flags.8?long can_transfer_at:flags.13?int can_resell_at:flags.14?int = SavedStarGift; +savedStarGift#1ea646df flags:# name_hidden:flags.0?true unsaved:flags.5?true refunded:flags.9?true can_upgrade:flags.10?true pinned_to_top:flags.12?true from_id:flags.1?Peer date:int gift:StarGift message:flags.2?TextWithEntities msg_id:flags.3?int saved_id:flags.11?long convert_stars:flags.4?long upgrade_stars:flags.6?long can_export_at:flags.7?int transfer_stars:flags.8?long can_transfer_at:flags.13?int can_resell_at:flags.14?int collection_id:flags.15?Vector = SavedStarGift; payments.savedStarGifts#95f389b1 flags:# count:int chat_notifications_enabled:flags.1?Bool gifts:Vector next_offset:flags.0?string chats:Vector users:Vector = payments.SavedStarGifts; inputSavedStarGiftUser#69279795 msg_id:int = InputSavedStarGift; inputSavedStarGiftChat#f101aa7f peer:InputPeer saved_id:long = InputSavedStarGift; @@ -1445,6 +1445,10 @@ todoItem#cba9a52f id:int title:TextWithEntities = TodoItem; todoList#49b92a26 flags:# others_can_append:flags.0?true others_can_complete:flags.1?true title:TextWithEntities list:Vector = TodoList; todoCompletion#4cc120b7 id:int completed_by:long date:int = TodoCompletion; suggestedPost#e8e37e5 flags:# accepted:flags.1?true rejected:flags.2?true price:flags.3?StarsAmount schedule_date:flags.0?int = SuggestedPost; +starsRating#1b0e4f07 flags:# level:int current_level_stars:long stars:long next_level_stars:flags.0?long = StarsRating; +starGiftCollection#9d6b13b0 flags:# collection_id:int title:string icon:flags.0?Document gifts_count:int hash:long = StarGiftCollection; +payments.starGiftCollectionsNotModified#a0ba4f17 = payments.StarGiftCollections; +payments.starGiftCollections#8a2932f3 collections:Vector = payments.StarGiftCollections; ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; initConnection#c1cd5ea9 {X:Type} flags:# api_id:int device_model:string system_version:string app_version:string system_lang_code:string lang_pack:string lang_code:string proxy:flags.0?InputClientProxy params:flags.1?JSONValue query:!X = X; @@ -1779,7 +1783,7 @@ payments.getStarGiftUpgradePreview#9c9abcb1 gift_id:long = payments.StarGiftUpgr payments.upgradeStarGift#aed6e4f5 flags:# keep_original_details:flags.0?true stargift:InputSavedStarGift = Updates; payments.transferStarGift#7f18176a stargift:InputSavedStarGift to_id:InputPeer = Updates; payments.getUniqueStarGift#a1974d72 slug:string = payments.UniqueStarGift; -payments.getSavedStarGifts#23830de9 flags:# exclude_unsaved:flags.0?true exclude_saved:flags.1?true exclude_unlimited:flags.2?true exclude_limited:flags.3?true exclude_unique:flags.4?true sort_by_value:flags.5?true peer:InputPeer offset:string limit:int = payments.SavedStarGifts; +payments.getSavedStarGifts#a319e569 flags:# exclude_unsaved:flags.0?true exclude_saved:flags.1?true exclude_unlimited:flags.2?true exclude_limited:flags.3?true exclude_unique:flags.4?true sort_by_value:flags.5?true peer:InputPeer collection_id:flags.6?int offset:string limit:int = payments.SavedStarGifts; payments.getStarGiftWithdrawalUrl#d06e93a8 stargift:InputSavedStarGift password:InputCheckPasswordSRP = payments.StarGiftWithdrawalUrl; payments.toggleStarGiftsPinnedToTop#1513e7b0 peer:InputPeer stargift:Vector = Bool; payments.getResaleStarGifts#7a5fa236 flags:# sort_by_price:flags.1?true sort_by_num:flags.2?true attributes_hash:flags.0?long gift_id:long attributes:flags.3?Vector offset:string limit:int = payments.ResaleStarGifts; diff --git a/src/lib/gramjs/tl/static/api.tl b/src/lib/gramjs/tl/static/api.tl index 45adc634b..4bf0503b7 100644 --- a/src/lib/gramjs/tl/static/api.tl +++ b/src/lib/gramjs/tl/static/api.tl @@ -248,7 +248,7 @@ inputReportReasonFake#f5ddd6e7 = ReportReason; inputReportReasonIllegalDrugs#a8eb2be = ReportReason; inputReportReasonPersonalDetails#9ec7863d = ReportReason; -userFull#99e78045 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true wallpaper_overridden:flags.28?true contact_require_premium:flags.29?true read_dates_private:flags.30?true flags2:# sponsored_enabled:flags2.7?true can_view_revenue:flags2.9?true bot_can_manage_emoji_status:flags2.10?true display_gifts_button:flags2.16?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights wallpaper:flags.24?WallPaper stories:flags.25?PeerStories business_work_hours:flags2.0?BusinessWorkHours business_location:flags2.1?BusinessLocation business_greeting_message:flags2.2?BusinessGreetingMessage business_away_message:flags2.3?BusinessAwayMessage business_intro:flags2.4?BusinessIntro birthday:flags2.5?Birthday personal_channel_id:flags2.6?long personal_channel_message:flags2.6?int stargifts_count:flags2.8?int starref_program:flags2.11?StarRefProgram bot_verification:flags2.12?BotVerification send_paid_messages_stars:flags2.14?long disallowed_gifts:flags2.15?DisallowedGiftsSettings = UserFull; +userFull#29de80be flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true wallpaper_overridden:flags.28?true contact_require_premium:flags.29?true read_dates_private:flags.30?true flags2:# sponsored_enabled:flags2.7?true can_view_revenue:flags2.9?true bot_can_manage_emoji_status:flags2.10?true display_gifts_button:flags2.16?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights wallpaper:flags.24?WallPaper stories:flags.25?PeerStories business_work_hours:flags2.0?BusinessWorkHours business_location:flags2.1?BusinessLocation business_greeting_message:flags2.2?BusinessGreetingMessage business_away_message:flags2.3?BusinessAwayMessage business_intro:flags2.4?BusinessIntro birthday:flags2.5?Birthday personal_channel_id:flags2.6?long personal_channel_message:flags2.6?int stargifts_count:flags2.8?int starref_program:flags2.11?StarRefProgram bot_verification:flags2.12?BotVerification send_paid_messages_stars:flags2.14?long disallowed_gifts:flags2.15?DisallowedGiftsSettings stars_rating:flags2.17?StarsRating = UserFull; contact#145ade0b user_id:long mutual:Bool = Contact; @@ -1344,7 +1344,7 @@ messages.messageViews#b6c4f543 views:Vector chats:Vector use messages.discussionMessage#a6341782 flags:# messages:Vector max_id:flags.0?int read_inbox_max_id:flags.1?int read_outbox_max_id:flags.2?int unread_count:int chats:Vector users:Vector = messages.DiscussionMessage; -messageReplyHeader#afbc09db flags:# reply_to_scheduled:flags.2?true forum_topic:flags.3?true quote:flags.9?true reply_to_msg_id:flags.4?int reply_to_peer_id:flags.0?Peer reply_from:flags.5?MessageFwdHeader reply_media:flags.8?MessageMedia reply_to_top_id:flags.1?int quote_text:flags.6?string quote_entities:flags.7?Vector quote_offset:flags.10?int = MessageReplyHeader; +messageReplyHeader#6917560b flags:# reply_to_scheduled:flags.2?true forum_topic:flags.3?true quote:flags.9?true reply_to_msg_id:flags.4?int reply_to_peer_id:flags.0?Peer reply_from:flags.5?MessageFwdHeader reply_media:flags.8?MessageMedia reply_to_top_id:flags.1?int quote_text:flags.6?string quote_entities:flags.7?Vector quote_offset:flags.10?int todo_item_id:flags.11?int = MessageReplyHeader; messageReplyStoryHeader#e5af939 peer:Peer story_id:int = MessageReplyHeader; messageReplies#83d60fc2 flags:# comments:flags.0?true replies:int replies_pts:int recent_repliers:flags.1?Vector channel_id:flags.0?long max_id:flags.2?int read_max_id:flags.3?int = MessageReplies; @@ -1649,7 +1649,7 @@ stories.storyViewsList#59d78fc5 flags:# count:int views_count:int forwards_count stories.storyViews#de9eed1d views:Vector users:Vector = stories.StoryViews; -inputReplyToMessage#b07038b0 flags:# reply_to_msg_id:int top_msg_id:flags.0?int reply_to_peer_id:flags.1?InputPeer quote_text:flags.2?string quote_entities:flags.3?Vector quote_offset:flags.4?int monoforum_peer_id:flags.5?InputPeer = InputReplyTo; +inputReplyToMessage#869fbe10 flags:# reply_to_msg_id:int top_msg_id:flags.0?int reply_to_peer_id:flags.1?InputPeer quote_text:flags.2?string quote_entities:flags.3?Vector quote_offset:flags.4?int monoforum_peer_id:flags.5?InputPeer todo_item_id:flags.6?int = InputReplyTo; inputReplyToStory#5881323a peer:InputPeer story_id:int = InputReplyTo; inputReplyToMonoForum#69d66c45 monoforum_peer_id:InputPeer = InputReplyTo; @@ -1890,8 +1890,8 @@ starsGiveawayOption#94ce852a flags:# extended:flags.0?true default:flags.1?true starsGiveawayWinnersOption#54236209 flags:# default:flags.0?true users:int per_user_stars:long = StarsGiveawayWinnersOption; -starGift#7f853c12 flags:# limited:flags.0?true sold_out:flags.1?true birthday:flags.2?true id:long sticker:Document stars:long availability_remains:flags.0?int availability_total:flags.0?int availability_resale:flags.4?long convert_stars:long first_sale_date:flags.1?int last_sale_date:flags.1?int upgrade_stars:flags.3?long resell_min_stars:flags.4?long title:flags.5?string released_by:flags.6?Peer = StarGift; -starGiftUnique#f63778ae flags:# id:long title:string slug:string num:int owner_id:flags.0?Peer owner_name:flags.1?string owner_address:flags.2?string attributes:Vector availability_issued:int availability_total:int gift_address:flags.3?string resell_stars:flags.4?long released_by:flags.5?Peer = StarGift; +starGift#bcff5b flags:# limited:flags.0?true sold_out:flags.1?true birthday:flags.2?true require_premium:flags.7?true limited_per_user:flags.8?true id:long sticker:Document stars:long availability_remains:flags.0?int availability_total:flags.0?int availability_resale:flags.4?long convert_stars:long first_sale_date:flags.1?int last_sale_date:flags.1?int upgrade_stars:flags.3?long resell_min_stars:flags.4?long title:flags.5?string released_by:flags.6?Peer per_user_total:flags.8?int per_user_remains:flags.8?int = StarGift; +starGiftUnique#f63778ae flags:# require_premium:flags.6?true id:long title:string slug:string num:int owner_id:flags.0?Peer owner_name:flags.1?string owner_address:flags.2?string attributes:Vector availability_issued:int availability_total:int gift_address:flags.3?string resell_stars:flags.4?long released_by:flags.5?Peer = StarGift; payments.starGiftsNotModified#a388a368 = payments.StarGifts; payments.starGifts#2ed82995 hash:int gifts:Vector chats:Vector users:Vector = payments.StarGifts; @@ -1940,7 +1940,7 @@ payments.uniqueStarGift#caa2f60b gift:StarGift users:Vector = payments.Uni messages.webPagePreview#b53e8b21 media:MessageMedia users:Vector = messages.WebPagePreview; -savedStarGift#dfda0499 flags:# name_hidden:flags.0?true unsaved:flags.5?true refunded:flags.9?true can_upgrade:flags.10?true pinned_to_top:flags.12?true from_id:flags.1?Peer date:int gift:StarGift message:flags.2?TextWithEntities msg_id:flags.3?int saved_id:flags.11?long convert_stars:flags.4?long upgrade_stars:flags.6?long can_export_at:flags.7?int transfer_stars:flags.8?long can_transfer_at:flags.13?int can_resell_at:flags.14?int = SavedStarGift; +savedStarGift#1ea646df flags:# name_hidden:flags.0?true unsaved:flags.5?true refunded:flags.9?true can_upgrade:flags.10?true pinned_to_top:flags.12?true from_id:flags.1?Peer date:int gift:StarGift message:flags.2?TextWithEntities msg_id:flags.3?int saved_id:flags.11?long convert_stars:flags.4?long upgrade_stars:flags.6?long can_export_at:flags.7?int transfer_stars:flags.8?long can_transfer_at:flags.13?int can_resell_at:flags.14?int collection_id:flags.15?Vector = SavedStarGift; payments.savedStarGifts#95f389b1 flags:# count:int chat_notifications_enabled:flags.1?Bool gifts:Vector next_offset:flags.0?string chats:Vector users:Vector = payments.SavedStarGifts; @@ -1989,6 +1989,13 @@ todoCompletion#4cc120b7 id:int completed_by:long date:int = TodoCompletion; suggestedPost#e8e37e5 flags:# accepted:flags.1?true rejected:flags.2?true price:flags.3?StarsAmount schedule_date:flags.0?int = SuggestedPost; +starsRating#1b0e4f07 flags:# level:int current_level_stars:long stars:long next_level_stars:flags.0?long = StarsRating; + +starGiftCollection#9d6b13b0 flags:# collection_id:int title:string icon:flags.0?Document gifts_count:int hash:long = StarGiftCollection; + +payments.starGiftCollectionsNotModified#a0ba4f17 = payments.StarGiftCollections; +payments.starGiftCollections#8a2932f3 collections:Vector = payments.StarGiftCollections; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -2000,7 +2007,7 @@ invokeWithMessagesRange#365275f2 {X:Type} range:MessageRange query:!X = X; invokeWithTakeout#aca9fd2e {X:Type} takeout_id:long query:!X = X; invokeWithBusinessConnection#dd289f8e {X:Type} connection_id:string query:!X = X; invokeWithGooglePlayIntegrity#1df92984 {X:Type} nonce:string token:string query:!X = X; -invokeWithApnsSecret#dae54f8 {X:Type} nonce:string secret:string query:!X = X; +invokeWithApnsSecret#0dae54f8 {X:Type} nonce:string secret:string query:!X = X; invokeWithReCaptcha#adbb0f94 {X:Type} token:string query:!X = X; auth.sendCode#a677244f phone_number:string api_id:int api_hash:string settings:CodeSettings = auth.SentCode; @@ -2590,7 +2597,7 @@ payments.getStarGiftUpgradePreview#9c9abcb1 gift_id:long = payments.StarGiftUpgr payments.upgradeStarGift#aed6e4f5 flags:# keep_original_details:flags.0?true stargift:InputSavedStarGift = Updates; payments.transferStarGift#7f18176a stargift:InputSavedStarGift to_id:InputPeer = Updates; payments.getUniqueStarGift#a1974d72 slug:string = payments.UniqueStarGift; -payments.getSavedStarGifts#23830de9 flags:# exclude_unsaved:flags.0?true exclude_saved:flags.1?true exclude_unlimited:flags.2?true exclude_limited:flags.3?true exclude_unique:flags.4?true sort_by_value:flags.5?true peer:InputPeer offset:string limit:int = payments.SavedStarGifts; +payments.getSavedStarGifts#a319e569 flags:# exclude_unsaved:flags.0?true exclude_saved:flags.1?true exclude_unlimited:flags.2?true exclude_limited:flags.3?true exclude_unique:flags.4?true sort_by_value:flags.5?true peer:InputPeer collection_id:flags.6?int offset:string limit:int = payments.SavedStarGifts; payments.getSavedStarGift#b455a106 stargift:Vector = payments.SavedStarGifts; payments.getStarGiftWithdrawalUrl#d06e93a8 stargift:InputSavedStarGift password:InputCheckPasswordSRP = payments.StarGiftWithdrawalUrl; payments.toggleChatStarGiftNotifications#60eaefa1 flags:# enabled:flags.0?true peer:InputPeer = Bool; @@ -2598,6 +2605,11 @@ payments.toggleStarGiftsPinnedToTop#1513e7b0 peer:InputPeer stargift:Vector offset:string limit:int = payments.ResaleStarGifts; payments.updateStarGiftPrice#3baea4e1 stargift:InputSavedStarGift resell_stars:long = Updates; +payments.createStarGiftCollection#1f4a0e87 peer:InputPeer title:string stargift:Vector = StarGiftCollection; +payments.updateStarGiftCollection#4fddbee7 flags:# peer:InputPeer collection_id:int title:flags.0?string delete_stargift:flags.1?Vector add_stargift:flags.2?Vector order:flags.3?Vector = StarGiftCollection; +payments.reorderStarGiftCollections#c32af4cc peer:InputPeer order:Vector = Bool; +payments.deleteStarGiftCollection#ad5648e8 peer:InputPeer collection_id:int = Bool; +payments.getStarGiftCollections#981b91dd peer:InputPeer hash:long = payments.StarGiftCollections; stickers.createStickerSet#9021ab67 flags:# masks:flags.0?true emojis:flags.5?true text_color:flags.6?true user_id:InputUser title:string short_name:string thumb:flags.2?InputDocument stickers:Vector software:flags.3?string = messages.StickerSet; stickers.removeStickerFromSet#f7760f51 sticker:InputDocument = messages.StickerSet; @@ -2718,4 +2730,4 @@ smsjobs.getStatus#10a698e8 = smsjobs.Status; smsjobs.getSmsJob#778d902f job_id:string = SmsJob; smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool; -fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo; +fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo; \ No newline at end of file diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 047c21657..cf8beb086 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -1600,6 +1600,16 @@ export interface LangPair { 'ButtonTopUpViaFragment': undefined; 'TonModalHint': undefined; 'TonGiftReceived': undefined; + 'MediaSpoilerSensitive': undefined; + 'TitleSensitiveModal': undefined; + 'TextSensitiveModal': undefined; + 'ButtonSensitiveAlways': undefined; + 'ButtonSensitiveView': undefined; + 'TitleAgeVerificationModal': undefined; + 'DescriptionAgeVerificationModal': undefined; + 'TitleAgeCheckFailed': undefined; + 'TitleAgeCheckSuccess': undefined; + 'ButtonAgeVerification': undefined; } export interface LangPairWithVariables { @@ -3089,6 +3099,9 @@ export interface LangPairPluralWithVariables { 'MessageActionTodoTaskCount': { 'count': V; }; + 'TextAgeVerificationModal': { + 'count': V; + }; } export type RegularLangKey = keyof LangPair; export type RegularLangKeyWithVariables = keyof LangPairWithVariables; diff --git a/src/types/webapp.ts b/src/types/webapp.ts index 3c68e66f6..5eb90d0b9 100644 --- a/src/types/webapp.ts +++ b/src/types/webapp.ts @@ -91,6 +91,7 @@ interface WebAppInboundEventMap { web_app_biometry_request_auth: { reason: string }; web_app_biometry_update_token: { token: string }; web_app_set_emoji_status: { custom_emoji_id: string; duration?: number }; + web_app_verify_age: { passed: boolean; age?: number }; web_app_request_file_download: { url: string; file_name: string }; web_app_send_prepared_message: { id: string }; web_app_device_storage_save_key: { diff --git a/src/util/folderManager.ts b/src/util/folderManager.ts index c0c99f9e4..8185d47a6 100644 --- a/src/util/folderManager.ts +++ b/src/util/folderManager.ts @@ -14,6 +14,7 @@ import { import { getIsChatMuted } from '../global/helpers/notifications'; import { selectChatLastMessage, + selectIsChatRestricted, selectNotifyDefaults, selectTabState, selectTopics, @@ -527,10 +528,11 @@ function buildChatSummary( isRemovedFromSaved?: boolean, ): ChatSummary { const { - id, type, isRestricted, isNotJoined, migratedTo, folderId, + id, type, isNotJoined, migratedTo, folderId, unreadCount: chatUnreadCount, unreadMentionsCount: chatUnreadMentionsCount, hasUnreadMark, isForum, } = chat; + const isRestricted = selectIsChatRestricted(global, id); const topics = selectTopics(global, chat.id); const { unreadCount, unreadMentionsCount } = isForum